完善模型库权限与工艺数据解析
This commit is contained in:
@@ -5,9 +5,10 @@ DB_PATH=
|
|||||||
|
|
||||||
# login: show login page
|
# login: show login page
|
||||||
# fixed: auto login with FIXED_LOGIN_USERNAME and open model library directly
|
# fixed: auto login with FIXED_LOGIN_USERNAME and open model library directly
|
||||||
APP_AUTH_MODE=login
|
APP_AUTH_MODE=fixed
|
||||||
FIXED_LOGIN_USERNAME=viewer
|
FIXED_LOGIN_USERNAME=viewer
|
||||||
FIXED_LOGIN_ROLE=user
|
FIXED_LOGIN_ROLE=user
|
||||||
|
ALLOW_USER_EDIT_SYSTEM_LIBRARY=false
|
||||||
|
|
||||||
# local: write to server/storage and serve via /storage
|
# local: write to server/storage and serve via /storage
|
||||||
# cos: upload models to Tencent Cloud COS and read through CDN_BASE_URL
|
# cos: upload models to Tencent Cloud COS and read through CDN_BASE_URL
|
||||||
|
|||||||
@@ -13,26 +13,32 @@ function fixedLoginRole(): UserRole {
|
|||||||
return process.env.FIXED_LOGIN_ROLE === "admin" ? "admin" : "user";
|
return process.env.FIXED_LOGIN_ROLE === "admin" ? "admin" : "user";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function envValue(name: string) {
|
||||||
|
const value = process.env[name]?.trim();
|
||||||
|
return value || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
host: process.env.HOST ?? "0.0.0.0",
|
host: envValue("HOST") ?? "0.0.0.0",
|
||||||
port: Number(process.env.PORT ?? 3001),
|
port: Number(envValue("PORT") ?? 3001),
|
||||||
jwtSecret: process.env.JWT_SECRET ?? "dev-only-change-me",
|
jwtSecret: envValue("JWT_SECRET") ?? "dev-only-change-me",
|
||||||
authMode: process.env.APP_AUTH_MODE === "fixed" ? "fixed" : "login",
|
authMode: process.env.APP_AUTH_MODE === "fixed" ? "fixed" : "login",
|
||||||
fixedLogin: {
|
fixedLogin: {
|
||||||
username: process.env.FIXED_LOGIN_USERNAME ?? "viewer",
|
username: envValue("FIXED_LOGIN_USERNAME") ?? "viewer",
|
||||||
role: fixedLoginRole()
|
role: fixedLoginRole()
|
||||||
},
|
},
|
||||||
|
allowUserEditSystemLibrary: envValue("ALLOW_USER_EDIT_SYSTEM_LIBRARY") === "true",
|
||||||
dataDir: path.resolve(serverRoot, "data"),
|
dataDir: path.resolve(serverRoot, "data"),
|
||||||
storageDir: path.resolve(serverRoot, "storage"),
|
storageDir: path.resolve(serverRoot, "storage"),
|
||||||
databasePath: process.env.DB_PATH ?? path.resolve(serverRoot, "data", "model-library.db"),
|
databasePath: envValue("DB_PATH") ?? path.resolve(serverRoot, "data", "model-library.db"),
|
||||||
storageDriver: (process.env.STORAGE_DRIVER ?? "local").toLowerCase(),
|
storageDriver: (envValue("STORAGE_DRIVER") ?? "local").toLowerCase(),
|
||||||
publicBaseUrl: process.env.PUBLIC_BASE_URL ?? "",
|
publicBaseUrl: envValue("PUBLIC_BASE_URL") ?? "",
|
||||||
cdnBaseUrl: process.env.CDN_BASE_URL ?? "",
|
cdnBaseUrl: envValue("CDN_BASE_URL") ?? "",
|
||||||
cos: {
|
cos: {
|
||||||
secretId: process.env.COS_SECRET_ID ?? "",
|
secretId: envValue("COS_SECRET_ID") ?? "",
|
||||||
secretKey: process.env.COS_SECRET_KEY ?? "",
|
secretKey: envValue("COS_SECRET_KEY") ?? "",
|
||||||
bucket: process.env.COS_BUCKET ?? "",
|
bucket: envValue("COS_BUCKET") ?? "",
|
||||||
region: process.env.COS_REGION ?? "",
|
region: envValue("COS_REGION") ?? "",
|
||||||
prefix: (process.env.COS_PREFIX ?? "").replace(/^\/+|\/+$/g, "")
|
prefix: (envValue("COS_PREFIX") ?? "").replace(/^\/+|\/+$/g, "")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,11 +10,6 @@ export const db = new DatabaseSync(config.databasePath);
|
|||||||
db.exec("PRAGMA journal_mode = WAL");
|
db.exec("PRAGMA journal_mode = WAL");
|
||||||
db.exec("PRAGMA foreign_keys = ON");
|
db.exec("PRAGMA foreign_keys = ON");
|
||||||
|
|
||||||
function hasColumn(table: string, column: string) {
|
|
||||||
const rows = db.prepare(`PRAGMA table_info(${table})`).all() as unknown as { name: string }[];
|
|
||||||
return rows.some((row) => row.name === column);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function migrate() {
|
export function migrate() {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
@@ -33,6 +28,9 @@ export function migrate() {
|
|||||||
parent_id INTEGER REFERENCES folders(id) ON DELETE CASCADE,
|
parent_id INTEGER REFERENCES folders(id) ON DELETE CASCADE,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
path TEXT NOT NULL UNIQUE,
|
path TEXT NOT NULL UNIQUE,
|
||||||
|
library_type TEXT NOT NULL DEFAULT 'system',
|
||||||
|
owner_user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
is_system INTEGER NOT NULL DEFAULT 0,
|
||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE(parent_id, name)
|
UNIQUE(parent_id, name)
|
||||||
@@ -60,11 +58,13 @@ export function migrate() {
|
|||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
original_filename TEXT NOT NULL,
|
original_filename TEXT NOT NULL,
|
||||||
file_path TEXT NOT NULL UNIQUE,
|
file_path TEXT NOT NULL UNIQUE,
|
||||||
|
library_type TEXT NOT NULL DEFAULT 'system',
|
||||||
|
owner_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||||
storage_provider TEXT NOT NULL DEFAULT 'local',
|
storage_provider TEXT NOT NULL DEFAULT 'local',
|
||||||
file_size INTEGER NOT NULL,
|
file_size INTEGER NOT NULL,
|
||||||
thumbnail TEXT,
|
|
||||||
thumbnail_path TEXT,
|
thumbnail_path TEXT,
|
||||||
thumbnail_provider TEXT,
|
thumbnail_provider TEXT,
|
||||||
|
operation_tree TEXT NOT NULL DEFAULT '{"OperationTree":"[]"}',
|
||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE(folder_id, name)
|
UNIQUE(folder_id, name)
|
||||||
@@ -83,32 +83,13 @@ export function migrate() {
|
|||||||
CREATE INDEX IF NOT EXISTS idx_model_properties_model_id ON model_properties(model_id);
|
CREATE INDEX IF NOT EXISTS idx_model_properties_model_id ON model_properties(model_id);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
if (!hasColumn("models", "brand_id")) {
|
|
||||||
db.exec("ALTER TABLE models ADD COLUMN brand_id INTEGER REFERENCES brands(id) ON DELETE SET NULL");
|
|
||||||
}
|
|
||||||
if (!hasColumn("models", "type_id")) {
|
|
||||||
db.exec("ALTER TABLE models ADD COLUMN type_id INTEGER REFERENCES model_types(id) ON DELETE SET NULL");
|
|
||||||
}
|
|
||||||
if (!hasColumn("models", "storage_provider")) {
|
|
||||||
db.exec("ALTER TABLE models ADD COLUMN storage_provider TEXT NOT NULL DEFAULT 'local'");
|
|
||||||
}
|
|
||||||
if (!hasColumn("models", "thumbnail_path")) {
|
|
||||||
db.exec("ALTER TABLE models ADD COLUMN thumbnail_path TEXT");
|
|
||||||
}
|
|
||||||
if (!hasColumn("models", "thumbnail_provider")) {
|
|
||||||
db.exec("ALTER TABLE models ADD COLUMN thumbnail_provider TEXT");
|
|
||||||
}
|
|
||||||
|
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE INDEX IF NOT EXISTS idx_models_brand_id ON models(brand_id);
|
CREATE INDEX IF NOT EXISTS idx_models_brand_id ON models(brand_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_models_type_id ON models(type_id);
|
CREATE INDEX IF NOT EXISTS idx_models_type_id ON models(type_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_folders_library_owner ON folders(library_type, owner_user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_models_library_owner ON models(library_type, owner_user_id);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const rootFolder = db.prepare("SELECT id FROM folders WHERE parent_id IS NULL AND name = ?").get("模型库");
|
|
||||||
if (!rootFolder) {
|
|
||||||
db.prepare("INSERT INTO folders (parent_id, name, path) VALUES (NULL, ?, ?)").run("模型库", "模型库");
|
|
||||||
}
|
|
||||||
|
|
||||||
const admin = db.prepare("SELECT id FROM users WHERE username = ?").get("admin");
|
const admin = db.prepare("SELECT id FROM users WHERE username = ?").get("admin");
|
||||||
if (!admin) {
|
if (!admin) {
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
@@ -116,4 +97,12 @@ export function migrate() {
|
|||||||
VALUES (?, ?, 'admin', 1)
|
VALUES (?, ?, 'admin', 1)
|
||||||
`).run("admin", bcrypt.hashSync("admin123", 10));
|
`).run("admin", bcrypt.hashSync("admin123", 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const systemRoot = db.prepare("SELECT id FROM folders WHERE path = ?").get("system") as { id: number } | undefined;
|
||||||
|
if (!systemRoot) {
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO folders (parent_id, name, path, library_type, owner_user_id, is_system)
|
||||||
|
VALUES (NULL, ?, ?, 'system', NULL, 1)
|
||||||
|
`).run("系统模型库", "system");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import path from "node:path";
|
|||||||
import { FastifyInstance } from "fastify";
|
import { FastifyInstance } from "fastify";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "./db.js";
|
import { db } from "./db.js";
|
||||||
import { requireAdmin, requireAuth } from "./auth.js";
|
import { requireAuth } from "./auth.js";
|
||||||
|
import { folderPermissions, getFolder, requireFolderWrite } from "./permissions.js";
|
||||||
import { storage } from "./storage.js";
|
import { storage } from "./storage.js";
|
||||||
import { FolderRow } from "./types.js";
|
import { FolderRow } from "./types.js";
|
||||||
import { assertValidFolderName, ensureDir, removeIfExists, safeStoragePath, toFolderStorageRelative } from "./utils.js";
|
import { assertValidFolderName, ensureDir, removeIfExists, safeStoragePath, toFolderStorageRelative } from "./utils.js";
|
||||||
@@ -17,65 +18,125 @@ const updateSchema = z.object({
|
|||||||
name: z.string()
|
name: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
function buildTree(rows: FolderRow[]) {
|
function folderPayload(request: { userInfo: { id: number; username: string; role: "admin" | "user" } }, row: FolderRow) {
|
||||||
const nodes = rows.map((row) => ({
|
return {
|
||||||
|
...row,
|
||||||
|
permissions: folderPermissions(request.userInfo, row)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFolderNodes(rows: FolderRow[], userInfo: { id: number; username: string; role: "admin" | "user" }) {
|
||||||
|
return rows.map((row) => ({
|
||||||
id: String(row.id),
|
id: String(row.id),
|
||||||
text: `${row.name}(${row.model_count ?? 0})`,
|
text: `${displayFolderName(row)}(${row.model_count ?? 0})`,
|
||||||
parentId: row.parent_id,
|
parentId: row.parent_id,
|
||||||
path: row.path,
|
path: row.path,
|
||||||
|
data: {
|
||||||
|
permissions: folderPermissions(userInfo, row),
|
||||||
|
libraryType: row.library_type,
|
||||||
|
ownerUserId: row.owner_user_id,
|
||||||
|
isSystem: Boolean(row.is_system)
|
||||||
|
},
|
||||||
children: [] as unknown[]
|
children: [] as unknown[]
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTree(rows: FolderRow[], userInfo: { id: number; username: string; role: "admin" | "user" }) {
|
||||||
|
const nodes = buildFolderNodes(rows, userInfo);
|
||||||
|
const userGroupNode = {
|
||||||
|
id: "virtual:user-libraries",
|
||||||
|
text: "用户模型库",
|
||||||
|
parentId: null as number | null,
|
||||||
|
path: "users",
|
||||||
|
data: {
|
||||||
|
permissions: { read: true, write: false },
|
||||||
|
virtual: true
|
||||||
|
},
|
||||||
|
children: [] as unknown[]
|
||||||
|
};
|
||||||
const byId = new Map(nodes.map((node) => [Number(node.id), node]));
|
const byId = new Map(nodes.map((node) => [Number(node.id), node]));
|
||||||
const roots = [];
|
const roots = [];
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
if (node.parentId && byId.has(node.parentId)) {
|
if (node.parentId && byId.has(node.parentId)) {
|
||||||
byId.get(node.parentId)!.children.push(node);
|
byId.get(node.parentId)!.children.push(node);
|
||||||
|
} else if (node.data.libraryType === "user") {
|
||||||
|
userGroupNode.children.push(node);
|
||||||
} else {
|
} else {
|
||||||
roots.push(node);
|
roots.push(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (userGroupNode.children.length > 0) {
|
||||||
|
roots.push(userGroupNode);
|
||||||
|
}
|
||||||
return roots;
|
return roots;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFolder(id: number) {
|
|
||||||
return db.prepare("SELECT * FROM folders WHERE id = ?").get(id) as FolderRow | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function childPath(parent: FolderRow | undefined, name: string) {
|
function childPath(parent: FolderRow | undefined, name: string) {
|
||||||
return parent ? path.posix.join(parent.path, name) : name;
|
return parent ? path.posix.join(parent.path, name) : name;
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertCanRenameRoot(folder: FolderRow) {
|
function assertCanRenameRoot(folder: FolderRow) {
|
||||||
if (folder.parent_id === null) {
|
if (folder.is_system) {
|
||||||
throw new Error("根目录不能重命名");
|
throw new Error("系统保留目录不能重命名");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function displayFolderName(row: FolderRow) {
|
||||||
|
if (row.library_type === "user" && row.is_system && row.owner_username) {
|
||||||
|
return row.owner_username;
|
||||||
|
}
|
||||||
|
return row.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureUserLibrary(user: { id: number; username: string }) {
|
||||||
|
const pathValue = `users/${user.id}`;
|
||||||
|
const existing = db.prepare("SELECT id FROM folders WHERE path = ?").get(pathValue) as { id: number } | undefined;
|
||||||
|
if (existing) return existing.id;
|
||||||
|
const result = db.prepare(`
|
||||||
|
INSERT INTO folders (parent_id, name, path, library_type, owner_user_id, is_system)
|
||||||
|
VALUES (NULL, ?, ?, 'user', ?, 1)
|
||||||
|
`).run("用户模型库", pathValue, user.id);
|
||||||
|
return Number(result.lastInsertRowid);
|
||||||
|
}
|
||||||
|
|
||||||
export async function folderRoutes(app: FastifyInstance) {
|
export async function folderRoutes(app: FastifyInstance) {
|
||||||
app.get("/api/folders", { preHandler: [requireAuth] }, async () => {
|
app.get("/api/folders", { preHandler: [requireAuth] }, async (request) => {
|
||||||
|
ensureUserLibrary(request.userInfo);
|
||||||
const rows = db.prepare(`
|
const rows = db.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
f.*,
|
f.*,
|
||||||
|
u.username AS owner_username,
|
||||||
COUNT(m.id) AS model_count
|
COUNT(m.id) AS model_count
|
||||||
FROM folders f
|
FROM folders f
|
||||||
|
LEFT JOIN users u ON u.id = f.owner_user_id
|
||||||
LEFT JOIN models m ON m.folder_id = f.id
|
LEFT JOIN models m ON m.folder_id = f.id
|
||||||
GROUP BY f.id
|
GROUP BY f.id
|
||||||
ORDER BY f.parent_id, f.name
|
ORDER BY
|
||||||
|
CASE f.library_type WHEN 'system' THEN 0 ELSE 1 END,
|
||||||
|
f.owner_user_id,
|
||||||
|
f.parent_id,
|
||||||
|
f.name
|
||||||
`).all() as unknown as FolderRow[];
|
`).all() as unknown as FolderRow[];
|
||||||
return { folders: rows, tree: buildTree(rows) };
|
return { folders: rows.map((row) => folderPayload(request, row)), tree: buildTree(rows, request.userInfo) };
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/api/folders", { preHandler: [requireAdmin] }, async (request, reply) => {
|
app.post("/api/folders", { preHandler: [requireAuth] }, async (request, reply) => {
|
||||||
const body = createSchema.parse(request.body);
|
const body = createSchema.parse(request.body);
|
||||||
const name = assertValidFolderName(body.name);
|
const name = assertValidFolderName(body.name);
|
||||||
const parent = body.parentId ? getFolder(body.parentId) : undefined;
|
const parent = body.parentId ? getFolder(body.parentId) : undefined;
|
||||||
if (body.parentId && !parent) {
|
if (body.parentId && !parent) {
|
||||||
return reply.code(404).send({ message: "父目录不存在" });
|
return reply.code(404).send({ message: "父目录不存在" });
|
||||||
}
|
}
|
||||||
|
if (!parent) {
|
||||||
|
return reply.code(400).send({ message: "请选择父目录" });
|
||||||
|
}
|
||||||
|
if (!requireFolderWrite(request, reply, parent)) return;
|
||||||
const folderPath = childPath(parent, name);
|
const folderPath = childPath(parent, name);
|
||||||
try {
|
try {
|
||||||
const result = db.prepare("INSERT INTO folders (parent_id, name, path) VALUES (?, ?, ?)")
|
const result = db.prepare(`
|
||||||
.run(body.parentId ?? null, name, folderPath);
|
INSERT INTO folders (parent_id, name, path, library_type, owner_user_id, is_system)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 0)
|
||||||
|
`).run(parent.id, name, folderPath, parent.library_type, parent.owner_user_id);
|
||||||
await ensureDir(safeStoragePath(toFolderStorageRelative(folderPath)));
|
await ensureDir(safeStoragePath(toFolderStorageRelative(folderPath)));
|
||||||
return reply.code(201).send({ id: result.lastInsertRowid, name, path: folderPath });
|
return reply.code(201).send({ id: result.lastInsertRowid, name, path: folderPath });
|
||||||
} catch {
|
} catch {
|
||||||
@@ -83,7 +144,7 @@ export async function folderRoutes(app: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.put("/api/folders/:id", { preHandler: [requireAdmin] }, async (request, reply) => {
|
app.put("/api/folders/:id", { preHandler: [requireAuth] }, async (request, reply) => {
|
||||||
const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
|
const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
|
||||||
const body = updateSchema.parse(request.body);
|
const body = updateSchema.parse(request.body);
|
||||||
const name = assertValidFolderName(body.name);
|
const name = assertValidFolderName(body.name);
|
||||||
@@ -91,6 +152,7 @@ export async function folderRoutes(app: FastifyInstance) {
|
|||||||
if (!folder) {
|
if (!folder) {
|
||||||
return reply.code(404).send({ message: "目录不存在" });
|
return reply.code(404).send({ message: "目录不存在" });
|
||||||
}
|
}
|
||||||
|
if (!requireFolderWrite(request, reply, folder)) return;
|
||||||
assertCanRenameRoot(folder);
|
assertCanRenameRoot(folder);
|
||||||
const parent = folder.parent_id ? getFolder(folder.parent_id) : undefined;
|
const parent = folder.parent_id ? getFolder(folder.parent_id) : undefined;
|
||||||
const oldPath = folder.path;
|
const oldPath = folder.path;
|
||||||
@@ -146,14 +208,15 @@ export async function folderRoutes(app: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.delete("/api/folders/:id", { preHandler: [requireAdmin] }, async (request, reply) => {
|
app.delete("/api/folders/:id", { preHandler: [requireAuth] }, async (request, reply) => {
|
||||||
const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
|
const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
|
||||||
const folder = getFolder(params.id);
|
const folder = getFolder(params.id);
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
return reply.code(404).send({ message: "目录不存在" });
|
return reply.code(404).send({ message: "目录不存在" });
|
||||||
}
|
}
|
||||||
if (folder.parent_id === null) {
|
if (!requireFolderWrite(request, reply, folder)) return;
|
||||||
return reply.code(400).send({ message: "根目录不能删除" });
|
if (folder.is_system) {
|
||||||
|
return reply.code(400).send({ message: "系统保留目录不能删除" });
|
||||||
}
|
}
|
||||||
db.prepare("DELETE FROM folders WHERE id = ?").run(folder.id);
|
db.prepare("DELETE FROM folders WHERE id = ?").run(folder.id);
|
||||||
await removeIfExists(safeStoragePath(toFolderStorageRelative(folder.path)));
|
await removeIfExists(safeStoragePath(toFolderStorageRelative(folder.path)));
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { db } from "./db.js";
|
import { db } from "./db.js";
|
||||||
import { requireAdmin, requireAuth } from "./auth.js";
|
import { requireAdmin, requireAuth } from "./auth.js";
|
||||||
|
import { getFolder, modelPermissions, requireFolderWrite, requireModelWrite } from "./permissions.js";
|
||||||
import { deleteStoredObject, publicObjectUrl, storage } from "./storage.js";
|
import { deleteStoredObject, publicObjectUrl, storage } from "./storage.js";
|
||||||
import { DictionaryRow, FolderRow, ModelRow } from "./types.js";
|
import { DictionaryRow, FolderRow, ModelRow } from "./types.js";
|
||||||
import {
|
import {
|
||||||
@@ -16,7 +17,7 @@ const updateSchema = z.object({
|
|||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
brandName: z.string().optional(),
|
brandName: z.string().optional(),
|
||||||
typeName: z.string().optional(),
|
typeName: z.string().optional(),
|
||||||
thumbnail: z.string().nullable().optional(),
|
operationTree: z.string().optional(),
|
||||||
properties: z.record(z.string()).optional()
|
properties: z.record(z.string()).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -32,9 +33,7 @@ const dictionaryBodySchema = z.object({
|
|||||||
name: z.string().min(1).max(80)
|
name: z.string().min(1).max(80)
|
||||||
});
|
});
|
||||||
|
|
||||||
function getFolder(id: number) {
|
const defaultOperationTree = JSON.stringify({ OperationTree: "[]" });
|
||||||
return db.prepare("SELECT * FROM folders WHERE id = ?").get(id) as FolderRow | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProperties(modelId: number) {
|
function getProperties(modelId: number) {
|
||||||
const rows = db.prepare("SELECT property_key, property_value FROM model_properties WHERE model_id = ?")
|
const rows = db.prepare("SELECT property_key, property_value FROM model_properties WHERE model_id = ?")
|
||||||
@@ -56,13 +55,14 @@ function saveProperties(modelId: number, properties: Record<string, string>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function modelPayload(row: ModelRow) {
|
function modelPayload(row: ModelRow, userInfo?: { id: number; username: string; role: "admin" | "user" }) {
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
file_url: publicObjectUrl(row.file_path, row.storage_provider),
|
file_url: publicObjectUrl(row.file_path, row.storage_provider),
|
||||||
thumbnail_url: row.thumbnail_path && row.thumbnail_provider
|
thumbnail_url: row.thumbnail_path && row.thumbnail_provider
|
||||||
? publicObjectUrl(row.thumbnail_path, row.thumbnail_provider)
|
? publicObjectUrl(row.thumbnail_path, row.thumbnail_provider)
|
||||||
: row.thumbnail,
|
: null,
|
||||||
|
permissions: userInfo ? modelPermissions(userInfo, row) : { read: true, write: false },
|
||||||
properties: getProperties(row.id)
|
properties: getProperties(row.id)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -78,6 +78,26 @@ function dataImageToBuffer(dataUrl: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeOperationTree(value: string | undefined) {
|
||||||
|
const clean = value?.trim();
|
||||||
|
if (!clean) return defaultOperationTree;
|
||||||
|
let parsed: unknown;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(clean);
|
||||||
|
} catch {
|
||||||
|
throw new Error("工艺数据必须是合法 JSON 字符串");
|
||||||
|
}
|
||||||
|
if (!parsed || typeof parsed !== "object" || typeof (parsed as { OperationTree?: unknown }).OperationTree !== "string") {
|
||||||
|
throw new Error("工艺数据格式必须符合 OperationTreeDB");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JSON.parse((parsed as { OperationTree: string }).OperationTree);
|
||||||
|
} catch {
|
||||||
|
throw new Error("OperationTree 字段必须是合法 JSON 字符串");
|
||||||
|
}
|
||||||
|
return JSON.stringify(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeDictionaryName(name: string | undefined) {
|
function normalizeDictionaryName(name: string | undefined) {
|
||||||
const clean = name?.trim();
|
const clean = name?.trim();
|
||||||
return clean || null;
|
return clean || null;
|
||||||
@@ -205,19 +225,19 @@ export async function modelRoutes(app: FastifyInstance) {
|
|||||||
total: total.count,
|
total: total.count,
|
||||||
page: query.page,
|
page: query.page,
|
||||||
pageSize: query.pageSize,
|
pageSize: query.pageSize,
|
||||||
items: rows.map(modelPayload)
|
items: rows.map((row) => modelPayload(row, request.userInfo))
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/api/models/upload", { preHandler: [requireAdmin] }, async (request, reply) => {
|
app.post("/api/models/upload", { preHandler: [requireAuth] }, async (request, reply) => {
|
||||||
const parts = request.parts();
|
const parts = request.parts();
|
||||||
let folderId: number | undefined;
|
let folderId: number | undefined;
|
||||||
let thumbnail: string | null = null;
|
|
||||||
const properties: Record<string, string> = {};
|
const properties: Record<string, string> = {};
|
||||||
let uploaded: { filename: string; buffer: Buffer } | undefined;
|
let uploaded: { filename: string; buffer: Buffer } | undefined;
|
||||||
let modelName: string | undefined;
|
let modelName: string | undefined;
|
||||||
let brandName: string | undefined;
|
let brandName: string | undefined;
|
||||||
let typeName: string | undefined;
|
let typeName: string | undefined;
|
||||||
|
let operationTree: string | undefined;
|
||||||
|
|
||||||
for await (const part of parts) {
|
for await (const part of parts) {
|
||||||
if (part.type === "file") {
|
if (part.type === "file") {
|
||||||
@@ -234,7 +254,7 @@ export async function modelRoutes(app: FastifyInstance) {
|
|||||||
if (part.fieldname === "name") modelName = String(part.value ?? "");
|
if (part.fieldname === "name") modelName = String(part.value ?? "");
|
||||||
if (part.fieldname === "brandName") brandName = String(part.value ?? "");
|
if (part.fieldname === "brandName") brandName = String(part.value ?? "");
|
||||||
if (part.fieldname === "typeName") typeName = String(part.value ?? "");
|
if (part.fieldname === "typeName") typeName = String(part.value ?? "");
|
||||||
if (part.fieldname === "thumbnail") thumbnail = String(part.value || "") || null;
|
if (part.fieldname === "operationTree") operationTree = String(part.value ?? "");
|
||||||
if (part.fieldname.startsWith("prop.")) {
|
if (part.fieldname.startsWith("prop.")) {
|
||||||
properties[part.fieldname.slice(5)] = String(part.value ?? "");
|
properties[part.fieldname.slice(5)] = String(part.value ?? "");
|
||||||
}
|
}
|
||||||
@@ -248,6 +268,7 @@ export async function modelRoutes(app: FastifyInstance) {
|
|||||||
if (!folder) {
|
if (!folder) {
|
||||||
return reply.code(404).send({ message: "目录不存在" });
|
return reply.code(404).send({ message: "目录不存在" });
|
||||||
}
|
}
|
||||||
|
if (!requireFolderWrite(request, reply, folder)) return;
|
||||||
|
|
||||||
const originalModelName = assertValidFolderName(modelName || normalizeModelName(uploaded.filename));
|
const originalModelName = assertValidFolderName(modelName || normalizeModelName(uploaded.filename));
|
||||||
const storageFilename = `${normalizeModelName(uploaded.filename)}-${nanoid(8)}.glb`;
|
const storageFilename = `${normalizeModelName(uploaded.filename)}-${nanoid(8)}.glb`;
|
||||||
@@ -262,10 +283,11 @@ export async function modelRoutes(app: FastifyInstance) {
|
|||||||
try {
|
try {
|
||||||
const brandId = findOrCreateDictionary("brands", brandName);
|
const brandId = findOrCreateDictionary("brands", brandName);
|
||||||
const typeId = findOrCreateDictionary("model_types", typeName);
|
const typeId = findOrCreateDictionary("model_types", typeName);
|
||||||
|
const normalizedOperationTree = normalizeOperationTree(operationTree);
|
||||||
const result = db.prepare(`
|
const result = db.prepare(`
|
||||||
INSERT INTO models (folder_id, brand_id, type_id, name, original_filename, file_path, storage_provider, file_size, thumbnail)
|
INSERT INTO models (folder_id, brand_id, type_id, name, original_filename, file_path, library_type, owner_user_id, storage_provider, file_size, operation_tree)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`).run(folderId, brandId, typeId, originalModelName, uploaded.filename, stored.key, stored.provider, uploaded.buffer.length, thumbnail);
|
`).run(folderId, brandId, typeId, originalModelName, uploaded.filename, stored.key, folder.library_type, folder.owner_user_id, stored.provider, uploaded.buffer.length, normalizedOperationTree);
|
||||||
saveProperties(Number(result.lastInsertRowid), properties);
|
saveProperties(Number(result.lastInsertRowid), properties);
|
||||||
return reply.code(201).send({ id: result.lastInsertRowid });
|
return reply.code(201).send({ id: result.lastInsertRowid });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -274,22 +296,24 @@ export async function modelRoutes(app: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.put("/api/models/:id", { preHandler: [requireAdmin] }, async (request, reply) => {
|
app.put("/api/models/:id", { preHandler: [requireAuth] }, async (request, reply) => {
|
||||||
const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
|
const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
|
||||||
const body = updateSchema.parse(request.body);
|
const body = updateSchema.parse(request.body);
|
||||||
const model = db.prepare("SELECT * FROM models WHERE id = ?").get(params.id) as ModelRow | undefined;
|
const model = db.prepare("SELECT * FROM models WHERE id = ?").get(params.id) as ModelRow | undefined;
|
||||||
if (!model) {
|
if (!model) {
|
||||||
return reply.code(404).send({ message: "模型不存在" });
|
return reply.code(404).send({ message: "模型不存在" });
|
||||||
}
|
}
|
||||||
|
if (!requireModelWrite(request, reply, model)) return;
|
||||||
const name = assertValidFolderName(body.name);
|
const name = assertValidFolderName(body.name);
|
||||||
try {
|
try {
|
||||||
const brandId = findOrCreateDictionary("brands", body.brandName);
|
const brandId = findOrCreateDictionary("brands", body.brandName);
|
||||||
const typeId = findOrCreateDictionary("model_types", body.typeName);
|
const typeId = findOrCreateDictionary("model_types", body.typeName);
|
||||||
|
const operationTree = body.operationTree === undefined ? model.operation_tree : normalizeOperationTree(body.operationTree);
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
UPDATE models
|
UPDATE models
|
||||||
SET name = ?, brand_id = ?, type_id = ?, thumbnail = ?, updated_at = CURRENT_TIMESTAMP
|
SET name = ?, brand_id = ?, type_id = ?, operation_tree = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`).run(name, brandId, typeId, body.thumbnail ?? model.thumbnail, model.id);
|
`).run(name, brandId, typeId, operationTree, model.id);
|
||||||
if (body.properties) {
|
if (body.properties) {
|
||||||
saveProperties(model.id, body.properties);
|
saveProperties(model.id, body.properties);
|
||||||
}
|
}
|
||||||
@@ -299,19 +323,20 @@ export async function modelRoutes(app: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.put("/api/models/:id/thumbnail", { preHandler: [requireAdmin] }, async (request, reply) => {
|
app.put("/api/models/:id/thumbnail", { preHandler: [requireAuth] }, async (request, reply) => {
|
||||||
const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
|
const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
|
||||||
const body = thumbnailSchema.parse(request.body);
|
const body = thumbnailSchema.parse(request.body);
|
||||||
const model = db.prepare("SELECT * FROM models WHERE id = ?").get(params.id) as ModelRow | undefined;
|
const model = db.prepare("SELECT * FROM models WHERE id = ?").get(params.id) as ModelRow | undefined;
|
||||||
if (!model) {
|
if (!model) {
|
||||||
return reply.code(404).send({ message: "模型不存在" });
|
return reply.code(404).send({ message: "模型不存在" });
|
||||||
}
|
}
|
||||||
|
if (!requireModelWrite(request, reply, model)) return;
|
||||||
const image = dataImageToBuffer(body.thumbnail);
|
const image = dataImageToBuffer(body.thumbnail);
|
||||||
const stored = await storage.putObject(`thumbnails/model-${model.id}-${nanoid(8)}.${image.ext}`, image.buffer);
|
const stored = await storage.putObject(`thumbnails/model-${model.id}-${nanoid(8)}.${image.ext}`, image.buffer);
|
||||||
try {
|
try {
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
UPDATE models
|
UPDATE models
|
||||||
SET thumbnail = NULL, thumbnail_path = ?, thumbnail_provider = ?, updated_at = CURRENT_TIMESTAMP
|
SET thumbnail_path = ?, thumbnail_provider = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`).run(stored.key, stored.provider, model.id);
|
`).run(stored.key, stored.provider, model.id);
|
||||||
if (model.thumbnail_path && model.thumbnail_provider) {
|
if (model.thumbnail_path && model.thumbnail_provider) {
|
||||||
@@ -324,12 +349,13 @@ export async function modelRoutes(app: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.delete("/api/models/:id", { preHandler: [requireAdmin] }, async (request, reply) => {
|
app.delete("/api/models/:id", { preHandler: [requireAuth] }, async (request, reply) => {
|
||||||
const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
|
const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
|
||||||
const model = db.prepare("SELECT * FROM models WHERE id = ?").get(params.id) as ModelRow | undefined;
|
const model = db.prepare("SELECT * FROM models WHERE id = ?").get(params.id) as ModelRow | undefined;
|
||||||
if (!model) {
|
if (!model) {
|
||||||
return reply.code(404).send({ message: "模型不存在" });
|
return reply.code(404).send({ message: "模型不存在" });
|
||||||
}
|
}
|
||||||
|
if (!requireModelWrite(request, reply, model)) return;
|
||||||
db.prepare("DELETE FROM models WHERE id = ?").run(model.id);
|
db.prepare("DELETE FROM models WHERE id = ?").run(model.id);
|
||||||
await deleteStoredObject(model.file_path, model.storage_provider);
|
await deleteStoredObject(model.file_path, model.storage_provider);
|
||||||
if (model.thumbnail_path && model.thumbnail_provider) {
|
if (model.thumbnail_path && model.thumbnail_provider) {
|
||||||
|
|||||||
55
server/src/permissions.ts
Normal file
55
server/src/permissions.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { FastifyReply, FastifyRequest } from "fastify";
|
||||||
|
import { config } from "./config.js";
|
||||||
|
import { db } from "./db.js";
|
||||||
|
import { FolderRow, ModelRow } from "./types.js";
|
||||||
|
|
||||||
|
export type PermissionPayload = {
|
||||||
|
read: boolean;
|
||||||
|
write: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function canWriteLibrary(user: FastifyRequest["userInfo"], libraryType: "system" | "user", ownerUserId: number | null) {
|
||||||
|
if (user.role === "admin") return true;
|
||||||
|
if (libraryType === "user" && ownerUserId === user.id) return true;
|
||||||
|
if (libraryType === "system" && config.allowUserEditSystemLibrary) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function folderPermissions(user: FastifyRequest["userInfo"], folder: Pick<FolderRow, "library_type" | "owner_user_id">): PermissionPayload {
|
||||||
|
return {
|
||||||
|
read: true,
|
||||||
|
write: canWriteLibrary(user, folder.library_type, folder.owner_user_id)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function modelPermissions(user: FastifyRequest["userInfo"], model: Pick<ModelRow, "library_type" | "owner_user_id">): PermissionPayload {
|
||||||
|
return {
|
||||||
|
read: true,
|
||||||
|
write: canWriteLibrary(user, model.library_type, model.owner_user_id)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFolder(id: number) {
|
||||||
|
return db.prepare(`
|
||||||
|
SELECT f.*, u.username AS owner_username
|
||||||
|
FROM folders f
|
||||||
|
LEFT JOIN users u ON u.id = f.owner_user_id
|
||||||
|
WHERE f.id = ?
|
||||||
|
`).get(id) as FolderRow | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requireFolderWrite(request: FastifyRequest, reply: FastifyReply, folder: FolderRow) {
|
||||||
|
if (!folderPermissions(request.userInfo, folder).write) {
|
||||||
|
reply.code(403).send({ message: "没有该模型库的操作权限" });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requireModelWrite(request: FastifyRequest, reply: FastifyReply, model: ModelRow) {
|
||||||
|
if (!modelPermissions(request.userInfo, model).write) {
|
||||||
|
reply.code(403).send({ message: "没有该模型的操作权限" });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -22,6 +22,10 @@ export interface FolderRow {
|
|||||||
parent_id: number | null;
|
parent_id: number | null;
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
library_type: "system" | "user";
|
||||||
|
owner_user_id: number | null;
|
||||||
|
is_system: number;
|
||||||
|
owner_username?: string | null;
|
||||||
model_count?: number;
|
model_count?: number;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
@@ -35,11 +39,13 @@ export interface ModelRow {
|
|||||||
name: string;
|
name: string;
|
||||||
original_filename: string;
|
original_filename: string;
|
||||||
file_path: string;
|
file_path: string;
|
||||||
|
library_type: "system" | "user";
|
||||||
|
owner_user_id: number | null;
|
||||||
storage_provider: "local" | "cos";
|
storage_provider: "local" | "cos";
|
||||||
file_size: number;
|
file_size: number;
|
||||||
thumbnail: string | null;
|
|
||||||
thumbnail_path: string | null;
|
thumbnail_path: string | null;
|
||||||
thumbnail_provider: "local" | "cos" | null;
|
thumbnail_provider: "local" | "cos" | null;
|
||||||
|
operation_tree: string;
|
||||||
brand_name?: string | null;
|
brand_name?: string | null;
|
||||||
type_name?: string | null;
|
type_name?: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
|||||||
@@ -134,6 +134,12 @@
|
|||||||
margin: 0 0 4px;
|
margin: 0 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-process-empty {
|
||||||
|
padding: 28px 8px;
|
||||||
|
color: #7a8997;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.preview-process-tree button {
|
.preview-process-tree button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -155,6 +161,14 @@
|
|||||||
border-top: 1px solid #d8e0e8;
|
border-top: 1px solid #d8e0e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-process-status {
|
||||||
|
padding: 6px 8px 8px;
|
||||||
|
border-top: 1px solid #eef2f5;
|
||||||
|
color: #647586;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
.preview-scene-section {
|
.preview-scene-section {
|
||||||
border-top: 1px solid #d8e0e8;
|
border-top: 1px solid #d8e0e8;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export async function renderApp() {
|
|||||||
<div class="content-actions">
|
<div class="content-actions">
|
||||||
${isAdmin ? `<button id="manageUsersBtn" type="button">人员权限</button>` : ""}
|
${isAdmin ? `<button id="manageUsersBtn" type="button">人员权限</button>` : ""}
|
||||||
${isAdmin ? `<button id="manageDictionariesBtn" type="button">字典维护</button>` : ""}
|
${isAdmin ? `<button id="manageDictionariesBtn" type="button">字典维护</button>` : ""}
|
||||||
${isAdmin ? `<button id="addModelBtn" class="primary-btn" type="button">增加模型</button>` : ""}
|
<button id="addModelBtn" class="primary-btn" type="button" hidden>增加模型</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-toolbar">
|
<div class="filter-toolbar">
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ type JsTreeNode = {
|
|||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isRealFolderId(id: string) {
|
||||||
|
return /^\d+$/.test(id);
|
||||||
|
}
|
||||||
|
|
||||||
function selectFolder(node: JsTreeNode) {
|
function selectFolder(node: JsTreeNode) {
|
||||||
appState.selectedFolderId = Number(node.id);
|
appState.selectedFolderId = Number(node.id);
|
||||||
appState.selectedFolderName = appState.folders.find((folder) => folder.id === Number(node.id))?.name ?? node.text;
|
appState.selectedFolderName = appState.folders.find((folder) => folder.id === Number(node.id))?.name ?? node.text;
|
||||||
@@ -60,7 +64,6 @@ async function runFolderAction(action: () => Promise<void>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function loadFolders() {
|
export async function loadFolders() {
|
||||||
const isAdmin = getCurrentUser()?.role === "admin";
|
|
||||||
const result = await api<FolderTreeResponse>("/api/folders");
|
const result = await api<FolderTreeResponse>("/api/folders");
|
||||||
appState.folders = result.folders;
|
appState.folders = result.folders;
|
||||||
const root = appState.folders.find((folder) => folder.parent_id === null);
|
const root = appState.folders.find((folder) => folder.parent_id === null);
|
||||||
@@ -73,16 +76,19 @@ export async function loadFolders() {
|
|||||||
data: result.tree,
|
data: result.tree,
|
||||||
multiple: false
|
multiple: false
|
||||||
},
|
},
|
||||||
plugins: isAdmin ? ["contextmenu"] : [],
|
plugins: ["contextmenu"],
|
||||||
contextmenu: {
|
contextmenu: {
|
||||||
items(node: JsTreeNode) {
|
items(node: JsTreeNode) {
|
||||||
|
if (!isRealFolderId(node.id)) return {};
|
||||||
const folderId = Number(node.id);
|
const folderId = Number(node.id);
|
||||||
const folder = appState.folders.find((item) => item.id === folderId);
|
const folder = appState.folders.find((item) => item.id === folderId);
|
||||||
const isRoot = folder?.parent_id === null;
|
const canWrite = Boolean(folder?.permissions.write);
|
||||||
|
const isSystem = Boolean(folder?.is_system);
|
||||||
return {
|
return {
|
||||||
create: {
|
create: {
|
||||||
label: "新建目录",
|
label: "新建目录",
|
||||||
icon: "tree-menu-icon tree-menu-icon-add",
|
icon: "tree-menu-icon tree-menu-icon-add",
|
||||||
|
_disabled: !canWrite,
|
||||||
action: () => runFolderAction(async () => {
|
action: () => runFolderAction(async () => {
|
||||||
selectFolder(node);
|
selectFolder(node);
|
||||||
await createFolder(folderId);
|
await createFolder(folderId);
|
||||||
@@ -91,7 +97,7 @@ export async function loadFolders() {
|
|||||||
rename: {
|
rename: {
|
||||||
label: "重命名",
|
label: "重命名",
|
||||||
icon: "tree-menu-icon tree-menu-icon-edit",
|
icon: "tree-menu-icon tree-menu-icon-edit",
|
||||||
_disabled: isRoot,
|
_disabled: !canWrite || isSystem,
|
||||||
action: () => runFolderAction(async () => {
|
action: () => runFolderAction(async () => {
|
||||||
selectFolder(node);
|
selectFolder(node);
|
||||||
await renameFolder(folderId);
|
await renameFolder(folderId);
|
||||||
@@ -100,7 +106,7 @@ export async function loadFolders() {
|
|||||||
remove: {
|
remove: {
|
||||||
label: "删除",
|
label: "删除",
|
||||||
icon: "tree-menu-icon tree-menu-icon-delete",
|
icon: "tree-menu-icon tree-menu-icon-delete",
|
||||||
_disabled: isRoot,
|
_disabled: !canWrite || isSystem,
|
||||||
action: () => runFolderAction(async () => {
|
action: () => runFolderAction(async () => {
|
||||||
selectFolder(node);
|
selectFolder(node);
|
||||||
await deleteFolder(folderId);
|
await deleteFolder(folderId);
|
||||||
@@ -110,6 +116,7 @@ export async function loadFolders() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).on("select_node.jstree", async (_event: JQuery.Event, data: { node: { id: string; text: string } }) => {
|
}).on("select_node.jstree", async (_event: JQuery.Event, data: { node: { id: string; text: string } }) => {
|
||||||
|
if (!isRealFolderId(data.node.id)) return;
|
||||||
appState.selectedFolderId = Number(data.node.id);
|
appState.selectedFolderId = Number(data.node.id);
|
||||||
appState.selectedFolderName = appState.folders.find((folder) => folder.id === appState.selectedFolderId)?.name ?? data.node.text;
|
appState.selectedFolderName = appState.folders.find((folder) => folder.id === appState.selectedFolderId)?.name ?? data.node.text;
|
||||||
appState.page = 1;
|
appState.page = 1;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ type UploadFormState = {
|
|||||||
file: File | null;
|
file: File | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultOperationTree = JSON.stringify({ OperationTree: "[]" });
|
||||||
|
|
||||||
export function bindModelActions() {
|
export function bindModelActions() {
|
||||||
const isAdmin = getCurrentUser()?.role === "admin";
|
const isAdmin = getCurrentUser()?.role === "admin";
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
@@ -31,15 +33,16 @@ export function bindModelActions() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector("#addModelBtn")?.addEventListener("click", async () => {
|
|
||||||
try {
|
|
||||||
await openUploadModelDialog();
|
|
||||||
} catch (error) {
|
|
||||||
notifyError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.querySelector("#addModelBtn")?.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
await openUploadModelDialog();
|
||||||
|
} catch (error) {
|
||||||
|
notifyError(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.querySelector<HTMLSelectElement>("#brandFilter")!.addEventListener("change", async (event) => {
|
document.querySelector<HTMLSelectElement>("#brandFilter")!.addEventListener("change", async (event) => {
|
||||||
appState.filters.brandId = (event.currentTarget as HTMLSelectElement).value;
|
appState.filters.brandId = (event.currentTarget as HTMLSelectElement).value;
|
||||||
appState.page = 1;
|
appState.page = 1;
|
||||||
@@ -77,6 +80,7 @@ export async function loadModels() {
|
|||||||
const grid = document.querySelector<HTMLDivElement>("#modelGrid");
|
const grid = document.querySelector<HTMLDivElement>("#modelGrid");
|
||||||
if (!grid || !appState.selectedFolderId) return;
|
if (!grid || !appState.selectedFolderId) return;
|
||||||
await loadDictionaries();
|
await loadDictionaries();
|
||||||
|
syncSelectedFolderActions();
|
||||||
document.querySelector("#folderCrumb")!.textContent = appState.selectedFolderName || "模型库";
|
document.querySelector("#folderCrumb")!.textContent = appState.selectedFolderName || "模型库";
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
folderId: String(appState.selectedFolderId),
|
folderId: String(appState.selectedFolderId),
|
||||||
@@ -144,7 +148,7 @@ function syncDictionarySelect(selector: string, items: { id: number; name: strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderModelCard(item: ModelItem) {
|
function renderModelCard(item: ModelItem) {
|
||||||
const isAdmin = getCurrentUser()?.role === "admin";
|
const canWrite = item.permissions.write;
|
||||||
const prop = item.properties ?? {};
|
const prop = item.properties ?? {};
|
||||||
const thumb = item.thumbnail_url
|
const thumb = item.thumbnail_url
|
||||||
? `<img src="${item.thumbnail_url}" alt="" />`
|
? `<img src="${item.thumbnail_url}" alt="" />`
|
||||||
@@ -158,9 +162,9 @@ function renderModelCard(item: ModelItem) {
|
|||||||
${thumb}
|
${thumb}
|
||||||
<div class="thumb-actions">
|
<div class="thumb-actions">
|
||||||
<button data-action="preview" data-id="${item.id}">预览</button>
|
<button data-action="preview" data-id="${item.id}">预览</button>
|
||||||
${isAdmin ? `<button data-action="edit" data-id="${item.id}">编辑</button>` : ""}
|
${canWrite ? `<button data-action="edit" data-id="${item.id}">编辑</button>` : ""}
|
||||||
<button data-action="import" data-id="${item.id}">导入</button>
|
<button data-action="import" data-id="${item.id}">导入</button>
|
||||||
${isAdmin ? `<button data-action="delete" data-id="${item.id}">删除</button>` : ""}
|
${canWrite ? `<button data-action="delete" data-id="${item.id}">删除</button>` : ""}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<dl>
|
<dl>
|
||||||
@@ -174,6 +178,13 @@ function renderModelCard(item: ModelItem) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncSelectedFolderActions() {
|
||||||
|
const currentFolder = appState.folders.find((folder) => folder.id === appState.selectedFolderId);
|
||||||
|
const canWrite = Boolean(currentFolder?.permissions.write);
|
||||||
|
const addModelBtn = document.querySelector<HTMLButtonElement>("#addModelBtn");
|
||||||
|
if (addModelBtn) addModelBtn.hidden = !canWrite;
|
||||||
|
}
|
||||||
|
|
||||||
async function openUploadModelDialog() {
|
async function openUploadModelDialog() {
|
||||||
if (!appState.selectedFolderId) {
|
if (!appState.selectedFolderId) {
|
||||||
notify("请先选择目录");
|
notify("请先选择目录");
|
||||||
@@ -226,6 +237,7 @@ async function openUploadModelDialog() {
|
|||||||
payload.set("name", name);
|
payload.set("name", name);
|
||||||
payload.set("brandName", String(form.get("brandName") ?? ""));
|
payload.set("brandName", String(form.get("brandName") ?? ""));
|
||||||
payload.set("typeName", String(form.get("typeName") ?? ""));
|
payload.set("typeName", String(form.get("typeName") ?? ""));
|
||||||
|
payload.set("operationTree", defaultOperationTree);
|
||||||
payload.set("file", state.file);
|
payload.set("file", state.file);
|
||||||
for (const key of ["model", "price", "weight"]) {
|
for (const key of ["model", "price", "weight"]) {
|
||||||
payload.set(`prop.${key}`, String(form.get(key) ?? ""));
|
payload.set(`prop.${key}`, String(form.get(key) ?? ""));
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
|||||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||||
import { w2popup } from "w2ui";
|
import { w2popup } from "w2ui";
|
||||||
import type { ModelItem } from "../../../types";
|
import type { ModelItem } from "../../../types";
|
||||||
|
import type { OperationTree, OperationTreeDB } from "../../../types/OperationTree";
|
||||||
|
import { P_OPERATION } from "../../../types/OPERATION_BaseClass";
|
||||||
import { escapeHtml, formatBytes } from "../../../utils/format";
|
import { escapeHtml, formatBytes } from "../../../utils/format";
|
||||||
import { notify, notifyError } from "../../../ui/dialogs";
|
import { notify, notifyError } from "../../../ui/dialogs";
|
||||||
import { api } from "../../../services/api";
|
import { api } from "../../../services/api";
|
||||||
import { getCurrentUser } from "../../../services/authState";
|
|
||||||
|
|
||||||
type PreviewRuntime = {
|
type PreviewRuntime = {
|
||||||
renderer: THREE.WebGLRenderer;
|
renderer: THREE.WebGLRenderer;
|
||||||
@@ -17,14 +18,21 @@ type PreviewRuntime = {
|
|||||||
resizeObserver: ResizeObserver;
|
resizeObserver: ResizeObserver;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PreviewOperation = OperationTree & {
|
||||||
|
parsedCraftPlayData: P_OPERATION | null;
|
||||||
|
frameCount: number;
|
||||||
|
parseError: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
let runtime: PreviewRuntime | null = null;
|
let runtime: PreviewRuntime | null = null;
|
||||||
|
|
||||||
export function openModelPreview(model: ModelItem, onThumbnailSaved?: () => Promise<void> | void) {
|
export function openModelPreview(model: ModelItem, onThumbnailSaved?: () => Promise<void> | void) {
|
||||||
disposePreview();
|
disposePreview();
|
||||||
const isAdmin = getCurrentUser()?.role === "admin";
|
const canWrite = model.permissions.write;
|
||||||
const url = model.file_url;
|
const url = model.file_url;
|
||||||
|
const operations = parseModelOperations(model.operation_tree);
|
||||||
const popup = w2popup.open({
|
const popup = w2popup.open({
|
||||||
title: `模型预览 - ${escapeHtml(model.name)}`,
|
title: `模型预览 - ${escapeHtml(model.name)}【${formatBytes(model.file_size)}】`,
|
||||||
width: 860,
|
width: 860,
|
||||||
height: 620,
|
height: 620,
|
||||||
modal: true,
|
modal: true,
|
||||||
@@ -34,35 +42,22 @@ export function openModelPreview(model: ModelItem, onThumbnailSaved?: () => Prom
|
|||||||
<div class="preview-process-section">
|
<div class="preview-process-section">
|
||||||
<div class="preview-process-header">
|
<div class="preview-process-header">
|
||||||
<strong>工艺播放</strong>
|
<strong>工艺播放</strong>
|
||||||
<span>占位</span>
|
<span>${operations.length} 项</span>
|
||||||
</div>
|
|
||||||
<div class="preview-model-meta">
|
|
||||||
<span>模型大小</span>
|
|
||||||
<strong>${formatBytes(model.file_size)}</strong>
|
|
||||||
</div>
|
</div>
|
||||||
<ul class="preview-process-tree">
|
<ul class="preview-process-tree">
|
||||||
<li class="is-active">
|
${operationListHtml(operations)}
|
||||||
<button type="button" data-process-id="process-1">工艺 1</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button type="button" data-process-id="process-2">工艺 2</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button type="button" data-process-id="process-3">工艺 3</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<div class="preview-process-actions">
|
<div class="preview-process-actions">
|
||||||
<button id="previewPlayBtn" type="button">播放</button>
|
<button id="previewPlayBtn" type="button">播放</button>
|
||||||
<button id="previewPauseBtn" type="button">暂停</button>
|
<button id="previewPauseBtn" type="button">暂停</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="previewProcessStatus" class="preview-process-status">
|
||||||
|
${operationStatusHtml(operations[0] ?? null)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-scene-section">
|
<div class="preview-scene-section">
|
||||||
<div class="preview-process-header">
|
|
||||||
<strong>导入场景</strong>
|
|
||||||
<span>占位</span>
|
|
||||||
</div>
|
|
||||||
<div class="preview-scene-body">
|
<div class="preview-scene-body">
|
||||||
${isAdmin ? `
|
${canWrite ? `
|
||||||
<button id="previewCaptureThumbBtn" type="button">截缩略图</button>
|
<button id="previewCaptureThumbBtn" type="button">截缩略图</button>
|
||||||
<label class="preview-switch">
|
<label class="preview-switch">
|
||||||
<input id="previewTransparentThumb" type="checkbox" />
|
<input id="previewTransparentThumb" type="checkbox" />
|
||||||
@@ -102,13 +97,81 @@ export function openModelPreview(model: ModelItem, onThumbnailSaved?: () => Prom
|
|||||||
|
|
||||||
popup.self
|
popup.self
|
||||||
.on("open:after", () => {
|
.on("open:after", () => {
|
||||||
bindProcessPlaceholder();
|
bindProcessPlaceholder(operations);
|
||||||
if (isAdmin) bindThumbnailCapture(model.id, onThumbnailSaved);
|
if (canWrite) bindThumbnailCapture(model.id, onThumbnailSaved);
|
||||||
initPreview(url).catch((error) => notifyError(error));
|
initPreview(url).catch((error) => notifyError(error));
|
||||||
})
|
})
|
||||||
.on("close:after", () => disposePreview());
|
.on("close:after", () => disposePreview());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseModelOperations(value: string): PreviewOperation[] {
|
||||||
|
try {
|
||||||
|
const dbValue = JSON.parse(value || "{}") as Partial<OperationTreeDB>;
|
||||||
|
const list = JSON.parse(dbValue.OperationTree || "[]") as unknown;
|
||||||
|
if (!Array.isArray(list)) return [];
|
||||||
|
return list
|
||||||
|
.filter((item): item is OperationTree => Boolean(item && typeof item === "object"))
|
||||||
|
.map((operation) => {
|
||||||
|
const parsed = parseCraftPlayData(operation.CraftPlayData);
|
||||||
|
return {
|
||||||
|
...operation,
|
||||||
|
parsedCraftPlayData: parsed.data,
|
||||||
|
frameCount: parsed.data?.OPERATION.frames.length ?? 0,
|
||||||
|
parseError: parsed.error
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCraftPlayData(value?: string | null): { data: P_OPERATION | null; error: string | null } {
|
||||||
|
const text = value?.trim();
|
||||||
|
if (!text) return { data: null, error: null };
|
||||||
|
try {
|
||||||
|
const jsonData = parseJsonText(text);
|
||||||
|
return { data: P_OPERATION.fromjson(jsonData), error: null };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
error: error instanceof Error ? error.message : "工艺播放数据解析失败"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJsonText(text: string): unknown {
|
||||||
|
let value: unknown = text;
|
||||||
|
for (let index = 0; index < 2 && typeof value === "string"; index += 1) {
|
||||||
|
value = JSON.parse(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function operationListHtml(operations: PreviewOperation[]) {
|
||||||
|
if (operations.length === 0) {
|
||||||
|
return `<li class="preview-process-empty">暂无工艺数据</li>`;
|
||||||
|
}
|
||||||
|
return operations.map((operation, index) => {
|
||||||
|
const name = operation.CraftName?.trim() || `工艺 ${index + 1}`;
|
||||||
|
const id = operation.ID || operation.CraftCode || String(index + 1);
|
||||||
|
const title = operation.parseError
|
||||||
|
? `${name} / 播放数据解析失败`
|
||||||
|
: `${name} / ${operation.frameCount} 帧`;
|
||||||
|
return `
|
||||||
|
<li class="${index === 0 ? "is-active" : ""}">
|
||||||
|
<button type="button" data-process-id="${escapeHtml(String(id))}" data-process-index="${index}" title="${escapeHtml(title)}">${escapeHtml(name)}</button>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function operationStatusHtml(operation: PreviewOperation | null) {
|
||||||
|
if (!operation) return "未选择工艺";
|
||||||
|
if (operation.parseError) return `播放数据解析失败:${escapeHtml(operation.parseError)}`;
|
||||||
|
if (!operation.parsedCraftPlayData) return "当前工艺暂无播放数据";
|
||||||
|
return `已读取 ${operation.frameCount} 帧播放数据`;
|
||||||
|
}
|
||||||
|
|
||||||
function bindThumbnailCapture(modelId: number, onThumbnailSaved?: () => Promise<void> | void) {
|
function bindThumbnailCapture(modelId: number, onThumbnailSaved?: () => Promise<void> | void) {
|
||||||
document.querySelector<HTMLButtonElement>("#previewCaptureThumbBtn")?.addEventListener("click", async () => {
|
document.querySelector<HTMLButtonElement>("#previewCaptureThumbBtn")?.addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
@@ -142,13 +205,51 @@ function bindThumbnailCapture(modelId: number, onThumbnailSaved?: () => Promise<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindProcessPlaceholder() {
|
function bindProcessPlaceholder(operations: PreviewOperation[]) {
|
||||||
|
const getSelectedOperation = () => {
|
||||||
|
const selectedButton = document.querySelector<HTMLButtonElement>(".preview-process-tree li.is-active button");
|
||||||
|
const index = Number(selectedButton?.dataset.processIndex ?? 0);
|
||||||
|
return operations[index] ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateStatus = (operation: PreviewOperation | null) => {
|
||||||
|
const status = document.querySelector<HTMLDivElement>("#previewProcessStatus");
|
||||||
|
if (status) {
|
||||||
|
status.innerHTML = operationStatusHtml(operation);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
document.querySelectorAll<HTMLButtonElement>(".preview-process-tree button").forEach((button) => {
|
document.querySelectorAll<HTMLButtonElement>(".preview-process-tree button").forEach((button) => {
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
document.querySelectorAll(".preview-process-tree li").forEach((item) => item.classList.remove("is-active"));
|
document.querySelectorAll(".preview-process-tree li").forEach((item) => item.classList.remove("is-active"));
|
||||||
button.closest("li")?.classList.add("is-active");
|
button.closest("li")?.classList.add("is-active");
|
||||||
|
const index = Number(button.dataset.processIndex ?? 0);
|
||||||
|
updateStatus(operations[index] ?? null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.querySelector<HTMLButtonElement>("#previewPlayBtn")?.addEventListener("click", () => {
|
||||||
|
const operation = getSelectedOperation();
|
||||||
|
updateStatus(operation);
|
||||||
|
if (!operation) {
|
||||||
|
notify("请先选择工艺");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (operation.parseError) {
|
||||||
|
notifyError(`工艺播放数据解析失败:${operation.parseError}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!operation.parsedCraftPlayData) {
|
||||||
|
notify("当前工艺暂无播放数据");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notify(`${operation.CraftName || "当前工艺"} 已读取 ${operation.frameCount} 帧,播放逻辑待接入`);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector<HTMLButtonElement>("#previewPauseBtn")?.addEventListener("click", () => {
|
||||||
|
updateStatus(getSelectedOperation());
|
||||||
|
notify("暂停逻辑待接入");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initPreview(url: string) {
|
async function initPreview(url: string) {
|
||||||
|
|||||||
@@ -29,7 +29,17 @@ export type Folder = {
|
|||||||
parent_id: number | null;
|
parent_id: number | null;
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
library_type: "system" | "user";
|
||||||
|
owner_user_id: number | null;
|
||||||
|
is_system: number;
|
||||||
|
owner_username?: string | null;
|
||||||
model_count: number;
|
model_count: number;
|
||||||
|
permissions: PermissionPayload;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PermissionPayload = {
|
||||||
|
read: boolean;
|
||||||
|
write: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModelItem = {
|
export type ModelItem = {
|
||||||
@@ -41,14 +51,17 @@ export type ModelItem = {
|
|||||||
original_filename: string;
|
original_filename: string;
|
||||||
file_path: string;
|
file_path: string;
|
||||||
file_url: string;
|
file_url: string;
|
||||||
|
library_type: "system" | "user";
|
||||||
|
owner_user_id: number | null;
|
||||||
storage_provider: "local" | "cos";
|
storage_provider: "local" | "cos";
|
||||||
file_size: number;
|
file_size: number;
|
||||||
thumbnail: string | null;
|
|
||||||
thumbnail_path: string | null;
|
thumbnail_path: string | null;
|
||||||
thumbnail_provider: "local" | "cos" | null;
|
thumbnail_provider: "local" | "cos" | null;
|
||||||
thumbnail_url: string | null;
|
thumbnail_url: string | null;
|
||||||
|
operation_tree: string;
|
||||||
brand_name: string | null;
|
brand_name: string | null;
|
||||||
type_name: string | null;
|
type_name: string | null;
|
||||||
|
permissions: PermissionPayload;
|
||||||
properties: Record<string, string>;
|
properties: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
449
web/src/types/OPERATION_BaseClass.ts
Normal file
449
web/src/types/OPERATION_BaseClass.ts
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
type JsonRecord = Record<string, unknown>;
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is JsonRecord {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readString(data: JsonRecord, key: string, fallback = "") {
|
||||||
|
const value = data[key];
|
||||||
|
return value === null || value === undefined ? fallback : String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readNumber(data: JsonRecord, key: string, fallback = 0) {
|
||||||
|
const value = data[key];
|
||||||
|
if (typeof value === "number") return Number.isFinite(value) ? value : fallback;
|
||||||
|
if (typeof value === "string" && value.trim() !== "") {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readBoolean(data: JsonRecord, key: string, fallback = false) {
|
||||||
|
const value = data[key];
|
||||||
|
if (typeof value === "boolean") return value;
|
||||||
|
if (typeof value === "number") return value !== 0;
|
||||||
|
if (typeof value === "string") {
|
||||||
|
if (value.toLowerCase() === "true") return true;
|
||||||
|
if (value.toLowerCase() === "false") return false;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readArray(data: JsonRecord, key: string) {
|
||||||
|
const value = data[key];
|
||||||
|
return Array.isArray(value) ? value : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class P_OPERATION {
|
||||||
|
/**
|
||||||
|
* 一组项目帧
|
||||||
|
*/
|
||||||
|
OPERATION = new C_OPERATION();
|
||||||
|
|
||||||
|
constructor(operation?: C_OPERATION) {
|
||||||
|
if (operation) {
|
||||||
|
this.OPERATION = operation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从JSON数据反序列化
|
||||||
|
* @param jsonData JSON格式的数据
|
||||||
|
* @returns P_OPERATION
|
||||||
|
*/
|
||||||
|
static fromjson(jsonData: unknown) {
|
||||||
|
const operation = new P_OPERATION();
|
||||||
|
if (isRecord(jsonData) && isRecord(jsonData.OPERATION)) {
|
||||||
|
operation.OPERATION = C_OPERATION.fromjson(jsonData.OPERATION);
|
||||||
|
}
|
||||||
|
return operation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class C_OPERATION {
|
||||||
|
/**
|
||||||
|
* 帧集合
|
||||||
|
*/
|
||||||
|
frames: C_Frames[] = [];
|
||||||
|
|
||||||
|
constructor(frames?: C_Frames[]) {
|
||||||
|
this.frames = frames || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从JSON数据反序列化
|
||||||
|
* @param jsonData JSON格式的数据
|
||||||
|
* @returns C_OPERATION
|
||||||
|
*/
|
||||||
|
static fromjson(jsonData: unknown) {
|
||||||
|
const operation = new C_OPERATION();
|
||||||
|
if (isRecord(jsonData)) {
|
||||||
|
operation.frames = readArray(jsonData, "frames").map((frameData) => C_Frames.fromjson(frameData));
|
||||||
|
}
|
||||||
|
return operation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class C_Frames {
|
||||||
|
/**
|
||||||
|
* 帧时间
|
||||||
|
*/
|
||||||
|
time = "0.01";
|
||||||
|
/**
|
||||||
|
* 帧内容集合
|
||||||
|
*/
|
||||||
|
objStates: C_ObjStates[] = [];
|
||||||
|
crafts: C_Craft[] = [];
|
||||||
|
signalwrites: C_Signalwrite[] = [];
|
||||||
|
visibles: C_Visible[] = [];
|
||||||
|
rotations: C_Rotation[] = [];
|
||||||
|
waits: C_Wait[] = [];
|
||||||
|
attachs: C_Attach[] = [];
|
||||||
|
|
||||||
|
constructor(frame?: Partial<C_Frames>) {
|
||||||
|
if (frame) {
|
||||||
|
this.time = frame.time ?? this.time;
|
||||||
|
this.objStates = frame.objStates ?? this.objStates;
|
||||||
|
this.crafts = frame.crafts ?? this.crafts;
|
||||||
|
this.signalwrites = frame.signalwrites ?? this.signalwrites;
|
||||||
|
this.visibles = frame.visibles ?? this.visibles;
|
||||||
|
this.rotations = frame.rotations ?? this.rotations;
|
||||||
|
this.waits = frame.waits ?? this.waits;
|
||||||
|
this.attachs = frame.attachs ?? this.attachs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从JSON数据反序列化
|
||||||
|
* @param jsonData JSON格式的数据
|
||||||
|
* @returns C_Frames实例
|
||||||
|
*/
|
||||||
|
static fromjson(jsonData: unknown) {
|
||||||
|
const frame = new C_Frames();
|
||||||
|
if (isRecord(jsonData)) {
|
||||||
|
frame.time = readString(jsonData, "time", frame.time);
|
||||||
|
frame.objStates = readArray(jsonData, "objStates").map((objStateData) => C_ObjStates.fromjson(objStateData));
|
||||||
|
frame.crafts = readArray(jsonData, "crafts").map((craftData) => C_Craft.fromjson(craftData));
|
||||||
|
frame.signalwrites = readArray(jsonData, "signalwrites").map((signalData) => C_Signalwrite.fromjson(signalData));
|
||||||
|
frame.visibles = readArray(jsonData, "visibles").map((visibleData) => C_Visible.fromjson(visibleData));
|
||||||
|
frame.rotations = readArray(jsonData, "rotations").map((rotationData) => C_Rotation.fromjson(rotationData));
|
||||||
|
frame.waits = readArray(jsonData, "waits").map((waitData) => C_Wait.fromjson(waitData));
|
||||||
|
frame.attachs = readArray(jsonData, "attachs").map((attachData) => C_Attach.fromjson(attachData));
|
||||||
|
}
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class C_ObjStates {
|
||||||
|
/**
|
||||||
|
* 模型代码
|
||||||
|
*/
|
||||||
|
i = "";
|
||||||
|
/**
|
||||||
|
* tx
|
||||||
|
*/
|
||||||
|
tx = "";
|
||||||
|
/**
|
||||||
|
* ty
|
||||||
|
*/
|
||||||
|
ty = "";
|
||||||
|
/**
|
||||||
|
* tz
|
||||||
|
*/
|
||||||
|
tz = "";
|
||||||
|
/**
|
||||||
|
* qx
|
||||||
|
*/
|
||||||
|
qx = "";
|
||||||
|
/**
|
||||||
|
* qy
|
||||||
|
*/
|
||||||
|
qy = "";
|
||||||
|
/**
|
||||||
|
* qz
|
||||||
|
*/
|
||||||
|
qz = "";
|
||||||
|
/**
|
||||||
|
* qw
|
||||||
|
*/
|
||||||
|
qw = "";
|
||||||
|
|
||||||
|
constructor(objState?: Partial<C_ObjStates>) {
|
||||||
|
if (objState) {
|
||||||
|
this.i = objState.i ?? this.i;
|
||||||
|
this.tx = objState.tx ?? this.tx;
|
||||||
|
this.ty = objState.ty ?? this.ty;
|
||||||
|
this.tz = objState.tz ?? this.tz;
|
||||||
|
this.qx = objState.qx ?? this.qx;
|
||||||
|
this.qy = objState.qy ?? this.qy;
|
||||||
|
this.qz = objState.qz ?? this.qz;
|
||||||
|
this.qw = objState.qw ?? this.qw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从JSON数据反序列化
|
||||||
|
* @param jsonData JSON格式的数据
|
||||||
|
* @returns C_ObjStates实例
|
||||||
|
*/
|
||||||
|
static fromjson(jsonData: unknown) {
|
||||||
|
const objState = new C_ObjStates();
|
||||||
|
if (isRecord(jsonData)) {
|
||||||
|
objState.i = readString(jsonData, "i", objState.i);
|
||||||
|
objState.tx = readString(jsonData, "tx", objState.tx);
|
||||||
|
objState.ty = readString(jsonData, "ty", objState.ty);
|
||||||
|
objState.tz = readString(jsonData, "tz", objState.tz);
|
||||||
|
objState.qx = readString(jsonData, "qx", objState.qx);
|
||||||
|
objState.qy = readString(jsonData, "qy", objState.qy);
|
||||||
|
objState.qz = readString(jsonData, "qz", objState.qz);
|
||||||
|
objState.qw = readString(jsonData, "qw", objState.qw);
|
||||||
|
}
|
||||||
|
return objState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class C_Craft {
|
||||||
|
CraftCode = "";
|
||||||
|
CraftValue = "";
|
||||||
|
|
||||||
|
constructor(craft?: Partial<C_Craft>) {
|
||||||
|
if (craft) {
|
||||||
|
this.CraftCode = craft.CraftCode ?? this.CraftCode;
|
||||||
|
this.CraftValue = craft.CraftValue ?? this.CraftValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从JSON数据反序列化
|
||||||
|
* @param jsonData JSON格式的数据
|
||||||
|
* @returns C_Craft实例
|
||||||
|
*/
|
||||||
|
static fromjson(jsonData: unknown) {
|
||||||
|
const craft = new C_Craft();
|
||||||
|
if (isRecord(jsonData)) {
|
||||||
|
craft.CraftCode = readString(jsonData, "CraftCode", craft.CraftCode);
|
||||||
|
craft.CraftValue = readString(jsonData, "CraftValue", craft.CraftValue);
|
||||||
|
}
|
||||||
|
return craft;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class C_Signalwrite {
|
||||||
|
/**
|
||||||
|
* TagID
|
||||||
|
*/
|
||||||
|
TagID = "";
|
||||||
|
/**
|
||||||
|
* TagValue
|
||||||
|
*/
|
||||||
|
TagValue = "";
|
||||||
|
|
||||||
|
constructor(signal?: Partial<C_Signalwrite>) {
|
||||||
|
if (signal) {
|
||||||
|
this.TagID = signal.TagID ?? this.TagID;
|
||||||
|
this.TagValue = signal.TagValue ?? this.TagValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从JSON数据反序列化
|
||||||
|
* @param jsonData JSON格式的数据
|
||||||
|
* @returns C_Signalwrite实例
|
||||||
|
*/
|
||||||
|
static fromjson(jsonData: unknown) {
|
||||||
|
const signal = new C_Signalwrite();
|
||||||
|
if (isRecord(jsonData)) {
|
||||||
|
signal.TagID = readString(jsonData, "TagID", signal.TagID);
|
||||||
|
signal.TagValue = readString(jsonData, "TagValue", signal.TagValue);
|
||||||
|
}
|
||||||
|
return signal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class C_Visible {
|
||||||
|
ModelCode = "";
|
||||||
|
Visible = "";
|
||||||
|
|
||||||
|
constructor(visible?: Partial<C_Visible>) {
|
||||||
|
if (visible) {
|
||||||
|
this.ModelCode = visible.ModelCode ?? this.ModelCode;
|
||||||
|
this.Visible = visible.Visible ?? this.Visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从JSON数据反序列化
|
||||||
|
* @param jsonData JSON格式的数据
|
||||||
|
* @returns C_Visible
|
||||||
|
*/
|
||||||
|
static fromjson(jsonData: unknown) {
|
||||||
|
const visible = new C_Visible();
|
||||||
|
if (isRecord(jsonData)) {
|
||||||
|
visible.ModelCode = readString(jsonData, "ModelCode", visible.ModelCode);
|
||||||
|
visible.Visible = readString(jsonData, "Visible", visible.Visible);
|
||||||
|
}
|
||||||
|
return visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class C_Rotation {
|
||||||
|
ModelCode = "";
|
||||||
|
Axis = "";
|
||||||
|
Rate = 0;
|
||||||
|
|
||||||
|
constructor(rotation?: Partial<C_Rotation>) {
|
||||||
|
if (rotation) {
|
||||||
|
this.ModelCode = rotation.ModelCode ?? this.ModelCode;
|
||||||
|
this.Axis = rotation.Axis ?? this.Axis;
|
||||||
|
this.Rate = rotation.Rate ?? this.Rate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从JSON数据反序列化
|
||||||
|
* @param jsonData JSON格式的数据
|
||||||
|
* @returns C_Rotation
|
||||||
|
*/
|
||||||
|
static fromjson(jsonData: unknown) {
|
||||||
|
const rotation = new C_Rotation();
|
||||||
|
if (isRecord(jsonData)) {
|
||||||
|
rotation.ModelCode = readString(jsonData, "ModelCode", rotation.ModelCode);
|
||||||
|
rotation.Axis = readString(jsonData, "Axis", rotation.Axis);
|
||||||
|
rotation.Rate = readNumber(jsonData, "Rate", rotation.Rate);
|
||||||
|
}
|
||||||
|
return rotation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class C_Wait {
|
||||||
|
WaitStr = "";
|
||||||
|
|
||||||
|
constructor(wait?: Partial<C_Wait>) {
|
||||||
|
if (wait) {
|
||||||
|
this.WaitStr = wait.WaitStr ?? this.WaitStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从JSON数据反序列化
|
||||||
|
* @param jsonData JSON格式的数据
|
||||||
|
* @returns C_Wait实例
|
||||||
|
*/
|
||||||
|
static fromjson(jsonData: unknown) {
|
||||||
|
const wait = new C_Wait();
|
||||||
|
if (isRecord(jsonData)) {
|
||||||
|
wait.WaitStr = readString(jsonData, "WaitStr", wait.WaitStr);
|
||||||
|
}
|
||||||
|
return wait;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class C_Attach {
|
||||||
|
AttachToModelCode = "";
|
||||||
|
AttachToModelName = "";
|
||||||
|
List_AttachModel: ModelGroup[] = [];
|
||||||
|
IsTwoWay = false;
|
||||||
|
IsAttach = false;
|
||||||
|
|
||||||
|
constructor(attach?: Partial<C_Attach>) {
|
||||||
|
if (attach) {
|
||||||
|
this.AttachToModelCode = attach.AttachToModelCode ?? this.AttachToModelCode;
|
||||||
|
this.AttachToModelName = attach.AttachToModelName ?? this.AttachToModelName;
|
||||||
|
this.List_AttachModel = attach.List_AttachModel ?? this.List_AttachModel;
|
||||||
|
this.IsTwoWay = attach.IsTwoWay ?? this.IsTwoWay;
|
||||||
|
this.IsAttach = attach.IsAttach ?? this.IsAttach;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从JSON数据反序列化
|
||||||
|
* @param jsonData JSON格式的数据
|
||||||
|
* @returns C_Attach实例
|
||||||
|
*/
|
||||||
|
static fromjson(jsonData: unknown) {
|
||||||
|
const attach = new C_Attach();
|
||||||
|
if (isRecord(jsonData)) {
|
||||||
|
attach.AttachToModelCode = readString(jsonData, "AttachToModelCode", attach.AttachToModelCode);
|
||||||
|
attach.AttachToModelName = readString(jsonData, "AttachToModelName", attach.AttachToModelName);
|
||||||
|
attach.IsTwoWay = readBoolean(jsonData, "IsTwoWay", attach.IsTwoWay);
|
||||||
|
attach.IsAttach = readBoolean(jsonData, "IsAttach", attach.IsAttach);
|
||||||
|
attach.List_AttachModel = readArray(jsonData, "List_AttachModel")
|
||||||
|
.filter(isRecord)
|
||||||
|
.map((modelData) => new ModelGroup(
|
||||||
|
readString(modelData, "ModelCode"),
|
||||||
|
readString(modelData, "ModelName")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return attach;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// / <summary>
|
||||||
|
// / 焦点选择描述
|
||||||
|
// / 2025.08.17.2050.LLX
|
||||||
|
// / </summary>
|
||||||
|
class C_Focus {
|
||||||
|
id = "";
|
||||||
|
name = "";
|
||||||
|
time = "3000";
|
||||||
|
|
||||||
|
constructor(focus?: Partial<C_Focus>) {
|
||||||
|
if (focus) {
|
||||||
|
this.id = focus.id ?? this.id;
|
||||||
|
this.name = focus.name ?? this.name;
|
||||||
|
this.time = focus.time ?? this.time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从JSON数据反序列化
|
||||||
|
* @param jsonData JSON格式的数据
|
||||||
|
* @returns C_Focus
|
||||||
|
*/
|
||||||
|
static fromjson(jsonData: unknown) {
|
||||||
|
const focus = new C_Focus();
|
||||||
|
if (isRecord(jsonData)) {
|
||||||
|
focus.id = readString(jsonData, "id", focus.id);
|
||||||
|
focus.name = readString(jsonData, "name", focus.name);
|
||||||
|
focus.time = readString(jsonData, "time", focus.time);
|
||||||
|
}
|
||||||
|
return focus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModelGroup {
|
||||||
|
/**
|
||||||
|
* 模型代码
|
||||||
|
*/
|
||||||
|
ModelCode = "";
|
||||||
|
/**
|
||||||
|
* 模型名称
|
||||||
|
*/
|
||||||
|
ModelName = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param modelCode 模型代码
|
||||||
|
* @param modelName 模型名称
|
||||||
|
*/
|
||||||
|
constructor(modelCode = "", modelName = "") {
|
||||||
|
this.ModelCode = modelCode;
|
||||||
|
this.ModelName = modelName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
C_Attach,
|
||||||
|
C_Craft,
|
||||||
|
C_Frames,
|
||||||
|
C_OPERATION,
|
||||||
|
C_ObjStates,
|
||||||
|
C_Rotation,
|
||||||
|
C_Signalwrite,
|
||||||
|
C_Visible,
|
||||||
|
C_Wait,
|
||||||
|
ModelGroup,
|
||||||
|
P_OPERATION,
|
||||||
|
C_Focus
|
||||||
|
};
|
||||||
148
web/src/types/OperationTree.ts
Normal file
148
web/src/types/OperationTree.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
// 数据库存储的数据
|
||||||
|
interface OperationTreeDB {
|
||||||
|
OperationTree: string; // JSON字符串,格式化之后是 OperationTree[] 列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工艺数据机构
|
||||||
|
interface OperationTree {
|
||||||
|
/** 主键ID */
|
||||||
|
ID: string;
|
||||||
|
/** 是否自动插入 */
|
||||||
|
IsAutoInsert?: boolean | null;
|
||||||
|
/** 工艺代码(主键) */
|
||||||
|
CraftCode: string;
|
||||||
|
/** 工艺类型代码 */
|
||||||
|
CraftTypeCode: string;
|
||||||
|
/** 品种 */
|
||||||
|
Varieties: string;
|
||||||
|
/** 工艺名称 */
|
||||||
|
CraftName: string;
|
||||||
|
/** 工艺设备类型代码 */
|
||||||
|
CraftDeviceTypeCode: number;
|
||||||
|
/** 项目代码(主键) */
|
||||||
|
ProjectCode: string;
|
||||||
|
/** 操作时间 */
|
||||||
|
OperationTime: Date | string;
|
||||||
|
/** 序列号 */
|
||||||
|
SequenceNo?: number | null;
|
||||||
|
/** 开始时间 */
|
||||||
|
StartTime?: number | null;
|
||||||
|
/** 持续时间 */
|
||||||
|
KeepTime?: number | null;
|
||||||
|
/** 模型代码 */
|
||||||
|
ModelCode: string | null;
|
||||||
|
/** 工艺播放数据 */
|
||||||
|
CraftPlayData?: string | null;
|
||||||
|
/** 关节工艺播放数据 */
|
||||||
|
CraftPlayData_Joint?: string | null;
|
||||||
|
/** 原始工艺播放数据 */
|
||||||
|
CraftPlayData_Raw: string;
|
||||||
|
/** 速率 */
|
||||||
|
Rate: number | 0;
|
||||||
|
/** 说明 */
|
||||||
|
Explain?: string | null;
|
||||||
|
/** 反向 */
|
||||||
|
Reverse?: number | null;
|
||||||
|
/** 当前播放 */
|
||||||
|
CurrPlay?: boolean | null;
|
||||||
|
/** 脚本文本 */
|
||||||
|
ScriptText?: string | null;
|
||||||
|
/** 是否存在脚本 */
|
||||||
|
ExistScript?: boolean | null;
|
||||||
|
/** 甘特图父节点 */
|
||||||
|
GanttParent?: string | null;
|
||||||
|
/** 父模型代码 */
|
||||||
|
ModelCodeParent?: string | null;
|
||||||
|
/** 父模型名称 */
|
||||||
|
ModelNameParent?: string | null;
|
||||||
|
/** 模型名称 */
|
||||||
|
ModelName?: string | null;
|
||||||
|
/** 标签ID */
|
||||||
|
TagID?: string | null;
|
||||||
|
/** 标签值 */
|
||||||
|
TagValue?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果需要创建新对象的接口(所有字段可选)
|
||||||
|
interface OperationTreeCreate {
|
||||||
|
ID?: string;
|
||||||
|
IsAutoInsert?: boolean;
|
||||||
|
CraftCode: string;
|
||||||
|
CraftTypeCode?: string;
|
||||||
|
Varieties?: string;
|
||||||
|
CraftName?: string;
|
||||||
|
CraftDeviceTypeCode?: number;
|
||||||
|
ProjectCode: string;
|
||||||
|
OperationTime?: Date | string;
|
||||||
|
SequenceNo?: number;
|
||||||
|
StartTime?: number;
|
||||||
|
KeepTime?: number;
|
||||||
|
ModelCode?: string;
|
||||||
|
CraftPlayData?: string;
|
||||||
|
CraftPlayData_Joint?: string;
|
||||||
|
CraftPlayData_Raw?: string;
|
||||||
|
Rate?: number;
|
||||||
|
Explain?: string;
|
||||||
|
Reverse?: number;
|
||||||
|
CurrPlay?: boolean;
|
||||||
|
ScriptText?: string;
|
||||||
|
ExistScript?: boolean;
|
||||||
|
GanttParent?: string;
|
||||||
|
ModelCodeParent?: string;
|
||||||
|
ModelNameParent?: string;
|
||||||
|
ModelName?: string;
|
||||||
|
TagID?: string;
|
||||||
|
TagValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OperationTreeExtra extends OperationTree {
|
||||||
|
pId: string;
|
||||||
|
id: string;
|
||||||
|
parent: string;
|
||||||
|
state: any;
|
||||||
|
text: string;
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
checked: boolean;
|
||||||
|
open: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JsTreeData_node_OperationTree {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
original: OperationTreeExtra;
|
||||||
|
icon: string;
|
||||||
|
children: string[]
|
||||||
|
children_d: string[]
|
||||||
|
parent: string
|
||||||
|
parents: string[]
|
||||||
|
state: {
|
||||||
|
loaded: boolean;
|
||||||
|
checked: boolean;
|
||||||
|
opened: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
selected: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
interface JsTreeData_OperationTree_drop {
|
||||||
|
node: JsTreeData_node_OperationTree;
|
||||||
|
old_parent: string;
|
||||||
|
parent: string;
|
||||||
|
old_position: number;
|
||||||
|
position: number;
|
||||||
|
}
|
||||||
|
interface OperationTree_SequenceNo {
|
||||||
|
CraftTypeCode: string;
|
||||||
|
CraftCode: string;
|
||||||
|
SequenceNo: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type {
|
||||||
|
OperationTreeDB,
|
||||||
|
OperationTree,
|
||||||
|
OperationTreeCreate,
|
||||||
|
OperationTreeExtra,
|
||||||
|
OperationTree_SequenceNo,
|
||||||
|
JsTreeData_node_OperationTree,
|
||||||
|
JsTreeData_OperationTree_drop
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user