@tmagic/form 1.8.0-beta.25 → 1.8.0-beta.26

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 (130) hide show
  1. package/dist/es/Form.vue_vue_type_script_setup_true_lang.js +47 -120
  2. package/dist/es/FormBox.vue_vue_type_script_setup_true_lang.js +3 -3
  3. package/dist/es/FormDialog.vue_vue_type_script_setup_true_lang.js +3 -3
  4. package/dist/es/FormDrawer.vue_vue_type_script_setup_true_lang.js +3 -3
  5. package/dist/es/containers/Col.vue_vue_type_script_setup_true_lang.js +3 -0
  6. package/dist/es/containers/Container.vue_vue_type_script_setup_true_lang.js +36 -51
  7. package/dist/es/containers/Fieldset.vue_vue_type_script_setup_true_lang.js +6 -0
  8. package/dist/es/containers/FlexLayout.vue_vue_type_script_setup_true_lang.js +4 -1
  9. package/dist/es/containers/GroupList.vue_vue_type_script_setup_true_lang.js +37 -15
  10. package/dist/es/containers/GroupListItem.vue_vue_type_script_setup_true_lang.js +109 -97
  11. package/dist/es/containers/Panel.vue_vue_type_script_setup_true_lang.js +7 -2
  12. package/dist/es/containers/Row.vue_vue_type_script_setup_true_lang.js +3 -0
  13. package/dist/es/containers/Step.vue_vue_type_script_setup_true_lang.js +4 -1
  14. package/dist/es/containers/Tabs.vue_vue_type_script_setup_true_lang.js +7 -0
  15. package/dist/es/containers/table/Table.vue_vue_type_script_setup_true_lang.js +7 -5
  16. package/dist/es/containers/table/useTableColumns.js +5 -13
  17. package/dist/es/containers/table-group-list/TableGroupList.vue_vue_type_script_setup_true_lang.js +71 -78
  18. package/dist/es/containers/table-group-list/useAdd.js +26 -16
  19. package/dist/es/containers/table-group-list/useScrollLastItemIntoView.js +72 -0
  20. package/dist/es/fields/Cascader.vue_vue_type_script_setup_true_lang.js +1 -1
  21. package/dist/es/fields/CheckboxGroup/Index.js +5 -0
  22. package/dist/es/fields/{CheckboxGroup.vue_vue_type_script_setup_true_lang.js → CheckboxGroup/Index.vue_vue_type_script_setup_true_lang.js} +6 -7
  23. package/dist/es/fields/CheckboxGroup/effect.js +8 -0
  24. package/dist/es/fields/Date/Index.js +5 -0
  25. package/dist/es/fields/{Date.vue_vue_type_script_setup_true_lang.js → Date/Index.vue_vue_type_script_setup_true_lang.js} +5 -7
  26. package/dist/es/fields/Date/effect.js +10 -0
  27. package/dist/es/fields/DateTime/Index.js +5 -0
  28. package/dist/es/fields/{DateTime.vue_vue_type_script_setup_true_lang.js → DateTime/Index.vue_vue_type_script_setup_true_lang.js} +5 -9
  29. package/dist/es/fields/DateTime/effect.js +15 -0
  30. package/dist/es/fields/Display/Index.js +5 -0
  31. package/dist/es/fields/{Display.vue_vue_type_script_setup_true_lang.js → Display/Index.vue_vue_type_script_setup_true_lang.js} +6 -7
  32. package/dist/es/fields/Display/effect.js +8 -0
  33. package/dist/es/fields/DynamicField/Index.js +5 -0
  34. package/dist/es/fields/{DynamicField.vue_vue_type_script_setup_true_lang.js → DynamicField/Index.vue_vue_type_script_setup_true_lang.js} +11 -15
  35. package/dist/es/fields/DynamicField/effect.js +40 -0
  36. package/dist/es/fields/Link.vue_vue_type_script_setup_true_lang.js +1 -1
  37. package/dist/es/fields/Number.vue_vue_type_script_setup_true_lang.js +1 -1
  38. package/dist/es/fields/NumberRange/Index.js +5 -0
  39. package/dist/es/fields/{NumberRange.vue_vue_type_script_setup_true_lang.js → NumberRange/Index.vue_vue_type_script_setup_true_lang.js} +5 -6
  40. package/dist/es/fields/NumberRange/effect.js +8 -0
  41. package/dist/es/fields/Select.vue_vue_type_script_setup_true_lang.js +2 -2
  42. package/dist/es/fields/Text.vue_vue_type_script_setup_true_lang.js +1 -1
  43. package/dist/es/headless.js +13 -0
  44. package/dist/es/index.js +17 -12
  45. package/dist/es/plugin.js +57 -41
  46. package/dist/es/schema.js +6 -8
  47. package/dist/es/style.css +81 -1
  48. package/dist/es/submitForm.js +44 -194
  49. package/dist/es/utils/builtInFields.js +49 -0
  50. package/dist/es/utils/collectFields.js +407 -0
  51. package/dist/es/utils/config.js +1 -8
  52. package/dist/es/utils/fieldInnerConfig.js +42 -0
  53. package/dist/es/utils/fieldValueEffects.js +80 -0
  54. package/dist/es/utils/form.js +96 -43
  55. package/dist/es/utils/formStateProxy.js +75 -0
  56. package/dist/es/utils/registerField.js +158 -0
  57. package/dist/es/utils/submitHeadless.js +54 -0
  58. package/dist/es/utils/tableGroupList.js +85 -0
  59. package/dist/es/utils/typeMatch.js +17 -19
  60. package/dist/es/utils/validateError.js +57 -0
  61. package/dist/es/utils/validateValues.js +115 -0
  62. package/dist/style.css +81 -1
  63. package/dist/themes/magic-admin.css +83 -7
  64. package/dist/tmagic-form-headless.umd.cjs +4044 -0
  65. package/dist/tmagic-form.umd.cjs +1975 -874
  66. package/package.json +10 -4
  67. package/src/Form.vue +49 -149
  68. package/src/FormBox.vue +5 -4
  69. package/src/FormDialog.vue +5 -5
  70. package/src/FormDrawer.vue +5 -5
  71. package/src/containers/Col.vue +2 -0
  72. package/src/containers/Container.vue +86 -157
  73. package/src/containers/Fieldset.vue +5 -0
  74. package/src/containers/FlexLayout.vue +2 -0
  75. package/src/containers/GroupList.vue +41 -11
  76. package/src/containers/GroupListItem.vue +83 -80
  77. package/src/containers/Panel.vue +3 -0
  78. package/src/containers/Row.vue +2 -0
  79. package/src/containers/Step.vue +2 -0
  80. package/src/containers/Tabs.vue +13 -0
  81. package/src/containers/table/ActionsColumn.vue +1 -1
  82. package/src/containers/table/Table.vue +4 -3
  83. package/src/containers/table/usePagination.ts +1 -1
  84. package/src/containers/table/useSortable.ts +1 -1
  85. package/src/containers/table/useTableColumns.ts +9 -19
  86. package/src/containers/table-group-list/TableGroupList.vue +43 -71
  87. package/src/containers/table-group-list/useAdd.ts +40 -25
  88. package/src/containers/table-group-list/useScrollLastItemIntoView.ts +94 -0
  89. package/src/fields/{CheckboxGroup.vue → CheckboxGroup/Index.vue} +3 -8
  90. package/src/fields/CheckboxGroup/effect.ts +27 -0
  91. package/src/fields/{Date.vue → Date/Index.vue} +2 -5
  92. package/src/fields/Date/effect.ts +29 -0
  93. package/src/fields/{DateTime.vue → DateTime/Index.vue} +2 -16
  94. package/src/fields/DateTime/effect.ts +36 -0
  95. package/src/fields/{Display.vue → Display/Index.vue} +3 -7
  96. package/src/fields/Display/effect.ts +28 -0
  97. package/src/fields/{DynamicField.vue → DynamicField/Index.vue} +11 -11
  98. package/src/fields/DynamicField/effect.ts +74 -0
  99. package/src/fields/{NumberRange.vue → NumberRange/Index.vue} +2 -6
  100. package/src/fields/NumberRange/effect.ts +27 -0
  101. package/src/headless.ts +78 -0
  102. package/src/index.ts +32 -15
  103. package/src/plugin.ts +63 -42
  104. package/src/schema.ts +6 -8
  105. package/src/submitForm.ts +81 -409
  106. package/src/theme/group-list.scss +107 -1
  107. package/src/theme/themes/magic-admin/index.scss +2 -8
  108. package/src/utils/builtInFields.ts +69 -0
  109. package/src/utils/collectFields.ts +561 -0
  110. package/src/utils/config.ts +1 -15
  111. package/src/utils/fieldInnerConfig.ts +127 -0
  112. package/src/utils/fieldValueEffects.ts +140 -0
  113. package/src/utils/form.ts +141 -75
  114. package/src/utils/formStateProxy.ts +104 -0
  115. package/src/utils/registerField.ts +308 -0
  116. package/src/utils/submitHeadless.ts +206 -0
  117. package/src/utils/tableGroupList.ts +120 -0
  118. package/src/utils/typeMatch.ts +19 -15
  119. package/src/utils/validateError.ts +92 -0
  120. package/src/utils/validateValues.ts +177 -0
  121. package/types/headless.d.ts +682 -0
  122. package/types/index.d.ts +631 -272
  123. package/dist/es/fields/CheckboxGroup.js +0 -5
  124. package/dist/es/fields/Date.js +0 -5
  125. package/dist/es/fields/DateTime.js +0 -5
  126. package/dist/es/fields/Display.js +0 -5
  127. package/dist/es/fields/DynamicField.js +0 -5
  128. package/dist/es/fields/NumberRange.js +0 -5
  129. package/dist/es/utils/silentLeafFieldTypes.js +0 -49
  130. package/src/utils/silentLeafFieldTypes.ts +0 -71
@@ -1,10 +1,12 @@
1
+ import { getConfig } from "./config.js";
1
2
  import { createTypeMatchValidator } from "./typeMatch.js";
2
- import { readonly } from "vue";
3
- import dayjs from "dayjs";
4
- import utc from "dayjs/plugin/utc";
3
+ import { createFormStateProxy, mergeFormContexts } from "./formStateProxy.js";
4
+ import { readonly, unref } from "vue";
5
5
  import { cloneDeep } from "lodash-es";
6
- import { getDesignConfig } from "@tmagic/design";
7
6
  import { getValueByKeyPath } from "@tmagic/utils";
7
+ import dayjs from "dayjs";
8
+ import utc from "dayjs/plugin/utc.js";
9
+ import { getDesignConfig } from "@tmagic/design/headless";
8
10
  //#region packages/form/src/utils/form.ts
9
11
  var isTDesignAdapter = () => getDesignConfig("adapterType") === "tdesign-vue-next";
10
12
  /**
@@ -116,6 +118,79 @@ var getDefaultValue = function(mForm, { defaultValue, type, filter, multiple, na
116
118
  if (type === "daterange" && !names) return [];
117
119
  return "";
118
120
  };
121
+ /**
122
+ * formState 中与表单 props 无关的公共部分:字段注册表、消息组件、post 请求。
123
+ *
124
+ * 由渲染式表单(`Form.vue`)与无渲染校验(`createHeadlessFormState`)共用,
125
+ * 保证两者提供给字段配置(`onChange` / `validator` / 异步 options 等)的能力一致。
126
+ */
127
+ var fallbackMessage = {
128
+ error: (msg) => console.error(msg),
129
+ success: (msg) => console.log(msg),
130
+ warning: (msg) => console.warn(msg),
131
+ info: (msg) => console.info(msg),
132
+ closeAll: () => void 0
133
+ };
134
+ var fallbackMessageBox = {
135
+ alert: (msg) => console.log(msg),
136
+ confirm: (msg) => console.log(msg),
137
+ close: (msg) => console.log(msg)
138
+ };
139
+ var createFormStateBase = (ui) => {
140
+ const fields = /* @__PURE__ */ new Map();
141
+ const requestFuc = getConfig("request");
142
+ return {
143
+ fields,
144
+ setField: (prop, field) => fields.set(prop, field),
145
+ getField: (prop) => fields.get(prop),
146
+ deleteField: (prop) => fields.delete(prop),
147
+ $messageBox: ui?.$messageBox ?? fallbackMessageBox,
148
+ $message: ui?.$message ?? fallbackMessage,
149
+ post: (options) => {
150
+ if (requestFuc) return requestFuc({
151
+ method: "POST",
152
+ ...options
153
+ });
154
+ }
155
+ };
156
+ };
157
+ /**
158
+ * 配置项的 `name` 是否可用于从 model 中下钻取值。
159
+ *
160
+ * 供 `Container.vue` 与无渲染校验(`collectValidatableFields`)共用,两者对「该不该下钻」
161
+ * 的判断必须一致。
162
+ */
163
+ var isValidName = (name) => {
164
+ const valueType = typeof name;
165
+ if (valueType !== "string" && valueType !== "symbol" && valueType !== "number") return false;
166
+ if (name === "") return false;
167
+ if (valueType === "number") return name >= 0;
168
+ return true;
169
+ };
170
+ /**
171
+ * 在 `prop` 后追加一段路径。
172
+ *
173
+ * 用于容器把 `prop` 透传给子级时再拼上索引 / key(如 group-list 的行、table 的行)。
174
+ */
175
+ var appendProp = (prop = "", key) => `${prop}${prop ? "." : ""}${key}`;
176
+ /**
177
+ * 由父级 `prop` 与配置项 `name` 拼出该配置项的完整 `prop`(即 FormItem 的 prop)。
178
+ *
179
+ * 调用方传入的 name 已按 `config.name || ''` 归一化,空 name 表示该配置项不占据值路径上的一层,
180
+ * 此时沿用父级 `prop`。
181
+ */
182
+ var getItemProp = (prop = "", name = "") => name === "" ? prop ?? "" : appendProp(prop, name);
183
+ /**
184
+ * 归一化配置项的 `type`:求值动态 type、剥离容器语义的 type、驼峰转中划线、补默认值。
185
+ *
186
+ * 供 `Container.vue` 与无渲染校验共用,保证两者分派到的 type 完全一致。
187
+ */
188
+ var resolveItemType = (mForm, config, props) => {
189
+ let type = "type" in (config || {}) ? config.type : "";
190
+ type = type && filterFunction(mForm, type, props);
191
+ if (type === "form" || type === "container") return "";
192
+ return type?.replace(/([A-Z])/g, "-$1").toLowerCase() || (config?.items ? "" : "text");
193
+ };
119
194
  var filterFunction = (mForm, config, props) => {
120
195
  if (typeof config === "function") return config(mForm, {
121
196
  values: readonly(mForm?.initValues || {}),
@@ -135,18 +210,18 @@ var display = function(mForm, config, props) {
135
210
  if (config === false) return false;
136
211
  return true;
137
212
  };
138
- var getRules = function(mForm, r = [], props, typeMatchValid) {
213
+ var buildRules = function(mForm, r = [], props, typeMatchValid, adapt = (validator) => validator) {
139
214
  let rules = cloneDeep(r);
140
215
  if (typeof rules === "object" && !Array.isArray(rules)) rules = [rules];
141
- if (typeMatchValid?.value && !rules.some((r) => typeof r.typeMatch !== "undefined")) rules.push({ typeMatch: true });
216
+ if (unref(typeMatchValid) && !rules.some((r) => typeof r.typeMatch !== "undefined")) rules.push({ typeMatch: true });
142
217
  return rules.map((item) => {
143
218
  if (item.typeMatch) {
144
- item.validator = adaptFormValidator(createTypeMatchValidator(mForm, props, item));
219
+ item.validator = adapt(createTypeMatchValidator(mForm, props, item));
145
220
  return item;
146
221
  }
147
222
  if (typeof item.validator === "function") {
148
223
  const fnc = item.validator;
149
- item.validator = adaptFormValidator((rule, value, callback, source, options) => fnc({
224
+ item.validator = adapt((rule, value, callback, source, options) => fnc({
150
225
  rule,
151
226
  value: props.config.names ? props.model : value,
152
227
  callback,
@@ -167,6 +242,18 @@ var getRules = function(mForm, r = [], props, typeMatchValid) {
167
242
  return true;
168
243
  });
169
244
  };
245
+ var getRules = function(mForm, r = [], props, typeMatchValid) {
246
+ return buildRules(mForm, r, props, typeMatchValid, adaptFormValidator);
247
+ };
248
+ /**
249
+ * 与 `getRules` 相同,但 validator 保持 async-validator(Element Plus)原生签名,不做 UI 库适配。
250
+ *
251
+ * 供无渲染校验(`validateValues`)使用:它直接把规则交给 async-validator 执行,
252
+ * 不经过 TDesign 的 `validator(val)` 调用约定,因此不能套 `adaptFormValidator`。
253
+ */
254
+ var getNativeRules = function(mForm, r = [], props, typeMatchValid) {
255
+ return buildRules(mForm, r, props, typeMatchValid);
256
+ };
170
257
  var initValue = async (mForm, { initValues, config }) => {
171
258
  if (!Array.isArray(config)) throw new Error("config应该为数组");
172
259
  const initValuesCopy = cloneDeep(initValues);
@@ -207,40 +294,6 @@ var sortChange = (data, { prop, order }) => {
207
294
  if (order === "ascending") data.sort((a, b) => a[prop] - b[prop]);
208
295
  else if (order === "descending") data.sort((a, b) => b[prop] - a[prop]);
209
296
  };
210
- /**
211
- * 将 extendState 返回的扩展字段合并进 formState。
212
- *
213
- * - data 描述符(普通字段)通过 `formState[key] = value` 写入,走 reactive proxy 的 set,
214
- * 触发依赖通知;
215
- * - accessor 描述符(`{ get stage() { return ... } }`)按原样 defineProperty,调用方
216
- * 可控制读时求值;强制 `configurable: true` 以便下一次合并可再 define。
217
- *
218
- * 注意:extendState 只能向 formState「新增」字段,不允许覆盖其已有 key。
219
- * 调用方可通过 `reservedKeys` 传入合并前已存在的内置 key 快照(keyProp / popperClass /
220
- * config / initValues / isCompare / lastValues / parentValues / values / $emit / fields /
221
- * post 等),命中这些 key 时统一跳过并告警。
222
- *
223
- * 兜底:未传 `reservedKeys` 时,仍会拦截 props 派生的只读 getter 字段(无 setter),
224
- * 否则以普通字段形式赋值会让 proxy 的 set trap 抛出
225
- * `TypeError: 'set' on proxy: trap returned falsish`。
226
- */
227
- var applyExtendState = (formState, state, reservedKeys) => {
228
- if (!state) return;
229
- for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(state))) {
230
- if (reservedKeys?.has(key)) continue;
231
- if (!("value" in descriptor)) {
232
- descriptor.configurable = true;
233
- Object.defineProperty(formState, key, descriptor);
234
- continue;
235
- }
236
- const targetDescriptor = Object.getOwnPropertyDescriptor(formState, key);
237
- if (targetDescriptor && !("value" in targetDescriptor) && typeof targetDescriptor.set !== "function") {
238
- console.warn(`[MForm] extendState: "${key}" is a read-only field derived from props and cannot be assigned a plain value. Return it as a getter accessor if you really need to override it.`);
239
- continue;
240
- }
241
- formState[key] = state[key];
242
- }
243
- };
244
297
  var createObjectProp = (prop, key, name) => {
245
298
  if (prop === "") return key;
246
299
  const itemPath = `${prop}`.split(".");
@@ -250,4 +303,4 @@ var createObjectProp = (prop, key, name) => {
250
303
  return `${[...itemPath, key].join(".")}`;
251
304
  };
252
305
  //#endregion
253
- export { adaptFormValidator, applyExtendState, createObjectProp, createValues, datetimeFormatter, display, filterFunction, getDataByPage, getRules, initValue, sortArray, sortChange };
306
+ export { adaptFormValidator, appendProp, createFormStateBase, createFormStateProxy, createObjectProp, createValues, datetimeFormatter, display, filterFunction, getDataByPage, getItemProp, getNativeRules, getRules, initValue, isValidName, mergeFormContexts, resolveItemType, sortArray, sortChange };
@@ -0,0 +1,75 @@
1
+ import { unref } from "vue";
2
+ //#region packages/form/src/utils/formStateProxy.ts
3
+ /** 无上下文时共用的空对象,避免每次读取都分配新对象 */
4
+ var EMPTY_CONTEXT = Object.freeze({});
5
+ /**
6
+ * 按优先级分层合并上下文,**靠后的层优先**。
7
+ *
8
+ * 用读穿 Proxy 而非 `{ ...a, ...b }`:展开会立即执行 accessor,
9
+ * 而宿主允许用 `{ get stage() { ... } }` 这类读时求值的描述符。
10
+ */
11
+ var mergeFormContexts = (...layers) => {
12
+ const stack = layers.filter((layer) => Boolean(layer));
13
+ if (stack.length === 0) return EMPTY_CONTEXT;
14
+ if (stack.length === 1) return stack[0];
15
+ const ordered = stack.slice().reverse();
16
+ const owner = (k) => ordered.find((layer) => Reflect.has(layer, k));
17
+ return new Proxy({}, {
18
+ get: (_t, k) => owner(k)?.[k],
19
+ has: (_t, k) => Boolean(owner(k)),
20
+ ownKeys: () => [...new Set(ordered.flatMap((layer) => Reflect.ownKeys(layer)))],
21
+ getOwnPropertyDescriptor: (_t, k) => {
22
+ for (const layer of ordered) {
23
+ const descriptor = Object.getOwnPropertyDescriptor(layer, k);
24
+ if (descriptor) return {
25
+ ...descriptor,
26
+ configurable: true
27
+ };
28
+ }
29
+ }
30
+ });
31
+ };
32
+ /**
33
+ * 将 coreState 与宿主业务上下文关联:读取时优先 core,miss 再读穿到 context。
34
+ *
35
+ * - `get` 用属性访问而非 `Reflect.get(t, k, receiver)`,避免破坏 Vue reactive 的 `__v_raw`;
36
+ * - symbol 键一律只走 core,四个 trap 保持一致。context 是业务数据袋,不承载 symbol 键,
37
+ * 把 Vue / 工具链的内部 symbol 隔离在 core 上才能让 `toRaw` / `isReactive` 判定正确;
38
+ * - `ownKeys` + `getOwnPropertyDescriptor` 保证 `Object.entries(formState)` 能枚举到扩展字段
39
+ * (admin-web-next 的 `pickPanelFormStateExtendFields` 依赖此语义);
40
+ * - `set` 写入 coreState,第三方 `formState.xxx = v` 仍生效且优先于 context。
41
+ *
42
+ * 独立成文件,避免与 `form.ts` ↔ `typeMatch.ts` 形成循环依赖。
43
+ */
44
+ var createFormStateProxy = (coreState, getContext) => {
45
+ const resolve = () => (typeof getContext === "function" ? getContext() : unref(getContext)) || EMPTY_CONTEXT;
46
+ return new Proxy(coreState, {
47
+ get(t, k) {
48
+ if (typeof k === "symbol") return Reflect.get(t, k);
49
+ const v = t[k];
50
+ if (v !== void 0 || Reflect.has(t, k)) return v;
51
+ return resolve()[k];
52
+ },
53
+ set(t, k, value) {
54
+ t[k] = value;
55
+ return true;
56
+ },
57
+ has: (t, k) => Reflect.has(t, k) || typeof k !== "symbol" && k in resolve(),
58
+ ownKeys: (t) => [.../* @__PURE__ */ new Set([...Reflect.ownKeys(t), ...Reflect.ownKeys(resolve()).filter((k) => typeof k !== "symbol")])],
59
+ getOwnPropertyDescriptor: (t, k) => {
60
+ const own = Reflect.getOwnPropertyDescriptor(t, k);
61
+ if (own) return own;
62
+ if (typeof k === "symbol") return void 0;
63
+ const ctx = resolve();
64
+ if (!(k in ctx)) return void 0;
65
+ return {
66
+ configurable: true,
67
+ enumerable: true,
68
+ writable: true,
69
+ value: ctx[k]
70
+ };
71
+ }
72
+ });
73
+ };
74
+ //#endregion
75
+ export { createFormStateProxy, mergeFormContexts };
@@ -0,0 +1,158 @@
1
+ import { clearFieldInnerConfigs, deleteFieldInnerConfig, registerFieldInnerConfig } from "./fieldInnerConfig.js";
2
+ import { clearLeafFieldTypes, deleteLeafFieldType, registerLeafFieldType } from "./fieldValueEffects.js";
3
+ import { clearTypeMatchRules, deleteTypeMatchRule, registerTypeMatchRule } from "./typeMatch.js";
4
+ import { clearContainerWalkers, deleteContainerWalker, registerContainerWalker } from "./collectFields.js";
5
+ import { toLine } from "@tmagic/utils";
6
+ //#region packages/form/src/utils/registerField.ts
7
+ var extraComponents = /* @__PURE__ */ new Map();
8
+ var builtInComponents = /* @__PURE__ */ new Map();
9
+ var applyVueComponent = (type, component, app, builtIn, kind) => {
10
+ const key = toLine(type);
11
+ if (builtIn) builtInComponents.set(key, component);
12
+ else extraComponents.set(key, component);
13
+ app?.component(`m-${kind}-${key}`, component);
14
+ };
15
+ /**
16
+ * 把多份字段登记表按 type 浅合并。后一份只覆盖自己带了的 key,未出现的 key 保留前一份。
17
+ *
18
+ * 安装插件时用:Node 侧只登记 `headless`,浏览器再补 `component` / `container`。
19
+ *
20
+ * @param tables - 待合并的登记表,`undefined` 会被跳过
21
+ * @returns 合并后的登记表
22
+ */
23
+ var mergeFieldOptions = (...tables) => {
24
+ const result = {};
25
+ for (const table of tables) {
26
+ if (!table) continue;
27
+ for (const [type, options] of Object.entries(table)) result[type] = {
28
+ ...result[type],
29
+ ...pickDefinedFieldOptions(options)
30
+ };
31
+ }
32
+ return result;
33
+ };
34
+ var FIELD_OPTION_KEYS = [
35
+ "component",
36
+ "container",
37
+ "effect",
38
+ "walk",
39
+ "innerConfig",
40
+ "typeMatch"
41
+ ];
42
+ var pickDefinedFieldOptions = (options) => {
43
+ if (!options) return {};
44
+ const next = {};
45
+ for (const key of FIELD_OPTION_KEYS) if (options[key] !== void 0) next[key] = options[key];
46
+ return next;
47
+ };
48
+ var extraFieldOptions = /* @__PURE__ */ new Map();
49
+ var builtInFieldOptions = /* @__PURE__ */ new Map();
50
+ var removeFormComponent = (type) => {
51
+ extraComponents.delete(toLine(type));
52
+ };
53
+ var clearFormComponents = () => {
54
+ extraComponents.clear();
55
+ };
56
+ /**
57
+ * 按字段 type 取已登记的渲染组件(`codeSelect` 与 `code-select` 等价)。
58
+ *
59
+ * @param type - 字段 type
60
+ * @returns 已登记的 Vue 组件;未登记则为 `undefined`
61
+ */
62
+ var getField = (type) => {
63
+ const key = toLine(type);
64
+ return extraComponents.get(key) ?? builtInComponents.get(key);
65
+ };
66
+ var registerFieldImpl = (type, options, app, builtIn) => {
67
+ if (typeof type !== "string" || !type) return;
68
+ const key = toLine(type);
69
+ const store = builtIn ? builtInFieldOptions : extraFieldOptions;
70
+ const incoming = pickDefinedFieldOptions(options);
71
+ const merged = {
72
+ ...store.get(key),
73
+ ...incoming
74
+ };
75
+ store.set(key, merged);
76
+ if (incoming.walk && (incoming.innerConfig || typeof incoming.effect === "function")) console.warn(`[MForm] registerField("${key}"): walk is set together with innerConfig/effect; headless validation will use walk and innerConfig/effect will be ignored.`);
77
+ if (incoming.component && incoming.container) console.warn(`[MForm] registerField("${key}"): component and container are both set; getField will use container, and both m-fields-* / m-form-* will be registered.`);
78
+ if (merged.component) applyVueComponent(type, merged.component, app, builtIn, "fields");
79
+ if (merged.container) applyVueComponent(type, merged.container, app, builtIn, "form");
80
+ if (merged.typeMatch) registerTypeMatchRule(type, merged.typeMatch, builtIn);
81
+ if (merged.walk) {
82
+ registerContainerWalker(type, merged.walk, builtIn);
83
+ if (!builtIn) {
84
+ deleteLeafFieldType(type);
85
+ deleteFieldInnerConfig(type);
86
+ }
87
+ return;
88
+ }
89
+ if (!builtIn) deleteContainerWalker(type);
90
+ if (merged.innerConfig) {
91
+ registerFieldInnerConfig(type, merged.innerConfig, builtIn);
92
+ if (typeof merged.effect === "function") registerLeafFieldType(type, merged.effect, builtIn);
93
+ else if (!builtIn) deleteLeafFieldType(type);
94
+ return;
95
+ }
96
+ if (merged.container && !merged.component && typeof merged.effect !== "function") {
97
+ if (!builtIn) deleteLeafFieldType(type);
98
+ return;
99
+ }
100
+ if (!builtIn) deleteFieldInnerConfig(type);
101
+ registerLeafFieldType(type, merged.effect, builtIn);
102
+ };
103
+ /**
104
+ * 登记一个字段 type 在无渲染校验里的行为,以及可选的渲染组件。
105
+ *
106
+ * `type` 会按 Container 的规则归一化为中划线形式(`codeSelect` 与 `code-select` 等价)。
107
+ * 对同一 type 重复登记按字段浅合并:后一次只覆盖自己传入的 key,未传入的 key 保留。
108
+ * 传入 `app` 且带了 `component` / `container` 时,会同步 `app.component('m-fields-*'` / `'m-form-*')`。
109
+ *
110
+ * @param type - 字段 type
111
+ * @param [options] - 登记项;省略或空对象视为叶子
112
+ * @param [app] - Vue 应用;省略则不调用 `app.component`
113
+ */
114
+ var registerField = (type, options, app) => {
115
+ registerFieldImpl(type, options, app, false);
116
+ };
117
+ /**
118
+ * 批量登记字段。
119
+ *
120
+ * @param fields - type 到登记项的映射
121
+ * @param [app] - Vue 应用;省略则不调用 `app.component`
122
+ */
123
+ var registerFields = (fields, app) => {
124
+ for (const [type, options] of Object.entries(fields)) registerFieldImpl(type, options, app, false);
125
+ };
126
+ /**
127
+ * 登记内置字段(`clearFields` / `unregisterField` 不会清掉)。
128
+ *
129
+ * @param fields - type 到登记项的映射
130
+ * @param [app] - Vue 应用;省略则不调用 `app.component`
131
+ */
132
+ var registerBuiltInFields = (fields, app) => {
133
+ for (const [type, options] of Object.entries(fields)) registerFieldImpl(type, options, app, true);
134
+ };
135
+ /**
136
+ * 删除业务侧对指定 type 的登记(不影响内置;主要用于单测)。
137
+ *
138
+ * @param type - 字段 type
139
+ */
140
+ var unregisterField = (type) => {
141
+ extraFieldOptions.delete(toLine(type));
142
+ deleteLeafFieldType(type);
143
+ deleteFieldInnerConfig(type);
144
+ deleteTypeMatchRule(type);
145
+ deleteContainerWalker(type);
146
+ removeFormComponent(type);
147
+ };
148
+ /** 清空业务侧登记(不影响内置;主要用于单测)。 */
149
+ var clearFields = () => {
150
+ extraFieldOptions.clear();
151
+ clearLeafFieldTypes();
152
+ clearFieldInnerConfigs();
153
+ clearTypeMatchRules();
154
+ clearContainerWalkers();
155
+ clearFormComponents();
156
+ };
157
+ //#endregion
158
+ export { clearFields, getField, mergeFieldOptions, registerBuiltInFields, registerField, registerFields, unregisterField };
@@ -0,0 +1,54 @@
1
+ import { validateValues } from "./validateValues.js";
2
+ import { cloneDeep } from "lodash-es";
3
+ //#region packages/form/src/utils/submitHeadless.ts
4
+ var throwIfAborted = (signal, fnName) => {
5
+ if (signal?.aborted) throw signal.reason ?? /* @__PURE__ */ new Error(`${fnName} aborted`);
6
+ };
7
+ var throwIfDialog = (dialog, fnName) => {
8
+ if (dialog) throw new Error(`[MForm] ${fnName}({ dialog: true }) is not available from @tmagic/form/headless. Import from @tmagic/form instead.`);
9
+ };
10
+ /**
11
+ * `submitForm` 与 `validateForm` 共用的无渲染校验流程:中断检查 → `validateValues`。
12
+ */
13
+ var validateWithoutRender = async (fnName, options) => {
14
+ const { signal, config, initValues, parentValues, keyProp, typeMatchValid, useFieldTextInError, context } = options;
15
+ throwIfAborted(signal, fnName);
16
+ const result = await validateValues({
17
+ config,
18
+ initValues,
19
+ parentValues,
20
+ keyProp,
21
+ popperClass: options.popperClass,
22
+ typeMatchValid,
23
+ useFieldTextInError,
24
+ context
25
+ });
26
+ throwIfAborted(signal, fnName);
27
+ return result;
28
+ };
29
+ /**
30
+ * 以命令式方式对一份「表单配置 + 值」做一次校验并取回表单值(无渲染)。
31
+ *
32
+ * `@tmagic/form/headless` 不支持 `dialog: true`。
33
+ */
34
+ var submitForm = async (options) => {
35
+ throwIfDialog(options.dialog, "submitForm");
36
+ const validated = await validateWithoutRender("submitForm", options);
37
+ if (validated.error) throw new Error(validated.error);
38
+ const values = options.native ? validated.values : cloneDeep(validated.values);
39
+ return options.returnChangeRecords ? {
40
+ values,
41
+ changeRecords: []
42
+ } : values;
43
+ };
44
+ /**
45
+ * 以命令式方式对一份「表单配置 + 值」做一次静默校验(无渲染)。
46
+ *
47
+ * `@tmagic/form/headless` 不支持 `dialog: true`。
48
+ */
49
+ var validateForm = async (options) => {
50
+ throwIfDialog(options.dialog, "validateForm");
51
+ return (await validateWithoutRender("validateForm", options)).error;
52
+ };
53
+ //#endregion
54
+ export { submitForm, validateForm, validateWithoutRender };
@@ -0,0 +1,85 @@
1
+ import { cloneDeep } from "lodash-es";
2
+ //#region packages/form/src/utils/tableGroupList.ts
3
+ /**
4
+ * table / group-list 是同一份配置的两种展示形态,可以互相切换(`TableGroupList.vue`)。
5
+ *
6
+ * 本模块收口两种形态之间的配置派生,以及各形态下「子项配置怎么算出来」的规则,
7
+ * 供渲染(`TableGroupList.vue` / `GroupListItem.vue` / `useTableColumns.ts`)与无渲染校验
8
+ * (`collectValidatableFields`)共用,避免两条链路各写一份而产生偏差。
9
+ */
10
+ /** group-list 形态的 type(兼容驼峰与中划线两种写法) */
11
+ var isGroupListType = (type) => type === "groupList" || type === "group-list";
12
+ /** 按 label 文案长度估算 label 宽度(中文按 20px、其他按 8px,最小 80px) */
13
+ var calcLabelWidth = (label) => {
14
+ if (!label) return "0px";
15
+ const zhLength = label.match(/[^\x00-\xff]/g)?.length || 0;
16
+ const chLength = label.length - zhLength;
17
+ return `${Math.max(chLength * 8 + zhLength * 20, 80)}px`;
18
+ };
19
+ /** 由 group-list 形态的配置派生出 table 形态所需的配置;本身已是 table 形态则原样返回 */
20
+ var toTableConfig = (config) => {
21
+ if (!isGroupListType(config.type)) return config;
22
+ const source = config;
23
+ return {
24
+ ...config,
25
+ type: "table",
26
+ groupItems: source.items,
27
+ items: source.tableItems || source.items.map((item) => ({
28
+ ...item,
29
+ label: item.label || item.text,
30
+ text: null
31
+ }))
32
+ };
33
+ };
34
+ /** 由 table 形态的配置派生出 group-list 形态所需的配置;本身已是 group-list 形态则原样返回 */
35
+ var toGroupListConfig = (config) => {
36
+ if (isGroupListType(config.type)) return config;
37
+ const source = config;
38
+ return {
39
+ ...config,
40
+ type: "groupList",
41
+ tableItems: source.items,
42
+ items: source.groupItems || source.items.map((item) => {
43
+ const text = item.text || item.label;
44
+ return {
45
+ ...item,
46
+ text,
47
+ labelWidth: calcLabelWidth(text),
48
+ span: item.span || 12
49
+ };
50
+ })
51
+ };
52
+ };
53
+ /**
54
+ * group-list 每一行渲染的 row 容器配置(`GroupListItem.vue` 渲染的 `Container` 的 config)。
55
+ *
56
+ * `keyProp` 由调用方从 `mForm.keyProp` 取,默认 `__key`。
57
+ */
58
+ var getGroupListRowConfig = (config, index, keyProp) => {
59
+ const key = keyProp || "__key";
60
+ return {
61
+ type: "row",
62
+ span: config.span || 24,
63
+ items: config.items,
64
+ labelWidth: config.labelWidth,
65
+ labelPosition: config.labelPosition,
66
+ [key]: `${config[key]}${String(index)}`
67
+ };
68
+ };
69
+ /**
70
+ * 列是否会被渲染成一列(`hidden` 列不渲染,`display` 为假的列也不渲染)。
71
+ *
72
+ * `display` 的求值上下文是表格自身的 props,由调用方通过 `evalDisplay` 注入。
73
+ */
74
+ var isTableColumnRendered = (column, evalDisplay) => column.type !== "hidden" && Boolean(evalDisplay(column.display));
75
+ /**
76
+ * 单元格内 `Container` 实际收到的列配置:按行求值 `itemsFunction`,并去掉已在列层面消费过的 `display`。
77
+ */
78
+ var makeTableColumnConfig = (column, row) => {
79
+ const newConfig = cloneDeep(column);
80
+ if (typeof column.itemsFunction === "function") newConfig.items = column.itemsFunction(row);
81
+ delete newConfig.display;
82
+ return newConfig;
83
+ };
84
+ //#endregion
85
+ export { calcLabelWidth, getGroupListRowConfig, isGroupListType, isTableColumnRendered, makeTableColumnConfig, toGroupListConfig, toTableConfig };
@@ -1,29 +1,27 @@
1
1
  import { readonly } from "vue";
2
- import dayjs from "dayjs";
3
- import { appendValidateSuggestion } from "@tmagic/design";
4
2
  import { getValueByKeyPath, toLine } from "@tmagic/utils";
5
- import customParseFormat from "dayjs/plugin/customParseFormat";
3
+ import dayjs from "dayjs";
4
+ import { appendValidateSuggestion } from "@tmagic/design/headless";
5
+ import customParseFormat from "dayjs/plugin/customParseFormat.js";
6
6
  //#region packages/form/src/utils/typeMatch.ts
7
7
  dayjs.extend(customParseFormat);
8
- var typeMatchRuleRegistry = /* @__PURE__ */ new Map();
8
+ var extraTypeMatchRules = /* @__PURE__ */ new Map();
9
+ var builtInTypeMatchRules = /* @__PURE__ */ new Map();
9
10
  var isPromise = (value) => typeof value === "object" && value !== null && typeof value.then === "function";
10
- /** 注册或覆盖某个字段 type 的 typeMatch 校验规则 */
11
- var registerTypeMatchRule = (type, validator) => {
12
- typeMatchRuleRegistry.set(toLine(type), validator);
11
+ /** 注册或覆盖某个字段 type 的 typeMatch 校验规则。`builtIn` 登记不受 `clearTypeMatchRules` / `deleteTypeMatchRule` 影响。 */
12
+ var registerTypeMatchRule = (type, validator, builtIn = false) => {
13
+ (builtIn ? builtInTypeMatchRules : extraTypeMatchRules).set(toLine(type), validator);
13
14
  };
14
- /** 批量注册 typeMatch 校验规则 */
15
- var registerTypeMatchRules = (rules) => {
16
- Object.entries(rules).forEach(([type, validator]) => {
17
- registerTypeMatchRule(type, validator);
18
- });
15
+ /** 获取某个字段 type 的 typeMatch 校验规则(业务侧优先于内置) */
16
+ var getTypeMatchRule = (type) => {
17
+ const key = toLine(type);
18
+ return extraTypeMatchRules.get(key) ?? builtInTypeMatchRules.get(key);
19
19
  };
20
- /** 获取某个字段 type 的自定义 typeMatch 校验规则 */
21
- var getTypeMatchRule = (type) => typeMatchRuleRegistry.get(toLine(type));
22
- /** 删除某个字段 type 的自定义 typeMatch 校验规则 */
23
- var deleteTypeMatchRule = (type) => typeMatchRuleRegistry.delete(toLine(type));
24
- /** 清空所有自定义 typeMatch 校验规则 */
20
+ /** 删除业务侧 typeMatch 校验规则(不影响内置) */
21
+ var deleteTypeMatchRule = (type) => extraTypeMatchRules.delete(toLine(type));
22
+ /** 清空业务侧 typeMatch 校验规则(不影响内置) */
25
23
  var clearTypeMatchRules = () => {
26
- typeMatchRuleRegistry.clear();
24
+ extraTypeMatchRules.clear();
27
25
  };
28
26
  /** 本地解析配置函数,避免与 form.ts 循环依赖 */
29
27
  var resolveConfig = (mForm, config, props) => {
@@ -487,4 +485,4 @@ var createTypeMatchValidator = (mForm, props, rule) => {
487
485
  };
488
486
  };
489
487
  //#endregion
490
- export { MAX_SUGGESTION_OPTIONS, clearTypeMatchRules, createTypeMatchValidator, deleteTypeMatchRule, getTypeMatchRule, optionSuggestion, registerTypeMatchRule, registerTypeMatchRules, stringifyExampleValue, validateTypeMatch };
488
+ export { MAX_SUGGESTION_OPTIONS, clearTypeMatchRules, createTypeMatchValidator, deleteTypeMatchRule, getTypeMatchRule, optionSuggestion, registerTypeMatchRule, stringifyExampleValue, validateTypeMatch };
@@ -0,0 +1,57 @@
1
+ //#region packages/form/src/utils/validateError.ts
2
+ /**
3
+ * 通过 name 从 config 中查找对应的 text
4
+ *
5
+ * @param name - 字段名,支持点分隔的路径格式,如 'a.b.c'
6
+ * @param config - 表单配置数组
7
+ * @returns 找到的 text 值,如果未找到则返回 undefined
8
+ */
9
+ var getTextByName = (name, config = []) => {
10
+ if (!name || !Array.isArray(config)) return void 0;
11
+ const nameParts = name.split(".");
12
+ const findInConfig = (configs, parts) => {
13
+ if (parts.length === 0) return void 0;
14
+ const [currentPart, ...remainingParts] = parts;
15
+ for (const item of configs) {
16
+ if (item.name === currentPart) {
17
+ if (remainingParts.length === 0) return typeof item.text === "string" ? item.text : void 0;
18
+ if ("items" in item && Array.isArray(item.items)) {
19
+ const result = findInConfig(item.items, remainingParts);
20
+ if (result !== void 0) return result;
21
+ }
22
+ }
23
+ if ("items" in item && Array.isArray(item.items)) {
24
+ const result = findInConfig(item.items, parts);
25
+ if (result !== void 0) return result;
26
+ }
27
+ }
28
+ };
29
+ return findInConfig(config, nameParts);
30
+ };
31
+ /**
32
+ * 将校验返回的 invalidFields 汇总为可读的错误文案(多条以 `<br>` 拼接)。
33
+ *
34
+ * `useFieldTextInError` 为 `true`(默认)时用字段的 text 文案作为错误前缀,找不到则回退为字段 name。
35
+ *
36
+ * 由渲染式校验(Form.vue 的 `submitForm` / `validate`)与无渲染校验(`validateValues`)共用,
37
+ * 保证两条校验链路产出的错误文案格式完全一致。
38
+ *
39
+ * @param invalidFields - async-validator 产出的字段错误表
40
+ * @param [options] - 文案选项
41
+ * @param [options.config] - 用于查找 text 的表单配置
42
+ * @param [options.useFieldTextInError=true] - 错误前缀是否使用字段 text
43
+ * @returns 拼接后的错误文案;无错误时为空串
44
+ */
45
+ var formatValidateError = (invalidFields, { config = [], useFieldTextInError = true } = {}) => {
46
+ const error = [];
47
+ Object.entries(invalidFields).forEach(([prop, validateError]) => {
48
+ validateError.forEach(({ field, message }) => {
49
+ const name = field || prop;
50
+ const text = (useFieldTextInError ? getTextByName(name, config) : void 0) || name;
51
+ error.push(`${text} -> ${message}`);
52
+ });
53
+ });
54
+ return error.join("<br>");
55
+ };
56
+ //#endregion
57
+ export { formatValidateError, getTextByName };