angular-component
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAngular Component Development Workflow
Angular组件开发工作流
Pre-Flight Checklist
前置检查清单
- Identify correct app: ,
playground-text-snippet, etc.employee - Read the design system docs for the target application (see below)
- Search for similar components:
grep "{FeatureName}Component" --include="*.ts" - Determine component type (list, form, detail, dialog)
- Check if store is needed (complex state)
- 确定所属应用:、
playground-text-snippet等employee - 阅读目标应用的设计系统文档(见下文)
- 检索是否存在相似组件:
grep "{FeatureName}Component" --include="*.ts" - 确定组件类型(列表、表单、详情、弹窗)
- 判断是否需要Store(复杂状态场景)
🎨 Design System Documentation (MANDATORY)
🎨 设计系统文档(强制要求)
Before creating any component, read the design system documentation for your target application:
| Application | Design System Location |
|---|---|
| WebV2 Apps | |
| TextSnippetClient | |
Key docs to read:
- - Component overview, base classes, library summary
README.md - - Available components and usage examples
02-component-catalog.md - - Colors, typography, spacing tokens
01-design-tokens.md - - Implementation checklist
07-technical-guide.md
创建任何组件前,请先阅读目标应用的设计系统文档:
| 应用 | 设计系统地址 |
|---|---|
| WebV2应用 | |
| TextSnippetClient | |
必读核心文档:
- - 组件概述、基类、库摘要
README.md - - 可用组件及用法示例
02-component-catalog.md - - 颜色、排版、间距token
01-design-tokens.md - - 实现检查清单
07-technical-guide.md
Component Hierarchy
组件层级
PlatformComponent # Base: lifecycle, subscriptions, signals
├── PlatformVmComponent # + ViewModel injection
├── PlatformFormComponent # + Reactive forms integration
└── PlatformVmStoreComponent # + ComponentStore state management
AppBaseComponent # + Auth, roles, company context
├── AppBaseVmComponent # + ViewModel + auth context
├── AppBaseFormComponent # + Forms + auth + validation
└── AppBaseVmStoreComponent # + Store + auth + loading/errorPlatformComponent # 基类:生命周期、订阅、signals
├── PlatformVmComponent # + ViewModel注入
├── PlatformFormComponent # + Reactive forms集成
└── PlatformVmStoreComponent # + ComponentStore状态管理
AppBaseComponent # + 权限、角色、企业上下文
├── AppBaseVmComponent # + ViewModel + 权限上下文
├── AppBaseFormComponent # + 表单 + 权限 + 校验
└── AppBaseVmStoreComponent # + Store + 权限 + 加载/错误状态Component Type Decision
组件类型选择
| Scenario | Base Class | Use When |
|---|---|---|
| Simple display | | Static content, no state |
| With ViewModel | | Needs mutable view model |
| Form with validation | | User input forms |
| Complex state/CRUD | | Lists, dashboards, multi-step |
| 场景 | 基类 | 适用场景 |
|---|---|---|
| 简单展示 | | 静态内容、无状态 |
| 带ViewModel | | 需要可变视图模型 |
| 带校验的表单 | | 用户输入表单 |
| 复杂状态/CRUD | | 列表、仪表盘、多步骤流程 |
File Location
文件位置
src/PlatformExampleAppWeb/apps/{app-name}/src/app/
└── features/
└── {feature}/
├── {feature}.component.ts
├── {feature}.component.html
├── {feature}.component.scss
└── {feature}.store.ts (if using store)src/PlatformExampleAppWeb/apps/{app-name}/src/app/
└── features/
└── {feature}/
├── {feature}.component.ts
├── {feature}.component.html
├── {feature}.component.scss
└── {feature}.store.ts (使用Store时新增)Component HTML Template Standard (BEM Classes)
组件HTML模板规范(BEM类名)
All UI elements in component templates MUST have BEM classes, even without styling needs. This makes HTML self-documenting like OOP class hierarchy.
html
<!-- ✅ CORRECT: All elements have BEM classes for structure clarity -->
<div class="feature-list">
<div class="feature-list__header">
<h1 class="feature-list__title">Features</h1>
<button class="feature-list__btn --add" (click)="onAdd()">Add New</button>
</div>
<div class="feature-list__content">
@for (item of vm.items; track trackByItem) {
<div class="feature-list__item">
<span class="feature-list__item-name">{{ item.name }}</span>
<div class="feature-list__item-actions">
<button class="feature-list__item-btn" (click)="onDelete(item)">Delete</button>
</div>
</div>
} @empty {
<div class="feature-list__empty">No items found</div>
}
</div>
</div>
<!-- ❌ WRONG: Elements without classes - structure unclear -->
<div class="feature-list">
<div>
<h1>Features</h1>
<button (click)="onAdd()">Add New</button>
</div>
<div>
@for (item of vm.items; track trackByItem) {
<div>
<span>{{ item.name }}</span>
<div>
<button (click)="onDelete(item)">Delete</button>
</div>
</div>
}
</div>
</div>BEM Naming Convention:
- Block: Component name (e.g., )
feature-list - Element: Child using (e.g.,
block__element)feature-list__header - Modifier: Separate class with prefix (e.g.,
--)feature-list__btn --add --large
组件模板中所有UI元素必须添加BEM类名,即使没有样式需求也需要添加。 这可以让HTML像OOP类层级一样具备自说明性。
html
<!-- ✅ 正确:所有元素都有BEM类名,结构清晰 -->
<div class="feature-list">
<div class="feature-list__header">
<h1 class="feature-list__title">Features</h1>
<button class="feature-list__btn --add" (click)="onAdd()">Add New</button>
</div>
<div class="feature-list__content">
@for (item of vm.items; track trackByItem) {
<div class="feature-list__item">
<span class="feature-list__item-name">{{ item.name }}</span>
<div class="feature-list__item-actions">
<button class="feature-list__item-btn" (click)="onDelete(item)">Delete</button>
</div>
</div>
} @empty {
<div class="feature-list__empty">No items found</div>
}
</div>
</div>
<!-- ❌ 错误:元素没有类名,结构不清晰 -->
<div class="feature-list">
<div>
<h1>Features</h1>
<button (click)="onAdd()">Add New</button>
</div>
<div>
@for (item of vm.items; track trackByItem) {
<div>
<span>{{ item.name }}</span>
<div>
<button (click)="onDelete(item)">Delete</button>
</div>
</div>
}
</div>
</div>BEM命名约定:
- Block:组件名称(例如:)
feature-list - Element:子元素使用格式(例如:
block__element)feature-list__header - Modifier:修饰类使用前缀(例如:
--)feature-list__btn --add --large
Component SCSS Standard
组件SCSS规范
Always style both the host element (Angular selector) and the main wrapper class:
scss
@import '~assets/scss/variables';
// Host element styling - ensures Angular element is a proper block container
my-component {
display: flex;
flex-direction: column;
}
// Main wrapper class with full styling
.my-component {
display: flex;
flex-direction: column;
width: 100%;
flex-grow: 1;
&__header {
// BEM child elements...
}
&__content {
flex: 1;
overflow-y: auto;
}
}Why both?
- Host element: Makes the Angular element a real layout element (not an unknown element without display)
- Main class: Contains the full styling, matches the wrapper div in HTML
必须同时为宿主元素(Angular选择器)和主容器类添加样式:
scss
@import '~assets/scss/variables';
// 宿主元素样式 - 确保Angular元素是合法的块级容器
my-component {
display: flex;
flex-direction: column;
}
// 主容器类,包含完整样式
.my-component {
display: flex;
flex-direction: column;
width: 100%;
flex-grow: 1;
&__header {
// BEM子元素样式...
}
&__content {
flex: 1;
overflow-y: auto;
}
}为什么需要同时设置两者?
- 宿主元素:让Angular元素成为真实的布局元素(而非无display属性的未知元素)
- 主容器类:包含完整样式,与HTML中的容器div匹配
Pattern 1: List Component with Store
模式1:带Store的列表组件
Store Definition
Store定义
typescript
// {feature}.store.ts
import { Injectable } from '@angular/core';
import { PlatformVmStore } from '@libs/platform-core';
export interface FeatureListState {
items: FeatureDto[];
selectedItem?: FeatureDto;
filters: FeatureFilters;
}
@Injectable()
export class FeatureListStore extends PlatformVmStore<FeatureListState> {
// Initial state
protected override vmConstructor = (data?: Partial<FeatureListState>) => ({ items: [], filters: {}, ...data }) as FeatureListState;
// Selectors
public readonly items$ = this.select(state => state.items);
public readonly selectedItem$ = this.select(state => state.selectedItem);
// Effects
public loadItems = this.effectSimple(() =>
this.featureApi.getList(this.currentVm().filters).pipe(
this.observerLoadingErrorState('loadItems'),
this.tapResponse(items => this.updateState({ items }))
)
);
public saveItem = this.effectSimple((item: FeatureDto) =>
this.featureApi.save(item).pipe(
this.observerLoadingErrorState('saveItem'),
this.tapResponse(saved => {
this.updateState(state => ({
items: state.items.upsertBy(x => x.id, [saved])
}));
})
)
);
public deleteItem = this.effectSimple((id: string) =>
this.featureApi.delete(id).pipe(
this.observerLoadingErrorState('deleteItem'),
this.tapResponse(() => {
this.updateState(state => ({
items: state.items.filter(x => x.id !== id)
}));
})
)
);
constructor(private featureApi: FeatureApiService) {
super();
}
}typescript
// {feature}.store.ts
import { Injectable } from '@angular/core';
import { PlatformVmStore } from '@libs/platform-core';
export interface FeatureListState {
items: FeatureDto[];
selectedItem?: FeatureDto;
filters: FeatureFilters;
}
@Injectable()
export class FeatureListStore extends PlatformVmStore<FeatureListState> {
// Initial state
protected override vmConstructor = (data?: Partial<FeatureListState>) => ({ items: [], filters: {}, ...data }) as FeatureListState;
// Selectors
public readonly items$ = this.select(state => state.items);
public readonly selectedItem$ = this.select(state => state.selectedItem);
// Effects
public loadItems = this.effectSimple(() =>
this.featureApi.getList(this.currentVm().filters).pipe(
this.observerLoadingErrorState('loadItems'),
this.tapResponse(items => this.updateState({ items }))
)
);
public saveItem = this.effectSimple((item: FeatureDto) =>
this.featureApi.save(item).pipe(
this.observerLoadingErrorState('saveItem'),
this.tapResponse(saved => {
this.updateState(state => ({
items: state.items.upsertBy(x => x.id, [saved])
}));
})
)
);
public deleteItem = this.effectSimple((id: string) =>
this.featureApi.delete(id).pipe(
this.observerLoadingErrorState('deleteItem'),
this.tapResponse(() => {
this.updateState(state => ({
items: state.items.filter(x => x.id !== id)
}));
})
)
);
constructor(private featureApi: FeatureApiService) {
super();
}
}List Component
列表组件
typescript
// {feature}-list.component.ts
import { Component, OnInit } from '@angular/core';
import { AppBaseVmStoreComponent } from '@libs/apps-domains';
import { FeatureListStore, FeatureListState } from './feature-list.store';
@Component({
selector: 'app-feature-list',
templateUrl: './feature-list.component.html',
styleUrls: ['./feature-list.component.scss'],
providers: [FeatureListStore] // Provide store at component level
})
export class FeatureListComponent extends AppBaseVmStoreComponent<FeatureListState, FeatureListStore> implements OnInit {
// Track-by for performance
trackByItem = this.ngForTrackByItemProp<FeatureDto>('id');
constructor(store: FeatureListStore) {
super(store);
}
ngOnInit(): void {
this.store.loadItems();
}
onRefresh(): void {
this.reload(); // Reloads all store data
}
onDelete(item: FeatureDto): void {
this.store.deleteItem(item.id);
}
// Check loading state for specific request
get isDeleting$() {
return this.store.isLoading$('deleteItem');
}
}typescript
// {feature}-list.component.ts
import { Component, OnInit } from '@angular/core';
import { AppBaseVmStoreComponent } from '@libs/apps-domains';
import { FeatureListStore, FeatureListState } from './feature-list.store';
@Component({
selector: 'app-feature-list',
templateUrl: './feature-list.component.html',
styleUrls: ['./feature-list.component.scss'],
providers: [FeatureListStore] // 在组件层级提供Store
})
export class FeatureListComponent extends AppBaseVmStoreComponent<FeatureListState, FeatureListStore> implements OnInit {
// Track-by优化性能
trackByItem = this.ngForTrackByItemProp<FeatureDto>('id');
constructor(store: FeatureListStore) {
super(store);
}
ngOnInit(): void {
this.store.loadItems();
}
onRefresh(): void {
this.reload(); // 重载所有Store数据
}
onDelete(item: FeatureDto): void {
this.store.deleteItem(item.id);
}
// 获取特定请求的加载状态
get isDeleting$() {
return this.store.isLoading$('deleteItem');
}
}List Template
列表模板
html
<!-- {feature}-list.component.html -->
<app-loading-and-error-indicator [target]="this">
@if (vm(); as vm) {
<div class="feature-list">
<!-- Header with actions -->
<div class="header">
<h1>Features</h1>
<button (click)="onRefresh()" [disabled]="isStateLoading()()">Refresh</button>
</div>
<!-- List items -->
@for (item of vm.items; track trackByItem) {
<div class="item">
<span>{{ item.name }}</span>
<button (click)="onDelete(item)" [disabled]="isDeleting$() === true">Delete</button>
</div>
} @empty {
<div class="empty">No items found</div>
}
</div>
}
</app-loading-and-error-indicator>html
<!-- {feature}-list.component.html -->
<app-loading-and-error-indicator [target]="this">
@if (vm(); as vm) {
<div class="feature-list">
<!-- 带操作的头部 -->
<div class="header">
<h1>Features</h1>
<button (click)="onRefresh()" [disabled]="isStateLoading()()">Refresh</button>
</div>
<!-- 列表项 -->
@for (item of vm.items; track trackByItem) {
<div class="item">
<span>{{ item.name }}</span>
<button (click)="onDelete(item)" [disabled]="isDeleting$() === true">Delete</button>
</div>
} @empty {
<div class="empty">No items found</div>
}
</div>
}
</app-loading-and-error-indicator>Pattern 2: Form Component
模式2:表单组件
typescript
// {feature}-form.component.ts
import { Component } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { AppBaseFormComponent } from '@libs/apps-domains';
import { ifAsyncValidator, noWhitespaceValidator } from '@libs/platform-core';
export interface FeatureFormVm {
id?: string;
name: string;
code: string;
status: FeatureStatus;
effectiveDate?: Date;
}
@Component({
selector: 'app-feature-form',
templateUrl: './feature-form.component.html'
})
export class FeatureFormComponent extends AppBaseFormComponent<FeatureFormVm> {
// Form configuration
protected initialFormConfig = () => ({
controls: {
name: new FormControl(this.currentVm().name, [Validators.required, Validators.maxLength(200), noWhitespaceValidator]),
code: new FormControl(
this.currentVm().code,
[Validators.required, Validators.pattern(/^[A-Z0-9-]+$/)],
[
// Async validator only runs if sync validators pass
ifAsyncValidator(() => !this.isViewMode(), this.checkCodeUniqueValidator())
]
),
status: new FormControl(this.currentVm().status, [Validators.required]),
effectiveDate: new FormControl(this.currentVm().effectiveDate)
},
// Re-validate code when status changes
dependentValidations: {
code: ['status']
}
});
// Initialize or reload view model
protected initOrReloadVm = (isReload: boolean) => {
if (this.mode === 'create') {
return of<FeatureFormVm>({
name: '',
code: '',
status: FeatureStatus.Draft
});
}
return this.featureApi.getById(this.featureId);
};
// Custom async validator
private checkCodeUniqueValidator() {
return async (control: AbstractControl) => {
const exists = await firstValueFrom(this.featureApi.checkCodeExists(control.value, this.currentVm().id));
return exists ? { codeExists: true } : null;
};
}
onSubmit(): void {
if (!this.validateForm()) return;
const vm = this.currentVm();
this.featureApi
.save(vm)
.pipe(
this.observerLoadingErrorState('save'),
this.tapResponse(
saved => this.onSaveSuccess(saved),
error => this.onSaveError(error)
),
this.untilDestroyed()
)
.subscribe();
}
constructor(private featureApi: FeatureApiService) {
super();
}
}typescript
// {feature}-form.component.ts
import { Component } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { AppBaseFormComponent } from '@libs/apps-domains';
import { ifAsyncValidator, noWhitespaceValidator } from '@libs/platform-core';
export interface FeatureFormVm {
id?: string;
name: string;
code: string;
status: FeatureStatus;
effectiveDate?: Date;
}
@Component({
selector: 'app-feature-form',
templateUrl: './feature-form.component.html'
})
export class FeatureFormComponent extends AppBaseFormComponent<FeatureFormVm> {
// 表单配置
protected initialFormConfig = () => ({
controls: {
name: new FormControl(this.currentVm().name, [Validators.required, Validators.maxLength(200), noWhitespaceValidator]),
code: new FormControl(
this.currentVm().code,
[Validators.required, Validators.pattern(/^[A-Z0-9-]+$/)],
[
// 仅同步校验通过后才运行异步校验
ifAsyncValidator(() => !this.isViewMode(), this.checkCodeUniqueValidator())
]
),
status: new FormControl(this.currentVm().status, [Validators.required]),
effectiveDate: new FormControl(this.currentVm().effectiveDate)
},
// 状态变更时重新校验code
dependentValidations: {
code: ['status']
}
});
// 初始化或重载ViewModel
protected initOrReloadVm = (isReload: boolean) => {
if (this.mode === 'create') {
return of<FeatureFormVm>({
name: '',
code: '',
status: FeatureStatus.Draft
});
}
return this.featureApi.getById(this.featureId);
};
// 自定义异步校验器
private checkCodeUniqueValidator() {
return async (control: AbstractControl) => {
const exists = await firstValueFrom(this.featureApi.checkCodeExists(control.value, this.currentVm().id));
return exists ? { codeExists: true } : null;
};
}
onSubmit(): void {
if (!this.validateForm()) return;
const vm = this.currentVm();
this.featureApi
.save(vm)
.pipe(
this.observerLoadingErrorState('save'),
this.tapResponse(
saved => this.onSaveSuccess(saved),
error => this.onSaveError(error)
),
this.untilDestroyed()
)
.subscribe();
}
constructor(private featureApi: FeatureApiService) {
super();
}
}Form Template
表单模板
html
<!-- {feature}-form.component.html -->
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<!-- Name field -->
<div class="form-field">
<label for="name">Name *</label>
<input id="name" formControlName="name" />
@if (formControls('name').errors?.['required']) {
<span class="error">Name is required</span>
}
</div>
<!-- Code field with async validation -->
<div class="form-field">
<label for="code">Code *</label>
<input id="code" formControlName="code" />
@if (formControls('code').errors?.['codeExists']) {
<span class="error">Code already exists</span>
} @if (formControls('code').pending) {
<span class="info">Checking...</span>
}
</div>
<!-- Status dropdown -->
<div class="form-field">
<label for="status">Status *</label>
<select id="status" formControlName="status">
@for (status of statusOptions; track status.value) {
<option [value]="status.value">{{ status.label }}</option>
}
</select>
</div>
<!-- Actions -->
<div class="actions">
<button type="button" (click)="onCancel()">Cancel</button>
<button type="submit" [disabled]="!form.valid || isLoading$('save')()">{{ isLoading$('save')() ? 'Saving...' : 'Save' }}</button>
</div>
</form>html
<!-- {feature}-form.component.html -->
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<!-- 名称字段 -->
<div class="form-field">
<label for="name">Name *</label>
<input id="name" formControlName="name" />
@if (formControls('name').errors?.['required']) {
<span class="error">Name is required</span>
}
</div>
<!-- 带异步校验的编码字段 -->
<div class="form-field">
<label for="code">Code *</label>
<input id="code" formControlName="code" />
@if (formControls('code').errors?.['codeExists']) {
<span class="error">Code already exists</span>
} @if (formControls('code').pending) {
<span class="info">Checking...</span>
}
</div>
<!-- 状态下拉框 -->
<div class="form-field">
<label for="status">Status *</label>
<select id="status" formControlName="status">
@for (status of statusOptions; track status.value) {
<option [value]="status.value">{{ status.label }}</option>
}
</select>
</div>
<!-- 操作按钮 -->
<div class="actions">
<button type="button" (click)="onCancel()">Cancel</button>
<button type="submit" [disabled]="!form.valid || isLoading$('save')()">{{ isLoading$('save')() ? 'Saving...' : 'Save' }}</button>
</div>
</form>Pattern 3: Simple Component
模式3:简单组件
typescript
// {feature}-card.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { AppBaseComponent } from '@libs/apps-domains';
@Component({
selector: 'app-feature-card',
template: `
<div class="card" [class.selected]="isSelected">
<h3>{{ feature.name }}</h3>
<p>{{ feature.description }}</p>
@if (canEdit) {
<button (click)="onEdit.emit(feature)">Edit</button>
}
</div>
`
})
export class FeatureCardComponent extends AppBaseComponent {
@Input() feature!: FeatureDto;
@Input() isSelected = false;
@Output() onEdit = new EventEmitter<FeatureDto>();
get canEdit(): boolean {
return this.hasRole('Admin', 'Manager');
}
}typescript
// {feature}-card.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { AppBaseComponent } from '@libs/apps-domains';
@Component({
selector: 'app-feature-card',
template: `
<div class="card" [class.selected]="isSelected">
<h3>{{ feature.name }}</h3>
<p>{{ feature.description }}</p>
@if (canEdit) {
<button (click)="onEdit.emit(feature)">Edit</button>
}
</div>
`
})
export class FeatureCardComponent extends AppBaseComponent {
@Input() feature!: FeatureDto;
@Input() isSelected = false;
@Output() onEdit = new EventEmitter<FeatureDto>();
get canEdit(): boolean {
return this.hasRole('Admin', 'Manager');
}
}Key Platform APIs
核心平台API
Lifecycle & Subscriptions
生命周期与订阅
typescript
// Auto-cleanup subscription
this.data$.pipe(this.untilDestroyed()).subscribe();
// Store named subscriptions
this.storeSubscription('key', observable.subscribe());
this.cancelStoredSubscription('key');typescript
// 自动清理订阅
this.data$.pipe(this.untilDestroyed()).subscribe();
// Store命名订阅
this.storeSubscription('key', observable.subscribe());
this.cancelStoredSubscription('key');Loading/Error State
加载/错误状态
typescript
// Track request state
observable.pipe(this.observerLoadingErrorState('requestKey'));
// Check states in template
isLoading$('requestKey')();
getErrorMsg$('requestKey')();
isStateLoading()();
isStateError()();typescript
// 追踪请求状态
observable.pipe(this.observerLoadingErrorState('requestKey'));
// 在模板中检查状态
isLoading$('requestKey')();
getErrorMsg$('requestKey')();
isStateLoading()();
isStateError()();Response Handling
响应处理
typescript
// Handle success/error
observable.pipe(
this.tapResponse(
result => {
/* success */
},
error => {
/* error */
}
)
);typescript
// 处理成功/错误
observable.pipe(
this.tapResponse(
result => {
/* success */
},
error => {
/* error */
}
)
);Track-by Functions
Track-by函数
typescript
// For @for loops
trackByItem = this.ngForTrackByItemProp<Item>('id');
trackByList = this.ngForTrackByImmutableList(this.items);typescript
// 用于@for循环
trackByItem = this.ngForTrackByItemProp<Item>('id');
trackByList = this.ngForTrackByImmutableList(this.items);Code Responsibility Hierarchy (CRITICAL)
代码职责分层(关键要求)
Place logic in the LOWEST appropriate layer to enable reuse and prevent duplication:
| Layer | Responsibility |
|---|---|
| Entity/Model | Display helpers, static factory methods, default values, dropdown options |
| Service | API calls, command factories, data transformation |
| Component | UI event handling ONLY - delegates all logic to lower layers |
typescript
// ❌ WRONG: Logic in component (leads to duplication if another component needs it)
readonly authTypes = [{ value: AuthType.OAuth2, label: 'OAuth2' }, ...];
getDefaultBaseUrl(type) { return this.providerUrls[type] ?? ''; }
// ✅ CORRECT: Logic in entity/model (single source of truth, reusable)
readonly authTypes = AuthConfigurationDisplay.getApiAuthTypeOptions();
getDefaultBaseUrl(type) { return JobBoardProviderConfiguration.getDefaultBaseUrl(type); }Common Refactoring Patterns:
- Dropdown options → static method in entity:
Entity.getOptions() - Display logic (CSS class, text) → instance method in entity:
entity.getStatusCssClass() - Default values → static method in entity:
Entity.getDefaultValue() - Command building → factory class in service:
CommandFactory.buildSaveCommand(formValues)
将逻辑放在最低层级的合适位置,以实现复用并避免重复:
| 层级 | 职责 |
|---|---|
| 实体/模型 | 展示辅助方法、静态工厂方法、默认值、下拉选项 |
| Service | API调用、命令工厂、数据转换 |
| 组件 | 仅处理UI事件 - 所有逻辑委托给下层实现 |
typescript
// ❌ 错误:逻辑放在组件中(如果其他组件需要会导致重复)
readonly authTypes = [{ value: AuthType.OAuth2, label: 'OAuth2' }, ...];
getDefaultBaseUrl(type) { return this.providerUrls[type] ?? ''; }
// ✅ 正确:逻辑放在实体/模型中(单一数据源,可复用)
readonly authTypes = AuthConfigurationDisplay.getApiAuthTypeOptions();
getDefaultBaseUrl(type) { return JobBoardProviderConfiguration.getDefaultBaseUrl(type); }常见重构模式:
- 下拉选项 → 实体中的静态方法:
Entity.getOptions() - 展示逻辑(CSS类、文本) → 实体中的实例方法:
entity.getStatusCssClass() - 默认值 → 实体中的静态方法:
Entity.getDefaultValue() - 命令构建 → Service中的工厂类:
CommandFactory.buildSaveCommand(formValues)
Anti-Patterns to AVOID
需要避免的反模式
:x: Using wrong base class
typescript
// WRONG - using PlatformComponent when auth needed
export class MyComponent extends PlatformComponent {}
// CORRECT - using AppBaseComponent for auth context
export class MyComponent extends AppBaseComponent {}:x: Manual subscription management
typescript
// WRONG
private sub: Subscription;
ngOnDestroy() { this.sub.unsubscribe(); }
// CORRECT
this.data$.pipe(this.untilDestroyed()).subscribe();:x: Direct HTTP calls
typescript
// WRONG
constructor(private http: HttpClient) { }
// CORRECT
constructor(private featureApi: FeatureApiService) { }:x: Missing loading states
html
<!-- WRONG - no loading indicator -->
<div>{{ items }}</div>
<!-- CORRECT - with loading wrapper -->
<app-loading-and-error-indicator [target]="this">
<div>{{ items }}</div>
</app-loading-and-error-indicator>:x: 使用错误的基类
typescript
// 错误 - 需要权限时使用PlatformComponent
export class MyComponent extends PlatformComponent {}
// 正确 - 使用AppBaseComponent获取权限上下文
export class MyComponent extends AppBaseComponent {}:x: 手动管理订阅
typescript
// 错误
private sub: Subscription;
ngOnDestroy() { this.sub.unsubscribe(); }
// 正确
this.data$.pipe(this.untilDestroyed()).subscribe();:x: 直接调用HTTP接口
typescript
// 错误
constructor(private http: HttpClient) { }
// 正确
constructor(private featureApi: FeatureApiService) { }:x: 缺失加载状态
html
<!-- 错误 - 无加载指示器 -->
<div>{{ items }}</div>
<!-- 正确 - 带加载包裹组件 -->
<app-loading-and-error-indicator [target]="this">
<div>{{ items }}</div>
</app-loading-and-error-indicator>Verification Checklist
验证清单
- Correct base class selected for use case
- Store provided at component level (if using store)
- Loading/error states handled with
app-loading-and-error-indicator - Subscriptions use
untilDestroyed() - Track-by functions used in loops
@for - Form validation configured properly
- Auth checks use from base class
hasRole() - API calls use service extending
PlatformApiService
- 为当前场景选择了正确的基类
- Store在组件层级提供(使用Store时)
- 使用处理加载/错误状态
app-loading-and-error-indicator - 订阅使用自动清理
untilDestroyed() - 循环使用了track-by函数
@for - 表单校验配置正确
- 权限校验使用基类提供的方法
hasRole() - API调用使用继承自的服务
PlatformApiService