# Frontend Architecture

## 1. 基本信息

- Feature ID: `FEAT-TICKET-FILTER`
- 页面 / 模块: 工单列表筛选与保存视图
- 关联需求: `FR-001`, `FR-002`, `FR-003`
- 关联路由: `/tickets`
- 目标质量等级: P2
- 设计真相源: `docs/design/design-brief.md`, `docs/design/image-to-frontend-spec.md`
- Contract 真相源: `contracts/openapi.yaml`

## 2. 架构目标

- 把筛选、表格、保存视图弹窗拆成可独立实现和验证的 feature 组件。
- 让筛选状态可被 URL 分享、刷新恢复和 E2E 验证。
- 保持通用表格能力和工单业务字段分离，避免把业务枚举写进 UI primitive。
- 覆盖 desktop / tablet / mobile，不把移动端降级为横向滚动表格。

## 3. 分层与目录

| 层级 | 建议路径 | 职责 | 禁止事项 |
| --- | --- | --- | --- |
| Page / Route | `app/tickets/page.tsx` | 组合页面、读取 query、挂载数据查询 | 直接写筛选控件细节 |
| Feature | `features/tickets/` | 工单筛选、表格、保存视图、权限表现 | 放跨业务 primitive |
| UI primitives | `packages/ui/` | button、select、dialog、badge、table shell | 依赖工单状态枚举 |
| Data table | `packages/ui/data-table/` | 分页、列显隐、空态容器、行状态 | 写死 ticket 字段 |
| API client | `packages/api-client/` | `listTickets`, `createTicketView` | 直接操作 React 状态 |
| Model / Schema | `features/tickets/model/` | `TicketFilter`、query schema、优先级枚举 | 写入展示样式 |

## 4. 组件树

```text
<TicketListPage>
  <TicketListShell>
    <TicketToolbar />
    <TicketFilterBar />
    <TicketFilterDrawer />
    <TicketTable />
    <TicketPagination />
    <SavedViewModal />
  </TicketListShell>
</TicketListPage>
```

| 组件 | 层级 | 职责 | 数据输入 | 事件输出 | 可复用范围 |
| --- | --- | --- | --- | --- | --- |
| `TicketFilterBar` | Feature | 展示 desktop/tablet 筛选控件和 dirty 状态 | `TicketFilter`, `dirty`, `loading`, `canSaveView` | `onChange`, `onReset`, `onSaveView` | feature |
| `TicketFilterDrawer` | Feature | 移动端筛选抽屉 | `TicketFilter`, `open` | `onApply`, `onClose` | feature |
| `SavedViewModal` | Feature | 命名并保存筛选视图 | `TicketFilter`, `saving`, `error` | `onSubmit`, `onCancel` | feature |
| `TicketTable` | Feature | 展示工单结果和错误 / 空态 | `Ticket[]`, `pagination`, `status` | `onRetry`, `onPageChange` | feature |
| `DataTableShell` | Primitive | 表格布局、列显隐、分页容器 | generic rows | generic callbacks | project |

## 5. 状态管理

| 状态 | 所属层级 | 来源 | 持久化方式 | 变化事件 | 测试方式 |
| --- | --- | --- | --- | --- | --- |
| `status`, `priority`, `assigneeId`, `search` | Page / Feature | query string | URL | filter change / reset | e2e |
| tickets remote data | Feature | `GET /tickets` | query cache | filter change / retry | mock + e2e |
| saved view form | Component | user input | local | submit / cancel | story interaction |
| drawer / modal open | Component | user interaction | local | open / close | story |
| permission | Feature | user role | auth context / API | role change | e2e |

## 6. 响应式架构

| 视口 | 结构 | 筛选策略 | 列表 / 表格策略 | 操作区策略 | 必需截图 |
| --- | --- | --- | --- | --- | --- |
| Desktop >= 1200px | Header + inline filters + table | 横向筛选条，chip 回显 | 完整表格，保留分页 | 保存视图按钮在 header | default / empty / error |
| Tablet 768-1199px | Header + wrapping filters + table | 筛选条换行，次要筛选收进菜单 | 隐藏低优先级列 | 操作区折叠为菜单 | default |
| Mobile < 768px | Header + filter drawer + cards | 顶部筛选按钮打开抽屉 | 表格转工单卡片 | 保存视图进更多菜单 | default / mobile |

## 7. 动效与可访问性

| 组件 / 场景 | 动效 | reduced motion 行为 | 焦点 / 键盘规则 | ARIA / 语义 |
| --- | --- | --- | --- | --- |
| `SavedViewModal` | opacity + scale, 160ms | 直接显示 / 隐藏 | 打开聚焦名称输入，Esc 关闭并恢复焦点 | `role=dialog`, `aria-modal=true` |
| `TicketFilterDrawer` | translateX + opacity, 180ms | 直接显示 / 隐藏 | focus trap，关闭后回到筛选按钮 | drawer label |
| filter chips | opacity + slight translate, 120ms | 无位移动画 | 删除按钮可键盘访问 | chip 文案含字段名 |
| table refresh | skeleton + row fade, 160ms | skeleton 不做 shimmer | 重试按钮可聚焦 | loading / error 状态可感知 |

## 8. 性能预算

| 风险 | 阈值 / 预算 | 处理策略 | 验证方式 |
| --- | --- | --- | --- |
| 表格行数 | 单页超过 100 行 | 服务端分页或虚拟滚动 | e2e + performance smoke |
| 筛选请求 | 输入搜索 | debounce 300ms | interaction test |
| 动效 | 表格刷新频繁 | 高频刷新禁用行级动画 | visual / manual |
| 图标 | 操作按钮较多 | 使用 tree-shakeable icon import | bundle check |

## 9. Story 与测试矩阵

| 状态 / 场景 | Story | Interaction | E2E | Visual | A11y | Performance |
| --- | --- | --- | --- | --- | --- | --- |
| default | `TicketListPage/Default` | filters change | `/tickets` query | desktop | smoke | N/A |
| loading | `TicketListPage/Loading` | disabled controls | N/A | desktop | smoke | shimmer off under reduced motion |
| empty | `TicketListPage/Empty` | clear filters | no results | desktop | smoke | N/A |
| error | `TicketListPage/Error` | retry | API failure | desktop | smoke | N/A |
| permission | `TicketListPage/PermissionDenied` | save hidden | role=agent | desktop | smoke | N/A |
| mobile | `TicketListPage/Mobile` | drawer apply | mobile viewport | mobile | focus trap | N/A |

## 10. 复用决策

| 候选组件 / Hook | 复用范围 | 是否抽象 | 原因 | 约束 |
| --- | --- | --- | --- | --- |
| `DataTableShell` | project | Yes | 表格分页、空态和列显隐可跨后台页面复用 | 不写业务字段 |
| `TicketFilterBar` | feature | No | 工单字段和权限语义强 | 留在 tickets feature |
| `SavedViewModal` | feature | No | 视图保存依赖当前 filter schema | 后续第二个业务场景出现再抽象 |
