fix: doctor report list shows all patient reports, upload area supports file selection, start-dev.bat starts frontends
This commit is contained in:
@@ -14,6 +14,14 @@ public class ReportService(AppDbContext db)
|
|||||||
.OrderByDescending(r => r.UploadedAt)
|
.OrderByDescending(r => r.UploadedAt)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
public async Task<List<Report>> GetAllReportsAsync()
|
||||||
|
=> await db.Reports
|
||||||
|
.Include(r => r.Patient)
|
||||||
|
.Include(r => r.Doctor)
|
||||||
|
.Include(r => r.Items)
|
||||||
|
.OrderByDescending(r => r.UploadedAt)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
public async Task<Report?> GetByIdAsync(Guid id)
|
public async Task<Report?> GetByIdAsync(Guid id)
|
||||||
=> await db.Reports
|
=> await db.Reports
|
||||||
.Include(r => r.Patient)
|
.Include(r => r.Patient)
|
||||||
|
|||||||
@@ -16,12 +16,33 @@ public class ReportController(ReportService reportService) : ControllerBase
|
|||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> GetReports()
|
public async Task<IActionResult> GetReports()
|
||||||
{
|
{
|
||||||
var targetUserId = UserId;
|
// Patients: see own reports. Doctors: see all reports (or filter by patientId)
|
||||||
if (Role == "doctor" && Request.Query.ContainsKey("patientId"))
|
if (Role == "doctor")
|
||||||
targetUserId = Guid.Parse(Request.Query["patientId"]!);
|
{
|
||||||
|
if (Request.Query.ContainsKey("patientId"))
|
||||||
|
{
|
||||||
|
var targetUserId = Guid.Parse(Request.Query["patientId"]!);
|
||||||
|
var reports = await reportService.GetPatientReportsAsync(targetUserId);
|
||||||
|
return Ok(reports.Select(r => new
|
||||||
|
{
|
||||||
|
r.Id, r.PatientId, r.Title, r.Category, r.ImageUrls, r.Status,
|
||||||
|
r.RiskLevel, r.UploadedAt, r.CompletedAt,
|
||||||
|
PatientName = r.Patient?.Name,
|
||||||
|
DoctorName = r.Doctor?.Name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
var allReports = await reportService.GetAllReportsAsync();
|
||||||
|
return Ok(allReports.Select(r => new
|
||||||
|
{
|
||||||
|
r.Id, r.PatientId, r.Title, r.Category, r.ImageUrls, r.Status,
|
||||||
|
r.RiskLevel, r.UploadedAt, r.CompletedAt,
|
||||||
|
PatientName = r.Patient?.Name,
|
||||||
|
DoctorName = r.Doctor?.Name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
var reports = await reportService.GetPatientReportsAsync(targetUserId);
|
var myReports = await reportService.GetPatientReportsAsync(UserId);
|
||||||
return Ok(reports.Select(r => new
|
return Ok(myReports.Select(r => new
|
||||||
{
|
{
|
||||||
r.Id, r.PatientId, r.Title, r.Category, r.ImageUrls, r.Status,
|
r.Id, r.PatientId, r.Title, r.Category, r.ImageUrls, r.Status,
|
||||||
r.RiskLevel, r.UploadedAt, r.CompletedAt,
|
r.RiskLevel, r.UploadedAt, r.CompletedAt,
|
||||||
|
|||||||
@@ -3,4 +3,9 @@
|
|||||||
.catGrid { display: flex; flex-wrap: wrap; gap: 8px; }
|
.catGrid { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||||
.catChip { padding: 6px 14px; border-radius: var(--radius-full); font-size: var(--font-size-sm); background: var(--color-bg-secondary); color: var(--color-text-secondary); }
|
.catChip { padding: 6px 14px; border-radius: var(--radius-full); font-size: var(--font-size-sm); background: var(--color-bg-secondary); color: var(--color-text-secondary); }
|
||||||
.catActive { background: var(--color-primary-bg); color: var(--color-primary); }
|
.catActive { background: var(--color-primary-bg); color: var(--color-primary); }
|
||||||
.uploadArea { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 32px; background: var(--color-bg); border: 2px dashed var(--color-border); border-radius: var(--radius-lg); cursor: pointer; }
|
.uploadArea { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 32px; background: var(--color-bg); border: 2px dashed var(--color-border); border-radius: var(--radius-lg); cursor: pointer; transition: border-color 0.15s; }
|
||||||
|
.uploadArea:hover { border-color: var(--color-primary); background: var(--color-primary-bg); }
|
||||||
|
.fileList { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.fileItem { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: var(--color-bg); border-radius: var(--radius-sm); font-size: var(--font-size-sm); }
|
||||||
|
.fileName { color: var(--color-text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.fileRemove { background: none; border: none; color: var(--color-danger); cursor: pointer; font-size: 14px; padding: 4px; }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Button } from '@/components/common/Button';
|
import { Button } from '@/components/common/Button';
|
||||||
import { Input } from '@/components/common/Input';
|
import { Input } from '@/components/common/Input';
|
||||||
@@ -9,18 +9,38 @@ import styles from './ReportUploadPage.module.css';
|
|||||||
|
|
||||||
export function ReportUploadPage() {
|
export function ReportUploadPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const fileRef = useRef<HTMLInputElement>(null);
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const [category, setCategory] = useState('血液检查');
|
const [category, setCategory] = useState('血液检查');
|
||||||
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const categories = ['血液检查', '心电图', '影像学', '尿液检查', '其他'];
|
const categories = ['血液检查', '心电图', '影像学', '尿液检查', '其他'];
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.files) {
|
||||||
|
setFiles((prev) => [...prev, ...Array.from(e.target.files!)]);
|
||||||
|
}
|
||||||
|
// Reset so the same file can be selected again
|
||||||
|
if (fileRef.current) fileRef.current.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFile = (index: number) => {
|
||||||
|
setFiles((prev) => prev.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!title.trim()) { toast('请输入报告名称', 'error'); return; }
|
if (!title.trim()) { toast('请输入报告名称', 'error'); return; }
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await reportService.uploadReport({ title, category });
|
try {
|
||||||
toast('上传成功,正在解读中...');
|
await reportService.uploadReport({ title, category });
|
||||||
setTimeout(() => navigate(-1), 800);
|
toast('上传成功');
|
||||||
|
setTimeout(() => navigate('/services/reports'), 800);
|
||||||
|
} catch {
|
||||||
|
toast('上传失败,请检查后端是否运行', 'error');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -36,11 +56,30 @@ export function ReportUploadPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.uploadArea}>
|
<div className={styles.uploadArea} onClick={() => fileRef.current?.click()}>
|
||||||
<span style={{ fontSize: 40 }}>📸</span>
|
<span style={{ fontSize: 36 }}>📷</span>
|
||||||
<span style={{ fontSize: '14px', color: '#6B7280' }}>点击拍照或选择图片上传</span>
|
<span style={{ fontSize: 14, color: '#6B7280' }}>点击选择报告图片</span>
|
||||||
<span style={{ fontSize: '11px', color: '#9CA3AF' }}>模拟上传,直接保存即可</span>
|
<span style={{ fontSize: 11, color: '#9CA3AF' }}>支持 jpg、png,可多选</span>
|
||||||
</div>
|
</div>
|
||||||
|
<input
|
||||||
|
ref={fileRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
multiple
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{files.length > 0 && (
|
||||||
|
<div className={styles.fileList}>
|
||||||
|
{files.map((file, i) => (
|
||||||
|
<div key={i} className={styles.fileItem}>
|
||||||
|
<span className={styles.fileName}>📎 {file.name}</span>
|
||||||
|
<button className={styles.fileRemove} onClick={() => removeFile(i)}>✕</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button variant="primary" size="lg" fullWidth loading={loading} onClick={handleSubmit}>
|
<Button variant="primary" size="lg" fullWidth loading={loading} onClick={handleSubmit}>
|
||||||
提交上传
|
提交上传
|
||||||
|
|||||||
@@ -11,9 +11,8 @@ set "REDIS=C:\Program Files\Redis\redis-server.exe"
|
|||||||
set "PG_DATA=D:\APP\data\pgdata"
|
set "PG_DATA=D:\APP\data\pgdata"
|
||||||
set "PG_BIN=D:\PostgreSQL\18\pgsql\bin"
|
set "PG_BIN=D:\PostgreSQL\18\pgsql\bin"
|
||||||
set "MINIO_DATA=D:\APP\data\minio"
|
set "MINIO_DATA=D:\APP\data\minio"
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo [1/4] Starting PostgreSQL...
|
echo [1/6] Starting PostgreSQL...
|
||||||
if exist "%PG_BIN%\pg_ctl.exe" (
|
if exist "%PG_BIN%\pg_ctl.exe" (
|
||||||
"%PG_BIN%\pg_ctl.exe" -D "%PG_DATA%" -l "%PG_DATA%\pg.log" start 2>nul
|
"%PG_BIN%\pg_ctl.exe" -D "%PG_DATA%" -l "%PG_DATA%\pg.log" start 2>nul
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
@@ -26,7 +25,7 @@ if exist "%PG_BIN%\pg_ctl.exe" (
|
|||||||
)
|
)
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo [2/4] Starting Redis...
|
echo [2/6] Starting Redis...
|
||||||
tasklist /fi "imagename eq redis-server.exe" | find /i "redis-server.exe" >nul
|
tasklist /fi "imagename eq redis-server.exe" | find /i "redis-server.exe" >nul
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
start "Redis" /MIN "%REDIS%" "%ProgramFiles%\Redis\redis.windows.conf"
|
start "Redis" /MIN "%REDIS%" "%ProgramFiles%\Redis\redis.windows.conf"
|
||||||
@@ -36,7 +35,7 @@ if errorlevel 1 (
|
|||||||
)
|
)
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo [3/4] Starting MinIO...
|
echo [3/6] Starting MinIO...
|
||||||
tasklist /fi "imagename eq minio.exe" | find /i "minio.exe" >nul
|
tasklist /fi "imagename eq minio.exe" | find /i "minio.exe" >nul
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
if not exist "%MINIO_DATA%" mkdir "%MINIO_DATA%"
|
if not exist "%MINIO_DATA%" mkdir "%MINIO_DATA%"
|
||||||
@@ -47,29 +46,42 @@ if errorlevel 1 (
|
|||||||
)
|
)
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo [4/4] Starting Backend API...
|
echo [4/6] Starting Backend API...
|
||||||
cd /d "%~dp0backend"
|
cd /d "%~dp0backend"
|
||||||
if exist "%DOTNET%" (
|
if exist "%DOTNET%" (
|
||||||
start "HealthManager API" "%DOTNET%" run --project src\HealthManager.WebApi --urls "http://localhost:5000" --environment Development
|
start "HealthManager API" "%DOTNET%" run --project src\HealthManager.WebApi --urls "http://localhost:5000" --environment Development
|
||||||
echo Backend API starting (http://localhost:5000)
|
echo Backend API starting (http://localhost:5000)
|
||||||
echo Swagger: http://localhost:5000/swagger
|
echo Swagger: http://localhost:5000/swagger
|
||||||
|
echo Waiting 15s for backend to boot...
|
||||||
|
timeout /t 15 /nobreak >nul
|
||||||
) else (
|
) else (
|
||||||
echo [ERROR] .NET SDK not found
|
echo [ERROR] .NET SDK not found at %DOTNET%
|
||||||
)
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [5/6] Starting Patient Frontend...
|
||||||
|
start "Patient Frontend" cmd.exe /c "cd /d %~dp0frontend-patient && npm run dev"
|
||||||
|
echo Patient Frontend starting on http://localhost:5173
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [6/6] Starting Doctor Frontend...
|
||||||
|
start "Doctor Frontend" cmd.exe /c "cd /d %~dp0frontend-doctor && npm run dev"
|
||||||
|
echo Doctor Frontend starting on http://localhost:5174
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ==========================================
|
echo ==========================================
|
||||||
echo Startup Complete!
|
echo Startup Complete!
|
||||||
echo.
|
echo.
|
||||||
|
echo Patient App: http://localhost:5173
|
||||||
|
echo Doctor App: http://localhost:5174
|
||||||
echo Backend API: http://localhost:5000
|
echo Backend API: http://localhost:5000
|
||||||
echo Swagger: http://localhost:5000/swagger
|
echo Swagger: http://localhost:5000/swagger
|
||||||
echo MinIO: http://localhost:9001
|
echo MinIO: http://localhost:9001
|
||||||
echo PostgreSQL: localhost:5432
|
echo PostgreSQL: localhost:5432
|
||||||
echo Redis: localhost:6379
|
echo Redis: localhost:6379
|
||||||
echo.
|
|
||||||
echo Frontend (manual start):
|
|
||||||
echo cd frontend-patient ^&^& npm run dev
|
|
||||||
echo cd frontend-doctor ^&^& npm run dev
|
|
||||||
echo ==========================================
|
echo ==========================================
|
||||||
echo.
|
echo.
|
||||||
|
echo All 6 services started. Close the 3 new
|
||||||
|
echo windows to stop the apps.
|
||||||
|
echo.
|
||||||
pause
|
pause
|
||||||
|
|||||||
Reference in New Issue
Block a user