@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
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usethink/cf-admin-fe",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Vue 3 frontend admin kit for CF product family
|
|
3
|
+
"version": "0.2.6",
|
|
4
|
+
"description": "Vue 3 frontend admin kit for CF product family — composables, shell, ConfigField, list loader, login chrome, request/token/raw factories, error/batch helpers, system-config engine, nav helpers, i18n seeds, recipes (FE only; not cf-core)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
7
7
|
"files": [
|
|
8
8
|
"src",
|
|
9
9
|
"dist",
|
|
10
|
-
"
|
|
10
|
+
"recipes",
|
|
11
|
+
"docs",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
11
14
|
],
|
|
12
15
|
"main": "./src/index.ts",
|
|
13
16
|
"module": "./src/index.ts",
|
|
@@ -26,14 +29,32 @@
|
|
|
26
29
|
"types": "./src/components/index.ts",
|
|
27
30
|
"import": "./src/components/index.ts"
|
|
28
31
|
},
|
|
32
|
+
"./utils": {
|
|
33
|
+
"types": "./src/utils/index.ts",
|
|
34
|
+
"import": "./src/utils/index.ts"
|
|
35
|
+
},
|
|
36
|
+
"./types": {
|
|
37
|
+
"types": "./src/types/index.ts",
|
|
38
|
+
"import": "./src/types/index.ts"
|
|
39
|
+
},
|
|
40
|
+
"./i18n": {
|
|
41
|
+
"types": "./src/i18n/index.ts",
|
|
42
|
+
"import": "./src/i18n/index.ts"
|
|
43
|
+
},
|
|
29
44
|
"./styles": "./src/styles/index.css",
|
|
30
45
|
"./styles/tokens.css": "./src/styles/tokens.css",
|
|
31
46
|
"./styles/admin-primitives.css": "./src/styles/admin-primitives.css",
|
|
47
|
+
"./styles/login-primitives.css": "./src/styles/login-primitives.css",
|
|
32
48
|
"./package.json": "./package.json",
|
|
33
49
|
"./components/AdminPagination.vue": "./src/components/AdminPagination.vue",
|
|
34
50
|
"./components/AdminModal.vue": "./src/components/AdminModal.vue",
|
|
35
51
|
"./components/ConfirmDialog.vue": "./src/components/ConfirmDialog.vue",
|
|
36
|
-
"./components/ToastContainer.vue": "./src/components/ToastContainer.vue"
|
|
52
|
+
"./components/ToastContainer.vue": "./src/components/ToastContainer.vue",
|
|
53
|
+
"./components/ConfigField.vue": "./src/components/ConfigField.vue",
|
|
54
|
+
"./components/AdminShell.vue": "./src/components/AdminShell.vue",
|
|
55
|
+
"./components/AdminLoginShell.vue": "./src/components/AdminLoginShell.vue",
|
|
56
|
+
"./recipes/*": "./recipes/*",
|
|
57
|
+
"./components/AdminMetadataDetail.vue": "./src/components/AdminMetadataDetail.vue"
|
|
37
58
|
},
|
|
38
59
|
"sideEffects": [
|
|
39
60
|
"**/*.css"
|
|
@@ -47,6 +68,7 @@
|
|
|
47
68
|
}
|
|
48
69
|
},
|
|
49
70
|
"devDependencies": {
|
|
71
|
+
"@types/node": "^22.10.0",
|
|
50
72
|
"@vitejs/plugin-vue": "^5.2.1",
|
|
51
73
|
"typescript": "^5.7.2",
|
|
52
74
|
"vite": "^6.0.0",
|
|
@@ -55,10 +77,12 @@
|
|
|
55
77
|
"vue-tsc": "^2.1.10"
|
|
56
78
|
},
|
|
57
79
|
"scripts": {
|
|
58
|
-
"type-check": "tsc --noEmit && vue-tsc --noEmit -p tsconfig.json",
|
|
80
|
+
"type-check": "tsc --noEmit -p tsconfig.build.json && vue-tsc --noEmit -p tsconfig.json",
|
|
59
81
|
"test": "vitest run",
|
|
60
82
|
"test:watch": "vitest",
|
|
61
|
-
"build": "tsc -p tsconfig.build.json"
|
|
83
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
84
|
+
"prepack": "npm run build",
|
|
85
|
+
"clean": "node scripts/clean-dist.mjs"
|
|
62
86
|
},
|
|
63
87
|
"keywords": [
|
|
64
88
|
"admin",
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!--
|
|
3
|
+
RECIPE — 产品 AdminLayout 外壳。
|
|
4
|
+
将 menuItems + brand 替换为产品的 i18n / 配置。
|
|
5
|
+
可选插槽:#nav-extra(平台链接)、#banner(仿冒登录)、#header-actions(语言,在顶栏用户信息左侧)。
|
|
6
|
+
用户头像/名称/退出默认在顶栏右上;侧栏底默认不占位(菜单多时省高)。
|
|
7
|
+
若需自定义侧栏底:#sidebar-footer(有内容才渲染)。
|
|
8
|
+
-->
|
|
9
|
+
<AdminShell
|
|
10
|
+
:brand="brand"
|
|
11
|
+
:menu-items="shellMenuItems"
|
|
12
|
+
:current-name="currentName ?? null"
|
|
13
|
+
:page-title="pageTitle"
|
|
14
|
+
:user-name="t('adminLayout.admin')"
|
|
15
|
+
@logout="handleLogout"
|
|
16
|
+
@navigate="onNavClick"
|
|
17
|
+
>
|
|
18
|
+
<template #header-actions>
|
|
19
|
+
<!-- 产品:语言选择、环境徽标等(出现在用户信息与退出左侧) -->
|
|
20
|
+
<slot name="header-actions" />
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<RouterView v-slot="{ Component }">
|
|
24
|
+
<transition name="admin-fade" mode="out-in">
|
|
25
|
+
<component :is="Component" />
|
|
26
|
+
</transition>
|
|
27
|
+
</RouterView>
|
|
28
|
+
</AdminShell>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<script setup lang="ts">
|
|
32
|
+
import { computed } from 'vue'
|
|
33
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
34
|
+
import { useI18n } from 'vue-i18n'
|
|
35
|
+
import { AdminShell, type AdminShellMenuItem } from '@usethink/cf-admin-fe/components'
|
|
36
|
+
import { useAdminAuth } from '@/composables/useAdminAuth'
|
|
37
|
+
import { useToast } from '@/composables/useToast'
|
|
38
|
+
|
|
39
|
+
const route = useRoute()
|
|
40
|
+
const router = useRouter()
|
|
41
|
+
const { t } = useI18n()
|
|
42
|
+
const { clearToken } = useAdminAuth()
|
|
43
|
+
const { showToast } = useToast()
|
|
44
|
+
|
|
45
|
+
/** PRODUCT: 品牌标题 */
|
|
46
|
+
const brand = computed(() => t('adminLayout.title'))
|
|
47
|
+
|
|
48
|
+
/** PRODUCT: 替换为真实菜单 */
|
|
49
|
+
const menuItems = computed(() => [
|
|
50
|
+
{ name: 'AdminDashboard', to: '/admin', label: t('adminLayout.title'), icon: '📊' },
|
|
51
|
+
// { name: 'AdminOrders', to: '/admin/orders', label: '…', icon: '📋' },
|
|
52
|
+
])
|
|
53
|
+
|
|
54
|
+
const shellMenuItems = computed<AdminShellMenuItem[]>(() => menuItems.value)
|
|
55
|
+
|
|
56
|
+
const currentName = computed(() => route.name as string | undefined)
|
|
57
|
+
|
|
58
|
+
const pageTitle = computed(() => {
|
|
59
|
+
const item = menuItems.value.find((m) => m.name === currentName.value)
|
|
60
|
+
return item?.label || t('adminLayout.title')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
function onNavClick(to: string) {
|
|
64
|
+
if (route.fullPath === to) return
|
|
65
|
+
router.push(to)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handleLogout() {
|
|
69
|
+
clearToken()
|
|
70
|
+
showToast(t('adminLayout.loggedOut'), 'success')
|
|
71
|
+
router.replace('/admin/login')
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Recipes — 复制到新的 CF 管理后台前端
|
|
2
|
+
|
|
3
|
+
这些文件是**起点模板**,不是运行时导入。将它们复制(或重新导出)到你的产品 `src/` 目录下,然后补齐产品专属部分(菜单、领域 API 方法、登录 UI)。
|
|
4
|
+
|
|
5
|
+
**完整用法、注意事项、能力榨干清单:** 见 [`docs/002_cf-admin-fe使用说明书与注意事项.md`](../docs/002_cf-admin-fe使用说明书与注意事项.md)。
|
|
6
|
+
|
|
7
|
+
## 推荐复制顺序
|
|
8
|
+
|
|
9
|
+
| 步骤 | Recipe | 产品路径(示例) |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| 1 | `reexports/` | `src/composables/*`、`src/components/*`、`src/lib/*` |
|
|
12
|
+
| 2a | **Bearer:** `useAdminAuth.shop.ts` | `src/composables/useAdminAuth.ts` |
|
|
13
|
+
| 2b | **Cookie:** `useAdminAuth.cookie.ts` | 同上 — 使用 `createAdminFlagSession` |
|
|
14
|
+
| 3a | **Bearer:** `useAdminRequest.ts` | `src/composables/useAdminRequest.ts`(或 `api/admin.ts`) |
|
|
15
|
+
| 3b | **Cookie:** `useAdminRequest.cookie.ts` | `credentials: 'include'` + `onSuccess` 标志修复 |
|
|
16
|
+
| 4 | `AdminLayout.vue` | `src/views/AdminLayout.vue` |
|
|
17
|
+
| 5 | `router-guard.snippet.ts` | 粘贴到 `src/router/index.ts`(Bearer 用 `hasToken`;cookie 产品可在**产品内**添加健康探测) |
|
|
18
|
+
| 6 | `i18n-merge.snippet.ts` | 粘贴到 `src/i18n/index.ts` |
|
|
19
|
+
| 7 | 样式 | `import '@usethink/cf-admin-fe/styles'` **或** `@import` 基础样式(+ 登录 CSS) |
|
|
20
|
+
| 8 | 应用外壳 | 在应用根节点挂载一次 `ToastContainer` |
|
|
21
|
+
| 9 | 登录 | 用 `AdminLoginShell` 包裹**产品**表单;用 `resolveAdminLoginRedirect` 处理 `?redirect=` |
|
|
22
|
+
| 10 | 列表页 | 见 `list-page.snippet.ts`:`useAdminListLoader` + 分页/多选 + `toErrorMessage` / `copyWithToast`;**先确认**后端是 string query 还是 number body offset |
|
|
23
|
+
|
|
24
|
+
### 两种一等会话模型
|
|
25
|
+
|
|
26
|
+
| 模型 | 工具包工厂 | 请求 | 实际参考 |
|
|
27
|
+
|---|---|---|---|
|
|
28
|
+
| Bearer JWT | `createAdminTokenSession` | `getToken` + 可选 Bearer | **cf-shop**、**cf-lottery** |
|
|
29
|
+
| Cookie + 本地标志 | `createAdminFlagSession` | `credentials: 'include'`,可选 `onSuccess` 修复 | **cf-auth** |
|
|
30
|
+
|
|
31
|
+
Cookie 产品**不应**被强制套用 token TTL / 密码表单。它们的特性(OAuth、健康探测、登出 cookie)留在产品侧;工具包只提供标志存储 + 请求工厂 + 外壳 UI。
|
|
32
|
+
|
|
33
|
+
## 不要放入工具包 / 不要按领域复制
|
|
34
|
+
|
|
35
|
+
- 领域页面(`views/admin/*` 业务 CRUD)
|
|
36
|
+
- 领域 API 方法目录
|
|
37
|
+
- 抽奖仿冒登录 / 平台角色(产品 L3)
|
|
38
|
+
- Better Auth / OAuth / PKCE **流程**和允许列表策略(产品)
|
|
39
|
+
- 多币种 / 订单状态映射
|
|
40
|
+
- 通用组件(日期选择器、级联选择、树、富文本)— 产品自选轻量库
|
|
41
|
+
- 一次性产品辅助函数“以防将来共享”
|
|
42
|
+
|
|
43
|
+
## 导入风格
|
|
44
|
+
|
|
45
|
+
产品重新导出时优先使用**子路径**导入,这样 Vitest/node 不会解析根 SFC barrel 文件:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
from '@usethink/cf-admin-fe/composables'
|
|
49
|
+
from '@usethink/cf-admin-fe/utils'
|
|
50
|
+
from '@usethink/cf-admin-fe/components/ConfigField.vue'
|
|
51
|
+
from '@usethink/cf-admin-fe/i18n'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 错误 / 批量纯辅助函数(0.2.6+)
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import {
|
|
58
|
+
toErrorMessage,
|
|
59
|
+
buildBatchToast,
|
|
60
|
+
resolveBatchToastType,
|
|
61
|
+
checkAdminBatchLimit,
|
|
62
|
+
} from '@usethink/cf-admin-fe/utils'
|
|
63
|
+
|
|
64
|
+
// catch
|
|
65
|
+
showToast(toErrorMessage(e), 'error')
|
|
66
|
+
// 列表 loader
|
|
67
|
+
onError: (err) => toErrorMessage(err, t('…loadFailed'))
|
|
68
|
+
|
|
69
|
+
// 服务端批量响应 { success, failed }
|
|
70
|
+
const toast = buildBatchToast(res, {
|
|
71
|
+
mixed: '完成:成功 {success},失败 {failed}',
|
|
72
|
+
success: '已处理 {count} 条',
|
|
73
|
+
})
|
|
74
|
+
showToast(toast.message, toast.type)
|
|
75
|
+
|
|
76
|
+
// 或保留 vue-i18n 文案,只复用成功/失败色调:
|
|
77
|
+
showToast(t('…', counts), resolveBatchToastType(counts))
|
|
78
|
+
|
|
79
|
+
// POST 前多选上限
|
|
80
|
+
// 默认 20 = auth / docs 风格;后端若 z.array.max(200) 必须传 limit: 200
|
|
81
|
+
const limitCheck = checkAdminBatchLimit(ids, { unit: '人' })
|
|
82
|
+
// shop 示例:checkAdminBatchLimit(ids, { limit: 200, unit: '单' })
|
|
83
|
+
if (!limitCheck.ok) {
|
|
84
|
+
showToast(limitCheck.message, 'error')
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
不要把业务成功文案或硬删除策略用语放进套件——只放纯形状。
|
|
90
|
+
**`limit` 必须与后端单次批量 max 一致**,默认 20 不是「所有 CF 产品的真理」。
|
|
91
|
+
|
|
92
|
+
## 列表 limit / offset(string vs number)
|
|
93
|
+
|
|
94
|
+
| 形态 | 工具 | 参考产品 |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `URLSearchParams` / query:`limit`、`offset` 为 **字符串** | `buildAdminOffsetParams(page, limit)` 或 `applyAdminOffsetParams` | cf-lottery 平台审计、租户、webhook、兑换码 |
|
|
97
|
+
| 类型化 filter / body:`limit`、`offset` 为 **数字** | **手写** `(page - 1) * limit`;**不要**套 string helper | cf-shop 余额 / 充值 / 代金券等 |
|
|
98
|
+
| Cursor | 产品自管 | shop 部分订单/日志 |
|
|
99
|
+
|
|
100
|
+
`buildAdminOffsetParams` 的返回值是 `{ limit: string; offset: string }`,塞进需要 `number` 的 TypeScript filter 会类型错误或静默把数字变成字符串——属于误用,不是「再抽一个兼容层就能糊弄过去」。
|
|
101
|
+
|
|
102
|
+
完整对照与升级清单:`docs/002` **§5.8**。
|
|
103
|
+
|
|
104
|
+
## 复制 + toast(0.2.6+)
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { copyWithToast } from '@usethink/cf-admin-fe/composables'
|
|
108
|
+
// 或产品 reexport:@/composables/useClipboard
|
|
109
|
+
|
|
110
|
+
const ok = await copyWithToast(text, {
|
|
111
|
+
successMessage: t('…copySuccess'),
|
|
112
|
+
errorMessage: t('…copyFailed'),
|
|
113
|
+
})
|
|
114
|
+
if (ok) {
|
|
115
|
+
// 可选:关弹层、清选中
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- **`copyWithToast`**:剪贴板 + 全局 toast(管理端列表/详情主路径)
|
|
120
|
+
- **`copyText(text, event)`**:按钮内 ✅/❌(前台交付明细等,不抢 toast)
|
|
121
|
+
- **`writeClipboardText`**:仅写入,由调用方自管反馈(例如创建成功后静默复制链接)
|
|
122
|
+
|
|
123
|
+
## 列表页骨架
|
|
124
|
+
|
|
125
|
+
见 [`list-page.snippet.ts`](./list-page.snippet.ts)(含形态 A/B 注释与 batch `limit` 提示)。
|
|
126
|
+
|
|
127
|
+
## 审计元数据
|
|
128
|
+
|
|
129
|
+
- 纯函数:`formatMetadata`(旧页、自研详情 markup 足够时只用这个)
|
|
130
|
+
- 组件:`AdminMetadataDetail` — **新审计详情页优先**;已有产品详情 DOM 的旧页 **不必** 为对齐而全量迁移(见 docs/001 R4 尾巴、docs/002 步骤 11)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RECIPE — 合并工具包消息种子,使 ConfigField / Pagination / Confirm 在新产品
|
|
3
|
+
* 上不会显示原始 key。冲突时产品宿主 key 始终优先。
|
|
4
|
+
*
|
|
5
|
+
* 示例(vue-i18n):
|
|
6
|
+
*
|
|
7
|
+
* import { mergeAdminKitMessages } from '@usethink/cf-admin-fe/i18n'
|
|
8
|
+
* import zhHost from './zh-CN'
|
|
9
|
+
* import enHost from './en'
|
|
10
|
+
*
|
|
11
|
+
* const i18n = createI18n({
|
|
12
|
+
* locale: 'zh-CN',
|
|
13
|
+
* messages: {
|
|
14
|
+
* 'zh-CN': mergeAdminKitMessages(zhHost, 'zh-CN'),
|
|
15
|
+
* en: mergeAdminKitMessages(enHost, 'en'),
|
|
16
|
+
* },
|
|
17
|
+
* })
|
|
18
|
+
*/
|
|
19
|
+
import { mergeAdminKitMessages } from '@usethink/cf-admin-fe/i18n'
|
|
20
|
+
|
|
21
|
+
export function withAdminKitI18n<T extends Record<string, unknown>>(
|
|
22
|
+
zhHost: T,
|
|
23
|
+
enHost: T,
|
|
24
|
+
): { 'zh-CN': T; en: T } {
|
|
25
|
+
return {
|
|
26
|
+
'zh-CN': mergeAdminKitMessages(zhHost, 'zh-CN'),
|
|
27
|
+
en: mergeAdminKitMessages(enHost, 'en'),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 管理端列表页标准骨架(recipe / 文档,非运行时模块)。
|
|
3
|
+
*
|
|
4
|
+
* 组合:
|
|
5
|
+
* - useAdminListLoader — loading / loadError / 竞态安全序号
|
|
6
|
+
* - useTablePagination — page / limit / total / offset(分页 state 的 offset 是 number)
|
|
7
|
+
* - useTableSelection — 多选(可选)
|
|
8
|
+
* - buildAdminOffsetParams / applyAdminOffsetParams — **仅 string** limit+offset(URLSearchParams)
|
|
9
|
+
* - toErrorMessage — catch / onError 统一文案
|
|
10
|
+
* - copyWithToast — 复制选中行 / ID / 链接(可选)
|
|
11
|
+
* - checkAdminBatchLimit + buildBatchToast / resolveBatchToastType — 批量(可选)
|
|
12
|
+
*
|
|
13
|
+
* 不要把领域列定义、筛选表单、业务 API 放进套件。
|
|
14
|
+
*
|
|
15
|
+
* ── 分页查询:两种契约,勿混用 ──
|
|
16
|
+
*
|
|
17
|
+
* A) Query string / URLSearchParams(本 snippet 主示例)
|
|
18
|
+
* buildAdminOffsetParams(page, limit) → { limit: string, offset: string }
|
|
19
|
+
* 参考:cf-lottery 平台审计 / 租户 / webhook / 兑换码
|
|
20
|
+
*
|
|
21
|
+
* B) 类型化 filter / JSON body:limit、offset 为 number
|
|
22
|
+
* 手写即可,例如:
|
|
23
|
+
* { limit: limit.value, offset: (page.value - 1) * limit.value }
|
|
24
|
+
* 参考:cf-shop AdminBalance / Recharges / Vouchers
|
|
25
|
+
* 禁止把 B 强行改成 A 只为「用上 helper」。number helper 观察期:≥2 产品同形再提案。
|
|
26
|
+
*
|
|
27
|
+
* 真实 API 签名(务必与 src/utils/admin-query.ts 一致):
|
|
28
|
+
* buildAdminOffsetParams(page: number, limit: number, options?)
|
|
29
|
+
* applyAdminOffsetParams(params: URLSearchParams, page: number, limit: number, options?)
|
|
30
|
+
*
|
|
31
|
+
* 批量上限:DEFAULT_ADMIN_BATCH_LIMIT = 20(auth 风格)。
|
|
32
|
+
* 后端若为 max(200),必须 checkAdminBatchLimit(ids, { limit: 200, unit: '…' })。
|
|
33
|
+
* 详见 docs/002 §5.8(cf-shop 接线对照)。
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
37
|
+
import { ref, watch } from 'vue'
|
|
38
|
+
import {
|
|
39
|
+
useAdminListLoader,
|
|
40
|
+
useTablePagination,
|
|
41
|
+
useTableSelection,
|
|
42
|
+
copyWithToast,
|
|
43
|
+
} from '@usethink/cf-admin-fe/composables'
|
|
44
|
+
import {
|
|
45
|
+
toErrorMessage,
|
|
46
|
+
buildAdminOffsetParams,
|
|
47
|
+
applyAdminOffsetParams,
|
|
48
|
+
checkAdminBatchLimit,
|
|
49
|
+
buildBatchToast,
|
|
50
|
+
} from '@usethink/cf-admin-fe/utils'
|
|
51
|
+
|
|
52
|
+
// —— 伪代码:复制到产品 views/admin/XxxView.vue 后接领域 API ——
|
|
53
|
+
|
|
54
|
+
type Row = { id: string /* … */ }
|
|
55
|
+
|
|
56
|
+
const items = ref<Row[]>([])
|
|
57
|
+
const { page, limit, total, offset, setTotal } = useTablePagination()
|
|
58
|
+
const {
|
|
59
|
+
selectedIds,
|
|
60
|
+
clearSelection,
|
|
61
|
+
selectedCount,
|
|
62
|
+
} = useTableSelection(items, (row) => row.id)
|
|
63
|
+
const { loading, loadError, runLoad } = useAdminListLoader()
|
|
64
|
+
|
|
65
|
+
// 示例:产品侧 admin 请求(Bearer 或 cookie 由 useAdminRequest recipe 决定)
|
|
66
|
+
declare function listDomainItems(params: Record<string, string>): Promise<{ items: Row[]; total: number }>
|
|
67
|
+
declare function batchDisable(ids: string[]): Promise<{ success: number; failed: number }>
|
|
68
|
+
declare function showToast(message: string, type?: 'success' | 'error' | 'info'): void
|
|
69
|
+
declare function t(key: string, params?: Record<string, unknown>): string
|
|
70
|
+
|
|
71
|
+
async function loadData() {
|
|
72
|
+
await runLoad(
|
|
73
|
+
async (ctx) => {
|
|
74
|
+
// ── 形态 A:string query(本示例)──
|
|
75
|
+
// 位置参数:page(1-based)、limit(每页条数)→ 字符串 limit/offset
|
|
76
|
+
const params = buildAdminOffsetParams(page.value, limit.value)
|
|
77
|
+
// 若已有 URLSearchParams(含业务筛选),再写入 limit/offset:
|
|
78
|
+
// const qs = new URLSearchParams({ status: filter.status })
|
|
79
|
+
// applyAdminOffsetParams(qs, page.value, limit.value)
|
|
80
|
+
const res = await listDomainItems(params)
|
|
81
|
+
|
|
82
|
+
// ── 形态 B:number body / 类型化 filter(勿用 buildAdminOffsetParams)──
|
|
83
|
+
// const res = await listDomainItems({
|
|
84
|
+
// status: filter.status,
|
|
85
|
+
// limit: limit.value,
|
|
86
|
+
// offset: (page.value - 1) * limit.value,
|
|
87
|
+
// })
|
|
88
|
+
|
|
89
|
+
if (ctx.isStale()) return
|
|
90
|
+
items.value = res.items
|
|
91
|
+
setTotal(res.total)
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
onError: (err) => toErrorMessage(err, t('adminXxx.loadFailed')),
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
watch([page, limit], () => {
|
|
100
|
+
void loadData()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
/** 复制选中 ID / 业务字段:成功与失败文案走产品 i18n */
|
|
104
|
+
async function copySelected() {
|
|
105
|
+
if (selectedCount.value === 0) return
|
|
106
|
+
const idSet = new Set(selectedIds.value)
|
|
107
|
+
const rows = items.value.filter((r) => idSet.has(r.id))
|
|
108
|
+
await copyWithToast(rows.map((r) => r.id).join('\n'), {
|
|
109
|
+
successMessage: t('adminXxx.copySelectedSuccess', { count: rows.length }),
|
|
110
|
+
errorMessage: t('adminXxx.copySelectedFailed'),
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function runBatchDisable() {
|
|
115
|
+
const ids = [...selectedIds.value]
|
|
116
|
+
// 默认 limit=20(auth)。shop 类后端 max(200) 时改为:
|
|
117
|
+
// checkAdminBatchLimit(ids, { limit: 200, unit: t('adminXxx.unit') })
|
|
118
|
+
const limitCheck = checkAdminBatchLimit(ids, { unit: t('adminXxx.unit') })
|
|
119
|
+
if (!limitCheck.ok) {
|
|
120
|
+
showToast(limitCheck.message, 'error')
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const res = await batchDisable(ids)
|
|
125
|
+
const toast = buildBatchToast(res, {
|
|
126
|
+
mixed: t('adminXxx.batchMixed'), // 模板可含 {success}/{failed}/{count}
|
|
127
|
+
success: t('adminXxx.batchSuccess'),
|
|
128
|
+
})
|
|
129
|
+
showToast(toast.message, toast.type)
|
|
130
|
+
clearSelection()
|
|
131
|
+
await loadData()
|
|
132
|
+
} catch (e) {
|
|
133
|
+
showToast(toErrorMessage(e), 'error')
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
void offset
|
|
138
|
+
void applyAdminOffsetParams
|
|
139
|
+
void total
|
|
140
|
+
void loadError
|
|
141
|
+
void loading
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RECIPE — 样式入口。
|
|
3
|
+
*
|
|
4
|
+
* 方案 A(完整工具包 token + 基础样式):
|
|
5
|
+
* import '@usethink/cf-admin-fe/styles'
|
|
6
|
+
*
|
|
7
|
+
* 方案 B(产品已有与商城共享的 base.css token):
|
|
8
|
+
* // assets/admin.css
|
|
9
|
+
* @import '@usethink/cf-admin-fe/styles/admin-primitives.css';
|
|
10
|
+
*
|
|
11
|
+
* 在应用根节点挂载一次:
|
|
12
|
+
* <ToastContainer />
|
|
13
|
+
*/
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 轻量组件重新导出 — 产品中每个组件一个文件:
|
|
3
|
+
*
|
|
4
|
+
* src/components/AdminPagination.ts
|
|
5
|
+
* export { default } from '@usethink/cf-admin-fe/components/AdminPagination.vue'
|
|
6
|
+
*
|
|
7
|
+
* 使用深 `.vue` 路径可避免把整个包根目录拉进 Vitest。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { default as AdminPagination } from '@usethink/cf-admin-fe/components/AdminPagination.vue'
|
|
11
|
+
export { default as AdminModal } from '@usethink/cf-admin-fe/components/AdminModal.vue'
|
|
12
|
+
export { default as ConfirmDialog } from '@usethink/cf-admin-fe/components/ConfirmDialog.vue'
|
|
13
|
+
export { default as ToastContainer } from '@usethink/cf-admin-fe/components/ToastContainer.vue'
|
|
14
|
+
export { default as ConfigField } from '@usethink/cf-admin-fe/components/ConfigField.vue'
|
|
15
|
+
export { default as AdminShell } from '@usethink/cf-admin-fe/components/AdminShell.vue'
|
|
16
|
+
export { default as AdminLoginShell } from '@usethink/cf-admin-fe/components/AdminLoginShell.vue'
|
|
17
|
+
export { default as AdminMetadataDetail } from '@usethink/cf-admin-fe/components/AdminMetadataDetail.vue'
|
|
18
|
+
export type { AdminShellMenuItem } from '@usethink/cf-admin-fe/components'
|
|
19
|
+
export type { AdminLogEntry } from '@usethink/cf-admin-fe/components'
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 将每个导出复制到各自的产品轻量文件中,使现有的
|
|
3
|
+
* `@/composables/useX` 导入无需大规模改写视图即可继续工作。
|
|
4
|
+
*
|
|
5
|
+
* 示例:`src/composables/useTablePagination.ts`
|
|
6
|
+
* export { useTablePagination, type UseTablePaginationOptions, type UseTablePaginationReturn } from '@usethink/cf-admin-fe/composables'
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
useTablePagination,
|
|
11
|
+
type UseTablePaginationOptions,
|
|
12
|
+
type UseTablePaginationReturn,
|
|
13
|
+
useTableSelection,
|
|
14
|
+
type UseTableSelectionOptions,
|
|
15
|
+
useConfirmDialog,
|
|
16
|
+
type ConfirmOptionDef,
|
|
17
|
+
type ConfirmAskOptions,
|
|
18
|
+
type ConfirmResult,
|
|
19
|
+
useToast,
|
|
20
|
+
MAX_TOASTS,
|
|
21
|
+
type ToastType,
|
|
22
|
+
type ToastItem,
|
|
23
|
+
useAdminBatchOperation,
|
|
24
|
+
type AdminBatchOperationResult,
|
|
25
|
+
writeClipboardText,
|
|
26
|
+
copyText,
|
|
27
|
+
copyWithToast,
|
|
28
|
+
type CopyWithToastOptions,
|
|
29
|
+
} from '@usethink/cf-admin-fe/composables'
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 产品侧再导出示例:`src/lib/csv-export.ts`、`body-scroll-lock.ts`、
|
|
3
|
+
* `composables/useFormat.ts`(日期 + 错误/批量助手等)。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
formatDate,
|
|
8
|
+
toDateTimeLocalValue,
|
|
9
|
+
dateTimeLocalToIso,
|
|
10
|
+
isoToDateTimeLocal,
|
|
11
|
+
formatIpFingerprint,
|
|
12
|
+
downloadCsv,
|
|
13
|
+
safeCsvCell,
|
|
14
|
+
lockBodyScroll,
|
|
15
|
+
unlockBodyScroll,
|
|
16
|
+
isBodyScrollLocked,
|
|
17
|
+
normalizeCents,
|
|
18
|
+
formatCents,
|
|
19
|
+
parseYuanToCents,
|
|
20
|
+
createAdminUnauthorizedHandler,
|
|
21
|
+
decideAdminRouteAccess,
|
|
22
|
+
resolveAdminLoginRedirect,
|
|
23
|
+
toErrorMessage,
|
|
24
|
+
resolveBatchToastType,
|
|
25
|
+
buildBatchToast,
|
|
26
|
+
checkAdminBatchLimit,
|
|
27
|
+
DEFAULT_ADMIN_BATCH_LIMIT,
|
|
28
|
+
formatMetadata,
|
|
29
|
+
buildAdminOffsetParams,
|
|
30
|
+
applyAdminOffsetParams,
|
|
31
|
+
} from '@usethink/cf-admin-fe/utils'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RECIPE — 将 `beforeEach` 主体粘贴到产品 `src/router/index.ts`。
|
|
3
|
+
* 抽奖产品:先运行工具包门禁,再运行产品 onboarding / 平台 / 仿冒登录。
|
|
4
|
+
*/
|
|
5
|
+
import type { Router } from 'vue-router'
|
|
6
|
+
import { decideAdminRouteAccess } from '@usethink/cf-admin-fe/utils'
|
|
7
|
+
// import { readAdminToken } from '@/composables/useAdminAuth'
|
|
8
|
+
|
|
9
|
+
export function installAdminRouteGuard(
|
|
10
|
+
router: Router,
|
|
11
|
+
readAdminToken: () => string,
|
|
12
|
+
): void {
|
|
13
|
+
router.beforeEach((to) => {
|
|
14
|
+
const adminToken = readAdminToken()
|
|
15
|
+
const decision = decideAdminRouteAccess({
|
|
16
|
+
hasToken: !!adminToken,
|
|
17
|
+
requiresAuth: to.meta.requiresAuth as boolean | undefined,
|
|
18
|
+
requiresGuest: to.meta.requiresGuest as boolean | undefined,
|
|
19
|
+
fullPath: to.fullPath,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
if (decision.type === 'login') {
|
|
23
|
+
return { name: 'AdminLogin', query: { redirect: decision.redirect }, replace: true }
|
|
24
|
+
}
|
|
25
|
+
if (decision.type === 'home') {
|
|
26
|
+
return { name: 'AdminDashboard', replace: true }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 产品专属钩子写在这里(抽奖 onboarding、平台角色等)
|
|
30
|
+
return true
|
|
31
|
+
})
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RECIPE — cookie 管理员本地标志会话(cf-auth 风格)。
|
|
3
|
+
*
|
|
4
|
+
* 不是 Bearer token。HTTP 会话是 cookie;这里只是给路由守卫用的
|
|
5
|
+
* localStorage 标记。在模块作用域创建一次。
|
|
6
|
+
*
|
|
7
|
+
* PRODUCT:健康探测 /api/admin/health、登录、登出、允许列表 — 永远留在产品侧。
|
|
8
|
+
*/
|
|
9
|
+
import { createAdminFlagSession } from '@usethink/cf-admin-fe/composables'
|
|
10
|
+
|
|
11
|
+
export const adminFlagSession = createAdminFlagSession({
|
|
12
|
+
storageKey: 'cf_auth_admin_ok', // PRODUCT:产品方自定义 key
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export const readAdminFlag = () => adminFlagSession.readFlag()
|
|
16
|
+
export const writeAdminFlag = (ok: boolean) => adminFlagSession.writeFlag(ok)
|
|
17
|
+
|
|
18
|
+
export function useAdminAuth() {
|
|
19
|
+
return {
|
|
20
|
+
isLoggedIn: adminFlagSession.isLoggedIn,
|
|
21
|
+
loggedIn: adminFlagSession.loggedIn,
|
|
22
|
+
markLoggedIn: adminFlagSession.markLoggedIn,
|
|
23
|
+
/** 为仍使用 clearToken 命名的产品 API 保留的别名 */
|
|
24
|
+
clearToken: adminFlagSession.clearSession,
|
|
25
|
+
clearSession: adminFlagSession.clearSession,
|
|
26
|
+
writeFlag: adminFlagSession.writeFlag,
|
|
27
|
+
readFlag: adminFlagSession.readFlag,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RECIPE — shop 简易 Bearer 会话(单个 localStorage key + 8 小时 TTL)。
|
|
3
|
+
* 复制到 `src/composables/useAdminAuth.ts`,必要时调整 TOKEN_KEY。
|
|
4
|
+
*
|
|
5
|
+
* 抽奖多租户 / 仿冒登录:保留产品胶水代码;仅复用工具包中的
|
|
6
|
+
* `parseStoredAdminToken` + `DEFAULT_ADMIN_TOKEN_TTL_MS`。
|
|
7
|
+
*/
|
|
8
|
+
import { createAdminTokenSession } from '@usethink/cf-admin-fe/composables'
|
|
9
|
+
|
|
10
|
+
const TOKEN_KEY = 'admin_token'
|
|
11
|
+
|
|
12
|
+
const session = createAdminTokenSession({
|
|
13
|
+
storageKey: TOKEN_KEY,
|
|
14
|
+
ttlMs: 8 * 60 * 60 * 1000,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
/** 供 setup 之外的 Router 守卫使用 */
|
|
18
|
+
export function readAdminToken(): string {
|
|
19
|
+
return session.readToken()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useAdminAuth() {
|
|
23
|
+
return {
|
|
24
|
+
token: session.token,
|
|
25
|
+
isLoggedIn: session.isLoggedIn,
|
|
26
|
+
setToken: session.setToken,
|
|
27
|
+
clearToken: session.clearToken,
|
|
28
|
+
}
|
|
29
|
+
}
|