@zerodep-dev/vue-tsx-pro-ui 0.1.0 → 0.2.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/dist/code-editor/CodeEditor.d.ts +18 -0
- package/dist/code-editor/CodeEditor.js +57 -0
- package/dist/code-editor/CodeEditor.js.map +1 -0
- package/dist/code-editor/editor.d.ts +52 -0
- package/dist/code-editor/editor.js +225 -0
- package/dist/code-editor/editor.js.map +1 -0
- package/dist/code-editor/index.d.ts +9 -0
- package/dist/code-editor/index.js +3 -0
- package/dist/config/lang/en-US.js +21 -3
- package/dist/config/lang/en-US.js.map +1 -1
- package/dist/config/lang/format.d.ts +2 -1
- package/dist/config/lang/format.js +9 -2
- package/dist/config/lang/format.js.map +1 -1
- package/dist/config/lang/lang.d.ts +19 -6
- package/dist/config/lang/lang.js +11 -2
- package/dist/config/lang/lang.js.map +1 -1
- package/dist/config/lang/zh-CN.js +21 -3
- package/dist/config/lang/zh-CN.js.map +1 -1
- package/dist/diagram/index.d.ts +2 -1
- package/dist/diagram/index.js +2 -1
- package/dist/diagram/layout.d.ts +57 -0
- package/dist/diagram/layout.js +50 -0
- package/dist/diagram/layout.js.map +1 -0
- package/dist/form/Field.d.ts +7 -0
- package/dist/form/Field.js +14 -3
- package/dist/form/Field.js.map +1 -1
- package/dist/form/useForm.d.ts +4 -1
- package/dist/form/useForm.js +35 -13
- package/dist/form/useForm.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +8 -5
- package/dist/input/Combobox.js +1 -1
- package/dist/input/Combobox.js.map +1 -1
- package/dist/input/Mentions.js +1 -1
- package/dist/input/Mentions.js.map +1 -1
- package/dist/input/PeoplePickerPanel.js +11 -11
- package/dist/input/PeoplePickerPanel.js.map +1 -1
- package/dist/input/Select.js +1 -1
- package/dist/input/Select.js.map +1 -1
- package/dist/input/TreeSelect.js +1 -1
- package/dist/input/TreeSelect.js.map +1 -1
- package/dist/input/Upload.js +2 -1
- package/dist/input/Upload.js.map +1 -1
- package/dist/input/condition.d.ts +1 -1
- package/dist/input/condition.js +32 -14
- package/dist/input/condition.js.map +1 -1
- package/dist/input/people.d.ts +15 -14
- package/dist/input/people.js +5 -5
- package/dist/input/people.js.map +1 -1
- package/dist/input/remote-items.d.ts +4 -2
- package/dist/input/remote-items.js +7 -2
- package/dist/input/remote-items.js.map +1 -1
- package/dist/patterns/QueryTable.d.ts +3 -0
- package/dist/patterns/QueryTable.js +39 -14
- package/dist/patterns/QueryTable.js.map +1 -1
- package/dist/patterns/messages.d.ts +2 -0
- package/dist/patterns/messages.js +2 -0
- package/dist/patterns/messages.js.map +1 -1
- package/dist/utils/data/clone.d.ts +8 -0
- package/dist/utils/data/clone.js +21 -0
- package/dist/utils/data/clone.js.map +1 -0
- package/dist/utils/data/date.d.ts +10 -0
- package/dist/utils/data/date.js +13 -1
- package/dist/utils/data/date.js.map +1 -1
- package/dist/utils/data/numeric.d.ts +7 -0
- package/dist/utils/data/numeric.js +46 -1
- package/dist/utils/data/numeric.js.map +1 -1
- package/dist/utils/dom/download.d.ts +8 -1
- package/dist/utils/dom/download.js +19 -2
- package/dist/utils/dom/download.js.map +1 -1
- package/dist/utils/request/error-message.d.ts +17 -0
- package/dist/utils/request/error-message.js +50 -0
- package/dist/utils/request/error-message.js.map +1 -0
- package/dist/utils/request/http.d.ts +68 -4
- package/dist/utils/request/http.js +116 -23
- package/dist/utils/request/http.js.map +1 -1
- package/dist/utils/request/use-autosave.d.ts +35 -0
- package/dist/utils/request/use-autosave.js +69 -0
- package/dist/utils/request/use-autosave.js.map +1 -0
- package/package.json +57 -4
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//#region src/diagram/layout.ts
|
|
2
|
+
/** 排好每个节点的位置,按传入的先后返回 */
|
|
3
|
+
function layoutLayers(nodes, edges, { roots = [], columnGap = 190, rowGap = 96, margin = 32 } = {}) {
|
|
4
|
+
const ids = nodes.map((n) => n.id);
|
|
5
|
+
const forward = new Map(ids.map((id) => [id, []]));
|
|
6
|
+
for (const edge of edges) forward.get(edge.from)?.push(edge.to);
|
|
7
|
+
const back = /* @__PURE__ */ new Set();
|
|
8
|
+
const state = /* @__PURE__ */ new Map();
|
|
9
|
+
const order = [];
|
|
10
|
+
const visit = (id) => {
|
|
11
|
+
state.set(id, "visiting");
|
|
12
|
+
for (const next of forward.get(id) ?? []) if (state.get(next) === "visiting") back.add(`${id}>${next}`);
|
|
13
|
+
else if (!state.has(next)) visit(next);
|
|
14
|
+
state.set(id, "done");
|
|
15
|
+
order.unshift(id);
|
|
16
|
+
};
|
|
17
|
+
for (const id of [...roots, ...ids]) if (forward.has(id) && !state.has(id)) visit(id);
|
|
18
|
+
const column = new Map(ids.map((id) => [id, 0]));
|
|
19
|
+
for (const id of order) for (const next of forward.get(id) ?? []) {
|
|
20
|
+
if (back.has(`${id}>${next}`)) continue;
|
|
21
|
+
column.set(next, Math.max(column.get(next) ?? 0, (column.get(id) ?? 0) + 1));
|
|
22
|
+
}
|
|
23
|
+
const cellWidth = Math.max(0, ...nodes.map((n) => n.width));
|
|
24
|
+
const cellHeight = Math.max(0, ...nodes.map((n) => n.height));
|
|
25
|
+
const rows = /* @__PURE__ */ new Map();
|
|
26
|
+
for (const id of order) {
|
|
27
|
+
const c = column.get(id) ?? 0;
|
|
28
|
+
rows.set(c, [...rows.get(c) ?? [], id]);
|
|
29
|
+
}
|
|
30
|
+
const tallest = Math.max(1, ...[...rows.values()].map((r) => r.length));
|
|
31
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
32
|
+
const placed = /* @__PURE__ */ new Map();
|
|
33
|
+
for (const [c, members] of rows) members.forEach((id, i) => {
|
|
34
|
+
const node = byId.get(id);
|
|
35
|
+
const x = node.x ?? margin + c * columnGap + (cellWidth - node.width) / 2;
|
|
36
|
+
const y = node.y ?? margin + (i + (tallest - members.length) / 2) * rowGap + (cellHeight - node.height) / 2;
|
|
37
|
+
placed.set(id, {
|
|
38
|
+
id,
|
|
39
|
+
x,
|
|
40
|
+
y,
|
|
41
|
+
width: node.width,
|
|
42
|
+
height: node.height
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
return nodes.map((n) => placed.get(n.id));
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { layoutLayers };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.js","names":[],"sources":["../../src/diagram/layout.ts"],"sourcesContent":["/**\n * 分层自动排版:从根节点沿连线走,按最长步数从左往右分列,同一列从上往下排,每列在最高的那列里垂直居中。\n * 绕回去的线(环)不算步数,否则会越排越远;不该影响排版的线(流程图的退回、提级)由调用方先去掉。\n * 已经有位置的节点用它自己的 x、y(只给了一个时另一个照排),照样占一个格子。流程图、审批链、简单的依赖图都够用。\n *\n * const placed = layoutLayers(\n * nodes.map((n) => ({ id: n.id, width: 140, height: 52 })),\n * edges.filter((e) => e.kind !== 'return'),\n * { roots: ['start'] },\n * );\n */\n\n/** 要排的一个节点:大小,已有的位置 */\nexport interface LayerNode {\n /** 节点的标识,连线按它连 */\n readonly id: string;\n /** 宽 */\n readonly width: number;\n /** 高 */\n readonly height: number;\n /** 已有的横坐标(左上角);没有时排出来 */\n readonly x?: number | null;\n /** 已有的纵坐标(左上角);没有时排出来 */\n readonly y?: number | null;\n}\n\n/** 一条连线:从哪到哪 */\nexport interface LayerEdge {\n /** 起点节点的标识 */\n readonly from: string;\n /** 终点节点的标识 */\n readonly to: string;\n}\n\n/** 排好的一个节点(画布坐标) */\nexport interface PlacedLayerNode {\n /** 节点的标识 */\n readonly id: string;\n /** 横坐标(左上角) */\n readonly x: number;\n /** 纵坐标(左上角) */\n readonly y: number;\n /** 宽 */\n readonly width: number;\n /** 高 */\n readonly height: number;\n}\n\n/** layoutLayers 的选项 */\nexport interface LayerOptions {\n /** 从这些节点开始走(流程的开始节点);没走到的节点之后各自起头 */\n roots?: readonly string[];\n /** 列距(相邻两列左边的距离),默认 190 */\n columnGap?: number;\n /** 行距(相邻两行上边的距离),默认 96 */\n rowGap?: number;\n /** 画布边距,默认 32 */\n margin?: number;\n}\n\n/** 排好每个节点的位置,按传入的先后返回 */\nexport function layoutLayers(\n nodes: readonly LayerNode[],\n edges: readonly LayerEdge[],\n { roots = [], columnGap = 190, rowGap = 96, margin = 32 }: LayerOptions = {},\n): PlacedLayerNode[] {\n const ids = nodes.map((n) => n.id);\n const forward = new Map<string, string[]>(ids.map((id) => [id, []]));\n for (const edge of edges) forward.get(edge.from)?.push(edge.to);\n\n // 深度优先找出绕回去的线(指向还在路上的节点),去掉它们之后是无环的\n const back = new Set<string>();\n const state = new Map<string, 'visiting' | 'done'>();\n const order: string[] = [];\n const visit = (id: string) => {\n state.set(id, 'visiting');\n for (const next of forward.get(id) ?? []) {\n if (state.get(next) === 'visiting') back.add(`${id}>${next}`);\n else if (!state.has(next)) visit(next);\n }\n state.set(id, 'done');\n order.unshift(id);\n };\n for (const id of [...roots, ...ids]) if (forward.has(id) && !state.has(id)) visit(id);\n\n // 按拓扑序算最长步数:列号\n const column = new Map<string, number>(ids.map((id) => [id, 0]));\n for (const id of order) {\n for (const next of forward.get(id) ?? []) {\n if (back.has(`${id}>${next}`)) continue;\n column.set(next, Math.max(column.get(next) ?? 0, (column.get(id) ?? 0) + 1));\n }\n }\n\n // 每个格子按最大的节点算,节点在格子里居中\n const cellWidth = Math.max(0, ...nodes.map((n) => n.width));\n const cellHeight = Math.max(0, ...nodes.map((n) => n.height));\n const rows = new Map<number, string[]>();\n for (const id of order) {\n const c = column.get(id) ?? 0;\n rows.set(c, [...(rows.get(c) ?? []), id]);\n }\n const tallest = Math.max(1, ...[...rows.values()].map((r) => r.length));\n const byId = new Map(nodes.map((n) => [n.id, n]));\n const placed = new Map<string, PlacedLayerNode>();\n for (const [c, members] of rows) {\n members.forEach((id, i) => {\n const node = byId.get(id)!;\n const x = node.x ?? margin + c * columnGap + (cellWidth - node.width) / 2;\n const y =\n node.y ??\n margin + (i + (tallest - members.length) / 2) * rowGap + (cellHeight - node.height) / 2;\n placed.set(id, { id, x, y, width: node.width, height: node.height });\n });\n }\n return nodes.map((n) => placed.get(n.id)!);\n}\n"],"mappings":";;AA6DA,SAAgB,aACd,OACA,OACA,EAAE,QAAQ,CAAC,GAAG,YAAY,KAAK,SAAS,IAAI,SAAS,OAAqB,CAAC,GACxD;CACnB,MAAM,MAAM,MAAM,KAAK,MAAM,EAAE,EAAE;CACjC,MAAM,UAAU,IAAI,IAAsB,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;CACnE,KAAK,MAAM,QAAQ,OAAO,QAAQ,IAAI,KAAK,IAAI,CAAC,EAAE,KAAK,KAAK,EAAE;CAG9D,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,wBAAQ,IAAI,IAAiC;CACnD,MAAM,QAAkB,CAAC;CACzB,MAAM,SAAS,OAAe;EAC5B,MAAM,IAAI,IAAI,UAAU;EACxB,KAAK,MAAM,QAAQ,QAAQ,IAAI,EAAE,KAAK,CAAC,GACrC,IAAI,MAAM,IAAI,IAAI,MAAM,YAAY,KAAK,IAAI,GAAG,GAAG,GAAG,MAAM;OACvD,IAAI,CAAC,MAAM,IAAI,IAAI,GAAG,MAAM,IAAI;EAEvC,MAAM,IAAI,IAAI,MAAM;EACpB,MAAM,QAAQ,EAAE;CAClB;CACA,KAAK,MAAM,MAAM,CAAC,GAAG,OAAO,GAAG,GAAG,GAAG,IAAI,QAAQ,IAAI,EAAE,KAAK,CAAC,MAAM,IAAI,EAAE,GAAG,MAAM,EAAE;CAGpF,MAAM,SAAS,IAAI,IAAoB,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CAC/D,KAAK,MAAM,MAAM,OACf,KAAK,MAAM,QAAQ,QAAQ,IAAI,EAAE,KAAK,CAAC,GAAG;EACxC,IAAI,KAAK,IAAI,GAAG,GAAG,GAAG,MAAM,GAAG;EAC/B,OAAO,IAAI,MAAM,KAAK,IAAI,OAAO,IAAI,IAAI,KAAK,IAAI,OAAO,IAAI,EAAE,KAAK,KAAK,CAAC,CAAC;CAC7E;CAIF,MAAM,YAAY,KAAK,IAAI,GAAG,GAAG,MAAM,KAAK,MAAM,EAAE,KAAK,CAAC;CAC1D,MAAM,aAAa,KAAK,IAAI,GAAG,GAAG,MAAM,KAAK,MAAM,EAAE,MAAM,CAAC;CAC5D,MAAM,uBAAO,IAAI,IAAsB;CACvC,KAAK,MAAM,MAAM,OAAO;EACtB,MAAM,IAAI,OAAO,IAAI,EAAE,KAAK;EAC5B,KAAK,IAAI,GAAG,CAAC,GAAI,KAAK,IAAI,CAAC,KAAK,CAAC,GAAI,EAAE,CAAC;CAC1C;CACA,MAAM,UAAU,KAAK,IAAI,GAAG,GAAG,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,MAAM,CAAC;CACtE,MAAM,OAAO,IAAI,IAAI,MAAM,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;CAChD,MAAM,yBAAS,IAAI,IAA6B;CAChD,KAAK,MAAM,CAAC,GAAG,YAAY,MACzB,QAAQ,SAAS,IAAI,MAAM;EACzB,MAAM,OAAO,KAAK,IAAI,EAAE;EACxB,MAAM,IAAI,KAAK,KAAK,SAAS,IAAI,aAAa,YAAY,KAAK,SAAS;EACxE,MAAM,IACJ,KAAK,KACL,UAAU,KAAK,UAAU,QAAQ,UAAU,KAAK,UAAU,aAAa,KAAK,UAAU;EACxF,OAAO,IAAI,IAAI;GAAE;GAAI;GAAG;GAAG,OAAO,KAAK;GAAO,QAAQ,KAAK;EAAO,CAAC;CACrE,CAAC;CAEH,OAAO,MAAM,KAAK,MAAM,OAAO,IAAI,EAAE,EAAE,CAAE;AAC3C"}
|
package/dist/form/Field.d.ts
CHANGED
|
@@ -23,6 +23,13 @@ export interface FieldState {
|
|
|
23
23
|
readonly readonly: boolean;
|
|
24
24
|
/** 从表单 schema 推出的约束(在 Form 里写了 name 时);控件据此加 maxlength、min / max 等 */
|
|
25
25
|
readonly constraints: FieldConstraints;
|
|
26
|
+
/**
|
|
27
|
+
* 认领 Field 的 id 与标签:Field 里有几个控件时(一组勾选框),只有第一个控件拿 Field 的 id、用它的标签当名字,
|
|
28
|
+
* 其余控件留着自己的名字(勾选框自己的文字),id 也不重复。认领到了返回 true
|
|
29
|
+
*/
|
|
30
|
+
claim?(token: symbol): boolean;
|
|
31
|
+
/** 认领的控件卸载了:下一个控件可以认领 */
|
|
32
|
+
release?(token: symbol): void;
|
|
26
33
|
}
|
|
27
34
|
/** Field 的上下文:自定义控件用 useFieldControl 接入,一般不直接读它;null 表示子树不属于任何 Field */
|
|
28
35
|
export declare const FieldContext: import("@zerodep-dev/vue-tsx-pro-tsx").Context<FieldState | null>;
|
package/dist/form/Field.js
CHANGED
|
@@ -7,7 +7,7 @@ import { plainText } from "../utils/data/text.js";
|
|
|
7
7
|
import { Text as Text$1 } from "../base/Text.js";
|
|
8
8
|
import { FieldLayoutContext } from "./field-layout.js";
|
|
9
9
|
import { FormContext } from "./Form.js";
|
|
10
|
-
import { Fragment, cloneVNode, createVNode, isVNode, mergeProps, unref, useId, watch } from "vue";
|
|
10
|
+
import { Fragment, cloneVNode, createVNode, isVNode, mergeProps, onBeforeUnmount, unref, useId, watch } from "vue";
|
|
11
11
|
import { _component, childrenProp, createContext, effect, readonlyProp, restProps } from "@zerodep-dev/vue-tsx-pro-tsx";
|
|
12
12
|
import * as css from "@zerodep-dev/vue-tsx-pro-tsx/styles";
|
|
13
13
|
import { Asterisk } from "@lucide/icons";
|
|
@@ -58,11 +58,14 @@ function useFieldControl(read, { delegate = false } = {}) {
|
|
|
58
58
|
const field = FieldContext.inject(null);
|
|
59
59
|
if (!delegate) FieldContext.provide(null);
|
|
60
60
|
const layout = field ? null : FieldLayoutContext.inject(null);
|
|
61
|
+
const token = Symbol("field-control");
|
|
62
|
+
const owns = field == null ? false : delegate || (field.claim?.(token) ?? true);
|
|
63
|
+
if (field && !delegate) onBeforeUnmount(() => field.release?.(token));
|
|
61
64
|
return () => {
|
|
62
65
|
const own = read();
|
|
63
66
|
return {
|
|
64
|
-
id: own.id ?? field?.controlId,
|
|
65
|
-
labelledBy: field?.labelId,
|
|
67
|
+
id: own.id ?? (owns ? field?.controlId : void 0),
|
|
68
|
+
labelledBy: owns ? field?.labelId : void 0,
|
|
66
69
|
describedBy: field?.describedBy,
|
|
67
70
|
invalid: own.invalid ?? field?.invalid ?? false,
|
|
68
71
|
required: own.required ?? field?.required ?? false,
|
|
@@ -217,7 +220,15 @@ var Field = /*#__PURE__*/ _component((_props2, _ctx2) => {
|
|
|
217
220
|
const isRequired = () => required.value ?? constraints().required ?? false;
|
|
218
221
|
const isDisabled = () => disabled.value ?? layout?.disabled ?? false;
|
|
219
222
|
const isReadonly = () => readonly.value ?? layout?.readonly ?? false;
|
|
223
|
+
let owner = null;
|
|
220
224
|
FieldContext.provide({
|
|
225
|
+
claim: (token) => {
|
|
226
|
+
owner ??= token;
|
|
227
|
+
return owner === token;
|
|
228
|
+
},
|
|
229
|
+
release: (token) => {
|
|
230
|
+
if (owner === token) owner = null;
|
|
231
|
+
},
|
|
221
232
|
controlId: ids.control,
|
|
222
233
|
get labelId() {
|
|
223
234
|
return hasContent(label.value) ? ids.label : void 0;
|
package/dist/form/Field.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Field.js","names":["_vtp_readonlyProp","_vtp_childrenProp","_vtp_restProps","_createVNode","_mergeProps","Text","_vue_unref"],"sources":["../../src/form/Field.tsx"],"sourcesContent":["/**\n * 表单字段:标签 + 控件 + 说明 + 错误信息,并把它们关联起来(参考 Ark UI Field、React Aria 的 Label / Description / FieldError)。\n *\n * <Field label=\"邮箱\" description=\"用于找回密码\" error={errors.email} required>\n * <TextField _value={form.email} type=\"email\" />\n * </Field>\n *\n * Field 经上下文告诉里面的控件:控件的 id(标签的 for 指向它)、标签的 id(不是原生输入的控件用 aria-labelledby)、\n * 说明与错误的 id(aria-describedby),以及 invalid / required / disabled / readonly。\n * 控件上显式写的状态优先于 Field 的。控件用 useFieldControl 读取这些信息,单独使用(不在 Field 里)时照常工作。\n * 在 Form 里写了 name 时,错误信息取自表单控制器(useForm),焦点离开字段时报告失焦(何时显示错误见 useForm);\n * 里面只有一个控件、控件上没写值时自动绑定表单的这个字段(valueProp 决定写 _value 还是 _checked),\n * 标签与控件登记给表单,错误汇总(FormErrorSummary)据此列出字段名、跳到控件。\n *\n * 标签可以放在起始边(`labelPlacement=\"start\"`):标签一列、控件一列,标签与控件的第一行文字按基线对齐,\n * 多行文本框、勾选组也对得齐。通常在 Form、FormSection 上统一设置(见 field-layout),整组的只读、不可用同样从那里来。\n *\n * 部件:Root(div)、Label(Label)、Control(div,标签在起始边时包住控件、说明与错误)、\n * Description(Text)、Error(Text,带警示图标)。\n */\nimport { Asterisk } from '@lucide/icons';\nimport { createContext, _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport * as css from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport { cloneVNode, Fragment, isVNode, useId, watch, type VNode, type VNodeChild } from 'vue';\nimport { Icon, type IconProps } from '../base/Icon';\nimport { Text, type TextProps } from '../base/Text';\nimport { visuallyHiddenStyles } from '../base/VisuallyHidden';\nimport { plainText } from '../utils/data/text';\nimport { focusControl } from '../utils/dom/focus';\nimport { hasContent, mergeAttrs, mergePartProps, type PartProps } from '../utils/props';\nimport { statusIcons } from '../utils/style/tone-styles';\nimport {\n FieldLayoutContext,\n type FieldLabelAligns,\n type FieldLabelPlacements,\n} from './field-layout';\nimport { FormContext } from './Form';\nimport type { FieldConstraints } from './form-constraints';\n\nconst NO_CONSTRAINTS: FieldConstraints = Object.freeze({});\n\n/** Field 交给控件的信息 */\nexport interface FieldState {\n /** 控件(主元素)的 id,标签的 for 指向它 */\n readonly controlId: string;\n /** 标签的 id:控件不是原生输入元素(滑块、单选组)时用 aria-labelledby 指向它 */\n readonly labelId: string | undefined;\n /** 说明与错误信息的 id,空格分隔,给 aria-describedby */\n readonly describedBy: string | undefined;\n /** 校验未通过 */\n readonly invalid: boolean;\n /** 必填 */\n readonly required: boolean;\n /** 不可用 */\n readonly disabled: boolean;\n /** 只读 */\n readonly readonly: boolean;\n /** 从表单 schema 推出的约束(在 Form 里写了 name 时);控件据此加 maxlength、min / max 等 */\n readonly constraints: FieldConstraints;\n}\n\n/** Field 的上下文:自定义控件用 useFieldControl 接入,一般不直接读它;null 表示子树不属于任何 Field */\nexport const FieldContext = createContext<FieldState | null>('Field');\n\n/** 控件自己的状态(不写时取 Field 的) */\nexport interface FieldControlOptions {\n /** 控件上显式写的 id */\n id?: string;\n /** 校验未通过 */\n invalid?: boolean;\n /** 必填;在 Form 里写了 name 时默认按表单的 schema(如 z.string().min(1)) */\n required?: boolean;\n /** 不可用 */\n disabled?: boolean;\n /** 只读 */\n readonly?: boolean;\n}\n\n/** useFieldControl 的选项 */\nexport interface FieldControlUse {\n /** 输入交给里面的另一个控件(DatePicker 里的 DateField):子树仍属于 Field,由那个控件接 */\n delegate?: boolean;\n}\n\n/** 控件合并 Field 之后的状态与关联属性 */\nexport interface FieldControl {\n /** 控件的 id:显式写的优先,其次是 Field 给的 */\n id: string | undefined;\n /** 不是原生输入元素的控件用它作 aria-labelledby */\n labelledBy: string | undefined;\n /** aria-describedby */\n describedBy: string | undefined;\n /** 校验未通过 */\n invalid: boolean;\n /** 必填 */\n required: boolean;\n /** 不可用 */\n disabled: boolean;\n /** 只读 */\n readonly: boolean;\n /** 从表单 schema 推出的约束;不在 Field 里、或推不出时为空对象。控件上显式写的属性优先 */\n constraints: FieldConstraints;\n}\n\n/**\n * 所在的表单重置时(form.reset()、type=\"reset\" 的按钮)调用:控件丢掉自己还没写回的输入(日期分段打了一半)。\n * 值本身由表单改回去;只是半截输入时值一直是 null,重置前后没有变化,控件不知道要清。在 setup 里调用\n */\nexport function useFormReset(onReset: () => void): void {\n const form = FormContext.inject(null);\n if (form) watch(() => form.resetCount, onReset);\n}\n\n/**\n * 控件读取所在 Field 的信息(在 setup 里调用,返回的函数在渲染时调用):\n * 控件上显式写的状态优先;不在 Field 里时用控件自己的,不可用与只读再看所在的 Form、FormSection。\n * 控件里再嵌的控件(Select 的搜索框、区间的两个框、时间面板的列表)不是 Field 的控件,\n * 拿到 Field 的 id、说明与必填会重复 id、让表单提交被空的搜索框拦住:调用后子树与 Field 隔开,\n * 除非写了 delegate\n */\nexport function useFieldControl(\n read: () => FieldControlOptions,\n { delegate = false }: FieldControlUse = {},\n): () => FieldControl {\n const field = FieldContext.inject(null);\n if (!delegate) FieldContext.provide(null);\n const layout = field ? null : FieldLayoutContext.inject(null);\n return () => {\n const own = read();\n return {\n id: own.id ?? field?.controlId,\n labelledBy: field?.labelId,\n describedBy: field?.describedBy,\n invalid: own.invalid ?? field?.invalid ?? false,\n required: own.required ?? field?.required ?? false,\n disabled: own.disabled ?? field?.disabled ?? layout?.disabled ?? false,\n readonly: own.readonly ?? field?.readonly ?? layout?.readonly ?? false,\n constraints: field?.constraints ?? NO_CONSTRAINTS,\n };\n };\n}\n\n/** 未声明的属性落到 label 上 */\nexport interface LabelProps extends PartProps<'label'> {\n /** 必填:文字后加一个星号图标(对读屏隐藏,必填由控件的 required 告诉读屏) */\n required?: boolean;\n /** 不可用:变淡 */\n disabled?: boolean;\n /** 必填标记的部件属性 */\n propsRequired?: Partial<IconProps>;\n children?: VNodeChild;\n}\n\n/** Field 的样式:root、disabled、required */\nexport const labelStyles = css.create({\n root: {\n display: 'inline-flex',\n alignItems: 'baseline',\n gap: '_xs',\n fontSize: '_sm',\n fontWeight: '_medium',\n lineHeight: '_tight',\n color: '_text',\n },\n disabled: { opacity: '_disabled' },\n required: { alignSelf: 'flex-start', width: '0.7em', height: '0.7em', color: '_dangerText' },\n});\n\n/**\n * for 指向的不是原生能被标签关联的元素(单选组、滑块组、日期分段这类 role=\"group\")时,点标签浏览器什么也不做:\n * 改为聚焦它里面第一个能聚焦的控件(同点原生输入框的标签)\n */\nfunction focusGroupControl(event: MouseEvent) {\n const label = event.currentTarget as HTMLLabelElement;\n if (event.defaultPrevented || label.control || !label.htmlFor) return;\n const target = label.ownerDocument.getElementById(label.htmlFor);\n if (target) focusControl(target);\n}\n\n/** 表单标签(label):for 指向控件;Field 里自动关联,单独使用时自己写 for */\nexport const Label = _component(\n ({ required = false, disabled = false, propsRequired, children, ...rest }: LabelProps) =>\n () => (\n <label\n {...mergeAttrs<'label'>(\n { sx: [labelStyles.root, disabled && labelStyles.disabled] },\n rest,\n { onClick: focusGroupControl },\n )}\n data-disabled={disabled || undefined}\n >\n {children}\n {required && (\n <Icon\n {...mergePartProps<IconProps>(\n { icon: Asterisk, sx: labelStyles.required },\n propsRequired,\n )}\n />\n )}\n </label>\n ),\n);\n\n/** 未声明的属性落到外层 div 上 */\nexport interface FieldProps extends PartProps<'div'> {\n /** 在 Form 里对应的字段路径(如 `email`、`address.city`):错误取自表单控制器,只有一个控件时自动绑定它的值 */\n name?: string;\n /**\n * 自动绑定值时写到控件的哪个属性:默认 _value;控件声明了 _checked 而没有 _value(Switch、单个 Checkbox)时是 _checked\n */\n valueProp?: '_value' | '_checked';\n /** 标签 */\n label?: VNodeChild;\n /** 控件下方的说明 */\n description?: VNodeChild;\n /** 错误信息:给了(且 invalid 不是 false)就显示,并把控件标为出错;在 Form 里写了 name 时默认取表单的错误 */\n error?: VNodeChild;\n /** 校验未通过;不写时有 error 就算出错 */\n invalid?: boolean;\n /** 必填 */\n required?: boolean;\n /** 不可用;不写时取所在 Form、FormSection 的 */\n disabled?: boolean;\n /** 只读;不写时取所在 Form、FormSection 的 */\n readonly?: boolean;\n /** 标签放在哪;不写时取所在 Form、FormSection 的,都没写时在上方 */\n labelPlacement?: keyof FieldLabelPlacements;\n /** 标签在起始边时标签列的宽度:数字是 px,也可以写 CSS 长度;默认 `'8em'` */\n labelWidth?: number | string;\n /** 标签在起始边时文字的对齐,默认 end(贴着控件) */\n labelAlign?: keyof FieldLabelAligns;\n /**\n * 标签只给读屏:视觉上隐藏,仍是控件的名称(表格式的重复行只在第一行显示列名、紧凑的筛选条);\n * 这时不占标签列,控件单独一列\n */\n labelHidden?: boolean;\n /** 标签的部件属性 */\n propsLabel?: Partial<LabelProps>;\n /** 控件列(标签在起始边时包住控件、说明与错误的 div)的部件属性 */\n propsControl?: PartProps<'div'>;\n /** 说明的部件属性 */\n propsDescription?: Partial<TextProps>;\n /** 错误信息的部件属性 */\n propsError?: Partial<TextProps>;\n /** 控件 */\n children?: VNodeChild;\n}\n\n/** 控件的组件声明了这个属性(运行时的 props 定义) */\nfunction declares(node: VNode, key: string): boolean {\n const declared = (node.type as { props?: unknown }).props;\n if (Array.isArray(declared)) return declared.includes(key);\n return typeof declared === 'object' && declared !== null && key in declared;\n}\n\n/** Field 里唯一的控件:一个组件的 VNode(数组里只有一项也算);不是时 undefined */\nfunction soleControl(children: VNodeChild): VNode | undefined {\n const node = Array.isArray(children) && children.length === 1 ? children[0] : children;\n if (!isVNode(node) || typeof node.type === 'string' || node.type === Fragment) return undefined;\n return node;\n}\n\n/** 标签在起始边时默认的标签列宽度:放得下七八个汉字 */\nconst DEFAULT_LABEL_WIDTH = '8em';\n\n/** Field 的样式:root、start、label…、control、hint、error、errorIcon */\nexport const fieldStyles = css.create({\n root: { display: 'flex', flexDirection: 'column', gap: '_xs', minWidth: 0 },\n // 标签一列、控件一列;第一行按基线对齐:标签与输入框里的文字、勾选项的文字在同一条线上\n start: {\n display: 'grid',\n columnGap: '_md',\n rowGap: '_xs',\n alignItems: 'baseline',\n },\n labelStart: { justifySelf: 'start', textAlign: 'start' },\n labelEnd: { justifySelf: 'end', textAlign: 'end' },\n // 没有标签时控件仍放在第二列,与上下的字段对齐\n controlWithoutLabel: { gridColumnStart: '2' },\n control: { display: 'flex', flexDirection: 'column', gap: '_xs', minWidth: 0 },\n hint: { fontSize: '_sm', lineHeight: '_normal' },\n error: { display: 'inline-flex', alignItems: 'flex-start', gap: '_xs' },\n errorIcon: { flexShrink: 0, marginBlock: '0.25em' },\n});\n\n/** Field 的动态样式:标签列的宽度 */\nexport const fieldColumnStyles = css.create({\n columns: (labelWidth: string) => ({ gridTemplateColumns: `${labelWidth} minmax(0, 1fr)` }),\n});\n\n/** 表单字段:标签、控件、说明、错误信息,自动关联 id 与状态 */\nexport const Field = _component(\n ({\n name,\n valueProp,\n label,\n description,\n error,\n invalid,\n required,\n disabled,\n readonly,\n labelPlacement,\n labelWidth,\n labelAlign,\n labelHidden = false,\n propsLabel,\n propsControl,\n propsDescription,\n propsError,\n children,\n ...rest\n }: FieldProps) => {\n const id = useId();\n const ids = {\n control: `${id}-control`,\n label: `${id}-label`,\n description: `${id}-description`,\n error: `${id}-error`,\n };\n const form = FormContext.inject(null);\n const layout = FieldLayoutContext.inject(null);\n // error={cond && '…'} 条件不成立时是 false:当作没写,用表单的错误\n const message = () =>\n hasContent(error) ? error : name != null ? form?.visibleError(name) : undefined;\n const showError = () => hasContent(message()) && invalid !== false;\n const constraints = () => (name != null && form ? form.constraints(name) : NO_CONSTRAINTS);\n const isRequired = () => required ?? constraints().required ?? false;\n const isDisabled = () => disabled ?? layout?.disabled ?? false;\n const isReadonly = () => readonly ?? layout?.readonly ?? false;\n\n FieldContext.provide({\n controlId: ids.control,\n get labelId() {\n return hasContent(label) ? ids.label : undefined;\n },\n get describedBy() {\n const parts = [hasContent(description) && ids.description, showError() && ids.error];\n return parts.filter(Boolean).join(' ') || undefined;\n },\n get invalid() {\n return invalid ?? hasContent(message());\n },\n get required() {\n return isRequired();\n },\n get constraints() {\n return constraints();\n },\n get disabled() {\n return isDisabled();\n },\n get readonly() {\n return isReadonly();\n },\n });\n\n // 登记给表单:错误汇总据此显示字段名、跳到控件\n _effect(() =>\n name != null && form\n ? form.register(name, { label: plainText(label) ?? name, controlId: ids.control })\n : undefined,\n );\n\n /** 只有一个控件、控件上没写值时,自动绑定表单的这个字段(同 Ant Design Form.Item 的 name) */\n function bindValue(content: VNodeChild): VNodeChild {\n const control = name != null && form ? soleControl(content) : undefined;\n if (!control || !form) return content;\n const key =\n valueProp ??\n (declares(control, '_checked') && !declares(control, '_value') ? '_checked' : '_value');\n if (control.props && key in control.props) return content;\n const path = name!;\n return cloneVNode(control, {\n [key]: form.getValue(path),\n [`onUpdate:${key}`]: (value: unknown) => form.setValue(path, value),\n });\n }\n\n // 焦点离开整个字段(不是在字段内部移动)时报告失焦\n function onFocusout(event: FocusEvent) {\n const next = event.relatedTarget;\n const field = event.currentTarget as HTMLElement;\n if (name != null && !(next instanceof Node && field.contains(next))) form?.blur(name);\n }\n\n return () => {\n const placement = labelPlacement ?? layout?.labelPlacement ?? 'top';\n const start = placement === 'start' && !labelHidden;\n const width = labelWidth ?? layout?.labelWidth ?? DEFAULT_LABEL_WIDTH;\n const align = labelAlign ?? layout?.labelAlign ?? 'end';\n const hints = [\n hasContent(description) && (\n <Text\n {...mergePartProps<TextProps>(\n { tone: 'muted', sx: fieldStyles.hint },\n propsDescription,\n {\n id: ids.description,\n },\n )}\n >\n {description}\n </Text>\n ),\n showError() && (\n <Text\n {...mergePartProps<TextProps>(\n { tone: 'danger', sx: [fieldStyles.hint, fieldStyles.error] },\n propsError,\n { id: ids.error },\n )}\n >\n <Icon icon={statusIcons.danger} sx={fieldStyles.errorIcon} />\n {message()}\n </Text>\n ),\n ];\n return (\n <div\n {...mergeAttrs<'div'>(\n {\n sx: start\n ? [\n fieldStyles.start,\n fieldColumnStyles.columns(typeof width === 'number' ? `${width}px` : width),\n ]\n : fieldStyles.root,\n },\n rest,\n { onFocusout },\n )}\n data-invalid={(invalid ?? message() != null) || undefined}\n data-disabled={isDisabled() || undefined}\n data-label-placement={placement}\n >\n {hasContent(label) && (\n <Label\n {...mergePartProps<LabelProps>(\n {\n required: isRequired(),\n disabled: isDisabled(),\n sx: labelHidden\n ? visuallyHiddenStyles.hidden\n : start && (align === 'end' ? fieldStyles.labelEnd : fieldStyles.labelStart),\n },\n propsLabel,\n { id: ids.label, for: ids.control },\n )}\n >\n {label}\n </Label>\n )}\n {start ? (\n <div\n {...mergeAttrs<'div'>(\n {\n sx: [fieldStyles.control, !hasContent(label) && fieldStyles.controlWithoutLabel],\n },\n propsControl,\n )}\n >\n {bindValue(children)}\n {hints}\n </div>\n ) : (\n [bindValue(children), hints]\n )}\n </div>\n );\n };\n },\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Field.js","names":["_vtp_readonlyProp","_vtp_childrenProp","_vtp_restProps","_createVNode","_mergeProps","Text","_vue_unref"],"sources":["../../src/form/Field.tsx"],"sourcesContent":["/**\n * 表单字段:标签 + 控件 + 说明 + 错误信息,并把它们关联起来(参考 Ark UI Field、React Aria 的 Label / Description / FieldError)。\n *\n * <Field label=\"邮箱\" description=\"用于找回密码\" error={errors.email} required>\n * <TextField _value={form.email} type=\"email\" />\n * </Field>\n *\n * Field 经上下文告诉里面的控件:控件的 id(标签的 for 指向它)、标签的 id(不是原生输入的控件用 aria-labelledby)、\n * 说明与错误的 id(aria-describedby),以及 invalid / required / disabled / readonly。\n * 控件上显式写的状态优先于 Field 的。控件用 useFieldControl 读取这些信息,单独使用(不在 Field 里)时照常工作。\n * 在 Form 里写了 name 时,错误信息取自表单控制器(useForm),焦点离开字段时报告失焦(何时显示错误见 useForm);\n * 里面只有一个控件、控件上没写值时自动绑定表单的这个字段(valueProp 决定写 _value 还是 _checked),\n * 标签与控件登记给表单,错误汇总(FormErrorSummary)据此列出字段名、跳到控件。\n *\n * 标签可以放在起始边(`labelPlacement=\"start\"`):标签一列、控件一列,标签与控件的第一行文字按基线对齐,\n * 多行文本框、勾选组也对得齐。通常在 Form、FormSection 上统一设置(见 field-layout),整组的只读、不可用同样从那里来。\n *\n * 部件:Root(div)、Label(Label)、Control(div,标签在起始边时包住控件、说明与错误)、\n * Description(Text)、Error(Text,带警示图标)。\n */\nimport { Asterisk } from '@lucide/icons';\nimport { createContext, _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport * as css from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport {\n cloneVNode,\n Fragment,\n isVNode,\n onBeforeUnmount,\n useId,\n watch,\n type VNode,\n type VNodeChild,\n} from 'vue';\nimport { Icon, type IconProps } from '../base/Icon';\nimport { Text, type TextProps } from '../base/Text';\nimport { visuallyHiddenStyles } from '../base/VisuallyHidden';\nimport { plainText } from '../utils/data/text';\nimport { focusControl } from '../utils/dom/focus';\nimport { hasContent, mergeAttrs, mergePartProps, type PartProps } from '../utils/props';\nimport { statusIcons } from '../utils/style/tone-styles';\nimport {\n FieldLayoutContext,\n type FieldLabelAligns,\n type FieldLabelPlacements,\n} from './field-layout';\nimport { FormContext } from './Form';\nimport type { FieldConstraints } from './form-constraints';\n\nconst NO_CONSTRAINTS: FieldConstraints = Object.freeze({});\n\n/** Field 交给控件的信息 */\nexport interface FieldState {\n /** 控件(主元素)的 id,标签的 for 指向它 */\n readonly controlId: string;\n /** 标签的 id:控件不是原生输入元素(滑块、单选组)时用 aria-labelledby 指向它 */\n readonly labelId: string | undefined;\n /** 说明与错误信息的 id,空格分隔,给 aria-describedby */\n readonly describedBy: string | undefined;\n /** 校验未通过 */\n readonly invalid: boolean;\n /** 必填 */\n readonly required: boolean;\n /** 不可用 */\n readonly disabled: boolean;\n /** 只读 */\n readonly readonly: boolean;\n /** 从表单 schema 推出的约束(在 Form 里写了 name 时);控件据此加 maxlength、min / max 等 */\n readonly constraints: FieldConstraints;\n /**\n * 认领 Field 的 id 与标签:Field 里有几个控件时(一组勾选框),只有第一个控件拿 Field 的 id、用它的标签当名字,\n * 其余控件留着自己的名字(勾选框自己的文字),id 也不重复。认领到了返回 true\n */\n claim?(token: symbol): boolean;\n /** 认领的控件卸载了:下一个控件可以认领 */\n release?(token: symbol): void;\n}\n\n/** Field 的上下文:自定义控件用 useFieldControl 接入,一般不直接读它;null 表示子树不属于任何 Field */\nexport const FieldContext = createContext<FieldState | null>('Field');\n\n/** 控件自己的状态(不写时取 Field 的) */\nexport interface FieldControlOptions {\n /** 控件上显式写的 id */\n id?: string;\n /** 校验未通过 */\n invalid?: boolean;\n /** 必填;在 Form 里写了 name 时默认按表单的 schema(如 z.string().min(1)) */\n required?: boolean;\n /** 不可用 */\n disabled?: boolean;\n /** 只读 */\n readonly?: boolean;\n}\n\n/** useFieldControl 的选项 */\nexport interface FieldControlUse {\n /** 输入交给里面的另一个控件(DatePicker 里的 DateField):子树仍属于 Field,由那个控件接 */\n delegate?: boolean;\n}\n\n/** 控件合并 Field 之后的状态与关联属性 */\nexport interface FieldControl {\n /** 控件的 id:显式写的优先,其次是 Field 给的 */\n id: string | undefined;\n /** 不是原生输入元素的控件用它作 aria-labelledby */\n labelledBy: string | undefined;\n /** aria-describedby */\n describedBy: string | undefined;\n /** 校验未通过 */\n invalid: boolean;\n /** 必填 */\n required: boolean;\n /** 不可用 */\n disabled: boolean;\n /** 只读 */\n readonly: boolean;\n /** 从表单 schema 推出的约束;不在 Field 里、或推不出时为空对象。控件上显式写的属性优先 */\n constraints: FieldConstraints;\n}\n\n/**\n * 所在的表单重置时(form.reset()、type=\"reset\" 的按钮)调用:控件丢掉自己还没写回的输入(日期分段打了一半)。\n * 值本身由表单改回去;只是半截输入时值一直是 null,重置前后没有变化,控件不知道要清。在 setup 里调用\n */\nexport function useFormReset(onReset: () => void): void {\n const form = FormContext.inject(null);\n if (form) watch(() => form.resetCount, onReset);\n}\n\n/**\n * 控件读取所在 Field 的信息(在 setup 里调用,返回的函数在渲染时调用):\n * 控件上显式写的状态优先;不在 Field 里时用控件自己的,不可用与只读再看所在的 Form、FormSection。\n * 控件里再嵌的控件(Select 的搜索框、区间的两个框、时间面板的列表)不是 Field 的控件,\n * 拿到 Field 的 id、说明与必填会重复 id、让表单提交被空的搜索框拦住:调用后子树与 Field 隔开,\n * 除非写了 delegate\n */\nexport function useFieldControl(\n read: () => FieldControlOptions,\n { delegate = false }: FieldControlUse = {},\n): () => FieldControl {\n const field = FieldContext.inject(null);\n if (!delegate) FieldContext.provide(null);\n const layout = field ? null : FieldLayoutContext.inject(null);\n // 把输入交给里面控件的(delegate)不认领:由里面那个控件认领;它自己仍用 Field 的标签\n const token = Symbol('field-control');\n const owns = field == null ? false : delegate || (field.claim?.(token) ?? true);\n if (field && !delegate) onBeforeUnmount(() => field.release?.(token));\n return () => {\n const own = read();\n return {\n id: own.id ?? (owns ? field?.controlId : undefined),\n labelledBy: owns ? field?.labelId : undefined,\n describedBy: field?.describedBy,\n invalid: own.invalid ?? field?.invalid ?? false,\n required: own.required ?? field?.required ?? false,\n disabled: own.disabled ?? field?.disabled ?? layout?.disabled ?? false,\n readonly: own.readonly ?? field?.readonly ?? layout?.readonly ?? false,\n constraints: field?.constraints ?? NO_CONSTRAINTS,\n };\n };\n}\n\n/** 未声明的属性落到 label 上 */\nexport interface LabelProps extends PartProps<'label'> {\n /** 必填:文字后加一个星号图标(对读屏隐藏,必填由控件的 required 告诉读屏) */\n required?: boolean;\n /** 不可用:变淡 */\n disabled?: boolean;\n /** 必填标记的部件属性 */\n propsRequired?: Partial<IconProps>;\n children?: VNodeChild;\n}\n\n/** Field 的样式:root、disabled、required */\nexport const labelStyles = css.create({\n root: {\n display: 'inline-flex',\n alignItems: 'baseline',\n gap: '_xs',\n fontSize: '_sm',\n fontWeight: '_medium',\n lineHeight: '_tight',\n color: '_text',\n },\n disabled: { opacity: '_disabled' },\n required: { alignSelf: 'flex-start', width: '0.7em', height: '0.7em', color: '_dangerText' },\n});\n\n/**\n * for 指向的不是原生能被标签关联的元素(单选组、滑块组、日期分段这类 role=\"group\")时,点标签浏览器什么也不做:\n * 改为聚焦它里面第一个能聚焦的控件(同点原生输入框的标签)\n */\nfunction focusGroupControl(event: MouseEvent) {\n const label = event.currentTarget as HTMLLabelElement;\n if (event.defaultPrevented || label.control || !label.htmlFor) return;\n const target = label.ownerDocument.getElementById(label.htmlFor);\n if (target) focusControl(target);\n}\n\n/** 表单标签(label):for 指向控件;Field 里自动关联,单独使用时自己写 for */\nexport const Label = _component(\n ({ required = false, disabled = false, propsRequired, children, ...rest }: LabelProps) =>\n () => (\n <label\n {...mergeAttrs<'label'>(\n { sx: [labelStyles.root, disabled && labelStyles.disabled] },\n rest,\n { onClick: focusGroupControl },\n )}\n data-disabled={disabled || undefined}\n >\n {children}\n {required && (\n <Icon\n {...mergePartProps<IconProps>(\n { icon: Asterisk, sx: labelStyles.required },\n propsRequired,\n )}\n />\n )}\n </label>\n ),\n);\n\n/** 未声明的属性落到外层 div 上 */\nexport interface FieldProps extends PartProps<'div'> {\n /** 在 Form 里对应的字段路径(如 `email`、`address.city`):错误取自表单控制器,只有一个控件时自动绑定它的值 */\n name?: string;\n /**\n * 自动绑定值时写到控件的哪个属性:默认 _value;控件声明了 _checked 而没有 _value(Switch、单个 Checkbox)时是 _checked\n */\n valueProp?: '_value' | '_checked';\n /** 标签 */\n label?: VNodeChild;\n /** 控件下方的说明 */\n description?: VNodeChild;\n /** 错误信息:给了(且 invalid 不是 false)就显示,并把控件标为出错;在 Form 里写了 name 时默认取表单的错误 */\n error?: VNodeChild;\n /** 校验未通过;不写时有 error 就算出错 */\n invalid?: boolean;\n /** 必填 */\n required?: boolean;\n /** 不可用;不写时取所在 Form、FormSection 的 */\n disabled?: boolean;\n /** 只读;不写时取所在 Form、FormSection 的 */\n readonly?: boolean;\n /** 标签放在哪;不写时取所在 Form、FormSection 的,都没写时在上方 */\n labelPlacement?: keyof FieldLabelPlacements;\n /** 标签在起始边时标签列的宽度:数字是 px,也可以写 CSS 长度;默认 `'8em'` */\n labelWidth?: number | string;\n /** 标签在起始边时文字的对齐,默认 end(贴着控件) */\n labelAlign?: keyof FieldLabelAligns;\n /**\n * 标签只给读屏:视觉上隐藏,仍是控件的名称(表格式的重复行只在第一行显示列名、紧凑的筛选条);\n * 这时不占标签列,控件单独一列\n */\n labelHidden?: boolean;\n /** 标签的部件属性 */\n propsLabel?: Partial<LabelProps>;\n /** 控件列(标签在起始边时包住控件、说明与错误的 div)的部件属性 */\n propsControl?: PartProps<'div'>;\n /** 说明的部件属性 */\n propsDescription?: Partial<TextProps>;\n /** 错误信息的部件属性 */\n propsError?: Partial<TextProps>;\n /** 控件 */\n children?: VNodeChild;\n}\n\n/** 控件的组件声明了这个属性(运行时的 props 定义) */\nfunction declares(node: VNode, key: string): boolean {\n const declared = (node.type as { props?: unknown }).props;\n if (Array.isArray(declared)) return declared.includes(key);\n return typeof declared === 'object' && declared !== null && key in declared;\n}\n\n/** Field 里唯一的控件:一个组件的 VNode(数组里只有一项也算);不是时 undefined */\nfunction soleControl(children: VNodeChild): VNode | undefined {\n const node = Array.isArray(children) && children.length === 1 ? children[0] : children;\n if (!isVNode(node) || typeof node.type === 'string' || node.type === Fragment) return undefined;\n return node;\n}\n\n/** 标签在起始边时默认的标签列宽度:放得下七八个汉字 */\nconst DEFAULT_LABEL_WIDTH = '8em';\n\n/** Field 的样式:root、start、label…、control、hint、error、errorIcon */\nexport const fieldStyles = css.create({\n root: { display: 'flex', flexDirection: 'column', gap: '_xs', minWidth: 0 },\n // 标签一列、控件一列;第一行按基线对齐:标签与输入框里的文字、勾选项的文字在同一条线上\n start: {\n display: 'grid',\n columnGap: '_md',\n rowGap: '_xs',\n alignItems: 'baseline',\n },\n labelStart: { justifySelf: 'start', textAlign: 'start' },\n labelEnd: { justifySelf: 'end', textAlign: 'end' },\n // 没有标签时控件仍放在第二列,与上下的字段对齐\n controlWithoutLabel: { gridColumnStart: '2' },\n control: { display: 'flex', flexDirection: 'column', gap: '_xs', minWidth: 0 },\n hint: { fontSize: '_sm', lineHeight: '_normal' },\n error: { display: 'inline-flex', alignItems: 'flex-start', gap: '_xs' },\n errorIcon: { flexShrink: 0, marginBlock: '0.25em' },\n});\n\n/** Field 的动态样式:标签列的宽度 */\nexport const fieldColumnStyles = css.create({\n columns: (labelWidth: string) => ({ gridTemplateColumns: `${labelWidth} minmax(0, 1fr)` }),\n});\n\n/** 表单字段:标签、控件、说明、错误信息,自动关联 id 与状态 */\nexport const Field = _component(\n ({\n name,\n valueProp,\n label,\n description,\n error,\n invalid,\n required,\n disabled,\n readonly,\n labelPlacement,\n labelWidth,\n labelAlign,\n labelHidden = false,\n propsLabel,\n propsControl,\n propsDescription,\n propsError,\n children,\n ...rest\n }: FieldProps) => {\n const id = useId();\n const ids = {\n control: `${id}-control`,\n label: `${id}-label`,\n description: `${id}-description`,\n error: `${id}-error`,\n };\n const form = FormContext.inject(null);\n const layout = FieldLayoutContext.inject(null);\n // error={cond && '…'} 条件不成立时是 false:当作没写,用表单的错误\n const message = () =>\n hasContent(error) ? error : name != null ? form?.visibleError(name) : undefined;\n const showError = () => hasContent(message()) && invalid !== false;\n const constraints = () => (name != null && form ? form.constraints(name) : NO_CONSTRAINTS);\n const isRequired = () => required ?? constraints().required ?? false;\n const isDisabled = () => disabled ?? layout?.disabled ?? false;\n const isReadonly = () => readonly ?? layout?.readonly ?? false;\n\n // 第一个接入的控件认领 id 与标签(见 FieldState.claim)\n let owner: symbol | null = null;\n FieldContext.provide({\n claim: (token) => {\n owner ??= token;\n return owner === token;\n },\n release: (token) => {\n if (owner === token) owner = null;\n },\n controlId: ids.control,\n get labelId() {\n return hasContent(label) ? ids.label : undefined;\n },\n get describedBy() {\n const parts = [hasContent(description) && ids.description, showError() && ids.error];\n return parts.filter(Boolean).join(' ') || undefined;\n },\n get invalid() {\n return invalid ?? hasContent(message());\n },\n get required() {\n return isRequired();\n },\n get constraints() {\n return constraints();\n },\n get disabled() {\n return isDisabled();\n },\n get readonly() {\n return isReadonly();\n },\n });\n\n // 登记给表单:错误汇总据此显示字段名、跳到控件\n _effect(() =>\n name != null && form\n ? form.register(name, { label: plainText(label) ?? name, controlId: ids.control })\n : undefined,\n );\n\n /** 只有一个控件、控件上没写值时,自动绑定表单的这个字段(同 Ant Design Form.Item 的 name) */\n function bindValue(content: VNodeChild): VNodeChild {\n const control = name != null && form ? soleControl(content) : undefined;\n if (!control || !form) return content;\n const key =\n valueProp ??\n (declares(control, '_checked') && !declares(control, '_value') ? '_checked' : '_value');\n if (control.props && key in control.props) return content;\n const path = name!;\n return cloneVNode(control, {\n [key]: form.getValue(path),\n [`onUpdate:${key}`]: (value: unknown) => form.setValue(path, value),\n });\n }\n\n // 焦点离开整个字段(不是在字段内部移动)时报告失焦\n function onFocusout(event: FocusEvent) {\n const next = event.relatedTarget;\n const field = event.currentTarget as HTMLElement;\n if (name != null && !(next instanceof Node && field.contains(next))) form?.blur(name);\n }\n\n return () => {\n const placement = labelPlacement ?? layout?.labelPlacement ?? 'top';\n const start = placement === 'start' && !labelHidden;\n const width = labelWidth ?? layout?.labelWidth ?? DEFAULT_LABEL_WIDTH;\n const align = labelAlign ?? layout?.labelAlign ?? 'end';\n const hints = [\n hasContent(description) && (\n <Text\n {...mergePartProps<TextProps>(\n { tone: 'muted', sx: fieldStyles.hint },\n propsDescription,\n {\n id: ids.description,\n },\n )}\n >\n {description}\n </Text>\n ),\n showError() && (\n <Text\n {...mergePartProps<TextProps>(\n { tone: 'danger', sx: [fieldStyles.hint, fieldStyles.error] },\n propsError,\n { id: ids.error },\n )}\n >\n <Icon icon={statusIcons.danger} sx={fieldStyles.errorIcon} />\n {message()}\n </Text>\n ),\n ];\n return (\n <div\n {...mergeAttrs<'div'>(\n {\n sx: start\n ? [\n fieldStyles.start,\n fieldColumnStyles.columns(typeof width === 'number' ? `${width}px` : width),\n ]\n : fieldStyles.root,\n },\n rest,\n { onFocusout },\n )}\n data-invalid={(invalid ?? message() != null) || undefined}\n data-disabled={isDisabled() || undefined}\n data-label-placement={placement}\n >\n {hasContent(label) && (\n <Label\n {...mergePartProps<LabelProps>(\n {\n required: isRequired(),\n disabled: isDisabled(),\n sx: labelHidden\n ? visuallyHiddenStyles.hidden\n : start && (align === 'end' ? fieldStyles.labelEnd : fieldStyles.labelStart),\n },\n propsLabel,\n { id: ids.label, for: ids.control },\n )}\n >\n {label}\n </Label>\n )}\n {start ? (\n <div\n {...mergeAttrs<'div'>(\n {\n sx: [fieldStyles.control, !hasContent(label) && fieldStyles.controlWithoutLabel],\n },\n propsControl,\n )}\n >\n {bindValue(children)}\n {hints}\n </div>\n ) : (\n [bindValue(children), hints]\n )}\n </div>\n );\n };\n },\n);\n"],"mappings":"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"}
|
package/dist/form/useForm.d.ts
CHANGED
|
@@ -137,7 +137,10 @@ export interface FormControllerBase {
|
|
|
137
137
|
* 返回它们是否都通过(分步表单进下一步前用)
|
|
138
138
|
*/
|
|
139
139
|
validate(paths?: readonly string[]): Promise<boolean>;
|
|
140
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* 提交:先校验,通过后调用 onSubmit;返回是否提交成功。校验没通过,或 onSubmit 抛了带业务错误码的错误(后端回的
|
|
142
|
+
* 校验失败、数据已被修改等,按字段的明细标到字段上,其余是 formError)时为 false;别的错误原样抛出
|
|
143
|
+
*/
|
|
141
144
|
submit(): Promise<boolean>;
|
|
142
145
|
/** 回到初始值,清空错误与失焦记录 */
|
|
143
146
|
reset(): void;
|
package/dist/form/useForm.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useLang } from "../config/context.js";
|
|
2
2
|
import { sameValue } from "../utils/data/equality.js";
|
|
3
3
|
import { Field } from "./Field.js";
|
|
4
|
+
import { codeText, errorCodeOf, errorMessage } from "../utils/request/error-message.js";
|
|
4
5
|
import { createConstraints } from "./form-constraints.js";
|
|
5
|
-
import {
|
|
6
|
+
import { cloneValue } from "../utils/data/clone.js";
|
|
7
|
+
import { computed, reactive, shallowReactive, watch } from "vue";
|
|
6
8
|
import { safeParseAsync } from "zod/v4/core";
|
|
7
9
|
//#region src/form/useForm.ts
|
|
8
10
|
/** 字段路径 path 是否在 scope 里:就是它,或是它的子路径('address' 包含 'address.city') */
|
|
@@ -50,18 +52,8 @@ function shiftIndices(key, prefix, map) {
|
|
|
50
52
|
].join(".");
|
|
51
53
|
}
|
|
52
54
|
var nextArrayKey = 0;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
* 不用 structuredClone:Decimal、@internationalized/date 的日期这些带方法的值克隆不了(会抛错),
|
|
56
|
-
* 它们本身不可变,共用同一个实例没有问题
|
|
57
|
-
*/
|
|
58
|
-
function cloneValue(value) {
|
|
59
|
-
const raw = toRaw(value);
|
|
60
|
-
if (Array.isArray(raw)) return raw.map(cloneValue);
|
|
61
|
-
if (raw === null || typeof raw !== "object") return raw;
|
|
62
|
-
const prototype = Object.getPrototypeOf(raw);
|
|
63
|
-
if (prototype !== Object.prototype && prototype !== null) return raw;
|
|
64
|
-
return Object.fromEntries(Object.entries(raw).map(([key, item]) => [key, cloneValue(item)]));
|
|
55
|
+
function isFieldIssue(item) {
|
|
56
|
+
return typeof item === "object" && item !== null && typeof item.field === "string";
|
|
65
57
|
}
|
|
66
58
|
/**
|
|
67
59
|
* 创建表单控制器(组件 setup 里或模块顶层都可以)。
|
|
@@ -172,6 +164,31 @@ function useForm(options) {
|
|
|
172
164
|
output
|
|
173
165
|
};
|
|
174
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* onSubmit 抛的带业务错误码的错误(后端回的、请求层的,见 utils/request/error-message.ts)标到表单上:
|
|
169
|
+
* errors 明细里带 field 的标到字段(文案按明细的码取,没有时用它的 message),一项都没有时整份表单的错误是错误的文案。
|
|
170
|
+
* 和 setError 一样,值改了就不再显示;下一次提交开始时清掉(交给后端重新判断:网络断了之后不改值也能再提交)。
|
|
171
|
+
* 认不出的错误返回 false,由 submit 原样抛出
|
|
172
|
+
*/
|
|
173
|
+
function markSubmitError(error) {
|
|
174
|
+
if (errorCodeOf(error) === void 0) return false;
|
|
175
|
+
const details = error.errors;
|
|
176
|
+
let marked = false;
|
|
177
|
+
for (const item of Array.isArray(details) ? details : []) {
|
|
178
|
+
if (!isFieldIssue(item)) continue;
|
|
179
|
+
const text = item.code && codeText(item.code, item.params, lang) || item.message || item.code;
|
|
180
|
+
controller.setError(item.field, text ?? lang.requestFailed);
|
|
181
|
+
submitErrorPaths.add(item.field);
|
|
182
|
+
marked = true;
|
|
183
|
+
}
|
|
184
|
+
if (!marked) {
|
|
185
|
+
controller.setError("", errorMessage(error, lang));
|
|
186
|
+
submitErrorPaths.add("");
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
/** 上一次提交失败时标上的路径 */
|
|
191
|
+
const submitErrorPaths = /* @__PURE__ */ new Set();
|
|
175
192
|
/** setError 给的错误,值改过就不算了;'' 是整份表单的错误,任一字段改了都不算 */
|
|
176
193
|
function externalError(path) {
|
|
177
194
|
const entry = state.externalErrors[path];
|
|
@@ -317,11 +334,16 @@ function useForm(options) {
|
|
|
317
334
|
submit() {
|
|
318
335
|
submission ??= (async () => {
|
|
319
336
|
state.submitCount++;
|
|
337
|
+
for (const path of submitErrorPaths) controller.setError(path, void 0);
|
|
338
|
+
submitErrorPaths.clear();
|
|
320
339
|
const valid = await check(true);
|
|
321
340
|
if (!valid || !onSubmit) return valid;
|
|
322
341
|
state.submitting = true;
|
|
323
342
|
try {
|
|
324
343
|
await onSubmit(parsed);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
if (!markSubmitError(error)) throw error;
|
|
346
|
+
return false;
|
|
325
347
|
} finally {
|
|
326
348
|
state.submitting = false;
|
|
327
349
|
}
|
package/dist/form/useForm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useForm.js","names":[],"sources":["../../src/form/useForm.ts"],"sourcesContent":["/**\n * 表单控制器:值、校验、错误、提交状态放在一个对象里,交给 <Form controller>(COMPONENTS.md §1 的“命令式能力”)。\n * 校验用 zod(使用方安装的 peer 依赖,zod 与 zod/mini 都可以),也可以写 validate 函数。\n * 参考 TanStack Form、React Hook Form 的做法,取其中最常用的部分。\n *\n * const form = useForm({\n * schema: z.object({ email: z.email('邮箱格式不对'), age: z.coerce.number().int().min(18, '未满 18 岁') }),\n * initial: { email: '', age: '' }, // 类型是 schema 的输入(z.input)\n * onSubmit: (values) => api.signup(values), // 拿到解析后的输出(z.output):age 已是数字\n * });\n * <Form controller={form}>\n * <FormErrorSummary /> // 提交没通过时在顶上列出各项错误,点了跳到字段\n * <form.Field name=\"email\" label=\"邮箱\"><TextField /></form.Field> // 只有一个控件时自动绑定值\n * <Button type=\"submit\" loading={form.submitting}>登录</Button>\n * </Form>\n *\n * form.Field 里只有一个控件、控件上没写值时,自动绑定 form.values 的这个字段(同 Ant Design Form.Item 的 name):\n * 写回的是 _value;声明了 _checked 而没有 _value 的控件(Switch、单个 Checkbox)写回 _checked,也可以用 Field 的 valueProp 指定。\n * 自己写了 _value={form.values.email} 时照旧。\n *\n * 何时显示错误(“迟一点批评、早一点肯定”):字段失焦后才显示它的错误,之后它随输入实时更新;提交过一次之后,所有字段随输入实时更新。\n * 不属于哪个字段的错误(对象级 refine 没写 path、validate 返回的 '' 键、setError('', …))是整份表单的错误,见 formError。\n * 字段的值经 v-model 写回,中文输入法组合输入期间不写回,所以组合中途不会触发校验。\n *\n * schema 还决定字段的约束(form-constraints.ts):标签的必填标记、输入框的 maxlength、NumberField 的 min / max\n * 都从 schema 来,不用在控件上再写一遍。\n *\n * - 数组字段:`form.array('items')` 增删、插入、移动,给每项一个稳定的 key;错误与失焦记录跟着下标移动。\n * - 单字段异步校验:`fields: { username: { validate: (name, values, signal) => api.taken(name, { signal }) } }`,\n * 这个字段变化时防抖运行、只采用最后一次的结果,提交时等它完成;`form.validating('username')` 显示校验中。\n * - 改动:`form.dirty` 是值与初始值(或上次 reset 的值)不同;未保存离开的提醒用 useLeaveConfirmation(() => form.dirty)。\n * - 局部校验:`form.validate(['name', 'address'])` 只校验这些字段(含子路径)并显示它们的错误,分步表单进下一步前用。\n * - 字段联动:`dependencies: { confirm: ['password'] }`,password 改了重新校验 confirm(confirm 填过或失焦过时显示它的错误),\n * confirm 的单字段校验也跟着重跑。\n */\nimport type { TsxComponent } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport { computed, reactive, shallowReactive, toRaw, watch } from 'vue';\nimport { safeParseAsync, type $ZodIssue, type $ZodRawIssue, type $ZodType } from 'zod/v4/core';\nimport { useLang } from '../config/context';\nimport { Field, type FieldProps } from './Field';\nimport { createConstraints, type FieldConstraints } from './form-constraints';\nimport { sameValue } from '../utils/data/equality';\n\n/** 字段路径 → 错误信息;路径用点连接(`address.city`、`items.0.name`) */\nexport type FormErrors = Partial<Record<string, string>>;\n\n/** 当作一个整体的值:不再往里展开路径 */\ntype FieldLeaf = string | number | boolean | bigint | symbol | null | undefined | Date | Blob;\n\n/**\n * 值里的字段路径:点连接,数组下标是数字(`email`、`address.city`、`items.0.name`)。\n * 最多展开 6 层,再深的结构写成 string 也照样能用(Field 的 name、setError 都接受)\n */\nexport type FieldPath<Values, Depth extends unknown[] = []> = Depth['length'] extends 6\n ? never\n : Values extends readonly (infer Item)[]\n ? `${number}` | `${number}.${FieldPath<Item, [...Depth, unknown]>}`\n : Values extends object\n ? {\n [K in keyof Values & string]:\n | K\n | (NonNullable<Values[K]> extends FieldLeaf\n ? never\n : `${K}.${FieldPath<NonNullable<Values[K]>, [...Depth, unknown]>}`);\n }[keyof Values & string]\n : never;\n\n/** 字段路径 P 处的值的类型(`PathValue<Values, 'items.0.name'>` → string) */\nexport type PathValue<Values, P extends string> = P extends `${infer Key}.${infer Rest}`\n ? Values extends readonly (infer Item)[]\n ? PathValue<NonNullable<Item>, Rest>\n : Key extends keyof Values\n ? PathValue<NonNullable<Values[Key]>, Rest>\n : never\n : Values extends readonly (infer Item)[]\n ? Item\n : P extends keyof Values\n ? Values[P]\n : never;\n\n/** 值是数组的字段路径:form.array 只接受这些 */\nexport type ArrayPath<Values> = {\n [P in FieldPath<Values>]: NonNullable<PathValue<Values, P>> extends readonly unknown[]\n ? P\n : never;\n}[FieldPath<Values>];\n\n/** 单个字段的校验:只在这个字段变化时运行(防抖),适合要请求接口的“用户名已占用” */\nexport interface FieldValidator<Value, Values> {\n /** 返回错误信息,通过时返回 undefined;signal 在值又变了、表单重置时中止 */\n validate: (\n value: Value,\n values: Values,\n signal: AbortSignal,\n ) => string | undefined | Promise<string | undefined>;\n /** 值停止变化多久(毫秒)后运行,默认 300 */\n debounce?: number;\n}\n\n/** 字段路径 path 是否在 scope 里:就是它,或是它的子路径('address' 包含 'address.city') */\nconst inScope = (path: string, scope: readonly string[]) =>\n scope.some((prefix) => path === prefix || path.startsWith(`${prefix}.`));\n\n/** 数组字段的操作:form.array(path) 的返回值 */\nexport interface FieldArray<Item> {\n /** 当前的各项(就是 values 里的数组) */\n readonly items: readonly Item[];\n /** 每项一个稳定的 key:渲染列表时用,增删、移动后不变 */\n readonly keys: readonly string[];\n /** 加到末尾 */\n append(item: Item): void;\n /** 插到 index 处 */\n insert(index: number, item: Item): void;\n /** 删掉 index 处的一项 */\n remove(index: number): void;\n /** 把 from 处的一项移到 to 处 */\n move(from: number, to: number): void;\n}\n\n/** 什么时候开始校验;提交过一次之后总是随输入更新 */\nexport interface FormValidateTimings {\n /** 失焦后(默认) */\n blur: unknown;\n /** 输入时 */\n change: unknown;\n /** 只在提交时 */\n submit: unknown;\n}\n\n/** useForm 的选项;Values 是表单里的值(有 schema 时是它的输入类型),Output 是校验通过后交给 onSubmit 的值 */\nexport interface FormOptions<Values extends object, Output = Values> {\n /** 初始值;reset() 回到它 */\n initial: Values;\n /** zod schema:整份值一起校验,issue 的 path 决定错误挂在哪个字段上;字段约束也从它推出 */\n schema?: $ZodType<Output, Values>;\n /** 自定义校验:返回各字段的错误(可以是 Promise),与 schema 的结果合并 */\n validate?: (values: Values) => FormErrors | Promise<FormErrors>;\n /**\n * 单字段异步校验:按字段路径给出,这个字段变化时防抖运行,结果与 schema 的合并(schema 的错误优先)。\n * NoInfer:Values 只从 initial 与 schema 推出,validate 的参数类型才能跟着推出来\n */\n fields?: NoInfer<{ [P in FieldPath<Values>]?: FieldValidator<PathValue<Values, P>, Values> }>;\n /** 什么时候开始校验,默认 blur;提交过一次之后总是随输入更新 */\n validateOn?: keyof FormValidateTimings;\n /**\n * 字段联动:字段 → 它依赖的字段。依赖的值改了就重新校验它(它填过或失焦过时显示错误),它的单字段校验也重跑。\n * 如 `{ confirm: ['password'], endDate: ['startDate'] }`\n */\n dependencies?: NoInfer<{ [P in FieldPath<Values>]?: readonly FieldPath<Values>[] }>;\n /** 校验通过后调用,拿到 schema 解析后的值(转换、默认值、coerce 都已生效);返回 Promise 时 submitting 一直为 true 直到完成 */\n onSubmit?: (values: Output) => unknown;\n}\n\n/** 控制器里与值的类型无关的部分:Form、Field 只用到它,任何 FormController 都能交给 <Form controller> */\nexport interface FormControllerBase {\n /** 所有字段当前的校验错误(含还没显示的);整份表单的错误在 '' 键上 */\n readonly errors: Readonly<FormErrors>;\n /** 整份表单的错误(不属于哪个字段),提交过之后才有;放在提交按钮旁或表单顶上 */\n readonly formError: string | undefined;\n /** 正在执行 onSubmit */\n readonly submitting: boolean;\n /** 提交过几次(不论成功与否) */\n readonly submitCount: number;\n /** 重置过几次:控件据此丢掉自己还没写回的输入(useFormReset) */\n readonly resetCount: number;\n /** 值与初始值(或上次 reset 的值)不同 */\n readonly dirty: boolean;\n /** 字段(不写时为任一字段)的单字段校验正在进行 */\n validating(path?: string): boolean;\n /** 字段的错误是否该显示:失焦过、或提交过之后才显示;Field 用它 */\n visibleError(path: string): string | undefined;\n /** 字段失焦:Field 在焦点离开字段时调用 */\n blur(path: string): void;\n /**\n * 校验整份表单,返回是否通过。给了 paths 时只看这些字段(含子路径):它们的错误立即显示,\n * 返回它们是否都通过(分步表单进下一步前用)\n */\n validate(paths?: readonly string[]): Promise<boolean>;\n /** 提交:先校验,通过后调用 onSubmit;返回是否通过校验 */\n submit(): Promise<boolean>;\n /** 回到初始值,清空错误与失焦记录 */\n reset(): void;\n /** 字段从 schema 推出的约束(必填、长度、范围…);Field 交给控件,控件上显式写的优先 */\n constraints(path: string): FieldConstraints;\n /** 字段的当前值(按点连接的路径):Field 自动绑定值时用 */\n getValue(path: string): unknown;\n /** 写字段的值:中途是 null / undefined 时什么也不做 */\n setValue(path: string, value: unknown): void;\n /** 写了 name 的 Field 登记标签与控件的 id:错误汇总据此显示字段名、跳到控件。返回注销函数 */\n register(path: string, field: FormFieldInfo): () => void;\n /** 登记过的字段,按登记(渲染)的顺序 */\n readonly fields: ReadonlyMap<string, FormFieldInfo>;\n}\n\n/** Field 登记的字段信息 */\nexport interface FormFieldInfo {\n /** 标签的文字 */\n label: string;\n /** 控件的 id:错误汇总的链接指向它 */\n controlId: string;\n}\n\n/** 表单控制器:useForm 的返回值 */\nexport interface FormController<Values extends object> extends FormControllerBase {\n /** 当前值(响应式):控件写 _value={form.values.email} */\n readonly values: Values;\n /** 回到初始值(或给定的值,之后 dirty 以它为准),清空错误与失焦记录;保存成功后写 reset(form.values) */\n reset(values?: Values): void;\n /** 字段与初始值不同 */\n isDirty(path: FieldPath<Values>): boolean;\n /** 校验整份表单,或只校验 paths 里的字段(含子路径)并显示它们的错误;返回是否通过 */\n validate(paths?: readonly FieldPath<Values>[]): Promise<boolean>;\n /** 数组字段的增删、插入、移动 */\n array<P extends ArrayPath<Values>>(\n path: P,\n ): FieldArray<NonNullable<PathValue<Values, P>> extends readonly (infer Item)[] ? Item : never>;\n /**\n * 设置某个字段的错误(如服务端返回的“邮箱已注册”),undefined 清除;'' 是整份表单的错误。\n * 校验不会冲掉它,这个字段(整份表单的错误是任一字段)的值改了之后自动消失\n */\n setError(path: FieldPath<Values> | '', message: string | undefined): void;\n /**\n * 就是 Field 组件,只是 name 的类型收窄为这份值的字段路径,写错字段名时类型检查报错:\n * `<form.Field name=\"email\" label=\"邮箱\">…</form.Field>`(参考 TanStack Form 的 form.Field)\n */\n readonly Field: TsxComponent<Omit<FieldProps, 'name'> & { name?: FieldPath<Values> }>;\n}\n\n/** zod issue 的路径 → 点连接的字符串 */\nconst issuePath = (issue: $ZodIssue) => issue.path.map(String).join('.');\n\n/**\n * 该填的没有填:类型不对且是空值(null、undefined,包括缺少的键),或长度不够且是空字符串、空数组。\n * 选择、数字、日期控件清空后是 null(COMPONENTS.md §1 空值),z.string() 遇到它报的是类型不对,读起来应当是“必填”\n */\nfunction isBlankIssue({ code, input }: $ZodRawIssue): boolean {\n if (code === 'invalid_type') return input == null;\n if (code === 'too_small') return input === '' || (Array.isArray(input) && input.length === 0);\n return false;\n}\n\n/** 按点连接的路径写值;中途是 null / undefined 时什么也不做 */\nfunction writePath(target: unknown, path: string, value: unknown): void {\n const keys = path.split('.');\n const parent = readPath(target, keys.slice(0, -1).join('.'));\n if (keys.length === 1) (target as Record<string, unknown>)[keys[0]!] = value;\n else if (parent != null) (parent as Record<string, unknown>)[keys.at(-1)!] = value;\n}\n\n/** 按点连接的路径取值;中途是 null / undefined 时为 undefined */\nfunction readPath(source: unknown, path: string): unknown {\n let current = source;\n for (const key of path.split('.')) {\n if (current == null) return undefined;\n current = (current as Record<string, unknown>)[key];\n }\n return current;\n}\n\n/**\n * 数组字段的下标变了:`prefix.旧下标…` 的键改成 `prefix.新下标…`,map 返回 undefined 的(删掉的项)丢弃\n */\nfunction shiftIndices(key: string, prefix: string, map: (index: number) => number | undefined) {\n if (!key.startsWith(`${prefix}.`)) return key;\n const rest = key.slice(prefix.length + 1);\n const [head, ...tail] = rest.split('.');\n const index = Number(head);\n if (!Number.isInteger(index)) return key;\n const next = map(index);\n return next === undefined ? undefined : [prefix, next, ...tail].join('.');\n}\n\nlet nextArrayKey = 0;\n\n/** 一个字段的单字段校验的进行状态 */\ninterface FieldRun {\n /** 防抖的定时器 */\n timer?: ReturnType<typeof setTimeout>;\n /** 正在校验的那次 */\n running?: { value: unknown; controller: AbortController; done: Promise<void> };\n /** 上次校验完成时的值 */\n checked?: unknown;\n}\n\n/**\n * 复制表单的值:普通对象与数组逐层复制(各份互不影响),其余值原样保留。\n * 不用 structuredClone:Decimal、@internationalized/date 的日期这些带方法的值克隆不了(会抛错),\n * 它们本身不可变,共用同一个实例没有问题\n */\nfunction cloneValue<T>(value: T): T {\n const raw = toRaw(value);\n if (Array.isArray(raw)) return raw.map(cloneValue) as T;\n if (raw === null || typeof raw !== 'object') return raw;\n const prototype = Object.getPrototypeOf(raw);\n if (prototype !== Object.prototype && prototype !== null) return raw;\n return Object.fromEntries(Object.entries(raw).map(([key, item]) => [key, cloneValue(item)])) as T;\n}\n\n/**\n * 创建表单控制器(组件 setup 里或模块顶层都可以)。\n * 有 schema 时值的类型取自 schema(initial 要符合它的输入类型),onSubmit 拿到解析后的输出\n */\nexport function useForm<Values extends object, Output = Values>(\n options: FormOptions<Values, Output>,\n): FormController<Values> {\n const { schema, validateOn = 'blur', onSubmit } = options;\n const lang = useLang();\n // zod 的报错跟随语言包;schema 里自己写的提示优先于它(zod 的规则)\n const parseOptions = {\n error: (issue: $ZodRawIssue) =>\n isBlankIssue(issue) ? lang.formRequired : lang.formIssues(issue),\n };\n const fieldValidators = Object.entries(options.fields ?? {}) as [\n string,\n FieldValidator<unknown, Values>,\n ][];\n const values = reactive(cloneValue(options.initial)) as Values;\n const state = shallowReactive({\n errors: {} as FormErrors,\n // 单字段校验的结果,与 errors 分开存:整份校验不会冲掉还在路上的单字段结果\n fieldErrors: {} as FormErrors,\n // setError 给的错误与当时的值:值一改就不再算\n externalErrors: {} as Record<string, { message: string; value: unknown }>,\n touched: new Set<string>(),\n pending: new Set<string>(),\n submitting: false,\n submitCount: 0,\n resetCount: 0,\n // dirty 的比较基准:初始值,或上次 reset 的值\n baseline: cloneValue(options.initial),\n });\n // 异步校验可能乱序返回:只采用最后一次的结果\n let run = 0;\n // 正在进行的提交:校验、onSubmit 期间再次提交(连按回车)拿到同一个结果,不重复调用 onSubmit\n let submission: Promise<boolean> | undefined;\n // 最近一次通过校验时 schema 解析出的值,交给 onSubmit\n let parsed: Output | undefined;\n const constraints = createConstraints(schema);\n const dirty = computed(() => !sameValue(values, state.baseline));\n // 数组字段每项的 key,与数组等长;数组被外部整个换掉(长度不同)时重新生成\n const arrayKeys = new Map<string, string[]>();\n\n // 写了 name 的 Field:错误汇总按它显示字段名、跳到控件\n const fields = shallowReactive(new Map<string, FormFieldInfo>());\n\n /** 单字段校验的进行状态:每个字段一份 */\n const fieldRuns = new Map<string, FieldRun>();\n\n function setPending(path: string, pending: boolean) {\n if (state.pending.has(path) === pending) return;\n const next = new Set(state.pending);\n if (pending) next.add(path);\n else next.delete(path);\n state.pending = next;\n }\n\n /**\n * 运行一个字段的校验。同一个值正在校验时等它;上次校验的就是这个值时直接用上次的结果;\n * 值变了就中止还在路上的那次(只采用最后一次的结果)\n */\n function runField(path: string, validator: FieldValidator<unknown, Values>): Promise<void> {\n const entry = fieldRuns.get(path) ?? {};\n fieldRuns.set(path, entry);\n clearTimeout(entry.timer);\n // 依赖的字段也算进“这次校验的输入”:依赖变了,同一个值也要重新校验\n const value = cloneValue([\n readPath(values, path),\n ...dependsOn(path).map((dep) => readPath(values, dep)),\n ]);\n if (entry.running && sameValue(entry.running.value, value)) return entry.running.done;\n if (!entry.running && 'checked' in entry && sameValue(entry.checked, value)) {\n return Promise.resolve();\n }\n entry.running?.controller.abort();\n const controller = new AbortController();\n setPending(path, true);\n const done = (async () => {\n try {\n const message = await validator.validate(value[0], values, controller.signal);\n if (controller.signal.aborted) return;\n entry.checked = value;\n state.fieldErrors = { ...state.fieldErrors, [path]: message };\n } catch (error) {\n // 校验本身失败(接口出错):当作没有结论,不挡提交(服务端还会再校验),但要报出来\n if (!controller.signal.aborted) console.error(`useForm:字段 ${path} 的校验出错`, error);\n } finally {\n if (entry.running?.controller === controller) {\n entry.running = undefined;\n setPending(path, false);\n }\n }\n })();\n entry.running = { value, controller, done };\n return done;\n }\n\n function cancelFieldRuns() {\n for (const entry of fieldRuns.values()) {\n clearTimeout(entry.timer);\n entry.running?.controller.abort();\n }\n fieldRuns.clear();\n state.pending = new Set();\n state.fieldErrors = {};\n }\n\n /** 字段依赖的字段 */\n const dependsOn = (path: string): readonly string[] =>\n (options.dependencies as Partial<Record<string, readonly string[]>> | undefined)?.[path] ?? [];\n\n /**\n * 校验整份值:schema 与 validate 的错误(errors)。full 时单字段校验也立刻跑完(提交、显式调用 validate;\n * 给了 scope 时只跑 scope 里的),结果在 state.fieldErrors;失焦、输入时只算 schema 与 validate,\n * 单字段校验照旧防抖、只在它自己(或它依赖的字段)的值变了时跑\n */\n async function collect(\n full: boolean,\n scope?: readonly string[],\n ): Promise<{ errors: FormErrors; output: Output }> {\n const errors: FormErrors = {};\n let output = values as unknown as Output;\n if (schema) {\n const result = await safeParseAsync(schema, values, parseOptions);\n if (result.success) output = result.data;\n for (const issue of result.error?.issues ?? []) {\n errors[issuePath(issue)] ??= issue.message;\n }\n }\n if (options.validate) {\n for (const [path, message] of Object.entries(await options.validate(values))) {\n if (message) errors[path] ??= message;\n }\n }\n // 单字段校验:不等防抖,立刻跑完(值没变的直接用上次结果)\n if (full) {\n await Promise.all(\n fieldValidators\n .filter(([path]) => !scope || inScope(path, scope))\n .map(([path, validator]) => runField(path, validator)),\n );\n }\n return { errors, output };\n }\n\n /** setError 给的错误,值改过就不算了;'' 是整份表单的错误,任一字段改了都不算 */\n function externalError(path: string): string | undefined {\n const entry = state.externalErrors[path];\n if (!entry) return undefined;\n const current = path === '' ? values : readPath(values, path);\n return sameValue(current, entry.value) ? entry.message : undefined;\n }\n\n /** schema 与 validate 的错误优先,再补上 setError 与单字段校验的 */\n function mergedErrors(): FormErrors {\n const merged = { ...state.errors };\n for (const path of Object.keys(state.externalErrors)) {\n const message = externalError(path);\n if (message) merged[path] ??= message;\n }\n for (const [path, message] of Object.entries(state.fieldErrors)) {\n if (message) merged[path] ??= message;\n }\n return merged;\n }\n\n /**\n * 校验并记下结果(只采用最后一次);返回是否通过:算出的、setError 给的、单字段校验的都没有错误。\n * 给了 scope 时只看 scope 里的字段,别的字段的错误保持原样\n */\n async function check(full: boolean, scope?: readonly string[]): Promise<boolean> {\n const current = ++run;\n const { errors, output } = await collect(full, scope);\n if (!scope) {\n if (current === run) {\n state.errors = errors;\n parsed = output;\n }\n return Object.keys(errors).length === 0 && Object.keys(mergedErrors()).length === 0;\n }\n const scoped = (source: FormErrors) =>\n Object.entries(source).filter(([path, message]) => message && inScope(path, scope));\n if (current === run) {\n state.errors = {\n ...Object.fromEntries(\n Object.entries(state.errors).filter(([path]) => !inScope(path, scope)),\n ),\n ...Object.fromEntries(scoped(errors)),\n };\n }\n return scoped(errors).length === 0 && scoped(mergedErrors()).length === 0;\n }\n\n /** 把这些字段记为失焦过:它们的错误开始显示 */\n function touch(paths: readonly string[]) {\n const next = new Set(state.touched);\n for (const path of paths) next.add(path);\n if (next.size !== state.touched.size) state.touched = next;\n }\n\n /** 数组字段的下标变了:错误、失焦记录、单字段校验的结果跟着移 */\n function remapIndices(prefix: string, map: (index: number) => number | undefined) {\n const remap = (errors: FormErrors) =>\n Object.fromEntries(\n Object.entries(errors).flatMap(([key, message]) => {\n const next = shiftIndices(key, prefix, map);\n return next === undefined ? [] : [[next, message]];\n }),\n ) as FormErrors;\n state.errors = remap(state.errors);\n state.fieldErrors = remap(state.fieldErrors);\n state.externalErrors = Object.fromEntries(\n Object.entries(state.externalErrors).flatMap(([key, entry]) => {\n const next = shiftIndices(key, prefix, map);\n return next === undefined ? [] : [[next, entry]];\n }),\n );\n state.touched = new Set(\n [...state.touched].flatMap((key) => shiftIndices(key, prefix, map) ?? []),\n );\n }\n\n function fieldArray(path: string): FieldArray<unknown> {\n const list = () => (readPath(values, path) as unknown[] | undefined) ?? [];\n const keys = () => {\n let current = arrayKeys.get(path);\n if (current?.length !== list().length) {\n current = list().map(() => `item-${++nextArrayKey}`);\n arrayKeys.set(path, current);\n }\n return current;\n };\n /** 改数组与 key:splice 的参数同时作用在两者上 */\n const splice = (start: number, deleteCount: number, ...items: unknown[]) => {\n keys().splice(start, deleteCount, ...items.map(() => `item-${++nextArrayKey}`));\n list().splice(start, deleteCount, ...items);\n };\n return {\n get items() {\n return list();\n },\n get keys() {\n return keys();\n },\n append: (item) => splice(list().length, 0, item),\n insert(index, item) {\n remapIndices(path, (old) => (old >= index ? old + 1 : old));\n splice(index, 0, item);\n },\n remove(index) {\n remapIndices(path, (old) => (old === index ? undefined : old > index ? old - 1 : old));\n splice(index, 1);\n },\n move(from, to) {\n if (from === to) return;\n remapIndices(path, (old) => {\n if (old === from) return to;\n if (from < to && old > from && old <= to) return old - 1;\n if (from > to && old >= to && old < from) return old + 1;\n return old;\n });\n const [key] = keys().splice(from, 1);\n const [item] = list().splice(from, 1);\n keys().splice(to, 0, key!);\n list().splice(to, 0, item);\n },\n };\n }\n\n const controller: FormController<Values> = {\n values,\n get errors() {\n return mergedErrors();\n },\n get formError() {\n if (state.submitCount === 0) return undefined;\n return state.errors[''] ?? externalError('');\n },\n get submitting() {\n return state.submitting;\n },\n get resetCount() {\n return state.resetCount;\n },\n get submitCount() {\n return state.submitCount;\n },\n get dirty() {\n return dirty.value;\n },\n validating(path) {\n return path === undefined ? state.pending.size > 0 : state.pending.has(path);\n },\n visibleError(path) {\n // 失焦过的字段,或局部校验过的字段(含子路径:validate(['address']) 之后 address.city 也显示)\n if (state.submitCount === 0 && !inScope(path, [...state.touched])) return undefined;\n return state.errors[path] ?? externalError(path) ?? state.fieldErrors[path];\n },\n blur(path) {\n if (state.touched.has(path)) return;\n state.touched = new Set(state.touched).add(path);\n if (validateOn !== 'submit') void check(false);\n },\n validate(paths) {\n if (!paths) return check(true);\n touch(paths);\n return check(true, paths);\n },\n submit() {\n submission ??= (async () => {\n state.submitCount++;\n const valid = await check(true);\n if (!valid || !onSubmit) return valid;\n state.submitting = true;\n try {\n await onSubmit(parsed as Output);\n } finally {\n state.submitting = false;\n }\n return true;\n })().finally(() => (submission = undefined));\n return submission;\n },\n reset(next = options.initial) {\n run++;\n const snapshot = cloneValue(next);\n cancelFieldRuns();\n Object.assign(values, cloneValue(snapshot));\n arrayKeys.clear();\n state.baseline = snapshot;\n state.errors = {};\n state.externalErrors = {};\n state.touched = new Set();\n state.submitCount = 0;\n state.resetCount++;\n },\n isDirty(path) {\n return !sameValue(readPath(values, path), readPath(state.baseline, path));\n },\n array(path) {\n return fieldArray(path) as never;\n },\n setError(path, message) {\n const { [path]: _previous, ...rest } = state.externalErrors;\n const value = path === '' ? values : readPath(values, path);\n state.externalErrors =\n message === undefined ? rest : { ...rest, [path]: { message, value: cloneValue(value) } };\n },\n constraints,\n Field,\n getValue: (path) => readPath(values, path),\n setValue: (path, value) => writePath(values, path, value),\n register(path, field) {\n fields.set(path, field);\n return () => {\n if (fields.get(path) === field) fields.delete(path);\n };\n },\n fields,\n };\n\n // 输入时:change 模式一开始就校验;blur 模式在有字段失焦过之后(只显示失焦过的字段);提交过之后总是\n watch(\n () => values,\n () => {\n const live =\n validateOn === 'change' ||\n state.submitCount > 0 ||\n (validateOn === 'blur' && state.touched.size > 0);\n if (live) void check(false);\n },\n { deep: true },\n );\n\n // 字段联动:依赖的值改了,字段填过或失焦过就显示它的错误并重新校验(单字段校验由下面的监听跟着重跑)\n for (const [path, deps] of Object.entries(options.dependencies ?? {}) as [string, string[]][]) {\n watch(\n () => deps.map((dep) => readPath(values, dep)),\n () => {\n const value = readPath(values, path);\n const filled = !(value == null || value === '' || (Array.isArray(value) && !value.length));\n if (!filled && !state.touched.has(path)) return;\n touch([path]);\n if (validateOn !== 'submit' || state.submitCount > 0) void check(false);\n },\n { deep: true },\n );\n }\n\n // 单字段校验:这个字段(或它依赖的字段)的值变了,防抖之后运行\n for (const [path, validator] of fieldValidators) {\n watch(\n () => [readPath(values, path), ...dependsOn(path).map((dep) => readPath(values, dep))],\n () => {\n const entry = fieldRuns.get(path) ?? {};\n fieldRuns.set(path, entry);\n clearTimeout(entry.timer);\n entry.timer = setTimeout(() => void runField(path, validator), validator.debounce ?? 300);\n },\n { deep: true },\n );\n }\n\n return controller;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useForm.js","names":[],"sources":["../../src/form/useForm.ts"],"sourcesContent":["/**\n * 表单控制器:值、校验、错误、提交状态放在一个对象里,交给 <Form controller>(COMPONENTS.md §1 的“命令式能力”)。\n * 校验用 zod(使用方安装的 peer 依赖,zod 与 zod/mini 都可以),也可以写 validate 函数。\n * 参考 TanStack Form、React Hook Form 的做法,取其中最常用的部分。\n *\n * const form = useForm({\n * schema: z.object({ email: z.email('邮箱格式不对'), age: z.coerce.number().int().min(18, '未满 18 岁') }),\n * initial: { email: '', age: '' }, // 类型是 schema 的输入(z.input)\n * onSubmit: (values) => api.signup(values), // 拿到解析后的输出(z.output):age 已是数字\n * });\n * <Form controller={form}>\n * <FormErrorSummary /> // 提交没通过时在顶上列出各项错误,点了跳到字段\n * <form.Field name=\"email\" label=\"邮箱\"><TextField /></form.Field> // 只有一个控件时自动绑定值\n * <Button type=\"submit\" loading={form.submitting}>登录</Button>\n * </Form>\n *\n * form.Field 里只有一个控件、控件上没写值时,自动绑定 form.values 的这个字段(同 Ant Design Form.Item 的 name):\n * 写回的是 _value;声明了 _checked 而没有 _value 的控件(Switch、单个 Checkbox)写回 _checked,也可以用 Field 的 valueProp 指定。\n * 自己写了 _value={form.values.email} 时照旧。\n *\n * 何时显示错误(“迟一点批评、早一点肯定”):字段失焦后才显示它的错误,之后它随输入实时更新;提交过一次之后,所有字段随输入实时更新。\n * 不属于哪个字段的错误(对象级 refine 没写 path、validate 返回的 '' 键、setError('', …))是整份表单的错误,见 formError。\n * 字段的值经 v-model 写回,中文输入法组合输入期间不写回,所以组合中途不会触发校验。\n *\n * schema 还决定字段的约束(form-constraints.ts):标签的必填标记、输入框的 maxlength、NumberField 的 min / max\n * 都从 schema 来,不用在控件上再写一遍。\n *\n * - 数组字段:`form.array('items')` 增删、插入、移动,给每项一个稳定的 key;错误与失焦记录跟着下标移动。\n * - 单字段异步校验:`fields: { username: { validate: (name, values, signal) => api.taken(name, { signal }) } }`,\n * 这个字段变化时防抖运行、只采用最后一次的结果,提交时等它完成;`form.validating('username')` 显示校验中。\n * - 改动:`form.dirty` 是值与初始值(或上次 reset 的值)不同;未保存离开的提醒用 useLeaveConfirmation(() => form.dirty)。\n * - 局部校验:`form.validate(['name', 'address'])` 只校验这些字段(含子路径)并显示它们的错误,分步表单进下一步前用。\n * - 字段联动:`dependencies: { confirm: ['password'] }`,password 改了重新校验 confirm(confirm 填过或失焦过时显示它的错误),\n * confirm 的单字段校验也跟着重跑。\n */\nimport type { TsxComponent } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport { computed, reactive, shallowReactive, watch } from 'vue';\nimport { safeParseAsync, type $ZodIssue, type $ZodRawIssue, type $ZodType } from 'zod/v4/core';\nimport { useLang } from '../config/context';\nimport { Field, type FieldProps } from './Field';\nimport { createConstraints, type FieldConstraints } from './form-constraints';\nimport { cloneValue } from '../utils/data/clone';\nimport { sameValue } from '../utils/data/equality';\nimport { codeText, errorCodeOf, errorMessage } from '../utils/request/error-message';\n\n/** 字段路径 → 错误信息;路径用点连接(`address.city`、`items.0.name`) */\nexport type FormErrors = Partial<Record<string, string>>;\n\n/** 当作一个整体的值:不再往里展开路径 */\ntype FieldLeaf = string | number | boolean | bigint | symbol | null | undefined | Date | Blob;\n\n/**\n * 值里的字段路径:点连接,数组下标是数字(`email`、`address.city`、`items.0.name`)。\n * 最多展开 6 层,再深的结构写成 string 也照样能用(Field 的 name、setError 都接受)\n */\nexport type FieldPath<Values, Depth extends unknown[] = []> = Depth['length'] extends 6\n ? never\n : Values extends readonly (infer Item)[]\n ? `${number}` | `${number}.${FieldPath<Item, [...Depth, unknown]>}`\n : Values extends object\n ? {\n [K in keyof Values & string]:\n | K\n | (NonNullable<Values[K]> extends FieldLeaf\n ? never\n : `${K}.${FieldPath<NonNullable<Values[K]>, [...Depth, unknown]>}`);\n }[keyof Values & string]\n : never;\n\n/** 字段路径 P 处的值的类型(`PathValue<Values, 'items.0.name'>` → string) */\nexport type PathValue<Values, P extends string> = P extends `${infer Key}.${infer Rest}`\n ? Values extends readonly (infer Item)[]\n ? PathValue<NonNullable<Item>, Rest>\n : Key extends keyof Values\n ? PathValue<NonNullable<Values[Key]>, Rest>\n : never\n : Values extends readonly (infer Item)[]\n ? Item\n : P extends keyof Values\n ? Values[P]\n : never;\n\n/** 值是数组的字段路径:form.array 只接受这些 */\nexport type ArrayPath<Values> = {\n [P in FieldPath<Values>]: NonNullable<PathValue<Values, P>> extends readonly unknown[]\n ? P\n : never;\n}[FieldPath<Values>];\n\n/** 单个字段的校验:只在这个字段变化时运行(防抖),适合要请求接口的“用户名已占用” */\nexport interface FieldValidator<Value, Values> {\n /** 返回错误信息,通过时返回 undefined;signal 在值又变了、表单重置时中止 */\n validate: (\n value: Value,\n values: Values,\n signal: AbortSignal,\n ) => string | undefined | Promise<string | undefined>;\n /** 值停止变化多久(毫秒)后运行,默认 300 */\n debounce?: number;\n}\n\n/** 字段路径 path 是否在 scope 里:就是它,或是它的子路径('address' 包含 'address.city') */\nconst inScope = (path: string, scope: readonly string[]) =>\n scope.some((prefix) => path === prefix || path.startsWith(`${prefix}.`));\n\n/** 数组字段的操作:form.array(path) 的返回值 */\nexport interface FieldArray<Item> {\n /** 当前的各项(就是 values 里的数组) */\n readonly items: readonly Item[];\n /** 每项一个稳定的 key:渲染列表时用,增删、移动后不变 */\n readonly keys: readonly string[];\n /** 加到末尾 */\n append(item: Item): void;\n /** 插到 index 处 */\n insert(index: number, item: Item): void;\n /** 删掉 index 处的一项 */\n remove(index: number): void;\n /** 把 from 处的一项移到 to 处 */\n move(from: number, to: number): void;\n}\n\n/** 什么时候开始校验;提交过一次之后总是随输入更新 */\nexport interface FormValidateTimings {\n /** 失焦后(默认) */\n blur: unknown;\n /** 输入时 */\n change: unknown;\n /** 只在提交时 */\n submit: unknown;\n}\n\n/** useForm 的选项;Values 是表单里的值(有 schema 时是它的输入类型),Output 是校验通过后交给 onSubmit 的值 */\nexport interface FormOptions<Values extends object, Output = Values> {\n /** 初始值;reset() 回到它 */\n initial: Values;\n /** zod schema:整份值一起校验,issue 的 path 决定错误挂在哪个字段上;字段约束也从它推出 */\n schema?: $ZodType<Output, Values>;\n /** 自定义校验:返回各字段的错误(可以是 Promise),与 schema 的结果合并 */\n validate?: (values: Values) => FormErrors | Promise<FormErrors>;\n /**\n * 单字段异步校验:按字段路径给出,这个字段变化时防抖运行,结果与 schema 的合并(schema 的错误优先)。\n * NoInfer:Values 只从 initial 与 schema 推出,validate 的参数类型才能跟着推出来\n */\n fields?: NoInfer<{ [P in FieldPath<Values>]?: FieldValidator<PathValue<Values, P>, Values> }>;\n /** 什么时候开始校验,默认 blur;提交过一次之后总是随输入更新 */\n validateOn?: keyof FormValidateTimings;\n /**\n * 字段联动:字段 → 它依赖的字段。依赖的值改了就重新校验它(它填过或失焦过时显示错误),它的单字段校验也重跑。\n * 如 `{ confirm: ['password'], endDate: ['startDate'] }`\n */\n dependencies?: NoInfer<{ [P in FieldPath<Values>]?: readonly FieldPath<Values>[] }>;\n /** 校验通过后调用,拿到 schema 解析后的值(转换、默认值、coerce 都已生效);返回 Promise 时 submitting 一直为 true 直到完成 */\n onSubmit?: (values: Output) => unknown;\n}\n\n/** 控制器里与值的类型无关的部分:Form、Field 只用到它,任何 FormController 都能交给 <Form controller> */\nexport interface FormControllerBase {\n /** 所有字段当前的校验错误(含还没显示的);整份表单的错误在 '' 键上 */\n readonly errors: Readonly<FormErrors>;\n /** 整份表单的错误(不属于哪个字段),提交过之后才有;放在提交按钮旁或表单顶上 */\n readonly formError: string | undefined;\n /** 正在执行 onSubmit */\n readonly submitting: boolean;\n /** 提交过几次(不论成功与否) */\n readonly submitCount: number;\n /** 重置过几次:控件据此丢掉自己还没写回的输入(useFormReset) */\n readonly resetCount: number;\n /** 值与初始值(或上次 reset 的值)不同 */\n readonly dirty: boolean;\n /** 字段(不写时为任一字段)的单字段校验正在进行 */\n validating(path?: string): boolean;\n /** 字段的错误是否该显示:失焦过、或提交过之后才显示;Field 用它 */\n visibleError(path: string): string | undefined;\n /** 字段失焦:Field 在焦点离开字段时调用 */\n blur(path: string): void;\n /**\n * 校验整份表单,返回是否通过。给了 paths 时只看这些字段(含子路径):它们的错误立即显示,\n * 返回它们是否都通过(分步表单进下一步前用)\n */\n validate(paths?: readonly string[]): Promise<boolean>;\n /**\n * 提交:先校验,通过后调用 onSubmit;返回是否提交成功。校验没通过,或 onSubmit 抛了带业务错误码的错误(后端回的\n * 校验失败、数据已被修改等,按字段的明细标到字段上,其余是 formError)时为 false;别的错误原样抛出\n */\n submit(): Promise<boolean>;\n /** 回到初始值,清空错误与失焦记录 */\n reset(): void;\n /** 字段从 schema 推出的约束(必填、长度、范围…);Field 交给控件,控件上显式写的优先 */\n constraints(path: string): FieldConstraints;\n /** 字段的当前值(按点连接的路径):Field 自动绑定值时用 */\n getValue(path: string): unknown;\n /** 写字段的值:中途是 null / undefined 时什么也不做 */\n setValue(path: string, value: unknown): void;\n /** 写了 name 的 Field 登记标签与控件的 id:错误汇总据此显示字段名、跳到控件。返回注销函数 */\n register(path: string, field: FormFieldInfo): () => void;\n /** 登记过的字段,按登记(渲染)的顺序 */\n readonly fields: ReadonlyMap<string, FormFieldInfo>;\n}\n\n/** Field 登记的字段信息 */\nexport interface FormFieldInfo {\n /** 标签的文字 */\n label: string;\n /** 控件的 id:错误汇总的链接指向它 */\n controlId: string;\n}\n\n/** 表单控制器:useForm 的返回值 */\nexport interface FormController<Values extends object> extends FormControllerBase {\n /** 当前值(响应式):控件写 _value={form.values.email} */\n readonly values: Values;\n /** 回到初始值(或给定的值,之后 dirty 以它为准),清空错误与失焦记录;保存成功后写 reset(form.values) */\n reset(values?: Values): void;\n /** 字段与初始值不同 */\n isDirty(path: FieldPath<Values>): boolean;\n /** 校验整份表单,或只校验 paths 里的字段(含子路径)并显示它们的错误;返回是否通过 */\n validate(paths?: readonly FieldPath<Values>[]): Promise<boolean>;\n /** 数组字段的增删、插入、移动 */\n array<P extends ArrayPath<Values>>(\n path: P,\n ): FieldArray<NonNullable<PathValue<Values, P>> extends readonly (infer Item)[] ? Item : never>;\n /**\n * 设置某个字段的错误(如服务端返回的“邮箱已注册”),undefined 清除;'' 是整份表单的错误。\n * 校验不会冲掉它,这个字段(整份表单的错误是任一字段)的值改了之后自动消失\n */\n setError(path: FieldPath<Values> | '', message: string | undefined): void;\n /**\n * 就是 Field 组件,只是 name 的类型收窄为这份值的字段路径,写错字段名时类型检查报错:\n * `<form.Field name=\"email\" label=\"邮箱\">…</form.Field>`(参考 TanStack Form 的 form.Field)\n */\n readonly Field: TsxComponent<Omit<FieldProps, 'name'> & { name?: FieldPath<Values> }>;\n}\n\n/** zod issue 的路径 → 点连接的字符串 */\nconst issuePath = (issue: $ZodIssue) => issue.path.map(String).join('.');\n\n/**\n * 该填的没有填:类型不对且是空值(null、undefined,包括缺少的键),或长度不够且是空字符串、空数组。\n * 选择、数字、日期控件清空后是 null(COMPONENTS.md §1 空值),z.string() 遇到它报的是类型不对,读起来应当是“必填”\n */\nfunction isBlankIssue({ code, input }: $ZodRawIssue): boolean {\n if (code === 'invalid_type') return input == null;\n if (code === 'too_small') return input === '' || (Array.isArray(input) && input.length === 0);\n return false;\n}\n\n/** 按点连接的路径写值;中途是 null / undefined 时什么也不做 */\nfunction writePath(target: unknown, path: string, value: unknown): void {\n const keys = path.split('.');\n const parent = readPath(target, keys.slice(0, -1).join('.'));\n if (keys.length === 1) (target as Record<string, unknown>)[keys[0]!] = value;\n else if (parent != null) (parent as Record<string, unknown>)[keys.at(-1)!] = value;\n}\n\n/** 按点连接的路径取值;中途是 null / undefined 时为 undefined */\nfunction readPath(source: unknown, path: string): unknown {\n let current = source;\n for (const key of path.split('.')) {\n if (current == null) return undefined;\n current = (current as Record<string, unknown>)[key];\n }\n return current;\n}\n\n/**\n * 数组字段的下标变了:`prefix.旧下标…` 的键改成 `prefix.新下标…`,map 返回 undefined 的(删掉的项)丢弃\n */\nfunction shiftIndices(key: string, prefix: string, map: (index: number) => number | undefined) {\n if (!key.startsWith(`${prefix}.`)) return key;\n const rest = key.slice(prefix.length + 1);\n const [head, ...tail] = rest.split('.');\n const index = Number(head);\n if (!Number.isInteger(index)) return key;\n const next = map(index);\n return next === undefined ? undefined : [prefix, next, ...tail].join('.');\n}\n\nlet nextArrayKey = 0;\n\n/** 一个字段的单字段校验的进行状态 */\ninterface FieldRun {\n /** 防抖的定时器 */\n timer?: ReturnType<typeof setTimeout>;\n /** 正在校验的那次 */\n running?: { value: unknown; controller: AbortController; done: Promise<void> };\n /** 上次校验完成时的值 */\n checked?: unknown;\n}\n\n/** 业务错误明细里按字段的一项(0008 的 errors:表单校验的问题) */\ninterface FieldIssue {\n readonly field: string;\n readonly code?: string;\n readonly message?: string;\n readonly params?: Readonly<Record<string, unknown>>;\n}\n\nfunction isFieldIssue(item: unknown): item is FieldIssue {\n return (\n typeof item === 'object' && item !== null && typeof (item as FieldIssue).field === 'string'\n );\n}\n\n/**\n * 创建表单控制器(组件 setup 里或模块顶层都可以)。\n * 有 schema 时值的类型取自 schema(initial 要符合它的输入类型),onSubmit 拿到解析后的输出\n */\nexport function useForm<Values extends object, Output = Values>(\n options: FormOptions<Values, Output>,\n): FormController<Values> {\n const { schema, validateOn = 'blur', onSubmit } = options;\n const lang = useLang();\n // zod 的报错跟随语言包;schema 里自己写的提示优先于它(zod 的规则)\n const parseOptions = {\n error: (issue: $ZodRawIssue) =>\n isBlankIssue(issue) ? lang.formRequired : lang.formIssues(issue),\n };\n const fieldValidators = Object.entries(options.fields ?? {}) as [\n string,\n FieldValidator<unknown, Values>,\n ][];\n const values = reactive(cloneValue(options.initial)) as Values;\n const state = shallowReactive({\n errors: {} as FormErrors,\n // 单字段校验的结果,与 errors 分开存:整份校验不会冲掉还在路上的单字段结果\n fieldErrors: {} as FormErrors,\n // setError 给的错误与当时的值:值一改就不再算\n externalErrors: {} as Record<string, { message: string; value: unknown }>,\n touched: new Set<string>(),\n pending: new Set<string>(),\n submitting: false,\n submitCount: 0,\n resetCount: 0,\n // dirty 的比较基准:初始值,或上次 reset 的值\n baseline: cloneValue(options.initial),\n });\n // 异步校验可能乱序返回:只采用最后一次的结果\n let run = 0;\n // 正在进行的提交:校验、onSubmit 期间再次提交(连按回车)拿到同一个结果,不重复调用 onSubmit\n let submission: Promise<boolean> | undefined;\n // 最近一次通过校验时 schema 解析出的值,交给 onSubmit\n let parsed: Output | undefined;\n const constraints = createConstraints(schema);\n const dirty = computed(() => !sameValue(values, state.baseline));\n // 数组字段每项的 key,与数组等长;数组被外部整个换掉(长度不同)时重新生成\n const arrayKeys = new Map<string, string[]>();\n\n // 写了 name 的 Field:错误汇总按它显示字段名、跳到控件\n const fields = shallowReactive(new Map<string, FormFieldInfo>());\n\n /** 单字段校验的进行状态:每个字段一份 */\n const fieldRuns = new Map<string, FieldRun>();\n\n function setPending(path: string, pending: boolean) {\n if (state.pending.has(path) === pending) return;\n const next = new Set(state.pending);\n if (pending) next.add(path);\n else next.delete(path);\n state.pending = next;\n }\n\n /**\n * 运行一个字段的校验。同一个值正在校验时等它;上次校验的就是这个值时直接用上次的结果;\n * 值变了就中止还在路上的那次(只采用最后一次的结果)\n */\n function runField(path: string, validator: FieldValidator<unknown, Values>): Promise<void> {\n const entry = fieldRuns.get(path) ?? {};\n fieldRuns.set(path, entry);\n clearTimeout(entry.timer);\n // 依赖的字段也算进“这次校验的输入”:依赖变了,同一个值也要重新校验\n const value = cloneValue([\n readPath(values, path),\n ...dependsOn(path).map((dep) => readPath(values, dep)),\n ]);\n if (entry.running && sameValue(entry.running.value, value)) return entry.running.done;\n if (!entry.running && 'checked' in entry && sameValue(entry.checked, value)) {\n return Promise.resolve();\n }\n entry.running?.controller.abort();\n const controller = new AbortController();\n setPending(path, true);\n const done = (async () => {\n try {\n const message = await validator.validate(value[0], values, controller.signal);\n if (controller.signal.aborted) return;\n entry.checked = value;\n state.fieldErrors = { ...state.fieldErrors, [path]: message };\n } catch (error) {\n // 校验本身失败(接口出错):当作没有结论,不挡提交(服务端还会再校验),但要报出来\n if (!controller.signal.aborted) console.error(`useForm:字段 ${path} 的校验出错`, error);\n } finally {\n if (entry.running?.controller === controller) {\n entry.running = undefined;\n setPending(path, false);\n }\n }\n })();\n entry.running = { value, controller, done };\n return done;\n }\n\n function cancelFieldRuns() {\n for (const entry of fieldRuns.values()) {\n clearTimeout(entry.timer);\n entry.running?.controller.abort();\n }\n fieldRuns.clear();\n state.pending = new Set();\n state.fieldErrors = {};\n }\n\n /** 字段依赖的字段 */\n const dependsOn = (path: string): readonly string[] =>\n (options.dependencies as Partial<Record<string, readonly string[]>> | undefined)?.[path] ?? [];\n\n /**\n * 校验整份值:schema 与 validate 的错误(errors)。full 时单字段校验也立刻跑完(提交、显式调用 validate;\n * 给了 scope 时只跑 scope 里的),结果在 state.fieldErrors;失焦、输入时只算 schema 与 validate,\n * 单字段校验照旧防抖、只在它自己(或它依赖的字段)的值变了时跑\n */\n async function collect(\n full: boolean,\n scope?: readonly string[],\n ): Promise<{ errors: FormErrors; output: Output }> {\n const errors: FormErrors = {};\n let output = values as unknown as Output;\n if (schema) {\n const result = await safeParseAsync(schema, values, parseOptions);\n if (result.success) output = result.data;\n for (const issue of result.error?.issues ?? []) {\n errors[issuePath(issue)] ??= issue.message;\n }\n }\n if (options.validate) {\n for (const [path, message] of Object.entries(await options.validate(values))) {\n if (message) errors[path] ??= message;\n }\n }\n // 单字段校验:不等防抖,立刻跑完(值没变的直接用上次结果)\n if (full) {\n await Promise.all(\n fieldValidators\n .filter(([path]) => !scope || inScope(path, scope))\n .map(([path, validator]) => runField(path, validator)),\n );\n }\n return { errors, output };\n }\n\n /**\n * onSubmit 抛的带业务错误码的错误(后端回的、请求层的,见 utils/request/error-message.ts)标到表单上:\n * errors 明细里带 field 的标到字段(文案按明细的码取,没有时用它的 message),一项都没有时整份表单的错误是错误的文案。\n * 和 setError 一样,值改了就不再显示;下一次提交开始时清掉(交给后端重新判断:网络断了之后不改值也能再提交)。\n * 认不出的错误返回 false,由 submit 原样抛出\n */\n function markSubmitError(error: unknown): boolean {\n if (errorCodeOf(error) === undefined) return false;\n const details = (error as { errors?: unknown }).errors;\n let marked = false;\n for (const item of Array.isArray(details) ? (details as unknown[]) : []) {\n if (!isFieldIssue(item)) continue;\n const text =\n (item.code && codeText(item.code, item.params, lang)) || item.message || item.code;\n controller.setError(item.field as FieldPath<Values>, text ?? lang.requestFailed);\n submitErrorPaths.add(item.field);\n marked = true;\n }\n if (!marked) {\n controller.setError('', errorMessage(error, lang));\n submitErrorPaths.add('');\n }\n return true;\n }\n\n /** 上一次提交失败时标上的路径 */\n const submitErrorPaths = new Set<string>();\n\n /** setError 给的错误,值改过就不算了;'' 是整份表单的错误,任一字段改了都不算 */\n function externalError(path: string): string | undefined {\n const entry = state.externalErrors[path];\n if (!entry) return undefined;\n const current = path === '' ? values : readPath(values, path);\n return sameValue(current, entry.value) ? entry.message : undefined;\n }\n\n /** schema 与 validate 的错误优先,再补上 setError 与单字段校验的 */\n function mergedErrors(): FormErrors {\n const merged = { ...state.errors };\n for (const path of Object.keys(state.externalErrors)) {\n const message = externalError(path);\n if (message) merged[path] ??= message;\n }\n for (const [path, message] of Object.entries(state.fieldErrors)) {\n if (message) merged[path] ??= message;\n }\n return merged;\n }\n\n /**\n * 校验并记下结果(只采用最后一次);返回是否通过:算出的、setError 给的、单字段校验的都没有错误。\n * 给了 scope 时只看 scope 里的字段,别的字段的错误保持原样\n */\n async function check(full: boolean, scope?: readonly string[]): Promise<boolean> {\n const current = ++run;\n const { errors, output } = await collect(full, scope);\n if (!scope) {\n if (current === run) {\n state.errors = errors;\n parsed = output;\n }\n return Object.keys(errors).length === 0 && Object.keys(mergedErrors()).length === 0;\n }\n const scoped = (source: FormErrors) =>\n Object.entries(source).filter(([path, message]) => message && inScope(path, scope));\n if (current === run) {\n state.errors = {\n ...Object.fromEntries(\n Object.entries(state.errors).filter(([path]) => !inScope(path, scope)),\n ),\n ...Object.fromEntries(scoped(errors)),\n };\n }\n return scoped(errors).length === 0 && scoped(mergedErrors()).length === 0;\n }\n\n /** 把这些字段记为失焦过:它们的错误开始显示 */\n function touch(paths: readonly string[]) {\n const next = new Set(state.touched);\n for (const path of paths) next.add(path);\n if (next.size !== state.touched.size) state.touched = next;\n }\n\n /** 数组字段的下标变了:错误、失焦记录、单字段校验的结果跟着移 */\n function remapIndices(prefix: string, map: (index: number) => number | undefined) {\n const remap = (errors: FormErrors) =>\n Object.fromEntries(\n Object.entries(errors).flatMap(([key, message]) => {\n const next = shiftIndices(key, prefix, map);\n return next === undefined ? [] : [[next, message]];\n }),\n ) as FormErrors;\n state.errors = remap(state.errors);\n state.fieldErrors = remap(state.fieldErrors);\n state.externalErrors = Object.fromEntries(\n Object.entries(state.externalErrors).flatMap(([key, entry]) => {\n const next = shiftIndices(key, prefix, map);\n return next === undefined ? [] : [[next, entry]];\n }),\n );\n state.touched = new Set(\n [...state.touched].flatMap((key) => shiftIndices(key, prefix, map) ?? []),\n );\n }\n\n function fieldArray(path: string): FieldArray<unknown> {\n const list = () => (readPath(values, path) as unknown[] | undefined) ?? [];\n const keys = () => {\n let current = arrayKeys.get(path);\n if (current?.length !== list().length) {\n current = list().map(() => `item-${++nextArrayKey}`);\n arrayKeys.set(path, current);\n }\n return current;\n };\n /** 改数组与 key:splice 的参数同时作用在两者上 */\n const splice = (start: number, deleteCount: number, ...items: unknown[]) => {\n keys().splice(start, deleteCount, ...items.map(() => `item-${++nextArrayKey}`));\n list().splice(start, deleteCount, ...items);\n };\n return {\n get items() {\n return list();\n },\n get keys() {\n return keys();\n },\n append: (item) => splice(list().length, 0, item),\n insert(index, item) {\n remapIndices(path, (old) => (old >= index ? old + 1 : old));\n splice(index, 0, item);\n },\n remove(index) {\n remapIndices(path, (old) => (old === index ? undefined : old > index ? old - 1 : old));\n splice(index, 1);\n },\n move(from, to) {\n if (from === to) return;\n remapIndices(path, (old) => {\n if (old === from) return to;\n if (from < to && old > from && old <= to) return old - 1;\n if (from > to && old >= to && old < from) return old + 1;\n return old;\n });\n const [key] = keys().splice(from, 1);\n const [item] = list().splice(from, 1);\n keys().splice(to, 0, key!);\n list().splice(to, 0, item);\n },\n };\n }\n\n const controller: FormController<Values> = {\n values,\n get errors() {\n return mergedErrors();\n },\n get formError() {\n if (state.submitCount === 0) return undefined;\n return state.errors[''] ?? externalError('');\n },\n get submitting() {\n return state.submitting;\n },\n get resetCount() {\n return state.resetCount;\n },\n get submitCount() {\n return state.submitCount;\n },\n get dirty() {\n return dirty.value;\n },\n validating(path) {\n return path === undefined ? state.pending.size > 0 : state.pending.has(path);\n },\n visibleError(path) {\n // 失焦过的字段,或局部校验过的字段(含子路径:validate(['address']) 之后 address.city 也显示)\n if (state.submitCount === 0 && !inScope(path, [...state.touched])) return undefined;\n return state.errors[path] ?? externalError(path) ?? state.fieldErrors[path];\n },\n blur(path) {\n if (state.touched.has(path)) return;\n state.touched = new Set(state.touched).add(path);\n if (validateOn !== 'submit') void check(false);\n },\n validate(paths) {\n if (!paths) return check(true);\n touch(paths);\n return check(true, paths);\n },\n submit() {\n submission ??= (async () => {\n state.submitCount++;\n for (const path of submitErrorPaths)\n controller.setError(path as FieldPath<Values>, undefined);\n submitErrorPaths.clear();\n const valid = await check(true);\n if (!valid || !onSubmit) return valid;\n state.submitting = true;\n try {\n await onSubmit(parsed as Output);\n } catch (error) {\n if (!markSubmitError(error)) throw error;\n return false;\n } finally {\n state.submitting = false;\n }\n return true;\n })().finally(() => (submission = undefined));\n return submission;\n },\n reset(next = options.initial) {\n run++;\n const snapshot = cloneValue(next);\n cancelFieldRuns();\n Object.assign(values, cloneValue(snapshot));\n arrayKeys.clear();\n state.baseline = snapshot;\n state.errors = {};\n state.externalErrors = {};\n state.touched = new Set();\n state.submitCount = 0;\n state.resetCount++;\n },\n isDirty(path) {\n return !sameValue(readPath(values, path), readPath(state.baseline, path));\n },\n array(path) {\n return fieldArray(path) as never;\n },\n setError(path, message) {\n const { [path]: _previous, ...rest } = state.externalErrors;\n const value = path === '' ? values : readPath(values, path);\n state.externalErrors =\n message === undefined ? rest : { ...rest, [path]: { message, value: cloneValue(value) } };\n },\n constraints,\n Field,\n getValue: (path) => readPath(values, path),\n setValue: (path, value) => writePath(values, path, value),\n register(path, field) {\n fields.set(path, field);\n return () => {\n if (fields.get(path) === field) fields.delete(path);\n };\n },\n fields,\n };\n\n // 输入时:change 模式一开始就校验;blur 模式在有字段失焦过之后(只显示失焦过的字段);提交过之后总是\n watch(\n () => values,\n () => {\n const live =\n validateOn === 'change' ||\n state.submitCount > 0 ||\n (validateOn === 'blur' && state.touched.size > 0);\n if (live) void check(false);\n },\n { deep: true },\n );\n\n // 字段联动:依赖的值改了,字段填过或失焦过就显示它的错误并重新校验(单字段校验由下面的监听跟着重跑)\n for (const [path, deps] of Object.entries(options.dependencies ?? {}) as [string, string[]][]) {\n watch(\n () => deps.map((dep) => readPath(values, dep)),\n () => {\n const value = readPath(values, path);\n const filled = !(value == null || value === '' || (Array.isArray(value) && !value.length));\n if (!filled && !state.touched.has(path)) return;\n touch([path]);\n if (validateOn !== 'submit' || state.submitCount > 0) void check(false);\n },\n { deep: true },\n );\n }\n\n // 单字段校验:这个字段(或它依赖的字段)的值变了,防抖之后运行\n for (const [path, validator] of fieldValidators) {\n watch(\n () => [readPath(values, path), ...dependsOn(path).map((dep) => readPath(values, dep))],\n () => {\n const entry = fieldRuns.get(path) ?? {};\n fieldRuns.set(path, entry);\n clearTimeout(entry.timer);\n entry.timer = setTimeout(() => void runField(path, validator), validator.debounce ?? 300);\n },\n { deep: true },\n );\n }\n\n return controller;\n}\n"],"mappings":"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"}
|