@usethink/cf-admin-fe 0.1.0

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/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # @usethink/cf-admin-fe
2
+
3
+ Vue 3 **admin foundation** for the Cloudflare product family (`cf-shop`, `cf-lottery`, future CF admin apps).
4
+
5
+ This package is the **frontend** counterpart to backend `@usethink/cf-core`:
6
+
7
+ | Layer | Package | Contents |
8
+ |---|---|---|
9
+ | Workers / infra | `@usethink/cf-core` | HTTP, crypto, media, secrets, … |
10
+ | Admin UI kit | **`@usethink/cf-admin-fe`** | Composables, shell components, admin CSS tokens |
11
+
12
+
13
+ ## Why `cf-admin-fe` (not `cf-admin`)
14
+
15
+ This package is **frontend-only**. The `-fe` suffix makes that explicit next to backend `@usethink/cf-core`, so operators never confuse UI kit code with Worker/API infrastructure.
16
+
17
+ **Not** a business mid-tier: no orders, lottery draws, RBAC pages, or payment UIs.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ # monorepo / sibling template (recommended during development)
23
+ npm install file:../cf-admin-fe
24
+
25
+ # or once published
26
+ npm install @usethink/cf-admin-fe
27
+ ```
28
+
29
+ Peer: `vue` ^3.4.
30
+
31
+ ## Quick start (new admin app)
32
+
33
+ ```ts
34
+ // main.ts
35
+ import { createApp } from 'vue'
36
+ import App from './App.vue'
37
+ import '@usethink/cf-admin-fe/styles' // tokens + admin primitives
38
+
39
+ createApp(App).mount('#app')
40
+ ```
41
+
42
+ ```vue
43
+ <script setup lang="ts">
44
+ import {
45
+ useTablePagination,
46
+ useTableSelection,
47
+ useConfirmDialog,
48
+ useToast,
49
+ useAdminBatchOperation,
50
+ } from '@usethink/cf-admin-fe'
51
+ import {
52
+ AdminPagination,
53
+ ConfirmDialog,
54
+ ToastContainer,
55
+ AdminModal,
56
+ } from '@usethink/cf-admin-fe/components'
57
+ </script>
58
+ ```
59
+
60
+ Products may keep thin re-exports under `@/composables/*` so existing views do not mass-rewrite.
61
+
62
+ ## Public surface
63
+
64
+ ### Composables (`@usethink/cf-admin-fe` or `/composables`)
65
+
66
+ - `useTablePagination` — page/limit/total/offset
67
+ - `useTableSelection` — multi-select + shift range
68
+ - `useConfirmDialog` — confirm + optional checkboxes
69
+ - `useToast` / `MAX_TOASTS` — global toast queue
70
+ - `useAdminBatchOperation` — sequential batch with progress
71
+ - `writeClipboardText` / `copyText` — clipboard helpers
72
+
73
+ ### Components (`@usethink/cf-admin-fe/components`)
74
+
75
+ - `AdminPagination`, `AdminModal`, `ConfirmDialog`, `ToastContainer`
76
+
77
+ Components use `$t(...)` for labels when `vue-i18n` is present; provide the usual keys (`pagination.*`, `confirm.*`, `adminModal.*`) in the host app.
78
+
79
+ ### Styles
80
+
81
+ ```ts
82
+ import '@usethink/cf-admin-fe/styles'
83
+ // or granular:
84
+ import '@usethink/cf-admin-fe/styles/tokens.css'
85
+ import '@usethink/cf-admin-fe/styles/admin-primitives.css'
86
+ ```
87
+
88
+ ## What stays in the product
89
+
90
+ - Domain admin **pages** (`views/admin/*`)
91
+ - Auth/session composables, API clients, ConfigField registries
92
+ - Product-only CSS beyond the shared primitives
93
+
94
+ ## Develop
95
+
96
+ ```bash
97
+ npm install
98
+ npm test
99
+ npm run type-check
100
+ ```
101
+
102
+ ## License
103
+
104
+ MIT
@@ -0,0 +1,4 @@
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';
@@ -0,0 +1,6 @@
1
+ export { useTablePagination, type UseTablePaginationOptions, type UseTablePaginationReturn, } from './useTablePagination';
2
+ export { useTableSelection, type UseTableSelectionOptions, } from './useTableSelection';
3
+ export { useConfirmDialog, type ConfirmOptionDef, type ConfirmAskOptions, type ConfirmResult, } from './useConfirmDialog';
4
+ export { useToast, MAX_TOASTS, type ToastType, type ToastItem, } from './useToast';
5
+ export { useAdminBatchOperation, type AdminBatchOperationResult, } from './useAdminBatchOperation';
6
+ export { writeClipboardText, copyText, } from './useClipboard';
@@ -0,0 +1,12 @@
1
+ export interface AdminBatchOperationResult<T> {
2
+ total: number;
3
+ success: number;
4
+ failed: number;
5
+ failedItems: T[];
6
+ }
7
+ export declare function useAdminBatchOperation(): {
8
+ operating: import("vue").Ref<boolean, boolean>;
9
+ completed: import("vue").Ref<number, number>;
10
+ total: import("vue").Ref<number, number>;
11
+ runSequential: <T>(items: T[], action: (item: T) => Promise<void>) => Promise<AdminBatchOperationResult<T> | null>;
12
+ };
@@ -0,0 +1,4 @@
1
+ /** 剪贴板工具:一键复制文本到剪贴板,带按钮视觉反馈 */
2
+ /** 复制文本,更新按钮文字反馈 */
3
+ export declare function writeClipboardText(text: string): Promise<void>;
4
+ export declare function copyText(text: string, e: Event): void;
@@ -0,0 +1,43 @@
1
+ export type ConfirmOptionDef = {
2
+ key: string;
3
+ label: string;
4
+ /** 补充说明,显示在 label 下方 */
5
+ hint?: string;
6
+ /** 默认 false */
7
+ defaultChecked?: boolean;
8
+ };
9
+ export type ConfirmAskOptions = {
10
+ /** 危险操作样式(红按钮),由调用方通过 ConfirmDialog danger prop 控制亦可 */
11
+ danger?: boolean;
12
+ /** 可选勾选项,默认全部未勾选;打开弹窗时按 defaultChecked 重置 */
13
+ options?: ConfirmOptionDef[];
14
+ };
15
+ export type ConfirmResult = {
16
+ confirmed: boolean;
17
+ /** key → 是否勾选;取消时各值为 defaultChecked(调用方应以 confirmed 为准) */
18
+ options: Record<string, boolean>;
19
+ };
20
+ export declare function useConfirmDialog(): {
21
+ confirmVisible: import("vue").Ref<boolean, boolean>;
22
+ confirmMessage: import("vue").Ref<string, string>;
23
+ confirmOptionDefs: import("vue").Ref<{
24
+ key: string;
25
+ label: string;
26
+ hint?: string
27
+ /** 默认 false */
28
+ | undefined;
29
+ defaultChecked?: boolean | undefined;
30
+ }[], ConfirmOptionDef[] | {
31
+ key: string;
32
+ label: string;
33
+ hint?: string
34
+ /** 默认 false */
35
+ | undefined;
36
+ defaultChecked?: boolean | undefined;
37
+ }[]>;
38
+ confirmOptionValues: import("vue").Ref<Record<string, boolean>, Record<string, boolean>>;
39
+ askConfirm: (message: string) => Promise<boolean>;
40
+ askConfirmWithOptions: (message: string, options?: ConfirmAskOptions) => Promise<ConfirmResult>;
41
+ onConfirm: () => void;
42
+ setConfirmOption: (key: string, checked: boolean) => void;
43
+ };
@@ -0,0 +1,19 @@
1
+ import { type Ref, type ComputedRef } from 'vue';
2
+ export interface UseTablePaginationOptions {
3
+ initialPage?: number;
4
+ initialLimit?: number;
5
+ }
6
+ export interface UseTablePaginationReturn {
7
+ page: Ref<number>;
8
+ limit: Ref<number>;
9
+ total: Ref<number>;
10
+ totalPages: ComputedRef<number>;
11
+ setPage: (page: number) => void;
12
+ setLimit: (limit: number) => void;
13
+ setTotal: (total: number) => boolean;
14
+ nextPage: () => void;
15
+ prevPage: () => void;
16
+ reset: () => void;
17
+ offset: ComputedRef<number>;
18
+ }
19
+ export declare function useTablePagination(options?: UseTablePaginationOptions): UseTablePaginationReturn;
@@ -0,0 +1,20 @@
1
+ import { type MaybeRef } from 'vue';
2
+ export interface UseTableSelectionOptions<T> {
3
+ isSelectable?: (row: T) => boolean;
4
+ }
5
+ /**
6
+ * Accepts Ref or ComputedRef of row lists (MaybeRef) so product views can pass
7
+ * filtered computed tables without dual-package Ref assignability friction.
8
+ */
9
+ export declare function useTableSelection<T>(rows: MaybeRef<T[]>, getKey: (row: T) => string, options?: UseTableSelectionOptions<T>): {
10
+ selectedIds: import("vue").Ref<string[], string[]>;
11
+ selectedSet: import("vue").ComputedRef<Set<string>>;
12
+ selectedCount: import("vue").ComputedRef<number>;
13
+ selectableCount: import("vue").ComputedRef<number>;
14
+ allVisibleSelected: import("vue").ComputedRef<boolean>;
15
+ partiallySelected: import("vue").ComputedRef<boolean>;
16
+ isSelected: (id: string) => boolean;
17
+ setSelected: (id: string, checked: boolean, extendRange?: boolean) => void;
18
+ toggleAllVisible: (checked: boolean) => void;
19
+ clearSelection: () => void;
20
+ };
@@ -0,0 +1,22 @@
1
+ export type ToastType = 'success' | 'error' | 'info';
2
+ export type ToastItem = {
3
+ id: number;
4
+ message: string;
5
+ type: ToastType;
6
+ };
7
+ /** Max stacked toasts; older entries drop when exceeded (lottery hardening). */
8
+ export declare const MAX_TOASTS = 5;
9
+ declare function showToast(message: string, type?: ToastType, duration?: number): void;
10
+ export declare function useToast(): {
11
+ toasts: import("vue").Ref<{
12
+ id: number;
13
+ message: string;
14
+ type: ToastType;
15
+ }[], ToastItem[] | {
16
+ id: number;
17
+ message: string;
18
+ type: ToastType;
19
+ }[]>;
20
+ showToast: typeof showToast;
21
+ };
22
+ export {};
@@ -0,0 +1,17 @@
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';
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@usethink/cf-admin-fe",
3
+ "version": "0.1.0",
4
+ "description": "Vue 3 frontend admin kit for CF product family \u2014 composables, shell components, design tokens (FE only; not cf-core)",
5
+ "type": "module",
6
+ "private": false,
7
+ "files": [
8
+ "src",
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "main": "./src/index.ts",
13
+ "module": "./src/index.ts",
14
+ "types": "./src/index.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./src/index.ts",
18
+ "import": "./src/index.ts",
19
+ "default": "./src/index.ts"
20
+ },
21
+ "./composables": {
22
+ "types": "./src/composables/index.ts",
23
+ "import": "./src/composables/index.ts"
24
+ },
25
+ "./components": {
26
+ "types": "./src/components/index.ts",
27
+ "import": "./src/components/index.ts"
28
+ },
29
+ "./styles": "./src/styles/index.css",
30
+ "./styles/tokens.css": "./src/styles/tokens.css",
31
+ "./styles/admin-primitives.css": "./src/styles/admin-primitives.css",
32
+ "./package.json": "./package.json",
33
+ "./components/AdminPagination.vue": "./src/components/AdminPagination.vue",
34
+ "./components/AdminModal.vue": "./src/components/AdminModal.vue",
35
+ "./components/ConfirmDialog.vue": "./src/components/ConfirmDialog.vue",
36
+ "./components/ToastContainer.vue": "./src/components/ToastContainer.vue"
37
+ },
38
+ "sideEffects": [
39
+ "**/*.css"
40
+ ],
41
+ "peerDependencies": {
42
+ "vue": "^3.4.0"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "vue-i18n": {
46
+ "optional": true
47
+ }
48
+ },
49
+ "devDependencies": {
50
+ "@vitejs/plugin-vue": "^5.2.1",
51
+ "typescript": "^5.7.2",
52
+ "vite": "^6.0.0",
53
+ "vitest": "^3.0.0",
54
+ "vue": "^3.5.39",
55
+ "vue-tsc": "^2.1.10"
56
+ },
57
+ "scripts": {
58
+ "type-check": "tsc --noEmit && vue-tsc --noEmit -p tsconfig.json",
59
+ "test": "vitest run",
60
+ "test:watch": "vitest",
61
+ "build": "tsc -p tsconfig.build.json"
62
+ },
63
+ "keywords": [
64
+ "admin",
65
+ "cf-admin-fe",
66
+ "cloudflare",
67
+ "composables",
68
+ "frontend",
69
+ "vue3"
70
+ ],
71
+ "license": "MIT",
72
+ "repository": {
73
+ "type": "git",
74
+ "url": "git+https://github.com/qwdingyu/cf-admin-fe.git"
75
+ },
76
+ "bugs": {
77
+ "url": "https://github.com/qwdingyu/cf-admin-fe/issues"
78
+ },
79
+ "homepage": "https://github.com/qwdingyu/cf-admin-fe#readme",
80
+ "publishConfig": {
81
+ "access": "public",
82
+ "registry": "https://registry.npmjs.org/"
83
+ }
84
+ }
@@ -0,0 +1,193 @@
1
+ <template>
2
+ <div v-if="modelValue" class="modal-mask" @click.self="handleBackdropClick">
3
+ <div
4
+ ref="modalRef"
5
+ class="modal"
6
+ :style="{ maxWidth }"
7
+ role="dialog"
8
+ aria-modal="true"
9
+ :aria-labelledby="title ? titleId : undefined"
10
+ :aria-label="$t('adminModal.title')"
11
+ tabindex="-1"
12
+ @keydown="handleKeydown"
13
+ >
14
+ <h3 v-if="title" :id="titleId" class="modal-title">{{ title }}</h3>
15
+ <div ref="bodyRef" class="modal-body">
16
+ <slot />
17
+ </div>
18
+ <div v-if="$slots.actions || !hideActions" class="modal-actions">
19
+ <slot name="actions">
20
+ <button type="button" class="btn btn-ghost" @click="close">{{ $t('adminModal.close') }}</button>
21
+ </slot>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+ <script setup lang="ts">
28
+ import { nextTick, onBeforeUnmount, ref, useId, watch } from 'vue'
29
+
30
+ const props = withDefaults(defineProps<{
31
+ modelValue: boolean
32
+ title?: string
33
+ maxWidth?: string
34
+ hideActions?: boolean
35
+ closeOnBackdrop?: boolean
36
+ closeOnEscape?: boolean
37
+ }>(), {
38
+ hideActions: false,
39
+ closeOnBackdrop: false,
40
+ closeOnEscape: false,
41
+ })
42
+
43
+ const emit = defineEmits<{
44
+ 'update:modelValue': [value: boolean]
45
+ }>()
46
+
47
+ const modalRef = ref<HTMLElement | null>(null)
48
+ const bodyRef = ref<HTMLElement | null>(null)
49
+ const titleId = `admin-modal-title-${useId()}`
50
+ let restoreFocus: HTMLElement | null = null
51
+
52
+ function close() {
53
+ emit('update:modelValue', false)
54
+ }
55
+
56
+ function handleBackdropClick() {
57
+ if (props.closeOnBackdrop) close()
58
+ }
59
+
60
+ function getFocusableElements() {
61
+ return Array.from(modalRef.value?.querySelectorAll<HTMLElement>(
62
+ 'button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])',
63
+ ) || [])
64
+ }
65
+
66
+ function handleKeydown(event: KeyboardEvent) {
67
+ if (event.key === 'Escape') {
68
+ if (!props.closeOnEscape) return
69
+ event.preventDefault()
70
+ close()
71
+ return
72
+ }
73
+ if (event.key !== 'Tab') return
74
+
75
+ const focusable = getFocusableElements()
76
+ if (focusable.length === 0) {
77
+ event.preventDefault()
78
+ modalRef.value?.focus()
79
+ return
80
+ }
81
+
82
+ const first = focusable[0]
83
+ const last = focusable[focusable.length - 1]
84
+ if (event.shiftKey && (document.activeElement === first || document.activeElement === modalRef.value)) {
85
+ event.preventDefault()
86
+ last.focus()
87
+ } else if (!event.shiftKey && document.activeElement === last) {
88
+ event.preventDefault()
89
+ first.focus()
90
+ }
91
+ }
92
+
93
+ watch(() => props.modelValue, async (visible, wasVisible) => {
94
+ if (visible && !wasVisible) {
95
+ restoreFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null
96
+ await nextTick()
97
+ // 长详情中的第一个按钮通常位于正文底部;若直接聚焦它,浏览器会把滚动区自动拉到底部。
98
+ // 打开时先回到正文顶部并聚焦对话框容器,用户可以从订单基本信息开始阅读,再用 Tab 进入控件。
99
+ if (bodyRef.value) bodyRef.value.scrollTop = 0
100
+ modalRef.value?.focus({ preventScroll: true })
101
+ } else if (!visible && wasVisible) {
102
+ restoreFocus?.focus()
103
+ restoreFocus = null
104
+ }
105
+ })
106
+
107
+ onBeforeUnmount(() => {
108
+ restoreFocus?.focus()
109
+ })
110
+ </script>
111
+
112
+ <style scoped>
113
+ .modal-mask {
114
+ position: fixed;
115
+ inset: 0;
116
+ background: var(--overlay, rgba(0, 0, 0, 0.55));
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: center;
120
+ padding: 24px;
121
+ overflow: hidden;
122
+ z-index: 200;
123
+ backdrop-filter: saturate(180%) blur(10px);
124
+ -webkit-backdrop-filter: saturate(180%) blur(10px);
125
+ }
126
+
127
+ .modal {
128
+ box-sizing: border-box;
129
+ width: 100%;
130
+ max-height: calc(100dvh - 48px);
131
+ min-height: 0;
132
+ display: flex;
133
+ flex-direction: column;
134
+ /* 比页面底略抬一层,避免与遮罩糊成一团 */
135
+ background: var(--tg-secondary-bg, #151b28);
136
+ color: var(--tg-text);
137
+ border-radius: var(--r-lg, 12px);
138
+ padding: 22px;
139
+ border: 1px solid var(--border-strong, rgba(255, 255, 255, 0.16));
140
+ box-shadow: var(--shadow-lg, 0 20px 50px rgba(0, 0, 0, 0.45));
141
+ /* 勿用 overflow:hidden:会裁切 :focus-visible 描边(控件四边显示不全) */
142
+ overflow: visible;
143
+ }
144
+
145
+ .modal-title {
146
+ flex: 0 0 auto;
147
+ margin: 0 0 16px;
148
+ font-size: 18px;
149
+ font-weight: 650;
150
+ letter-spacing: 0.01em;
151
+ color: var(--tg-text);
152
+ line-height: 1.35;
153
+ }
154
+
155
+ .modal-body {
156
+ min-height: 0;
157
+ flex: 1 1 auto;
158
+ display: flex;
159
+ flex-direction: column;
160
+ gap: 14px;
161
+ overflow-x: visible;
162
+ overflow-y: auto;
163
+ overscroll-behavior: contain;
164
+ /* 给 focus 描边(outline-offset: 2px)留出左右上下内边距,避免被滚动裁切 */
165
+ padding: 4px 6px 6px;
166
+ margin: -4px -6px -6px;
167
+ scrollbar-gutter: stable;
168
+ }
169
+
170
+ /* 子级 form 需要吃满高度时(如商品编辑粘性脚部) */
171
+ .modal-body > form {
172
+ min-height: 0;
173
+ }
174
+
175
+ .modal-actions {
176
+ flex: 0 0 auto;
177
+ display: flex;
178
+ justify-content: flex-end;
179
+ gap: 10px;
180
+ margin-top: 14px;
181
+ }
182
+
183
+ @media (max-width: 640px) {
184
+ .modal-mask {
185
+ padding: 12px;
186
+ }
187
+
188
+ .modal {
189
+ max-height: calc(100dvh - 24px);
190
+ padding: 16px;
191
+ }
192
+ }
193
+ </style>