From 3ef25e734f8d83ceb7f2e49cb31ca1f8b8f7aa7e Mon Sep 17 00:00:00 2001 From: MingNian <1281442923@qq.com> Date: Thu, 21 May 2026 15:20:55 +0800 Subject: [PATCH] fix: bat uses dotnet directly, doctor frontend gets port 5174 --- .../Controllers/FileController.cs | 104 ++++++++ .../src/components/layout/DoctorLayout.tsx | 7 +- .../src/pages/reports/ReportDetailPage.tsx | 226 ++++++++++++++---- frontend-doctor/vite.config.ts | 3 + .../src/components/layout/AppLayout.tsx | 17 +- .../src/components/layout/StackLayout.tsx | 19 +- .../src/pages/services/ReportUploadPage.tsx | 46 ++-- .../src/services/report.service.ts | 4 +- start-dev.bat | 15 +- 9 files changed, 358 insertions(+), 83 deletions(-) create mode 100644 backend/src/HealthManager.WebApi/Controllers/FileController.cs diff --git a/backend/src/HealthManager.WebApi/Controllers/FileController.cs b/backend/src/HealthManager.WebApi/Controllers/FileController.cs new file mode 100644 index 0000000..0e96ae5 --- /dev/null +++ b/backend/src/HealthManager.WebApi/Controllers/FileController.cs @@ -0,0 +1,104 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace HealthManager.WebApi.Controllers; + +[ApiController] +[Route("api/files")] +[Authorize] +public class FileController : ControllerBase +{ + private static readonly string UploadDir = Path.Combine( + Directory.GetCurrentDirectory(), "..", "..", "..", "data", "uploads"); + + public FileController() + { + if (!Directory.Exists(UploadDir)) + Directory.CreateDirectory(UploadDir); + } + + [HttpPost("upload")] + public async Task Upload(IFormFile file) + { + if (file == null || file.Length == 0) + return BadRequest(new { message = "No file selected" }); + + if (file.Length > 10 * 1024 * 1024) + return BadRequest(new { message = "File too large (max 10MB)" }); + + var allowedTypes = new[] { "image/jpeg", "image/png", "image/gif", "image/webp", "application/pdf" }; + if (!allowedTypes.Contains(file.ContentType.ToLower())) + return BadRequest(new { message = "Unsupported file type" }); + + var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}"; + var filePath = Path.Combine(UploadDir, fileName); + + using (var stream = new FileStream(filePath, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + + var url = $"/api/files/{fileName}"; + return Ok(new { url, fileName }); + } + + [HttpPost("upload-multiple")] + public async Task UploadMultiple(List files) + { + if (files == null || files.Count == 0) + return BadRequest(new { message = "No files selected" }); + + var results = new List(); + foreach (var file in files) + { + if (file.Length == 0) continue; + if (file.Length > 10 * 1024 * 1024) continue; + + var allowedTypes = new[] { "image/jpeg", "image/png", "image/gif", "image/webp", "application/pdf" }; + if (!allowedTypes.Contains(file.ContentType.ToLower())) continue; + + var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}"; + var filePath = Path.Combine(UploadDir, fileName); + + using (var stream = new FileStream(filePath, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + + results.Add(new { url = $"/api/files/{fileName}", fileName }); + } + + return Ok(results); + } + + [HttpGet("{fileName}")] + [AllowAnonymous] + public IActionResult Download(string fileName) + { + var filePath = Path.Combine(UploadDir, fileName); + + // Security: prevent path traversal + if (!filePath.StartsWith(UploadDir)) + return NotFound(); + + if (!System.IO.File.Exists(filePath)) + return NotFound(); + + var contentType = GetContentType(fileName); + return PhysicalFile(filePath, contentType); + } + + private static string GetContentType(string fileName) + { + var ext = Path.GetExtension(fileName).ToLower(); + return ext switch + { + ".jpg" or ".jpeg" => "image/jpeg", + ".png" => "image/png", + ".gif" => "image/gif", + ".webp" => "image/webp", + ".pdf" => "application/pdf", + _ => "application/octet-stream", + }; + } +} diff --git a/frontend-doctor/src/components/layout/DoctorLayout.tsx b/frontend-doctor/src/components/layout/DoctorLayout.tsx index 575a6bd..99c1515 100644 --- a/frontend-doctor/src/components/layout/DoctorLayout.tsx +++ b/frontend-doctor/src/components/layout/DoctorLayout.tsx @@ -1,4 +1,4 @@ -import { Outlet, NavLink, useNavigate } from 'react-router-dom'; +import { Outlet, NavLink, useNavigate, useLocation } from 'react-router-dom'; import { useAuthStore } from '../../stores/auth.store'; const navItems = [ @@ -16,6 +16,7 @@ const textMuted = '#8E9DB5'; export function DoctorLayout() { const { user, logout } = useAuthStore(); const navigate = useNavigate(); + const location = useLocation(); const handleLogout = () => { logout(); @@ -76,7 +77,9 @@ export function DoctorLayout() { {/* Main content */}
- +
+ +
); diff --git a/frontend-doctor/src/pages/reports/ReportDetailPage.tsx b/frontend-doctor/src/pages/reports/ReportDetailPage.tsx index 19b8de2..1690a24 100644 --- a/frontend-doctor/src/pages/reports/ReportDetailPage.tsx +++ b/frontend-doctor/src/pages/reports/ReportDetailPage.tsx @@ -8,13 +8,26 @@ interface RawReport { summary?: string; suggestions?: string; patientName?: string; doctorName?: string; createdAt: string; completedAt?: string; - items?: { id: string; itemName: string; resultValue: string; unit?: string; referenceRange?: string; isAbnormal: boolean }[]; + items?: RawItem[]; +} + +interface RawItem { + id: string; itemName: string; resultValue: string; + unit?: string; referenceRange?: string; isAbnormal: boolean; } export function ReportDetailPage() { const { id } = useParams<{ id: string }>(); const [report, setReport] = useState(null); - const [interpretation, setInterpretation] = useState(''); + const [lightbox, setLightbox] = useState(null); + + // Interpretation form + const [summary, setSummary] = useState(''); + const [riskLevel, setRiskLevel] = useState('normal'); + const [suggestions, setSuggestions] = useState(''); + const [items, setItems] = useState<{ itemName: string; resultValue: string; unit: string; referenceRange: string; isAbnormal: boolean }[]>([ + { itemName: '', resultValue: '', unit: '', referenceRange: '', isAbnormal: false }, + ]); const [submitting, setSubmitting] = useState(false); useEffect(() => { @@ -22,69 +35,130 @@ export function ReportDetailPage() { api.get(`/api/reports/${id}`).then((r) => setReport(r.data)).catch(() => {}); }, [id]); + const addItem = () => { + setItems((prev) => [...prev, { itemName: '', resultValue: '', unit: '', referenceRange: '', isAbnormal: false }]); + }; + + const updateItem = (index: number, field: string, value: string | boolean) => { + setItems((prev) => prev.map((item, i) => i === index ? { ...item, [field]: value } : item)); + }; + + const removeItem = (index: number) => { + if (items.length <= 1) return; + setItems((prev) => prev.filter((_, i) => i !== index)); + }; + const handleInterpret = async () => { - if (!interpretation.trim() || !id) return; + if (!summary.trim() || !id) return; setSubmitting(true); try { await api.post(`/api/reports/${id}/interpret`, { - summary: interpretation, - items: [], - riskLevel: 'normal', - suggestions: null, + summary, + items: items.filter((it) => it.itemName.trim()), + riskLevel, + suggestions: suggestions || null, }); - // Refetch report to show updated status const updated = await api.get(`/api/reports/${id}`); setReport(updated.data); - setInterpretation(''); - alert('interpretation submitted'); } catch { alert('submit failed'); } finally { setSubmitting(false); } }; - if (!report) return
loading...
; + if (!report) return
Loading...
; + + const isCompleted = report.status === 'completed'; return (
- ← back to reports + ← Back to Reports
-

{report.title}

-
-
Patient: {report.patientName || 'unknown'}
-
Category: {report.category}
-
Status: {report.status}
-
Submitted: {report.createdAt?.split('T')[0]}
+
+
+

{report.title}

+
+ Patient: {report.patientName || 'unknown'}  |  + Category: {report.category}  |  + Date: {report.createdAt?.split('T')[0]} +
+
+ + {isCompleted ? 'Completed' : 'Pending Review'} +
+ {/* Image gallery */} {report.imageUrls && report.imageUrls.length > 0 && ( -
-

Report Images

- {report.imageUrls.map((url, i) => ( -
- {url} -
- ))} +
+

Uploaded Images ({report.imageUrls.length})

+
+ {report.imageUrls.map((url, i) => ( +
setLightbox(url)} + style={{ + width: 120, height: 120, borderRadius: 8, overflow: 'hidden', + cursor: 'pointer', border: '2px solid #eee', + background: '#f5f5f5', display: 'flex', alignItems: 'center', justifyContent: 'center', + }} + > + {`report-${i}`} { (e.target as HTMLImageElement).style.display = 'none'; }} + /> +
+ ))} +
)} - {report.status === 'completed' && ( -
-

Interpretation Result

-

{report.summary || 'No summary'}

- {report.riskLevel &&

Risk: {report.riskLevel}

} - {report.suggestions &&

Suggestions: {report.suggestions}

} + {/* Lightbox */} + {lightbox && ( +
setLightbox(null)} style={{ + position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.85)', + display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 999, + cursor: 'pointer', + }}> + preview +
+ )} + + {/* Completed interpretation display */} + {isCompleted && ( +
+

Interpretation Result

+
+

Risk Level: {report.riskLevel || '-'}

+

Summary: {report.summary || '-'}

+ {report.suggestions &&

Suggestions: {report.suggestions}

} +
+ {report.items && report.items.length > 0 && ( - - - +
ItemValueRangeAbnormal
+ + + + + {report.items.map((item) => ( - - - - - + + + + + ))} @@ -93,16 +167,72 @@ export function ReportDetailPage() { )} - {report.status !== 'completed' && ( -
-

Doctor Interpretation

-
ItemResultReferenceAbnormal
{item.itemName}{item.resultValue} {item.unit || ''}{item.referenceRange || '-'}{item.isAbnormal ? 'Yes' : 'No'}
{item.itemName}{item.resultValue} {item.unit || ''}{item.referenceRange || '-'} + {item.isAbnormal ? 'Yes' : 'No'} +