diff --git a/PACS_DICOM处理/数据处理网页端/app.py b/PACS_DICOM处理/数据处理网页端/app.py index 352e94e..5628d1d 100644 --- a/PACS_DICOM处理/数据处理网页端/app.py +++ b/PACS_DICOM处理/数据处理网页端/app.py @@ -317,6 +317,7 @@ def ensure_study_summary_table() -> None: completed boolean NOT NULL DEFAULT false, completed_by text, completed_at timestamptz, + completion_updated_at timestamptz, refreshed_at timestamptz NOT NULL DEFAULT now() ); ALTER TABLE public.pacs_dicom_study_summaries @@ -327,6 +328,8 @@ def ensure_study_summary_table() -> None: ADD COLUMN IF NOT EXISTS completed_by text; ALTER TABLE public.pacs_dicom_study_summaries ADD COLUMN IF NOT EXISTS completed_at timestamptz; + ALTER TABLE public.pacs_dicom_study_summaries + ADD COLUMN IF NOT EXISTS completion_updated_at timestamptz; """ pg_scalar(sql) @@ -511,7 +514,8 @@ def annotation_overview_by_study() -> dict[str, dict[str, Any]]: COALESCE( jsonb_agg(DISTINCT part.value) FILTER (WHERE part.value IS NOT NULL), '[]'::jsonb - ) AS body_parts + ) AS body_parts, + max(a.updated_at) FILTER (WHERE COALESCE(a.updated_by, '') <> 'system') AS annotation_updated_at FROM public.pacs_dicom_series_annotations a LEFT JOIN LATERAL jsonb_array_elements_text(a.body_parts) AS part(value) ON true GROUP BY a.ct_number @@ -530,7 +534,7 @@ def summary_rows_by_study() -> dict[str, dict[str, Any]]: """ SELECT ct_number, series_count, annotated_series, unannotated_series, - body_parts, completed, completed_by, completed_at, refreshed_at + body_parts, completed, completed_by, completed_at, completion_updated_at, refreshed_at FROM public.pacs_dicom_study_summaries """, timeout=8, @@ -545,7 +549,7 @@ def study_completion_fields(ct_number: str) -> dict[str, Any]: ensure_study_summary_table() rows = pg_json_rows( f""" - SELECT completed, completed_by, completed_at + SELECT completed, completed_by, completed_at, completion_updated_at FROM public.pacs_dicom_study_summaries WHERE ct_number = {sql_literal(ct_number)} LIMIT 1 @@ -559,6 +563,7 @@ def study_completion_fields(ct_number: str) -> dict[str, Any]: "completed": bool(row.get("completed", False)), "completed_by": row.get("completed_by", ""), "completed_at": row.get("completed_at", ""), + "completion_updated_at": row.get("completion_updated_at", ""), } @@ -593,6 +598,12 @@ def studies(_: str = Depends(require_auth), q: str = "", limit: int = 200) -> li row["completed"] = bool((summary or {}).get("completed", False)) row["completed_by"] = (summary or {}).get("completed_by", "") row["completed_at"] = (summary or {}).get("completed_at", "") + row["completion_updated_at"] = (summary or {}).get("completion_updated_at", "") + row["annotation_updated_at"] = annotation_summary.get("annotation_updated_at", "") + row["modified_at"] = max( + str(row.get("annotation_updated_at") or ""), + str(row.get("completion_updated_at") or ""), + ) row["summary_updated_at"] = (summary or {}).get("refreshed_at", "") return rows @@ -1218,19 +1229,21 @@ def update_study_completion(ct_number: str, data: StudyCompletionIn, user: str = pg_scalar( f""" INSERT INTO public.pacs_dicom_study_summaries ( - ct_number, completed, completed_by, completed_at, refreshed_at + ct_number, completed, completed_by, completed_at, completion_updated_at, refreshed_at ) VALUES ( {sql_literal(ct_number)}, {'true' if data.completed else 'false'}, {sql_literal(user)}, {'now()' if data.completed else 'NULL'}, + now(), now() ) ON CONFLICT (ct_number) DO UPDATE SET completed = EXCLUDED.completed, completed_by = EXCLUDED.completed_by, - completed_at = EXCLUDED.completed_at + completed_at = EXCLUDED.completed_at, + completion_updated_at = EXCLUDED.completion_updated_at """ ) return {"ok": True, "ct_number": ct_number, **study_completion_fields(ct_number)} @@ -1593,7 +1606,9 @@ def save_annotation_payload( "skipped": skipped, "ai_skipped": ai_skipped, "notes": notes, + "updated_at": time.strftime("%Y-%m-%dT%H:%M:%S"), "ai_model": ai_model or series_row.get("annotation", {}).get("ai_model", ""), + "updated_by": user, } ) diff --git a/PACS_DICOM处理/数据处理网页端/static/app.js b/PACS_DICOM处理/数据处理网页端/static/app.js index bdd6f6c..07e793f 100644 --- a/PACS_DICOM处理/数据处理网页端/static/app.js +++ b/PACS_DICOM处理/数据处理网页端/static/app.js @@ -24,7 +24,7 @@ const app = { saving: false, pendingImage: null, drag: null, - studySortField: "time", + studySortField: "modified", studySortDir: "desc", studyFilter: "all", studyPartFilter: "all", @@ -117,6 +117,11 @@ function studyTimeKey(study) { return `${date}${time}`; } +function studyModifiedKey(study) { + const raw = String(study.modified_at || study.annotation_updated_at || study.completion_updated_at || study.completed_at || ""); + return raw ? raw.replace(/\D/g, "").padEnd(14, "0") : studyTimeKey(study); +} + function sortArrow(direction) { return direction === "asc" ? "↑" : "↓"; } @@ -240,8 +245,8 @@ function sortStudies(list) { const timeResult = studyTimeKey(a).localeCompare(studyTimeKey(b)) || String(a.ct_number || "").localeCompare(String(b.ct_number || ""), undefined, { numeric: true }); - const unannotatedResult = Number(a.unannotated_series || 0) - Number(b.unannotated_series || 0) || timeResult; - const result = app.studySortField === "unannotated" ? unannotatedResult : timeResult; + const modifiedResult = studyModifiedKey(a).localeCompare(studyModifiedKey(b)) || timeResult; + const result = app.studySortField === "study_time" ? timeResult : modifiedResult; return app.studySortDir === "desc" ? -result : result; }); } @@ -509,16 +514,21 @@ function renderStudies() { function applyStudyCompletion(data) { const completed = Boolean(data.completed); + const modifiedAt = data.completion_updated_at || data.modified_at || data.completed_at || ""; const study = app.studies.find((item) => item.ct_number === data.ct_number); if (study) { study.completed = completed; study.completed_by = data.completed_by || ""; study.completed_at = data.completed_at || ""; + study.completion_updated_at = data.completion_updated_at || study.completion_updated_at || ""; + if (modifiedAt) study.modified_at = modifiedAt; } if (app.study?.ct_number === data.ct_number) { app.study.completed = completed; app.study.completed_by = data.completed_by || ""; app.study.completed_at = data.completed_at || ""; + app.study.completion_updated_at = data.completion_updated_at || app.study.completion_updated_at || ""; + if (modifiedAt) app.study.modified_at = modifiedAt; } } @@ -993,6 +1003,16 @@ function updateLocalAnnotation(annotation, targetSeriesUid = app.activeSeries?.s if (options.updateNotes !== false) $("annotationNotes").value = normalized.notes || ""; } refreshStudyAnnotationSummary(); + const modifiedAt = normalized.updated_at || new Date().toISOString(); + if (app.study && modifiedAt) { + app.study.modified_at = modifiedAt; + app.study.annotation_updated_at = modifiedAt; + const study = app.studies.find((item) => item.ct_number === app.study.ct_number); + if (study) { + study.modified_at = modifiedAt; + study.annotation_updated_at = modifiedAt; + } + } renderSeries(); renderStudies(); if (isActive && applyToActive) applyAnnotationControls(); @@ -1353,7 +1373,11 @@ function changeZoom(multiplier) { function wireImageGestures() { const wrap = document.querySelector(".image-wrap"); - document.querySelector(".slice-rail").addEventListener("pointerdown", (event) => event.stopPropagation()); + const sliceRail = document.querySelector(".slice-rail"); + ["pointerdown", "pointermove", "pointerup", "pointercancel", "click"].forEach((name) => { + sliceRail.addEventListener(name, (event) => event.stopPropagation()); + }); + sliceRail.addEventListener("wheel", (event) => event.stopPropagation(), { passive: true }); wrap.addEventListener( "wheel", (event) => { diff --git a/PACS_DICOM处理/数据处理网页端/static/index.html b/PACS_DICOM处理/数据处理网页端/static/index.html index 39aa30e..83f375b 100644 --- a/PACS_DICOM处理/数据处理网页端/static/index.html +++ b/PACS_DICOM处理/数据处理网页端/static/index.html @@ -45,8 +45,8 @@