@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.
- 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/src/index.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @usethink/cf-admin-fe — Vue 3
|
|
2
|
+
* @usethink/cf-admin-fe — CF 产品族 Vue 3 管理端基础套件。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
+
}
|
package/src/styles/index.css
CHANGED
|
@@ -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,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
|
+
}
|