Improve review export and AI progress UI
This commit is contained in:
@@ -192,6 +192,10 @@ function userPermissions() {
|
||||
return state.currentUser?.permissions || {};
|
||||
}
|
||||
|
||||
function isAdminUser() {
|
||||
return state.currentUser?.source === "env" || state.currentUser?.username === "admin";
|
||||
}
|
||||
|
||||
function canOpenPage(page) {
|
||||
const key = page === "auditHistory" ? "audit_history" : page;
|
||||
return userPermissions()[key] !== false;
|
||||
@@ -400,8 +404,11 @@ function renderRecords() {
|
||||
const active = record.id === state.selectedId ? " is-active" : "";
|
||||
const manual = record.manual_corrected ? " · 人工已改" : "";
|
||||
const activity = record.last_activity_at ? `<span class="record-time">最近 ${escapeHtml(formatTime(record.last_activity_at))}</span>` : "";
|
||||
const exportButton = isAdminUser() && record.has_pdf
|
||||
? `<button class="record-download" type="button" data-export-record="${record.id}" title="下载影像/PDF">下载</button>`
|
||||
: "";
|
||||
return `
|
||||
<button class="record-item${active}" type="button" data-id="${record.id}">
|
||||
<div class="record-item${active}" role="button" tabindex="0" data-id="${record.id}">
|
||||
<div class="record-main">
|
||||
<span class="record-name">${escapeHtml(record.patient_name || "未命名")}</span>
|
||||
${statusTag(displayStatusValue(record))}
|
||||
@@ -409,19 +416,37 @@ function renderRecords() {
|
||||
<div class="record-sub">
|
||||
${escapeHtml(record.inpatient_no || record.medical_record_no || "")} · ${escapeHtml(record.major_department || record.discharge_dept || "")}${manual}<br>
|
||||
${escapeHtml(record.primary_diagnosis || "")}
|
||||
${activity}
|
||||
<div class="record-footer">
|
||||
<span>${activity}</span>
|
||||
${exportButton}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}).join("") + loadingNotice + endNotice;
|
||||
list.querySelectorAll(".record-item").forEach((button) => {
|
||||
button.addEventListener("click", () => selectRecord(Number(button.dataset.id)));
|
||||
list.querySelectorAll(".record-item").forEach((item) => {
|
||||
item.addEventListener("click", (event) => {
|
||||
if (event.target.closest("[data-export-record]")) return;
|
||||
selectRecord(Number(item.dataset.id));
|
||||
});
|
||||
item.addEventListener("keydown", (event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
selectRecord(Number(item.dataset.id));
|
||||
});
|
||||
});
|
||||
list.querySelectorAll("[data-export-record]").forEach((button) => {
|
||||
button.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
exportRecordPdf(Number(button.dataset.exportRecord));
|
||||
});
|
||||
});
|
||||
list.scrollTop = previousScrollTop;
|
||||
}
|
||||
|
||||
function renderFilterActions() {
|
||||
const button = $("approveAiNoIssueBtn");
|
||||
const exportButton = $("batchExportBtn");
|
||||
if (!button) return;
|
||||
const filter = $("statusFilter")?.value || "review_all";
|
||||
const aiPassedCount = Number(state.status?.ai_passed || 0);
|
||||
@@ -429,6 +454,33 @@ function renderFilterActions() {
|
||||
button.classList.toggle("is-hidden", !shouldShow);
|
||||
button.disabled = !shouldShow || aiPassedCount <= 0 || Boolean(state.bulkJob?.running);
|
||||
button.textContent = aiPassedCount > 0 ? `通过所有AI已通过(${aiPassedCount})` : "通过所有AI已通过";
|
||||
if (exportButton) {
|
||||
exportButton.classList.toggle("is-hidden", !isAdminUser());
|
||||
exportButton.disabled = !isAdminUser() || state.recordsLoading;
|
||||
}
|
||||
}
|
||||
|
||||
function downloadUrl(url) {
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.rel = "noopener";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
|
||||
function exportRecordPdf(recordId) {
|
||||
if (!isAdminUser() || !recordId) return;
|
||||
downloadUrl(`/api/records/${recordId}/export`);
|
||||
}
|
||||
|
||||
function exportCurrentFilter() {
|
||||
if (!isAdminUser()) return;
|
||||
const params = new URLSearchParams({
|
||||
q: $("searchInput").value.trim(),
|
||||
status_filter: $("statusFilter").value || "review_all",
|
||||
});
|
||||
downloadUrl(`/api/records/export?${params.toString()}`);
|
||||
}
|
||||
|
||||
function scrollSelectedRecordIntoView() {
|
||||
@@ -576,19 +628,18 @@ function baseReviewNotes(record) {
|
||||
|
||||
function aiAttentionNotes(record) {
|
||||
const notes = [];
|
||||
(record.review_logs || []).forEach((log) => {
|
||||
const parsed = log.ai_result?.parsed;
|
||||
if (!parsed) return;
|
||||
if (Array.isArray(parsed.remaining_issues)) {
|
||||
parsed.remaining_issues.forEach((item) => {
|
||||
if (isReviewNeededText(item)) notes.push(item);
|
||||
});
|
||||
}
|
||||
const classification = String(parsed.classification || parsed.decision || "").toLowerCase();
|
||||
if (!notes.length && ["problem", "not_ok", "not ok", "confirm", "需复核", "待确认"].includes(classification)) {
|
||||
notes.push(...aiAttentionSegments(parsed.summary));
|
||||
}
|
||||
});
|
||||
const latestAiLog = (record.review_logs || []).find((log) => Boolean(log.ai_result));
|
||||
const parsed = latestAiLog?.ai_result?.parsed;
|
||||
if (!parsed) return notes;
|
||||
if (Array.isArray(parsed.remaining_issues)) {
|
||||
parsed.remaining_issues.forEach((item) => {
|
||||
if (isReviewNeededText(item)) notes.push(item);
|
||||
});
|
||||
}
|
||||
const classification = String(parsed.classification || parsed.decision || "").toLowerCase();
|
||||
if (!notes.length && ["problem", "not_ok", "not ok", "confirm", "需复核", "待确认"].includes(classification)) {
|
||||
notes.push(...aiAttentionSegments(parsed.summary));
|
||||
}
|
||||
return notes.map(displayText).filter(Boolean);
|
||||
}
|
||||
|
||||
@@ -651,7 +702,7 @@ function renderTargetSummary(targets, targetId = "targetSummary", formId = "deta
|
||||
? targets.aiNotes.map((note) => `<li>${escapeHtml(note)}</li>`).join("")
|
||||
: "<li>AI未提出需要复核的内容</li>";
|
||||
$(targetId).innerHTML = `
|
||||
<details class="collapsible-panel target-panel" open>
|
||||
<details class="collapsible-panel target-panel">
|
||||
<summary>
|
||||
<strong>复核定位</strong>
|
||||
<span>${baseGroups.length ? `${baseGroups.length} 个模块` : "未定位"}</span>
|
||||
@@ -662,7 +713,7 @@ function renderTargetSummary(targets, targetId = "targetSummary", formId = "deta
|
||||
<ul>${baseNotesHtml}</ul>
|
||||
</details>
|
||||
${targets.hasAiNotes ? `
|
||||
<details class="collapsible-panel target-panel is-ai-target" open>
|
||||
<details class="collapsible-panel target-panel is-ai-target">
|
||||
<summary>
|
||||
<strong>AI建议复核点</strong>
|
||||
<span>${aiGroups.length ? `${aiGroups.length} 个模块` : "无待看点"}</span>
|
||||
@@ -1427,6 +1478,7 @@ function clearAiProgressPanel() {
|
||||
$("aiProgressBar").style.width = "0%";
|
||||
$("aiProgressText").textContent = "0/0";
|
||||
$("aiProgressMeta").textContent = "等待开始";
|
||||
$("aiCancelBtn").textContent = "中止AI";
|
||||
$("aiCancelBtn").classList.remove("is-hidden");
|
||||
$("aiCancelBtn").disabled = true;
|
||||
}
|
||||
@@ -1434,7 +1486,7 @@ function clearAiProgressPanel() {
|
||||
function renderAiProgress(job = state.aiJob) {
|
||||
const panel = $("aiProgressPanel");
|
||||
if (!panel) return;
|
||||
if (!job || (!job.running && !job.total && !job.message) || (!job.running && job.cancel_requested)) {
|
||||
if (!job || job.cancel_requested || (!job.running && !job.total && !job.message)) {
|
||||
clearAiProgressPanel();
|
||||
return;
|
||||
}
|
||||
@@ -1452,7 +1504,8 @@ function renderAiProgress(job = state.aiJob) {
|
||||
? `已通过 ${job.updated || 0} · 剩余 ${Math.max(0, total - processed)} · 失败 ${job.failed || 0}`
|
||||
: `OK ${job.ok || 0} · 不OK ${job.pending || 0} · 失败 ${job.failed || 0} · 并发 ${job.concurrency || 1}`;
|
||||
$("aiCancelBtn").classList.toggle("is-hidden", isBulkApprove);
|
||||
$("aiCancelBtn").disabled = isBulkApprove || !job.running;
|
||||
$("aiCancelBtn").textContent = job.running ? "中止AI" : "确认";
|
||||
$("aiCancelBtn").disabled = isBulkApprove;
|
||||
}
|
||||
|
||||
async function cancelAiReview({ silent = false } = {}) {
|
||||
@@ -1468,6 +1521,19 @@ async function cancelAiReview({ silent = false } = {}) {
|
||||
return job;
|
||||
}
|
||||
|
||||
async function confirmAiProgress() {
|
||||
clearAiProgressPanel();
|
||||
state.aiJob = await api("/api/ai/review/ack", { method: "POST" }).catch(() => null);
|
||||
}
|
||||
|
||||
async function handleAiProgressAction() {
|
||||
if (state.aiJob?.running) {
|
||||
await cancelAiReview();
|
||||
return;
|
||||
}
|
||||
await confirmAiProgress();
|
||||
}
|
||||
|
||||
async function pollAiReviewJob() {
|
||||
const job = await api("/api/ai/review/status");
|
||||
state.aiJob = job;
|
||||
@@ -1828,8 +1894,9 @@ async function boot() {
|
||||
$("aiCurrentBtn").addEventListener("click", () => runAiReview("current"));
|
||||
$("aiFiveBtn").addEventListener("click", () => runAiReview("five"));
|
||||
$("aiAllBtn").addEventListener("click", () => runAiReview("all"));
|
||||
$("aiCancelBtn").addEventListener("click", () => cancelAiReview().catch((error) => showMessage(error.message, "error")));
|
||||
$("aiCancelBtn").addEventListener("click", () => handleAiProgressAction().catch((error) => showMessage(error.message, "error")));
|
||||
$("approveAiNoIssueBtn").addEventListener("click", approveAiNoIssueRecords);
|
||||
$("batchExportBtn").addEventListener("click", exportCurrentFilter);
|
||||
$("pdfZoomOutBtn").addEventListener("click", () => changePdfZoom(-PDF_ZOOM_STEP));
|
||||
$("pdfZoomInBtn").addEventListener("click", () => changePdfZoom(PDF_ZOOM_STEP));
|
||||
$("pdfZoomResetBtn").addEventListener("click", () => setPdfZoom(100));
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
<button id="aiCancelBtn" type="button">中止AI</button>
|
||||
</div>
|
||||
<button id="approveAiNoIssueBtn" class="bulk-action is-hidden" type="button">通过所有AI已通过</button>
|
||||
<button id="batchExportBtn" class="bulk-action export-action is-hidden" type="button">批量导出当前筛选</button>
|
||||
</div>
|
||||
<div class="record-list" id="recordList" tabindex="0"></div>
|
||||
</aside>
|
||||
@@ -133,7 +134,7 @@
|
||||
</div>
|
||||
<div id="targetSummary" class="target-summary"></div>
|
||||
<form id="detailForm" class="detail-form structured-form"></form>
|
||||
<details class="review-log-panel collapsible-panel" open>
|
||||
<details class="review-log-panel collapsible-panel">
|
||||
<summary class="section-head compact">
|
||||
<h2>修改记录</h2>
|
||||
<span id="changeLogCount">0 条</span>
|
||||
@@ -207,13 +208,13 @@
|
||||
<label for="auditNotes">抽查人工备注</label>
|
||||
<textarea id="auditNotes" rows="3" placeholder="填写抽查判断、修改原因或后续处理说明"></textarea>
|
||||
</div>
|
||||
<section class="review-log-panel">
|
||||
<div class="section-head compact">
|
||||
<details class="review-log-panel collapsible-panel">
|
||||
<summary class="section-head compact">
|
||||
<h2>修改记录</h2>
|
||||
<span id="auditChangeLogCount">0 条</span>
|
||||
</div>
|
||||
</summary>
|
||||
<div id="auditChangeLogTable" class="data-table-wrap"></div>
|
||||
</section>
|
||||
</details>
|
||||
</aside>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -362,6 +362,12 @@ h2 {
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.export-action {
|
||||
border-color: #b6c3d0;
|
||||
background: #f7fafc;
|
||||
color: var(--accent-dark);
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
@@ -397,6 +403,7 @@ textarea {
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
padding: 9px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.record-item:hover,
|
||||
@@ -471,10 +478,34 @@ textarea {
|
||||
|
||||
.record-time {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
color: #7b8794;
|
||||
}
|
||||
|
||||
.record-footer {
|
||||
min-height: 26px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.record-download {
|
||||
flex: 0 0 auto;
|
||||
min-height: 24px;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid #b6c3d0;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
color: var(--accent);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.record-download:hover {
|
||||
border-color: var(--accent);
|
||||
background: #eef6fb;
|
||||
}
|
||||
|
||||
.pdf-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user