@tmagic/form 1.8.0-beta.26 → 1.8.0-beta.28
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/dist/es/containers/GroupList.vue_vue_type_script_setup_true_lang.js +26 -12
- package/dist/es/fields/Select.vue_vue_type_script_setup_true_lang.js +11 -4
- package/dist/es/style.css +40 -14
- package/dist/es/utils/formStateProxy.js +40 -4
- package/dist/es/utils/tableGroupList.js +20 -1
- package/dist/style.css +40 -14
- package/dist/themes/magic-admin.css +40 -14
- package/dist/tmagic-form-headless.umd.cjs +40 -4
- package/dist/tmagic-form.umd.cjs +101 -27
- package/package.json +4 -4
- package/src/containers/GroupList.vue +23 -2
- package/src/fields/Select.vue +13 -4
- package/src/theme/group-list.scss +52 -27
- package/src/utils/formStateProxy.ts +50 -5
- package/src/utils/tableGroupList.ts +28 -1
|
@@ -1,15 +1,15 @@
|
|
|
1
|
+
import { getGroupListHeaderVars } from "../utils/tableGroupList.js";
|
|
1
2
|
import GroupListItem_default from "./GroupListItem.js";
|
|
2
|
-
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, defineComponent, mergeProps, normalizeClass, openBlock, renderList, renderSlot, toDisplayString, withCtx } from "vue";
|
|
3
|
+
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, defineComponent, mergeProps, normalizeClass, normalizeStyle, openBlock, renderList, renderSlot, toDisplayString, withCtx } from "vue";
|
|
3
4
|
import { cloneDeep } from "lodash-es";
|
|
4
5
|
//#region packages/form/src/containers/GroupList.vue?vue&type=script&setup=true&lang.ts
|
|
5
|
-
var _hoisted_1 =
|
|
6
|
-
var _hoisted_2 =
|
|
7
|
-
var _hoisted_3 = {
|
|
6
|
+
var _hoisted_1 = ["innerHTML"];
|
|
7
|
+
var _hoisted_2 = {
|
|
8
8
|
key: 1,
|
|
9
9
|
class: "el-table__empty-block"
|
|
10
10
|
};
|
|
11
|
-
var
|
|
12
|
-
var
|
|
11
|
+
var _hoisted_3 = { class: "el-table__empty-text t-table__empty" };
|
|
12
|
+
var _hoisted_4 = { style: {
|
|
13
13
|
"display": "flex",
|
|
14
14
|
"justify-content": "flex-end",
|
|
15
15
|
"flex": "1"
|
|
@@ -54,6 +54,14 @@ var GroupList_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
|
|
|
54
54
|
};
|
|
55
55
|
const onAddDiffCount = () => emit("addDiffCount");
|
|
56
56
|
const asList = (value) => Array.isArray(value) ? value : [];
|
|
57
|
+
const headerVars = computed(() => getGroupListHeaderVars(props.config.header));
|
|
58
|
+
/**
|
|
59
|
+
* 取 `$slots` 而不是 `useSlots()`:宿主切换 `addable` 时插槽会增删,
|
|
60
|
+
* computed 缓存不随插槽变化失效,只有在渲染期读才拿得到最新的。
|
|
61
|
+
*/
|
|
62
|
+
const hasFooter = (slots) => !props.isCompare && Boolean(slots["toggle-button"] || slots["add-button"]);
|
|
63
|
+
/** 只有真的渲染出吸底 footer,内层列表才需要为它让位 */
|
|
64
|
+
const isFooterSticky = (slots) => hasFooter(slots) && Boolean(props.config.addButtonConfig?.sticky);
|
|
57
65
|
const currentList = computed(() => asList(props.model[props.name]));
|
|
58
66
|
/** 对比时按当前/历史较长一侧对齐,已删除的项也能渲染出来 */
|
|
59
67
|
const displayItems = computed(() => {
|
|
@@ -71,13 +79,19 @@ var GroupList_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
|
|
|
71
79
|
}));
|
|
72
80
|
});
|
|
73
81
|
return (_ctx, _cache) => {
|
|
74
|
-
return openBlock(), createElementBlock("div",
|
|
82
|
+
return openBlock(), createElementBlock("div", {
|
|
83
|
+
class: normalizeClass(["m-fields-group-list", {
|
|
84
|
+
"is-header-sticky": Boolean(__props.config.header?.sticky),
|
|
85
|
+
"is-footer-sticky": isFooterSticky(_ctx.$slots)
|
|
86
|
+
}]),
|
|
87
|
+
style: normalizeStyle(headerVars.value)
|
|
88
|
+
}, [
|
|
75
89
|
__props.config.extra ? (openBlock(), createElementBlock("div", {
|
|
76
90
|
key: 0,
|
|
77
91
|
innerHTML: __props.config.extra,
|
|
78
92
|
style: { "color": "rgba(0, 0, 0, 0.45)" }
|
|
79
|
-
}, null, 8,
|
|
80
|
-
!displayItems.value.length ? (openBlock(), createElementBlock("div",
|
|
93
|
+
}, null, 8, _hoisted_1)) : createCommentVNode("v-if", true),
|
|
94
|
+
!displayItems.value.length ? (openBlock(), createElementBlock("div", _hoisted_2, [createElementVNode("span", _hoisted_3, "暂无" + toDisplayString(__props.config.titlePrefix || "") + "数据", 1)])) : (openBlock(true), createElementBlock(Fragment, { key: 2 }, renderList(displayItems.value, (entry) => {
|
|
81
95
|
return openBlock(), createBlock(GroupListItem_default, {
|
|
82
96
|
key: entry.index,
|
|
83
97
|
model: entry.item,
|
|
@@ -114,11 +128,11 @@ var GroupList_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
|
|
|
114
128
|
"group-model"
|
|
115
129
|
]);
|
|
116
130
|
}), 128)),
|
|
117
|
-
|
|
131
|
+
hasFooter(_ctx.$slots) ? (openBlock(), createElementBlock("div", {
|
|
118
132
|
key: 3,
|
|
119
133
|
class: normalizeClass(["m-fields-group-list-footer", { "is-sticky-full": Boolean(__props.config.addButtonConfig?.sticky) }])
|
|
120
|
-
}, [renderSlot(_ctx.$slots, "toggle-button"), createElementVNode("div",
|
|
121
|
-
]);
|
|
134
|
+
}, [renderSlot(_ctx.$slots, "toggle-button"), createElementVNode("div", _hoisted_4, [renderSlot(_ctx.$slots, "add-button")])], 2)) : createCommentVNode("v-if", true)
|
|
135
|
+
], 6);
|
|
122
136
|
};
|
|
123
137
|
}
|
|
124
138
|
});
|
|
@@ -199,7 +199,10 @@ var Select_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCom
|
|
|
199
199
|
prop: props.prop,
|
|
200
200
|
postOptions
|
|
201
201
|
});
|
|
202
|
-
let initData
|
|
202
|
+
let initData;
|
|
203
|
+
try {
|
|
204
|
+
initData = getValueByKeyPath(initRoot || root, res);
|
|
205
|
+
} catch {}
|
|
203
206
|
if (initData) {
|
|
204
207
|
if (!Array.isArray(initData)) initData = [initData];
|
|
205
208
|
if (typeof option.item === "function") options = option.item(initData);
|
|
@@ -230,8 +233,12 @@ var Select_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCom
|
|
|
230
233
|
const v = props.model[props.name];
|
|
231
234
|
if (Array.isArray(v) ? !v.length : typeof v === "undefined") return;
|
|
232
235
|
if (typeof v === "undefined" || hasOption(v)) return;
|
|
233
|
-
|
|
234
|
-
|
|
236
|
+
try {
|
|
237
|
+
const data = await getInitOption();
|
|
238
|
+
setOptions(data);
|
|
239
|
+
} catch {
|
|
240
|
+
setOptions([]);
|
|
241
|
+
}
|
|
235
242
|
};
|
|
236
243
|
watch(() => props.model?.[props.name], () => {
|
|
237
244
|
initOption();
|
|
@@ -326,7 +333,7 @@ var Select_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCom
|
|
|
326
333
|
}), 128)) : (openBlock(true), createElementBlock(Fragment, { key: 1 }, renderList(options.value, (option) => {
|
|
327
334
|
return openBlock(), createBlock(resolveDynamicComponent(unref(optionComponent)?.component || "el-option"), mergeProps({
|
|
328
335
|
class: "tmagic-design-option",
|
|
329
|
-
key: __props.config.valueKey ? option.value[__props.config.valueKey] : option.value
|
|
336
|
+
key: __props.config.valueKey ? option.value?.[__props.config.valueKey] : option.value
|
|
330
337
|
}, { ref_for: true }, unref(optionComponent)?.props({
|
|
331
338
|
label: option.text,
|
|
332
339
|
value: option.value,
|
package/dist/es/style.css
CHANGED
|
@@ -333,18 +333,48 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
333
333
|
flex-shrink: 0;
|
|
334
334
|
gap: 8px;
|
|
335
335
|
}
|
|
336
|
-
.m-fields-group-list .m-fields-group-list-item
|
|
337
|
-
|
|
336
|
+
.m-fields-group-list > .m-fields-group-list-item {
|
|
337
|
+
--m-group-list-item-footer-bottom: var(
|
|
338
|
+
--m-group-list-nested-footer-bottom,
|
|
339
|
+
0px
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
.m-fields-group-list.is-footer-sticky > .m-fields-group-list-item {
|
|
343
|
+
--m-group-list-item-footer-bottom: calc(
|
|
344
|
+
var(--m-group-list-nested-footer-bottom, 0px) +
|
|
345
|
+
var(--m-group-list-footer-height)
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
.m-fields-group-list .m-fields-group-list-item .m-fields-group-list {
|
|
349
|
+
--m-group-list-nested-footer-bottom: var(
|
|
350
|
+
--m-group-list-item-footer-bottom,
|
|
351
|
+
0px
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
.m-fields-group-list.is-header-sticky {
|
|
355
|
+
--m-group-list-nested-sticky-top: var(--m-group-list-child-list-sticky-top);
|
|
356
|
+
--m-group-list-nested-header-z: var(--m-group-list-child-list-header-z);
|
|
357
|
+
}
|
|
358
|
+
.m-fields-group-list.is-header-sticky > .m-fields-group-list-item {
|
|
359
|
+
--m-group-list-item-sticky-top: var(
|
|
360
|
+
--m-group-list-nested-sticky-top,
|
|
361
|
+
var(--m-group-list-header-sticky-top, 0px)
|
|
362
|
+
);
|
|
363
|
+
--m-group-list-item-header-z: var(--m-group-list-nested-header-z, 7);
|
|
364
|
+
--m-group-list-child-list-sticky-top: calc(
|
|
365
|
+
var(--m-group-list-item-sticky-top) + var(--m-group-list-header-height)
|
|
366
|
+
);
|
|
367
|
+
--m-group-list-child-list-header-z: calc(
|
|
368
|
+
var(--m-group-list-item-header-z) - 1
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
.m-fields-group-list.is-header-sticky > .m-fields-group-list-item > .el-card__header,
|
|
372
|
+
.m-fields-group-list.is-header-sticky > .m-fields-group-list-item > .t-card__header {
|
|
338
373
|
position: sticky;
|
|
339
|
-
top: var(--m-group-list-
|
|
340
|
-
z-index:
|
|
374
|
+
top: var(--m-group-list-item-sticky-top);
|
|
375
|
+
z-index: var(--m-group-list-item-header-z);
|
|
341
376
|
background-color: var(--m-group-list-header-bg, #fff);
|
|
342
377
|
}
|
|
343
|
-
.m-fields-group-list .m-fields-group-list-item .m-fields-group-list-item > .el-card__header,
|
|
344
|
-
.m-fields-group-list .m-fields-group-list-item .m-fields-group-list-item > .t-card__header {
|
|
345
|
-
top: calc(var(--m-group-list-header-sticky-top, 0px) + var(--m-group-list-header-height));
|
|
346
|
-
z-index: 6;
|
|
347
|
-
}
|
|
348
378
|
.m-fields-group-list .el-table__empty-block {
|
|
349
379
|
width: 100%;
|
|
350
380
|
min-height: 0;
|
|
@@ -362,7 +392,7 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
362
392
|
}
|
|
363
393
|
.m-fields-group-list > .m-fields-group-list-footer.is-sticky-full {
|
|
364
394
|
position: sticky;
|
|
365
|
-
bottom:
|
|
395
|
+
bottom: var(--m-group-list-nested-footer-bottom, 0px);
|
|
366
396
|
z-index: 7;
|
|
367
397
|
margin-bottom: 0;
|
|
368
398
|
padding: var(--m-group-list-footer-padding-top) 0 var(--m-group-list-footer-padding-bottom);
|
|
@@ -376,10 +406,6 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
376
406
|
width: 100%;
|
|
377
407
|
height: var(--m-group-list-footer-button-height);
|
|
378
408
|
}
|
|
379
|
-
.m-fields-group-list .m-fields-group-list-item .m-fields-group-list > .m-fields-group-list-footer.is-sticky-full {
|
|
380
|
-
bottom: var(--m-group-list-footer-height);
|
|
381
|
-
z-index: 7;
|
|
382
|
-
}
|
|
383
409
|
|
|
384
410
|
/** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
|
|
385
411
|
.m-container-group-list.outer-gorup_list > .m-fields-group-list > .m-fields-group-list-item {
|
|
@@ -30,6 +30,36 @@ var mergeFormContexts = (...layers) => {
|
|
|
30
30
|
});
|
|
31
31
|
};
|
|
32
32
|
/**
|
|
33
|
+
* 存量下发配置沿用 Vue2 时代的写法,把回调第一个参数当组件实例用,靠 `vm.mForm.xxx`
|
|
34
|
+
* 取表单状态。这类配置除了读,还会往上面挂方法做跨字段通信,例如:
|
|
35
|
+
*
|
|
36
|
+
* ```js
|
|
37
|
+
* vm.mForm.checkPropertyLimit = async (...) => { ... } // 一个字段的 validator 里挂
|
|
38
|
+
* await vm.mForm.checkPropertyLimit(...) // 另一处再取出来调用
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* 所以 `mForm` 必须能指回某个 formState:读能落到 core / context,写能经 `set` trap
|
|
42
|
+
* 落到 coreState 并持久化。
|
|
43
|
+
*
|
|
44
|
+
* 优先级是 core > context > 合成自引用,三者都是刻意的:
|
|
45
|
+
*
|
|
46
|
+
* - **core**:`formState.mForm = formState` 这类直写(如 FormPreview 用自建 services 时)
|
|
47
|
+
* 必须最优先。
|
|
48
|
+
* - **context**:唯一的生产者是把父 formState 整体当 context 传下来的嵌套表单
|
|
49
|
+
* (ComponentForm)。此时 `mForm` 命中 context 指向**父表单**,与 `extendState` 时代
|
|
50
|
+
* 把父 formState 并入子状态的结果一致,跨字段通信仍落在同一份对象上。父 formState
|
|
51
|
+
* 本身也是 Proxy,写入照样持久化,所以这里让 context 赢是对的,别「修正」成指向子表单。
|
|
52
|
+
* - **合成自引用**:前两者都没有时才兜底,让 `vm.mForm.xxx = fn` 落到自己的 coreState。
|
|
53
|
+
*
|
|
54
|
+
* 由此推出一条约束:**不要往 context 里塞普通对象充当 `mForm`**。context 通常是 computed
|
|
55
|
+
* 产物,依赖一变就重建,挂上去的方法会静默丢失。要么直写 core,要么什么都不放交给兜底。
|
|
56
|
+
*
|
|
57
|
+
* 枚举语义上合成的自引用表现得像原型链上的属性:`'mForm' in state` 为真,但不出现在
|
|
58
|
+
* `Object.keys(state)` 里,`getOwnPropertyDescriptor` 也返回 undefined。这是故意的——
|
|
59
|
+
* 按扩展字段打包 formState 的调用方(如发给 AI 的逻辑)会因循环引用炸掉。
|
|
60
|
+
*/
|
|
61
|
+
var SELF_REF_KEY = "mForm";
|
|
62
|
+
/**
|
|
33
63
|
* 将 coreState 与宿主业务上下文关联:读取时优先 core,miss 再读穿到 context。
|
|
34
64
|
*
|
|
35
65
|
* - `get` 用属性访问而非 `Reflect.get(t, k, receiver)`,避免破坏 Vue reactive 的 `__v_raw`;
|
|
@@ -42,19 +72,24 @@ var mergeFormContexts = (...layers) => {
|
|
|
42
72
|
* 独立成文件,避免与 `form.ts` ↔ `typeMatch.ts` 形成循环依赖。
|
|
43
73
|
*/
|
|
44
74
|
var createFormStateProxy = (coreState, getContext) => {
|
|
45
|
-
const resolve = () =>
|
|
46
|
-
|
|
75
|
+
const resolve = () => {
|
|
76
|
+
const ctx = (typeof getContext === "function" ? getContext() : unref(getContext)) || EMPTY_CONTEXT;
|
|
77
|
+
return ctx === proxy ? EMPTY_CONTEXT : ctx;
|
|
78
|
+
};
|
|
79
|
+
const proxy = new Proxy(coreState, {
|
|
47
80
|
get(t, k) {
|
|
48
81
|
if (typeof k === "symbol") return Reflect.get(t, k);
|
|
49
82
|
const v = t[k];
|
|
50
83
|
if (v !== void 0 || Reflect.has(t, k)) return v;
|
|
51
|
-
|
|
84
|
+
const ctx = resolve();
|
|
85
|
+
if (k in ctx) return ctx[k];
|
|
86
|
+
return k === SELF_REF_KEY ? proxy : void 0;
|
|
52
87
|
},
|
|
53
88
|
set(t, k, value) {
|
|
54
89
|
t[k] = value;
|
|
55
90
|
return true;
|
|
56
91
|
},
|
|
57
|
-
has: (t, k) => Reflect.has(t, k) || typeof k !== "symbol" && k in resolve(),
|
|
92
|
+
has: (t, k) => Reflect.has(t, k) || typeof k !== "symbol" && (k in resolve() || k === SELF_REF_KEY),
|
|
58
93
|
ownKeys: (t) => [.../* @__PURE__ */ new Set([...Reflect.ownKeys(t), ...Reflect.ownKeys(resolve()).filter((k) => typeof k !== "symbol")])],
|
|
59
94
|
getOwnPropertyDescriptor: (t, k) => {
|
|
60
95
|
const own = Reflect.getOwnPropertyDescriptor(t, k);
|
|
@@ -70,6 +105,7 @@ var createFormStateProxy = (coreState, getContext) => {
|
|
|
70
105
|
};
|
|
71
106
|
}
|
|
72
107
|
});
|
|
108
|
+
return proxy;
|
|
73
109
|
};
|
|
74
110
|
//#endregion
|
|
75
111
|
export { createFormStateProxy, mergeFormContexts };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cloneDeep } from "lodash-es";
|
|
2
|
+
import { isNumber } from "@tmagic/utils";
|
|
2
3
|
//#region packages/form/src/utils/tableGroupList.ts
|
|
3
4
|
/**
|
|
4
5
|
* table / group-list 是同一份配置的两种展示形态,可以互相切换(`TableGroupList.vue`)。
|
|
@@ -9,6 +10,24 @@ import { cloneDeep } from "lodash-es";
|
|
|
9
10
|
*/
|
|
10
11
|
/** group-list 形态的 type(兼容驼峰与中划线两种写法) */
|
|
11
12
|
var isGroupListType = (type) => type === "groupList" || type === "group-list";
|
|
13
|
+
/**
|
|
14
|
+
* 归一成带单位的 CSS 长度。
|
|
15
|
+
*
|
|
16
|
+
* 配置常常来自 JSON 或表单输入,`'48'` 这类无单位数字串很常见;直接写进 CSS 变量会让
|
|
17
|
+
* 嵌套层的 `calc()` 整体失效、吸顶静默失灵,所以纯数字一律补 `px`。
|
|
18
|
+
*/
|
|
19
|
+
var toCssLength = (height) => {
|
|
20
|
+
const value = `${height ?? ""}`.trim();
|
|
21
|
+
if (!value) return void 0;
|
|
22
|
+
if (isNumber(value)) return `${value}px`;
|
|
23
|
+
return typeof height === "number" ? void 0 : value;
|
|
24
|
+
};
|
|
25
|
+
/** header.sticky 开启时把 height 写成 CSS 变量,给嵌套吸顶偏移用 */
|
|
26
|
+
var getGroupListHeaderVars = (header) => {
|
|
27
|
+
if (!header?.sticky) return void 0;
|
|
28
|
+
const height = toCssLength(header.height);
|
|
29
|
+
return height ? { "--m-group-list-header-height": height } : void 0;
|
|
30
|
+
};
|
|
12
31
|
/** 按 label 文案长度估算 label 宽度(中文按 20px、其他按 8px,最小 80px) */
|
|
13
32
|
var calcLabelWidth = (label) => {
|
|
14
33
|
if (!label) return "0px";
|
|
@@ -82,4 +101,4 @@ var makeTableColumnConfig = (column, row) => {
|
|
|
82
101
|
return newConfig;
|
|
83
102
|
};
|
|
84
103
|
//#endregion
|
|
85
|
-
export { calcLabelWidth, getGroupListRowConfig, isGroupListType, isTableColumnRendered, makeTableColumnConfig, toGroupListConfig, toTableConfig };
|
|
104
|
+
export { calcLabelWidth, getGroupListHeaderVars, getGroupListRowConfig, isGroupListType, isTableColumnRendered, makeTableColumnConfig, toGroupListConfig, toTableConfig };
|
package/dist/style.css
CHANGED
|
@@ -333,18 +333,48 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
333
333
|
flex-shrink: 0;
|
|
334
334
|
gap: 8px;
|
|
335
335
|
}
|
|
336
|
-
.m-fields-group-list .m-fields-group-list-item
|
|
337
|
-
|
|
336
|
+
.m-fields-group-list > .m-fields-group-list-item {
|
|
337
|
+
--m-group-list-item-footer-bottom: var(
|
|
338
|
+
--m-group-list-nested-footer-bottom,
|
|
339
|
+
0px
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
.m-fields-group-list.is-footer-sticky > .m-fields-group-list-item {
|
|
343
|
+
--m-group-list-item-footer-bottom: calc(
|
|
344
|
+
var(--m-group-list-nested-footer-bottom, 0px) +
|
|
345
|
+
var(--m-group-list-footer-height)
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
.m-fields-group-list .m-fields-group-list-item .m-fields-group-list {
|
|
349
|
+
--m-group-list-nested-footer-bottom: var(
|
|
350
|
+
--m-group-list-item-footer-bottom,
|
|
351
|
+
0px
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
.m-fields-group-list.is-header-sticky {
|
|
355
|
+
--m-group-list-nested-sticky-top: var(--m-group-list-child-list-sticky-top);
|
|
356
|
+
--m-group-list-nested-header-z: var(--m-group-list-child-list-header-z);
|
|
357
|
+
}
|
|
358
|
+
.m-fields-group-list.is-header-sticky > .m-fields-group-list-item {
|
|
359
|
+
--m-group-list-item-sticky-top: var(
|
|
360
|
+
--m-group-list-nested-sticky-top,
|
|
361
|
+
var(--m-group-list-header-sticky-top, 0px)
|
|
362
|
+
);
|
|
363
|
+
--m-group-list-item-header-z: var(--m-group-list-nested-header-z, 7);
|
|
364
|
+
--m-group-list-child-list-sticky-top: calc(
|
|
365
|
+
var(--m-group-list-item-sticky-top) + var(--m-group-list-header-height)
|
|
366
|
+
);
|
|
367
|
+
--m-group-list-child-list-header-z: calc(
|
|
368
|
+
var(--m-group-list-item-header-z) - 1
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
.m-fields-group-list.is-header-sticky > .m-fields-group-list-item > .el-card__header,
|
|
372
|
+
.m-fields-group-list.is-header-sticky > .m-fields-group-list-item > .t-card__header {
|
|
338
373
|
position: sticky;
|
|
339
|
-
top: var(--m-group-list-
|
|
340
|
-
z-index:
|
|
374
|
+
top: var(--m-group-list-item-sticky-top);
|
|
375
|
+
z-index: var(--m-group-list-item-header-z);
|
|
341
376
|
background-color: var(--m-group-list-header-bg, #fff);
|
|
342
377
|
}
|
|
343
|
-
.m-fields-group-list .m-fields-group-list-item .m-fields-group-list-item > .el-card__header,
|
|
344
|
-
.m-fields-group-list .m-fields-group-list-item .m-fields-group-list-item > .t-card__header {
|
|
345
|
-
top: calc(var(--m-group-list-header-sticky-top, 0px) + var(--m-group-list-header-height));
|
|
346
|
-
z-index: 6;
|
|
347
|
-
}
|
|
348
378
|
.m-fields-group-list .el-table__empty-block {
|
|
349
379
|
width: 100%;
|
|
350
380
|
min-height: 0;
|
|
@@ -362,7 +392,7 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
362
392
|
}
|
|
363
393
|
.m-fields-group-list > .m-fields-group-list-footer.is-sticky-full {
|
|
364
394
|
position: sticky;
|
|
365
|
-
bottom:
|
|
395
|
+
bottom: var(--m-group-list-nested-footer-bottom, 0px);
|
|
366
396
|
z-index: 7;
|
|
367
397
|
margin-bottom: 0;
|
|
368
398
|
padding: var(--m-group-list-footer-padding-top) 0 var(--m-group-list-footer-padding-bottom);
|
|
@@ -376,10 +406,6 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
376
406
|
width: 100%;
|
|
377
407
|
height: var(--m-group-list-footer-button-height);
|
|
378
408
|
}
|
|
379
|
-
.m-fields-group-list .m-fields-group-list-item .m-fields-group-list > .m-fields-group-list-footer.is-sticky-full {
|
|
380
|
-
bottom: var(--m-group-list-footer-height);
|
|
381
|
-
z-index: 7;
|
|
382
|
-
}
|
|
383
409
|
|
|
384
410
|
/** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
|
|
385
411
|
.m-container-group-list.outer-gorup_list > .m-fields-group-list > .m-fields-group-list-item {
|
|
@@ -333,18 +333,48 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
333
333
|
flex-shrink: 0;
|
|
334
334
|
gap: 8px;
|
|
335
335
|
}
|
|
336
|
-
.m-fields-group-list .m-fields-group-list-item
|
|
337
|
-
|
|
336
|
+
.m-fields-group-list > .m-fields-group-list-item {
|
|
337
|
+
--m-group-list-item-footer-bottom: var(
|
|
338
|
+
--m-group-list-nested-footer-bottom,
|
|
339
|
+
0px
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
.m-fields-group-list.is-footer-sticky > .m-fields-group-list-item {
|
|
343
|
+
--m-group-list-item-footer-bottom: calc(
|
|
344
|
+
var(--m-group-list-nested-footer-bottom, 0px) +
|
|
345
|
+
var(--m-group-list-footer-height)
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
.m-fields-group-list .m-fields-group-list-item .m-fields-group-list {
|
|
349
|
+
--m-group-list-nested-footer-bottom: var(
|
|
350
|
+
--m-group-list-item-footer-bottom,
|
|
351
|
+
0px
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
.m-fields-group-list.is-header-sticky {
|
|
355
|
+
--m-group-list-nested-sticky-top: var(--m-group-list-child-list-sticky-top);
|
|
356
|
+
--m-group-list-nested-header-z: var(--m-group-list-child-list-header-z);
|
|
357
|
+
}
|
|
358
|
+
.m-fields-group-list.is-header-sticky > .m-fields-group-list-item {
|
|
359
|
+
--m-group-list-item-sticky-top: var(
|
|
360
|
+
--m-group-list-nested-sticky-top,
|
|
361
|
+
var(--m-group-list-header-sticky-top, 0px)
|
|
362
|
+
);
|
|
363
|
+
--m-group-list-item-header-z: var(--m-group-list-nested-header-z, 7);
|
|
364
|
+
--m-group-list-child-list-sticky-top: calc(
|
|
365
|
+
var(--m-group-list-item-sticky-top) + var(--m-group-list-header-height)
|
|
366
|
+
);
|
|
367
|
+
--m-group-list-child-list-header-z: calc(
|
|
368
|
+
var(--m-group-list-item-header-z) - 1
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
.m-fields-group-list.is-header-sticky > .m-fields-group-list-item > .el-card__header,
|
|
372
|
+
.m-fields-group-list.is-header-sticky > .m-fields-group-list-item > .t-card__header {
|
|
338
373
|
position: sticky;
|
|
339
|
-
top: var(--m-group-list-
|
|
340
|
-
z-index:
|
|
374
|
+
top: var(--m-group-list-item-sticky-top);
|
|
375
|
+
z-index: var(--m-group-list-item-header-z);
|
|
341
376
|
background-color: var(--m-group-list-header-bg, #fff);
|
|
342
377
|
}
|
|
343
|
-
.m-fields-group-list .m-fields-group-list-item .m-fields-group-list-item > .el-card__header,
|
|
344
|
-
.m-fields-group-list .m-fields-group-list-item .m-fields-group-list-item > .t-card__header {
|
|
345
|
-
top: calc(var(--m-group-list-header-sticky-top, 0px) + var(--m-group-list-header-height));
|
|
346
|
-
z-index: 6;
|
|
347
|
-
}
|
|
348
378
|
.m-fields-group-list .el-table__empty-block {
|
|
349
379
|
width: 100%;
|
|
350
380
|
min-height: 0;
|
|
@@ -362,7 +392,7 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
362
392
|
}
|
|
363
393
|
.m-fields-group-list > .m-fields-group-list-footer.is-sticky-full {
|
|
364
394
|
position: sticky;
|
|
365
|
-
bottom:
|
|
395
|
+
bottom: var(--m-group-list-nested-footer-bottom, 0px);
|
|
366
396
|
z-index: 7;
|
|
367
397
|
margin-bottom: 0;
|
|
368
398
|
padding: var(--m-group-list-footer-padding-top) 0 var(--m-group-list-footer-padding-bottom);
|
|
@@ -376,10 +406,6 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
376
406
|
width: 100%;
|
|
377
407
|
height: var(--m-group-list-footer-button-height);
|
|
378
408
|
}
|
|
379
|
-
.m-fields-group-list .m-fields-group-list-item .m-fields-group-list > .m-fields-group-list-footer.is-sticky-full {
|
|
380
|
-
bottom: var(--m-group-list-footer-height);
|
|
381
|
-
z-index: 7;
|
|
382
|
-
}
|
|
383
409
|
|
|
384
410
|
/** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
|
|
385
411
|
.m-container-group-list.outer-gorup_list > .m-fields-group-list > .m-fields-group-list-item {
|
|
@@ -2557,6 +2557,36 @@
|
|
|
2557
2557
|
});
|
|
2558
2558
|
};
|
|
2559
2559
|
/**
|
|
2560
|
+
* 存量下发配置沿用 Vue2 时代的写法,把回调第一个参数当组件实例用,靠 `vm.mForm.xxx`
|
|
2561
|
+
* 取表单状态。这类配置除了读,还会往上面挂方法做跨字段通信,例如:
|
|
2562
|
+
*
|
|
2563
|
+
* ```js
|
|
2564
|
+
* vm.mForm.checkPropertyLimit = async (...) => { ... } // 一个字段的 validator 里挂
|
|
2565
|
+
* await vm.mForm.checkPropertyLimit(...) // 另一处再取出来调用
|
|
2566
|
+
* ```
|
|
2567
|
+
*
|
|
2568
|
+
* 所以 `mForm` 必须能指回某个 formState:读能落到 core / context,写能经 `set` trap
|
|
2569
|
+
* 落到 coreState 并持久化。
|
|
2570
|
+
*
|
|
2571
|
+
* 优先级是 core > context > 合成自引用,三者都是刻意的:
|
|
2572
|
+
*
|
|
2573
|
+
* - **core**:`formState.mForm = formState` 这类直写(如 FormPreview 用自建 services 时)
|
|
2574
|
+
* 必须最优先。
|
|
2575
|
+
* - **context**:唯一的生产者是把父 formState 整体当 context 传下来的嵌套表单
|
|
2576
|
+
* (ComponentForm)。此时 `mForm` 命中 context 指向**父表单**,与 `extendState` 时代
|
|
2577
|
+
* 把父 formState 并入子状态的结果一致,跨字段通信仍落在同一份对象上。父 formState
|
|
2578
|
+
* 本身也是 Proxy,写入照样持久化,所以这里让 context 赢是对的,别「修正」成指向子表单。
|
|
2579
|
+
* - **合成自引用**:前两者都没有时才兜底,让 `vm.mForm.xxx = fn` 落到自己的 coreState。
|
|
2580
|
+
*
|
|
2581
|
+
* 由此推出一条约束:**不要往 context 里塞普通对象充当 `mForm`**。context 通常是 computed
|
|
2582
|
+
* 产物,依赖一变就重建,挂上去的方法会静默丢失。要么直写 core,要么什么都不放交给兜底。
|
|
2583
|
+
*
|
|
2584
|
+
* 枚举语义上合成的自引用表现得像原型链上的属性:`'mForm' in state` 为真,但不出现在
|
|
2585
|
+
* `Object.keys(state)` 里,`getOwnPropertyDescriptor` 也返回 undefined。这是故意的——
|
|
2586
|
+
* 按扩展字段打包 formState 的调用方(如发给 AI 的逻辑)会因循环引用炸掉。
|
|
2587
|
+
*/
|
|
2588
|
+
var SELF_REF_KEY = "mForm";
|
|
2589
|
+
/**
|
|
2560
2590
|
* 将 coreState 与宿主业务上下文关联:读取时优先 core,miss 再读穿到 context。
|
|
2561
2591
|
*
|
|
2562
2592
|
* - `get` 用属性访问而非 `Reflect.get(t, k, receiver)`,避免破坏 Vue reactive 的 `__v_raw`;
|
|
@@ -2569,19 +2599,24 @@
|
|
|
2569
2599
|
* 独立成文件,避免与 `form.ts` ↔ `typeMatch.ts` 形成循环依赖。
|
|
2570
2600
|
*/
|
|
2571
2601
|
var createFormStateProxy = (coreState, getContext) => {
|
|
2572
|
-
const resolve = () =>
|
|
2573
|
-
|
|
2602
|
+
const resolve = () => {
|
|
2603
|
+
const ctx = (typeof getContext === "function" ? getContext() : (0, vue.unref)(getContext)) || EMPTY_CONTEXT;
|
|
2604
|
+
return ctx === proxy ? EMPTY_CONTEXT : ctx;
|
|
2605
|
+
};
|
|
2606
|
+
const proxy = new Proxy(coreState, {
|
|
2574
2607
|
get(t, k) {
|
|
2575
2608
|
if (typeof k === "symbol") return Reflect.get(t, k);
|
|
2576
2609
|
const v = t[k];
|
|
2577
2610
|
if (v !== void 0 || Reflect.has(t, k)) return v;
|
|
2578
|
-
|
|
2611
|
+
const ctx = resolve();
|
|
2612
|
+
if (k in ctx) return ctx[k];
|
|
2613
|
+
return k === SELF_REF_KEY ? proxy : void 0;
|
|
2579
2614
|
},
|
|
2580
2615
|
set(t, k, value) {
|
|
2581
2616
|
t[k] = value;
|
|
2582
2617
|
return true;
|
|
2583
2618
|
},
|
|
2584
|
-
has: (t, k) => Reflect.has(t, k) || typeof k !== "symbol" && k in resolve(),
|
|
2619
|
+
has: (t, k) => Reflect.has(t, k) || typeof k !== "symbol" && (k in resolve() || k === SELF_REF_KEY),
|
|
2585
2620
|
ownKeys: (t) => [.../* @__PURE__ */ new Set([...Reflect.ownKeys(t), ...Reflect.ownKeys(resolve()).filter((k) => typeof k !== "symbol")])],
|
|
2586
2621
|
getOwnPropertyDescriptor: (t, k) => {
|
|
2587
2622
|
const own = Reflect.getOwnPropertyDescriptor(t, k);
|
|
@@ -2597,6 +2632,7 @@
|
|
|
2597
2632
|
};
|
|
2598
2633
|
}
|
|
2599
2634
|
});
|
|
2635
|
+
return proxy;
|
|
2600
2636
|
};
|
|
2601
2637
|
//#endregion
|
|
2602
2638
|
//#region packages/form/src/utils/form.ts
|
package/dist/tmagic-form.umd.cjs
CHANGED
|
@@ -3411,6 +3411,36 @@
|
|
|
3411
3411
|
});
|
|
3412
3412
|
};
|
|
3413
3413
|
/**
|
|
3414
|
+
* 存量下发配置沿用 Vue2 时代的写法,把回调第一个参数当组件实例用,靠 `vm.mForm.xxx`
|
|
3415
|
+
* 取表单状态。这类配置除了读,还会往上面挂方法做跨字段通信,例如:
|
|
3416
|
+
*
|
|
3417
|
+
* ```js
|
|
3418
|
+
* vm.mForm.checkPropertyLimit = async (...) => { ... } // 一个字段的 validator 里挂
|
|
3419
|
+
* await vm.mForm.checkPropertyLimit(...) // 另一处再取出来调用
|
|
3420
|
+
* ```
|
|
3421
|
+
*
|
|
3422
|
+
* 所以 `mForm` 必须能指回某个 formState:读能落到 core / context,写能经 `set` trap
|
|
3423
|
+
* 落到 coreState 并持久化。
|
|
3424
|
+
*
|
|
3425
|
+
* 优先级是 core > context > 合成自引用,三者都是刻意的:
|
|
3426
|
+
*
|
|
3427
|
+
* - **core**:`formState.mForm = formState` 这类直写(如 FormPreview 用自建 services 时)
|
|
3428
|
+
* 必须最优先。
|
|
3429
|
+
* - **context**:唯一的生产者是把父 formState 整体当 context 传下来的嵌套表单
|
|
3430
|
+
* (ComponentForm)。此时 `mForm` 命中 context 指向**父表单**,与 `extendState` 时代
|
|
3431
|
+
* 把父 formState 并入子状态的结果一致,跨字段通信仍落在同一份对象上。父 formState
|
|
3432
|
+
* 本身也是 Proxy,写入照样持久化,所以这里让 context 赢是对的,别「修正」成指向子表单。
|
|
3433
|
+
* - **合成自引用**:前两者都没有时才兜底,让 `vm.mForm.xxx = fn` 落到自己的 coreState。
|
|
3434
|
+
*
|
|
3435
|
+
* 由此推出一条约束:**不要往 context 里塞普通对象充当 `mForm`**。context 通常是 computed
|
|
3436
|
+
* 产物,依赖一变就重建,挂上去的方法会静默丢失。要么直写 core,要么什么都不放交给兜底。
|
|
3437
|
+
*
|
|
3438
|
+
* 枚举语义上合成的自引用表现得像原型链上的属性:`'mForm' in state` 为真,但不出现在
|
|
3439
|
+
* `Object.keys(state)` 里,`getOwnPropertyDescriptor` 也返回 undefined。这是故意的——
|
|
3440
|
+
* 按扩展字段打包 formState 的调用方(如发给 AI 的逻辑)会因循环引用炸掉。
|
|
3441
|
+
*/
|
|
3442
|
+
var SELF_REF_KEY = "mForm";
|
|
3443
|
+
/**
|
|
3414
3444
|
* 将 coreState 与宿主业务上下文关联:读取时优先 core,miss 再读穿到 context。
|
|
3415
3445
|
*
|
|
3416
3446
|
* - `get` 用属性访问而非 `Reflect.get(t, k, receiver)`,避免破坏 Vue reactive 的 `__v_raw`;
|
|
@@ -3423,19 +3453,24 @@
|
|
|
3423
3453
|
* 独立成文件,避免与 `form.ts` ↔ `typeMatch.ts` 形成循环依赖。
|
|
3424
3454
|
*/
|
|
3425
3455
|
var createFormStateProxy = (coreState, getContext) => {
|
|
3426
|
-
const resolve = () =>
|
|
3427
|
-
|
|
3456
|
+
const resolve = () => {
|
|
3457
|
+
const ctx = (typeof getContext === "function" ? getContext() : (0, vue.unref)(getContext)) || EMPTY_CONTEXT;
|
|
3458
|
+
return ctx === proxy ? EMPTY_CONTEXT : ctx;
|
|
3459
|
+
};
|
|
3460
|
+
const proxy = new Proxy(coreState, {
|
|
3428
3461
|
get(t, k) {
|
|
3429
3462
|
if (typeof k === "symbol") return Reflect.get(t, k);
|
|
3430
3463
|
const v = t[k];
|
|
3431
3464
|
if (v !== void 0 || Reflect.has(t, k)) return v;
|
|
3432
|
-
|
|
3465
|
+
const ctx = resolve();
|
|
3466
|
+
if (k in ctx) return ctx[k];
|
|
3467
|
+
return k === SELF_REF_KEY ? proxy : void 0;
|
|
3433
3468
|
},
|
|
3434
3469
|
set(t, k, value) {
|
|
3435
3470
|
t[k] = value;
|
|
3436
3471
|
return true;
|
|
3437
3472
|
},
|
|
3438
|
-
has: (t, k) => Reflect.has(t, k) || typeof k !== "symbol" && k in resolve(),
|
|
3473
|
+
has: (t, k) => Reflect.has(t, k) || typeof k !== "symbol" && (k in resolve() || k === SELF_REF_KEY),
|
|
3439
3474
|
ownKeys: (t) => [.../* @__PURE__ */ new Set([...Reflect.ownKeys(t), ...Reflect.ownKeys(resolve()).filter((k) => typeof k !== "symbol")])],
|
|
3440
3475
|
getOwnPropertyDescriptor: (t, k) => {
|
|
3441
3476
|
const own = Reflect.getOwnPropertyDescriptor(t, k);
|
|
@@ -3451,6 +3486,7 @@
|
|
|
3451
3486
|
};
|
|
3452
3487
|
}
|
|
3453
3488
|
});
|
|
3489
|
+
return proxy;
|
|
3454
3490
|
};
|
|
3455
3491
|
//#endregion
|
|
3456
3492
|
//#region packages/form/src/utils/form.ts
|
|
@@ -3759,6 +3795,24 @@
|
|
|
3759
3795
|
*/
|
|
3760
3796
|
/** group-list 形态的 type(兼容驼峰与中划线两种写法) */
|
|
3761
3797
|
var isGroupListType = (type) => type === "groupList" || type === "group-list";
|
|
3798
|
+
/**
|
|
3799
|
+
* 归一成带单位的 CSS 长度。
|
|
3800
|
+
*
|
|
3801
|
+
* 配置常常来自 JSON 或表单输入,`'48'` 这类无单位数字串很常见;直接写进 CSS 变量会让
|
|
3802
|
+
* 嵌套层的 `calc()` 整体失效、吸顶静默失灵,所以纯数字一律补 `px`。
|
|
3803
|
+
*/
|
|
3804
|
+
var toCssLength = (height) => {
|
|
3805
|
+
const value = `${height ?? ""}`.trim();
|
|
3806
|
+
if (!value) return void 0;
|
|
3807
|
+
if ((0, _tmagic_utils.isNumber)(value)) return `${value}px`;
|
|
3808
|
+
return typeof height === "number" ? void 0 : value;
|
|
3809
|
+
};
|
|
3810
|
+
/** header.sticky 开启时把 height 写成 CSS 变量,给嵌套吸顶偏移用 */
|
|
3811
|
+
var getGroupListHeaderVars = (header) => {
|
|
3812
|
+
if (!header?.sticky) return void 0;
|
|
3813
|
+
const height = toCssLength(header.height);
|
|
3814
|
+
return height ? { "--m-group-list-header-height": height } : void 0;
|
|
3815
|
+
};
|
|
3762
3816
|
/** 按 label 文案长度估算 label 宽度(中文按 20px、其他按 8px,最小 80px) */
|
|
3763
3817
|
var calcLabelWidth = (label) => {
|
|
3764
3818
|
if (!label) return "0px";
|
|
@@ -4689,7 +4743,7 @@
|
|
|
4689
4743
|
};
|
|
4690
4744
|
var _hoisted_3$7 = ["innerHTML"];
|
|
4691
4745
|
var _hoisted_4$6 = ["innerHTML"];
|
|
4692
|
-
var _hoisted_5$
|
|
4746
|
+
var _hoisted_5$4 = ["innerHTML"];
|
|
4693
4747
|
var _hoisted_6$3 = ["innerHTML"];
|
|
4694
4748
|
var _hoisted_7$2 = ["innerHTML"];
|
|
4695
4749
|
var _hoisted_8$2 = ["innerHTML"];
|
|
@@ -5044,7 +5098,7 @@
|
|
|
5044
5098
|
key: 0,
|
|
5045
5099
|
placement: tooltip.value.placement
|
|
5046
5100
|
}, {
|
|
5047
|
-
content: (0, vue.withCtx)(() => [(0, vue.createElementVNode)("div", { innerHTML: tooltip.value.text }, null, 8, _hoisted_5$
|
|
5101
|
+
content: (0, vue.withCtx)(() => [(0, vue.createElementVNode)("div", { innerHTML: tooltip.value.text }, null, 8, _hoisted_5$4)]),
|
|
5048
5102
|
default: (0, vue.withCtx)(() => [((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.resolveDynamicComponent)(tagName.value), (0, vue.mergeProps)(fieldsProps.value, {
|
|
5049
5103
|
model: __props.model,
|
|
5050
5104
|
"last-values": __props.lastValues,
|
|
@@ -6442,7 +6496,7 @@
|
|
|
6442
6496
|
var _hoisted_2$7 = ["innerHTML"];
|
|
6443
6497
|
var _hoisted_3$6 = { key: 1 };
|
|
6444
6498
|
var _hoisted_4$5 = ["innerHTML"];
|
|
6445
|
-
var _hoisted_5$
|
|
6499
|
+
var _hoisted_5$3 = ["innerHTML"];
|
|
6446
6500
|
var _hoisted_6$2 = {
|
|
6447
6501
|
key: 2,
|
|
6448
6502
|
style: { "display": "flex" }
|
|
@@ -6533,7 +6587,7 @@
|
|
|
6533
6587
|
key: 0,
|
|
6534
6588
|
innerHTML: __props.config.extra,
|
|
6535
6589
|
class: "m-form-tip"
|
|
6536
|
-
}, null, 8, _hoisted_5$
|
|
6590
|
+
}, null, 8, _hoisted_5$3)) : (0, vue.createCommentVNode)("v-if", true)])), __props.config.schematic && show.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_6$2, [(0, vue.createElementVNode)("div", _hoisted_7$1, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.config.items, (item, index) => {
|
|
6537
6591
|
return (0, vue.openBlock)(), (0, vue.createBlock)(Container_default, {
|
|
6538
6592
|
key: key(item, index),
|
|
6539
6593
|
model: name.value ? __props.model[name.value] : __props.model,
|
|
@@ -6671,7 +6725,7 @@
|
|
|
6671
6725
|
style: { "display": "flex" }
|
|
6672
6726
|
};
|
|
6673
6727
|
var _hoisted_4$4 = { style: { "flex": "1" } };
|
|
6674
|
-
var _hoisted_5$
|
|
6728
|
+
var _hoisted_5$2 = ["src"];
|
|
6675
6729
|
//#endregion
|
|
6676
6730
|
//#region packages/form/src/containers/Panel.vue
|
|
6677
6731
|
var Panel_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
@@ -6764,7 +6818,7 @@
|
|
|
6764
6818
|
}), 128))]), (0, vue.createElementVNode)("img", {
|
|
6765
6819
|
class: "m-form-schematic",
|
|
6766
6820
|
src: __props.config.schematic
|
|
6767
|
-
}, null, 8, _hoisted_5$
|
|
6821
|
+
}, null, 8, _hoisted_5$2)])) : ((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, { key: 1 }, (0, vue.renderList)(items.value, (item, index) => {
|
|
6768
6822
|
return (0, vue.openBlock)(), (0, vue.createBlock)(Container_default, {
|
|
6769
6823
|
key: item[(0, vue.unref)(mForm)?.keyProp || "__key"] ?? index,
|
|
6770
6824
|
config: item,
|
|
@@ -7122,7 +7176,7 @@
|
|
|
7122
7176
|
var _hoisted_2$5 = { class: "m-fields-group-list-item-title" };
|
|
7123
7177
|
var _hoisted_3$4 = ["innerHTML"];
|
|
7124
7178
|
var _hoisted_4$3 = { class: "m-fields-group-list-item-actions" };
|
|
7125
|
-
var _hoisted_5$
|
|
7179
|
+
var _hoisted_5$1 = { style: {
|
|
7126
7180
|
"text-align": "right",
|
|
7127
7181
|
"margin-top": "20px"
|
|
7128
7182
|
} };
|
|
@@ -7287,7 +7341,7 @@
|
|
|
7287
7341
|
disabled: __props.disabled
|
|
7288
7342
|
}, null, 8, ["modelValue", "disabled"]),
|
|
7289
7343
|
_cache[11] || (_cache[11] = (0, vue.createTextVNode)("行 ", -1))
|
|
7290
|
-
]), (0, vue.createElementVNode)("div", _hoisted_5$
|
|
7344
|
+
]), (0, vue.createElementVNode)("div", _hoisted_5$1, [(0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicButton), {
|
|
7291
7345
|
size: "small",
|
|
7292
7346
|
text: "",
|
|
7293
7347
|
onClick: _cache[4] || (_cache[4] = ($event) => moveSpecifyLocationVisible.value = false)
|
|
@@ -7357,14 +7411,13 @@
|
|
|
7357
7411
|
});
|
|
7358
7412
|
//#endregion
|
|
7359
7413
|
//#region packages/form/src/containers/GroupList.vue?vue&type=script&setup=true&lang.ts
|
|
7360
|
-
var _hoisted_1$8 =
|
|
7361
|
-
var _hoisted_2$4 =
|
|
7362
|
-
var _hoisted_3$3 = {
|
|
7414
|
+
var _hoisted_1$8 = ["innerHTML"];
|
|
7415
|
+
var _hoisted_2$4 = {
|
|
7363
7416
|
key: 1,
|
|
7364
7417
|
class: "el-table__empty-block"
|
|
7365
7418
|
};
|
|
7366
|
-
var
|
|
7367
|
-
var
|
|
7419
|
+
var _hoisted_3$3 = { class: "el-table__empty-text t-table__empty" };
|
|
7420
|
+
var _hoisted_4$2 = { style: {
|
|
7368
7421
|
"display": "flex",
|
|
7369
7422
|
"justify-content": "flex-end",
|
|
7370
7423
|
"flex": "1"
|
|
@@ -7411,6 +7464,14 @@
|
|
|
7411
7464
|
};
|
|
7412
7465
|
const onAddDiffCount = () => emit("addDiffCount");
|
|
7413
7466
|
const asList = (value) => Array.isArray(value) ? value : [];
|
|
7467
|
+
const headerVars = (0, vue.computed)(() => getGroupListHeaderVars(props.config.header));
|
|
7468
|
+
/**
|
|
7469
|
+
* 取 `$slots` 而不是 `useSlots()`:宿主切换 `addable` 时插槽会增删,
|
|
7470
|
+
* computed 缓存不随插槽变化失效,只有在渲染期读才拿得到最新的。
|
|
7471
|
+
*/
|
|
7472
|
+
const hasFooter = (slots) => !props.isCompare && Boolean(slots["toggle-button"] || slots["add-button"]);
|
|
7473
|
+
/** 只有真的渲染出吸底 footer,内层列表才需要为它让位 */
|
|
7474
|
+
const isFooterSticky = (slots) => hasFooter(slots) && Boolean(props.config.addButtonConfig?.sticky);
|
|
7414
7475
|
const currentList = (0, vue.computed)(() => asList(props.model[props.name]));
|
|
7415
7476
|
/** 对比时按当前/历史较长一侧对齐,已删除的项也能渲染出来 */
|
|
7416
7477
|
const displayItems = (0, vue.computed)(() => {
|
|
@@ -7428,13 +7489,19 @@
|
|
|
7428
7489
|
}));
|
|
7429
7490
|
});
|
|
7430
7491
|
return (_ctx, _cache) => {
|
|
7431
|
-
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div",
|
|
7492
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
7493
|
+
class: (0, vue.normalizeClass)(["m-fields-group-list", {
|
|
7494
|
+
"is-header-sticky": Boolean(__props.config.header?.sticky),
|
|
7495
|
+
"is-footer-sticky": isFooterSticky(_ctx.$slots)
|
|
7496
|
+
}]),
|
|
7497
|
+
style: (0, vue.normalizeStyle)(headerVars.value)
|
|
7498
|
+
}, [
|
|
7432
7499
|
__props.config.extra ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
7433
7500
|
key: 0,
|
|
7434
7501
|
innerHTML: __props.config.extra,
|
|
7435
7502
|
style: { "color": "rgba(0, 0, 0, 0.45)" }
|
|
7436
|
-
}, null, 8,
|
|
7437
|
-
!displayItems.value.length ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div",
|
|
7503
|
+
}, null, 8, _hoisted_1$8)) : (0, vue.createCommentVNode)("v-if", true),
|
|
7504
|
+
!displayItems.value.length ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_2$4, [(0, vue.createElementVNode)("span", _hoisted_3$3, "暂无" + (0, vue.toDisplayString)(__props.config.titlePrefix || "") + "数据", 1)])) : ((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, { key: 2 }, (0, vue.renderList)(displayItems.value, (entry) => {
|
|
7438
7505
|
return (0, vue.openBlock)(), (0, vue.createBlock)(GroupListItem_default, {
|
|
7439
7506
|
key: entry.index,
|
|
7440
7507
|
model: entry.item,
|
|
@@ -7471,11 +7538,11 @@
|
|
|
7471
7538
|
"group-model"
|
|
7472
7539
|
]);
|
|
7473
7540
|
}), 128)),
|
|
7474
|
-
|
|
7541
|
+
hasFooter(_ctx.$slots) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
7475
7542
|
key: 3,
|
|
7476
7543
|
class: (0, vue.normalizeClass)(["m-fields-group-list-footer", { "is-sticky-full": Boolean(__props.config.addButtonConfig?.sticky) }])
|
|
7477
|
-
}, [(0, vue.renderSlot)(_ctx.$slots, "toggle-button"), (0, vue.createElementVNode)("div",
|
|
7478
|
-
]);
|
|
7544
|
+
}, [(0, vue.renderSlot)(_ctx.$slots, "toggle-button"), (0, vue.createElementVNode)("div", _hoisted_4$2, [(0, vue.renderSlot)(_ctx.$slots, "add-button")])], 2)) : (0, vue.createCommentVNode)("v-if", true)
|
|
7545
|
+
], 6);
|
|
7479
7546
|
};
|
|
7480
7547
|
}
|
|
7481
7548
|
});
|
|
@@ -9882,7 +9949,10 @@
|
|
|
9882
9949
|
prop: props.prop,
|
|
9883
9950
|
postOptions
|
|
9884
9951
|
});
|
|
9885
|
-
let initData
|
|
9952
|
+
let initData;
|
|
9953
|
+
try {
|
|
9954
|
+
initData = (0, _tmagic_utils.getValueByKeyPath)(initRoot || root, res);
|
|
9955
|
+
} catch {}
|
|
9886
9956
|
if (initData) {
|
|
9887
9957
|
if (!Array.isArray(initData)) initData = [initData];
|
|
9888
9958
|
if (typeof option.item === "function") options = option.item(initData);
|
|
@@ -9913,8 +9983,12 @@
|
|
|
9913
9983
|
const v = props.model[props.name];
|
|
9914
9984
|
if (Array.isArray(v) ? !v.length : typeof v === "undefined") return;
|
|
9915
9985
|
if (typeof v === "undefined" || hasOption(v)) return;
|
|
9916
|
-
|
|
9917
|
-
|
|
9986
|
+
try {
|
|
9987
|
+
const data = await getInitOption();
|
|
9988
|
+
setOptions(data);
|
|
9989
|
+
} catch {
|
|
9990
|
+
setOptions([]);
|
|
9991
|
+
}
|
|
9918
9992
|
};
|
|
9919
9993
|
(0, vue.watch)(() => props.model?.[props.name], () => {
|
|
9920
9994
|
initOption();
|
|
@@ -10009,7 +10083,7 @@
|
|
|
10009
10083
|
}), 128)) : ((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, { key: 1 }, (0, vue.renderList)(options.value, (option) => {
|
|
10010
10084
|
return (0, vue.openBlock)(), (0, vue.createBlock)((0, vue.resolveDynamicComponent)((0, vue.unref)(optionComponent)?.component || "el-option"), (0, vue.mergeProps)({
|
|
10011
10085
|
class: "tmagic-design-option",
|
|
10012
|
-
key: __props.config.valueKey ? option.value[__props.config.valueKey] : option.value
|
|
10086
|
+
key: __props.config.valueKey ? option.value?.[__props.config.valueKey] : option.value
|
|
10013
10087
|
}, { ref_for: true }, (0, vue.unref)(optionComponent)?.props({
|
|
10014
10088
|
label: option.text,
|
|
10015
10089
|
value: option.value,
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.8.0-beta.
|
|
2
|
+
"version": "1.8.0-beta.28",
|
|
3
3
|
"name": "@tmagic/form",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": [
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"typescript": "^6.0.3",
|
|
60
60
|
"vue": "^3.5.41",
|
|
61
|
-
"@tmagic/design": "1.8.0-beta.
|
|
62
|
-
"@tmagic/utils": "1.8.0-beta.
|
|
63
|
-
"@tmagic/form-schema": "1.8.0-beta.
|
|
61
|
+
"@tmagic/design": "1.8.0-beta.28",
|
|
62
|
+
"@tmagic/utils": "1.8.0-beta.28",
|
|
63
|
+
"@tmagic/form-schema": "1.8.0-beta.28"
|
|
64
64
|
},
|
|
65
65
|
"peerDependenciesMeta": {
|
|
66
66
|
"typescript": {
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
class="m-fields-group-list"
|
|
4
|
+
:class="{
|
|
5
|
+
'is-header-sticky': Boolean(config.header?.sticky),
|
|
6
|
+
'is-footer-sticky': isFooterSticky($slots),
|
|
7
|
+
}"
|
|
8
|
+
:style="headerVars"
|
|
9
|
+
>
|
|
3
10
|
<div v-if="config.extra" v-html="config.extra" style="color: rgba(0, 0, 0, 0.45)"></div>
|
|
4
11
|
<div v-if="!displayItems.length" class="el-table__empty-block">
|
|
5
12
|
<span class="el-table__empty-text t-table__empty">暂无{{ config.titlePrefix || '' }}数据</span>
|
|
@@ -34,7 +41,7 @@
|
|
|
34
41
|
<div
|
|
35
42
|
class="m-fields-group-list-footer"
|
|
36
43
|
:class="{ 'is-sticky-full': Boolean(config.addButtonConfig?.sticky) }"
|
|
37
|
-
v-if="
|
|
44
|
+
v-if="hasFooter($slots)"
|
|
38
45
|
>
|
|
39
46
|
<slot name="toggle-button"></slot>
|
|
40
47
|
<div style="display: flex; justify-content: flex-end; flex: 1">
|
|
@@ -49,6 +56,7 @@ import { computed } from 'vue';
|
|
|
49
56
|
import { cloneDeep } from 'lodash-es';
|
|
50
57
|
|
|
51
58
|
import type { ContainerChangeEventData, GroupListConfig } from '../schema';
|
|
59
|
+
import { getGroupListHeaderVars } from '../utils/tableGroupList';
|
|
52
60
|
|
|
53
61
|
import MFieldsGroupListItem from './GroupListItem.vue';
|
|
54
62
|
|
|
@@ -104,6 +112,19 @@ const onAddDiffCount = () => emit('addDiffCount');
|
|
|
104
112
|
|
|
105
113
|
const asList = (value: unknown): any[] => (Array.isArray(value) ? value : []);
|
|
106
114
|
|
|
115
|
+
const headerVars = computed(() => getGroupListHeaderVars(props.config.header));
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 取 `$slots` 而不是 `useSlots()`:宿主切换 `addable` 时插槽会增删,
|
|
119
|
+
* computed 缓存不随插槽变化失效,只有在渲染期读才拿得到最新的。
|
|
120
|
+
*/
|
|
121
|
+
const hasFooter = (slots: Record<string, unknown>) =>
|
|
122
|
+
!props.isCompare && Boolean(slots['toggle-button'] || slots['add-button']);
|
|
123
|
+
|
|
124
|
+
/** 只有真的渲染出吸底 footer,内层列表才需要为它让位 */
|
|
125
|
+
const isFooterSticky = (slots: Record<string, unknown>) =>
|
|
126
|
+
hasFooter(slots) && Boolean(props.config.addButtonConfig?.sticky);
|
|
127
|
+
|
|
107
128
|
const currentList = computed(() => asList(props.model[props.name]));
|
|
108
129
|
|
|
109
130
|
/** 对比时按当前/历史较长一侧对齐,已删除的项也能渲染出来 */
|
package/src/fields/Select.vue
CHANGED
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
<component
|
|
58
58
|
v-for="option in options as SelectOption[]"
|
|
59
59
|
class="tmagic-design-option"
|
|
60
|
-
:key="config.valueKey ? option.value[config.valueKey] : option.value"
|
|
60
|
+
:key="config.valueKey ? option.value?.[config.valueKey] : option.value"
|
|
61
61
|
:is="optionComponent?.component || 'el-option'"
|
|
62
62
|
v-bind="
|
|
63
63
|
optionComponent?.props({
|
|
@@ -372,7 +372,12 @@ const getInitOption = async () => {
|
|
|
372
372
|
});
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
let initData
|
|
375
|
+
let initData: any;
|
|
376
|
+
try {
|
|
377
|
+
initData = getValueByKeyPath(initRoot || root, res);
|
|
378
|
+
} catch {
|
|
379
|
+
// 响应中不存在 initRoot/root 路径时按无数据处理
|
|
380
|
+
}
|
|
376
381
|
if (initData) {
|
|
377
382
|
if (!Array.isArray(initData)) {
|
|
378
383
|
initData = [initData];
|
|
@@ -417,8 +422,12 @@ if (typeof props.config.options === 'function') {
|
|
|
417
422
|
const v = props.model[props.name];
|
|
418
423
|
if (Array.isArray(v) ? !v.length : typeof v === 'undefined') return;
|
|
419
424
|
if (typeof v === 'undefined' || hasOption(v)) return;
|
|
420
|
-
|
|
421
|
-
|
|
425
|
+
try {
|
|
426
|
+
const data = await getInitOption();
|
|
427
|
+
setOptions(data);
|
|
428
|
+
} catch {
|
|
429
|
+
setOptions([]);
|
|
430
|
+
}
|
|
422
431
|
};
|
|
423
432
|
|
|
424
433
|
watch(
|
|
@@ -70,27 +70,60 @@
|
|
|
70
70
|
flex-shrink: 0;
|
|
71
71
|
gap: 8px;
|
|
72
72
|
}
|
|
73
|
+
}
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
// 嵌套吸底按钮叠在外层 footer 之上(如「新增条件」叠在「新增条件组」上面)。
|
|
76
|
+
// 每穿过一层「自己渲染了吸底 footer」的列表才多让出一个 footer 高度,外层没有吸底按钮时
|
|
77
|
+
// 内层不能凭空抬高。自定义属性不能自增,所以用 item / list 两个变量交替接力。
|
|
78
|
+
> .m-fields-group-list-item {
|
|
79
|
+
--m-group-list-item-footer-bottom: var(
|
|
80
|
+
--m-group-list-nested-footer-bottom,
|
|
81
|
+
0px
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
&.is-footer-sticky > .m-fields-group-list-item {
|
|
86
|
+
--m-group-list-item-footer-bottom: calc(
|
|
87
|
+
var(--m-group-list-nested-footer-bottom, 0px) +
|
|
88
|
+
var(--m-group-list-footer-height)
|
|
89
|
+
);
|
|
83
90
|
}
|
|
84
91
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
.m-fields-group-list-item .m-fields-group-list {
|
|
93
|
+
--m-group-list-nested-footer-bottom: var(
|
|
94
|
+
--m-group-list-item-footer-bottom,
|
|
95
|
+
0px
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 仅 header.sticky 时标题吸顶;直接子 item,避免外层吸顶带动未开启的内层
|
|
100
|
+
&.is-header-sticky {
|
|
101
|
+
// 从外层 item 接过已累加的偏移;最外层没有该变量时走 fallback
|
|
102
|
+
--m-group-list-nested-sticky-top: var(--m-group-list-child-list-sticky-top);
|
|
103
|
+
--m-group-list-nested-header-z: var(--m-group-list-child-list-header-z);
|
|
104
|
+
|
|
105
|
+
> .m-fields-group-list-item {
|
|
106
|
+
--m-group-list-item-sticky-top: var(
|
|
107
|
+
--m-group-list-nested-sticky-top,
|
|
108
|
+
var(--m-group-list-header-sticky-top, 0px)
|
|
92
109
|
);
|
|
93
|
-
z
|
|
110
|
+
--m-group-list-item-header-z: var(--m-group-list-nested-header-z, 7);
|
|
111
|
+
--m-group-list-child-list-sticky-top: calc(
|
|
112
|
+
var(--m-group-list-item-sticky-top) + var(--m-group-list-header-height)
|
|
113
|
+
);
|
|
114
|
+
--m-group-list-child-list-header-z: calc(
|
|
115
|
+
var(--m-group-list-item-header-z) - 1
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// 卡片标题吸顶;补不透明背景,避免滚动时正文透出。
|
|
119
|
+
// z-index 必须低于吸底 footer,否则标题会盖住「新增」按钮。
|
|
120
|
+
> .el-card__header,
|
|
121
|
+
> .t-card__header {
|
|
122
|
+
position: sticky;
|
|
123
|
+
top: var(--m-group-list-item-sticky-top);
|
|
124
|
+
z-index: var(--m-group-list-item-header-z);
|
|
125
|
+
background-color: var(--m-group-list-header-bg, #fff);
|
|
126
|
+
}
|
|
94
127
|
}
|
|
95
128
|
}
|
|
96
129
|
|
|
@@ -113,10 +146,10 @@
|
|
|
113
146
|
}
|
|
114
147
|
|
|
115
148
|
// 吸底全宽主按钮(event-select / code-select / display-conds 外层列表)
|
|
116
|
-
// z-index 高于 item header
|
|
149
|
+
// z-index 高于 item header,避免吸顶标题挡住新增按钮;多层时 bottom 由内层 list 累加
|
|
117
150
|
> .m-fields-group-list-footer.is-sticky-full {
|
|
118
151
|
position: sticky;
|
|
119
|
-
bottom:
|
|
152
|
+
bottom: var(--m-group-list-nested-footer-bottom, 0px);
|
|
120
153
|
z-index: 7;
|
|
121
154
|
margin-bottom: 0;
|
|
122
155
|
padding: var(--m-group-list-footer-padding-top) 0
|
|
@@ -133,14 +166,6 @@
|
|
|
133
166
|
height: var(--m-group-list-footer-button-height);
|
|
134
167
|
}
|
|
135
168
|
}
|
|
136
|
-
|
|
137
|
-
// 嵌套吸底按钮叠在外层 footer 之上(如「新增条件」叠在「新增条件组」上面)
|
|
138
|
-
.m-fields-group-list-item
|
|
139
|
-
.m-fields-group-list
|
|
140
|
-
> .m-fields-group-list-footer.is-sticky-full {
|
|
141
|
-
bottom: var(--m-group-list-footer-height);
|
|
142
|
-
z-index: 7;
|
|
143
|
-
}
|
|
144
169
|
}
|
|
145
170
|
|
|
146
171
|
/** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
|
|
@@ -56,6 +56,37 @@ export const mergeFormContexts = (...layers: (FormContext | undefined | null)[])
|
|
|
56
56
|
}) as FormContext;
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* 存量下发配置沿用 Vue2 时代的写法,把回调第一个参数当组件实例用,靠 `vm.mForm.xxx`
|
|
61
|
+
* 取表单状态。这类配置除了读,还会往上面挂方法做跨字段通信,例如:
|
|
62
|
+
*
|
|
63
|
+
* ```js
|
|
64
|
+
* vm.mForm.checkPropertyLimit = async (...) => { ... } // 一个字段的 validator 里挂
|
|
65
|
+
* await vm.mForm.checkPropertyLimit(...) // 另一处再取出来调用
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* 所以 `mForm` 必须能指回某个 formState:读能落到 core / context,写能经 `set` trap
|
|
69
|
+
* 落到 coreState 并持久化。
|
|
70
|
+
*
|
|
71
|
+
* 优先级是 core > context > 合成自引用,三者都是刻意的:
|
|
72
|
+
*
|
|
73
|
+
* - **core**:`formState.mForm = formState` 这类直写(如 FormPreview 用自建 services 时)
|
|
74
|
+
* 必须最优先。
|
|
75
|
+
* - **context**:唯一的生产者是把父 formState 整体当 context 传下来的嵌套表单
|
|
76
|
+
* (ComponentForm)。此时 `mForm` 命中 context 指向**父表单**,与 `extendState` 时代
|
|
77
|
+
* 把父 formState 并入子状态的结果一致,跨字段通信仍落在同一份对象上。父 formState
|
|
78
|
+
* 本身也是 Proxy,写入照样持久化,所以这里让 context 赢是对的,别「修正」成指向子表单。
|
|
79
|
+
* - **合成自引用**:前两者都没有时才兜底,让 `vm.mForm.xxx = fn` 落到自己的 coreState。
|
|
80
|
+
*
|
|
81
|
+
* 由此推出一条约束:**不要往 context 里塞普通对象充当 `mForm`**。context 通常是 computed
|
|
82
|
+
* 产物,依赖一变就重建,挂上去的方法会静默丢失。要么直写 core,要么什么都不放交给兜底。
|
|
83
|
+
*
|
|
84
|
+
* 枚举语义上合成的自引用表现得像原型链上的属性:`'mForm' in state` 为真,但不出现在
|
|
85
|
+
* `Object.keys(state)` 里,`getOwnPropertyDescriptor` 也返回 undefined。这是故意的——
|
|
86
|
+
* 按扩展字段打包 formState 的调用方(如发给 AI 的逻辑)会因循环引用炸掉。
|
|
87
|
+
*/
|
|
88
|
+
const SELF_REF_KEY = 'mForm';
|
|
89
|
+
|
|
59
90
|
/**
|
|
60
91
|
* 将 coreState 与宿主业务上下文关联:读取时优先 core,miss 再读穿到 context。
|
|
61
92
|
*
|
|
@@ -72,21 +103,33 @@ export const createFormStateProxy = (
|
|
|
72
103
|
coreState: FormState,
|
|
73
104
|
getContext: (() => FormContext) | Ref<FormContext>,
|
|
74
105
|
): FormState => {
|
|
75
|
-
const resolve = (): Record<string | symbol, any> =>
|
|
76
|
-
(typeof getContext === 'function' ? getContext() : unref(getContext)) || EMPTY_CONTEXT;
|
|
106
|
+
const resolve = (): Record<string | symbol, any> => {
|
|
107
|
+
const ctx = (typeof getContext === 'function' ? getContext() : unref(getContext)) || EMPTY_CONTEXT;
|
|
108
|
+
// 把 formState 自己当 context 传回来(`:context="formState"`)会让 get / has 无限递归,
|
|
109
|
+
// 直接爆栈。这种自引用本就提供不了任何额外字段,断掉即可。
|
|
110
|
+
return ctx === proxy ? EMPTY_CONTEXT : ctx;
|
|
111
|
+
};
|
|
77
112
|
|
|
78
|
-
|
|
113
|
+
const proxy = new Proxy(coreState as object, {
|
|
79
114
|
get(t, k) {
|
|
80
115
|
if (typeof k === 'symbol') return Reflect.get(t, k);
|
|
81
116
|
const v = (t as any)[k];
|
|
82
117
|
if (v !== undefined || Reflect.has(t, k)) return v;
|
|
83
|
-
|
|
118
|
+
|
|
119
|
+
const ctx = resolve();
|
|
120
|
+
if (k in ctx) return ctx[k];
|
|
121
|
+
// 自引用不进 ownKeys:枚举 formState 的调用方(如按扩展字段打包发给 AI 的逻辑)
|
|
122
|
+
// 会因为循环引用炸掉,这里只在显式读取时才合成
|
|
123
|
+
return k === SELF_REF_KEY ? proxy : undefined;
|
|
84
124
|
},
|
|
85
125
|
set(t, k, value) {
|
|
86
126
|
(t as any)[k] = value;
|
|
87
127
|
return true;
|
|
88
128
|
},
|
|
89
|
-
|
|
129
|
+
// `mForm` 在这里为真,但不进 ownKeys、getOwnPropertyDescriptor 也返回 undefined
|
|
130
|
+
// (见 SELF_REF_KEY 注释)。因此展开 / Object.entries 不会循环引用,
|
|
131
|
+
// 但对 proxy 直接做递归遍历(`for...in`、深拷贝、直接 JSON.stringify(formState))仍会。
|
|
132
|
+
has: (t, k) => Reflect.has(t, k) || (typeof k !== 'symbol' && (k in resolve() || k === SELF_REF_KEY)),
|
|
90
133
|
ownKeys: (t) => [
|
|
91
134
|
...new Set([...Reflect.ownKeys(t), ...Reflect.ownKeys(resolve()).filter((k) => typeof k !== 'symbol')]),
|
|
92
135
|
],
|
|
@@ -101,4 +144,6 @@ export const createFormStateProxy = (
|
|
|
101
144
|
return { configurable: true, enumerable: true, writable: true, value: ctx[k] };
|
|
102
145
|
},
|
|
103
146
|
}) as FormState;
|
|
147
|
+
|
|
148
|
+
return proxy;
|
|
104
149
|
};
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
import { cloneDeep } from 'lodash-es';
|
|
20
20
|
|
|
21
|
-
import
|
|
21
|
+
import { isNumber } from '@tmagic/utils';
|
|
22
|
+
|
|
23
|
+
import type { GroupListConfig, GroupListHeaderConfig, TableColumnConfig, TableConfig } from '../schema';
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* table / group-list 是同一份配置的两种展示形态,可以互相切换(`TableGroupList.vue`)。
|
|
@@ -31,6 +33,31 @@ import type { GroupListConfig, TableColumnConfig, TableConfig } from '../schema'
|
|
|
31
33
|
/** group-list 形态的 type(兼容驼峰与中划线两种写法) */
|
|
32
34
|
export const isGroupListType = (type: unknown): boolean => type === 'groupList' || type === 'group-list';
|
|
33
35
|
|
|
36
|
+
/**
|
|
37
|
+
* 归一成带单位的 CSS 长度。
|
|
38
|
+
*
|
|
39
|
+
* 配置常常来自 JSON 或表单输入,`'48'` 这类无单位数字串很常见;直接写进 CSS 变量会让
|
|
40
|
+
* 嵌套层的 `calc()` 整体失效、吸顶静默失灵,所以纯数字一律补 `px`。
|
|
41
|
+
*/
|
|
42
|
+
const toCssLength = (height: number | string | undefined): string | undefined => {
|
|
43
|
+
const value = `${height ?? ''}`.trim();
|
|
44
|
+
if (!value) return undefined;
|
|
45
|
+
|
|
46
|
+
if (isNumber(value)) return `${value}px`;
|
|
47
|
+
// 走到这里还是 number 的只剩 NaN / Infinity,不写变量,交给样式默认值兜底
|
|
48
|
+
return typeof height === 'number' ? undefined : value;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** header.sticky 开启时把 height 写成 CSS 变量,给嵌套吸顶偏移用 */
|
|
52
|
+
export const getGroupListHeaderVars = (
|
|
53
|
+
header: GroupListHeaderConfig | undefined,
|
|
54
|
+
): Record<string, string> | undefined => {
|
|
55
|
+
if (!header?.sticky) return undefined;
|
|
56
|
+
|
|
57
|
+
const height = toCssLength(header.height);
|
|
58
|
+
return height ? { '--m-group-list-header-height': height } : undefined;
|
|
59
|
+
};
|
|
60
|
+
|
|
34
61
|
/** 按 label 文案长度估算 label 宽度(中文按 20px、其他按 8px,最小 80px) */
|
|
35
62
|
export const calcLabelWidth = (label: string): string => {
|
|
36
63
|
if (!label) return '0px';
|