Angular 21 + PrimeNG Development Guidelines
Applicable Scenarios
- MES Manufacturing Execution System
- ERP Enterprise Resource Planning
- Backend Management System
- Data-Intensive Applications
Core Principles
1. Project Structure
src/app/
├── core/ # 核心模組(單例服務)
│ ├── auth/ # 認證相關
│ ├── guards/ # 路由守衛
│ ├── interceptors/ # HTTP 攔截器
│ ├── models/ # 資料模型
│ └── services/ # 共用服務
├── shared/ # 共用模組
│ ├── components/ # 可復用元件
│ ├── directives/ # 自定義指令
│ ├── pipes/ # 資料管線
│ └── utils/ # 工具函數
├── features/ # 功能模組
│ ├── dashboard/
│ ├── production/
│ └── settings/
└── layout/ # 版面配置
2. PrimeNG Configuration (app.config.ts)
import { ApplicationConfig } from '@angular/core';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { providePrimeNG } from 'primeng/config';
import Aura from '@primeng/themes/aura';
export const appConfig: ApplicationConfig = {
providers: [
provideAnimationsAsync(),
providePrimeNG({
theme: {
preset: Aura,
options: {
darkModeSelector: '.dark-mode',
cssLayer: {
name: 'primeng',
order: 'tailwind-base, primeng, tailwind-utilities'
}
}
},
ripple: true
})
]
};
3. PrimeNG Component Reference Table
| Purpose |
Component |
Example |
| Form/Detail Sidebar |
<p-drawer> |
Edit work order, add customer |
| Confirmation Dialog |
<p-dialog> |
Delete confirmation, warning message |
| Data Table |
<p-table> |
Work order list, report |
| Button |
<p-button> |
All button operations |
| Text Input |
<input pInputText> |
Used with ngModel |
| Dropdown |
<p-select> |
Status selection, category |
| Date Picker |
<p-datepicker> |
Date range filtering |
| Tag/Badge |
<p-tag> |
Status indication |
| Toast Notification |
<p-toast> |
Operation feedback |
| Loading Mask |
<p-blockui> |
Asynchronous operation |
4. Signals State Management (Angular 21)
// 推薦:使用 Signals
import { signal, computed, effect } from '@angular/core';
@Component({...})
export class WorkOrderListComponent {
// 狀態
workOrders = signal<WorkOrder[]>([]);
selectedId = signal<string | null>(null);
isLoading = signal(false);
// 計算屬性
selectedOrder = computed(() =>
this.workOrders().find(o => o.id === this.selectedId())
);
pendingCount = computed(() =>
this.workOrders().filter(o => o.status === 'pending').length
);
// 副作用
constructor() {
effect(() => {
console.log('選中的工單:', this.selectedOrder());
});
}
}
5. Service Layer Design
@Injectable({ providedIn: 'root' })
export class WorkOrderService {
private apiUrl = environment.apiUrl + '/work-orders';
constructor(private http: HttpClient) {}
// 列表查詢(支援篩選)
getList(params?: WorkOrderQueryParams): Observable<ApiResponse<WorkOrder[]>> {
return this.http.get<ApiResponse<WorkOrder[]>>(this.apiUrl, { params });
}
// 單筆查詢
getById(id: string): Observable<ApiResponse<WorkOrder>> {
return this.http.get<ApiResponse<WorkOrder>>(`${this.apiUrl}/${id}`);
}
// 新增
create(data: CreateWorkOrderDto): Observable<ApiResponse<WorkOrder>> {
return this.http.post<ApiResponse<WorkOrder>>(this.apiUrl, data);
}
// 更新
update(id: string, data: UpdateWorkOrderDto): Observable<ApiResponse<WorkOrder>> {
return this.http.put<ApiResponse<WorkOrder>>(`${this.apiUrl}/${id}`, data);
}
// 刪除
delete(id: string): Observable<ApiResponse<void>> {
return this.http.delete<ApiResponse<void>>(`${this.apiUrl}/${id}`);
}
}
6. API Response Format
// 統一回應格式
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: {
code: string;
message: string;
};
}
// 分頁回應
interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
total: number;
page: number;
limit: number;
};
}
7. Form Handling
// 推薦:Reactive Forms + PrimeNG
@Component({
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="field">
<label for="orderNumber">工單編號</label>
<input pInputText id="orderNumber" formControlName="orderNumber" />
<small *ngIf="form.get('orderNumber')?.errors?.['required']" class="p-error">
必填欄位
</small>
</div>
<div class="field">
<label for="customer">客戶</label>
<p-select
id="customer"
formControlName="customerId"
[options]="customers()"
optionLabel="name"
optionValue="id"
placeholder="選擇客戶"
/>
</div>
<div class="field">
<label for="dueDate">交期</label>
<p-datepicker
id="dueDate"
formControlName="dueDate"
dateFormat="yy-mm-dd"
/>
</div>
<p-button type="submit" label="儲存" [loading]="isSubmitting()" />
</form>
`
})
export class WorkOrderFormComponent {
form = new FormGroup({
orderNumber: new FormControl('', Validators.required),
customerId: new FormControl('', Validators.required),
dueDate: new FormControl<Date | null>(null)
});
customers = signal<Customer[]>([]);
isSubmitting = signal(false);
}
8. Table Best Practices
@Component({
template: `
<p-table
[value]="workOrders()"
[paginator]="true"
[rows]="20"
[rowsPerPageOptions]="[10, 20, 50]"
[loading]="isLoading()"
[globalFilterFields]="['orderNumber', 'customerName']"
styleClass="p-datatable-sm"
>
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="orderNumber">
工單編號 <p-sortIcon field="orderNumber" />
</th>
<th pSortableColumn="status">狀態</th>
<th>操作</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-order>
<tr>
<td>{{ order.orderNumber }}</td>
<td>
<p-tag [value]="order.status" [severity]="getStatusSeverity(order.status)" />
</td>
<td>
<p-button icon="pi pi-pencil" [text]="true" (click)="edit(order)" />
<p-button icon="pi pi-trash" [text]="true" severity="danger" (click)="confirmDelete(order)" />
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="3" class="text-center">無資料</td>
</tr>
</ng-template>
</p-table>
`
})
export class WorkOrderTableComponent {
workOrders = signal<WorkOrder[]>([]);
isLoading = signal(false);
getStatusSeverity(status: string): 'success' | 'info' | 'warn' | 'danger' {
const map: Record<string, 'success' | 'info' | 'warn' | 'danger'> = {
completed: 'success',
in_progress: 'info',
pending: 'warn',
cancelled: 'danger'
};
return map[status] || 'info';
}
}
9. Drawer Sidebar Pattern
@Component({
template: `
<p-drawer
[(visible)]="drawerVisible"
[header]="isEditMode() ? '編輯工單' : '新增工單'"
position="right"
[style]="{ width: '500px' }"
(onHide)="onClose()"
>
<app-work-order-form
[workOrder]="selectedOrder()"
(save)="onSave($event)"
(cancel)="drawerVisible = false"
/>
</p-drawer>
`
})
export class WorkOrderListComponent {
drawerVisible = false;
selectedOrder = signal<WorkOrder | null>(null);
isEditMode = computed(() => this.selectedOrder() !== null);
openCreate() {
this.selectedOrder.set(null);
this.drawerVisible = true;
}
openEdit(order: WorkOrder) {
this.selectedOrder.set(order);
this.drawerVisible = true;
}
}
10. Error Handling
// HTTP 攔截器
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private messageService: MessageService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
catchError((error: HttpErrorResponse) => {
let message = '發生錯誤,請稍後再試';
if (error.status === 401) {
message = '請重新登入';
} else if (error.status === 403) {
message = '權限不足';
} else if (error.status === 404) {
message = '資料不存在';
} else if (error.status === 503) {
message = '服務暫時無法使用';
} else if (error.error?.error?.message) {
message = error.error.error.message;
}
this.messageService.add({
severity: 'error',
summary: '錯誤',
detail: message
});
return throwError(() => error);
})
);
}
}
Prohibitions
- Prohibit Fallback Mock Data - API failures should display errors; fake data must not be used
- Prohibit Emoji - Do not use emoji in code, comments, or UI
- Prohibit Simplified Chinese Characters - Use Traditional Chinese (Taiwan terminology) throughout
- Prohibit Single Files Over 500 Lines - Must be split if exceeded
Performance Optimization
- OnPush Change Detection - Use with Signals
- trackBy - Table lists must use it
- Lazy Loading - Feature modules lazy loading
- Virtual Scrolling - Use for large amounts of data
<p-scroller>
Reference Resources