@qin-ui/antd-vue-pro 2.4.4 → 2.5.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 (90) hide show
  1. package/AI-CONTEXT.md +26 -2
  2. package/README.md +44 -37
  3. package/api.json +24 -9
  4. package/es/antd-vue-pro.css +15 -15
  5. package/es/components/component-provider/components/index.vue.d.ts +3 -3
  6. package/es/components/component-provider/components/index.vue.d.ts.map +1 -1
  7. package/es/components/component-provider/constants/index.d.ts +20 -4
  8. package/es/components/component-provider/constants/index.d.ts.map +1 -1
  9. package/es/components/component-provider/types/index.d.ts +8 -4
  10. package/es/components/component-provider/types/index.d.ts.map +1 -1
  11. package/es/components/form/components/BaseField/index.vue.d.ts +7 -1
  12. package/es/components/form/components/BaseField/index.vue.d.ts.map +1 -1
  13. package/es/components/form/components/BaseField/utils/index.d.ts +18 -0
  14. package/es/components/form/components/BaseField/utils/index.d.ts.map +1 -1
  15. package/es/components/form/components/BaseFormItem/index.vue.d.ts +3 -0
  16. package/es/components/form/components/BaseFormItem/index.vue.d.ts.map +1 -1
  17. package/es/components/form/components/ContainerFragment/index.vue.d.ts +1 -0
  18. package/es/components/form/components/ContainerFragment/index.vue.d.ts.map +1 -1
  19. package/es/components/form/components/ProForm/index.vue.d.ts.map +1 -1
  20. package/es/components/form/components/SlotComponent/index.vue.d.ts +1 -0
  21. package/es/components/form/components/SlotComponent/index.vue.d.ts.map +1 -1
  22. package/es/components/form/components/index.d.ts +4 -0
  23. package/es/components/form/components/index.d.ts.map +1 -1
  24. package/es/components/form/constants/index.d.ts.map +1 -1
  25. package/es/components/form/hooks/useFields.d.ts +2 -0
  26. package/es/components/form/hooks/useFields.d.ts.map +1 -1
  27. package/es/components/form/hooks/useGroupFieldAttrs.d.ts +10 -0
  28. package/es/components/form/hooks/useGroupFieldAttrs.d.ts.map +1 -1
  29. package/es/components/form/index.d.ts +2 -2
  30. package/es/components/form/index.d.ts.map +1 -1
  31. package/es/components/form/types/index.d.ts +75 -44
  32. package/es/components/form/types/index.d.ts.map +1 -1
  33. package/es/components/form/utils/fieldKey.d.ts +13 -0
  34. package/es/components/form/utils/fieldKey.d.ts.map +1 -0
  35. package/es/components/table/components/ColumnControl.vue.d.ts +3 -0
  36. package/es/components/table/components/ColumnControl.vue.d.ts.map +1 -1
  37. package/es/components/table/components/DefaultSearchFormContainer.vue.d.ts.map +1 -1
  38. package/es/components/table/components/DefaultTableContainer.vue.d.ts.map +1 -1
  39. package/es/components/table/components/ProTable.vue.d.ts +8 -1
  40. package/es/components/table/components/ProTable.vue.d.ts.map +1 -1
  41. package/es/components/table/components/SearchForm.vue.d.ts +10 -2
  42. package/es/components/table/components/SearchForm.vue.d.ts.map +1 -1
  43. package/es/components/table/components/SizeControl.vue.d.ts +7 -2
  44. package/es/components/table/components/SizeControl.vue.d.ts.map +1 -1
  45. package/es/components/table/hooks/useTable.d.ts.map +1 -1
  46. package/es/components/table/index.d.ts +4 -1
  47. package/es/components/table/index.d.ts.map +1 -1
  48. package/es/components/table/types/index.d.ts +14 -6
  49. package/es/components/table/types/index.d.ts.map +1 -1
  50. package/es/index.d.ts +8 -4
  51. package/es/index.d.ts.map +1 -1
  52. package/es/index.js +528 -343
  53. package/es/index.js.map +1 -1
  54. package/es/locale/index.d.ts +2 -0
  55. package/es/locale/index.d.ts.map +1 -1
  56. package/es/shared/core/index.d.ts +1 -1
  57. package/es/shared/core/index.d.ts.map +1 -1
  58. package/es/shared/ui/index.d.ts +15 -3
  59. package/es/shared/ui/index.d.ts.map +1 -1
  60. package/package.json +7 -4
  61. package/src/components/component-provider/components/index.vue +26 -12
  62. package/src/components/component-provider/constants/index.ts +33 -6
  63. package/src/components/component-provider/types/index.ts +17 -9
  64. package/src/components/form/components/BaseField/index.vue +48 -16
  65. package/src/components/form/components/BaseField/utils/index.ts +19 -1
  66. package/src/components/form/components/BaseFormItem/index.vue +65 -17
  67. package/src/components/form/components/ContainerFragment/index.vue +3 -1
  68. package/src/components/form/components/ProForm/index.vue +30 -18
  69. package/src/components/form/components/SlotComponent/index.vue +14 -4
  70. package/src/components/form/components/index.ts +8 -1
  71. package/src/components/form/constants/index.ts +3 -4
  72. package/src/components/form/hooks/useFields.ts +3 -1
  73. package/src/components/form/hooks/useGroupFieldAttrs.ts +34 -27
  74. package/src/components/form/index.ts +1 -3
  75. package/src/components/form/types/index.ts +79 -46
  76. package/src/components/form/utils/fieldKey.ts +27 -0
  77. package/src/components/table/components/ColumnControl.vue +21 -5
  78. package/src/components/table/components/DefaultSearchFormContainer.vue +5 -0
  79. package/src/components/table/components/DefaultTableContainer.vue +5 -0
  80. package/src/components/table/components/ProTable.vue +239 -66
  81. package/src/components/table/components/SearchForm.vue +58 -21
  82. package/src/components/table/components/SizeControl.vue +21 -1
  83. package/src/components/table/hooks/useTable.ts +1 -1
  84. package/src/components/table/index.ts +8 -3
  85. package/src/components/table/types/index.ts +15 -7
  86. package/src/index.ts +12 -5
  87. package/src/locale/index.ts +2 -0
  88. package/src/shared/core/index.ts +11 -0
  89. package/src/shared/ui/index.ts +22 -4
  90. package/tsconfig.json +22 -0
@@ -1,7 +1,8 @@
1
1
  <script lang="ts" setup generic="T extends Table<any> = Table">
2
2
  /**
3
+ * 配置驱动的表格组件
4
+ *
3
5
  * @component ProTable
4
- * @description 配置驱动的表格组件
5
6
  *
6
7
  * ## 架构概览
7
8
  *
@@ -25,52 +26,67 @@
25
26
  * └── 转发所有 table 插槽
26
27
  * ```
27
28
  *
28
- * ## 搜索流程
29
+ * ## 搜索行为
30
+ *
31
+ * 1. 搜索:分页回到第一页后发起查询
32
+ * 2. 分页变化:分页参数同步后发起查询
33
+ * 3. 重置:查询参数恢复初始值后重新查询
34
+ *
35
+ * @template T - 表格实例类型(useTable 返回值)
29
36
  *
30
- * 1. 用户点击搜索 → SearchForm @search → searchPage1st()
31
- * 2. searchPage1st 重置分页到第一页,触发 _search()
32
- * 3. 分页变化 → onTableChange → setPageParam + _search()
33
- * 4. 重置 → reset() → resetQueryParams + _search()
37
+ * @param {T} [table] - 列表表格对象,来自 useTable
38
+ * @param {() => Promise<unknown>} [search] - 列表数据查询方法
39
+ * @param {boolean} [addIndexColumn] - 是否在首列插入序号列
40
+ * @param {boolean} [immediateSearch] - 挂载后是否立即触发一次查询
41
+ * @param {boolean | Partial<Control>} [control] - 是否展示尺寸与列控制按钮;显式传入 columns 时列控入口不渲染
42
+ * @param {SearchFormConfig} [searchFormConfig] - 搜索栏查询字段表单配置
43
+ * @param {ContainerComponent | false} [tableContainer] - 表格区域包裹容器
44
+ *
45
+ * @slot search-form - 搜索区域插槽,作用域参数 form / search / reset
46
+ * @slot button-bar - 表格头部按钮区插槽
47
+ * @slot toolbar - 工具栏插槽
48
+ * @slot default - 透传给宿主表格的默认插槽(可用于以宿主列组件声明列)
49
+ * @slot table - 表格区域插槽,作用域参数 columns / dataSource / pagination / loading / size / onChange
34
50
  */
35
- import {
51
+ import type {
36
52
  PaginationProps,
37
- Table as ATable,
38
53
  TableProps,
39
54
  ColumnType,
40
- tableProps,
41
55
  } from '../../../shared/ui';
56
+ import { Table as ATable, tableProps } from '../../../shared/ui';
42
57
  import {
43
58
  computed,
59
+ getCurrentInstance,
44
60
  nextTick,
45
- useAttrs,
46
61
  onMounted,
47
62
  inject,
48
63
  toValue,
49
64
  type Slot,
50
65
  } from 'vue';
51
66
  import { INJECT_CONFIG } from '../../component-provider/constants';
52
- import { ContainerFragment } from '../../form';
67
+ import ContainerFragment from '../../form/components/ContainerFragment/index.vue';
53
68
  import type { ContainerComponent } from '../../form/types';
54
69
  import SearchForm from './SearchForm.vue';
55
- import { SearchFormProps } from './SearchForm.vue';
70
+ import type { SearchFormProps } from './SearchForm.vue';
56
71
  import type { Table } from '../hooks/useTable';
57
72
  import { useLocale } from '../../../locale';
73
+ import type { Path } from '../../../shared/core';
58
74
  import {
75
+ camelizeProperties,
59
76
  getObject,
77
+ isObjectItem,
60
78
  isPlainObject,
61
- pick,
62
79
  isPromise,
63
- camelizeProperties,
64
- Path,
80
+ omitBy,
81
+ pick,
82
+ useReactiveAttrs,
65
83
  } from '../../../shared/core';
66
- import { ComponentSlots } from 'vue-component-type-helpers';
67
- import {
68
- DefaultSearchFormContainer,
69
- DefaultTableContainer,
70
- SizeControl,
71
- ColumnControl,
72
- } from '.';
73
- import { Column, Columns, ColumnControlConfig } from '../types';
84
+ import type { ComponentSlots } from 'vue-component-type-helpers';
85
+ import DefaultSearchFormContainer from './DefaultSearchFormContainer.vue';
86
+ import DefaultTableContainer from './DefaultTableContainer.vue';
87
+ import SizeControl from './SizeControl.vue';
88
+ import ColumnControl from './ColumnControl.vue';
89
+ import type { Column, Columns, ColumnControlConfig } from '../types';
74
90
 
75
91
  defineOptions({ name: 'ProTable', inheritAttrs: false });
76
92
 
@@ -89,19 +105,19 @@ type SearchFormConfig = Omit<SearchFormProps, 'form'> & {
89
105
  };
90
106
 
91
107
  type Props = {
92
- // 列表表格对象,returns from useTable
108
+ /** 列表表格对象,来自 `useTable` */
93
109
  table?: T;
94
- // 列表数据查询方法
110
+ /** 列表数据查询方法 */
95
111
  search?: () => Promise<unknown>;
96
- // 是否在首列插入index列
112
+ /** 是否在首列插入序号列 */
97
113
  addIndexColumn?: boolean;
98
- // onMounted 时立即触发一次search事件
114
+ /** 挂载后是否立即触发一次查询 */
99
115
  immediateSearch?: boolean;
100
- // 是否展示表格 size 和 column 控制按钮
116
+ /** 是否展示尺寸与列控制按钮;显式传入 columns 时列控入口不渲染 */
101
117
  control?: boolean | Partial<Control>;
102
- // 搜索栏查询字段表单配置
118
+ /** 搜索栏查询字段表单配置 */
103
119
  searchFormConfig?: SearchFormConfig;
104
- // 表格区域包裹容器
120
+ /** 表格区域包裹容器 */
105
121
  tableContainer?: ContainerComponent | false;
106
122
  } & /* @vue-ignore */ TableProps;
107
123
 
@@ -121,6 +137,7 @@ const config = INJECT_CONFIG['pro-table'];
121
137
  const injectPropsRaw = inject(config.injectionKey, config.default);
122
138
  const injectProps = computed(() => toValue(injectPropsRaw) || {});
123
139
 
140
+ /* Provider 注入的不可注入键(columns / dataSource / loading)已在注入层剔除,此处直接取用其注入属性 */
124
141
  const injectAttrs = computed<TableProps>(
125
142
  () => pick(injectProps.value, tableProperties) as TableProps
126
143
  );
@@ -137,20 +154,48 @@ const computedSearchFormConfig = computed(() => {
137
154
  };
138
155
  });
139
156
 
140
- const attrs: Record<string, any> = useAttrs();
141
- const tableAttrs = computed<TableProps>(() => {
142
- return pick(camelizeProperties(attrs), tableProperties) as TableProps;
157
+ /** SearchForm 属性面:容器与显隐由 ProTable 自身消费,不下发 SearchForm */
158
+ const searchFormProps = computed(() => {
159
+ const {
160
+ container: _container,
161
+ hidden: _hidden,
162
+ ...rest
163
+ } = computedSearchFormConfig.value;
164
+ return rest;
143
165
  });
144
166
 
167
+ const attrs: Record<string, any> = useReactiveAttrs();
168
+
145
169
  const size = defineModel<TableProps['size']>('size');
146
170
  const loading = defineModel<boolean>('loading', { default: false });
147
171
 
148
172
  // 递增序号做请求竞态守卫,仅最新一次请求可变更 loading
149
173
  let searchSequence = 0;
174
+ // 告警文案按原因分键,各原因独立去重
175
+ const SEARCH_WARN_MESSAGES = {
176
+ duplicate:
177
+ '同时配置了 :search prop 与 @search 监听,@search 监听不会被执行,请仅保留其一。',
178
+ 'not-promise':
179
+ 'search 未返回 Promise,本次查询不参与 loading 竞态管理(在途请求的 loading 状态保持不变)。',
180
+ } as const;
181
+ const warnedSearchReasons = new Set<keyof typeof SEARCH_WARN_MESSAGES>();
182
+ const warnSearchConfig = (reason: keyof typeof SEARCH_WARN_MESSAGES) => {
183
+ if (warnedSearchReasons.has(reason)) return;
184
+ warnedSearchReasons.add(reason);
185
+ console.warn(`[ProTable] ${SEARCH_WARN_MESSAGES[reason]}`);
186
+ };
150
187
  const _search = async () => {
188
+ const search = props.search || attrs.onSearch;
189
+ if (props.search && attrs.onSearch) {
190
+ warnSearchConfig('duplicate');
191
+ }
192
+ if (!search) return;
193
+ const searchPromise = search();
194
+ if (!isPromise(searchPromise)) {
195
+ warnSearchConfig('not-promise');
196
+ return;
197
+ }
151
198
  const seq = ++searchSequence;
152
- const searchPromise = (props.search || attrs.onSearch)?.();
153
- if (!isPromise(searchPromise)) return;
154
199
  loading.value = true;
155
200
  try {
156
201
  await searchPromise;
@@ -204,10 +249,15 @@ const columnRequiredSlotNames = new Set([
204
249
  ]);
205
250
 
206
251
  /**
207
- * 判定插槽是否具备渲染条件(守卫过渡状态或空列下 column 为 undefined 的情况)
208
- * 当列被动态隐藏或所有列均隐藏时,旧单元格销毁过渡帧或空占位中 column 可能为 undefined,
209
- * 此时阻断依赖 column 的业务插槽调用;
210
- * 非列依赖的表格级插槽(如 emptyText、footer、summary 等)始终正常渲染。
252
+ * 判定插槽在当前渲染状态下是否可渲染。
253
+ *
254
+ * @description
255
+ * 依赖列信息的插槽(`bodyCell` / `headerCell` / `customFilterDropdown` / `customFilterIcon`)仅在存在可见列且作用域中存在 `column` 时渲染;
256
+ * 非列依赖的表格级插槽(如 `emptyText`、`footer`、`summary` 等)始终渲染。
257
+ *
258
+ * @param name - 插槽名
259
+ * @param scoped - 插槽作用域参数
260
+ * @returns 该插槽是否参与渲染
211
261
  */
212
262
  const isSlotRenderable = (name: string, scoped: any) => {
213
263
  if (columnRequiredSlotNames.has(name)) {
@@ -221,13 +271,65 @@ const searchPage1st = () => {
221
271
  _search();
222
272
  };
223
273
 
274
+ const instance = getCurrentInstance();
275
+
276
+ /** 调用方是否显式绑定该 prop(含 kebab-case 与 v-model 写法) */
277
+ const isBoundProp = (name: string) => {
278
+ const vnodeProps = instance?.vnode.props || {};
279
+ const kebab = name.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`);
280
+ return (
281
+ name in vnodeProps ||
282
+ kebab in vnodeProps ||
283
+ `onUpdate:${name}` in vnodeProps
284
+ );
285
+ };
286
+
287
+ /** attrs 的驼峰视图:模板中以 kebab-case 写法的绑定键在此归一 */
288
+ const camelAttrs = computed<Record<string, any>>(() =>
289
+ camelizeProperties(attrs)
290
+ );
291
+
292
+ /*
293
+ * 组件自有键取值:`Props` 中声明的键取 prop,其余宿主键取 attrs(模板 kebab-case 绑定经归一后同键);
294
+ * 均以调用方显式绑定为唯一来源,未绑定时不取值(留给 Provider 注入兜底)
295
+ */
296
+ const resolveOwnedProp = (name: string) => {
297
+ const attrValue = camelAttrs.value[name];
298
+ return isBoundProp(name)
299
+ ? ((props as Record<string, any>)[name] ?? attrValue)
300
+ : attrValue;
301
+ };
302
+
303
+ /** 调用方是否显式传入 columns(prop 或 attrs 形态,值为 undefined 时视为未指定) */
304
+ const hasExplicitColumns = computed(
305
+ () => resolveOwnedProp('columns') !== undefined
306
+ );
307
+
308
+ /*
309
+ * 表格尺寸单一来源:本地模型(显式绑定或 SizeControl 回写)> Provider 注入;
310
+ * 本地模型为 undefined 时视为未指定,Provider 注入值继续生效
311
+ */
312
+ const effectiveSize = computed<TableProps['size']>(() => {
313
+ return size.value ?? injectAttrs.value.size;
314
+ });
315
+
316
+ /*
317
+ * 下发给宿主表格的属性:Provider 注入为底(其不可注入键已在注入层剔除),调用方绑定与 v-model 透传值覆盖同键。
318
+ * size / loading 为声明模型,其 onUpdate:* 回写通道不进入 attrs,无需另行剔除
319
+ */
224
320
  const computedTableProps = computed<TableProps>(() => {
225
- return {
321
+ const merged = {
226
322
  ...injectAttrs.value,
227
- ...tableAttrs.value,
228
- pagination: undefined,
229
- onChange: undefined,
230
- };
323
+ ...omitBy(
324
+ pick(camelAttrs.value, tableProperties),
325
+ value => value === undefined
326
+ ),
327
+ } as Record<string, any>;
328
+ // onChange 由模板监听器收口,散落 attrs 中的同名监听器并入会形成重复回调
329
+ delete merged.onChange;
330
+ merged.size = effectiveSize.value;
331
+ merged.loading = undefined;
332
+ return merged as TableProps;
231
333
  });
232
334
 
233
335
  const computedTableContainer = computed(() => {
@@ -241,15 +343,17 @@ const computedTableContainer = computed(() => {
241
343
  type ReturnGenericParameterTypes<V> = V extends Table<any, infer R> ? R : never;
242
344
  type RecordType = ReturnGenericParameterTypes<T>;
243
345
 
244
- // 序号列随分页偏移:第 n 页首行序号 = (n-1)*pageSize + i + 1(无分页时退化为 index+1)
346
+ /*
347
+ * 序号列随有效分页偏移:分页关闭时退化为 index + 1;
348
+ * current / pageSize 取有效分页(显式 :pagination 或 Provider 注入),table.pageParam 仅作兜底
349
+ */
245
350
  const getIndex = (i: number) => {
351
+ const pagination = computedPagination.value;
352
+ if (pagination === false) return i + 1;
246
353
  const pageParam = props.table?.pageParam;
247
- if (pageParam && computedPagination.value !== false) {
248
- const current = pageParam.current ?? 1;
249
- const pageSize = pageParam.pageSize ?? 10;
250
- return (current - 1) * pageSize + i + 1;
251
- }
252
- return i + 1;
354
+ const current = pagination.current ?? pageParam?.current ?? 1;
355
+ const pageSize = pagination.pageSize ?? pageParam?.pageSize ?? 10;
356
+ return (current - 1) * pageSize + i + 1;
253
357
  };
254
358
 
255
359
  const indexColumn = computed<ColumnType>(() => ({
@@ -260,26 +364,72 @@ const indexColumn = computed<ColumnType>(() => ({
260
364
  customRender: ({ index }) => getIndex(index),
261
365
  }));
262
366
 
367
+ // 列来源优先级:显式绑定 > table 实例(useTable 创建的 columns)
263
368
  const computedColumns = computed<Columns<RecordType>>(() => {
264
- const cols = (tableAttrs.value.columns ??
369
+ const cols = (resolveOwnedProp('columns') ??
265
370
  toValue(props.table?.columns) ??
266
371
  []) as any;
267
372
  const needIndex = props.addIndexColumn ?? injectProps.value.addIndexColumn;
268
373
  return needIndex ? [indexColumn.value, ...cols] : cols;
269
374
  });
270
375
 
376
+ /*
377
+ * 递归过滤 hidden 列(含分组子列):仅对被过滤过子列的列生成浅拷贝,
378
+ * 未发生变动的列保持原对象引用(每次 columns 变化执行一次,不在渲染期递归)
379
+ */
380
+ const filterVisibleColumns = (
381
+ columns: any[],
382
+ ancestors: Set<object> = new Set()
383
+ ): { list: any[]; changed: boolean } => {
384
+ const list: any[] = [];
385
+ let changed = false;
386
+ for (const column of columns) {
387
+ if (!isObjectItem(column) || toValue((column as Column).hidden)) {
388
+ changed = true;
389
+ continue;
390
+ }
391
+ if (Array.isArray(column.children) && column.children.length > 0) {
392
+ // 环状分组列不再深入,重复出现的祖先引用直接剔除
393
+ if (ancestors.has(column)) {
394
+ changed = true;
395
+ continue;
396
+ }
397
+ ancestors.add(column);
398
+ const filtered = filterVisibleColumns(column.children, ancestors);
399
+ ancestors.delete(column);
400
+ if (filtered.changed) {
401
+ changed = true;
402
+ list.push({ ...column, children: filtered.list });
403
+ continue;
404
+ }
405
+ }
406
+ list.push(column);
407
+ }
408
+ return { list, changed };
409
+ };
410
+
271
411
  const visibleComputedColumns = computed(() => {
272
- return computedColumns.value.filter(item => !(item as Column).hidden);
412
+ return filterVisibleColumns(computedColumns.value).list;
273
413
  });
274
414
 
415
+ // 数据源优先级:显式绑定 > table 实例(useTable 创建的 dataSource)
275
416
  const computedDataSource = computed<RecordType[]>(() => {
276
- return tableAttrs.value.dataSource ?? toValue(props.table?.dataSource) ?? [];
417
+ return (
418
+ resolveOwnedProp('dataSource') ?? toValue(props.table?.dataSource) ?? []
419
+ );
277
420
  });
278
421
 
279
- // 分页优先级:tableAttrs 显式配置 > provider 全局配置 > 默认合成
422
+ /*
423
+ * 分页优先级:显式绑定 > provider 全局配置 > 默认合成;
424
+ * provider 作为基础配置与 table.pageParam 合并,显式绑定的对象形态在其之上覆盖
425
+ */
280
426
  const computedPagination = computed<PaginationProps | false>(() => {
281
- if (tableAttrs.value.pagination === false) return false;
282
- if (isPlainObject(tableAttrs.value.pagination)) {
427
+ const attrsPagination = resolveOwnedProp('pagination') as
428
+ | PaginationProps
429
+ | false
430
+ | undefined;
431
+ if (attrsPagination === false) return false;
432
+ if (isPlainObject(attrsPagination)) {
283
433
  const globalPagination =
284
434
  injectAttrs.value.pagination === false
285
435
  ? {}
@@ -287,9 +437,9 @@ const computedPagination = computed<PaginationProps | false>(() => {
287
437
  return {
288
438
  ...globalPagination,
289
439
  ...props.table?.pageParam,
290
- ...tableAttrs.value.pagination,
440
+ ...attrsPagination,
291
441
  showTotal:
292
- tableAttrs.value.pagination?.showTotal ??
442
+ attrsPagination?.showTotal ??
293
443
  globalPagination?.showTotal ??
294
444
  ((total: number) => t('pagination.total', { total })),
295
445
  };
@@ -318,11 +468,15 @@ const searchFormVisible = computed(() => {
318
468
  return !!(
319
469
  computedSearchFormConfig.value?.hidden !== true &&
320
470
  props.table?.searchForm &&
321
- fieldList.filter(item => !toValue(item.hidden)).length > 0
471
+ fieldList.filter(item => isObjectItem(item) && !toValue(item.hidden))
472
+ .length > 0
322
473
  );
323
474
  });
324
475
 
325
- const computedControl = computed<{
476
+ /*
477
+ * 控制项来源:显式控制配置 > Provider 全局配置
478
+ */
479
+ const resolvedControl = computed<{
326
480
  sizeControl: boolean;
327
481
  columnControl: boolean | ColumnControlConfig<Path<RecordType>>;
328
482
  }>(() => {
@@ -339,6 +493,13 @@ const computedControl = computed<{
339
493
  };
340
494
  });
341
495
 
496
+ /* 显式 columns 模式下渲染源不受 table.setColumn 影响,列控入口隐藏 */
497
+ const computedControl = computed(() =>
498
+ hasExplicitColumns.value && resolvedControl.value.columnControl
499
+ ? { ...resolvedControl.value, columnControl: false }
500
+ : resolvedControl.value
501
+ );
502
+
342
503
  const columnControlConfig = computed(() => {
343
504
  const cc = computedControl.value.columnControl;
344
505
  return typeof cc === 'object' ? cc : undefined;
@@ -362,11 +523,14 @@ defineExpose({
362
523
  v-if="searchFormVisible || $slots['search-form']"
363
524
  :component="computedSearchFormConfig.container"
364
525
  >
365
- <slot name="search-form">
526
+ <slot
527
+ name="search-form"
528
+ v-bind="{ form: props.table?.searchForm, search: searchPage1st, reset }"
529
+ >
366
530
  <SearchForm
367
531
  v-if="props.table?.searchForm"
368
532
  :form="props.table.searchForm"
369
- v-bind="computedSearchFormConfig"
533
+ v-bind="searchFormProps"
370
534
  @search="searchPage1st"
371
535
  @reset="reset"
372
536
  />
@@ -385,7 +549,7 @@ defineExpose({
385
549
  v-if="computedControl.sizeControl"
386
550
  class="pro-table_header_size-control"
387
551
  >
388
- <SizeControl v-model:size="size" />
552
+ <SizeControl v-model:size="size" :effective-size="effectiveSize" />
389
553
  </div>
390
554
  <div
391
555
  v-if="computedControl.columnControl"
@@ -399,11 +563,20 @@ defineExpose({
399
563
  </div>
400
564
  </div>
401
565
 
402
- <slot name="table">
566
+ <slot
567
+ name="table"
568
+ v-bind="{
569
+ columns: visibleComputedColumns,
570
+ dataSource: computedDataSource,
571
+ pagination: computedPagination,
572
+ loading: loading,
573
+ size: effectiveSize,
574
+ onChange: onTableChange,
575
+ }"
576
+ >
403
577
  <ATable
404
578
  class="pro-table_content"
405
579
  v-bind="computedTableProps"
406
- :size="size"
407
580
  :loading="loading"
408
581
  :columns="visibleComputedColumns"
409
582
  :data-source="computedDataSource"
@@ -21,17 +21,26 @@ type _FormProps = Pick<
21
21
  type ButtonProps = { onClick: () => void };
22
22
  type ExpandButtonProps = ButtonProps & { expandStatus: boolean };
23
23
 
24
+ /** ProTable 搜索表单的属性面:表单实例、布局、展开与按钮自定义 */
24
25
  export type SearchFormProps = {
26
+ /** 搜索表单实例,由 table.searchForm 提供 */
25
27
  form: Form;
28
+ /** 布局形态,默认 'grid' */
26
29
  layout?: 'grid' | 'inline';
30
+ /** 展开配置,默认 true */
27
31
  expand?: boolean | Expand;
32
+ /** 自定义搜索按钮,false 时不渲染 */
28
33
  searchButton?: Component<ButtonProps> | DefineComponent<ButtonProps> | false;
34
+ /** 自定义重置按钮,false 时不渲染 */
29
35
  resetButton?: Component<ButtonProps> | DefineComponent<ButtonProps> | false;
36
+ /** 自定义展开/折叠按钮,false 时不渲染 */
30
37
  expandButton?:
31
38
  | Component<ExpandButtonProps>
32
39
  | DefineComponent<ExpandButtonProps>
33
40
  | false;
41
+ /** 行间距,默认 16 */
34
42
  rowGap?: number;
43
+ /** 列间距,默认 24 */
35
44
  columnGap?: number;
36
45
  } & /* @vue-ignore */ _FormProps &
37
46
  AllowedComponentProps;
@@ -39,32 +48,42 @@ export type SearchFormProps = {
39
48
 
40
49
  <script lang="ts" setup>
41
50
  /**
51
+ * ProTable 的搜索表单子组件
52
+ *
42
53
  * @component SearchForm
43
- * @description ProTable 的搜索表单子组件
44
54
  *
45
55
  * ## 核心功能
46
56
  *
47
57
  * 1. 内部渲染 ProForm,使用 table.searchForm 管理搜索条件
48
58
  * 2. 支持 grid(网格)和 inline(行内)两种布局
49
- * 3. 网格布局下支持展开/折叠(溢出检测与初值自动展开见下方机制说明)
59
+ * 3. 网格布局下支持展开/折叠
50
60
  * 4. 提供搜索、重置、展开/折叠按钮(支持自定义)
51
61
  *
52
- * ## 展开/折叠机制
62
+ * ## 展开/折叠行为
63
+ *
64
+ * 1. 内容高度超出折叠高度(由 minExpandRows 决定,默认 2 行)时显示展开/折叠按钮
65
+ * 2. 隐藏区域存在搜索值时自动展开
66
+ * 3. 用户手动切换后保持手动状态,不随重新测量回退
53
67
  *
54
- * 1. 首次渲染后,测量 ProForm 的实际高度
55
- * 2. 根据 minExpandRows 计算折叠高度(默认 2 行)
56
- * 3. 如果实际高度 > 折叠高度,显示展开/折叠按钮
57
- * 4. 使用 IntersectionObserver 检测隐藏字段是否有值,有值则自动展开
68
+ * @param {Form} form - 搜索表单实例,由 table.searchForm 提供
69
+ * @param {'grid' | 'inline'} [layout] - 布局形态,默认 'grid'
70
+ * @param {boolean | Expand} [expand] - 展开配置,默认 true
71
+ * @param {Component<ButtonProps> | DefineComponent<ButtonProps> | false} [searchButton] - 自定义搜索按钮,false 时不渲染
72
+ * @param {Component<ButtonProps> | DefineComponent<ButtonProps> | false} [resetButton] - 自定义重置按钮,false 时不渲染
73
+ * @param {Component<ExpandButtonProps> | DefineComponent<ExpandButtonProps> | false} [expandButton] - 自定义展开/折叠按钮,false 时不渲染
74
+ * @param {number} [rowGap] - 行间距,默认 16
75
+ * @param {number} [columnGap] - 列间距,默认 24
76
+ *
77
+ * @slot reset-button - 重置按钮插槽,作用域参数 onReset
78
+ * @slot search-button - 搜索按钮插槽,作用域参数 onSearch
79
+ * @slot expand-button - 展开/折叠按钮插槽,作用域参数 expandStatus / changeExpandStatus
58
80
  */
59
- import { Space, Button, FormProps } from '../../../shared/ui';
60
- import {
61
- AllowedComponentProps,
62
- computed,
63
- type Component,
64
- type DefineComponent,
65
- } from 'vue';
66
- import ProForm from '../../form';
67
- import { type Form } from '../../form';
81
+ import type { FormProps } from '../../../shared/ui';
82
+ import { Space, Button } from '../../../shared/ui';
83
+ import type { AllowedComponentProps } from 'vue';
84
+ import { computed, nextTick, type Component, type DefineComponent } from 'vue';
85
+ import ProForm from '../../form/components/ProForm/index.vue';
86
+ import { type Form } from '../../form/hooks/useForm';
68
87
  import DownOutlined from './icons/DownOutlined.vue';
69
88
  import { useLocale } from '../../../locale';
70
89
  import { useSearchFormExpand } from '../../../shared/core';
@@ -81,7 +100,6 @@ const props = withDefaults(defineProps<SearchFormProps>(), {
81
100
  expandButton: undefined,
82
101
  rowGap: 16,
83
102
  columnGap: 24,
84
- form: undefined,
85
103
  });
86
104
 
87
105
  type Emits = {
@@ -117,9 +135,28 @@ const onReset = () => {
117
135
  emit('reset');
118
136
  };
119
137
 
120
- const onSearch = () => {
138
+ // 同一 tick 内 click 与 submit 只放行首个触达者,下一 tick 释放标记
139
+ let searchTriggeredInTick = false;
140
+
141
+ const emitSearch = () => {
142
+ searchTriggeredInTick = true;
143
+ nextTick(() => {
144
+ searchTriggeredInTick = false;
145
+ });
121
146
  emit('search');
122
147
  };
148
+
149
+ // 默认查询按钮为 html-type=submit:点击与回车统一经表单 submit 事件触发
150
+ const onSearchSubmit = () => {
151
+ if (searchTriggeredInTick) return;
152
+ emitSearch();
153
+ };
154
+
155
+ // 自定义查询按钮与 search-button 插槽经 click 触发
156
+ const onSearchClick = () => {
157
+ if (searchTriggeredInTick) return;
158
+ emitSearch();
159
+ };
123
160
  </script>
124
161
 
125
162
  <template>
@@ -127,6 +164,7 @@ const onSearch = () => {
127
164
  v-bind="layoutProps"
128
165
  :form="props.form"
129
166
  class="pro-table_search-form transition"
167
+ @submit.prevent="onSearchSubmit"
130
168
  >
131
169
  <Space align="start" class="pro-table_search-form_button-group">
132
170
  <slot name="reset-button" :on-reset="onReset">
@@ -144,19 +182,18 @@ const onSearch = () => {
144
182
  >
145
183
  </template>
146
184
  </slot>
147
- <slot name="search-button" :on-search="onSearch">
185
+ <slot name="search-button" :on-search="onSearchClick">
148
186
  <template v-if="props.searchButton !== false">
149
187
  <component
150
188
  :is="props.searchButton"
151
189
  v-if="props.searchButton"
152
- @click="onSearch"
190
+ @click="onSearchClick"
153
191
  />
154
192
  <Button
155
193
  v-else
156
194
  class="pro-table_search-form_search-button"
157
195
  type="primary"
158
196
  html-type="submit"
159
- @click="onSearch"
160
197
  >{{ t('table.query') }}
161
198
  </Button>
162
199
  </template>
@@ -1,4 +1,12 @@
1
1
  <script lang="ts" setup>
2
+ /**
3
+ * 表格尺寸切换控件,以图标按钮 + 下拉菜单呈现
4
+ *
5
+ * @component SizeControl
6
+ *
7
+ * @param {Size} [size] - 表格尺寸(`v-model:size`),仅用于回写选中结果
8
+ * @param {Size} [effectiveSize] - 表格实际使用的尺寸,用于高亮选中项;未提供时依次回退 `size`、宿主 ConfigProvider 的 componentSize
9
+ */
2
10
  import {
3
11
  Dropdown,
4
12
  Menu,
@@ -17,10 +25,22 @@ const { t } = useLocale();
17
25
 
18
26
  const configContext = useConfigContextInject();
19
27
 
28
+ const props = withDefaults(
29
+ defineProps<{
30
+ effectiveSize?: Size;
31
+ }>(),
32
+ { effectiveSize: undefined }
33
+ );
34
+
20
35
  const size = defineModel<Size>('size');
21
36
 
37
+ // 高亮项:表格有效尺寸 > 本地绑定 > 宿主 ConfigProvider 的 componentSize
22
38
  const currentSize = computed(
23
- () => size.value ?? configContext.componentSize?.value ?? 'large'
39
+ () =>
40
+ props.effectiveSize ??
41
+ size.value ??
42
+ configContext.componentSize?.value ??
43
+ 'large'
24
44
  );
25
45
 
26
46
  const onSizeChange = (val: any) => {
@@ -9,7 +9,7 @@ import type {
9
9
  PageParam,
10
10
  } from '../../../shared/core';
11
11
  import type { Column, Columns } from '../types';
12
- import { Fields } from '../../form/types';
12
+ import type { Fields } from '../../form/types';
13
13
 
14
14
  /**
15
15
  * 表格实例类型