fix model del & rename
This commit is contained in:
@@ -222,6 +222,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</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 id="image-lightbox" class="lightbox" hidden role="dialog" aria-modal="true" aria-labelledby="lightbox-title">
|
||||||
<div class="lightbox-shell">
|
<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>
|
<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"),
|
reviewTarget: document.querySelector("#review-target"),
|
||||||
approveReview: document.querySelector("#approve-review"),
|
approveReview: document.querySelector("#approve-review"),
|
||||||
rejectReview: document.querySelector("#reject-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"),
|
lightbox: document.querySelector("#image-lightbox"),
|
||||||
lightboxTitle: document.querySelector("#lightbox-title"),
|
lightboxTitle: document.querySelector("#lightbox-title"),
|
||||||
lightboxImage: document.querySelector("#lightbox-image"),
|
lightboxImage: document.querySelector("#lightbox-image"),
|
||||||
@@ -346,6 +354,65 @@ function showError(error) {
|
|||||||
elements.publishResult.dataset.tone = "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) {
|
function showImage(result) {
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
const mediaType = result.mediaType === "json" || result.fileName?.toLowerCase().endsWith(".json")
|
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;
|
state.models = models.fileList;
|
||||||
const selected = await runBusy("正在选择模型文件", () => window.reinloop.chooseModelUploadFile());
|
const selected = await runBusy("正在选择模型文件", () => window.reinloop.chooseModelUploadFile());
|
||||||
if (!selected) return;
|
if (!selected) return;
|
||||||
const enteredModelName = window.prompt("请输入模型在服务器上的文件名:", selected.fileName);
|
const modelName = await requestModelName({
|
||||||
if (enteredModelName === null) return;
|
title: "设置服务器文件名",
|
||||||
const modelName = enteredModelName.trim();
|
message: `已选择 ${selected.fileName}。可在上传前修改模型在服务器上的文件名。`,
|
||||||
if (!modelName) return showError(new Error("服务器文件名不能为空"));
|
value: selected.fileName,
|
||||||
|
confirmLabel: "继续上传"
|
||||||
|
});
|
||||||
|
if (modelName === null) return;
|
||||||
const existing = state.models.find((model) => model.fileName === modelName);
|
const existing = state.models.find((model) => model.fileName === modelName);
|
||||||
let overwrite = false;
|
let overwrite = false;
|
||||||
if (existing) {
|
if (existing) {
|
||||||
if (!window.confirm(`服务器已存在模型 ${modelName},覆盖后无法恢复。是否继续?`)) return;
|
const confirmation = await requestModelName({
|
||||||
const confirmation = window.prompt(`请输入服务器文件名以确认覆盖:${modelName}`);
|
title: "确认覆盖模型",
|
||||||
if (confirmation !== modelName) {
|
message: `服务器已存在 ${modelName},覆盖后无法恢复。请输入完整文件名以确认。`,
|
||||||
setStatus("文件名不匹配,已取消覆盖", "idle");
|
confirmLabel: "覆盖并上传",
|
||||||
return;
|
danger: true,
|
||||||
}
|
expectedValue: modelName
|
||||||
|
});
|
||||||
|
if (confirmation === null) return;
|
||||||
overwrite = true;
|
overwrite = true;
|
||||||
}
|
}
|
||||||
const result = await runBusy("正在上传模型", () => window.reinloop.uploadModel({
|
const result = await runBusy("正在上传模型", () => window.reinloop.uploadModel({
|
||||||
@@ -726,25 +798,33 @@ document.querySelector("#upload-model").addEventListener("click", async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
elements.modelList.addEventListener("click", async (event) => {
|
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;
|
if (!fileID) return;
|
||||||
const model = state.models.find((item) => item.fileID === fileID);
|
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() }));
|
const result = await runBusy("正在下载模型", () => window.reinloop.downloadModel({ fileID, fileName: model.fileName, credentials: credentials() }));
|
||||||
if (result) setStatus(`模型已下载: ${result.filePath}`, "success");
|
if (result) setStatus(`模型已下载: ${result.filePath}`, "success");
|
||||||
} else {
|
} else {
|
||||||
if (!window.confirm(`确认删除模型 ${model.fileName}?`)) return;
|
const confirmation = await requestModelName({
|
||||||
const confirmation = window.prompt(`删除不可恢复。请输入完整文件名以确认:${model.fileName}`);
|
title: "确认删除模型",
|
||||||
if (confirmation !== model.fileName) {
|
message: `删除 ${model.fileName} 后无法恢复。请输入完整文件名以确认。`,
|
||||||
setStatus("文件名不匹配,已取消删除", "idle");
|
confirmLabel: "永久删除",
|
||||||
return;
|
danger: true,
|
||||||
}
|
expectedValue: model.fileName
|
||||||
event.target.disabled = true;
|
});
|
||||||
|
if (confirmation === null) return;
|
||||||
|
button.disabled = true;
|
||||||
try {
|
try {
|
||||||
const result = await runBusy("正在删除模型", () => window.reinloop.deleteModel({ fileID, credentials: credentials() }));
|
const result = await runBusy("正在删除模型", () => window.reinloop.deleteModel({ fileID, credentials: credentials() }));
|
||||||
if (result) await refreshModels();
|
if (result) {
|
||||||
|
await refreshModels();
|
||||||
|
setStatus("模型已删除", "success");
|
||||||
|
}
|
||||||
} finally {
|
} 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; }
|
.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; }
|
.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 { 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-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); }
|
.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);
|
const modelName = path.basename(request.modelName || fileName);
|
||||||
await fs.promises.access(sourcePath, fs.constants.R_OK);
|
await fs.promises.access(sourcePath, fs.constants.R_OK);
|
||||||
const issued = await callServer({
|
const issued = await callServer({
|
||||||
type: "uploadDataFile",
|
type: "issueModelUpload",
|
||||||
|
deviceId: request.deviceId,
|
||||||
fileName,
|
fileName,
|
||||||
folder: `${request.deviceId}/model_config`,
|
|
||||||
modelName,
|
modelName,
|
||||||
overwrite: request.overwrite === true
|
overwrite: request.overwrite === true
|
||||||
}, request.credentials);
|
}, request.credentials);
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ test("panel model kernel renames, overwrites, and deletes by fileID", async (con
|
|||||||
const requests = [];
|
const requests = [];
|
||||||
const callServer = async (payload) => {
|
const callServer = async (payload) => {
|
||||||
requests.push(payload);
|
requests.push(payload);
|
||||||
if (payload.type === "uploadDataFile") {
|
if (payload.type === "issueModelUpload") {
|
||||||
const fileID = `model://${payload.folder.replace(/\/model_config$/, "")}/${payload.modelName}`;
|
const fileID = `model://${payload.deviceId}/${payload.modelName}`;
|
||||||
if (records.has(fileID) && !payload.overwrite) throw new Error("文件已存在");
|
if (records.has(fileID) && !payload.overwrite) throw new Error("文件已存在");
|
||||||
records.set(fileID, {
|
records.set(fileID, {
|
||||||
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.fileName, "pressure-controller.bin");
|
||||||
assert.equal(uploaded.originalFileName, "controller-original.bin");
|
assert.equal(uploaded.originalFileName, "controller-original.bin");
|
||||||
assert.equal(records.size, 1);
|
assert.equal(records.size, 1);
|
||||||
|
assert.equal(requests[0].deviceId, request.deviceId);
|
||||||
|
|
||||||
await fs.promises.writeFile(sourcePath, "second-version");
|
await fs.promises.writeFile(sourcePath, "second-version");
|
||||||
const overwritten = await handlers.upload({ ...request, overwrite: true });
|
const overwritten = await handlers.upload({ ...request, overwrite: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user