@shwfed/config 2.12.11 → 2.12.14
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/module.json +1 -1
- package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/config.d.vue.ts +8 -0
- package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/config.vue +118 -25
- package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/config.vue.d.ts +8 -0
- package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/runtime.vue +48 -21
- package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/schema.d.ts +5 -0
- package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/schema.js +24 -1
- package/dist/runtime/components/form/fields/2026-04-22/com.shwfed.form.field.textarea/config.d.vue.ts +2 -2
- package/dist/runtime/components/form/fields/2026-04-22/com.shwfed.form.field.textarea/config.vue.d.ts +2 -2
- package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.number/runtime.vue +22 -15
- package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.numberrange/runtime.vue +34 -13
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.multi/runtime.vue +22 -18
- package/dist/runtime/components/form/utils/derived.js +2 -2
- package/dist/runtime/components/form/utils/state.d.ts +18 -0
- package/dist/runtime/components/form/utils/state.js +15 -2
- package/dist/runtime/components/table/columns/2026-05-20/com.shwfed.table.column.number-input/runtime.vue +22 -14
- package/dist/runtime/components/table/config.vue +1 -4
- package/dist/runtime/components/table/index.vue +2 -2
- package/dist/runtime/share/number-format.d.ts +1 -0
- package/dist/runtime/share/number-format.js +12 -13
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -13,6 +13,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
|
|
|
13
13
|
readonly target: string;
|
|
14
14
|
readonly operation: string;
|
|
15
15
|
}[] | undefined;
|
|
16
|
+
readonly requests?: readonly {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly request: string;
|
|
19
|
+
}[] | undefined;
|
|
16
20
|
readonly writes: readonly {
|
|
17
21
|
readonly path: string;
|
|
18
22
|
readonly value: string;
|
|
@@ -29,6 +33,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
|
|
|
29
33
|
readonly target: string;
|
|
30
34
|
readonly operation: string;
|
|
31
35
|
}[] | undefined;
|
|
36
|
+
readonly requests?: readonly {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly request: string;
|
|
39
|
+
}[] | undefined;
|
|
32
40
|
readonly writes: readonly {
|
|
33
41
|
readonly path: string;
|
|
34
42
|
readonly value: string;
|
|
@@ -8,6 +8,7 @@ import { Markdown } from "../../../../ui/markdown";
|
|
|
8
8
|
import { InputGroupButton } from "../../../../ui/input-group";
|
|
9
9
|
import { getStructFieldDescription, getStructFieldTitle } from "../../../schema";
|
|
10
10
|
import TriggersField from "../../../components/triggers-field.vue";
|
|
11
|
+
import TriggerRowScope from "../../../components/trigger-row-scope.vue";
|
|
11
12
|
import { schema } from "./schema";
|
|
12
13
|
defineOptions({ name: "ShwfedStateWriteActionConfig" });
|
|
13
14
|
const value = defineModel({ type: null, ...{ required: true } });
|
|
@@ -16,9 +17,33 @@ const actionSchema = schema(() => {
|
|
|
16
17
|
const fieldTitle = (f) => getStructFieldTitle(actionSchema, f) ?? f;
|
|
17
18
|
const fieldDescription = (f) => getStructFieldDescription(actionSchema, f);
|
|
18
19
|
const writes = computed(() => value.value.writes ?? []);
|
|
20
|
+
const requests = computed(() => value.value.requests ?? []);
|
|
21
|
+
const roster = computed(
|
|
22
|
+
() => requests.value.map((r, i) => ({
|
|
23
|
+
id: r.id,
|
|
24
|
+
label: `\u7B2C ${i + 1} \u6B65`,
|
|
25
|
+
valid: true,
|
|
26
|
+
description: "HTTP \u54CD\u5E94"
|
|
27
|
+
}))
|
|
28
|
+
);
|
|
19
29
|
function patchWrites(next) {
|
|
20
30
|
value.value = { ...value.value, writes: next };
|
|
21
31
|
}
|
|
32
|
+
function patchRequests(next) {
|
|
33
|
+
const draft = { ...value.value };
|
|
34
|
+
if (next.length === 0) Reflect.deleteProperty(draft, "requests");
|
|
35
|
+
else draft.requests = next;
|
|
36
|
+
value.value = draft;
|
|
37
|
+
}
|
|
38
|
+
function updateRequestRow(index, patch) {
|
|
39
|
+
patchRequests(requests.value.map((r, i) => i === index ? { ...r, ...patch } : r));
|
|
40
|
+
}
|
|
41
|
+
function addRequestRow() {
|
|
42
|
+
patchRequests([...requests.value, { id: crypto.randomUUID(), request: "" }]);
|
|
43
|
+
}
|
|
44
|
+
function removeRequestRow(index) {
|
|
45
|
+
patchRequests(requests.value.filter((_, i) => i !== index));
|
|
46
|
+
}
|
|
22
47
|
function updateRow(index, patch) {
|
|
23
48
|
patchWrites(writes.value.map((w, i) => i === index ? { ...w, ...patch } : w));
|
|
24
49
|
}
|
|
@@ -41,40 +66,37 @@ function updateTriggers(next) {
|
|
|
41
66
|
<Field orientation="vertical">
|
|
42
67
|
<FieldLabel class="text-xs text-zinc-500">
|
|
43
68
|
<template
|
|
44
|
-
v-if="fieldDescription('
|
|
69
|
+
v-if="fieldDescription('requests')"
|
|
45
70
|
#tooltip
|
|
46
71
|
>
|
|
47
72
|
<Markdown
|
|
48
|
-
:source="fieldDescription('
|
|
73
|
+
:source="fieldDescription('requests')"
|
|
49
74
|
block
|
|
50
75
|
class="prose prose-sm prose-zinc"
|
|
51
76
|
/>
|
|
52
77
|
</template>
|
|
53
|
-
{{ fieldTitle("
|
|
78
|
+
{{ fieldTitle("requests") }}
|
|
54
79
|
</FieldLabel>
|
|
55
80
|
|
|
56
81
|
<div class="flex flex-col gap-3">
|
|
57
|
-
<!-- One
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
the `trailing` slot.
|
|
82
|
+
<!-- One「写入前」request per line: a static「第 N 步」marker notched into
|
|
83
|
+
the editor's `leading` slot (its response is referenced positionally
|
|
84
|
+
as `steps["<id>"]`), the request expression in the editor, and a
|
|
85
|
+
destructive delete in the `trailing` slot. Outside `TriggerRowScope`
|
|
86
|
+
so the request expressions keep the base scope (no `steps`). -->
|
|
61
87
|
<ExpressionEditor
|
|
62
|
-
v-for="(row, index) in
|
|
63
|
-
:key="
|
|
64
|
-
:model-value="row.
|
|
65
|
-
|
|
66
|
-
|
|
88
|
+
v-for="(row, index) in requests"
|
|
89
|
+
:key="row.id"
|
|
90
|
+
:model-value="row.request"
|
|
91
|
+
result-type="HttpRequest"
|
|
92
|
+
placeholder="请求,如 http.get('/api/rate')"
|
|
93
|
+
@update:model-value="(v) => updateRequestRow(index, { request: v })"
|
|
67
94
|
>
|
|
68
95
|
<template #leading>
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
placeholder="路径"
|
|
74
|
-
class="w-44 min-w-0 border-r border-zinc-200 bg-transparent pr-3 font-mono text-sm text-zinc-700 outline-none placeholder:text-zinc-300"
|
|
75
|
-
:aria-label="`\u5199\u5165\u8DEF\u5F84 ${index + 1}`"
|
|
76
|
-
@input="updateRow(index, { path: $event.target.value })"
|
|
77
|
-
>
|
|
96
|
+
<span
|
|
97
|
+
data-slot="state-write-request-step"
|
|
98
|
+
class="w-16 shrink-0 border-r border-zinc-200 pr-3 font-mono text-xs whitespace-nowrap text-zinc-500 select-none"
|
|
99
|
+
>第 {{ index + 1 }} 步</span>
|
|
78
100
|
</template>
|
|
79
101
|
<template #trailing>
|
|
80
102
|
<InputGroupButton
|
|
@@ -84,8 +106,8 @@ function updateTriggers(next) {
|
|
|
84
106
|
>
|
|
85
107
|
<button
|
|
86
108
|
type="button"
|
|
87
|
-
:aria-label="`\u79FB\u9664\
|
|
88
|
-
@click="
|
|
109
|
+
:aria-label="`\u79FB\u9664\u8BF7\u6C42 ${index + 1}`"
|
|
110
|
+
@click="removeRequestRow(index)"
|
|
89
111
|
>
|
|
90
112
|
<Icon icon="fluent:delete-20-regular" />
|
|
91
113
|
</button>
|
|
@@ -96,14 +118,85 @@ function updateTriggers(next) {
|
|
|
96
118
|
<Button
|
|
97
119
|
type="button"
|
|
98
120
|
class="justify-center"
|
|
99
|
-
@click="
|
|
121
|
+
@click="addRequestRow"
|
|
100
122
|
>
|
|
101
123
|
<Icon icon="fluent:add-20-regular" />
|
|
102
|
-
<span
|
|
124
|
+
<span>添加请求</span>
|
|
103
125
|
</Button>
|
|
104
126
|
</div>
|
|
105
127
|
</Field>
|
|
106
128
|
|
|
129
|
+
<Field orientation="vertical">
|
|
130
|
+
<FieldLabel class="text-xs text-zinc-500">
|
|
131
|
+
<template
|
|
132
|
+
v-if="fieldDescription('writes')"
|
|
133
|
+
#tooltip
|
|
134
|
+
>
|
|
135
|
+
<Markdown
|
|
136
|
+
:source="fieldDescription('writes')"
|
|
137
|
+
block
|
|
138
|
+
class="prose prose-sm prose-zinc"
|
|
139
|
+
/>
|
|
140
|
+
</template>
|
|
141
|
+
{{ fieldTitle("writes") }}
|
|
142
|
+
</FieldLabel>
|
|
143
|
+
|
|
144
|
+
<!-- Advertise the「写入前」requests as the step roster to the value
|
|
145
|
+
editors only: they get the「第 N 步」picker chips + `steps` inline
|
|
146
|
+
validation, while the request editors above keep the base scope. -->
|
|
147
|
+
<TriggerRowScope :roster="roster">
|
|
148
|
+
<div class="flex flex-col gap-3">
|
|
149
|
+
<!-- One write item, on a single line: a monofont binding path notched
|
|
150
|
+
into the editor's `leading` slot, the value expression in the
|
|
151
|
+
editor itself (its own `{x}` badge), and a destructive delete in
|
|
152
|
+
the `trailing` slot. Reuses the ExpressionEditor's own InputGroup. -->
|
|
153
|
+
<ExpressionEditor
|
|
154
|
+
v-for="(row, index) in writes"
|
|
155
|
+
:key="index"
|
|
156
|
+
:model-value="row.value"
|
|
157
|
+
placeholder="值,如 selected[?0].?exchangeRate"
|
|
158
|
+
@update:model-value="(v) => updateRow(index, { value: v })"
|
|
159
|
+
>
|
|
160
|
+
<template #leading>
|
|
161
|
+
<input
|
|
162
|
+
type="text"
|
|
163
|
+
data-slot="state-write-path"
|
|
164
|
+
:value="row.path"
|
|
165
|
+
placeholder="路径"
|
|
166
|
+
class="w-44 min-w-0 border-r border-zinc-200 bg-transparent pr-3 font-mono text-sm text-zinc-700 outline-none placeholder:text-zinc-300"
|
|
167
|
+
:aria-label="`\u5199\u5165\u8DEF\u5F84 ${index + 1}`"
|
|
168
|
+
@input="updateRow(index, { path: $event.target.value })"
|
|
169
|
+
>
|
|
170
|
+
</template>
|
|
171
|
+
<template #trailing>
|
|
172
|
+
<InputGroupButton
|
|
173
|
+
variant="destructive"
|
|
174
|
+
size="icon-xs"
|
|
175
|
+
as-child
|
|
176
|
+
>
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
:aria-label="`\u79FB\u9664\u5199\u5165\u9879 ${index + 1}`"
|
|
180
|
+
@click="removeRow(index)"
|
|
181
|
+
>
|
|
182
|
+
<Icon icon="fluent:delete-20-regular" />
|
|
183
|
+
</button>
|
|
184
|
+
</InputGroupButton>
|
|
185
|
+
</template>
|
|
186
|
+
</ExpressionEditor>
|
|
187
|
+
|
|
188
|
+
<Button
|
|
189
|
+
type="button"
|
|
190
|
+
class="justify-center"
|
|
191
|
+
@click="addRow"
|
|
192
|
+
>
|
|
193
|
+
<Icon icon="fluent:add-20-regular" />
|
|
194
|
+
<span>添加写入项</span>
|
|
195
|
+
</Button>
|
|
196
|
+
</div>
|
|
197
|
+
</TriggerRowScope>
|
|
198
|
+
</Field>
|
|
199
|
+
|
|
107
200
|
<Field orientation="vertical">
|
|
108
201
|
<FieldLabel class="text-xs text-zinc-500">
|
|
109
202
|
<template
|
|
@@ -13,6 +13,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
|
|
|
13
13
|
readonly target: string;
|
|
14
14
|
readonly operation: string;
|
|
15
15
|
}[] | undefined;
|
|
16
|
+
readonly requests?: readonly {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly request: string;
|
|
19
|
+
}[] | undefined;
|
|
16
20
|
readonly writes: readonly {
|
|
17
21
|
readonly path: string;
|
|
18
22
|
readonly value: string;
|
|
@@ -29,6 +33,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
|
|
|
29
33
|
readonly target: string;
|
|
30
34
|
readonly operation: string;
|
|
31
35
|
}[] | undefined;
|
|
36
|
+
readonly requests?: readonly {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly request: string;
|
|
39
|
+
}[] | undefined;
|
|
32
40
|
readonly writes: readonly {
|
|
33
41
|
readonly path: string;
|
|
34
42
|
readonly value: string;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { Effect } from "effect";
|
|
2
|
+
import { Effect, Option } from "effect";
|
|
3
|
+
import { Fetch } from "fx-fetch";
|
|
3
4
|
import { createDefu } from "defu";
|
|
4
5
|
import { cel as _rawCel } from "../../../../../utils/cel";
|
|
5
6
|
import { celBindings, injectCELContext } from "../../../../../utils/cel-context";
|
|
6
7
|
import { dispatchTriggers, useEventChannel } from "../../../../../share/event-bus";
|
|
8
|
+
import { asRequest } from "../../../../../share/request";
|
|
7
9
|
import { useFormState } from "../../../../form/utils/state";
|
|
8
10
|
import ShwfedActionDefinition from "../../../components/definition.vue";
|
|
9
11
|
defineOptions({ name: "ShwfedStateWriteActionRuntime" });
|
|
@@ -31,27 +33,52 @@ function mergeAtPath(current, next) {
|
|
|
31
33
|
}
|
|
32
34
|
return next;
|
|
33
35
|
}
|
|
34
|
-
const effect = Effect.suspend(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
36
|
+
const effect = Effect.suspend(() => {
|
|
37
|
+
const steps = {};
|
|
38
|
+
return Effect.forEach(
|
|
39
|
+
// 写入前: issue each request in list order against the base scope (no
|
|
40
|
+
// `steps` — requests can't reference each other). `asRequest` maps the
|
|
41
|
+
// evaluated value to `Some(builder)` / `None` (author opted out → skip the
|
|
42
|
+
// fetch, leaving `steps["<id>"]` unset). A failed `.json()` short-circuits
|
|
43
|
+
// the whole click before any write runs — the effect fails and the action
|
|
44
|
+
// runner absorbs it, exactly as a failing `value` expression does.
|
|
45
|
+
props.config.requests ?? [],
|
|
46
|
+
(request) => Effect.flatMap($cel(request.request), (evaluated) => {
|
|
47
|
+
const builder = asRequest(evaluated);
|
|
48
|
+
if (Option.isNone(builder)) return Effect.void;
|
|
49
|
+
return Effect.map(builder.value.json(), (json) => {
|
|
50
|
+
steps[request.id] = json;
|
|
51
|
+
});
|
|
52
|
+
}),
|
|
53
|
+
{ discard: true }
|
|
54
|
+
).pipe(
|
|
55
|
+
Effect.flatMap(
|
|
56
|
+
() => (
|
|
57
|
+
// Evaluate every row's `value` in list order. A row with a blank `path` is
|
|
58
|
+
// skipped (a half-authored row writes nowhere) but its value is never
|
|
59
|
+
// evaluated — `Effect.forEach` keeps the click sequential and short-circuits
|
|
60
|
+
// on the first failing expression, matching the old map-eval-then-write.
|
|
61
|
+
Effect.forEach(
|
|
62
|
+
props.config.writes ?? [],
|
|
63
|
+
(write) => {
|
|
64
|
+
const path = write.path?.trim() ?? "";
|
|
65
|
+
if (path.length === 0) return Effect.void;
|
|
66
|
+
return Effect.map($cel(write.value, { steps }), (result) => {
|
|
67
|
+
const merged = mergeAtPath(formState.getAt(path), result);
|
|
68
|
+
formState.setAt(path, merged);
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
{ discard: true }
|
|
72
|
+
)
|
|
73
|
+
)
|
|
51
74
|
),
|
|
52
|
-
() => dispatchTriggers(channel, props.config.onSuccess, $cel)
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
Effect.flatMap(() => dispatchTriggers(channel, props.config.onSuccess, $cel)),
|
|
76
|
+
// The request builders need a `Fetch` layer; `ShwfedActionDefinition` runs
|
|
77
|
+
// the effect with no requirements channel, so provide it here — same layer
|
|
78
|
+
// the request op/button use.
|
|
79
|
+
Effect.provide(Fetch.layer)
|
|
80
|
+
);
|
|
81
|
+
});
|
|
55
82
|
</script>
|
|
56
83
|
|
|
57
84
|
<template>
|
|
@@ -9,6 +9,10 @@ export declare const metadata: {
|
|
|
9
9
|
export declare function schema(configure: (env: Environment) => void): Schema.Struct<{
|
|
10
10
|
type: Schema.Literal<["com.shwfed.actions.button.state.write"]>;
|
|
11
11
|
compatibilityDate: Schema.Literal<["2026-06-25"]>;
|
|
12
|
+
requests: Schema.optional<Schema.Array$<Schema.Struct<{
|
|
13
|
+
id: Schema.refine<string, typeof Schema.String>;
|
|
14
|
+
request: Schema.Schema<string, string, never>;
|
|
15
|
+
}>>>;
|
|
12
16
|
writes: Schema.Array$<Schema.Struct<{
|
|
13
17
|
path: Schema.SchemaClass<string, string, never>;
|
|
14
18
|
value: Schema.Schema<string, string, never>;
|
|
@@ -23,3 +27,4 @@ export declare function schema(configure: (env: Environment) => void): Schema.St
|
|
|
23
27
|
}>;
|
|
24
28
|
export type Value = Schema.Schema.Type<ReturnType<typeof schema>>;
|
|
25
29
|
export type WriteValue = Value['writes'][number];
|
|
30
|
+
export type RequestValue = NonNullable<Value['requests']>[number];
|
|
@@ -2,6 +2,7 @@ import { Schema } from "effect";
|
|
|
2
2
|
import { Expression } from "../../../../../share/expression.js";
|
|
3
3
|
import { Triggers } from "../../../../../share/event-bus.js";
|
|
4
4
|
import { md } from "../../../../../share/markdown.js";
|
|
5
|
+
import { registerStepsVariableIfAbsent } from "../../../../operations/utils/step-vars.js";
|
|
5
6
|
export const type = "com.shwfed.actions.button.state.write";
|
|
6
7
|
export const compatibilityDate = "2026-06-25";
|
|
7
8
|
export const metadata = {
|
|
@@ -9,19 +10,41 @@ export const metadata = {
|
|
|
9
10
|
icon: "fluent:document-edit-20-regular"
|
|
10
11
|
};
|
|
11
12
|
export function schema(configure) {
|
|
13
|
+
const withSteps = (env) => {
|
|
14
|
+
configure(env);
|
|
15
|
+
registerStepsVariableIfAbsent(env);
|
|
16
|
+
};
|
|
12
17
|
const Write = Schema.Struct({
|
|
13
18
|
path: Schema.String.annotations({
|
|
14
19
|
title: "\u8DEF\u5F84",
|
|
15
20
|
description: "\u5199\u5165\u7684\u7ED1\u5B9A\u8DEF\u5F84\uFF0C\u5982 `dataJson.cus_currency`"
|
|
16
21
|
}),
|
|
17
|
-
value: Expression({ configure }).annotations({
|
|
22
|
+
value: Expression({ configure: withSteps }).annotations({
|
|
18
23
|
title: "\u503C",
|
|
19
24
|
description: "\u70B9\u51FB\u540E\u6C42\u503C\uFF0C\u7ED3\u679C\u5199\u5165\u8BE5\u8DEF\u5F84"
|
|
20
25
|
})
|
|
21
26
|
});
|
|
27
|
+
const Request = Schema.Struct({
|
|
28
|
+
id: Schema.UUID.annotations({
|
|
29
|
+
title: "ID",
|
|
30
|
+
description: '\u8BF7\u6C42\u6807\u8BC6\uFF0C\u5199\u5165\u503C\u7ECF `steps["<id>"]` \u5F15\u7528'
|
|
31
|
+
}),
|
|
32
|
+
request: Expression({ configure, resultType: "HttpRequest" }).annotations({
|
|
33
|
+
title: "\u8BF7\u6C42",
|
|
34
|
+
description: "\u6784\u9020\u4E00\u4E2A\u8BF7\u6C42\uFF0C\u670D\u52A1\u5668\u987B\u8FD4\u56DE JSON \u54CD\u5E94\uFF1B\u7ED3\u679C\u6309\u300C\u7B2C N \u6B65\u300D\u5B58\u5165 `steps`"
|
|
35
|
+
})
|
|
36
|
+
});
|
|
22
37
|
return Schema.Struct({
|
|
23
38
|
type: Schema.Literal(type),
|
|
24
39
|
compatibilityDate: Schema.Literal(compatibilityDate),
|
|
40
|
+
requests: Schema.optional(Schema.Array(Request).annotations({
|
|
41
|
+
title: "\u5199\u5165\u524D",
|
|
42
|
+
description: md`
|
|
43
|
+
点击后先按列表顺序发起这些请求,其响应可在「写入值」中经
|
|
44
|
+
\`steps["<第 N 步>"]\` 引用。任一请求失败(网络错误 / 非 2xx / 不可解析)
|
|
45
|
+
则中止,不进行任何写入。
|
|
46
|
+
`
|
|
47
|
+
})),
|
|
25
48
|
writes: Schema.Array(Write).annotations({
|
|
26
49
|
title: "\u5199\u5165\u503C",
|
|
27
50
|
description: md`
|
|
@@ -39,6 +39,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
|
|
|
39
39
|
readonly mode: "formula" | "prefill";
|
|
40
40
|
readonly expression: string;
|
|
41
41
|
} | undefined;
|
|
42
|
+
readonly maxLength?: number | undefined;
|
|
42
43
|
readonly validations?: readonly {
|
|
43
44
|
readonly message: readonly [{
|
|
44
45
|
readonly locale: "zh";
|
|
@@ -50,7 +51,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
|
|
|
50
51
|
readonly warning?: boolean | undefined;
|
|
51
52
|
readonly when: string;
|
|
52
53
|
}[] | undefined;
|
|
53
|
-
readonly maxLength?: number | undefined;
|
|
54
54
|
}) => any;
|
|
55
55
|
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
56
56
|
"onUpdate:modelValue"?: ((value: {
|
|
@@ -89,6 +89,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
|
|
|
89
89
|
readonly mode: "formula" | "prefill";
|
|
90
90
|
readonly expression: string;
|
|
91
91
|
} | undefined;
|
|
92
|
+
readonly maxLength?: number | undefined;
|
|
92
93
|
readonly validations?: readonly {
|
|
93
94
|
readonly message: readonly [{
|
|
94
95
|
readonly locale: "zh";
|
|
@@ -100,7 +101,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
|
|
|
100
101
|
readonly warning?: boolean | undefined;
|
|
101
102
|
readonly when: string;
|
|
102
103
|
}[] | undefined;
|
|
103
|
-
readonly maxLength?: number | undefined;
|
|
104
104
|
}) => any) | undefined;
|
|
105
105
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
106
106
|
declare const _default: typeof __VLS_export;
|
|
@@ -39,6 +39,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
|
|
|
39
39
|
readonly mode: "formula" | "prefill";
|
|
40
40
|
readonly expression: string;
|
|
41
41
|
} | undefined;
|
|
42
|
+
readonly maxLength?: number | undefined;
|
|
42
43
|
readonly validations?: readonly {
|
|
43
44
|
readonly message: readonly [{
|
|
44
45
|
readonly locale: "zh";
|
|
@@ -50,7 +51,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
|
|
|
50
51
|
readonly warning?: boolean | undefined;
|
|
51
52
|
readonly when: string;
|
|
52
53
|
}[] | undefined;
|
|
53
|
-
readonly maxLength?: number | undefined;
|
|
54
54
|
}) => any;
|
|
55
55
|
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
56
56
|
"onUpdate:modelValue"?: ((value: {
|
|
@@ -89,6 +89,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
|
|
|
89
89
|
readonly mode: "formula" | "prefill";
|
|
90
90
|
readonly expression: string;
|
|
91
91
|
} | undefined;
|
|
92
|
+
readonly maxLength?: number | undefined;
|
|
92
93
|
readonly validations?: readonly {
|
|
93
94
|
readonly message: readonly [{
|
|
94
95
|
readonly locale: "zh";
|
|
@@ -100,7 +101,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
|
|
|
100
101
|
readonly warning?: boolean | undefined;
|
|
101
102
|
readonly when: string;
|
|
102
103
|
}[] | undefined;
|
|
103
|
-
readonly maxLength?: number | undefined;
|
|
104
104
|
}) => any) | undefined;
|
|
105
105
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
106
106
|
declare const _default: typeof __VLS_export;
|
package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.number/runtime.vue
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { Icon } from "@iconify/vue";
|
|
3
3
|
import { Effect } from "effect";
|
|
4
|
-
import { computed, nextTick } from "vue";
|
|
4
|
+
import { computed, nextTick, watch } from "vue";
|
|
5
5
|
import { useI18n } from "vue-i18n";
|
|
6
6
|
import { cel as _rawCel } from "../../../../../utils/cel";
|
|
7
7
|
import { celBindings, injectCELContext } from "../../../../../utils/cel-context";
|
|
8
8
|
import { getLocalizedText } from "../../../../../share/locale";
|
|
9
9
|
import { interpolateMarkdown } from "../../../../table/utils/runtime";
|
|
10
|
-
import { formatNumberDisplay } from "../../../../../share/number-format";
|
|
10
|
+
import { formatNumberDisplay, roundToPrecision } from "../../../../../share/number-format";
|
|
11
11
|
import { Field, FieldLabel, FieldMessages } from "../../../../ui/field";
|
|
12
12
|
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupNumberField } from "../../../../ui/input-group";
|
|
13
13
|
import { Markdown } from "../../../../ui/markdown";
|
|
@@ -16,7 +16,7 @@ import { DEFAULT_FIELD_ORIENTATION } from "../../../utils/common";
|
|
|
16
16
|
import { useFieldValue } from "../../../utils/field-value";
|
|
17
17
|
import { useFieldValidation } from "../../../utils/validation";
|
|
18
18
|
import { useFormReadonly } from "../../../utils/readonly";
|
|
19
|
-
import { useFormScope } from "../../../utils/state";
|
|
19
|
+
import { useFormScope, useFormState } from "../../../utils/state";
|
|
20
20
|
defineOptions({ name: "ShwfedNumberFieldRuntime" });
|
|
21
21
|
const props = defineProps({
|
|
22
22
|
fieldId: { type: String, required: true },
|
|
@@ -86,22 +86,29 @@ const { draft, commit } = useFieldValue({
|
|
|
86
86
|
return props.config.valueAsString ? String(next) : next;
|
|
87
87
|
}
|
|
88
88
|
});
|
|
89
|
+
const bag = useFormState();
|
|
90
|
+
watch(
|
|
91
|
+
() => [props.config.binding, props.config.precision, props.config.roundingMode, props.config.valueAsString],
|
|
92
|
+
([binding, precision, roundingMode, valueAsString], _prev, onCleanup) => {
|
|
93
|
+
if (binding == null || precision === void 0) return;
|
|
94
|
+
const mode = roundingMode ?? "round";
|
|
95
|
+
const unregister = bag.registerCoercion(binding, (value) => {
|
|
96
|
+
let n;
|
|
97
|
+
if (typeof value === "number" && Number.isFinite(value)) n = value;
|
|
98
|
+
else if (typeof value === "string" && value.length > 0 && Number.isFinite(Number(value))) n = Number(value);
|
|
99
|
+
if (n === void 0) return value;
|
|
100
|
+
const rounded = roundToPrecision(n, precision, mode);
|
|
101
|
+
return valueAsString ? String(rounded) : rounded;
|
|
102
|
+
});
|
|
103
|
+
onCleanup(unregister);
|
|
104
|
+
},
|
|
105
|
+
{ immediate: true }
|
|
106
|
+
);
|
|
89
107
|
const readonlyDisplay = computed(() => formatNumberDisplay(draft.value, {
|
|
90
108
|
displayMode: props.config.displayMode,
|
|
91
109
|
precision: props.config.precision,
|
|
92
110
|
roundingMode: props.config.roundingMode
|
|
93
111
|
}));
|
|
94
|
-
function applyRounding(n, precision, mode) {
|
|
95
|
-
const factor = 10 ** precision;
|
|
96
|
-
switch (mode) {
|
|
97
|
-
case "floor":
|
|
98
|
-
return Math.floor(n * factor) / factor;
|
|
99
|
-
case "ceil":
|
|
100
|
-
return Math.ceil(n * factor) / factor;
|
|
101
|
-
default:
|
|
102
|
-
return Math.round(n * factor) / factor;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
112
|
const addonConfig = computed(() => {
|
|
106
113
|
const addon = props.config.addon;
|
|
107
114
|
if (!addon || addon.items.length === 0) return null;
|
|
@@ -125,7 +132,7 @@ async function onBlur() {
|
|
|
125
132
|
if (precision !== void 0) {
|
|
126
133
|
const current = draft.value;
|
|
127
134
|
if (current !== void 0) {
|
|
128
|
-
const rounded =
|
|
135
|
+
const rounded = roundToPrecision(current, precision, props.config.roundingMode ?? "round");
|
|
129
136
|
if (rounded !== current) draft.value = rounded;
|
|
130
137
|
}
|
|
131
138
|
}
|
package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.numberrange/runtime.vue
CHANGED
|
@@ -6,6 +6,7 @@ import { useI18n } from "vue-i18n";
|
|
|
6
6
|
import { cel as _rawCel } from "../../../../../utils/cel";
|
|
7
7
|
import { celBindings, injectCELContext } from "../../../../../utils/cel-context";
|
|
8
8
|
import { getLocalizedText } from "../../../../../share/locale";
|
|
9
|
+
import { roundToPrecision } from "../../../../../share/number-format";
|
|
9
10
|
import { interpolateMarkdown } from "../../../../table/utils/runtime";
|
|
10
11
|
import { Field, FieldLabel, FieldMessages } from "../../../../ui/field";
|
|
11
12
|
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupNumberField } from "../../../../ui/input-group";
|
|
@@ -21,7 +22,8 @@ const props = defineProps({
|
|
|
21
22
|
config: { type: null, required: true }
|
|
22
23
|
});
|
|
23
24
|
const { locale } = useI18n();
|
|
24
|
-
const
|
|
25
|
+
const bag = useFormState();
|
|
26
|
+
const { getAt, setAt } = bag;
|
|
25
27
|
const formScope = useFormScope();
|
|
26
28
|
const inherited = injectCELContext();
|
|
27
29
|
const $cel = (expression, context) => _rawCel(expression, { ...celBindings(inherited), ...context });
|
|
@@ -158,17 +160,6 @@ const endMin = computed(() => {
|
|
|
158
160
|
if (typeof s === "number") return s;
|
|
159
161
|
return m;
|
|
160
162
|
});
|
|
161
|
-
function applyRounding(n, precision, mode) {
|
|
162
|
-
const factor = 10 ** precision;
|
|
163
|
-
switch (mode) {
|
|
164
|
-
case "floor":
|
|
165
|
-
return Math.floor(n * factor) / factor;
|
|
166
|
-
case "ceil":
|
|
167
|
-
return Math.ceil(n * factor) / factor;
|
|
168
|
-
default:
|
|
169
|
-
return Math.round(n * factor) / factor;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
163
|
async function onBlurSide(side) {
|
|
173
164
|
await nextTick();
|
|
174
165
|
const precision = props.config.precision;
|
|
@@ -176,12 +167,42 @@ async function onBlurSide(side) {
|
|
|
176
167
|
const ref_ = side === "start" ? localStart : localEnd;
|
|
177
168
|
const current = ref_.value;
|
|
178
169
|
if (current !== void 0) {
|
|
179
|
-
const rounded =
|
|
170
|
+
const rounded = roundToPrecision(current, precision, props.config.roundingMode ?? "round");
|
|
180
171
|
if (rounded !== current) ref_.value = rounded;
|
|
181
172
|
}
|
|
182
173
|
}
|
|
183
174
|
commit();
|
|
184
175
|
}
|
|
176
|
+
function roundScalar(precision, mode, value) {
|
|
177
|
+
let n;
|
|
178
|
+
if (typeof value === "number" && Number.isFinite(value)) n = value;
|
|
179
|
+
else if (typeof value === "string" && value.length > 0 && Number.isFinite(Number(value))) n = Number(value);
|
|
180
|
+
if (n === void 0) return value;
|
|
181
|
+
const rounded = roundToPrecision(n, precision, mode);
|
|
182
|
+
return props.config.valueAsString ? String(rounded) : rounded;
|
|
183
|
+
}
|
|
184
|
+
watch(
|
|
185
|
+
() => [props.config.binding, props.config.precision, props.config.roundingMode, props.config.valueAsString],
|
|
186
|
+
([binding, precision, roundingMode], _prev, onCleanup) => {
|
|
187
|
+
if (binding == null || precision === void 0) return;
|
|
188
|
+
const mode = roundingMode ?? "round";
|
|
189
|
+
const unregisters = [];
|
|
190
|
+
if (typeof binding === "string") {
|
|
191
|
+
unregisters.push(bag.registerCoercion(
|
|
192
|
+
binding,
|
|
193
|
+
(value) => Array.isArray(value) && value.length === 2 ? [roundScalar(precision, mode, value[0]), roundScalar(precision, mode, value[1])] : value
|
|
194
|
+
));
|
|
195
|
+
} else {
|
|
196
|
+
for (const path of binding) {
|
|
197
|
+
unregisters.push(bag.registerCoercion(path, (value) => roundScalar(precision, mode, value)));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
onCleanup(() => {
|
|
201
|
+
for (const off of unregisters) off();
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
{ immediate: true }
|
|
205
|
+
);
|
|
185
206
|
const showClear = computed(
|
|
186
207
|
() => !isDisabled.value && (localStart.value !== void 0 || localEnd.value !== void 0)
|
|
187
208
|
);
|
package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.multi/runtime.vue
CHANGED
|
@@ -397,29 +397,33 @@ const readonlyText = computed(() => {
|
|
|
397
397
|
:class="isDisabled ? 'pointer-events-none opacity-60' : void 0"
|
|
398
398
|
>
|
|
399
399
|
<template #node="{ node, selected, indeterminate }">
|
|
400
|
-
<span class="flex
|
|
401
|
-
<!--
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
<
|
|
406
|
-
:
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
400
|
+
<span class="flex flex-col gap-0.5">
|
|
401
|
+
<!-- Checkbox shares a centered row with the label so it aligns to
|
|
402
|
+
the label's line box (not the top of the label+tooltip stack).
|
|
403
|
+
The tooltip drops below, indented `pl-6` (checkbox `size-4` +
|
|
404
|
+
`gap-2`) to hang under the label text. -->
|
|
405
|
+
<span class="flex items-center gap-2">
|
|
406
|
+
<!-- Presentational checkbox: mirrors the row's selection state but
|
|
407
|
+
never owns it. `pointer-events-none` + `tabindex=-1` keep the
|
|
408
|
+
existing row-click / keyboard toggle in `<UiTree>` the single
|
|
409
|
+
source of truth — the control is a visual reflection only. -->
|
|
410
|
+
<Checkbox
|
|
411
|
+
:model-value="indeterminate ? 'indeterminate' : selected"
|
|
412
|
+
class="pointer-events-none shrink-0"
|
|
413
|
+
tabindex="-1"
|
|
414
|
+
aria-hidden="true"
|
|
415
|
+
/>
|
|
412
416
|
<Markdown
|
|
413
417
|
:source="nodeLabelText(node)"
|
|
414
418
|
class="prose prose-sm prose-zinc text-zinc-700"
|
|
415
419
|
/>
|
|
416
|
-
<Markdown
|
|
417
|
-
v-if="nodeTooltipText(node)"
|
|
418
|
-
:source="nodeTooltipText(node)"
|
|
419
|
-
block
|
|
420
|
-
class="prose prose-xs prose-zinc text-zinc-400"
|
|
421
|
-
/>
|
|
422
420
|
</span>
|
|
421
|
+
<Markdown
|
|
422
|
+
v-if="nodeTooltipText(node)"
|
|
423
|
+
:source="nodeTooltipText(node)"
|
|
424
|
+
block
|
|
425
|
+
class="prose prose-xs prose-zinc text-zinc-400 pl-6"
|
|
426
|
+
/>
|
|
423
427
|
</span>
|
|
424
428
|
</template>
|
|
425
429
|
|
|
@@ -55,7 +55,7 @@ export function useDerivedQuiescence() {
|
|
|
55
55
|
return inject(QUIESCENCE_KEY, null) ?? createQuiescence();
|
|
56
56
|
}
|
|
57
57
|
export function useDerived(options) {
|
|
58
|
-
const { state, getAt, setAtSilent, isDirty, markDirty } = options.formState;
|
|
58
|
+
const { state, getAt, setAtSilent, isDirty, markDirty, coerce } = options.formState;
|
|
59
59
|
const quiescence = options.quiescence;
|
|
60
60
|
const derivedFields = computed(
|
|
61
61
|
() => options.fields().flatMap((field) => {
|
|
@@ -76,7 +76,7 @@ export function useDerived(options) {
|
|
|
76
76
|
(result) => {
|
|
77
77
|
if (gen !== generation) return;
|
|
78
78
|
if (field.mode === "prefill" && isDirty(field.binding)) return;
|
|
79
|
-
const normalized = result === void 0 ? null : result;
|
|
79
|
+
const normalized = coerce(field.binding, result === void 0 ? null : result);
|
|
80
80
|
if (!deepEqual(getAt(field.binding), normalized)) setAtSilent(field.binding, normalized);
|
|
81
81
|
},
|
|
82
82
|
() => {
|
|
@@ -63,6 +63,24 @@ export type FormStateBag = {
|
|
|
63
63
|
* expression will not overwrite it.
|
|
64
64
|
*/
|
|
65
65
|
markDirty: (path: string) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Register a value coercion for one binding. It runs on EVERY write to that
|
|
68
|
+
* path — user commit, `setAtSilent`, `state.write` action, or a `derived`
|
|
69
|
+
* write-back — so a field whose stored shape is constrained can enforce that
|
|
70
|
+
* constraint at the storage boundary rather than only in its own input
|
|
71
|
+
* handler. The number field registers rounding-to-小数位数 here, so a value a
|
|
72
|
+
* `derived` formula computes (which never passes through the field's blur
|
|
73
|
+
* handler) still lands rounded. Returns an unregister.
|
|
74
|
+
*
|
|
75
|
+
* The coercion MUST be idempotent — `coerce(coerce(x))` has to equal
|
|
76
|
+
* `coerce(x)`. `useDerived` compares its freshly evaluated result against the
|
|
77
|
+
* already-coerced stored value to decide whether to re-write; a non-idempotent
|
|
78
|
+
* coercion would make that comparison never converge and loop the formula on
|
|
79
|
+
* its own write-back.
|
|
80
|
+
*/
|
|
81
|
+
registerCoercion: (path: string, fn: (value: unknown) => unknown) => () => void;
|
|
82
|
+
/** Apply the coercion registered for `path` (identity when none). */
|
|
83
|
+
coerce: (path: string, value: unknown) => unknown;
|
|
66
84
|
};
|
|
67
85
|
export declare const FORM_STATE_KEY: InjectionKey<FormStateBag>;
|
|
68
86
|
export declare function provideFormState(state: Ref<unknown>, kind?: FormStateBagKind): FormStateBag;
|
|
@@ -5,8 +5,14 @@ export const FORM_STATE_KEY = Symbol("shwfed-form-state");
|
|
|
5
5
|
export function provideFormState(state, kind = "form") {
|
|
6
6
|
const parent = inject(FORM_STATE_KEY, void 0);
|
|
7
7
|
const dirty = ref(/* @__PURE__ */ new Set());
|
|
8
|
+
const coercions = /* @__PURE__ */ new Map();
|
|
9
|
+
function coerce(path, value) {
|
|
10
|
+
const fn = coercions.get(path);
|
|
11
|
+
return fn ? fn(value) : value;
|
|
12
|
+
}
|
|
8
13
|
function write(path, value) {
|
|
9
|
-
const
|
|
14
|
+
const coerced = coerce(path, value === void 0 ? null : value);
|
|
15
|
+
const normalized = coerced === void 0 ? null : coerced;
|
|
10
16
|
if (path === SELF_BINDING) {
|
|
11
17
|
state.value = normalized;
|
|
12
18
|
return;
|
|
@@ -30,7 +36,14 @@ export function provideFormState(state, kind = "form") {
|
|
|
30
36
|
setAtSilent: write,
|
|
31
37
|
dirty,
|
|
32
38
|
isDirty: (path) => dirty.value.has(path),
|
|
33
|
-
markDirty
|
|
39
|
+
markDirty,
|
|
40
|
+
registerCoercion: (path, fn) => {
|
|
41
|
+
coercions.set(path, fn);
|
|
42
|
+
return () => {
|
|
43
|
+
if (coercions.get(path) === fn) coercions.delete(path);
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
coerce
|
|
34
47
|
};
|
|
35
48
|
provide(FORM_STATE_KEY, bag);
|
|
36
49
|
return bag;
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { Icon } from "@iconify/vue";
|
|
3
3
|
import { Effect } from "effect";
|
|
4
|
-
import { computed, nextTick } from "vue";
|
|
4
|
+
import { computed, nextTick, watch } from "vue";
|
|
5
5
|
import { useI18n } from "vue-i18n";
|
|
6
6
|
import { cel as _rawCel } from "../../../../../utils/cel";
|
|
7
7
|
import { celScope, injectCELContext } from "../../../../../utils/cel-context";
|
|
8
8
|
import { useFormReadonly } from "../../../../form/utils/readonly";
|
|
9
9
|
import { getLocalizedText } from "../../../../../share/locale";
|
|
10
|
-
import { formatNumberDisplay } from "../../../../../share/number-format";
|
|
10
|
+
import { formatNumberDisplay, roundToPrecision } from "../../../../../share/number-format";
|
|
11
11
|
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupNumberField } from "../../../../ui/input-group";
|
|
12
12
|
import ShwfedActions from "../../../../actions/components/group.vue";
|
|
13
13
|
import { useFieldValue } from "../../../../form/utils/field-value";
|
|
14
|
+
import { useFormState } from "../../../../form/utils/state";
|
|
14
15
|
import { JUSTIFY_CLASS, TEXT_ALIGN_CLASS } from "../../../utils/runtime";
|
|
15
16
|
defineOptions({ name: "ShwfedTableNumberInputRendererRuntime" });
|
|
16
17
|
const props = defineProps({
|
|
@@ -76,17 +77,24 @@ const readonlyDisplay = computed(() => formatNumberDisplay(draft.value, {
|
|
|
76
77
|
precision: props.column.precision,
|
|
77
78
|
roundingMode: props.column.roundingMode
|
|
78
79
|
}));
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
const bag = useFormState();
|
|
81
|
+
watch(
|
|
82
|
+
() => [props.column.binding, props.column.precision, props.column.roundingMode, props.column.valueAsString],
|
|
83
|
+
([binding, precision, roundingMode, valueAsString], _prev, onCleanup) => {
|
|
84
|
+
if (binding == null || precision === void 0) return;
|
|
85
|
+
const mode = roundingMode ?? "round";
|
|
86
|
+
const unregister = bag.registerCoercion(binding, (value) => {
|
|
87
|
+
let n;
|
|
88
|
+
if (typeof value === "number" && Number.isFinite(value)) n = value;
|
|
89
|
+
else if (typeof value === "string" && value.length > 0 && Number.isFinite(Number(value))) n = Number(value);
|
|
90
|
+
if (n === void 0) return value;
|
|
91
|
+
const rounded = roundToPrecision(n, precision, mode);
|
|
92
|
+
return valueAsString ? String(rounded) : rounded;
|
|
93
|
+
});
|
|
94
|
+
onCleanup(unregister);
|
|
95
|
+
},
|
|
96
|
+
{ immediate: true }
|
|
97
|
+
);
|
|
90
98
|
const addonConfig = computed(() => {
|
|
91
99
|
const addon = props.column.addon;
|
|
92
100
|
if (!addon || addon.items.length === 0) return null;
|
|
@@ -112,7 +120,7 @@ async function onBlur() {
|
|
|
112
120
|
if (precision !== void 0) {
|
|
113
121
|
const current = draft.value;
|
|
114
122
|
if (current !== void 0) {
|
|
115
|
-
const rounded =
|
|
123
|
+
const rounded = roundToPrecision(current, precision, props.column.roundingMode ?? "round");
|
|
116
124
|
if (rounded !== current) draft.value = rounded;
|
|
117
125
|
}
|
|
118
126
|
}
|
|
@@ -612,10 +612,7 @@ async function pasteColumns() {
|
|
|
612
612
|
const clip = await readClip();
|
|
613
613
|
if (!clip || clip.surface !== "table") return;
|
|
614
614
|
const { items } = reidFragment(clip.items);
|
|
615
|
-
const pasted = items.map(({ groupId: _g, ...rest }) =>
|
|
616
|
-
...rest,
|
|
617
|
-
id: crypto.randomUUID()
|
|
618
|
-
}));
|
|
615
|
+
const pasted = items.map(({ groupId: _g, ...rest }) => rest);
|
|
619
616
|
if (pasted.length === 0) return;
|
|
620
617
|
editingColumns.value = [...editingColumns.value, ...pasted];
|
|
621
618
|
const ids = pasted.map((c) => c.id);
|
|
@@ -810,7 +810,7 @@ export { TableConfig, createTableConfig, getColumnTechnicalKey } from "./schema"
|
|
|
810
810
|
/>
|
|
811
811
|
</div>
|
|
812
812
|
<nav
|
|
813
|
-
v-if="tableActionsConfig"
|
|
813
|
+
v-if="tableActionsConfig && !formReadonly"
|
|
814
814
|
class="flex items-center justify-between"
|
|
815
815
|
>
|
|
816
816
|
<ShwfedActions
|
|
@@ -1246,7 +1246,7 @@ export { TableConfig, createTableConfig, getColumnTechnicalKey } from "./schema"
|
|
|
1246
1246
|
</ClientOnly>
|
|
1247
1247
|
</div>
|
|
1248
1248
|
<nav
|
|
1249
|
-
v-if="tableBottomActionsConfig"
|
|
1249
|
+
v-if="tableBottomActionsConfig && !formReadonly"
|
|
1250
1250
|
class="flex items-center justify-between"
|
|
1251
1251
|
>
|
|
1252
1252
|
<ShwfedActions
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type NumberDisplayMode = 'plain' | 'uppercaseChinese' | 'monoThousand';
|
|
2
2
|
export declare function toChineseUppercase(n: number): string;
|
|
3
|
+
export declare function roundToPrecision(n: number, precision: number, mode?: 'round' | 'floor' | 'ceil'): number;
|
|
3
4
|
export declare function formatNumberDisplay(value: unknown, opts: {
|
|
4
5
|
displayMode?: NumberDisplayMode;
|
|
5
6
|
precision?: number;
|
|
@@ -35,6 +35,17 @@ export function toChineseUppercase(n) {
|
|
|
35
35
|
}
|
|
36
36
|
return isNegative ? `\u8D1F${result}` : result;
|
|
37
37
|
}
|
|
38
|
+
export function roundToPrecision(n, precision, mode = "round") {
|
|
39
|
+
const factor = 10 ** precision;
|
|
40
|
+
switch (mode) {
|
|
41
|
+
case "floor":
|
|
42
|
+
return Math.floor(n * factor) / factor;
|
|
43
|
+
case "ceil":
|
|
44
|
+
return Math.ceil(n * factor) / factor;
|
|
45
|
+
default:
|
|
46
|
+
return Math.round(n * factor) / factor;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
38
49
|
export function formatNumberDisplay(value, opts) {
|
|
39
50
|
if (value === void 0 || value === null) return "-";
|
|
40
51
|
const n = Number(value);
|
|
@@ -45,20 +56,8 @@ export function formatNumberDisplay(value, opts) {
|
|
|
45
56
|
}
|
|
46
57
|
let result;
|
|
47
58
|
if (opts.precision !== void 0) {
|
|
48
|
-
const mode = opts.roundingMode ?? "round";
|
|
49
59
|
const digits = opts.precision;
|
|
50
|
-
const
|
|
51
|
-
let processed;
|
|
52
|
-
switch (mode) {
|
|
53
|
-
case "floor":
|
|
54
|
-
processed = Math.floor(n * factor) / factor;
|
|
55
|
-
break;
|
|
56
|
-
case "ceil":
|
|
57
|
-
processed = Math.ceil(n * factor) / factor;
|
|
58
|
-
break;
|
|
59
|
-
default:
|
|
60
|
-
processed = Math.round(n * factor) / factor;
|
|
61
|
-
}
|
|
60
|
+
const processed = roundToPrecision(n, digits, opts.roundingMode ?? "round");
|
|
62
61
|
result = processed.toFixed(digits);
|
|
63
62
|
} else {
|
|
64
63
|
result = String(n);
|