@shwfed/config 3.0.9 → 3.0.11
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-04-21/com.shwfed.actions.button.navigation/runtime.vue +3 -2
- package/dist/runtime/components/config/blocks/2026-06-17/com.shwfed.block.tree.single/config.d.vue.ts +18 -18
- package/dist/runtime/components/config/blocks/2026-06-17/com.shwfed.block.tree.single/config.vue.d.ts +18 -18
- package/dist/runtime/components/config/blocks/2026-06-17/com.shwfed.block.tree.single/runtime.d.vue.ts +18 -18
- package/dist/runtime/components/config/blocks/2026-06-17/com.shwfed.block.tree.single/runtime.vue.d.ts +18 -18
- package/dist/runtime/components/config/index.d.vue.ts +2 -2
- package/dist/runtime/components/config/index.vue.d.ts +2 -2
- package/dist/runtime/components/form/fields/2026-04-24/com.shwfed.form.field.combobox.single/config.d.vue.ts +16 -16
- package/dist/runtime/components/form/fields/2026-04-24/com.shwfed.form.field.combobox.single/config.vue.d.ts +16 -16
- package/dist/runtime/components/form/fields/2026-05-13/com.shwfed.form.field.combobox.single.remote/config.d.vue.ts +16 -16
- package/dist/runtime/components/form/fields/2026-05-13/com.shwfed.form.field.combobox.single.remote/config.vue.d.ts +16 -16
- package/dist/runtime/components/form/fields/2026-05-23/com.shwfed.form.field.tree.multi/config.d.vue.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-23/com.shwfed.form.field.tree.multi/config.vue.d.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-25/com.shwfed.form.field.combobox.multi/config.d.vue.ts +16 -16
- package/dist/runtime/components/form/fields/2026-05-25/com.shwfed.form.field.combobox.multi/config.vue.d.ts +16 -16
- package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.multi/config.d.vue.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.multi/config.vue.d.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.single/config.d.vue.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.single/config.vue.d.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.multi/config.d.vue.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.multi/config.vue.d.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.single/config.d.vue.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.single/config.vue.d.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.multi/config.d.vue.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.multi/config.vue.d.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.single/config.d.vue.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.single/config.vue.d.ts +26 -26
- package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/config.d.vue.ts +34 -0
- package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/config.vue +109 -1
- package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/config.vue.d.ts +34 -0
- package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/prompt.md +7 -0
- package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/runtime.vue +66 -22
- package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/schema.d.ts +23 -0
- package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/schema.js +26 -0
- package/dist/runtime/components/operations/2026-06-15/com.shwfed.operation.navigation/runtime.js +3 -2
- package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +10 -10
- package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +10 -10
- package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +10 -10
- package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +10 -10
- package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +10 -10
- package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +10 -10
- package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +10 -10
- package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +10 -10
- package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +10 -10
- package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +10 -10
- package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +10 -10
- package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +10 -10
- package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/config.d.vue.ts +10 -0
- package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/config.vue +1004 -0
- package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/config.vue.d.ts +10 -0
- package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/prompt.md +19 -0
- package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/runtime.d.vue.ts +9 -0
- package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/runtime.vue +503 -0
- package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/runtime.vue.d.ts +9 -0
- package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/schema.d.ts +138 -0
- package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/schema.js +181 -0
- package/dist/runtime/share/navigation.d.ts +23 -0
- package/dist/runtime/share/navigation.js +15 -0
- package/dist/runtime/vendor/cel-js/CLAUDE.md +3 -1
- package/dist/runtime/vendor/cel-js/PROMPT.md +13 -0
- package/dist/runtime/vendor/cel-js/lib/macros.js +64 -1
- package/package.json +2 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type __VLS_ModelProps = {
|
|
2
|
+
modelValue: Record<string, any>;
|
|
3
|
+
};
|
|
4
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
5
|
+
"update:modelValue": (value: Record<string, any>) => any;
|
|
6
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
7
|
+
"onUpdate:modelValue"?: ((value: Record<string, any>) => any) | undefined;
|
|
8
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
9
|
+
declare const _default: typeof __VLS_export;
|
|
10
|
+
export default _default;
|
package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/prompt.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
## 文件上传列
|
|
2
|
+
|
|
3
|
+
可编辑单元格,读写 `binding`(`dot-prop` 路径)上的**文件项列表**,与表单的「文件上传」字段同源,只是绑定的是当前行数据(`row` / `index` / `selections` 在 CEL 中可用),而非表单状态。
|
|
4
|
+
|
|
5
|
+
两种模式,取决于是否配置「即时上传」(`upload`):
|
|
6
|
+
|
|
7
|
+
- **不配置**(延迟模式):选中的文件以原始 `File` 数组存在该行 `binding` 上,等待整行提交时由消费方编码。
|
|
8
|
+
- **配置后**(即时模式):每次选择文件立刻发起 `upload.request`(CEL 里用 `files` 引用本次选中的文件,如 `http.post(url).body(form({"files": files}))`),再用 `upload.handle` 把响应 `json` **映射成一个列表**写回 `binding`(如 `json.data.map(d, {"cacheKey": d.key, "filename": d.fileOriginalName})`);`upload.filename` 用 `file` 引用单个已存文件项返回其展示名。绑定值里存的是映射后的对象,不是 `File`。
|
|
9
|
+
|
|
10
|
+
三个表达式各有各的变量:`request` 见 `files`(本次选择)、`handle` 见 `json`(上传响应)、`filename` 见 `file`(单个存量项)——都在行作用域(`row`/`index`)之上,不要混用。
|
|
11
|
+
|
|
12
|
+
`templates` 在上传区旁渲染「下载模板」按钮:只配 `request` 则直接下载其响应;再配 `download` 则是两步式——第二步经 `json` 引用第一步响应(如用 `json.data.key` 拼真正的下载地址)。
|
|
13
|
+
|
|
14
|
+
`download`(文件下载)给每个**文件项**渲染一个下载按钮,点击时执行一条「触发操作」列表(复用 actions 的操作子系统),列表里的表达式可用 `file` 引用被点击的那一项。分两种情况:
|
|
15
|
+
|
|
16
|
+
- **已上传/回填的对象项**:走操作列表,如放一个 `http.download` 操作、请求写 `http.get('/api/file').query('key', file.cacheKey)`;或两步式——先一个 `发送请求` 行取凭据、再一个 `下载文件` 行经 `steps[?"<步骤标识>"]` 引用其响应。凡下载要用到的字段,都必须已由 `upload.handle` 落进文件项。
|
|
17
|
+
- **尚未上传的本地文件**(延迟模式刚选中的原始 `File`):内存里没有服务端标识,故绕过列表、直接从 blob 下载。
|
|
18
|
+
|
|
19
|
+
下载按钮在只读表格/表单里仍显示(查看已提交记录并下载附件是主要场景)。约束类字段(`accept` / `maxFileSize` / `maxTotalSize` / `maxFiles` / `multiple`)按需设置即可。`disabled` / `readonly` 是行作用域 CEL(返回 `bool`)。
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CellContext } from '@tanstack/vue-table';
|
|
2
|
+
import type { Value } from './schema.js';
|
|
3
|
+
type __VLS_Props = {
|
|
4
|
+
column: Value;
|
|
5
|
+
ctx: CellContext<unknown, unknown>;
|
|
6
|
+
};
|
|
7
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
8
|
+
declare const _default: typeof __VLS_export;
|
|
9
|
+
export default _default;
|
package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/runtime.vue
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { Icon } from "@iconify/vue";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import { Fetch } from "fx-fetch";
|
|
5
|
+
import { computed, ref } from "vue";
|
|
6
|
+
import { useI18n } from "vue-i18n";
|
|
7
|
+
import { toast } from "vue-sonner";
|
|
8
|
+
import { cel as _rawCel } from "../../../../../utils/cel";
|
|
9
|
+
import { celScope, injectCELContext } from "../../../../../utils/cel-context";
|
|
10
|
+
import { dispatchTriggers, useEventChannel } from "../../../../../share/event-bus";
|
|
11
|
+
import { getLocalizedText } from "../../../../../share/locale";
|
|
12
|
+
import { Button } from "../../../../ui/button";
|
|
13
|
+
import { useFormReadonly } from "../../../../form/utils/readonly";
|
|
14
|
+
import { useFormState } from "../../../../form/utils/state";
|
|
15
|
+
const ICONS = {
|
|
16
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "vscode-icons:file-type-excel",
|
|
17
|
+
"application/vnd.ms-excel": "vscode-icons:file-type-excel",
|
|
18
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "vscode-icons:file-type-powerpoint",
|
|
19
|
+
"application/vnd.ms-powerpoint": "vscode-icons:file-type-powerpoint",
|
|
20
|
+
"application/zip": "vscode-icons:file-type-zip",
|
|
21
|
+
"application/x-zip-compressed": "vscode-icons:file-type-zip",
|
|
22
|
+
"application/vnd.rar": "vscode-icons:file-type-zip",
|
|
23
|
+
"application/x-rar-compressed": "vscode-icons:file-type-zip",
|
|
24
|
+
"application/pdf": "vscode-icons:file-type-pdf2",
|
|
25
|
+
"application/ofd": "vscode-icons:file-type-ofd",
|
|
26
|
+
"application/xml": "vscode-icons:file-type-xml",
|
|
27
|
+
"image/png": "vscode-icons:file-type-image",
|
|
28
|
+
"image/jpg": "vscode-icons:file-type-image",
|
|
29
|
+
"image/jpeg": "vscode-icons:file-type-image",
|
|
30
|
+
"application/msword": "vscode-icons:file-type-word",
|
|
31
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "vscode-icons:file-type-word"
|
|
32
|
+
};
|
|
33
|
+
const EXTENSIONS = {
|
|
34
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"],
|
|
35
|
+
"application/vnd.ms-excel": [".xls"],
|
|
36
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": [".pptx"],
|
|
37
|
+
"application/vnd.ms-powerpoint": [".ppt"],
|
|
38
|
+
"application/zip": [".zip"],
|
|
39
|
+
"application/x-zip-compressed": [".zip"],
|
|
40
|
+
"application/vnd.rar": [".rar"],
|
|
41
|
+
"application/x-rar-compressed": [".rar"],
|
|
42
|
+
"application/pdf": [".pdf"],
|
|
43
|
+
"application/ofd": [".ofd"],
|
|
44
|
+
"application/xml": [".xml"],
|
|
45
|
+
"image/png": [".png"],
|
|
46
|
+
"image/jpg": [".jpg"],
|
|
47
|
+
"image/jpeg": [".jpeg", ".jpg"],
|
|
48
|
+
"application/msword": [".doc"],
|
|
49
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"]
|
|
50
|
+
};
|
|
51
|
+
const SHORT_NAMES = {
|
|
52
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "XLSX",
|
|
53
|
+
"application/vnd.ms-excel": "XLS",
|
|
54
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "PPTX",
|
|
55
|
+
"application/vnd.ms-powerpoint": "PPT",
|
|
56
|
+
"application/zip": "ZIP",
|
|
57
|
+
"application/x-zip-compressed": "ZIP",
|
|
58
|
+
"application/vnd.rar": "RAR",
|
|
59
|
+
"application/x-rar-compressed": "RAR",
|
|
60
|
+
"application/pdf": "PDF",
|
|
61
|
+
"application/ofd": "OFD",
|
|
62
|
+
"application/xml": "XML",
|
|
63
|
+
"image/png": "PNG",
|
|
64
|
+
"image/jpg": "JPG",
|
|
65
|
+
"image/jpeg": "JPEG",
|
|
66
|
+
"application/msword": "DOC",
|
|
67
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "DOCX"
|
|
68
|
+
};
|
|
69
|
+
defineOptions({ name: "ShwfedTableUploadRendererRuntime" });
|
|
70
|
+
const props = defineProps({
|
|
71
|
+
column: { type: null, required: true },
|
|
72
|
+
ctx: { type: Object, required: true }
|
|
73
|
+
});
|
|
74
|
+
const { locale, t } = useI18n({
|
|
75
|
+
inheritLocale: true,
|
|
76
|
+
messages: {
|
|
77
|
+
zh: {
|
|
78
|
+
"upload-add-file": "\u6DFB\u52A0\u6587\u4EF6",
|
|
79
|
+
"upload-accept-hint": "\u4EC5\u63A5\u53D7 {types}",
|
|
80
|
+
"upload-rejected-mime": "\u300C{name}\u300D\u7C7B\u578B\u4E0D\u88AB\u5141\u8BB8",
|
|
81
|
+
"upload-rejected-size": "\u300C{name}\u300D\u8D85\u51FA\u5355\u6587\u4EF6\u5927\u5C0F\u9650\u5236",
|
|
82
|
+
"upload-rejected-total": "\u300C{name}\u300D\u52A0\u5165\u540E\u5C06\u8D85\u51FA\u603B\u5927\u5C0F\u9650\u5236",
|
|
83
|
+
"upload-rejected-count": "\u5DF2\u8FBE\u5230\u6587\u4EF6\u6570\u91CF\u4E0A\u9650\uFF0C\u300C{name}\u300D\u672A\u6DFB\u52A0",
|
|
84
|
+
"upload-uploading": "\u4E0A\u4F20\u4E2D\u2026",
|
|
85
|
+
"upload-failed": "\u6587\u4EF6\u4E0A\u4F20\u5931\u8D25"
|
|
86
|
+
},
|
|
87
|
+
en: {
|
|
88
|
+
"upload-add-file": "Add file",
|
|
89
|
+
"upload-accept-hint": "Only {types} are accepted",
|
|
90
|
+
"upload-rejected-mime": "{name} is not an allowed file type",
|
|
91
|
+
"upload-rejected-size": "{name} exceeds the maximum file size",
|
|
92
|
+
"upload-rejected-total": "{name} would exceed the total size limit",
|
|
93
|
+
"upload-rejected-count": "File count limit reached; {name} was not added",
|
|
94
|
+
"upload-uploading": "Uploading\u2026",
|
|
95
|
+
"upload-failed": "File upload failed"
|
|
96
|
+
},
|
|
97
|
+
ja: {
|
|
98
|
+
"upload-add-file": "\u30D5\u30A1\u30A4\u30EB\u3092\u8FFD\u52A0",
|
|
99
|
+
"upload-accept-hint": "{types} \u306E\u307F\u53D7\u3051\u4ED8\u3051\u307E\u3059",
|
|
100
|
+
"upload-rejected-mime": "\u300C{name}\u300D\u306F\u8A31\u53EF\u3055\u308C\u305F\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F\u3067\u306F\u3042\u308A\u307E\u305B\u3093",
|
|
101
|
+
"upload-rejected-size": "\u300C{name}\u300D\u306F\u30D5\u30A1\u30A4\u30EB\u30B5\u30A4\u30BA\u306E\u4E0A\u9650\u3092\u8D85\u3048\u3066\u3044\u307E\u3059",
|
|
102
|
+
"upload-rejected-total": "\u300C{name}\u300D\u3092\u8FFD\u52A0\u3059\u308B\u3068\u5408\u8A08\u30B5\u30A4\u30BA\u306E\u4E0A\u9650\u3092\u8D85\u3048\u307E\u3059",
|
|
103
|
+
"upload-rejected-count": "\u30D5\u30A1\u30A4\u30EB\u6570\u306E\u4E0A\u9650\u306B\u9054\u3057\u305F\u305F\u3081\u300C{name}\u300D\u306F\u8FFD\u52A0\u3055\u308C\u307E\u305B\u3093",
|
|
104
|
+
"upload-uploading": "\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u4E2D\u2026",
|
|
105
|
+
"upload-failed": "\u30D5\u30A1\u30A4\u30EB\u306E\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u306B\u5931\u6557\u3057\u307E\u3057\u305F"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
const formState = useFormState();
|
|
110
|
+
const inherited = injectCELContext();
|
|
111
|
+
const $cel = (expression, context) => _rawCel(expression, celScope(inherited, context));
|
|
112
|
+
const eventChannel = useEventChannel();
|
|
113
|
+
const placeholderText = computed(
|
|
114
|
+
() => props.column.placeholder ? getLocalizedText(props.column.placeholder, locale.value) : void 0
|
|
115
|
+
);
|
|
116
|
+
function evalBool(expression, label) {
|
|
117
|
+
if (!expression) return false;
|
|
118
|
+
try {
|
|
119
|
+
return Effect.runSync($cel(expression, {})) === true;
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error(`[shwfed-table] upload ${label} failed for ${props.column.id}:`, err);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const isDisabled = computed(() => evalBool(props.column.disabled, "disabled"));
|
|
126
|
+
const formReadonly = useFormReadonly();
|
|
127
|
+
const isReadonly = computed(
|
|
128
|
+
() => props.column.readonly != null ? evalBool(props.column.readonly, "readonly") : formReadonly.value
|
|
129
|
+
);
|
|
130
|
+
const entries = computed({
|
|
131
|
+
get: () => {
|
|
132
|
+
const raw = formState.getAt(props.column.binding);
|
|
133
|
+
return Array.isArray(raw) ? raw : [];
|
|
134
|
+
},
|
|
135
|
+
set: (next) => {
|
|
136
|
+
formState.setAt(props.column.binding, next.length === 0 ? null : next);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
const immediate = computed(() => props.column.upload != null);
|
|
140
|
+
const uploading = ref(false);
|
|
141
|
+
function entryName(entry) {
|
|
142
|
+
if (entry instanceof File) return entry.name;
|
|
143
|
+
const expr = props.column.upload?.filename;
|
|
144
|
+
if (expr) {
|
|
145
|
+
try {
|
|
146
|
+
const name = Effect.runSync($cel(expr, { file: entry }));
|
|
147
|
+
if (typeof name === "string" && name.length > 0) return name;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error(`[shwfed-table] upload filename failed for ${props.column.id}:`, err);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (entry != null && typeof entry === "object") {
|
|
153
|
+
const rec = entry;
|
|
154
|
+
if (typeof rec.filename === "string") return rec.filename;
|
|
155
|
+
if (typeof rec.name === "string") return rec.name;
|
|
156
|
+
}
|
|
157
|
+
return "(\u6587\u4EF6)";
|
|
158
|
+
}
|
|
159
|
+
function entryIcon(entry) {
|
|
160
|
+
if (entry instanceof File) return ICONS[entry.type] ?? "fluent:document-20-regular";
|
|
161
|
+
return "fluent:document-20-regular";
|
|
162
|
+
}
|
|
163
|
+
const inputRef = ref();
|
|
164
|
+
const acceptAttr = computed(() => {
|
|
165
|
+
const list = props.column.accept;
|
|
166
|
+
return list && list.length > 0 ? list.join(",") : void 0;
|
|
167
|
+
});
|
|
168
|
+
const acceptHint = computed(() => {
|
|
169
|
+
const list = props.column.accept;
|
|
170
|
+
if (!list || list.length === 0) return void 0;
|
|
171
|
+
return Array.from(new Set(list.map((m) => SHORT_NAMES[m] ?? m))).join("\u3001");
|
|
172
|
+
});
|
|
173
|
+
const isFull = computed(
|
|
174
|
+
() => props.column.maxFiles !== void 0 && entries.value.length >= props.column.maxFiles
|
|
175
|
+
);
|
|
176
|
+
const canAdd = computed(() => !isDisabled.value && !isReadonly.value && !isFull.value && !uploading.value);
|
|
177
|
+
function isMimeAllowed(file) {
|
|
178
|
+
const list = props.column.accept;
|
|
179
|
+
if (!list || list.length === 0) return true;
|
|
180
|
+
if (file.type && list.includes(file.type)) return true;
|
|
181
|
+
const dot = file.name.lastIndexOf(".");
|
|
182
|
+
if (dot < 0) return false;
|
|
183
|
+
const ext = file.name.slice(dot).toLowerCase();
|
|
184
|
+
return list.some((mime) => EXTENSIONS[mime]?.includes(ext));
|
|
185
|
+
}
|
|
186
|
+
function partition(incoming) {
|
|
187
|
+
const accepted = [];
|
|
188
|
+
const rejected = [];
|
|
189
|
+
let runningTotal = entries.value.reduce((acc, f) => acc + (f instanceof File ? f.size : 0), 0);
|
|
190
|
+
let count = entries.value.length;
|
|
191
|
+
for (const file of incoming) {
|
|
192
|
+
if (!isMimeAllowed(file)) {
|
|
193
|
+
rejected.push({ file, reason: "mime" });
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (props.column.maxFileSize !== void 0 && file.size > props.column.maxFileSize) {
|
|
197
|
+
rejected.push({ file, reason: "size" });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (props.column.maxFiles !== void 0 && count >= props.column.maxFiles) {
|
|
201
|
+
rejected.push({ file, reason: "count" });
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (props.column.maxTotalSize !== void 0 && runningTotal + file.size > props.column.maxTotalSize) {
|
|
205
|
+
rejected.push({ file, reason: "total" });
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
accepted.push(file);
|
|
209
|
+
runningTotal += file.size;
|
|
210
|
+
count += 1;
|
|
211
|
+
}
|
|
212
|
+
return { accepted, rejected };
|
|
213
|
+
}
|
|
214
|
+
async function uploadFiles(accepted) {
|
|
215
|
+
const upload = props.column.upload;
|
|
216
|
+
if (!upload) return [];
|
|
217
|
+
const program = Effect.gen(function* () {
|
|
218
|
+
const builder = yield* $cel(upload.request, { files: accepted });
|
|
219
|
+
const json = yield* builder.json();
|
|
220
|
+
return yield* $cel(upload.handle, { json });
|
|
221
|
+
});
|
|
222
|
+
try {
|
|
223
|
+
const items = await Effect.runPromise(Effect.provide(program, Fetch.layer));
|
|
224
|
+
if (!Array.isArray(items)) {
|
|
225
|
+
console.error(`[shwfed-table] upload handle for ${props.column.id} did not return a list`);
|
|
226
|
+
toast.error(t("upload-failed"));
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
return items;
|
|
230
|
+
} catch (err) {
|
|
231
|
+
console.error(`[shwfed-table] failed to upload files for ${props.column.id}:`, err);
|
|
232
|
+
toast.error(t("upload-failed"));
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function ingest(list) {
|
|
237
|
+
if (!list || list.length === 0) return;
|
|
238
|
+
const multi = props.column.multiple ?? false;
|
|
239
|
+
const incoming = multi ? Array.from(list) : Array.from(list).slice(0, 1);
|
|
240
|
+
const { accepted, rejected } = partition(incoming);
|
|
241
|
+
for (const r of rejected) {
|
|
242
|
+
toast.error(t(`upload-rejected-${r.reason}`, { name: r.file.name }));
|
|
243
|
+
}
|
|
244
|
+
if (inputRef.value) inputRef.value.value = "";
|
|
245
|
+
if (accepted.length === 0) return;
|
|
246
|
+
if (!immediate.value) {
|
|
247
|
+
entries.value = multi ? [...entries.value, ...accepted] : accepted;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
uploading.value = true;
|
|
251
|
+
try {
|
|
252
|
+
const items = await uploadFiles(accepted);
|
|
253
|
+
if (items.length > 0) {
|
|
254
|
+
entries.value = multi ? [...entries.value, ...items] : items;
|
|
255
|
+
}
|
|
256
|
+
} finally {
|
|
257
|
+
uploading.value = false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function onPickerChange(event) {
|
|
261
|
+
void ingest(event.target.files);
|
|
262
|
+
}
|
|
263
|
+
function onAddClick() {
|
|
264
|
+
if (!canAdd.value) return;
|
|
265
|
+
inputRef.value?.click();
|
|
266
|
+
}
|
|
267
|
+
function onKeydown(event) {
|
|
268
|
+
if (!canAdd.value) return;
|
|
269
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
inputRef.value?.click();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function onDelete(index) {
|
|
275
|
+
if (isDisabled.value || isReadonly.value || uploading.value) return;
|
|
276
|
+
const next = [...entries.value];
|
|
277
|
+
next.splice(index, 1);
|
|
278
|
+
entries.value = next;
|
|
279
|
+
}
|
|
280
|
+
function saveFile(file) {
|
|
281
|
+
const url = URL.createObjectURL(file);
|
|
282
|
+
const a = document.createElement("a");
|
|
283
|
+
a.href = url;
|
|
284
|
+
a.download = file.name;
|
|
285
|
+
document.body.appendChild(a);
|
|
286
|
+
a.click();
|
|
287
|
+
a.remove();
|
|
288
|
+
URL.revokeObjectURL(url);
|
|
289
|
+
}
|
|
290
|
+
const templates = computed(() => props.column.templates ?? []);
|
|
291
|
+
function templateIcon(template) {
|
|
292
|
+
return template.icon ?? "fluent:arrow-download-20-regular";
|
|
293
|
+
}
|
|
294
|
+
function templateLabelText(template) {
|
|
295
|
+
return (template.label ? getLocalizedText(template.label, locale.value) : void 0) ?? "\u4E0B\u8F7D\u6A21\u677F";
|
|
296
|
+
}
|
|
297
|
+
async function onDownloadTemplate(template) {
|
|
298
|
+
const program = Effect.gen(function* () {
|
|
299
|
+
const requestBuilder = yield* $cel(template.request, {});
|
|
300
|
+
if (!template.download) {
|
|
301
|
+
return yield* requestBuilder.file();
|
|
302
|
+
}
|
|
303
|
+
const json = yield* requestBuilder.json();
|
|
304
|
+
const downloadBuilder = yield* $cel(template.download, { json });
|
|
305
|
+
return yield* downloadBuilder.file();
|
|
306
|
+
});
|
|
307
|
+
try {
|
|
308
|
+
const file = await Effect.runPromise(Effect.provide(program, Fetch.layer));
|
|
309
|
+
if (!(file instanceof File)) {
|
|
310
|
+
console.error(`[shwfed-table] template request for ${props.column.id} did not produce a File`);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
saveFile(file);
|
|
314
|
+
} catch (err) {
|
|
315
|
+
console.error(`[shwfed-table] failed to download template for ${props.column.id}:`, err);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const downloadEnabled = computed(() => props.column.download != null);
|
|
319
|
+
const downloadIcon = computed(() => props.column.download?.icon ?? "fluent:arrow-download-20-regular");
|
|
320
|
+
const downloadLabel = computed(
|
|
321
|
+
() => (props.column.download?.label ? getLocalizedText(props.column.download.label, locale.value) : void 0) ?? "\u4E0B\u8F7D"
|
|
322
|
+
);
|
|
323
|
+
function canDownload(entry) {
|
|
324
|
+
if (!downloadEnabled.value) return false;
|
|
325
|
+
if (entry instanceof File) return true;
|
|
326
|
+
return (props.column.download?.operations.length ?? 0) > 0;
|
|
327
|
+
}
|
|
328
|
+
async function onDownloadEntry(entry) {
|
|
329
|
+
if (!downloadEnabled.value) return;
|
|
330
|
+
if (entry instanceof File) {
|
|
331
|
+
saveFile(entry);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const download = props.column.download;
|
|
335
|
+
if (!download || download.operations.length === 0) return;
|
|
336
|
+
const triggerCel = (expression, context) => _rawCel(expression, celScope(inherited, { file: entry, ...context }));
|
|
337
|
+
try {
|
|
338
|
+
await Effect.runPromise(dispatchTriggers(eventChannel, download.operations, triggerCel));
|
|
339
|
+
} catch (err) {
|
|
340
|
+
if (import.meta.dev) console.warn(`[shwfed-table] download operations halted for ${props.column.id}:`, err);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
</script>
|
|
344
|
+
|
|
345
|
+
<template>
|
|
346
|
+
<div class="p-[0.125rem] w-full">
|
|
347
|
+
<div
|
|
348
|
+
class="flex flex-col gap-1.5"
|
|
349
|
+
:data-disabled="isDisabled || void 0"
|
|
350
|
+
:data-readonly="isReadonly || void 0"
|
|
351
|
+
>
|
|
352
|
+
<!--
|
|
353
|
+
Readonly empty state mirrors the other columns: a centered, faded,
|
|
354
|
+
non-selectable `-` in `font-mono`, so a readonly grid reads uniformly
|
|
355
|
+
regardless of which column type backs each cell.
|
|
356
|
+
-->
|
|
357
|
+
<span
|
|
358
|
+
v-if="entries.length === 0 && isReadonly"
|
|
359
|
+
class="flex items-center justify-center h-7 w-full px-2 text-[0.75rem] font-mono text-zinc-300 select-none"
|
|
360
|
+
data-slot="upload-empty"
|
|
361
|
+
>
|
|
362
|
+
{{ "-" }}
|
|
363
|
+
</span>
|
|
364
|
+
|
|
365
|
+
<!-- File list — shown whenever there are entries (readonly or not). -->
|
|
366
|
+
<ul
|
|
367
|
+
v-else-if="entries.length > 0"
|
|
368
|
+
data-slot="upload-zone-list"
|
|
369
|
+
class="flex flex-col gap-0.5"
|
|
370
|
+
>
|
|
371
|
+
<li
|
|
372
|
+
v-for="(entry, index) in entries"
|
|
373
|
+
:key="`${entryName(entry)}-${index}`"
|
|
374
|
+
data-slot="upload-zone-item"
|
|
375
|
+
class="flex items-center justify-between gap-1 rounded px-1.5 py-1 text-[0.75rem] leading-snug transition-[background-color] duration-180 ease-out hover:bg-zinc-50"
|
|
376
|
+
>
|
|
377
|
+
<span class="flex min-w-0 items-center gap-1.5">
|
|
378
|
+
<Icon
|
|
379
|
+
:icon="entryIcon(entry)"
|
|
380
|
+
class="size-3.5 shrink-0"
|
|
381
|
+
/>
|
|
382
|
+
<span class="truncate text-zinc-700">{{ entryName(entry) }}</span>
|
|
383
|
+
</span>
|
|
384
|
+
<span class="flex shrink-0 items-center gap-0.5">
|
|
385
|
+
<Button
|
|
386
|
+
v-if="canDownload(entry)"
|
|
387
|
+
type="button"
|
|
388
|
+
variant="ghost"
|
|
389
|
+
size="sm"
|
|
390
|
+
data-slot="upload-zone-download"
|
|
391
|
+
class="size-5 p-0 text-zinc-500 hover:bg-zinc-100 hover:text-zinc-700"
|
|
392
|
+
:aria-label="`${downloadLabel}\uFF1A${entryName(entry)}`"
|
|
393
|
+
@click="onDownloadEntry(entry)"
|
|
394
|
+
>
|
|
395
|
+
<Icon
|
|
396
|
+
:icon="downloadIcon"
|
|
397
|
+
class="size-3.5"
|
|
398
|
+
/>
|
|
399
|
+
</Button>
|
|
400
|
+
<Button
|
|
401
|
+
v-if="!isDisabled && !isReadonly"
|
|
402
|
+
type="button"
|
|
403
|
+
variant="ghost"
|
|
404
|
+
size="sm"
|
|
405
|
+
data-slot="upload-zone-delete"
|
|
406
|
+
class="size-5 p-0 text-zinc-500 hover:bg-red-50 hover:text-red-600"
|
|
407
|
+
:disabled="uploading"
|
|
408
|
+
:aria-label="`\u5220\u9664\uFF1A${entryName(entry)}`"
|
|
409
|
+
@click="onDelete(index)"
|
|
410
|
+
>
|
|
411
|
+
<Icon
|
|
412
|
+
icon="fluent:delete-20-regular"
|
|
413
|
+
class="size-3.5"
|
|
414
|
+
/>
|
|
415
|
+
</Button>
|
|
416
|
+
</span>
|
|
417
|
+
</li>
|
|
418
|
+
</ul>
|
|
419
|
+
|
|
420
|
+
<!--
|
|
421
|
+
Empty editable state: a compact dashed add zone. Kept short (min-h-16)
|
|
422
|
+
so an empty row does not tower over its neighbours; the non-empty list
|
|
423
|
+
grows only as far as the files it holds.
|
|
424
|
+
-->
|
|
425
|
+
<div
|
|
426
|
+
v-else
|
|
427
|
+
role="button"
|
|
428
|
+
:tabindex="canAdd ? 0 : -1"
|
|
429
|
+
data-slot="upload-zone"
|
|
430
|
+
:data-disabled="isDisabled || void 0"
|
|
431
|
+
:aria-disabled="isDisabled || void 0"
|
|
432
|
+
class="flex min-h-16 w-full flex-col items-center justify-center gap-1 rounded border border-dashed border-zinc-200 bg-zinc-50/60 px-2 py-2 outline-none transition-colors duration-180 ease-out focus-visible:border-(--primary) data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-60"
|
|
433
|
+
:class="canAdd && 'cursor-pointer hover:border-zinc-400 hover:bg-zinc-100'"
|
|
434
|
+
@click="onAddClick"
|
|
435
|
+
@keydown="onKeydown"
|
|
436
|
+
>
|
|
437
|
+
<Icon
|
|
438
|
+
:icon="uploading ? 'fluent:spinner-ios-20-regular' : 'fluent:cloud-arrow-up-20-regular'"
|
|
439
|
+
class="size-5 text-zinc-400"
|
|
440
|
+
:class="uploading && 'animate-spin'"
|
|
441
|
+
/>
|
|
442
|
+
<p class="text-[0.7rem] text-zinc-500">
|
|
443
|
+
{{ uploading ? t("upload-uploading") : placeholderText ?? t("upload-add-file") }}
|
|
444
|
+
</p>
|
|
445
|
+
<p
|
|
446
|
+
v-if="acceptHint"
|
|
447
|
+
class="text-[0.65rem] text-zinc-400"
|
|
448
|
+
>
|
|
449
|
+
{{ t("upload-accept-hint", { types: acceptHint }) }}
|
|
450
|
+
</p>
|
|
451
|
+
</div>
|
|
452
|
+
|
|
453
|
+
<!-- Add button — only when the list is non-empty and adding is allowed. -->
|
|
454
|
+
<Button
|
|
455
|
+
v-if="entries.length > 0 && canAdd"
|
|
456
|
+
type="button"
|
|
457
|
+
variant="default"
|
|
458
|
+
size="sm"
|
|
459
|
+
data-slot="upload-zone-add"
|
|
460
|
+
class="h-6 w-full shrink-0 text-[0.75rem]"
|
|
461
|
+
@click="onAddClick"
|
|
462
|
+
>
|
|
463
|
+
<Icon
|
|
464
|
+
:icon="uploading ? 'fluent:spinner-ios-20-regular' : 'fluent:add-20-regular'"
|
|
465
|
+
:class="uploading && 'animate-spin'"
|
|
466
|
+
/>
|
|
467
|
+
{{ uploading ? t("upload-uploading") : t("upload-add-file") }}
|
|
468
|
+
</Button>
|
|
469
|
+
|
|
470
|
+
<input
|
|
471
|
+
ref="inputRef"
|
|
472
|
+
type="file"
|
|
473
|
+
class="hidden"
|
|
474
|
+
:accept="acceptAttr"
|
|
475
|
+
:multiple="column.multiple"
|
|
476
|
+
:disabled="isDisabled || isReadonly"
|
|
477
|
+
@change="onPickerChange"
|
|
478
|
+
>
|
|
479
|
+
|
|
480
|
+
<div
|
|
481
|
+
v-if="templates.length > 0 && !isReadonly"
|
|
482
|
+
class="flex flex-wrap justify-end gap-1"
|
|
483
|
+
>
|
|
484
|
+
<Button
|
|
485
|
+
v-for="(template, index) in templates"
|
|
486
|
+
:key="index"
|
|
487
|
+
type="button"
|
|
488
|
+
variant="link"
|
|
489
|
+
size="sm"
|
|
490
|
+
data-slot="upload-zone-template"
|
|
491
|
+
class="h-5 px-1 text-[0.7rem]"
|
|
492
|
+
@click="onDownloadTemplate(template)"
|
|
493
|
+
>
|
|
494
|
+
<Icon
|
|
495
|
+
:icon="templateIcon(template)"
|
|
496
|
+
class="size-3.5"
|
|
497
|
+
/>
|
|
498
|
+
{{ templateLabelText(template) }}
|
|
499
|
+
</Button>
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
</template>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CellContext } from '@tanstack/vue-table';
|
|
2
|
+
import type { Value } from './schema.js';
|
|
3
|
+
type __VLS_Props = {
|
|
4
|
+
column: Value;
|
|
5
|
+
ctx: CellContext<unknown, unknown>;
|
|
6
|
+
};
|
|
7
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
8
|
+
declare const _default: typeof __VLS_export;
|
|
9
|
+
export default _default;
|