@qin-ui/element-plus-pro 1.3.5 → 1.4.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 (93) hide show
  1. package/AI-CONTEXT.md +26 -2
  2. package/README.md +41 -30
  3. package/api.json +69 -38
  4. package/es/components/component-provider/components/index.vue.d.ts +10 -3
  5. package/es/components/component-provider/components/index.vue.d.ts.map +1 -1
  6. package/es/components/component-provider/constants/index.d.ts +22 -4
  7. package/es/components/component-provider/constants/index.d.ts.map +1 -1
  8. package/es/components/component-provider/types/index.d.ts +15 -1
  9. package/es/components/component-provider/types/index.d.ts.map +1 -1
  10. package/es/components/form/components/BaseField/index.vue.d.ts.map +1 -1
  11. package/es/components/form/components/BaseField/utils/index.d.ts +15 -0
  12. package/es/components/form/components/BaseField/utils/index.d.ts.map +1 -1
  13. package/es/components/form/components/BaseFormItem/index.vue.d.ts +3 -1
  14. package/es/components/form/components/BaseFormItem/index.vue.d.ts.map +1 -1
  15. package/es/components/form/components/ContainerFragment/index.vue.d.ts.map +1 -1
  16. package/es/components/form/components/ProForm/index.vue.d.ts.map +1 -1
  17. package/es/components/form/components/SlotComponent/index.vue.d.ts.map +1 -1
  18. package/es/components/form/components/index.d.ts +4 -0
  19. package/es/components/form/components/index.d.ts.map +1 -1
  20. package/es/components/form/constants/index.d.ts +8 -2
  21. package/es/components/form/constants/index.d.ts.map +1 -1
  22. package/es/components/form/hooks/useFields.d.ts.map +1 -1
  23. package/es/components/form/hooks/useForm.d.ts.map +1 -1
  24. package/es/components/form/hooks/useGroupFieldAttrs.d.ts +9 -0
  25. package/es/components/form/hooks/useGroupFieldAttrs.d.ts.map +1 -1
  26. package/es/components/form/index.d.ts +2 -2
  27. package/es/components/form/index.d.ts.map +1 -1
  28. package/es/components/form/types/index.d.ts +65 -50
  29. package/es/components/form/types/index.d.ts.map +1 -1
  30. package/es/components/form/utils/fieldKey.d.ts +13 -0
  31. package/es/components/form/utils/fieldKey.d.ts.map +1 -0
  32. package/es/components/table/components/ColumnControl.vue.d.ts +1 -1
  33. package/es/components/table/components/ColumnControl.vue.d.ts.map +1 -1
  34. package/es/components/table/components/ProTable.vue.d.ts +15 -1
  35. package/es/components/table/components/ProTable.vue.d.ts.map +1 -1
  36. package/es/components/table/components/SearchForm.vue.d.ts +13 -1
  37. package/es/components/table/components/SearchForm.vue.d.ts.map +1 -1
  38. package/es/components/table/components/SizeControl.vue.d.ts +7 -2
  39. package/es/components/table/components/SizeControl.vue.d.ts.map +1 -1
  40. package/es/components/table/components/TableColumnRenderer.vue.d.ts +14 -14
  41. package/es/components/table/components/TableColumnRenderer.vue.d.ts.map +1 -1
  42. package/es/components/table/hooks/index.d.ts +2 -0
  43. package/es/components/table/hooks/index.d.ts.map +1 -0
  44. package/es/components/table/{useTable.d.ts → hooks/useTable.d.ts} +3 -3
  45. package/es/components/table/hooks/useTable.d.ts.map +1 -0
  46. package/es/components/table/index.d.ts +6 -2
  47. package/es/components/table/index.d.ts.map +1 -1
  48. package/es/components/table/types/index.d.ts +16 -12
  49. package/es/components/table/types/index.d.ts.map +1 -1
  50. package/es/element-plus-pro.css +14 -14
  51. package/es/index.d.ts +9 -6
  52. package/es/index.d.ts.map +1 -1
  53. package/es/index.js +307 -224
  54. package/es/index.js.map +1 -1
  55. package/es/locale/index.d.ts +2 -1
  56. package/es/locale/index.d.ts.map +1 -1
  57. package/es/shared/core/index.d.ts +1 -1
  58. package/es/shared/core/index.d.ts.map +1 -1
  59. package/es/shared/ui/index.d.ts +20 -2
  60. package/es/shared/ui/index.d.ts.map +1 -1
  61. package/package.json +6 -4
  62. package/src/components/component-provider/components/index.vue +48 -21
  63. package/src/components/component-provider/constants/index.ts +34 -6
  64. package/src/components/component-provider/types/index.ts +21 -7
  65. package/src/components/form/components/BaseField/index.vue +25 -12
  66. package/src/components/form/components/BaseField/utils/index.ts +16 -1
  67. package/src/components/form/components/BaseFormItem/index.vue +24 -28
  68. package/src/components/form/components/ContainerFragment/index.vue +3 -1
  69. package/src/components/form/components/ProForm/index.vue +42 -22
  70. package/src/components/form/components/SlotComponent/index.vue +4 -2
  71. package/src/components/form/components/index.ts +8 -1
  72. package/src/components/form/constants/index.ts +9 -2
  73. package/src/components/form/hooks/useFields.ts +5 -2
  74. package/src/components/form/hooks/useForm.ts +8 -1
  75. package/src/components/form/hooks/useGroupFieldAttrs.ts +24 -17
  76. package/src/components/form/index.ts +1 -3
  77. package/src/components/form/types/index.ts +71 -52
  78. package/src/components/form/utils/fieldKey.ts +27 -0
  79. package/src/components/table/components/ColumnControl.vue +16 -6
  80. package/src/components/table/components/ProTable.vue +183 -93
  81. package/src/components/table/components/SearchForm.vue +60 -22
  82. package/src/components/table/components/SizeControl.vue +23 -1
  83. package/src/components/table/components/TableColumnRenderer.vue +82 -63
  84. package/src/components/table/hooks/index.ts +1 -0
  85. package/src/components/table/{useTable.ts → hooks/useTable.ts} +6 -4
  86. package/src/components/table/index.ts +11 -2
  87. package/src/components/table/types/index.ts +17 -13
  88. package/src/index.ts +13 -7
  89. package/src/locale/index.ts +4 -2
  90. package/src/shared/core/index.ts +11 -0
  91. package/src/shared/ui/index.ts +29 -5
  92. package/tsconfig.json +0 -1
  93. package/es/components/table/useTable.d.ts.map +0 -1
@@ -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,12 +26,27 @@
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 default - 保留给列渲染器,传入内容不参与渲染,自定义区域请使用下列具名插槽
46
+ * @slot search-form - 搜索区域插槽(无作用域)
47
+ * @slot button-bar - 表格头部按钮区插槽
48
+ * @slot toolbar - 工具栏插槽
49
+ * @slot table - 表格区域插槽(无作用域)
34
50
  */
35
51
  import {
36
52
  Pagination as ElPagination,
@@ -43,9 +59,13 @@ import {
43
59
  } from '../../../shared/ui';
44
60
  import { ElLoading } from 'element-plus';
45
61
  import {
62
+ Comment,
63
+ Fragment,
64
+ Text,
46
65
  computed,
66
+ effect,
67
+ stop,
47
68
  nextTick,
48
- useAttrs,
49
69
  onMounted,
50
70
  inject,
51
71
  toValue,
@@ -57,15 +77,18 @@ import ContainerFragment from '../../form/components/ContainerFragment/index.vue
57
77
  import type { ContainerComponent } from '../../form/types';
58
78
  import SearchForm from './SearchForm.vue';
59
79
  import type { SearchFormProps } from './SearchForm.vue';
60
- import type { Table } from '../useTable';
80
+ import type { Table } from '../hooks';
61
81
  import { useLocale } from '../../../locale';
82
+ import type { Path } from '../../../shared/core';
62
83
  import {
63
84
  camelizeProperties,
64
85
  getObject,
65
- pick,
66
- isPromise,
86
+ isObjectItem,
67
87
  isPlainObject,
68
- Path,
88
+ isPromise,
89
+ omitBy,
90
+ pick,
91
+ useReactiveAttrs,
69
92
  } from '../../../shared/core';
70
93
  import DefaultSearchFormContainer from './DefaultSearchFormContainer.vue';
71
94
  import DefaultTableContainer from './DefaultTableContainer.vue';
@@ -86,14 +109,17 @@ const tableProperties = Object.keys(tableProps());
86
109
  const paginationProperties = Object.keys(paginationProps());
87
110
 
88
111
  type Control = {
112
+ /** 是否展示表格尺寸控制 */
89
113
  sizeControl: boolean;
114
+ /** 是否展示列控制,传对象时作为列控面板配置 */
90
115
  columnControl: boolean | ColumnControlConfig<Path<RecordType>>;
91
116
  };
92
117
 
118
+ /** 搜索表单配置:SearchFormProps 去掉 form,并加上搜索区显隐与容器 */
93
119
  type SearchFormConfig = Omit<SearchFormProps, 'form'> & {
94
- // 是否隐藏搜索表单
120
+ /** 是否隐藏搜索表单 */
95
121
  hidden?: boolean;
96
- // 搜索区域包裹容器
122
+ /** 搜索区域包裹容器 */
97
123
  container?: ContainerComponent | false;
98
124
  };
99
125
 
@@ -101,25 +127,31 @@ type ReturnTableRecord<V> = V extends Table<any, infer R> ? R : any;
101
127
  type RecordType = ReturnTableRecord<T>;
102
128
 
103
129
  type Props = {
104
- // 列表表格对象,returns from useTable
130
+ /** 列表表格对象,来自 useTable */
105
131
  table?: T;
106
- // 列表数据查询方法
132
+ /** 列表数据查询方法 */
107
133
  search?: () => Promise<unknown>;
108
- // 是否在首列插入index列
134
+ /** 是否在首列插入序号列 */
109
135
  addIndexColumn?: boolean;
110
- // onMounted 时立即触发一次 search 事件
136
+ /** 挂载后是否立即触发一次查询 */
111
137
  immediateSearch?: boolean;
112
- // 是否展示表格 size 和 column 控制按钮
138
+ /** 是否展示尺寸与列控制按钮,支持分别控制;显式传入 columns 时列控入口不渲染 */
113
139
  control?: boolean | Partial<Control>;
114
- // 搜索栏查询字段表单配置
140
+ /** 搜索栏查询字段表单配置 */
115
141
  searchFormConfig?: SearchFormConfig;
116
- // 表格区域包裹容器
142
+ /** 表格区域包裹容器 */
117
143
  tableContainer?: ContainerComponent | false;
144
+ /** 直接传入列配置(覆盖 :table 实例的 columns) */
118
145
  columns?: Columns<RecordType>;
146
+ /** 直接传入数据源(覆盖 :table 实例的 data) */
119
147
  data?: RecordType[];
148
+ /** 分页配置,传 false 禁用 */
120
149
  pagination?: false | Partial<PaginationProps>;
121
150
  } & /* @vue-ignore */ Omit<TableProps, 'data'>;
122
151
 
152
+ /*
153
+ * 组件自身仅触发模型事件;宿主表格事件不入运行时 emits,监听入口保留在 attrs 中透传至 ElTable
154
+ */
123
155
  type Emits = {} & /* @vue-ignore */ TableEmits<RecordType>;
124
156
 
125
157
  defineEmits<Emits>();
@@ -143,34 +175,23 @@ const config = INJECT_CONFIG['pro-table'];
143
175
  const injectPropsRaw = inject(config.injectionKey, config.default);
144
176
  const injectProps = computed(() => toValue(injectPropsRaw) || {});
145
177
 
146
- const attrs = useAttrs() as Record<string, any>;
178
+ const attrs = useReactiveAttrs() as Record<string, any>;
147
179
 
148
- const PRO_TABLE_EXCLUDED_ATTR_KEYS = new Set([
180
+ // 组件自身消费的 attrs 键:class / style 绑定根节点,onSearch / onChange 由搜索与分页流程消费
181
+ const PRO_TABLE_CONSUMED_ATTR_KEYS = new Set([
149
182
  'class',
150
183
  'style',
151
184
  'onSearch',
152
185
  'onChange',
153
- 'onUpdate:size',
154
- 'onUpdate:loading',
155
- 'data',
156
- 'columns',
157
- 'size',
158
- 'pagination',
159
186
  ]);
160
187
 
161
- const PRO_TABLE_PROVIDER_KEYS = new Set([
162
- 'pagination',
163
- 'searchFormConfig',
164
- 'control',
165
- 'immediateSearch',
166
- 'addIndexColumn',
167
- 'tableContainer',
168
- ]);
188
+ // 组件自身消费的 props 键:对应值按 prop / provider 单一来源派生,provider 注入不再透传这些键
189
+ const PRO_TABLE_PROP_KEYS = new Set(Object.keys(props));
169
190
 
170
191
  const injectTableAttrs = computed(() => {
171
192
  const tableConfig: Record<string, any> = {};
172
193
  for (const [key, value] of Object.entries(injectProps.value)) {
173
- if (!PRO_TABLE_PROVIDER_KEYS.has(key)) {
194
+ if (!PRO_TABLE_PROP_KEYS.has(key)) {
174
195
  tableConfig[key] = value;
175
196
  }
176
197
  }
@@ -178,20 +199,20 @@ const injectTableAttrs = computed(() => {
178
199
  });
179
200
 
180
201
  const attrsTableProps = computed(() => {
181
- return pick(
182
- camelizeProperties(attrs),
183
- tableProperties
202
+ return omitBy(
203
+ pick(camelizeProperties(attrs), tableProperties),
204
+ value => value === undefined
184
205
  ) as Partial<TableProps>;
185
206
  });
186
207
 
187
208
  const restTableAttrs = computed(() => {
188
209
  const rest: Record<string, any> = {};
189
210
  for (const [key, value] of Object.entries(attrs)) {
190
- if (!PRO_TABLE_EXCLUDED_ATTR_KEYS.has(key)) {
211
+ if (!PRO_TABLE_CONSUMED_ATTR_KEYS.has(key)) {
191
212
  rest[key] = value;
192
213
  }
193
214
  }
194
- return rest;
215
+ return omitBy(rest, value => value === undefined);
195
216
  });
196
217
 
197
218
  const size = defineModel<TableProps['size']>('size');
@@ -199,23 +220,28 @@ const loading = defineModel<boolean>('loading', { default: false });
199
220
 
200
221
  // 递增序号做请求竞态守卫,仅最新一次请求可变更 loading
201
222
  let searchSequence = 0;
202
- let warnedSearchConfig = false;
203
- const warnSearchConfig = (reason: string) => {
204
- if (warnedSearchConfig) return;
205
- warnedSearchConfig = true;
206
- console.warn(`[ProTable] ${reason}`);
223
+ // 告警文案按原因分键,各原因独立去重
224
+ const SEARCH_WARN_MESSAGES = {
225
+ duplicate:
226
+ '同时配置了 :search prop 与 @search 监听,@search 监听不会被执行,请仅保留其一。',
227
+ 'not-promise':
228
+ 'search 未返回 Promise,本次查询不参与 loading 竞态管理(在途请求的 loading 状态保持不变)。',
229
+ } as const;
230
+ const warnedSearchReasons = new Set<keyof typeof SEARCH_WARN_MESSAGES>();
231
+ const warnSearchConfig = (reason: keyof typeof SEARCH_WARN_MESSAGES) => {
232
+ if (warnedSearchReasons.has(reason)) return;
233
+ warnedSearchReasons.add(reason);
234
+ console.warn(`[ProTable] ${SEARCH_WARN_MESSAGES[reason]}`);
207
235
  };
208
236
  const _search = async () => {
237
+ const search = props.search || attrs.onSearch;
209
238
  if (props.search && attrs.onSearch) {
210
- warnSearchConfig(
211
- '同时配置了 :search prop 与 @search 监听,@search 监听不会被执行,请仅保留其一。'
212
- );
239
+ warnSearchConfig('duplicate');
213
240
  }
214
- const searchPromise = (props.search || attrs.onSearch)?.();
241
+ if (!search) return;
242
+ const searchPromise = search();
215
243
  if (!isPromise(searchPromise)) {
216
- warnSearchConfig(
217
- 'search 未返回 Promise,本次查询不参与 loading 竞态管理(在途请求的 loading 状态保持不变)。'
218
- );
244
+ warnSearchConfig('not-promise');
219
245
  return;
220
246
  }
221
247
  const seq = ++searchSequence;
@@ -250,8 +276,57 @@ type CustomSlots = Readonly<
250
276
  type Slots = CustomSlots & Readonly<Record<string, Slot>>;
251
277
 
252
278
  const slots = useSlots() as Slots;
253
- const forwardedSlotNames = () =>
254
- Object.keys(slots).filter(name => !customSlotNames.includes(name as any));
279
+
280
+ /** 插槽产出是否含可渲染内容:注释节点与空白文本不计 */
281
+ const hasRenderableContent = (nodes: unknown): boolean => {
282
+ if (nodes === null || nodes === undefined || nodes === false) return false;
283
+ if (typeof nodes === 'string') return nodes.trim() !== '';
284
+ if (typeof nodes === 'number') return true;
285
+ if (Array.isArray(nodes)) return nodes.some(hasRenderableContent);
286
+ const vnode = nodes as { type?: unknown; children?: unknown };
287
+ if (vnode.type === Comment) return false;
288
+ if (vnode.type === Text) return String(vnode.children ?? '').trim() !== '';
289
+ if (vnode.type === Fragment) return hasRenderableContent(vnode.children);
290
+ return true;
291
+ };
292
+
293
+ /*
294
+ * 求值一次 default 插槽产出,用于判定是否有可渲染内容(空插槽 / 包装组件透传不提示)。
295
+ * 求值由一次性 effect 执行并立即停止,插槽内的响应式读取不进入 ProTable 的渲染依赖。
296
+ */
297
+ const evaluateSlotContent = (): boolean => {
298
+ let hasContent = false;
299
+ const runner = effect(() => {
300
+ try {
301
+ hasContent = hasRenderableContent(slots.default?.());
302
+ } catch {
303
+ // 产出无法求值时按有内容处理,照常提示 default 插槽被保留
304
+ hasContent = true;
305
+ }
306
+ });
307
+ stop(runner);
308
+ return hasContent;
309
+ };
310
+
311
+ // default 插槽保留给列渲染器,同名用户插槽不透传宿主表格
312
+ let defaultSlotChecked = false;
313
+ const checkDefaultSlot = () => {
314
+ if (defaultSlotChecked) return;
315
+ if (!Object.prototype.hasOwnProperty.call(slots, 'default')) return;
316
+ defaultSlotChecked = true;
317
+ if (evaluateSlotContent()) {
318
+ console.warn(
319
+ '[pro-components] ProTable 的 default 插槽保留用于列渲染,已忽略传入的 default 插槽内容;自定义区域请使用 search-form / button-bar / toolbar / table 具名插槽。'
320
+ );
321
+ }
322
+ };
323
+
324
+ const forwardedSlotNames = () => {
325
+ checkDefaultSlot();
326
+ return Object.keys(slots).filter(
327
+ name => name !== 'default' && !customSlotNames.includes(name as any)
328
+ );
329
+ };
255
330
 
256
331
  const searchPage1st = () => {
257
332
  props.table?.setPageParam?.({ current: 1 });
@@ -270,6 +345,16 @@ const computedSearchFormConfig = computed(() => {
270
345
  };
271
346
  });
272
347
 
348
+ /** SearchForm 属性面:容器与显隐由 ProTable 自身消费,不下发 SearchForm */
349
+ const searchFormProps = computed(() => {
350
+ const {
351
+ container: _container,
352
+ hidden: _hidden,
353
+ ...rest
354
+ } = computedSearchFormConfig.value;
355
+ return rest;
356
+ });
357
+
273
358
  const computedTableContainer = computed(() => {
274
359
  const container =
275
360
  props.tableContainer ??
@@ -278,19 +363,26 @@ const computedTableContainer = computed(() => {
278
363
  return container ? container : undefined;
279
364
  });
280
365
 
366
+ /*
367
+ * 序号列随有效分页偏移:分页关闭时退化为 index + 1;
368
+ * current / pageSize 取有效分页(显式 :pagination 或 Provider 注入),table.pageParam 仅作兜底
369
+ */
281
370
  const getIndex = (i: number) => {
282
- if (props.table?.pageParam && computedPagination.value !== false) {
283
- const current = props.table.pageParam.current ?? 1;
284
- const pageSize = props.table.pageParam.pageSize ?? 10;
285
- return (current - 1) * pageSize + i + 1;
286
- }
287
- return i + 1;
371
+ const pagination = computedPagination.value;
372
+ if (pagination === false) return i + 1;
373
+ const pageParam = props.table?.pageParam;
374
+ const current = pagination.currentPage ?? pageParam?.current ?? 1;
375
+ const pageSize = pagination.pageSize ?? pageParam?.pageSize ?? 10;
376
+ return (current - 1) * pageSize + i + 1;
288
377
  };
289
378
 
379
+ /** 调用方是否显式传入 columns(值为 undefined 时视为未指定) */
380
+ const hasExplicitColumns = computed(() => props.columns !== undefined);
381
+
290
382
  const normalizedRawColumns = computed(() => {
291
- const source = (props.columns ??
292
- toValue(props.table?.columns) ??
293
- []) as any[];
383
+ const source = (
384
+ (props.columns ?? toValue(props.table?.columns) ?? []) as any[]
385
+ ).filter(isObjectItem);
294
386
  if (props.addIndexColumn ?? injectProps.value.addIndexColumn) {
295
387
  return [
296
388
  {
@@ -307,35 +399,25 @@ const normalizedRawColumns = computed(() => {
307
399
  });
308
400
 
309
401
  const computedDataSource = computed<any[]>(() => {
310
- const attrsData = attrsTableProps.value.data as any[] | undefined;
311
- return props.data ?? attrsData ?? toValue(props.table?.data) ?? [];
402
+ return props.data ?? toValue(props.table?.data) ?? [];
312
403
  });
313
404
 
314
405
  const computedTableProps = computed<TableProps>(() => {
315
- const merged = {
406
+ return {
316
407
  ...injectTableAttrs.value,
317
408
  ...restTableAttrs.value,
318
409
  ...attrsTableProps.value,
319
410
  } as TableProps;
320
- delete (merged as any).data;
321
- delete (merged as any).columns;
322
- delete (merged as any).size;
323
- delete (merged as any).pagination;
324
- return merged;
325
411
  });
326
412
 
327
- // 分页优先级:tableAttrs 显式配置 > provider 全局配置 > 默认合成
413
+ /*
414
+ * 分页来源:分页开关与展示配置取 props.pagination 显式配置 > provider 全局配置 > 默认合成;
415
+ * currentPage / pageSize / total 以 table.pageParam 为实时状态,显式配置仅在 pageParam 缺省时兜底
416
+ */
328
417
  const computedPagination = computed<false | Partial<PaginationProps>>(() => {
329
- const attrsPagination = (attrsTableProps.value as any).pagination;
330
- if (props.pagination === false || attrsPagination === false) {
331
- return false;
332
- }
333
- const hasUserPagination =
334
- isPlainObject(props.pagination) || isPlainObject(attrsPagination);
335
- if (hasUserPagination) {
336
- const pageConfig = (props.pagination ??
337
- attrsPagination ??
338
- {}) as Partial<PaginationProps>;
418
+ if (props.pagination === false) return false;
419
+ if (isPlainObject(props.pagination)) {
420
+ const pageConfig = props.pagination as Partial<PaginationProps>;
339
421
  const globalPagination =
340
422
  (injectProps.value.pagination as any) === false
341
423
  ? {}
@@ -386,11 +468,15 @@ const searchFormVisible = computed(() => {
386
468
  return !!(
387
469
  (computedSearchFormConfig.value as any)?.hidden !== true &&
388
470
  props.table?.searchForm &&
389
- fieldList.filter((item: any) => !toValue(item.hidden))?.length > 0
471
+ fieldList.filter((item: any) => isObjectItem(item) && !toValue(item.hidden))
472
+ .length > 0
390
473
  );
391
474
  });
392
475
 
393
- const computedControl = computed<{
476
+ /*
477
+ * 控制项来源:显式控制配置 > Provider 全局配置
478
+ */
479
+ const resolvedControl = computed<{
394
480
  sizeControl: boolean;
395
481
  columnControl: boolean | ColumnControlConfig<Path<RecordType>>;
396
482
  }>(() => {
@@ -404,17 +490,21 @@ const computedControl = computed<{
404
490
  return { sizeControl: !!sizeControl, columnControl };
405
491
  });
406
492
 
493
+ /* 显式 columns 模式下渲染源不受 table.setColumn 影响,列控入口隐藏 */
494
+ const computedControl = computed(() =>
495
+ hasExplicitColumns.value && resolvedControl.value.columnControl
496
+ ? { ...resolvedControl.value, columnControl: false }
497
+ : resolvedControl.value
498
+ );
499
+
407
500
  const columnControlConfig = computed(() => {
408
501
  const cc = computedControl.value.columnControl;
409
502
  return typeof cc === 'object' ? cc : undefined;
410
503
  });
411
504
 
505
+ // 尺寸来源:size 模型 > provider 全局配置
412
506
  const resolvedSize = computed(() => {
413
- return (
414
- size.value ??
415
- (attrsTableProps.value.size as any) ??
416
- injectTableAttrs.value.size
417
- );
507
+ return size.value ?? injectProps.value.size;
418
508
  });
419
509
 
420
510
  onMounted(() => {
@@ -439,7 +529,7 @@ defineExpose({
439
529
  <SearchForm
440
530
  v-if="props.table?.searchForm"
441
531
  :form="props.table.searchForm"
442
- v-bind="computedSearchFormConfig"
532
+ v-bind="searchFormProps"
443
533
  @search="searchPage1st"
444
534
  @reset="reset"
445
535
  />
@@ -458,7 +548,7 @@ defineExpose({
458
548
  v-if="computedControl.sizeControl"
459
549
  class="pro-table_header_size-control"
460
550
  >
461
- <SizeControl v-model:size="size" />
551
+ <SizeControl v-model:size="size" :effective-size="resolvedSize" />
462
552
  </div>
463
553
  <div
464
554
  v-if="computedControl.columnControl"
@@ -1,8 +1,8 @@
1
1
  <script lang="ts">
2
2
  type Expand = {
3
- // 网格布局时默认展开行数量
3
+ /** 网格布局时默认展开行数量 */
4
4
  minExpandRows?: number;
5
- // 网格布局时展开状态
5
+ /** 网格布局时展开状态 */
6
6
  expandStatus?: boolean;
7
7
  };
8
8
 
@@ -21,17 +21,28 @@ type _FormProps = Pick<
21
21
  type ButtonProps = { onClick: () => void };
22
22
  type ExpandButtonProps = ButtonProps & { expandStatus: boolean };
23
23
 
24
+ /**
25
+ * 搜索表单属性面,由 ProTable 经 `searchFormConfig` 下发
26
+ */
24
27
  export type SearchFormProps = {
28
+ /** 搜索表单实例,由 table.searchForm 提供 */
25
29
  form: Form;
30
+ /** 布局形态,默认 'grid' */
26
31
  layout?: 'grid' | 'inline';
32
+ /** 展开配置,默认 true */
27
33
  expand?: boolean | Expand;
34
+ /** 自定义搜索按钮,false 时不渲染 */
28
35
  searchButton?: Component<ButtonProps> | DefineComponent<ButtonProps> | false;
36
+ /** 自定义重置按钮,false 时不渲染 */
29
37
  resetButton?: Component<ButtonProps> | DefineComponent<ButtonProps> | false;
38
+ /** 自定义展开/折叠按钮,false 时不渲染 */
30
39
  expandButton?:
31
40
  | Component<ExpandButtonProps>
32
41
  | DefineComponent<ExpandButtonProps>
33
42
  | false;
43
+ /** 行间距,默认 16 */
34
44
  rowGap?: number;
45
+ /** 列间距,默认 24 */
35
46
  columnGap?: number;
36
47
  } & /* @vue-ignore */ _FormProps &
37
48
  AllowedComponentProps;
@@ -39,30 +50,40 @@ export type SearchFormProps = {
39
50
 
40
51
  <script lang="ts" setup>
41
52
  /**
53
+ * ProTable 的搜索表单子组件
54
+ *
42
55
  * @component SearchForm
43
- * @description ProTable 的搜索表单子组件
44
56
  *
45
57
  * ## 核心功能
46
58
  *
47
59
  * 1. 内部渲染 ProForm,使用 table.searchForm 管理搜索条件
48
60
  * 2. 支持 grid(网格)和 inline(行内)两种布局
49
- * 3. 网格布局下支持展开/折叠(溢出检测与初值自动展开见下方机制说明)
61
+ * 3. 网格布局下支持展开/折叠
50
62
  * 4. 提供搜索、重置、展开/折叠按钮(支持自定义)
51
63
  *
52
- * ## 展开/折叠机制
64
+ * ## 展开/折叠行为
65
+ *
66
+ * 1. 内容高度超出折叠高度(由 minExpandRows 决定,默认 2 行)时显示展开/折叠按钮
67
+ * 2. 隐藏区域存在搜索值时自动展开
68
+ * 3. 用户手动切换后保持手动状态,不随重新测量回退
69
+ *
70
+ * @param {Form} form - 搜索表单实例,由 table.searchForm 提供
71
+ * @param {'grid' | 'inline'} [layout] - 布局形态,默认 'grid'
72
+ * @param {boolean | Expand} [expand] - 展开配置,默认 true
73
+ * @param {Component<ButtonProps> | DefineComponent<ButtonProps> | false} [searchButton] - 自定义搜索按钮,false 时不渲染
74
+ * @param {Component<ButtonProps> | DefineComponent<ButtonProps> | false} [resetButton] - 自定义重置按钮,false 时不渲染
75
+ * @param {Component<ExpandButtonProps> | DefineComponent<ExpandButtonProps> | false} [expandButton] - 自定义展开/折叠按钮,false 时不渲染
76
+ * @param {number} [rowGap] - 行间距,默认 16
77
+ * @param {number} [columnGap] - 列间距,默认 24
53
78
  *
54
- * 1. 首次渲染后,测量 ProForm 的实际高度
55
- * 2. 根据 minExpandRows 计算折叠高度(默认 2 行)
56
- * 3. 如果实际高度 > 折叠高度,显示展开/折叠按钮
57
- * 4. 使用 IntersectionObserver 检测隐藏字段是否有值,有值则自动展开
79
+ * @slot reset-button - 重置按钮插槽,作用域参数 onReset
80
+ * @slot search-button - 搜索按钮插槽,作用域参数 onSearch
81
+ * @slot expand-button - 展开/折叠按钮插槽,作用域参数 expandStatus / changeExpandStatus
58
82
  */
59
- import { Space, Button, FormProps } from '../../../shared/ui';
60
- import {
61
- AllowedComponentProps,
62
- computed,
63
- type Component,
64
- type DefineComponent,
65
- } from 'vue';
83
+ import type { FormProps } from '../../../shared/ui';
84
+ import { Space, Button } from '../../../shared/ui';
85
+ import type { AllowedComponentProps } from 'vue';
86
+ import { computed, nextTick, type Component, type DefineComponent } from 'vue';
66
87
  import ProForm from '../../form/components/ProForm/index.vue';
67
88
  import { type Form } from '../../form/hooks/useForm';
68
89
  import DownOutlined from './icons/DownOutlined.vue';
@@ -81,7 +102,6 @@ const props = withDefaults(defineProps<SearchFormProps>(), {
81
102
  expandButton: undefined,
82
103
  rowGap: 16,
83
104
  columnGap: 24,
84
- form: undefined,
85
105
  });
86
106
 
87
107
  type Emits = {
@@ -122,10 +142,28 @@ const onReset = () => {
122
142
  emit('reset');
123
143
  };
124
144
 
125
- // 查询按钮为 native-type=submit:点击与回车统一经表单 submit 事件触发
126
- const onSearch = () => {
145
+ // 同一 tick 内 click 与 submit 只放行首个触达者,下一 tick 释放标记
146
+ let searchTriggeredInTick = false;
147
+
148
+ const emitSearch = () => {
149
+ searchTriggeredInTick = true;
150
+ nextTick(() => {
151
+ searchTriggeredInTick = false;
152
+ });
127
153
  emit('search');
128
154
  };
155
+
156
+ // 默认查询按钮为 native-type=submit:点击与回车统一经表单 submit 事件触发
157
+ const onSearchSubmit = () => {
158
+ if (searchTriggeredInTick) return;
159
+ emitSearch();
160
+ };
161
+
162
+ // 自定义查询按钮与 search-button 插槽经 click 触发
163
+ const onSearchClick = () => {
164
+ if (searchTriggeredInTick) return;
165
+ emitSearch();
166
+ };
129
167
  </script>
130
168
 
131
169
  <template>
@@ -133,7 +171,7 @@ const onSearch = () => {
133
171
  v-bind="layoutProps"
134
172
  :form="props.form"
135
173
  class="pro-table_search-form transition"
136
- @submit.prevent="onSearch"
174
+ @submit.prevent="onSearchSubmit"
137
175
  >
138
176
  <Space alignment="start" class="pro-table_search-form_button-group">
139
177
  <slot name="reset-button" :on-reset="onReset">
@@ -152,12 +190,12 @@ const onSearch = () => {
152
190
  </Button>
153
191
  </template>
154
192
  </slot>
155
- <slot name="search-button" :on-search="onSearch">
193
+ <slot name="search-button" :on-search="onSearchClick">
156
194
  <template v-if="props.searchButton !== false">
157
195
  <component
158
196
  :is="props.searchButton"
159
197
  v-if="props.searchButton"
160
- @click="onSearch"
198
+ @click="onSearchClick"
161
199
  />
162
200
  <Button
163
201
  v-else
@@ -1,9 +1,18 @@
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 的尺寸
9
+ */
2
10
  import {
3
11
  Dropdown,
4
12
  DropdownMenu,
5
13
  DropdownItem,
6
14
  Button,
15
+ useSize,
7
16
  type TableProps,
8
17
  } from '../../../shared/ui';
9
18
  import ColumnHeightOutlined from './icons/ColumnHeightOutlined.vue';
@@ -14,9 +23,22 @@ type Size = TableProps['size'];
14
23
 
15
24
  const { t } = useLocale();
16
25
 
26
+ const props = withDefaults(
27
+ defineProps<{
28
+ effectiveSize?: Size;
29
+ }>(),
30
+ { effectiveSize: undefined }
31
+ );
32
+
17
33
  const size = defineModel<Size>('size');
18
34
 
19
- const currentSize = computed(() => size.value ?? 'default');
35
+ // 宿主 ConfigProvider / 表单上下文的尺寸兜底
36
+ const hostSize = useSize();
37
+
38
+ // 高亮项:表格有效尺寸 > 本地绑定 > 宿主 ConfigProvider 尺寸 > 默认
39
+ const currentSize = computed(
40
+ () => (props.effectiveSize ?? size.value ?? hostSize.value) || 'default'
41
+ );
20
42
 
21
43
  const items = computed<Array<{ label: string; key: NonNullable<Size> }>>(() => [
22
44
  { label: t('table.sizeDefault'), key: 'default' },