@qin-ui/element-plus-pro 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/AI-CONTEXT.md +49 -37
  2. package/LICENSE +9 -0
  3. package/api.json +44 -27
  4. package/bin/init-ai.mjs +0 -0
  5. package/es/component-provider/{index-Bmx9Q9Q-.js → index-BGGIWSkO.js} +2 -1
  6. package/es/component-provider/index-BGGIWSkO.js.map +1 -0
  7. package/es/component-provider/index.js +2 -1
  8. package/es/component-provider/index.js.map +1 -0
  9. package/es/components/component-provider/components/index.vue.d.ts +27 -0
  10. package/es/components/component-provider/components/index.vue.d.ts.map +1 -0
  11. package/es/components/component-provider/constants/index.d.ts +16 -0
  12. package/es/components/component-provider/constants/index.d.ts.map +1 -0
  13. package/es/components/component-provider/index.d.ts +4 -0
  14. package/es/components/component-provider/index.d.ts.map +1 -0
  15. package/es/components/component-provider/types/index.d.ts +35 -0
  16. package/es/components/component-provider/types/index.d.ts.map +1 -0
  17. package/es/components/form/components/BaseField/index.vue.d.ts +13 -0
  18. package/es/components/form/components/BaseField/index.vue.d.ts.map +1 -0
  19. package/es/components/form/components/BaseField/utils/index.d.ts +3 -0
  20. package/es/components/form/components/BaseField/utils/index.d.ts.map +1 -0
  21. package/es/components/form/components/BaseFormItem/index.vue.d.ts +9 -0
  22. package/es/components/form/components/BaseFormItem/index.vue.d.ts.map +1 -0
  23. package/es/components/form/components/ContainerFragment/index.vue.d.ts +23 -0
  24. package/es/components/form/components/ContainerFragment/index.vue.d.ts.map +1 -0
  25. package/es/components/form/components/GroupedFieldAttrs/index.vue.d.ts +31 -0
  26. package/es/components/form/components/GroupedFieldAttrs/index.vue.d.ts.map +1 -0
  27. package/es/components/form/components/PathProvider/index.vue.d.ts +24 -0
  28. package/es/components/form/components/PathProvider/index.vue.d.ts.map +1 -0
  29. package/es/components/form/components/ProForm/index.vue.d.ts +36 -0
  30. package/es/components/form/components/ProForm/index.vue.d.ts.map +1 -0
  31. package/es/components/form/components/SlotComponent/index.vue.d.ts +7 -0
  32. package/es/components/form/components/SlotComponent/index.vue.d.ts.map +1 -0
  33. package/es/components/form/components/index.d.ts +12 -0
  34. package/es/components/form/components/index.d.ts.map +1 -0
  35. package/es/components/form/constants/index.d.ts +67 -0
  36. package/es/components/form/constants/index.d.ts.map +1 -0
  37. package/es/components/form/hooks/index.d.ts +5 -0
  38. package/es/components/form/hooks/index.d.ts.map +1 -0
  39. package/es/components/form/hooks/useFields.d.ts +57 -0
  40. package/es/components/form/hooks/useFields.d.ts.map +1 -0
  41. package/es/components/form/hooks/useForm.d.ts +93 -0
  42. package/es/components/form/hooks/useForm.d.ts.map +1 -0
  43. package/es/components/form/hooks/useFormRef.d.ts +30 -0
  44. package/es/components/form/hooks/useFormRef.d.ts.map +1 -0
  45. package/es/components/form/index.d.ts +7 -0
  46. package/es/components/form/index.d.ts.map +1 -0
  47. package/es/components/form/types/index.d.ts +234 -0
  48. package/es/components/form/types/index.d.ts.map +1 -0
  49. package/es/components/table/components/ColumnControl.vue.d.ts +9 -0
  50. package/es/components/table/components/ColumnControl.vue.d.ts.map +1 -0
  51. package/es/components/table/components/DefaultSearchFormContainer.vue.d.ts +18 -0
  52. package/es/components/table/components/DefaultSearchFormContainer.vue.d.ts.map +1 -0
  53. package/es/components/table/components/DefaultTableContainer.vue.d.ts +18 -0
  54. package/es/components/table/components/DefaultTableContainer.vue.d.ts.map +1 -0
  55. package/es/components/table/components/ProTable.vue.d.ts +47 -0
  56. package/es/components/table/components/ProTable.vue.d.ts.map +1 -0
  57. package/es/components/table/components/SearchForm.vue.d.ts +48 -0
  58. package/es/components/table/components/SearchForm.vue.d.ts.map +1 -0
  59. package/es/components/table/components/SizeControl.vue.d.ts +12 -0
  60. package/es/components/table/components/SizeControl.vue.d.ts.map +1 -0
  61. package/es/components/table/components/TableColumnRenderer.vue.d.ts +17 -0
  62. package/es/components/table/components/TableColumnRenderer.vue.d.ts.map +1 -0
  63. package/es/components/table/components/icons/ColumnHeightOutlined.vue.d.ts +3 -0
  64. package/es/components/table/components/icons/ColumnHeightOutlined.vue.d.ts.map +1 -0
  65. package/es/components/table/components/icons/DownOutlined.vue.d.ts +3 -0
  66. package/es/components/table/components/icons/DownOutlined.vue.d.ts.map +1 -0
  67. package/es/components/table/components/icons/FilterOutlined.vue.d.ts +3 -0
  68. package/es/components/table/components/icons/FilterOutlined.vue.d.ts.map +1 -0
  69. package/es/components/table/components/index.d.ts +5 -0
  70. package/es/components/table/components/index.d.ts.map +1 -0
  71. package/es/components/table/index.d.ts +33 -0
  72. package/es/components/table/index.d.ts.map +1 -0
  73. package/es/components/table/types/index.d.ts +67 -0
  74. package/es/components/table/types/index.d.ts.map +1 -0
  75. package/es/components/table/useTable.d.ts +111 -0
  76. package/es/components/table/useTable.d.ts.map +1 -0
  77. package/es/element-plus-pro.css +10 -10
  78. package/es/form/{index-D8btGgqa.js → index-oI6LGSpA.js} +13 -7
  79. package/es/form/index-oI6LGSpA.js.map +1 -0
  80. package/es/form/index.js +4 -3
  81. package/es/form/index.js.map +1 -0
  82. package/es/index.d.ts +76 -1472
  83. package/es/index.d.ts.map +1 -0
  84. package/es/index.js +14 -23
  85. package/es/index.js.map +1 -0
  86. package/es/shared/core/index.d.ts +6 -0
  87. package/es/shared/core/index.d.ts.map +1 -0
  88. package/es/shared/ui/index.d.ts +12 -0
  89. package/es/shared/ui/index.d.ts.map +1 -0
  90. package/es/table/index.js +10 -10
  91. package/es/table/index.js.map +1 -0
  92. package/package.json +10 -11
  93. package/src/components/component-provider/components/index.vue +67 -0
  94. package/src/components/component-provider/constants/index.ts +209 -0
  95. package/src/components/component-provider/index.ts +6 -0
  96. package/src/components/component-provider/types/index.ts +64 -0
  97. package/src/components/form/components/BaseField/index.vue +181 -0
  98. package/src/components/form/components/BaseField/utils/index.ts +15 -0
  99. package/src/components/form/components/BaseFormItem/index.vue +170 -0
  100. package/src/components/form/components/ContainerFragment/index.vue +24 -0
  101. package/src/components/form/components/GroupedFieldAttrs/index.vue +115 -0
  102. package/src/components/form/components/PathProvider/index.vue +19 -0
  103. package/src/components/form/components/ProForm/index.vue +106 -0
  104. package/src/components/form/components/SlotComponent/index.vue +26 -0
  105. package/src/components/form/components/index.ts +15 -0
  106. package/src/components/form/constants/index.ts +116 -0
  107. package/src/components/form/hooks/index.ts +5 -0
  108. package/src/components/form/hooks/useFields.ts +67 -0
  109. package/src/components/form/hooks/useForm.ts +110 -0
  110. package/src/components/form/hooks/useFormRef.ts +35 -0
  111. package/src/components/form/index.ts +29 -0
  112. package/src/components/form/types/index.ts +300 -0
  113. package/src/components/table/components/ColumnControl.vue +136 -0
  114. package/src/components/table/components/DefaultSearchFormContainer.vue +14 -0
  115. package/src/components/table/components/DefaultTableContainer.vue +14 -0
  116. package/src/components/table/components/ProTable.vue +420 -0
  117. package/src/components/table/components/SearchForm.vue +302 -0
  118. package/src/components/table/components/SizeControl.vue +55 -0
  119. package/src/components/table/components/TableColumnRenderer.vue +71 -0
  120. package/src/components/table/components/icons/ColumnHeightOutlined.vue +4 -0
  121. package/src/components/table/components/icons/DownOutlined.vue +4 -0
  122. package/src/components/table/components/icons/FilterOutlined.vue +4 -0
  123. package/src/components/table/components/index.ts +4 -0
  124. package/src/components/table/index.ts +38 -0
  125. package/src/components/table/types/index.ts +77 -0
  126. package/src/components/table/useTable.ts +144 -0
  127. package/src/index.ts +81 -0
  128. package/src/shared/core/index.ts +38 -0
  129. package/src/shared/ui/index.ts +106 -0
  130. package/es/core/index-C0Pbv44C.js +0 -360
  131. package/es/vendor/utils/lodash-es-p6jau26B.js +0 -1120
@@ -0,0 +1,420 @@
1
+ <script lang="ts" setup generic="T extends Table<any> = Table">
2
+ /**
3
+ * @component ProTable
4
+ * @description 配置驱动的表格组件
5
+ *
6
+ * ## 架构概览
7
+ *
8
+ * ProTable 是表格系统的顶层容器,负责:
9
+ * 1. 接收 useTable() 创建的表格实例,驱动列、数据源、分页的渲染
10
+ * 2. 内部集成 SearchForm 搜索区域(基于 ProForm)
11
+ * 3. 支持列动态控制(显示/隐藏)和尺寸控制
12
+ * 4. 将分页变化和搜索事件串联到数据查询流程
13
+ *
14
+ * ## 数据流
15
+ *
16
+ * ```
17
+ * useTable({ columns, dataSource, searchFields, ... })
18
+ * ↓ 传入 :table prop
19
+ * ProTable
20
+ * ├── SearchForm (使用 table.searchForm 管理搜索条件)
21
+ * │ └── ProForm
22
+ * ├── SizeControl (表格尺寸切换)
23
+ * ├── ColumnControl (列显示/隐藏)
24
+ * └── ElTable (element-plus Table)
25
+ * └── 转发所有 table 插槽
26
+ * ```
27
+ *
28
+ * ## 搜索流程
29
+ *
30
+ * 1. 用户点击搜索 → SearchForm @search → searchPage1st()
31
+ * 2. searchPage1st 重置分页到第一页,触发 _search()
32
+ * 3. 分页变化 → onTableChange → setPageParam + _search()
33
+ * 4. 重置 → reset() → resetQueryParams + _search()
34
+ */
35
+ import {
36
+ Pagination as ElPagination,
37
+ type PaginationProps,
38
+ Table as ElTable,
39
+ type TableProps,
40
+ tableProps,
41
+ paginationProps,
42
+ } from '../../../shared/ui';
43
+ import {
44
+ computed,
45
+ nextTick,
46
+ useAttrs,
47
+ onMounted,
48
+ inject,
49
+ type Slot,
50
+ useSlots,
51
+ } from 'vue';
52
+ import { INJECT_CONFIG } from '../../component-provider/constants';
53
+ import { ContainerFragment } from '../../form';
54
+ import type { ContainerComponent } from '../../form/types';
55
+ import SearchForm from './SearchForm.vue';
56
+ import type { SearchFormProps } from './SearchForm.vue';
57
+ import type { Table } from '../useTable';
58
+ import { camelizeProperties, getObject, pick } from '../../../shared/core';
59
+ import {
60
+ DefaultSearchFormContainer,
61
+ DefaultTableContainer,
62
+ SizeControl,
63
+ ColumnControl,
64
+ } from '.';
65
+ import type { Columns } from '../types';
66
+ import TableColumnRenderer from './TableColumnRenderer.vue';
67
+
68
+ defineOptions({ name: 'ProTable', inheritAttrs: false });
69
+
70
+ type Control = { sizeControl: boolean; columnControl: boolean };
71
+
72
+ type SearchFormConfig = Omit<SearchFormProps, 'form'> & {
73
+ // 是否隐藏搜索表单
74
+ hidden?: boolean;
75
+ // 搜索区域包裹容器
76
+ container?: ContainerComponent | false;
77
+ };
78
+
79
+ type ReturnTableRecord<V> = V extends Table<any, infer R> ? R : any;
80
+ type RecordType = ReturnTableRecord<T>;
81
+
82
+ type Props = {
83
+ // 列表表格对象,returns from useTable
84
+ table?: T;
85
+ // 列表数据查询方法
86
+ search?: () => Promise<unknown>;
87
+ // 是否在首列插入index列
88
+ addIndexColumn?: boolean;
89
+ // onMounted 时立即触发一次 search 事件
90
+ immediateSearch?: boolean;
91
+ // 是否展示表格 size 和 column 控制按钮
92
+ control?: boolean | Partial<Control>;
93
+ // 搜索栏查询字段表单配置
94
+ searchFormConfig?: SearchFormConfig;
95
+ // 表格区域包裹容器
96
+ tableContainer?: ContainerComponent | false;
97
+ columns?: Columns<RecordType>;
98
+ data?: RecordType[];
99
+ pagination?: false | Partial<PaginationProps>;
100
+ } & Omit<TableProps, 'data'>;
101
+
102
+ const {
103
+ table = undefined,
104
+ search = undefined,
105
+ addIndexColumn = undefined,
106
+ control = undefined,
107
+ tableContainer = undefined,
108
+ immediateSearch = undefined,
109
+ searchFormConfig = undefined,
110
+ columns = undefined,
111
+ data = undefined,
112
+ pagination = undefined,
113
+ } = defineProps<Props>();
114
+
115
+ const config = INJECT_CONFIG['pro-table'];
116
+ const injectProps = inject(config.injectionKey, config.default) as any;
117
+
118
+ const tableProperties = Object.keys(tableProps());
119
+ const paginationProperties = Object.keys(paginationProps());
120
+
121
+ const attrs = useAttrs() as Record<string, any>;
122
+
123
+ const injectTableAttrs = computed(() => {
124
+ return pick(injectProps, tableProperties) as Partial<TableProps>;
125
+ });
126
+
127
+ const attrsTableProps = computed(() => {
128
+ return pick(
129
+ camelizeProperties(attrs),
130
+ tableProperties
131
+ ) as Partial<TableProps>;
132
+ });
133
+
134
+ const {
135
+ columns: tableColumns,
136
+ data: tableData,
137
+ searchForm,
138
+ pageParam,
139
+ setPageParam,
140
+ } = table || {};
141
+
142
+ const size = defineModel<TableProps['size']>('size');
143
+ const loading = defineModel<boolean>('loading');
144
+
145
+ // 核心搜索方法:设置 loading 状态,调用外部 search 函数
146
+ const _search = async () => {
147
+ try {
148
+ loading.value = true;
149
+ await search?.();
150
+ } finally {
151
+ loading.value = false;
152
+ }
153
+ };
154
+
155
+ // 重置:恢复查询参数到初始值,重新搜索
156
+ const reset = () => {
157
+ table?.resetQueryParams();
158
+ nextTick(() => {
159
+ _search();
160
+ });
161
+ };
162
+
163
+ type CustomSlots = Readonly<
164
+ Partial<Record<'search-form' | 'button-bar' | 'toolbar' | 'table', Slot>>
165
+ >;
166
+
167
+ type Slots = CustomSlots & Readonly<Record<string, Slot>>;
168
+
169
+ const slots = useSlots() as Slots;
170
+ const searchFormSlot = slots['search-form'];
171
+ const buttonBarSlot = slots['button-bar'];
172
+ const toolbarSlot = slots['toolbar'];
173
+
174
+ const tableSlots = computed(() => {
175
+ const customNames = new Set([
176
+ 'search-form',
177
+ 'button-bar',
178
+ 'toolbar',
179
+ 'table',
180
+ ]);
181
+ return Object.fromEntries(
182
+ Object.entries(slots).filter(([name]) => !customNames.has(name))
183
+ ) as Record<string, Slot>;
184
+ });
185
+ const tableSlotNames = computed(() => Object.keys(tableSlots.value));
186
+
187
+ const searchPage1st = () => {
188
+ setPageParam?.({ current: 1 });
189
+ _search();
190
+ };
191
+
192
+ const computedSearchFormConfig = computed(() => {
193
+ const container =
194
+ searchFormConfig?.container ??
195
+ injectProps.searchFormConfig?.container ??
196
+ DefaultSearchFormContainer;
197
+ return {
198
+ ...injectProps.searchFormConfig,
199
+ ...getObject(searchFormConfig),
200
+ container: container ? container : undefined,
201
+ };
202
+ });
203
+
204
+ const computedTableContainer = computed(() => {
205
+ const container =
206
+ tableContainer ?? injectProps.tableContainer ?? DefaultTableContainer;
207
+ return container ? container : undefined;
208
+ });
209
+
210
+ const normalizedRawColumns = computed(() => {
211
+ const source = (columns ?? tableColumns?.value ?? []) as any[];
212
+
213
+ const normalized = source.map((item, index) => {
214
+ const current: any = { ...item };
215
+ if (current.label === undefined && current.title !== undefined) {
216
+ current.label = current.title;
217
+ }
218
+ if (current.prop === undefined && current.dataIndex !== undefined) {
219
+ current.prop = Array.isArray(current.dataIndex)
220
+ ? current.dataIndex.join('.')
221
+ : current.dataIndex;
222
+ }
223
+ if (current.key === undefined) {
224
+ current.key =
225
+ current.prop ??
226
+ (typeof current.columnKey === 'string'
227
+ ? current.columnKey
228
+ : undefined) ??
229
+ `col-${index}`;
230
+ }
231
+ return current;
232
+ });
233
+ if (addIndexColumn ?? injectProps.addIndexColumn) {
234
+ return [
235
+ {
236
+ key: '_index',
237
+ type: 'index',
238
+ label: '序号',
239
+ width: 80,
240
+ },
241
+ ...normalized,
242
+ ];
243
+ }
244
+ return normalized;
245
+ });
246
+
247
+ const computedDataSource = computed<any[]>(() => {
248
+ const attrsData = attrsTableProps.value.data as any[] | undefined;
249
+ return data ?? attrsData ?? tableData?.value ?? [];
250
+ });
251
+
252
+ const computedTableProps = computed<TableProps>(() => {
253
+ const merged = {
254
+ ...injectTableAttrs.value,
255
+ ...attrsTableProps.value,
256
+ } as TableProps;
257
+ delete (merged as any).data;
258
+ return merged;
259
+ });
260
+
261
+ const computedPagination = computed<false | Partial<PaginationProps>>(() => {
262
+ if (pagination === false) return false;
263
+ const pageConfig = pagination ?? {};
264
+ return {
265
+ ...injectProps.pagination,
266
+ ...pick(pageConfig as Record<string, any>, paginationProperties),
267
+ currentPage: (pageConfig as any).currentPage ?? pageParam?.current ?? 1,
268
+ pageSize: (pageConfig as any).pageSize ?? pageParam?.pageSize ?? 10,
269
+ total: (pageConfig as any).total ?? pageParam?.total ?? 0,
270
+ };
271
+ });
272
+
273
+ const showPagination = computed(() => computedPagination.value !== false);
274
+
275
+ const onCurrentChange = (current: number) => {
276
+ setPageParam?.({ current });
277
+ _search();
278
+ };
279
+
280
+ const onSizeChange = (pageSize: number) => {
281
+ setPageParam?.({ pageSize, current: 1 });
282
+ _search();
283
+ };
284
+
285
+ const onPaginationChange = (currentPage: number, pageSize: number) => {
286
+ attrs.onChange?.(currentPage, pageSize);
287
+ };
288
+
289
+ const showSearch = computed(() => {
290
+ return !!(
291
+ searchFormSlot ||
292
+ (computedSearchFormConfig.value?.hidden !== true &&
293
+ searchForm &&
294
+ searchForm.fields.value.filter(item => !item.hidden)?.length > 0)
295
+ );
296
+ });
297
+
298
+ const computedControl = computed<Control>(() => {
299
+ const mergedControl = control ?? injectProps.control;
300
+ const sizeControl = (mergedControl as Control)?.sizeControl ?? mergedControl;
301
+ const columnControl =
302
+ (mergedControl as Control)?.columnControl ?? mergedControl;
303
+ return { sizeControl: !!sizeControl, columnControl: !!columnControl };
304
+ });
305
+
306
+ const resolvedSize = computed(() => {
307
+ return (
308
+ size.value ??
309
+ (attrsTableProps.value.size as any) ??
310
+ injectTableAttrs.value.size
311
+ );
312
+ });
313
+
314
+ onMounted(() => {
315
+ if (immediateSearch ?? injectProps.immediateSearch) {
316
+ _search();
317
+ }
318
+ });
319
+ </script>
320
+
321
+ <template>
322
+ <div
323
+ class="pro-table"
324
+ :class="($attrs as any).class"
325
+ :style="($attrs as any).style"
326
+ >
327
+ <ContainerFragment
328
+ v-if="showSearch"
329
+ :component="computedSearchFormConfig.container"
330
+ >
331
+ <slot name="search-form">
332
+ <SearchForm
333
+ v-if="searchForm"
334
+ :form="searchForm"
335
+ v-bind="computedSearchFormConfig"
336
+ @search="searchPage1st"
337
+ @reset="reset"
338
+ />
339
+ </slot>
340
+ </ContainerFragment>
341
+
342
+ <ContainerFragment :component="computedTableContainer">
343
+ <div class="pro-table_header">
344
+ <div v-if="buttonBarSlot" class="pro-table_header_button-bar">
345
+ <slot name="button-bar" />
346
+ </div>
347
+ <div v-if="toolbarSlot" class="pro-table_header_toolbar">
348
+ <slot name="toolbar" />
349
+ </div>
350
+ <div
351
+ v-if="computedControl.sizeControl"
352
+ class="pro-table_header_size-control"
353
+ >
354
+ <SizeControl v-model:size="size" />
355
+ </div>
356
+ <div
357
+ v-if="computedControl.columnControl"
358
+ class="pro-table_header_column-control"
359
+ >
360
+ <ColumnControl :columns="normalizedRawColumns" :table="table" />
361
+ </div>
362
+ </div>
363
+
364
+ <slot name="table">
365
+ <ElTable
366
+ v-loading="loading"
367
+ class="pro-table_content"
368
+ v-bind="computedTableProps"
369
+ :size="resolvedSize"
370
+ :data="computedDataSource"
371
+ >
372
+ <TableColumnRenderer :columns="normalizedRawColumns" />
373
+ <template v-for="name in tableSlotNames" :key="name" #[name]="scoped">
374
+ <slot :name="name" v-bind="scoped" />
375
+ </template>
376
+ </ElTable>
377
+ </slot>
378
+
379
+ <ElPagination
380
+ v-if="showPagination"
381
+ class="pro-table_pagination"
382
+ v-bind="computedPagination"
383
+ @current-change="onCurrentChange"
384
+ @size-change="onSizeChange"
385
+ @change="onPaginationChange"
386
+ />
387
+ </ContainerFragment>
388
+ </div>
389
+ </template>
390
+
391
+ <style scoped lang="less">
392
+ .pro-table {
393
+ &_header {
394
+ display: flex;
395
+ align-items: center;
396
+ justify-content: flex-end;
397
+
398
+ &:empty {
399
+ display: none;
400
+ }
401
+
402
+ & + &_content {
403
+ margin-top: 16px;
404
+ }
405
+
406
+ &_button-bar {
407
+ flex: 1;
408
+ }
409
+
410
+ &_toolbar {
411
+ margin-left: 12px;
412
+ }
413
+ }
414
+
415
+ &_pagination {
416
+ justify-content: flex-end;
417
+ padding: 16px 0;
418
+ }
419
+ }
420
+ </style>
@@ -0,0 +1,302 @@
1
+ <script lang="ts">
2
+ type Expand = {
3
+ // 网格布局时默认展开行数量
4
+ minExpandRows?: number;
5
+ // 网格布局时展开状态
6
+ expandStatus?: boolean;
7
+ };
8
+
9
+ type _FormProps = Pick<
10
+ Partial<FormProps>,
11
+ | 'disabled'
12
+ | 'labelPosition'
13
+ | 'labelWidth'
14
+ | 'inline'
15
+ | 'inlineMessage'
16
+ | 'statusIcon'
17
+ | 'showMessage'
18
+ | 'size'
19
+ >;
20
+
21
+ type ButtonProps = { onClick: () => void };
22
+ type ExpandButtonProps = ButtonProps & { expandStatus: boolean };
23
+
24
+ export type SearchFormProps = {
25
+ form: Form;
26
+ layout?: 'grid' | 'inline';
27
+ expand?: boolean | Expand;
28
+ searchButton?: Component<ButtonProps> | DefineComponent<ButtonProps> | false;
29
+ resetButton?: Component<ButtonProps> | DefineComponent<ButtonProps> | false;
30
+ expandButton?:
31
+ | Component<ExpandButtonProps>
32
+ | DefineComponent<ExpandButtonProps>
33
+ | false;
34
+ rowGap?: number;
35
+ columnGap?: number;
36
+ } & /* @vue-ignore */ _FormProps &
37
+ AllowedComponentProps;
38
+ </script>
39
+
40
+ <script lang="ts" setup>
41
+ /**
42
+ * @component SearchForm
43
+ * @description ProTable 的搜索表单子组件
44
+ *
45
+ * ## 核心功能
46
+ *
47
+ * 1. 内部渲染 ProForm,使用 table.searchForm 管理搜索条件
48
+ * 2. 支持 grid(网格)和 inline(行内)两种布局
49
+ * 3. 网格布局下支持展开/折叠:通过 IntersectionObserver 自动检测哪些字段溢出
50
+ * 4. 提供搜索、重置、展开/折叠按钮(支持自定义)
51
+ *
52
+ * ## 展开/折叠机制
53
+ *
54
+ * 1. 首次渲染后,测量 ProForm 的实际高度
55
+ * 2. 根据 minExpandRows 计算折叠高度(默认 2 行)
56
+ * 3. 如果实际高度 > 折叠高度,显示展开/折叠按钮
57
+ * 4. 使用 IntersectionObserver 检测隐藏字段是否有值,有值则自动展开
58
+ */
59
+ import { Space, Button, FormProps } from '../../../shared/ui';
60
+ import {
61
+ AllowedComponentProps,
62
+ computed,
63
+ ref,
64
+ watch,
65
+ watchEffect,
66
+ type Component,
67
+ type DefineComponent,
68
+ } from 'vue';
69
+ import ProForm from '../../form';
70
+ import { type Form } from '../../form';
71
+ import DownOutlined from './icons/DownOutlined.vue';
72
+
73
+ defineOptions({ name: 'SearchForm' });
74
+
75
+ const {
76
+ layout = 'grid',
77
+ expand = true,
78
+ searchButton = undefined,
79
+ resetButton = undefined,
80
+ expandButton = undefined,
81
+ rowGap = 16,
82
+ columnGap = 24,
83
+ form,
84
+ } = defineProps<SearchFormProps>();
85
+
86
+ type Emits = {
87
+ (e: 'search'): void;
88
+ (e: 'reset'): void;
89
+ };
90
+ const emit = defineEmits<Emits>();
91
+
92
+ const proFormHeight = ref<'unset' | number>('unset');
93
+ const collapseHeight = ref(0);
94
+ let rowHeight = 32;
95
+
96
+ const computedExpand = computed(() => {
97
+ if (!expand) return false;
98
+ if (expand === true) return { minExpandRows: 2, expandStatus: false };
99
+ return {
100
+ minExpandRows: Math.max(Math.floor(expand.minExpandRows ?? 2), 1),
101
+ expandStatus: expand.expandStatus ?? false,
102
+ };
103
+ });
104
+
105
+ const showExpandToggle = ref(false);
106
+
107
+ const expandStatus = ref(true);
108
+
109
+ const changeExpandStatus = () => {
110
+ expandStatus.value = !expandStatus.value;
111
+ };
112
+
113
+ const { formRef, getFormData } = form;
114
+
115
+ /*
116
+ * 使用 IntersectionObserver 检测折叠状态下隐藏字段是否有值
117
+ * 如果有值则自动展开,让用户看到已填写的搜索条件
118
+ */
119
+ const setInitExpandStatus = () => {
120
+ expandStatus.value = false;
121
+ if (formRef.value && expand) {
122
+ const formEl = (formRef.value as any).$el;
123
+ const formItemsEl = formEl.querySelectorAll(
124
+ "[class*='-form-item'] > [path]"
125
+ );
126
+ const observer = new IntersectionObserver(
127
+ entries => {
128
+ expandStatus.value = entries.some(e => {
129
+ // 如果隐藏字段(intersectionRatio === 0)有值,则自动展开
130
+ if (e.intersectionRatio === 0) {
131
+ const path = e.target.getAttribute('path');
132
+ const searchFieldValue = path ? getFormData?.(path) : undefined;
133
+ return ![null, undefined].includes(searchFieldValue);
134
+ }
135
+ return (expand as any).expandStatus;
136
+ });
137
+ observer.disconnect();
138
+ },
139
+ { root: formEl }
140
+ );
141
+ formItemsEl.forEach((element: Element) => {
142
+ observer.observe(element);
143
+ });
144
+ }
145
+ };
146
+
147
+ watch(
148
+ [
149
+ () => form?.fields.value?.filter((f: any) => !f.hidden)?.length,
150
+ () => formRef.value,
151
+ () => expand,
152
+ ],
153
+ () => {
154
+ if (!expand || !expandStatus.value || !formRef.value) return;
155
+ const proFormEl = (formRef.value as any)?.$el;
156
+ const { height = 0 } = proFormEl?.getBoundingClientRect?.() || {};
157
+ proFormHeight.value = height;
158
+ rowHeight = proFormEl
159
+ ?.querySelector("[class*='-form-item']")
160
+ ?.getBoundingClientRect?.()?.height;
161
+ },
162
+ { flush: 'post', immediate: true }
163
+ );
164
+
165
+ /*
166
+ * 计算折叠高度和是否显示展开按钮
167
+ * 折叠高度 = minExpandRows * 行高 + (minExpandRows - 1) * 行间距
168
+ * 如果实际高度与折叠高度差异 > 1px,说明有字段溢出,显示展开按钮
169
+ */
170
+ watchEffect(
171
+ () => {
172
+ if (typeof proFormHeight.value !== 'number') return;
173
+ if (layout === 'grid' && computedExpand.value) {
174
+ const { minExpandRows } = computedExpand.value;
175
+ collapseHeight.value = Math.min(
176
+ minExpandRows * rowHeight + (minExpandRows - 1) * rowGap,
177
+ +proFormHeight.value
178
+ );
179
+ showExpandToggle.value = +proFormHeight.value - collapseHeight.value > 1;
180
+ if (showExpandToggle.value) {
181
+ setInitExpandStatus();
182
+ }
183
+ } else {
184
+ showExpandToggle.value = false;
185
+ }
186
+ },
187
+ { flush: 'post' }
188
+ );
189
+
190
+ const layoutProps = computed(() =>
191
+ layout === 'grid'
192
+ ? {
193
+ grid: {
194
+ gutter: columnGap,
195
+ style: { rowGap: `${rowGap}px` },
196
+ },
197
+ style: {
198
+ display: 'flex',
199
+ overflow: 'hidden',
200
+ columnGap: `${columnGap}px`,
201
+ height: `${expandStatus.value ? proFormHeight.value : collapseHeight.value}px`,
202
+ },
203
+ }
204
+ : {
205
+ inline: true,
206
+ style: {
207
+ display: 'flex',
208
+ flexWrap: 'wrap',
209
+ gap: `${rowGap}px ${columnGap}px`,
210
+ },
211
+ grid: false,
212
+ }
213
+ );
214
+
215
+ const onReset = () => {
216
+ emit('reset');
217
+ };
218
+
219
+ const onSearch = () => {
220
+ emit('search');
221
+ };
222
+ </script>
223
+
224
+ <template>
225
+ <ProForm
226
+ v-bind="layoutProps"
227
+ :form="form"
228
+ class="pro-table_search-form transition"
229
+ @keyup.enter="onSearch"
230
+ >
231
+ <Space alignment="start" class="pro-table_search-form_button-group">
232
+ <slot name="reset-button" @click="onReset">
233
+ <template v-if="resetButton !== false">
234
+ <component :is="resetButton" v-if="resetButton" @click="onReset" />
235
+ <Button
236
+ v-else
237
+ class="pro-table_search-form_reset-button"
238
+ @click="onReset"
239
+ >重置</Button
240
+ >
241
+ </template>
242
+ </slot>
243
+ <slot name="search-button" @click="onSearch">
244
+ <template v-if="searchButton !== false">
245
+ <component :is="searchButton" v-if="searchButton" @click="onSearch" />
246
+ <Button
247
+ v-else
248
+ class="pro-table_search-form_search-button"
249
+ type="primary"
250
+ native-type="submit"
251
+ @click="onSearch"
252
+ >
253
+ 查询
254
+ </Button>
255
+ </template>
256
+ </slot>
257
+ <slot name="expand-button" @click="changeExpandStatus">
258
+ <template v-if="expandButton !== false && showExpandToggle">
259
+ <component
260
+ :is="expandButton"
261
+ v-if="expandButton"
262
+ :expand-status="expandStatus"
263
+ @click="changeExpandStatus"
264
+ />
265
+ <Button
266
+ v-else
267
+ text
268
+ class="pro-table_search-form_expand-toggle-button"
269
+ @click="changeExpandStatus"
270
+ >
271
+ {{ expandStatus ? '收起' : '展开' }}
272
+ <DownOutlined
273
+ class="transition"
274
+ style="margin-left: 4px"
275
+ :style="{ transform: `rotate(${expandStatus ? -180 : 0}deg)` }"
276
+ />
277
+ </Button>
278
+ </template>
279
+ </slot>
280
+ </Space>
281
+ </ProForm>
282
+ </template>
283
+
284
+ <style scoped lang="less">
285
+ .pro-table_search-form {
286
+ :deep {
287
+ [class*='-form-item'] {
288
+ margin: 0;
289
+ }
290
+ }
291
+ &_expand-toggle-button {
292
+ display: flex;
293
+ align-items: center;
294
+ padding: 0;
295
+ padding-left: 4px;
296
+ }
297
+
298
+ .transition {
299
+ transition: all 0.25s;
300
+ }
301
+ }
302
+ </style>