@qin-ui/element-plus-pro 1.3.4 → 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 +17 -3
  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 +14 -2
  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 +577 -432
  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 +8 -5
  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 +32 -15
  66. package/src/components/form/components/BaseField/utils/index.ts +16 -1
  67. package/src/components/form/components/BaseFormItem/index.vue +61 -15
  68. package/src/components/form/components/ContainerFragment/index.vue +3 -1
  69. package/src/components/form/components/ProForm/index.vue +54 -27
  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 +29 -30
  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 +20 -8
  80. package/src/components/table/components/ProTable.vue +194 -96
  81. package/src/components/table/components/SearchForm.vue +62 -23
  82. package/src/components/table/components/SizeControl.vue +23 -1
  83. package/src/components/table/components/TableColumnRenderer.vue +93 -61
  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 +22 -0
  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,
@@ -53,26 +73,27 @@ import {
53
73
  useSlots,
54
74
  } from 'vue';
55
75
  import { INJECT_CONFIG } from '../../component-provider/constants';
56
- import { ContainerFragment } from '../../form';
76
+ 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
- import {
71
- DefaultSearchFormContainer,
72
- DefaultTableContainer,
73
- SizeControl,
74
- ColumnControl,
75
- } from '.';
93
+ import DefaultSearchFormContainer from './DefaultSearchFormContainer.vue';
94
+ import DefaultTableContainer from './DefaultTableContainer.vue';
95
+ import SizeControl from './SizeControl.vue';
96
+ import ColumnControl from './ColumnControl.vue';
76
97
  import type { Columns, ColumnControlConfig } from '../types';
77
98
  import TableColumnRenderer from './TableColumnRenderer.vue';
78
99
 
@@ -88,14 +109,17 @@ const tableProperties = Object.keys(tableProps());
88
109
  const paginationProperties = Object.keys(paginationProps());
89
110
 
90
111
  type Control = {
112
+ /** 是否展示表格尺寸控制 */
91
113
  sizeControl: boolean;
114
+ /** 是否展示列控制,传对象时作为列控面板配置 */
92
115
  columnControl: boolean | ColumnControlConfig<Path<RecordType>>;
93
116
  };
94
117
 
118
+ /** 搜索表单配置:SearchFormProps 去掉 form,并加上搜索区显隐与容器 */
95
119
  type SearchFormConfig = Omit<SearchFormProps, 'form'> & {
96
- // 是否隐藏搜索表单
120
+ /** 是否隐藏搜索表单 */
97
121
  hidden?: boolean;
98
- // 搜索区域包裹容器
122
+ /** 搜索区域包裹容器 */
99
123
  container?: ContainerComponent | false;
100
124
  };
101
125
 
@@ -103,25 +127,31 @@ type ReturnTableRecord<V> = V extends Table<any, infer R> ? R : any;
103
127
  type RecordType = ReturnTableRecord<T>;
104
128
 
105
129
  type Props = {
106
- // 列表表格对象,returns from useTable
130
+ /** 列表表格对象,来自 useTable */
107
131
  table?: T;
108
- // 列表数据查询方法
132
+ /** 列表数据查询方法 */
109
133
  search?: () => Promise<unknown>;
110
- // 是否在首列插入index列
134
+ /** 是否在首列插入序号列 */
111
135
  addIndexColumn?: boolean;
112
- // onMounted 时立即触发一次 search 事件
136
+ /** 挂载后是否立即触发一次查询 */
113
137
  immediateSearch?: boolean;
114
- // 是否展示表格 size 和 column 控制按钮
138
+ /** 是否展示尺寸与列控制按钮,支持分别控制;显式传入 columns 时列控入口不渲染 */
115
139
  control?: boolean | Partial<Control>;
116
- // 搜索栏查询字段表单配置
140
+ /** 搜索栏查询字段表单配置 */
117
141
  searchFormConfig?: SearchFormConfig;
118
- // 表格区域包裹容器
142
+ /** 表格区域包裹容器 */
119
143
  tableContainer?: ContainerComponent | false;
144
+ /** 直接传入列配置(覆盖 :table 实例的 columns) */
120
145
  columns?: Columns<RecordType>;
146
+ /** 直接传入数据源(覆盖 :table 实例的 data) */
121
147
  data?: RecordType[];
148
+ /** 分页配置,传 false 禁用 */
122
149
  pagination?: false | Partial<PaginationProps>;
123
150
  } & /* @vue-ignore */ Omit<TableProps, 'data'>;
124
151
 
152
+ /*
153
+ * 组件自身仅触发模型事件;宿主表格事件不入运行时 emits,监听入口保留在 attrs 中透传至 ElTable
154
+ */
125
155
  type Emits = {} & /* @vue-ignore */ TableEmits<RecordType>;
126
156
 
127
157
  defineEmits<Emits>();
@@ -145,34 +175,23 @@ const config = INJECT_CONFIG['pro-table'];
145
175
  const injectPropsRaw = inject(config.injectionKey, config.default);
146
176
  const injectProps = computed(() => toValue(injectPropsRaw) || {});
147
177
 
148
- const attrs = useAttrs() as Record<string, any>;
178
+ const attrs = useReactiveAttrs() as Record<string, any>;
149
179
 
150
- const PRO_TABLE_EXCLUDED_ATTR_KEYS = new Set([
180
+ // 组件自身消费的 attrs 键:class / style 绑定根节点,onSearch / onChange 由搜索与分页流程消费
181
+ const PRO_TABLE_CONSUMED_ATTR_KEYS = new Set([
151
182
  'class',
152
183
  'style',
153
184
  'onSearch',
154
185
  'onChange',
155
- 'onUpdate:size',
156
- 'onUpdate:loading',
157
- 'data',
158
- 'columns',
159
- 'size',
160
- 'pagination',
161
186
  ]);
162
187
 
163
- const PRO_TABLE_PROVIDER_KEYS = new Set([
164
- 'pagination',
165
- 'searchFormConfig',
166
- 'control',
167
- 'immediateSearch',
168
- 'addIndexColumn',
169
- 'tableContainer',
170
- ]);
188
+ // 组件自身消费的 props 键:对应值按 prop / provider 单一来源派生,provider 注入不再透传这些键
189
+ const PRO_TABLE_PROP_KEYS = new Set(Object.keys(props));
171
190
 
172
191
  const injectTableAttrs = computed(() => {
173
192
  const tableConfig: Record<string, any> = {};
174
193
  for (const [key, value] of Object.entries(injectProps.value)) {
175
- if (!PRO_TABLE_PROVIDER_KEYS.has(key)) {
194
+ if (!PRO_TABLE_PROP_KEYS.has(key)) {
176
195
  tableConfig[key] = value;
177
196
  }
178
197
  }
@@ -180,20 +199,20 @@ const injectTableAttrs = computed(() => {
180
199
  });
181
200
 
182
201
  const attrsTableProps = computed(() => {
183
- return pick(
184
- camelizeProperties(attrs),
185
- tableProperties
202
+ return omitBy(
203
+ pick(camelizeProperties(attrs), tableProperties),
204
+ value => value === undefined
186
205
  ) as Partial<TableProps>;
187
206
  });
188
207
 
189
208
  const restTableAttrs = computed(() => {
190
209
  const rest: Record<string, any> = {};
191
210
  for (const [key, value] of Object.entries(attrs)) {
192
- if (!PRO_TABLE_EXCLUDED_ATTR_KEYS.has(key)) {
211
+ if (!PRO_TABLE_CONSUMED_ATTR_KEYS.has(key)) {
193
212
  rest[key] = value;
194
213
  }
195
214
  }
196
- return rest;
215
+ return omitBy(rest, value => value === undefined);
197
216
  });
198
217
 
199
218
  const size = defineModel<TableProps['size']>('size');
@@ -201,10 +220,31 @@ const loading = defineModel<boolean>('loading', { default: false });
201
220
 
202
221
  // 递增序号做请求竞态守卫,仅最新一次请求可变更 loading
203
222
  let searchSequence = 0;
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]}`);
235
+ };
204
236
  const _search = async () => {
237
+ const search = props.search || attrs.onSearch;
238
+ if (props.search && attrs.onSearch) {
239
+ warnSearchConfig('duplicate');
240
+ }
241
+ if (!search) return;
242
+ const searchPromise = search();
243
+ if (!isPromise(searchPromise)) {
244
+ warnSearchConfig('not-promise');
245
+ return;
246
+ }
205
247
  const seq = ++searchSequence;
206
- const searchPromise = (props.search || attrs.onSearch)?.();
207
- if (!isPromise(searchPromise)) return;
208
248
  loading.value = true;
209
249
  try {
210
250
  await searchPromise;
@@ -236,8 +276,57 @@ type CustomSlots = Readonly<
236
276
  type Slots = CustomSlots & Readonly<Record<string, Slot>>;
237
277
 
238
278
  const slots = useSlots() as Slots;
239
- const forwardedSlotNames = () =>
240
- 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
+ };
241
330
 
242
331
  const searchPage1st = () => {
243
332
  props.table?.setPageParam?.({ current: 1 });
@@ -256,6 +345,16 @@ const computedSearchFormConfig = computed(() => {
256
345
  };
257
346
  });
258
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
+
259
358
  const computedTableContainer = computed(() => {
260
359
  const container =
261
360
  props.tableContainer ??
@@ -264,19 +363,26 @@ const computedTableContainer = computed(() => {
264
363
  return container ? container : undefined;
265
364
  });
266
365
 
366
+ /*
367
+ * 序号列随有效分页偏移:分页关闭时退化为 index + 1;
368
+ * current / pageSize 取有效分页(显式 :pagination 或 Provider 注入),table.pageParam 仅作兜底
369
+ */
267
370
  const getIndex = (i: number) => {
268
- if (props.table?.pageParam && computedPagination.value !== false) {
269
- const current = props.table.pageParam.current ?? 1;
270
- const pageSize = props.table.pageParam.pageSize ?? 10;
271
- return (current - 1) * pageSize + i + 1;
272
- }
273
- 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;
274
377
  };
275
378
 
379
+ /** 调用方是否显式传入 columns(值为 undefined 时视为未指定) */
380
+ const hasExplicitColumns = computed(() => props.columns !== undefined);
381
+
276
382
  const normalizedRawColumns = computed(() => {
277
- const source = (props.columns ??
278
- toValue(props.table?.columns) ??
279
- []) as any[];
383
+ const source = (
384
+ (props.columns ?? toValue(props.table?.columns) ?? []) as any[]
385
+ ).filter(isObjectItem);
280
386
  if (props.addIndexColumn ?? injectProps.value.addIndexColumn) {
281
387
  return [
282
388
  {
@@ -293,41 +399,25 @@ const normalizedRawColumns = computed(() => {
293
399
  });
294
400
 
295
401
  const computedDataSource = computed<any[]>(() => {
296
- const attrsData = attrsTableProps.value.data as any[] | undefined;
297
- return (
298
- props.data ??
299
- attrsData ??
300
- toValue((props.table as any)?.dataSource) ??
301
- toValue(props.table?.data) ??
302
- []
303
- );
402
+ return props.data ?? toValue(props.table?.data) ?? [];
304
403
  });
305
404
 
306
405
  const computedTableProps = computed<TableProps>(() => {
307
- const merged = {
406
+ return {
308
407
  ...injectTableAttrs.value,
309
408
  ...restTableAttrs.value,
310
409
  ...attrsTableProps.value,
311
410
  } as TableProps;
312
- delete (merged as any).data;
313
- delete (merged as any).columns;
314
- delete (merged as any).size;
315
- delete (merged as any).pagination;
316
- return merged;
317
411
  });
318
412
 
319
- // 分页优先级:tableAttrs 显式配置 > provider 全局配置 > 默认合成
413
+ /*
414
+ * 分页来源:分页开关与展示配置取 props.pagination 显式配置 > provider 全局配置 > 默认合成;
415
+ * currentPage / pageSize / total 以 table.pageParam 为实时状态,显式配置仅在 pageParam 缺省时兜底
416
+ */
320
417
  const computedPagination = computed<false | Partial<PaginationProps>>(() => {
321
- const attrsPagination = (attrsTableProps.value as any).pagination;
322
- if (props.pagination === false || attrsPagination === false) {
323
- return false;
324
- }
325
- const hasUserPagination =
326
- isPlainObject(props.pagination) || isPlainObject(attrsPagination);
327
- if (hasUserPagination) {
328
- const pageConfig = (props.pagination ??
329
- attrsPagination ??
330
- {}) as Partial<PaginationProps>;
418
+ if (props.pagination === false) return false;
419
+ if (isPlainObject(props.pagination)) {
420
+ const pageConfig = props.pagination as Partial<PaginationProps>;
331
421
  const globalPagination =
332
422
  (injectProps.value.pagination as any) === false
333
423
  ? {}
@@ -378,11 +468,15 @@ const searchFormVisible = computed(() => {
378
468
  return !!(
379
469
  (computedSearchFormConfig.value as any)?.hidden !== true &&
380
470
  props.table?.searchForm &&
381
- fieldList.filter((item: any) => !toValue(item.hidden))?.length > 0
471
+ fieldList.filter((item: any) => isObjectItem(item) && !toValue(item.hidden))
472
+ .length > 0
382
473
  );
383
474
  });
384
475
 
385
- const computedControl = computed<{
476
+ /*
477
+ * 控制项来源:显式控制配置 > Provider 全局配置
478
+ */
479
+ const resolvedControl = computed<{
386
480
  sizeControl: boolean;
387
481
  columnControl: boolean | ColumnControlConfig<Path<RecordType>>;
388
482
  }>(() => {
@@ -396,17 +490,21 @@ const computedControl = computed<{
396
490
  return { sizeControl: !!sizeControl, columnControl };
397
491
  });
398
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
+
399
500
  const columnControlConfig = computed(() => {
400
501
  const cc = computedControl.value.columnControl;
401
502
  return typeof cc === 'object' ? cc : undefined;
402
503
  });
403
504
 
505
+ // 尺寸来源:size 模型 > provider 全局配置
404
506
  const resolvedSize = computed(() => {
405
- return (
406
- size.value ??
407
- (attrsTableProps.value.size as any) ??
408
- injectTableAttrs.value.size
409
- );
507
+ return size.value ?? injectProps.value.size;
410
508
  });
411
509
 
412
510
  onMounted(() => {
@@ -431,7 +529,7 @@ defineExpose({
431
529
  <SearchForm
432
530
  v-if="props.table?.searchForm"
433
531
  :form="props.table.searchForm"
434
- v-bind="computedSearchFormConfig"
532
+ v-bind="searchFormProps"
435
533
  @search="searchPage1st"
436
534
  @reset="reset"
437
535
  />
@@ -450,7 +548,7 @@ defineExpose({
450
548
  v-if="computedControl.sizeControl"
451
549
  class="pro-table_header_size-control"
452
550
  >
453
- <SizeControl v-model:size="size" />
551
+ <SizeControl v-model:size="size" :effective-size="resolvedSize" />
454
552
  </div>
455
553
  <div
456
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,32 +50,42 @@ 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';
66
- import ProForm from '../../form';
67
- import { type Form } from '../../form';
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';
87
+ import ProForm from '../../form/components/ProForm/index.vue';
88
+ import { type Form } from '../../form/hooks/useForm';
68
89
  import DownOutlined from './icons/DownOutlined.vue';
69
90
  import { useLocale } from '../../../locale';
70
91
  import { useSearchFormExpand } from '../../../shared/core';
@@ -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,9 +142,28 @@ const onReset = () => {
122
142
  emit('reset');
123
143
  };
124
144
 
125
- const onSearch = () => {
145
+ // 同一 tick 内 click 与 submit 只放行首个触达者,下一 tick 释放标记
146
+ let searchTriggeredInTick = false;
147
+
148
+ const emitSearch = () => {
149
+ searchTriggeredInTick = true;
150
+ nextTick(() => {
151
+ searchTriggeredInTick = false;
152
+ });
126
153
  emit('search');
127
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
+ };
128
167
  </script>
129
168
 
130
169
  <template>
@@ -132,6 +171,7 @@ const onSearch = () => {
132
171
  v-bind="layoutProps"
133
172
  :form="props.form"
134
173
  class="pro-table_search-form transition"
174
+ @submit.prevent="onSearchSubmit"
135
175
  >
136
176
  <Space alignment="start" class="pro-table_search-form_button-group">
137
177
  <slot name="reset-button" :on-reset="onReset">
@@ -150,19 +190,18 @@ const onSearch = () => {
150
190
  </Button>
151
191
  </template>
152
192
  </slot>
153
- <slot name="search-button" :on-search="onSearch">
193
+ <slot name="search-button" :on-search="onSearchClick">
154
194
  <template v-if="props.searchButton !== false">
155
195
  <component
156
196
  :is="props.searchButton"
157
197
  v-if="props.searchButton"
158
- @click="onSearch"
198
+ @click="onSearchClick"
159
199
  />
160
200
  <Button
161
201
  v-else
162
202
  class="pro-table_search-form_search-button"
163
203
  type="primary"
164
204
  native-type="submit"
165
- @click="onSearch"
166
205
  >
167
206
  {{ t('table.query') }}
168
207
  </Button>
@@ -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' },