@qin-ui/antd-vue-pro 2.4.5 → 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 +7 -3
  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 +9 -1
  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 +259 -123
  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 +5 -3
  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 +41 -13
  65. package/src/components/form/components/BaseField/utils/index.ts +19 -1
  66. package/src/components/form/components/BaseFormItem/index.vue +28 -30
  67. package/src/components/form/components/ContainerFragment/index.vue +3 -1
  68. package/src/components/form/components/ProForm/index.vue +19 -14
  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 +29 -13
  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 +17 -3
  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 +228 -69
  81. package/src/components/table/components/SearchForm.vue +56 -19
  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 +0 -1
@@ -1,4 +1,13 @@
1
1
  <script lang="ts" setup>
2
+ /**
3
+ * 列显隐控制控件,以下拉菜单列出可控列并支持全选/取消全选
4
+ *
5
+ * @component ColumnControl
6
+ *
7
+ * @param {Columns} columns - 参与控制的列配置(由 ProTable 下发)
8
+ * @param {Table} [table] - 表格实例,勾选结果经其 `setColumn` 写回列 `hidden`
9
+ * @param {ColumnControlConfig} [columnControl] - 控制项配置:labels / excludedKeys / disabledKeys / disableCheckAll
10
+ */
2
11
  import {
3
12
  Button,
4
13
  MenuDivider,
@@ -7,15 +16,19 @@ import {
7
16
  MenuItem,
8
17
  Checkbox,
9
18
  } from '../../../shared/ui';
10
- import { computed, ref, watch } from 'vue';
19
+ import { computed, ref, toValue, watch } from 'vue';
20
+ import { isObjectItem } from '../../../shared/core';
11
21
  import type { Columns, ColumnControlConfig } from '../types';
12
22
  import { type Table } from '../hooks/useTable';
13
23
  import FilterOutlined from './icons/FilterOutlined.vue';
14
24
  import { useLocale } from '../../../locale';
15
25
 
16
26
  type Props = {
27
+ /** 参与控制的列配置(由 ProTable 下发) */
17
28
  columns: Columns;
29
+ /** 表格实例,勾选结果经其 `setColumn` 写回列 `hidden` */
18
30
  table?: Table;
31
+ /** 控制项配置:labels / excludedKeys / disabledKeys / disableCheckAll */
19
32
  columnControl?: ColumnControlConfig;
20
33
  };
21
34
  const props = withDefaults(defineProps<Props>(), {
@@ -30,6 +43,7 @@ const open = ref(false);
30
43
  const checkAll = ref(false);
31
44
  const indeterminate = ref(false);
32
45
 
46
+ /** 下拉项:逐列解析出 key、显示名、勾选态与是否禁用 */
33
47
  const checkedColumnsOptions = computed<
34
48
  Array<{
35
49
  label: string;
@@ -38,7 +52,7 @@ const checkedColumnsOptions = computed<
38
52
  disabled: boolean;
39
53
  }>
40
54
  >(() =>
41
- props.columns.flatMap(item => {
55
+ props.columns.filter(isObjectItem).flatMap(item => {
42
56
  const rawKey =
43
57
  item.key ??
44
58
  (Array.isArray(item.dataIndex)
@@ -62,7 +76,7 @@ const checkedColumnsOptions = computed<
62
76
  return {
63
77
  label,
64
78
  value: key,
65
- checked: !item.hidden || disabled,
79
+ checked: !toValue(item.hidden) || disabled,
66
80
  disabled,
67
81
  };
68
82
  })
@@ -1,4 +1,9 @@
1
1
  <script lang="ts" setup>
2
+ /**
3
+ * 搜索区域默认包裹容器,提供统一的背景色与间距
4
+ *
5
+ * @component DefaultSearchFormContainer
6
+ */
2
7
  import { theme } from '../../../shared/ui';
3
8
  const { token } = theme.useToken();
4
9
  </script>
@@ -1,4 +1,9 @@
1
1
  <script lang="ts" setup>
2
+ /**
3
+ * 表格区域默认包裹容器,提供统一的背景色与内边距
4
+ *
5
+ * @component DefaultTableContainer
6
+ */
2
7
  import { theme } from '../../../shared/ui';
3
8
  const { token } = theme.useToken();
4
9
  </script>
@@ -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,24 +26,38 @@
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,
@@ -52,23 +67,26 @@ import { INJECT_CONFIG } from '../../component-provider/constants';
52
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';
84
+ import type { ComponentSlots } from 'vue-component-type-helpers';
67
85
  import DefaultSearchFormContainer from './DefaultSearchFormContainer.vue';
68
86
  import DefaultTableContainer from './DefaultTableContainer.vue';
69
87
  import SizeControl from './SizeControl.vue';
70
88
  import ColumnControl from './ColumnControl.vue';
71
- import { Column, Columns, ColumnControlConfig } from '../types';
89
+ import type { Column, Columns, ColumnControlConfig } from '../types';
72
90
 
73
91
  defineOptions({ name: 'ProTable', inheritAttrs: false });
74
92
 
@@ -87,19 +105,19 @@ type SearchFormConfig = Omit<SearchFormProps, 'form'> & {
87
105
  };
88
106
 
89
107
  type Props = {
90
- // 列表表格对象,returns from useTable
108
+ /** 列表表格对象,来自 `useTable` */
91
109
  table?: T;
92
- // 列表数据查询方法
110
+ /** 列表数据查询方法 */
93
111
  search?: () => Promise<unknown>;
94
- // 是否在首列插入index列
112
+ /** 是否在首列插入序号列 */
95
113
  addIndexColumn?: boolean;
96
- // onMounted 时立即触发一次search事件
114
+ /** 挂载后是否立即触发一次查询 */
97
115
  immediateSearch?: boolean;
98
- // 是否展示表格 size 和 column 控制按钮
116
+ /** 是否展示尺寸与列控制按钮;显式传入 columns 时列控入口不渲染 */
99
117
  control?: boolean | Partial<Control>;
100
- // 搜索栏查询字段表单配置
118
+ /** 搜索栏查询字段表单配置 */
101
119
  searchFormConfig?: SearchFormConfig;
102
- // 表格区域包裹容器
120
+ /** 表格区域包裹容器 */
103
121
  tableContainer?: ContainerComponent | false;
104
122
  } & /* @vue-ignore */ TableProps;
105
123
 
@@ -119,6 +137,7 @@ const config = INJECT_CONFIG['pro-table'];
119
137
  const injectPropsRaw = inject(config.injectionKey, config.default);
120
138
  const injectProps = computed(() => toValue(injectPropsRaw) || {});
121
139
 
140
+ /* Provider 注入的不可注入键(columns / dataSource / loading)已在注入层剔除,此处直接取用其注入属性 */
122
141
  const injectAttrs = computed<TableProps>(
123
142
  () => pick(injectProps.value, tableProperties) as TableProps
124
143
  );
@@ -135,33 +154,45 @@ const computedSearchFormConfig = computed(() => {
135
154
  };
136
155
  });
137
156
 
138
- const attrs: Record<string, any> = useAttrs();
139
- const tableAttrs = computed<TableProps>(() => {
140
- 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;
141
165
  });
142
166
 
167
+ const attrs: Record<string, any> = useReactiveAttrs();
168
+
143
169
  const size = defineModel<TableProps['size']>('size');
144
170
  const loading = defineModel<boolean>('loading', { default: false });
145
171
 
146
172
  // 递增序号做请求竞态守卫,仅最新一次请求可变更 loading
147
173
  let searchSequence = 0;
148
- let warnedSearchConfig = false;
149
- const warnSearchConfig = (reason: string) => {
150
- if (warnedSearchConfig) return;
151
- warnedSearchConfig = true;
152
- console.warn(`[ProTable] ${reason}`);
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]}`);
153
186
  };
154
187
  const _search = async () => {
188
+ const search = props.search || attrs.onSearch;
155
189
  if (props.search && attrs.onSearch) {
156
- warnSearchConfig(
157
- '同时配置了 :search prop 与 @search 监听,@search 监听不会被执行,请仅保留其一。'
158
- );
190
+ warnSearchConfig('duplicate');
159
191
  }
160
- const searchPromise = (props.search || attrs.onSearch)?.();
192
+ if (!search) return;
193
+ const searchPromise = search();
161
194
  if (!isPromise(searchPromise)) {
162
- warnSearchConfig(
163
- 'search 未返回 Promise,本次查询不参与 loading 竞态管理(在途请求的 loading 状态保持不变)。'
164
- );
195
+ warnSearchConfig('not-promise');
165
196
  return;
166
197
  }
167
198
  const seq = ++searchSequence;
@@ -218,10 +249,15 @@ const columnRequiredSlotNames = new Set([
218
249
  ]);
219
250
 
220
251
  /**
221
- * 判定插槽是否具备渲染条件(守卫过渡状态或空列下 column 为 undefined 的情况)
222
- * 当列被动态隐藏或所有列均隐藏时,旧单元格销毁过渡帧或空占位中 column 可能为 undefined,
223
- * 此时阻断依赖 column 的业务插槽调用;
224
- * 非列依赖的表格级插槽(如 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 该插槽是否参与渲染
225
261
  */
226
262
  const isSlotRenderable = (name: string, scoped: any) => {
227
263
  if (columnRequiredSlotNames.has(name)) {
@@ -235,13 +271,65 @@ const searchPage1st = () => {
235
271
  _search();
236
272
  };
237
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
+ */
238
320
  const computedTableProps = computed<TableProps>(() => {
239
- return {
321
+ const merged = {
240
322
  ...injectAttrs.value,
241
- ...tableAttrs.value,
242
- pagination: undefined,
243
- onChange: undefined,
244
- };
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;
245
333
  });
246
334
 
247
335
  const computedTableContainer = computed(() => {
@@ -255,15 +343,17 @@ const computedTableContainer = computed(() => {
255
343
  type ReturnGenericParameterTypes<V> = V extends Table<any, infer R> ? R : never;
256
344
  type RecordType = ReturnGenericParameterTypes<T>;
257
345
 
258
- // 序号列随分页偏移:第 n 页首行序号 = (n-1)*pageSize + i + 1(无分页时退化为 index+1)
346
+ /*
347
+ * 序号列随有效分页偏移:分页关闭时退化为 index + 1;
348
+ * current / pageSize 取有效分页(显式 :pagination 或 Provider 注入),table.pageParam 仅作兜底
349
+ */
259
350
  const getIndex = (i: number) => {
351
+ const pagination = computedPagination.value;
352
+ if (pagination === false) return i + 1;
260
353
  const pageParam = props.table?.pageParam;
261
- if (pageParam && computedPagination.value !== false) {
262
- const current = pageParam.current ?? 1;
263
- const pageSize = pageParam.pageSize ?? 10;
264
- return (current - 1) * pageSize + i + 1;
265
- }
266
- 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;
267
357
  };
268
358
 
269
359
  const indexColumn = computed<ColumnType>(() => ({
@@ -274,26 +364,72 @@ const indexColumn = computed<ColumnType>(() => ({
274
364
  customRender: ({ index }) => getIndex(index),
275
365
  }));
276
366
 
367
+ // 列来源优先级:显式绑定 > table 实例(useTable 创建的 columns)
277
368
  const computedColumns = computed<Columns<RecordType>>(() => {
278
- const cols = (tableAttrs.value.columns ??
369
+ const cols = (resolveOwnedProp('columns') ??
279
370
  toValue(props.table?.columns) ??
280
371
  []) as any;
281
372
  const needIndex = props.addIndexColumn ?? injectProps.value.addIndexColumn;
282
373
  return needIndex ? [indexColumn.value, ...cols] : cols;
283
374
  });
284
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
+
285
411
  const visibleComputedColumns = computed(() => {
286
- return computedColumns.value.filter(item => !(item as Column).hidden);
412
+ return filterVisibleColumns(computedColumns.value).list;
287
413
  });
288
414
 
415
+ // 数据源优先级:显式绑定 > table 实例(useTable 创建的 dataSource)
289
416
  const computedDataSource = computed<RecordType[]>(() => {
290
- return tableAttrs.value.dataSource ?? toValue(props.table?.dataSource) ?? [];
417
+ return (
418
+ resolveOwnedProp('dataSource') ?? toValue(props.table?.dataSource) ?? []
419
+ );
291
420
  });
292
421
 
293
- // 分页优先级:tableAttrs 显式配置 > provider 全局配置 > 默认合成
422
+ /*
423
+ * 分页优先级:显式绑定 > provider 全局配置 > 默认合成;
424
+ * provider 作为基础配置与 table.pageParam 合并,显式绑定的对象形态在其之上覆盖
425
+ */
294
426
  const computedPagination = computed<PaginationProps | false>(() => {
295
- if (tableAttrs.value.pagination === false) return false;
296
- 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)) {
297
433
  const globalPagination =
298
434
  injectAttrs.value.pagination === false
299
435
  ? {}
@@ -301,9 +437,9 @@ const computedPagination = computed<PaginationProps | false>(() => {
301
437
  return {
302
438
  ...globalPagination,
303
439
  ...props.table?.pageParam,
304
- ...tableAttrs.value.pagination,
440
+ ...attrsPagination,
305
441
  showTotal:
306
- tableAttrs.value.pagination?.showTotal ??
442
+ attrsPagination?.showTotal ??
307
443
  globalPagination?.showTotal ??
308
444
  ((total: number) => t('pagination.total', { total })),
309
445
  };
@@ -332,11 +468,15 @@ const searchFormVisible = computed(() => {
332
468
  return !!(
333
469
  computedSearchFormConfig.value?.hidden !== true &&
334
470
  props.table?.searchForm &&
335
- fieldList.filter(item => !toValue(item.hidden)).length > 0
471
+ fieldList.filter(item => isObjectItem(item) && !toValue(item.hidden))
472
+ .length > 0
336
473
  );
337
474
  });
338
475
 
339
- const computedControl = computed<{
476
+ /*
477
+ * 控制项来源:显式控制配置 > Provider 全局配置
478
+ */
479
+ const resolvedControl = computed<{
340
480
  sizeControl: boolean;
341
481
  columnControl: boolean | ColumnControlConfig<Path<RecordType>>;
342
482
  }>(() => {
@@ -353,6 +493,13 @@ const computedControl = computed<{
353
493
  };
354
494
  });
355
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
+
356
503
  const columnControlConfig = computed(() => {
357
504
  const cc = computedControl.value.columnControl;
358
505
  return typeof cc === 'object' ? cc : undefined;
@@ -376,11 +523,14 @@ defineExpose({
376
523
  v-if="searchFormVisible || $slots['search-form']"
377
524
  :component="computedSearchFormConfig.container"
378
525
  >
379
- <slot name="search-form">
526
+ <slot
527
+ name="search-form"
528
+ v-bind="{ form: props.table?.searchForm, search: searchPage1st, reset }"
529
+ >
380
530
  <SearchForm
381
531
  v-if="props.table?.searchForm"
382
532
  :form="props.table.searchForm"
383
- v-bind="computedSearchFormConfig"
533
+ v-bind="searchFormProps"
384
534
  @search="searchPage1st"
385
535
  @reset="reset"
386
536
  />
@@ -399,7 +549,7 @@ defineExpose({
399
549
  v-if="computedControl.sizeControl"
400
550
  class="pro-table_header_size-control"
401
551
  >
402
- <SizeControl v-model:size="size" />
552
+ <SizeControl v-model:size="size" :effective-size="effectiveSize" />
403
553
  </div>
404
554
  <div
405
555
  v-if="computedControl.columnControl"
@@ -413,11 +563,20 @@ defineExpose({
413
563
  </div>
414
564
  </div>
415
565
 
416
- <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
+ >
417
577
  <ATable
418
578
  class="pro-table_content"
419
579
  v-bind="computedTableProps"
420
- :size="size"
421
580
  :loading="loading"
422
581
  :columns="visibleComputedColumns"
423
582
  :data-source="computedDataSource"