vome-core 0.0.89 → 0.0.91
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/admin/config/plugin-dev.js +67 -1
- package/dist/admin/crud/config.js +33 -1
- package/dist/admin/crud/confirm.js +97 -1
- package/dist/admin/crud/dict.js +67 -1
- package/dist/admin/crud/index.js +81 -1
- package/dist/admin/crud/key.js +20 -1
- package/dist/admin/crud/mitt.js +21 -1
- package/dist/admin/crud/plugins.js +278 -1
- package/dist/admin/crud/span.js +35 -1
- package/dist/admin/crud/style.js +132 -1
- package/dist/admin/crud/validate.js +92 -1
- package/dist/admin/crud/vm-upsert.vue +29 -0
- package/dist/admin/directives/perm.js +14 -1
- package/dist/admin/hooks/useUpload.js +63 -1
- package/dist/admin/lib/browser.js +24 -1
- package/dist/admin/lib/cn.js +5 -1
- package/dist/admin/lib/dialog-float.js +13 -1
- package/dist/admin/lib/export-excel.js +64 -1
- package/dist/admin/lib/import-excel.js +61 -1
- package/dist/admin/lib/json.js +42 -1
- package/dist/admin/lib/menu.js +11 -1
- package/dist/admin/lib/tree.js +1 -1
- package/dist/admin/lib/upload.js +88 -1
- package/dist/admin/lib/video-frame.js +80 -1
- package/dist/client/dict.d.ts +8 -1
- package/dist/client/dict.js +13 -2
- package/dist/index.js +28215 -1
- package/dist/server/index.js +36228 -1
- package/dist/shared/excel.js +93 -1
- package/dist/shared/index.js +20 -1
- package/dist/shared/locale-json.js +68 -1
- package/dist/shared/tree.js +39 -1
- package/package.json +1 -1
|
@@ -1 +1,63 @@
|
|
|
1
|
-
|
|
1
|
+
import { toast } from "vue-sonner";
|
|
2
|
+
import { request } from "../api/client";
|
|
3
|
+
import { extname, filename, pathJoin, uploadUid } from "../lib/upload";
|
|
4
|
+
const UPLOAD_PATH = "/admin/base/comm/upload";
|
|
5
|
+
const SIGN_SKIP = new Set([
|
|
6
|
+
"url",
|
|
7
|
+
"host",
|
|
8
|
+
"uploadUrl",
|
|
9
|
+
"publicDomain",
|
|
10
|
+
"previewUrl",
|
|
11
|
+
"preview"
|
|
12
|
+
]);
|
|
13
|
+
export function useUpload() {
|
|
14
|
+
async function toUpload(file, opts = {}) {
|
|
15
|
+
const { prefixPath = "app/public", onProgress } = opts;
|
|
16
|
+
const fileId = uploadUid();
|
|
17
|
+
const ext = extname(file.name);
|
|
18
|
+
const name = `${filename(file.name)}_${fileId}${ext ? `.${ext}` : ""}`;
|
|
19
|
+
const key = pathJoin(prefixPath, name);
|
|
20
|
+
const sign = await request(UPLOAD_PATH, {
|
|
21
|
+
method: "POST",
|
|
22
|
+
body: JSON.stringify({})
|
|
23
|
+
});
|
|
24
|
+
const host = String(sign.host || sign.url || sign.uploadUrl || "");
|
|
25
|
+
if (!host)
|
|
26
|
+
throw new Error("上传地址无效");
|
|
27
|
+
const fd = new FormData;
|
|
28
|
+
fd.append("key", key);
|
|
29
|
+
for (const [k, v] of Object.entries(sign)) {
|
|
30
|
+
if (SIGN_SKIP.has(k) || v == null || fd.has(k))
|
|
31
|
+
continue;
|
|
32
|
+
fd.append(k, String(v));
|
|
33
|
+
}
|
|
34
|
+
fd.append("file", file);
|
|
35
|
+
await xhrUpload(host, fd, onProgress);
|
|
36
|
+
const preview = String(sign.publicDomain || sign.previewUrl || host);
|
|
37
|
+
return { url: pathJoin(preview, key), key, fileId };
|
|
38
|
+
}
|
|
39
|
+
return { toUpload };
|
|
40
|
+
}
|
|
41
|
+
function xhrUpload(url, body, onProgress) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const xhr = new XMLHttpRequest;
|
|
44
|
+
xhr.open("POST", url);
|
|
45
|
+
xhr.upload.onprogress = (e) => {
|
|
46
|
+
if (!e.lengthComputable)
|
|
47
|
+
return;
|
|
48
|
+
onProgress?.(Math.min(100, Math.floor(e.loaded / e.total * 100)));
|
|
49
|
+
};
|
|
50
|
+
xhr.onload = () => {
|
|
51
|
+
if (xhr.status < 200 || xhr.status >= 300) {
|
|
52
|
+
const msg = xhr.responseText || `上传失败 (${xhr.status})`;
|
|
53
|
+
toast.error(msg.slice(0, 200));
|
|
54
|
+
reject(new Error(msg));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
onProgress?.(100);
|
|
58
|
+
resolve();
|
|
59
|
+
};
|
|
60
|
+
xhr.onerror = () => reject(new Error("上传网络错误"));
|
|
61
|
+
xhr.send(body);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -1 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
export function getBrowser() {
|
|
2
|
+
const { clientHeight: height, clientWidth: width } = document.documentElement;
|
|
3
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
4
|
+
let type = (ua.match(/firefox|chrome|safari|opera/g) || ["other"])[0];
|
|
5
|
+
if ((ua.match(/msie|trident/g) || [])[0])
|
|
6
|
+
type = "msie";
|
|
7
|
+
let screen = "full";
|
|
8
|
+
if (width < 768)
|
|
9
|
+
screen = "xs";
|
|
10
|
+
else if (width < 992)
|
|
11
|
+
screen = "sm";
|
|
12
|
+
else if (width < 1200)
|
|
13
|
+
screen = "md";
|
|
14
|
+
else if (width < 1920)
|
|
15
|
+
screen = "xl";
|
|
16
|
+
const isPC = !/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(ua);
|
|
17
|
+
return {
|
|
18
|
+
height,
|
|
19
|
+
width,
|
|
20
|
+
type,
|
|
21
|
+
screen,
|
|
22
|
+
isMini: screen === "xs" || !isPC
|
|
23
|
+
};
|
|
24
|
+
}
|
package/dist/admin/lib/cn.js
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { clsx } from "clsx";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
export function cn(...inputs) {
|
|
4
|
+
return twMerge(clsx(inputs));
|
|
5
|
+
}
|
|
@@ -1 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
export const DIALOG_FLOAT_CLASS = "vm-dialog-float";
|
|
2
|
+
export function isDialogFloatTarget(event) {
|
|
3
|
+
const detail = event.detail;
|
|
4
|
+
const t = detail?.originalEvent?.target ?? event.target;
|
|
5
|
+
if (!t)
|
|
6
|
+
return false;
|
|
7
|
+
const el = t instanceof Element ? t : t.parentElement;
|
|
8
|
+
return Boolean(el?.closest(`.${DIALOG_FLOAT_CLASS}`));
|
|
9
|
+
}
|
|
10
|
+
export function preventDialogOutsideClose(event) {
|
|
11
|
+
if (isDialogFloatTarget(event))
|
|
12
|
+
event.preventDefault();
|
|
13
|
+
}
|
|
@@ -1 +1,64 @@
|
|
|
1
|
-
|
|
1
|
+
import { toValue } from "vue";
|
|
2
|
+
import {
|
|
3
|
+
buildExcelBuffer,
|
|
4
|
+
excelFilename
|
|
5
|
+
} from "../../shared/excel";
|
|
6
|
+
import { flattenTree } from "./tree";
|
|
7
|
+
export {
|
|
8
|
+
buildExcelBuffer,
|
|
9
|
+
excelFilename,
|
|
10
|
+
excelResponseHeaders
|
|
11
|
+
} from "../../shared/excel";
|
|
12
|
+
export function pickExportColumns(columns) {
|
|
13
|
+
return columns.filter((c) => Boolean(c.prop) && c.type !== "selection" && c.type !== "op" && c.type !== "expand" && c.type !== "index" && !c.hidden);
|
|
14
|
+
}
|
|
15
|
+
function formatExportCell(col, row) {
|
|
16
|
+
if (!col.prop)
|
|
17
|
+
return "";
|
|
18
|
+
const val = row[col.prop];
|
|
19
|
+
if (col.formatter)
|
|
20
|
+
return col.formatter(row, val);
|
|
21
|
+
const dict = col.dict == null ? [] : toValue(col.dict);
|
|
22
|
+
if (Array.isArray(dict) && dict.length && val != null && val !== "") {
|
|
23
|
+
const hit = dict.find((d) => d.value === val || String(d.value) === String(val));
|
|
24
|
+
if (hit)
|
|
25
|
+
return hit.label;
|
|
26
|
+
}
|
|
27
|
+
if (val == null)
|
|
28
|
+
return "";
|
|
29
|
+
return String(val);
|
|
30
|
+
}
|
|
31
|
+
export function flattenExportRows(rows, tree) {
|
|
32
|
+
if (!tree)
|
|
33
|
+
return rows;
|
|
34
|
+
const nested = rows.some((r) => Array.isArray(r.children) && r.children.length);
|
|
35
|
+
if (!nested)
|
|
36
|
+
return rows;
|
|
37
|
+
return flattenTree(rows);
|
|
38
|
+
}
|
|
39
|
+
export function crudColumnsToExcel(columns) {
|
|
40
|
+
return pickExportColumns(columns).map((c) => ({
|
|
41
|
+
prop: c.prop,
|
|
42
|
+
label: c.label || c.prop
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
export function downloadExcelBuffer(buffer, filename) {
|
|
46
|
+
const blob = new Blob([buffer], {
|
|
47
|
+
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
48
|
+
});
|
|
49
|
+
const url = URL.createObjectURL(blob);
|
|
50
|
+
const link = document.createElement("a");
|
|
51
|
+
link.href = url;
|
|
52
|
+
link.download = excelFilename(filename);
|
|
53
|
+
link.click();
|
|
54
|
+
URL.revokeObjectURL(url);
|
|
55
|
+
}
|
|
56
|
+
export function exportCrudExcel(columns, rows, filename) {
|
|
57
|
+
const exportCols = pickExportColumns(columns);
|
|
58
|
+
const excelCols = crudColumnsToExcel(exportCols);
|
|
59
|
+
const buffer = buildExcelBuffer(excelCols, rows, (prop, row) => {
|
|
60
|
+
const col = exportCols.find((c) => c.prop === prop);
|
|
61
|
+
return col ? formatExportCell(col, row) : String(row[prop] ?? "");
|
|
62
|
+
});
|
|
63
|
+
downloadExcelBuffer(buffer, filename);
|
|
64
|
+
}
|
|
@@ -1 +1,61 @@
|
|
|
1
|
-
|
|
1
|
+
import { apiUrl, getAccessToken } from "../api/client";
|
|
2
|
+
import { downloadExcelBuffer } from "./export-excel";
|
|
3
|
+
import { parseExcelBuffer } from "../../shared/excel";
|
|
4
|
+
function authHeaders(extra) {
|
|
5
|
+
const headers = new Headers(extra);
|
|
6
|
+
const token = getAccessToken();
|
|
7
|
+
if (token)
|
|
8
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
9
|
+
return headers;
|
|
10
|
+
}
|
|
11
|
+
function resolveNamespace(ns) {
|
|
12
|
+
if (!ns)
|
|
13
|
+
return "";
|
|
14
|
+
return ns.startsWith("/") ? ns : `/${ns}`;
|
|
15
|
+
}
|
|
16
|
+
function headersFromRows(rows) {
|
|
17
|
+
const ordered = [];
|
|
18
|
+
const seen = new Set;
|
|
19
|
+
for (const row of rows) {
|
|
20
|
+
for (const key of Object.keys(row)) {
|
|
21
|
+
if (seen.has(key))
|
|
22
|
+
continue;
|
|
23
|
+
seen.add(key);
|
|
24
|
+
ordered.push(key);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return ordered;
|
|
28
|
+
}
|
|
29
|
+
export async function parseImportExcelPreview(file) {
|
|
30
|
+
const buf = new Uint8Array(await file.arrayBuffer());
|
|
31
|
+
const rows = parseExcelBuffer(buf);
|
|
32
|
+
return { headers: headersFromRows(rows), rows };
|
|
33
|
+
}
|
|
34
|
+
export async function downloadImportTemplate(namespace, filename = "导入模板") {
|
|
35
|
+
const url = apiUrl(`${resolveNamespace(namespace)}/importTemplate`);
|
|
36
|
+
const res = await fetch(url, { headers: authHeaders() });
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
const text = await res.text();
|
|
39
|
+
throw new Error(text || `下载模板失败 (${res.status})`);
|
|
40
|
+
}
|
|
41
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
42
|
+
downloadExcelBuffer(buf, filename);
|
|
43
|
+
}
|
|
44
|
+
export async function uploadImportExcel(namespace, file) {
|
|
45
|
+
const url = apiUrl(`${resolveNamespace(namespace)}/import`);
|
|
46
|
+
const fd = new FormData;
|
|
47
|
+
fd.append("file", file);
|
|
48
|
+
const res = await fetch(url, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: authHeaders(),
|
|
51
|
+
body: fd
|
|
52
|
+
});
|
|
53
|
+
const raw = await res.text();
|
|
54
|
+
let json = null;
|
|
55
|
+
try {
|
|
56
|
+
json = raw ? JSON.parse(raw) : null;
|
|
57
|
+
} catch {}
|
|
58
|
+
if (json?.code === 1000 && json.data)
|
|
59
|
+
return json.data;
|
|
60
|
+
throw new Error(json?.message || raw || `导入失败 (${res.status})`);
|
|
61
|
+
}
|
package/dist/admin/lib/json.js
CHANGED
|
@@ -1 +1,42 @@
|
|
|
1
|
-
|
|
1
|
+
export function textOfJson(v) {
|
|
2
|
+
if (v == null)
|
|
3
|
+
return "";
|
|
4
|
+
if (typeof v === "string")
|
|
5
|
+
return v;
|
|
6
|
+
try {
|
|
7
|
+
return JSON.stringify(v);
|
|
8
|
+
} catch {
|
|
9
|
+
return String(v);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function prettyJson(raw, empty = "(空)") {
|
|
13
|
+
const s = textOfJson(raw).trim();
|
|
14
|
+
if (!s)
|
|
15
|
+
return empty;
|
|
16
|
+
try {
|
|
17
|
+
return JSON.stringify(JSON.parse(s), null, 2);
|
|
18
|
+
} catch {
|
|
19
|
+
if (typeof raw === "object" && raw !== null) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.stringify(raw, null, 2);
|
|
22
|
+
} catch {}
|
|
23
|
+
}
|
|
24
|
+
return s;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function hasJsonContent(raw) {
|
|
28
|
+
return Boolean(textOfJson(raw).trim());
|
|
29
|
+
}
|
|
30
|
+
export function parseJsonInput(raw) {
|
|
31
|
+
const s = raw.trim();
|
|
32
|
+
if (!s)
|
|
33
|
+
return { ok: true, value: null };
|
|
34
|
+
try {
|
|
35
|
+
return { ok: true, value: JSON.parse(s) };
|
|
36
|
+
} catch (e) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
error: e instanceof Error ? e.message : "JSON 格式错误"
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
package/dist/admin/lib/menu.js
CHANGED
|
@@ -1 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
export function filterShowMenus(nodes) {
|
|
2
|
+
if (!Array.isArray(nodes) || !nodes.length)
|
|
3
|
+
return [];
|
|
4
|
+
return nodes.filter((n) => n && n.isShow !== false).map((n) => ({
|
|
5
|
+
...n,
|
|
6
|
+
children: n.children?.length ? filterShowMenus(n.children) : undefined
|
|
7
|
+
})).filter((n) => {
|
|
8
|
+
const t = Number(n.type);
|
|
9
|
+
return t === 0 || t === 1 && n.router || (n.children?.length ?? 0) > 0;
|
|
10
|
+
});
|
|
11
|
+
}
|
package/dist/admin/lib/tree.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
export { deepTree, flattenTree } from "../../shared/tree";
|
package/dist/admin/lib/upload.js
CHANGED
|
@@ -1 +1,88 @@
|
|
|
1
|
-
|
|
1
|
+
export const DEFAULT_UPLOAD_RULES = [
|
|
2
|
+
{
|
|
3
|
+
name: "图片",
|
|
4
|
+
type: "image",
|
|
5
|
+
color: "#67C23A",
|
|
6
|
+
exts: ["bmp", "jpg", "jpeg", "png", "tif", "gif", "svg", "webp"]
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: "视频",
|
|
10
|
+
type: "video",
|
|
11
|
+
color: "#826aec",
|
|
12
|
+
exts: ["avi", "wmv", "mpg", "mpeg", "mov", "rm", "ram", "swf", "flv", "mp4", "3gp"]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "音频",
|
|
16
|
+
type: "audio",
|
|
17
|
+
color: "#826aec",
|
|
18
|
+
exts: ["mp3", "wav", "wma", "mp2", "flac", "midi", "ra", "ape", "aac", "cda"]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "文档",
|
|
22
|
+
type: "word",
|
|
23
|
+
color: "#53B7F4",
|
|
24
|
+
exts: ["doc", "docx", "docm", "dot", "dotx", "dotm"]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "表格",
|
|
28
|
+
type: "excel",
|
|
29
|
+
color: "#53D39C",
|
|
30
|
+
exts: ["xls", "xlsx", "xlsm", "xlt", "xltx", "xltm"]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "演示",
|
|
34
|
+
type: "ppt",
|
|
35
|
+
color: "#F56C6C",
|
|
36
|
+
exts: ["ppt", "pptx", "pptm", "ppsx", "ppsm", "pps", "potx", "potm", "dpt", "dps"]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "PDF",
|
|
40
|
+
type: "pdf",
|
|
41
|
+
color: "#8f3500",
|
|
42
|
+
exts: ["pdf"]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "压缩包",
|
|
46
|
+
type: "rar",
|
|
47
|
+
color: "#FFC757",
|
|
48
|
+
exts: ["rar", "zip", "7z", "tar", "gz"]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "文件",
|
|
52
|
+
type: "file",
|
|
53
|
+
color: "#909399",
|
|
54
|
+
exts: []
|
|
55
|
+
}
|
|
56
|
+
];
|
|
57
|
+
export function extname(path = "") {
|
|
58
|
+
const m = String(path).split("?")[0]?.match(/\.([^.]+)$/);
|
|
59
|
+
return m ? m[1].toLowerCase() : "";
|
|
60
|
+
}
|
|
61
|
+
export function filename(path = "") {
|
|
62
|
+
const base = String(path).split("?")[0]?.split("/").pop() || "";
|
|
63
|
+
const ext = extname(base);
|
|
64
|
+
return ext ? base.slice(0, -(ext.length + 1)) : base;
|
|
65
|
+
}
|
|
66
|
+
export function fileRule(path, rules = DEFAULT_UPLOAD_RULES) {
|
|
67
|
+
if (path?.includes("data:image/")) {
|
|
68
|
+
return rules.find((r) => r.type === "image");
|
|
69
|
+
}
|
|
70
|
+
const ext = extname(path || "");
|
|
71
|
+
const hit = rules.find((r) => r.exts.includes(ext));
|
|
72
|
+
return hit || rules.find((r) => r.type === "file");
|
|
73
|
+
}
|
|
74
|
+
export function getFileType(path, rules = DEFAULT_UPLOAD_RULES) {
|
|
75
|
+
return fileRule(path, rules).type;
|
|
76
|
+
}
|
|
77
|
+
export function getUploadRule(type, rules = DEFAULT_UPLOAD_RULES) {
|
|
78
|
+
return rules.find((r) => r.type === type) || rules[rules.length - 1];
|
|
79
|
+
}
|
|
80
|
+
export function getUrls(list) {
|
|
81
|
+
return list.map((e) => String(e.url || "").replace(/,/g, encodeURIComponent(",")));
|
|
82
|
+
}
|
|
83
|
+
export function pathJoin(...parts) {
|
|
84
|
+
return parts.map((p) => p.replace(/(^\/+|\/+$)/g, "")).filter(Boolean).join("/");
|
|
85
|
+
}
|
|
86
|
+
export function uploadUid() {
|
|
87
|
+
return `${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
88
|
+
}
|
|
@@ -1 +1,80 @@
|
|
|
1
|
-
|
|
1
|
+
import { onUnmounted, ref, watch } from "vue";
|
|
2
|
+
export const FRAME_CAPTURE_TIMEOUT_MS = 8000;
|
|
3
|
+
export function captureVideoFrame(url) {
|
|
4
|
+
if (typeof document === "undefined")
|
|
5
|
+
return Promise.resolve(null);
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
const video = document.createElement("video");
|
|
8
|
+
video.crossOrigin = "anonymous";
|
|
9
|
+
video.muted = true;
|
|
10
|
+
video.playsInline = true;
|
|
11
|
+
video.preload = "auto";
|
|
12
|
+
let settled = false;
|
|
13
|
+
const finish = (value) => {
|
|
14
|
+
if (settled)
|
|
15
|
+
return;
|
|
16
|
+
settled = true;
|
|
17
|
+
window.clearTimeout(timer);
|
|
18
|
+
video.removeAttribute("src");
|
|
19
|
+
video.load();
|
|
20
|
+
resolve(value);
|
|
21
|
+
};
|
|
22
|
+
const timer = window.setTimeout(() => finish(null), FRAME_CAPTURE_TIMEOUT_MS);
|
|
23
|
+
video.addEventListener("error", () => finish(null), { once: true });
|
|
24
|
+
video.addEventListener("loadeddata", () => {
|
|
25
|
+
try {
|
|
26
|
+
video.currentTime = 0.01;
|
|
27
|
+
} catch {
|
|
28
|
+
finish(null);
|
|
29
|
+
}
|
|
30
|
+
}, { once: true });
|
|
31
|
+
video.addEventListener("seeked", () => {
|
|
32
|
+
try {
|
|
33
|
+
const w = video.videoWidth;
|
|
34
|
+
const h = video.videoHeight;
|
|
35
|
+
if (!w || !h) {
|
|
36
|
+
finish(null);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const canvas = document.createElement("canvas");
|
|
40
|
+
canvas.width = w;
|
|
41
|
+
canvas.height = h;
|
|
42
|
+
const ctx = canvas.getContext("2d");
|
|
43
|
+
if (!ctx) {
|
|
44
|
+
finish(null);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
ctx.drawImage(video, 0, 0, w, h);
|
|
48
|
+
finish(canvas.toDataURL("image/jpeg", 0.82));
|
|
49
|
+
} catch {
|
|
50
|
+
finish(null);
|
|
51
|
+
}
|
|
52
|
+
}, { once: true });
|
|
53
|
+
video.src = url;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export function useVideoFrame(source) {
|
|
57
|
+
const videoFrameSrc = ref("");
|
|
58
|
+
const frameFailed = ref(false);
|
|
59
|
+
let captureToken = 0;
|
|
60
|
+
watch(source, (url) => {
|
|
61
|
+
captureToken += 1;
|
|
62
|
+
const token = captureToken;
|
|
63
|
+
videoFrameSrc.value = "";
|
|
64
|
+
frameFailed.value = false;
|
|
65
|
+
if (!url)
|
|
66
|
+
return;
|
|
67
|
+
captureVideoFrame(url).then((dataUrl) => {
|
|
68
|
+
if (token !== captureToken)
|
|
69
|
+
return;
|
|
70
|
+
if (dataUrl)
|
|
71
|
+
videoFrameSrc.value = dataUrl;
|
|
72
|
+
else
|
|
73
|
+
frameFailed.value = true;
|
|
74
|
+
});
|
|
75
|
+
}, { immediate: true });
|
|
76
|
+
onUnmounted(() => {
|
|
77
|
+
captureToken += 1;
|
|
78
|
+
});
|
|
79
|
+
return { videoFrameSrc, frameFailed };
|
|
80
|
+
}
|
package/dist/client/dict.d.ts
CHANGED
|
@@ -14,8 +14,15 @@ export type EpsDictNode = EpsDictRow & {
|
|
|
14
14
|
};
|
|
15
15
|
export type EpsDictMap = Record<string, EpsDictNode[]>;
|
|
16
16
|
export declare function getDictMap(): EpsDictMap;
|
|
17
|
+
/**
|
|
18
|
+
* 一级业务字典(无 children、不含 name===color)
|
|
19
|
+
* 用于表单下拉 / switch 等不需要色值的场景
|
|
20
|
+
*/
|
|
17
21
|
export declare function getDict(typeKey: string): EpsDictNode[];
|
|
18
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* 一级业务项平铺为 { label, value, color? }
|
|
24
|
+
* color 取自该根项下子节点 name===color;供彩色 tag / 菜单类型等
|
|
25
|
+
*/
|
|
19
26
|
export declare function getDictOptions(typeKey: string): {
|
|
20
27
|
label: string;
|
|
21
28
|
value: string | number;
|
package/dist/client/dict.js
CHANGED
|
@@ -3,14 +3,25 @@ function isEmpty(val) {
|
|
|
3
3
|
return val === "" || val === null || val === undefined;
|
|
4
4
|
}
|
|
5
5
|
const data = Object.create(null);
|
|
6
|
+
function isColorMeta(n) {
|
|
7
|
+
return String(n.name || "") === "color";
|
|
8
|
+
}
|
|
6
9
|
export function getDictMap() {
|
|
7
10
|
return data;
|
|
8
11
|
}
|
|
9
12
|
export function getDict(typeKey) {
|
|
10
|
-
|
|
13
|
+
const roots = data[typeKey] || [];
|
|
14
|
+
return roots.filter((n) => !isColorMeta(n)).map((n) => {
|
|
15
|
+
const { children: _c, ...rest } = n;
|
|
16
|
+
return {
|
|
17
|
+
...rest,
|
|
18
|
+
label: n.label || n.name
|
|
19
|
+
};
|
|
20
|
+
});
|
|
11
21
|
}
|
|
12
22
|
export function getDictOptions(typeKey) {
|
|
13
|
-
|
|
23
|
+
const roots = data[typeKey] || [];
|
|
24
|
+
return roots.filter((n) => !isColorMeta(n)).map((n) => {
|
|
14
25
|
const colorChild = n.children?.find((c) => c.name === "color");
|
|
15
26
|
return {
|
|
16
27
|
label: n.label || n.name,
|