feat: 医生端Web后台 + SignalR实时问诊

- 新增 doctor_web/ React前端 (dashboard/患者/问诊/报告/随访)
- 后端新增 doctor_endpoints (14个医生API) + ConsultationHub (SignalR)
- Flutter端 SignalR 替换轮询实现实时聊天
This commit is contained in:
MingNian
2026-06-05 17:46:07 +08:00
parent 20a5ce1199
commit d0d7d8428d
41 changed files with 5892 additions and 18 deletions

View File

@@ -0,0 +1,72 @@
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { api } from '../../services/api-client';
import type { DashboardStats } from '../../types';
const CARD_CONFIG = [
{ key: 'totalPatients', label: '患者总数', color: '#4F6EF7', bg: '#EDF0FD' },
{ key: 'activeConsultations', label: '进行中问诊', color: '#20C997', bg: '#E6F9F2' },
{ key: 'pendingReports', label: '待审核报告', color: '#F59E0B', bg: '#FFF8E6' },
{ key: 'todayFollowUps', label: '今日随访', color: '#845EF7', bg: '#F3E8FF' },
];
const QUICK_LINKS = [
{ to: '/patients', label: '患者列表', color: '#4F6EF7' },
{ to: '/consultations', label: '在线问诊', color: '#20C997' },
{ to: '/reports', label: '报告审核', color: '#F59E0B' },
{ to: '/follow-ups', label: '随访管理', color: '#845EF7' },
];
export default function DashboardPage() {
const [stats, setStats] = useState<DashboardStats | null>(null);
useEffect(() => {
api.get<DashboardStats>('/api/doctor/dashboard').then(setStats).catch(() => {});
}, []);
if (!stats) return <div style={{ padding: 40, color: '#9BA0B4' }}>...</div>;
return (
<div style={{ padding: 32, maxWidth: 1200 }}>
{/* 欢迎 */}
<div style={{ marginBottom: 32 }}>
<h1 style={{ fontSize: 24, fontWeight: 700 }}>{stats.doctorName}</h1>
<p style={{ color: '#9BA0B4', marginTop: 4 }}>{stats.doctorTitle} · {stats.doctorDepartment}</p>
</div>
{/* 统计卡片 */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 20, marginBottom: 32 }}>
{CARD_CONFIG.map(c => (
<div key={c.key} style={{ background: '#FFF', borderRadius: 16, padding: '24px 20px', position: 'relative', overflow: 'hidden', boxShadow: '0 2px 12px rgba(0,0,0,0.04)' }}>
<div style={{ position: 'absolute', top: 0, left: 0, width: 4, height: '100%', background: c.color }} />
<div style={{ fontSize: 30, fontWeight: 800, color: c.color }}>{(stats as any)[c.key]}</div>
<div style={{ fontSize: 13, color: '#5A6072', marginTop: 4 }}>{c.label}</div>
</div>
))}
</div>
{/* 快捷操作 */}
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 20 }}>
<div style={{ background: '#FFF', borderRadius: 16, padding: 24, boxShadow: '0 2px 12px rgba(0,0,0,0.04)' }}>
<h2 style={{ fontSize: 16, fontWeight: 600, marginBottom: 16 }}></h2>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
{QUICK_LINKS.map(l => (
<Link key={l.to} to={l.to} style={{ padding: '14px 16px', borderRadius: 12, border: `1px solid ${l.color}20`, color: l.color, fontWeight: 500, textAlign: 'center', transition: 'all 0.15s', fontSize: 14 }}>
{l.label}
</Link>
))}
</div>
</div>
<div style={{ background: '#FFF', borderRadius: 16, padding: 24, boxShadow: '0 2px 12px rgba(0,0,0,0.04)' }}>
<h2 style={{ fontSize: 16, fontWeight: 600, marginBottom: 16 }}></h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<div style={{ fontSize: 14, color: '#5A6072' }}>📋 <b style={{ color: '#F59E0B' }}>{stats.pendingReports}</b> </div>
<div style={{ fontSize: 14, color: '#5A6072' }}>💬 <b style={{ color: '#20C997' }}>{stats.activeConsultations}</b> </div>
<div style={{ fontSize: 14, color: '#5A6072' }}>📅 访 <b style={{ color: '#845EF7' }}>{stats.todayFollowUps}</b> </div>
</div>
</div>
</div>
</div>
);
}