- AI 技能
- AI 插件
- 人工智能
【免费下载链接】agentic-awesome-skills
AAS Core is the local, agent-first control plane for complete catalog discovery, agent-owned selection, stack validation, and planning, backed by 2,600+ agentic skills. Includes CLI, local MCP, catalog, plugins, and Workbench.
导读
本指南围绕 agentic-awesome-skills 仓库中angular-ui-patterns技能(位于 plugins/agentic-awesome-skills-claude/skills/angular-ui-patterns/)展开,系统讲解现代 Angular 应用中加载状态、错误处理、空状态、表单提交、对话框等 UI 状态模式的统一实践。读完本文,你将掌握一套可直接落地的"UI 状态决策框架":何时该显示骨架屏而非 spinner、如何用新控制流语法@if/@for/@defer组织组件模板、为什么"绝不静默吞掉错误"是铁律,以及如何在提交按钮上正确管理禁用与 loading 态,最终交付对 AI Agent 与 LLM 也易于理解和维护的健壮组件。
一、技能定位:何时该启用这套 UI 模式
本技能在仓库中以"技能 + 详细指南"的形式组织:入口文件 SKILL.md 负责定义激活条件、风险等级与使用边界,而完整的过程与参考材料保留在 references/detailed-guide.md 中,SKILL.md 明确要求"执行该技能前先阅读详细指南"。
按照 SKILL.md 的定义,该技能的适用场景是:
- 构建 UI 组件(Component)时;
- 处理异步数据(async data)时;
- 管理组件状态(component states)时;
- 需要为列表、表单、按钮、对话框设计加载、错误、空数据等状态的完整 UI 时。
配套的 metadata.json 给出了更精炼的定位:"面向 AI Agent 与 LLM 优化的现代 Angular 应用 UI 模式,涵盖加载状态、错误处理、渐进式披露与数据展示模式,强调仅在无数据时显示加载、surface 所有错误、乐观更新以及使用@defer实现优雅降级,并包含决策树与反模式清单。"
使用边界(Limitations)
SKILL.md 同时划定了三条边界,在实际工程中同样适用:
- 仅在任务明确匹配上述范围时才启用本技能;
- 技能输出不能替代环境相关的验证、测试与专家评审;
- 当输入、权限、安全边界或成功标准缺失时,应停下来向用户澄清,而不是盲目执行。
二、五大核心原则:先定原则,再写代码
无论组件规模大小,详细指南给出的五条原则是所有后续模式的总纲:
- 绝不显示陈旧 UI—— 只有在真正加载时、且没有数据可展示时才显示加载态;
- 永远 surface 错误—— 用户必须知道某件事失败了,禁止静默吞错;
- 乐观更新—— 在服务端确认之前先更新 UI,让界面感觉"即时";
- 渐进式披露—— 用
@defer按内容可用性逐步展示非关键内容; - 优雅降级—— 部分数据好过没有数据,功能失败要有兜底回退。
其中第一条"绝不显示陈旧 UI"被指南称为The Golden Rule(黄金法则),它贯穿了后面所有加载态、刷新态与按钮态的设计。
三、加载状态模式:只在"没有数据"时显示加载
3.1 黄金法则的实现范式
黄金法则的模板实现是一个典型的信号驱动状态机,按"错误 → 加载且无数据 → 空数据 → 有数据"的优先级渲染:
@Component({ template: ` @if (error()) { <app-error-state [error]="error()" (retry)="load()" /> } @else if (loading() && !items().length) { <app-skeleton-list /> } @else if (!items().length) { <app-empty-state message="No items found" /> } @else { <app-item-list [items]="items()" /> } `, }) export class ItemListComponent { private store = inject(ItemStore); items = this.store.items; loading = this.store.loading; error = this.store.error; }注意这里的两个关键设计:
loading()与!items().length用&&绑定:数据已存在时,即使正在后台刷新,也绝不回退成加载态,避免"刷新一下闪一下 spinner"的视觉抖动;items、loading、error三个信号都来自ItemStore(一个基于 Signal 的组件级 store),组件本身零状态、纯声明式,这正是 angular-state-management 技能所倡导的 Signal store 范式在 UI 层的落地。
3.2 加载状态决策树
当不确定该渲染什么时,按以下决策树逐级判断:
Is there an error? → Yes: Show error state with retry option → No: Continue Is it loading AND we have no data? → Yes: Show loading indicator (spinner/skeleton) → No: Continue Do we have data? → Yes, with items: Show the data → Yes, but empty: Show empty state → No: Show loading (fallback)这条决策树的本质是:错误优先级最高,加载态只在数据缺失时兜底,空数据有独立的空状态,正常数据最后兜住。
3.3 Skeleton vs Spinner:何时用哪种
| 使用 Skeleton(骨架屏) | 使用 Spinner(旋转器) |
|---|---|
| 已知内容形状 | 未知内容形状 |
| 列表 / 卡片布局 | 模态框操作 |
| 页面首次加载 | 按钮提交 |
| 内容占位 | 行内操作 |
判读标准一句话:你预知内容长什么样,就用骨架屏给用户"内容即将到来"的心理预期;内容形态未知或只是瞬时操作,就用轻量的 spinner。初始页面加载、列表页等场景优先骨架屏;模态框内的提交按钮、行内小操作优先 spinner。
四、控制流模式:用 Angular 新语法组织状态渲染
详细指南给出的所有示例均基于 Angular 的现代控制流语法(内置@if、@for、@defer块),这些语法取代了旧的*ngIf/*ngFor结构性指令。
4.1@if/@else条件渲染
@if (user(); as user) { <span>Welcome, {{ user.name }}</span> } @else if (loading()) { <app-spinner size="small" /> } @else { <a routerLink="/login">Sign In</a> }as user局部变量声明避免了重复调用user()信号;三个分支依次覆盖"已登录 / 加载中 / 未登录",是登录态组件的标准形态。
4.2@for+track+@empty
@for (item of items(); track item.id) { <app-item-card [item]="item" (delete)="remove(item.id)" /> } @empty { <app-empty-state icon="inbox" message="No items yet" actionLabel="Create Item" (action)="create()" /> }要点:track item.id是列表 diffing 的性能关键(取代旧trackBy),而@empty块是列表组件内置空状态的第一选择——指南在空状态章节再次强调"每个列表/集合必须有空状态"。
4.3@defer渐进式加载
<!-- Critical content loads immediately --> <app-header /> <app-hero-section /> <!-- Non-critical content deferred --> @defer (on viewport) { <app-comments [postId]="postId()" /> } @placeholder { <div class="h-32 bg-gray-100 animate-pulse"></div> } @loading (minimum 200ms) { <app-spinner /> } @error { <app-error-state message="Failed to load comments" /> }@defer是"渐进式披露 + 优雅降级"两大原则的直接实现:
on viewport触发条件:评论区滚动进入视口才加载,首屏不被非关键内容拖慢;@placeholder:内容加载前的占位骨架;@loading (minimum 200ms):设定最短加载时长阈值,避免快速加载时的闪烁;@error:延迟加载失败时的独立错误兜底,不影响首屏。
五、错误处理模式:绝不静默吞错
5.1 错误处理层级
不同严重程度的错误应使用不同粒度的 UI 呈现,从"字段级"到"整页级"依次升级:
1. Inline error (field-level) → 表单字段校验错误 2. Toast notification → 可恢复错误,用户可重试 3. Error banner → 页面级错误,数据仍部分可用 4. Full error screen → 不可恢复,需要用户操作5.2 正反对照:错误必须 surface 给用户
指南将"绝不静默吞错"标为CRITICAL:
// CORRECT - Error always surfaced to user export class CreateItemComponent { private store = inject(ItemStore); private toast = inject(ToastService); async create(data: CreateItemDto) { try { await this.store.create(data); this.toast.success('Item created successfully'); this.router.navigate(['/items']); } catch (error) { console.error('createItem failed:', error); this.toast.error('Failed to create item. Please try again.'); } } } // WRONG - Error silently caught async create(data: CreateItemDto) { try { await this.store.create(data); } catch (error) { console.error(error); // User sees nothing! } }错误示例与正确示例的差别看起来只有几行,但对用户体验是本质差异:前者用户收到 toast 反馈并可重试,后者用户点击按钮后"毫无反应"。这也是检查清单中"所有用户操作都要有反馈(toast/视觉)"的来源。
5.3 可复用的 ErrorState 组件
指南提供了一个完整的 standalone 错误状态组件模板,包含可选的 retry 出口:
@Component({ selector: "app-error-state", standalone: true, imports: [NgOptimizedImage], template: ` <div class="error-state"> <img ngSrc="/assets/error-icon.svg" width="64" height="64" alt="" /> <h3>{{ title() }}</h3> <p>{{ message() }}</p> @if (retry.observed) { <button (click)="retry.emit()" class="btn-primary">Try Again</button> } </div> `, }) export class ErrorStateComponent { title = input("Something went wrong"); message = input("An unexpected error occurred"); retry = output<void>(); }值得注意的细节:
- 使用
input()信号化输入并带默认值,外部可不传参直接使用; retry.observed是信号输出(output())的observed属性,只有父组件真正监听了 retry 事件时才渲染"Try Again"按钮,避免出现"点了没反应的死按钮";- 图片用
ngSrc(NgOptimizedImage)走 Angular 原生图片优化管线。
六、按钮状态模式:异步期间必须禁用触发
6.1 按钮加载态模板
<button (click)="handleSubmit()" [disabled]="isSubmitting() || !form.valid" class="btn-primary" > @if (isSubmitting()) { <app-spinner size="small" class="mr-2" /> Saving... } @else { Save Changes } </button>按钮在提交中同时做两件事:disabled禁止重复点击,内容区切换为 spinner + "Saving..." 文案。
6.2 CRITICAL:异步操作期间必须禁用触发
// CORRECT - Button disabled while loading @Component({ template: ` <button [disabled]="saving()" (click)="save()"> @if (saving()) { <app-spinner size="sm" /> Saving... } @else { Save } </button> ` }) export class SaveButtonComponent { saving = signal(false); async save() { this.saving.set(true); try { await this.service.save(); } finally { this.saving.set(false); } } } // WRONG - User can click multiple times <button (click)="save()"> {{ saving() ? 'Saving...' : 'Save' }} </button>正确示例用finally保证无论成功失败saving都会被复位;错误示例只在文案上提示"保存中",按钮仍可点击,用户连点会触发并发提交。这条规则与"乐观更新回滚"配合,构成了表单类组件防抖、防重复提交的完整防线。
七、空状态模式:每个集合都必须有
7.1 空状态的强制要求
指南要求"每个列表/集合(collection)必须有空状态",首选实现是@for的@empty块:
@for (item of items(); track item.id) { <app-item-card [item]="item" /> } @empty { <app-empty-state icon="folder-open" title="No items yet" description="Create your first item to get started" actionLabel="Create Item" (action)="openCreateDialog()" /> }空状态不是简单的"没有数据"提示,而是引导用户下一步动作的转化入口:给出图标、标题、描述,并在有动作时提供 CTA 按钮。
7.2 上下文相关的 EmptyState 组件
@Component({ selector: "app-empty-state", template: ` <div class="empty-state"> <span class="icon" [class]="icon()"></span> <h3>{{ title() }}</h3> <p>{{ description() }}</p> @if (actionLabel()) { <button (click)="action.emit()" class="btn-primary"> {{ actionLabel() }} </button> } </div> `, }) export class EmptyStateComponent { icon = input("inbox"); title = input.required<string>(); description = input(""); actionLabel = input<string | null>(null); action = output<void>(); }设计要点:actionLabel为null时整个按钮分支不渲染(@if (actionLabel())也过滤空字符串),组件因此同时胜任"纯提示型空态"和"引导型空态"两种场景。
八、表单模式:加载、校验与提交一体化
详细指南给出一个完整的响应式表单组件,覆盖字段级校验、错误文本与提交态管理:
@Component({ template: ` <form [formGroup]="form" (ngSubmit)="onSubmit()"> <div class="form-field"> <label for="name">Name</label> <input id="name" formControlName="name" [class.error]="isFieldInvalid('name')" /> @if (isFieldInvalid("name")) { <span class="error-text">{{ getFieldError("name") }}</span> } </div> <div class="form-field"> <label for="email">Email</label> <input id="email" type="email" formControlName="email" /> @if (isFieldInvalid("email")) { <span class="error-text">{{ getFieldError("email") }}</span> } </div> <button type="submit" [disabled]="form.invalid || submitting()"> @if (submitting()) { <app-spinner size="sm" /> Submitting... } @else { Submit } </button> </form> `, }) export class UserFormComponent { private fb = inject(FormBuilder); submitting = signal(false); form = this.fb.group({ name: ["", [Validators.required, Validators.minLength(2)]], email: ["", [Validators.required, Validators.email]], }); isFieldInvalid(field: string): boolean { const control = this.form.get(field); return control ? control.invalid && control.touched : false; } getFieldError(field: string): string { const control = this.form.get(field); if (control?.hasError("required")) return "This field is required"; if (control?.hasError("email")) return "Invalid email format"; if (control?.hasError("minlength")) return "Too short"; return ""; } async onSubmit() { if (this.form.invalid) return; this.submitting.set(true); try { await this.service.submit(this.form.value); this.toast.success("Submitted successfully"); } catch { this.toast.error("Submission failed"); } finally { this.submitting.set(false); } } }该示例集中体现了本技能多条模式的协同:
- 字段级错误(Inline error):
isFieldInvalid只在控件invalid && touched时置红,避免"表单还没碰就满屏报错"; - 错误文本映射:
getFieldError按required/email/minlength依次返回中文友好提示,对应错误处理层级的最底层; - 提交按钮:
disabled绑定form.invalid || submitting(),submitting期间显示 spinner; - 提交流程:
try / catch / finally结构保证成功 toast、失败 toast、状态复位三者齐备,呼应"绝不静默吞错"。
九、对话框/模态模式:确认对话框的生命周期管理
指南提供了一个基于DialogService的确认对话框封装,将"打开 → 等待结果 → 处理结果"封装为 Promise 语义:
// dialog.service.ts @Injectable({ providedIn: 'root' }) export class DialogService { private dialog = inject(Dialog); // CDK Dialog or custom async confirm(options: { title: string; message: string; confirmText?: string; cancelText?: string; }): Promise<boolean> { const dialogRef = this.dialog.open(ConfirmDialogComponent, { data: options, }); return await firstValueFrom(dialogRef.closed) ?? false; } } // Usage async deleteItem(item: Item) { const confirmed = await this.dialog.confirm({ title: 'Delete Item', message: `Are you sure you want to delete "${item.name}"?`, confirmText: 'Delete', }); if (confirmed) { await this.store.delete(item.id); } }两个关键点:
firstValueFrom(dialogRef.closed) ?? false:把"对话框关闭流"转换为单值 Promise,用户取消(结果为null/undefined)时默认返回false,调用方无需区分取消与拒绝;- 消费方代码变成线性 await 流程,删除、清空等危险操作的确认逻辑集中在一个服务里,可测试性更强。
十、反模式清单:这些坑必须避开
指南用正反对照的方式列出了三类最高频反模式:
10.1 加载态反模式
// WRONG - Spinner when data exists (causes flash on refetch) @if (loading()) { <app-spinner /> } // CORRECT - Only show loading without data @if (loading() && !items().length) { <app-spinner /> }数据已存在时还渲染 spinner,会在每次后台刷新时造成界面闪烁(flash on refetch),这正是黄金法则要消灭的"陈旧 UI"。
10.2 错误处理反模式
// WRONG - Error swallowed try { await this.service.save(); } catch (e) { console.log(e); // User has no idea! } // CORRECT - Error surfaced try { await this.service.save(); } catch (e) { console.error("Save failed:", e); this.toast.error("Failed to save. Please try again."); }注意错误示例连console.error都写成了console.log——用户对失败毫无感知;正确示例同时记录日志并给用户可行动的提示。
10.3 按钮状态反模式
<!-- WRONG - Button not disabled during submission --> <button (click)="submit()">Submit</button> <!-- CORRECT - Disabled and shows loading --> <button (click)="submit()" [disabled]="loading()"> @if (loading()) { <app-spinner size="sm" /> } Submit </button>十一、UI 状态检查清单:组件完成前的自检表
详细指南在收尾处提供了一份可直接作为 code review 标准的检查清单:
UI 状态
- 错误状态已处理并向用户展示
- 加载状态仅在无数据时显示
- 集合类组件提供了
@empty空状态 - 异步操作期间按钮已禁用
- 合适的场景下按钮显示了加载指示器
数据与变更
- 所有异步操作都有错误处理
- 所有用户操作都有反馈(toast / 视觉反馈)
- 乐观更新在失败时可回滚(rollback)
可访问性(Accessibility)
- 加载状态对屏幕阅读器播报(announced)
- 错误消息与对应表单字段建立关联(
aria-describedby等) - 状态变化后的焦点管理(focus management)
其中可访问性三项容易被忽略,但它们与"surface 错误""加载播报"直接相关,是 UI 状态完备性的最后一环。
十二、与其他技能的集成关系
详细指南末尾给出了本技能在仓库技能体系中的协作方式:
- angular-state-management:使用 Signal store(如
ItemStore)承载状态。UI 组件从 store 直接读取items/loading/error信号,保持"组件纯展示、store 管状态"的分层; - angular:整套模式建立在现代 Angular 特性之上——Signals、
@defer、新控制流语法; - testing-patterns:测试应覆盖所有 UI 状态——错误态、加载态、空态、正常态各自都要有测试用例。
十三、仓库中的落地形态与元信息
在 agentic-awesome-skills 仓库中,该技能以同一套内容存在于三处,便于不同运行环境的 Agent 装载:
- plugins/agentic-awesome-skills-claude/skills/angular-ui-patterns/(本文的关联目录,Claude 插件形态);
- plugins/agentic-awesome-skills/skills/angular-ui-patterns/(通用插件形态的同名副本);
- skills/angular-ui-patterns/(技能库根目录下的独立副本)。
每个副本的目录结构一致,均由四部分组成:入口 SKILL.md(frontmatter + 激活说明)、完整过程 references/detailed-guide.md、概述 README.md 与机器可读的 metadata.json。
从 metadata.json 可以确认该技能的版本信息:当前版本 1.0.0(2026 年 2 月),组织归属为 Agentic Awesome Skills。SKILL.md 的 frontmatter 同时标注了元数据:name: angular-ui-patterns、description(激活描述)、risk: safe(安全等级为安全)、source: self(自研技能)、date_added: "2026-02-27"。metadata 中登记的参考资源还包括 Material Design 与 ng-spartan 等社区生态,可作为进一步学习方向的线索,但本指南中的所有模式与代码均以仓库内文档为准,未做任何外部断言。
结语
angular-ui-patterns提供的不是零散的"好看组件",而是一套可判定的状态决策框架:错误优先、加载仅在无数据时出现、空状态必然存在、异步操作必禁用触发、错误必 surface。将这五条原则与 Angular 的信号化输入输出、新控制流语法、@defer渐进披露结合,配合文末的 UI 状态检查清单,任何团队成员(包括 AI Agent)都能按同一标准产出健壮、可测试、可维护的 Angular 组件 UI。
- AI 技能
- AI 插件
- 人工智能
【免费下载链接】agentic-awesome-skills
AAS Core is the local, agent-first control plane for complete catalog discovery, agent-owned selection, stack validation, and planning, backed by 2,600+ agentic skills. Includes CLI, local MCP, catalog, plugins, and Workbench.
相关推荐
Angular UI Patterns 实战指南:用 Signals 与 @defer 构建健壮的加载、错误与数据展示状态
Angular UI Patterns 实战指南:用 Signals 与 @defer 构建健壮的加载、错误与数据展示状态 本文以 Agentic Awesom
AI 技能AI 插件人工智能用 competitor-analysis 技能跑通完整竞品分析:从竞争格局扫描到差异化定位一站式指南
用 competitor analysis 技能跑通完整竞品分析:从竞争格局扫描到差异化定位一站式指南 pm skills https://link.gitco
AI 技能/插件插件系统需求管理Ark UI Vue 实战指南:用 @ark-ui/vue 构建无样式、可访问、状态机驱动的 Vue 设计系统
Ark UI Vue 实战指南:用 @ark ui/vue 构建无样式、可访问、状态机驱动的 Vue 设计系统 Ark UI 是一套面向设计系统场景的开源无头(
前端UI组件设计系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考