@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 +104 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/composables/index.d.ts +6 -0
- package/dist/composables/useAdminBatchOperation.d.ts +12 -0
- package/dist/composables/useClipboard.d.ts +4 -0
- package/dist/composables/useConfirmDialog.d.ts +43 -0
- package/dist/composables/useTablePagination.d.ts +19 -0
- package/dist/composables/useTableSelection.d.ts +20 -0
- package/dist/composables/useToast.d.ts +22 -0
- package/dist/index.d.ts +17 -0
- package/package.json +84 -0
- package/src/components/AdminModal.vue +193 -0
- package/src/components/AdminPagination.vue +271 -0
- package/src/components/ConfirmDialog.vue +160 -0
- package/src/components/ToastContainer.vue +120 -0
- package/src/components/index.ts +4 -0
- package/src/composables/index.ts +34 -0
- package/src/composables/useAdminBatchOperation.ts +51 -0
- package/src/composables/useClipboard.ts +37 -0
- package/src/composables/useConfirmDialog.ts +103 -0
- package/src/composables/useTablePagination.ts +81 -0
- package/src/composables/useTableSelection.ts +90 -0
- package/src/composables/useToast.ts +29 -0
- package/src/env.d.ts +30 -0
- package/src/index.ts +47 -0
- package/src/styles/admin-primitives.css +2006 -0
- package/src/styles/index.css +3 -0
- package/src/styles/tokens.css +102 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ref, watch } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type ConfirmOptionDef = {
|
|
4
|
+
key: string
|
|
5
|
+
label: string
|
|
6
|
+
/** 补充说明,显示在 label 下方 */
|
|
7
|
+
hint?: string
|
|
8
|
+
/** 默认 false */
|
|
9
|
+
defaultChecked?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ConfirmAskOptions = {
|
|
13
|
+
/** 危险操作样式(红按钮),由调用方通过 ConfirmDialog danger prop 控制亦可 */
|
|
14
|
+
danger?: boolean
|
|
15
|
+
/** 可选勾选项,默认全部未勾选;打开弹窗时按 defaultChecked 重置 */
|
|
16
|
+
options?: ConfirmOptionDef[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ConfirmResult = {
|
|
20
|
+
confirmed: boolean
|
|
21
|
+
/** key → 是否勾选;取消时各值为 defaultChecked(调用方应以 confirmed 为准) */
|
|
22
|
+
options: Record<string, boolean>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useConfirmDialog() {
|
|
26
|
+
const confirmVisible = ref(false)
|
|
27
|
+
const confirmMessage = ref('')
|
|
28
|
+
const confirmOptionDefs = ref<ConfirmOptionDef[]>([])
|
|
29
|
+
const confirmOptionValues = ref<Record<string, boolean>>({})
|
|
30
|
+
let confirmCallback: ((result: ConfirmResult) => void) | null = null
|
|
31
|
+
|
|
32
|
+
function snapshotOptions(): Record<string, boolean> {
|
|
33
|
+
const out: Record<string, boolean> = {}
|
|
34
|
+
for (const def of confirmOptionDefs.value) {
|
|
35
|
+
out[def.key] = Boolean(confirmOptionValues.value[def.key])
|
|
36
|
+
}
|
|
37
|
+
return out
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resetOptionValues(defs: ConfirmOptionDef[]) {
|
|
41
|
+
const next: Record<string, boolean> = {}
|
|
42
|
+
for (const def of defs) {
|
|
43
|
+
next[def.key] = def.defaultChecked === true
|
|
44
|
+
}
|
|
45
|
+
confirmOptionValues.value = next
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 简单确认:仅返回 boolean(兼容旧调用)。
|
|
50
|
+
* 需要读取 Checkbox 时请用 askConfirmWithOptions。
|
|
51
|
+
*/
|
|
52
|
+
function askConfirm(message: string): Promise<boolean> {
|
|
53
|
+
return askConfirmWithOptions(message).then((r) => r.confirmed)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function askConfirmWithOptions(
|
|
57
|
+
message: string,
|
|
58
|
+
options: ConfirmAskOptions = {},
|
|
59
|
+
): Promise<ConfirmResult> {
|
|
60
|
+
confirmCallback?.({ confirmed: false, options: snapshotOptions() })
|
|
61
|
+
confirmMessage.value = message
|
|
62
|
+
const defs = options.options ?? []
|
|
63
|
+
confirmOptionDefs.value = defs
|
|
64
|
+
resetOptionValues(defs)
|
|
65
|
+
confirmVisible.value = true
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
confirmCallback = (result) => resolve(result)
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function onConfirm() {
|
|
72
|
+
if (!confirmCallback) return
|
|
73
|
+
const callback = confirmCallback
|
|
74
|
+
confirmCallback = null
|
|
75
|
+
callback({ confirmed: true, options: snapshotOptions() })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function setConfirmOption(key: string, checked: boolean) {
|
|
79
|
+
confirmOptionValues.value = {
|
|
80
|
+
...confirmOptionValues.value,
|
|
81
|
+
[key]: checked,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
watch(confirmVisible, (visible, wasVisible) => {
|
|
86
|
+
if (wasVisible && !visible && confirmCallback) {
|
|
87
|
+
const callback = confirmCallback
|
|
88
|
+
confirmCallback = null
|
|
89
|
+
callback({ confirmed: false, options: snapshotOptions() })
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
confirmVisible,
|
|
95
|
+
confirmMessage,
|
|
96
|
+
confirmOptionDefs,
|
|
97
|
+
confirmOptionValues,
|
|
98
|
+
askConfirm,
|
|
99
|
+
askConfirmWithOptions,
|
|
100
|
+
onConfirm,
|
|
101
|
+
setConfirmOption,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ref, computed, type Ref, type ComputedRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
export interface UseTablePaginationOptions {
|
|
4
|
+
initialPage?: number
|
|
5
|
+
initialLimit?: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface UseTablePaginationReturn {
|
|
9
|
+
page: Ref<number>
|
|
10
|
+
limit: Ref<number>
|
|
11
|
+
total: Ref<number>
|
|
12
|
+
totalPages: ComputedRef<number>
|
|
13
|
+
setPage: (page: number) => void
|
|
14
|
+
setLimit: (limit: number) => void
|
|
15
|
+
setTotal: (total: number) => boolean
|
|
16
|
+
nextPage: () => void
|
|
17
|
+
prevPage: () => void
|
|
18
|
+
reset: () => void
|
|
19
|
+
offset: ComputedRef<number>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useTablePagination(options: UseTablePaginationOptions = {}): UseTablePaginationReturn {
|
|
23
|
+
const page = ref(options.initialPage ?? 1)
|
|
24
|
+
const limit = ref(options.initialLimit ?? 20)
|
|
25
|
+
const total = ref(0)
|
|
26
|
+
|
|
27
|
+
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / limit.value)))
|
|
28
|
+
const offset = computed(() => (page.value - 1) * limit.value)
|
|
29
|
+
|
|
30
|
+
function setPage(p: number) {
|
|
31
|
+
if (!Number.isFinite(p)) return
|
|
32
|
+
page.value = Math.max(1, Math.min(Math.trunc(p), totalPages.value))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function setLimit(l: number) {
|
|
36
|
+
if (!Number.isFinite(l) || l <= 0) return
|
|
37
|
+
limit.value = Math.trunc(l)
|
|
38
|
+
page.value = 1
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function setTotal(nextTotal: number) {
|
|
42
|
+
const numericTotal = Number(nextTotal)
|
|
43
|
+
total.value = Number.isFinite(numericTotal) ? Math.max(0, Math.trunc(numericTotal)) : 0
|
|
44
|
+
const nextPage = Math.max(1, Math.min(page.value, totalPages.value))
|
|
45
|
+
if (nextPage === page.value) return false
|
|
46
|
+
page.value = nextPage
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function nextPage() {
|
|
51
|
+
if (page.value < totalPages.value) {
|
|
52
|
+
page.value += 1
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function prevPage() {
|
|
57
|
+
if (page.value > 1) {
|
|
58
|
+
page.value -= 1
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function reset() {
|
|
63
|
+
page.value = options.initialPage ?? 1
|
|
64
|
+
limit.value = options.initialLimit ?? 20
|
|
65
|
+
total.value = 0
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
page,
|
|
70
|
+
limit,
|
|
71
|
+
total,
|
|
72
|
+
totalPages,
|
|
73
|
+
setPage,
|
|
74
|
+
setLimit,
|
|
75
|
+
setTotal,
|
|
76
|
+
nextPage,
|
|
77
|
+
prevPage,
|
|
78
|
+
reset,
|
|
79
|
+
offset,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { computed, ref, watch, unref, type MaybeRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
export interface UseTableSelectionOptions<T> {
|
|
4
|
+
isSelectable?: (row: T) => boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Accepts Ref or ComputedRef of row lists (MaybeRef) so product views can pass
|
|
9
|
+
* filtered computed tables without dual-package Ref assignability friction.
|
|
10
|
+
*/
|
|
11
|
+
export function useTableSelection<T>(
|
|
12
|
+
rows: MaybeRef<T[]>,
|
|
13
|
+
getKey: (row: T) => string,
|
|
14
|
+
options: UseTableSelectionOptions<T> = {},
|
|
15
|
+
) {
|
|
16
|
+
const selectedIds = ref<string[]>([])
|
|
17
|
+
const rowIds = computed(() => Array.from(new Set(
|
|
18
|
+
unref(rows)
|
|
19
|
+
.filter((row) => options.isSelectable?.(row) ?? true)
|
|
20
|
+
.map(getKey)
|
|
21
|
+
.filter(Boolean),
|
|
22
|
+
)))
|
|
23
|
+
const selectableSet = computed(() => new Set(rowIds.value))
|
|
24
|
+
const selectedSet = computed(() => new Set(selectedIds.value))
|
|
25
|
+
const selectedCount = computed(() => selectedIds.value.length)
|
|
26
|
+
const selectableCount = computed(() => rowIds.value.length)
|
|
27
|
+
const allVisibleSelected = computed(() => rowIds.value.length > 0 && rowIds.value.every((id) => selectedSet.value.has(id)))
|
|
28
|
+
const partiallySelected = computed(() => selectedCount.value > 0 && selectedCount.value < rowIds.value.length)
|
|
29
|
+
let rangeAnchorId = ''
|
|
30
|
+
|
|
31
|
+
function isSelected(id: string) {
|
|
32
|
+
return selectedSet.value.has(id)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function setSelected(id: string, checked: boolean, extendRange = false) {
|
|
36
|
+
if (!id || !selectableSet.value.has(id)) return
|
|
37
|
+
const next = new Set(selectedIds.value)
|
|
38
|
+
const anchorIndex = rowIds.value.indexOf(rangeAnchorId)
|
|
39
|
+
const currentIndex = rowIds.value.indexOf(id)
|
|
40
|
+
|
|
41
|
+
if (extendRange && anchorIndex >= 0 && currentIndex >= 0) {
|
|
42
|
+
const start = Math.min(anchorIndex, currentIndex)
|
|
43
|
+
const end = Math.max(anchorIndex, currentIndex)
|
|
44
|
+
for (const rangeId of rowIds.value.slice(start, end + 1)) {
|
|
45
|
+
if (checked) next.add(rangeId)
|
|
46
|
+
else next.delete(rangeId)
|
|
47
|
+
}
|
|
48
|
+
} else if (checked) {
|
|
49
|
+
next.add(id)
|
|
50
|
+
} else {
|
|
51
|
+
next.delete(id)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
selectedIds.value = Array.from(next)
|
|
55
|
+
rangeAnchorId = id
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function toggleAllVisible(checked: boolean) {
|
|
59
|
+
const next = new Set(selectedIds.value)
|
|
60
|
+
for (const id of rowIds.value) {
|
|
61
|
+
if (checked) next.add(id)
|
|
62
|
+
else next.delete(id)
|
|
63
|
+
}
|
|
64
|
+
selectedIds.value = Array.from(next)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function clearSelection() {
|
|
68
|
+
selectedIds.value = []
|
|
69
|
+
rangeAnchorId = ''
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
watch(rowIds, (ids) => {
|
|
73
|
+
const visible = new Set(ids)
|
|
74
|
+
selectedIds.value = selectedIds.value.filter((id) => visible.has(id))
|
|
75
|
+
if (rangeAnchorId && !visible.has(rangeAnchorId)) rangeAnchorId = ''
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
selectedIds,
|
|
80
|
+
selectedSet,
|
|
81
|
+
selectedCount,
|
|
82
|
+
selectableCount,
|
|
83
|
+
allVisibleSelected,
|
|
84
|
+
partiallySelected,
|
|
85
|
+
isSelected,
|
|
86
|
+
setSelected,
|
|
87
|
+
toggleAllVisible,
|
|
88
|
+
clearSelection,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type ToastType = 'success' | 'error' | 'info'
|
|
4
|
+
|
|
5
|
+
export type ToastItem = { id: number; message: string; type: ToastType }
|
|
6
|
+
|
|
7
|
+
const toasts = ref<ToastItem[]>([])
|
|
8
|
+
let count = 0
|
|
9
|
+
|
|
10
|
+
/** Max stacked toasts; older entries drop when exceeded (lottery hardening). */
|
|
11
|
+
export const MAX_TOASTS = 5
|
|
12
|
+
|
|
13
|
+
function showToast(message: string, type: ToastType = 'info', duration = 3000) {
|
|
14
|
+
const id = ++count
|
|
15
|
+
if (toasts.value.length >= MAX_TOASTS) {
|
|
16
|
+
toasts.value.shift()
|
|
17
|
+
}
|
|
18
|
+
toasts.value.push({ id, message, type })
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
toasts.value = toasts.value.filter((t) => t.id !== id)
|
|
21
|
+
}, duration)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useToast() {
|
|
25
|
+
return {
|
|
26
|
+
toasts,
|
|
27
|
+
showToast,
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
declare module '*.vue' {
|
|
4
|
+
import type { DefineComponent } from 'vue'
|
|
5
|
+
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>
|
|
6
|
+
export default component
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
declare module '*.css' {
|
|
10
|
+
const css: string
|
|
11
|
+
export default css
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Host apps (shop/lottery/auth) inject vue-i18n; kit components call `$t('pagination.*')` etc.
|
|
16
|
+
* Declared on both entry points so vue-tsc consumers share one augmentation path.
|
|
17
|
+
*/
|
|
18
|
+
declare module 'vue' {
|
|
19
|
+
interface ComponentCustomProperties {
|
|
20
|
+
$t: (key: string, params?: Record<string, unknown>) => string
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare module '@vue/runtime-core' {
|
|
25
|
+
interface ComponentCustomProperties {
|
|
26
|
+
$t: (key: string, params?: Record<string, unknown>) => string
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export {}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @usethink/cf-admin-fe — Vue 3 admin foundation for CF product family.
|
|
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'`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
useTablePagination,
|
|
11
|
+
type UseTablePaginationOptions,
|
|
12
|
+
type UseTablePaginationReturn,
|
|
13
|
+
} from './composables/useTablePagination'
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
useTableSelection,
|
|
17
|
+
type UseTableSelectionOptions,
|
|
18
|
+
} from './composables/useTableSelection'
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
useConfirmDialog,
|
|
22
|
+
type ConfirmOptionDef,
|
|
23
|
+
type ConfirmAskOptions,
|
|
24
|
+
type ConfirmResult,
|
|
25
|
+
} from './composables/useConfirmDialog'
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
useToast,
|
|
29
|
+
MAX_TOASTS,
|
|
30
|
+
type ToastType,
|
|
31
|
+
type ToastItem,
|
|
32
|
+
} from './composables/useToast'
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
useAdminBatchOperation,
|
|
36
|
+
type AdminBatchOperationResult,
|
|
37
|
+
} from './composables/useAdminBatchOperation'
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
writeClipboardText,
|
|
41
|
+
copyText,
|
|
42
|
+
} from './composables/useClipboard'
|
|
43
|
+
|
|
44
|
+
export { default as AdminPagination } from './components/AdminPagination.vue'
|
|
45
|
+
export { default as AdminModal } from './components/AdminModal.vue'
|
|
46
|
+
export { default as ConfirmDialog } from './components/ConfirmDialog.vue'
|
|
47
|
+
export { default as ToastContainer } from './components/ToastContainer.vue'
|