@qin-ui/antd-vue-pro 2.5.2 → 2.6.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.
- package/AI-CONTEXT.md +30 -10
- package/api.json +24 -4
- package/es/antd-vue-pro.css +6 -6
- package/es/components/component-provider/types/index.d.ts +1 -1
- package/es/components/component-provider/types/index.d.ts.map +1 -1
- package/es/components/form/components/BaseField/index.vue.d.ts.map +1 -1
- package/es/components/form/components/BaseFormItem/index.vue.d.ts +59 -11
- package/es/components/form/components/BaseFormItem/index.vue.d.ts.map +1 -1
- package/es/components/form/components/BaseFormItems/index.vue.d.ts +16 -0
- package/es/components/form/components/BaseFormItems/index.vue.d.ts.map +1 -0
- package/es/components/form/components/index.d.ts +6 -6
- package/es/components/form/components/index.d.ts.map +1 -1
- package/es/components/form/hooks/useGroupFieldAttrs.d.ts +13 -3
- package/es/components/form/hooks/useGroupFieldAttrs.d.ts.map +1 -1
- package/es/components/form/utils/fieldKey.d.ts +12 -0
- package/es/components/form/utils/fieldKey.d.ts.map +1 -1
- package/es/components/table/components/ProTable.vue.d.ts +3 -1
- package/es/components/table/components/ProTable.vue.d.ts.map +1 -1
- package/es/components/table/components/TableEllipsisTooltip.vue.d.ts +19 -0
- package/es/components/table/components/TableEllipsisTooltip.vue.d.ts.map +1 -0
- package/es/components/table/hooks/useEllipsisTooltip.d.ts +45 -0
- package/es/components/table/hooks/useEllipsisTooltip.d.ts.map +1 -0
- package/es/components/table/hooks/useEllipsisTooltipColumns.d.ts +19 -0
- package/es/components/table/hooks/useEllipsisTooltipColumns.d.ts.map +1 -0
- package/es/components/table/index.d.ts +1 -1
- package/es/components/table/index.d.ts.map +1 -1
- package/es/components/table/types/index.d.ts +46 -2
- package/es/components/table/types/index.d.ts.map +1 -1
- package/es/index.js +718 -228
- package/es/index.js.map +1 -1
- package/es/shared/ui/index.d.ts +2 -1
- package/es/shared/ui/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/component-provider/types/index.ts +1 -0
- package/src/components/form/components/BaseField/index.vue +30 -9
- package/src/components/form/components/BaseFormItem/index.vue +153 -186
- package/src/components/form/components/BaseFormItems/index.vue +186 -0
- package/src/components/form/components/ProForm/index.vue +5 -5
- package/src/components/form/components/index.ts +6 -6
- package/src/components/form/hooks/useGroupFieldAttrs.ts +114 -88
- package/src/components/form/utils/fieldKey.ts +63 -0
- package/src/components/table/components/ProTable.vue +128 -56
- package/src/components/table/components/TableEllipsisTooltip.vue +90 -0
- package/src/components/table/hooks/useEllipsisTooltip.ts +244 -0
- package/src/components/table/hooks/useEllipsisTooltipColumns.ts +139 -0
- package/src/components/table/index.ts +7 -1
- package/src/components/table/types/index.ts +62 -2
- package/src/shared/ui/index.ts +2 -0
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 架构设计说明 (Architecture Overview)
|
|
8
8
|
*
|
|
9
|
-
* [ 数据层 (Core) ] => [ 绑定层 (ProForm) ] => [ 渲染层 (BaseFormItem / BaseField) ]
|
|
9
|
+
* [ 数据层 (Core) ] => [ 绑定层 (ProForm) ] => [ 渲染层 (BaseFormItems / BaseFormItem / BaseField) ]
|
|
10
10
|
*
|
|
11
11
|
* 1. 数据隔离:ProForm 本身不负责存储和管理表单状态,所有核心状态(formData, fields, 校验规则)
|
|
12
12
|
* 均交由底层的 `useForm`(@qin-ui/pro-components-core)驱动。
|
|
13
13
|
* 2. 上下文透传:通过 `provide` 将核心状态下发给子组件。
|
|
14
|
-
* 3. 递归渲染:网格布局和具体字段的解析交由 `<
|
|
14
|
+
* 3. 递归渲染:网格布局和具体字段的解析交由 `<BaseFormItems>` 递归完成。
|
|
15
15
|
*
|
|
16
16
|
* @public
|
|
17
17
|
*
|
|
@@ -57,7 +57,7 @@ import {
|
|
|
57
57
|
type Slot,
|
|
58
58
|
} from 'vue';
|
|
59
59
|
import { INJECT_CONFIG } from '../../../component-provider/constants';
|
|
60
|
-
import
|
|
60
|
+
import BaseFormItems from '../BaseFormItems/index.vue';
|
|
61
61
|
import { InjectionFormKey, InjectionFormSlotsKey } from '../../constants';
|
|
62
62
|
import type { Grid, VModelProps, PathProps } from '../../types';
|
|
63
63
|
import type { Path } from '../../../../shared/core';
|
|
@@ -82,7 +82,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
82
82
|
/**
|
|
83
83
|
* 依赖注入中心:
|
|
84
84
|
* 未传 form 实例(含运行期置空)时回退内部表单实例;
|
|
85
|
-
* 将 Form 实例注入到 Vue Context 中,供深层子组件(如
|
|
85
|
+
* 将 Form 实例注入到 Vue Context 中,供深层子组件(如 BaseFormItems, BaseField, 自定义插槽)跨层级直接访问数据和方法。
|
|
86
86
|
*/
|
|
87
87
|
// 兜底表单实例:setup 同步期创建,未传 :form(含运行期置空)时经 currentForm 生效
|
|
88
88
|
const fallbackForm = useForm() as unknown as F;
|
|
@@ -165,7 +165,7 @@ provide(InjectionFormSlotsKey, formSlotsRef);
|
|
|
165
165
|
v-bind="formAttrs"
|
|
166
166
|
class="pro-form"
|
|
167
167
|
>
|
|
168
|
-
<
|
|
168
|
+
<BaseFormItems :fields="fields" :grid="props.grid" />
|
|
169
169
|
<slot />
|
|
170
170
|
</AForm>
|
|
171
171
|
</template>
|
|
@@ -3,12 +3,12 @@ import type {
|
|
|
3
3
|
ComponentExposed,
|
|
4
4
|
} from 'vue-component-type-helpers';
|
|
5
5
|
import ProForm from './ProForm/index.vue';
|
|
6
|
-
import
|
|
6
|
+
import BaseFormItems from './BaseFormItems/index.vue';
|
|
7
7
|
|
|
8
8
|
export { default as BaseField } from './BaseField/index.vue';
|
|
9
9
|
export { default as ContainerFragment } from './ContainerFragment/index.vue';
|
|
10
10
|
export { default as SlotComponent } from './SlotComponent/index.vue';
|
|
11
|
-
export { ProForm,
|
|
11
|
+
export { ProForm, BaseFormItems };
|
|
12
12
|
|
|
13
13
|
// instance
|
|
14
14
|
/** ProForm 的组件实例类型,用于 `ref` 标注与 `defineExpose` 访问 */
|
|
@@ -16,7 +16,7 @@ export type ProFormInstance = ComponentExposed<typeof ProForm>;
|
|
|
16
16
|
/** ProForm 的组件属性类型 */
|
|
17
17
|
export type ProFormProps = ComponentProps<typeof ProForm>;
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
export type
|
|
21
|
-
/**
|
|
22
|
-
export type
|
|
19
|
+
/** BaseFormItems 的组件实例类型 */
|
|
20
|
+
export type ProFormItemsInstance = ComponentExposed<typeof BaseFormItems>;
|
|
21
|
+
/** BaseFormItems 的组件属性类型 */
|
|
22
|
+
export type ProFormItemsProps = ComponentProps<typeof BaseFormItems>;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
inject,
|
|
4
|
+
mergeProps,
|
|
5
|
+
toValue,
|
|
6
|
+
type ComputedRef,
|
|
7
|
+
type MaybeRefOrGetter,
|
|
8
|
+
} from 'vue';
|
|
3
9
|
import {
|
|
4
10
|
formItemProps,
|
|
5
11
|
gridItemProps,
|
|
@@ -35,16 +41,102 @@ export interface GroupedField {
|
|
|
35
41
|
formItemContainer?: Base['formItemContainer'];
|
|
36
42
|
/** 字段原始对象,供实例登记与访问器挂载使用 */
|
|
37
43
|
rawField: Field;
|
|
38
|
-
/** 渲染 key,取自字段对象身份 */
|
|
39
|
-
renderKey: string | number;
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
/** 将单个字段配置拆分为 grid / formItem / component / slots 分组 */
|
|
47
|
+
const groupField = (
|
|
48
|
+
field: Field,
|
|
49
|
+
rawInject: Record<string, any>
|
|
50
|
+
): GroupedField => {
|
|
51
|
+
let gridItemProps: Record<string, any> = {};
|
|
52
|
+
let formItemProps: Record<string, any> = {};
|
|
53
|
+
const componentProps: Record<string, any> = { slots: {} };
|
|
54
|
+
const formItemSlots: Record<string, any> = {};
|
|
55
|
+
|
|
56
|
+
if (isPlainObject(field)) {
|
|
57
|
+
// prettier-ignore
|
|
58
|
+
const { path: _path, name: _name, fields, formItemClass, formItemStyle, hidden, span, getFormItemRef, getComponentRef, getFormItemComputedProps, getComponentComputedProps, slots = {}, formItemDataAttrs = {}, componentDataAttrs = {}, extraProps, formItemContainer, ...rest } =
|
|
59
|
+
field as any;
|
|
60
|
+
|
|
61
|
+
// prettier-ignore
|
|
62
|
+
const { class: injectClassName, style: injectStyle, span: injectSpan, xs, sm, md, lg, xl, xxl, formItemContainer: injectFormItemContainer, ...injectRest } = rawInject;
|
|
63
|
+
|
|
64
|
+
/*
|
|
65
|
+
* 字段显式指定 span 时整体接管栅格,不再叠加注入的响应式断点;
|
|
66
|
+
* 未指定 span 时使用注入的断点配置
|
|
67
|
+
*/
|
|
68
|
+
const initGridItemProps =
|
|
69
|
+
span || span === 0
|
|
70
|
+
? { span }
|
|
71
|
+
: { xs, sm, md, lg, xl, xxl, span: injectSpan };
|
|
72
|
+
|
|
73
|
+
// 分组字段未指定 span 时整行渲染(span 兜底 24),注入的响应式断点保持生效
|
|
74
|
+
gridItemProps = fields
|
|
75
|
+
? { ...initGridItemProps, span: span ?? 24 }
|
|
76
|
+
: initGridItemProps;
|
|
77
|
+
// 字段级容器优先,回退注入级配置
|
|
78
|
+
const container = formItemContainer ?? injectFormItemContainer;
|
|
79
|
+
formItemProps = mergeProps(
|
|
80
|
+
injectRest,
|
|
81
|
+
{ class: injectClassName, style: injectStyle },
|
|
82
|
+
{ class: toValue(formItemClass), style: toValue(formItemStyle) },
|
|
83
|
+
formItemDataAttrs
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
Object.keys(rest).forEach(k => {
|
|
87
|
+
if (gridItemPropKeySet.has(k)) {
|
|
88
|
+
gridItemProps[k] = rest[k];
|
|
89
|
+
} else if (formItemPropKeySet.has(k)) {
|
|
90
|
+
formItemProps[k] = rest[k];
|
|
91
|
+
} else {
|
|
92
|
+
componentProps[k] = rest[k];
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
Object.assign(componentProps, componentDataAttrs);
|
|
97
|
+
Object.keys(slots).forEach(k => {
|
|
98
|
+
if (formItemSlotKeySet.has(k)) {
|
|
99
|
+
formItemSlots[k] = slots[k];
|
|
100
|
+
} else {
|
|
101
|
+
componentProps.slots[k] = slots[k];
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// label 插槽与字段 label 同时存在时由插槽渲染,剥离 label 属性
|
|
106
|
+
if (formItemSlots.label && formItemProps.label !== undefined) {
|
|
107
|
+
delete formItemProps.label;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
path: resolveFieldIdentity(field),
|
|
112
|
+
gridItemProps: gridItemProps as GridItemProps,
|
|
113
|
+
formItemProps: formItemProps as any,
|
|
114
|
+
formItemSlots,
|
|
115
|
+
componentProps,
|
|
116
|
+
formItemContainer: container,
|
|
117
|
+
rawField: field,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
path: resolveFieldIdentity(field),
|
|
123
|
+
gridItemProps: gridItemProps as GridItemProps,
|
|
124
|
+
formItemProps: formItemProps as any,
|
|
125
|
+
formItemSlots,
|
|
126
|
+
componentProps,
|
|
127
|
+
rawField: field,
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
42
131
|
/**
|
|
43
132
|
* 将 fields 配置数组逐个拆分为 grid / formItem / component / slots 分组。
|
|
44
133
|
*
|
|
45
134
|
* @description
|
|
46
135
|
* 1. 响应式集成 ProComponentProvider 的 `pro-form-item` 全局变量配置;
|
|
47
136
|
* 2. 剥离 `extraProps`(用户自定义字段标识袋):保留在字段原始对象上供业务层存取,渲染层经解构剥离,不落入 componentProps 透传。
|
|
137
|
+
*
|
|
138
|
+
* @param {ComputedRef<Field[]>} validFields - 已过滤隐藏字段的字段配置数组
|
|
139
|
+
* @returns 字段渲染分组数组
|
|
48
140
|
*/
|
|
49
141
|
export function useGroupFieldAttrs(validFields: ComputedRef<Field[]>) {
|
|
50
142
|
const config = INJECT_CONFIG['pro-form-item'];
|
|
@@ -52,89 +144,23 @@ export function useGroupFieldAttrs(validFields: ComputedRef<Field[]>) {
|
|
|
52
144
|
|
|
53
145
|
return computed<GroupedField[]>(() => {
|
|
54
146
|
const rawInject = toValue(injectFormItemProps) || {};
|
|
55
|
-
return validFields.value.map(field =>
|
|
56
|
-
let gridItemProps: Record<string, any> = {};
|
|
57
|
-
let formItemProps: Record<string, any> = {};
|
|
58
|
-
const componentProps: Record<string, any> = { slots: {} };
|
|
59
|
-
const formItemSlots: Record<string, any> = {};
|
|
60
|
-
|
|
61
|
-
if (isPlainObject(field)) {
|
|
62
|
-
// prettier-ignore
|
|
63
|
-
const { path: _path, name: _name, fields, formItemClass, formItemStyle, hidden, span, getFormItemRef, getComponentRef, getFormItemComputedProps, getComponentComputedProps, slots = {}, formItemDataAttrs = {}, componentDataAttrs = {}, extraProps, formItemContainer, ...rest } =
|
|
64
|
-
field as any;
|
|
65
|
-
|
|
66
|
-
// prettier-ignore
|
|
67
|
-
const { class: injectClassName, style: injectStyle, span: injectSpan, xs, sm, md, lg, xl, xxl, formItemContainer: injectFormItemContainer, ...injectRest } = rawInject;
|
|
68
|
-
|
|
69
|
-
/*
|
|
70
|
-
* 字段显式指定 span 时整体接管栅格,不再叠加注入的响应式断点;
|
|
71
|
-
* 未指定 span 时使用注入的断点配置
|
|
72
|
-
*/
|
|
73
|
-
const initGridItemProps =
|
|
74
|
-
span || span === 0
|
|
75
|
-
? { span }
|
|
76
|
-
: { xs, sm, md, lg, xl, xxl, span: injectSpan };
|
|
77
|
-
|
|
78
|
-
// 分组字段未指定 span 时整行渲染(span 兜底 24),注入的响应式断点保持生效
|
|
79
|
-
gridItemProps = fields
|
|
80
|
-
? { ...initGridItemProps, span: span ?? 24 }
|
|
81
|
-
: initGridItemProps;
|
|
82
|
-
// 字段级容器优先,回退注入级配置
|
|
83
|
-
const container = formItemContainer ?? injectFormItemContainer;
|
|
84
|
-
formItemProps = mergeProps(
|
|
85
|
-
injectRest,
|
|
86
|
-
{ class: injectClassName, style: injectStyle },
|
|
87
|
-
{ class: toValue(formItemClass), style: toValue(formItemStyle) },
|
|
88
|
-
formItemDataAttrs
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
Object.keys(rest).forEach(k => {
|
|
92
|
-
if (gridItemPropKeySet.has(k)) {
|
|
93
|
-
gridItemProps[k] = rest[k];
|
|
94
|
-
} else if (formItemPropKeySet.has(k)) {
|
|
95
|
-
formItemProps[k] = rest[k];
|
|
96
|
-
} else {
|
|
97
|
-
componentProps[k] = rest[k];
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
Object.assign(componentProps, componentDataAttrs);
|
|
102
|
-
Object.keys(slots).forEach(k => {
|
|
103
|
-
if (formItemSlotKeySet.has(k)) {
|
|
104
|
-
formItemSlots[k] = slots[k];
|
|
105
|
-
} else {
|
|
106
|
-
componentProps.slots[k] = slots[k];
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// label 插槽与字段 label 同时存在时由插槽渲染,剥离 label 属性
|
|
111
|
-
if (formItemSlots.label && formItemProps.label !== undefined) {
|
|
112
|
-
delete formItemProps.label;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const raw = resolveFieldIdentity(field);
|
|
116
|
-
return {
|
|
117
|
-
path: raw,
|
|
118
|
-
gridItemProps: gridItemProps as GridItemProps,
|
|
119
|
-
formItemProps: formItemProps as any,
|
|
120
|
-
formItemSlots,
|
|
121
|
-
componentProps,
|
|
122
|
-
formItemContainer: container,
|
|
123
|
-
rawField: field,
|
|
124
|
-
renderKey: getFieldKey(field),
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const raw = resolveFieldIdentity(field);
|
|
129
|
-
return {
|
|
130
|
-
path: raw,
|
|
131
|
-
gridItemProps: gridItemProps as GridItemProps,
|
|
132
|
-
formItemProps: formItemProps as any,
|
|
133
|
-
formItemSlots,
|
|
134
|
-
componentProps,
|
|
135
|
-
rawField: field,
|
|
136
|
-
renderKey: getFieldKey(field),
|
|
137
|
-
};
|
|
138
|
-
});
|
|
147
|
+
return validFields.value.map(field => groupField(field, rawInject));
|
|
139
148
|
});
|
|
140
149
|
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 将单个字段配置拆分为 grid / formItem / component / slots 分组
|
|
153
|
+
*
|
|
154
|
+
* @description 拆分规则与 `useGroupFieldAttrs` 一致,结果依赖该字段自身配置与 Provider 的 `pro-form-item` 默认值。
|
|
155
|
+
*
|
|
156
|
+
* @param {MaybeRefOrGetter<Field>} field - 待拆分的字段配置
|
|
157
|
+
* @returns {ComputedRef<GroupedField>} 单字段渲染分组
|
|
158
|
+
*/
|
|
159
|
+
export function useGroupedField(field: MaybeRefOrGetter<Field>) {
|
|
160
|
+
const config = INJECT_CONFIG['pro-form-item'];
|
|
161
|
+
const injectFormItemProps = inject(config.injectionKey, config.default);
|
|
162
|
+
|
|
163
|
+
return computed<GroupedField>(() =>
|
|
164
|
+
groupField(toValue(field), toValue(injectFormItemProps) || {})
|
|
165
|
+
);
|
|
166
|
+
}
|
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
import { toRaw } from 'vue';
|
|
2
|
+
import { resolveFieldIdentity, toPath } from '../../../shared/core';
|
|
2
3
|
|
|
3
4
|
const fieldKeyMap = new WeakMap<object, number>();
|
|
4
5
|
let fieldKeySeed = 0;
|
|
5
6
|
|
|
7
|
+
/** 数据行对象身份编号 */
|
|
8
|
+
const rowKeyMap = new WeakMap<object, number>();
|
|
9
|
+
let rowKeySeed = 0;
|
|
10
|
+
|
|
11
|
+
const getRowKey = (row: object) => {
|
|
12
|
+
const raw = toRaw(row);
|
|
13
|
+
let key = rowKeyMap.get(raw);
|
|
14
|
+
if (key === undefined) {
|
|
15
|
+
key = ++rowKeySeed;
|
|
16
|
+
rowKeyMap.set(raw, key);
|
|
17
|
+
}
|
|
18
|
+
return key;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** 数组下标段判定:仅十进制非负整数段按数组下标处理 */
|
|
22
|
+
const isIndexSegment = (segment: string) => /^(?:0|[1-9]\d*)$/.test(segment);
|
|
23
|
+
|
|
6
24
|
/**
|
|
7
25
|
* 字段渲染 key
|
|
8
26
|
*
|
|
@@ -25,3 +43,48 @@ export const getFieldKey = (field: unknown): string | number => {
|
|
|
25
43
|
}
|
|
26
44
|
return key;
|
|
27
45
|
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 字段的数据行渲染 key
|
|
49
|
+
*
|
|
50
|
+
* @description 沿字段路径逐段解析原始表单数据,取路径经过的对象数组行元素身份与行内剩余路径组成 key;
|
|
51
|
+
* 同一数据行对象在数组增删 / 重排后恒得同一 key。路径不经过对象数组行、行元素为原始值或数据缺失时,
|
|
52
|
+
* 回退到 {@link getFieldKey} 的字段对象身份。同 key 的去重由调用方按兄弟列表收敛。
|
|
53
|
+
*
|
|
54
|
+
* @param field - 字段配置项
|
|
55
|
+
* @param rawFormData - 原始表单数据(非响应式对象)
|
|
56
|
+
* @returns 数据行 key;无法定位数据行时返回字段对象身份 key
|
|
57
|
+
*/
|
|
58
|
+
export const getFieldDataKey = (
|
|
59
|
+
field: unknown,
|
|
60
|
+
rawFormData: unknown
|
|
61
|
+
): string | number => {
|
|
62
|
+
const path = resolveFieldIdentity(field);
|
|
63
|
+
if (!path || rawFormData === null || rawFormData === undefined) {
|
|
64
|
+
return getFieldKey(field);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const segments = toPath(path);
|
|
68
|
+
let node: any = rawFormData;
|
|
69
|
+
const rowIds: number[] = [];
|
|
70
|
+
let lastRowIndex = -1;
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < segments.length; i++) {
|
|
73
|
+
if (Array.isArray(node)) {
|
|
74
|
+
const segment = segments[i];
|
|
75
|
+
if (!isIndexSegment(segment)) break;
|
|
76
|
+
const element = node[Number(segment)];
|
|
77
|
+
if (element === null || typeof element !== 'object') break;
|
|
78
|
+
rowIds.push(getRowKey(toRaw(element) as object));
|
|
79
|
+
lastRowIndex = i;
|
|
80
|
+
node = element;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (node === null || typeof node !== 'object') break;
|
|
84
|
+
node = node[segments[i]];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (lastRowIndex === -1) return getFieldKey(field);
|
|
88
|
+
const rest = segments.slice(lastRowIndex + 1).join('.');
|
|
89
|
+
return `data:${rowIds.join('/')}:${rest}`;
|
|
90
|
+
};
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
* @param {boolean | Partial<Control>} [control] - 是否展示尺寸与列控制按钮;显式传入 columns 时列控入口不渲染
|
|
47
47
|
* @param {SearchFormConfig} [searchFormConfig] - 搜索栏查询字段表单配置
|
|
48
48
|
* @param {ContainerComponent | false} [tableContainer] - 表格区域包裹容器
|
|
49
|
+
* @param {boolean | EllipsisTooltipConfig} [ellipsisTooltip] - truthy 时全局启用所有列;对象同时作为 Tooltip 配置;列上 `showTooltip: false` 或 `ellipsis: false` 可退出
|
|
49
50
|
*
|
|
50
51
|
* @slot search-form - 搜索区域插槽,作用域参数 form / search / reset
|
|
51
52
|
* @slot button-bar - 表格头部按钮区插槽
|
|
@@ -65,7 +66,9 @@ import {
|
|
|
65
66
|
nextTick,
|
|
66
67
|
onMounted,
|
|
67
68
|
inject,
|
|
69
|
+
ref,
|
|
68
70
|
toValue,
|
|
71
|
+
watch,
|
|
69
72
|
type Slot,
|
|
70
73
|
} from 'vue';
|
|
71
74
|
import { INJECT_CONFIG } from '../../component-provider/constants';
|
|
@@ -91,7 +94,15 @@ import DefaultSearchFormContainer from './DefaultSearchFormContainer.vue';
|
|
|
91
94
|
import DefaultTableContainer from './DefaultTableContainer.vue';
|
|
92
95
|
import SizeControl from './SizeControl.vue';
|
|
93
96
|
import ColumnControl from './ColumnControl.vue';
|
|
94
|
-
import
|
|
97
|
+
import TableEllipsisTooltip from './TableEllipsisTooltip.vue';
|
|
98
|
+
import { useEllipsisTooltip } from '../hooks/useEllipsisTooltip';
|
|
99
|
+
import { useEllipsisTooltipColumns } from '../hooks/useEllipsisTooltipColumns';
|
|
100
|
+
import type {
|
|
101
|
+
Column,
|
|
102
|
+
Columns,
|
|
103
|
+
ColumnControlConfig,
|
|
104
|
+
EllipsisTooltipConfig,
|
|
105
|
+
} from '../types';
|
|
95
106
|
|
|
96
107
|
defineOptions({ name: 'ProTable', inheritAttrs: false });
|
|
97
108
|
|
|
@@ -124,6 +135,8 @@ type Props = {
|
|
|
124
135
|
searchFormConfig?: SearchFormConfig;
|
|
125
136
|
/** 表格区域包裹容器 */
|
|
126
137
|
tableContainer?: ContainerComponent | false;
|
|
138
|
+
/** 表格级省略 Tooltip 配置;truthy 时全局启用 */
|
|
139
|
+
ellipsisTooltip?: boolean | EllipsisTooltipConfig;
|
|
127
140
|
} & /* @vue-ignore */ TableProps;
|
|
128
141
|
|
|
129
142
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -134,6 +147,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
134
147
|
control: undefined,
|
|
135
148
|
searchFormConfig: undefined,
|
|
136
149
|
tableContainer: undefined,
|
|
150
|
+
ellipsisTooltip: undefined,
|
|
137
151
|
});
|
|
138
152
|
|
|
139
153
|
const { t } = useLocale();
|
|
@@ -171,6 +185,8 @@ const searchFormProps = computed(() => {
|
|
|
171
185
|
|
|
172
186
|
const attrs: Record<string, any> = useReactiveAttrs();
|
|
173
187
|
|
|
188
|
+
const rootRef = ref<HTMLElement | null>(null);
|
|
189
|
+
|
|
174
190
|
const size = defineModel<TableProps['size']>('size');
|
|
175
191
|
const loading = defineModel<boolean>('loading', { default: false });
|
|
176
192
|
|
|
@@ -318,25 +334,6 @@ const effectiveSize = computed<TableProps['size']>(() => {
|
|
|
318
334
|
return size.value ?? injectAttrs.value.size;
|
|
319
335
|
});
|
|
320
336
|
|
|
321
|
-
/*
|
|
322
|
-
* 下发给宿主表格的属性:Provider 注入为底(其不可注入键已在注入层剔除),调用方绑定与 v-model 透传值覆盖同键。
|
|
323
|
-
* size / loading 为声明模型,其 onUpdate:* 回写通道不进入 attrs,无需另行剔除
|
|
324
|
-
*/
|
|
325
|
-
const computedTableProps = computed<TableProps>(() => {
|
|
326
|
-
const merged = {
|
|
327
|
-
...injectAttrs.value,
|
|
328
|
-
...omitBy(
|
|
329
|
-
pick(camelAttrs.value, tableProperties),
|
|
330
|
-
value => value === undefined
|
|
331
|
-
),
|
|
332
|
-
} as Record<string, any>;
|
|
333
|
-
// onChange 由模板监听器收口,散落 attrs 中的同名监听器并入会形成重复回调
|
|
334
|
-
delete merged.onChange;
|
|
335
|
-
merged.size = effectiveSize.value;
|
|
336
|
-
merged.loading = undefined;
|
|
337
|
-
return merged as TableProps;
|
|
338
|
-
});
|
|
339
|
-
|
|
340
337
|
const computedTableContainer = computed(() => {
|
|
341
338
|
const container =
|
|
342
339
|
props.tableContainer ??
|
|
@@ -378,45 +375,93 @@ const computedColumns = computed<Columns<RecordType>>(() => {
|
|
|
378
375
|
return needIndex ? [indexColumn.value, ...cols] : cols;
|
|
379
376
|
});
|
|
380
377
|
|
|
378
|
+
const ellipsisTooltipValue = computed(
|
|
379
|
+
() => props.ellipsisTooltip ?? injectProps.value.ellipsisTooltip
|
|
380
|
+
);
|
|
381
|
+
const ellipsisTooltipConfig = computed<EllipsisTooltipConfig>(() =>
|
|
382
|
+
typeof ellipsisTooltipValue.value === 'object' && ellipsisTooltipValue.value
|
|
383
|
+
? ellipsisTooltipValue.value
|
|
384
|
+
: {}
|
|
385
|
+
);
|
|
386
|
+
const ellipsisTooltipGlobal = computed(() =>
|
|
387
|
+
Boolean(ellipsisTooltipValue.value)
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const {
|
|
391
|
+
columns: visibleComputedColumns,
|
|
392
|
+
hasTooltip: ellipsisTooltipEnabled,
|
|
393
|
+
resolveOriginalColumn,
|
|
394
|
+
resolveSorterColumn,
|
|
395
|
+
} = useEllipsisTooltipColumns(
|
|
396
|
+
() => computedColumns.value,
|
|
397
|
+
() => ellipsisTooltipGlobal.value
|
|
398
|
+
);
|
|
399
|
+
|
|
381
400
|
/*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
401
|
+
* 下发给宿主表格的属性:Provider 注入为底(其不可注入键已在注入层剔除),调用方绑定与 v-model 透传值覆盖同键。
|
|
402
|
+
* size / loading 为声明模型,其 onUpdate:* 回写通道不进入 attrs,无需另行剔除
|
|
384
403
|
*/
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
ancestors.add(column);
|
|
403
|
-
const filtered = filterVisibleColumns(column.children, ancestors);
|
|
404
|
-
ancestors.delete(column);
|
|
405
|
-
if (filtered.changed) {
|
|
406
|
-
changed = true;
|
|
407
|
-
list.push({ ...column, children: filtered.list });
|
|
408
|
-
continue;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
list.push(column);
|
|
404
|
+
const computedTableProps = computed<TableProps>(() => {
|
|
405
|
+
const merged = {
|
|
406
|
+
...injectAttrs.value,
|
|
407
|
+
...omitBy(
|
|
408
|
+
pick(camelAttrs.value, tableProperties),
|
|
409
|
+
value => value === undefined
|
|
410
|
+
),
|
|
411
|
+
} as Record<string, any>;
|
|
412
|
+
// onChange 由模板监听器收口,散落 attrs 中的同名监听器并入会形成重复回调
|
|
413
|
+
delete merged.onChange;
|
|
414
|
+
// resizeColumn 回传转换列时,用户回调仍操作原始列
|
|
415
|
+
const resizeColumn = merged.onResizeColumn;
|
|
416
|
+
if (typeof resizeColumn === 'function') {
|
|
417
|
+
merged.onResizeColumn = (width: number, column: any) =>
|
|
418
|
+
resizeColumn(width, resolveOriginalColumn(column));
|
|
412
419
|
}
|
|
413
|
-
|
|
414
|
-
|
|
420
|
+
merged.size = effectiveSize.value;
|
|
421
|
+
merged.loading = undefined;
|
|
422
|
+
return merged as TableProps;
|
|
423
|
+
});
|
|
415
424
|
|
|
416
|
-
const
|
|
417
|
-
|
|
425
|
+
const ellipsisTooltipContainer = computed(() => {
|
|
426
|
+
const getPopupContainer = ellipsisTooltipConfig.value.getPopupContainer;
|
|
427
|
+
if (!getPopupContainer) return undefined;
|
|
428
|
+
return () => {
|
|
429
|
+
if (typeof document === 'undefined') return undefined;
|
|
430
|
+
const root = rootRef.value ?? document.body;
|
|
431
|
+
return getPopupContainer(root) ?? document.body;
|
|
432
|
+
};
|
|
433
|
+
});
|
|
434
|
+
const ellipsisTooltipDelay = computed(() =>
|
|
435
|
+
Math.max(0, (ellipsisTooltipConfig.value.mouseEnterDelay ?? 0.2) * 1000)
|
|
436
|
+
);
|
|
437
|
+
const ellipsisTooltipLeaveDelay = computed(() =>
|
|
438
|
+
Math.max(0, (ellipsisTooltipConfig.value.mouseLeaveDelay ?? 0.1) * 1000)
|
|
439
|
+
);
|
|
440
|
+
const ellipsisTooltipProps = computed(() => {
|
|
441
|
+
const config = ellipsisTooltipConfig.value;
|
|
442
|
+
const {
|
|
443
|
+
getPopupContainer: _getPopupContainer,
|
|
444
|
+
mouseEnterDelay: _mouseEnterDelay,
|
|
445
|
+
mouseLeaveDelay: _mouseLeaveDelay,
|
|
446
|
+
...rest
|
|
447
|
+
} = config;
|
|
448
|
+
return rest;
|
|
418
449
|
});
|
|
419
450
|
|
|
451
|
+
const {
|
|
452
|
+
open: ellipsisTooltipOpen,
|
|
453
|
+
content: ellipsisTooltipContent,
|
|
454
|
+
anchorStyle: ellipsisTooltipAnchorStyle,
|
|
455
|
+
onMouseOver: onEllipsisMouseOver,
|
|
456
|
+
onMouseOut: onEllipsisMouseOut,
|
|
457
|
+
close: closeEllipsisTooltip,
|
|
458
|
+
} = useEllipsisTooltip(
|
|
459
|
+
rootRef,
|
|
460
|
+
ellipsisTooltipEnabled,
|
|
461
|
+
ellipsisTooltipDelay,
|
|
462
|
+
ellipsisTooltipLeaveDelay
|
|
463
|
+
);
|
|
464
|
+
|
|
420
465
|
// 数据源优先级:显式绑定 > table 实例(useTable 创建的 dataSource)
|
|
421
466
|
const computedDataSource = computed<RecordType[]>(() => {
|
|
422
467
|
return (
|
|
@@ -424,6 +469,9 @@ const computedDataSource = computed<RecordType[]>(() => {
|
|
|
424
469
|
);
|
|
425
470
|
});
|
|
426
471
|
|
|
472
|
+
// 列或数据变化后旧锚点失效,关闭共享 Tooltip
|
|
473
|
+
watch([visibleComputedColumns, computedDataSource], closeEllipsisTooltip);
|
|
474
|
+
|
|
427
475
|
/*
|
|
428
476
|
* 分页优先级:显式绑定 > provider 全局配置 > 默认合成;
|
|
429
477
|
* provider 作为基础配置与 table.pageParam 合并,显式绑定的对象形态在其之上覆盖
|
|
@@ -479,8 +527,15 @@ const onTableChange: TableProps['onChange'] = (...args) => {
|
|
|
479
527
|
);
|
|
480
528
|
if (shadowed.length) warnPaginationShadow(shadowed);
|
|
481
529
|
}
|
|
482
|
-
// change 回放:模板监听器优先,未绑定时回退 Provider
|
|
483
|
-
|
|
530
|
+
// change 回放:模板监听器优先,未绑定时回退 Provider 全局配置;sorter.column 还原为原始列
|
|
531
|
+
const nextArgs = [...args] as any[];
|
|
532
|
+
const sorterArg = nextArgs[2];
|
|
533
|
+
if (Array.isArray(sorterArg)) {
|
|
534
|
+
nextArgs[2] = sorterArg.map(resolveSorterColumn);
|
|
535
|
+
} else if (sorterArg) {
|
|
536
|
+
nextArgs[2] = resolveSorterColumn(sorterArg);
|
|
537
|
+
}
|
|
538
|
+
(attrs.onChange ?? injectProps.value.onChange)?.(...nextArgs);
|
|
484
539
|
_search();
|
|
485
540
|
};
|
|
486
541
|
|
|
@@ -541,7 +596,15 @@ defineExpose({
|
|
|
541
596
|
</script>
|
|
542
597
|
|
|
543
598
|
<template>
|
|
544
|
-
<div
|
|
599
|
+
<div
|
|
600
|
+
ref="rootRef"
|
|
601
|
+
class="pro-table"
|
|
602
|
+
data-pro-table-root
|
|
603
|
+
:class="attrs.class"
|
|
604
|
+
:style="attrs.style"
|
|
605
|
+
@mouseover="onEllipsisMouseOver"
|
|
606
|
+
@mouseout="onEllipsisMouseOut"
|
|
607
|
+
>
|
|
545
608
|
<ContainerFragment
|
|
546
609
|
v-if="searchFormVisible || $slots['search-form']"
|
|
547
610
|
:component="computedSearchFormConfig.container"
|
|
@@ -620,6 +683,15 @@ defineExpose({
|
|
|
620
683
|
</ATable>
|
|
621
684
|
</slot>
|
|
622
685
|
</ContainerFragment>
|
|
686
|
+
|
|
687
|
+
<TableEllipsisTooltip
|
|
688
|
+
v-if="ellipsisTooltipEnabled"
|
|
689
|
+
:open="ellipsisTooltipOpen"
|
|
690
|
+
:content="ellipsisTooltipContent"
|
|
691
|
+
:anchor-style="ellipsisTooltipAnchorStyle"
|
|
692
|
+
:container="ellipsisTooltipContainer"
|
|
693
|
+
v-bind="ellipsisTooltipProps"
|
|
694
|
+
/>
|
|
623
695
|
</div>
|
|
624
696
|
</template>
|
|
625
697
|
|