p-pc-ui 1.3.11 → 1.3.13

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.
@@ -56,7 +56,24 @@ export interface InputCode<K extends string = string> extends Base<K> {
56
56
 
57
57
 
58
58
 
59
- type LoadOptionsData = () => Promise<{ label: string, value: any }[]>
59
+ type LoadOptionsParams = {
60
+ p?: number,
61
+ pc?: number,
62
+ [key: string]: any,
63
+ }
64
+
65
+ type LoadOptionsResult =
66
+ | any[]
67
+ | {
68
+ data?: any[],
69
+ count?: number,
70
+ pages?: {
71
+ total?: number,
72
+ now?: number,
73
+ },
74
+ }
75
+
76
+ type LoadOptionsData = (params?: LoadOptionsParams) => Promise<LoadOptionsResult>
60
77
 
61
78
 
62
79
  interface SelectBase<K extends string = string> extends Base<K> {
@@ -65,7 +82,12 @@ interface SelectBase<K extends string = string> extends Base<K> {
65
82
  label?: string;
66
83
  value?: string;
67
84
  };
68
- isMultiple?: boolean
85
+ isMultiple?: boolean,
86
+ enableSearch?: boolean,
87
+ pageSize?: number,
88
+ pageNumKey?: string,
89
+ pageSizeKey?: string,
90
+ keywordKey?: string,
69
91
  }
70
92
 
71
93
  type GetOptionList =
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { ref, reactive, onMounted, render, onBeforeMount, useSlots, toRaw, shallowRef } from "vue";
2
+ import { ref, reactive, onMounted, render, onBeforeMount, useSlots, toRaw, shallowRef, isRef, isReactive } from "vue";
3
3
  import type { FormDataItem } from "./index";
4
4
  import {
5
5
  Button as AButton,
@@ -16,6 +16,7 @@ import {
16
16
  Upload as AUpload,
17
17
  Textarea as ATextarea,
18
18
  Space as ASpace,
19
+ DatePicker as ADatePicker,
19
20
  } from "ant-design-vue";
20
21
  import type { FormInstance } from "ant-design-vue";
21
22
  import * as _ from "../../utils/dataUtils";
@@ -23,7 +24,6 @@ import * as _ from "../../utils/dataUtils";
23
24
  import { api as viewerApi } from "v-viewer";
24
25
  import dayjs from "dayjs";
25
26
  import { UploadOutlined, PlusOutlined } from "@ant-design/icons-vue";
26
- import { log } from "echarts/types/src/util/log.js";
27
27
 
28
28
  const uSlots = useSlots();
29
29
 
@@ -59,6 +59,169 @@ const formRef = ref<FormInstance>();
59
59
 
60
60
  const emits = defineEmits(["confirm"]);
61
61
 
62
+ const DEFAULT_SELECT_PAGE_SIZE = 20;
63
+
64
+ type SelectLoadResult =
65
+ | any[]
66
+ | {
67
+ data?: any[];
68
+ count?: number;
69
+ pages?: {
70
+ total?: number;
71
+ now?: number;
72
+ };
73
+ };
74
+
75
+ type SelectLoadState = {
76
+ pageNum: number;
77
+ pageSize: number;
78
+ keyword: string;
79
+ hasMore: boolean;
80
+ loading: boolean;
81
+ };
82
+
83
+ const getSelectValueField = (renderItem: any) => {
84
+ return renderItem.fieldNames?.value || "value";
85
+ };
86
+
87
+ const ensureSelectLoadState = (renderItem: FormDataItem & { [key: string]: any }) => {
88
+ if (!renderItem.__selectState) {
89
+ renderItem.__selectState = {
90
+ pageNum: 1,
91
+ pageSize: renderItem.pageSize || DEFAULT_SELECT_PAGE_SIZE,
92
+ keyword: "",
93
+ hasMore: true,
94
+ loading: false,
95
+ } as SelectLoadState;
96
+ }
97
+
98
+ return renderItem.__selectState as SelectLoadState;
99
+ };
100
+
101
+ const mergeSelectOptionList = (renderItem: FormDataItem & { [key: string]: any }, list: any[]) => {
102
+ const valueKey = getSelectValueField(renderItem);
103
+ const oldList = renderItem.optionList || [];
104
+ const map = new Map<any, any>();
105
+
106
+ for (const item of oldList) {
107
+ map.set(item?.[valueKey], item);
108
+ }
109
+
110
+ for (const item of list) {
111
+ map.set(item?.[valueKey], item);
112
+ }
113
+
114
+ renderItem.optionList = Array.from(map.values());
115
+ };
116
+
117
+ const buildSelectLoadParams = (renderItem: FormDataItem & { [key: string]: any }, state: SelectLoadState) => {
118
+ const pageNumKey = renderItem.pageNumKey || "p";
119
+ const pageSizeKey = renderItem.pageSizeKey || "pc";
120
+ const keywordKey = renderItem.keywordKey || "keyword";
121
+ const params: Record<string, any> = {
122
+ p: state.pageNum,
123
+ pc: state.pageSize,
124
+ [pageNumKey]: state.pageNum,
125
+ [pageSizeKey]: state.pageSize,
126
+ };
127
+
128
+ const keyword = (state.keyword || "").replaceAll("=", "").trim();
129
+ if (!keyword) {
130
+ return params;
131
+ }
132
+
133
+ params[keywordKey] = keyword;
134
+
135
+ return params;
136
+ };
137
+
138
+ const normalizeSelectLoadResult = (res: SelectLoadResult, state: SelectLoadState) => {
139
+ const list = Array.isArray(res) ? res : res?.data || [];
140
+
141
+ const hasMore = Array.isArray(res)
142
+ ? list.length >= state.pageSize
143
+ : typeof res?.pages?.total === "number" && typeof res?.pages?.now === "number"
144
+ ? res.pages.now < res.pages.total
145
+ : typeof res?.count === "number"
146
+ ? state.pageNum * state.pageSize < res.count
147
+ : list.length >= state.pageSize;
148
+
149
+ return {
150
+ list,
151
+ hasMore,
152
+ };
153
+ };
154
+
155
+ const fetchSelectOptions = async (
156
+ renderItem: FormDataItem & { [key: string]: any },
157
+ options: { reset?: boolean; keyword?: string } = {},
158
+ ) => {
159
+ if (renderItem.type !== "select" || !renderItem.loadDataFunc) return;
160
+ const state = ensureSelectLoadState(renderItem);
161
+
162
+ if (state.loading) return;
163
+
164
+ if (options.keyword != undefined) {
165
+ state.keyword = options.keyword;
166
+ }
167
+
168
+ if (options.reset) {
169
+ state.pageNum = 1;
170
+ state.hasMore = true;
171
+ renderItem.optionList = [];
172
+ }
173
+
174
+ if (!state.hasMore) return;
175
+
176
+ state.loading = true;
177
+ try {
178
+ const res = (await renderItem.loadDataFunc(buildSelectLoadParams(renderItem, state))) as SelectLoadResult;
179
+ const normalizedRes = normalizeSelectLoadResult(res, state);
180
+ const list = normalizedRes.list;
181
+ if (options.reset) {
182
+ renderItem.optionList = [];
183
+ }
184
+ mergeSelectOptionList(renderItem, list);
185
+
186
+ state.hasMore = normalizedRes.hasMore;
187
+ if (list.length > 0) {
188
+ state.pageNum += 1;
189
+ }
190
+ } finally {
191
+ state.loading = false;
192
+ }
193
+ };
194
+
195
+ const selectSearchTimerMap = new WeakMap<any, number>();
196
+
197
+ const onSelectSearch = (keyword: string, renderItem: FormDataItem & { [key: string]: any }) => {
198
+ if (renderItem.type !== "select") return;
199
+ if (!renderItem.loadDataFunc) return;
200
+
201
+ const timer = selectSearchTimerMap.get(renderItem);
202
+ if (timer) {
203
+ clearTimeout(timer);
204
+ }
205
+
206
+ const nextTimer = window.setTimeout(() => {
207
+ fetchSelectOptions(renderItem, { reset: true, keyword: keyword || "" });
208
+ }, 300);
209
+ selectSearchTimerMap.set(renderItem, nextTimer);
210
+ };
211
+
212
+ const onSelectPopupScroll = (e: Event, renderItem: FormDataItem & { [key: string]: any }) => {
213
+ if (renderItem.type !== "select") return;
214
+ if (!renderItem.loadDataFunc) return;
215
+
216
+ const target = e.target as HTMLElement;
217
+ if (!target) return;
218
+
219
+ const distanceToBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
220
+ if (distanceToBottom <= 24) {
221
+ fetchSelectOptions(renderItem);
222
+ }
223
+ };
224
+
62
225
  /**
63
226
  * 富文本相关配置
64
227
  */
@@ -95,7 +258,6 @@ const disposeRenderData = async (renderData: FormDataItem[]) => {
95
258
  fieldType = "array";
96
259
  }
97
260
 
98
-
99
261
  if (required) {
100
262
  rules.push({
101
263
  type: renderItem.type == "uploadOss" || renderItem.type == "uploadTos" ? "array" : renderItem.fieldType,
@@ -131,8 +293,6 @@ const disposeRenderData = async (renderData: FormDataItem[]) => {
131
293
  }
132
294
  }
133
295
 
134
-
135
-
136
296
  renderItem.rules = rules;
137
297
 
138
298
  // 处理下拉选择
@@ -146,8 +306,12 @@ const disposeRenderData = async (renderData: FormDataItem[]) => {
146
306
  renderItem.placeholder = "加载中" + point.join("");
147
307
  }, 500);
148
308
 
149
- const res = await renderItem.loadDataFunc();
150
- renderItem["optionList"] = res;
309
+ if (renderItem.type == "select") {
310
+ await fetchSelectOptions(renderItem as FormDataItem & { [key: string]: any }, { reset: true });
311
+ } else {
312
+ const res = await renderItem.loadDataFunc();
313
+ renderItem["optionList"] = res as any[];
314
+ }
151
315
  clearInterval(interval);
152
316
  renderItem.placeholder = `请选择${renderItem.label}`;
153
317
  }
@@ -225,12 +389,21 @@ const initFormState = (renderData: FormDataItem[], initFromData = {}) => {
225
389
  renderItem.optionList = [];
226
390
  }
227
391
 
392
+ if (renderItem.type == "select" && renderItem.isMultiple) {
393
+ if (!_.isArray(initValue)) {
394
+ initValue = [];
395
+ }
396
+ }
397
+
228
398
  if (!initValue && renderItem.optionList) {
229
- initValue = renderItem["optionList"][0]?.id;
399
+ if (!(renderItem.type == "select" && renderItem.isMultiple)) {
400
+ const valueKey = renderItem.fieldNames?.value || (renderItem.type == "treeSelect" ? "id" : "value");
401
+ initValue = renderItem["optionList"][0]?.[valueKey];
402
+ }
230
403
  }
231
404
 
232
405
  if (renderItem.loadDataFunc) {
233
- initValue = undefined;
406
+ initValue = renderItem.type == "select" && renderItem.isMultiple ? [] : undefined;
234
407
  }
235
408
  } else if (renderItem.type == "uploadOss") {
236
409
  if (!_.isArray(initValue)) {
@@ -315,6 +488,10 @@ const getFormData = async (options: { no_check?: Boolean } = {}) => {
315
488
  _.set(returnFormData, key.split("__"), formData[key]);
316
489
  } else if (renderItem.type == "datePicker") {
317
490
  returnFormData[key] = dayjs(formData[key]).format("YYYY-MM-DD HH:mm:ss");
491
+ }
492
+ else if ( isRef(formData[key]) || isReactive(formData[key]) ) {
493
+
494
+ returnFormData[key] = toRaw(formData[key]);
318
495
  } else {
319
496
  returnFormData[key] = formData[key];
320
497
  }
@@ -334,9 +511,13 @@ const resetForm = () => {
334
511
  * 确认按钮点击
335
512
  */
336
513
  const confirmClick = async () => {
337
- const formData = await getFormData();
338
- if (Object.keys(formData).length == 0) return;
339
- emits("confirm", formData, resetForm);
514
+ try {
515
+ const formData = await getFormData();
516
+ if (Object.keys(formData).length == 0) return;
517
+ emits("confirm", formData, resetForm);
518
+ } catch (error) {
519
+ console.warn("Form validation failed:", error);
520
+ }
340
521
  };
341
522
 
342
523
  const uploadChange = (e, renderItem) => {
@@ -445,26 +626,46 @@ defineExpose({
445
626
  </script>
446
627
 
447
628
  <template>
448
- <a-form ref="formRef" :model="formState" name="basic" autocomplete="off" :layout="layout"
449
- :required-mark="requiredMark">
629
+ <a-form
630
+ ref="formRef"
631
+ :model="formState"
632
+ name="basic"
633
+ autocomplete="off"
634
+ :layout="layout"
635
+ :required-mark="requiredMark"
636
+ >
450
637
  <div v-for="(renderItem, index) in renderData">
451
- <a-form-item :label-col="{ span: renderItem.hideLabel ? 0 : renderItem.labelSpan || labelSpan }"
638
+ <a-form-item
639
+ :label-col="{ span: renderItem.hideLabel ? 0 : renderItem.labelSpan || labelSpan }"
452
640
  :wrapper-col="{ span: renderItem.hideLabel ? undefined : renderItem.valueSpan || valueSpan }"
453
641
  v-show="!renderItem.visibleHook || (renderItem.visibleHook && renderItem.visibleHook(formState))"
454
- :label="renderItem.label && !renderItem.hideLabel ? renderItem.label : ''" :name="renderItem.key"
455
- :colon="renderItem.colon == undefined ? true : renderItem.colon" :rules="!renderItem.visibleHook || (renderItem.visibleHook && renderItem.visibleHook(formState))
456
- ? renderItem.rules
457
- : undefined
458
- " :style="{
459
- marginBottom: isSearchForm ? '20px' : '0px',
460
- }" :tooltip="renderItem.tooltip" :label-align="renderItem.labelAlign || 'right'">
461
- <div :style="{
462
- opacity: renderItem.disabled ? 0.6 : 1,
463
- cursor: renderItem.disabled ? 'not-allowed' : 'auto',
464
- }">
465
- <a-input v-model:value="formState[renderItem.key]"
466
- v-if="renderItem.type == 'input' && [undefined, 'string'].includes(renderItem.fieldType)" type="text"
467
- :disabled="renderItem.disabled" :placeholder="renderItem.placeholder || `请输入${renderItem.label}`">
642
+ :label="renderItem.label && !renderItem.hideLabel ? renderItem.label : ''"
643
+ :name="renderItem.key"
644
+ :colon="renderItem.colon == undefined ? true : renderItem.colon"
645
+ :rules="
646
+ !renderItem.visibleHook || (renderItem.visibleHook && renderItem.visibleHook(formState))
647
+ ? renderItem.rules
648
+ : undefined
649
+ "
650
+ :style="{
651
+ marginBottom: isSearchForm ? '20px' : '0px',
652
+ }"
653
+ :tooltip="renderItem.tooltip"
654
+ :label-align="renderItem.labelAlign || 'right'"
655
+ >
656
+ <div
657
+ :style="{
658
+ opacity: renderItem.disabled ? 0.6 : 1,
659
+ cursor: renderItem.disabled ? 'not-allowed' : 'auto',
660
+ }"
661
+ >
662
+ <a-input
663
+ v-model:value="formState[renderItem.key]"
664
+ v-if="renderItem.type == 'input' && [undefined, 'string'].includes(renderItem.fieldType)"
665
+ type="text"
666
+ :disabled="renderItem.disabled"
667
+ :placeholder="renderItem.placeholder || `请输入${renderItem.label}`"
668
+ >
468
669
  <template v-if="renderItem.prefix" v-slot:prefix>
469
670
  <component :is="renderItem.prefix" />
470
671
  </template>
@@ -473,9 +674,13 @@ defineExpose({
473
674
  </template>
474
675
  </a-input>
475
676
 
476
- <a-input v-model:value.number="formState[renderItem.key]"
477
- v-if="renderItem.type == 'input' && renderItem.fieldType == 'number'" type="number"
478
- :disabled="renderItem.disabled" :placeholder="renderItem.placeholder || `请输入${renderItem.label}`">
677
+ <a-input
678
+ v-model:value.number="formState[renderItem.key]"
679
+ v-if="renderItem.type == 'input' && renderItem.fieldType == 'number'"
680
+ type="number"
681
+ :disabled="renderItem.disabled"
682
+ :placeholder="renderItem.placeholder || `请输入${renderItem.label}`"
683
+ >
479
684
  <template v-if="renderItem.prefix" v-slot:prefix>
480
685
  <component :is="renderItem.prefix" />
481
686
  </template>
@@ -484,25 +689,37 @@ defineExpose({
484
689
  </template>
485
690
  </a-input>
486
691
 
487
- <a-input-password v-model:value="formState[renderItem.key]" v-if="renderItem.type == 'password'"
488
- type="password" :disabled="renderItem.disabled"
489
- :placeholder="renderItem.placeholder || `请输入${renderItem.label}`">
692
+ <a-input-password
693
+ v-model:value="formState[renderItem.key]"
694
+ v-if="renderItem.type == 'password'"
695
+ type="password"
696
+ :disabled="renderItem.disabled"
697
+ :placeholder="renderItem.placeholder || `请输入${renderItem.label}`"
698
+ >
490
699
  <template v-if="renderItem.prefix" v-slot:prefix>
491
700
  <component :is="renderItem.prefix" />
492
701
  </template>
493
702
  </a-input-password>
494
703
 
495
- <a-input v-model:value="formState[renderItem.key]"
496
- v-if="renderItem.type == 'code' && [undefined, 'string'].includes(renderItem.fieldType)" type="text"
497
- :disabled="renderItem.disabled" :placeholder="renderItem.placeholder || `请输入${renderItem.label}`">
704
+ <a-input
705
+ v-model:value="formState[renderItem.key]"
706
+ v-if="renderItem.type == 'code' && [undefined, 'string'].includes(renderItem.fieldType)"
707
+ type="text"
708
+ :disabled="renderItem.disabled"
709
+ :placeholder="renderItem.placeholder || `请输入${renderItem.label}`"
710
+ >
498
711
  <template v-if="renderItem.prefix" v-slot:prefix>
499
712
  <component :is="renderItem.prefix" />
500
713
  </template>
501
714
  <template v-slot:suffix>
502
- <span v-if="
503
- (!renderItem.activityFunc || (renderItem.activityFunc && renderItem.activityFunc(formState))) &&
504
- renderItem.sendStatus != 'sending'
505
- " class="send-notice send-notice-activity" @click="sendNoticeClick(renderItem)">
715
+ <span
716
+ v-if="
717
+ (!renderItem.activityFunc || (renderItem.activityFunc && renderItem.activityFunc(formState))) &&
718
+ renderItem.sendStatus != 'sending'
719
+ "
720
+ class="send-notice send-notice-activity"
721
+ @click="sendNoticeClick(renderItem)"
722
+ >
506
723
  {{ renderItem.codeName }}
507
724
  </span>
508
725
  <span v-else class="send-notice">
@@ -511,44 +728,83 @@ defineExpose({
511
728
  </template>
512
729
  </a-input>
513
730
 
514
- <a-textarea v-model:value="formState[renderItem.key]" v-if="renderItem.type == 'textarea'"
515
- :disabled="renderItem.disabled" :placeholder="renderItem.placeholder || `请输入${renderItem.label}`"
516
- :auto-size="{ minRows: 2, maxRows: 10 }" />
517
-
518
- <a-select style="min-width: 170px; margin-right: 15px" v-model:value="formState[renderItem.key]"
519
- :placeholder="renderItem.placeholder || `请选择`" v-if="renderItem.type == 'select'"
520
- :disabled="renderItem.disabled" :fieldNames="renderItem.fieldNames || { label: 'label', value: 'value' }"
521
- :options="renderItem.optionList" :mode="renderItem.isMultiple ? 'multiple' : undefined"></a-select>
522
-
523
- <a-radio-group v-if="renderItem.type == 'radio'" v-model:value="formState[renderItem.key]"
524
- :disabled="renderItem.disabled">
731
+ <a-textarea
732
+ v-model:value="formState[renderItem.key]"
733
+ v-if="renderItem.type == 'textarea'"
734
+ :disabled="renderItem.disabled"
735
+ :placeholder="renderItem.placeholder || `请输入${renderItem.label}`"
736
+ :auto-size="{ minRows: 2, maxRows: 10 }"
737
+ />
738
+
739
+ <a-select
740
+ style="min-width: 170px; margin-right: 15px"
741
+ v-model:value="formState[renderItem.key]"
742
+ :placeholder="renderItem.placeholder || `请选择`"
743
+ v-if="renderItem.type == 'select'"
744
+ :disabled="renderItem.disabled"
745
+ :fieldNames="renderItem.fieldNames || { label: 'label', value: 'value' }"
746
+ :options="renderItem.optionList"
747
+ :mode="renderItem.isMultiple ? 'multiple' : undefined"
748
+ :show-search="renderItem.enableSearch || !!renderItem.loadDataFunc"
749
+ :filter-option="renderItem.loadDataFunc ? false : !!renderItem.enableSearch"
750
+ :option-filter-prop="renderItem.fieldNames?.label || 'label'"
751
+ :loading="(renderItem as any).__selectState?.loading"
752
+ @search="(keyword) => onSelectSearch(keyword, renderItem as any)"
753
+ @popupScroll="(e) => onSelectPopupScroll(e, renderItem as any)"
754
+ ></a-select>
755
+
756
+ <a-radio-group
757
+ v-if="renderItem.type == 'radio'"
758
+ v-model:value="formState[renderItem.key]"
759
+ :disabled="renderItem.disabled"
760
+ >
525
761
  <a-radio v-for="item in renderItem.optionList" :value="item[renderItem.fieldNames?.value || 'value']">
526
762
  {{ item[renderItem.fieldNames?.label || "label"] }}
527
763
  </a-radio>
528
764
  </a-radio-group>
529
765
 
530
- <a-date-picker v-model:value="formState[renderItem.key]" v-if="renderItem.type === 'datePicker'" show-time
531
- :disabled="renderItem.disabled" :placeholder="`请选择${renderItem.label}`" class="ant-input"
532
- format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" />
533
-
534
- <a-tree-select style="min-width: 170px; margin-right: 15px" v-model:value="formState[renderItem.key]"
535
- :tree-data="renderItem.optionList" v-if="renderItem.type == 'treeSelect'" :disabled="renderItem.disabled"
766
+ <a-date-picker
767
+ v-model:value="formState[renderItem.key]"
768
+ v-if="renderItem.type === 'datePicker'"
769
+ show-time
770
+ :disabled="renderItem.disabled"
771
+ :placeholder="`请选择${renderItem.label}`"
772
+ class="ant-input"
773
+ format="YYYY-MM-DD HH:mm:ss"
774
+ value-format="YYYY-MM-DD HH:mm:ss"
775
+ />
776
+
777
+ <a-tree-select
778
+ style="min-width: 170px; margin-right: 15px"
779
+ v-model:value="formState[renderItem.key]"
780
+ :tree-data="renderItem.optionList"
781
+ v-if="renderItem.type == 'treeSelect'"
782
+ :disabled="renderItem.disabled"
536
783
  :fieldNames="renderItem.fieldNames || { children: 'children', label: 'name', value: 'id' }"
537
- :placeholder="renderItem.placeholder || `请选择`"></a-tree-select>
784
+ :placeholder="renderItem.placeholder || `请选择`"
785
+ ></a-tree-select>
538
786
 
539
787
  <a-upload
540
788
  v-if="renderItem.type == 'uploadOss' || renderItem.type == 'uploadTos' || renderItem.type == 'upload'"
541
- :file-list="formState[renderItem.key]" :multiple="true" :max-count="renderItem?.maxCount || 1"
542
- @preview="handlePicPreview" :customRequest="(e) => {
543
- uploadRequest(e, renderItem);
544
- }
545
- " @change="
546
- (e) => {
547
- uploadChange(e, renderItem);
548
- }
549
- " :headers="{
550
- 'Cache-Control': 'max-age=15552000',
551
- }" :list-type="renderItem.uploadType == 'pic' ? 'picture-card' : undefined">
789
+ :file-list="formState[renderItem.key]"
790
+ :multiple="true"
791
+ :max-count="renderItem?.maxCount || 1"
792
+ @preview="handlePicPreview"
793
+ :customRequest="
794
+ (e) => {
795
+ uploadRequest(e, renderItem);
796
+ }
797
+ "
798
+ @change="
799
+ (e) => {
800
+ uploadChange(e, renderItem);
801
+ }
802
+ "
803
+ :headers="{
804
+ 'Cache-Control': 'max-age=15552000',
805
+ }"
806
+ :list-type="renderItem.uploadType == 'pic' ? 'picture-card' : undefined"
807
+ >
552
808
  <div v-if="renderItem.uploadType == 'pic'">
553
809
  <plus-outlined />
554
810
  <div style="margin-top: 8px">{{ renderItem.label }}</div>
@@ -562,8 +818,11 @@ defineExpose({
562
818
  </div>
563
819
  </a-upload>
564
820
 
565
- <component v-if="renderItem.type == 'component'" :is="renderItem.component"
566
- v-model:[renderItem.key]="formState[renderItem.key]"></component>
821
+ <component
822
+ v-if="renderItem.type == 'component'"
823
+ :is="renderItem.component"
824
+ v-model:[renderItem.key]="formState[renderItem.key]"
825
+ ></component>
567
826
  </div>
568
827
  </a-form-item>
569
828
  </div>
@@ -62,7 +62,7 @@ const confirmClick = async () => {
62
62
 
63
63
  <template>
64
64
  <a-modal
65
- v-model:visible="show"
65
+ v-model:open="show"
66
66
  :width="width"
67
67
  centered
68
68
  wrapClassName="p-modal"
@@ -44,7 +44,7 @@ const confirmClick = async () => {
44
44
  </script>
45
45
 
46
46
  <template>
47
- <a-modal v-model:visible="show" :width="width" centered wrapClassName="p-modal">
47
+ <a-modal v-model:open="show" :width="width" centered wrapClassName="p-modal">
48
48
  <template #title v-if="title">
49
49
  <div class="modal-title">
50
50
  {{ title }}
@@ -65,8 +65,12 @@ onMounted(() => {
65
65
 
66
66
 
67
67
  const ok = async () => {
68
- let formData = await PFormRef.value.getFormData();
69
- emits('confirm', formData)
68
+ try {
69
+ let formData = await PFormRef.value.getFormData();
70
+ emits('confirm', formData)
71
+ } catch (error) {
72
+ console.warn('Form validation failed:', error);
73
+ }
70
74
  }
71
75
 
72
76
 
@@ -2,10 +2,21 @@
2
2
  // import "viewerjs/dist/viewer.css";
3
3
  import { api as viewerApi } from "v-viewer";
4
4
  import PForm from "../p-form/p-form.vue";
5
- import { h, ref, reactive, onMounted, toRaw } from "vue";
5
+ import { h, ref, reactive, onMounted, toRaw, computed } from "vue";
6
6
  import * as _ from "../../utils/dataUtils";
7
7
  import dayjs from "dayjs";
8
- import { Table as ATable, Button as AButton, Textarea as ATextarea } from "ant-design-vue";
8
+ import * as XLSX from "xlsx";
9
+ import {
10
+ Table as ATable,
11
+ Button as AButton,
12
+ Textarea as ATextarea,
13
+ Modal as AModal,
14
+ Radio as ARadio,
15
+ Checkbox as ACheckbox,
16
+ CheckboxGroup as ACheckboxGroup,
17
+ InputNumber as AInputNumber,
18
+ message,
19
+ } from "ant-design-vue";
9
20
  import type { FormDataItem } from "../p-form/index.d";
10
21
  import type { TableColumn } from "./index.d";
11
22
  import { useSlots } from "vue";
@@ -21,6 +32,8 @@ interface Props {
21
32
  searchRenderData?: FormDataItem[];
22
33
  initSearchFormData?: any;
23
34
  data?: any[];
35
+ enableExport?: boolean;
36
+ exportFileName?: string;
24
37
  }
25
38
 
26
39
  const {
@@ -31,6 +44,8 @@ const {
31
44
  editColumns = [],
32
45
  tableColumns = [],
33
46
  initSearchFormData = [],
47
+ enableExport = true,
48
+ exportFileName = "table_export",
34
49
  } = defineProps<Props>();
35
50
 
36
51
  const selectedIds = defineModel<string[]>("selectedIds", { default: [] });
@@ -66,6 +81,15 @@ const rowSelection = {
66
81
  const editableData = reactive({});
67
82
 
68
83
  const loading = ref(false);
84
+ const exportVisible = ref(false);
85
+ const exportLoading = ref(false);
86
+
87
+ const exportForm = reactive({
88
+ mode: "current",
89
+ page: 1,
90
+ count: 100,
91
+ columns: [] as string[],
92
+ });
69
93
 
70
94
  let searchQuery = {};
71
95
 
@@ -73,25 +97,6 @@ const renderTableColumns = (tableColumns: TableColumn[]) => {
73
97
  const returnColumns: any = [];
74
98
  for (const [index, renderItem] of tableColumns.entries()) {
75
99
  if (renderItem.type == "operate") {
76
- // renderItem.customRender = ({ text, record }) => {
77
- // const buttons: any = [];
78
- // for (const buttonItem of renderItem.operateButtons) {
79
- // if (buttonItem.visibleFunc && !buttonItem.visibleFunc(record)) continue;
80
- // buttons.push(
81
- // h(
82
- // "a",
83
- // {
84
- // onClick: () => {
85
- // buttonItem.click(record);
86
- // },
87
- // },
88
- // buttonItem.label
89
- // )
90
- // );
91
- // }
92
- // return h("div", { style: { display: "flex", justifyContent: "space-around" } }, buttons);
93
- // };
94
-
95
100
  if (!renderItem.width) {
96
101
  renderItem.width = renderItem.operateButtons.length * 60;
97
102
  }
@@ -175,6 +180,193 @@ const renderTableColumns = (tableColumns: TableColumn[]) => {
175
180
 
176
181
  const columns = renderTableColumns(tableColumns);
177
182
 
183
+ const getAllExportColumns = () => {
184
+ return tableColumns.filter((item) => item.type !== "operate" && !!item.key);
185
+ };
186
+
187
+ const getExportColumns = (selectedKeys?: string[]) => {
188
+ const allExportColumns = getAllExportColumns();
189
+ if (selectedKeys === undefined) {
190
+ return allExportColumns;
191
+ }
192
+
193
+ const selectedSet = new Set((selectedKeys || []).map((key) => String(key)));
194
+ return allExportColumns.filter((item) => selectedSet.has(String(item.key)));
195
+ };
196
+
197
+ const exportColumnOptions = computed(() => {
198
+ return getAllExportColumns().map((col) => {
199
+ const title = col.title as any;
200
+ return {
201
+ label: typeof title === "string" ? title : col.key || "",
202
+ value: String(col.key),
203
+ };
204
+ });
205
+ });
206
+
207
+ const onExportColumnsChange = (values: Array<string | number>) => {
208
+ console.log("selected export columns: ", values);
209
+ exportForm.columns = (values || []).map((item) => String(item));
210
+ };
211
+
212
+ const resetExportColumns = () => {
213
+ exportForm.columns = getAllExportColumns().map((item) => String(item.key));
214
+ };
215
+
216
+ const getCellValue = (record: any, column: TableColumn) => {
217
+ const key = column.key || "";
218
+ let value = key.includes(".") ? _.get(record, key.split(".")) : _.get(record, key);
219
+
220
+ if (column.mapPath) {
221
+ try {
222
+ const mapInfoStr: any = localStorage.getItem("map");
223
+ const mapInfo = JSON.parse(mapInfoStr || "{}");
224
+ const mapItem = _.get(mapInfo, column.mapPath) || {};
225
+ value = mapItem[value] ?? value;
226
+ } catch (error) {
227
+ // ignore map parse error and fallback to raw value
228
+ }
229
+ }
230
+
231
+ if (column.type === "date" && value) {
232
+ value = dayjs(value).format(column.format || "YYYY-MM-DD HH:mm:ss");
233
+ }
234
+
235
+ if (column.type === "pic") {
236
+ if (_.isEmpty(value)) return "";
237
+ const pics = _.isArray(value) ? value : [value];
238
+ value = pics.join(",");
239
+ }
240
+
241
+ if (_.isEmpty(value) && !_.isEmpty(column.default)) {
242
+ value = column.default;
243
+ }
244
+
245
+ if (_.isArray(value) || (typeof value === "object" && value !== null)) {
246
+ return JSON.stringify(value);
247
+ }
248
+
249
+ return value ?? "";
250
+ };
251
+
252
+ const downloadXlsx = (rows: any[], exportColumns: TableColumn[]) => {
253
+ const headers = exportColumns.map((col: any) => {
254
+ const title = col.title as any;
255
+ return typeof title === "string" ? title : col.key || "";
256
+ });
257
+
258
+ const dataRows = rows.map((row: any) => {
259
+ return exportColumns.map((col: any) => getCellValue(row, col));
260
+ });
261
+
262
+ const worksheet = XLSX.utils.aoa_to_sheet([headers, ...dataRows]);
263
+ const workbook = XLSX.utils.book_new();
264
+ XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1");
265
+ XLSX.writeFile(workbook, `${exportFileName}_${dayjs().format("YYYYMMDDHHmmss")}.xlsx`);
266
+ };
267
+
268
+ const openExportModal = () => {
269
+ exportForm.mode = "current";
270
+ exportForm.page = pageQuery.p;
271
+ exportForm.count = pageQuery.pc;
272
+ resetExportColumns();
273
+ exportVisible.value = true;
274
+ };
275
+
276
+ const toggleExportMode = (mode: string) => {
277
+ exportForm.mode = exportForm.mode === mode ? "" : mode;
278
+ };
279
+
280
+ const getExportRowsByMode = async () => {
281
+ const sourceRows = data && data.length > 0 ? data : tableData.data || [];
282
+
283
+ if (exportForm.mode === "current") {
284
+ return sourceRows;
285
+ }
286
+
287
+ if (getData) {
288
+ const _searchInitFormData = initSearchFormData ? JSON.parse(JSON.stringify(initSearchFormData)) : {};
289
+ const baseQuery = { ..._searchInitFormData, ..._.cloneDeep(searchQuery), od: pageQuery.od } as any;
290
+
291
+ if (exportForm.mode === "page") {
292
+ const _tableData = await getData({
293
+ ...baseQuery,
294
+ p: Math.max(1, Number(exportForm.page) || 1),
295
+ pc: pageQuery.pc,
296
+ });
297
+ return _tableData.data || [];
298
+ }
299
+
300
+ if (exportForm.mode === "count") {
301
+ const _tableData = await getData({
302
+ ...baseQuery,
303
+ p: 1,
304
+ pc: Math.max(1, Number(exportForm.count) || 1),
305
+ });
306
+ return _tableData.data || [];
307
+ }
308
+
309
+ if (exportForm.mode === "all") {
310
+ const _tableData = await getData({
311
+ ...baseQuery,
312
+ p: 1,
313
+ pc: Math.max(1, Number(tableData.count) || 1),
314
+ });
315
+ return _tableData.data || [];
316
+ }
317
+
318
+ return [];
319
+ }
320
+
321
+ if (exportForm.mode === "page") {
322
+ const page = Math.max(1, Number(exportForm.page) || 1);
323
+ const start = (page - 1) * pageQuery.pc;
324
+ return sourceRows.slice(start, start + pageQuery.pc);
325
+ }
326
+
327
+ if (exportForm.mode === "count") {
328
+ return sourceRows.slice(0, Math.max(1, Number(exportForm.count) || 1));
329
+ }
330
+
331
+ if (exportForm.mode === "all") {
332
+ return sourceRows.slice(0, tableData.count || sourceRows.length);
333
+ }
334
+
335
+ return sourceRows;
336
+ };
337
+
338
+ const confirmExport = async () => {
339
+ try {
340
+ if (!exportForm.mode) {
341
+ message.warning("请选择导出方式");
342
+ return;
343
+ }
344
+ if (_.isEmpty(exportForm.columns)) {
345
+ message.warning("请选择导出列");
346
+ return;
347
+ }
348
+ exportLoading.value = true;
349
+ const exportColumns = getExportColumns(exportForm.columns);
350
+
351
+ if (_.isEmpty(exportColumns)) {
352
+ message.warning("请选择导出列");
353
+ return;
354
+ }
355
+ const rows = await getExportRowsByMode();
356
+ if (_.isEmpty(rows)) {
357
+ message.warning("暂无可导出的数据");
358
+ return;
359
+ }
360
+ downloadXlsx(rows, exportColumns);
361
+ exportVisible.value = false;
362
+ message.success("导出成功");
363
+ } catch (error: any) {
364
+ message.error(error?.message || "导出失败");
365
+ } finally {
366
+ exportLoading.value = false;
367
+ }
368
+ };
369
+
178
370
  const renderTableData = async () => {
179
371
  loading.value = true;
180
372
  if (getData) {
@@ -213,8 +405,7 @@ const onPageSizeChange = (current, pc) => {
213
405
  const searchClick = async () => {
214
406
  const query = await SearchFormRef.value.getFormData();
215
407
 
216
- if (_.isEmpty(query)) return;
217
- searchQuery = _.pickBy(query, (value, key) => {
408
+ searchQuery = _.pickBy(query || {}, (value, key) => {
218
409
  return value !== "" && value != undefined;
219
410
  });
220
411
 
@@ -289,8 +480,12 @@ defineExpose({
289
480
  </PForm>
290
481
  </div>
291
482
 
292
- <div v-if="uSlots.button" style="display: flex; justify-content: flex-end; margin-bottom: 10px; padding: 20px">
483
+ <div
484
+ v-if="uSlots.button"
485
+ style="display: flex; justify-content: flex-end; gap: 10px; margin-bottom: 10px; padding: 20px"
486
+ >
293
487
  <slot name="button"></slot>
488
+ <a-button v-if="enableExport" type="primary" @click="openExportModal">导出Excel</a-button>
294
489
  </div>
295
490
 
296
491
  <a-table
@@ -350,6 +545,47 @@ defineExpose({
350
545
  <slot name="bodyCell" v-bind="{ column, text, record, index }" />
351
546
  </template>
352
547
  </a-table>
548
+
549
+ <a-modal v-model:open="exportVisible" title="导出Excel" :confirm-loading="exportLoading" @ok="confirmExport">
550
+ <div class="export-panel">
551
+ <div class="export-columns-wrap">
552
+ <div class="export-columns-title">导出列(默认全选)</div>
553
+ <a-checkbox-group
554
+ v-model:value="exportForm.columns"
555
+ class="export-columns-group"
556
+ :options="exportColumnOptions"
557
+ >
558
+ </a-checkbox-group>
559
+ </div>
560
+
561
+ <div class="export-mode-list">
562
+ <label class="export-mode-item" :class="{ selected: exportForm.mode === 'current' }">
563
+ <a-radio :checked="exportForm.mode === 'current'" @click="toggleExportMode('current')"
564
+ >导出当前页数据</a-radio
565
+ >
566
+ </label>
567
+ <label class="export-mode-item" :class="{ selected: exportForm.mode === 'page' }">
568
+ <a-radio :checked="exportForm.mode === 'page'" @click="toggleExportMode('page')">导出指定页码数据</a-radio>
569
+ </label>
570
+ <label class="export-mode-item" :class="{ selected: exportForm.mode === 'count' }">
571
+ <a-radio :checked="exportForm.mode === 'count'" @click="toggleExportMode('count')">导出指定条数数据</a-radio>
572
+ </label>
573
+ <label class="export-mode-item" :class="{ selected: exportForm.mode === 'all' }">
574
+ <a-radio :checked="exportForm.mode === 'all'" @click="toggleExportMode('all')">导出全部数据(count)</a-radio>
575
+ </label>
576
+ </div>
577
+
578
+ <div v-if="exportForm.mode === 'page'" class="export-input-row">
579
+ <span class="export-input-label">页码:</span>
580
+ <a-input-number v-model:value="exportForm.page" :min="1" :precision="0" style="width: 180px" />
581
+ </div>
582
+
583
+ <div v-if="exportForm.mode === 'count'" class="export-input-row">
584
+ <span class="export-input-label">条数:</span>
585
+ <a-input-number v-model:value="exportForm.count" :min="1" :precision="0" style="width: 180px" />
586
+ </div>
587
+ </div>
588
+ </a-modal>
353
589
  </template>
354
590
 
355
591
  <style>
@@ -376,4 +612,76 @@ defineExpose({
376
612
  .operate-a {
377
613
  color: #126cff;
378
614
  }
615
+
616
+ .export-panel {
617
+ display: flex;
618
+ flex-direction: column;
619
+ gap: 12px;
620
+ }
621
+
622
+ .export-columns-wrap {
623
+ padding: 10px 12px;
624
+ border: 1px solid #e6eaf2;
625
+ border-radius: 8px;
626
+ background: #f8fafc;
627
+ }
628
+
629
+ .export-columns-title {
630
+ margin-bottom: 8px;
631
+ color: #334155;
632
+ font-weight: 500;
633
+ }
634
+
635
+ .export-columns-group {
636
+ display: grid;
637
+ grid-template-columns: repeat(2, minmax(0, 1fr));
638
+ gap: 8px;
639
+ }
640
+
641
+ .export-column-item {
642
+ margin-inline-start: 0 !important;
643
+ }
644
+
645
+ .export-mode-list {
646
+ display: flex;
647
+ flex-direction: column;
648
+ gap: 8px;
649
+ }
650
+
651
+ .export-mode-item {
652
+ display: flex;
653
+ align-items: center;
654
+ min-height: 36px;
655
+ padding: 0 12px;
656
+ border: 1px solid #e6eaf2;
657
+ border-radius: 8px;
658
+ background: #f8fafc;
659
+ transition: all 0.2s ease;
660
+ cursor: pointer;
661
+ }
662
+
663
+ .export-mode-item:hover {
664
+ border-color: #8cb8ff;
665
+ background: #eef5ff;
666
+ }
667
+
668
+ .export-mode-item.selected {
669
+ border-color: #3b82f6;
670
+ background: #e8f1ff;
671
+ box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.15);
672
+ }
673
+
674
+ .export-input-row {
675
+ display: flex;
676
+ align-items: center;
677
+ gap: 8px;
678
+ padding: 10px 12px;
679
+ border-radius: 8px;
680
+ background: #f7f9fc;
681
+ }
682
+
683
+ .export-input-label {
684
+ color: #334155;
685
+ font-weight: 500;
686
+ }
379
687
  </style>
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "p-pc-ui",
3
- "version": "1.3.11",
4
- "type": "module",
3
+ "version": "1.3.13",
5
4
  "module": "dist/index.ts",
6
5
  "main": "dist/index.ts",
7
6
  "files": [
@@ -18,7 +17,8 @@
18
17
  "license": "ISC",
19
18
  "description": "",
20
19
  "dependencies": {
21
- "vue": "3.5.13"
20
+ "vue": "3.5.13",
21
+ "xlsx": "^0.18.5"
22
22
  },
23
23
  "devDependencies": {
24
24
  "ts-node": "^10.9.2",