@shwfed/config 2.9.7 → 2.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/mcp.mjs +1054 -855
  2. package/dist/module.json +1 -1
  3. package/dist/preview/assets/{FieldGroup.vue_vue_type_script_setup_true_lang-tkU7rYrs.js → FieldGroup.vue_vue_type_script_setup_true_lang-CdbT3cSj.js} +1 -1
  4. package/dist/preview/assets/{badge-C55P2V8D.js → badge-DL-wtgr8.js} +1 -1
  5. package/dist/preview/assets/{config-DeaHg7ps.js → config-80OJWswM.js} +1 -1
  6. package/dist/preview/assets/{config-D0j4njOQ.js → config-B8F7X1oF.js} +1 -1
  7. package/dist/preview/assets/{config-rL7m78ZC.js → config-B9BcmOZt.js} +1 -1
  8. package/dist/preview/assets/{config-Bc2N2jlD.js → config-C-Ta4ALL.js} +1 -1
  9. package/dist/preview/assets/{config-DqibnZH4.js → config-C8R7tsmC.js} +1 -1
  10. package/dist/preview/assets/{config-CnxzerC5.js → config-CoObmLL2.js} +1 -1
  11. package/dist/preview/assets/{config-Bu1S_DR5.js → config-DBQ-1J66.js} +1 -1
  12. package/dist/preview/assets/{config-CH2QpakU.js → config-DVk8b1Qf.js} +1 -1
  13. package/dist/preview/assets/{config-DzKcQKJj.js → config-DjsQoLiy.js} +1 -1
  14. package/dist/preview/assets/{config-j1oKH3Ci.js → config-RmdB0dwp.js} +1 -1
  15. package/dist/preview/assets/{config-B3-xqMew.js → config-h_hDvpHt.js} +1 -1
  16. package/dist/preview/assets/{definition.vue_vue_type_script_setup_true_lang-G748uLwi.js → definition.vue_vue_type_script_setup_true_lang-SX94Z692.js} +1 -1
  17. package/dist/preview/assets/index-BIwwDhfa.js +1 -0
  18. package/dist/preview/assets/index-C1h9lV52.css +1 -0
  19. package/dist/preview/assets/index-CsN0336H.js +717 -0
  20. package/dist/preview/assets/{index-dIh1Jn4U.js → index-Df0XE7Lz.js} +1 -1
  21. package/dist/preview/assets/{item-BCWcrKaL.js → item-B9XcOWjy.js} +1 -1
  22. package/dist/preview/assets/{runtime-CBg0VVCJ.js → runtime-BBsXfAZm.js} +1 -1
  23. package/dist/preview/assets/{runtime-D3LGPlv3.js → runtime-BHlUDfbv.js} +1 -1
  24. package/dist/preview/assets/{runtime-BTFesnrJ.js → runtime-BrwIgXY3.js} +1 -1
  25. package/dist/preview/assets/{runtime-zoq6YaMz.js → runtime-C--WPQ3o.js} +1 -1
  26. package/dist/preview/assets/{runtime-BsqCaddm.js → runtime-C0yBdMlV.js} +1 -1
  27. package/dist/preview/assets/{runtime-C-KLJ71q.js → runtime-C6uebSDU.js} +1 -1
  28. package/dist/preview/assets/{runtime-DZiqhnCm.js → runtime-DU0iHs9-.js} +1 -1
  29. package/dist/preview/assets/{runtime-bRCsEM7S.js → runtime-Dqa4phvv.js} +1 -1
  30. package/dist/preview/assets/{runtime-C5-cN3M1.js → runtime-Dr-MmEyE.js} +1 -1
  31. package/dist/preview/assets/{runtime-DO8ov3J6.js → runtime-v_nJoD5I.js} +1 -1
  32. package/dist/preview/index.html +2 -2
  33. package/dist/runtime/components/actions/buttons/2026-04-18/com.shwfed.actions.button.http.request.json/schema.d.ts +1 -1
  34. package/dist/runtime/components/actions/buttons/2026-04-18/com.shwfed.actions.button.http.request.json/schema.js +2 -2
  35. package/dist/runtime/components/actions/buttons/2026-06-08/com.shwfed.actions.button.http.request.json.batch/schema.d.ts +1 -1
  36. package/dist/runtime/components/actions/buttons/2026-06-08/com.shwfed.actions.button.http.request.json.batch/schema.js +1 -1
  37. package/dist/runtime/components/block-layout-editor/index.vue +98 -3
  38. package/dist/runtime/components/config/footer.vue +22 -20
  39. package/dist/runtime/components/config/use-editor.js +5 -9
  40. package/dist/runtime/components/config/utils/validation-error.d.ts +1 -0
  41. package/dist/runtime/components/config/utils/validation-error.js +34 -0
  42. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.daterange/config.d.vue.ts +4 -4
  43. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.daterange/config.vue.d.ts +4 -4
  44. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.datetimerange/config.d.vue.ts +6 -6
  45. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.datetimerange/config.vue.d.ts +6 -6
  46. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.timerange/config.d.vue.ts +2 -2
  47. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.timerange/config.vue.d.ts +2 -2
  48. package/dist/runtime/components/form/fields/2026-05-24/com.shwfed.form.field.monthrange/config.d.vue.ts +4 -4
  49. package/dist/runtime/components/form/fields/2026-05-24/com.shwfed.form.field.monthrange/config.vue.d.ts +4 -4
  50. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/config.d.vue.ts +155 -0
  51. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/config.vue +918 -0
  52. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/config.vue.d.ts +155 -0
  53. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/runtime.d.vue.ts +8 -0
  54. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/runtime.vue +482 -0
  55. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/runtime.vue.d.ts +8 -0
  56. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/schema.d.ts +126 -0
  57. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/schema.js +178 -0
  58. package/dist/runtime/components/table/columns/2026-04-14/com.shwfed.table.column.markdown/runtime.vue +1 -1
  59. package/dist/runtime/components/ui/date-picker/DatePickerInput.d.vue.ts +1 -1
  60. package/dist/runtime/components/ui/date-picker/DatePickerInput.vue.d.ts +1 -1
  61. package/dist/runtime/components/ui/date-picker/DatePickerTimeInput.d.vue.ts +1 -1
  62. package/dist/runtime/components/ui/date-picker/DatePickerTimeInput.vue.d.ts +1 -1
  63. package/dist/runtime/components/ui/date-range-picker/DateRangePickerInput.d.vue.ts +1 -1
  64. package/dist/runtime/components/ui/date-range-picker/DateRangePickerInput.vue.d.ts +1 -1
  65. package/dist/runtime/components/ui/date-range-picker/DateRangePickerTimeInput.d.vue.ts +2 -2
  66. package/dist/runtime/components/ui/date-range-picker/DateRangePickerTimeInput.vue.d.ts +2 -2
  67. package/dist/runtime/vendor/cel-js/CLAUDE.md +1 -1
  68. package/dist/runtime/vendor/cel-js/PROMPT.md +2 -2
  69. package/dist/runtime/vendor/cel-js/lib/http-builder.js +13 -3
  70. package/package.json +1 -1
  71. package/dist/preview/assets/index-C16exop7.js +0 -717
  72. package/dist/preview/assets/index-CSm6dB4o.js +0 -1
  73. package/dist/preview/assets/index-vPcvbp7e.css +0 -1
@@ -0,0 +1,918 @@
1
+ <script setup>
2
+ import { Icon } from "@iconify/vue";
3
+ import { computed, ref, watch } from "vue";
4
+ import {
5
+ useTreeDnd
6
+ } from "../../../../../composables/useTreeDnd";
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuTrigger
12
+ } from "../../../../ui/dropdown-menu";
13
+ import { ExpressionEditor } from "../../../../ui/expression-editor";
14
+ import ValidationRulesField from "../../../ValidationRulesField.vue";
15
+ import { Button } from "../../../../ui/button";
16
+ import { Field, FieldLabel } from "../../../../ui/field";
17
+ import { IconPicker } from "../../../../ui/icon-picker";
18
+ import {
19
+ InputGroup,
20
+ InputGroupAddon,
21
+ InputGroupButton,
22
+ InputGroupInput,
23
+ InputGroupNumberField
24
+ } from "../../../../ui/input-group";
25
+ import { Locale as LocaleField } from "../../../../ui/locale";
26
+ import { Markdown } from "../../../../ui/markdown";
27
+ import {
28
+ Select,
29
+ SelectContent,
30
+ SelectItem,
31
+ SelectTrigger,
32
+ SelectValue
33
+ } from "../../../../ui/select";
34
+ import { Switch } from "../../../../ui/switch";
35
+ import { getStructFieldDescription, getStructFieldTitle } from "../../../schema";
36
+ import { DEFAULT_FIELD_ORIENTATION, FIELD_ORIENTATION_OPTIONS } from "../../../utils/common";
37
+ import { FILES_VAR, FILE_VAR, JSON_VAR, UPLOAD_JSON_VAR, schema } from "./schema";
38
+ defineOptions({ name: "ShwfedUploadFieldConfig" });
39
+ const value = defineModel({ type: null, ...{ required: true } });
40
+ const fieldSchema = schema(() => {
41
+ });
42
+ const fieldTitle = (f) => getStructFieldTitle(fieldSchema, f) ?? f;
43
+ const fieldDescription = (f) => getStructFieldDescription(fieldSchema, f);
44
+ const pathText = computed({
45
+ get: () => value.value.binding ?? "",
46
+ set: (next) => {
47
+ const trimmed = next.trim();
48
+ if (trimmed.length === 0) {
49
+ const { binding: _omit, ...rest } = value.value;
50
+ value.value = rest;
51
+ } else {
52
+ value.value = { ...value.value, binding: trimmed };
53
+ }
54
+ }
55
+ });
56
+ const uploadEnabled = computed(() => value.value.upload != null);
57
+ function setUploadEnabled(on) {
58
+ if (on) {
59
+ if (value.value.upload) return;
60
+ value.value = { ...value.value, upload: { request: "", handle: "" } };
61
+ } else {
62
+ const { upload: _omit, ...rest } = value.value;
63
+ value.value = rest;
64
+ }
65
+ }
66
+ const uploadRequestText = computed({
67
+ get: () => value.value.upload?.request ?? "",
68
+ set: (next) => {
69
+ const current = value.value.upload;
70
+ if (!current) return;
71
+ value.value = { ...value.value, upload: { ...current, request: next } };
72
+ }
73
+ });
74
+ const uploadHandleText = computed({
75
+ get: () => value.value.upload?.handle ?? "",
76
+ set: (next) => {
77
+ const current = value.value.upload;
78
+ if (!current) return;
79
+ value.value = { ...value.value, upload: { ...current, handle: next } };
80
+ }
81
+ });
82
+ const uploadFilenameText = computed({
83
+ get: () => value.value.upload?.filename ?? "",
84
+ set: (next) => {
85
+ const current = value.value.upload;
86
+ if (!current) return;
87
+ if (next.length === 0) {
88
+ const { filename: _omit, ...rest } = current;
89
+ value.value = { ...value.value, upload: rest };
90
+ } else {
91
+ value.value = { ...value.value, upload: { ...current, filename: next } };
92
+ }
93
+ }
94
+ });
95
+ const templates = computed(() => value.value.templates ?? []);
96
+ function setTemplates(next) {
97
+ if (next.length === 0) {
98
+ const { templates: _omit, ...rest } = value.value;
99
+ value.value = rest;
100
+ } else {
101
+ value.value = { ...value.value, templates: next };
102
+ }
103
+ }
104
+ function makeId() {
105
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
106
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
107
+ const r = Math.random() * 16 | 0;
108
+ return (c === "x" ? r : r & 3 | 8).toString(16);
109
+ });
110
+ }
111
+ const rowKeys = ref([]);
112
+ watch(
113
+ () => templates.value.length,
114
+ (len) => {
115
+ if (len === rowKeys.value.length) return;
116
+ rowKeys.value = Array.from({ length: len }, () => makeId());
117
+ },
118
+ { immediate: true }
119
+ );
120
+ function addTemplate() {
121
+ setTemplates([...templates.value, { request: "" }]);
122
+ rowKeys.value = [...rowKeys.value, makeId()];
123
+ }
124
+ function removeTemplate(index) {
125
+ setTemplates(templates.value.filter((_, i) => i !== index));
126
+ rowKeys.value = rowKeys.value.filter((_, i) => i !== index);
127
+ }
128
+ function move(from, to) {
129
+ if (from === to) return;
130
+ if (from < 0 || to < 0 || from >= templates.value.length || to >= templates.value.length) return;
131
+ const next = [...templates.value];
132
+ const [entry] = next.splice(from, 1);
133
+ if (!entry) return;
134
+ next.splice(to, 0, entry);
135
+ setTemplates(next);
136
+ const keys = [...rowKeys.value];
137
+ const [key] = keys.splice(from, 1);
138
+ if (!key) return;
139
+ keys.splice(to, 0, key);
140
+ rowKeys.value = keys;
141
+ }
142
+ function patchTemplate(index, patch) {
143
+ setTemplates(templates.value.map((entry, i) => i === index ? patch(entry) : entry));
144
+ }
145
+ const TEMPLATE_ROW_KIND = "shwfed-upload-template";
146
+ const rowId = (i) => rowKeys.value[i] ?? `idx-${i}`;
147
+ function onTemplateRowDrop(e) {
148
+ if (e.source.kind !== TEMPLATE_ROW_KIND || e.target.kind !== TEMPLATE_ROW_KIND) return;
149
+ const from = rowKeys.value.findIndex((k) => k === e.source.id);
150
+ const to = rowKeys.value.findIndex((k) => k === e.target.id);
151
+ if (from < 0 || to < 0) return;
152
+ if (e.instruction === "reorder-above") move(from, from < to ? to - 1 : to);
153
+ else if (e.instruction === "reorder-below") move(from, from <= to ? to : to + 1);
154
+ }
155
+ const templateDnd = useTreeDnd({ onRowDrop: onTemplateRowDrop });
156
+ const pickDragHandle = (el) => el.querySelector(".drag-handle");
157
+ function templateRowConfig(i) {
158
+ return {
159
+ kind: TEMPLATE_ROW_KIND,
160
+ canDrop: (src) => src.kind === TEMPLATE_ROW_KIND,
161
+ blockInstructions: (src) => {
162
+ const block = ["make-child", "reparent"];
163
+ if (src.id === rowId(i)) block.push("reorder-above", "reorder-below");
164
+ return block;
165
+ },
166
+ dragHandle: pickDragHandle
167
+ };
168
+ }
169
+ function updateTemplateRequest(index, request) {
170
+ patchTemplate(index, (entry) => ({ ...entry, request }));
171
+ }
172
+ function updateTemplateDownload(index, download) {
173
+ patchTemplate(index, (entry) => {
174
+ if (download.length === 0) {
175
+ const { download: _omit, ...rest } = entry;
176
+ return rest;
177
+ }
178
+ return { ...entry, download };
179
+ });
180
+ }
181
+ function updateTemplateIcon(index, icon) {
182
+ patchTemplate(index, (entry) => {
183
+ const trimmed = icon.trim();
184
+ if (trimmed.length === 0) {
185
+ const { icon: _omit, ...rest } = entry;
186
+ return rest;
187
+ }
188
+ return { ...entry, icon: trimmed };
189
+ });
190
+ }
191
+ function updateTemplateLabel(index, label) {
192
+ patchTemplate(index, (entry) => {
193
+ if (label == null || label.length === 0) {
194
+ const { label: _omit, ...rest } = entry;
195
+ return rest;
196
+ }
197
+ return { ...entry, label };
198
+ });
199
+ }
200
+ const MIME_GROUPS = [
201
+ { id: "pdf", label: "PDF", mimes: ["application/pdf"] },
202
+ { id: "docx", label: "DOCX", mimes: ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"] },
203
+ { id: "doc", label: "DOC", mimes: ["application/msword"] },
204
+ { id: "xlsx", label: "XLSX", mimes: ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"] },
205
+ { id: "xls", label: "XLS", mimes: ["application/vnd.ms-excel"] },
206
+ { id: "pptx", label: "PPTX", mimes: ["application/vnd.openxmlformats-officedocument.presentationml.presentation"] },
207
+ { id: "ppt", label: "PPT", mimes: ["application/vnd.ms-powerpoint"] },
208
+ { id: "zip", label: "ZIP", mimes: ["application/zip", "application/x-zip-compressed"] },
209
+ { id: "rar", label: "RAR", mimes: ["application/vnd.rar", "application/x-rar-compressed"] },
210
+ { id: "ofd", label: "OFD", mimes: ["application/ofd"] },
211
+ { id: "xml", label: "XML", mimes: ["application/xml"] },
212
+ { id: "png", label: "PNG", mimes: ["image/png"] },
213
+ { id: "jpeg", label: "JPEG", mimes: ["image/jpeg", "image/jpg"] }
214
+ ];
215
+ const GROUP_BY_ID = new Map(MIME_GROUPS.map((g) => [g.id, g]));
216
+ const acceptList = computed(() => value.value.accept ?? []);
217
+ const selectedGroupIds = computed(
218
+ () => MIME_GROUPS.filter((g) => g.mimes.every((m) => acceptList.value.includes(m))).map((g) => g.id)
219
+ );
220
+ const selectedGroupLabels = computed(
221
+ () => selectedGroupIds.value.map((id) => GROUP_BY_ID.get(id)?.label).filter(Boolean).join("\u3001")
222
+ );
223
+ function onAcceptChange(next) {
224
+ const ids = Array.isArray(next) ? next.filter((v) => typeof v === "string") : typeof next === "string" ? [next] : [];
225
+ const mimes = ids.flatMap((id) => GROUP_BY_ID.get(id)?.mimes ?? []);
226
+ if (mimes.length === 0) {
227
+ const { accept: _omit, ...rest } = value.value;
228
+ value.value = rest;
229
+ } else {
230
+ value.value = { ...value.value, accept: mimes };
231
+ }
232
+ }
233
+ function pickUnitFor(bytes) {
234
+ if (bytes === void 0) return "MB";
235
+ return bytes < 1024 * 1024 ? "KB" : "MB";
236
+ }
237
+ function multiplierFor(unit) {
238
+ return unit === "KB" ? 1024 : 1024 * 1024;
239
+ }
240
+ function makeSizeBinding(prop) {
241
+ const unit = ref(pickUnitFor(value.value[prop]));
242
+ const amount = computed({
243
+ get: () => {
244
+ const v = value.value[prop];
245
+ return v === void 0 ? void 0 : v / multiplierFor(unit.value);
246
+ },
247
+ set: (next) => {
248
+ if (next === void 0 || !(next > 0)) {
249
+ const { [prop]: _omit, ...rest } = value.value;
250
+ value.value = rest;
251
+ return;
252
+ }
253
+ value.value = { ...value.value, [prop]: Math.round(next * multiplierFor(unit.value)) };
254
+ }
255
+ });
256
+ return { unit, amount };
257
+ }
258
+ const perFile = makeSizeBinding("maxFileSize");
259
+ const total = makeSizeBinding("maxTotalSize");
260
+ function setMaxFiles(v) {
261
+ if (v === void 0 || !(v > 0)) {
262
+ const { maxFiles: _omit, ...rest } = value.value;
263
+ value.value = rest;
264
+ } else {
265
+ value.value = { ...value.value, maxFiles: Math.round(v) };
266
+ }
267
+ }
268
+ function setMultiple(v) {
269
+ if (!v) {
270
+ const { multiple: _omit, ...rest } = value.value;
271
+ value.value = rest;
272
+ } else {
273
+ value.value = { ...value.value, multiple: true };
274
+ }
275
+ }
276
+ </script>
277
+
278
+ <template>
279
+ <div class="flex flex-col gap-3">
280
+ <div class="grid grid-cols-2 gap-3">
281
+ <Field orientation="vertical">
282
+ <FieldLabel class="text-xs text-zinc-500">
283
+ <template
284
+ v-if="fieldDescription('displayName')"
285
+ #tooltip
286
+ >
287
+ <Markdown
288
+ :source="fieldDescription('displayName')"
289
+ block
290
+ class="prose prose-sm prose-zinc"
291
+ />
292
+ </template>
293
+ {{ fieldTitle("displayName") }}
294
+ </FieldLabel>
295
+ <InputGroup>
296
+ <InputGroupInput
297
+ :model-value="value.displayName ?? ''"
298
+ placeholder="例:附件"
299
+ @update:model-value="(v) => {
300
+ const s = String(v ?? '');
301
+ value = { ...value, displayName: s.length > 0 ? s : void 0 };
302
+ }"
303
+ />
304
+ </InputGroup>
305
+ </Field>
306
+
307
+ <Field orientation="vertical">
308
+ <FieldLabel class="text-xs text-zinc-500">
309
+ <template #tooltip>
310
+ <Markdown
311
+ source="写入表单状态的嵌套键路径,使用 `.` 分隔,例如 `attachments.5du4fbuv`"
312
+ block
313
+ class="prose prose-sm prose-zinc"
314
+ />
315
+ </template>
316
+ {{ fieldTitle("binding") }}
317
+ </FieldLabel>
318
+ <InputGroup>
319
+ <InputGroupInput
320
+ v-model="pathText"
321
+ placeholder="例:attachments.5du4fbuv"
322
+ class="font-mono"
323
+ />
324
+ </InputGroup>
325
+ </Field>
326
+ </div>
327
+
328
+ <div class="grid grid-cols-3 gap-3">
329
+ <Field orientation="vertical">
330
+ <FieldLabel class="text-xs text-zinc-500">
331
+ <template
332
+ v-if="fieldDescription('label')"
333
+ #tooltip
334
+ >
335
+ <Markdown
336
+ :source="fieldDescription('label')"
337
+ block
338
+ class="prose prose-sm prose-zinc"
339
+ />
340
+ </template>
341
+ {{ fieldTitle("label") }}
342
+ </FieldLabel>
343
+ <LocaleField
344
+ translate-hint="form field label"
345
+ :model-value="value.label"
346
+ @update:model-value="(v) => value = { ...value, label: v }"
347
+ />
348
+ </Field>
349
+
350
+ <Field orientation="vertical">
351
+ <FieldLabel class="text-xs text-zinc-500">
352
+ <template
353
+ v-if="fieldDescription('tooltip')"
354
+ #tooltip
355
+ >
356
+ <Markdown
357
+ :source="fieldDescription('tooltip')"
358
+ block
359
+ class="prose prose-sm prose-zinc"
360
+ />
361
+ </template>
362
+ {{ fieldTitle("tooltip") }}
363
+ </FieldLabel>
364
+ <LocaleField
365
+ markdown
366
+ translate-hint="form field tooltip"
367
+ :model-value="value.tooltip"
368
+ @update:model-value="(v) => value = { ...value, tooltip: v }"
369
+ />
370
+ </Field>
371
+
372
+ <Field orientation="vertical">
373
+ <FieldLabel class="text-xs text-zinc-500">
374
+ <template
375
+ v-if="fieldDescription('description')"
376
+ #tooltip
377
+ >
378
+ <Markdown
379
+ :source="fieldDescription('description')"
380
+ block
381
+ class="prose prose-sm prose-zinc"
382
+ />
383
+ </template>
384
+ {{ fieldTitle("description") }}
385
+ </FieldLabel>
386
+ <LocaleField
387
+ markdown
388
+ translate-hint="upload zone description"
389
+ :model-value="value.description"
390
+ @update:model-value="(v) => value = { ...value, description: v }"
391
+ />
392
+ </Field>
393
+ </div>
394
+
395
+ <div class="grid grid-cols-3 gap-3">
396
+ <Field orientation="vertical">
397
+ <FieldLabel class="text-xs text-zinc-500">
398
+ <template
399
+ v-if="fieldDescription('orientation')"
400
+ #tooltip
401
+ >
402
+ <Markdown
403
+ :source="fieldDescription('orientation')"
404
+ block
405
+ class="prose prose-sm prose-zinc"
406
+ />
407
+ </template>
408
+ {{ fieldTitle("orientation") }}
409
+ </FieldLabel>
410
+ <Select
411
+ :model-value="value.orientation ?? DEFAULT_FIELD_ORIENTATION"
412
+ @update:model-value="(v) => value = { ...value, orientation: v }"
413
+ >
414
+ <SelectTrigger class="w-full">
415
+ <SelectValue />
416
+ </SelectTrigger>
417
+ <SelectContent>
418
+ <SelectItem
419
+ v-for="opt in FIELD_ORIENTATION_OPTIONS"
420
+ :key="opt.value"
421
+ :value="opt.value"
422
+ >
423
+ {{ opt.label }}
424
+ </SelectItem>
425
+ </SelectContent>
426
+ </Select>
427
+ </Field>
428
+
429
+ <Field orientation="vertical">
430
+ <FieldLabel class="text-xs text-zinc-500">
431
+ <template
432
+ v-if="fieldDescription('multiple')"
433
+ #tooltip
434
+ >
435
+ <Markdown
436
+ :source="fieldDescription('multiple')"
437
+ block
438
+ class="prose prose-sm prose-zinc"
439
+ />
440
+ </template>
441
+ {{ fieldTitle("multiple") }}
442
+ </FieldLabel>
443
+ <div>
444
+ <Switch
445
+ :model-value="value.multiple ?? false"
446
+ @update:model-value="setMultiple"
447
+ />
448
+ </div>
449
+ </Field>
450
+
451
+ <Field orientation="vertical">
452
+ <FieldLabel class="text-xs text-zinc-500">
453
+ <template
454
+ v-if="fieldDescription('maxFiles')"
455
+ #tooltip
456
+ >
457
+ <Markdown
458
+ :source="fieldDescription('maxFiles')"
459
+ block
460
+ class="prose prose-sm prose-zinc"
461
+ />
462
+ </template>
463
+ {{ fieldTitle("maxFiles") }}
464
+ </FieldLabel>
465
+ <InputGroup>
466
+ <InputGroupNumberField
467
+ :model-value="value.maxFiles"
468
+ :min="1"
469
+ @update:model-value="setMaxFiles"
470
+ />
471
+ </InputGroup>
472
+ </Field>
473
+ </div>
474
+
475
+ <div class="grid grid-cols-3 gap-3">
476
+ <Field orientation="vertical">
477
+ <FieldLabel class="text-xs text-zinc-500">
478
+ <template
479
+ v-if="fieldDescription('accept')"
480
+ #tooltip
481
+ >
482
+ <Markdown
483
+ :source="fieldDescription('accept')"
484
+ block
485
+ class="prose prose-sm prose-zinc"
486
+ />
487
+ </template>
488
+ {{ fieldTitle("accept") }}
489
+ </FieldLabel>
490
+ <Select
491
+ multiple
492
+ :model-value="selectedGroupIds"
493
+ @update:model-value="onAcceptChange"
494
+ >
495
+ <SelectTrigger class="w-full">
496
+ <SelectValue placeholder="不限制">
497
+ <span v-if="selectedGroupLabels.length > 0">{{ selectedGroupLabels }}</span>
498
+ <span
499
+ v-else
500
+ class="text-zinc-400"
501
+ >不限制</span>
502
+ </SelectValue>
503
+ </SelectTrigger>
504
+ <SelectContent>
505
+ <SelectItem
506
+ v-for="group in MIME_GROUPS"
507
+ :key="group.id"
508
+ :value="group.id"
509
+ >
510
+ {{ group.label }}
511
+ </SelectItem>
512
+ </SelectContent>
513
+ </Select>
514
+ </Field>
515
+
516
+ <Field orientation="vertical">
517
+ <FieldLabel class="text-xs text-zinc-500">
518
+ <template
519
+ v-if="fieldDescription('maxFileSize')"
520
+ #tooltip
521
+ >
522
+ <Markdown
523
+ :source="fieldDescription('maxFileSize')"
524
+ block
525
+ class="prose prose-sm prose-zinc"
526
+ />
527
+ </template>
528
+ {{ fieldTitle("maxFileSize") }}
529
+ </FieldLabel>
530
+ <InputGroup>
531
+ <InputGroupNumberField
532
+ :model-value="perFile.amount.value"
533
+ :min="0"
534
+ @update:model-value="(v) => perFile.amount.value = v"
535
+ />
536
+ <InputGroupAddon align="inline-end">
537
+ <DropdownMenu>
538
+ <DropdownMenuTrigger as-child>
539
+ <InputGroupButton
540
+ variant="ghost"
541
+ class="pr-1.5! text-xs"
542
+ >
543
+ {{ perFile.unit.value }}
544
+ <Icon
545
+ icon="lucide:chevron-down"
546
+ class="size-3"
547
+ />
548
+ </InputGroupButton>
549
+ </DropdownMenuTrigger>
550
+ <DropdownMenuContent align="end">
551
+ <DropdownMenuItem @select="perFile.unit.value = 'KB'">
552
+ KB
553
+ </DropdownMenuItem>
554
+ <DropdownMenuItem @select="perFile.unit.value = 'MB'">
555
+ MB
556
+ </DropdownMenuItem>
557
+ </DropdownMenuContent>
558
+ </DropdownMenu>
559
+ </InputGroupAddon>
560
+ </InputGroup>
561
+ </Field>
562
+
563
+ <Field orientation="vertical">
564
+ <FieldLabel class="text-xs text-zinc-500">
565
+ <template
566
+ v-if="fieldDescription('maxTotalSize')"
567
+ #tooltip
568
+ >
569
+ <Markdown
570
+ :source="fieldDescription('maxTotalSize')"
571
+ block
572
+ class="prose prose-sm prose-zinc"
573
+ />
574
+ </template>
575
+ {{ fieldTitle("maxTotalSize") }}
576
+ </FieldLabel>
577
+ <InputGroup>
578
+ <InputGroupNumberField
579
+ :model-value="total.amount.value"
580
+ :min="0"
581
+ @update:model-value="(v) => total.amount.value = v"
582
+ />
583
+ <InputGroupAddon align="inline-end">
584
+ <DropdownMenu>
585
+ <DropdownMenuTrigger as-child>
586
+ <InputGroupButton
587
+ variant="ghost"
588
+ class="pr-1.5! text-xs"
589
+ >
590
+ {{ total.unit.value }}
591
+ <Icon
592
+ icon="lucide:chevron-down"
593
+ class="size-3"
594
+ />
595
+ </InputGroupButton>
596
+ </DropdownMenuTrigger>
597
+ <DropdownMenuContent align="end">
598
+ <DropdownMenuItem @select="total.unit.value = 'KB'">
599
+ KB
600
+ </DropdownMenuItem>
601
+ <DropdownMenuItem @select="total.unit.value = 'MB'">
602
+ MB
603
+ </DropdownMenuItem>
604
+ </DropdownMenuContent>
605
+ </DropdownMenu>
606
+ </InputGroupAddon>
607
+ </InputGroup>
608
+ </Field>
609
+ </div>
610
+
611
+ <!-- Immediate upload -->
612
+ <div class="flex items-center gap-2">
613
+ <h3 class="text-xs font-medium text-zinc-500">
614
+ {{ fieldTitle("upload") }}
615
+ </h3>
616
+ <Switch
617
+ size="sm"
618
+ :model-value="uploadEnabled"
619
+ aria-label="启用即时上传"
620
+ @update:model-value="setUploadEnabled"
621
+ />
622
+ </div>
623
+
624
+ <div
625
+ v-if="uploadEnabled"
626
+ class="flex flex-col gap-3"
627
+ >
628
+ <div class="grid grid-cols-2 gap-3">
629
+ <Field orientation="vertical">
630
+ <FieldLabel class="text-xs text-zinc-500">
631
+ <template #tooltip>
632
+ <Markdown
633
+ source="返回 `HttpRequest` 的 CEL 表达式,可用 `files` 引用本次选择的文件,例如 `http.post(url).body(form({&quot;files&quot;: files}))`"
634
+ block
635
+ class="prose prose-sm prose-zinc"
636
+ />
637
+ </template>
638
+ 上传请求
639
+ </FieldLabel>
640
+ <ExpressionEditor
641
+ :model-value="uploadRequestText"
642
+ placeholder="例:http.post('/api/upload').body(form({ 'files': files }))"
643
+ result-type="HttpRequest"
644
+ :extra-vars="{ files: FILES_VAR }"
645
+ multiline
646
+ class="min-h-16"
647
+ @update:model-value="(v) => uploadRequestText = v"
648
+ />
649
+ </Field>
650
+
651
+ <Field orientation="vertical">
652
+ <FieldLabel class="text-xs text-zinc-500">
653
+ <template #tooltip>
654
+ <Markdown
655
+ source="返回**列表**的 CEL 表达式,可用 `json` 引用上传响应;映射为追加到绑定值的文件项"
656
+ block
657
+ class="prose prose-sm prose-zinc"
658
+ />
659
+ </template>
660
+ 处理响应
661
+ </FieldLabel>
662
+ <ExpressionEditor
663
+ :model-value="uploadHandleText"
664
+ placeholder="例:json.data.map(d, { 'cacheKey': d.key, 'filename': d.fileOriginalName })"
665
+ result-type="list"
666
+ :extra-vars="{ json: UPLOAD_JSON_VAR }"
667
+ multiline
668
+ class="min-h-16"
669
+ @update:model-value="(v) => uploadHandleText = v"
670
+ />
671
+ </Field>
672
+ </div>
673
+
674
+ <Field orientation="vertical">
675
+ <FieldLabel class="text-xs text-zinc-500">
676
+ <template #tooltip>
677
+ <Markdown
678
+ source="返回 `string` 的 CEL 表达式,可用 `file` 引用单个文件项;用于展示文件名。留空时回退到 `filename` / `name` 字段"
679
+ block
680
+ class="prose prose-sm prose-zinc"
681
+ />
682
+ </template>
683
+ 文件名
684
+ </FieldLabel>
685
+ <ExpressionEditor
686
+ :model-value="uploadFilenameText"
687
+ placeholder="例:file.filename"
688
+ result-type="string"
689
+ :extra-vars="{ file: FILE_VAR }"
690
+ class="min-h-10"
691
+ @update:model-value="(v) => uploadFilenameText = v"
692
+ />
693
+ </Field>
694
+ </div>
695
+
696
+ <!-- Download templates -->
697
+ <Field orientation="vertical">
698
+ <FieldLabel class="text-xs text-zinc-500">
699
+ <template #tooltip>
700
+ <Markdown
701
+ :source="fieldDescription('templates') ?? '\u4E0B\u8F7D\u6A21\u677F\u5217\u8868\uFF1B\u6BCF\u4E00\u9879\u6E32\u67D3\u4E00\u4E2A\u4E0B\u8F7D\u6309\u94AE'"
702
+ block
703
+ class="prose prose-sm prose-zinc"
704
+ />
705
+ </template>
706
+ {{ fieldTitle("templates") }}
707
+ </FieldLabel>
708
+
709
+ <div class="flex flex-col gap-2">
710
+ <div
711
+ v-for="(template, index) in templates"
712
+ :key="rowKeys[index]"
713
+ :ref="templateDnd.rowRef(rowId(index), templateRowConfig(index))"
714
+ data-slot="upload-template"
715
+ class="relative flex items-start gap-2 rounded border border-zinc-200 bg-zinc-50/40 p-2"
716
+ :data-instruction="templateDnd.instructionFor(rowId(index)) ?? void 0"
717
+ >
718
+ <Icon
719
+ icon="fluent:re-order-dots-vertical-20-regular"
720
+ class="drag-handle mt-2.5 size-4 shrink-0 cursor-grab text-zinc-400"
721
+ data-slot="upload-template-drag-handle"
722
+ />
723
+
724
+ <div class="flex min-w-0 flex-1 flex-col gap-2">
725
+ <div class="grid grid-cols-2 gap-2">
726
+ <Field orientation="vertical">
727
+ <FieldLabel class="text-xs text-zinc-500">
728
+ <template #tooltip>
729
+ <Markdown
730
+ source="返回 `HttpRequest` 的 CEL 表达式。未配置「下载请求」时直接发起并下载其响应"
731
+ block
732
+ class="prose prose-sm prose-zinc"
733
+ />
734
+ </template>
735
+ 请求
736
+ </FieldLabel>
737
+ <ExpressionEditor
738
+ :model-value="template.request"
739
+ placeholder="例:http.get('https://api.example.com/template.xlsx')"
740
+ result-type="HttpRequest"
741
+ class="min-h-10"
742
+ @update:model-value="(v) => updateTemplateRequest(index, v)"
743
+ />
744
+ </Field>
745
+
746
+ <Field orientation="vertical">
747
+ <FieldLabel class="text-xs text-zinc-500">
748
+ <template #tooltip>
749
+ <Markdown
750
+ source="可选的第二步:用第一步响应 `json` 中的凭据构造真正的下载请求,例如 `json.data.key`"
751
+ block
752
+ class="prose prose-sm prose-zinc"
753
+ />
754
+ </template>
755
+ 下载请求
756
+ </FieldLabel>
757
+ <ExpressionEditor
758
+ :model-value="template.download ?? ''"
759
+ placeholder="例:http.get('https://api.example.com/download').query('key', json.data.key)"
760
+ result-type="HttpRequest"
761
+ :extra-vars="{ json: JSON_VAR }"
762
+ class="min-h-10"
763
+ @update:model-value="(v) => updateTemplateDownload(index, v)"
764
+ />
765
+ </Field>
766
+ </div>
767
+
768
+ <div class="grid grid-cols-2 gap-2">
769
+ <Field orientation="vertical">
770
+ <FieldLabel class="text-xs text-zinc-500">
771
+ 模板按钮图标
772
+ </FieldLabel>
773
+ <IconPicker
774
+ :model-value="template.icon ?? ''"
775
+ @update:model-value="(v) => updateTemplateIcon(index, v)"
776
+ />
777
+ </Field>
778
+
779
+ <Field orientation="vertical">
780
+ <FieldLabel class="text-xs text-zinc-500">
781
+ 模板按钮文本
782
+ </FieldLabel>
783
+ <LocaleField
784
+ translate-hint="download template button label"
785
+ :model-value="template.label"
786
+ @update:model-value="(v) => updateTemplateLabel(index, v)"
787
+ />
788
+ </Field>
789
+ </div>
790
+ </div>
791
+
792
+ <Button
793
+ variant="ghost"
794
+ size="sm"
795
+ class="absolute top-1 right-1 size-6 p-0 text-red-600 hover:text-red-700"
796
+ aria-label="移除模板"
797
+ @click="removeTemplate(index)"
798
+ >
799
+ <Icon icon="fluent:delete-20-regular" />
800
+ </Button>
801
+ </div>
802
+
803
+ <Button
804
+ type="button"
805
+ data-slot="upload-template-add"
806
+ class="w-full justify-center"
807
+ @click="addTemplate"
808
+ >
809
+ <Icon icon="fluent:add-20-regular" />
810
+ <span>增加</span>
811
+ </Button>
812
+ </div>
813
+ </Field>
814
+
815
+ <div class="grid grid-cols-3 gap-3">
816
+ <Field orientation="vertical">
817
+ <FieldLabel class="text-xs text-zinc-500">
818
+ <template #tooltip>
819
+ <Markdown
820
+ :source="fieldDescription('required') ?? '\u8FD4\u56DE `true` \u65F6\u663E\u793A\u5FC5\u586B\u661F\u53F7\u5E76\u8981\u6C42\u975E\u7A7A'"
821
+ block
822
+ class="prose prose-sm prose-zinc"
823
+ />
824
+ </template>
825
+ {{ fieldTitle("required") }}
826
+ </FieldLabel>
827
+ <ExpressionEditor
828
+ :model-value="value.required ?? ''"
829
+ placeholder="例:form.type == 'company'"
830
+ result-type="bool"
831
+ class="min-h-10"
832
+ @update:model-value="(v) => value = { ...value, required: v.length > 0 ? v : void 0 }"
833
+ />
834
+ </Field>
835
+
836
+ <Field orientation="vertical">
837
+ <FieldLabel class="text-xs text-zinc-500">
838
+ <template #tooltip>
839
+ <Markdown
840
+ :source="fieldDescription('hidden') ?? '\u8FD4\u56DE `true` \u65F6\u5B57\u6BB5\u5728\u6240\u6709\u5E03\u5C40\u4E2D\u90FD\u4E0D\u6E32\u67D3'"
841
+ block
842
+ class="prose prose-sm prose-zinc"
843
+ />
844
+ </template>
845
+ {{ fieldTitle("hidden") }}
846
+ </FieldLabel>
847
+ <ExpressionEditor
848
+ :model-value="value.hidden ?? ''"
849
+ placeholder="例:form.role == 'guest'"
850
+ result-type="bool"
851
+ class="min-h-10"
852
+ @update:model-value="(v) => value = { ...value, hidden: v.length > 0 ? v : void 0 }"
853
+ />
854
+ </Field>
855
+
856
+ <Field orientation="vertical">
857
+ <FieldLabel class="text-xs text-zinc-500">
858
+ <template #tooltip>
859
+ <Markdown
860
+ :source="fieldDescription('disabled') ?? '\u8FD4\u56DE `true` \u65F6\u4E0A\u4F20\u533A\u57DF\u4ECD\u7136\u6E32\u67D3\u4F46\u4E0D\u53EF\u64CD\u4F5C'"
861
+ block
862
+ class="prose prose-sm prose-zinc"
863
+ />
864
+ </template>
865
+ {{ fieldTitle("disabled") }}
866
+ </FieldLabel>
867
+ <ExpressionEditor
868
+ :model-value="value.disabled ?? ''"
869
+ placeholder="例:form.status == 'locked'"
870
+ result-type="bool"
871
+ class="min-h-10"
872
+ @update:model-value="(v) => value = { ...value, disabled: v.length > 0 ? v : void 0 }"
873
+ />
874
+ </Field>
875
+
876
+ <Field orientation="vertical">
877
+ <FieldLabel class="text-xs text-zinc-500">
878
+ <template #tooltip>
879
+ <Markdown
880
+ :source="fieldDescription('readonly') ?? '\u8FD4\u56DE `true` \u65F6\u4EC5\u4EE5\u6587\u4EF6\u5217\u8868\u5C55\u793A\u5F53\u524D\u503C'"
881
+ block
882
+ class="prose prose-sm prose-zinc"
883
+ />
884
+ </template>
885
+ {{ fieldTitle("readonly") }}
886
+ </FieldLabel>
887
+ <ExpressionEditor
888
+ :model-value="value.readonly ?? ''"
889
+ placeholder="例:form.id != null"
890
+ result-type="bool"
891
+ class="min-h-10"
892
+ @update:model-value="(v) => value = { ...value, readonly: v.length > 0 ? v : void 0 }"
893
+ />
894
+ </Field>
895
+ </div>
896
+
897
+ <Field orientation="vertical">
898
+ <FieldLabel class="text-xs text-zinc-500">
899
+ <template #tooltip>
900
+ <Markdown
901
+ :source="fieldDescription('validations')"
902
+ block
903
+ class="prose prose-sm prose-zinc"
904
+ />
905
+ </template>
906
+ {{ fieldTitle("validations") }}
907
+ </FieldLabel>
908
+ <ValidationRulesField
909
+ :model-value="value.validations"
910
+ @update:model-value="(v) => value = { ...value, validations: v.length > 0 ? v : void 0 }"
911
+ />
912
+ </Field>
913
+ </div>
914
+ </template>
915
+
916
+ <style scoped>
917
+ [data-instruction=reorder-above]:before,[data-instruction=reorder-below]:after{background:var(--primary,#2563eb);content:"";height:2px;left:0;pointer-events:none;position:absolute;right:0;z-index:1}[data-instruction=reorder-above]:before{top:-5px}[data-instruction=reorder-below]:after{bottom:-5px}
918
+ </style>