@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.
@@ -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'