@teamix-evo/skills 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +18 -8
  2. package/_template/SKILL.md.hbs +14 -1
  3. package/manifest.json +79 -6
  4. package/package.json +7 -3
  5. package/src/teamix-evo-code-opentrek/SKILL.md +92 -0
  6. package/src/teamix-evo-code-opentrek/api-layering.md +225 -0
  7. package/src/teamix-evo-code-opentrek/checklist.md +173 -0
  8. package/src/teamix-evo-code-opentrek/error-and-loading.md +269 -0
  9. package/src/teamix-evo-code-opentrek/file-structure.md +273 -0
  10. package/src/teamix-evo-code-opentrek/forms-and-validation.md +220 -0
  11. package/src/teamix-evo-code-opentrek/reuse-first.md +130 -0
  12. package/src/teamix-evo-code-opentrek/routing-and-codesplit.md +298 -0
  13. package/src/teamix-evo-code-opentrek/testing.md +313 -0
  14. package/src/teamix-evo-code-uni-manager/SKILL.md +95 -0
  15. package/src/teamix-evo-code-uni-manager/api-layering.md +370 -0
  16. package/src/teamix-evo-code-uni-manager/checklist.md +193 -0
  17. package/src/teamix-evo-code-uni-manager/error-and-loading.md +389 -0
  18. package/src/teamix-evo-code-uni-manager/file-structure.md +339 -0
  19. package/src/teamix-evo-code-uni-manager/forms-and-validation.md +459 -0
  20. package/src/teamix-evo-code-uni-manager/reuse-first.md +188 -0
  21. package/src/teamix-evo-code-uni-manager/routing-and-codesplit.md +450 -0
  22. package/src/teamix-evo-code-uni-manager/testing.md +396 -0
  23. package/src/teamix-evo-design-opentrek/SKILL.md +71 -0
  24. package/src/teamix-evo-design-opentrek/boundaries.md +513 -0
  25. package/src/teamix-evo-design-opentrek/brand.md +154 -0
  26. package/src/teamix-evo-design-opentrek/checklist.md +83 -0
  27. package/src/teamix-evo-design-opentrek/components.md +245 -0
  28. package/src/teamix-evo-design-opentrek/flows.md +51 -0
  29. package/src/teamix-evo-design-opentrek/foundations.md +271 -0
  30. package/src/teamix-evo-design-opentrek/generation-flow.md +185 -0
  31. package/src/teamix-evo-design-opentrek/patterns/dashboard.md +31 -0
  32. package/src/teamix-evo-design-opentrek/patterns/detail-page.md +202 -0
  33. package/src/teamix-evo-design-opentrek/patterns/form-page.md +289 -0
  34. package/src/teamix-evo-design-opentrek/patterns/list-page.md +334 -0
  35. package/src/teamix-evo-design-opentrek/patterns/page-types.md +154 -0
  36. package/src/teamix-evo-design-opentrek/philosophy.md +96 -0
  37. package/src/teamix-evo-design-opentrek/rules/README.md +39 -0
  38. package/src/teamix-evo-design-opentrek/rules/boundaries.rules.json +391 -0
  39. package/src/teamix-evo-design-uni-manager/SKILL.md +73 -0
  40. package/src/teamix-evo-design-uni-manager/boundaries.md +564 -0
  41. package/src/teamix-evo-design-uni-manager/brand.md +202 -0
  42. package/src/teamix-evo-design-uni-manager/checklist.md +115 -0
  43. package/src/teamix-evo-design-uni-manager/components.md +254 -0
  44. package/src/teamix-evo-design-uni-manager/flows.md +63 -0
  45. package/src/teamix-evo-design-uni-manager/foundations.md +258 -0
  46. package/src/teamix-evo-design-uni-manager/generation-flow.md +194 -0
  47. package/src/teamix-evo-design-uni-manager/patterns/dashboard.md +95 -0
  48. package/src/teamix-evo-design-uni-manager/patterns/detail-page.md +224 -0
  49. package/src/teamix-evo-design-uni-manager/patterns/form-page.md +329 -0
  50. package/src/teamix-evo-design-uni-manager/patterns/list-page.md +356 -0
  51. package/src/teamix-evo-design-uni-manager/patterns/page-types.md +165 -0
  52. package/src/teamix-evo-design-uni-manager/philosophy.md +106 -0
  53. package/src/teamix-evo-design-uni-manager/rules/README.md +49 -0
  54. package/src/teamix-evo-design-uni-manager/rules/boundaries.rules.json +418 -0
  55. package/src/teamix-evo-manage/SKILL.md +281 -0
  56. package/skills/teamix-evo-manage/SKILL.md +0 -138
@@ -0,0 +1,173 @@
1
+ # 编码自检清单
2
+
3
+ > AI 写完 / 改完代码后,**必须**逐项核对。任何一项未通过都不应交付,需要先修复或显式告诉用户哪条不通过、为什么。
4
+
5
+ ---
6
+
7
+ ## 1. 复用判定 ✓
8
+
9
+ - [ ] 新建 UI 组件前,查过 `@teamix-evo/ui` 注册表(MCP `list_components` / `find_components`)
10
+ - [ ] 新建 UI 组件前,grep 过本项目 `src/components/`
11
+ - [ ] 新建工具函数前,grep 过 `src/utils/` `src/lib/` `src/hooks/`
12
+ - [ ] 没有重复实现 ui 包已经提供的能力(Button、Input、Dialog、DataTable 等)
13
+ - [ ] 没有 fork ui 包源码改样式(改样式应当走 design token)
14
+ - [ ] 响应里**显式列出**复用决策日志(✅ 复用了哪些 / 🆕 新写了哪些及原因)
15
+
16
+ ## 2. 数据层分层 ✓
17
+
18
+ - [ ] 组件**没有**直接调 `fetch` / `axios`
19
+ - [ ] 组件**没有**直接读 `import.meta.env` / `process.env`(env 走 `src/lib/env.ts`)
20
+ - [ ] 所有后端调用都落在 `src/services/<domain>.ts`,且是纯函数
21
+ - [ ] Service 函数**不依赖 React**(没有 `useXxx`、没有 `useState`)
22
+ - [ ] Service 函数**不弹通知 / 不跳路由**
23
+ - [ ] 数据 hook 在 `src/hooks/`,使用统一的数据库(react-query / swr,不混用)
24
+ - [ ] 全局只有一份 http 实例,在 `src/lib/http.ts`
25
+
26
+ ## 3. 目录归位 ✓
27
+
28
+ - [ ] 新文件位于约定的顶层目录(`pages/`、`components/`、`services/`、`hooks/`、`stores/`、`types/`、`utils/`、`lib/`)
29
+ - [ ] **没有**新增同义层(如 `views/`、`api/`、`helpers/` 与现有目录并存)
30
+ - [ ] 仅在单个页面使用的组件 / hook 放在 `src/pages/<id>/_components/` 或 `_hooks/`(下划线前缀)
31
+ - [ ] 跨页面复用的组件 / hook 才升到 `src/components/` `src/hooks/`
32
+ - [ ] React Hook 在 `hooks/`,不在 `utils/`(`utils/` 不依赖 React)
33
+ - [ ] 类型声明文件没有运行时代码(纯类型)
34
+
35
+ ## 4. 命名 ✓
36
+
37
+ - [ ] 目录 kebab-case(`order-detail/`)
38
+ - [ ] React 组件文件 PascalCase.tsx,文件名 = 组件名
39
+ - [ ] Hook 文件 camelCase.ts,以 `use` 开头(`useOrderList.ts`)
40
+ - [ ] Service 文件按 domain 命名(`order.ts`),不按 HTTP 动词(`getOrders.ts` ❌)
41
+ - [ ] 名字能让下次复用时被 grep 命中(`OrderStatusBadge` ✓ / `MyBadge` ❌)
42
+
43
+ ## 5. Import 边界 ✓
44
+
45
+ - [ ] 跨目录 import 走 `@/*` 别名,**未使用** `../../../` 相对路径上跳超过 2 层
46
+ - [ ] **未发生反向依赖**:
47
+ - `components/` 没有 import `pages/`
48
+ - `services/` 没有 import `hooks/` `components/` `pages/`
49
+ - `hooks/` 没有 import `pages/`
50
+ - `utils/` `lib/` `types/` 没有 import 业务层
51
+ - [ ] 页面私有目录(`_components/` `_hooks/`)**没有**被其他页面 import
52
+
53
+ ## 6. 类型 ✓
54
+
55
+ - [ ] Domain 类型放 `src/types/<domain>.ts`,不在 service / component 文件就地定义
56
+ - [ ] Service 函数有显式的入参 / 返回 `Promise<T>` 类型
57
+ - [ ] 没有 `any`(必要的转义类型用 `unknown` 配合 narrowing,或加注释说明原因)
58
+ - [ ] Hook 的返回类型由 `react-query` / `swr` 自动推断,无需手写
59
+
60
+ ## 7. 依赖卫生 ✓
61
+
62
+ - [ ] 没有引入与项目内现有库**功能重复**的新依赖(如同时有 `lodash` 和 `lodash-es`)
63
+ - [ ] 日期 / 数据请求 / 表单 / 状态等核心库**全项目统一**(看现有 `package.json`,不要新加同类)
64
+ - [ ] 没有 `import` 未在 `package.json` 声明的包
65
+
66
+ ## 8. 副作用与可测性 ✓
67
+
68
+ - [ ] Service 函数是纯函数(同样输入 → 同样输出,无隐式全局读写)
69
+ - [ ] 工具函数(`src/utils/`)是纯函数
70
+ - [ ] 没有在模块顶层(import 时)发请求 / 弹通知 / 改全局状态
71
+ - [ ] 异步操作有错误处理路径(hook 暴露 `error`,UI 有 fallback)
72
+
73
+ ## 9. 表单与校验 ✓
74
+
75
+ (若改动涉及表单 — 详见 [`forms-and-validation.md`](forms-and-validation.md))
76
+
77
+ - [ ] 表单状态用 `react-hook-form`,**未用** `useState` 拼字段
78
+ - [ ] 校验用 `zod` schema,schema 落 `src/services/<domain>.schema.ts`
79
+ - [ ] 类型从 schema 推导(`z.infer<...>`),**未手写**重复 interface
80
+ - [ ] 错误信息写在 schema 里,UI 只读取,**未在组件里硬编码**校验文案
81
+ - [ ] 提交走 mutation hook,**未在** `<form onSubmit>` 里直接 fetch
82
+ - [ ] 字段级 / 通用错误分别处理(`form.setError` vs `toast`)
83
+ - [ ] 表单组件使用 `@teamix-evo/ui` 的 `Form` / `FormField`,**未自撸** `<form>`
84
+
85
+ ## 10. 错误与加载态 ✓
86
+
87
+ (若改动包含 UI / 数据 hook — 详见 [`error-and-loading.md`](error-and-loading.md))
88
+
89
+ - [ ] App 根**存在**全局 `ErrorBoundary` + `Toaster`
90
+ - [ ] 全局 `onError` 调 `reportError`(`src/lib/monitor.ts`),**未**静默吞错
91
+ - [ ] 数据 hook 三态全处理:`isPending` → Skeleton、`isError` → 错误面板 + 重试、data → 内容
92
+ - [ ] **未用** `useState<boolean>` + `useEffect` 自管 loading
93
+ - [ ] Mutation 成功 → `toast.success` + 跳转 / invalidate;失败 → `toast.error`,**未用** `alert()`
94
+ - [ ] Skeleton 撑住布局,**未**用空 div 等待数据(防 CLS)
95
+ - [ ] 长操作(> 1s)按钮 `disabled`,**未**允许双击重复提交
96
+
97
+ ## 11. 路由与代码分包 ✓
98
+
99
+ (若改动涉及路由 — 详见 [`routing-and-codesplit.md`](routing-and-codesplit.md))
100
+
101
+ - [ ] 新页面用 `React.lazy()` 引入(登录页 / 错误页 / Layout 例外)
102
+ - [ ] Lazy 路由有 `<Suspense fallback>` 兜底
103
+ - [ ] 鉴权 / 角色判断在 `src/routes/guards.tsx`,**未**写在每个 page 顶部
104
+ - [ ] 路由声明集中在 `src/routes/index.tsx`,**未**分散在多文件
105
+ - [ ] 404 / 403 / 500 兜底路由存在
106
+ - [ ] 列表筛选 / 分页用 `useSearchParams`,**未**只放 `useState`
107
+ - [ ] 跳转用 `<Link>` / `useNavigate`,**未用** `window.location.href`
108
+ - [ ] 重型第三方库(图表 / 富文本 / PDF)用动态 import,**未**直接顶部 import
109
+
110
+ ## 12. 测试覆盖 ✓
111
+
112
+ (详见 [`testing.md`](testing.md))
113
+
114
+ - [ ] 新增纯函数(`src/utils/`、`src/services/`)有同名 `*.test.ts`(**必测**)
115
+ - [ ] 新增 zod schema 有 `parse` 成功 / 失败用例(**必测**)
116
+ - [ ] 关键业务路径(下单 / 支付 / 鉴权)有组件 / hook 测试(**必测**)
117
+ - [ ] 测试文件就近放在 `<source>.test.ts(x)`,**未**集中到顶层 `tests/`
118
+ - [ ] 用 `msw` mock 后端,**未**自己 mock fetch / axios
119
+ - [ ] 测试名是行为描述("保留两位小数"),**不是**函数名("test formatMoney")
120
+ - [ ] **未**为凑覆盖率测无价值代码(ui 包源码 / 三方库)
121
+ - [ ] 跳过测试时显式说明原因(纯组合 / 已有覆盖 / 一次性脚本)
122
+
123
+ ## 13. 与 design 规范的协作 ✓
124
+
125
+ (若改动包含 UI)
126
+
127
+ - [ ] 视觉部分按 [`teamix-evo-design-opentrek`](../teamix-evo-design-opentrek/SKILL.md) 自检过 token / 间距 / 圆角 / 动效
128
+ - [ ] 没有硬编码颜色 / 间距 / 字号(交给 ui 包 + design token)
129
+
130
+ ---
131
+
132
+ ## ⚠️ 红线(一票否决)
133
+
134
+ 任何一条触发即必须重写或回退:
135
+
136
+ 1. **组件直接 fetch / axios** —— 数据层未分层
137
+ 2. **新建 Button / Input / Dialog 等基础组件** —— 重造 ui 包能力
138
+ 3. **services 函数依赖 React** —— 边界穿透
139
+ 4. **`src/views/` 与 `src/pages/` 并存(或 `src/api/` 与 `src/services/`)** —— 同义层污染
140
+ 5. **响应里没有复用决策日志** —— 流程未跑通
141
+ 6. **反向依赖**(service 引 hook、component 引 page) —— 依赖图破坏
142
+ 7. **多份 http 实例** —— 鉴权 / 错误处理无法统一
143
+ 8. **没有全局 ErrorBoundary** —— 异常直接白屏给用户
144
+ 9. **表单用 `useState` 拼字段、用 onChange 手写校验** —— 弃 react-hook-form + zod
145
+ 10. **页面顶部 import 不 lazy + 没有 Suspense** —— 首屏 bundle 爆炸 / 进入即崩
146
+ 11. **纯函数 / zod schema 无单测** —— ROI 最高的测试缺位
147
+ 12. **`alert()` / `window.location.href` 出现在业务代码** —— 用 toast / useNavigate
148
+
149
+ ---
150
+
151
+ ## AI 输出格式建议
152
+
153
+ 完成改动时,在响应末尾附:
154
+
155
+ ```markdown
156
+ ## 编码合规自检
157
+
158
+ - 复用判定: ✅ 复用 ui 包 Button / DataTable;🆕 新写 OrderStatusBadge(原因:业务状态映射)
159
+ - 数据层: ✅ services/order.ts 纯函数;✅ hooks/useOrderList.ts 包 react-query;✅ 组件未 fetch
160
+ - 目录: ✅ 页面在 pages/orders/;✅ 跨页面组件在 components/
161
+ - 命名: ✅ kebab 目录 / Pascal 组件 / camel hook
162
+ - 边界: ✅ 全部走 @/\*;✅ 无反向依赖
163
+ - 类型: ✅ types/order.ts 集中声明
164
+ - 表单(若涉及): ✅ useForm + zodResolver;✅ schema 在 services/order.schema.ts
165
+ - 错误/加载(若涉及): ✅ 三态全处理;✅ ErrorBoundary 已在 App 根;✅ mutation toast
166
+ - 路由(若涉及): ✅ React.lazy + Suspense;✅ 守卫已复用;✅ 404 兜底已存在
167
+ - 测试: ✅ 新增 formatXxx → formatXxx.test.ts(5 case);✅ schema parse 失败用例已加
168
+ - 红线: ✅ 全过
169
+
170
+ 未通过项: 无
171
+ ```
172
+
173
+ 如有未通过项,明确写出"❌ 第 X 条:<原因> → <处理方式>"。
@@ -0,0 +1,269 @@
1
+ # 错误与加载态规范
2
+
3
+ > **核心约定**:渲染错误用 `ErrorBoundary`,异步加载用 `react-query` 的 `isPending/isError`(必要时叠 `Suspense`),全局兜底必须存在。**不允许**白屏 / 红屏直出给用户。
4
+
5
+ ---
6
+
7
+ ## 三层兜底
8
+
9
+ ```
10
+ ┌──────────────────────────────────────────┐
11
+ │ App 根:全局 ErrorBoundary + Toaster │ 最后一道:防白屏 / 兜未捕获
12
+ └──────────────┬───────────────────────────┘
13
+
14
+ ┌──────────────────────────────────────────┐
15
+ │ 路由 / 关键 widget:页面级 ErrorBoundary │ 范围隔离:一页崩不带垮全站
16
+ └──────────────┬───────────────────────────┘
17
+
18
+ ┌──────────────────────────────────────────┐
19
+ │ 数据 hook:isPending / isError │ 细粒度:每块数据自己的 loading
20
+ └──────────────────────────────────────────┘
21
+ ```
22
+
23
+ ---
24
+
25
+ ## §1 · 全局 ErrorBoundary
26
+
27
+ 每个 teamix-evo 业务应用**必须**在根挂一个全局 ErrorBoundary,以及一个 toast 容器。
28
+
29
+ ### 推荐实现:`react-error-boundary`
30
+
31
+ 业界主流(2024+,Vercel / shadcn 模板默认),比手写 class component 更易组合。
32
+
33
+ ```tsx
34
+ // src/main.tsx
35
+ import { ErrorBoundary } from 'react-error-boundary';
36
+ import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
37
+ import { Toaster } from '@/components/ui';
38
+ import { GlobalErrorFallback } from '@/components/GlobalErrorFallback';
39
+ import { reportError } from '@/lib/monitor';
40
+
41
+ const queryClient = new QueryClient({
42
+ defaultOptions: {
43
+ queries: { staleTime: 30_000, retry: 1 },
44
+ mutations: { retry: 0 },
45
+ },
46
+ });
47
+
48
+ createRoot(document.getElementById('root')!).render(
49
+ <ErrorBoundary
50
+ FallbackComponent={GlobalErrorFallback}
51
+ onError={(err, info) => reportError(err, info)}
52
+ >
53
+ <QueryClientProvider client={queryClient}>
54
+ <App />
55
+ <Toaster />
56
+ </QueryClientProvider>
57
+ </ErrorBoundary>,
58
+ );
59
+ ```
60
+
61
+ ```tsx
62
+ // src/components/GlobalErrorFallback.tsx
63
+ import { Button } from '@teamix-evo/ui';
64
+
65
+ export function GlobalErrorFallback({ resetErrorBoundary }: { resetErrorBoundary: () => void }) {
66
+ return (
67
+ <div className="flex min-h-screen items-center justify-center">
68
+ <div className="text-center">
69
+ <h1 className="text-2xl font-semibold">页面出错了</h1>
70
+ <p className="text-muted-foreground mt-2">已记录,可尝试刷新或返回首页</p>
71
+ <Button className="mt-4" onClick={resetErrorBoundary}>重新加载</Button>
72
+ </div>
73
+ </div>
74
+ );
75
+ }
76
+ ```
77
+
78
+ 要点:
79
+
80
+ - `onError` **必须**调 `reportError` 上报(Sentry / 自家日志)
81
+ - fallback **必须**给用户可执行的下一步动作(重试 / 返回 / 联系支持)
82
+ - **不要**只显示 "Something went wrong" —— 用户看到等于白屏
83
+
84
+ ---
85
+
86
+ ## §2 · 路由 / 页面级 ErrorBoundary
87
+
88
+ 跨路由的崩溃不应连带全站。在 router 配置或 Layout 里再嵌一层:
89
+
90
+ ```tsx
91
+ // src/routes/index.tsx
92
+ {
93
+ path: '/orders/:id',
94
+ element: (
95
+ <ErrorBoundary FallbackComponent={PageErrorFallback}>
96
+ <Suspense fallback={<PageSkeleton />}>
97
+ <OrderDetailPage />
98
+ </Suspense>
99
+ </ErrorBoundary>
100
+ ),
101
+ }
102
+ ```
103
+
104
+ 或使用 react-router v6.4+ 的 `errorElement`(推荐,内置):
105
+
106
+ ```tsx
107
+ {
108
+ path: '/orders/:id',
109
+ element: <OrderDetailPage />,
110
+ errorElement: <PageErrorFallback />,
111
+ }
112
+ ```
113
+
114
+ ---
115
+
116
+ ## §3 · 数据加载态(react-query 范式)
117
+
118
+ **不要**在组件里用 `useState<boolean>` + `useEffect` 自己管 loading。所有异步数据走 react-query:
119
+
120
+ ```tsx
121
+ function OrderListPage() {
122
+ const { data, isPending, isError, error, refetch } = useOrderList(params);
123
+
124
+ if (isPending) return <TableSkeleton rows={10} />;
125
+ if (isError) return <ErrorPanel message={error.message} onRetry={refetch} />;
126
+
127
+ return <OrderTable data={data} />;
128
+ }
129
+ ```
130
+
131
+ ### 三种状态都要处理
132
+
133
+ | 状态 | 必须输出 |
134
+ | --- | --- |
135
+ | `isPending`(首次加载) | Skeleton / Spinner,**不允许**显示空 div |
136
+ | `isError` | 错误面板 + 重试按钮,**不允许**直接 `throw` |
137
+ | `isFetching`(后台刷新) | 表格右上角小转圈即可,不要遮罩 |
138
+
139
+ ### Skeleton vs Spinner 怎么选
140
+
141
+ | 场景 | 用 |
142
+ | --- | --- |
143
+ | 列表 / 卡片 / 详情 | Skeleton(撑住布局,避免 CLS) |
144
+ | 短动作(按钮提交、< 500ms 加载) | Spinner |
145
+ | 全屏切换 | PageSkeleton 或 logo loading |
146
+
147
+ ---
148
+
149
+ ## §4 · Mutation 反馈
150
+
151
+ ```tsx
152
+ const mutation = useCreateOrder();
153
+
154
+ mutation.mutate(values, {
155
+ onSuccess: (order) => {
156
+ toast.success(`订单 ${order.id} 已创建`);
157
+ navigate(`/orders/${order.id}`);
158
+ },
159
+ onError: (err) => {
160
+ toast.error(err.message); // 已被 lib/http.ts 归一化
161
+ },
162
+ });
163
+ ```
164
+
165
+ 约束:
166
+
167
+ - 成功 → `toast.success` + 跳转 / invalidate
168
+ - 失败 → `toast.error`,**不要** `alert()`
169
+ - 长操作(> 1s) → 按钮 `disabled` + 文案改"提交中…",**不要**双击重复提交
170
+ - **不要**在 mutation 内部弹 toast —— hook 是数据通道,toast 由组件触发
171
+
172
+ ---
173
+
174
+ ## §5 · Suspense 数据边界(可选,渐进采用)
175
+
176
+ react-query v5 支持 `useSuspenseQuery`,可把 loading 转给 `<Suspense>`:
177
+
178
+ ```tsx
179
+ function OrderListPage() {
180
+ const { data } = useSuspenseQuery({ queryKey: ['orders'], queryFn: listOrders });
181
+ return <OrderTable data={data} />; // 不需要 isPending 分支
182
+ }
183
+
184
+ // 父级:
185
+ <Suspense fallback={<TableSkeleton rows={10} />}>
186
+ <OrderListPage />
187
+ </Suspense>
188
+ ```
189
+
190
+ **何时用**:
191
+
192
+ - 多个 query 需要"全部 ready 才渲染"时,Suspense 比手写 `if (a.isPending || b.isPending)` 简洁
193
+ - 与 `React.lazy` 配合做代码分包(见 [`routing-and-codesplit.md`](routing-and-codesplit.md))
194
+
195
+ **何时不用**:
196
+
197
+ - 单一 query 的简单页面,直接 `isPending` 分支更直观
198
+ - 团队对 Suspense 还不熟 → 先用 `isPending`,慢慢迁
199
+
200
+ ---
201
+
202
+ ## §6 · 全局错误上报
203
+
204
+ ```ts
205
+ // src/lib/monitor.ts
206
+ import * as Sentry from '@sentry/react';
207
+
208
+ if (import.meta.env.PROD) {
209
+ Sentry.init({
210
+ dsn: import.meta.env.VITE_SENTRY_DSN,
211
+ tracesSampleRate: 0.1,
212
+ });
213
+ }
214
+
215
+ export function reportError(err: unknown, extra?: Record<string, unknown>) {
216
+ if (import.meta.env.DEV) console.error('[reportError]', err, extra);
217
+ Sentry.captureException(err, { extra });
218
+ }
219
+
220
+ // window 级未捕获
221
+ window.addEventListener('unhandledrejection', (e) => reportError(e.reason));
222
+ window.addEventListener('error', (e) => reportError(e.error));
223
+ ```
224
+
225
+ 要点:
226
+
227
+ - 所有 ErrorBoundary 的 `onError` **必须**调 `reportError`
228
+ - mutation `onError` 在归一化 toast 之外也应调 `reportError`
229
+ - **不要**在 component 里直接 `import * as Sentry` —— 走 `monitor.ts` 这一层包装,便于替换
230
+
231
+ ---
232
+
233
+ ## 反模式速查
234
+
235
+ | 反模式 | 为什么禁 | 应该 |
236
+ | --- | --- | --- |
237
+ | 没有全局 ErrorBoundary | 异常直接白屏 | App 根挂一层 |
238
+ | `useState(false)` + `useEffect` 管 loading | 重复造轮,易漏 cancel | 走 react-query / useSuspenseQuery |
239
+ | 只判 `isLoading` 不判 `isError` | 错误态显示空白 | 三态都处理 |
240
+ | `if (data) return <>...</> else return null` | 无 skeleton,布局抖动 + CLS | 加 Skeleton |
241
+ | `alert('保存失败')` | 阻断用户、丑、不可样式化 | `toast.error()` |
242
+ | ErrorBoundary 不上报 | 错过线上 bug | `onError` 接 `reportError` |
243
+ | service 里 `try/catch` 吞错 | 上层失去判断依据 | 让 error 抛上来,hook / boundary 决定 |
244
+
245
+ ---
246
+
247
+ ## 与 ui 包的边界
248
+
249
+ `@teamix-evo/ui` 提供:
250
+
251
+ - `Skeleton` / `TableSkeleton` / `PageSkeleton`
252
+ - `Toaster` + `toast()` API
253
+ - `Spinner`(短动作)
254
+ - `EmptyState`(无数据态,区别于错误态)
255
+
256
+ **优先用 ui 包**,不要自己撸 spinner / toast。
257
+
258
+ ---
259
+
260
+ ## AI 必须输出的错误处理日志
261
+
262
+ ```
263
+ ## 错误 / 加载态
264
+
265
+ - 全局: ✅ App 根 ErrorBoundary + reportError 已存在
266
+ - 页面: ✅ /orders/:id 加 errorElement + Suspense fallback
267
+ - 数据: ✅ useOrderList 三态都处理(Skeleton / ErrorPanel / Table)
268
+ - Mutation: ✅ onSuccess toast + navigate;onError toast(已归一化)
269
+ ```