@usethink/cf-admin-fe 0.1.1 → 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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +242 -53
  3. package/dist/composables/createAdminFlagSession.d.ts +42 -0
  4. package/dist/composables/createAdminRequest.d.ts +95 -0
  5. package/dist/composables/createAdminTokenSession.d.ts +49 -0
  6. package/dist/composables/index.d.ts +7 -1
  7. package/dist/composables/useAdminListLoader.d.ts +45 -0
  8. package/dist/composables/useAdminSidebar.d.ts +17 -0
  9. package/dist/composables/useClipboard.d.ts +24 -2
  10. package/dist/composables/useConfirmDialog.d.ts +10 -4
  11. package/dist/composables/useNow.d.ts +3 -0
  12. package/dist/composables/useTablePagination.d.ts +6 -0
  13. package/dist/composables/useTableSelection.d.ts +2 -2
  14. package/dist/composables/useToast.d.ts +1 -1
  15. package/dist/types/index.d.ts +1 -0
  16. package/dist/types/system-config.d.ts +34 -0
  17. package/dist/utils/admin-navigation.d.ts +66 -0
  18. package/dist/utils/admin-query.d.ts +29 -0
  19. package/dist/utils/batch-limit.d.ts +38 -0
  20. package/dist/utils/batch-result.d.ts +41 -0
  21. package/dist/utils/body-scroll-lock.d.ts +13 -0
  22. package/dist/utils/csv-export.d.ts +8 -0
  23. package/dist/utils/currency-cents.d.ts +16 -0
  24. package/dist/utils/datetime.d.ts +20 -0
  25. package/dist/utils/error-message.d.ts +7 -0
  26. package/dist/utils/format-metadata.d.ts +5 -0
  27. package/dist/utils/index.d.ts +12 -0
  28. package/dist/utils/otp-input.d.ts +11 -0
  29. package/dist/utils/system-config-engine.d.ts +65 -0
  30. 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
  31. 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
  32. package/package.json +30 -6
  33. package/recipes/AdminLayout.vue +73 -0
  34. package/recipes/README.md +130 -0
  35. package/recipes/i18n-merge.snippet.ts +29 -0
  36. package/recipes/list-page.snippet.ts +141 -0
  37. package/recipes/main-styles.snippet.ts +13 -0
  38. package/recipes/reexports/components.ts +19 -0
  39. package/recipes/reexports/composables.ts +29 -0
  40. package/recipes/reexports/utils.ts +31 -0
  41. package/recipes/router-guard.snippet.ts +32 -0
  42. package/recipes/useAdminAuth.cookie.ts +29 -0
  43. package/recipes/useAdminAuth.shop.ts +29 -0
  44. package/recipes/useAdminRequest.cookie.ts +36 -0
  45. package/recipes/useAdminRequest.ts +37 -0
  46. package/src/components/AdminLoginShell.vue +31 -0
  47. package/src/components/AdminMetadataDetail.vue +121 -0
  48. package/src/components/AdminShell.vue +396 -0
  49. package/src/components/ConfigField.vue +401 -0
  50. package/src/components/ConfirmDialog.vue +67 -3
  51. package/src/components/index.ts +6 -0
  52. package/src/composables/createAdminFlagSession.ts +129 -0
  53. package/src/composables/createAdminRequest.ts +322 -0
  54. package/src/composables/createAdminTokenSession.ts +149 -0
  55. package/src/composables/index.ts +51 -0
  56. package/src/composables/useAdminBatchOperation.ts +4 -0
  57. package/src/composables/useAdminListLoader.ts +124 -0
  58. package/src/composables/useAdminSidebar.ts +68 -0
  59. package/src/composables/useClipboard.ts +41 -2
  60. package/src/composables/useConfirmDialog.ts +26 -4
  61. package/src/composables/useNow.ts +38 -0
  62. package/src/composables/useTablePagination.ts +6 -0
  63. package/src/composables/useTableSelection.ts +2 -2
  64. package/src/composables/useToast.ts +4 -1
  65. package/src/env.d.ts +2 -2
  66. package/src/i18n/index.ts +8 -0
  67. package/src/i18n/kit-messages.ts +297 -0
  68. package/src/index.ts +143 -4
  69. package/src/styles/admin-primitives.css +23 -0
  70. package/src/styles/index.css +1 -0
  71. package/src/styles/login-primitives.css +149 -0
  72. package/src/types/index.ts +7 -0
  73. package/src/types/system-config.ts +38 -0
  74. package/src/utils/admin-navigation.ts +138 -0
  75. package/src/utils/admin-query.ts +51 -0
  76. package/src/utils/batch-limit.ts +66 -0
  77. package/src/utils/batch-result.ts +81 -0
  78. package/src/utils/body-scroll-lock.ts +63 -0
  79. package/src/utils/csv-export.ts +25 -0
  80. package/src/utils/currency-cents.ts +30 -0
  81. package/src/utils/datetime.ts +54 -0
  82. package/src/utils/error-message.ts +32 -0
  83. package/src/utils/format-metadata.ts +11 -0
  84. package/src/utils/index.ts +81 -0
  85. package/src/utils/otp-input.ts +18 -0
  86. package/src/utils/system-config-engine.ts +212 -0
  87. package/dist/components/index.d.ts +0 -4
  88. package/dist/index.d.ts +0 -17
package/src/index.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
- * @usethink/cf-admin-fe — Vue 3 admin foundation for CF product family.
2
+ * @usethink/cf-admin-fe — CF 产品族 Vue 3 管理端基础套件。
3
3
  *
4
- * Import composables from root or `@usethink/cf-admin-fe/composables`.
5
- * Import components from `@usethink/cf-admin-fe/components`.
6
- * Import styles: `import '@usethink/cf-admin-fe/styles'`.
4
+ * - 组合式:根入口或 `@usethink/cf-admin-fe/composables`
5
+ * - 组件:`@usethink/cf-admin-fe/components`
6
+ * - 工具:`@usethink/cf-admin-fe/utils`
7
+ * - 样式:`import '@usethink/cf-admin-fe/styles'`
7
8
  */
8
9
 
9
10
  export {
@@ -39,9 +40,147 @@ export {
39
40
  export {
40
41
  writeClipboardText,
41
42
  copyText,
43
+ copyWithToast,
44
+ type CopyWithToastOptions,
42
45
  } from './composables/useClipboard'
43
46
 
47
+ export {
48
+ createAdminTokenSession,
49
+ useAdminTokenSession,
50
+ parseStoredAdminToken,
51
+ readAdminTokenFromStorage,
52
+ DEFAULT_ADMIN_TOKEN_TTL_MS,
53
+ type StoredAdminToken,
54
+ type CreateAdminTokenSessionOptions,
55
+ type AdminTokenSession,
56
+ } from './composables/createAdminTokenSession'
57
+
58
+ export {
59
+ createAdminFlagSession,
60
+ readAdminFlagFromStorage,
61
+ writeAdminFlagToStorage,
62
+ DEFAULT_ADMIN_FLAG_TRUTHY,
63
+ type CreateAdminFlagSessionOptions,
64
+ type AdminFlagSession,
65
+ } from './composables/createAdminFlagSession'
66
+
67
+ export {
68
+ createAdminRequest,
69
+ createAdminRequestRaw,
70
+ AdminRequestError,
71
+ type CreateAdminRequestOptions,
72
+ type AdminRequestCallOptions,
73
+ type AdminRequestFn,
74
+ type AdminRequestRawFn,
75
+ type AdminRequestResultMode,
76
+ } from './composables/createAdminRequest'
77
+
78
+ export {
79
+ useAdminSidebar,
80
+ type UseAdminSidebarOptions,
81
+ type UseAdminSidebarReturn,
82
+ } from './composables/useAdminSidebar'
83
+
84
+ export {
85
+ useAdminListLoader,
86
+ type UseAdminListLoaderOptions,
87
+ type UseAdminListLoaderReturn,
88
+ type AdminListLoadContext,
89
+ type AdminListRunLoadOptions,
90
+ } from './composables/useAdminListLoader'
91
+
92
+ export {
93
+ useNow,
94
+ } from './composables/useNow'
95
+
96
+ export {
97
+ formatDate,
98
+ toDateTimeLocalValue,
99
+ dateTimeLocalToIso,
100
+ isoToDateTimeLocal,
101
+ formatIpFingerprint,
102
+ downloadCsv,
103
+ safeCsvCell,
104
+ lockBodyScroll,
105
+ unlockBodyScroll,
106
+ isBodyScrollLocked,
107
+ __resetBodyScrollLockForTests,
108
+ normalizeCents,
109
+ formatCents,
110
+ parseYuanToCents,
111
+ createAdminUnauthorizedHandler,
112
+ decideAdminRouteAccess,
113
+ resolveAdminLoginRedirect,
114
+ groupDefinitionsByName,
115
+ countSectionItems,
116
+ buildActiveGroupedDefinitions,
117
+ countAdvancedItems,
118
+ resolveSystemConfigSectionId,
119
+ getSystemConfigSection,
120
+ listedSystemConfigGroups,
121
+ auditSystemConfigSectionCoverage,
122
+ toErrorMessage,
123
+ hasBatchFailures,
124
+ resolveBatchToastType,
125
+ formatBatchCountsMessage,
126
+ buildBatchToast,
127
+ formatDefaultBatchMixedMessage,
128
+ DEFAULT_ADMIN_BATCH_LIMIT,
129
+ checkAdminBatchLimit,
130
+ formatMetadata,
131
+ buildAdminOffsetParams,
132
+ applyAdminOffsetParams,
133
+ } from './utils'
134
+
135
+ export {
136
+ normalizeOtpDigits,
137
+ isOtpComplete,
138
+ OTP_CODE_LENGTH,
139
+ } from './utils/otp-input'
140
+
141
+ export type {
142
+ CreateAdminUnauthorizedHandlerOptions,
143
+ AdminRouteAuthDecision,
144
+ DecideAdminRouteAccessOptions,
145
+ ResolveAdminLoginRedirectOptions,
146
+ SystemConfigDefinitionLike,
147
+ SystemConfigSectionLike,
148
+ SystemConfigGroupedBlock,
149
+ BuildActiveGroupedOptions,
150
+ SystemConfigCoverageAudit,
151
+ AdminBatchCounts,
152
+ AdminBatchToast,
153
+ AdminBatchLimitOk,
154
+ AdminBatchLimitExceeded,
155
+ AdminBatchLimitCheck,
156
+ CheckAdminBatchLimitOptions,
157
+ AdminOffsetParams,
158
+ } from './utils'
159
+
160
+ export {
161
+ adminKitMessages,
162
+ adminKitMessagesZhCN,
163
+ adminKitMessagesEn,
164
+ mergeAdminKitMessages,
165
+ } from './i18n'
166
+
167
+ export type { AdminKitLocale, AdminKitMessageTree } from './i18n'
168
+
169
+ export type {
170
+ AdminConfigFieldType,
171
+ AdminConfigFieldUnit,
172
+ AdminSystemConfigFieldDefinition,
173
+ AdminSystemConfigDefinition,
174
+ ConfigFieldStatus,
175
+ } from './types'
176
+
44
177
  export { default as AdminPagination } from './components/AdminPagination.vue'
45
178
  export { default as AdminModal } from './components/AdminModal.vue'
46
179
  export { default as ConfirmDialog } from './components/ConfirmDialog.vue'
47
180
  export { default as ToastContainer } from './components/ToastContainer.vue'
181
+ export { default as ConfigField } from './components/ConfigField.vue'
182
+ export { default as AdminShell } from './components/AdminShell.vue'
183
+ export { default as AdminLoginShell } from './components/AdminLoginShell.vue'
184
+ export { default as AdminMetadataDetail } from './components/AdminMetadataDetail.vue'
185
+ export type { AdminShellMenuItem } from './components/AdminShell.vue'
186
+ export type { AdminLogEntry } from './components/AdminMetadataDetail.vue'
@@ -2004,3 +2004,26 @@
2004
2004
  white-space: normal;
2005
2005
  }
2006
2006
  }
2007
+
2008
+ /* ── Header 语言切换(AdminShell #header-actions) ── */
2009
+ .lang-select {
2010
+ height: 28px;
2011
+ padding: 0 8px;
2012
+ border: 1px solid var(--border, rgba(255, 255, 255, 0.1));
2013
+ border-radius: var(--r-sm, 6px);
2014
+ background: var(--tg-secondary-bg, #151b28);
2015
+ color: var(--tg-text, #f0f2f5);
2016
+ font-size: 12px;
2017
+ color-scheme: dark;
2018
+ cursor: pointer;
2019
+ outline: none;
2020
+ }
2021
+
2022
+ .lang-select:focus-visible {
2023
+ border-color: var(--admin-accent, #f59e0b);
2024
+ }
2025
+
2026
+ .lang-select option {
2027
+ background: var(--tg-secondary-bg, #151b28);
2028
+ color: var(--tg-text, #f0f2f5);
2029
+ }
@@ -1,3 +1,4 @@
1
1
  /* @usethink/cf-admin-fe styles entry */
2
2
  @import './tokens.css';
3
3
  @import './admin-primitives.css';
4
+ @import './login-primitives.css';
@@ -0,0 +1,149 @@
1
+ /* ═══════════════════════════════════════════════════════
2
+ @usethink/cf-admin-fe — Admin login chrome primitives
3
+ Shared by shop (token) / lottery (password+OTP+OAuth) shells.
4
+ Product owns form fields and auth logic.
5
+ ═══════════════════════════════════════════════════════ */
6
+
7
+ .admin-login {
8
+ min-height: 100vh;
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ padding: 24px;
13
+ background: var(--tg-bg, #0a0e17);
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ .admin-login .login-card {
18
+ width: 100%;
19
+ max-width: 420px;
20
+ background: var(--tg-secondary-bg, #151b28);
21
+ border: 1px solid var(--border, rgba(255, 255, 255, 0.1));
22
+ border-radius: var(--r-lg, 12px);
23
+ padding: 26px 22px;
24
+ box-shadow: var(--shadow-lg, 0 20px 50px rgba(0, 0, 0, 0.45));
25
+ color: var(--tg-text, #f0f2f5);
26
+ box-sizing: border-box;
27
+ }
28
+
29
+ .admin-login .login-header {
30
+ margin-bottom: 0;
31
+ }
32
+
33
+ .admin-login .login-title {
34
+ margin: 0 0 6px;
35
+ font-size: 20px;
36
+ font-weight: 700;
37
+ text-align: center;
38
+ color: var(--tg-text, #f0f2f5);
39
+ }
40
+
41
+ .admin-login .login-subtitle {
42
+ margin: 0 0 18px;
43
+ font-size: 13px;
44
+ color: var(--tg-hint, #9aa4b2);
45
+ text-align: center;
46
+ }
47
+
48
+ .admin-login .login-form,
49
+ .admin-login .register-form {
50
+ display: flex;
51
+ flex-direction: column;
52
+ gap: 12px;
53
+ }
54
+
55
+ .admin-login .field-label {
56
+ display: flex;
57
+ flex-direction: column;
58
+ gap: 6px;
59
+ font-size: 13px;
60
+ color: var(--tg-text, #f0f2f5);
61
+ }
62
+
63
+ .admin-login .field-label input {
64
+ box-sizing: border-box;
65
+ width: 100%;
66
+ height: 40px;
67
+ padding: 0 12px;
68
+ border: 1px solid var(--border, rgba(255, 255, 255, 0.1));
69
+ border-radius: var(--r-md, 8px);
70
+ background: var(--surface, rgba(255, 255, 255, 0.04));
71
+ color: var(--tg-text, #f0f2f5);
72
+ font-size: 14px;
73
+ outline: none;
74
+ }
75
+
76
+ .admin-login .field-label input:focus {
77
+ border-color: var(--admin-accent, #f59e0b);
78
+ }
79
+
80
+ .admin-login .field-label input:disabled {
81
+ opacity: 0.65;
82
+ cursor: not-allowed;
83
+ }
84
+
85
+ .admin-login .login-btn {
86
+ margin-top: 4px;
87
+ height: 40px;
88
+ font-size: 14px;
89
+ }
90
+
91
+ .admin-login .login-error {
92
+ margin: 0;
93
+ font-size: 13px;
94
+ color: var(--admin-danger, #ef4444);
95
+ text-align: center;
96
+ }
97
+
98
+ .admin-login .login-footer {
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ gap: 8px;
103
+ margin-top: 16px;
104
+ font-size: 13px;
105
+ color: var(--tg-hint, #9aa4b2);
106
+ }
107
+
108
+ .admin-login .field-hint {
109
+ margin: 0;
110
+ font-size: 12px;
111
+ color: var(--tg-hint, #9aa4b2);
112
+ line-height: 1.4;
113
+ }
114
+
115
+ /* Optional OAuth / divider chrome (lottery-style) */
116
+ .admin-login .oauth-section {
117
+ margin-top: 16px;
118
+ }
119
+
120
+ .admin-login .oauth-section.oauth-primary {
121
+ margin: 0 0 20px;
122
+ }
123
+
124
+ .admin-login .oauth-btn {
125
+ width: 100%;
126
+ height: 42px;
127
+ font-size: 14px;
128
+ font-weight: 600;
129
+ }
130
+
131
+ .admin-login .oauth-divider {
132
+ display: flex;
133
+ align-items: center;
134
+ gap: 12px;
135
+ margin: 12px 0;
136
+ }
137
+
138
+ .admin-login .oauth-divider::before,
139
+ .admin-login .oauth-divider::after {
140
+ content: '';
141
+ flex: 1;
142
+ height: 1px;
143
+ background: var(--border, rgba(255, 255, 255, 0.1));
144
+ }
145
+
146
+ .admin-login .divider-text {
147
+ font-size: 12px;
148
+ color: var(--tg-hint, #9aa4b2);
149
+ }
@@ -0,0 +1,7 @@
1
+ export type {
2
+ AdminConfigFieldType,
3
+ AdminConfigFieldUnit,
4
+ AdminSystemConfigFieldDefinition,
5
+ AdminSystemConfigDefinition,
6
+ ConfigFieldStatus,
7
+ } from './system-config'
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 供 Admin ConfigField UI 共用的系统配置字段定义。
3
+ * 是 shop + lottery 结构的超集(enum / difficulty 可选)。
4
+ */
5
+ export type AdminConfigFieldType = 'string' | 'boolean' | 'integer'
6
+
7
+ export type AdminConfigFieldUnit = 'cents' | 'count'
8
+
9
+ export interface AdminSystemConfigFieldDefinition {
10
+ key: string
11
+ label: string
12
+ description?: string
13
+ type: AdminConfigFieldType
14
+ /** 整数存储单位:cents → 在 UI 中按主货币单位编辑 */
15
+ unit?: AdminConfigFieldUnit
16
+ sensitive?: boolean
17
+ configured?: boolean
18
+ defaultValue?: string
19
+ min?: number
20
+ max?: number
21
+ maxLength?: number
22
+ effect?: string
23
+ group?: string
24
+ order?: number
25
+ scope?: 'public' | 'admin'
26
+ difficulty?: 'basic' | 'advanced'
27
+ /** 多选枚举(以逗号分隔的字符串存储) */
28
+ enum?: string[]
29
+ enumLabels?: Record<string, string>
30
+ }
31
+
32
+ /**
33
+ * shop/lottery 历史使用的产品别名(`AdminSystemConfigDefinition`)。
34
+ * 在领域类型的再导出中优先使用此名称,便于无缝对齐。
35
+ */
36
+ export type AdminSystemConfigDefinition = AdminSystemConfigFieldDefinition
37
+
38
+ export type ConfigFieldStatus = 'idle' | 'saving' | 'saved' | 'error'
@@ -0,0 +1,138 @@
1
+ /**
2
+ * 供批量脚手架生成的 CF 管理端应用共用的导航辅助函数。
3
+ *
4
+ * 纯 / 可注入 — 不依赖 Vue Router。产品将决策映射到
5
+ * `router.push` / 命名路由,并注入产品特定的会话清理逻辑。
6
+ */
7
+
8
+ export type CreateAdminUnauthorizedHandlerOptions = {
9
+ /** 登录路径前缀;默认 `/admin/login` */
10
+ loginPath?: string
11
+ /** 当前地址为空时,编码进 `?redirect=` 的回退路径 */
12
+ defaultRedirectPath?: string
13
+ /** 产品侧清除 token / 完整业务会话 */
14
+ clearSession?: () => void
15
+ /**
16
+ * 覆盖 location(测试用)。默认在可用时使用 `window.location`。
17
+ * 存在 `assign` 时使用它,否则设置 `href`。
18
+ */
19
+ location?: {
20
+ pathname: string
21
+ search: string
22
+ hash: string
23
+ assign?: (url: string) => void
24
+ href?: string
25
+ }
26
+ }
27
+
28
+ /**
29
+ * 构建 shop + lottery 使用的标准 401 副作用:
30
+ * 清除会话 → 带 `redirect` 查询参数硬跳转到登录页 → 已在登录页则跳过。
31
+ */
32
+ export function createAdminUnauthorizedHandler(
33
+ options: CreateAdminUnauthorizedHandlerOptions = {},
34
+ ): () => void {
35
+ const loginPath = options.loginPath ?? '/admin/login'
36
+ const defaultRedirectPath = options.defaultRedirectPath ?? '/admin'
37
+
38
+ return function handleUnauthorized(): void {
39
+ try {
40
+ options.clearSession?.()
41
+ } catch {
42
+ /* 隐私模式 / 存储失败 — 仍尝试跳转 */
43
+ }
44
+
45
+ const loc =
46
+ options.location ??
47
+ (typeof window !== 'undefined'
48
+ ? {
49
+ pathname: window.location.pathname,
50
+ search: window.location.search,
51
+ hash: window.location.hash,
52
+ assign: (url: string) => window.location.assign(url),
53
+ }
54
+ : null)
55
+
56
+ if (!loc) return
57
+
58
+ try {
59
+ if (loc.pathname === loginPath || loc.pathname.startsWith(`${loginPath}/`)) {
60
+ return
61
+ }
62
+ const current = `${loc.pathname}${loc.search}${loc.hash}`
63
+ const target = `${loginPath}?redirect=${encodeURIComponent(current || defaultRedirectPath)}`
64
+ if (typeof loc.assign === 'function') {
65
+ loc.assign(target)
66
+ } else if (loc.href !== undefined) {
67
+ loc.href = target
68
+ }
69
+ } catch {
70
+ /* 忽略导航失败;调用方仍会抛出 AdminRequestError */
71
+ }
72
+ }
73
+ }
74
+
75
+ export type AdminRouteAuthDecision =
76
+ | { type: 'allow' }
77
+ | { type: 'login'; redirect: string }
78
+ | { type: 'home' }
79
+
80
+ export type DecideAdminRouteAccessOptions = {
81
+ hasToken: boolean
82
+ requiresAuth?: boolean
83
+ requiresGuest?: boolean
84
+ /** 登录跳转查询参数使用的当前完整路径(如 `to.fullPath`) */
85
+ fullPath: string
86
+ }
87
+
88
+ /**
89
+ * shop-simple 和 lottery 共用的核心 Bearer 管理员路由守卫(在
90
+ * 产品专属的引导 / 平台 / 模拟身份规则之前执行)。
91
+ */
92
+ export function decideAdminRouteAccess(
93
+ options: DecideAdminRouteAccessOptions,
94
+ ): AdminRouteAuthDecision {
95
+ if (options.requiresAuth && !options.hasToken) {
96
+ return { type: 'login', redirect: options.fullPath }
97
+ }
98
+ if (options.requiresGuest && options.hasToken) {
99
+ return { type: 'home' }
100
+ }
101
+ return { type: 'allow' }
102
+ }
103
+
104
+ export type ResolveAdminLoginRedirectOptions = {
105
+ /** 登录后的默认落地页;默认 `/admin` */
106
+ defaultPath?: string
107
+ /**
108
+ * 仅允许此前缀下的跳转(开放重定向防护)。
109
+ * 默认 `/admin`。平台应用可传入 `/platform` 或自定义校验器。
110
+ */
111
+ allowPrefix?: string
112
+ /** 额外的白名单谓词(如 lottery 平台路径) */
113
+ isAllowed?: (path: string) => boolean
114
+ }
115
+
116
+ /**
117
+ * 从 `route.query.redirect` 解析登录后跳转目标。
118
+ * 拒绝协议相对路径和前缀之外的路径。
119
+ */
120
+ export function resolveAdminLoginRedirect(
121
+ redirectQuery: unknown,
122
+ options: ResolveAdminLoginRedirectOptions = {},
123
+ ): string {
124
+ const defaultPath = options.defaultPath ?? '/admin'
125
+ const allowPrefix = options.allowPrefix ?? '/admin'
126
+
127
+ if (typeof redirectQuery !== 'string' || !redirectQuery) return defaultPath
128
+ if (redirectQuery.startsWith('//') || /^[a-z][a-z0-9+.-]*:/i.test(redirectQuery)) {
129
+ return defaultPath
130
+ }
131
+ if (options.isAllowed) {
132
+ return options.isAllowed(redirectQuery) ? redirectQuery : defaultPath
133
+ }
134
+ if (redirectQuery === allowPrefix || redirectQuery.startsWith(`${allowPrefix}/`) || redirectQuery.startsWith(`${allowPrefix}?`)) {
135
+ return redirectQuery
136
+ }
137
+ return defaultPath
138
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 管理端列表常用的 offset 分页查询字段(**字符串**形态)。
3
+ *
4
+ * 适用:URLSearchParams / query string(如 lottery 平台审计、租户、webhook、兑换码)。
5
+ * 不适用:类型化 filter / JSON body 需要 **number** 的 limit/offset(如 shop 余额/充值/代金券)—
6
+ * 那些场景请产品手写 `offset: (page - 1) * limit`,不要把本函数的 string 结果硬塞进 number 字段。
7
+ *
8
+ * 过滤器键名因产品而异,只抽取这两项稳定字段。
9
+ * 完整对照见 docs/002 §5.8。
10
+ */
11
+
12
+ export type AdminOffsetParams = {
13
+ limit: string
14
+ offset: string
15
+ }
16
+
17
+ /**
18
+ * 由 1-based 页码与每页条数生成 **字符串化** 的 limit/offset(便于 URLSearchParams)。
19
+ * 非法页码回退为 1;非法 limit 回退为 20。
20
+ *
21
+ * @returns `{ limit: string, offset: string }` — 不是 number
22
+ */
23
+ export function buildAdminOffsetParams(
24
+ page: number,
25
+ limit: number,
26
+ options?: { fallbackLimit?: number },
27
+ ): AdminOffsetParams {
28
+ const fallbackLimit = options?.fallbackLimit ?? 20
29
+ const safePage = Number.isFinite(page) && page > 0 ? Math.floor(page) : 1
30
+ const safeLimit =
31
+ Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : fallbackLimit
32
+ return {
33
+ limit: String(safeLimit),
34
+ offset: String((safePage - 1) * safeLimit),
35
+ }
36
+ }
37
+
38
+ /**
39
+ * 将 limit/offset 写入已有 URLSearchParams(会覆盖同名键)。
40
+ */
41
+ export function applyAdminOffsetParams(
42
+ params: URLSearchParams,
43
+ page: number,
44
+ limit: number,
45
+ options?: { fallbackLimit?: number },
46
+ ): URLSearchParams {
47
+ const built = buildAdminOffsetParams(page, limit, options)
48
+ params.set('limit', built.limit)
49
+ params.set('offset', built.offset)
50
+ return params
51
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 在调用服务端批量接口前,守卫管理端的多选数量。
3
+ *
4
+ * 默认上限 {@link DEFAULT_ADMIN_BATCH_LIMIT}(20)对齐 **cf-auth** / docs 风格 API。
5
+ * 后端若为 `z.array(...).max(N)` 且 N≠20(例如 cf-shop 订单/卡密/日志批量 **200**),
6
+ * 必须传入 `options.limit: N`,否则会在未达后端上限前误拦,或(反过来)默认过宽时漏拦。
7
+ *
8
+ * 单位词 `unit` 只影响默认中文提示;完整文案可用 `message` 覆盖。
9
+ */
10
+
11
+ export const DEFAULT_ADMIN_BATCH_LIMIT = 20
12
+
13
+ export type AdminBatchLimitOk = {
14
+ ok: true
15
+ count: number
16
+ limit: number
17
+ }
18
+
19
+ export type AdminBatchLimitExceeded = {
20
+ ok: false
21
+ count: number
22
+ limit: number
23
+ message: string
24
+ }
25
+
26
+ export type AdminBatchLimitCheck = AdminBatchLimitOk | AdminBatchLimitExceeded
27
+
28
+ export type CheckAdminBatchLimitOptions = {
29
+ /** 每次请求的最大条数;默认 {@link DEFAULT_ADMIN_BATCH_LIMIT} */
30
+ limit?: number
31
+ /**
32
+ * 默认中文消息中的单位词,如 `人` / `条` / `个`。
33
+ * 设置了 `message` 时忽略。
34
+ */
35
+ unit?: string
36
+ /** 超限时的完整覆盖消息 */
37
+ message?: string
38
+ }
39
+
40
+ function defaultOverLimitMessage(limit: number, unit?: string): string {
41
+ if (unit) return `单次最多 ${limit} ${unit},请缩小选择`
42
+ return `单次最多 ${limit} 条,请缩小选择`
43
+ }
44
+
45
+ /**
46
+ * @param selection - 选中的 id 列表,或预先计算好的数量
47
+ * @param options.limit - 单次最大条数;省略则用 {@link DEFAULT_ADMIN_BATCH_LIMIT}(20)
48
+ */
49
+ export function checkAdminBatchLimit(
50
+ selection: readonly unknown[] | number,
51
+ options: CheckAdminBatchLimitOptions = {},
52
+ ): AdminBatchLimitCheck {
53
+ const count = typeof selection === 'number' ? selection : selection.length
54
+ const limit = options.limit ?? DEFAULT_ADMIN_BATCH_LIMIT
55
+
56
+ if (count <= limit) {
57
+ return { ok: true, count, limit }
58
+ }
59
+
60
+ return {
61
+ ok: false,
62
+ count,
63
+ limit,
64
+ message: options.message ?? defaultOverLimitMessage(limit, options.unit),
65
+ }
66
+ }