c-admin-kit 1.0.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.
Files changed (56) hide show
  1. package/README.md +232 -0
  2. package/dist/c-admin-kit.css +1 -0
  3. package/dist/c-admin-kit.js +2482 -0
  4. package/dist/c-admin-kit.umd.cjs +10 -0
  5. package/dist/components/AsyncButton/index.vue.d.ts +48 -0
  6. package/dist/components/CollapsibleContainer/index.vue.d.ts +73 -0
  7. package/dist/components/CustomDrawer/index.vue.d.ts +149 -0
  8. package/dist/components/ImagePreview/index.vue.d.ts +34 -0
  9. package/dist/components/SearchBox/index.vue.d.ts +122 -0
  10. package/dist/components/SelectWithAll/index.vue.d.ts +169 -0
  11. package/dist/components/SelectWithPage/index.vue.d.ts +78 -0
  12. package/dist/components/SimpleTable/index.vue.d.ts +337 -0
  13. package/dist/components/SimpleTable/useTableColumnConfig.d.ts +5 -0
  14. package/dist/components/diff/index.vue.d.ts +33 -0
  15. package/dist/components/index.d.ts +12 -0
  16. package/dist/composables/index.d.ts +6 -0
  17. package/dist/composables/useConfirmAction.d.ts +5 -0
  18. package/dist/composables/useConfirmSubmit.d.ts +5 -0
  19. package/dist/composables/useDialog.d.ts +5 -0
  20. package/dist/composables/useDownload.d.ts +5 -0
  21. package/dist/composables/useForm.d.ts +5 -0
  22. package/dist/composables/useListPage.d.ts +5 -0
  23. package/dist/index.d.ts +9 -0
  24. package/dist/types.d.ts +282 -0
  25. package/dist/utils/index.d.ts +4 -0
  26. package/dist/utils/scroll-to.d.ts +7 -0
  27. package/dist/utils/searchFieldFactory.d.ts +34 -0
  28. package/dist/utils/treeManager.d.ts +64 -0
  29. package/dist/utils/validate.d.ts +30 -0
  30. package/package.json +86 -0
  31. package/src/components/AsyncButton/index.vue +58 -0
  32. package/src/components/CollapsibleContainer/index.vue +240 -0
  33. package/src/components/CustomDrawer/index.vue +167 -0
  34. package/src/components/ImagePreview/index.vue +88 -0
  35. package/src/components/SearchBox/index.vue +582 -0
  36. package/src/components/SelectWithAll/index.vue +281 -0
  37. package/src/components/SelectWithPage/index.vue +204 -0
  38. package/src/components/SimpleTable/index.vue +781 -0
  39. package/src/components/SimpleTable/useTableColumnConfig.ts +139 -0
  40. package/src/components/diff/index.vue +265 -0
  41. package/src/components/index.ts +35 -0
  42. package/src/composables/index.ts +6 -0
  43. package/src/composables/useConfirmAction.ts +62 -0
  44. package/src/composables/useConfirmSubmit.ts +68 -0
  45. package/src/composables/useDialog.ts +48 -0
  46. package/src/composables/useDownload.ts +83 -0
  47. package/src/composables/useForm.ts +114 -0
  48. package/src/composables/useListPage.ts +243 -0
  49. package/src/env.d.ts +7 -0
  50. package/src/index.ts +44 -0
  51. package/src/types.ts +344 -0
  52. package/src/utils/index.ts +4 -0
  53. package/src/utils/scroll-to.ts +60 -0
  54. package/src/utils/searchFieldFactory.ts +302 -0
  55. package/src/utils/treeManager.ts +218 -0
  56. package/src/utils/validate.ts +64 -0
@@ -0,0 +1,282 @@
1
+ import { Ref, VNode } from 'vue';
2
+ /** 通用记录类型 */
3
+ export type AnyRecord = Record<string, any>;
4
+ /** API 函数类型 */
5
+ export type ApiFn<P = AnyRecord, R = any> = (params: P) => Promise<R>;
6
+ /** 路由跳转位置 */
7
+ export interface NavigateLocation {
8
+ path: string;
9
+ query?: AnyRecord;
10
+ }
11
+ export interface StatusMapItem {
12
+ text: string;
13
+ type: 'success' | 'danger' | 'warning' | 'info' | '';
14
+ }
15
+ export interface TableColumn {
16
+ /** 列类型 */
17
+ type?: 'selection' | 'index' | 'drag' | 'status';
18
+ /** 字段名 */
19
+ prop?: string;
20
+ /** 列标题 */
21
+ label?: string;
22
+ /** 列宽 */
23
+ width?: number | string;
24
+ /** 最小列宽 */
25
+ minWidth?: number | string;
26
+ /** 对齐方式 */
27
+ align?: 'left' | 'center' | 'right';
28
+ /** 固定列 */
29
+ fixed?: boolean | 'left' | 'right';
30
+ /** 是否可排序 */
31
+ sortable?: boolean | 'custom';
32
+ /** 是否显示溢出提示 */
33
+ showOverflowTooltip?: boolean;
34
+ /** 插槽名 */
35
+ slot?: string;
36
+ /** 头部插槽名 */
37
+ headerSlot?: string;
38
+ /** 自定义渲染 */
39
+ render?: (row: AnyRecord, column: AnyRecord, index: number) => VNode;
40
+ /** 格式化函数 */
41
+ formatter?: (row: AnyRecord, column: AnyRecord, cellValue: any, index: number) => string;
42
+ /** 状态映射 */
43
+ statusMap?: Record<string | number, StatusMapItem>;
44
+ /** Tag 效果 */
45
+ effect?: 'dark' | 'light' | 'plain';
46
+ /** Tag 尺寸 */
47
+ size?: 'large' | 'default' | 'small';
48
+ /** 内部标识 key */
49
+ _key?: string;
50
+ [key: string]: any;
51
+ }
52
+ export interface SearchFieldBase {
53
+ prop: string;
54
+ label: string;
55
+ component?: string;
56
+ type?: string;
57
+ placeholder?: string;
58
+ required?: boolean;
59
+ visible?: boolean | ((form: AnyRecord) => boolean);
60
+ defaultValue?: any;
61
+ multiple?: boolean;
62
+ slotName?: string;
63
+ props?: AnyRecord;
64
+ [key: string]: any;
65
+ }
66
+ export interface DateRangeConfig {
67
+ startField: string;
68
+ endField: string;
69
+ format: (date: any) => string;
70
+ }
71
+ export interface RelationConfig {
72
+ dependOn: string;
73
+ getOptions: ((parentValue: any, form?: AnyRecord) => any[] | Promise<any[]>) | Record<string, any[]>;
74
+ }
75
+ export interface SearchField extends SearchFieldBase {
76
+ options?: Array<{
77
+ label: string;
78
+ value: any;
79
+ disabled?: boolean;
80
+ }>;
81
+ dateRange?: DateRangeConfig;
82
+ relation?: RelationConfig;
83
+ defaultOptions?: Array<{
84
+ label: string;
85
+ value: any;
86
+ }>;
87
+ returnAllIds?: boolean;
88
+ fileIds?: string[];
89
+ }
90
+ export interface UseListPageOptions {
91
+ apiList?: ApiFn;
92
+ apiChangeState?: ApiFn;
93
+ apiDelete?: ApiFn & {
94
+ batch?: ApiFn<{
95
+ ids: any[];
96
+ }>;
97
+ };
98
+ apiBatchDelete?: ApiFn;
99
+ apiExport?: ApiFn;
100
+ addPath?: string;
101
+ editPath?: string;
102
+ navigate?: (location: NavigateLocation) => void;
103
+ exportFileName?: string;
104
+ stateKey?: string;
105
+ idKey?: string;
106
+ activeValue?: any;
107
+ inactiveValue?: any;
108
+ statusMap?: Record<string | number, string>;
109
+ deleteConfirmText?: string;
110
+ deleteSuccessText?: string;
111
+ deleteErrorText?: string;
112
+ openDialog?: (data?: any) => void;
113
+ }
114
+ export interface UseListPageReturn {
115
+ tableRef: Ref<any>;
116
+ searchParams: Ref<AnyRecord>;
117
+ handleSearch: (params?: AnyRecord) => void;
118
+ handleReset: () => void;
119
+ changeState: (row: AnyRecord, callback?: (row: AnyRecord) => void) => Promise<void>;
120
+ handleDelete: (row: AnyRecord, callback?: (row: AnyRecord) => void) => Promise<void>;
121
+ handleBatchDelete: (rows: AnyRecord[], callback?: (rows: AnyRecord[]) => void) => Promise<void>;
122
+ handleDialog: (data?: any) => void;
123
+ exportExcel: () => Promise<void>;
124
+ handleAdd: (query?: AnyRecord) => void;
125
+ handleEdit: (row: AnyRecord, query?: AnyRecord) => void;
126
+ handleView: (row: AnyRecord, detailPath: string, query?: AnyRecord) => void;
127
+ refresh: () => void;
128
+ apiList?: ApiFn;
129
+ }
130
+ export interface UseFormOptions<T extends AnyRecord = AnyRecord> {
131
+ initFormData?: T | (() => T);
132
+ rules?: AnyRecord;
133
+ onSuccess?: (result: any) => void;
134
+ onError?: (error: any) => void;
135
+ transform?: (formData: T) => any;
136
+ }
137
+ export interface UseFormReturn<T extends AnyRecord = AnyRecord> {
138
+ formRef: Ref<any>;
139
+ formData: T;
140
+ loading: Ref<boolean>;
141
+ rules: AnyRecord;
142
+ submit: (apiCall: ApiFn, successMessage?: string, customTransform?: (formData: T) => any, skipValidate?: boolean) => Promise<any>;
143
+ validate: () => Promise<boolean>;
144
+ validateField: (field: string, callback?: (...args: any[]) => void) => void;
145
+ reset: () => void;
146
+ clearValidate: (props?: string | string[]) => void;
147
+ setFormData: (data: Partial<T>) => void;
148
+ resetFormData: () => void;
149
+ }
150
+ export type ConfirmType = 'success' | 'warning' | 'info' | 'error';
151
+ export interface UseConfirmActionOptions {
152
+ onSuccess?: (result: any) => void;
153
+ onError?: (error: any) => void;
154
+ confirmTitle?: string;
155
+ confirmButtonText?: string;
156
+ cancelButtonText?: string;
157
+ confirmType?: ConfirmType;
158
+ }
159
+ export interface UseConfirmActionReturn {
160
+ execute: (params: AnyRecord) => Promise<any>;
161
+ loading: Ref<boolean>;
162
+ }
163
+ export interface UseConfirmSubmitOptions {
164
+ title?: string;
165
+ message?: string;
166
+ confirmButtonText?: string;
167
+ cancelButtonText?: string;
168
+ type?: ConfirmType;
169
+ successMessage?: string;
170
+ cancelMessage?: string;
171
+ errorMessage?: string;
172
+ showCancelMessage?: boolean;
173
+ messageBoxOptions?: AnyRecord;
174
+ }
175
+ export interface UseDownloadOptions {
176
+ filename?: string;
177
+ fileExtension?: string;
178
+ mimeType?: string;
179
+ timeout?: number;
180
+ loadingMessage?: string;
181
+ successMessage?: string;
182
+ }
183
+ export interface UseDownloadReturn {
184
+ downloading: Ref<boolean>;
185
+ download: (params?: AnyRecord) => Promise<void>;
186
+ }
187
+ export interface UseDialogOptions {
188
+ width?: string;
189
+ title?: string;
190
+ beforeClose?: (done: () => void) => void;
191
+ }
192
+ export interface UseDialogReturn {
193
+ visible: Ref<boolean>;
194
+ dialogTitle: Ref<string>;
195
+ dialogData: AnyRecord;
196
+ width: string;
197
+ open: (data?: AnyRecord, customTitle?: string) => void;
198
+ close: () => void;
199
+ handleClose: (done: () => void) => void;
200
+ }
201
+ export interface TableColumnConfigProps {
202
+ columns?: TableColumn[];
203
+ tableKey?: string;
204
+ getColumnConfigApi?: ApiFn | null;
205
+ saveColumnConfigApi?: ApiFn | null;
206
+ }
207
+ export interface UseTableColumnConfigReturn {
208
+ computedColumns: import('vue').ComputedRef<TableColumn[]>;
209
+ settingColumns: import('vue').ComputedRef<TableColumn[]>;
210
+ visibleColKeys: Ref<string[]>;
211
+ tempVisibleColKeys: Ref<string[]>;
212
+ popoverRef: Ref<any>;
213
+ savingColumns: Ref<boolean>;
214
+ initTableConfig: () => Promise<void>;
215
+ handlePopoverShow: () => void;
216
+ handleCancelSettings: () => void;
217
+ handleConfirmSettings: () => Promise<void>;
218
+ }
219
+ export interface DateRangeFieldConfig {
220
+ prop?: string;
221
+ label?: string;
222
+ startField?: string;
223
+ endField?: string;
224
+ format?: string;
225
+ props?: AnyRecord;
226
+ [key: string]: any;
227
+ }
228
+ export interface InputFieldConfig {
229
+ prop: string;
230
+ label: string;
231
+ placeholder?: string;
232
+ maxlength?: number;
233
+ props?: AnyRecord;
234
+ [key: string]: any;
235
+ }
236
+ export interface SelectFieldConfig {
237
+ prop: string;
238
+ label: string;
239
+ options?: Array<{
240
+ label: string;
241
+ value: any;
242
+ }>;
243
+ multiple?: boolean;
244
+ props?: AnyRecord;
245
+ [key: string]: any;
246
+ }
247
+ export interface CascadeSelectFieldConfig extends SelectFieldConfig {
248
+ dependOn: string;
249
+ getOptions: (parentValue: any) => Promise<any[]>;
250
+ defaultOptions?: Array<{
251
+ label: string;
252
+ value: any;
253
+ }>;
254
+ }
255
+ export interface CascaderFieldConfig {
256
+ prop: string;
257
+ label: string;
258
+ options?: any[];
259
+ labelKey?: string;
260
+ valueKey?: string;
261
+ checkStrictly?: boolean;
262
+ returnAllIds?: boolean;
263
+ props?: AnyRecord;
264
+ [key: string]: any;
265
+ }
266
+ export interface NumberFieldConfig {
267
+ prop: string;
268
+ label: string;
269
+ min?: number;
270
+ max?: number;
271
+ step?: number;
272
+ props?: AnyRecord;
273
+ [key: string]: any;
274
+ }
275
+ export interface SwitchFieldConfig {
276
+ prop: string;
277
+ label: string;
278
+ activeValue?: any;
279
+ inactiveValue?: any;
280
+ props?: AnyRecord;
281
+ [key: string]: any;
282
+ }
@@ -0,0 +1,4 @@
1
+ export * from './searchFieldFactory';
2
+ export * from './validate';
3
+ export * from './scroll-to';
4
+ export * from './treeManager';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 平滑滚动到指定位置
3
+ * @param to 目标位置
4
+ * @param duration 滚动时长(ms)
5
+ * @param callback 完成回调
6
+ */
7
+ export declare function scrollTo(to: number, duration?: number, callback?: () => void): void;
@@ -0,0 +1,34 @@
1
+ import { AnyRecord, DateRangeFieldConfig, InputFieldConfig, SelectFieldConfig, CascadeSelectFieldConfig, CascaderFieldConfig, NumberFieldConfig, SwitchFieldConfig } from '../types';
2
+ /**
3
+ * 通用标准日期格式常量
4
+ */
5
+ export declare const DATE_FORMAT: {
6
+ readonly DATE: "YYYY-MM-DD";
7
+ readonly DATETIME: "YYYY-MM-DD HH:mm:ss";
8
+ readonly TIME: "HH:mm:ss";
9
+ readonly MONTH: "YYYY-MM";
10
+ readonly YEAR: "YYYY";
11
+ readonly DATE_CN: "YYYY年MM月DD日";
12
+ readonly DATETIME_CN: "YYYY年MM月DD日 HH:mm:ss";
13
+ };
14
+ export declare const SearchFieldFactory: {
15
+ dateRange(config?: DateRangeFieldConfig): AnyRecord;
16
+ input(config?: InputFieldConfig): AnyRecord | null;
17
+ select(config?: SelectFieldConfig): AnyRecord | null;
18
+ cascadeSelect(config?: CascadeSelectFieldConfig): AnyRecord | null;
19
+ cascader(config?: CascaderFieldConfig): AnyRecord | null;
20
+ number(config?: NumberFieldConfig): AnyRecord | null;
21
+ date(config?: AnyRecord): AnyRecord | null;
22
+ switch(config?: SwitchFieldConfig): AnyRecord | null;
23
+ };
24
+ export declare const CommonSearchFields: {
25
+ updateTimeRange(config?: DateRangeFieldConfig): AnyRecord;
26
+ createTimeRange(config?: Partial<DateRangeFieldConfig>): AnyRecord;
27
+ status(options: Array<{
28
+ label: string;
29
+ value: any;
30
+ }>, config?: Partial<SelectFieldConfig>): AnyRecord | null;
31
+ keyword(config?: Partial<InputFieldConfig>): AnyRecord | null;
32
+ name(config?: Partial<InputFieldConfig>): AnyRecord | null;
33
+ category(options: any[], config?: Partial<CascaderFieldConfig>): AnyRecord | null;
34
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * 通用树结构处理工具
3
+ */
4
+ /**
5
+ * 递归从树中查找目标节点
6
+ * @param tree 树数组
7
+ * @param predicate 匹配函数
8
+ * @param childrenKey 子节点字段名,默认 'children'
9
+ * @returns 找到的节点对象
10
+ */
11
+ export declare function findTreeNode<T extends Record<string, any>>(tree: T[] | undefined, predicate: (node: T) => boolean, childrenKey?: string): T | null;
12
+ /**
13
+ * 获取从根节点到目标节点的完整祖先路径 (仅值列表,如 [1, 10, 102])
14
+ * @param tree 树结构
15
+ * @param targetValue 目标节点标识
16
+ * @param keyField 匹配键名,如 'id'
17
+ * @param childrenKey 子节点字段名
18
+ * @returns 包含从根到目标节点的所有路径标识
19
+ */
20
+ export declare function findNodePath<T extends Record<string, any>>(tree: T[] | undefined, targetValue: any, keyField?: string, childrenKey?: string): any[];
21
+ /**
22
+ * 获取从根节点到目标节点的完整祖先节点对象列表 (包含目标节点自身)
23
+ * @param tree 树结构
24
+ * @param predicate 匹配条件或目标节点键值
25
+ * @param childrenKey 子节点字段名
26
+ * @returns 祖先节点对象数组,根节点在前
27
+ */
28
+ export declare function findParentNodes<T extends Record<string, any>>(tree: T[] | undefined, predicate: ((node: T) => boolean) | any, childrenKey?: string, keyField?: string): T[];
29
+ /**
30
+ * 树结构扁平化
31
+ * @param tree 树数组
32
+ * @param childrenKey 子节点字段名
33
+ * @returns 扁平化后的数组
34
+ */
35
+ export declare function flattenTree<T extends Record<string, any>>(tree?: T[], childrenKey?: string): T[];
36
+ /** 树转列表别名 */
37
+ export declare const treeToList: typeof flattenTree;
38
+ /**
39
+ * 扁平列表转树结构 (时间复杂度 O(n) 的高效 Map 算法)
40
+ * @param list 扁平列表
41
+ * @param options 配置项 { id: 'id', pid: 'parentId', children: 'children', rootPid: [0, null, undefined, ''] }
42
+ * @returns 构造完成的树数组
43
+ */
44
+ export declare function listToTree<T extends Record<string, any>>(list?: T[], options?: {
45
+ id?: string;
46
+ pid?: string;
47
+ children?: string;
48
+ rootPid?: any[];
49
+ }): T[];
50
+ /**
51
+ * 过滤树结构,保留满足条件的节点及其父链路
52
+ * @param tree 树数组
53
+ * @param predicate 过滤条件
54
+ * @param childrenKey 子节点字段名
55
+ * @returns 过滤后的新树结构
56
+ */
57
+ export declare function filterTree<T extends Record<string, any>>(tree: T[] | undefined, predicate: (node: T) => boolean, childrenKey?: string): T[];
58
+ /**
59
+ * 树结构映射/转换 (类似于数组的 map)
60
+ * @param tree 树数组
61
+ * @param mapper 节点转换器
62
+ * @param childrenKey 子节点字段名
63
+ */
64
+ export declare function mapTree<T extends Record<string, any>, R extends Record<string, any>>(tree: T[] | undefined, mapper: (node: T) => R, childrenKey?: string): R[];
@@ -0,0 +1,30 @@
1
+ /**
2
+ * 路径匹配器
3
+ */
4
+ export declare function isPathMatch(pattern: string, path: string): boolean;
5
+ /**
6
+ * 判断value字符串是否为空
7
+ */
8
+ export declare function isEmpty(value: unknown): boolean;
9
+ /**
10
+ * 判断url是否是http或https
11
+ */
12
+ export declare function isHttp(url: string): boolean;
13
+ /**
14
+ * 判断path是否为外链
15
+ */
16
+ export declare function isExternal(path: string): boolean;
17
+ /**
18
+ * 验证合法 URL
19
+ */
20
+ export declare function validURL(url: string): boolean;
21
+ /**
22
+ * 验证邮箱
23
+ */
24
+ export declare function validEmail(email: string): boolean;
25
+ /**
26
+ * 验证手机号
27
+ */
28
+ export declare function validPhone(phone: string): boolean;
29
+ export declare function isString(str: unknown): str is string;
30
+ export declare function isArray(arg: unknown): arg is any[];
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "c-admin-kit",
3
+ "version": "1.0.0",
4
+ "description": "基于 Vue 3 + Element Plus 的中后台企业级高阶通用组件与 Hooks 套件(强制使用 c- 前缀)",
5
+ "type": "module",
6
+ "main": "./dist/c-admin-kit.umd.cjs",
7
+ "module": "./dist/c-admin-kit.js",
8
+ "types": "./dist/index.d.ts",
9
+ "style": "./dist/c-admin-kit.css",
10
+ "sideEffects": [
11
+ "**/*.css"
12
+ ],
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/c-admin-kit.js",
17
+ "require": "./dist/c-admin-kit.umd.cjs"
18
+ },
19
+ "./dist/c-admin-kit.css": "./dist/c-admin-kit.css",
20
+ "./components": {
21
+ "types": "./dist/components/index.d.ts",
22
+ "import": "./src/components/index.ts"
23
+ },
24
+ "./composables": {
25
+ "types": "./dist/composables/index.d.ts",
26
+ "import": "./src/composables/index.ts"
27
+ },
28
+ "./hooks": {
29
+ "types": "./dist/composables/index.d.ts",
30
+ "import": "./src/composables/index.ts"
31
+ },
32
+ "./utils": {
33
+ "types": "./dist/utils/index.d.ts",
34
+ "import": "./src/utils/index.ts"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src",
40
+ "README.md"
41
+ ],
42
+ "keywords": [
43
+ "vue3",
44
+ "element-plus",
45
+ "components",
46
+ "table",
47
+ "search-box",
48
+ "crud",
49
+ "admin",
50
+ "hooks"
51
+ ],
52
+ "author": "wllcyg",
53
+ "license": "MIT",
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/wllcyg/admin-kit.git"
57
+ },
58
+ "homepage": "https://github.com/wllcyg/admin-kit#readme",
59
+ "bugs": {
60
+ "url": "https://github.com/wllcyg/admin-kit/issues"
61
+ },
62
+ "peerDependencies": {
63
+ "@element-plus/icons-vue": "^2.0.0",
64
+ "dayjs": "^1.11.0",
65
+ "element-plus": "^2.3.0",
66
+ "sortablejs": "^1.15.0",
67
+ "vue": "^3.3.0"
68
+ },
69
+ "devDependencies": {
70
+ "@element-plus/icons-vue": "^2.3.2",
71
+ "@types/node": "^26.6.2",
72
+ "@types/sortablejs": "^1.15.9",
73
+ "@vitejs/plugin-vue": "^5.0.0",
74
+ "dayjs": "^1.11.23",
75
+ "element-plus": "^2.14.6",
76
+ "sortablejs": "^1.15.7",
77
+ "typescript": "^5.7.3",
78
+ "vite": "^5.0.0 || ^6.0.0",
79
+ "vite-plugin-dts": "^5.1.1",
80
+ "vue-tsc": "^3.3.11"
81
+ },
82
+ "scripts": {
83
+ "build": "vue-tsc --noEmit && vite build",
84
+ "typecheck": "vue-tsc --noEmit"
85
+ }
86
+ }
@@ -0,0 +1,58 @@
1
+ <template>
2
+ <el-button
3
+ v-bind="$attrs"
4
+ :loading="isLoading"
5
+ :disabled="isDisabled"
6
+ @click="handleClick"
7
+ >
8
+ <slot />
9
+ </el-button>
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ import { ref, computed, type PropType } from 'vue'
14
+
15
+ defineOptions({ name: 'CAsyncButton' })
16
+
17
+ const props = defineProps({
18
+ /** 接收一个可能是异步的点击处理函数,返回 Promise 时自动开启 loading */
19
+ onClick: {
20
+ type: Function as PropType<(event: MouseEvent) => any>,
21
+ default: null
22
+ },
23
+ /** 手动强制 loading,优先级最高 */
24
+ loading: {
25
+ type: Boolean,
26
+ default: false
27
+ },
28
+ disabled: {
29
+ type: Boolean,
30
+ default: false
31
+ }
32
+ })
33
+
34
+ // 内部自动 loading 状态
35
+ const innerLoading = ref(false)
36
+
37
+ // 实际 loading:外部手动传入 OR 内部自动触发
38
+ const isLoading = computed(() => props.loading || innerLoading.value)
39
+
40
+ // loading 期间同时禁用按钮,防止重复点击
41
+ const isDisabled = computed(() => props.disabled || isLoading.value)
42
+
43
+ async function handleClick(event: MouseEvent) {
44
+ if (!props.onClick || isDisabled.value) return
45
+
46
+ const result = props.onClick(event)
47
+
48
+ // 自动检测是否为 Promise 或 Thenable 对象,是则开启 loading
49
+ if (result && typeof (result as any).then === 'function') {
50
+ innerLoading.value = true
51
+ try {
52
+ await result
53
+ } finally {
54
+ innerLoading.value = false
55
+ }
56
+ }
57
+ }
58
+ </script>