@usethink/cf-admin-fe 0.1.0 → 0.2.6
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.
- package/LICENSE +21 -0
- package/README.md +242 -53
- package/dist/composables/createAdminFlagSession.d.ts +42 -0
- package/dist/composables/createAdminRequest.d.ts +95 -0
- package/dist/composables/createAdminTokenSession.d.ts +49 -0
- package/dist/composables/index.d.ts +7 -1
- package/dist/composables/useAdminListLoader.d.ts +45 -0
- package/dist/composables/useAdminSidebar.d.ts +17 -0
- package/dist/composables/useClipboard.d.ts +24 -2
- package/dist/composables/useConfirmDialog.d.ts +10 -4
- package/dist/composables/useNow.d.ts +3 -0
- package/dist/composables/useTablePagination.d.ts +6 -0
- package/dist/composables/useTableSelection.d.ts +2 -2
- package/dist/composables/useToast.d.ts +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/system-config.d.ts +34 -0
- package/dist/utils/admin-navigation.d.ts +66 -0
- package/dist/utils/admin-query.d.ts +29 -0
- package/dist/utils/batch-limit.d.ts +38 -0
- package/dist/utils/batch-result.d.ts +41 -0
- package/dist/utils/body-scroll-lock.d.ts +13 -0
- package/dist/utils/csv-export.d.ts +8 -0
- package/dist/utils/currency-cents.d.ts +16 -0
- package/dist/utils/datetime.d.ts +20 -0
- package/dist/utils/error-message.d.ts +7 -0
- package/dist/utils/format-metadata.d.ts +5 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/otp-input.d.ts +11 -0
- package/dist/utils/system-config-engine.d.ts +65 -0
- package/docs/001_cf-admin-fe/346/217/220/345/217/226/345/267/256/350/267/235/345/210/206/346/236/220/346/212/245/345/221/212_2026-08-05.md +224 -0
- package/docs/002_cf-admin-fe/344/275/277/347/224/250/350/257/264/346/230/216/344/271/246/344/270/216/346/263/250/346/204/217/344/272/213/351/241/271.md +801 -0
- package/package.json +30 -6
- package/recipes/AdminLayout.vue +73 -0
- package/recipes/README.md +130 -0
- package/recipes/i18n-merge.snippet.ts +29 -0
- package/recipes/list-page.snippet.ts +141 -0
- package/recipes/main-styles.snippet.ts +13 -0
- package/recipes/reexports/components.ts +19 -0
- package/recipes/reexports/composables.ts +29 -0
- package/recipes/reexports/utils.ts +31 -0
- package/recipes/router-guard.snippet.ts +32 -0
- package/recipes/useAdminAuth.cookie.ts +29 -0
- package/recipes/useAdminAuth.shop.ts +29 -0
- package/recipes/useAdminRequest.cookie.ts +36 -0
- package/recipes/useAdminRequest.ts +37 -0
- package/src/components/AdminLoginShell.vue +31 -0
- package/src/components/AdminMetadataDetail.vue +121 -0
- package/src/components/AdminShell.vue +396 -0
- package/src/components/ConfigField.vue +401 -0
- package/src/components/ConfirmDialog.vue +67 -3
- package/src/components/index.ts +6 -0
- package/src/composables/createAdminFlagSession.ts +129 -0
- package/src/composables/createAdminRequest.ts +322 -0
- package/src/composables/createAdminTokenSession.ts +149 -0
- package/src/composables/index.ts +51 -0
- package/src/composables/useAdminBatchOperation.ts +4 -0
- package/src/composables/useAdminListLoader.ts +124 -0
- package/src/composables/useAdminSidebar.ts +68 -0
- package/src/composables/useClipboard.ts +41 -2
- package/src/composables/useConfirmDialog.ts +26 -4
- package/src/composables/useNow.ts +38 -0
- package/src/composables/useTablePagination.ts +6 -0
- package/src/composables/useTableSelection.ts +2 -2
- package/src/composables/useToast.ts +4 -1
- package/src/env.d.ts +2 -2
- package/src/i18n/index.ts +8 -0
- package/src/i18n/kit-messages.ts +297 -0
- package/src/index.ts +143 -4
- package/src/styles/admin-primitives.css +23 -0
- package/src/styles/index.css +1 -0
- package/src/styles/login-primitives.css +149 -0
- package/src/types/index.ts +7 -0
- package/src/types/system-config.ts +38 -0
- package/src/utils/admin-navigation.ts +138 -0
- package/src/utils/admin-query.ts +51 -0
- package/src/utils/batch-limit.ts +66 -0
- package/src/utils/batch-result.ts +81 -0
- package/src/utils/body-scroll-lock.ts +63 -0
- package/src/utils/csv-export.ts +25 -0
- package/src/utils/currency-cents.ts +30 -0
- package/src/utils/datetime.ts +54 -0
- package/src/utils/error-message.ts +32 -0
- package/src/utils/format-metadata.ts +11 -0
- package/src/utils/index.ts +81 -0
- package/src/utils/otp-input.ts +18 -0
- package/src/utils/system-config-engine.ts +212 -0
- package/dist/components/index.d.ts +0 -4
- package/dist/index.d.ts +0 -17
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
# @usethink/cf-admin-fe 使用说明书与注意事项
|
|
2
|
+
|
|
3
|
+
> **版本:** 0.2.6(以 `package.json` 为准)
|
|
4
|
+
> **读者:** 新建 CF 管理端前端、或把既有 admin 接到本套件的开发者
|
|
5
|
+
> **原则:** 只写仓库里真实存在的 API 与约束;不发明业务能力。
|
|
6
|
+
> **配套:** 根 [`README.md`](../README.md)(API 面速查)· [`recipes/README.md`](../recipes/README.md)(复制清单)· [`docs/001_…差距分析`](./001_cf-admin-fe提取差距分析报告_2026-08-05.md)(提取决策背景)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 0. 一句话定位
|
|
11
|
+
|
|
12
|
+
`@usethink/cf-admin-fe` 是 **Vue 3 管理端前端基础套件(admin foundation)**,不是:
|
|
13
|
+
|
|
14
|
+
- 通用组件库(不是 Element Plus / Ant Design)
|
|
15
|
+
- 业务中间层(没有订单/抽奖/RBAC/支付页面)
|
|
16
|
+
- Worker / API 包(那是 `@usethink/cf-core` 等)
|
|
17
|
+
|
|
18
|
+
它要解决的是:**每个 CF 产品都要写一遍的管理端壳、表格 UX、请求/会话工厂、错误与批量反馈、系统配置字段渲染**,在 **≥2 个线上消费方**(或已验证的第三条路径)验证后进入套件,让新产品「几分钟搭壳、业务页面自己写」。
|
|
19
|
+
|
|
20
|
+
**线上消费方(验证面):**
|
|
21
|
+
|
|
22
|
+
| 产品 | 会话 | 角色 |
|
|
23
|
+
|------|------|------|
|
|
24
|
+
| **cf-shop** | Bearer JWT + localStorage TTL | 主路径参考 |
|
|
25
|
+
| **cf-lottery** | Bearer + 产品 L3 胶水(平台/仿冒) | Bearer 扩展参考 |
|
|
26
|
+
| **cf-auth** | Cookie HTTP 会话 + 本地 flag | **验证 cookie 路径**;不要克隆其 OAuth/业务页 |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 1. 心智模型:双会话 · 三层 · 子路径
|
|
31
|
+
|
|
32
|
+
### 1.1 两种一等会话(不要混)
|
|
33
|
+
|
|
34
|
+
| 模型 | 套件 API | 真实会话在哪 | 路由守卫读什么 |
|
|
35
|
+
|------|----------|--------------|----------------|
|
|
36
|
+
| **Bearer** | `createAdminTokenSession` | localStorage 的 token+TTL;请求带 `Authorization` | `readToken()` / `hasToken` |
|
|
37
|
+
| **Cookie + flag** | `createAdminFlagSession` | **HTTP cookie**(Better Auth 等,产品侧);localStorage 只存守卫快路径 flag | `readFlag()` / `isLoggedIn` |
|
|
38
|
+
|
|
39
|
+
- Bearer 产品:**不要**再发明第二套 token 读写;在 recipe 上叠产品 key / 多租户 / 仿冒。
|
|
40
|
+
- Cookie 产品:**不要**被强迫用 token TTL / 密码表单;OAuth、健康探测、登出清 cookie 永远在产品侧。套件只提供 flag 存储 + 请求工厂 + 外壳 UI。
|
|
41
|
+
|
|
42
|
+
### 1.2 三层分工
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
┌─────────────────────────────────────────────────────────┐
|
|
46
|
+
│ 产品 L3:领域页面、领域 API、菜单文案、OAuth、平台角色 │
|
|
47
|
+
├─────────────────────────────────────────────────────────┤
|
|
48
|
+
│ @usethink/cf-admin-fe:壳、表格 UX、request/session、 │
|
|
49
|
+
│ 错误/批量纯函数、ConfigField、i18n 种子、recipes │
|
|
50
|
+
├─────────────────────────────────────────────────────────┤
|
|
51
|
+
│ @usethink/cf-core 等:Worker、crypto、secrets… │
|
|
52
|
+
└─────────────────────────────────────────────────────────┘
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 1.3 导入:优先子路径
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
// 推荐(Vitest / node 不解析根 SFC barrel)
|
|
59
|
+
import { useTablePagination, copyWithToast } from '@usethink/cf-admin-fe/composables'
|
|
60
|
+
import { toErrorMessage, buildAdminOffsetParams } from '@usethink/cf-admin-fe/utils'
|
|
61
|
+
import { mergeAdminKitMessages } from '@usethink/cf-admin-fe/i18n'
|
|
62
|
+
import AdminModal from '@usethink/cf-admin-fe/components/AdminModal.vue'
|
|
63
|
+
import '@usethink/cf-admin-fe/styles'
|
|
64
|
+
|
|
65
|
+
// 根入口也可以,但测试环境若扫到 .vue 可能更麻烦
|
|
66
|
+
import { useToast } from '@usethink/cf-admin-fe'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**开发期 monorepo:** 常用 `file:../cf-admin-fe` 或 tsconfig `paths` 指到 `cf-admin-fe/src/*`(shop/lottery/auth 均已如此)。入口以 **`src/`** 为准;`dist/` 仅 `tsc` 生成的 d.ts,由 `prepack` 现场构建。
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 2. 从零搭一个新产品管理端(榨干路径)
|
|
74
|
+
|
|
75
|
+
按顺序做;每一步都对应真实 recipe / API。
|
|
76
|
+
|
|
77
|
+
### 步骤 0 — 安装与 peer
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm install @usethink/cf-admin-fe
|
|
81
|
+
# 或 monorepo:npm install file:../cf-admin-fe
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- **peer:** `vue` ^3.4
|
|
85
|
+
- **可选 peer:** `vue-i18n`(组件存在 `$t`/`$te` 时用;无 i18n 也能跑,回退英文/中文 fallback 字符串)
|
|
86
|
+
|
|
87
|
+
### 步骤 1 — 样式
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// main.ts
|
|
91
|
+
import '@usethink/cf-admin-fe/styles'
|
|
92
|
+
// 等价于 tokens + admin-primitives + login-primitives
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
或按需:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import '@usethink/cf-admin-fe/styles/tokens.css'
|
|
99
|
+
import '@usethink/cf-admin-fe/styles/admin-primitives.css' // .admin-page / .toolbar / .lang-select …
|
|
100
|
+
import '@usethink/cf-admin-fe/styles/login-primitives.css' // .admin-login / .login-card
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
产品可再 `@import` 业务 CSS(伪装横幅、平台强调色等),**不要**把领域样式回灌进套件。
|
|
104
|
+
|
|
105
|
+
### 步骤 2 — 根节点挂一次 Toast
|
|
106
|
+
|
|
107
|
+
```vue
|
|
108
|
+
<!-- App.vue -->
|
|
109
|
+
<template>
|
|
110
|
+
<RouterView />
|
|
111
|
+
<ToastContainer />
|
|
112
|
+
</template>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`useToast` 是 **模块单例**;多挂多次 Container 会重复渲染同一队列。`MAX_TOASTS = 5`,超出丢弃较早条目。
|
|
116
|
+
|
|
117
|
+
### 步骤 3 — i18n 合并套件种子
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
// 见 recipes/i18n-merge.snippet.ts
|
|
121
|
+
import { mergeAdminKitMessages } from '@usethink/cf-admin-fe/i18n'
|
|
122
|
+
|
|
123
|
+
const messages = {
|
|
124
|
+
'zh-CN': mergeAdminKitMessages(hostZh, 'zh-CN'),
|
|
125
|
+
en: mergeAdminKitMessages(hostEn, 'en'),
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**套件种子覆盖的命名空间(宿主同名键优先覆盖):**
|
|
130
|
+
|
|
131
|
+
- `pagination.*` — AdminPagination
|
|
132
|
+
- `confirm.*` — ConfirmDialog
|
|
133
|
+
- `adminModal.*`
|
|
134
|
+
- `adminConfigField.*` — ConfigField 通用文案
|
|
135
|
+
- `adminLayout.*` — 外壳 logout / 侧栏等(**菜单项 `adminLayout.menu*` 仍由产品提供**)
|
|
136
|
+
- `adminLogs.*` — AdminMetadataDetail 等日志详情
|
|
137
|
+
- `adminConfig.*` — 系统配置页字段级文案约定:`adminConfig.fields.<key>.{label,description,effect}`;缺失时 ConfigField 用 `$te` 回退后端文本
|
|
138
|
+
|
|
139
|
+
**不要**把业务成功文案、硬删除策略用语放进套件。
|
|
140
|
+
|
|
141
|
+
### 步骤 4 — 选会话模型并复制 auth recipe
|
|
142
|
+
|
|
143
|
+
**Bearer(shop / lottery 风格):** 复制 `recipes/useAdminAuth.shop.ts` → `src/composables/useAdminAuth.ts`
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
const session = createAdminTokenSession({
|
|
147
|
+
storageKey: 'admin_token', // 产品可改
|
|
148
|
+
ttlMs: 8 * 60 * 60 * 1000, // DEFAULT_ADMIN_TOKEN_TTL_MS
|
|
149
|
+
})
|
|
150
|
+
export function readAdminToken() { return session.readToken() }
|
|
151
|
+
export function useAdminAuth() {
|
|
152
|
+
return {
|
|
153
|
+
token: session.token,
|
|
154
|
+
isLoggedIn: session.isLoggedIn,
|
|
155
|
+
setToken: session.setToken,
|
|
156
|
+
clearToken: session.clearToken,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
要点:
|
|
162
|
+
|
|
163
|
+
- 存储形态:`{ token, expiry }` JSON;过期自动清键。
|
|
164
|
+
- 旧版裸字符串 token:解析时保留一次迁移。
|
|
165
|
+
- 路由守卫在 setup 外用 **`readAdminToken()` / `session.readToken()`**,不要只依赖组件内 ref。
|
|
166
|
+
|
|
167
|
+
**Cookie(cf-auth 风格):** 复制 `recipes/useAdminAuth.cookie.ts`
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
export const adminFlagSession = createAdminFlagSession({
|
|
171
|
+
storageKey: 'cf_auth_admin_ok',
|
|
172
|
+
})
|
|
173
|
+
// markLoggedIn / clearSession / readFlag / writeFlag
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
产品仍须实现:`/api/admin/health` 探测、登录写 cookie、登出清 cookie、允许列表。套件 **不管** cookie 本身。
|
|
177
|
+
|
|
178
|
+
### 步骤 5 — 管理端请求工厂
|
|
179
|
+
|
|
180
|
+
复制 `recipes/useAdminRequest.ts`(Bearer)或 `useAdminRequest.cookie.ts`。
|
|
181
|
+
|
|
182
|
+
**`createAdminRequest` 关键旋钮(务必按产品对齐后端):**
|
|
183
|
+
|
|
184
|
+
| 选项 | 含义 | 典型 |
|
|
185
|
+
|------|------|------|
|
|
186
|
+
| `getToken` | 闭包取 Bearer;空则不带 Authorization | lottery 风格 |
|
|
187
|
+
| `token`(单次调用) | 覆盖 `getToken`(含空串) | **shop** `api/admin.ts` 每方法传 token |
|
|
188
|
+
| `credentials` | cookie 管理端用 `'include'` | cf-auth |
|
|
189
|
+
| `resultMode` | `'http-ok'` 仅看 `res.ok`;`'ok-envelope'` 要 `res.ok && data.ok` | shop / lottery |
|
|
190
|
+
| `errorField` | `'error'` \| `'message'` \| `'auto'` | 对齐 JSON 错误字段 |
|
|
191
|
+
| `onUnauthorized` | 401 抛错前副作用 | 接 `createAdminUnauthorizedHandler` |
|
|
192
|
+
| `onSuccess` | 成功后回调 | cookie 产品「治愈」缺失 flag |
|
|
193
|
+
| `redirectOnUnauthorized` | `false` 时 401 仍抛错但跳过清会话 | **登录探测 token** 必备 |
|
|
194
|
+
| `createAdminRequestRaw` | 不解析 JSON,只 `res.ok` + 401 | 导出 blob / 模板下载 |
|
|
195
|
+
|
|
196
|
+
**401 标准副作用:**
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { createAdminUnauthorizedHandler } from '@usethink/cf-admin-fe/utils'
|
|
200
|
+
|
|
201
|
+
const handleUnauthorized = createAdminUnauthorizedHandler({
|
|
202
|
+
clearSession: () => useAdminAuth().clearToken(),
|
|
203
|
+
loginPath: '/admin/login',
|
|
204
|
+
defaultRedirectPath: '/admin',
|
|
205
|
+
})
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
行为:清会话 → 硬跳转 `loginPath?redirect=<当前 path+search+hash>` → **已在登录页则不再跳**(防死循环)。
|
|
209
|
+
|
|
210
|
+
结构化错误:`AdminRequestError`(`status` / `code` / `data` / `response`),产品可映射到自有 `AdminApiError`。
|
|
211
|
+
|
|
212
|
+
### 步骤 6 — 路由守卫
|
|
213
|
+
|
|
214
|
+
复制 `recipes/router-guard.snippet.ts`,核心决策用纯函数:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import { decideAdminRouteAccess, resolveAdminLoginRedirect } from '@usethink/cf-admin-fe/utils'
|
|
218
|
+
|
|
219
|
+
const decision = decideAdminRouteAccess({
|
|
220
|
+
hasToken: Boolean(readAdminToken()), // cookie 产品:readFlag()
|
|
221
|
+
requiresAuth: to.meta.requiresAuth,
|
|
222
|
+
requiresGuest: to.meta.requiresGuest,
|
|
223
|
+
fullPath: to.fullPath,
|
|
224
|
+
})
|
|
225
|
+
// allow | { type:'login', redirect } | { type:'home' }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
登录成功落地:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
const path = resolveAdminLoginRedirect(route.query.redirect, {
|
|
232
|
+
defaultPath: '/admin',
|
|
233
|
+
allowPrefix: '/admin',
|
|
234
|
+
// 平台应用可 isAllowed: (p) => p.startsWith('/platform')
|
|
235
|
+
})
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**开放重定向防护:** 拒绝 `//…`、带 scheme 的 URL;默认只允许 `allowPrefix` 下路径。平台应用务必收紧 `allowPrefix` / `isAllowed`。
|
|
239
|
+
|
|
240
|
+
产品专属规则(引导页、平台角色、仿冒身份)**叠在** `decideAdminRouteAccess` 之后,不要改套件函数语义。
|
|
241
|
+
|
|
242
|
+
### 步骤 7 — 布局外壳
|
|
243
|
+
|
|
244
|
+
复制 `recipes/AdminLayout.vue`:
|
|
245
|
+
|
|
246
|
+
- `AdminShell` + `useAdminSidebar`(默认 `max-width: 1023px` 汉堡)
|
|
247
|
+
- **菜单数据、标题** 是产品的;**用户信息默认在顶栏右上**(`userName` / `userAvatar` + 退出),侧栏底不再默认占位——菜单多时更省高、也更符合后台习惯
|
|
248
|
+
- 可选 `#header-actions`(语言等)在用户信息左侧;若必须把自定义块放回侧栏底,用 `#sidebar-footer`(有插槽才渲染)
|
|
249
|
+
- 退出:清会话 + toast + 跳转登录(`showLogout` / `showUser` 可关)
|
|
250
|
+
|
|
251
|
+
登录页:用 `AdminLoginShell` 包住 **产品表单**(密码 / OTP / OAuth 按钮都是产品的)。
|
|
252
|
+
|
|
253
|
+
### 步骤 8 — 列表页标准组合(最能「榨干」的一页)
|
|
254
|
+
|
|
255
|
+
完整伪代码见 **`recipes/list-page.snippet.ts`**。能力清单:
|
|
256
|
+
|
|
257
|
+
| 能力 | API | 注意 |
|
|
258
|
+
|------|-----|------|
|
|
259
|
+
| 加载态 / 错误 / 竞态 | `useAdminListLoader` → `runLoad` | await 后 `ctx.isStale()` 则丢弃结果;`onError` 建议 `toErrorMessage` |
|
|
260
|
+
| 分页 | `useTablePagination` → `page` / `limit` / `total` / `offset` / `setTotal` | **不是** `pageSize` 字段名 |
|
|
261
|
+
| 多选 | `useTableSelection(rows, getKey)` | 支持 MaybeRef 行列表;shift 连选;`isSelectable` |
|
|
262
|
+
| limit+offset(**string query**) | `buildAdminOffsetParams(page, limit)` | **位置参数**;产出 **字符串**;仅 URLSearchParams |
|
|
263
|
+
| 写入已有 QS | `applyAdminOffsetParams(params, page, limit)` | 覆盖同名键,保留业务筛选 |
|
|
264
|
+
| limit+offset(**number body**) | **产品手写** `(page-1)*limit` | **不要**套 string helper;见 §5.8 / shop |
|
|
265
|
+
| 复制选中 | `copyWithToast` | 文案产品 i18n |
|
|
266
|
+
| 批量上限 | `checkAdminBatchLimit(ids, { unit, limit?, message? })` | 默认 **20(auth)**;`max(N)` API 须 `{ limit: N }` |
|
|
267
|
+
| 批量结果 toast | `buildBatchToast` / `resolveBatchToastType` | 服务端一次返回 `{success,failed}` |
|
|
268
|
+
| 逐条串行批量 | `useAdminBatchOperation().runSequential` | 进度 ref;与「一次 HTTP 批量」不同 |
|
|
269
|
+
|
|
270
|
+
**offset API 真实签名(勿写成对象;返回值是 string):**
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
buildAdminOffsetParams(page: number, limit: number, options?: { fallbackLimit?: number })
|
|
274
|
+
// → { limit: string, offset: string } // 不是 number!
|
|
275
|
+
|
|
276
|
+
applyAdminOffsetParams(
|
|
277
|
+
params: URLSearchParams,
|
|
278
|
+
page: number,
|
|
279
|
+
limit: number,
|
|
280
|
+
options?: { fallbackLimit?: number },
|
|
281
|
+
)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
非法页码 → 1;非法 limit → `fallbackLimit` 或 20。
|
|
285
|
+
|
|
286
|
+
**number body 列表(shop 常见)手写即可:**
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
await listApi({
|
|
290
|
+
...filters,
|
|
291
|
+
limit: limit.value,
|
|
292
|
+
offset: (page.value - 1) * limit.value, // number
|
|
293
|
+
})
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**`runLoad` 推荐形态(string query):**
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
const { loading, loadError, runLoad, invalidate } = useAdminListLoader()
|
|
300
|
+
|
|
301
|
+
await runLoad(async (ctx) => {
|
|
302
|
+
const res = await listApi(buildAdminOffsetParams(page.value, limit.value))
|
|
303
|
+
if (ctx.isStale()) return
|
|
304
|
+
items.value = res.items
|
|
305
|
+
setTotal(res.total)
|
|
306
|
+
}, {
|
|
307
|
+
onError: (err) => toErrorMessage(err, t('adminXxx.loadFailed')),
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
onUnmounted(() => invalidate())
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### 步骤 9 — 确认框 / 弹层 / 分页组件
|
|
314
|
+
|
|
315
|
+
```vue
|
|
316
|
+
<ConfirmDialog
|
|
317
|
+
v-model="confirmVisible"
|
|
318
|
+
:message="confirmMessage"
|
|
319
|
+
:options="confirmOptionDefs"
|
|
320
|
+
:option-values="confirmOptionValues"
|
|
321
|
+
:require-phrase="confirmRequirePhrase"
|
|
322
|
+
danger
|
|
323
|
+
@confirm="onConfirm"
|
|
324
|
+
/>
|
|
325
|
+
<AdminModal v-model="visible" title="…">…</AdminModal>
|
|
326
|
+
<AdminPagination
|
|
327
|
+
:page="page"
|
|
328
|
+
:limit="limit"
|
|
329
|
+
:total="total"
|
|
330
|
+
@update:page="setPage"
|
|
331
|
+
@update:limit="setLimit"
|
|
332
|
+
/>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
`useConfirmDialog`:
|
|
336
|
+
|
|
337
|
+
- `askConfirm(msg)` → `Promise<boolean>`
|
|
338
|
+
- `askConfirmWithOptions(msg, { options, requirePhrase, danger })` → `{ confirmed, options }`
|
|
339
|
+
- `requirePhrase`:必须键入完全匹配才可确认(硬删除对齐服务端字面量)
|
|
340
|
+
|
|
341
|
+
弹层滚动:组件内部会用 `lockBodyScroll` / `unlockBodyScroll`(**引用计数**,嵌套弹层安全)。产品自写遮罩时也可直接调这两个 util。
|
|
342
|
+
|
|
343
|
+
### 步骤 10 — 系统配置页(有配置中心时)
|
|
344
|
+
|
|
345
|
+
1. 产品维护 **分区 id 联合、分组名列表、危险区 UI、加载/保存 API**。
|
|
346
|
+
2. 套件纯引擎(`/utils`):
|
|
347
|
+
|
|
348
|
+
- `groupDefinitionsByName`
|
|
349
|
+
- `countSectionItems` / `countAdvancedItems`
|
|
350
|
+
- `buildActiveGroupedDefinitions`(`showAdvanced`)
|
|
351
|
+
- `resolveSystemConfigSectionId` / `getSystemConfigSection` / `listedSystemConfigGroups`
|
|
352
|
+
- `auditSystemConfigSectionCoverage`(测试里审计分区覆盖)
|
|
353
|
+
|
|
354
|
+
3. 每项用 **`ConfigField`**:boolean / 枚举多选 / integer / cents / sensitive / textarea。
|
|
355
|
+
4. cents 编辑走套件 `formatCents` / `parseYuanToCents`(人民币风格 2 位小数);**多币种订单金额**仍用产品 `@shared/money`。
|
|
356
|
+
|
|
357
|
+
### 步骤 11 — 审计日志详情(可选)
|
|
358
|
+
|
|
359
|
+
- **纯函数(旧页 / 自研 markup):** `formatMetadata(record)`(安全 `JSON.stringify`,失败则 `String`)— shop/lottery 日志页已 re-export 此函数即可
|
|
360
|
+
- **组件(新页优先):** `AdminMetadataDetail`(深路径 `components/AdminMetadataDetail.vue`)
|
|
361
|
+
- **不要**为「对齐套件」把已有详情 DOM 全量迁组件(维护税高、收益低)
|
|
362
|
+
- 依赖宿主 `adminLogs.*` i18n(已由 kit messages 提供默认种子)
|
|
363
|
+
|
|
364
|
+
### 步骤 12 — 轻量 reexport(可选但推荐)
|
|
365
|
+
|
|
366
|
+
复制 `recipes/reexports/*`,让旧路径 `@/composables/useX` 继续工作,视图无需大改:
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
// src/composables/useClipboard.ts
|
|
370
|
+
export {
|
|
371
|
+
writeClipboardText,
|
|
372
|
+
copyText,
|
|
373
|
+
copyWithToast,
|
|
374
|
+
type CopyWithToastOptions,
|
|
375
|
+
} from '@usethink/cf-admin-fe/composables'
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## 3. 能力全表:什么时候用哪个
|
|
381
|
+
|
|
382
|
+
### 3.1 Composables
|
|
383
|
+
|
|
384
|
+
| API | 用途 | 不要用来… |
|
|
385
|
+
|-----|------|-----------|
|
|
386
|
+
| `useTablePagination` | page/limit/total/offset | 存业务筛选(筛选是产品 state) |
|
|
387
|
+
| `useTableSelection` | 多选、全选、shift 连选 | 跨页服务端选择模型(未实现) |
|
|
388
|
+
| `useConfirmDialog` + `ConfirmDialog` | 危险确认、键入短语 | 复杂多步向导 |
|
|
389
|
+
| `useToast` + `ToastContainer` | 全局提示 | 表单字段级校验(用本地 error) |
|
|
390
|
+
| `useAdminBatchOperation` | **逐条**串行、要进度条 | 服务端已返回汇总的一次 POST(改用 buildBatchToast) |
|
|
391
|
+
| `useAdminListLoader` | 列表竞态与 loading | 替换业务 fetch |
|
|
392
|
+
| `useAdminSidebar` | 窄屏侧栏开关 | 桌面信息架构 |
|
|
393
|
+
| `useNow` | 共享 60s 时钟(倒计时多组件) | 毫秒级动画时钟 |
|
|
394
|
+
| `writeClipboardText` | 只写剪贴板 | 管理端主复制路径(缺反馈) |
|
|
395
|
+
| `copyText(text, event)` | 按钮内 ✅/❌ | 与 toast **叠用**且不看结果 |
|
|
396
|
+
| `copyWithToast` | 管理端复制主路径 | 创建成功后「失败仍业务成功」的降级文案场景 |
|
|
397
|
+
| `createAdminTokenSession` | Bearer 会话 | Cookie 产品 |
|
|
398
|
+
| `createAdminFlagSession` | Cookie 守卫 flag | 当真会话存储 |
|
|
399
|
+
| `createAdminRequest` / `Raw` | 统一 fetch | 领域 URL 与 DTO 定义 |
|
|
400
|
+
|
|
401
|
+
### 3.2 剪贴板三件套(必读)
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
// ① 管理端列表/详情 — 推荐
|
|
405
|
+
const ok = await copyWithToast(text, {
|
|
406
|
+
successMessage: t('…success'),
|
|
407
|
+
errorMessage: t('…failed'),
|
|
408
|
+
// showToast?: 可注入,默认 useToast().showToast
|
|
409
|
+
})
|
|
410
|
+
if (ok) { /* 关弹层等 */ }
|
|
411
|
+
|
|
412
|
+
// ② 前台交付明细按钮 — 只改按钮文字
|
|
413
|
+
copyText(secret, event)
|
|
414
|
+
|
|
415
|
+
// ③ 静默 / 业务降级
|
|
416
|
+
try {
|
|
417
|
+
await writeClipboardText(url)
|
|
418
|
+
showToast(t('openedWithLink'), 'success')
|
|
419
|
+
} catch {
|
|
420
|
+
showToast(t('openedOnly'), 'success') // 注意:仍是业务成功,不是 copy error
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**禁止:** `copyText` 后立刻无条件 `showToast(..., 'success')`(失败时假成功)。管理端交付复制应只走 `copyWithToast`。
|
|
425
|
+
|
|
426
|
+
### 3.3 错误与批量纯函数
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
import {
|
|
430
|
+
toErrorMessage,
|
|
431
|
+
buildBatchToast,
|
|
432
|
+
resolveBatchToastType,
|
|
433
|
+
formatBatchCountsMessage,
|
|
434
|
+
formatDefaultBatchMixedMessage,
|
|
435
|
+
hasBatchFailures,
|
|
436
|
+
checkAdminBatchLimit,
|
|
437
|
+
DEFAULT_ADMIN_BATCH_LIMIT,
|
|
438
|
+
} from '@usethink/cf-admin-fe/utils'
|
|
439
|
+
|
|
440
|
+
// catch / loader
|
|
441
|
+
showToast(toErrorMessage(e, t('loadFailed')), 'error')
|
|
442
|
+
|
|
443
|
+
// 服务端 { success, failed }
|
|
444
|
+
const toast = buildBatchToast(res, {
|
|
445
|
+
mixed: '完成:成功 {success},失败 {failed}',
|
|
446
|
+
success: '已处理 {count} 条',
|
|
447
|
+
})
|
|
448
|
+
showToast(toast.message, toast.type)
|
|
449
|
+
|
|
450
|
+
// 已有 i18n 文案,只复用色调
|
|
451
|
+
showToast(t('batchDone', counts), resolveBatchToastType(counts))
|
|
452
|
+
|
|
453
|
+
// POST 前
|
|
454
|
+
const check = checkAdminBatchLimit(ids, { unit: '人' }) // 或 limit: 50, message: '…'
|
|
455
|
+
if (!check.ok) {
|
|
456
|
+
showToast(check.message, 'error')
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
`toErrorMessage` 顺序:`Error.message` → string → `object.message` → fallback → `String(error)`。
|
|
462
|
+
**空 fallback 时** 可能返回 `''`;展示给用户时请传产品默认句。
|
|
463
|
+
|
|
464
|
+
### 3.4 导航纯函数
|
|
465
|
+
|
|
466
|
+
- `createAdminUnauthorizedHandler` — 见步骤 5
|
|
467
|
+
- `decideAdminRouteAccess` — 见步骤 6
|
|
468
|
+
- `resolveAdminLoginRedirect` — 见步骤 6
|
|
469
|
+
|
|
470
|
+
均 **不依赖 vue-router**;产品负责 `router.push` / `location.assign`。
|
|
471
|
+
|
|
472
|
+
### 3.5 其它 utils
|
|
473
|
+
|
|
474
|
+
| API | 说明 |
|
|
475
|
+
|-----|------|
|
|
476
|
+
| `formatDate` / `toDateTimeLocalValue` / `dateTimeLocalToIso` / `isoToDateTimeLocal` | 管理表单与表格时间 |
|
|
477
|
+
| `formatIpFingerprint` | 日志 IP 指纹展示 |
|
|
478
|
+
| `downloadCsv` / `safeCsvCell` | 导出;单元格防 Excel 公式注入(`=+-@` 等) |
|
|
479
|
+
| `lockBodyScroll` / `unlockBodyScroll` / `isBodyScrollLocked` | 引用计数滚动锁 |
|
|
480
|
+
| `normalizeCents` / `formatCents` / `parseYuanToCents` | ConfigField 分/元;非多币种账本 |
|
|
481
|
+
| `normalizeOtpDigits` / `isOtpComplete` / `OTP_CODE_LENGTH` | 登录 OTP(默认 6 位) |
|
|
482
|
+
| `formatMetadata` | 日志 metadata 预览 |
|
|
483
|
+
| `buildAdminOffsetParams` / `applyAdminOffsetParams` | 列表 **string** offset 查询(URLSearchParams);number body 见 §5.8 |
|
|
484
|
+
|
|
485
|
+
### 3.6 组件深路径
|
|
486
|
+
|
|
487
|
+
发布 `exports` 已声明(可直接 deep import):
|
|
488
|
+
|
|
489
|
+
- `AdminPagination.vue` / `AdminModal.vue` / `ConfirmDialog.vue` / `ToastContainer.vue`
|
|
490
|
+
- `ConfigField.vue` / `AdminShell.vue` / `AdminLoginShell.vue` / `AdminMetadataDetail.vue`
|
|
491
|
+
|
|
492
|
+
CSS:`./styles`、`./styles/tokens.css`、`./styles/admin-primitives.css`、`./styles/login-primitives.css`
|
|
493
|
+
Recipes:`./recipes/*`
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
## 4. 按场景的「标准写法」
|
|
498
|
+
|
|
499
|
+
### 4.1 登录成功
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
// Bearer
|
|
503
|
+
setToken(res.token)
|
|
504
|
+
router.replace(resolveAdminLoginRedirect(route.query.redirect))
|
|
505
|
+
|
|
506
|
+
// Cookie
|
|
507
|
+
markLoggedIn() // 写 flag;cookie 已由 Set-Cookie 完成
|
|
508
|
+
router.replace(resolveAdminLoginRedirect(route.query.redirect))
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### 4.2 列表加载失败
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
onError: (err) => toErrorMessage(err, t('adminXxx.loadFailed'))
|
|
515
|
+
// 模板:v-if="loadError" 展示 + 重试按钮调 loadData
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### 4.3 单行危险删除
|
|
519
|
+
|
|
520
|
+
```ts
|
|
521
|
+
if (!(await askConfirm(t('confirmDelete'), { danger: true, requirePhrase: 'DELETE' }))) return
|
|
522
|
+
try {
|
|
523
|
+
await apiDelete(id)
|
|
524
|
+
showToast(t('deleted'), 'success')
|
|
525
|
+
await loadData()
|
|
526
|
+
} catch (e) {
|
|
527
|
+
showToast(toErrorMessage(e, t('deleteFailed')), 'error')
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### 4.4 多选批量(服务端一次返回汇总)
|
|
532
|
+
|
|
533
|
+
```ts
|
|
534
|
+
const check = checkAdminBatchLimit(selectedIds.value, { unit: '条' })
|
|
535
|
+
if (!check.ok) { showToast(check.message, 'error'); return }
|
|
536
|
+
const res = await apiBatch(selectedIds.value)
|
|
537
|
+
const toast = buildBatchToast(res, { mixed: t('mixed'), success: t('ok') })
|
|
538
|
+
showToast(toast.message, toast.type)
|
|
539
|
+
clearSelection()
|
|
540
|
+
await loadData()
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### 4.5 多选批量(只能逐条调旧 API)
|
|
544
|
+
|
|
545
|
+
```ts
|
|
546
|
+
const result = await runSequential(selectedItems, (row) => apiDisable(row.id))
|
|
547
|
+
if (!result) return // 已在 operating
|
|
548
|
+
showToast(
|
|
549
|
+
buildBatchToast(result, { mixed: t('mixed'), success: t('ok') }).message,
|
|
550
|
+
resolveBatchToastType(result),
|
|
551
|
+
)
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### 4.6 复制链接并关弹层
|
|
555
|
+
|
|
556
|
+
```ts
|
|
557
|
+
const ok = await copyWithToast(url, {
|
|
558
|
+
successMessage: t('linkCopied'),
|
|
559
|
+
errorMessage: t('linkCopyFailed'),
|
|
560
|
+
})
|
|
561
|
+
if (ok) copyLinkVisible.value = false
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### 4.7 导出 CSV
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
downloadCsv(`orders-${date}.csv`, [
|
|
568
|
+
['orderNo', 'email', 'amount'],
|
|
569
|
+
...rows.map((r) => [r.orderNo, r.email, r.amountCents]),
|
|
570
|
+
])
|
|
571
|
+
// 单元格已经过 safeCsvCell;不要自己拼未转义 CSV
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## 5. 与三端对齐时的注意事项(实战坑)
|
|
577
|
+
|
|
578
|
+
### 5.1 必须对齐的后端契约
|
|
579
|
+
|
|
580
|
+
| 点 | shop 倾向 | lottery 倾向 | auth 倾向 |
|
|
581
|
+
|----|-----------|-------------|-----------|
|
|
582
|
+
| 成功判定 | `http-ok` | `ok-envelope` | 按接口 |
|
|
583
|
+
| 错误字段 | `error` | `message` | 按接口 |
|
|
584
|
+
| 认证 | 每方法 `token` 或 getToken | getToken Bearer | cookie `credentials: 'include'` |
|
|
585
|
+
| 401 | 清 token + 硬跳登录 | 可能清整段业务会话 | 清 flag + 产品登出 |
|
|
586
|
+
|
|
587
|
+
**工厂配错 `resultMode` 会把成功当失败或吞掉业务 `ok:false`。** 新项目先读一条真实列表响再选模式。
|
|
588
|
+
|
|
589
|
+
### 5.2 有意不要用 `copyWithToast` 的路径
|
|
590
|
+
|
|
591
|
+
- 开启/创建活动后:复制分享链失败时 toast 仍是「已开启/已创建」→ 保留 `writeClipboardText` + 分支成功文案。
|
|
592
|
+
- 创建后静默复制(失败不阻断)→ 空 catch + `writeClipboardText`。
|
|
593
|
+
- 前台 C 端分享 → 可用 `writeClipboardText`,不强制套件 toast 文案。
|
|
594
|
+
|
|
595
|
+
### 5.3 不要抽进套件的东西
|
|
596
|
+
|
|
597
|
+
- 领域 CRUD 页面、领域 API 目录
|
|
598
|
+
- OAuth / PKCE / 允许列表策略
|
|
599
|
+
- 抽奖 L3、仿冒登录、平台角色矩阵
|
|
600
|
+
- 多币种金额展示(用产品 money 库)
|
|
601
|
+
- 日期选择器、DataTable 框架、富文本
|
|
602
|
+
- 「以后可能第三个产品会用」的一次性 helper
|
|
603
|
+
|
|
604
|
+
**提取标准:** 两个产品已复制粘贴同样逻辑 → 提取;只有一个产品需要 → 留产品。
|
|
605
|
+
|
|
606
|
+
### 5.4 i18n
|
|
607
|
+
|
|
608
|
+
- 套件组件键用 `mergeAdminKitMessages`,避免漏 `pagination.total` 等。
|
|
609
|
+
- ConfigField 字段文案:`adminConfig.fields.<key>.*`;不要把后端自由文本当 `$t` defaultMessage(裸 `@`/`{}` 会炸解析)。
|
|
610
|
+
- 批量/复制成功失败句 **全部产品侧**;套件 `formatDefaultBatchMixedMessage` 只是零依赖中文回退。
|
|
611
|
+
|
|
612
|
+
### 5.5 测试与类型
|
|
613
|
+
|
|
614
|
+
- 套件:`npm test`(Vitest)+ `npm run type-check`
|
|
615
|
+
- 产品:改 reexport 或升级套件后跑本仓 `vue-tsc`
|
|
616
|
+
- 单测优先 `from '@usethink/cf-admin-fe/composables'` / `/utils`,避免根 barrel 拉 SFC
|
|
617
|
+
- `createAdminTokenSession` / flag / request 均支持注入 `storage` / 测试 double
|
|
618
|
+
|
|
619
|
+
### 5.6 封包与 dist
|
|
620
|
+
|
|
621
|
+
```bash
|
|
622
|
+
npm run clean # scripts/clean-dist.mjs,防过期 d.ts
|
|
623
|
+
npm run build # clean + tsc 声明
|
|
624
|
+
npm pack # prepack → build;入口仍是 src/
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
- `files`:`src`、`dist`、`recipes`、`docs`、`README.md`、`LICENSE`
|
|
628
|
+
- **不要**手写或提交过期根 `dist/index.d.ts`
|
|
629
|
+
- 版本语义:行为兼容补丁可留在 0.2.6 文档/接线;破坏性 API 再 bump
|
|
630
|
+
|
|
631
|
+
### 5.7 monorepo paths
|
|
632
|
+
|
|
633
|
+
产品 tsconfig 常见:
|
|
634
|
+
|
|
635
|
+
```json
|
|
636
|
+
"paths": {
|
|
637
|
+
"@usethink/cf-admin-fe": ["../../cf-admin-fe/src/index.ts"],
|
|
638
|
+
"@usethink/cf-admin-fe/*": ["../../cf-admin-fe/src/*"]
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
Vite 需 `dedupe: ['vue']`,避免套件与应用两份 Vue(peer 类型/运行时分裂)。
|
|
643
|
+
|
|
644
|
+
### 5.8 cf-shop 接线对照(维护用 · 非强制全量统一)
|
|
645
|
+
|
|
646
|
+
> 目的:后人升级套件时知道 **shop 已经怎么接**、**哪些故意不接**。
|
|
647
|
+
> 范围仅 admin/platform foundation;**不含** storefront C 端。
|
|
648
|
+
|
|
649
|
+
| 能力 | shop 现状 | 备注 |
|
|
650
|
+
|------|-----------|------|
|
|
651
|
+
| 会话 | `createAdminTokenSession` → `useAdminAuth` | Bearer + localStorage TTL |
|
|
652
|
+
| 请求 | `createAdminRequest` / `createAdminRequestRaw` + 401 清会话 | `resultMode: 'http-ok'` 等产品约定 |
|
|
653
|
+
| 路由 | `decideAdminRouteAccess` | `router/index.ts` |
|
|
654
|
+
| 壳 / 登录 | `AdminShell` / `AdminLoginShell` | 产品菜单 + 登录 UI |
|
|
655
|
+
| i18n | `mergeAdminKitMessages` | 中英 |
|
|
656
|
+
| 样式 | kit primitives + 产品 `admin.css` | 不把 C 端视觉塞进 kit |
|
|
657
|
+
| Toast / Confirm / Pagination / Modal | 产品薄 reexport 或直连 kit 组件 | `ToastContainer` 根挂一次 |
|
|
658
|
+
| 表格 UX | `useTablePagination` / `useTableSelection` / `useAdminBatchOperation` reexport | |
|
|
659
|
+
| 列表加载 | **17/17** admin 视图 `useAdminListLoader` | 含 isStale |
|
|
660
|
+
| 错误文案 | `toErrorMessage`(via `useFormat` 再导出) | |
|
|
661
|
+
| 复制 | 多数路径 `copyWithToast`;lottery 开启/创建等有意 silent/success-fallback | 见 §5.2 |
|
|
662
|
+
| CSV | `downloadCsv` / `safeCsvCell` reexport | |
|
|
663
|
+
| 系统配置 | `ConfigField` + 产品 `system-config-sections` | 字段文案在产品 i18n |
|
|
664
|
+
| **offset 查询** | **多为 number body**:`limit`/`offset` 数字手写在 `AdminBalance` / `AdminRecharges` / `AdminVouchers` 等 filter 类型 | **不要**强行换成 `buildAdminOffsetParams`(它产出 **string**,给 URLSearchParams) |
|
|
665
|
+
| **string limit/offset** | shop 几乎无 1-based 页码 → string query 列表 | **对照面在 cf-lottery**:平台审计 / 租户 / webhook / 兑换码已用 `buildAdminOffsetParams` |
|
|
666
|
+
| **批量上限** | 服务端 batch:`Orders` 批量删、`Cards` 状态/删、`EmailLogs` 删、`Logs` 删 → `checkAdminBatchLimit(..., { limit: 200 })` | 后端 `z.array(...).max(200)`;**禁止**默认 20 误伤产品 |
|
|
667
|
+
| 批量 UX | 逐条 `runSequential` + `resolveBatchToastType`;服务端汇总路径直接 toast | Coupons/Products 等逐条路径可按需再加 limit(产品策略) |
|
|
668
|
+
| 不进 kit | 订单/支付/多币种、领域 CRUD、storefront 链接工具 | 见 §5.3 |
|
|
669
|
+
|
|
670
|
+
**升级 checklist(shop):**
|
|
671
|
+
|
|
672
|
+
1. 升 `@usethink/cf-admin-fe` 后跑产品 `vue-tsc` + 相关 contract 测试。
|
|
673
|
+
2. 新增 **URLSearchParams + limit/offset 字符串** 列表 → 用 `buildAdminOffsetParams(page, limit)` 或 `applyAdminOffsetParams`。
|
|
674
|
+
3. 新增 **number** filter offset 列表 → 继续手写 `(page-1)*limit` 数字;若 ≥2 产品同形再考虑 kit number helper。
|
|
675
|
+
4. 新增服务端 `max(N)` 批量 API → `checkAdminBatchLimit(ids, { limit: N, unit })`,N 跟后端一致。
|
|
676
|
+
5. 不要为了「对齐文档示例」把 number body 改成 string query。
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
680
|
+
## 6. 新产品 Checklist(可打印)
|
|
681
|
+
|
|
682
|
+
- [ ] 安装包 + peer Vue
|
|
683
|
+
- [ ] `import '@usethink/cf-admin-fe/styles'`(或 primitives)
|
|
684
|
+
- [ ] 根节点 `<ToastContainer />` 仅一次
|
|
685
|
+
- [ ] `mergeAdminKitMessages` 中英(或实际 locale)
|
|
686
|
+
- [ ] 选定 Bearer **或** Cookie,复制对应 `useAdminAuth` recipe
|
|
687
|
+
- [ ] `createAdminRequest`:`resultMode` / `errorField` / 401 /(cookie)`credentials`+`onSuccess`
|
|
688
|
+
- [ ] 路由:`decideAdminRouteAccess` + 登录页 `resolveAdminLoginRedirect`
|
|
689
|
+
- [ ] `AdminShell` 布局 + 产品菜单
|
|
690
|
+
- [ ] 登录 UI 包在 `AdminLoginShell`
|
|
691
|
+
- [ ] 第一页列表:`useAdminListLoader` + `useTablePagination` + **按契约选分页**:string query → `buildAdminOffsetParams`;number body → 手写数字 offset(见 §5.8)
|
|
692
|
+
- [ ] 多选页:`useTableSelection` + `checkAdminBatchLimit`(**`limit` 对齐后端 max**,默认 20 仅 auth 风格)+ batch toast
|
|
693
|
+
- [ ] 复制:`copyWithToast`;特殊降级才 `writeClipboardText`
|
|
694
|
+
- [ ] 错误:`toErrorMessage`
|
|
695
|
+
- [ ] 导出:`downloadCsv`
|
|
696
|
+
- [ ] (可选)系统配置:`ConfigField` + system-config-engine
|
|
697
|
+
- [ ] 产品 `vue-tsc` + 关键路径手工点一遍 401 / 登录 redirect
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
## 7. 目录地图(本仓库)
|
|
702
|
+
|
|
703
|
+
```
|
|
704
|
+
cf-admin-fe/
|
|
705
|
+
src/
|
|
706
|
+
composables/ # 会话、请求、表格、toast、clipboard、list loader…
|
|
707
|
+
components/ # Shell / Modal / Pagination / ConfigField / …
|
|
708
|
+
utils/ # 纯函数:错误、批量、导航、query、csv、cents…
|
|
709
|
+
i18n/ # kit 消息种子 + merge
|
|
710
|
+
types/ # ConfigField 等类型
|
|
711
|
+
styles/ # tokens + primitives
|
|
712
|
+
recipes/ # 复制即用模板(非运行时依赖)
|
|
713
|
+
docs/ # 本说明书 + 差距分析
|
|
714
|
+
tests/ # 公共 API 与行为契约
|
|
715
|
+
scripts/clean-dist.mjs
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
---
|
|
719
|
+
|
|
720
|
+
## 8. 常见误区速查
|
|
721
|
+
|
|
722
|
+
| 误区 | 正确做法 |
|
|
723
|
+
|------|----------|
|
|
724
|
+
| 把业务页面放进套件「图省事」 | 业务永在产品;套件只 foundation |
|
|
725
|
+
| Cookie 产品硬套 Bearer TTL | 用 `createAdminFlagSession` |
|
|
726
|
+
| `buildAdminOffsetParams({ page, pageSize })` | 位置参数 `(page, limit)` |
|
|
727
|
+
| shop number offset 强行套 string helper | number body 手写;string 仅 URLSearchParams(见 §5.8) |
|
|
728
|
+
| 批量 `checkAdminBatchLimit` 不传 `limit` 却对接 max(200) API | 显式 `{ limit: 200 }`,默认 20 是 auth 风格 |
|
|
729
|
+
| 列表不写 `isStale` | 快翻页会把旧响应写进新页 |
|
|
730
|
+
| 登录探测 token 触发 401 清会话 | 该次请求 `redirectOnUnauthorized: false` |
|
|
731
|
+
| `copyText` + 无条件 success toast | 改 `copyWithToast` |
|
|
732
|
+
| 用套件 cents 函数做多币种账本 | 产品 money 库 |
|
|
733
|
+
| 根 barrel 导入导致 vitest 挂 SFC | 子路径 `/composables` `/utils` |
|
|
734
|
+
| 提交过期 dist | `clean` + `prepack` 生成 |
|
|
735
|
+
| 为第三种日期库包一层进套件 | 产品自选轻量库 |
|
|
736
|
+
|
|
737
|
+
---
|
|
738
|
+
|
|
739
|
+
## 9. 能力边界声明(避免幻觉)
|
|
740
|
+
|
|
741
|
+
本套件 **当前没有**、也不计划在未经验证前加入:
|
|
742
|
+
|
|
743
|
+
- 通用 DataTable / 虚拟滚动表格框架
|
|
744
|
+
- 日期时间选择器、级联、树、上传套件
|
|
745
|
+
- OAuth/PKCE 完整流程组件
|
|
746
|
+
- 权限指令 / RBAC 组件
|
|
747
|
+
- 图表、富文本、表单 schema 引擎
|
|
748
|
+
- 与后端共享的 RPC/SDK
|
|
749
|
+
- **number** 版 `buildAdminOffsetParams`(观察期,见 001 **R5** / §5.8 升级清单第 3 条)
|
|
750
|
+
- 强迫旧审计页迁 `AdminMetadataDetail`(组件留给**新页**)
|
|
751
|
+
|
|
752
|
+
若新产品需要上述能力:在产品内选型;当 **≥2 个 CF 管理端** 出现可复用的同一薄封装时,再提案进入 `cf-admin-fe`。
|
|
753
|
+
|
|
754
|
+
---
|
|
755
|
+
|
|
756
|
+
## 10. 封版与部署注意(0.2.6)
|
|
757
|
+
|
|
758
|
+
### 10.1 本包如何被产品消费
|
|
759
|
+
|
|
760
|
+
| 方式 | 说明 |
|
|
761
|
+
|------|------|
|
|
762
|
+
| monorepo `file:../cf-admin-fe` | **当前 shop / lottery / auth 默认**;部署机需能读到同级源码目录,或先 `npm pack` 再装 tgz |
|
|
763
|
+
| registry / tgz | `npm pack` → `prepack` 会 `clean`+`build` 生成 `dist/*.d.ts`;运行时仍以 **`src/`** 为准 |
|
|
764
|
+
|
|
765
|
+
**不要**只拷 `dist/` 上线;Vue SFC 与 TS 源在 `src/`。
|
|
766
|
+
|
|
767
|
+
### 10.2 封版前自检(kit)
|
|
768
|
+
|
|
769
|
+
```bash
|
|
770
|
+
npm test # 预期全绿(约 122)
|
|
771
|
+
npm run type-check
|
|
772
|
+
npm run build # 生成 dist 声明;可选 npm pack 干跑
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
### 10.3 三端接线冻结摘要(部署对照)
|
|
776
|
+
|
|
777
|
+
| 产品 | 会话 | 本版关键接线 | 部署前至少 |
|
|
778
|
+
|------|------|--------------|------------|
|
|
779
|
+
| **cf-shop** | Bearer | 17 列表 loader;服务端 batch `checkAdminBatchLimit({ limit: 200 })`;number offset **手写** | `type-check:frontend` + `frontend:build` |
|
|
780
|
+
| **cf-lottery** | Bearer | 平台 3 页 + 兑换码 `buildAdminOffsetParams` | `type-check:frontend` + `frontend:build` |
|
|
781
|
+
| **cf-auth** | Cookie flag | `checkAdminBatchLimit` 默认 20 / 硬删显式 limit;**未改**默认 20 | `frontend` type-check + `build:frontend` + 根 `verify` |
|
|
782
|
+
|
|
783
|
+
升级套件后:**先**在 monorepo 跑通上表,再推生产。shop 批量 max 与 auth 默认 20 **不要**互相「统一」成同一个魔法数。
|
|
784
|
+
|
|
785
|
+
### 10.4 明确不进本封版
|
|
786
|
+
|
|
787
|
+
R5 观察项(number offset helper、silent copy 选项、cursor helper、batch profile 工厂)、旧页迁 `AdminMetadataDetail`、领域 CRUD、C 端 storefront。
|
|
788
|
+
|
|
789
|
+
---
|
|
790
|
+
|
|
791
|
+
## 11. 修订记录
|
|
792
|
+
|
|
793
|
+
| 日期 | 说明 |
|
|
794
|
+
|------|------|
|
|
795
|
+
| 2026-08-05 | 初版:对齐 0.2.6 全量公开 API、双会话、列表/复制/批量/封包注意事项;配套修正 `list-page.snippet` 签名与 admin 复制双反馈 |
|
|
796
|
+
| 2026-08-05 | §5.8 **cf-shop 接线对照**;lottery 四页 string offset 接 `buildAdminOffsetParams`;shop 四类服务端 batch 接 `checkAdminBatchLimit({ limit: 200 })` |
|
|
797
|
+
| 2026-08-05 | **P1–P3 文档收口(无 API)**:README/recipes/001/002 对称 string vs number offset、batch limit 跟后端 max、MetadataDetail 新页优先、R5 观察期 |
|
|
798
|
+
| 2026-08-05 | **§10 封版与部署注意**;三端验证全绿后的冻结摘要 |
|
|
799
|
+
| 2026-08-05 | **AdminShell 用户区上移顶栏右上**(省侧栏高度;`#sidebar-footer` 可选;窄屏可藏用户名) |
|
|
800
|
+
|
|
801
|
+
**维护约定:** API 变更时同步改本文件、根 README 公开 API 节、`recipes/README.md` 与 `tests/public-api.test.ts`。说明书只描述已实现行为;示例代码须与 `src/` 签名一致。
|