fix model del & rename

This commit is contained in:
2026-07-31 11:26:38 +08:00
parent feaddfee7b
commit 46c69607c5
5 changed files with 134 additions and 25 deletions
+18
View File
@@ -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>
+101 -21
View File
@@ -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;
}
}
});
+10
View File
@@ -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); }