Loading...
Loading...
Compare original and translation side by side
playground-text-snippetemployeegrep "{FeatureName}Component" --include="*.ts"playground-text-snippetemployeegrep "{FeatureName}Component" --include="*.ts"| Application | Design System Location |
|---|---|
| WebV2 Apps | |
| TextSnippetClient | |
README.md02-component-catalog.md01-design-tokens.md07-technical-guide.md| 应用 | 设计系统地址 |
|---|---|
| WebV2应用 | |
| TextSnippetClient | |
README.md02-component-catalog.md01-design-tokens.md07-technical-guide.mdPlatformComponent # 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 + 权限 + 加载/错误状态| 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 | | 列表、仪表盘、多步骤流程 |
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时新增)<!-- ✅ 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>feature-listblock__elementfeature-list__header--feature-list__btn --add --large<!-- ✅ 正确:所有元素都有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>feature-listblock__elementfeature-list__header--feature-list__btn --add --large@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;
}
}@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;
}
}// {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();
}
}// {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();
}
}// {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');
}
}// {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');
}
}<!-- {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><!-- {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>// {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();
}
}// {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();
}
}<!-- {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><!-- {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>// {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');
}
}// {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');
}
}// Auto-cleanup subscription
this.data$.pipe(this.untilDestroyed()).subscribe();
// Store named subscriptions
this.storeSubscription('key', observable.subscribe());
this.cancelStoredSubscription('key');// 自动清理订阅
this.data$.pipe(this.untilDestroyed()).subscribe();
// Store命名订阅
this.storeSubscription('key', observable.subscribe());
this.cancelStoredSubscription('key');// Track request state
observable.pipe(this.observerLoadingErrorState('requestKey'));
// Check states in template
isLoading$('requestKey')();
getErrorMsg$('requestKey')();
isStateLoading()();
isStateError()();// 追踪请求状态
observable.pipe(this.observerLoadingErrorState('requestKey'));
// 在模板中检查状态
isLoading$('requestKey')();
getErrorMsg$('requestKey')();
isStateLoading()();
isStateError()();// Handle success/error
observable.pipe(
this.tapResponse(
result => {
/* success */
},
error => {
/* error */
}
)
);// 处理成功/错误
observable.pipe(
this.tapResponse(
result => {
/* success */
},
error => {
/* error */
}
)
);// For @for loops
trackByItem = this.ngForTrackByItemProp<Item>('id');
trackByList = this.ngForTrackByImmutableList(this.items);// 用于@for循环
trackByItem = this.ngForTrackByItemProp<Item>('id');
trackByList = this.ngForTrackByImmutableList(this.items);| 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 |
// ❌ 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); }Entity.getOptions()entity.getStatusCssClass()Entity.getDefaultValue()CommandFactory.buildSaveCommand(formValues)| 层级 | 职责 |
|---|---|
| 实体/模型 | 展示辅助方法、静态工厂方法、默认值、下拉选项 |
| Service | API调用、命令工厂、数据转换 |
| 组件 | 仅处理UI事件 - 所有逻辑委托给下层实现 |
// ❌ 错误:逻辑放在组件中(如果其他组件需要会导致重复)
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()entity.getStatusCssClass()Entity.getDefaultValue()CommandFactory.buildSaveCommand(formValues)// WRONG - using PlatformComponent when auth needed
export class MyComponent extends PlatformComponent {}
// CORRECT - using AppBaseComponent for auth context
export class MyComponent extends AppBaseComponent {}// WRONG
private sub: Subscription;
ngOnDestroy() { this.sub.unsubscribe(); }
// CORRECT
this.data$.pipe(this.untilDestroyed()).subscribe();// WRONG
constructor(private http: HttpClient) { }
// CORRECT
constructor(private featureApi: FeatureApiService) { }<!-- 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>// 错误 - 需要权限时使用PlatformComponent
export class MyComponent extends PlatformComponent {}
// 正确 - 使用AppBaseComponent获取权限上下文
export class MyComponent extends AppBaseComponent {}// 错误
private sub: Subscription;
ngOnDestroy() { this.sub.unsubscribe(); }
// 正确
this.data$.pipe(this.untilDestroyed()).subscribe();// 错误
constructor(private http: HttpClient) { }
// 正确
constructor(private featureApi: FeatureApiService) { }<!-- 错误 - 无加载指示器 -->
<div>{{ items }}</div>
<!-- 正确 - 带加载包裹组件 -->
<app-loading-and-error-indicator [target]="this">
<div>{{ items }}</div>
</app-loading-and-error-indicator>app-loading-and-error-indicatoruntilDestroyed()@forhasRole()PlatformApiServiceapp-loading-and-error-indicatoruntilDestroyed()@forhasRole()PlatformApiService