fix model del & rename
This commit is contained in:
@@ -222,6 +222,24 @@
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<div id="action-dialog" class="action-dialog" hidden role="dialog" aria-modal="true" aria-labelledby="action-dialog-title">
|
||||
<form id="action-dialog-form" class="action-dialog-shell">
|
||||
<div class="action-dialog-head">
|
||||
<p class="section-kicker">MODEL CONTROL</p>
|
||||
<h2 id="action-dialog-title">模型操作</h2>
|
||||
</div>
|
||||
<p id="action-dialog-message" class="action-dialog-message"></p>
|
||||
<label class="action-dialog-field">
|
||||
<span>文件名</span>
|
||||
<input id="action-dialog-input" type="text" autocomplete="off" spellcheck="false">
|
||||
</label>
|
||||
<p id="action-dialog-error" class="action-dialog-error" hidden></p>
|
||||
<div class="action-dialog-actions">
|
||||
<button id="action-dialog-cancel" class="button secondary" type="button">取消</button>
|
||||
<button id="action-dialog-confirm" class="button primary" type="submit">确认</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="image-lightbox" class="lightbox" hidden role="dialog" aria-modal="true" aria-labelledby="lightbox-title">
|
||||
<div class="lightbox-shell">
|
||||
<div class="lightbox-toolbar"><strong id="lightbox-title">图像预览</strong><div class="lightbox-actions"><button id="zoom-out" class="button icon" title="缩小" aria-label="缩小图像">-</button><button id="zoom-in" class="button icon" title="放大" aria-label="放大图像">+</button><button id="zoom-fit" class="button secondary" type="button">适应窗口</button><button id="zoom-reset" class="button secondary" type="button">原始比例</button><button id="close-lightbox" class="button icon" title="关闭" aria-label="关闭图像预览">x</button></div></div>
|
||||
|
||||
@@ -96,6 +96,14 @@ const elements = {
|
||||
reviewTarget: document.querySelector("#review-target"),
|
||||
approveReview: document.querySelector("#approve-review"),
|
||||
rejectReview: document.querySelector("#reject-review"),
|
||||
actionDialog: document.querySelector("#action-dialog"),
|
||||
actionDialogForm: document.querySelector("#action-dialog-form"),
|
||||
actionDialogTitle: document.querySelector("#action-dialog-title"),
|
||||
actionDialogMessage: document.querySelector("#action-dialog-message"),
|
||||
actionDialogInput: document.querySelector("#action-dialog-input"),
|
||||
actionDialogError: document.querySelector("#action-dialog-error"),
|
||||
actionDialogCancel: document.querySelector("#action-dialog-cancel"),
|
||||
actionDialogConfirm: document.querySelector("#action-dialog-confirm"),
|
||||
lightbox: document.querySelector("#image-lightbox"),
|
||||
lightboxTitle: document.querySelector("#lightbox-title"),
|
||||
lightboxImage: document.querySelector("#lightbox-image"),
|
||||
@@ -346,6 +354,65 @@ function showError(error) {
|
||||
elements.publishResult.dataset.tone = "error";
|
||||
}
|
||||
|
||||
let actionDialogState = null;
|
||||
|
||||
function closeActionDialog(value) {
|
||||
if (!actionDialogState) return;
|
||||
const { resolve, previousFocus } = actionDialogState;
|
||||
actionDialogState = null;
|
||||
elements.actionDialog.hidden = true;
|
||||
elements.actionDialogForm.reset();
|
||||
elements.actionDialogError.hidden = true;
|
||||
previousFocus?.focus();
|
||||
resolve(value);
|
||||
}
|
||||
|
||||
function requestModelName({ title, message, value = "", confirmLabel = "确认", danger = false, expectedValue = null }) {
|
||||
if (actionDialogState) closeActionDialog(null);
|
||||
elements.actionDialogTitle.textContent = title;
|
||||
elements.actionDialogMessage.textContent = message;
|
||||
elements.actionDialogInput.value = value;
|
||||
elements.actionDialogConfirm.textContent = confirmLabel;
|
||||
elements.actionDialogConfirm.classList.toggle("primary", !danger);
|
||||
elements.actionDialogConfirm.classList.toggle("danger", danger);
|
||||
elements.actionDialogError.hidden = true;
|
||||
elements.actionDialog.hidden = false;
|
||||
const previousFocus = document.activeElement;
|
||||
window.requestAnimationFrame(() => {
|
||||
elements.actionDialogInput.focus();
|
||||
elements.actionDialogInput.select();
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
actionDialogState = { resolve, previousFocus, expectedValue };
|
||||
});
|
||||
}
|
||||
|
||||
elements.actionDialogForm.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
const value = elements.actionDialogInput.value.trim();
|
||||
const expectedValue = actionDialogState?.expectedValue;
|
||||
if (!value) {
|
||||
elements.actionDialogError.textContent = "文件名不能为空";
|
||||
elements.actionDialogError.hidden = false;
|
||||
elements.actionDialogInput.focus();
|
||||
return;
|
||||
}
|
||||
if (expectedValue !== null && value !== expectedValue) {
|
||||
elements.actionDialogError.textContent = "文件名不匹配,请输入完整文件名";
|
||||
elements.actionDialogError.hidden = false;
|
||||
elements.actionDialogInput.focus();
|
||||
return;
|
||||
}
|
||||
closeActionDialog(value);
|
||||
});
|
||||
elements.actionDialogCancel.addEventListener("click", () => closeActionDialog(null));
|
||||
elements.actionDialog.addEventListener("click", (event) => {
|
||||
if (event.target === elements.actionDialog) closeActionDialog(null);
|
||||
});
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape" && !elements.actionDialog.hidden) closeActionDialog(null);
|
||||
});
|
||||
|
||||
function showImage(result) {
|
||||
if (!result) return;
|
||||
const mediaType = result.mediaType === "json" || result.fileName?.toLowerCase().endsWith(".json")
|
||||
@@ -698,19 +765,24 @@ document.querySelector("#upload-model").addEventListener("click", async () => {
|
||||
state.models = models.fileList;
|
||||
const selected = await runBusy("正在选择模型文件", () => window.reinloop.chooseModelUploadFile());
|
||||
if (!selected) return;
|
||||
const enteredModelName = window.prompt("请输入模型在服务器上的文件名:", selected.fileName);
|
||||
if (enteredModelName === null) return;
|
||||
const modelName = enteredModelName.trim();
|
||||
if (!modelName) return showError(new Error("服务器文件名不能为空"));
|
||||
const modelName = await requestModelName({
|
||||
title: "设置服务器文件名",
|
||||
message: `已选择 ${selected.fileName}。可在上传前修改模型在服务器上的文件名。`,
|
||||
value: selected.fileName,
|
||||
confirmLabel: "继续上传"
|
||||
});
|
||||
if (modelName === null) return;
|
||||
const existing = state.models.find((model) => model.fileName === modelName);
|
||||
let overwrite = false;
|
||||
if (existing) {
|
||||
if (!window.confirm(`服务器已存在模型 ${modelName},覆盖后无法恢复。是否继续?`)) return;
|
||||
const confirmation = window.prompt(`请输入服务器文件名以确认覆盖:${modelName}`);
|
||||
if (confirmation !== modelName) {
|
||||
setStatus("文件名不匹配,已取消覆盖", "idle");
|
||||
return;
|
||||
}
|
||||
const confirmation = await requestModelName({
|
||||
title: "确认覆盖模型",
|
||||
message: `服务器已存在 ${modelName},覆盖后无法恢复。请输入完整文件名以确认。`,
|
||||
confirmLabel: "覆盖并上传",
|
||||
danger: true,
|
||||
expectedValue: modelName
|
||||
});
|
||||
if (confirmation === null) return;
|
||||
overwrite = true;
|
||||
}
|
||||
const result = await runBusy("正在上传模型", () => window.reinloop.uploadModel({
|
||||
@@ -726,25 +798,33 @@ document.querySelector("#upload-model").addEventListener("click", async () => {
|
||||
}
|
||||
});
|
||||
elements.modelList.addEventListener("click", async (event) => {
|
||||
const fileID = event.target.dataset.modelDownload || event.target.dataset.modelDelete;
|
||||
const button = event.target.closest("button");
|
||||
if (!button || !elements.modelList.contains(button)) return;
|
||||
const fileID = button.dataset.modelDownload || button.dataset.modelDelete;
|
||||
if (!fileID) return;
|
||||
const model = state.models.find((item) => item.fileID === fileID);
|
||||
if (event.target.dataset.modelDownload) {
|
||||
if (!model) return showError(new Error("模型记录已变化,请刷新列表后重试"));
|
||||
if (button.dataset.modelDownload) {
|
||||
const result = await runBusy("正在下载模型", () => window.reinloop.downloadModel({ fileID, fileName: model.fileName, credentials: credentials() }));
|
||||
if (result) setStatus(`模型已下载: ${result.filePath}`, "success");
|
||||
} else {
|
||||
if (!window.confirm(`确认删除模型 ${model.fileName}?`)) return;
|
||||
const confirmation = window.prompt(`删除不可恢复。请输入完整文件名以确认:${model.fileName}`);
|
||||
if (confirmation !== model.fileName) {
|
||||
setStatus("文件名不匹配,已取消删除", "idle");
|
||||
return;
|
||||
}
|
||||
event.target.disabled = true;
|
||||
const confirmation = await requestModelName({
|
||||
title: "确认删除模型",
|
||||
message: `删除 ${model.fileName} 后无法恢复。请输入完整文件名以确认。`,
|
||||
confirmLabel: "永久删除",
|
||||
danger: true,
|
||||
expectedValue: model.fileName
|
||||
});
|
||||
if (confirmation === null) return;
|
||||
button.disabled = true;
|
||||
try {
|
||||
const result = await runBusy("正在删除模型", () => window.reinloop.deleteModel({ fileID, credentials: credentials() }));
|
||||
if (result) await refreshModels();
|
||||
if (result) {
|
||||
await refreshModels();
|
||||
setStatus("模型已删除", "success");
|
||||
}
|
||||
} finally {
|
||||
event.target.disabled = false;
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -237,6 +237,16 @@ td:last-child { white-space: nowrap; }
|
||||
.empty-row { padding: 30px; color: var(--muted); text-align: center; font-size: 13px; }
|
||||
.detail-view { margin: 0; padding: 16px; max-height: 260px; overflow: auto; background: #18251f; color: #d9e9df; font: 12px/1.6 Consolas, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
|
||||
.action-dialog { position: fixed; inset: 0; z-index: 30; display: grid; place-items: center; padding: 24px; background: rgba(10, 20, 16, 0.68); }
|
||||
.action-dialog-shell { width: min(520px, 100%); padding: 24px; display: grid; gap: 17px; background: var(--surface); border: 1px solid #9aa9a1; border-top: 4px solid var(--green); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); }
|
||||
.action-dialog-head { display: grid; gap: 2px; }
|
||||
.action-dialog-message { color: var(--muted); font-size: 13px; line-height: 1.65; overflow-wrap: anywhere; }
|
||||
.action-dialog-field { display: grid; gap: 7px; }
|
||||
.action-dialog-field span { color: var(--muted); font-size: 12px; font-weight: 700; }
|
||||
.action-dialog-field input { width: 100%; }
|
||||
.action-dialog-error { color: var(--red); font-size: 12px; }
|
||||
.action-dialog-actions { display: flex; justify-content: end; gap: 8px; padding-top: 3px; }
|
||||
|
||||
.lightbox { position: fixed; inset: 0; z-index: 20; padding: 24px; background: rgba(10, 20, 16, 0.78); }
|
||||
.lightbox-shell { height: 100%; display: grid; grid-template-rows: auto minmax(0, 1fr); background: var(--surface); border: 1px solid #9aa9a1; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35); }
|
||||
.lightbox-toolbar { min-height: 58px; padding: 10px 14px; display: flex; align-items: center; justify-content: space-between; gap: 16px; border-bottom: 1px solid var(--line); }
|
||||
|
||||
@@ -17,9 +17,9 @@ function createModelHandlers({ callServer, fetchImpl = fetch }) {
|
||||
const modelName = path.basename(request.modelName || fileName);
|
||||
await fs.promises.access(sourcePath, fs.constants.R_OK);
|
||||
const issued = await callServer({
|
||||
type: "uploadDataFile",
|
||||
type: "issueModelUpload",
|
||||
deviceId: request.deviceId,
|
||||
fileName,
|
||||
folder: `${request.deviceId}/model_config`,
|
||||
modelName,
|
||||
overwrite: request.overwrite === true
|
||||
}, request.credentials);
|
||||
|
||||
@@ -15,8 +15,8 @@ test("panel model kernel renames, overwrites, and deletes by fileID", async (con
|
||||
const requests = [];
|
||||
const callServer = async (payload) => {
|
||||
requests.push(payload);
|
||||
if (payload.type === "uploadDataFile") {
|
||||
const fileID = `model://${payload.folder.replace(/\/model_config$/, "")}/${payload.modelName}`;
|
||||
if (payload.type === "issueModelUpload") {
|
||||
const fileID = `model://${payload.deviceId}/${payload.modelName}`;
|
||||
if (records.has(fileID) && !payload.overwrite) throw new Error("文件已存在");
|
||||
records.set(fileID, {
|
||||
fileID,
|
||||
@@ -47,6 +47,7 @@ test("panel model kernel renames, overwrites, and deletes by fileID", async (con
|
||||
assert.equal(uploaded.fileName, "pressure-controller.bin");
|
||||
assert.equal(uploaded.originalFileName, "controller-original.bin");
|
||||
assert.equal(records.size, 1);
|
||||
assert.equal(requests[0].deviceId, request.deviceId);
|
||||
|
||||
await fs.promises.writeFile(sourcePath, "second-version");
|
||||
const overwritten = await handlers.upload({ ...request, overwrite: true });
|
||||
|
||||
Reference in New Issue
Block a user