${escapeHtml(record.patient_name || "未命名")}
${statusTag(displayStatusValue(record))}
@@ -409,19 +416,37 @@ function renderRecords() {
${escapeHtml(record.inpatient_no || record.medical_record_no || "")} · ${escapeHtml(record.major_department || record.discharge_dept || "")}${manual}
${escapeHtml(record.primary_diagnosis || "")}
- ${activity}
+
-
+
`;
}).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) => `
${escapeHtml(note)}`).join("")
: "
AI未提出需要复核的内容";
$(targetId).innerHTML = `
-
+
复核定位
${baseGroups.length ? `${baseGroups.length} 个模块` : "未定位"}
@@ -662,7 +713,7 @@ function renderTargetSummary(targets, targetId = "targetSummary", formId = "deta
${targets.hasAiNotes ? `
-
+
AI建议复核点
${aiGroups.length ? `${aiGroups.length} 个模块` : "无待看点"}
@@ -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));
diff --git a/患者首页处理/数据可视化网页端/app/static/index.html b/患者首页处理/数据可视化网页端/app/static/index.html
index f26979e..9ef7a4d 100644
--- a/患者首页处理/数据可视化网页端/app/static/index.html
+++ b/患者首页处理/数据可视化网页端/app/static/index.html
@@ -98,6 +98,7 @@