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,582 @@
1
+ <template>
2
+ <div class="search-box">
3
+ <el-form
4
+ :model="searchForm"
5
+ :label-width="labelWidth"
6
+ class="search-form"
7
+ @keyup="handleFormKeyup"
8
+ >
9
+ <!-- 搜索字段区域 -->
10
+ <el-row :gutter="16" class="search-fields">
11
+ <el-col
12
+ v-for="field in displayFields"
13
+ :key="field.prop"
14
+ :xs="24"
15
+ :sm="field.props?.span || 12"
16
+ :md="field.props?.span || colSpan"
17
+ :lg="field.props?.span || colSpan"
18
+ :xl="field.props?.span || colSpan"
19
+ class="field-col"
20
+ >
21
+ <el-form-item
22
+ :label="field.label"
23
+ :prop="field.prop"
24
+ :class="{ 'is-required': field.required }"
25
+ >
26
+ <!-- 动态组件 -->
27
+ <component
28
+ :is="field.component || field.type"
29
+ v-model="searchForm[field.prop]"
30
+ v-bind="getComponentProps(field)"
31
+ style="width: 100%"
32
+ @change="(value: any) => handleFieldChange(field.prop, value)"
33
+ @keydown.enter="
34
+ field.component === 'el-input' ? handleSearch : null
35
+ "
36
+ >
37
+ <!-- 选择器选项 -->
38
+ <template
39
+ v-if="
40
+ field.component === 'el-select' || field.type === 'select'
41
+ "
42
+ >
43
+ <el-option
44
+ v-for="option in getFieldOptions(field)"
45
+ :key="option.value"
46
+ :label="option.label"
47
+ :value="option.value"
48
+ />
49
+ </template>
50
+
51
+ <!-- 单选框选项 -->
52
+ <template
53
+ v-else-if="
54
+ field.component === 'el-radio-group' || field.type === 'radio'
55
+ "
56
+ >
57
+ <el-radio
58
+ v-for="option in getFieldOptions(field)"
59
+ :key="option.value"
60
+ :label="option.value"
61
+ >
62
+ {{ option.label }}
63
+ </el-radio>
64
+ </template>
65
+
66
+ <!-- 复选框选项 -->
67
+ <template
68
+ v-else-if="
69
+ field.component === 'el-checkbox-group' ||
70
+ field.type === 'checkbox'
71
+ "
72
+ >
73
+ <el-checkbox
74
+ v-for="option in getFieldOptions(field)"
75
+ :key="option.value"
76
+ :label="option.value"
77
+ >
78
+ {{ option.label }}
79
+ </el-checkbox>
80
+ </template>
81
+ </component>
82
+
83
+ <!-- 自定义插槽 -->
84
+ <slot
85
+ v-if="field.type === 'slot'"
86
+ :name="field.slotName"
87
+ :value="searchForm[field.prop]"
88
+ :onChange="(val: any) => (searchForm[field.prop] = val)"
89
+ />
90
+ </el-form-item>
91
+ </el-col>
92
+ </el-row>
93
+
94
+ <!-- 操作按钮区域 -->
95
+ <el-row class="action-row">
96
+ <el-col :span="24" class="action-col">
97
+ <div class="button-group">
98
+ <el-button type="primary" :loading="loading" @click="handleSearch">
99
+ {{ searchText }}
100
+ </el-button>
101
+ <el-button @click="handleReset">{{ resetText }}</el-button>
102
+ <el-button v-if="showExport" type="success" @click="handleExport">
103
+ {{ exportText }}
104
+ </el-button>
105
+ <!-- 展开/折叠按钮 -->
106
+ <el-button
107
+ v-if="canToggle"
108
+ type="text"
109
+ class="toggle-btn"
110
+ @click="toggleExpand"
111
+ >
112
+ {{ isExpanded ? "收起" : "展开" }}
113
+ <el-icon class="toggle-icon">
114
+ <ArrowUp v-if="isExpanded" />
115
+ <ArrowDown v-else />
116
+ </el-icon>
117
+ </el-button>
118
+ </div>
119
+ </el-col>
120
+ </el-row>
121
+ </el-form>
122
+ </div>
123
+ </template>
124
+
125
+ <script setup lang="ts">
126
+ import { ref, computed, watch, reactive, onMounted, unref, type PropType } from "vue";
127
+ import { ArrowUp, ArrowDown } from "@element-plus/icons-vue";
128
+ import type { AnyRecord, SearchField } from "../../types";
129
+
130
+ defineOptions({ name: "CSearchBox" });
131
+
132
+ const props = defineProps({
133
+ fields: {
134
+ type: Array as PropType<SearchField[]>,
135
+ default: () => [],
136
+ },
137
+ searchText: {
138
+ type: String,
139
+ default: "查询",
140
+ },
141
+ resetText: {
142
+ type: String,
143
+ default: "重置",
144
+ },
145
+ exportText: {
146
+ type: String,
147
+ default: "导出",
148
+ },
149
+ showExport: {
150
+ type: Boolean,
151
+ default: false,
152
+ },
153
+ initialValues: {
154
+ type: Object as PropType<AnyRecord>,
155
+ default: () => ({}),
156
+ },
157
+ fieldsPerRow: {
158
+ type: Number,
159
+ default: 4,
160
+ },
161
+ labelWidth: {
162
+ type: String,
163
+ default: "80px",
164
+ },
165
+ defaultExpanded: {
166
+ type: Boolean,
167
+ default: false,
168
+ },
169
+ });
170
+
171
+ const emit = defineEmits(["search", "reset", "export", "field-change"]);
172
+
173
+ const loading = ref(false);
174
+ const isExpanded = ref(props.defaultExpanded);
175
+ const optionsMap = reactive<Record<string, any[]>>({});
176
+
177
+ const isFieldVisible = (field: SearchField) => {
178
+ if (field.visible === undefined) return true;
179
+ if (typeof field.visible === "function") {
180
+ return field.visible(searchForm.value);
181
+ }
182
+ return unref(field.visible);
183
+ };
184
+
185
+ const visibleFields = computed(() => {
186
+ return props.fields.filter((field) => isFieldVisible(field));
187
+ });
188
+
189
+ const colSpan = computed(() => {
190
+ return Math.floor(24 / props.fieldsPerRow);
191
+ });
192
+
193
+ const defaultFieldCount = computed(() => {
194
+ return 2 * props.fieldsPerRow;
195
+ });
196
+
197
+ const canToggle = computed(() => {
198
+ return visibleFields.value.length > defaultFieldCount.value;
199
+ });
200
+
201
+ const displayFields = computed(() => {
202
+ if (isExpanded.value || !canToggle.value) {
203
+ return visibleFields.value;
204
+ }
205
+ return visibleFields.value.slice(0, defaultFieldCount.value);
206
+ });
207
+
208
+ const initSearchForm = (): AnyRecord => {
209
+ const form: AnyRecord = {};
210
+ props.fields.forEach((field) => {
211
+ if (props.initialValues[field.prop] !== undefined) {
212
+ form[field.prop] = props.initialValues[field.prop];
213
+ } else {
214
+ switch (field.type) {
215
+ case "select":
216
+ form[field.prop] = field.multiple ? [] : "";
217
+ break;
218
+ case "daterange":
219
+ form[field.prop] = [];
220
+ break;
221
+ case "number":
222
+ form[field.prop] = field.defaultValue ?? null;
223
+ break;
224
+ default:
225
+ form[field.prop] = field.defaultValue ?? "";
226
+ }
227
+ }
228
+ });
229
+ return form;
230
+ };
231
+
232
+ const searchForm = ref<AnyRecord>(initSearchForm());
233
+
234
+ const loadFieldOptions = async (field: SearchField, dependValue: any) => {
235
+ if (!field.relation) return;
236
+
237
+ const { getOptions } = field.relation;
238
+
239
+ if (dependValue === undefined || dependValue === "" || dependValue === null) {
240
+ optionsMap[field.prop] = field.defaultOptions || [];
241
+ return;
242
+ }
243
+
244
+ try {
245
+ let options: any[] = [];
246
+ if (typeof getOptions === "function") {
247
+ const res = getOptions(dependValue, searchForm.value);
248
+ options = res instanceof Promise ? await res : res;
249
+ } else if (typeof getOptions === "object" && (getOptions as any)[dependValue]) {
250
+ options = (getOptions as any)[dependValue];
251
+ }
252
+ optionsMap[field.prop] = options || [];
253
+ } catch (error) {
254
+ console.error(`加载字段 ${field.prop} 选项失败:`, error);
255
+ optionsMap[field.prop] = [];
256
+ }
257
+ };
258
+
259
+ const initOptions = () => {
260
+ props.fields.forEach((field) => {
261
+ if (field.relation) {
262
+ const { dependOn } = field.relation;
263
+ const dependValue = searchForm.value[dependOn];
264
+ loadFieldOptions(field, dependValue);
265
+ }
266
+ });
267
+ };
268
+
269
+ const getComponentProps = (field: SearchField) => {
270
+ const baseProps = {
271
+ clearable: field.clearable !== false,
272
+ placeholder:
273
+ field.placeholder ||
274
+ `请${
275
+ field.type === "input" || field.type === "number" ? "输入" : "选择"
276
+ }${field.label}`,
277
+ };
278
+
279
+ return {
280
+ ...baseProps,
281
+ ...field.props,
282
+ };
283
+ };
284
+
285
+ const getFieldOptions = (field: SearchField) => {
286
+ if (field.relation) {
287
+ return optionsMap[field.prop] || field.defaultOptions || [];
288
+ }
289
+ return field.options || [];
290
+ };
291
+
292
+ const handleFieldChange = (prop: string, value: any) => {
293
+ const affectedFields = props.fields.filter(
294
+ (field) => field.relation && field.relation.dependOn === prop
295
+ );
296
+
297
+ affectedFields.forEach((field) => {
298
+ if (field.multiple) {
299
+ searchForm.value[field.prop] = [];
300
+ } else {
301
+ searchForm.value[field.prop] = "";
302
+ }
303
+ loadFieldOptions(field, value);
304
+ });
305
+
306
+ emit("field-change", { prop, value, form: { ...searchForm.value } });
307
+ };
308
+
309
+ const toggleExpand = () => {
310
+ isExpanded.value = !isExpanded.value;
311
+ };
312
+
313
+ const processDateRangeFields = (formData: AnyRecord) => {
314
+ const params: AnyRecord = {};
315
+
316
+ props.fields.forEach((field) => {
317
+ if (!isFieldVisible(field)) return;
318
+
319
+ const value = formData[field.prop];
320
+
321
+ if (field.dateRange) {
322
+ if (Array.isArray(value) && value.length === 2 && value[0] && value[1]) {
323
+ const { startField, endField, format } = field.dateRange;
324
+ if (typeof format === "function") {
325
+ params[startField] = format(value[0]);
326
+ params[endField] = format(value[1]);
327
+ } else {
328
+ params[startField] = value[0];
329
+ params[endField] = value[1];
330
+ }
331
+ }
332
+ } else if (field.returnAllIds) {
333
+ const { fileIds } = field as any;
334
+ if (value) {
335
+ const valueArr = Array.isArray(value) ? value : [value];
336
+ fileIds?.forEach((v: string, ind: number) => {
337
+ const item = valueArr[ind];
338
+ if (item) params[v] = item;
339
+ });
340
+ }
341
+ const valueArr = Array.isArray(value) ? value : [value];
342
+ const lastItem = valueArr[valueArr.length - 1];
343
+ if (lastItem) {
344
+ params["currentKey"] = lastItem;
345
+ params[field.prop] = lastItem;
346
+ }
347
+ } else {
348
+ if (value !== "" && value !== null && value !== undefined) {
349
+ if (Array.isArray(value) && value.length === 0) return;
350
+ params[field.prop] = value;
351
+ }
352
+ }
353
+ });
354
+
355
+ return params;
356
+ };
357
+
358
+ const handleSearch = () => {
359
+ loading.value = true;
360
+ const params = processDateRangeFields(searchForm.value);
361
+ emit("search", params);
362
+ setTimeout(() => {
363
+ loading.value = false;
364
+ }, 300);
365
+ };
366
+
367
+ const handleReset = () => {
368
+ searchForm.value = initSearchForm();
369
+ initOptions();
370
+ const params = processDateRangeFields(searchForm.value);
371
+ emit("reset", params);
372
+ };
373
+
374
+ const handleExport = () => {
375
+ const params = processDateRangeFields(searchForm.value);
376
+ emit("export", params);
377
+ };
378
+
379
+ watch(
380
+ () => props.fields,
381
+ (newFields, oldFields) => {
382
+ const fieldsChanged = () => {
383
+ if (!oldFields || newFields.length !== oldFields.length) return true;
384
+ return newFields.some((newField, index) => {
385
+ const oldField = oldFields[index];
386
+ if (!oldField) return true;
387
+ const { options: newOptions, ...newFieldRest } = newField as AnyRecord;
388
+ const { options: oldOptions, ...oldFieldRest } = oldField as AnyRecord;
389
+ return JSON.stringify(newFieldRest) !== JSON.stringify(oldFieldRest);
390
+ });
391
+ };
392
+
393
+ if (fieldsChanged()) {
394
+ searchForm.value = initSearchForm();
395
+ }
396
+ initOptions();
397
+ },
398
+ { deep: true }
399
+ );
400
+
401
+ watch(
402
+ () => props.initialValues,
403
+ (newVal) => {
404
+ Object.keys(newVal || {}).forEach((key) => {
405
+ if (key in searchForm.value) {
406
+ searchForm.value[key] = (newVal as AnyRecord)[key];
407
+ }
408
+ });
409
+ initOptions();
410
+ },
411
+ { deep: true }
412
+ );
413
+
414
+ const handleFormKeyup = (event: KeyboardEvent) => {
415
+ if (event.key === "Enter") {
416
+ const target = event.target as HTMLElement | null;
417
+ if (!target) return;
418
+ const isInputElement =
419
+ target.tagName === "INPUT" || target.tagName === "TEXTAREA";
420
+ const isInsidePopper =
421
+ target.closest(".el-popper") || target.closest(".el-select-dropdown");
422
+
423
+ if (isInputElement && !isInsidePopper) {
424
+ handleSearch();
425
+ }
426
+ }
427
+ };
428
+
429
+ onMounted(() => {
430
+ initOptions();
431
+ });
432
+
433
+ defineExpose({
434
+ search: handleSearch,
435
+ reset: handleReset,
436
+ export: handleExport,
437
+ getParams: () => processDateRangeFields(searchForm.value),
438
+ getAllParams: () => ({ ...searchForm.value }),
439
+ setValues: (values: AnyRecord) => {
440
+ Object.keys(values || {}).forEach((key) => {
441
+ if (key in searchForm.value) {
442
+ searchForm.value[key] = values[key];
443
+ }
444
+ });
445
+ initOptions();
446
+ },
447
+ toggleExpand: () => {
448
+ if (canToggle.value) {
449
+ toggleExpand();
450
+ }
451
+ },
452
+ });
453
+ </script>
454
+
455
+ <style scoped>
456
+ .search-box {
457
+ border-radius: 4px;
458
+ margin-bottom: 16px;
459
+ padding-bottom: 12px;
460
+ }
461
+
462
+ .search-form {
463
+ width: 100%;
464
+ }
465
+
466
+ .search-fields {
467
+ width: 100%;
468
+ }
469
+
470
+ .field-col {
471
+ margin-bottom: 12px;
472
+ }
473
+
474
+ .action-col {
475
+ display: flex;
476
+ justify-content: flex-end;
477
+ align-items: center;
478
+ }
479
+
480
+ .search-form :deep(.el-form-item) {
481
+ margin-bottom: 0;
482
+ display: flex;
483
+ align-items: center;
484
+ width: 100%;
485
+ }
486
+
487
+ .search-form :deep(.el-form-item__label) {
488
+ flex-shrink: 0;
489
+ font-weight: 500;
490
+ color: #606266;
491
+ line-height: 32px;
492
+ min-width: v-bind(labelWidth);
493
+ width: v-bind(labelWidth);
494
+ padding-right: 12px;
495
+ white-space: nowrap;
496
+ overflow: hidden;
497
+ text-overflow: ellipsis;
498
+ }
499
+
500
+ .search-form :deep(.el-form-item__content) {
501
+ flex: 1;
502
+ line-height: 32px;
503
+ min-width: 0;
504
+ width: 100%;
505
+ }
506
+
507
+ .is-required :deep(.el-form-item__label::before) {
508
+ content: "*";
509
+ color: #f56c6c;
510
+ margin-right: 4px;
511
+ }
512
+
513
+ .button-group {
514
+ display: flex;
515
+ gap: 8px;
516
+ align-items: center;
517
+ flex-wrap: wrap;
518
+ }
519
+
520
+ .toggle-btn {
521
+ padding: 8px 15px;
522
+ color: #409eff;
523
+ display: inline-flex;
524
+ align-items: center;
525
+ gap: 4px;
526
+ }
527
+
528
+ .toggle-btn:hover {
529
+ color: #66b1ff;
530
+ }
531
+
532
+ .toggle-icon {
533
+ font-size: 12px;
534
+ transition: transform 0.3s;
535
+ }
536
+
537
+ .search-form :deep(.el-input),
538
+ .search-form :deep(.el-select),
539
+ .search-form :deep(.el-date-editor),
540
+ .search-form :deep(.el-input-number),
541
+ .search-form :deep(.el-cascader) {
542
+ width: 100%;
543
+ flex: 1;
544
+ }
545
+
546
+ @media (max-width: 768px) {
547
+ .search-box {
548
+ padding: 16px;
549
+ }
550
+
551
+ .search-form :deep(.el-form-item) {
552
+ flex-direction: column;
553
+ align-items: flex-start;
554
+ }
555
+
556
+ .search-form :deep(.el-form-item__label) {
557
+ line-height: 20px;
558
+ margin-bottom: 8px;
559
+ width: 100%;
560
+ min-width: auto;
561
+ white-space: normal;
562
+ }
563
+
564
+ .action-col {
565
+ justify-content: flex-start;
566
+ }
567
+
568
+ .button-group {
569
+ width: 100%;
570
+ flex-direction: column;
571
+ }
572
+
573
+ .button-group .el-button {
574
+ width: 100%;
575
+ }
576
+
577
+ .toggle-btn {
578
+ width: 100%;
579
+ justify-content: center;
580
+ }
581
+ }
582
+ </style>