Use exact CT links for PACS STL view
This commit is contained in:
@@ -3,6 +3,8 @@ const app = {
|
||||
active: null,
|
||||
filter: "all",
|
||||
search: "",
|
||||
algorithmModel: "",
|
||||
dicomModel: "",
|
||||
searchTimer: null,
|
||||
};
|
||||
|
||||
@@ -63,16 +65,17 @@ function uniq(list) {
|
||||
|
||||
function statusLabel(value) {
|
||||
return {
|
||||
complete: "PACS + UPP + STL",
|
||||
no_stl: "PACS + UPP",
|
||||
complete: "PACS + STL",
|
||||
no_stl: "PACS + 列表",
|
||||
pacs_only: "仅 PACS",
|
||||
upp_only: "仅 UPP",
|
||||
stl_only: "仅 STL",
|
||||
list_only: "列表无STL",
|
||||
unknown: "未知",
|
||||
}[value] || value || "未知";
|
||||
}
|
||||
|
||||
function matchLabel(value) {
|
||||
return { exact: "CT 号精确一致", normalized: "去 D 前缀匹配" }[value] || "未匹配";
|
||||
return { exact: "CT 号一致" }[value] || "未匹配";
|
||||
}
|
||||
|
||||
function partLabel(value) {
|
||||
@@ -100,7 +103,7 @@ function renderMetrics(counts = {}) {
|
||||
const items = [
|
||||
["CT 索引", counts.total_ct],
|
||||
["PACS", counts.pacs_count],
|
||||
["UPP", counts.upp_count],
|
||||
["STL资产", counts.stl_asset_count],
|
||||
["STL", counts.stl_count],
|
||||
["完整关联", counts.complete_count],
|
||||
["待判别序列", counts.undetermined_series],
|
||||
@@ -112,12 +115,14 @@ function renderMetrics(counts = {}) {
|
||||
|
||||
function cardTitle(row) {
|
||||
return [
|
||||
`规范CT号:${row.norm_ct}`,
|
||||
`CT号:${row.ct_key}`,
|
||||
`PACS:${row.pacs_ct_number || "无"}`,
|
||||
`UPP:${row.upp_ct_number || "无"}`,
|
||||
`STL:${row.stl_ct_number || "无"}`,
|
||||
`状态:${statusLabel(row.relation_status)}`,
|
||||
`PACS患者:${row.pacs_patient_name || "无"}`,
|
||||
`UPP患者:${row.upp_patient_name || "无"}`,
|
||||
`STL列表患者:${row.upp_patient_name || "无"}`,
|
||||
`重建模型:${row.algorithm_model || "无"}`,
|
||||
`DICOM模型:${asList(row.dicom_models).join("、") || "无"}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -132,20 +137,22 @@ function renderList() {
|
||||
for (const row of app.rows) {
|
||||
const card = document.createElement("button");
|
||||
card.className = "relation-card";
|
||||
card.classList.toggle("active", app.active?.norm_ct === row.norm_ct);
|
||||
card.classList.toggle("active", app.active?.ct_key === row.ct_key);
|
||||
card.title = cardTitle(row);
|
||||
const date = row.study_date ? `${fmtDate(row.study_date)} ${fmtTime(row.study_time)}` : fmtDate(row.exam_date || row.task_created_at);
|
||||
const stlCount = Number(row.stl_file_count || row.stl_file_count_agg || 0);
|
||||
const dicomModels = asList(row.dicom_models).join("、") || "无";
|
||||
card.innerHTML = `
|
||||
<div class="card-row">
|
||||
<strong>${escapeHtml(row.norm_ct)}</strong>
|
||||
<strong>${escapeHtml(row.ct_key)}</strong>
|
||||
<i class="mini-badge ${escapeHtml(row.relation_status)}">${escapeHtml(statusLabel(row.relation_status))}</i>
|
||||
</div>
|
||||
<span>${escapeHtml(row.pacs_patient_name || row.upp_patient_name || "无姓名")} · ${escapeHtml(row.patient_id || row.patient_id_masked || "无ID")}</span>
|
||||
<small>${escapeHtml(date || "无时间")} · PACS ${Number(row.dicom_file_count || 0)} 张 · STL ${stlCount} 个</small>
|
||||
<small>重建 ${escapeHtml(row.algorithm_model || "无")} · DICOM ${escapeHtml(dicomModels)}</small>
|
||||
<small>${escapeHtml(matchLabel(row.match_type))}${Number(row.undetermined_series || 0) ? ` · ${Number(row.undetermined_series)} 待判别` : ""}</small>
|
||||
`;
|
||||
card.onclick = () => selectRelation(row.norm_ct);
|
||||
card.onclick = () => selectRelation(row.ct_key);
|
||||
list.appendChild(card);
|
||||
}
|
||||
}
|
||||
@@ -203,16 +210,15 @@ function renderSegments(row) {
|
||||
|
||||
function renderDetail(row) {
|
||||
app.active = row;
|
||||
$("activeCt").textContent = row.norm_ct || "未选择 CT";
|
||||
$("activeSubtitle").textContent = `${row.pacs_ct_number || "无 PACS"} ↔ ${row.upp_ct_number || "无 UPP"} · ${matchLabel(row.match_type)}`;
|
||||
$("activeCt").textContent = row.ct_key || "未选择 CT";
|
||||
$("activeSubtitle").textContent = `${row.pacs_ct_number || "无 PACS"} ↔ ${row.stl_ct_number || "无 STL"} · ${matchLabel(row.match_type)}`;
|
||||
const badge = $("relationBadge");
|
||||
badge.className = `relation-badge ${row.relation_status || "idle"} ${row.match_type === "normalized" ? "normalized" : ""}`;
|
||||
badge.className = `relation-badge ${row.relation_status || "idle"}`;
|
||||
badge.textContent = statusLabel(row.relation_status);
|
||||
|
||||
$("nodeCtValue").textContent = row.norm_ct || "-";
|
||||
$("nodeCtValue").textContent = row.ct_key || "-";
|
||||
markNode("nodePacs", row.pacs_present ? "ok" : "missing", row.pacs_ct_number || "缺失", `${Number(row.dicom_file_count || 0)} 张 · ${Number(row.pacs_series_count || 0)} 序列`);
|
||||
markNode("nodeUpp", row.upp_present ? "ok" : "missing", row.upp_ct_number || "缺失", row.upp_status || row.algorithm_model || "无任务状态");
|
||||
markNode("nodeStl", row.stl_present ? "ok" : "warn", row.stl_present ? "已生成" : "未生成", `${Number(row.stl_file_count || row.stl_file_count_agg || 0)} STL · ${fmtBytes(row.stl_total_bytes || row.stl_total_bytes_agg)}`);
|
||||
markNode("nodeStl", row.stl_asset_present ? (row.stl_present ? "ok" : "warn") : "missing", row.stl_ct_number || "缺失", `${row.algorithm_model || "无模型"} · ${Number(row.stl_file_count || row.stl_file_count_agg || 0)} STL`);
|
||||
|
||||
renderDl("pacsDetails", [
|
||||
["CT号", row.pacs_ct_number],
|
||||
@@ -224,22 +230,19 @@ function renderDetail(row) {
|
||||
["标注", `${Number(row.annotated_series || 0)} 已标注 · ${Number(row.undetermined_series || 0)} 待判别`],
|
||||
["完成", row.completed ? "已完成" : "待处理"],
|
||||
["部位", asList(row.body_parts).map(partLabel).join("、")],
|
||||
["DICOM模型", asList(row.dicom_models).join("、")],
|
||||
]);
|
||||
|
||||
renderDl("uppDetails", [
|
||||
["CT号", row.upp_ct_number],
|
||||
renderDl("stlDetails", [
|
||||
["CT号", row.stl_ct_number],
|
||||
["STL状态", row.stl_present ? "存在" : "缺失"],
|
||||
["重建模型", row.algorithm_model],
|
||||
["UPP状态", row.upp_status],
|
||||
["患者", row.upp_patient_name],
|
||||
["年龄/性别", [row.patient_age, row.upp_patient_sex].filter(Boolean).join(" / ")],
|
||||
["检查时间", fmtDate(row.exam_date)],
|
||||
["任务时间", fmtDate(row.task_created_at)],
|
||||
["算法模型", row.algorithm_model],
|
||||
["状态", row.upp_status],
|
||||
["列表记录", row.list_present ? `${Number(row.list_record_count || 0)} 条` : "无"],
|
||||
["患者名", row.patient_name_match === "same" ? "两侧一致" : row.patient_name_match === "different" ? "两侧不同" : "无法判断"],
|
||||
]);
|
||||
|
||||
renderDl("stlDetails", [
|
||||
["STL状态", row.stl_present ? "存在" : "缺失"],
|
||||
["文件数", Number(row.stl_file_count || row.stl_file_count_agg || 0)],
|
||||
["总大小", fmtBytes(row.stl_total_bytes || row.stl_total_bytes_agg)],
|
||||
["目录", row.processed_stl_dir],
|
||||
@@ -252,8 +255,8 @@ function renderDetail(row) {
|
||||
renderList();
|
||||
}
|
||||
|
||||
async function selectRelation(normCt) {
|
||||
const row = await json(`/api/relations/${encodeURIComponent(normCt)}`);
|
||||
async function selectRelation(ctKey) {
|
||||
const row = await json(`/api/relations/${encodeURIComponent(ctKey)}`);
|
||||
renderDetail(row);
|
||||
}
|
||||
|
||||
@@ -268,12 +271,14 @@ async function loadRelations() {
|
||||
params.set("status", app.filter);
|
||||
params.set("limit", "500");
|
||||
if (app.search) params.set("q", app.search);
|
||||
if (app.algorithmModel) params.set("algorithm_model", app.algorithmModel);
|
||||
if (app.dicomModel) params.set("dicom_model", app.dicomModel);
|
||||
app.rows = await json(`/api/relations?${params.toString()}`);
|
||||
if (app.rows.length && !app.rows.some((row) => row.norm_ct === app.active?.norm_ct)) {
|
||||
await selectRelation(app.rows[0].norm_ct);
|
||||
if (app.rows.length && !app.rows.some((row) => row.ct_key === app.active?.ct_key)) {
|
||||
await selectRelation(app.rows[0].ct_key);
|
||||
} else {
|
||||
renderList();
|
||||
if (!app.active && app.rows.length) await selectRelation(app.rows[0].norm_ct);
|
||||
if (!app.active && app.rows.length) await selectRelation(app.rows[0].ct_key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,6 +301,14 @@ function wire() {
|
||||
loadRelations();
|
||||
}, 250);
|
||||
});
|
||||
$("algorithmFilter").addEventListener("change", () => {
|
||||
app.algorithmModel = $("algorithmFilter").value;
|
||||
loadRelations();
|
||||
});
|
||||
$("dicomModelFilter").addEventListener("change", () => {
|
||||
app.dicomModel = $("dicomModelFilter").value;
|
||||
loadRelations();
|
||||
});
|
||||
document.querySelectorAll("[data-filter]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
app.filter = button.dataset.filter;
|
||||
|
||||
Reference in New Issue
Block a user