smart-admin-web 10.5.0 → 10.10.1

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 (70) hide show
  1. package/README.md +2 -1
  2. package/cli/icon-subset.mjs +132 -0
  3. package/cli/smart-admin-icons.mjs +57 -0
  4. package/dist/api/client.js.map +1 -1
  5. package/dist/api/index.d.ts +3 -3
  6. package/dist/api/index.js +2 -2
  7. package/dist/api/index.js.map +1 -1
  8. package/dist/components/DetailPage/index.vue.js +1 -1
  9. package/dist/components/DetailPage/index2.vue.js.map +1 -1
  10. package/dist/components/IconPicker/index.vue.js.map +1 -1
  11. package/dist/components/ImportWizard/index.vue.js +1 -1
  12. package/dist/components/ImportWizard/index2.vue.js.map +1 -1
  13. package/dist/components/TableZoomButton/index.vue.js.map +1 -1
  14. package/dist/composables/useConfirm.js.map +1 -1
  15. package/dist/composables/useLayoutMenu.js.map +1 -1
  16. package/dist/composables/useRealtime.d.ts +1 -1
  17. package/dist/composables/useRealtime.js.map +1 -1
  18. package/dist/createSmartAdmin.d.ts +5 -2
  19. package/dist/createSmartAdmin.js +33 -32
  20. package/dist/createSmartAdmin.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +257 -244
  23. package/dist/index.js.map +1 -1
  24. package/dist/layouts/default.vue.js +2 -2
  25. package/dist/layouts/default2.vue.js.map +1 -1
  26. package/dist/lib/icons.d.ts +5 -2
  27. package/dist/lib/icons.js +15 -13
  28. package/dist/lib/icons.js.map +1 -1
  29. package/dist/lib/markdown.js.map +1 -1
  30. package/dist/lib/runtime.js +1 -1
  31. package/dist/locales/en-US.js +1 -1
  32. package/dist/locales/en-US.js.map +1 -1
  33. package/dist/locales/zh-CN.js +2 -2
  34. package/dist/locales/zh-CN.js.map +1 -1
  35. package/dist/router/detailRoutes.js.map +1 -1
  36. package/dist/router/index.js.map +1 -1
  37. package/dist/router/routes.js +1 -1
  38. package/dist/router/routes.js.map +1 -1
  39. package/dist/stores/user.d.ts +2 -2
  40. package/dist/stores/user.js.map +1 -1
  41. package/dist/style.css +1 -1
  42. package/dist/theme/mix.d.ts +3 -3
  43. package/dist/theme/mix.js.map +1 -1
  44. package/dist/types/api.d.ts +1 -1
  45. package/dist/types/api.js.map +1 -1
  46. package/dist/utils/url.d.ts +2 -0
  47. package/dist/utils/url.js +9 -3
  48. package/dist/utils/url.js.map +1 -1
  49. package/dist/views/error/404.vue.js +2 -2
  50. package/dist/views/error/4042.vue.js.map +1 -1
  51. package/dist/views/login/skins/SplitPanel.vue.js +1 -1
  52. package/dist/views/login/skins/SplitPanel2.vue.js.map +1 -1
  53. package/dist/views/system/log/op/index.vue.js +1 -1
  54. package/dist/views/system/log/op/index2.vue.js +1 -1
  55. package/dist/views/system/log/op/index2.vue.js.map +1 -1
  56. package/dist/views/system/menu/components/ButtonManager.vue.js +1 -1
  57. package/dist/views/system/menu/components/ButtonManager2.vue.js.map +1 -1
  58. package/dist/views/system/menu/components/MenuFormModal.vue.js +3 -3
  59. package/dist/views/system/menu/components/MenuFormModal2.vue.js.map +1 -1
  60. package/dist/views/system/menu/index.vue.js +2 -2
  61. package/dist/views/system/menu/index2.vue.js +6 -6
  62. package/dist/views/system/menu/index2.vue.js.map +1 -1
  63. package/dist/views/system/module/index.vue.js +2 -2
  64. package/dist/views/system/module/index2.vue.js +2 -2
  65. package/dist/views/system/module/index2.vue.js.map +1 -1
  66. package/dist/views/system/user/components/ResetPasswordModal.vue.js +1 -1
  67. package/dist/views/system/user/components/ResetPasswordModal2.vue.js.map +1 -1
  68. package/dist/views/system/user/components/UserFormModal.vue.js +1 -1
  69. package/dist/views/system/user/components/UserFormModal2.vue.js.map +1 -1
  70. package/package.json +6 -2
package/README.md CHANGED
@@ -49,6 +49,7 @@ createSmartAdmin({
49
49
  | `locales` | 文案。`locales/ext/<locale>/<命名空间>.ts` 默认导出该命名空间的键,按命名空间深合并进内置文案 |
50
50
  | `routes` | 布局壳之外的顶级静态路由,比如整屏看板、打印页 |
51
51
  | `icons` | 本地 SVG:`import.meta.glob('./assets/svg/*.svg', { query: '?raw', import: 'default', eager: true })` |
52
+ | `iconSets` | 应用自己的 `ph` 图标子集,启动时同步注册。用包带的命令生成:`npx smart-admin-icons` 扫描 `src` 里的 `ph:*` 名字,写出 `src/assets/icons/ph-subset.json`;`--check` 给 CI 用 |
52
53
  | `menuTitles` | 菜单 path → i18n key,存量库的菜单标题是中文时用 |
53
54
  | `install(app)` | 需要 app 实例的注册:`registerHeaderTool`、`registerMenuBadge`、自己的 `app.use` |
54
55
  | `plugins` | 与上面同形的插件对象。覆盖顺序:内核 < 插件(按数组顺序)< 应用 |
@@ -65,7 +66,7 @@ import { SmartTable } from 'smart-naive-table'
65
66
 
66
67
  ## 版本
67
68
 
68
- 版本号与 NuGet 包 `SmartAdmin` 一致:主版本 = 内核所用的 .NET 主版本,次版本加功能,修订号修 bug。内置页照同一版后端的接口写,所以 `package.json` 里精确钉住版本、不带 `^`;升级时 `Directory.Packages.props` 与 `package.json` 填同一个数字,改动见 [CHANGELOG](https://github.com/SmartCode-X/SmartAdmin/blob/main/CHANGELOG.md)。
69
+ 版本号与 NuGet 包 `SmartAdmin` 一致:主版本 = 内核所用的 .NET 主版本,次版本加功能,修订号修 bug。内置页照同一版后端的接口写,所以 `package.json` 里精确钉住版本、不带 `^`;升级时后端的 `SmartAdmin` 包与前端的 `smart-admin-web` 填同一个数字,改动见 [CHANGELOG](https://github.com/SmartCode-X/SmartAdmin/blob/main/CHANGELOG.md)。
69
70
 
70
71
  ## 文档
71
72
 
@@ -0,0 +1,132 @@
1
+ // Phosphor 图标子集:扫描源码里的 ph:* 名字,从 @iconify-json/ph 整集裁出用到的那几个。
2
+ // 内核的 scripts/gen-icon-subset.mjs 与随包发布的 smart-admin-icons 命令共用这一份,本模块没有副作用。
3
+ //
4
+ // 为什么要子集:整套 ph 有 9000+ 个图标(4.5 MB / 946 KB gz),一个应用的静态代码只用到几十个。
5
+ // 启动时同步注册子集,首帧即可离线渲染;子集外的名字(菜单管理里配的图标)由 AppIcon 懒加载整集兜底。
6
+ //
7
+ // 裁剪不引 @iconify/utils:这里只用得到它的 getIcons,它却会给每个消费方多装三个传递依赖。
8
+ // buildSubset 与 getIcons(data, names, true) 的产物逐字节一致,由 src/lib/iconSubset.spec.ts 拿 getIcons 对照锁住。
9
+
10
+ import { readFile, readdir, writeFile, mkdir } from 'node:fs/promises'
11
+ import { createRequire } from 'node:module'
12
+ import path from 'node:path'
13
+
14
+ const require = createRequire(import.meta.url)
15
+
16
+ export const PREFIX = 'ph'
17
+
18
+ const SCAN_EXT = new Set(['.vue', '.ts', '.tsx', '.js', '.jsx', '.mjs'])
19
+ // 测试文件里的假图标名不进产物
20
+ const TEST_FILE = /\.(spec|test)\.[^.]+$/
21
+ const ICON_RE = /\bph:[a-z0-9]+(?:-[a-z0-9]+)*\b/g
22
+
23
+ // 整集级的尺寸与 provider 字段,裁出的子集原样带上(同 getIcons 的 propsToCopy)
24
+ const ROOT_PROPS = ['left', 'top', 'width', 'height', 'provider']
25
+
26
+ /** 递归扫描目录,收集全部 `ph:xxx` 引用并入 `extra`,去重后升序返回。跳过 node_modules 与 . 开头的目录。 */
27
+ export async function scanIconNames(srcDir, extra = []) {
28
+ const found = new Set(extra)
29
+ async function walk(dir) {
30
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
31
+ const full = path.join(dir, entry.name)
32
+ if (entry.isDirectory()) {
33
+ if (entry.name !== 'node_modules' && !entry.name.startsWith('.')) await walk(full)
34
+ continue
35
+ }
36
+ if (!SCAN_EXT.has(path.extname(entry.name)) || TEST_FILE.test(entry.name)) continue
37
+ const text = await readFile(full, 'utf8')
38
+ for (const m of text.matchAll(ICON_RE)) found.add(m[0])
39
+ }
40
+ }
41
+ await walk(srcDir)
42
+ return [...found].toSorted()
43
+ }
44
+
45
+ /** 读取完整的 @iconify-json/ph 图标集。它是本包的依赖,从本模块所在位置解析,消费方不必自己装。 */
46
+ export async function loadFullSet() {
47
+ return JSON.parse(await readFile(require.resolve('@iconify-json/ph/icons.json'), 'utf8'))
48
+ }
49
+
50
+ /**
51
+ * 按名字裁出子集;`missing` 是 ph 里查无此名的(基本都是拼错)。别名沿 parent 链追到真图标,
52
+ * 链上每一环都进子集。结果的键序、别名处理与 getIcons 相同:prefix、icons、lastModified、aliases,最后是整集级字段。
53
+ */
54
+ export function buildSubset(full, names) {
55
+ const bare = names.map(n => (n.startsWith(`${PREFIX}:`) ? n.slice(PREFIX.length + 1) : n))
56
+ const sourceIcons = full.icons
57
+ const sourceAliases = full.aliases || Object.create(null)
58
+
59
+ // 名字 → 追到真图标所经的 parent 链([] = 本身就是图标,null = 查无此名或链断了)
60
+ const resolved = Object.create(null)
61
+ const resolve = name => {
62
+ if (sourceIcons[name]) return (resolved[name] = [])
63
+ if (!(name in resolved)) {
64
+ resolved[name] = null
65
+ const parent = sourceAliases[name] && sourceAliases[name].parent
66
+ const chain = parent && resolve(parent)
67
+ if (chain) resolved[name] = [parent, ...chain]
68
+ }
69
+ return resolved[name]
70
+ }
71
+ bare.forEach(resolve)
72
+
73
+ const subset = { prefix: full.prefix, icons: Object.create(null) }
74
+ if (full.lastModified) subset.lastModified = full.lastModified
75
+ const missing = []
76
+ for (const name in resolved) {
77
+ if (!resolved[name]) {
78
+ if (bare.includes(name)) missing.push(`${PREFIX}:${name}`)
79
+ } else if (sourceIcons[name]) {
80
+ subset.icons[name] = { ...sourceIcons[name] }
81
+ } else {
82
+ subset.aliases ??= Object.create(null)
83
+ subset.aliases[name] = { ...sourceAliases[name] }
84
+ }
85
+ }
86
+ for (const key of ROOT_PROPS) if (key in full) subset[key] = full[key]
87
+ return { subset, missing }
88
+ }
89
+
90
+ /** 与写盘一致的序列化(--check 靠逐字节比对)。 */
91
+ export function serialize(subset) {
92
+ return `${JSON.stringify(subset, null, 2)}\n`
93
+ }
94
+
95
+ /**
96
+ * 扫描 `src`、裁出子集后写进 `out`;`check` 为真时只比对不写盘。`extra` 是源码里没有、也要进子集的名字,
97
+ * `staleHint` 接在「已过期」提示后面告诉人怎么重生成。返回进程退出码:0 成功,1 有拼错的名字或产物已过期。
98
+ */
99
+ export async function generate({ src, out, check = false, extra = [], staleHint }) {
100
+ let names
101
+ try {
102
+ names = await scanIconNames(src, extra)
103
+ } catch (e) {
104
+ if (e?.code !== 'ENOENT') throw e
105
+ console.error(`扫描目录不存在:${src}`)
106
+ return 1
107
+ }
108
+
109
+ const { subset, missing } = buildSubset(await loadFullSet(), names)
110
+ if (missing.length) {
111
+ console.error(`图标名在 Phosphor 里不存在(拼错?):\n ${missing.join('\n ')}`)
112
+ return 1
113
+ }
114
+
115
+ const text = serialize(subset)
116
+ const count = Object.keys(subset.icons).length
117
+ const shown = path.relative(process.cwd(), out) || out
118
+ if (check) {
119
+ const current = await readFile(out, 'utf8').catch(() => '')
120
+ if (current !== text) {
121
+ console.error(`${shown} 已过期,${staleHint}`)
122
+ return 1
123
+ }
124
+ console.log(`图标子集已是最新(${count} 个)`)
125
+ return 0
126
+ }
127
+
128
+ await mkdir(path.dirname(out), { recursive: true })
129
+ await writeFile(out, text, 'utf8')
130
+ console.log(`已写出 ${shown}(${count} 个图标)`)
131
+ return 0
132
+ }
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ // smart-admin-icons:给应用生成自己的 ph 离线图标子集,交给 createSmartAdmin({ iconSets }) 启动时同步注册。
3
+ // 扫描、裁剪、--check 的逻辑在 icon-subset.mjs,本文件只解析参数。退出码:0 成功;1 图标名拼错或 --check 下产物过期;2 参数错误。
4
+ //
5
+ // 不加「被 import 时不执行」的守卫:npm 在 POSIX 上把 node_modules/.bin 里的命令做成符号链接,
6
+ // process.argv[1] 是链接路径而不是本文件,拿它和 import.meta.url 比对,命令会静默什么都不做。
7
+
8
+ import path from 'node:path'
9
+ import { generate } from './icon-subset.mjs'
10
+
11
+ const USAGE = `用法:smart-admin-icons [--src <目录>] [--out <文件>] [--check]
12
+
13
+ 扫描 --src 下 .vue/.ts/.tsx/.js/.jsx/.mjs 里的 ph:* 图标名(跳过 *.spec.*、*.test.*、node_modules),
14
+ 从 Phosphor 整集裁出这些图标写进 --out,交给 createSmartAdmin({ iconSets }) 启动时同步注册。
15
+ 源码里没有、却要离线可用的名字(比如种子菜单的图标),写进 --src 下任意一个 .ts 就会被扫进来。
16
+
17
+ --src <目录> 扫描根,默认 src
18
+ --out <文件> 产物路径,默认 src/assets/icons/ph-subset.json
19
+ --check 只比对不写盘:产物过期或有拼错的名字时非 0 退出(CI 用)
20
+ --help 显示本说明`
21
+
22
+ function parseArgs(argv) {
23
+ const opts = { src: 'src', out: 'src/assets/icons/ph-subset.json', check: false, help: false }
24
+ for (let i = 0; i < argv.length; i++) {
25
+ const arg = argv[i]
26
+ if (arg === '--check') opts.check = true
27
+ else if (arg === '--help' || arg === '-h') opts.help = true
28
+ else {
29
+ // --src x 与 --src=x 两种写法都认
30
+ const m = /^--(src|out)(?:=(.*))?$/.exec(arg)
31
+ if (!m) throw new Error(`未知参数:${arg}`)
32
+ const value = m[2] ?? argv[++i]
33
+ if (!value || value.startsWith('--')) throw new Error(`--${m[1]} 缺少值`)
34
+ opts[m[1]] = value
35
+ }
36
+ }
37
+ return opts
38
+ }
39
+
40
+ let opts
41
+ try {
42
+ opts = parseArgs(process.argv.slice(2))
43
+ } catch (e) {
44
+ console.error(`${e.message}\n\n${USAGE}`)
45
+ process.exitCode = 2
46
+ }
47
+
48
+ if (opts?.help) {
49
+ console.log(USAGE)
50
+ } else if (opts) {
51
+ process.exitCode = await generate({
52
+ src: path.resolve(opts.src),
53
+ out: path.resolve(opts.out),
54
+ check: opts.check,
55
+ staleHint: '去掉 --check 再跑一次即可更新',
56
+ })
57
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sources":["../../src/api/client.ts"],"sourcesContent":["import createClient, { type Client, type Middleware } from 'openapi-fetch'\nimport type { paths } from './schema'\nimport { useUserStore } from '#/stores/user'\nimport { runtime } from '#/lib/runtime'\nimport { REAUTH_REQUIRED_CODE, REAUTH_RETRY_HEADER, requestReauth } from './reauthGate'\n\n// 默认空 apiBase = 同源:schema 的 path 键已含 /api/v1,dev 下由 Vite proxy 反代到后端(默认 :5100),\n// 生产下由 nginx 反代或后端自己托管 dist(见 docs/deployment.md 路线 A/B)。\n// 只有前端与 API 真的不同源(CDN / 独立域名)时才需要 createSmartAdmin({ apiBase: 'https://api.example.com' }),\n// 此时后端还必须配 SmartAdmin:Api:Cors:AllowedOrigins(默认 deny-all)。\n\n/**\n * 请求超时:没有超时的话,后端卡死或链路半开时 fetch 会一直挂着 —— 页面永远转圈,\n * 按钮的 loading 永不结束,用户只能刷新。\n * 默认 30 秒;上传/分片/导出/导入提交这类天然慢的调用在 api 层显式传 `timeout: LONG_TIMEOUT_MS`。\n */\nexport const DEFAULT_TIMEOUT_MS = 30_000\nexport const LONG_TIMEOUT_MS = 10 * 60_000\n\n/** 与后端 AuthCookieNames 对齐(禁硬编码纪律的前端镜像)。 */\nconst CSRF_COOKIE = 'smart_csrf'\nconst CSRF_HEADER = 'X-Smart-CSRF'\n\n/**\n * 读 document.cookie 中的可读 Cookie(双提交 CSRF 的 smart_csrf)。\n * HttpOnly 的 smart_rt 读不到,只能靠 credentials 自动携带。\n */\nexport function readCookie(name: string): string {\n if (typeof document === 'undefined') return ''\n const parts = document.cookie.split(';')\n for (const part of parts) {\n const i = part.indexOf('=')\n if (i < 0) continue\n const k = part.slice(0, i).trim()\n if (k === name) return decodeURIComponent(part.slice(i + 1).trim())\n }\n return ''\n}\n\n/** 写请求是否需要附 CSRF(有可读 CSRF Cookie 即附;无 Cookie 时不附)。 */\nfunction isMutating(method: string): boolean {\n const m = method.toUpperCase()\n return m !== 'GET' && m !== 'HEAD' && m !== 'OPTIONS'\n}\n\nfunction attachCsrf(headers: Headers) {\n const csrf = readCookie(CSRF_COOKIE)\n if (csrf) headers.set(CSRF_HEADER, csrf)\n}\n\n/**\n * 给每个请求装超时闸。必须**最先**注册:后面的重放中间件要克隆最终的 Request,顺序反了就克隆到旧的那份。\n *\n * 调用方传的 `signal` 依然生效 —— 这里用 `AbortSignal.any` 把它和超时信号并起来,谁先响算谁,\n * 不是把调用方的取消能力顶掉。单次调用要放宽时限就多传一个 `timeout`(毫秒),\n * openapi-fetch 会把它这类自定义键原样挂到 Request 上(见其 InitParam 的 `[key: string]: unknown`)。\n */\nconst timeoutMiddleware: Middleware = {\n onRequest({ request }) {\n const ms = (request as Request & { timeout?: unknown }).timeout\n const limit = typeof ms === 'number' && ms > 0 ? ms : DEFAULT_TIMEOUT_MS\n return new Request(request, {\n signal: AbortSignal.any([request.signal, AbortSignal.timeout(limit)]),\n })\n },\n}\n\n/** 请求前注入 Bearer + 写操作 CSRF。请求时读 store,始终拿最新令牌。 */\nconst authMiddleware: Middleware = {\n async onRequest({ request }) {\n const token = useUserStore().accessToken\n if (token) request.headers.set('Authorization', `Bearer ${token}`)\n if (isMutating(request.method)) attachCsrf(request.headers)\n return request\n },\n}\n\n// 令牌过期重放:Request 的 body 是一次性流,首次 fetch 就被消费,直接重放会丢 body\n//(GET 无 body 不受影响,但令牌恰好在一次 POST/PUT 时过期就会丢请求体)。\n// 发出前克隆一份副本(clone 会 tee body 流,与原请求各读各的),重放时用这份未消费的副本。\n// 键是发出前的 Request 实例(openapi-fetch 把它一路带到 onResponse),WeakMap 随请求回收自动清理。\nconst replayable = new WeakMap<Request, Request>()\n\n// 并发 401 合流到同一次刷新。\nlet refreshing: Promise<boolean> | null = null\n\n/** 单飞静默刷新;路由守卫 F5 重建 Cookie 会话时也可调用。 */\nexport function refreshOnce(): Promise<boolean> {\n refreshing ??= doRefresh().finally(() => {\n refreshing = null\n })\n return refreshing\n}\n\n/**\n * 确保有可用 accessToken:已有则 true;Cookie 会话或 body refresh 则尝试静默刷新。\n * 供路由守卫在 F5/深链时恢复内存令牌。\n */\nexport async function ensureAccessToken(): Promise<boolean> {\n const user = useUserStore()\n if (user.accessToken) return true\n if (!user.cookieSession && !user.refreshToken) return false\n return refreshOnce()\n}\n\nasync function doRefresh(): Promise<boolean> {\n const user = useUserStore()\n // Cookie 会话:body 不带 refresh(服务端读 smart_rt);body 模式:必须有 refreshToken\n if (!user.cookieSession && !user.refreshToken) return false\n\n const headers: Record<string, string> = {}\n if (user.cookieSession || readCookie(CSRF_COOKIE)) {\n const csrf = readCookie(CSRF_COOKIE)\n if (csrf) headers[CSRF_HEADER] = csrf\n }\n\n const { data, error } = await bare.POST('/api/v1/auth/refresh', {\n // Cookie 模式刷新令牌在 HttpOnly Cookie 里,body 的 refreshToken 留空:服务端 body 为空才读 Cookie\n body: user.cookieSession ? { refreshToken: '' } : { refreshToken: user.refreshToken },\n headers,\n })\n const env = data as { code?: number; data?: unknown } | undefined\n if (error || !env || env.code !== 0 || !env.data) return false\n user.setSession(env.data as Parameters<typeof user.setSession>[0])\n return true\n}\n\n/** 401 → 刷新一次并重放原请求;刷新失败 → 清会话 + 跳登录。 */\nconst refreshMiddleware: Middleware = {\n onRequest({ request }) {\n // 带 body 的写请求发出前存一份可重放副本(GET/HEAD 无 body,不必克隆)。\n if (request.method !== 'GET' && request.method !== 'HEAD')\n replayable.set(request, request.clone())\n return request\n },\n async onResponse({ request, response }) {\n if (response.status !== 401) return response\n const url = request.url\n if (url.includes('/api/v1/auth/refresh') || url.includes('/api/v1/auth/login')) return response\n\n const ok = await refreshOnce()\n if (!ok) {\n useUserStore().clear()\n const { useAuthStore } = await import('#/stores/auth')\n useAuthStore().reset()\n const { router, resetRouter } = await import('#/router') // 惰性引入,避免与 router 静态循环依赖\n resetRouter()\n if (router.currentRoute.value.path !== '/login') router.replace('/login')\n return response\n }\n // 重放:优先用发出前的克隆副本(body 未被消费),无副本的 GET 直接用原请求;裸 fetch 绕过中间件,手动补新令牌 + CSRF。\n const base = replayable.get(request) ?? request\n const retry = new Request(base, { headers: new Headers(base.headers), credentials: 'include' })\n retry.headers.set('Authorization', `Bearer ${useUserStore().accessToken}`)\n if (isMutating(retry.method)) attachCsrf(retry.headers)\n return fetch(retry)\n },\n}\n\n/**\n * 403 + 40024(ReauthRequired) → 弹再认证 → 成功后重放一次。\n * 与 refresh 同形:依赖 onRequest 预克隆 body;重试头防死循环。\n */\nconst reauthMiddleware: Middleware = {\n async onResponse({ request, response }) {\n if (response.status !== 403) return response\n if (request.headers.get(REAUTH_RETRY_HEADER) === '1') return response\n if (request.url.includes('/api/v1/auth/reauth') || request.url.includes('/api/v1/auth/login')) {\n return response\n }\n\n let code: number | undefined\n try {\n const body = (await response.clone().json()) as { code?: number }\n code = body?.code\n } catch {\n return response\n }\n if (code !== REAUTH_REQUIRED_CODE) return response\n\n const granted = await requestReauth()\n if (!granted) return response\n\n const base = replayable.get(request) ?? request\n const retry = new Request(base, { headers: new Headers(base.headers), credentials: 'include' })\n const token = useUserStore().accessToken\n if (token) retry.headers.set('Authorization', `Bearer ${token}`)\n if (isMutating(retry.method)) attachCsrf(retry.headers)\n retry.headers.set(REAUTH_RETRY_HEADER, '1')\n return fetch(retry)\n },\n}\n\n/** 内核同款中间件链:超时必须最先(后面两个要克隆最终 Request 做重放)→ Bearer + CSRF → 401 刷新重放 → 40024 再认证。 */\nfunction attachMiddlewares<P extends {}>(c: Client<P>): Client<P> {\n c.use(timeoutMiddleware)\n c.use(authMiddleware)\n c.use(refreshMiddleware)\n c.use(reauthMiddleware)\n return c\n}\n\n/**\n * 按调用时的 runtime.apiBase 取客户端。客户端大多在模块顶层创建(`export const client = createApiClient()`),\n * 那时 createSmartAdmin 可能还没跑、apiBase 尚未落定;所以每次调用前核对一次,变了就按新地址重建,\n * 调用方后加的中间件(use)一并带过去。\n */\nfunction followApiBase<P extends {}>(build: (baseUrl: string) => Client<P>): Client<P> {\n let base = runtime.apiBase\n let inner = build(base)\n const added: Middleware[] = []\n const current = () => {\n if (base !== runtime.apiBase) {\n base = runtime.apiBase\n inner = build(base)\n if (added.length) inner.use(...added)\n }\n return inner\n }\n return new Proxy({} as Client<P>, {\n get(_target, key) {\n if (key === 'use')\n return (...mw: Middleware[]) => {\n added.push(...mw)\n current().use(...mw)\n }\n if (key === 'eject')\n return (...mw: Middleware[]) => {\n for (const m of mw) {\n const i = added.indexOf(m)\n if (i >= 0) added.splice(i, 1)\n }\n current().eject(...mw)\n }\n const c = current() as unknown as Record<PropertyKey, unknown>\n const v = c[key]\n return typeof v === 'function' ? (v as (...args: unknown[]) => unknown).bind(c) : v\n },\n })\n}\n\n/**\n * 消费方自己端点的类型化客户端:与内核 client 同 apiBase、同一条中间件链,只是 paths 换成自己生成的 schema。\n * credentials:'include'——Cookie 会话(Session:CookieMode)静默刷新/登出依赖 HttpOnly refresh Cookie;\n * 同源默认也会带 Cookie,显式 include 覆盖 apiBase 跨源场景(需后端 CORS AllowCredentials)。\n */\nexport function createApiClient<P extends {}>(): Client<P> {\n return followApiBase(baseUrl =>\n attachMiddlewares(createClient<P>({ baseUrl, credentials: 'include' })),\n )\n}\n\n/** 内核客户端(内核端点的 paths)。 */\nexport const client = createApiClient<paths>()\n\n// 刷新专用客户端:不挂刷新中间件,避免刷新自身 401 触发递归;\n// 但也要有超时闸:它挂住时路由守卫会一起卡在 ensureAccessToken 上,整页停在白屏。\nconst bare = followApiBase(baseUrl => {\n const c = createClient<paths>({ baseUrl, credentials: 'include' })\n c.use(timeoutMiddleware)\n return c\n})\n"],"names":["DEFAULT_TIMEOUT_MS","LONG_TIMEOUT_MS","CSRF_COOKIE","CSRF_HEADER","readCookie","name","parts","part","i","isMutating","method","m","attachCsrf","headers","csrf","timeoutMiddleware","request","ms","limit","authMiddleware","token","useUserStore","replayable","refreshing","refreshOnce","doRefresh","ensureAccessToken","user","data","error","bare","env","refreshMiddleware","response","url","useAuthStore","router","resetRouter","base","retry","reauthMiddleware","REAUTH_RETRY_HEADER","code","body","REAUTH_REQUIRED_CODE","requestReauth","attachMiddlewares","c","followApiBase","build","runtime","inner","added","current","_target","key","mw","v","createApiClient","baseUrl","createClient","client"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sources":["../../src/api/client.ts"],"sourcesContent":["import createClient, { type Client, type Middleware } from 'openapi-fetch'\nimport type { paths } from './schema'\nimport { useUserStore } from '#/stores/user'\nimport { runtime } from '#/lib/runtime'\nimport { REAUTH_REQUIRED_CODE, REAUTH_RETRY_HEADER, requestReauth } from './reauthGate'\n\n// 默认空 apiBase = 同源:schema 的 path 键已含 /api/v1,dev 下由 Vite proxy 反代到后端(默认 :5100),\n// 生产下由 nginx 反代或后端自己托管 dist(见文档站部署指南的路线 A / B)。\n// 只有前端与 API 真的不同源(CDN / 独立域名)时才需要 createSmartAdmin({ apiBase: 'https://api.example.com' }),\n// 此时后端还必须配 SmartAdmin:Api:Cors:AllowedOrigins(默认 deny-all)。\n\n/**\n * 请求超时:没有超时的话,后端卡死或链路半开时 fetch 会一直挂着 —— 页面永远转圈,\n * 按钮的 loading 永不结束,用户只能刷新。\n * 默认 30 秒;上传/分片/导出/导入提交这类天然慢的调用在 api 层显式传 `timeout: LONG_TIMEOUT_MS`。\n */\nexport const DEFAULT_TIMEOUT_MS = 30_000\nexport const LONG_TIMEOUT_MS = 10 * 60_000\n\n/** 与后端 AuthCookieNames 对齐(禁硬编码纪律的前端镜像)。 */\nconst CSRF_COOKIE = 'smart_csrf'\nconst CSRF_HEADER = 'X-Smart-CSRF'\n\n/**\n * 读 document.cookie 中的可读 Cookie(双提交 CSRF 的 smart_csrf)。\n * HttpOnly 的 smart_rt 读不到,只能靠 credentials 自动携带。\n */\nexport function readCookie(name: string): string {\n if (typeof document === 'undefined') return ''\n const parts = document.cookie.split(';')\n for (const part of parts) {\n const i = part.indexOf('=')\n if (i < 0) continue\n const k = part.slice(0, i).trim()\n if (k === name) return decodeURIComponent(part.slice(i + 1).trim())\n }\n return ''\n}\n\n/** 写请求是否需要附 CSRF(有可读 CSRF Cookie 即附;无 Cookie 时不附)。 */\nfunction isMutating(method: string): boolean {\n const m = method.toUpperCase()\n return m !== 'GET' && m !== 'HEAD' && m !== 'OPTIONS'\n}\n\nfunction attachCsrf(headers: Headers) {\n const csrf = readCookie(CSRF_COOKIE)\n if (csrf) headers.set(CSRF_HEADER, csrf)\n}\n\n/**\n * 给每个请求装超时闸。必须**最先**注册:后面的重放中间件要克隆最终的 Request,顺序反了就克隆到旧的那份。\n *\n * 调用方传的 `signal` 依然生效 —— 这里用 `AbortSignal.any` 把它和超时信号并起来,谁先响算谁,\n * 不是把调用方的取消能力顶掉。单次调用要放宽时限就多传一个 `timeout`(毫秒),\n * openapi-fetch 会把它这类自定义键原样挂到 Request 上(见其 InitParam 的 `[key: string]: unknown`)。\n */\nconst timeoutMiddleware: Middleware = {\n onRequest({ request }) {\n const ms = (request as Request & { timeout?: unknown }).timeout\n const limit = typeof ms === 'number' && ms > 0 ? ms : DEFAULT_TIMEOUT_MS\n return new Request(request, {\n signal: AbortSignal.any([request.signal, AbortSignal.timeout(limit)]),\n })\n },\n}\n\n/** 请求前注入 Bearer + 写操作 CSRF。请求时读 store,始终拿最新令牌。 */\nconst authMiddleware: Middleware = {\n async onRequest({ request }) {\n const token = useUserStore().accessToken\n if (token) request.headers.set('Authorization', `Bearer ${token}`)\n if (isMutating(request.method)) attachCsrf(request.headers)\n return request\n },\n}\n\n// 令牌过期重放:Request 的 body 是一次性流,首次 fetch 就被消费,直接重放会丢 body\n//(GET 无 body 不受影响,但令牌恰好在一次 POST/PUT 时过期就会丢请求体)。\n// 发出前克隆一份副本(clone 会 tee body 流,与原请求各读各的),重放时用这份未消费的副本。\n// 键是发出前的 Request 实例(openapi-fetch 把它一路带到 onResponse),WeakMap 随请求回收自动清理。\nconst replayable = new WeakMap<Request, Request>()\n\n// 并发 401 合流到同一次刷新。\nlet refreshing: Promise<boolean> | null = null\n\n/** 单飞静默刷新;路由守卫 F5 重建 Cookie 会话时也可调用。 */\nexport function refreshOnce(): Promise<boolean> {\n refreshing ??= doRefresh().finally(() => {\n refreshing = null\n })\n return refreshing\n}\n\n/**\n * 确保有可用 accessToken:已有则 true;Cookie 会话或 body refresh 则尝试静默刷新。\n * 供路由守卫在 F5/深链时恢复内存令牌。\n */\nexport async function ensureAccessToken(): Promise<boolean> {\n const user = useUserStore()\n if (user.accessToken) return true\n if (!user.cookieSession && !user.refreshToken) return false\n return refreshOnce()\n}\n\nasync function doRefresh(): Promise<boolean> {\n const user = useUserStore()\n // Cookie 会话:body 不带 refresh(服务端读 smart_rt);body 模式:必须有 refreshToken\n if (!user.cookieSession && !user.refreshToken) return false\n\n const headers: Record<string, string> = {}\n if (user.cookieSession || readCookie(CSRF_COOKIE)) {\n const csrf = readCookie(CSRF_COOKIE)\n if (csrf) headers[CSRF_HEADER] = csrf\n }\n\n const { data, error } = await bare.POST('/api/v1/auth/refresh', {\n // Cookie 模式刷新令牌在 HttpOnly Cookie 里,body 的 refreshToken 留空:服务端 body 为空才读 Cookie\n body: user.cookieSession ? { refreshToken: '' } : { refreshToken: user.refreshToken },\n headers,\n })\n const env = data as { code?: number; data?: unknown } | undefined\n if (error || !env || env.code !== 0 || !env.data) return false\n user.setSession(env.data as Parameters<typeof user.setSession>[0])\n return true\n}\n\n/** 401 → 刷新一次并重放原请求;刷新失败 → 清会话 + 跳登录。 */\nconst refreshMiddleware: Middleware = {\n onRequest({ request }) {\n // 带 body 的写请求发出前存一份可重放副本(GET/HEAD 无 body,不必克隆)。\n if (request.method !== 'GET' && request.method !== 'HEAD')\n replayable.set(request, request.clone())\n return request\n },\n async onResponse({ request, response }) {\n if (response.status !== 401) return response\n const url = request.url\n if (url.includes('/api/v1/auth/refresh') || url.includes('/api/v1/auth/login')) return response\n\n const ok = await refreshOnce()\n if (!ok) {\n useUserStore().clear()\n const { useAuthStore } = await import('#/stores/auth')\n useAuthStore().reset()\n const { router, resetRouter } = await import('#/router') // 惰性引入,避免与 router 静态循环依赖\n resetRouter()\n if (router.currentRoute.value.path !== '/login') router.replace('/login')\n return response\n }\n // 重放:优先用发出前的克隆副本(body 未被消费),无副本的 GET 直接用原请求;裸 fetch 绕过中间件,手动补新令牌 + CSRF。\n const base = replayable.get(request) ?? request\n const retry = new Request(base, { headers: new Headers(base.headers), credentials: 'include' })\n retry.headers.set('Authorization', `Bearer ${useUserStore().accessToken}`)\n if (isMutating(retry.method)) attachCsrf(retry.headers)\n return fetch(retry)\n },\n}\n\n/**\n * 403 + 40024(ReauthRequired) → 弹再认证 → 成功后重放一次。\n * 与 refresh 同形:依赖 onRequest 预克隆 body;重试头防死循环。\n */\nconst reauthMiddleware: Middleware = {\n async onResponse({ request, response }) {\n if (response.status !== 403) return response\n if (request.headers.get(REAUTH_RETRY_HEADER) === '1') return response\n if (request.url.includes('/api/v1/auth/reauth') || request.url.includes('/api/v1/auth/login')) {\n return response\n }\n\n let code: number | undefined\n try {\n const body = (await response.clone().json()) as { code?: number }\n code = body?.code\n } catch {\n return response\n }\n if (code !== REAUTH_REQUIRED_CODE) return response\n\n const granted = await requestReauth()\n if (!granted) return response\n\n const base = replayable.get(request) ?? request\n const retry = new Request(base, { headers: new Headers(base.headers), credentials: 'include' })\n const token = useUserStore().accessToken\n if (token) retry.headers.set('Authorization', `Bearer ${token}`)\n if (isMutating(retry.method)) attachCsrf(retry.headers)\n retry.headers.set(REAUTH_RETRY_HEADER, '1')\n return fetch(retry)\n },\n}\n\n/** 内核同款中间件链:超时必须最先(后面两个要克隆最终 Request 做重放)→ Bearer + CSRF → 401 刷新重放 → 40024 再认证。 */\nfunction attachMiddlewares<P extends {}>(c: Client<P>): Client<P> {\n c.use(timeoutMiddleware)\n c.use(authMiddleware)\n c.use(refreshMiddleware)\n c.use(reauthMiddleware)\n return c\n}\n\n/**\n * 按调用时的 runtime.apiBase 取客户端。客户端大多在模块顶层创建(`export const client = createApiClient()`),\n * 那时 createSmartAdmin 可能还没跑、apiBase 尚未落定;所以每次调用前核对一次,变了就按新地址重建,\n * 调用方后加的中间件(use)一并带过去。\n */\nfunction followApiBase<P extends {}>(build: (baseUrl: string) => Client<P>): Client<P> {\n let base = runtime.apiBase\n let inner = build(base)\n const added: Middleware[] = []\n const current = () => {\n if (base !== runtime.apiBase) {\n base = runtime.apiBase\n inner = build(base)\n if (added.length) inner.use(...added)\n }\n return inner\n }\n return new Proxy({} as Client<P>, {\n get(_target, key) {\n if (key === 'use')\n return (...mw: Middleware[]) => {\n added.push(...mw)\n current().use(...mw)\n }\n if (key === 'eject')\n return (...mw: Middleware[]) => {\n for (const m of mw) {\n const i = added.indexOf(m)\n if (i >= 0) added.splice(i, 1)\n }\n current().eject(...mw)\n }\n const c = current() as unknown as Record<PropertyKey, unknown>\n const v = c[key]\n return typeof v === 'function' ? (v as (...args: unknown[]) => unknown).bind(c) : v\n },\n })\n}\n\n/**\n * 消费方自己端点的类型化客户端:与内核 client 同 apiBase、同一条中间件链,只是 paths 换成自己生成的 schema。\n * credentials:'include'——Cookie 会话(Session:CookieMode)静默刷新/登出依赖 HttpOnly refresh Cookie;\n * 同源默认也会带 Cookie,显式 include 覆盖 apiBase 跨源场景(需后端 CORS AllowCredentials)。\n */\nexport function createApiClient<P extends {}>(): Client<P> {\n return followApiBase(baseUrl =>\n attachMiddlewares(createClient<P>({ baseUrl, credentials: 'include' })),\n )\n}\n\n/** 内核客户端(内核端点的 paths)。 */\nexport const client = createApiClient<paths>()\n\n// 刷新专用客户端:不挂刷新中间件,避免刷新自身 401 触发递归;\n// 但也要有超时闸:它挂住时路由守卫会一起卡在 ensureAccessToken 上,整页停在白屏。\nconst bare = followApiBase(baseUrl => {\n const c = createClient<paths>({ baseUrl, credentials: 'include' })\n c.use(timeoutMiddleware)\n return c\n})\n"],"names":["DEFAULT_TIMEOUT_MS","LONG_TIMEOUT_MS","CSRF_COOKIE","CSRF_HEADER","readCookie","name","parts","part","i","isMutating","method","m","attachCsrf","headers","csrf","timeoutMiddleware","request","ms","limit","authMiddleware","token","useUserStore","replayable","refreshing","refreshOnce","doRefresh","ensureAccessToken","user","data","error","bare","env","refreshMiddleware","response","url","useAuthStore","router","resetRouter","base","retry","reauthMiddleware","REAUTH_RETRY_HEADER","code","body","REAUTH_REQUIRED_CODE","requestReauth","attachMiddlewares","c","followApiBase","build","runtime","inner","added","current","_target","key","mw","v","createApiClient","baseUrl","createClient","client"],"mappings":"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"}
@@ -81,7 +81,7 @@ export declare function toPage<T>(res: Parameters<typeof unwrap>[0]): {
81
81
  total: number;
82
82
  };
83
83
  /**
84
- * 下载 xlsx 等二进制:parseAs blob + 手查 ok(坑 2:unwrap 假定 JSON 信封)。
84
+ * 下载 xlsx 等二进制:parseAs blob + 手查 ok(unwrap 假定 JSON 信封,接不了二进制)。
85
85
  * AdminExceptionFilter 对业务失败仍回 HTTP 200 + JSON 信封,故还要辨 content-type /
86
86
  * 是否以 `{` 开头,避免把错误信封当 xlsx 触发下载。
87
87
  *
@@ -287,7 +287,7 @@ export declare const userApi: {
287
287
  importTemplate: () => Promise<Blob>;
288
288
  /**
289
289
  * 上传 xlsx 预览校验。multipart:`file` + 可选 `mapping`(JSON 字符串:表头→列 Key)。
290
- * bodySerializer 建 FormData(坑 3:与 fileApi.upload 同型)。
290
+ * bodySerializer 建 FormData(与 fileApi.upload 同型)。
291
291
  */
292
292
  importPreview: (file: File, mapping?: Record<string, string>) => Promise<ImportPreview>;
293
293
  /** 对前端改过的行重新校验(不碰文件)。 */
@@ -449,7 +449,7 @@ export declare const logApi: {
449
449
  opClear: () => Promise<boolean>;
450
450
  /**
451
451
  * 导出操作日志 xlsx。筛选与 opPage 同源 + `columns` 逗号分隔列 Key。
452
- * 返回 blob(坑 2);不进信封。
452
+ * 返回 blob,不进信封。
453
453
  */
454
454
  opExport: (params: {
455
455
  title?: string;
package/dist/api/index.js CHANGED
@@ -224,7 +224,7 @@ const C = {
224
224
  importTemplate: () => s.GET("/api/v1/sys/user/import/template", { parseAs: "blob" }).then((e) => v(e)),
225
225
  /**
226
226
  * 上传 xlsx 预览校验。multipart:`file` + 可选 `mapping`(JSON 字符串:表头→列 Key)。
227
- * bodySerializer 建 FormData(坑 3:与 fileApi.upload 同型)。
227
+ * bodySerializer 建 FormData(与 fileApi.upload 同型)。
228
228
  */
229
229
  importPreview: (e, t) => s.POST("/api/v1/sys/user/import/preview", {
230
230
  timeout: l,
@@ -398,7 +398,7 @@ const B = {
398
398
  opClear: () => s.DELETE("/api/v1/sys/log/op", {}).then((e) => a(e)),
399
399
  /**
400
400
  * 导出操作日志 xlsx。筛选与 opPage 同源 + `columns` 逗号分隔列 Key。
401
- * 返回 blob(坑 2);不进信封。
401
+ * 返回 blob,不进信封。
402
402
  */
403
403
  opExport: (e) => s.GET("/api/v1/sys/log/op/export", {
404
404
  timeout: l,