p-pc-ui 1.3.11 → 1.3.12

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.
@@ -60,7 +60,7 @@ const btnClick = useDebounceFn(async () => {
60
60
 
61
61
  <a-tooltip color="var(--secondary-color-light)" :overlayInnerStyle="{ color: '#fff' }" :title="tooltip"
62
62
  v-bind="$attrs">
63
- <a-button @click="btnClick" :loading="loading || builtInLoading" class="font-semibold"
63
+ <a-button @click="btnClick" @keydown.enter.prevent="btnClick" :loading="loading || builtInLoading" class="font-semibold"
64
64
  :class="disabled ? 'cursor-not-allowed' : 'hover:opacity-80 active:opacity-100'" :style="{
65
65
  background: disabled ? '#b5b5b5' : bgColor,
66
66
  color: disabled ? '#fff' : color,
@@ -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 =
@@ -23,7 +23,6 @@ import * as _ from "../../utils/dataUtils";
23
23
  import { api as viewerApi } from "v-viewer";
24
24
  import dayjs from "dayjs";
25
25
  import { UploadOutlined, PlusOutlined } from "@ant-design/icons-vue";
26
- import { log } from "echarts/types/src/util/log.js";
27
26
 
28
27
  const uSlots = useSlots();
29
28
 
@@ -59,6 +58,170 @@ const formRef = ref<FormInstance>();
59
58
 
60
59
  const emits = defineEmits(["confirm"]);
61
60
 
61
+ const DEFAULT_SELECT_PAGE_SIZE = 20;
62
+
63
+ type SelectLoadResult =
64
+ | any[]
65
+ | {
66
+ data?: any[];
67
+ count?: number;
68
+ pages?: {
69
+ total?: number;
70
+ now?: number;
71
+ };
72
+ };
73
+
74
+ type SelectLoadState = {
75
+ pageNum: number;
76
+ pageSize: number;
77
+ keyword: string;
78
+ hasMore: boolean;
79
+ loading: boolean;
80
+ };
81
+
82
+ const getSelectValueField = (renderItem: any) => {
83
+ return renderItem.fieldNames?.value || "value";
84
+ };
85
+
86
+ const ensureSelectLoadState = (renderItem: FormDataItem & { [key: string]: any }) => {
87
+ if (!renderItem.__selectState) {
88
+ renderItem.__selectState = {
89
+ pageNum: 1,
90
+ pageSize: renderItem.pageSize || DEFAULT_SELECT_PAGE_SIZE,
91
+ keyword: "",
92
+ hasMore: true,
93
+ loading: false,
94
+ } as SelectLoadState;
95
+ }
96
+
97
+ return renderItem.__selectState as SelectLoadState;
98
+ };
99
+
100
+ const mergeSelectOptionList = (renderItem: FormDataItem & { [key: string]: any }, list: any[]) => {
101
+ const valueKey = getSelectValueField(renderItem);
102
+ const oldList = renderItem.optionList || [];
103
+ const map = new Map<any, any>();
104
+
105
+ for (const item of oldList) {
106
+ map.set(item?.[valueKey], item);
107
+ }
108
+
109
+ for (const item of list) {
110
+ map.set(item?.[valueKey], item);
111
+ }
112
+
113
+ renderItem.optionList = Array.from(map.values());
114
+ };
115
+
116
+ const buildSelectLoadParams = (renderItem: FormDataItem & { [key: string]: any }, state: SelectLoadState) => {
117
+ const pageNumKey = renderItem.pageNumKey || "p";
118
+ const pageSizeKey = renderItem.pageSizeKey || "pc";
119
+ const keywordKey = renderItem.keywordKey || "keyword";
120
+ const params: Record<string, any> = {
121
+ p: state.pageNum,
122
+ pc: state.pageSize,
123
+ [pageNumKey]: state.pageNum,
124
+ [pageSizeKey]: state.pageSize,
125
+ };
126
+
127
+ const keyword = (state.keyword || "").replaceAll("=", "").trim();
128
+ if (!keyword) {
129
+ return params;
130
+ }
131
+
132
+ params[keywordKey] = keyword;
133
+
134
+ return params;
135
+ };
136
+
137
+ const normalizeSelectLoadResult = (res: SelectLoadResult, state: SelectLoadState) => {
138
+ const list = Array.isArray(res) ? res : res?.data || [];
139
+
140
+ const hasMore =
141
+ 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
  */
@@ -146,8 +309,12 @@ const disposeRenderData = async (renderData: FormDataItem[]) => {
146
309
  renderItem.placeholder = "加载中" + point.join("");
147
310
  }, 500);
148
311
 
149
- const res = await renderItem.loadDataFunc();
150
- renderItem["optionList"] = res;
312
+ if (renderItem.type == "select") {
313
+ await fetchSelectOptions(renderItem as FormDataItem & { [key: string]: any }, { reset: true });
314
+ } else {
315
+ const res = await renderItem.loadDataFunc();
316
+ renderItem["optionList"] = res as any[];
317
+ }
151
318
  clearInterval(interval);
152
319
  renderItem.placeholder = `请选择${renderItem.label}`;
153
320
  }
@@ -225,12 +392,21 @@ const initFormState = (renderData: FormDataItem[], initFromData = {}) => {
225
392
  renderItem.optionList = [];
226
393
  }
227
394
 
395
+ if (renderItem.type == "select" && renderItem.isMultiple) {
396
+ if (!_.isArray(initValue)) {
397
+ initValue = [];
398
+ }
399
+ }
400
+
228
401
  if (!initValue && renderItem.optionList) {
229
- initValue = renderItem["optionList"][0]?.id;
402
+ if (!(renderItem.type == "select" && renderItem.isMultiple)) {
403
+ const valueKey = renderItem.fieldNames?.value || (renderItem.type == "treeSelect" ? "id" : "value");
404
+ initValue = renderItem["optionList"][0]?.[valueKey];
405
+ }
230
406
  }
231
407
 
232
408
  if (renderItem.loadDataFunc) {
233
- initValue = undefined;
409
+ initValue = renderItem.type == "select" && renderItem.isMultiple ? [] : undefined;
234
410
  }
235
411
  } else if (renderItem.type == "uploadOss") {
236
412
  if (!_.isArray(initValue)) {
@@ -518,7 +694,13 @@ defineExpose({
518
694
  <a-select style="min-width: 170px; margin-right: 15px" v-model:value="formState[renderItem.key]"
519
695
  :placeholder="renderItem.placeholder || `请选择`" v-if="renderItem.type == 'select'"
520
696
  :disabled="renderItem.disabled" :fieldNames="renderItem.fieldNames || { label: 'label', value: 'value' }"
521
- :options="renderItem.optionList" :mode="renderItem.isMultiple ? 'multiple' : undefined"></a-select>
697
+ :options="renderItem.optionList" :mode="renderItem.isMultiple ? 'multiple' : undefined"
698
+ :show-search="renderItem.enableSearch || !!renderItem.loadDataFunc"
699
+ :filter-option="renderItem.loadDataFunc ? false : !!renderItem.enableSearch"
700
+ :option-filter-prop="renderItem.fieldNames?.label || 'label'"
701
+ :loading="(renderItem as any).__selectState?.loading"
702
+ @search="(keyword) => onSelectSearch(keyword, renderItem as any)"
703
+ @popupScroll="(e) => onSelectPopupScroll(e, renderItem as any)"></a-select>
522
704
 
523
705
  <a-radio-group v-if="renderItem.type == 'radio'" v-model:value="formState[renderItem.key]"
524
706
  :disabled="renderItem.disabled">
@@ -571,10 +753,10 @@ defineExpose({
571
753
  <div v-if="uSlots.button || showResetButton || showSubmitButton" style="display: flex; justify-content: center">
572
754
  <slot name="button"></slot>
573
755
  <a-space size="small">
574
- <a-button v-if="showResetButton" @click="resetForm" style="padding: 0 30px">
756
+ <a-button v-if="showResetButton" @click="resetForm" @keydown.enter.prevent="resetForm" style="padding: 0 30px">
575
757
  {{ resetButtonText || "取消" }}
576
758
  </a-button>
577
- <a-button type="primary" @click="confirmClick" v-if="showSubmitButton" style="padding: 0 30px">
759
+ <a-button type="primary" @click="confirmClick" @keydown.enter.prevent="confirmClick" v-if="showSubmitButton" style="padding: 0 30px">
578
760
  {{ submitButtonText || "确认" }}
579
761
  </a-button>
580
762
  </a-space>
@@ -60,6 +60,7 @@ const confirmClick = async () => {
60
60
  <a-button
61
61
  class="modal-btn modal-cancel-btn"
62
62
  @click="show = false"
63
+ @keydown.enter.prevent="show = false"
63
64
  >
64
65
  取消
65
66
  </a-button>
@@ -71,6 +72,7 @@ const confirmClick = async () => {
71
72
  backgroundColor: confrimBtnBgColor,
72
73
  }"
73
74
  @click="confirmClick"
75
+ @keydown.enter.prevent="confirmClick"
74
76
  :loading="loading"
75
77
  >
76
78
  确定
@@ -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
 
@@ -283,14 +474,18 @@ defineExpose({
283
474
  layout="inline"
284
475
  >
285
476
  <template v-slot:button>
286
- <a-button type="primary" @click="searchClick" style="margin-left: 20px"> 搜索 </a-button>
287
- <a-button style="margin-left: 10px" @click="resetSearch"> 重置 </a-button>
477
+ <a-button type="primary" @click="searchClick" @keydown.enter.prevent="searchClick" style="margin-left: 20px"> 搜索 </a-button>
478
+ <a-button style="margin-left: 10px" @click="resetSearch" @keydown.enter.prevent="resetSearch"> 重置 </a-button>
288
479
  </template>
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" @keydown.enter.prevent="openExportModal">导出Excel</a-button>
294
489
  </div>
295
490
 
296
491
  <a-table
@@ -337,19 +532,63 @@ defineExpose({
337
532
  v-if="!item.visibleFunc || (item.visibleFunc && item.visibleFunc(record))"
338
533
  class="operate-a"
339
534
  @click="item.click(record)"
535
+ @keydown.enter.prevent="item.click(record)"
536
+ tabindex="0"
537
+ role="button"
340
538
  >
341
539
  {{ item.label }}</a
342
540
  >
343
541
  </span>
344
- <a class="operate-a" v-if="editableData[record.index]" @click="save(record.index)">保存</a>
345
- <a class="operate-a" v-if="editableData[record.index]" @click="cancel(record.index)">退出</a>
346
- <a v-if="editColumns.length > 0" class="operate-a" @click="edit(record.index)">编辑</a>
542
+ <a class="operate-a" v-if="editableData[record.index]" @click="save(record.index)" @keydown.enter.prevent="save(record.index)" tabindex="0" role="button">保存</a>
543
+ <a class="operate-a" v-if="editableData[record.index]" @click="cancel(record.index)" @keydown.enter.prevent="cancel(record.index)" tabindex="0" role="button">退出</a>
544
+ <a v-if="editColumns.length > 0" class="operate-a" @click="edit(record.index)" @keydown.enter.prevent="edit(record.index)" tabindex="0" role="button">编辑</a>
347
545
  </div>
348
546
  </template>
349
547
 
350
548
  <slot name="bodyCell" v-bind="{ column, text, record, index }" />
351
549
  </template>
352
550
  </a-table>
551
+
552
+ <a-modal v-model:open="exportVisible" title="导出Excel" :confirm-loading="exportLoading" @ok="confirmExport">
553
+ <div class="export-panel">
554
+ <div class="export-columns-wrap">
555
+ <div class="export-columns-title">导出列(默认全选)</div>
556
+ <a-checkbox-group
557
+ v-model:value="exportForm.columns"
558
+ class="export-columns-group"
559
+ :options="exportColumnOptions"
560
+ >
561
+ </a-checkbox-group>
562
+ </div>
563
+
564
+ <div class="export-mode-list">
565
+ <label class="export-mode-item" :class="{ selected: exportForm.mode === 'current' }">
566
+ <a-radio :checked="exportForm.mode === 'current'" @click="toggleExportMode('current')"
567
+ >导出当前页数据</a-radio
568
+ >
569
+ </label>
570
+ <label class="export-mode-item" :class="{ selected: exportForm.mode === 'page' }">
571
+ <a-radio :checked="exportForm.mode === 'page'" @click="toggleExportMode('page')">导出指定页码数据</a-radio>
572
+ </label>
573
+ <label class="export-mode-item" :class="{ selected: exportForm.mode === 'count' }">
574
+ <a-radio :checked="exportForm.mode === 'count'" @click="toggleExportMode('count')">导出指定条数数据</a-radio>
575
+ </label>
576
+ <label class="export-mode-item" :class="{ selected: exportForm.mode === 'all' }">
577
+ <a-radio :checked="exportForm.mode === 'all'" @click="toggleExportMode('all')">导出全部数据(count)</a-radio>
578
+ </label>
579
+ </div>
580
+
581
+ <div v-if="exportForm.mode === 'page'" class="export-input-row">
582
+ <span class="export-input-label">页码:</span>
583
+ <a-input-number v-model:value="exportForm.page" :min="1" :precision="0" style="width: 180px" />
584
+ </div>
585
+
586
+ <div v-if="exportForm.mode === 'count'" class="export-input-row">
587
+ <span class="export-input-label">条数:</span>
588
+ <a-input-number v-model:value="exportForm.count" :min="1" :precision="0" style="width: 180px" />
589
+ </div>
590
+ </div>
591
+ </a-modal>
353
592
  </template>
354
593
 
355
594
  <style>
@@ -376,4 +615,76 @@ defineExpose({
376
615
  .operate-a {
377
616
  color: #126cff;
378
617
  }
618
+
619
+ .export-panel {
620
+ display: flex;
621
+ flex-direction: column;
622
+ gap: 12px;
623
+ }
624
+
625
+ .export-columns-wrap {
626
+ padding: 10px 12px;
627
+ border: 1px solid #e6eaf2;
628
+ border-radius: 8px;
629
+ background: #f8fafc;
630
+ }
631
+
632
+ .export-columns-title {
633
+ margin-bottom: 8px;
634
+ color: #334155;
635
+ font-weight: 500;
636
+ }
637
+
638
+ .export-columns-group {
639
+ display: grid;
640
+ grid-template-columns: repeat(2, minmax(0, 1fr));
641
+ gap: 8px;
642
+ }
643
+
644
+ .export-column-item {
645
+ margin-inline-start: 0 !important;
646
+ }
647
+
648
+ .export-mode-list {
649
+ display: flex;
650
+ flex-direction: column;
651
+ gap: 8px;
652
+ }
653
+
654
+ .export-mode-item {
655
+ display: flex;
656
+ align-items: center;
657
+ min-height: 36px;
658
+ padding: 0 12px;
659
+ border: 1px solid #e6eaf2;
660
+ border-radius: 8px;
661
+ background: #f8fafc;
662
+ transition: all 0.2s ease;
663
+ cursor: pointer;
664
+ }
665
+
666
+ .export-mode-item:hover {
667
+ border-color: #8cb8ff;
668
+ background: #eef5ff;
669
+ }
670
+
671
+ .export-mode-item.selected {
672
+ border-color: #3b82f6;
673
+ background: #e8f1ff;
674
+ box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.15);
675
+ }
676
+
677
+ .export-input-row {
678
+ display: flex;
679
+ align-items: center;
680
+ gap: 8px;
681
+ padding: 10px 12px;
682
+ border-radius: 8px;
683
+ background: #f7f9fc;
684
+ }
685
+
686
+ .export-input-label {
687
+ color: #334155;
688
+ font-weight: 500;
689
+ }
379
690
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "p-pc-ui",
3
- "version": "1.3.11",
3
+ "version": "1.3.12",
4
4
  "type": "module",
5
5
  "module": "dist/index.ts",
6
6
  "main": "dist/index.ts",
@@ -18,7 +18,8 @@
18
18
  "license": "ISC",
19
19
  "description": "",
20
20
  "dependencies": {
21
- "vue": "3.5.13"
21
+ "vue": "3.5.13",
22
+ "xlsx": "^0.18.5"
22
23
  },
23
24
  "devDependencies": {
24
25
  "ts-node": "^10.9.2",