diff --git a/ControlPanel/electron-ui/renderer.js b/ControlPanel/electron-ui/renderer.js index f9fbb64..434bee3 100644 --- a/ControlPanel/electron-ui/renderer.js +++ b/ControlPanel/electron-ui/renderer.js @@ -374,7 +374,8 @@ function requestModelName({ confirmLabel = "确认", danger = false, expectedValue = null, - expectedLabel = "输入内容" + expectedLabel = "输入内容", + ignoreCase = false }) { if (actionDialogState) closeActionDialog(null); elements.actionDialogTitle.textContent = title; @@ -391,7 +392,13 @@ function requestModelName({ elements.actionDialogInput.select(); }); return new Promise((resolve) => { - actionDialogState = { resolve, previousFocus, expectedValue, expectedLabel }; + actionDialogState = { + resolve, + previousFocus, + expectedValue, + expectedLabel, + ignoreCase + }; }); } @@ -400,17 +407,24 @@ elements.actionDialogForm.addEventListener("submit", (event) => { const value = elements.actionDialogInput.value.trim(); const expectedValue = actionDialogState?.expectedValue; const expectedLabel = actionDialogState?.expectedLabel || "输入内容"; + const ignoreCase = actionDialogState?.ignoreCase === true; if (!value) { elements.actionDialogError.textContent = `${expectedLabel}不能为空`; elements.actionDialogError.hidden = false; elements.actionDialogInput.focus(); return; } - if (expectedValue !== null && value !== expectedValue) { - elements.actionDialogError.textContent = `${expectedLabel}不匹配,请按提示完整输入`; - elements.actionDialogError.hidden = false; - elements.actionDialogInput.focus(); - return; + if (expectedValue !== null) { + const expected = String(expectedValue).trim(); + const matched = ignoreCase + ? value.toLowerCase() === expected.toLowerCase() + : value === expected; + if (!matched) { + elements.actionDialogError.textContent = `${expectedLabel}不匹配,请按提示完整输入`; + elements.actionDialogError.hidden = false; + elements.actionDialogInput.focus(); + return; + } } closeActionDialog(value); }); @@ -708,7 +722,8 @@ document.querySelector("#delete-line").addEventListener("click", async () => { confirmLabel: "删除产线", danger: true, expectedValue: line.deviceId, - expectedLabel: "deviceId" + expectedLabel: "deviceId", + ignoreCase: true }); if (confirmation === null) return; const result = await runBusy("正在删除产线", () => window.reinloop.deleteProductionLine({ @@ -734,7 +749,8 @@ document.querySelector("#delete-company").addEventListener("click", async () => confirmLabel: "删除公司", danger: true, expectedValue: company.code, - expectedLabel: "公司编码" + expectedLabel: "公司编码", + ignoreCase: true }); if (confirmation === null) return; const result = await runBusy("正在删除公司", () => window.reinloop.deleteCompany({ @@ -797,12 +813,13 @@ elements.licenseList.addEventListener("click", async (event) => { if (deleteId) { const confirmation = await requestModelName({ title: "确认删除许可证", - message: "已撤销许可证才能删除。请输入许可证 ID 以确认永久删除。", + message: `已撤销许可证才能删除。请输入许可证 ID 以确认永久删除。\n\n当前许可证 ID:${deleteId}`, value: "", confirmLabel: "永久删除", danger: true, expectedValue: deleteId, - expectedLabel: "许可证ID" + expectedLabel: "许可证ID", + ignoreCase: true }); if (confirmation === null) return; button.disabled = true; diff --git a/server/src/app.js b/server/src/app.js index 8289722..289b463 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -371,6 +371,28 @@ function createApp({ return before - database.fileRecords.length; } + async function removeDeviceDirectories(deviceId) { + const segments = String(deviceId || "").split("/"); + if (segments.length !== 2 || !segments[0] || !segments[1]) return 0; + const [companyCode, lineCode] = segments; + const targets = [ + path.join(store.modelsDirectory, companyCode, lineCode), + path.join(store.filesDirectory, "ReinLoop_GUI", companyCode, lineCode) + ]; + let removed = 0; + for (const directory of targets) { + try { + const stat = await fs.promises.stat(directory); + if (!stat.isDirectory()) continue; + } catch (_error) { + continue; + } + await fs.promises.rm(directory, { recursive: true, force: true }); + removed += 1; + } + return removed; + } + function backfillIdentificationFiles(database) { for (const record of database.fileRecords) { if (!record.folder.endsWith("/ind_data") || ![".csv", ".json"].includes(path.extname(record.fileName).toLowerCase())) continue; @@ -549,12 +571,14 @@ function createApp({ !(item.companyId === companyId && item.productionLineId === productionLineId) ); + const deletedDirectories = await removeDeviceDirectories(deviceId); database.productionLines = database.productionLines.filter((item) => item.id !== productionLineId); return { success: true, deletedProductionLineId: productionLineId, deletedFiles: deletedFileCount, + deletedDirectories, deletedLicenses: revokedLicenses, deletedFeedback, deletedVolumeRequests: deletedRequests, diff --git a/server/test/server.test.js b/server/test/server.test.js index 0a1dfdd..0ddc9c9 100644 --- a/server/test/server.test.js +++ b/server/test/server.test.js @@ -877,6 +877,14 @@ test("deleteProductionLine blocks active licenses and removes revoked data", asy assert.equal(blocked.errCode, "ACTIVE_LICENSES_PRESENT"); await post({ type: "revokeLicense", licenseId, reason: "产线删除", adminToken: "test-token" }); + + const orphanModelDir = path.join(dataDirectory, "models", line.productionLine.deviceId); + const orphanFileDir = path.join(dataDirectory, "files", "ReinLoop_GUI", line.productionLine.deviceId); + await fs.promises.mkdir(orphanModelDir, { recursive: true }); + await fs.promises.mkdir(orphanFileDir, { recursive: true }); + await fs.promises.writeFile(path.join(orphanModelDir, "orphan.bin"), "orphan-model"); + await fs.promises.writeFile(path.join(orphanFileDir, "orphan.json"), "orphan-file"); + const deleted = await post({ type: "deleteProductionLine", companyId: company.company.id, @@ -886,6 +894,10 @@ test("deleteProductionLine blocks active licenses and removes revoked data", asy assert.equal(deleted.success, true); assert.ok(deleted.deletedFiles >= 1); assert.ok(deleted.deletedLicenses >= 1); + assert.ok(deleted.deletedDirectories >= 1); + + await assert.rejects(fs.promises.access(orphanModelDir)); + await assert.rejects(fs.promises.access(orphanFileDir)); const organizations = await post({ type: "listOrganizations", adminToken: "test-token" }); const listedCompany = organizations.companies.find((item) => item.id === company.company.id);