Commit e26e5d42 authored by yuguo's avatar yuguo

fix

parent 45960530
......@@ -122,7 +122,7 @@ func (s *Service) Register(ctx context.Context, req *RegisterRequest) (*LoginRes
return nil, err
}
// 如果是患者,创建患者扩展信息
// 根据角色创建扩展信息
if role == "patient" {
patientProfile := &model.PatientProfile{
ID: uuid.New().String(),
......@@ -131,6 +131,16 @@ func (s *Service) Register(ctx context.Context, req *RegisterRequest) (*LoginRes
if err := s.db.Create(patientProfile).Error; err != nil {
fmt.Printf("创建患者扩展信息失败: %v\n", err)
}
} else if role == "doctor" {
doctor := &model.Doctor{
ID: uuid.New().String(),
UserID: user.ID,
Name: req.RealName,
Status: "pending", // 注册后需审核
}
if err := s.db.Create(doctor).Error; err != nil {
fmt.Printf("创建医生档案失败: %v\n", err)
}
}
tokenPair, err := utils.GenerateTokenPair(user.ID, user.Role)
......
......@@ -59,12 +59,23 @@ const LoginPage: React.FC = () => {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 20px rgba(43,109,232,0.3); }
50% { box-shadow: 0 0 40px rgba(43,109,232,0.6); }
@keyframes float-reverse {
0%, 100% { transform: translateY(-10px) rotate(0deg); }
50% { transform: translateY(10px) rotate(3deg); }
}
@keyframes wave-move {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
@keyframes particle-float {
0% { transform: translateY(0) scale(1); opacity: 0; }
10% { opacity: 0.8; }
50% { opacity: 1; }
90% { opacity: 0.6; }
100% { transform: translateY(-200px) scale(0.4); opacity: 0; }
}
.login-container {
background: linear-gradient(135deg, #0A1628 0%, #1A3B7A 25%, #2B6DE8 50%, #5B8FF9 75%, #2B6DE8 100%);
background: linear-gradient(135deg, #1a3a6c 0%, #2B6DE8 40%, #5B8FF9 70%, #7eb4ff 100%);
background-size: 200% 200%;
animation: gradient-shift 15s ease infinite;
}
......@@ -77,78 +88,160 @@ const LoginPage: React.FC = () => {
transform: translateY(-4px);
background: rgba(255,255,255,0.15);
}
.wave-container {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 100px;
overflow: hidden;
pointer-events: none;
z-index: 1;
}
.wave {
position: absolute;
bottom: 0;
width: 200%;
height: 100%;
animation: wave-move 10s linear infinite;
}
.wave:nth-child(2) {
animation-duration: 14s;
animation-direction: reverse;
opacity: 0.6;
bottom: 4px;
}
.wave:nth-child(3) {
animation-duration: 18s;
opacity: 0.35;
bottom: 8px;
}
.particle {
position: absolute;
border-radius: 50%;
background: radial-gradient(circle, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.3) 60%, transparent 100%);
box-shadow: 0 0 6px 2px rgba(255,255,255,0.3);
animation: particle-float 6s ease-in-out infinite;
}
@media (max-width: 1024px) {
.left-intro { display: none !important; }
.right-form { width: 100% !important; max-width: 440px !important; }
.right-form { width: 100% !important; max-width: 440px !important; min-width: 0 !important; }
}
`}</style>
<div className="login-container" style={{
position: 'fixed', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
overflow: 'auto', padding: 24,
overflow: 'hidden', padding: 24,
}}>
{/* 动态背景装饰 */}
<div style={{ position: 'absolute', inset: 0, overflow: 'hidden', pointerEvents: 'none' }}>
<div style={{ position: 'absolute', top: '15%', left: '10%', width: 400, height: 400, background: 'radial-gradient(circle, rgba(91,143,249,0.15) 0%, transparent 70%)', animation: 'float 8s ease-in-out infinite' }} />
<div style={{ position: 'absolute', bottom: '20%', right: '15%', width: 300, height: 300, background: 'radial-gradient(circle, rgba(43,109,232,0.2) 0%, transparent 70%)', animation: 'float 10s ease-in-out infinite 2s' }} />
<div style={{ position: 'absolute', top: '50%', left: '50%', width: 500, height: 500, background: 'radial-gradient(circle, rgba(255,255,255,0.03) 0%, transparent 70%)', animation: 'float 12s ease-in-out infinite 4s' }} />
{/* 浮动光晕 - 柔和 */}
<div style={{ position: 'absolute', top: '8%', left: '3%', width: 420, height: 420, background: 'radial-gradient(circle, rgba(255,255,255,0.06) 0%, transparent 70%)', animation: 'float 8s ease-in-out infinite' }} />
<div style={{ position: 'absolute', bottom: '10%', right: '5%', width: 350, height: 350, background: 'radial-gradient(circle, rgba(255,255,255,0.05) 0%, transparent 70%)', animation: 'float 10s ease-in-out infinite 2s' }} />
<div style={{ position: 'absolute', top: '50%', left: '45%', width: 500, height: 500, background: 'radial-gradient(circle, rgba(255,255,255,0.03) 0%, transparent 70%)', animation: 'float-reverse 12s ease-in-out infinite 3s' }} />
{/* 浮动粒子 - 更大更亮 */}
{[
{ left: '5%', bottom: '6%', size: 8, delay: '0s', dur: '6s' },
{ left: '14%', bottom: '3%', size: 6, delay: '1.2s', dur: '7s' },
{ left: '24%', bottom: '10%', size: 10, delay: '0.4s', dur: '5.5s' },
{ left: '35%', bottom: '5%', size: 7, delay: '2.5s', dur: '6.5s' },
{ left: '48%', bottom: '8%', size: 9, delay: '1s', dur: '5.8s' },
{ left: '58%', bottom: '4%', size: 6, delay: '0.7s', dur: '7.2s' },
{ left: '70%', bottom: '12%', size: 10, delay: '2s', dur: '6.2s' },
{ left: '80%', bottom: '7%', size: 8, delay: '0.3s', dur: '5.6s' },
{ left: '90%', bottom: '3%', size: 7, delay: '1.8s', dur: '6.8s' },
{ left: '10%', bottom: '15%', size: 5, delay: '3.2s', dur: '7.5s' },
{ left: '42%', bottom: '2%', size: 8, delay: '2.8s', dur: '6s' },
{ left: '65%', bottom: '14%', size: 6, delay: '0.5s', dur: '7s' },
{ left: '52%', bottom: '16%', size: 5, delay: '3.5s', dur: '8s' },
{ left: '78%', bottom: '18%', size: 7, delay: '1.5s', dur: '6.4s' },
{ left: '20%', bottom: '18%', size: 6, delay: '4s', dur: '7.8s' },
].map((p, i) => (
<div
key={i}
className="particle"
style={{
left: p.left,
bottom: p.bottom,
width: p.size,
height: p.size,
animationDelay: p.delay,
animationDuration: p.dur,
}}
/>
))}
{/* 底部波浪 */}
<div className="wave-container">
<svg className="wave" viewBox="0 0 1440 100" preserveAspectRatio="none" style={{ display: 'block' }}>
<path d="M0,50 C240,85 480,15 720,50 C960,85 1200,15 1440,50 C1680,85 1920,15 2160,50 C2400,85 2640,15 2880,50 L2880,100 L0,100 Z" fill="rgba(255,255,255,0.08)" />
</svg>
<svg className="wave" viewBox="0 0 1440 100" preserveAspectRatio="none" style={{ display: 'block' }}>
<path d="M0,65 C240,35 480,85 720,65 C960,35 1200,85 1440,65 C1680,35 1920,85 2160,65 C2400,35 2640,85 2880,65 L2880,100 L0,100 Z" fill="rgba(255,255,255,0.05)" />
</svg>
<svg className="wave" viewBox="0 0 1440 100" preserveAspectRatio="none" style={{ display: 'block' }}>
<path d="M0,75 C360,50 720,90 1080,75 C1440,50 1800,90 2160,75 C2520,50 2880,90 2880,75 L2880,100 L0,100 Z" fill="rgba(255,255,255,0.03)" />
</svg>
</div>
</div>
<div style={{
position: 'relative', zIndex: 10, display: 'flex', width: '100%', maxWidth: 1400,
minHeight: 680, borderRadius: 24, overflow: 'hidden',
boxShadow: '0 30px 80px rgba(0,0,0,0.4)',
maxHeight: 'calc(100vh - 48px)', borderRadius: 24, overflow: 'hidden',
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
}}>
{/* 左侧系统介绍 - 2/3 */}
<div className="left-intro" style={{
flex: 2, padding: 60, color: '#fff',
flex: 2, padding: '40px 48px', color: '#fff',
background: 'linear-gradient(135deg, rgba(26,59,122,0.95) 0%, rgba(43,109,232,0.92) 100%)',
backdropFilter: 'blur(30px)', display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
overflow: 'auto',
}}>
{/* Logo & 标题 */}
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 48 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 32 }}>
<div style={{
width: 56, height: 56, borderRadius: 16,
width: 48, height: 48, borderRadius: 14,
background: 'rgba(255,255,255,0.2)', backdropFilter: 'blur(10px)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<MedicineBoxOutlined style={{ fontSize: 28, color: '#fff' }} />
<MedicineBoxOutlined style={{ fontSize: 24, color: '#fff' }} />
</div>
<div>
<div style={{ fontSize: 24, fontWeight: 700, letterSpacing: 1 }}>互联网医院</div>
<div style={{ fontSize: 13, color: 'rgba(255,255,255,0.7)', marginTop: 2 }}>Internet Hospital Platform</div>
<div style={{ fontSize: 22, fontWeight: 700, letterSpacing: 1 }}>互联网医院</div>
<div style={{ fontSize: 12, color: 'rgba(255,255,255,0.7)', marginTop: 2 }}>Internet Hospital Platform</div>
</div>
</div>
<h1 style={{ fontSize: 42, fontWeight: 700, lineHeight: 1.3, marginBottom: 20 }}>
<h1 style={{ fontSize: 36, fontWeight: 700, lineHeight: 1.3, marginBottom: 14, margin: 0 }}>
智能医疗<br />触手可及
</h1>
<p style={{ fontSize: 16, color: 'rgba(255,255,255,0.8)', lineHeight: 1.8, marginBottom: 48 }}>
<p style={{ fontSize: 15, color: 'rgba(255,255,255,0.8)', lineHeight: 1.8, marginBottom: 28, marginTop: 10 }}>
连接全国优质医疗资源,AI 赋能精准诊疗<br />
为您提供 7×24 小时在线医疗健康服务
</p>
{/* 数据统计 */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 24, marginBottom: 48 }}>
<div className="glass-card" style={{ padding: 20, borderRadius: 16, transition: 'all 0.3s' }}>
<div style={{ fontSize: 32, fontWeight: 700, marginBottom: 4 }}>5000+</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16, marginBottom: 28 }}>
<div className="glass-card" style={{ padding: '14px 16px', borderRadius: 14, transition: 'all 0.3s' }}>
<div style={{ fontSize: 28, fontWeight: 700, marginBottom: 2 }}>5000+</div>
<div style={{ fontSize: 13, color: 'rgba(255,255,255,0.7)' }}>执业医师</div>
</div>
<div className="glass-card" style={{ padding: 20, borderRadius: 16, transition: 'all 0.3s' }}>
<div style={{ fontSize: 32, fontWeight: 700, marginBottom: 4 }}>200万+</div>
<div className="glass-card" style={{ padding: '14px 16px', borderRadius: 14, transition: 'all 0.3s' }}>
<div style={{ fontSize: 28, fontWeight: 700, marginBottom: 2 }}>200万+</div>
<div style={{ fontSize: 13, color: 'rgba(255,255,255,0.7)' }}>服务患者</div>
</div>
<div className="glass-card" style={{ padding: 20, borderRadius: 16, transition: 'all 0.3s' }}>
<div style={{ fontSize: 32, fontWeight: 700, marginBottom: 4 }}>98%</div>
<div className="glass-card" style={{ padding: '14px 16px', borderRadius: 14, transition: 'all 0.3s' }}>
<div style={{ fontSize: 28, fontWeight: 700, marginBottom: 2 }}>98%</div>
<div style={{ fontSize: 13, color: 'rgba(255,255,255,0.7)' }}>满意度</div>
</div>
</div>
{/* 核心功能 */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<div className="feature-card glass-card" style={{
display: 'flex', alignItems: 'center', gap: 16, padding: 20, borderRadius: 16,
display: 'flex', alignItems: 'center', gap: 14, padding: '14px 16px', borderRadius: 14,
transition: 'all 0.3s', cursor: 'pointer',
}}>
<div style={{
......@@ -166,7 +259,7 @@ const LoginPage: React.FC = () => {
</div>
<div className="feature-card glass-card" style={{
display: 'flex', alignItems: 'center', gap: 16, padding: 20, borderRadius: 16,
display: 'flex', alignItems: 'center', gap: 14, padding: '14px 16px', borderRadius: 14,
transition: 'all 0.3s', cursor: 'pointer',
}}>
<div style={{
......@@ -184,7 +277,7 @@ const LoginPage: React.FC = () => {
</div>
<div className="feature-card glass-card" style={{
display: 'flex', alignItems: 'center', gap: 16, padding: 20, borderRadius: 16,
display: 'flex', alignItems: 'center', gap: 14, padding: '14px 16px', borderRadius: 14,
transition: 'all 0.3s', cursor: 'pointer',
}}>
<div style={{
......@@ -213,10 +306,10 @@ const LoginPage: React.FC = () => {
{/* 右侧登录表单 - 1/3 */}
<div className="right-form" style={{
flex: 1, backgroundColor: '#fff', display: 'flex', flexDirection: 'column',
justifyContent: 'center', padding: 60, minWidth: 420,
justifyContent: 'center', padding: '40px 48px', minWidth: 400,
}}>
<div style={{ marginBottom: 40 }}>
<h2 style={{ fontSize: 28, fontWeight: 700, marginBottom: 8, color: '#1d2129' }}>欢迎登录</h2>
<div style={{ marginBottom: 32 }}>
<h2 style={{ fontSize: 26, fontWeight: 700, marginBottom: 6, color: '#1d2129' }}>欢迎登录</h2>
<p style={{ fontSize: 14, color: '#8c8c8c', margin: 0 }}>登录以使用完整的医疗服务功能</p>
</div>
......@@ -256,23 +349,35 @@ const LoginPage: React.FC = () => {
</Form.Item>
</Form>
<div style={{ textAlign: 'center', marginTop: 28, fontSize: 14 }}>
<div style={{ textAlign: 'center', marginTop: 20, fontSize: 14 }}>
<span style={{ color: '#8c8c8c' }}>还没有账号?</span>
<Link href="/register" style={{ marginLeft: 6, fontWeight: 600, color: '#2B6DE8' }}>
立即注册
</Link>
</div>
<div style={{ marginTop: 40, paddingTop: 24, textAlign: 'center', borderTop: '1px solid #f0f0f0' }}>
<div style={{
marginTop: 28, paddingTop: 16, textAlign: 'center',
borderTop: '1px solid #f0f0f0',
}}>
<p style={{ fontSize: 12, color: '#bfbfbf', margin: 0, lineHeight: 1.6 }}>
登录即表示同意
<a onClick={() => message.info('协议内容完善中')} style={{ color: '#2B6DE8', margin: '0 4px' }}>《用户服务协议》</a>
<a onClick={() => message.info('协议内容完善中')} style={{ color: '#2B6DE8', margin: '0 4px', cursor: 'pointer' }}>《用户服务协议》</a>
<a onClick={() => message.info('协议内容完善中')} style={{ color: '#2B6DE8', margin: '0 4px' }}>《隐私政策》</a>
<a onClick={() => message.info('协议内容完善中')} style={{ color: '#2B6DE8', margin: '0 4px', cursor: 'pointer' }}>《隐私政策》</a>
</p>
</div>
</div>
</div>
{/* 底部版权信息 */}
<div style={{
position: 'absolute', bottom: 16, left: 0, right: 0, zIndex: 20,
textAlign: 'center', color: 'rgba(255,255,255,0.4)', fontSize: 12,
letterSpacing: 0.5,
}}>
<span>{'\u00A9'} 2026 互联网医院 · 技术驱动健康未来</span>
</div>
</div>
<Modal
......
......@@ -362,6 +362,7 @@ const AIPanel: React.FC<AIPanelProps> = ({
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
}}>
{/* 标题 */}
<div style={{
......@@ -371,9 +372,17 @@ const AIPanel: React.FC<AIPanelProps> = ({
alignItems: 'center',
gap: 8,
flexShrink: 0,
background: 'linear-gradient(135deg, #f6ffed 0%, #e6fffb 100%)',
}}>
<RobotOutlined style={{ color: '#52c41a', fontSize: 16 }} />
<span style={{ fontWeight: 600, fontSize: 14 }}>AI 辅助</span>
<div style={{
width: 28, height: 28, borderRadius: 8,
background: 'linear-gradient(135deg, #52c41a 0%, #73d13d 100%)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: '0 2px 6px rgba(82, 196, 26, 0.3)',
}}>
<RobotOutlined style={{ color: '#fff', fontSize: 14 }} />
</div>
<span style={{ fontWeight: 600, fontSize: 14, color: '#1d2129' }}>AI 辅助</span>
{hasActiveConsult && (
<Badge status="processing" style={{ marginLeft: 2 }} />
)}
......@@ -470,9 +479,19 @@ const AIPanel: React.FC<AIPanelProps> = ({
]}
/>
) : (
<div style={{ textAlign: 'center', paddingTop: 60 }}>
<RobotOutlined style={{ fontSize: 40, color: '#d9d9d9', display: 'block', marginBottom: 14 }} />
<Text type="secondary" style={{ fontSize: 13 }}>接诊后自动开始AI辅助</Text>
<div style={{ textAlign: 'center', paddingTop: 60, padding: '60px 24px' }}>
<div style={{
width: 64, height: 64, borderRadius: 16, margin: '0 auto 16px',
background: 'linear-gradient(135deg, #f6ffed 0%, #e6fffb 100%)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<RobotOutlined style={{ fontSize: 28, color: '#b7eb8f' }} />
</div>
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 6, color: '#8c8c8c' }}>AI 辅助诊疗</Text>
<Text type="secondary" style={{ fontSize: 12, lineHeight: 1.6 }}>
接诊后将自动启动 AI 辅助分析,<br />
为您提供鉴别诊断、用药建议等支持
</Text>
</div>
)}
</div>
......
......@@ -382,6 +382,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
border: '1px solid #E5E8EF',
background: '#fff',
overflow: 'hidden',
boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
}}>
{/* 顶部患者信息栏 */}
<div style={{
......@@ -391,7 +392,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
alignItems: 'center',
gap: 10,
flexShrink: 0,
background: '#fff',
background: 'linear-gradient(135deg, #ffffff 0%, #f9fafb 100%)',
}}>
{activeConsult ? (
<>
......@@ -429,8 +430,9 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
{activeConsult.started_at && activeConsult.status === 'in_progress' && (
<div style={{
display: 'flex', alignItems: 'center', gap: 4,
background: '#f6ffed', border: '1px solid #b7eb8f', borderRadius: 6,
padding: '2px 10px', fontSize: 13, fontWeight: 500, color: '#52c41a',
background: 'linear-gradient(135deg, #f6ffed 0%, #e8ffd6 100%)',
border: '1px solid #b7eb8f', borderRadius: 8,
padding: '3px 12px', fontSize: 13, fontWeight: 600, color: '#389e0d',
fontVariantNumeric: 'tabular-nums',
}}>
<ClockCircleOutlined style={{ fontSize: 12 }} />
......@@ -488,7 +490,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
</div>
{/* 消息区域 */}
<div style={{ flex: 1, overflow: 'auto', padding: 16, background: '#f5f7fb' }}>
<div style={{ flex: 1, overflow: 'auto', padding: 16, background: 'linear-gradient(180deg, #f5f7fb 0%, #f0f2f5 100%)' }}>
{activeConsult ? (
<>
{messages.length === 0 && (
......@@ -617,13 +619,16 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
onClick={handleSend}
loading={sending}
style={{
background: '#52c41a',
background: 'linear-gradient(135deg, #52c41a 0%, #73d13d 100%)',
borderColor: '#52c41a',
borderRadius: 8,
borderRadius: 10,
height: 'auto',
alignSelf: 'flex-end',
paddingBottom: 8,
paddingTop: 8,
paddingBottom: 10,
paddingTop: 10,
paddingLeft: 16,
paddingRight: 16,
boxShadow: '0 2px 6px rgba(82, 196, 26, 0.3)',
}}
>
发送
......
......@@ -4,7 +4,7 @@ import {
UserOutlined, MessageOutlined, VideoCameraOutlined,
CheckOutlined, CloseOutlined, ClockCircleOutlined,
AlertOutlined, ThunderboltOutlined, SearchOutlined,
MedicineBoxOutlined, ProfileOutlined,
ProfileOutlined,
} from '@ant-design/icons';
import type { PatientListItem } from '@/api/consult';
......@@ -47,108 +47,150 @@ const PatientCard: React.FC<{
const isInProgress = patient.status === 'in_progress';
const isCompleted = patient.status === 'completed';
const avatarColor = isInProgress ? '#52c41a' : isWaiting ? '#fa8c16' : '#d9d9d9';
const avatarBg = isInProgress ? '#f6ffed' : isWaiting ? '#fff7e6' : '#fafafa';
const riskColor = riskColorMap[patient.risk_level] || '#52c41a';
const statusConfig = isInProgress
? { avatarBg: '#F6FFED', avatarBorder: '#52c41a', avatarIcon: '#52c41a' }
: isWaiting
? { avatarBg: '#FFF7E6', avatarBorder: '#fa8c16', avatarIcon: '#fa8c16' }
: { avatarBg: '#f5f5f5', avatarBorder: '#d9d9d9', avatarIcon: '#bfbfbf' };
const riskColor = riskColorMap[patient.risk_level] || '#e8e8e8';
const cardBg = isSelected
? 'linear-gradient(135deg, #F0FFF5 0%, #E6FFE6 100%)'
: isWaiting
? '#FFFCF5'
: '#fff';
return (
<div
onClick={() => !isWaiting && onSelect()}
style={{
padding: '10px 12px',
background: isSelected ? '#F0FFF5' : '#fff',
borderLeft: `3px solid ${isSelected ? '#2B9E6E' : riskColor}`,
borderBottom: '1px solid #f5f5f5',
margin: '4px 6px',
borderRadius: 10,
background: cardBg,
border: isSelected ? '1.5px solid #52c41a' : '1px solid #f0f0f0',
boxShadow: isSelected
? '0 2px 8px rgba(82, 196, 26, 0.15)'
: '0 1px 3px rgba(0,0,0,0.04)',
cursor: isWaiting ? 'default' : 'pointer',
transition: 'background 0.15s',
transition: 'all 0.2s ease',
position: 'relative',
}}
onMouseEnter={e => {
if (!isSelected && !isWaiting) (e.currentTarget as HTMLDivElement).style.background = '#fafafa';
if (!isSelected) {
(e.currentTarget as HTMLDivElement).style.boxShadow = '0 3px 10px rgba(0,0,0,0.08)';
(e.currentTarget as HTMLDivElement).style.transform = 'translateY(-1px)';
}
}}
onMouseLeave={e => {
if (!isSelected) (e.currentTarget as HTMLDivElement).style.background = '#fff';
if (!isSelected) {
(e.currentTarget as HTMLDivElement).style.boxShadow = '0 1px 3px rgba(0,0,0,0.04)';
(e.currentTarget as HTMLDivElement).style.transform = 'translateY(0)';
}
}}
>
<div style={{ display: 'flex', gap: 9, alignItems: 'flex-start' }}>
{/* 左侧风险条 */}
<div style={{
position: 'absolute', left: 0, top: 8, bottom: 8,
width: 3, borderRadius: 3, background: riskColor,
}} />
<div style={{ display: 'flex', gap: 10, alignItems: 'flex-start', paddingLeft: 4 }}>
{/* Avatar */}
<div style={{
width: 34, height: 34, borderRadius: '50%', flexShrink: 0,
background: avatarBg,
border: `2px solid ${avatarColor}`,
width: 36, height: 36, borderRadius: 10, flexShrink: 0,
background: statusConfig.avatarBg,
border: `1.5px solid ${statusConfig.avatarBorder}`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<UserOutlined style={{ fontSize: 15, color: avatarColor }} />
<UserOutlined style={{ fontSize: 16, color: statusConfig.avatarIcon }} />
</div>
{/* Info */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 3 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 2 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
<Text
strong
style={{ fontSize: 13, cursor: 'pointer' }}
style={{ fontSize: 13, cursor: 'pointer', lineHeight: 1.3 }}
onClick={e => { e.stopPropagation(); onOpenProfile?.(); }}
>
{patient.patient_name || '患者'}
</Text>
{patient.is_revisit && (
<Tooltip title={`复诊 · 第${patient.visit_count + 1}次就诊`}>
<ThunderboltOutlined style={{ fontSize: 11, color: '#2B9E6E' }} />
<Tag color="cyan" style={{ fontSize: 10, lineHeight: '16px', margin: 0, padding: '0 4px', borderRadius: 4 }}>
<ThunderboltOutlined style={{ marginRight: 1 }} />复诊
</Tag>
</Tooltip>
)}
</div>
{patient.type === 'video'
? <VideoCameraOutlined style={{ fontSize: 12, color: '#0891B2' }} />
: <MessageOutlined style={{ fontSize: 12, color: '#52c41a' }} />
}
</div>
<div style={{ fontSize: 10, color: '#2B9E6E', fontFamily: 'monospace', marginBottom: 2 }}>
{patient.serial_number}
<div style={{
width: 20, height: 20, borderRadius: 6,
background: patient.type === 'video' ? '#E6F7FF' : '#F6FFED',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
{patient.type === 'video'
? <VideoCameraOutlined style={{ fontSize: 11, color: '#0891B2' }} />
: <MessageOutlined style={{ fontSize: 11, color: '#52c41a' }} />
}
</div>
</div>
<div style={{ fontSize: 11, color: '#8c8c8c', marginBottom: 3 }}>
{patient.patient_gender && `${patient.patient_gender} · `}
{patient.patient_age ? `${patient.patient_age}岁` : ''}
{patient.visit_count > 0 && ` · 第${patient.visit_count + 1}次`}
<div style={{ fontSize: 11, color: '#999', marginBottom: 3, display: 'flex', alignItems: 'center', gap: 4 }}>
<span>
{patient.patient_gender && `${patient.patient_gender}`}
{patient.patient_age ? ` · ${patient.patient_age}岁` : ''}
</span>
{patient.visit_count > 0 && (
<span style={{ color: '#bbb' }}>· 第{patient.visit_count + 1}</span>
)}
</div>
<div style={{
fontSize: 11, color: '#666',
fontSize: 12, color: '#555', lineHeight: 1.5,
overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis',
}}>
{patient.chief_complaint || '暂无主诉'}
</div>
{/* 标签区域:过敏 + 慢病 */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 3, marginTop: 4 }}>
{/* 标签区域 */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 3, marginTop: 5 }}>
{patient.allergy_history && (
<Tag color="red" style={{ fontSize: 10, lineHeight: '16px', margin: 0, padding: '0 4px' }}>
<Tag color="error" style={{ fontSize: 10, lineHeight: '16px', margin: 0, padding: '0 5px', borderRadius: 4 }}>
<AlertOutlined style={{ marginRight: 2 }} />过敏
</Tag>
)}
{patient.chronic_diseases?.slice(0, 2).map(d => (
<Tag key={d} color="orange" style={{ fontSize: 10, lineHeight: '16px', margin: 0, padding: '0 4px' }}>
<Tag key={d} color="warning" style={{ fontSize: 10, lineHeight: '16px', margin: 0, padding: '0 5px', borderRadius: 4 }}>
{d.length > 4 ? d.slice(0, 4) + '..' : d}
</Tag>
))}
{patient.chronic_diseases?.length > 2 && (
<Tag style={{ fontSize: 10, lineHeight: '16px', margin: 0, padding: '0 4px' }}>
<Tag style={{ fontSize: 10, lineHeight: '16px', margin: 0, padding: '0 5px', borderRadius: 4, color: '#999' }}>
+{patient.chronic_diseases.length - 2}
</Tag>
)}
{patient.pre_consult_summary && (
<Tag color="blue" style={{ fontSize: 10, lineHeight: '16px', margin: 0, padding: '0 4px' }}>
<Tag color="processing" style={{ fontSize: 10, lineHeight: '16px', margin: 0, padding: '0 5px', borderRadius: 4 }}>
<ProfileOutlined style={{ marginRight: 2 }} />预问诊
</Tag>
)}
</div>
{isWaiting && patient.waiting_seconds > 0 && (
<div style={{ fontSize: 11, color: '#fa8c16', marginTop: 4, display: 'flex', alignItems: 'center', gap: 3 }}>
<ClockCircleOutlined />
<div style={{
fontSize: 11, color: '#fa8c16', marginTop: 5,
display: 'flex', alignItems: 'center', gap: 4,
background: '#FFF7E6', padding: '2px 8px', borderRadius: 4, width: 'fit-content',
}}>
<ClockCircleOutlined style={{ fontSize: 10 }} />
等待 {formatWaitTime(patient.waiting_seconds)}
</div>
)}
{isCompleted && (
<div style={{ fontSize: 11, color: '#8c8c8c', marginTop: 4 }}>
<div style={{ fontSize: 11, color: '#bbb', marginTop: 5 }}>
已完成{patient.last_visit_date ? ` · ${patient.last_visit_date}` : ''}
</div>
)}
......@@ -157,21 +199,28 @@ const PatientCard: React.FC<{
{/* 接诊/拒诊按钮 */}
{isWaiting && (
<div style={{ display: 'flex', gap: 6, marginTop: 8 }}>
<div style={{ display: 'flex', gap: 6, marginTop: 8, paddingLeft: 4 }}>
<Button
type="primary"
size="small"
icon={<CheckOutlined />}
style={{ flex: 1, fontSize: 11, height: 26 }}
style={{
flex: 1, fontSize: 12, height: 28, borderRadius: 6,
background: 'linear-gradient(135deg, #52c41a 0%, #73d13d 100%)',
borderColor: '#52c41a',
}}
onClick={e => { e.stopPropagation(); onAccept(); }}
>
接诊
</Button>
<Button
danger
size="small"
icon={<CloseOutlined />}
style={{ flex: 1, fontSize: 11, height: 26 }}
style={{
flex: 1, fontSize: 12, height: 28, borderRadius: 6,
color: '#ff4d4f', borderColor: '#ffccc7',
background: '#fff2f0',
}}
onClick={e => { e.stopPropagation(); onReject(); }}
>
拒诊
......@@ -189,7 +238,6 @@ const PatientList: React.FC<PatientListProps> = ({
const [sortMode, setSortMode] = useState<SortMode>('default');
const [activeTab, setActiveTab] = useState('waiting');
// 筛选
const filtered = useMemo(() => {
if (!patients) return [];
if (!searchText.trim()) return patients;
......@@ -200,7 +248,6 @@ const PatientList: React.FC<PatientListProps> = ({
);
}, [patients, searchText]);
// 排序
const sorted = useMemo(() => {
if (sortMode === 'default') return filtered;
return [...filtered].sort((a, b) => {
......@@ -228,25 +275,37 @@ const PatientList: React.FC<PatientListProps> = ({
const renderCardList = (list: PatientListItem[]) => {
if (list.length === 0) {
return (
<div style={{ textAlign: 'center', padding: '40px 0', color: '#bbb' }}>
<UserOutlined style={{ fontSize: 28, display: 'block', marginBottom: 8 }} />
<div style={{ fontSize: 12 }}>
<div style={{ textAlign: 'center', padding: '48px 16px', color: '#ccc' }}>
<div style={{
width: 52, height: 52, borderRadius: 14, margin: '0 auto 12px',
background: '#f8f9fa', display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<UserOutlined style={{ fontSize: 24, color: '#d9d9d9' }} />
</div>
<div style={{ fontSize: 13, color: '#bbb' }}>
{searchText ? '没有匹配的患者' : '暂无患者'}
</div>
<div style={{ fontSize: 11, color: '#ddd', marginTop: 4 }}>
{searchText ? '试试其他关键词' : '新的候诊患者将显示在这里'}
</div>
</div>
);
}
return list.map(p => (
<PatientCard
key={p.consult_id}
patient={p}
isSelected={p.consult_id === selectedConsultId}
onSelect={() => onSelectPatient(p)}
onAccept={() => onAccept(p)}
onReject={() => onReject(p)}
onOpenProfile={() => onOpenProfile?.(p.patient_id)}
/>
));
return (
<div style={{ padding: '2px 0' }}>
{list.map(p => (
<PatientCard
key={p.consult_id}
patient={p}
isSelected={p.consult_id === selectedConsultId}
onSelect={() => onSelectPatient(p)}
onAccept={() => onAccept(p)}
onReject={() => onReject(p)}
onOpenProfile={() => onOpenProfile?.(p.patient_id)}
/>
))}
</div>
);
};
return (
......@@ -258,9 +317,10 @@ const PatientList: React.FC<PatientListProps> = ({
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
}}>
{/* Tabs */}
<div style={{ flexShrink: 0 }}>
<div style={{ flexShrink: 0, borderBottom: '1px solid #f0f0f0' }}>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
......@@ -271,7 +331,7 @@ const PatientList: React.FC<PatientListProps> = ({
{
key: 'waiting',
label: (
<span style={{ fontSize: 12 }}>
<span style={{ fontSize: 13, fontWeight: activeTab === 'waiting' ? 600 : 400 }}>
候诊 <Badge count={waiting.length} size="small" style={{ backgroundColor: '#fa8c16' }} />
</span>
),
......@@ -279,7 +339,7 @@ const PatientList: React.FC<PatientListProps> = ({
{
key: 'inProgress',
label: (
<span style={{ fontSize: 12 }}>
<span style={{ fontSize: 13, fontWeight: activeTab === 'inProgress' ? 600 : 400 }}>
接诊 <Badge count={inProgress.length} size="small" style={{ backgroundColor: '#52c41a' }} />
</span>
),
......@@ -287,8 +347,8 @@ const PatientList: React.FC<PatientListProps> = ({
{
key: 'completed',
label: (
<span style={{ fontSize: 12 }}>
已完成 <Badge count={completed.length} size="small" style={{ backgroundColor: '#8c8c8c' }} />
<span style={{ fontSize: 13, fontWeight: activeTab === 'completed' ? 600 : 400 }}>
完成 <Badge count={completed.length} size="small" style={{ backgroundColor: '#bbb' }} />
</span>
),
},
......@@ -297,21 +357,21 @@ const PatientList: React.FC<PatientListProps> = ({
</div>
{/* 搜索 + 排序栏 */}
<div style={{ padding: '6px 10px', borderBottom: '1px solid #f5f5f5', flexShrink: 0 }}>
<div style={{ padding: '8px 10px', borderBottom: '1px solid #f5f5f5', flexShrink: 0 }}>
<Input
size="small"
placeholder="搜索姓名/主诉..."
prefix={<SearchOutlined style={{ color: '#bbb' }} />}
placeholder="搜索姓名 / 主诉..."
prefix={<SearchOutlined style={{ color: '#bfbfbf' }} />}
value={searchText}
onChange={e => setSearchText(e.target.value)}
allowClear
style={{ marginBottom: 4, borderRadius: 6 }}
style={{ marginBottom: 6, borderRadius: 8 }}
/>
<Select
size="small"
value={sortMode}
onChange={setSortMode}
style={{ width: '100%', fontSize: 11 }}
style={{ width: '100%' }}
options={[
{ value: 'default', label: '默认排序' },
{ value: 'wait_time', label: '等待时长优先' },
......@@ -322,7 +382,7 @@ const PatientList: React.FC<PatientListProps> = ({
</div>
{/* 列表区域 */}
<div style={{ flex: 1, overflow: 'auto' }}>
<div style={{ flex: 1, overflow: 'auto', background: '#fafbfc' }}>
{renderCardList(currentList)}
</div>
</div>
......
......@@ -287,74 +287,92 @@ const ConsultPage: React.FC = () => {
{ label: 'AI使用率', value: workbenchStats.ai_usage_rate.toFixed(0), unit: '%', icon: <RobotOutlined />, color: '#0891B2' },
] : [];
const statBgMap: Record<string, string> = {
'候诊中': 'linear-gradient(135deg, #FFF7E6 0%, #FFE7BA 100%)',
'接诊中': 'linear-gradient(135deg, #F6FFED 0%, #D9F7BE 100%)',
'今日完成': 'linear-gradient(135deg, #E6FFFB 0%, #B5F5EC 100%)',
'今日收入': 'linear-gradient(135deg, #E6F7FF 0%, #BAE7FF 100%)',
};
return (
<div style={{ height: 'calc(100vh - 72px)', display: 'flex', flexDirection: 'column', gap: 16, padding: '12px 16px' }}>
{/* 顶部标题 + 统计卡片 */}
<div style={{ height: 'calc(100vh - 72px)', display: 'flex', flexDirection: 'column', gap: 12, padding: '12px 16px' }}>
{/* 顶部统计栏 */}
<div style={{ flexShrink: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<div style={{ marginLeft: 'auto', display: 'flex', gap: 10, alignItems: 'center' }}>
{statItems.map(({ label, value, color, icon, unit }) => (
<div
key={label}
style={{
background: '#fff',
borderRadius: 10,
padding: '8px 18px',
border: '1px solid #E5E8EF',
minWidth: 100,
boxShadow: '0 1px 4px rgba(0,0,0,0.04)',
}}
>
<div style={{ fontSize: 11, color, display: 'flex', alignItems: 'center', gap: 4, marginBottom: 2 }}>
{icon}
{label}
</div>
<div style={{ fontSize: 22, fontWeight: 700, color, lineHeight: 1.2 }}>
{value}
<span style={{ fontSize: 11, fontWeight: 400, marginLeft: 2 }}>{unit}</span>
</div>
</div>
))}
<div style={{ display: 'flex', alignItems: 'stretch', gap: 10 }}>
{statItems.map(({ label, value, color, icon, unit }) => (
<div
onClick={() => setShowEfficiency(!showEfficiency)}
key={label}
style={{
cursor: 'pointer', padding: '6px 10px', borderRadius: 8,
background: showEfficiency ? '#F0FFF5' : '#f5f5f5',
border: `1px solid ${showEfficiency ? '#91d5ff' : '#e8e8e8'}`,
display: 'flex', alignItems: 'center', gap: 4,
fontSize: 12, color: showEfficiency ? '#2B9E6E' : '#8c8c8c',
transition: 'all 0.2s',
flex: 1,
background: statBgMap[label] || '#fff',
borderRadius: 12,
padding: '12px 16px',
border: '1px solid rgba(0,0,0,0.04)',
boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
transition: 'transform 0.2s, box-shadow 0.2s',
cursor: 'default',
}}
onMouseEnter={e => {
(e.currentTarget as HTMLDivElement).style.transform = 'translateY(-1px)';
(e.currentTarget as HTMLDivElement).style.boxShadow = '0 4px 12px rgba(0,0,0,0.08)';
}}
onMouseLeave={e => {
(e.currentTarget as HTMLDivElement).style.transform = 'translateY(0)';
(e.currentTarget as HTMLDivElement).style.boxShadow = '0 1px 3px rgba(0,0,0,0.04)';
}}
>
<DashboardOutlined />
效率
{showEfficiency ? <UpOutlined style={{ fontSize: 10 }} /> : <DownOutlined style={{ fontSize: 10 }} />}
<div style={{ fontSize: 12, color: '#666', display: 'flex', alignItems: 'center', gap: 5, marginBottom: 6 }}>
{React.cloneElement(icon as React.ReactElement, { style: { color, fontSize: 14 } })}
{label}
</div>
<div style={{ fontSize: 26, fontWeight: 700, color, lineHeight: 1, fontVariantNumeric: 'tabular-nums' }}>
{value}
<span style={{ fontSize: 12, fontWeight: 400, marginLeft: 3, color: '#999' }}>{unit}</span>
</div>
</div>
))}
<div
onClick={() => setShowEfficiency(!showEfficiency)}
style={{
cursor: 'pointer', padding: '12px 14px', borderRadius: 12,
background: showEfficiency ? 'linear-gradient(135deg, #F0FFF5 0%, #D9F7E0 100%)' : '#f8f9fa',
border: `1px solid ${showEfficiency ? '#b7eb8f' : '#e8e8e8'}`,
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 4,
fontSize: 12, color: showEfficiency ? '#2B9E6E' : '#8c8c8c',
transition: 'all 0.2s',
minWidth: 56,
}}
>
<DashboardOutlined style={{ fontSize: 16 }} />
<span style={{ fontSize: 11 }}>效率</span>
{showEfficiency ? <UpOutlined style={{ fontSize: 9 }} /> : <DownOutlined style={{ fontSize: 9 }} />}
</div>
</div>
{/* 可折叠效率指标行 */}
{showEfficiency && efficiencyItems.length > 0 && (
<div style={{
display: 'flex', gap: 8, marginTop: 8, padding: '8px 12px',
background: '#fafafa', borderRadius: 10, border: '1px solid #f0f0f0',
overflow: 'auto',
display: 'flex', gap: 8, marginTop: 8, padding: '10px 16px',
background: 'linear-gradient(135deg, #fafbfc 0%, #f0f2f5 100%)',
borderRadius: 12, border: '1px solid #eee',
}}>
{efficiencyItems.map(({ label, value, unit, icon, color }) => (
<div
key={label}
style={{
flex: 1, minWidth: 90, textAlign: 'center',
padding: '4px 8px',
flex: 1, minWidth: 80, textAlign: 'center',
padding: '6px 8px',
borderRadius: 8,
background: 'rgba(255,255,255,0.7)',
}}
>
<div style={{ fontSize: 11, color: '#8c8c8c', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 3, marginBottom: 2 }}>
{React.cloneElement(icon as React.ReactElement, { style: { color, fontSize: 11 } })}
<div style={{ fontSize: 11, color: '#8c8c8c', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 3, marginBottom: 4 }}>
{React.cloneElement(icon as React.ReactElement, { style: { color, fontSize: 12 } })}
{label}
</div>
<div style={{ fontSize: 18, fontWeight: 600, color, lineHeight: 1.3 }}>
<div style={{ fontSize: 20, fontWeight: 600, color, lineHeight: 1.2, fontVariantNumeric: 'tabular-nums' }}>
{value}
<span style={{ fontSize: 10, fontWeight: 400, marginLeft: 1 }}>{unit}</span>
<span style={{ fontSize: 10, fontWeight: 400, marginLeft: 2, color: '#999' }}>{unit}</span>
</div>
</div>
))}
......@@ -363,9 +381,9 @@ const ConsultPage: React.FC = () => {
</div>
{/* 主体三栏 */}
<div style={{ flex: 1, display: 'flex', gap: 12, overflow: 'hidden', minHeight: 0 }}>
<div style={{ flex: 1, display: 'flex', gap: 10, overflow: 'hidden', minHeight: 0 }}>
{/* 左:患者列表 */}
<div style={{ width: 272, flexShrink: 0 }}>
<div style={{ width: 280, flexShrink: 0 }}>
<PatientList
patients={patientList}
onSelectPatient={handleSelectPatient}
......
'use client';
import React, { useState, useEffect } from 'react';
import { Card, Calendar, Button, Modal, Form, TimePicker, InputNumber, Tag, Typography, Space, Row, Col, Badge, List, message, Spin } from 'antd';
import { Card, Calendar, Button, Modal, Form, TimePicker, InputNumber, Tag, Typography, Space, Row, Col, Badge, List, Spin, App } from 'antd';
import { DrawerForm } from '@ant-design/pro-components';
import { PlusOutlined, DeleteOutlined, CalendarOutlined } from '@ant-design/icons';
import dayjs, { Dayjs } from 'dayjs';
......@@ -10,6 +10,7 @@ import { doctorPortalApi, type ScheduleSlot } from '@/api/doctorPortal';
const { Text } = Typography;
const DoctorSchedulePage: React.FC = () => {
const { message, modal } = App.useApp();
const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());
const [isModalOpen, setIsModalOpen] = useState(false);
const [loading, setLoading] = useState(false);
......@@ -59,7 +60,7 @@ const DoctorSchedulePage: React.FC = () => {
setIsModalOpen(true);
};
const handleSaveSchedule = async (values: any) => {
const handleSaveSchedule = async (values: any): Promise<boolean> => {
try {
await doctorPortalApi.createSchedule([{
date: selectedDate.format('YYYY-MM-DD'),
......@@ -68,15 +69,16 @@ const DoctorSchedulePage: React.FC = () => {
max_count: values.max_count,
}]);
message.success('排班添加成功');
setIsModalOpen(false);
fetchSchedule();
return true;
} catch {
message.error('添加排班失败');
return false;
}
};
const handleDeleteSlot = (slotId: string) => {
Modal.confirm({
modal.confirm({
title: '确认删除',
content: '确定要删除该排班吗?',
onOk: async () => {
......@@ -159,10 +161,7 @@ const DoctorSchedulePage: React.FC = () => {
title={`添加排班 - ${selectedDate.format('YYYY-MM-DD')}`}
open={isModalOpen}
onOpenChange={setIsModalOpen}
onFinish={async (values: any) => {
await handleSaveSchedule(values);
return true;
}}
onFinish={handleSaveSchedule}
drawerProps={{ placement: 'right', destroyOnClose: true }}
width={480}
>
......
......@@ -2,7 +2,7 @@
import React, { useState } from 'react';
import {
Card, Tabs, Button, Tag, Space, Form, Input, Select, App,
Card, Button, Tag, Space, Form, Input, Select, App,
DatePicker, Switch, Popconfirm, Typography, Row, Col,
TimePicker, Statistic, Empty,
} from 'antd';
......@@ -566,17 +566,68 @@ const MetricsTab: React.FC = () => {
// ========== 主页面 ==========
const PatientChronicPage: React.FC = () => {
const tabItems = [
{ key: 'records', label: <><HeartOutlined /> 慢病档案</>, children: <ChronicRecordsTab /> },
{ key: 'renewals', label: <><FileAddOutlined /> 续方申请</>, children: <RenewalsTab /> },
{ key: 'reminders', label: <><BellOutlined /> 用药提醒</>, children: <RemindersTab /> },
{ key: 'metrics', label: <><LineChartOutlined /> 健康指标</>, children: <MetricsTab /> },
const [activeKey, setActiveKey] = useState('records');
const menuItems = [
{ key: 'records', icon: <HeartOutlined />, label: '慢病档案' },
{ key: 'renewals', icon: <FileAddOutlined />, label: '续方申请' },
{ key: 'reminders', icon: <BellOutlined />, label: '用药提醒' },
{ key: 'metrics', icon: <LineChartOutlined />, label: '健康指标' },
];
const contentMap: Record<string, React.ReactNode> = {
records: <ChronicRecordsTab />,
renewals: <RenewalsTab />,
reminders: <RemindersTab />,
metrics: <MetricsTab />,
};
const currentMenu = menuItems.find(m => m.key === activeKey);
return (
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Tabs items={tabItems} />
<div style={{ padding: '12px 16px', display: 'flex', gap: 16, minHeight: 'calc(100vh - 120px)' }}>
{/* 左侧导航 */}
<Card
style={{ width: 200, flexShrink: 0, borderRadius: 12, border: '1px solid #E5E8EF' }}
styles={{ body: { padding: '12px 8px' } }}
>
<div style={{ padding: '4px 12px 16px', marginBottom: 8, borderBottom: '1px solid #f0f0f0' }}>
<Space>
<MedicineBoxOutlined style={{ color: '#2B6DE8', fontSize: 16 }} />
<Text strong style={{ fontSize: 15 }}>慢病管理</Text>
</Space>
</div>
{menuItems.map(item => (
<div
key={item.key}
onClick={() => setActiveKey(item.key)}
style={{
display: 'flex', alignItems: 'center', gap: 10,
padding: '10px 16px', borderRadius: 8, cursor: 'pointer',
marginBottom: 4, transition: 'all 0.2s',
background: activeKey === item.key ? '#EBF1FF' : 'transparent',
color: activeKey === item.key ? '#2B6DE8' : '#595959',
fontWeight: activeKey === item.key ? 600 : 400,
}}
>
<span style={{ fontSize: 16 }}>{item.icon}</span>
<span style={{ fontSize: 14 }}>{item.label}</span>
</div>
))}
</Card>
{/* 右侧内容 */}
<Card
title={currentMenu && (
<Space>
<span style={{ color: '#2B6DE8' }}>{currentMenu.icon}</span>
<span>{currentMenu.label}</span>
</Space>
)}
style={{ flex: 1, borderRadius: 12, border: '1px solid #E5E8EF' }}
styles={{ body: { padding: '16px 20px' } }}
>
{contentMap[activeKey]}
</Card>
</div>
);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment