Files
ReinLoopTest/ControlPanel/electron-ui/renderer.js
T
2026-07-30 11:12:31 +08:00

716 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const volumeExample = {
q_in_val: 91,
dt: 0.1,
p_max: 200,
fit_low: 50,
fit_high: 200,
T_delta: 30,
xa_full: 1000,
num_runs: 6
};
const identificationExample = {
q_in_val: 91,
dt: 0.1,
n_order: 8,
t_c: 2.5,
levels: [10, 20, 30, 40, 50, 60, 70, 80],
dead_area: 0,
xa_full: 1000,
V_val: 1,
repeat: 2
};
const state = {
configType: "volume",
imagePaths: { csv: null, json: null },
imageDataUrls: { csv: null, json: null },
identificationFiles: [],
configs: { volume: volumeExample, identification: identificationExample },
companies: [],
models: [],
defaultDeviceId: "",
review: null,
retryDeviceId: null,
lightbox: { mediaType: null, scale: 1, fit: true, previousFocus: null },
connected: false
};
const elements = {
status: document.querySelector("#status"),
connectionScreen: document.querySelector("#connection-screen"),
connectionForm: document.querySelector("#connection-form"),
connectionMessage: document.querySelector("#connection-message"),
connectButton: document.querySelector("#connect-button"),
workspace: document.querySelector("#workspace"),
apiUrl: document.querySelector("#api-url"),
adminToken: document.querySelector("#admin-token"),
companySelect: document.querySelector("#company-select"),
deviceId: document.querySelector("#device-id"),
plots: {
csv: {
image: document.querySelector("#csv-plot-image"),
empty: document.querySelector("#empty-csv-plot"),
path: document.querySelector("#csv-image-path"),
uploadTime: document.querySelector("#csv-upload-time"),
showImage: document.querySelector("#show-csv-image"),
openImage: document.querySelector('[data-open-plot="csv"]')
},
json: {
image: document.querySelector("#json-plot-image"),
empty: document.querySelector("#empty-json-plot"),
path: document.querySelector("#json-image-path"),
uploadTime: document.querySelector("#json-upload-time"),
showImage: document.querySelector("#show-json-image"),
openImage: document.querySelector('[data-open-plot="json"]')
}
},
configEditor: document.querySelector("#config-editor"),
configLabel: document.querySelector("#config-label"),
configPath: document.querySelector("#config-path"),
publishTarget: document.querySelector("#publish-target"),
publishButton: document.querySelector("#publish-config"),
publishResult: document.querySelector("#publish-result"),
lineCompany: document.querySelector("#line-company"),
licenseTarget: document.querySelector("#license-target"),
licenseList: document.querySelector("#license-list"),
licenseEmpty: document.querySelector("#license-empty"),
licenseDetail: document.querySelector("#license-detail"),
modelList: document.querySelector("#model-list"),
modelEmpty: document.querySelector("#model-empty"),
identificationFileList: document.querySelector("#identification-file-list"),
identificationFileEmpty: document.querySelector("#identification-file-empty"),
reviewTarget: document.querySelector("#review-target"),
approveReview: document.querySelector("#approve-review"),
rejectReview: document.querySelector("#reject-review"),
lightbox: document.querySelector("#image-lightbox"),
lightboxTitle: document.querySelector("#lightbox-title"),
lightboxImage: document.querySelector("#lightbox-image"),
lightboxCanvas: document.querySelector("#lightbox-canvas"),
zoomIn: document.querySelector("#zoom-in"),
zoomOut: document.querySelector("#zoom-out"),
zoomFit: document.querySelector("#zoom-fit"),
zoomReset: document.querySelector("#zoom-reset"),
closeLightbox: document.querySelector("#close-lightbox")
};
function setStatus(message, tone = "idle") {
elements.status.textContent = message;
elements.status.dataset.tone = tone;
}
function credentials() {
return {
apiUrl: elements.apiUrl.value.trim(),
adminToken: elements.adminToken.value,
deviceId: elements.deviceId.value.trim()
};
}
function selectedCompany() {
return state.companies.find((company) => company.id === elements.companySelect.value);
}
function selectedLine() {
const company = selectedCompany();
return company?.productionLines.find((line) => line.deviceId === elements.deviceId.value);
}
function formatLicenseDate(value) {
return value.replace("T", " ");
}
function formatSize(value) {
if (!Number.isFinite(value)) return "-";
if (value < 1024) return `${value} B`;
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
return `${(value / 1024 / 1024).toFixed(1)} MB`;
}
function escapeHtml(value) {
const node = document.createElement("span");
node.textContent = String(value ?? "");
return node.innerHTML;
}
async function syncConnection() {
await window.reinloop.setConnection(credentials());
}
function showConnectionError(error) {
const message = (error?.message || String(error))
.replace(/^Error invoking remote method '[^']+': Error: /, "");
elements.connectionMessage.textContent = message;
elements.connectionMessage.dataset.tone = "error";
setStatus("连接失败", "error");
}
async function connectWorkspace() {
const apiUrl = elements.apiUrl.value.trim();
if (!apiUrl) return showConnectionError(new Error("请输入 Server API URL"));
elements.connectButton.disabled = true;
elements.connectionMessage.textContent = "正在校验 Server 与 Admin Token";
elements.connectionMessage.dataset.tone = "busy";
setStatus("正在连接", "busy");
try {
const result = await window.reinloop.testConnection({
apiUrl,
adminToken: elements.adminToken.value
});
state.companies = result.companies;
renderOrganizationOptions();
elements.connectionScreen.hidden = true;
elements.workspace.hidden = false;
state.connected = true;
delete elements.connectionMessage.dataset.tone;
setStatus("连接成功", "success");
} catch (error) {
showConnectionError(error);
} finally {
elements.connectButton.disabled = false;
}
}
function renderOrganizationOptions() {
const previousCompany = elements.companySelect.value;
const companyOptions = state.companies.map((company) => {
const lines = company.productionLines || [];
const onlineCount = lines.filter((line) => line.online === true).length;
const hasKnownStatus = lines.some((line) => typeof line.online === "boolean");
const statusSummary = !lines.length
? " · 暂无产线"
: hasKnownStatus
? `${onlineCount > 0 ? " · ● 在线" : " · ○ 离线"} (${onlineCount}/${lines.length})`
: " · ◇ 状态未知";
return `<option value="${escapeHtml(company.id)}">${escapeHtml(company.name)} (${escapeHtml(company.code)})${escapeHtml(statusSummary)}</option>`;
}).join("");
elements.companySelect.innerHTML = `<option value="">请选择公司</option>${companyOptions}`;
elements.lineCompany.innerHTML = `<option value="">请选择公司</option>${companyOptions}`;
if (state.companies.some((company) => company.id === previousCompany)) {
elements.companySelect.value = previousCompany;
} else if (state.defaultDeviceId) {
const defaultCompany = state.companies.find((company) =>
company.productionLines.some((line) => line.deviceId === state.defaultDeviceId));
if (defaultCompany) elements.companySelect.value = defaultCompany.id;
}
renderLineOptions();
}
function renderLineOptions() {
const company = selectedCompany();
const previousDevice = elements.deviceId.value;
const lines = company?.productionLines || [];
elements.deviceId.innerHTML = `<option value="">请选择产线</option>${lines.map((line) =>
`<option value="${escapeHtml(line.deviceId)}">${escapeHtml(line.online === true ? "● 在线" : line.online === false ? "○ 离线" : "◇ 状态未知")} · ${escapeHtml(line.name)} (${escapeHtml(line.code)})</option>`
).join("")}`;
if (lines.some((line) => line.deviceId === previousDevice)) {
elements.deviceId.value = previousDevice;
} else if (lines.some((line) => line.deviceId === state.defaultDeviceId)) {
elements.deviceId.value = state.defaultDeviceId;
}
updateSelectedTarget();
}
function updateSelectedTarget() {
const company = selectedCompany();
const line = selectedLine();
elements.licenseTarget.value = company && line ? `${company.name} / ${line.name}` : "";
void syncConnection();
}
async function refreshOrganizations(silent = false) {
if (silent) {
try {
const result = await window.reinloop.listOrganizations(credentials());
state.companies = result.companies;
renderOrganizationOptions();
} catch (_error) {
// Keep the last known status; explicit operations still report errors.
}
return;
}
const result = await runBusy("正在读取组织", () => window.reinloop.listOrganizations(credentials()));
if (!result) return;
state.companies = result.companies;
renderOrganizationOptions();
setStatus("组织已刷新", "success");
}
async function refreshLicenses() {
const result = await runBusy("正在读取许可证", () => window.reinloop.listLicenses(credentials()));
if (!result) return;
elements.licenseList.innerHTML = result.licenses.map((license) => `
<tr><td>${escapeHtml(license.companyName)} / ${escapeHtml(license.productionLineName)}</td>
<td>${escapeHtml(license.expiry)}</td><td>${license.status === "active" ? "有效" : "已撤销"}</td>
<td><button class="table-action" data-license-detail="${escapeHtml(license.licenseId)}">详情</button>${license.status === "active" ? `<button class="table-action danger" data-license-revoke="${escapeHtml(license.licenseId)}">撤销</button>` : ""}</td></tr>
`).join("");
elements.licenseEmpty.hidden = result.licenses.length > 0;
setStatus("许可证已刷新", "success");
}
async function refreshModels() {
if (!elements.deviceId.value) return showError(new Error("请先选择公司和产线"));
const result = await runBusy("正在读取模型", () => window.reinloop.listModels({
deviceId: elements.deviceId.value, credentials: credentials()
}));
if (!result) return;
state.models = result.fileList;
elements.modelList.innerHTML = state.models.map((model) => `
<tr><td>${escapeHtml(model.fileName)}</td><td>${escapeHtml(model.uploadTime || "-")}</td>
<td>${formatSize(model.size)}</td><td><button class="table-action" data-model-download="${escapeHtml(model.fileID)}">下载</button><button class="table-action danger" data-model-delete="${escapeHtml(model.fileID)}">删除</button></td></tr>
`).join("");
elements.modelEmpty.hidden = state.models.length > 0;
setStatus("模型已刷新", "success");
}
async function refreshIdentificationFiles() {
if (!elements.deviceId.value) return showError(new Error("请先选择公司和产线"));
const result = await runBusy("正在读取辨识暂存数据", () => window.reinloop.listIdentificationFiles({
deviceId: elements.deviceId.value, page: 1, pageSize: 100, credentials: credentials()
}));
if (!result) return;
state.identificationFiles = result.files || result.fileList || [];
elements.identificationFileList.innerHTML = state.identificationFiles.map((file) => `
<tr><td>${escapeHtml(file.uploadTime || "-")}</td><td>${escapeHtml(file.fileName)}</td>
<td>${file.mediaType === "json" ? "行程 JSON" : "辨识 CSV"}</td><td>${formatSize(file.size)}</td>
<td>${file.status === "processed" ? "已处理" : "待处理"}</td>
<td><button class="table-action" data-identification-preview="${escapeHtml(file.fileID)}">查看</button><button class="table-action" data-identification-download="${escapeHtml(file.fileID)}">下载</button><button class="table-action danger" data-identification-delete="${escapeHtml(file.fileID)}">删除</button></td></tr>
`).join("");
elements.identificationFileEmpty.hidden = state.identificationFiles.length > 0;
setStatus("辨识数据已刷新", "success");
}
function showError(error) {
const message = (error?.message || String(error))
.replace(/^Error invoking remote method '[^']+': Error: /, "");
setStatus(message, "error");
elements.status.title = message;
elements.publishResult.textContent = message;
elements.publishResult.dataset.tone = "error";
}
function showImage(result) {
if (!result) return;
const mediaType = result.mediaType === "json" || result.fileName?.toLowerCase().endsWith(".json")
? "json"
: "csv";
const plot = elements.plots[mediaType];
state.imagePaths[mediaType] = result.filePath;
state.imageDataUrls[mediaType] = result.dataUrl;
plot.image.src = result.dataUrl;
plot.image.hidden = false;
plot.empty.hidden = true;
plot.path.textContent = result.filePath;
if (result.uploadTime) {
const uploadedAt = new Date(result.uploadTime);
plot.uploadTime.textContent = `上传时间:${uploadedAt.toLocaleString("zh-CN", { hour12: false })}`;
} else {
plot.uploadTime.textContent = "已接收";
}
plot.showImage.disabled = false;
plot.openImage.disabled = false;
if (mediaType === "csv" && result.reviewable) {
state.review = { runId: result.fileName, deviceId: result.deviceId };
elements.reviewTarget.textContent = result.fileName;
elements.approveReview.disabled = false;
elements.rejectReview.disabled = false;
}
setStatus("图片已载入", "success");
}
function updateLightboxImage() {
const image = elements.lightboxImage;
if (state.lightbox.fit) {
image.classList.add("fit-image");
image.style.width = "";
return;
}
image.classList.remove("fit-image");
if (image.complete && image.naturalWidth) image.style.width = `${Math.round(image.naturalWidth * state.lightbox.scale)}px`;
}
function openLightbox(mediaType) {
const source = state.imageDataUrls[mediaType];
if (!source) return;
state.lightbox.mediaType = mediaType;
state.lightbox.scale = 1;
state.lightbox.fit = true;
state.lightbox.previousFocus = document.activeElement;
elements.lightboxTitle.textContent = mediaType === "json" ? "行程稳态压力图" : "辨识 CSV 图";
elements.lightboxImage.src = source;
elements.lightboxImage.onload = updateLightboxImage;
elements.lightbox.hidden = false;
elements.closeLightbox.focus();
}
function closeLightbox() {
if (elements.lightbox.hidden) return;
elements.lightbox.hidden = true;
elements.lightboxImage.removeAttribute("src");
state.lightbox.previousFocus?.focus();
state.lightbox.previousFocus = null;
}
function zoomLightbox(direction) {
state.lightbox.fit = false;
state.lightbox.scale = Math.min(4, Math.max(0.25, state.lightbox.scale * direction));
updateLightboxImage();
}
function finishReview(result) {
state.review = null;
elements.reviewTarget.textContent = result === 1 ? "已提交:通过" : "已提交:未通过";
elements.approveReview.disabled = true;
elements.rejectReview.disabled = true;
}
function activateTab(target) {
document.querySelectorAll(".tab").forEach((item) => item.classList.toggle("active", item.dataset.target === target));
document.querySelectorAll(".panel").forEach((panel) => panel.classList.toggle("active", panel.id === target));
}
function activateConfigType(configType) {
try {
state.configs[state.configType] = JSON.parse(elements.configEditor.value);
} catch (_error) {
// Keep the last valid configuration when changing views.
}
state.configType = configType;
document.querySelectorAll(".segment").forEach((item) => item.classList.toggle("active", item.dataset.type === configType));
renderConfig();
}
function switchToIdentificationConfigForRetry() {
if (!state.review) return;
state.retryDeviceId = state.review.deviceId;
activateTab("config-panel");
activateConfigType("identification");
elements.publishResult.textContent = `本轮辨识未通过。请检查配置后发布到设备 ${state.retryDeviceId},发布成功后将自动要求重测。`;
elements.publishResult.dataset.tone = "error";
setStatus("请重新发布辨识配置", "busy");
}
async function submitReview(result) {
if (!state.review) return;
if (result === 0) return switchToIdentificationConfigForRetry();
const review = state.review;
const submitted = await runBusy("正在提交辨识结论", () => window.reinloop.submitReview({
deviceId: review.deviceId,
runId: review.runId,
result,
credentials: credentials()
}));
if (submitted) {
finishReview(result);
state.retryDeviceId = null;
setStatus("辨识结果已通过", "success");
}
}
async function submitRetryReview() {
if (!state.review || !state.retryDeviceId) return;
const review = state.review;
const submitted = await runBusy("正在提交未通过结论", () => window.reinloop.submitReview({
deviceId: review.deviceId, runId: review.runId, result: 0, credentials: credentials()
}));
if (!submitted) return;
finishReview(0);
state.retryDeviceId = null;
setStatus("新配置已发布,已要求设备重测", "success");
}
function renderConfig() {
const isVolume = state.configType === "volume";
elements.configEditor.value = JSON.stringify(state.configs[state.configType], null, 2);
elements.configLabel.textContent = isVolume ? "容积测量配置" : "系统辨识配置";
elements.publishTarget.textContent = isVolume ? "Server 配置文件" : "设备辨识配置";
elements.publishButton.textContent = isVolume ? "上传容积配置" : "发布辨识配置";
elements.configPath.textContent = "可直接编辑,或从本地 JSON 导入";
elements.publishResult.textContent = "等待操作";
delete elements.publishResult.dataset.tone;
}
async function runBusy(label, action) {
setStatus(label, "busy");
try {
return await action();
} catch (error) {
showError(error);
return null;
}
}
document.querySelectorAll(".tab").forEach((button) => button.addEventListener("click", () => activateTab(button.dataset.target)));
document.querySelectorAll(".segment").forEach((button) => button.addEventListener("click", () => activateConfigType(button.dataset.type)));
Object.entries(elements.plots).forEach(([mediaType, plot]) => {
plot.showImage.addEventListener("click", () => window.reinloop.showInFolder(state.imagePaths[mediaType]));
plot.openImage.addEventListener("click", () => openLightbox(mediaType));
});
elements.approveReview.addEventListener("click", () => void submitReview(1));
elements.rejectReview.addEventListener("click", () => void submitReview(0));
elements.companySelect.addEventListener("change", renderLineOptions);
elements.deviceId.addEventListener("change", updateSelectedTarget);
elements.connectionForm.addEventListener("submit", (event) => {
event.preventDefault();
void connectWorkspace();
});
document.querySelector("#refresh-organizations").addEventListener("click", refreshOrganizations);
document.querySelector("#company-form").addEventListener("submit", async (event) => {
event.preventDefault();
const result = await runBusy("正在添加公司", () => window.reinloop.createCompany({
name: document.querySelector("#company-name").value,
code: document.querySelector("#company-code").value,
credentials: credentials()
}));
if (!result) return;
event.target.reset();
await refreshOrganizations();
});
document.querySelector("#line-form").addEventListener("submit", async (event) => {
event.preventDefault();
const result = await runBusy("正在添加产线", () => window.reinloop.createProductionLine({
companyId: elements.lineCompany.value,
name: document.querySelector("#line-name").value,
code: document.querySelector("#line-code").value,
credentials: credentials()
}));
if (!result) return;
event.target.reset();
await refreshOrganizations();
});
document.querySelector("#license-form").addEventListener("submit", async (event) => {
event.preventDefault();
const company = selectedCompany();
const line = selectedLine();
if (!company || !line) return showError(new Error("请先选择公司和产线"));
const result = await runBusy("正在签发许可证", () => window.reinloop.issueLicense({
companyId: company.id, companyCode: company.code, customer: company.name,
productionLineId: line.id, lineCode: line.code, deviceId: line.deviceId,
issued: formatLicenseDate(document.querySelector("#license-issued").value),
expiry: formatLicenseDate(document.querySelector("#license-expiry").value),
features: document.querySelector("#license-features").value,
credentials: credentials()
}));
if (!result) return;
setStatus(`许可证已保存: ${result.filePath}`, "success");
await refreshLicenses();
});
document.querySelector("#refresh-licenses").addEventListener("click", refreshLicenses);
elements.licenseList.addEventListener("click", async (event) => {
const detailId = event.target.dataset.licenseDetail;
const revokeId = event.target.dataset.licenseRevoke;
if (detailId) {
const result = await runBusy("正在读取许可证详情", () => window.reinloop.getLicense({ licenseId: detailId, credentials: credentials() }));
if (result) {
elements.licenseDetail.textContent = JSON.stringify(result.license, null, 2);
elements.licenseDetail.hidden = false;
}
}
if (revokeId) {
const reason = window.prompt("请输入撤销原因", "管理员撤销");
if (reason === null) return;
const result = await runBusy("正在撤销许可证", () => window.reinloop.revokeLicense({ licenseId: revokeId, reason, credentials: credentials() }));
if (result) await refreshLicenses();
}
});
document.querySelector("#refresh-models").addEventListener("click", refreshModels);
document.querySelector("#upload-model").addEventListener("click", async () => {
if (!elements.deviceId.value) return showError(new Error("请先选择公司和产线"));
const button = document.querySelector("#upload-model");
button.disabled = true;
try {
const models = await runBusy("正在读取模型", () => window.reinloop.listModels({
deviceId: elements.deviceId.value, credentials: credentials()
}));
if (!models) return;
state.models = models.fileList;
const selected = await runBusy("正在选择模型文件", () => window.reinloop.chooseModelUploadFile());
if (!selected) return;
const existing = state.models.find((model) => model.fileName === selected.fileName);
let overwrite = false;
if (existing) {
if (!window.confirm(`已存在同名模型 ${selected.fileName},覆盖后无法恢复。是否继续?`)) return;
const confirmation = window.prompt(`请输入完整文件名以确认覆盖:${selected.fileName}`);
if (confirmation !== selected.fileName) {
setStatus("文件名不匹配,已取消覆盖", "idle");
return;
}
overwrite = true;
}
const result = await runBusy("正在上传模型", () => window.reinloop.uploadModel({
deviceId: elements.deviceId.value, sourcePath: selected.sourcePath, fileName: selected.fileName,
overwrite, credentials: credentials()
}));
if (result) {
setStatus(overwrite ? "模型已覆盖" : "模型已上传", "success");
await refreshModels();
}
} finally {
button.disabled = false;
}
});
elements.modelList.addEventListener("click", async (event) => {
const fileID = event.target.dataset.modelDownload || event.target.dataset.modelDelete;
if (!fileID) return;
const model = state.models.find((item) => item.fileID === fileID);
if (event.target.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;
try {
const result = await runBusy("正在删除模型", () => window.reinloop.deleteModel({ fileID, credentials: credentials() }));
if (result) await refreshModels();
} finally {
event.target.disabled = false;
}
}
});
document.querySelector("#refresh-identification-files").addEventListener("click", refreshIdentificationFiles);
elements.identificationFileList.addEventListener("click", async (event) => {
const fileID = event.target.dataset.identificationPreview || event.target.dataset.identificationDownload || event.target.dataset.identificationDelete;
if (!fileID) return;
const file = state.identificationFiles.find((item) => item.fileID === fileID);
if (!file) return;
if (event.target.dataset.identificationPreview) {
const result = await runBusy("正在下载并生成预览", () => window.reinloop.previewIdentificationFile({
...file, deviceId: elements.deviceId.value, credentials: credentials()
}));
if (result) {
showImage(result);
activateTab("plot-panel");
}
} else if (event.target.dataset.identificationDownload) {
const result = await runBusy("正在保存辨识原始数据", () => window.reinloop.downloadIdentificationFile({
...file, credentials: credentials()
}));
if (result) setStatus(`已保存到: ${result.filePath}`, "success");
} else {
if (!window.confirm(`确认永久删除 ${file.fileName}`)) return;
if (!window.confirm("删除后无法恢复,确认继续?")) return;
event.target.disabled = true;
try {
const result = await runBusy("正在删除辨识数据", () => window.reinloop.deleteIdentificationFile({
fileID, credentials: credentials()
}));
if (result) await refreshIdentificationFiles();
} finally {
event.target.disabled = false;
}
}
});
document.querySelector("#import-config").addEventListener("click", async () => {
const result = await runBusy("正在导入配置", () => window.reinloop.chooseConfig());
if (!result) return;
state.configs[state.configType] = result.parameters;
elements.configEditor.value = JSON.stringify(result.parameters, null, 2);
elements.configPath.textContent = result.filePath;
setStatus("配置已导入", "success");
});
document.querySelector("#load-server").addEventListener("click", async () => {
const parameters = await runBusy("正在读取 Server 配置", () => window.reinloop.getConfig({
configType: state.configType,
credentials: credentials()
}));
if (!parameters) return;
state.configs[state.configType] = parameters;
elements.configEditor.value = JSON.stringify(parameters, null, 2);
elements.publishResult.textContent = "已读取当前 Server 配置";
elements.publishResult.dataset.tone = "success";
setStatus("读取完成", "success");
});
elements.publishButton.addEventListener("click", async () => {
let parameters;
try {
parameters = JSON.parse(elements.configEditor.value);
} catch (error) {
showError(new Error(`JSON 格式错误: ${error.message}`));
return;
}
const result = await runBusy("正在发布配置", () => window.reinloop.publishConfig({
configType: state.configType,
parameters,
credentials: {
...credentials(),
deviceId: state.configType === "identification" && state.retryDeviceId
? state.retryDeviceId
: credentials().deviceId
}
}));
if (!result) return;
state.configs[state.configType] = parameters;
elements.publishResult.textContent = result.storagePath
? `${result.message}: ${result.storagePath}`
: result.message;
elements.publishResult.dataset.tone = "success";
if (state.configType === "identification" && state.retryDeviceId) {
await submitRetryReview();
} else {
setStatus("发布完成", "success");
}
});
window.reinloop.getDefaults().then((defaults) => {
elements.apiUrl.value = defaults.apiUrl;
state.defaultDeviceId = defaults.deviceId;
elements.adminToken.placeholder = defaults.hasAdminToken
? "已使用环境变量中的 Token"
: "请输入 Admin Token";
});
window.reinloop.onCsvUpdated((result) => {
showImage(result);
if (state.connected) void refreshIdentificationFiles();
setStatus(`已接收 ${result.fileName}`, "success");
});
window.reinloop.onCsvWatchError((message) => {
setStatus("数据接收异常", "error");
const pendingPlot = Object.values(elements.plots).find((plot) => plot.image.hidden);
if (pendingPlot) pendingPlot.path.textContent = message;
});
setInterval(() => {
if (state.connected) void refreshOrganizations(true);
}, 10000);
renderConfig();
elements.closeLightbox.addEventListener("click", closeLightbox);
elements.zoomIn.addEventListener("click", () => zoomLightbox(1.25));
elements.zoomOut.addEventListener("click", () => zoomLightbox(0.8));
elements.zoomFit.addEventListener("click", () => {
state.lightbox.fit = true;
updateLightboxImage();
});
elements.zoomReset.addEventListener("click", () => {
state.lightbox.fit = false;
state.lightbox.scale = 1;
updateLightboxImage();
});
elements.lightbox.addEventListener("click", (event) => {
if (event.target === elements.lightbox) closeLightbox();
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") closeLightbox();
if (!elements.lightbox.hidden && event.key === "+") zoomLightbox(1.25);
if (!elements.lightbox.hidden && event.key === "-") zoomLightbox(0.8);
});