@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.
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
@@ -0,0 +1,81 @@
1
+ /**
2
+ * 供管理端批量操作 toast / 汇总文案使用的纯辅助函数。
3
+ *
4
+ * 各产品在 i18n 键和成功文案上存在差异;kit 只负责判定
5
+ * 成功/失败基调,以及可选的 `{success}` / `{failed}` / `{count}` 填充。
6
+ */
7
+
8
+ export type AdminBatchCounts = {
9
+ success: number
10
+ failed: number
11
+ }
12
+
13
+ export type AdminBatchToast = {
14
+ message: string
15
+ type: 'success' | 'error'
16
+ }
17
+
18
+ export function hasBatchFailures(result: AdminBatchCounts): boolean {
19
+ return (result.failed ?? 0) > 0
20
+ }
21
+
22
+ export function resolveBatchToastType(result: AdminBatchCounts): 'success' | 'error' {
23
+ return hasBatchFailures(result) ? 'error' : 'success'
24
+ }
25
+
26
+ /**
27
+ * 在模板中替换 `{success}`、`{failed}` 和 `{count}`(success 的别名)。
28
+ * 不依赖 vue-i18n — 产品可传入已翻译好的模板。
29
+ */
30
+ export function formatBatchCountsMessage(
31
+ template: string,
32
+ result: AdminBatchCounts,
33
+ ): string {
34
+ const success = String(result.success ?? 0)
35
+ const failed = String(result.failed ?? 0)
36
+ // 使用 split/join 使 TS lib 目标保持在 ES2021 以下(产品 vue-tsc 消费方)。
37
+ return template
38
+ .split('{success}')
39
+ .join(success)
40
+ .split('{failed}')
41
+ .join(failed)
42
+ .split('{count}')
43
+ .join(success)
44
+ }
45
+
46
+ /**
47
+ * 从混合/成功模板构建 toast 载荷。
48
+ *
49
+ * @example
50
+ * buildBatchToast(res, {
51
+ * mixed: '完成:成功 {success},失败 {failed}',
52
+ * success: '已处理 {count} 条',
53
+ * })
54
+ */
55
+ export function buildBatchToast(
56
+ result: AdminBatchCounts,
57
+ templates: {
58
+ /** 当存在任一失败项时 */
59
+ mixed: string
60
+ /** 当失败数 === 0 时 */
61
+ success: string
62
+ },
63
+ ): AdminBatchToast {
64
+ const type = resolveBatchToastType(result)
65
+ const message = formatBatchCountsMessage(
66
+ type === 'error' ? templates.mixed : templates.success,
67
+ result,
68
+ )
69
+ return { message, type }
70
+ }
71
+
72
+ /**
73
+ * cf-auth 管理端页面大量使用的默认中文混合汇总文案。
74
+ * 有产品 i18n 时优先使用;这是零依赖的回退方案。
75
+ */
76
+ export function formatDefaultBatchMixedMessage(
77
+ result: AdminBatchCounts,
78
+ prefix = '完成',
79
+ ): string {
80
+ return formatBatchCountsMessage(`${prefix}:成功 {success},失败 {failed}`, result)
81
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * 供叠加弹层(确认框 → 弹窗交接)共用的 body 滚动锁定。
3
+ * - 引用计数,保证嵌套层级不会让页面闪烁解锁
4
+ * - 用 padding-right 补偿滚动条宽度,避免布局跳动
5
+ *
6
+ * 仅修改 document.body 的内联样式;无 document 环境(SSR/测试)下为空操作。
7
+ */
8
+
9
+ let lockCount = 0
10
+ let previousOverflow = ''
11
+ let previousPaddingRight = ''
12
+
13
+ function scrollbarWidthPx(): number {
14
+ if (typeof window === 'undefined' || typeof document === 'undefined') return 0
15
+ return Math.max(0, window.innerWidth - document.documentElement.clientWidth)
16
+ }
17
+
18
+ /** 当前模块是否持有 body 滚动锁(测试 / 调试用)。 */
19
+ export function isBodyScrollLocked(): boolean {
20
+ return lockCount > 0
21
+ }
22
+
23
+ /** 仅供测试使用的重置 — 请勿在生产路径中调用。 */
24
+ export function __resetBodyScrollLockForTests(): void {
25
+ lockCount = 0
26
+ previousOverflow = ''
27
+ previousPaddingRight = ''
28
+ if (typeof document !== 'undefined') {
29
+ document.body.style.overflow = ''
30
+ document.body.style.paddingRight = ''
31
+ }
32
+ }
33
+
34
+ export function lockBodyScroll(): void {
35
+ if (typeof document === 'undefined') return
36
+ lockCount += 1
37
+ if (lockCount !== 1) return
38
+
39
+ previousOverflow = document.body.style.overflow
40
+ previousPaddingRight = document.body.style.paddingRight
41
+
42
+ const gap = scrollbarWidthPx()
43
+ document.body.style.overflow = 'hidden'
44
+ if (gap > 0) {
45
+ const currentPad = Number.parseFloat(previousPaddingRight || '0') || 0
46
+ document.body.style.paddingRight = `${currentPad + gap}px`
47
+ }
48
+ }
49
+
50
+ export function unlockBodyScroll(): void {
51
+ if (typeof document === 'undefined') return
52
+ if (lockCount <= 0) {
53
+ lockCount = 0
54
+ return
55
+ }
56
+ lockCount -= 1
57
+ if (lockCount !== 0) return
58
+
59
+ document.body.style.overflow = previousOverflow
60
+ document.body.style.paddingRight = previousPaddingRight
61
+ previousOverflow = ''
62
+ previousPaddingRight = ''
63
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * CSV 下载,针对电子表格客户端做了公式注入防护。
3
+ * 语义与后端服务端导出 CSV 的产品实现保持一致。
4
+ */
5
+
6
+ /** 转义单个单元格:加引号 + 内部双引号加倍;中和开头的公式字符。 */
7
+ export function safeCsvCell(value: unknown): string {
8
+ const text = String(value ?? '')
9
+ const escaped = text.replace(/"/g, '""')
10
+ // Excel/Sheets 将 = + - @(以及 tab/换行)视为公式起始字符。
11
+ return /^[=+\-@\t\n]/.test(escaped) ? `"\t${escaped}"` : `"${escaped}"`
12
+ }
13
+
14
+ /** 通过临时 object URL 下载 UTF-8 BOM 的 CSV。 */
15
+ export function downloadCsv(filename: string, rows: string[][]): void {
16
+ if (typeof document === 'undefined') return
17
+ const content = `\uFEFF${rows.map((row) => row.map(safeCsvCell).join(',')).join('\n')}`
18
+ const blob = new Blob([content], { type: 'text/csv;charset=utf-8' })
19
+ const url = URL.createObjectURL(blob)
20
+ const anchor = document.createElement('a')
21
+ anchor.href = url
22
+ anchor.download = filename
23
+ anchor.click()
24
+ URL.revokeObjectURL(url)
25
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * 供管理表单使用的纯最小单位(分)辅助函数。
3
+ * 不依赖产品资金库 — 人民币风格 2 位小数主单位。
4
+ * 使用多币种的产品应保留 `@shared/money`,仅对 ConfigField 风格的整数分字段
5
+ * 重新导出或包装这些函数。
6
+ */
7
+
8
+ /** 将未知值强制转换为安全的整数分;否则返回 0。 */
9
+ export function normalizeCents(value: unknown): number {
10
+ const cents = Number(value)
11
+ return Number.isSafeInteger(cents) ? cents : 0
12
+ }
13
+
14
+ /** 分 → 保留 2 位小数的主单位字符串(`123` → `"1.23"`)。 */
15
+ export function formatCents(cents: number | unknown): string {
16
+ if (!Number.isFinite(Number(cents))) return '0.00'
17
+ return (normalizeCents(cents) / 100).toFixed(2)
18
+ }
19
+
20
+ /**
21
+ * 主单位字符串 → 整数分。
22
+ * 空 / 非数字 → `null`,让调用方可以拒绝非法输入
23
+ * (避免意外把 `"50"` 元写成 50 分)。
24
+ */
25
+ export function parseYuanToCents(yuan: string): number | null {
26
+ if (!yuan.trim()) return null
27
+ const num = Number.parseFloat(yuan)
28
+ if (!Number.isFinite(num)) return null
29
+ return Math.round(num * 100)
30
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * 供 CF 产品管理员共用的日期时间辅助函数。
3
+ * 纯函数 — 不依赖 Vue,不含领域标签。
4
+ */
5
+
6
+ function pad(num: number): string {
7
+ return String(num).padStart(2, '0')
8
+ }
9
+
10
+ /**
11
+ * ISO 字符串 / Date → 本地 `YYYY-MM-DD HH:mm:ss`;空值 → `-`。
12
+ * 接受 `Date` 对象,使 cookie/JSON 管理表格无需产品包装层(cf-auth)。
13
+ */
14
+ export function formatDate(value?: string | Date | null): string {
15
+ if (value == null || value === '') return '-'
16
+ if (value instanceof Date) {
17
+ if (Number.isNaN(value.getTime())) return '-'
18
+ return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())} ${pad(value.getHours())}:${pad(value.getMinutes())}:${pad(value.getSeconds())}`
19
+ }
20
+ const date = new Date(value)
21
+ if (Number.isNaN(date.getTime())) return value
22
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
23
+ }
24
+
25
+ /** 后端时间 → `datetime-local` 输入值(本地挂钟时间,不含秒) */
26
+ export function toDateTimeLocalValue(value?: string | null): string {
27
+ if (!value) return ''
28
+ const date = new Date(value)
29
+ if (Number.isNaN(date.getTime())) return value.slice(0, 16)
30
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`
31
+ }
32
+
33
+ /** `datetime-local` 值 → ISO 字符串(跨浏览器稳定) */
34
+ export function dateTimeLocalToIso(value?: string | null): string {
35
+ if (!value) return ''
36
+ const date = new Date(value)
37
+ return Number.isNaN(date.getTime()) ? value : date.toISOString()
38
+ }
39
+
40
+ /** {@link toDateTimeLocalValue} 的别名,用于编辑表单回填 */
41
+ export function isoToDateTimeLocal(iso?: string | null): string {
42
+ return toDateTimeLocalValue(iso)
43
+ }
44
+
45
+ /**
46
+ * 对长不透明字符串(如哈希后的客户端 IP)生成短指纹。
47
+ * 完整值保留用于详情/导出;列表展示缩写形式。
48
+ */
49
+ export function formatIpFingerprint(value?: string | null, maxPlain = 18): string {
50
+ const text = value?.trim()
51
+ if (!text) return '-'
52
+ if (text.length <= maxPlain) return text
53
+ return `${text.slice(0, 10)}...${text.slice(-6)}`
54
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 将未知的 catch 值规范化为管理端 toast / loading 展示用的字符串。
3
+ *
4
+ * 供 cf-shop / cf-lottery / cf-auth 管理端 UI 共用,它们反复编写:
5
+ * `err instanceof Error ? err.message : fallback`。
6
+ */
7
+
8
+ export function toErrorMessage(error: unknown, fallback = ''): string {
9
+ if (error instanceof Error) {
10
+ const message = error.message.trim()
11
+ return message || fallback
12
+ }
13
+
14
+ if (typeof error === 'string') {
15
+ const message = error.trim()
16
+ return message || fallback
17
+ }
18
+
19
+ if (error != null && typeof error === 'object' && 'message' in error) {
20
+ const raw = (error as { message: unknown }).message
21
+ if (typeof raw === 'string') {
22
+ const message = raw.trim()
23
+ if (message) return message
24
+ }
25
+ }
26
+
27
+ if (fallback) return fallback
28
+ if (error == null) return ''
29
+
30
+ const asString = String(error).trim()
31
+ return asString || fallback
32
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 格式化元数据为可读的 JSON 字符串。
3
+ * 用于日志详情等场景展示结构化数据。
4
+ */
5
+ export function formatMetadata(metadata: Record<string, unknown>): string {
6
+ try {
7
+ return JSON.stringify(metadata, null, 2)
8
+ } catch {
9
+ return String(metadata)
10
+ }
11
+ }
@@ -0,0 +1,81 @@
1
+ export {
2
+ formatDate,
3
+ toDateTimeLocalValue,
4
+ dateTimeLocalToIso,
5
+ isoToDateTimeLocal,
6
+ formatIpFingerprint,
7
+ } from './datetime'
8
+
9
+ export { downloadCsv, safeCsvCell } from './csv-export'
10
+
11
+ export {
12
+ lockBodyScroll,
13
+ unlockBodyScroll,
14
+ isBodyScrollLocked,
15
+ __resetBodyScrollLockForTests,
16
+ } from './body-scroll-lock'
17
+
18
+ export { normalizeCents, formatCents, parseYuanToCents } from './currency-cents'
19
+
20
+ export {
21
+ createAdminUnauthorizedHandler,
22
+ decideAdminRouteAccess,
23
+ resolveAdminLoginRedirect,
24
+ type CreateAdminUnauthorizedHandlerOptions,
25
+ type AdminRouteAuthDecision,
26
+ type DecideAdminRouteAccessOptions,
27
+ type ResolveAdminLoginRedirectOptions,
28
+ } from './admin-navigation'
29
+
30
+ export {
31
+ groupDefinitionsByName,
32
+ countSectionItems,
33
+ buildActiveGroupedDefinitions,
34
+ countAdvancedItems,
35
+ resolveSystemConfigSectionId,
36
+ getSystemConfigSection,
37
+ listedSystemConfigGroups,
38
+ auditSystemConfigSectionCoverage,
39
+ type SystemConfigDefinitionLike,
40
+ type SystemConfigSectionLike,
41
+ type SystemConfigGroupedBlock,
42
+ type BuildActiveGroupedOptions,
43
+ type SystemConfigCoverageAudit,
44
+ } from './system-config-engine'
45
+
46
+ export {
47
+ normalizeOtpDigits,
48
+ isOtpComplete,
49
+ OTP_CODE_LENGTH,
50
+ } from './otp-input'
51
+
52
+ export { toErrorMessage } from './error-message'
53
+
54
+ export {
55
+ hasBatchFailures,
56
+ resolveBatchToastType,
57
+ formatBatchCountsMessage,
58
+ buildBatchToast,
59
+ formatDefaultBatchMixedMessage,
60
+ type AdminBatchCounts,
61
+ type AdminBatchToast,
62
+ } from './batch-result'
63
+
64
+ export {
65
+ DEFAULT_ADMIN_BATCH_LIMIT,
66
+ checkAdminBatchLimit,
67
+ type AdminBatchLimitOk,
68
+ type AdminBatchLimitExceeded,
69
+ type AdminBatchLimitCheck,
70
+ type CheckAdminBatchLimitOptions,
71
+ } from './batch-limit'
72
+
73
+ export {
74
+ formatMetadata,
75
+ } from './format-metadata'
76
+
77
+ export {
78
+ buildAdminOffsetParams,
79
+ applyAdminOffsetParams,
80
+ type AdminOffsetParams,
81
+ } from './admin-query'
@@ -0,0 +1,18 @@
1
+ /**
2
+ * OTP 输入规范化(纯函数)。
3
+ *
4
+ * - 只保留数字,最长 6 位
5
+ * - 便于 SMS/邮件自动填充后触发「满 6 位自动提交」
6
+ */
7
+
8
+ export const OTP_CODE_LENGTH = 6
9
+
10
+ /** 去掉非数字并截断到 OTP 长度 */
11
+ export function normalizeOtpDigits(raw: string, maxLen = OTP_CODE_LENGTH): string {
12
+ return String(raw || '').replace(/\D/g, '').slice(0, maxLen)
13
+ }
14
+
15
+ /** 是否已满位、可触发自动提交 */
16
+ export function isOtpComplete(digits: string, maxLen = OTP_CODE_LENGTH): boolean {
17
+ return digits.length === maxLen
18
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * shop/lottery AdminSystemConfigView 共用的纯系统配置页面引擎。
3
+ *
4
+ * 产品负责:分区 id 联合、分组名列表、危险区 UI、API 加载/保存。
5
+ * 套件负责:分组、分区计数、高级筛选、tab 解析、覆盖率审计。
6
+ */
7
+
8
+ /** 引擎使用的最小定义结构(ConfigField 项满足此结构)。 */
9
+ export type SystemConfigDefinitionLike = {
10
+ key: string
11
+ group?: string
12
+ order?: number
13
+ difficulty?: 'basic' | 'advanced' | string
14
+ }
15
+
16
+ /** 最小分区结构 — 产品负责填充标签/描述。 */
17
+ export type SystemConfigSectionLike = {
18
+ id: string
19
+ groups: readonly string[]
20
+ includeDangerZone?: boolean
21
+ }
22
+
23
+ export type SystemConfigGroupedBlock<T> = {
24
+ name: string
25
+ items: T[]
26
+ }
27
+
28
+ /**
29
+ * 按 `group` 对定义分组(为空时使用回退名称),并按 `order` 对每组排序。
30
+ */
31
+ export function groupDefinitionsByName<T extends SystemConfigDefinitionLike>(
32
+ definitions: readonly T[],
33
+ fallbackGroupName = '其他',
34
+ ): Map<string, T[]> {
35
+ const map = new Map<string, T[]>()
36
+ for (const item of definitions) {
37
+ const name = item.group || fallbackGroupName
38
+ const bucket = map.get(name)
39
+ if (bucket) bucket.push(item)
40
+ else map.set(name, [item])
41
+ }
42
+ for (const items of map.values()) {
43
+ items.sort((a, b) => (a.order ?? 999) - (b.order ?? 999))
44
+ }
45
+ return map
46
+ }
47
+
48
+ /**
49
+ * 按分区 id 统计定义项数量(所列分组长度之和)。
50
+ */
51
+ export function countSectionItems(
52
+ groupsByName: Map<string, readonly unknown[]>,
53
+ sections: readonly SystemConfigSectionLike[],
54
+ ): Record<string, number> {
55
+ const counts: Record<string, number> = {}
56
+ for (const section of sections) {
57
+ let n = 0
58
+ for (const group of section.groups) {
59
+ n += groupsByName.get(group)?.length ?? 0
60
+ }
61
+ counts[section.id] = n
62
+ }
63
+ return counts
64
+ }
65
+
66
+ export type BuildActiveGroupedOptions = {
67
+ /**
68
+ * 为 false 时,隐藏 `difficulty === 'advanced'` 的项(lottery 基础模式)。
69
+ * 默认 true = 显示全部(shop)。
70
+ */
71
+ showAdvanced?: boolean
72
+ }
73
+
74
+ /**
75
+ * 为当前激活分区构建有序分组块;丢弃空分组。
76
+ */
77
+ export function buildActiveGroupedDefinitions<T extends SystemConfigDefinitionLike>(
78
+ sectionGroups: readonly string[],
79
+ groupsByName: Map<string, readonly T[]>,
80
+ options: BuildActiveGroupedOptions = {},
81
+ ): SystemConfigGroupedBlock<T>[] {
82
+ const showAdvanced = options.showAdvanced !== false
83
+ return sectionGroups
84
+ .map((name) => {
85
+ const raw = groupsByName.get(name) ?? []
86
+ const items = showAdvanced
87
+ ? [...raw]
88
+ : raw.filter((item) => item.difficulty !== 'advanced')
89
+ return { name, items }
90
+ })
91
+ .filter((group) => group.items.length > 0)
92
+ }
93
+
94
+ /** 统计给定分区分组中高级难度的项数。 */
95
+ export function countAdvancedItems<T extends SystemConfigDefinitionLike>(
96
+ sectionGroups: readonly string[],
97
+ groupsByName: Map<string, readonly T[]>,
98
+ ): number {
99
+ let count = 0
100
+ for (const name of sectionGroups) {
101
+ const items = groupsByName.get(name) ?? []
102
+ count += items.filter((item) => item.difficulty === 'advanced').length
103
+ }
104
+ return count
105
+ }
106
+
107
+ /**
108
+ * 将 `?tab=` 解析为已知分区 id;无效/为空 → defaultId(绝不凭空编造 id)。
109
+ */
110
+ export function resolveSystemConfigSectionId<T extends string>(
111
+ raw: unknown,
112
+ validIds: readonly T[],
113
+ defaultId: T,
114
+ ): T {
115
+ if (typeof raw !== 'string') return defaultId
116
+ const id = raw.trim()
117
+ if ((validIds as readonly string[]).includes(id)) return id as T
118
+ return defaultId
119
+ }
120
+
121
+ export function getSystemConfigSection<T extends SystemConfigSectionLike>(
122
+ sections: readonly T[],
123
+ id: string,
124
+ ): T {
125
+ const found = sections.find((s) => s.id === id)
126
+ return found ?? sections[0]!
127
+ }
128
+
129
+ /** 按声明顺序展平 section.groups,并去重。 */
130
+ export function listedSystemConfigGroups(
131
+ sections: readonly SystemConfigSectionLike[],
132
+ ): string[] {
133
+ const seen = new Set<string>()
134
+ const out: string[] = []
135
+ for (const section of sections) {
136
+ for (const group of section.groups) {
137
+ if (seen.has(group)) continue
138
+ seen.add(group)
139
+ out.push(group)
140
+ }
141
+ }
142
+ return out
143
+ }
144
+
145
+ export type SystemConfigCoverageAudit = {
146
+ ok: boolean
147
+ unmappedGroups: string[]
148
+ unmappedKeys: string[]
149
+ duplicateGroups: string[]
150
+ extraGroupsInMap: string[]
151
+ sectionKeyCounts: Record<string, number>
152
+ totalKeys: number
153
+ }
154
+
155
+ /**
156
+ * 校验分区映射 ↔ 定义分组覆盖情况(单元/契约测试)。
157
+ * `fallbackGroupName` 必须与 UI 对空 `group` 字段的分组方式一致。
158
+ */
159
+ export function auditSystemConfigSectionCoverage(
160
+ definitions: readonly SystemConfigDefinitionLike[],
161
+ sections: readonly SystemConfigSectionLike[],
162
+ fallbackGroupName = '其他',
163
+ ): SystemConfigCoverageAudit {
164
+ const defGroups = new Set<string>()
165
+ for (const d of definitions) {
166
+ defGroups.add(d.group || fallbackGroupName)
167
+ }
168
+
169
+ const groupToSection = new Map<string, string>()
170
+ const duplicateGroups: string[] = []
171
+ for (const section of sections) {
172
+ for (const group of section.groups) {
173
+ if (groupToSection.has(group)) {
174
+ duplicateGroups.push(group)
175
+ continue
176
+ }
177
+ groupToSection.set(group, section.id)
178
+ }
179
+ }
180
+
181
+ const unmappedGroups = [...defGroups].filter((g) => !groupToSection.has(g)).sort()
182
+ const extraGroupsInMap = [...groupToSection.keys()].filter((g) => !defGroups.has(g)).sort()
183
+
184
+ const unmappedKeys: string[] = []
185
+ const sectionKeyCounts: Record<string, number> = {}
186
+ for (const section of sections) {
187
+ sectionKeyCounts[section.id] = 0
188
+ }
189
+ for (const d of definitions) {
190
+ const g = d.group || fallbackGroupName
191
+ const sectionId = groupToSection.get(g)
192
+ if (!sectionId) {
193
+ unmappedKeys.push(d.key)
194
+ continue
195
+ }
196
+ sectionKeyCounts[sectionId] = (sectionKeyCounts[sectionId] ?? 0) + 1
197
+ }
198
+
199
+ return {
200
+ ok:
201
+ unmappedGroups.length === 0
202
+ && unmappedKeys.length === 0
203
+ && duplicateGroups.length === 0
204
+ && extraGroupsInMap.length === 0,
205
+ unmappedGroups,
206
+ unmappedKeys,
207
+ duplicateGroups,
208
+ extraGroupsInMap,
209
+ sectionKeyCounts,
210
+ totalKeys: definitions.length,
211
+ }
212
+ }
@@ -1,4 +0,0 @@
1
- export { default as AdminPagination } from './AdminPagination.vue';
2
- export { default as AdminModal } from './AdminModal.vue';
3
- export { default as ConfirmDialog } from './ConfirmDialog.vue';
4
- export { default as ToastContainer } from './ToastContainer.vue';
package/dist/index.d.ts DELETED
@@ -1,17 +0,0 @@
1
- /**
2
- * @usethink/cf-admin — Vue 3 admin foundation for CF product family.
3
- *
4
- * Import composables from root or `@usethink/cf-admin/composables`.
5
- * Import components from `@usethink/cf-admin/components`.
6
- * Import styles: `import '@usethink/cf-admin/styles'`.
7
- */
8
- export { useTablePagination, type UseTablePaginationOptions, type UseTablePaginationReturn, } from './composables/useTablePagination.ts';
9
- export { useTableSelection, type UseTableSelectionOptions, } from './composables/useTableSelection.ts';
10
- export { useConfirmDialog, type ConfirmOptionDef, type ConfirmAskOptions, type ConfirmResult, } from './composables/useConfirmDialog.ts';
11
- export { useToast, MAX_TOASTS, type ToastType, type ToastItem, } from './composables/useToast.ts';
12
- export { useAdminBatchOperation, type AdminBatchOperationResult, } from './composables/useAdminBatchOperation.ts';
13
- export { writeClipboardText, copyText, } from './composables/useClipboard.ts';
14
- export { default as AdminPagination } from './components/AdminPagination.vue';
15
- export { default as AdminModal } from './components/AdminModal.vue';
16
- export { default as ConfirmDialog } from './components/ConfirmDialog.vue';
17
- export { default as ToastContainer } from './components/ToastContainer.vue';