Compare commits
10 Commits
738476c7f9
...
a059805458
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a059805458 | ||
|
|
6206c9556b | ||
|
|
00f7dc1686 | ||
|
|
57d3137640 | ||
|
|
cc58a6e98d | ||
|
|
b6c3bd9922 | ||
|
|
c14a6aa75c | ||
|
|
692d61105e | ||
|
|
fcec4f3db4 | ||
|
|
3dfed3a2df |
@@ -115,18 +115,18 @@
|
||||
overflow: auto;
|
||||
padding: 10px 12px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
align-content: start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.model-card {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
gap: 5px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #d7e0e8;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
|
||||
@@ -63,7 +63,8 @@ async function runFolderAction(action: () => Promise<void>) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadFolders() {
|
||||
export async function loadFolders(options: { reloadModels?: boolean } = {}) {
|
||||
const reloadModels = options.reloadModels ?? true;
|
||||
const result = await api<FolderTreeResponse>("/api/folders");
|
||||
appState.folders = result.folders;
|
||||
const root = appState.folders.find((folder) => folder.parent_id === null);
|
||||
@@ -125,8 +126,10 @@ export async function loadFolders() {
|
||||
|
||||
if (appState.selectedFolderId) {
|
||||
$("#folderTree").on("ready.jstree", () => {
|
||||
$("#folderTree").jstree(true).select_node(String(appState.selectedFolderId));
|
||||
$("#folderTree").jstree(true).select_node(String(appState.selectedFolderId), true);
|
||||
});
|
||||
}
|
||||
await loadModels();
|
||||
if (reloadModels) {
|
||||
await loadModels();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ type SceneModelMessagePayload = {
|
||||
|
||||
type SceneModelMessage = {
|
||||
type?: string;
|
||||
requestId?: string;
|
||||
payload?: SceneModelMessagePayload;
|
||||
message?: string;
|
||||
};
|
||||
@@ -42,6 +43,9 @@ type SceneModelMessage = {
|
||||
let dictionariesLoaded = false;
|
||||
let dictionariesLoading: Promise<void> | null = null;
|
||||
let sceneModelBridgeBound = false;
|
||||
let pendingSceneModelRequestId = "";
|
||||
let pendingSceneModelRequestTimer: number | null = null;
|
||||
let visibleModels = new Map<number, ModelItem>();
|
||||
|
||||
export function bindModelActions() {
|
||||
const isAdmin = getCurrentUser()?.role === "admin";
|
||||
@@ -120,7 +124,7 @@ export function bindModelActions() {
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadModels() {
|
||||
export async function loadModels(options: { preserveCards?: boolean } = {}) {
|
||||
const grid = document.querySelector<HTMLDivElement>("#modelGrid");
|
||||
if (!grid || !appState.selectedFolderId) return;
|
||||
await loadDictionaries();
|
||||
@@ -150,37 +154,85 @@ export async function loadModels() {
|
||||
await loadModels();
|
||||
}
|
||||
});
|
||||
grid.innerHTML = result.items.map(renderModelCard).join("") || `<div class="empty-state">当前目录暂无模型</div>`;
|
||||
grid.querySelectorAll<HTMLButtonElement>("[data-action='preview']").forEach((button) => {
|
||||
const model = result.items.find((item) => item.id === Number(button.dataset.id));
|
||||
if (model) {
|
||||
button.addEventListener("click", async () => {
|
||||
visibleModels = new Map(result.items.map((item) => [item.id, item]));
|
||||
renderModelGrid(grid, result.items, options.preserveCards);
|
||||
bindModelGridActions(grid);
|
||||
}
|
||||
|
||||
function bindModelGridActions(grid: HTMLDivElement) {
|
||||
if (grid.dataset.actionsBound === "true") return;
|
||||
grid.dataset.actionsBound = "true";
|
||||
grid.addEventListener("click", async (event) => {
|
||||
const target = event.target instanceof Element ? event.target : null;
|
||||
const button = target?.closest<HTMLButtonElement>("button[data-action][data-id]");
|
||||
if (!button || !grid.contains(button)) return;
|
||||
const id = Number(button.dataset.id);
|
||||
const model = visibleModels.get(id);
|
||||
try {
|
||||
switch (button.dataset.action) {
|
||||
case "preview": {
|
||||
if (!model) return;
|
||||
const { openModelPreview } = await import("./preview");
|
||||
openModelPreview(model, loadModels);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "edit":
|
||||
await editModel(id);
|
||||
break;
|
||||
case "import": {
|
||||
if (!model) return;
|
||||
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("导入场景数据已输出");
|
||||
break;
|
||||
}
|
||||
case "delete":
|
||||
await deleteModel(id);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
}
|
||||
});
|
||||
grid.querySelectorAll<HTMLButtonElement>("[data-action='edit']").forEach((button) => {
|
||||
button.addEventListener("click", () => editModel(Number(button.dataset.id)));
|
||||
});
|
||||
grid.querySelectorAll<HTMLButtonElement>("[data-action='import']").forEach((button) => {
|
||||
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) => {
|
||||
button.addEventListener("click", () => deleteModel(Number(button.dataset.id)));
|
||||
});
|
||||
}
|
||||
|
||||
function renderModelGrid(grid: HTMLDivElement, items: ModelItem[], preserveCards = false) {
|
||||
if (!preserveCards) {
|
||||
grid.innerHTML = items.map(renderModelCard).join("") || `<div class="empty-state">当前目录暂无模型</div>`;
|
||||
return;
|
||||
}
|
||||
if (items.length === 0) {
|
||||
grid.innerHTML = `<div class="empty-state">当前目录暂无模型</div>`;
|
||||
return;
|
||||
}
|
||||
const existingCards = new Map(
|
||||
Array.from(grid.querySelectorAll<HTMLElement>(".model-card[data-model-id]"))
|
||||
.map((card) => [Number(card.dataset.modelId), card])
|
||||
);
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (const item of items) {
|
||||
const existingCard = existingCards.get(item.id);
|
||||
if (existingCard) {
|
||||
fragment.append(existingCard);
|
||||
continue;
|
||||
}
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = renderModelCard(item).trim();
|
||||
fragment.append(template.content);
|
||||
}
|
||||
grid.replaceChildren(fragment);
|
||||
}
|
||||
|
||||
async function reloadFoldersAndModels(preserveModelCards = false) {
|
||||
const { loadFolders } = await import("./folders");
|
||||
await loadFolders({ reloadModels: false });
|
||||
await loadModels({ preserveCards: preserveModelCards });
|
||||
}
|
||||
|
||||
async function loadDictionaries(force = false) {
|
||||
@@ -300,13 +352,27 @@ function requestSceneModelImport() {
|
||||
notify("请在 DMT 主程序中使用导入场景模型");
|
||||
return;
|
||||
}
|
||||
const requestId = createSceneModelRequestId();
|
||||
pendingSceneModelRequestId = requestId;
|
||||
clearSceneModelRequestTimer();
|
||||
pendingSceneModelRequestTimer = window.setTimeout(() => {
|
||||
if (pendingSceneModelRequestId !== requestId) return;
|
||||
pendingSceneModelRequestId = "";
|
||||
pendingSceneModelRequestTimer = null;
|
||||
notifyError("主程序未响应导入场景模型请求,请确认模型库是在主程序面板中打开");
|
||||
}, 15000);
|
||||
window.parent.postMessage({
|
||||
type: requestSceneModelMessageType
|
||||
}, resolveParentOrigin());
|
||||
notify("已请求主程序导出当前选中模型");
|
||||
type: requestSceneModelMessageType,
|
||||
requestId
|
||||
}, resolveParentPostMessageOrigin());
|
||||
// notify("已请求主程序导出当前选中模型");
|
||||
}
|
||||
|
||||
async function handleSceneModelMessage(data: SceneModelMessage) {
|
||||
console.log("Received scene model message:", data);
|
||||
if (pendingSceneModelRequestId && data.requestId && data.requestId !== pendingSceneModelRequestId) return;
|
||||
pendingSceneModelRequestId = "";
|
||||
clearSceneModelRequestTimer();
|
||||
if (data.type === sceneModelErrorMessageType) {
|
||||
notifyError(data.message || "主程序导出模型失败");
|
||||
return;
|
||||
@@ -327,9 +393,24 @@ async function handleSceneModelMessage(data: SceneModelMessage) {
|
||||
});
|
||||
}
|
||||
|
||||
function clearSceneModelRequestTimer() {
|
||||
if (pendingSceneModelRequestTimer === null) return;
|
||||
window.clearTimeout(pendingSceneModelRequestTimer);
|
||||
pendingSceneModelRequestTimer = null;
|
||||
}
|
||||
|
||||
function createSceneModelRequestId() {
|
||||
return `scene-model-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
function isAllowedParentOrigin(origin: string) {
|
||||
const expectedOrigin = resolveParentOrigin();
|
||||
return expectedOrigin === "*" || origin === expectedOrigin;
|
||||
return expectedOrigin === "*" || origin === expectedOrigin || isLocalDmtParentOrigin(origin);
|
||||
}
|
||||
|
||||
function resolveParentPostMessageOrigin() {
|
||||
const origin = resolveParentOrigin();
|
||||
return origin === window.location.origin ? "*" : origin;
|
||||
}
|
||||
|
||||
function resolveParentOrigin() {
|
||||
@@ -344,6 +425,16 @@ function resolveParentOrigin() {
|
||||
return "*";
|
||||
}
|
||||
|
||||
function isLocalDmtParentOrigin(origin: string) {
|
||||
try {
|
||||
const url = new URL(origin);
|
||||
const isLocalHost = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
||||
return isLocalHost && url.port === "3000";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureGlbFileName(fileName: string) {
|
||||
return fileName.toLowerCase().endsWith(".glb") ? fileName : `${fileName}.glb`;
|
||||
}
|
||||
@@ -446,7 +537,7 @@ async function openUploadModelDialog() {
|
||||
|
||||
if (result) {
|
||||
invalidateDictionaries();
|
||||
await loadModels();
|
||||
await reloadFoldersAndModels();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,40 +555,20 @@ export async function openProcessModelUploadDialog(input?: {
|
||||
await loadDictionaries();
|
||||
const initialName = input?.modelName || (input?.file ? modelNameFromFile(input.file.name) : "");
|
||||
const result = await formDialog<boolean>({
|
||||
title: "上传工艺模型",
|
||||
width: 640,
|
||||
height: 650,
|
||||
title: `上传工艺模型-【${appState.selectedFolderName || "当前目录"}】`,
|
||||
width: 400,
|
||||
height: 300,
|
||||
blockPage: false,
|
||||
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>
|
||||
<label><span>模型名称</span><input id="processModelUploadName" name="name" value="${escapeHtml(initialName)}" autocomplete="off" /></label>
|
||||
<label><span>品牌</span><input name="brandName" list="brandOptions" autocomplete="off" /></label>
|
||||
<label><span>类型</span><input name="typeName" list="typeOptions" autocomplete="off" /></label>
|
||||
<label><span>型号</span><input name="model" autocomplete="off" /></label>
|
||||
<label><span>价钱</span><input name="price" autocomplete="off" /></label>
|
||||
<label><span>重量</span><input name="weight" autocomplete="off" /></label>
|
||||
</div>
|
||||
<label>
|
||||
<span>工艺数据 JSON</span>
|
||||
<textarea id="processOperationTreeJson" name="operationTree" placeholder='可以粘贴 {"OperationTree":"[...]"} 或 OperationTree[] 数组 JSON'>${escapeHtml(input?.operationTreeJson ?? "")}</textarea>
|
||||
</label>
|
||||
${dictionaryDatalistHtml()}
|
||||
</form>
|
||||
`,
|
||||
@@ -527,7 +598,7 @@ export async function openProcessModelUploadDialog(input?: {
|
||||
|
||||
if (result) {
|
||||
invalidateDictionaries();
|
||||
await loadModels();
|
||||
await reloadFoldersAndModels();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -574,52 +645,52 @@ 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 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;
|
||||
}
|
||||
};
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
});
|
||||
// fileInput.addEventListener("change", () => {
|
||||
// const file = fileInput.files?.[0];
|
||||
// if (file) selectFile(file);
|
||||
// });
|
||||
|
||||
dropZone.addEventListener("click", (event) => {
|
||||
if (event.target !== fileInput) fileInput.click();
|
||||
});
|
||||
// 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);
|
||||
});
|
||||
// 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) {
|
||||
@@ -740,5 +811,5 @@ async function deleteModel(id: number) {
|
||||
const confirmed = await confirmDialog("确认删除该模型?");
|
||||
if (!confirmed) return;
|
||||
await api(`/api/models/${id}`, { method: "DELETE" });
|
||||
await loadModels();
|
||||
await reloadFoldersAndModels(true);
|
||||
}
|
||||
|
||||
@@ -258,7 +258,7 @@ export function emitImportScenePayload(payload: PreviewImportScenePayload) {
|
||||
window.parent.postMessage({
|
||||
type: "DMT_MODEL_LIBRARY_IMPORT_SCENE",
|
||||
payload: buildParentImportScenePayload(payload)
|
||||
}, resolveParentOrigin());
|
||||
}, resolveParentPostMessageOrigin());
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
}
|
||||
@@ -280,6 +280,11 @@ function buildParentImportScenePayload(payload: PreviewImportScenePayload): Pare
|
||||
};
|
||||
}
|
||||
|
||||
function resolveParentPostMessageOrigin() {
|
||||
const origin = resolveParentOrigin();
|
||||
return origin === window.location.origin ? "*" : origin;
|
||||
}
|
||||
|
||||
function resolveParentOrigin() {
|
||||
try {
|
||||
const meta = import.meta as ImportMeta & { env?: Record<string, string | undefined> };
|
||||
@@ -624,6 +629,10 @@ function bindProcessPlaceholder(operations: PreviewOperation[]) {
|
||||
});
|
||||
|
||||
document.querySelector<HTMLButtonElement>("#previewPlayBtn")?.addEventListener("click", () => {
|
||||
if (operations.length === 0) {
|
||||
notify("当前模型暂无工艺数据");
|
||||
return;
|
||||
}
|
||||
const operation = getSelectedOperation(operations);
|
||||
updateStatus(operation);
|
||||
if (!operation) {
|
||||
@@ -634,7 +643,7 @@ function bindProcessPlaceholder(operations: PreviewOperation[]) {
|
||||
notifyError(`工艺播放数据解析失败:${operation.parseError}`);
|
||||
return;
|
||||
}
|
||||
if (!operation.parsedCraftPlayData) {
|
||||
if (!operation.parsedCraftPlayData || operation.parsedCraftPlayData.OPERATION.frames.length === 0) {
|
||||
notify("当前工艺暂无播放数据");
|
||||
return;
|
||||
}
|
||||
@@ -648,10 +657,14 @@ function bindProcessPlaceholder(operations: PreviewOperation[]) {
|
||||
}
|
||||
|
||||
function startPreviewPlayback(operation: PreviewOperation) {
|
||||
if (!runtime?.context.modelObject || !operation.parsedCraftPlayData) {
|
||||
if (!runtime?.context.modelObject) {
|
||||
notify("模型还未加载完成");
|
||||
return;
|
||||
}
|
||||
if (!operation.parsedCraftPlayData || operation.parsedCraftPlayData.OPERATION.frames.length === 0) {
|
||||
notify("当前工艺暂无播放数据");
|
||||
return;
|
||||
}
|
||||
|
||||
if (previewPlayback.isPlaying && previewPlayback.operation === operation) {
|
||||
return;
|
||||
@@ -868,13 +881,7 @@ function resetPreviewPlaybackScene() {
|
||||
}
|
||||
|
||||
function buildPreviewFrameTimes(frames: P_OPERATION["OPERATION"]["frames"]) {
|
||||
let previousTime = 0;
|
||||
return frames.map((frame, index) => {
|
||||
const parsed = Number(frame.time);
|
||||
const timeMs = Number.isFinite(parsed) ? Math.max(0, parsed * SECOND_MS) : index * DEFAULT_PREVIEW_FRAME_INTERVAL_MS;
|
||||
previousTime = index === 0 ? timeMs : Math.max(previousTime + 1, timeMs);
|
||||
return previousTime;
|
||||
});
|
||||
return frames.map((_, index) => index * DEFAULT_PREVIEW_FRAME_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function resolvePreviewFrameIndex(currentTimeMs: number, frameTimesMs: number[]) {
|
||||
|
||||
@@ -6,10 +6,11 @@ function authHeaders(): Record<string, string> {
|
||||
}
|
||||
|
||||
export async function api<T>(url: string, options: RequestInit = {}): Promise<T> {
|
||||
const hasBody = options.body !== undefined && options.body !== null;
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...(options.body instanceof FormData ? {} : { "Content-Type": "application/json" }),
|
||||
...(hasBody && !(options.body instanceof FormData) ? { "Content-Type": "application/json" } : {}),
|
||||
...authHeaders(),
|
||||
...((options.headers as Record<string, string> | undefined) ?? {})
|
||||
} as HeadersInit
|
||||
@@ -20,4 +21,3 @@ export async function api<T>(url: string, options: RequestInit = {}): Promise<T>
|
||||
}
|
||||
return data as T;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { w2confirm, w2popup, w2utils } from "../vendor/w2ui";
|
||||
import { w2popup, w2utils } from "../vendor/w2ui";
|
||||
|
||||
type PopupActionEvent = {
|
||||
detail: {
|
||||
@@ -23,14 +23,27 @@ export function notifyError(error: unknown) {
|
||||
|
||||
export function confirmDialog(message: string, title = "确认") {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
w2confirm({
|
||||
msg: message,
|
||||
let settled = false;
|
||||
const settle = (value: boolean) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
resolve(value);
|
||||
};
|
||||
|
||||
w2utils.confirm({
|
||||
box: "body",
|
||||
title,
|
||||
yes: "确定",
|
||||
no: "取消"
|
||||
}, undefined, (action: string) => {
|
||||
resolve(action === "yes" || action === "Yes");
|
||||
});
|
||||
text: message,
|
||||
btn_yes: {
|
||||
text: "确定"
|
||||
},
|
||||
btn_no: {
|
||||
text: "取消"
|
||||
}
|
||||
})
|
||||
.yes(() => settle(true))
|
||||
.no(() => settle(false))
|
||||
.close(() => settle(false));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
23
web/src/vendor/w2ui.ts
vendored
23
web/src/vendor/w2ui.ts
vendored
@@ -28,6 +28,29 @@ type W2Utils = {
|
||||
title?: string;
|
||||
text?: string;
|
||||
}): unknown;
|
||||
confirm(options: {
|
||||
box?: string | HTMLElement;
|
||||
title?: string;
|
||||
text?: string;
|
||||
btn_yes?: {
|
||||
text?: string;
|
||||
class?: string;
|
||||
style?: string;
|
||||
attrs?: string;
|
||||
};
|
||||
btn_no?: {
|
||||
text?: string;
|
||||
class?: string;
|
||||
style?: string;
|
||||
attrs?: string;
|
||||
};
|
||||
}): W2ConfirmPromise;
|
||||
};
|
||||
|
||||
type W2ConfirmPromise = {
|
||||
yes(callback: (event: unknown) => void): W2ConfirmPromise;
|
||||
no(callback: (event: unknown) => void): W2ConfirmPromise;
|
||||
close(callback: (event: unknown) => void): W2ConfirmPromise;
|
||||
};
|
||||
|
||||
type W2LayoutOptions = {
|
||||
|
||||
Reference in New Issue
Block a user