@shwfed/config 3.0.10 → 3.0.12

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 (42) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/config.d.vue.ts +86 -0
  3. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/config.vue +398 -0
  4. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/config.vue.d.ts +86 -0
  5. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/expr-field.d.vue.ts +14 -0
  6. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/expr-field.vue +39 -0
  7. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/expr-field.vue.d.ts +14 -0
  8. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/items-colgroup.d.vue.ts +3 -0
  9. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/items-colgroup.vue +20 -0
  10. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/items-colgroup.vue.d.ts +3 -0
  11. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/json-scope-provider.d.vue.ts +13 -0
  12. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/json-scope-provider.vue +19 -0
  13. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/json-scope-provider.vue.d.ts +13 -0
  14. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/rmb.d.ts +11 -0
  15. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/rmb.js +58 -0
  16. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/row-scope-provider.d.vue.ts +13 -0
  17. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/row-scope-provider.vue +22 -0
  18. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/row-scope-provider.vue.d.ts +13 -0
  19. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/runtime.d.vue.ts +86 -0
  20. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/runtime.vue +346 -0
  21. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/runtime.vue.d.ts +86 -0
  22. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/schema.d.ts +109 -0
  23. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/schema.js +189 -0
  24. package/dist/runtime/components/form/fields/2026-04-22/com.shwfed.form.field.textarea/config.d.vue.ts +2 -2
  25. package/dist/runtime/components/form/fields/2026-04-22/com.shwfed.form.field.textarea/config.vue.d.ts +2 -2
  26. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/config.d.vue.ts +34 -0
  27. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/config.vue +109 -1
  28. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/config.vue.d.ts +34 -0
  29. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/prompt.md +7 -0
  30. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/runtime.vue +66 -22
  31. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/schema.d.ts +23 -0
  32. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/schema.js +26 -0
  33. package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/config.d.vue.ts +10 -0
  34. package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/config.vue +1004 -0
  35. package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/config.vue.d.ts +10 -0
  36. package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/prompt.md +19 -0
  37. package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/runtime.d.vue.ts +9 -0
  38. package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/runtime.vue +503 -0
  39. package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/runtime.vue.d.ts +9 -0
  40. package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/schema.d.ts +138 -0
  41. package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/schema.js +181 -0
  42. package/package.json +1 -1
@@ -0,0 +1,1004 @@
1
+ <script setup>
2
+ import { computed, defineComponent, ref, watch } from "vue";
3
+ import { Icon } from "@iconify/vue";
4
+ import {
5
+ useTreeDnd
6
+ } from "../../../../../composables/useTreeDnd";
7
+ import { provideCELContext } from "../../../../../utils/cel-context";
8
+ import TriggersField from "../../../../actions/components/triggers-field.vue";
9
+ import { ExpressionEditor } from "../../../../ui/expression-editor";
10
+ import { Separator } from "../../../../ui/separator";
11
+ import { Switch } from "../../../../ui/switch";
12
+ import { Button } from "../../../../ui/button";
13
+ import { Field, FieldLabel } from "../../../../ui/field";
14
+ import { IconPicker } from "../../../../ui/icon-picker";
15
+ import { Locale } from "../../../../ui/locale";
16
+ import { Markdown } from "../../../../ui/markdown";
17
+ import {
18
+ DropdownMenu,
19
+ DropdownMenuContent,
20
+ DropdownMenuItem,
21
+ DropdownMenuTrigger
22
+ } from "../../../../ui/dropdown-menu";
23
+ import {
24
+ InputGroup,
25
+ InputGroupAddon,
26
+ InputGroupButton,
27
+ InputGroupInput,
28
+ InputGroupNumberField
29
+ } from "../../../../ui/input-group";
30
+ import {
31
+ Select,
32
+ SelectContent,
33
+ SelectItem,
34
+ SelectTrigger,
35
+ SelectValue
36
+ } from "../../../../ui/select";
37
+ import { getStructFieldDescription, getStructFieldTitle } from "../../../utils/schema-meta";
38
+ import { FILE_VAR, FILES_VAR, JSON_VAR, UPLOAD_JSON_VAR, schema } from "./schema";
39
+ defineOptions({ name: "ShwfedTableUploadRendererConfig" });
40
+ const value = defineModel({ type: Object, ...{ required: true } });
41
+ const fieldSchema = schema(() => {
42
+ });
43
+ const fieldTitle = (field) => getStructFieldTitle(fieldSchema, field) ?? field;
44
+ const fieldDescription = (field) => getStructFieldDescription(fieldSchema, field);
45
+ const ROW_VARS = {
46
+ row: { type: "dyn", label: "\u5F53\u524D\u884C\u6570\u636E" },
47
+ index: { type: "number", label: "\u884C\u7D22\u5F15" }
48
+ };
49
+ const UPLOAD_REQUEST_VARS = { ...ROW_VARS, files: FILES_VAR };
50
+ const UPLOAD_HANDLE_VARS = { ...ROW_VARS, json: UPLOAD_JSON_VAR };
51
+ const UPLOAD_FILENAME_VARS = { ...ROW_VARS, file: FILE_VAR };
52
+ const TEMPLATE_DOWNLOAD_VARS = { ...ROW_VARS, json: JSON_VAR };
53
+ const bindingText = computed({
54
+ get: () => value.value.binding ?? "",
55
+ set: (next) => {
56
+ const trimmed = next.trim();
57
+ if (trimmed.length === 0) {
58
+ const { binding: _drop, ...rest } = value.value;
59
+ value.value = rest;
60
+ } else {
61
+ value.value = { ...value.value, binding: trimmed };
62
+ }
63
+ }
64
+ });
65
+ const disabledModel = computed({
66
+ get: () => value.value.disabled ?? "",
67
+ set: (v) => {
68
+ if (v === "") {
69
+ const { disabled: _drop, ...rest } = value.value;
70
+ value.value = rest;
71
+ } else {
72
+ value.value = { ...value.value, disabled: v };
73
+ }
74
+ }
75
+ });
76
+ const readonlyModel = computed({
77
+ get: () => value.value.readonly ?? "",
78
+ set: (v) => {
79
+ if (v === "") {
80
+ const { readonly: _drop, ...rest } = value.value;
81
+ value.value = rest;
82
+ } else {
83
+ value.value = { ...value.value, readonly: v };
84
+ }
85
+ }
86
+ });
87
+ function onHiddenChange(v) {
88
+ if (v) {
89
+ value.value = { ...value.value, hidden: true };
90
+ } else {
91
+ const { hidden: _drop, ...rest } = value.value;
92
+ value.value = rest;
93
+ }
94
+ }
95
+ const uploadEnabled = computed(() => value.value.upload != null);
96
+ function setUploadEnabled(on) {
97
+ if (on) {
98
+ if (value.value.upload) return;
99
+ value.value = { ...value.value, upload: { request: "", handle: "" } };
100
+ } else {
101
+ const { upload: _omit, ...rest } = value.value;
102
+ value.value = rest;
103
+ }
104
+ }
105
+ const uploadRequestText = computed({
106
+ get: () => value.value.upload?.request ?? "",
107
+ set: (next) => {
108
+ const current = value.value.upload;
109
+ if (!current) return;
110
+ value.value = { ...value.value, upload: { ...current, request: next } };
111
+ }
112
+ });
113
+ const uploadHandleText = computed({
114
+ get: () => value.value.upload?.handle ?? "",
115
+ set: (next) => {
116
+ const current = value.value.upload;
117
+ if (!current) return;
118
+ value.value = { ...value.value, upload: { ...current, handle: next } };
119
+ }
120
+ });
121
+ const uploadFilenameText = computed({
122
+ get: () => value.value.upload?.filename ?? "",
123
+ set: (next) => {
124
+ const current = value.value.upload;
125
+ if (!current) return;
126
+ if (next.length === 0) {
127
+ const { filename: _omit, ...rest } = current;
128
+ value.value = { ...value.value, upload: rest };
129
+ } else {
130
+ value.value = { ...value.value, upload: { ...current, filename: next } };
131
+ }
132
+ }
133
+ });
134
+ const templates = computed(() => value.value.templates ?? []);
135
+ function setTemplates(next) {
136
+ if (next.length === 0) {
137
+ const { templates: _omit, ...rest } = value.value;
138
+ value.value = rest;
139
+ } else {
140
+ value.value = { ...value.value, templates: next };
141
+ }
142
+ }
143
+ function makeId() {
144
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
145
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
146
+ const r = Math.random() * 16 | 0;
147
+ return (c === "x" ? r : r & 3 | 8).toString(16);
148
+ });
149
+ }
150
+ const rowKeys = ref([]);
151
+ watch(
152
+ () => templates.value.length,
153
+ (len) => {
154
+ if (len === rowKeys.value.length) return;
155
+ rowKeys.value = Array.from({ length: len }, () => makeId());
156
+ },
157
+ { immediate: true }
158
+ );
159
+ function addTemplate() {
160
+ setTemplates([...templates.value, { request: "" }]);
161
+ rowKeys.value = [...rowKeys.value, makeId()];
162
+ }
163
+ function removeTemplate(index) {
164
+ setTemplates(templates.value.filter((_, i) => i !== index));
165
+ rowKeys.value = rowKeys.value.filter((_, i) => i !== index);
166
+ }
167
+ function move(from, to) {
168
+ if (from === to) return;
169
+ if (from < 0 || to < 0 || from >= templates.value.length || to >= templates.value.length) return;
170
+ const next = [...templates.value];
171
+ const [entry] = next.splice(from, 1);
172
+ if (!entry) return;
173
+ next.splice(to, 0, entry);
174
+ setTemplates(next);
175
+ const keys = [...rowKeys.value];
176
+ const [key] = keys.splice(from, 1);
177
+ if (!key) return;
178
+ keys.splice(to, 0, key);
179
+ rowKeys.value = keys;
180
+ }
181
+ function patchTemplate(index, patch) {
182
+ setTemplates(templates.value.map((entry, i) => i === index ? patch(entry) : entry));
183
+ }
184
+ const TEMPLATE_ROW_KIND = "shwfed-table-upload-template";
185
+ const rowId = (i) => rowKeys.value[i] ?? `idx-${i}`;
186
+ function onTemplateRowDrop(e) {
187
+ if (e.source.kind !== TEMPLATE_ROW_KIND || e.target.kind !== TEMPLATE_ROW_KIND) return;
188
+ const from = rowKeys.value.findIndex((k) => k === e.source.id);
189
+ const to = rowKeys.value.findIndex((k) => k === e.target.id);
190
+ if (from < 0 || to < 0) return;
191
+ if (e.instruction === "reorder-above") move(from, from < to ? to - 1 : to);
192
+ else if (e.instruction === "reorder-below") move(from, from <= to ? to : to + 1);
193
+ }
194
+ const templateDnd = useTreeDnd({ onRowDrop: onTemplateRowDrop });
195
+ const pickDragHandle = (el) => el.querySelector(".drag-handle");
196
+ function templateRowConfig(i) {
197
+ return {
198
+ kind: TEMPLATE_ROW_KIND,
199
+ canDrop: (src) => src.kind === TEMPLATE_ROW_KIND,
200
+ blockInstructions: (src) => {
201
+ const block = ["make-child", "reparent"];
202
+ if (src.id === rowId(i)) block.push("reorder-above", "reorder-below");
203
+ return block;
204
+ },
205
+ dragHandle: pickDragHandle
206
+ };
207
+ }
208
+ function updateTemplateRequest(index, request) {
209
+ patchTemplate(index, (entry) => ({ ...entry, request }));
210
+ }
211
+ function updateTemplateDownload(index, download) {
212
+ patchTemplate(index, (entry) => {
213
+ if (download.length === 0) {
214
+ const { download: _omit, ...rest } = entry;
215
+ return rest;
216
+ }
217
+ return { ...entry, download };
218
+ });
219
+ }
220
+ function updateTemplateIcon(index, icon) {
221
+ patchTemplate(index, (entry) => {
222
+ const trimmed = icon.trim();
223
+ if (trimmed.length === 0) {
224
+ const { icon: _omit, ...rest } = entry;
225
+ return rest;
226
+ }
227
+ return { ...entry, icon: trimmed };
228
+ });
229
+ }
230
+ function updateTemplateLabel(index, label) {
231
+ patchTemplate(index, (entry) => {
232
+ if (label == null || label.length === 0) {
233
+ const { label: _omit, ...rest } = entry;
234
+ return rest;
235
+ }
236
+ return { ...entry, label };
237
+ });
238
+ }
239
+ const MIME_GROUPS = [
240
+ { id: "pdf", label: "PDF", mimes: ["application/pdf"] },
241
+ { id: "docx", label: "DOCX", mimes: ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"] },
242
+ { id: "doc", label: "DOC", mimes: ["application/msword"] },
243
+ { id: "xlsx", label: "XLSX", mimes: ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"] },
244
+ { id: "xls", label: "XLS", mimes: ["application/vnd.ms-excel"] },
245
+ { id: "pptx", label: "PPTX", mimes: ["application/vnd.openxmlformats-officedocument.presentationml.presentation"] },
246
+ { id: "ppt", label: "PPT", mimes: ["application/vnd.ms-powerpoint"] },
247
+ { id: "zip", label: "ZIP", mimes: ["application/zip", "application/x-zip-compressed"] },
248
+ { id: "rar", label: "RAR", mimes: ["application/vnd.rar", "application/x-rar-compressed"] },
249
+ { id: "ofd", label: "OFD", mimes: ["application/ofd"] },
250
+ { id: "xml", label: "XML", mimes: ["application/xml"] },
251
+ { id: "png", label: "PNG", mimes: ["image/png"] },
252
+ { id: "jpeg", label: "JPEG", mimes: ["image/jpeg", "image/jpg"] }
253
+ ];
254
+ const GROUP_BY_ID = new Map(MIME_GROUPS.map((g) => [g.id, g]));
255
+ const acceptList = computed(() => value.value.accept ?? []);
256
+ const selectedGroupIds = computed(
257
+ () => MIME_GROUPS.filter((g) => g.mimes.every((m) => acceptList.value.includes(m))).map((g) => g.id)
258
+ );
259
+ const selectedGroupLabels = computed(
260
+ () => selectedGroupIds.value.map((id) => GROUP_BY_ID.get(id)?.label).filter(Boolean).join("\u3001")
261
+ );
262
+ function onAcceptChange(next) {
263
+ const ids = Array.isArray(next) ? next.filter((v) => typeof v === "string") : typeof next === "string" ? [next] : [];
264
+ const mimes = ids.flatMap((id) => GROUP_BY_ID.get(id)?.mimes ?? []);
265
+ if (mimes.length === 0) {
266
+ const { accept: _omit, ...rest } = value.value;
267
+ value.value = rest;
268
+ } else {
269
+ value.value = { ...value.value, accept: mimes };
270
+ }
271
+ }
272
+ function pickUnitFor(bytes) {
273
+ if (bytes === void 0) return "MB";
274
+ return bytes < 1024 * 1024 ? "KB" : "MB";
275
+ }
276
+ function multiplierFor(unit) {
277
+ return unit === "KB" ? 1024 : 1024 * 1024;
278
+ }
279
+ function makeSizeBinding(prop) {
280
+ const unit = ref(pickUnitFor(value.value[prop]));
281
+ const amount = computed({
282
+ get: () => {
283
+ const v = value.value[prop];
284
+ return v === void 0 ? void 0 : v / multiplierFor(unit.value);
285
+ },
286
+ set: (next) => {
287
+ if (next === void 0 || !(next > 0)) {
288
+ const { [prop]: _omit, ...rest } = value.value;
289
+ value.value = rest;
290
+ return;
291
+ }
292
+ value.value = { ...value.value, [prop]: Math.round(next * multiplierFor(unit.value)) };
293
+ }
294
+ });
295
+ return { unit, amount };
296
+ }
297
+ const perFile = makeSizeBinding("maxFileSize");
298
+ const total = makeSizeBinding("maxTotalSize");
299
+ function setMaxFiles(v) {
300
+ if (v === void 0 || !(v > 0)) {
301
+ const { maxFiles: _omit, ...rest } = value.value;
302
+ value.value = rest;
303
+ } else {
304
+ value.value = { ...value.value, maxFiles: Math.round(v) };
305
+ }
306
+ }
307
+ function setMultiple(v) {
308
+ if (!v) {
309
+ const { multiple: _omit, ...rest } = value.value;
310
+ value.value = rest;
311
+ } else {
312
+ value.value = { ...value.value, multiple: true };
313
+ }
314
+ }
315
+ const downloadEnabled = computed(() => value.value.download != null);
316
+ function setDownloadEnabled(on) {
317
+ if (on) {
318
+ if (value.value.download) return;
319
+ value.value = { ...value.value, download: { operations: [] } };
320
+ } else {
321
+ const { download: _omit, ...rest } = value.value;
322
+ value.value = rest;
323
+ }
324
+ }
325
+ const downloadOperations = computed(() => value.value.download?.operations ?? []);
326
+ function updateDownloadOperations(next) {
327
+ const current = value.value.download;
328
+ if (!current) return;
329
+ value.value = { ...value.value, download: { ...current, operations: next } };
330
+ }
331
+ function updateDownloadIcon(icon) {
332
+ const current = value.value.download;
333
+ if (!current) return;
334
+ const trimmed = icon.trim();
335
+ if (trimmed.length === 0) {
336
+ const { icon: _omit, ...rest } = current;
337
+ value.value = { ...value.value, download: rest };
338
+ } else {
339
+ value.value = { ...value.value, download: { ...current, icon: trimmed } };
340
+ }
341
+ }
342
+ function updateDownloadLabel(label) {
343
+ const current = value.value.download;
344
+ if (!current) return;
345
+ if (label == null || label.length === 0) {
346
+ const { label: _omit, ...rest } = current;
347
+ value.value = { ...value.value, download: rest };
348
+ } else {
349
+ value.value = { ...value.value, download: { ...current, label } };
350
+ }
351
+ }
352
+ const DownloadScope = defineComponent({
353
+ name: "ShwfedTableUploadDownloadScope",
354
+ setup(_, { slots }) {
355
+ provideCELContext({
356
+ file: { type: FILE_VAR.type, label: FILE_VAR.label, description: FILE_VAR.description, value: () => void 0 }
357
+ });
358
+ return () => slots.default?.();
359
+ }
360
+ });
361
+ </script>
362
+
363
+ <template>
364
+ <div class="space-y-5">
365
+ <div class="grid grid-cols-2 gap-x-6 gap-y-4">
366
+ <Field orientation="vertical">
367
+ <FieldLabel class="text-xs text-zinc-500">
368
+ <template
369
+ v-if="fieldDescription('title')"
370
+ #tooltip
371
+ >
372
+ <Markdown
373
+ :source="fieldDescription('title')"
374
+ block
375
+ class="prose prose-sm prose-zinc"
376
+ />
377
+ </template>
378
+ {{ fieldTitle("title") }}
379
+ </FieldLabel>
380
+ <Locale
381
+ :model-value="value.title"
382
+ @update:model-value="(v) => value = { ...value, title: v }"
383
+ />
384
+ </Field>
385
+ <Field orientation="vertical">
386
+ <FieldLabel class="text-xs text-zinc-500">
387
+ <template
388
+ v-if="fieldDescription('binding')"
389
+ #tooltip
390
+ >
391
+ <Markdown
392
+ :source="fieldDescription('binding')"
393
+ block
394
+ class="prose prose-sm prose-zinc"
395
+ />
396
+ </template>
397
+ {{ fieldTitle("binding") }}
398
+ </FieldLabel>
399
+ <InputGroup>
400
+ <InputGroupInput
401
+ v-model="bindingText"
402
+ placeholder="例:attachments"
403
+ class="font-mono"
404
+ />
405
+ </InputGroup>
406
+ </Field>
407
+ <Field orientation="vertical">
408
+ <FieldLabel class="text-xs text-zinc-500">
409
+ <template
410
+ v-if="fieldDescription('tooltip')"
411
+ #tooltip
412
+ >
413
+ <Markdown
414
+ :source="fieldDescription('tooltip')"
415
+ block
416
+ class="prose prose-sm prose-zinc"
417
+ />
418
+ </template>
419
+ {{ fieldTitle("tooltip") }}
420
+ </FieldLabel>
421
+ <Locale
422
+ :model-value="value.tooltip"
423
+ markdown
424
+ @update:model-value="(v) => value = { ...value, tooltip: v }"
425
+ />
426
+ </Field>
427
+ <Field orientation="vertical">
428
+ <FieldLabel class="text-xs text-zinc-500">
429
+ <template
430
+ v-if="fieldDescription('placeholder')"
431
+ #tooltip
432
+ >
433
+ <Markdown
434
+ :source="fieldDescription('placeholder')"
435
+ block
436
+ class="prose prose-sm prose-zinc"
437
+ />
438
+ </template>
439
+ {{ fieldTitle("placeholder") }}
440
+ </FieldLabel>
441
+ <Locale
442
+ :model-value="value.placeholder"
443
+ @update:model-value="(v) => value = { ...value, placeholder: v }"
444
+ />
445
+ </Field>
446
+ </div>
447
+
448
+ <div class="grid grid-cols-3 gap-x-6 gap-y-4">
449
+ <Field orientation="vertical">
450
+ <FieldLabel class="text-xs text-zinc-500">
451
+ <template
452
+ v-if="fieldDescription('multiple')"
453
+ #tooltip
454
+ >
455
+ <Markdown
456
+ :source="fieldDescription('multiple')"
457
+ block
458
+ class="prose prose-sm prose-zinc"
459
+ />
460
+ </template>
461
+ {{ fieldTitle("multiple") }}
462
+ </FieldLabel>
463
+ <div>
464
+ <Switch
465
+ :model-value="value.multiple ?? false"
466
+ @update:model-value="setMultiple"
467
+ />
468
+ </div>
469
+ </Field>
470
+ <Field orientation="vertical">
471
+ <FieldLabel class="text-xs text-zinc-500">
472
+ <template
473
+ v-if="fieldDescription('maxFiles')"
474
+ #tooltip
475
+ >
476
+ <Markdown
477
+ :source="fieldDescription('maxFiles')"
478
+ block
479
+ class="prose prose-sm prose-zinc"
480
+ />
481
+ </template>
482
+ {{ fieldTitle("maxFiles") }}
483
+ </FieldLabel>
484
+ <InputGroup>
485
+ <InputGroupNumberField
486
+ :model-value="value.maxFiles"
487
+ :min="1"
488
+ @update:model-value="setMaxFiles"
489
+ />
490
+ </InputGroup>
491
+ </Field>
492
+ <Field orientation="vertical">
493
+ <FieldLabel class="text-xs text-zinc-500">
494
+ <template
495
+ v-if="fieldDescription('accept')"
496
+ #tooltip
497
+ >
498
+ <Markdown
499
+ :source="fieldDescription('accept')"
500
+ block
501
+ class="prose prose-sm prose-zinc"
502
+ />
503
+ </template>
504
+ {{ fieldTitle("accept") }}
505
+ </FieldLabel>
506
+ <Select
507
+ multiple
508
+ :model-value="selectedGroupIds"
509
+ @update:model-value="onAcceptChange"
510
+ >
511
+ <SelectTrigger class="w-full">
512
+ <SelectValue placeholder="不限制">
513
+ <span v-if="selectedGroupLabels.length > 0">{{ selectedGroupLabels }}</span>
514
+ <span
515
+ v-else
516
+ class="text-zinc-400"
517
+ >不限制</span>
518
+ </SelectValue>
519
+ </SelectTrigger>
520
+ <SelectContent>
521
+ <SelectItem
522
+ v-for="group in MIME_GROUPS"
523
+ :key="group.id"
524
+ :value="group.id"
525
+ >
526
+ {{ group.label }}
527
+ </SelectItem>
528
+ </SelectContent>
529
+ </Select>
530
+ </Field>
531
+ <Field orientation="vertical">
532
+ <FieldLabel class="text-xs text-zinc-500">
533
+ <template
534
+ v-if="fieldDescription('maxFileSize')"
535
+ #tooltip
536
+ >
537
+ <Markdown
538
+ :source="fieldDescription('maxFileSize')"
539
+ block
540
+ class="prose prose-sm prose-zinc"
541
+ />
542
+ </template>
543
+ {{ fieldTitle("maxFileSize") }}
544
+ </FieldLabel>
545
+ <InputGroup>
546
+ <InputGroupNumberField
547
+ :model-value="perFile.amount.value"
548
+ :min="0"
549
+ @update:model-value="(v) => perFile.amount.value = v"
550
+ />
551
+ <InputGroupAddon align="inline-end">
552
+ <DropdownMenu>
553
+ <DropdownMenuTrigger as-child>
554
+ <InputGroupButton
555
+ variant="ghost"
556
+ class="pr-1.5! text-xs"
557
+ >
558
+ {{ perFile.unit.value }}
559
+ <Icon
560
+ icon="lucide:chevron-down"
561
+ class="size-3"
562
+ />
563
+ </InputGroupButton>
564
+ </DropdownMenuTrigger>
565
+ <DropdownMenuContent align="end">
566
+ <DropdownMenuItem @select="perFile.unit.value = 'KB'">
567
+ KB
568
+ </DropdownMenuItem>
569
+ <DropdownMenuItem @select="perFile.unit.value = 'MB'">
570
+ MB
571
+ </DropdownMenuItem>
572
+ </DropdownMenuContent>
573
+ </DropdownMenu>
574
+ </InputGroupAddon>
575
+ </InputGroup>
576
+ </Field>
577
+ <Field orientation="vertical">
578
+ <FieldLabel class="text-xs text-zinc-500">
579
+ <template
580
+ v-if="fieldDescription('maxTotalSize')"
581
+ #tooltip
582
+ >
583
+ <Markdown
584
+ :source="fieldDescription('maxTotalSize')"
585
+ block
586
+ class="prose prose-sm prose-zinc"
587
+ />
588
+ </template>
589
+ {{ fieldTitle("maxTotalSize") }}
590
+ </FieldLabel>
591
+ <InputGroup>
592
+ <InputGroupNumberField
593
+ :model-value="total.amount.value"
594
+ :min="0"
595
+ @update:model-value="(v) => total.amount.value = v"
596
+ />
597
+ <InputGroupAddon align="inline-end">
598
+ <DropdownMenu>
599
+ <DropdownMenuTrigger as-child>
600
+ <InputGroupButton
601
+ variant="ghost"
602
+ class="pr-1.5! text-xs"
603
+ >
604
+ {{ total.unit.value }}
605
+ <Icon
606
+ icon="lucide:chevron-down"
607
+ class="size-3"
608
+ />
609
+ </InputGroupButton>
610
+ </DropdownMenuTrigger>
611
+ <DropdownMenuContent align="end">
612
+ <DropdownMenuItem @select="total.unit.value = 'KB'">
613
+ KB
614
+ </DropdownMenuItem>
615
+ <DropdownMenuItem @select="total.unit.value = 'MB'">
616
+ MB
617
+ </DropdownMenuItem>
618
+ </DropdownMenuContent>
619
+ </DropdownMenu>
620
+ </InputGroupAddon>
621
+ </InputGroup>
622
+ </Field>
623
+ </div>
624
+
625
+ <Separator />
626
+
627
+ <!-- Immediate upload -->
628
+ <div class="flex items-center gap-2">
629
+ <h3 class="text-xs font-medium text-zinc-500">
630
+ {{ fieldTitle("upload") }}
631
+ </h3>
632
+ <Switch
633
+ size="sm"
634
+ :model-value="uploadEnabled"
635
+ aria-label="启用即时上传"
636
+ @update:model-value="setUploadEnabled"
637
+ />
638
+ </div>
639
+
640
+ <div
641
+ v-if="uploadEnabled"
642
+ class="flex flex-col gap-4"
643
+ >
644
+ <div class="grid grid-cols-2 gap-x-6 gap-y-4">
645
+ <Field orientation="vertical">
646
+ <FieldLabel class="text-xs text-zinc-500">
647
+ <template #tooltip>
648
+ <Markdown
649
+ source="返回 `HttpRequest` 的 CEL 表达式,可用 `files` 引用本次选择的文件,例如 `http.post(url).body(form({&quot;files&quot;: files}))`"
650
+ block
651
+ class="prose prose-sm prose-zinc"
652
+ />
653
+ </template>
654
+ 上传请求
655
+ </FieldLabel>
656
+ <ExpressionEditor
657
+ :model-value="uploadRequestText"
658
+ placeholder="例:http.post('/api/upload').body(form({ 'files': files }))"
659
+ result-type="HttpRequest"
660
+ :extra-vars="UPLOAD_REQUEST_VARS"
661
+ multiline
662
+ class="min-h-16"
663
+ @update:model-value="(v) => uploadRequestText = v"
664
+ />
665
+ </Field>
666
+
667
+ <Field orientation="vertical">
668
+ <FieldLabel class="text-xs text-zinc-500">
669
+ <template #tooltip>
670
+ <Markdown
671
+ source="返回**列表**的 CEL 表达式,可用 `json` 引用上传响应;映射为写入绑定值的文件项"
672
+ block
673
+ class="prose prose-sm prose-zinc"
674
+ />
675
+ </template>
676
+ 处理响应
677
+ </FieldLabel>
678
+ <ExpressionEditor
679
+ :model-value="uploadHandleText"
680
+ placeholder="例:json.data.map(d, { 'cacheKey': d.key, 'filename': d.fileOriginalName })"
681
+ result-type="list"
682
+ :extra-vars="UPLOAD_HANDLE_VARS"
683
+ multiline
684
+ class="min-h-16"
685
+ @update:model-value="(v) => uploadHandleText = v"
686
+ />
687
+ </Field>
688
+ </div>
689
+
690
+ <Field orientation="vertical">
691
+ <FieldLabel class="text-xs text-zinc-500">
692
+ <template #tooltip>
693
+ <Markdown
694
+ source="返回 `string` 的 CEL 表达式,可用 `file` 引用单个文件项;用于展示文件名。留空时回退到 `filename` / `name` 字段"
695
+ block
696
+ class="prose prose-sm prose-zinc"
697
+ />
698
+ </template>
699
+ 文件名
700
+ </FieldLabel>
701
+ <ExpressionEditor
702
+ :model-value="uploadFilenameText"
703
+ placeholder="例:file.filename"
704
+ result-type="string"
705
+ :extra-vars="UPLOAD_FILENAME_VARS"
706
+ class="min-h-10"
707
+ @update:model-value="(v) => uploadFilenameText = v"
708
+ />
709
+ </Field>
710
+ </div>
711
+
712
+ <Separator />
713
+
714
+ <!-- Per-file download -->
715
+ <div class="flex items-center gap-2">
716
+ <h3 class="text-xs font-medium text-zinc-500">
717
+ {{ fieldTitle("download") }}
718
+ </h3>
719
+ <Switch
720
+ size="sm"
721
+ :model-value="downloadEnabled"
722
+ aria-label="启用文件下载"
723
+ @update:model-value="setDownloadEnabled"
724
+ />
725
+ </div>
726
+
727
+ <div
728
+ v-if="downloadEnabled"
729
+ class="flex flex-col gap-4"
730
+ >
731
+ <div class="grid grid-cols-2 gap-x-6 gap-y-4">
732
+ <Field orientation="vertical">
733
+ <FieldLabel class="text-xs text-zinc-500">
734
+ 下载按钮图标
735
+ </FieldLabel>
736
+ <IconPicker
737
+ :model-value="value.download?.icon ?? ''"
738
+ @update:model-value="updateDownloadIcon"
739
+ />
740
+ </Field>
741
+
742
+ <Field orientation="vertical">
743
+ <FieldLabel class="text-xs text-zinc-500">
744
+ 下载按钮文本
745
+ </FieldLabel>
746
+ <Locale
747
+ :model-value="value.download?.label"
748
+ @update:model-value="updateDownloadLabel"
749
+ />
750
+ </Field>
751
+ </div>
752
+
753
+ <Field orientation="vertical">
754
+ <FieldLabel class="text-xs text-zinc-500">
755
+ <template #tooltip>
756
+ <Markdown
757
+ source="点击文件项的下载按钮时按顺序执行的操作;可用 `file` 引用该文件项。尚未上传的本地文件会直接从内存下载,不经过此列表"
758
+ block
759
+ class="prose prose-sm prose-zinc"
760
+ />
761
+ </template>
762
+ 触发操作
763
+ </FieldLabel>
764
+ <DownloadScope>
765
+ <TriggersField
766
+ :triggers="downloadOperations"
767
+ @update:triggers="updateDownloadOperations"
768
+ />
769
+ </DownloadScope>
770
+ </Field>
771
+ </div>
772
+
773
+ <Separator />
774
+
775
+ <!-- Download templates -->
776
+ <Field orientation="vertical">
777
+ <FieldLabel class="text-xs text-zinc-500">
778
+ <template #tooltip>
779
+ <Markdown
780
+ :source="fieldDescription('templates') ?? '\u4E0B\u8F7D\u6A21\u677F\u5217\u8868\uFF1B\u6BCF\u4E00\u9879\u6E32\u67D3\u4E00\u4E2A\u4E0B\u8F7D\u6309\u94AE'"
781
+ block
782
+ class="prose prose-sm prose-zinc"
783
+ />
784
+ </template>
785
+ {{ fieldTitle("templates") }}
786
+ </FieldLabel>
787
+
788
+ <div class="flex flex-col gap-2">
789
+ <div
790
+ v-for="(template, index) in templates"
791
+ :key="rowKeys[index]"
792
+ :ref="templateDnd.rowRef(rowId(index), templateRowConfig(index))"
793
+ data-slot="upload-template"
794
+ class="relative flex items-start gap-2 rounded border border-zinc-200 bg-zinc-50/40 p-2"
795
+ :data-instruction="templateDnd.instructionFor(rowId(index)) ?? void 0"
796
+ >
797
+ <Icon
798
+ icon="fluent:re-order-dots-vertical-20-regular"
799
+ class="drag-handle mt-2.5 size-4 shrink-0 cursor-grab text-zinc-400"
800
+ data-slot="upload-template-drag-handle"
801
+ />
802
+
803
+ <div class="flex min-w-0 flex-1 flex-col gap-2">
804
+ <div class="grid grid-cols-2 gap-2">
805
+ <Field orientation="vertical">
806
+ <FieldLabel class="text-xs text-zinc-500">
807
+ <template #tooltip>
808
+ <Markdown
809
+ source="返回 `HttpRequest` 的 CEL 表达式。未配置「下载请求」时直接发起并下载其响应"
810
+ block
811
+ class="prose prose-sm prose-zinc"
812
+ />
813
+ </template>
814
+ 请求
815
+ </FieldLabel>
816
+ <ExpressionEditor
817
+ :model-value="template.request"
818
+ placeholder="例:http.get('https://api.example.com/template.xlsx')"
819
+ result-type="HttpRequest"
820
+ :extra-vars="ROW_VARS"
821
+ class="min-h-10"
822
+ @update:model-value="(v) => updateTemplateRequest(index, v)"
823
+ />
824
+ </Field>
825
+
826
+ <Field orientation="vertical">
827
+ <FieldLabel class="text-xs text-zinc-500">
828
+ <template #tooltip>
829
+ <Markdown
830
+ source="可选的第二步:用第一步响应 `json` 中的凭据构造真正的下载请求,例如 `json.data.key`"
831
+ block
832
+ class="prose prose-sm prose-zinc"
833
+ />
834
+ </template>
835
+ 下载请求
836
+ </FieldLabel>
837
+ <ExpressionEditor
838
+ :model-value="template.download ?? ''"
839
+ placeholder="例:http.get('https://api.example.com/download').query('key', json.data.key)"
840
+ result-type="HttpRequest"
841
+ :extra-vars="TEMPLATE_DOWNLOAD_VARS"
842
+ class="min-h-10"
843
+ @update:model-value="(v) => updateTemplateDownload(index, v)"
844
+ />
845
+ </Field>
846
+ </div>
847
+
848
+ <div class="grid grid-cols-2 gap-2">
849
+ <Field orientation="vertical">
850
+ <FieldLabel class="text-xs text-zinc-500">
851
+ 模板按钮图标
852
+ </FieldLabel>
853
+ <IconPicker
854
+ :model-value="template.icon ?? ''"
855
+ @update:model-value="(v) => updateTemplateIcon(index, v)"
856
+ />
857
+ </Field>
858
+
859
+ <Field orientation="vertical">
860
+ <FieldLabel class="text-xs text-zinc-500">
861
+ 模板按钮文本
862
+ </FieldLabel>
863
+ <Locale
864
+ :model-value="template.label"
865
+ @update:model-value="(v) => updateTemplateLabel(index, v)"
866
+ />
867
+ </Field>
868
+ </div>
869
+ </div>
870
+
871
+ <Button
872
+ variant="ghost"
873
+ size="sm"
874
+ class="absolute top-1 right-1 size-6 p-0 text-red-600 hover:text-red-700"
875
+ aria-label="移除模板"
876
+ @click="removeTemplate(index)"
877
+ >
878
+ <Icon icon="fluent:delete-20-regular" />
879
+ </Button>
880
+ </div>
881
+
882
+ <Button
883
+ type="button"
884
+ data-slot="upload-template-add"
885
+ class="w-full justify-center"
886
+ @click="addTemplate"
887
+ >
888
+ <Icon icon="fluent:add-20-regular" />
889
+ <span>增加</span>
890
+ </Button>
891
+ </div>
892
+ </Field>
893
+
894
+ <Separator />
895
+
896
+ <div class="grid grid-cols-2 gap-x-6 gap-y-4">
897
+ <Field orientation="vertical">
898
+ <FieldLabel class="text-xs text-zinc-500">
899
+ <template
900
+ v-if="fieldDescription('disabled')"
901
+ #tooltip
902
+ >
903
+ <Markdown
904
+ :source="fieldDescription('disabled')"
905
+ block
906
+ class="prose prose-sm prose-zinc"
907
+ />
908
+ </template>
909
+ {{ fieldTitle("disabled") }}
910
+ </FieldLabel>
911
+ <ExpressionEditor
912
+ v-model="disabledModel"
913
+ placeholder="例:row.locked"
914
+ result-type="bool"
915
+ :extra-vars="ROW_VARS"
916
+ />
917
+ </Field>
918
+ <Field orientation="vertical">
919
+ <FieldLabel class="text-xs text-zinc-500">
920
+ <template
921
+ v-if="fieldDescription('readonly')"
922
+ #tooltip
923
+ >
924
+ <Markdown
925
+ :source="fieldDescription('readonly')"
926
+ block
927
+ class="prose prose-sm prose-zinc"
928
+ />
929
+ </template>
930
+ {{ fieldTitle("readonly") }}
931
+ </FieldLabel>
932
+ <ExpressionEditor
933
+ v-model="readonlyModel"
934
+ placeholder="例:row.id != null"
935
+ result-type="bool"
936
+ :extra-vars="ROW_VARS"
937
+ />
938
+ </Field>
939
+ </div>
940
+
941
+ <div class="flex flex-wrap gap-x-8 gap-y-3">
942
+ <Field
943
+ orientation="vertical"
944
+ class="w-full max-w-xs"
945
+ >
946
+ <FieldLabel class="text-xs text-zinc-500">
947
+ <template
948
+ v-if="fieldDescription('enableSorting')"
949
+ #tooltip
950
+ >
951
+ <Markdown
952
+ :source="fieldDescription('enableSorting')"
953
+ block
954
+ class="prose prose-sm prose-zinc"
955
+ />
956
+ </template>
957
+ {{ fieldTitle("enableSorting") }}
958
+ </FieldLabel>
959
+ <InputGroup>
960
+ <InputGroupInput
961
+ :model-value="value.sortKey"
962
+ placeholder="例:created_at"
963
+ @update:model-value="(v) => value = { ...value, sortKey: v || void 0 }"
964
+ />
965
+ <InputGroupAddon align="inline-end">
966
+ <InputGroupButton
967
+ :variant="value.enableSorting ? 'primary' : 'ghost'"
968
+ size="xs"
969
+ @click="value = { ...value, enableSorting: !value.enableSorting }"
970
+ >
971
+ <Icon :icon="value.enableSorting ? 'fluent:arrow-sort-up-16-regular' : 'fluent:arrow-sort-16-regular'" />
972
+ {{ fieldTitle("enableSorting") }}
973
+ </InputGroupButton>
974
+ </InputGroupAddon>
975
+ </InputGroup>
976
+ </Field>
977
+ <Field orientation="vertical">
978
+ <FieldLabel class="text-xs text-zinc-500">
979
+ <template
980
+ v-if="fieldDescription('hidden')"
981
+ #tooltip
982
+ >
983
+ <Markdown
984
+ :source="fieldDescription('hidden')"
985
+ block
986
+ class="prose prose-sm prose-zinc"
987
+ />
988
+ </template>
989
+ {{ fieldTitle("hidden") }}
990
+ </FieldLabel>
991
+ <div>
992
+ <Switch
993
+ :model-value="value.hidden ?? false"
994
+ @update:model-value="onHiddenChange"
995
+ />
996
+ </div>
997
+ </Field>
998
+ </div>
999
+ </div>
1000
+ </template>
1001
+
1002
+ <style scoped>
1003
+ [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}
1004
+ </style>