diff --git a/WebSite/src/App.tsx b/WebSite/src/App.tsx index 81a13c5..a961289 100644 --- a/WebSite/src/App.tsx +++ b/WebSite/src/App.tsx @@ -30,7 +30,9 @@ import { FolderOpen, Server, AlertCircle, - Eye + Eye, + Info, + X } from 'lucide-react'; // --- Types --- @@ -66,6 +68,16 @@ type LibraryItem = { source?: 'seed' | 'upload'; }; +type LibraryInfo = { + id: string; + patientId: string; + fileCount: number; + groups: { + title: string; + items: { label: string; value: string }[]; + }[]; +}; + const API_BASE = typeof window === 'undefined' ? 'http://127.0.0.1:8787' : `${window.location.protocol}//${window.location.hostname}:8787`; @@ -183,6 +195,8 @@ export default function App() { const [libraryData, setLibraryData] = useState([]); const [selectedLibraryId, setSelectedLibraryId] = useState(''); const [isUploadingDicom, setIsUploadingDicom] = useState(false); + const [libraryInfo, setLibraryInfo] = useState(null); + const [isLibraryInfoLoading, setIsLibraryInfoLoading] = useState(false); const folderUploadInputRef = useRef(null); const zipUploadInputRef = useRef(null); @@ -416,6 +430,19 @@ export default function App() { } }; + const showLibraryInfo = async (item: LibraryItem) => { + setIsLibraryInfoLoading(true); + setLibraryInfo(null); + try { + const data = await apiRequest(`/api/library/info?id=${encodeURIComponent(item.id)}`) as LibraryInfo; + setLibraryInfo(data); + } catch (error) { + showToast((error as Error).message); + } finally { + setIsLibraryInfoLoading(false); + } + }; + const changePassword = (userId: string, newPass: string) => { setUsers(users.map(u => u.id === userId ? { ...u, password: newPass } : u)); setPwChangeInput(''); @@ -904,7 +931,7 @@ export default function App() { {item.fileCount || 0} 张 -
+
+
@@ -1093,6 +1126,54 @@ export default function App() { + {(libraryInfo || isLibraryInfoLoading) && ( +
+
+
+
+

DICOM 基本信息

+

+ {libraryInfo?.patientId || '正在读取影像信息'} +

+
+ +
+ + {isLibraryInfoLoading ? ( +
+ 正在读取 DICOM 头信息... +
+ ) : ( +
+
+ {libraryInfo?.groups.map(group => ( +
+

{group.title}

+
+ {group.items.map(item => ( +
+ {item.label} + {item.value || '-'} +
+ ))} +
+
+ ))} +
+
+ )} +
+
+ )} + {/* Toast */} {toastMessage && (
diff --git a/web_backend.py b/web_backend.py index 5f14988..9e9fa22 100644 --- a/web_backend.py +++ b/web_backend.py @@ -17,6 +17,7 @@ from urllib.parse import parse_qs, quote, unquote, urlparse os.environ.setdefault("MPLCONFIGDIR", "/tmp/head_ct_morph_matplotlib") import pydicom +from pydicom.multival import MultiValue from PIL import Image from generate_head_extension_video import generate_video @@ -187,6 +188,103 @@ def make_library_slice_preview(item_id, index): } +def dicom_value(ds, name, fallback="-"): + value = getattr(ds, name, fallback) + if value in [None, ""]: + return fallback + if isinstance(value, (list, tuple, MultiValue)): + return " / ".join(str(item) for item in value) + return str(value) + + +def dicom_date(value): + value = str(value or "") + if len(value) == 8 and value.isdigit(): + return f"{value[0:4]}-{value[4:6]}-{value[6:8]}" + return value or "-" + + +def dicom_time(value): + value = str(value or "") + if len(value) >= 6 and value[:6].isdigit(): + return f"{value[0:2]}:{value[2:4]}:{value[4:6]}" + return value or "-" + + +def make_library_info(item_id): + item = find_library_item(item_id) + if not item: + raise RuntimeError("影像库中没有找到该数据。") + + dicom_files = sorted_dicom_files(item["dicomPath"]) + if not dicom_files: + raise RuntimeError("该影像数据没有可读取的 .dcm 文件。") + + first = pydicom.dcmread(str(dicom_files[0]), stop_before_pixels=True, force=True) + last = pydicom.dcmread(str(dicom_files[-1]), stop_before_pixels=True, force=True) + pixel_spacing = dicom_value(first, "PixelSpacing") + matrix = f"{dicom_value(first, 'Columns')} x {dicom_value(first, 'Rows')}" + instance_range = f"{dicom_value(first, 'InstanceNumber')} - {dicom_value(last, 'InstanceNumber')}" + + return { + "id": item["id"], + "patientId": item["patientId"], + "fileCount": len(dicom_files), + "groups": [ + { + "title": "患者信息", + "items": [ + {"label": "患者姓名", "value": dicom_value(first, "PatientName")}, + {"label": "患者 ID", "value": dicom_value(first, "PatientID")}, + {"label": "性别", "value": dicom_value(first, "PatientSex")}, + {"label": "年龄", "value": dicom_value(first, "PatientAge")}, + ], + }, + { + "title": "检查信息", + "items": [ + {"label": "检查日期", "value": dicom_date(getattr(first, "StudyDate", ""))}, + {"label": "检查时间", "value": dicom_time(getattr(first, "StudyTime", ""))}, + {"label": "检查描述", "value": dicom_value(first, "StudyDescription")}, + {"label": "检查号", "value": dicom_value(first, "AccessionNumber")}, + {"label": "机构", "value": dicom_value(first, "InstitutionName")}, + ], + }, + { + "title": "序列信息", + "items": [ + {"label": "模态", "value": dicom_value(first, "Modality")}, + {"label": "部位", "value": dicom_value(first, "BodyPartExamined")}, + {"label": "序列描述", "value": dicom_value(first, "SeriesDescription")}, + {"label": "序列号", "value": dicom_value(first, "SeriesNumber")}, + {"label": "制造商", "value": dicom_value(first, "Manufacturer")}, + ], + }, + { + "title": "图像参数", + "items": [ + {"label": "切片数", "value": str(len(dicom_files))}, + {"label": "Instance 范围", "value": instance_range}, + {"label": "矩阵", "value": matrix}, + {"label": "像素间距", "value": pixel_spacing}, + {"label": "层厚", "value": dicom_value(first, "SliceThickness")}, + {"label": "层间距", "value": dicom_value(first, "SpacingBetweenSlices")}, + {"label": "卷积核", "value": dicom_value(first, "ConvolutionKernel")}, + ], + }, + { + "title": "扫描参数", + "items": [ + {"label": "KVP", "value": dicom_value(first, "KVP")}, + {"label": "管电流", "value": dicom_value(first, "XRayTubeCurrent")}, + {"label": "曝光时间", "value": dicom_value(first, "ExposureTime")}, + {"label": "重建直径", "value": dicom_value(first, "ReconstructionDiameter")}, + ], + }, + ], + } + + def parse_multipart(headers, body): content_type = headers.get("content-type", "") message = BytesParser(policy=default).parsebytes( @@ -418,6 +516,12 @@ class Handler(BaseHTTPRequestHandler): self.send_json(make_library_slice_preview(item_id, index)) return + if parsed.path == "/api/library/info": + params = parse_qs(parsed.query) + item_id = params.get("id", [""])[0] + self.send_json(make_library_info(item_id)) + return + if parsed.path == "/api/job": params = parse_qs(parsed.query) job_id = params.get("id", [""])[0]