优化模型上传与预览体验
This commit is contained in:
@@ -10,6 +10,17 @@ type UploadFormState = {
|
||||
file: File | null;
|
||||
};
|
||||
|
||||
type UploadModelPayload = {
|
||||
file: File;
|
||||
name: string;
|
||||
fileName?: string;
|
||||
folderId?: number;
|
||||
brandName?: string;
|
||||
typeName?: string;
|
||||
operationTree?: string;
|
||||
properties?: Record<string, string>;
|
||||
};
|
||||
|
||||
const defaultOperationTree = JSON.stringify({ OperationTree: "[]" });
|
||||
|
||||
export function bindModelActions() {
|
||||
@@ -43,6 +54,14 @@ export function bindModelActions() {
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector("#addProcessModelBtn")?.addEventListener("click", async () => {
|
||||
try {
|
||||
await openProcessModelUploadDialog();
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector<HTMLSelectElement>("#brandFilter")!.addEventListener("change", async (event) => {
|
||||
appState.filters.brandId = (event.currentTarget as HTMLSelectElement).value;
|
||||
appState.page = 1;
|
||||
@@ -120,8 +139,18 @@ export async function loadModels() {
|
||||
button.addEventListener("click", () => editModel(Number(button.dataset.id)));
|
||||
});
|
||||
grid.querySelectorAll<HTMLButtonElement>("[data-action='import']").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
notify("导入功能预留,后续接入当前模型的导入逻辑");
|
||||
const model = result.items.find((item) => item.id === Number(button.dataset.id));
|
||||
if (!model) return;
|
||||
button.addEventListener("click", async () => {
|
||||
const { createImportScenePayload, emitImportScenePayload } = await import("./preview");
|
||||
const payload = createImportScenePayload({
|
||||
model,
|
||||
basePointEnabled: false,
|
||||
basePoint: { x: 0, y: 0, z: 0, rx: 0, ry: 0, rz: 0 },
|
||||
selectedOperationIndex: null
|
||||
});
|
||||
emitImportScenePayload(payload);
|
||||
notify("导入场景数据已输出");
|
||||
});
|
||||
});
|
||||
grid.querySelectorAll<HTMLButtonElement>("[data-action='delete']").forEach((button) => {
|
||||
@@ -182,7 +211,40 @@ function syncSelectedFolderActions() {
|
||||
const currentFolder = appState.folders.find((folder) => folder.id === appState.selectedFolderId);
|
||||
const canWrite = Boolean(currentFolder?.permissions.write);
|
||||
const addModelBtn = document.querySelector<HTMLButtonElement>("#addModelBtn");
|
||||
const addProcessModelBtn = document.querySelector<HTMLButtonElement>("#addProcessModelBtn");
|
||||
if (addModelBtn) addModelBtn.hidden = !canWrite;
|
||||
if (addProcessModelBtn) addProcessModelBtn.hidden = !canWrite;
|
||||
}
|
||||
|
||||
export async function uploadModelToBackend(payload: UploadModelPayload) {
|
||||
const fileName = (payload.fileName || payload.file.name || payload.name).trim();
|
||||
const displayName = (payload.name || modelNameFromFile(fileName)).trim();
|
||||
if (!displayName) {
|
||||
throw new Error("模型名称不能为空");
|
||||
}
|
||||
if (!fileName.toLowerCase().endsWith(".glb")) {
|
||||
throw new Error("当前阶段只允许上传 .glb 模型");
|
||||
}
|
||||
const folderId = payload.folderId ?? appState.selectedFolderId;
|
||||
if (!folderId) {
|
||||
throw new Error("请先选择目录");
|
||||
}
|
||||
|
||||
const form = new FormData();
|
||||
form.set("folderId", String(folderId));
|
||||
form.set("name", modelNameFromFile(displayName));
|
||||
form.set("brandName", payload.brandName ?? "");
|
||||
form.set("typeName", payload.typeName ?? "");
|
||||
form.set("operationTree", normalizeOperationTreeInput(payload.operationTree));
|
||||
form.set("file", payload.file, fileName);
|
||||
for (const [key, value] of Object.entries(payload.properties ?? {})) {
|
||||
form.set(`prop.${key}`, value);
|
||||
}
|
||||
|
||||
await api("/api/models/upload", {
|
||||
method: "POST",
|
||||
body: form
|
||||
});
|
||||
}
|
||||
|
||||
async function openUploadModelDialog() {
|
||||
@@ -232,19 +294,97 @@ async function openUploadModelDialog() {
|
||||
throw new Error("模型名称不能为空");
|
||||
}
|
||||
const form = new FormData(document.querySelector<HTMLFormElement>("#modelUploadPopupForm")!);
|
||||
const payload = new FormData();
|
||||
payload.set("folderId", String(appState.selectedFolderId));
|
||||
payload.set("name", name);
|
||||
payload.set("brandName", String(form.get("brandName") ?? ""));
|
||||
payload.set("typeName", String(form.get("typeName") ?? ""));
|
||||
payload.set("operationTree", defaultOperationTree);
|
||||
payload.set("file", state.file);
|
||||
for (const key of ["model", "price", "weight"]) {
|
||||
payload.set(`prop.${key}`, String(form.get(key) ?? ""));
|
||||
await uploadModelToBackend({
|
||||
file: state.file,
|
||||
name,
|
||||
fileName: state.file.name,
|
||||
brandName: String(form.get("brandName") ?? ""),
|
||||
typeName: String(form.get("typeName") ?? ""),
|
||||
operationTree: defaultOperationTree,
|
||||
properties: {
|
||||
model: String(form.get("model") ?? ""),
|
||||
price: String(form.get("price") ?? ""),
|
||||
weight: String(form.get("weight") ?? "")
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (result) {
|
||||
await loadModels();
|
||||
}
|
||||
}
|
||||
|
||||
export async function openProcessModelUploadDialog(input?: {
|
||||
file?: File | null;
|
||||
modelName?: string;
|
||||
operationTreeJson?: string;
|
||||
}) {
|
||||
if (!appState.selectedFolderId) {
|
||||
notify("请先选择目录");
|
||||
return;
|
||||
}
|
||||
|
||||
const state: UploadFormState = { file: input?.file ?? null };
|
||||
await loadDictionaries();
|
||||
const initialName = input?.modelName || (input?.file ? modelNameFromFile(input.file.name) : "");
|
||||
const result = await formDialog<boolean>({
|
||||
title: "上传工艺模型",
|
||||
width: 640,
|
||||
height: 650,
|
||||
body: `
|
||||
<form id="processModelUploadPopupForm" class="popup-form upload-popup-form">
|
||||
<div class="upload-target-path">
|
||||
<span>上传目录</span>
|
||||
<strong>${escapeHtml(appState.selectedFolderName || "当前目录")}</strong>
|
||||
</div>
|
||||
<label>
|
||||
<span>模型文件</span>
|
||||
<div id="processModelDropZone" class="drop-zone">
|
||||
<input id="processModelUploadFile" type="file" accept=".glb" />
|
||||
<strong>选择模型</strong>
|
||||
<em>或拖拽 .glb 模型到这里</em>
|
||||
<small id="processSelectedFileName">${escapeHtml(input?.file?.name ?? "未选择文件")}</small>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
<span>模型名称</span>
|
||||
<input id="processModelUploadName" name="name" placeholder="例如 AA.glb" value="${escapeHtml(initialName)}" />
|
||||
</label>
|
||||
<div class="popup-form-grid">
|
||||
<label><span>品牌</span><input name="brandName" list="brandOptions" /></label>
|
||||
<label><span>类型</span><input name="typeName" list="typeOptions" /></label>
|
||||
<label><span>型号</span><input name="model" /></label>
|
||||
<label><span>价钱</span><input name="price" /></label>
|
||||
<label><span>重量</span><input name="weight" /></label>
|
||||
</div>
|
||||
<label>
|
||||
<span>工艺数据 JSON</span>
|
||||
<textarea id="processOperationTreeJson" name="operationTree" placeholder='可以粘贴 {"OperationTree":"[...]"} 或 OperationTree[] 数组 JSON'>${escapeHtml(input?.operationTreeJson ?? "")}</textarea>
|
||||
</label>
|
||||
${dictionaryDatalistHtml()}
|
||||
</form>
|
||||
`,
|
||||
onOpen: () => bindProcessUploadDialogEvents(state),
|
||||
onSubmit: async () => {
|
||||
if (!state.file) {
|
||||
throw new Error("请选择 .glb 模型文件");
|
||||
}
|
||||
await api("/api/models/upload", {
|
||||
method: "POST",
|
||||
body: payload
|
||||
const form = new FormData(document.querySelector<HTMLFormElement>("#processModelUploadPopupForm")!);
|
||||
const name = String(form.get("name") ?? "").trim();
|
||||
await uploadModelToBackend({
|
||||
file: state.file,
|
||||
name,
|
||||
fileName: name || state.file.name,
|
||||
brandName: String(form.get("brandName") ?? ""),
|
||||
typeName: String(form.get("typeName") ?? ""),
|
||||
operationTree: String(form.get("operationTree") ?? ""),
|
||||
properties: {
|
||||
model: String(form.get("model") ?? ""),
|
||||
price: String(form.get("price") ?? ""),
|
||||
weight: String(form.get("weight") ?? "")
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -297,6 +437,83 @@ function bindUploadDialogEvents(state: UploadFormState) {
|
||||
});
|
||||
}
|
||||
|
||||
function bindProcessUploadDialogEvents(state: UploadFormState) {
|
||||
const dropZone = document.querySelector<HTMLDivElement>("#processModelDropZone")!;
|
||||
const fileInput = document.querySelector<HTMLInputElement>("#processModelUploadFile")!;
|
||||
const nameInput = document.querySelector<HTMLInputElement>("#processModelUploadName")!;
|
||||
const selectedFileName = document.querySelector<HTMLElement>("#processSelectedFileName")!;
|
||||
|
||||
const selectFile = (file: File) => {
|
||||
if (!file.name.toLowerCase().endsWith(".glb")) {
|
||||
notify("当前阶段只允许上传 .glb 模型");
|
||||
return;
|
||||
}
|
||||
state.file = file;
|
||||
selectedFileName.textContent = file.name;
|
||||
if (!nameInput.value.trim()) {
|
||||
nameInput.value = file.name;
|
||||
}
|
||||
};
|
||||
|
||||
if (state.file) {
|
||||
selectedFileName.textContent = state.file.name;
|
||||
if (!nameInput.value.trim()) {
|
||||
nameInput.value = state.file.name;
|
||||
}
|
||||
}
|
||||
|
||||
fileInput.addEventListener("change", () => {
|
||||
const file = fileInput.files?.[0];
|
||||
if (file) selectFile(file);
|
||||
});
|
||||
|
||||
dropZone.addEventListener("click", (event) => {
|
||||
if (event.target !== fileInput) fileInput.click();
|
||||
});
|
||||
|
||||
dropZone.addEventListener("dragover", (event) => {
|
||||
event.preventDefault();
|
||||
dropZone.classList.add("is-dragover");
|
||||
});
|
||||
dropZone.addEventListener("dragleave", () => {
|
||||
dropZone.classList.remove("is-dragover");
|
||||
});
|
||||
dropZone.addEventListener("drop", (event) => {
|
||||
event.preventDefault();
|
||||
dropZone.classList.remove("is-dragover");
|
||||
const file = event.dataTransfer?.files?.[0];
|
||||
if (file) selectFile(file);
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeOperationTreeInput(value?: string) {
|
||||
const clean = value?.trim();
|
||||
if (!clean) return defaultOperationTree;
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(clean);
|
||||
} catch {
|
||||
throw new Error("工艺数据必须是合法 JSON 字符串");
|
||||
}
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
return JSON.stringify({ OperationTree: JSON.stringify(parsed) });
|
||||
}
|
||||
|
||||
if (parsed && typeof parsed === "object" && "OperationTree" in parsed) {
|
||||
const operationTree = (parsed as { OperationTree: unknown }).OperationTree;
|
||||
if (typeof operationTree === "string") {
|
||||
JSON.parse(operationTree);
|
||||
return JSON.stringify({ OperationTree: operationTree });
|
||||
}
|
||||
if (Array.isArray(operationTree)) {
|
||||
return JSON.stringify({ OperationTree: JSON.stringify(operationTree) });
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('工艺数据格式必须是 {"OperationTree":"[...]"} 或 OperationTree[] 数组');
|
||||
}
|
||||
|
||||
async function editModel(id: number) {
|
||||
const card = document.querySelector<HTMLButtonElement>(`button[data-id="${id}"]`)?.closest(".model-card");
|
||||
const oldName = card?.querySelector("strong")?.textContent ?? "";
|
||||
|
||||
Reference in New Issue
Block a user