Commit ef9bb5d9 authored by yuguo's avatar yuguo

fix

parent ad3de4e4
......@@ -21,7 +21,9 @@
"Bash(lsof:*)",
"Bash(ipconfig:*)",
"Bash(npm:*)",
"Bash(./api.exe:*)"
"Bash(./api.exe:*)",
"Bash(PGPASSWORD=123456 psql:*)",
"Bash(go mod:*)"
]
}
}
module testdata
go 1.25.0
require (
github.com/google/uuid v1.6.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
)
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/text v0.21.0 // indirect
)
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -13,6 +13,7 @@
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@tailwindcss/postcss": "^4",
"@tanstack/react-query": "^5.80.6",
"@xyflow/react": "^12.10.1",
"antd": "^5.25.3",
"axios": "^1.9.0",
"dayjs": "^1.11.13",
......
'use client';
import { useEffect, useState } from 'react';
import { Card, Table, Tag, Button, Modal, Input, message, Space, Collapse, Timeline } from 'antd';
import { useState } from 'react';
import { Card, Table, Tag, Button, Modal, Input, message, Space, Collapse, Timeline, Typography } from 'antd';
import { RobotOutlined, PlayCircleOutlined, ToolOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
import { useUserStore } from '@/store/userStore';
const { Text } = Typography;
const API = '';
const BUILTIN_AGENTS = [
......@@ -14,6 +15,20 @@ const BUILTIN_AGENTS = [
{ id: 'follow_up_agent', name: '随访管理Agent', description: '管理患者随访,提醒用药、复诊,收集健康数据', category: 'follow_up', tools: ['query_medical_record', 'query_drug', 'query_symptom_knowledge'], max_iterations: 8 },
];
const categoryColor: Record<string, string> = {
pre_consult: 'blue',
diagnosis: 'purple',
prescription: 'orange',
follow_up: 'green',
};
const categoryLabel: Record<string, string> = {
pre_consult: '预问诊',
diagnosis: '诊断辅助',
prescription: '处方审核',
follow_up: '随访管理',
};
interface ToolCall {
tool_name: string;
call_id: string;
......@@ -59,11 +74,11 @@ export default function AgentsPage() {
const data = await res.json();
const agentData = data.data as AgentResponse;
const reply = agentData?.response || '无响应';
setTestMessages(prev => [...prev, {
role: 'assistant',
setTestMessages(prev => [...prev, {
role: 'assistant',
content: reply,
toolCalls: agentData?.tool_calls,
meta: { iterations: agentData?.iterations, tokens: agentData?.total_tokens }
meta: { iterations: agentData?.iterations, tokens: agentData?.total_tokens },
}]);
} catch {
message.error('请求失败');
......@@ -75,61 +90,84 @@ export default function AgentsPage() {
const renderToolCalls = (toolCalls?: ToolCall[]) => {
if (!toolCalls || toolCalls.length === 0) return null;
return (
<Collapse size="small" className="mt-2" items={[{
<Collapse size="small" style={{ marginTop: 8 }} items={[{
key: 'tools',
label: <span className="text-xs"><ToolOutlined className="mr-1" />Tool调用记录 ({toolCalls.length})</span>,
label: <span style={{ fontSize: 12 }}><ToolOutlined style={{ marginRight: 4 }} />Tool调用记录 ({toolCalls.length})</span>,
children: (
<Timeline
items={toolCalls.map((tc, idx) => ({
color: tc.success ? 'green' : 'red',
dot: tc.success ? <CheckCircleOutlined /> : <CloseCircleOutlined />,
children: (
<div key={idx} className="text-xs">
<div className="font-medium">{tc.tool_name}</div>
<div className="text-gray-500">参数: {tc.arguments}</div>
{tc.result && (
<div className={tc.success ? 'text-green-600' : 'text-red-500'}>
结果: {tc.success ? JSON.stringify(tc.result.data).slice(0, 100) : tc.result.error}
{JSON.stringify(tc.result.data).length > 100 && '...'}
</div>
)}
</div>
),
}))}
/>
<Timeline items={toolCalls.map((tc, idx) => ({
color: tc.success ? 'green' : 'red',
dot: tc.success ? <CheckCircleOutlined /> : <CloseCircleOutlined />,
children: (
<div key={idx} style={{ fontSize: 12 }}>
<div style={{ fontWeight: 500 }}>{tc.tool_name}</div>
<div style={{ color: '#8c8c8c' }}>参数: {tc.arguments}</div>
{tc.result && (
<div style={{ color: tc.success ? '#52c41a' : '#ff4d4f' }}>
结果: {tc.success ? JSON.stringify(tc.result.data).slice(0, 100) : tc.result.error}
{JSON.stringify(tc.result.data).length > 100 && '...'}
</div>
)}
</div>
),
}))} />
),
}]} />
);
};
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 200 },
{ title: '名称', dataIndex: 'name', key: 'name' },
{ title: '描述', dataIndex: 'description', key: 'description' },
{ title: '类别', dataIndex: 'category', key: 'category', render: (v: string) => <Tag color="blue">{v}</Tag> },
{ title: '工具', dataIndex: 'tools', key: 'tools', render: (v: string[]) => v.map(t => <Tag key={t}>{t}</Tag>) },
{ title: '最大迭代', dataIndex: 'max_iterations', key: 'max_iterations', width: 100 },
{
title: '操作', key: 'action', render: (_: unknown, r: typeof BUILTIN_AGENTS[0]) => (
<Button size="small" icon={<PlayCircleOutlined />} onClick={() => openTest(r.id, r.name)}>测试</Button>
title: '智能体名称', key: 'name',
render: (_: unknown, r: typeof BUILTIN_AGENTS[0]) => (
<Space>
<RobotOutlined style={{ color: '#1890ff' }} />
<div>
<Text strong>{r.name}</Text>
<br />
<Text type="secondary" style={{ fontSize: 12 }}>{r.id}</Text>
</div>
</Space>
),
},
{ title: '描述', dataIndex: 'description', key: 'description', ellipsis: true },
{
title: '类别', dataIndex: 'category', key: 'category', width: 110,
render: (v: string) => <Tag color={categoryColor[v] || 'default'}>{categoryLabel[v] || v}</Tag>,
},
{
title: '工具', dataIndex: 'tools', key: 'tools',
render: (v: string[]) => (
<Space size={4} wrap>{v.map(t => <Tag key={t} style={{ fontSize: 11, margin: 0 }}>{t}</Tag>)}</Space>
),
},
{ title: '最大迭代', dataIndex: 'max_iterations', key: 'max_iterations', width: 90, render: (v: number) => <Tag>{v} 次</Tag> },
{
title: '操作', key: 'action', width: 90,
render: (_: unknown, r: typeof BUILTIN_AGENTS[0]) => (
<Button type="link" size="small" icon={<PlayCircleOutlined />} onClick={() => openTest(r.id, r.name)}>测试</Button>
),
},
];
return (
<div>
<Card title={<Space><RobotOutlined />Agent管理</Space>}>
<Table dataSource={BUILTIN_AGENTS} columns={columns} rowKey="id" pagination={false} />
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>智能体管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理平台内置 AI 智能体,支持对话测试与工具调用查看</div>
</div>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Table dataSource={BUILTIN_AGENTS} columns={columns} rowKey="id" pagination={false} size="small" />
</Card>
<Modal
title={`测试 - ${testModal.agentName}`}
title={`测试 · ${testModal.agentName}`}
open={testModal.open}
onCancel={() => setTestModal({ open: false, agentId: '', agentName: '' })}
footer={null}
width={700}
>
<div style={{ height: 400, overflowY: 'auto', border: '1px solid #f0f0f0', borderRadius: 6, padding: 12, marginBottom: 12 }}>
<div style={{ height: 400, overflowY: 'auto', border: '1px solid #f0f0f0', borderRadius: 8, padding: 12, marginBottom: 12 }}>
{testMessages.map((m, i) => (
<div key={i} style={{ marginBottom: 12, textAlign: m.role === 'user' ? 'right' : 'left' }}>
<div style={{
......@@ -140,7 +178,7 @@ export default function AgentsPage() {
}}>
{m.content}
{m.role === 'assistant' && m.meta && (
<div className="text-xs text-gray-400 mt-1 border-t pt-1">
<div style={{ fontSize: 11, color: '#bbb', marginTop: 4, borderTop: '1px solid #e8e8e8', paddingTop: 4 }}>
迭代: {m.meta.iterations || 0} | Tokens: {m.meta.tokens || 0}
</div>
)}
......@@ -148,7 +186,7 @@ export default function AgentsPage() {
{m.role === 'assistant' && renderToolCalls(m.toolCalls)}
</div>
))}
{loading && <div style={{ color: '#999', fontSize: 12 }}>Agent思考中...</div>}
{loading && <div style={{ color: '#999', fontSize: 12 }}>Agent 思考中...</div>}
</div>
<Space.Compact style={{ width: '100%' }}>
<Input value={inputMsg} onChange={e => setInputMsg(e.target.value)} onPressEnter={sendMessage} placeholder="输入消息..." />
......
This diff is collapsed.
......@@ -34,6 +34,7 @@ const menuItems = [
key: 'ai-platform', icon: <ApiOutlined />, label: '智能体平台',
children: [
{ key: '/admin/agents', icon: <RobotOutlined />, label: 'Agent管理' },
{ key: '/admin/tools', icon: <ApiOutlined />, label: 'Tools工具' },
{ key: '/admin/workflows', icon: <DeploymentUnitOutlined />, label: '工作流' },
{ key: '/admin/knowledge', icon: <BookOutlined />, label: '知识库' },
],
......@@ -67,7 +68,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
const getSelectedKeys = () => {
const allKeys = ['/admin/dashboard', '/admin/patients', '/admin/doctors', '/admin/admins',
'/admin/departments', '/admin/consultations', '/admin/prescription', '/admin/pharmacy',
'/admin/ai-config', '/admin/compliance', '/admin/agents', '/admin/workflows', '/admin/knowledge'];
'/admin/ai-config', '/admin/compliance', '/admin/agents', '/admin/tools', '/admin/workflows', '/admin/knowledge'];
const match = allKeys.find(k => currentPath.startsWith(k));
return match ? [match] : [];
};
......@@ -77,18 +78,21 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
<header
style={{
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 1000,
height: 48, display: 'flex', alignItems: 'center', padding: '0 16px',
background: 'linear-gradient(135deg, #001529 0%, #003a8c 50%, #0050b3 100%)',
boxShadow: '0 2px 8px rgba(0,21,41,0.15)',
height: 56, display: 'flex', alignItems: 'center', padding: '0 24px',
background: '#ffffff',
borderBottom: '1px solid #e8f0fc',
boxShadow: '0 1px 4px rgba(0, 82, 204, 0.06)',
}}
>
<div
style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', marginRight: 16, flexShrink: 0 }}
style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', marginRight: 20, flexShrink: 0 }}
onClick={() => router.push('/admin/dashboard')}
>
<MedicineBoxOutlined style={{ fontSize: 18, color: '#fff' }} />
<span style={{ fontSize: 14, fontWeight: 700, color: '#fff' }}>互联网医院</span>
<Tag color="geekblue" style={{ marginLeft: 4, fontSize: 10, lineHeight: '18px', padding: '0 4px' }}>管理后台</Tag>
<div style={{ width: 30, height: 30, borderRadius: 8, background: 'linear-gradient(135deg, #722ed1 0%, #531dab 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<MedicineBoxOutlined style={{ fontSize: 16, color: '#fff' }} />
</div>
<span style={{ fontSize: 15, fontWeight: 700, color: '#1d2129' }}>互联网医院</span>
<Tag color="purple" style={{ marginLeft: 2, fontSize: 10, lineHeight: '18px', padding: '0 5px' }}>管理后台</Tag>
</div>
<Menu
......@@ -96,20 +100,20 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
selectedKeys={getSelectedKeys()}
items={menuItems}
onClick={handleMenuClick}
theme="dark"
className="top-nav-menu"
style={{ flex: 1, minWidth: 0 }}
theme="light"
className="patient-nav-menu"
style={{ flex: 1, minWidth: 0, borderBottom: 'none' }}
/>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0 }}>
<Badge count={2} size="small">
<BellOutlined style={{ fontSize: 16, color: 'rgba(255,255,255,0.7)', cursor: 'pointer' }} />
<BellOutlined style={{ fontSize: 16, color: '#595959', cursor: 'pointer' }} />
</Badge>
{user ? (
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}>
<Space style={{ cursor: 'pointer' }}>
<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: '#722ed1' }} />
<Text style={{ color: 'rgba(255,255,255,0.9)', fontSize: 12 }}>{user.real_name || '管理员'}</Text>
<Text style={{ color: '#1d2129', fontSize: 13 }}>{user.real_name || '管理员'}</Text>
</Space>
</Dropdown>
) : (
......@@ -118,7 +122,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
</div>
</header>
<Content style={{ marginTop: 48, padding: 16, minHeight: 'calc(100vh - 48px)', background: '#f0f5ff' }}>
<Content style={{ marginTop: 56, minHeight: 'calc(100vh - 56px)', background: '#f5f6fa' }}>
{children}
</Content>
</Layout>
......
This diff is collapsed.
This diff is collapsed.
......@@ -16,7 +16,6 @@ const { Text } = Typography;
const menuItems = [
{ key: '/doctor/workbench', icon: <DashboardOutlined />, label: '工作台' },
{ key: '/doctor/consult', icon: <MessageOutlined />, label: '问诊大厅' },
{ key: '/doctor/ai-assist', icon: <RobotOutlined />, label: 'AI辅助诊断' },
{ key: '/doctor/chronic/review', icon: <SafetyCertificateOutlined />, label: '慢病续方' },
{ key: '/doctor/patient', icon: <FolderOpenOutlined />, label: '患者档案' },
{ key: '/doctor/schedule', icon: <CalendarOutlined />, label: '排班管理' },
......@@ -55,22 +54,30 @@ export default function DoctorLayout({ children }: { children: React.ReactNode }
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Layout style={{ minHeight: '100vh', background: '#f5f8ff' }}>
<header
style={{
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 1000,
height: 48, display: 'flex', alignItems: 'center', padding: '0 16px',
background: 'linear-gradient(135deg, #002140 0%, #003a8c 50%, #0958d9 100%)',
boxShadow: '0 2px 8px rgba(0,21,41,0.15)',
height: 56, display: 'flex', alignItems: 'center', padding: '0 24px',
background: '#ffffff',
borderBottom: '1px solid #e8f0fc',
boxShadow: '0 1px 4px rgba(0, 82, 204, 0.06)',
}}
>
{/* Logo */}
<div
style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', marginRight: 16, flexShrink: 0 }}
style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', marginRight: 24, flexShrink: 0 }}
onClick={() => router.push('/doctor/workbench')}
>
<MedicineBoxOutlined style={{ fontSize: 18, color: '#fff' }} />
<span style={{ fontSize: 14, fontWeight: 700, color: '#fff' }}>互联网医院</span>
<Tag color="green" style={{ marginLeft: 4, fontSize: 10, lineHeight: '18px', padding: '0 4px' }}>医生工作站</Tag>
<div style={{
width: 32, height: 32, borderRadius: 8,
background: 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<MedicineBoxOutlined style={{ fontSize: 16, color: '#fff' }} />
</div>
<span style={{ fontSize: 15, fontWeight: 700, color: '#1d2129', letterSpacing: '-0.3px' }}>互联网医院</span>
<Tag color="green" style={{ marginLeft: 2, fontSize: 10, lineHeight: '18px', padding: '0 6px', borderRadius: 4 }}>医生工作站</Tag>
</div>
<Menu
......@@ -78,30 +85,35 @@ export default function DoctorLayout({ children }: { children: React.ReactNode }
selectedKeys={getSelectedKeys()}
items={menuItems}
onClick={handleMenuClick}
theme="dark"
className="top-nav-menu"
style={{ flex: 1, minWidth: 0 }}
className="patient-nav-menu"
style={{ flex: 1, minWidth: 0, border: 'none' }}
/>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0 }}>
<Switch checked={isOnline} onChange={setIsOnline} checkedChildren="在线" unCheckedChildren="离线" size="small" />
<div style={{ display: 'flex', alignItems: 'center', gap: 16, flexShrink: 0 }}>
<Switch
checked={isOnline}
onChange={setIsOnline}
checkedChildren="接诊中"
unCheckedChildren="离线"
style={{ backgroundColor: isOnline ? '#52c41a' : undefined }}
/>
<Badge count={5} size="small">
<BellOutlined style={{ fontSize: 16, color: 'rgba(255,255,255,0.7)', cursor: 'pointer' }} />
<BellOutlined style={{ fontSize: 18, color: '#595959', cursor: 'pointer' }} />
</Badge>
{user ? (
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}>
<Space style={{ cursor: 'pointer' }}>
<Avatar size="small" src={user.avatar} icon={<UserOutlined />} style={{ backgroundColor: '#52c41a' }} />
<Text style={{ color: 'rgba(255,255,255,0.9)', fontSize: 12 }}>{user.real_name || user.phone}</Text>
<Avatar size={30} src={user.avatar || null} icon={<UserOutlined />} style={{ backgroundColor: '#52c41a' }} />
<Text style={{ color: '#1d2129', fontSize: 13, fontWeight: 500 }}>{user.real_name || user.phone}</Text>
</Space>
</Dropdown>
) : (
<Button size="small" type="primary" onClick={() => router.push('/login')}>登录</Button>
<Button type="primary" size="small" onClick={() => router.push('/login')} style={{ borderRadius: 8 }}>登录</Button>
)}
</div>
</header>
<Content style={{ marginTop: 48, padding: 16, minHeight: 'calc(100vh - 48px)', background: '#f0f5ff' }}>
<Content style={{ marginTop: 56, minHeight: 'calc(100vh - 56px)', background: '#f5f8ff' }}>
{children}
</Content>
</Layout>
......
import GlobalAIFloat from '@/components/GlobalAIFloat';
export default function MainLayout({ children }: { children: React.ReactNode }) {
return <div className="compact-ui">{children}</div>;
return (
<div className="compact-ui">
{children}
<GlobalAIFloat />
</div>
);
}
import { Suspense } from 'react';
import { Spin } from 'antd';
import PageComponent from '@/pages/patient/ConsultCreate';
export default function Page() { return <PageComponent />; }
export default function Page() {
return (
<Suspense fallback={<div style={{ textAlign: 'center', padding: 80 }}><Spin size="large" tip="加载中..." /></div>}>
<PageComponent />
</Suspense>
);
}
import { Suspense } from 'react';
import { Spin } from 'antd';
import PageComponent from '@/pages/patient/Doctors';
export default function Page() { return <PageComponent />; }
export default function Page() {
return (
<Suspense fallback={<div style={{ textAlign: 'center', padding: 80 }}><Spin size="large" /></div>}>
<PageComponent />
</Suspense>
);
}
......@@ -20,7 +20,6 @@ const menuItems = [
key: 'consult-services', icon: <VideoCameraOutlined />, label: '问诊服务',
children: [
{ key: '/patient/doctors', icon: <SearchOutlined />, label: '找医生' },
{ key: '/patient/pre-consult', icon: <RobotOutlined />, label: 'AI预问诊' },
{ key: '/patient/consult', icon: <VideoCameraOutlined />, label: '我的问诊' },
],
},
......@@ -42,9 +41,8 @@ const menuItems = [
{ key: '/patient/profile', icon: <UserOutlined />, label: '个人中心' },
];
// 所有可路由的 key 列表(用于选中匹配)
const allRouteKeys = [
'/patient/home', '/patient/doctors', '/patient/pre-consult', '/patient/consult',
'/patient/home', '/patient/doctors', '/patient/consult',
'/patient/prescription', '/patient/pharmacy/order', '/patient/payment',
'/patient/chronic', '/patient/health-records', '/patient/profile',
];
......@@ -63,9 +61,7 @@ export default function PatientLayout({ children }: { children: React.ReactNode
];
const handleMenuClick = ({ key }: { key: string }) => {
if (key.startsWith('/')) {
router.push(key);
}
if (key.startsWith('/')) router.push(key);
};
const handleUserMenuClick = ({ key }: { key: string }) => {
......@@ -79,52 +75,81 @@ export default function PatientLayout({ children }: { children: React.ReactNode
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Layout style={{ minHeight: '100vh', background: '#f5f8ff' }}>
{/* 顶部导航 - 白色现代风格 */}
<header
style={{
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 1000,
height: 48, display: 'flex', alignItems: 'center', padding: '0 16px',
background: 'linear-gradient(135deg, #003a8c 0%, #0050b3 50%, #1890ff 100%)',
boxShadow: '0 2px 8px rgba(0,21,41,0.12)',
height: 56, display: 'flex', alignItems: 'center', padding: '0 24px',
background: '#ffffff',
borderBottom: '1px solid #e8f0fc',
boxShadow: '0 1px 4px rgba(0, 82, 204, 0.06)',
}}
>
{/* Logo */}
<div
style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', marginRight: 16, flexShrink: 0 }}
style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', marginRight: 24, flexShrink: 0 }}
onClick={() => router.push('/patient/home')}
>
<MedicineBoxOutlined style={{ fontSize: 18, color: '#fff' }} />
<span style={{ fontSize: 14, fontWeight: 700, color: '#fff' }}>互联网医院</span>
<Tag color="blue" style={{ marginLeft: 4, fontSize: 10, lineHeight: '18px', padding: '0 4px' }}>患者端</Tag>
<div style={{
width: 32, height: 32, borderRadius: 8,
background: 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<MedicineBoxOutlined style={{ fontSize: 16, color: '#fff' }} />
</div>
<span style={{ fontSize: 15, fontWeight: 700, color: '#1d2129', letterSpacing: '-0.3px' }}>互联网医院</span>
<Tag color="blue" style={{ marginLeft: 2, fontSize: 10, lineHeight: '18px', padding: '0 6px', borderRadius: 4 }}>患者端</Tag>
</div>
{/* 导航菜单 */}
<Menu
mode="horizontal"
selectedKeys={getSelectedKeys()}
items={menuItems}
onClick={handleMenuClick}
theme="dark"
className="top-nav-menu"
style={{ flex: 1, minWidth: 0 }}
className="patient-nav-menu"
style={{ flex: 1, minWidth: 0, border: 'none' }}
/>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0 }}>
{/* 右侧用户区 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 16, flexShrink: 0 }}>
<Badge count={3} size="small">
<BellOutlined style={{ fontSize: 16, color: 'rgba(255,255,255,0.7)', cursor: 'pointer' }} />
<BellOutlined style={{ fontSize: 18, color: '#595959', cursor: 'pointer' }} />
</Badge>
{user ? (
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}>
<Space style={{ cursor: 'pointer' }}>
<Avatar size="small" src={user.avatar} icon={<UserOutlined />} style={{ backgroundColor: '#1890ff' }} />
<Text style={{ color: 'rgba(255,255,255,0.9)', fontSize: 12 }}>{user.real_name || user.phone}</Text>
<Avatar
size={30}
src={user.avatar || null}
icon={<UserOutlined />}
style={{ backgroundColor: '#1890ff' }}
/>
<Text style={{ color: '#1d2129', fontSize: 13, fontWeight: 500 }}>
{user.real_name || user.phone}
</Text>
</Space>
</Dropdown>
) : (
<Button size="small" type="primary" onClick={() => router.push('/login')}>登录 / 注册</Button>
<Button
type="primary"
size="small"
onClick={() => router.push('/login')}
style={{ borderRadius: 8, fontWeight: 500 }}
>
登录 / 注册
</Button>
)}
</div>
</header>
<Content style={{ marginTop: 48, padding: 16, minHeight: 'calc(100vh - 48px)', background: '#f0f5ff' }}>
{/* 内容区 - 去掉 padding,让各页面自己控制,避免左右留白过大 */}
<Content style={{
marginTop: 56,
minHeight: 'calc(100vh - 56px)',
background: '#f5f8ff',
}}>
{children}
</Content>
</Layout>
......
......@@ -36,13 +36,13 @@ html, body {
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
font-size: 14px;
line-height: 1.5;
line-height: 1.6;
color: #1d2129;
background-color: #f0f5ff;
background-color: #f5f8ff;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
......@@ -73,7 +73,45 @@ a:hover {
background: #91c2f0;
}
/* ===== 顶部导航栏菜单样式 ===== */
/* ===== 患者端白色导航菜单 ===== */
.patient-nav-menu.ant-menu-horizontal {
border-bottom: none !important;
background: transparent !important;
line-height: 56px !important;
}
.patient-nav-menu .ant-menu-item,
.patient-nav-menu .ant-menu-submenu-title {
color: #595959 !important;
font-size: 14px !important;
font-weight: 500 !important;
padding: 0 14px !important;
margin: 0 2px !important;
border-radius: 8px !important;
transition: all 0.2s !important;
}
.patient-nav-menu .ant-menu-item:hover,
.patient-nav-menu .ant-menu-submenu-title:hover,
.patient-nav-menu .ant-menu-item-active,
.patient-nav-menu .ant-menu-submenu-active > .ant-menu-submenu-title {
color: #1890ff !important;
background: #e6f7ff !important;
}
.patient-nav-menu .ant-menu-item-selected {
color: #1890ff !important;
background: #e6f7ff !important;
font-weight: 600 !important;
}
.patient-nav-menu .ant-menu-item::after,
.patient-nav-menu .ant-menu-submenu::after {
display: none !important;
}
/* ===== 顶部导航栏菜单样式(深色,其他布局用) ===== */
.top-nav-menu.ant-menu-horizontal {
border-bottom: none !important;
......@@ -116,18 +154,24 @@ a:hover {
/* ===== Ant Design 紧凑覆盖(仅作用于 .compact-ui 内部) ===== */
.compact-ui .ant-card {
border-radius: 8px !important;
box-shadow: 0 1px 4px rgba(24, 144, 255, 0.06) !important;
border: 1px solid #e6f0fa !important;
border-radius: 10px !important;
box-shadow: 0 1px 3px rgba(0, 82, 204, 0.06) !important;
border: 1px solid #e8f0fc !important;
transition: box-shadow 0.2s ease !important;
}
.compact-ui .ant-card:hover {
box-shadow: 0 4px 12px rgba(0, 82, 204, 0.1) !important;
}
.compact-ui .ant-card .ant-card-head {
min-height: 40px !important;
padding: 0 16px !important;
min-height: 44px !important;
padding: 0 20px !important;
font-size: 14px !important;
border-bottom: 1px solid #e6f0fa !important;
font-weight: 600 !important;
border-bottom: 1px solid #edf2fc !important;
color: #1d2129 !important;
}
.compact-ui .ant-card .ant-card-body {
padding: 16px !important;
padding: 20px !important;
}
.compact-ui .ant-table {
......
......@@ -7,6 +7,7 @@ import { StyleProvider } from '@ant-design/cssinjs';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import GlobalAIFloat from '@/components/GlobalAIFloat';
const queryClient = new QueryClient({
defaultOptions: {
......@@ -26,11 +27,29 @@ export default function Providers({ children }: { children: React.ReactNode }) {
locale={zhCN}
theme={{
token: {
colorPrimary: '#1677ff',
colorPrimary: '#1890ff',
borderRadius: 8,
colorBgLayout: '#f5f5f5',
colorLink: '#1677ff',
colorLinkHover: '#4096ff',
colorBgLayout: '#f5f8ff',
colorLink: '#1890ff',
colorLinkHover: '#40a9ff',
fontSize: 14,
colorTextBase: '#1d2129',
colorBgContainer: '#ffffff',
boxShadow: '0 1px 3px rgba(0, 82, 204, 0.06)',
boxShadowSecondary: '0 2px 8px rgba(0, 82, 204, 0.08)',
},
components: {
Card: {
boxShadow: '0 1px 3px rgba(0, 82, 204, 0.06)',
boxShadowTertiary: '0 1px 3px rgba(0, 82, 204, 0.06)',
},
Button: {
borderRadius: 8,
controlHeight: 36,
},
Input: {
borderRadius: 8,
},
},
}}
>
......
This diff is collapsed.
'use client';
import React from 'react';
import { Tooltip } from 'antd';
import {
RobotOutlined,
CloseOutlined,
HeartOutlined,
MessageOutlined,
} from '@ant-design/icons';
import { useUserStore } from '../../store/userStore';
import { useAIAssistStore } from '../../store/aiAssistStore';
import ChatPanel from './ChatPanel';
const FloatContainer: React.FC = () => {
const { user } = useUserStore();
const { isOpen, patientContext, openDrawer, closeDrawer } = useAIAssistStore();
if (!user) return null;
const isDoctor = user.role === 'doctor';
const isPatient = user.role === 'patient';
if (!isDoctor && !isPatient) return null;
const primaryColor = isPatient
? 'linear-gradient(135deg, #22c55e, #06b6d4)'
: 'linear-gradient(135deg, #3b82f6, #8b5cf6)';
const title = isPatient ? 'AI智能助手' : 'AI诊断助手';
const subtitle = isPatient ? '智能预问诊 · 健康咨询' : '辅助诊断 · 处方审核';
return (
<>
{/* 悬浮按钮 - 右侧固定 */}
{!isOpen && (
<Tooltip title={title} placement="left">
<div
onClick={() => openDrawer(patientContext || undefined)}
style={{
position: 'fixed',
right: 0,
top: '50%',
transform: 'translateY(-50%)',
zIndex: 1001,
width: 48,
height: 96,
background: primaryColor,
borderRadius: '12px 0 0 12px',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
boxShadow: '-2px 0 12px rgba(0,0,0,0.15)',
transition: 'all 0.3s ease',
}}
onMouseEnter={e => { e.currentTarget.style.width = '56px'; }}
onMouseLeave={e => { e.currentTarget.style.width = '48px'; }}
>
{isPatient
? <HeartOutlined style={{ color: '#fff', fontSize: 20 }} />
: <RobotOutlined style={{ color: '#fff', fontSize: 20 }} />
}
<div style={{
color: '#fff', fontSize: 10, writingMode: 'vertical-rl',
letterSpacing: 2, fontWeight: 500,
}}>
{isPatient ? 'AI助手' : 'AI诊断'}
</div>
</div>
</Tooltip>
)}
{/* 展开的对话面板 */}
{isOpen && (
<div style={{
position: 'fixed',
right: 0,
top: 0,
bottom: 0,
width: 400,
zIndex: 1002,
background: '#fff',
boxShadow: '-4px 0 24px rgba(0,0,0,0.12)',
display: 'flex',
flexDirection: 'column',
transition: 'all 0.3s ease',
borderLeft: '1px solid #e5e7eb',
}}>
{/* 头部 */}
<div style={{
padding: '12px 16px',
background: primaryColor,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{
width: 36, height: 36, borderRadius: 10,
background: 'rgba(255,255,255,0.2)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
{isPatient
? <MessageOutlined style={{ color: '#fff', fontSize: 18 }} />
: <RobotOutlined style={{ color: '#fff', fontSize: 18 }} />
}
</div>
<div>
<div style={{ color: '#fff', fontWeight: 600, fontSize: 14 }}>{title}</div>
<div style={{ color: 'rgba(255,255,255,0.8)', fontSize: 11 }}>{subtitle}</div>
</div>
</div>
<div
onClick={() => closeDrawer()}
style={{
width: 28, height: 28, borderRadius: '50%',
background: 'rgba(255,255,255,0.2)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', transition: 'background 0.2s',
}}
onMouseEnter={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.3)'; }}
onMouseLeave={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.2)'; }}
>
<CloseOutlined style={{ color: '#fff', fontSize: 14 }} />
</div>
</div>
{/* 聊天面板 */}
<div style={{ flex: 1, overflow: 'hidden' }}>
<ChatPanel
role={isPatient ? 'patient' : 'doctor'}
patientContext={isDoctor ? (patientContext || undefined) : undefined}
/>
</div>
</div>
)}
{/* 遮罩层 */}
{isOpen && (
<div
onClick={() => closeDrawer()}
style={{
position: 'fixed',
inset: 0,
zIndex: 1001,
background: 'rgba(0,0,0,0.1)',
}}
/>
)}
</>
);
};
export default FloatContainer;
export { default } from './FloatContainer';
export interface ToolCall {
tool_name: string;
call_id: string;
arguments: string;
result: { success: boolean; data?: unknown; error?: string };
success: boolean;
}
export interface ChatMessage {
role: 'user' | 'assistant' | 'system';
content: string;
toolCalls?: ToolCall[];
meta?: { iterations?: number; tokens?: number; agent_id?: string; workflow_id?: string };
timestamp: Date;
}
export interface AgentOption {
id: string;
name: string;
description: string;
icon: string;
}
export const PATIENT_AGENTS: AgentOption[] = [
{ id: 'pre_consult_agent', name: '预问诊助手', description: '智能收集症状,生成预问诊报告', icon: 'heart' },
{ id: 'follow_up_agent', name: '随访助手', description: '用药提醒、复诊管理', icon: 'calendar' },
];
export const DOCTOR_AGENTS: AgentOption[] = [
{ id: 'diagnosis_agent', name: '诊断辅助', description: '鉴别诊断、检查建议', icon: 'robot' },
{ id: 'prescription_agent', name: '处方审核', description: '药物相互作用、禁忌症检查', icon: 'medicine' },
];
'use client';
import React, { useState } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { Layout, Menu, Avatar, Dropdown, Button, Badge, Space, Switch, Typography, Tag } from 'antd';
import {
DashboardOutlined, UserOutlined, MessageOutlined, CalendarOutlined,
SettingOutlined, LogoutOutlined, BellOutlined, MedicineBoxOutlined,
RobotOutlined, DollarOutlined, FolderOpenOutlined, SafetyCertificateOutlined,
} from '@ant-design/icons';
import { useUserStore } from '../../store/userStore';
const { Header, Content } = Layout;
const { Text } = Typography;
const menuItems = [
{ key: '/doctor/workbench', icon: <DashboardOutlined />, label: '工作台' },
{ key: '/doctor/consult', icon: <MessageOutlined />, label: '问诊大厅' },
{ key: '/doctor/ai-assist', icon: <RobotOutlined />, label: 'AI辅助诊断' },
{ key: '/doctor/chronic/review', icon: <SafetyCertificateOutlined />, label: '慢病续方' },
{ key: '/doctor/patient', icon: <FolderOpenOutlined />, label: '患者档案' },
{ key: '/doctor/schedule', icon: <CalendarOutlined />, label: '排班管理' },
{ key: '/doctor/income', icon: <DollarOutlined />, label: '收入统计' },
{ key: '/doctor/profile', icon: <UserOutlined />, label: '个人信息' },
];
const DoctorLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const router = useRouter();
const pathname = usePathname();
const { user, logout } = useUserStore();
const [isOnline, setIsOnline] = useState(false);
const userMenuItems = [
{ key: 'profile', icon: <UserOutlined />, label: '个人信息' },
{ key: 'settings', icon: <SettingOutlined />, label: '设置' },
{ type: 'divider' as const },
{ key: 'logout', icon: <LogoutOutlined />, label: '退出登录', danger: true },
];
const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') { logout(); router.push('/login'); }
else if (key === 'profile') router.push('/doctor/profile');
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Header className="fixed! top-0! left-0! right-0! z-100! flex! items-center! h-12! px-4! leading-12!"
style={{ background: 'linear-gradient(135deg, #002140 0%, #003a8c 50%, #0958d9 100%)', borderBottom: 'none', boxShadow: '0 2px 8px rgba(0,21,41,0.15)', padding: '0 16px' }}>
<div className="flex items-center gap-2 cursor-pointer mr-4 shrink-0"
onClick={() => router.push('/doctor/workbench')}>
<MedicineBoxOutlined className="text-lg text-white" />
<span className="text-sm font-bold text-white">互联网医院</span>
<Tag color="green" className="ml-1! text-[10px]! leading-4! px-1!">医生工作站</Tag>
</div>
<Menu
mode="horizontal"
selectedKeys={[pathname || '']}
items={menuItems}
onClick={({ key }) => router.push(key)}
theme="dark"
className="flex-1! border-none! min-w-0! bg-transparent! text-xs! [&_.ant-menu-item]:text-blue-100/80! [&_.ant-menu-item-selected]:text-white! [&_.ant-menu-item-selected]:bg-white/10! [&_.ant-menu-item:hover]:text-white!"
/>
<div className="flex items-center gap-3 shrink-0">
<Switch checked={isOnline} onChange={setIsOnline} checkedChildren="在线" unCheckedChildren="离线" size="small" />
<Badge count={5} size="small">
<BellOutlined className="text-base text-white/70 cursor-pointer hover:text-white" />
</Badge>
{user ? (
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}>
<Space className="cursor-pointer">
<Avatar size="small" src={user.avatar} icon={<UserOutlined />} style={{ backgroundColor: '#52c41a' }} />
<Text className="text-white/90! text-xs!">{user.real_name || user.phone}</Text>
</Space>
</Dropdown>
) : (
<Button size="small" type="primary" onClick={() => router.push('/login')}>登录</Button>
)}
</div>
</Header>
<Content className="mt-12! p-3! min-h-[calc(100vh-48px)]! bg-[#f0f5ff]!">
{children}
</Content>
</Layout>
);
};
export default DoctorLayout;
'use client';
import React, { useState } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { Layout, Menu, Avatar, Dropdown, Button, Badge, Space, Switch, Typography, Tag } from 'antd';
import {
DashboardOutlined, UserOutlined, MessageOutlined, CalendarOutlined,
SettingOutlined, LogoutOutlined, BellOutlined, MedicineBoxOutlined,
RobotOutlined, DollarOutlined, FolderOpenOutlined, SafetyCertificateOutlined,
} from '@ant-design/icons';
import { useUserStore } from '../../store/userStore';
const { Header, Content } = Layout;
const { Text } = Typography;
const menuItems = [
{ key: '/doctor/workbench', icon: <DashboardOutlined />, label: '工作台' },
{ key: '/doctor/consult', icon: <MessageOutlined />, label: '问诊大厅' },
{ key: '/doctor/chronic/review', icon: <SafetyCertificateOutlined />, label: '慢病续方' },
{ key: '/doctor/patient', icon: <FolderOpenOutlined />, label: '患者档案' },
{ key: '/doctor/schedule', icon: <CalendarOutlined />, label: '排班管理' },
{ key: '/doctor/income', icon: <DollarOutlined />, label: '收入统计' },
{ key: '/doctor/profile', icon: <UserOutlined />, label: '个人信息' },
];
const DoctorLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const router = useRouter();
const pathname = usePathname();
const { user, logout } = useUserStore();
const [isOnline, setIsOnline] = useState(false);
const userMenuItems = [
{ key: 'profile', icon: <UserOutlined />, label: '个人信息' },
{ key: 'settings', icon: <SettingOutlined />, label: '设置' },
{ type: 'divider' as const },
{ key: 'logout', icon: <LogoutOutlined />, label: '退出登录', danger: true },
];
const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') { logout(); router.push('/login'); }
else if (key === 'profile') router.push('/doctor/profile');
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Header className="fixed! top-0! left-0! right-0! z-100! flex! items-center! h-12! px-4! leading-12!"
style={{ background: 'linear-gradient(135deg, #002140 0%, #003a8c 50%, #0958d9 100%)', borderBottom: 'none', boxShadow: '0 2px 8px rgba(0,21,41,0.15)', padding: '0 16px' }}>
<div className="flex items-center gap-2 cursor-pointer mr-4 shrink-0"
onClick={() => router.push('/doctor/workbench')}>
<MedicineBoxOutlined className="text-lg text-white" />
<span className="text-sm font-bold text-white">互联网医院</span>
<Tag color="green" className="ml-1! text-[10px]! leading-4! px-1!">医生工作站</Tag>
</div>
<Menu
mode="horizontal"
selectedKeys={[pathname || '']}
items={menuItems}
onClick={({ key }) => router.push(key)}
theme="dark"
className="flex-1! border-none! min-w-0! bg-transparent! text-xs! [&_.ant-menu-item]:text-blue-100/80! [&_.ant-menu-item-selected]:text-white! [&_.ant-menu-item-selected]:bg-white/10! [&_.ant-menu-item:hover]:text-white!"
/>
<div className="flex items-center gap-3 shrink-0">
<Switch checked={isOnline} onChange={setIsOnline} checkedChildren="在线" unCheckedChildren="离线" size="small" />
<Badge count={5} size="small">
<BellOutlined className="text-base text-white/70 cursor-pointer hover:text-white" />
</Badge>
{user ? (
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}>
<Space className="cursor-pointer">
<Avatar size="small" src={user.avatar || null} icon={<UserOutlined />} style={{ backgroundColor: '#52c41a' }} />
<Text className="text-white/90! text-xs!">{user.real_name || user.phone}</Text>
</Space>
</Dropdown>
) : (
<Button size="small" type="primary" onClick={() => router.push('/login')}>登录</Button>
)}
</div>
</Header>
<Content className="mt-12! p-3! min-h-[calc(100vh-48px)]! bg-[#f0f5ff]!">
{children}
</Content>
</Layout>
);
};
export default DoctorLayout;
......@@ -85,7 +85,7 @@ const MainLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
{user ? (
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }} placement="bottomRight">
<div style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 8 }}>
<Avatar src={user.avatar} icon={<UserOutlined />} style={{ backgroundColor: '#1677ff' }} />
<Avatar src={user.avatar || null} icon={<UserOutlined />} style={{ backgroundColor: '#1677ff' }} />
<span style={{ color: 'rgba(0,0,0,0.88)' }}>{user.real_name || user.phone}</span>
</div>
</Dropdown>
......
'use client';
import React from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { Layout, Menu, Avatar, Dropdown, Button, Badge, Space, Typography, Tag } from 'antd';
import {
HomeOutlined, UserOutlined, MedicineBoxOutlined, FileTextOutlined,
HeartOutlined, SettingOutlined, LogoutOutlined, BellOutlined,
RobotOutlined, VideoCameraOutlined, PayCircleOutlined,
ShoppingCartOutlined, FolderOpenOutlined,
} from '@ant-design/icons';
import { useUserStore } from '../../store/userStore';
const { Header, Content } = Layout;
const { Text } = Typography;
const menuItems = [
{ key: '/patient/home', icon: <HomeOutlined />, label: '首页' },
{ key: '/patient/doctors', icon: <MedicineBoxOutlined />, label: '医生列表' },
{ key: '/patient/pre-consult', icon: <RobotOutlined />, label: 'AI预问诊' },
{ key: '/patient/consult', icon: <VideoCameraOutlined />, label: '问诊' },
{ key: '/patient/prescription', icon: <FileTextOutlined />, label: '处方' },
{ key: '/patient/payment', icon: <PayCircleOutlined />, label: '支付' },
{ key: '/patient/pharmacy/order', icon: <ShoppingCartOutlined />, label: '药品配送' },
{ key: '/patient/chronic', icon: <HeartOutlined />, label: '慢病管理' },
{ key: '/patient/health-records', icon: <FolderOpenOutlined />, label: '健康档案' },
{ key: '/patient/profile', icon: <UserOutlined />, label: '个人中心' },
];
const PatientLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const router = useRouter();
const pathname = usePathname();
const { user, logout } = useUserStore();
const userMenuItems = [
{ key: 'profile', icon: <UserOutlined />, label: '个人中心' },
{ key: 'settings', icon: <SettingOutlined />, label: '设置' },
{ type: 'divider' as const },
{ key: 'logout', icon: <LogoutOutlined />, label: '退出登录', danger: true },
];
const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') { logout(); router.push('/login'); }
else if (key === 'profile') router.push('/patient/profile');
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Header className="fixed! top-0! left-0! right-0! z-100! flex! items-center! h-12! px-4! leading-12!"
style={{ background: 'linear-gradient(135deg, #003a8c 0%, #0050b3 50%, #1890ff 100%)', borderBottom: 'none', boxShadow: '0 2px 8px rgba(0,21,41,0.12)', padding: '0 16px' }}>
<div className="flex items-center gap-2 cursor-pointer mr-4 shrink-0"
onClick={() => router.push('/patient/home')}>
<MedicineBoxOutlined className="text-lg text-white" />
<span className="text-sm font-bold text-white">互联网医院</span>
<Tag color="blue" className="ml-1! text-[10px]! leading-4! px-1! border-blue-300/30!">患者端</Tag>
</div>
<Menu
mode="horizontal"
selectedKeys={[pathname || '']}
items={menuItems}
onClick={({ key }) => router.push(key)}
theme="dark"
className="flex-1! border-none! min-w-0! bg-transparent! text-xs! [&_.ant-menu-item]:text-blue-100/80! [&_.ant-menu-item-selected]:text-white! [&_.ant-menu-item-selected]:bg-white/10! [&_.ant-menu-item:hover]:text-white!"
/>
<div className="flex items-center gap-3 shrink-0">
<Badge count={3} size="small">
<BellOutlined className="text-base text-white/70 cursor-pointer hover:text-white" />
</Badge>
{user ? (
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}>
<Space className="cursor-pointer">
<Avatar size="small" src={user.avatar} icon={<UserOutlined />} style={{ backgroundColor: '#1890ff' }} />
<Text className="text-white/90! text-xs!">{user.real_name || user.phone}</Text>
</Space>
</Dropdown>
) : (
<Button size="small" type="primary" onClick={() => router.push('/login')}>登录 / 注册</Button>
)}
</div>
</Header>
<Content className="mt-12! p-3! min-h-[calc(100vh-48px)]! bg-[#f0f5ff]!">
{children}
</Content>
</Layout>
);
};
export default PatientLayout;
'use client';
import React from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { Layout, Menu, Avatar, Dropdown, Button, Badge, Space, Typography, Tag } from 'antd';
import {
HomeOutlined, UserOutlined, MedicineBoxOutlined, FileTextOutlined,
HeartOutlined, SettingOutlined, LogoutOutlined, BellOutlined,
RobotOutlined, VideoCameraOutlined, PayCircleOutlined,
ShoppingCartOutlined, FolderOpenOutlined,
} from '@ant-design/icons';
import { useUserStore } from '../../store/userStore';
const { Header, Content } = Layout;
const { Text } = Typography;
const menuItems = [
{ key: '/patient/home', icon: <HomeOutlined />, label: '首页' },
{ key: '/patient/doctors', icon: <MedicineBoxOutlined />, label: '医生列表' },
{ key: '/patient/consult', icon: <VideoCameraOutlined />, label: '问诊' },
{ key: '/patient/prescription', icon: <FileTextOutlined />, label: '处方' },
{ key: '/patient/payment', icon: <PayCircleOutlined />, label: '支付' },
{ key: '/patient/pharmacy/order', icon: <ShoppingCartOutlined />, label: '药品配送' },
{ key: '/patient/chronic', icon: <HeartOutlined />, label: '慢病管理' },
{ key: '/patient/health-records', icon: <FolderOpenOutlined />, label: '健康档案' },
{ key: '/patient/profile', icon: <UserOutlined />, label: '个人中心' },
];
const PatientLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const router = useRouter();
const pathname = usePathname();
const { user, logout } = useUserStore();
const userMenuItems = [
{ key: 'profile', icon: <UserOutlined />, label: '个人中心' },
{ key: 'settings', icon: <SettingOutlined />, label: '设置' },
{ type: 'divider' as const },
{ key: 'logout', icon: <LogoutOutlined />, label: '退出登录', danger: true },
];
const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') { logout(); router.push('/login'); }
else if (key === 'profile') router.push('/patient/profile');
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Header className="fixed! top-0! left-0! right-0! z-100! flex! items-center! h-12! px-4! leading-12!"
style={{ background: 'linear-gradient(135deg, #003a8c 0%, #0050b3 50%, #1890ff 100%)', borderBottom: 'none', boxShadow: '0 2px 8px rgba(0,21,41,0.12)', padding: '0 16px' }}>
<div className="flex items-center gap-2 cursor-pointer mr-4 shrink-0"
onClick={() => router.push('/patient/home')}>
<MedicineBoxOutlined className="text-lg text-white" />
<span className="text-sm font-bold text-white">互联网医院</span>
<Tag color="blue" className="ml-1! text-[10px]! leading-4! px-1! border-blue-300/30!">患者端</Tag>
</div>
<Menu
mode="horizontal"
selectedKeys={[pathname || '']}
items={menuItems}
onClick={({ key }) => router.push(key)}
theme="dark"
className="flex-1! border-none! min-w-0! bg-transparent! text-xs! [&_.ant-menu-item]:text-blue-100/80! [&_.ant-menu-item-selected]:text-white! [&_.ant-menu-item-selected]:bg-white/10! [&_.ant-menu-item:hover]:text-white!"
/>
<div className="flex items-center gap-3 shrink-0">
<Badge count={3} size="small">
<BellOutlined className="text-base text-white/70 cursor-pointer hover:text-white" />
</Badge>
{user ? (
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}>
<Space className="cursor-pointer">
<Avatar size="small" src={user.avatar || null} icon={<UserOutlined />} style={{ backgroundColor: '#1890ff' }} />
<Text className="text-white/90! text-xs!">{user.real_name || user.phone}</Text>
</Space>
</Dropdown>
) : (
<Button size="small" type="primary" onClick={() => router.push('/login')}>登录 / 注册</Button>
)}
</div>
</Header>
<Content className="mt-12! p-3! min-h-[calc(100vh-48px)]! bg-[#f0f5ff]!">
{children}
</Content>
</Layout>
);
};
export default PatientLayout;
This diff is collapsed.
......@@ -323,12 +323,13 @@ const AdminAIConfigPage: React.FC = () => {
};
return (
<div className="space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">
<RobotOutlined className="mr-1" />AI 配置
</h4>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>AI 配置</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>配置平台 AI 模型与 Prompt 模板</div>
</div>
<Tabs defaultActiveKey="model" size="small" items={[
<Tabs defaultActiveKey="model" items={[
{
key: 'model',
label: <span><SettingOutlined /> 模型选择</span>,
......
......@@ -227,25 +227,28 @@ const AdminAdminsPage: React.FC = () => {
];
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-gray-800 m-0">管理员管理</h4>
<span className="text-xs text-gray-400">共 {total} 条记录</span>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>管理员管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理后台管理员账号与权限</div>
</div>
<span style={{ fontSize: 13, color: '#8c8c8c' }}>共 {total} 条记录</span>
</div>
<Card size="small">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
<Input placeholder="搜索姓名/用户名" prefix={<SearchOutlined />} value={keyword}
onChange={(e) => setKeyword(e.target.value)} onPressEnter={handleSearch} style={{ width: 180 }} size="small" />
<Button size="small" type="primary" icon={<SearchOutlined />} onClick={handleSearch}>搜索</Button>
<Button size="small" icon={<ReloadOutlined />} onClick={() => { setKeyword(''); setPage(1); fetchAdmins(); }}>重置</Button>
onChange={(e) => setKeyword(e.target.value)} onPressEnter={handleSearch} style={{ width: 200, borderRadius: 8 }} />
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>搜索</Button>
<Button icon={<ReloadOutlined />} onClick={() => { setKeyword(''); setPage(1); fetchAdmins(); }}>重置</Button>
</div>
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={() => setAddModalVisible(true)}>添加管理员</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setAddModalVisible(true)}>添加管理员</Button>
</div>
</Card>
<Card size="small">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Table
columns={columns}
dataSource={admins}
......
......@@ -109,47 +109,48 @@ const AdminCompliancePage: React.FC = () => {
];
return (
<div className="space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">
<SafetyCertificateOutlined className="mr-1" />合规报表
</h4>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>合规报表</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>审计日志、数据导出与监管上报</div>
</div>
<Row gutter={[8, 8]}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #1890ff' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #1890ff' }}>
<Statistic title="审计日志" value={logsTotal} prefix={<FileSearchOutlined />} suffix="条" />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #fa8c16' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #fa8c16' }}>
<Statistic title="待处理" value={reports.filter((r) => r.status === 'pending').length} valueStyle={{ color: '#fa8c16' }} suffix="项" />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #52c41a' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #52c41a' }}>
<Statistic title="已上报" value={reports.filter((r) => r.status === 'submitted').length} suffix="项" />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #722ed1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #722ed1' }}>
<Statistic title="已生成" value={reports.filter((r) => r.status === 'generated').length} suffix="项" />
</Card>
</Col>
</Row>
<Tabs defaultActiveKey="logs" size="small" items={[
<Tabs defaultActiveKey="logs" items={[
{
key: 'logs',
label: <span><AuditOutlined /> 审计日志</span>,
children: (
<Card size="small">
<div className="flex flex-wrap items-center gap-2 mb-2">
<Input placeholder="搜索操作内容" style={{ width: 160 }} size="small" allowClear />
<RangePicker size="small" />
<Button type="primary" size="small" ghost onClick={() => fetchLogs(1)}>查询</Button>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<Input placeholder="搜索操作内容" style={{ width: 180, borderRadius: 8 }} allowClear />
<RangePicker />
<Button type="primary" ghost onClick={() => fetchLogs(1)}>查询</Button>
</div>
<Table columns={logColumns} dataSource={logs} rowKey="id" size="small" loading={logsLoading}
pagination={{ current: logsPage, pageSize: 10, total: logsTotal, size: 'small',
pagination={{ current: logsPage, pageSize: 10, total: logsTotal,
onChange: (p) => { setLogsPage(p); fetchLogs(p); } }} />
</Card>
),
......@@ -158,19 +159,19 @@ const AdminCompliancePage: React.FC = () => {
key: 'export',
label: <span><DownloadOutlined /> 数据导出</span>,
children: (
<Card size="small">
<Row gutter={[8, 8]}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Row gutter={[16, 16]}>
{[
{ title: '问诊数据', desc: '问诊记录、诊断、处方', icon: <FileSearchOutlined className="text-xl text-[#1890ff]" /> },
{ title: '用户数据', desc: '患者和医生基本信息', icon: <UserOutlined className="text-xl text-green-500" /> },
{ title: '处方数据', desc: '处方记录、用药统计', icon: <AuditOutlined className="text-xl text-purple-500" /> },
{ title: '系统日志', desc: '操作日志和审计记录', icon: <SettingOutlined className="text-xl text-orange-500" /> },
{ title: '问诊数据', desc: '问诊记录、诊断、处方', icon: <FileSearchOutlined style={{ fontSize: 24, color: '#1890ff' }} /> },
{ title: '用户数据', desc: '患者和医生基本信息', icon: <UserOutlined style={{ fontSize: 24, color: '#52c41a' }} /> },
{ title: '处方数据', desc: '处方记录、用药统计', icon: <AuditOutlined style={{ fontSize: 24, color: '#722ed1' }} /> },
{ title: '系统日志', desc: '操作日志和审计记录', icon: <SettingOutlined style={{ fontSize: 24, color: '#fa8c16' }} /> },
].map((item, i) => (
<Col span={6} key={i}>
<Card size="small" hoverable className="text-center" onClick={() => setExportModalVisible(true)}>
<div className="mb-2">{item.icon}</div>
<div className="text-xs font-semibold mb-0.5">{item.title}</div>
<div className="text-[11px] text-gray-400">{item.desc}</div>
<Card style={{ borderRadius: 12, textAlign: 'center', cursor: 'pointer' }} hoverable onClick={() => setExportModalVisible(true)}>
<div style={{ marginBottom: 10 }}>{item.icon}</div>
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 4 }}>{item.title}</div>
<div style={{ fontSize: 12, color: '#8c8c8c' }}>{item.desc}</div>
</Card>
</Col>
))}
......@@ -182,10 +183,10 @@ const AdminCompliancePage: React.FC = () => {
key: 'reports',
label: <span><CloudUploadOutlined /> 监管上报</span>,
children: (
<Card size="small">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-gray-400">管理合规报告,支持下载和上报</span>
<Button type="primary" size="small">生成新报告</Button>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<span style={{ fontSize: 13, color: '#8c8c8c' }}>管理合规报告,支持下载和上报</span>
<Button type="primary">生成新报告</Button>
</div>
<Table columns={reportColumns} dataSource={reports} rowKey="id" size="small" pagination={false} />
</Card>
......
......@@ -136,19 +136,21 @@ const AdminConsultationsPage: React.FC = () => {
];
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-gray-800 m-0">问诊管理</h4>
<span className="text-xs text-gray-400">共 {total} 条记录</span>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>问诊管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>查看和管理平台所有问诊记录</div>
</div>
<span style={{ fontSize: 13, color: '#8c8c8c' }}>共 {total} 条记录</span>
</div>
<Card size="small">
<div className="flex flex-wrap items-center gap-2">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
<Input
placeholder="搜索患者/医生"
prefix={<SearchOutlined />}
style={{ width: 160 }}
size="small"
style={{ width: 180, borderRadius: 8 }}
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onPressEnter={handleSearch}
......@@ -156,7 +158,6 @@ const AdminConsultationsPage: React.FC = () => {
<Select
placeholder="类型"
style={{ width: 100 }}
size="small"
allowClear
value={typeFilter}
onChange={setTypeFilter}
......@@ -167,19 +168,18 @@ const AdminConsultationsPage: React.FC = () => {
/>
<Select
placeholder="状态"
style={{ width: 100 }}
size="small"
style={{ width: 110 }}
allowClear
value={statusFilter}
onChange={setStatusFilter}
options={Object.entries(statusMap).map(([value, { text }]) => ({ label: text, value }))}
/>
<RangePicker size="small" value={dateRange} onChange={(dates) => setDateRange(dates)} />
<Button size="small" type="primary" icon={<SearchOutlined />} onClick={handleSearch}>搜索</Button>
<RangePicker value={dateRange} onChange={(dates) => setDateRange(dates)} />
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>搜索</Button>
</div>
</Card>
<Card size="small">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Table
columns={columns}
dataSource={data}
......
This diff is collapsed.
......@@ -114,13 +114,16 @@ const AdminDepartmentsPage: React.FC = () => {
];
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-gray-800 m-0">科室管理</h4>
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={handleAdd}>添加科室</Button>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>科室管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理医院科室分类与排序</div>
</div>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>添加科室</Button>
</div>
<Card size="small">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Table columns={columns} dataSource={departments} rowKey="id" loading={loading} size="small" pagination={false} />
</Card>
......
......@@ -233,29 +233,25 @@ const AdminDoctorsPage: React.FC = () => {
];
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-gray-800 m-0">医生管理</h4>
<Space>
<span className="text-xs text-gray-400">共 {total} 位医生</span>
<Button type="primary" size="small" icon={<PlusOutlined />} onClick={() => setAddModalVisible(true)}>
添加医生
</Button>
</Space>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>医生管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理平台注册医生,审核资质认证</div>
</div>
<Card size="small">
<div className="flex flex-wrap items-center gap-2">
<Input placeholder="搜索姓名/手机号" prefix={<SearchOutlined />} style={{ width: 160 }} size="small"
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
<Input placeholder="搜索姓名/手机号" prefix={<SearchOutlined />} style={{ width: 180, borderRadius: 8 }}
value={keyword} onChange={(e) => setKeyword(e.target.value)} onPressEnter={handleSearch} />
<Select placeholder="认证状态" style={{ width: 120 }} size="small" allowClear value={statusFilter} onChange={setStatusFilter}
<Select placeholder="认证状态" style={{ width: 130 }} allowClear value={statusFilter} onChange={setStatusFilter}
options={Object.entries(statusMap).map(([k, v]) => ({ label: v.text, value: k }))} />
<Button size="small" type="primary" icon={<SearchOutlined />} onClick={handleSearch}>搜索</Button>
<Button size="small" icon={<ReloadOutlined />} onClick={() => { setKeyword(''); setStatusFilter(undefined); setPage(1); }}>重置</Button>
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>搜索</Button>
<Button icon={<ReloadOutlined />} onClick={() => { setKeyword(''); setStatusFilter(undefined); setPage(1); }}>重置</Button>
<Button type="primary" icon={<PlusOutlined />} style={{ marginLeft: 'auto' }} onClick={() => setAddModalVisible(true)}>添加医生</Button>
</div>
</Card>
<Card size="small">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Table columns={columns} dataSource={doctors} rowKey="id" loading={loading} size="small"
pagination={{
current: page, pageSize, total, showSizeChanger: true, size: 'small',
......
......@@ -197,28 +197,31 @@ const AdminPatientsPage: React.FC = () => {
];
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-gray-800 m-0">患者管理</h4>
<span className="text-xs text-gray-400">共 {total} 条记录</span>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>患者管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理平台注册患者信息与账号状态</div>
</div>
<span style={{ fontSize: 13, color: '#8c8c8c' }}>共 {total} 条记录</span>
</div>
<Card size="small">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
<Input placeholder="搜索姓名/手机号" prefix={<SearchOutlined />} value={keyword}
onChange={(e) => setKeyword(e.target.value)} onPressEnter={handleSearch} style={{ width: 180 }} size="small" />
<Select placeholder="状态" allowClear size="small" value={statusFilter || undefined}
onChange={(v) => setStatusFilter(v || '')} style={{ width: 100 }}
onChange={(e) => setKeyword(e.target.value)} onPressEnter={handleSearch} style={{ width: 200, borderRadius: 8 }} />
<Select placeholder="状态" allowClear value={statusFilter || undefined}
onChange={(v) => setStatusFilter(v || '')} style={{ width: 110 }}
options={[{ label: '正常', value: 'active' }, { label: '已禁用', value: 'disabled' }]} />
<Button size="small" type="primary" icon={<SearchOutlined />} onClick={handleSearch}>搜索</Button>
<Button size="small" icon={<ReloadOutlined />} onClick={() => { setKeyword(''); setStatusFilter(''); setPage(1); }}>重置</Button>
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>搜索</Button>
<Button icon={<ReloadOutlined />} onClick={() => { setKeyword(''); setStatusFilter(''); setPage(1); }}>重置</Button>
</div>
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={handleAdd}>添加患者</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>添加患者</Button>
</div>
</Card>
<Card size="small">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Table columns={columns} dataSource={patients} rowKey="id" loading={loading} size="small"
pagination={{
current: page, pageSize, total, showTotal: (t) => `共 ${t} 条`, size: 'small',
......
......@@ -144,29 +144,30 @@ const AdminPharmacyPage: React.FC = () => {
];
return (
<div className="space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">
<MedicineBoxOutlined className="mr-1" />药品库管理
</h4>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>药品库管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理平台药品信息与库存状态</div>
</div>
<Row gutter={[8, 8]}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #1890ff' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #1890ff' }}>
<Statistic title="药品总数" value={stats.total_count} suffix="种" />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #52c41a' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #52c41a' }}>
<Statistic title="库存充足" value={stats.normal_count} valueStyle={{ color: '#52c41a' }} suffix="种" />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #fa8c16' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #fa8c16' }}>
<Statistic title="库存预警" value={stats.low_stock_count} valueStyle={{ color: '#fa8c16' }} suffix="种" />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #f5222d' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #f5222d' }}>
<Statistic title="缺货药品" value={stats.out_of_stock_count} valueStyle={{ color: '#f5222d' }} suffix="种" />
</Card>
</Col>
......@@ -174,29 +175,29 @@ const AdminPharmacyPage: React.FC = () => {
{(stats.low_stock_count > 0 || stats.out_of_stock_count > 0) && (
<Alert message={`当前有 ${stats.low_stock_count} 种药品库存不足,${stats.out_of_stock_count} 种药品缺货,请及时补货`}
type="warning" showIcon className="text-xs!" />
type="warning" showIcon style={{ borderRadius: 8 }} />
)}
<Tabs defaultActiveKey="drugs" size="small" items={[
<Tabs defaultActiveKey="drugs" items={[
{
key: 'drugs',
label: <span><MedicineBoxOutlined /> 药品信息</span>,
children: (
<Card size="small">
<div className="flex flex-wrap items-center justify-between gap-2 mb-2">
<div className="flex flex-wrap items-center gap-2">
<Input prefix={<SearchOutlined />} placeholder="搜索药品" style={{ width: 180 }} size="small" allowClear
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginBottom: 12 }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
<Input prefix={<SearchOutlined />} placeholder="搜索药品" style={{ width: 200, borderRadius: 8 }} allowClear
value={keyword} onChange={(e) => setKeyword(e.target.value)} onPressEnter={() => { setPage(1); fetchList(); }} />
<Select placeholder="类别" style={{ width: 100 }} size="small" allowClear options={categoryOptions}
<Select placeholder="类别" style={{ width: 110 }} allowClear options={categoryOptions}
value={filterCategory || undefined} onChange={(v) => { setFilterCategory(v || ''); setPage(1); }} />
<Select placeholder="库存" style={{ width: 100 }} size="small" allowClear
<Select placeholder="库存" style={{ width: 110 }} allowClear
value={filterStatus || undefined} onChange={(v) => { setFilterStatus(v || ''); setPage(1); }}
options={[{ label: '充足', value: 'normal' }, { label: '预警', value: 'low_stock' }, { label: '缺货', value: 'out_of_stock' }]} />
</div>
<Button type="primary" size="small" icon={<PlusOutlined />} onClick={handleAddDrug}>添加药品</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddDrug}>添加药品</Button>
</div>
<Table columns={drugColumns} dataSource={drugs} rowKey="id" loading={loading} size="small"
pagination={{ current: page, total, pageSize: 10, size: 'small', onChange: setPage, showTotal: (t) => `共 ${t} 种` }} />
pagination={{ current: page, total, pageSize: 10, onChange: setPage, showTotal: (t) => `共 ${t} 种` }} />
</Card>
),
},
......@@ -204,10 +205,10 @@ const AdminPharmacyPage: React.FC = () => {
key: 'stock',
label: <Badge count={stats.low_stock_count + stats.out_of_stock_count} offset={[10, 0]}>库存预警</Badge>,
children: (
<Card size="small">
<Table columns={drugColumns} size="small"
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Table columns={drugColumns}
dataSource={drugs.filter((d) => d.status !== 'normal')}
rowKey="id" pagination={false} loading={loading} />
rowKey="id" pagination={false} loading={loading} size="small" />
</Card>
),
},
......
......@@ -124,50 +124,48 @@ const AdminPrescriptionPage: React.FC = () => {
];
return (
<div className="space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">
<FileTextOutlined className="mr-1" />处方监管
</h4>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>处方监管</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>监督平台处方合规性,审查异常用药</div>
</div>
<Row gutter={[8, 8]}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #1890ff' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #1890ff' }}>
<Statistic title="处方总数" value={stats.total_count} suffix="张" />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #52c41a' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #52c41a' }}>
<Statistic title="合规" value={stats.normal_count} valueStyle={{ color: '#52c41a' }} suffix="张" />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #fa8c16' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #fa8c16' }}>
<Statistic title="预警" value={stats.warning_count} valueStyle={{ color: '#fa8c16' }} suffix="张" />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #f5222d' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #f5222d' }}>
<Statistic title="异常" value={stats.rejected_count} valueStyle={{ color: '#f5222d' }} suffix="张" />
</Card>
</Col>
</Row>
<Tabs defaultActiveKey="all" size="small" onChange={(key) => {
if (key === 'warning') { setWarningLevel(''); fetchList('warning'); }
else { setWarningLevel(''); fetchList(''); }
}} items={[
<Tabs defaultActiveKey="all" items={[
{
key: 'all', label: '全部处方',
children: (
<Card size="small">
<div className="flex flex-wrap items-center gap-2 mb-2">
<Input prefix={<SearchOutlined />} placeholder="搜索处方/患者/医生" style={{ width: 180 }} size="small" allowClear
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<Input prefix={<SearchOutlined />} placeholder="搜索处方/患者/医生" style={{ width: 200, borderRadius: 8 }} allowClear
value={keyword} onChange={(e) => setKeyword(e.target.value)} onPressEnter={() => { setPage(1); fetchList(); }} />
<RangePicker size="small" />
<Button type="primary" size="small" ghost onClick={() => { setPage(1); fetchList(); }}>查询</Button>
<RangePicker />
<Button type="primary" ghost onClick={() => { setPage(1); fetchList(); }}>查询</Button>
</div>
<Table columns={columns} dataSource={prescriptions} rowKey="id" loading={loading} size="small"
pagination={{ current: page, total, pageSize: 10, size: 'small', onChange: setPage, showTotal: (t) => `共 ${t} 张` }} />
pagination={{ current: page, total, pageSize: 10, onChange: setPage, showTotal: (t) => `共 ${t} 张` }} />
</Card>
),
},
......@@ -175,13 +173,13 @@ const AdminPrescriptionPage: React.FC = () => {
key: 'warning',
label: <Badge count={stats.warning_count + stats.rejected_count} offset={[10, 0]}>异常预警</Badge>,
children: (
<Card style={{ borderRadius: 12 }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Alert message="以下处方存在合规风险,请及时审查处理" type="warning" showIcon style={{ marginBottom: 16 }} />
<List dataSource={prescriptions.filter((d) => d.warning_level !== 'normal')} renderItem={(item) => (
<Card style={{ marginBottom: 12, borderRadius: 8, borderLeft: `4px solid ${item.warning_level === 'rejected' ? '#f5222d' : '#fa8c16'}` }} hoverable>
<Row align="middle">
<Col flex="auto">
<Space><Text strong>{item.prescription_no}</Text>{getWarningTag(item.warning_level)}</Space><br />
<Space><Text strong>{item.prescription_no}</Text>{getWarningTag(item.warning_level)}</Space><br />
<Text type="secondary">{item.doctor_name} → {item.patient_name} | {item.diagnosis}</Text><br />
{item.warning_reason && <Text type="danger"><ExclamationCircleOutlined style={{ marginRight: 4 }} />{item.warning_reason}</Text>}
</Col>
......
......@@ -88,39 +88,40 @@ const DoctorCertificationPage: React.FC = () => {
if (currentStep === 1) {
return (
<div className="max-w-[500px] mx-auto mt-20 text-center">
<Card size="small">
<div className="py-6">
<div className="w-14 h-14 rounded-full bg-green-50 flex items-center justify-center mx-auto mb-4">
<CheckCircleOutlined className="text-2xl text-green-500" />
</div>
<h3 className="text-base font-bold mb-2">认证申请已提交</h3>
<Paragraph type="secondary" className="text-xs!">
您的资质认证申请已成功提交,管理员将在 1-3 个工作日内完成审核。 </Paragraph>
<Button type="primary" size="small" onClick={() => router.push('/doctor/workbench')}>返回工作台</Button>
<div style={{ padding: '20px 24px' }}>
<Card style={{ maxWidth: 480, margin: '60px auto', textAlign: 'center', borderRadius: 12, border: '1px solid #edf2fc', padding: '40px 20px' }}>
<div style={{ width: 64, height: 64, borderRadius: '50%', background: '#f6ffed', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 16px', fontSize: 32, color: '#52c41a' }}>
<CheckCircleOutlined />
</div>
<h3 style={{ fontSize: 18, fontWeight: 700, marginBottom: 8, color: '#1d2129' }}>认证申请已提交</h3>
<Paragraph type="secondary" style={{ fontSize: 13 }}>
您的资质认证申请已成功提交,管理员将在 1-3 个工作日内完成审核。
</Paragraph>
<Button type="primary" onClick={() => router.push('/doctor/workbench')} style={{ borderRadius: 8 }}>返回工作台</Button>
</Card>
</div>
);
}
return (
<div className="max-w-[700px] mx-auto pb-6 space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">医生资质认证</h4>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>医生资质认证</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>提交执业资质,通过审核后即可开始接诊</div>
</div>
<Steps
current={currentStep}
size="small"
className="mb-4"
items={[
{ title: '填写资质信息' },
{ title: '等待审核' },
{ title: '审核通过' },
]}
style={{ maxWidth: 600 }}
/>
<Card size="small">
<Form form={form} layout="vertical" size="small" onFinish={handleSubmit}>
<Card style={{ maxWidth: 680, borderRadius: 12, border: '1px solid #edf2fc' }}>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item name="license_no" label="执业证号" rules={[{ required: true, message: '请输入执业证书号' }]}>
<Input placeholder="请输入医师执业证书号" />
</Form.Item>
......@@ -184,3 +185,4 @@ const DoctorCertificationPage: React.FC = () => {
export default DoctorCertificationPage;
......@@ -158,33 +158,33 @@ const ChronicReviewPage: React.FC = () => {
const pendingCount = data.filter((d) => d.status === 'pending').length;
return (
<div className="space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">
<MedicineBoxOutlined className="mr-1" />慢病续方审核
</h4>
<Tabs defaultActiveKey="pending" size="small" items={[
{
key: 'pending',
label: <Badge count={pendingCount} offset={[10, 0]}>待审</Badge>,
children: (
<Card size="small">
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>慢病续方审核</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>审核患者的慢病续方申请,AI 已预生成处方草稿</div>
</div>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Tabs defaultActiveKey="pending" items={[
{
key: 'pending',
label: <Badge count={pendingCount} offset={[10, 0]}>待审核</Badge>,
children: (
<Table columns={columns} dataSource={data.filter((d) => d.status === 'pending')}
rowKey="id" size="small" pagination={false} />
</Card>
),
},
{
key: 'all',
label: '全部记录',
children: (
<Card size="small">
<Table columns={columns} dataSource={data} rowKey="id" size="small"
pagination={{ pageSize: 10, size: 'small' }} />
</Card>
),
},
]} />
rowKey="id" pagination={false}
locale={{ emptyText: <div style={{ padding: '40px 0', textAlign: 'center', color: '#bfbfbf' }}>暂无待审核续方</div> }} />
),
},
{
key: 'all',
label: '全部记录',
children: (
<Table columns={columns} dataSource={data} rowKey="id"
pagination={{ pageSize: 10 }} />
),
},
]} />
</Card>
<Modal
title="续方详情审核"
......
......@@ -114,42 +114,46 @@ const IncomePage: React.FC = () => {
return (
<Spin spinning={loading}>
<div className="space-y-3">
<h4 className="text-sm font-bold text-gray-800 m-0">
<DollarOutlined className="mr-1" />
收入统计
</h4>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>收入统计</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>查看您的问诊收入与提现记录</div>
</div>
</div>
<Row gutter={[8, 8]}>
<Row gutter={[16, 16]}>
<Col span={8}>
<div className="rounded-lg p-3 text-white" style={{ background: 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)' }}>
<div className="text-xs text-white/80 mb-1">可提现余额</div>
<div className="text-2xl font-bold">¥{(stats.total_balance / 100).toFixed(2)}</div>
<Button ghost size="small" className="mt-2 text-white! border-white/60!" icon={<WalletOutlined />}
onClick={() => setWithdrawModalVisible(true)}>申请提现</Button>
<div style={{ borderRadius: 12, padding: '20px 24px', background: 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: -30, right: -30, width: 100, height: 100, borderRadius: '50%', background: 'rgba(255,255,255,0.08)' }} />
<div style={{ fontSize: 13, color: 'rgba(255,255,255,0.8)', marginBottom: 6 }}>可提现余额</div>
<div style={{ fontSize: 28, fontWeight: 700, color: '#fff', marginBottom: 12 }}>¥{(stats.total_balance / 100).toFixed(2)}</div>
<Button ghost icon={<WalletOutlined />} onClick={() => setWithdrawModalVisible(true)}
style={{ borderColor: 'rgba(255,255,255,0.6)', color: '#fff', borderRadius: 8 }}>申请提现</Button>
</div>
</Col>
<Col span={8}>
<div className="bg-white rounded-lg p-3 border border-[#e6f0fa]">
<div className="text-xs text-gray-500 mb-1">本月收入</div>
<div className="text-xl font-bold text-green-500">¥{(stats.month_income / 100).toFixed(2)}</div>
<div className="mt-1 text-[11px] text-gray-400">
<RiseOutlined className="text-green-500 mr-0.5" />本月累计
<div style={{ background: '#fff', borderRadius: 12, padding: '20px 24px', border: '1px solid #edf2fc', borderLeft: '4px solid #52c41a' }}>
<div style={{ fontSize: 13, color: '#8c8c8c', marginBottom: 6 }}>本月收入</div>
<div style={{ fontSize: 26, fontWeight: 700, color: '#52c41a', marginBottom: 6 }}>¥{(stats.month_income / 100).toFixed(2)}</div>
<div style={{ fontSize: 12, color: '#bfbfbf', display: 'flex', alignItems: 'center', gap: 4 }}>
<RiseOutlined style={{ color: '#52c41a' }} />本月累计
</div>
</div>
</Col>
<Col span={8}>
<div className="bg-white rounded-lg p-3 border border-[#e6f0fa]">
<div className="text-xs text-gray-500 mb-1">本月问诊量</div>
<div className="text-xl font-bold text-[#1890ff]">{stats.month_consults} <span className="text-xs font-normal text-gray-400">次</span></div>
<div className="mt-1 text-[11px] text-gray-400">日均 {(stats.month_consults / Math.max(1, new Date().getDate())).toFixed(1)} 次</div>
<div style={{ background: '#fff', borderRadius: 12, padding: '20px 24px', border: '1px solid #edf2fc', borderLeft: '4px solid #1890ff' }}>
<div style={{ fontSize: 13, color: '#8c8c8c', marginBottom: 6 }}>本月问诊量</div>
<div style={{ fontSize: 26, fontWeight: 700, color: '#1890ff', marginBottom: 6 }}>
{stats.month_consults} <span style={{ fontSize: 14, color: '#bfbfbf', fontWeight: 400 }}>次</span>
</div>
<div style={{ fontSize: 12, color: '#bfbfbf' }}>日均 {(stats.month_consults / Math.max(1, new Date().getDate())).toFixed(1)} 次</div>
</div>
</Col>
</Row>
<Tabs
defaultActiveKey="records"
size="small"
items={[
{
key: 'records',
......@@ -282,6 +286,7 @@ const IncomePage: React.FC = () => {
</div>
</Spin>
);
};
export default IncomePage;
......@@ -139,42 +139,57 @@ const PatientRecordPage: React.FC = () => {
];
return (
<div className="space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">
<UserOutlined className="mr-1" />患者档案
</h4>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>患者档案</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>查看您接诊过的患者健康记录与历史问诊</div>
</div>
<Row gutter={8}>
<Row gutter={16} align="stretch">
<Col span={8}>
<Card title={<span className="text-xs font-semibold">患者列表</span>} size="small"
extra={<Text type="secondary" className="text-[11px]!">{total} 人</Text>}>
<Input prefix={<SearchOutlined />} placeholder="搜索姓名/手机" size="small"
<Card
title={<span style={{ fontSize: 14, fontWeight: 600 }}>患者列表</span>}
extra={<Text type="secondary" style={{ fontSize: 13 }}>{total} 人</Text>}
style={{ borderRadius: 12, border: '1px solid #edf2fc', height: '100%' }}
>
<Input prefix={<SearchOutlined />} placeholder="搜索姓名/手机"
value={searchKeyword} onChange={(e) => setSearchKeyword(e.target.value)}
onPressEnter={(e) => handleSearch((e.target as HTMLInputElement).value)}
onBlur={(e) => handleSearch(e.target.value)} allowClear onClear={() => handleSearch('')}
className="mb-2" />
style={{ marginBottom: 12, borderRadius: 8 }} />
<Spin spinning={listLoading}>
<List size="small" dataSource={patients}
<List dataSource={patients}
locale={{ emptyText: <Empty description="暂无患者" /> }}
pagination={total > 20 ? { current: page, pageSize: 20, total, size: 'small',
onChange: (p) => { setPage(p); fetchPatients(searchKeyword, p); } } : false}
renderItem={(patient) => (
<List.Item style={{ cursor: 'pointer', padding: '6px 8px', borderRadius: 6,
background: selectedPatient?.id === patient.id ? '#f6ffed' : 'transparent',
border: selectedPatient?.id === patient.id ? '1px solid #b7eb8f' : '1px solid transparent',
marginBottom: 2 }}
onClick={() => handleSelectPatient(patient.id)}>
<List.Item
style={{
cursor: 'pointer', padding: '10px 12px', borderRadius: 8,
background: selectedPatient?.id === patient.id ? '#f6ffed' : 'transparent',
border: `1px solid ${selectedPatient?.id === patient.id ? '#b7eb8f' : 'transparent'}`,
marginBottom: 4, transition: 'all 0.15s',
}}
onClick={() => handleSelectPatient(patient.id)}
>
<List.Item.Meta
avatar={patient.avatar ? <Avatar src={patient.avatar} size={32} /> :
<div className="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center text-white font-bold text-xs">{patient.name.charAt(0)}</div>}
title={<span className="text-xs"><Text strong>{patient.name}</Text> <Text type="secondary">{genderLabel(patient.gender)}/{patient.age}岁</Text></span>}
avatar={patient.avatar
? <Avatar src={patient.avatar} size={36} />
: <Avatar size={36} style={{ backgroundColor: '#52c41a', fontWeight: 700 }}>{patient.name.charAt(0)}</Avatar>
}
title={
<div style={{ fontSize: 13 }}>
<Text strong>{patient.name}</Text>
<Text type="secondary" style={{ marginLeft: 6 }}>{genderLabel(patient.gender)}/{patient.age}岁</Text>
</div>
}
description={
<div className="text-[11px]">
<div className="text-gray-400"><PhoneOutlined className="mr-0.5" />{patient.phone}</div>
<div style={{ fontSize: 12 }}>
<div style={{ color: '#8c8c8c' }}><PhoneOutlined style={{ marginRight: 4 }} />{patient.phone}</div>
{patient.allergy_history && patient.allergy_history !== '无' && (
<Tag color="red" className="text-[10px]!"><SafetyOutlined /> {patient.allergy_history}</Tag>
<Tag color="red" style={{ fontSize: 11, marginTop: 2 }}><SafetyOutlined /> {patient.allergy_history}</Tag>
)}
<div className="text-gray-400">{patient.consultation_count}次问诊{patient.last_consult_at && ` · ${patient.last_consult_at}`}</div>
<div style={{ color: '#bfbfbf', marginTop: 2 }}>{patient.consultation_count}次问诊{patient.last_consult_at && ` · ${patient.last_consult_at}`}</div>
</div>
}
/>
......@@ -188,9 +203,9 @@ const PatientRecordPage: React.FC = () => {
<Col span={16}>
<Spin spinning={detailLoading}>
{selectedPatient ? (
<div className="space-y-2">
<Card size="small">
<Descriptions title={<span className="text-xs font-semibold">基本信息</span>} bordered size="small" column={3}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Descriptions title={<span style={{ fontSize: 14, fontWeight: 600 }}>基本信息</span>} bordered size="small" column={3}>
<Descriptions.Item label="姓名">{selectedPatient.name}</Descriptions.Item>
<Descriptions.Item label="性别">{genderLabel(selectedPatient.gender)}</Descriptions.Item>
<Descriptions.Item label="年龄">{selectedPatient.age}岁</Descriptions.Item>
......@@ -206,26 +221,26 @@ const PatientRecordPage: React.FC = () => {
</Descriptions>
</Card>
<Card size="small">
<Tabs defaultActiveKey="consult" size="small" items={[
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Tabs defaultActiveKey="consult" items={[
{
key: 'consult',
label: <span><FileTextOutlined /> 问诊({selectedPatient.consults.length})</span>,
children: <Table<PatientConsultRecord> dataSource={selectedPatient.consults} rowKey="id" size="small" pagination={false} columns={consultColumns} locale={{ emptyText: '暂无' }} />,
label: <span><FileTextOutlined /> 问诊记录({selectedPatient.consults.length})</span>,
children: <Table<PatientConsultRecord> dataSource={selectedPatient.consults} rowKey="id" pagination={false} columns={consultColumns} locale={{ emptyText: '暂无问诊记录' }} />,
},
{
key: 'prescription',
label: <span><MedicineBoxOutlined /> 处方({selectedPatient.prescriptions.length})</span>,
children: <Table<PatientPrescriptionRecord> dataSource={selectedPatient.prescriptions} rowKey="id" size="small" pagination={false} columns={prescriptionColumns} locale={{ emptyText: '暂无' }} />,
label: <span><MedicineBoxOutlined /> 处方记录({selectedPatient.prescriptions.length})</span>,
children: <Table<PatientPrescriptionRecord> dataSource={selectedPatient.prescriptions} rowKey="id" pagination={false} columns={prescriptionColumns} locale={{ emptyText: '暂无处方记录' }} />,
},
{
key: 'health',
label: <span><LineChartOutlined /> 趋势</span>,
label: <span><LineChartOutlined /> 健康趋势</span>,
children: (
<div className="text-center py-6">
<HeartOutlined className="text-3xl text-red-400 mb-2" />
<div className="text-xs font-semibold mb-1">健康趋势图表</div>
<Text type="secondary" className="text-xs!">功能开发中...</Text>
<div style={{ textAlign: 'center', padding: '48px 0' }}>
<HeartOutlined style={{ fontSize: 40, color: '#ffadd2', marginBottom: 12 }} />
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 6, color: '#1d2129' }}>健康趋势图表</div>
<Text type="secondary">功能开发中...</Text>
</div>
),
},
......@@ -233,7 +248,7 @@ const PatientRecordPage: React.FC = () => {
</Card>
</div>
) : (
<Card size="small" className="text-center py-10">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', textAlign: 'center', padding: '60px 0' }}>
<Empty description="请从左侧选择患者查看档案" />
</Card>
)}
......
'use client';
'use client';
import React, { useState, useEffect } from 'react';
import { Card, Avatar, Button, Descriptions, Tag, Typography, Row, Col, Divider, Switch, Space, Spin, message } from 'antd';
import {
......@@ -63,70 +63,83 @@ const DoctorProfilePage: React.FC = () => {
if (!profile) return null;
return (
<div className="max-w-[800px] mx-auto space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">个人信息</h4>
<Card size="small">
<Row gutter={12} align="middle">
<Col>
<Avatar size={64} src={profile.avatar} icon={<UserOutlined />} />
</Col>
<Col flex="auto">
<div className="flex items-center gap-2 mb-0.5">
<span className="text-base font-bold">{profile.name}</span>
<Tag color="blue">{profile.title}</Tag>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Hero 卡片 */}
<div style={{
borderRadius: 12, overflow: 'hidden',
background: 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)',
padding: '28px 32px', position: 'relative',
}}>
<div style={{ position: 'absolute', top: -40, right: -40, width: 160, height: 160, borderRadius: '50%', background: 'rgba(255,255,255,0.06)' }} />
<div style={{ position: 'relative', zIndex: 1, display: 'flex', alignItems: 'center', gap: 20 }}>
<Avatar size={72} src={profile.avatar || null} icon={<UserOutlined />}
style={{ backgroundColor: 'rgba(255,255,255,0.2)', color: '#fff', fontSize: 32, flexShrink: 0 }} />
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
<h2 style={{ fontSize: 20, fontWeight: 700, margin: 0, color: '#fff' }}>{profile.name}</h2>
<Tag color="blue" style={{ borderRadius: 20 }}>{profile.title}</Tag>
</div>
<div className="text-xs text-gray-500 mb-1">{profile.hospital} · {profile.department_name}</div>
<Space size={4}>
<Tag icon={<SafetyCertificateOutlined />} color="success">已认证</Tag>
<div style={{ fontSize: 13, color: 'rgba(255,255,255,0.8)', marginBottom: 10 }}>
{profile.hospital} · {profile.department_name}
</div>
<Space size={6}>
<Tag icon={<SafetyCertificateOutlined />} color="success" style={{ borderRadius: 20 }}>已认证</Tag>
</Space>
</Col>
<Col>
<Button type="primary" size="small" icon={<EditOutlined />}>编辑资料</Button>
</Col>
</Row>
</Card>
</div>
<Button icon={<EditOutlined />} style={{ borderRadius: 8, background: 'rgba(255,255,255,0.15)', border: 'none', color: '#fff' }}>
编辑资料
</Button>
</div>
</div>
<Card title={<span className="text-xs font-semibold">详细信息</span>} size="small">
<Descriptions column={2} size="small">
<Descriptions.Item label="姓名">{profile.name}</Descriptions.Item>
<Descriptions.Item label="职称">{profile.title}</Descriptions.Item>
<Descriptions.Item label="医院">{profile.hospital}</Descriptions.Item>
<Descriptions.Item label="科室">{profile.department_name}</Descriptions.Item>
<Descriptions.Item label="价格">¥{(profile.price / 100).toFixed(0)}/次</Descriptions.Item>
</Descriptions>
</Card>
<Row gutter={[16, 16]}>
<Col xs={24} md={12}>
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>详细信息</span>} style={{ height: '100%', borderRadius: 12, border: '1px solid #edf2fc' }}>
<Descriptions column={2} size="small">
<Descriptions.Item label="姓名">{profile.name}</Descriptions.Item>
<Descriptions.Item label="职称">{profile.title}</Descriptions.Item>
<Descriptions.Item label="医院">{profile.hospital}</Descriptions.Item>
<Descriptions.Item label="科室">{profile.department_name}</Descriptions.Item>
<Descriptions.Item label="问诊价格">
<span style={{ fontSize: 16, fontWeight: 700, color: '#52c41a' }}>¥{(profile.price / 100).toFixed(0)}</span>
<span style={{ fontSize: 12, color: '#8c8c8c' }}>/次</span>
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
<Col xs={24} md={12}>
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>接诊设置</span>} style={{ height: '100%', borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', background: '#f8faff', borderRadius: 10, border: '1px solid #edf2fc' }}>
<div>
<div style={{ fontSize: 13, fontWeight: 600, color: '#1d2129' }}>AI 辅助接诊</div>
<div style={{ fontSize: 12, color: '#8c8c8c', marginTop: 2 }}>开启后 AI 将协助分析患者症状</div>
</div>
<Switch checked={profile.ai_assist_enabled} onChange={handleToggleAI} />
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', background: '#f8faff', borderRadius: 10, border: '1px solid #edf2fc' }}>
<div>
<div style={{ fontSize: 13, fontWeight: 600, color: '#1d2129' }}>自动接诊</div>
<div style={{ fontSize: 12, color: '#8c8c8c', marginTop: 2 }}>开启后图文问诊将自动接诊</div>
</div>
<Switch />
</div>
</div>
</Card>
</Col>
</Row>
<Card title={<span className="text-xs font-semibold">擅长领域</span>} size="small">
<Space wrap size={4}>
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>擅长领域</span>} style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Space wrap size={8}>
{profile.specialties.map((s, i) => (
<Tag key={i} color="processing">{s}</Tag>
<Tag key={i} color="processing" style={{ borderRadius: 20, padding: '4px 12px', fontSize: 13 }}>{s}</Tag>
))}
</Space>
</Card>
<Card title={<span className="text-xs font-semibold">个人简介</span>} size="small">
<Text className="text-xs!">{profile.introduction}</Text>
</Card>
<Card title={<span className="text-xs font-semibold">接诊设置</span>} size="small">
<div className="space-y-2">
<div className="flex items-center justify-between">
<div>
<Text strong className="text-xs!">AI 辅助接诊</Text>
<div className="text-[11px] text-gray-400">开启后 AI 将协助分析患者症状</div>
</div>
<Switch size="small" checked={profile.ai_assist_enabled} onChange={handleToggleAI} />
</div>
<Divider className="my-0!" />
<div className="flex items-center justify-between">
<div>
<Text strong className="text-xs!">自动接诊</Text>
<div className="text-[11px] text-gray-400">开启后图文问诊将自动接诊</div>
</div>
<Switch size="small" />
</div>
</div>
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>个人简介</span>} style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Text style={{ fontSize: 14, color: '#595959', lineHeight: 1.7 }}>{profile.introduction}</Text>
</Card>
</div>
);
......
'use client';
'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 { PlusOutlined, DeleteOutlined, CalendarOutlined } from '@ant-design/icons';
......@@ -94,16 +94,19 @@ const DoctorSchedulePage: React.FC = () => {
};
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-gray-800 m-0">排班管理</h4>
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={handleAddSchedule}>添加排班</Button>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>排班管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理您的接诊时间段安排</div>
</div>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddSchedule} style={{ borderRadius: 8 }}>添加排班</Button>
</div>
<Spin spinning={loading}>
<Row gutter={12}>
<Row gutter={16}>
<Col span={16}>
<Card size="small">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Calendar
fullscreen={false}
value={selectedDate}
......@@ -115,29 +118,34 @@ const DoctorSchedulePage: React.FC = () => {
<Col span={8}>
<Card
title={<span className="text-xs font-semibold"><CalendarOutlined className="mr-1" />{selectedDate.format('MM月DD日')} 排班</span>}
extra={<Button type="link" size="small" icon={<PlusOutlined />} onClick={handleAddSchedule}>添加</Button>}
size="small"
title={
<span style={{ fontSize: 14, fontWeight: 600 }}>
<CalendarOutlined style={{ marginRight: 6, color: '#1890ff' }} />
{selectedDate.format('MM月DD日')} 排班
</span>
}
extra={<Button type="link" icon={<PlusOutlined />} onClick={handleAddSchedule}>添加</Button>}
style={{ borderRadius: 12, border: '1px solid #edf2fc' }}
>
{todaySchedule.length === 0 ? (
<div className="text-center py-6">
<Text type="secondary" className="text-xs!">当日暂无排班</Text>
<div style={{ textAlign: 'center', padding: '32px 0' }}>
<Text type="secondary" style={{ fontSize: 13 }}>当日暂无排班</Text>
<br />
<Button type="link" size="small" icon={<PlusOutlined />} onClick={handleAddSchedule} className="mt-1">添加排班</Button>
<Button type="link" icon={<PlusOutlined />} onClick={handleAddSchedule} style={{ marginTop: 8 }}>添加排班</Button>
</div>
) : (
<List
size="small"
dataSource={todaySchedule}
renderItem={(slot) => (
<List.Item
style={{ padding: '12px 0' }}
actions={[
<Button type="text" danger size="small" icon={<DeleteOutlined />} onClick={() => handleDeleteSlot(slot.id!)} />,
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => handleDeleteSlot(slot.id!)} />,
]}
>
<div>
<div className="mb-1">
<Text strong className="text-xs!">{slot.start_time} - {slot.end_time}</Text>
<div style={{ marginBottom: 6 }}>
<Text strong style={{ fontSize: 14 }}>{slot.start_time} - {slot.end_time}</Text>
</div>
<Space size={4}>
<Tag color="blue">最大 {slot.max_count} 人</Tag>
......
This diff is collapsed.
......@@ -519,12 +519,13 @@ const PatientChronicPage: React.FC = () => {
];
return (
<div className="space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">
<MedicineBoxOutlined className="mr-1" />慢病管理
</h4>
<Card size="small">
<Tabs items={tabItems} size="small" />
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>慢病管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理您的慢性病档案、续方申请、用药提醒和健康指标</div>
</div>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Tabs items={tabItems} />
</Card>
</div>
);
......
This diff is collapsed.
This diff is collapsed.
......@@ -366,10 +366,13 @@ const FamilyTab: React.FC = () => {
// ==================== 主页面 ====================
const PatientHealthRecordsPage: React.FC = () => (
<div className="space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">健康档案</h4>
<Card size="small">
<Tabs defaultActiveKey="profile" size="small" items={[
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>健康档案</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理您的健康信息、检验报告、家庭成员和健康趋势</div>
</div>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Tabs defaultActiveKey="profile" items={[
{ key: 'profile', label: <span><MedicineBoxOutlined /> 健康信息</span>, children: <HealthProfileTab /> },
{ key: 'reports', label: <span><FileTextOutlined /> 检验报告</span>, children: <LabReportsTab /> },
{ key: 'history', label: <span><MedicineBoxOutlined /> 就诊历史</span>, children: <ConsultHistoryTab /> },
......
This diff is collapsed.
This diff is collapsed.
......@@ -77,7 +77,7 @@ const PatientPrescriptionPage: React.FC = () => {
},
{
title: '金额', dataIndex: 'total_amount', key: 'total_amount', width: 100,
render: (v: number) => <Text strong style={{ color: '#f5222d' }}>¥{(v / 100).toFixed(2)}</Text>,
render: (v: number) => <Text strong style={{ color: '#1890ff' }}>¥{(v / 100).toFixed(2)}</Text>,
},
{ title: '状态', dataIndex: 'status', key: 'status', width: 90, render: (v: string) => getStatusTag(v) },
{
......@@ -94,28 +94,28 @@ const PatientPrescriptionPage: React.FC = () => {
if (!loading && prescriptions.length === 0) {
return (
<div className="max-w-[600px] mx-auto pt-10">
<Card size="small" className="text-center py-6">
<FileTextOutlined className="text-4xl text-[#1890ff] mb-2" />
<div className="text-sm font-bold mb-1">电子处方</div>
<Text type="secondary" className="text-xs!">暂无处方记录,完成问诊后医生会为您开具处方</Text>
<div className="mt-4">
<Button type="primary" size="small" onClick={() => router.push('/patient/doctors')}>去问诊</Button>
</div>
<div style={{ padding: '20px 24px' }}>
<Card style={{ maxWidth: 500, margin: '60px auto', textAlign: 'center', borderRadius: 12, border: '1px solid #edf2fc', padding: '40px 20px' }}>
<FileTextOutlined style={{ fontSize: 48, color: '#1890ff', marginBottom: 16 }} />
<div style={{ fontSize: 16, fontWeight: 600, marginBottom: 8, color: '#1d2129' }}>暂无处方记录</div>
<div style={{ fontSize: 13, color: '#8c8c8c', marginBottom: 20 }}>完成问诊后医生会为您开具处方</div>
<Button type="primary" onClick={() => router.push('/patient/doctors')} style={{ borderRadius: 8 }}>去问诊</Button>
</Card>
</div>
);
}
return (
<div className="space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">
<FileTextOutlined className="mr-1" />电子处方
</h4>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* 页面标题 */}
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>电子处方</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>查看医生为您开具的所有电子处方</div>
</div>
<Spin spinning={loading}>
<Card size="small">
<Table columns={columns} dataSource={prescriptions} rowKey="id" size="small"
pagination={{ current: page, total, pageSize: 10, size: 'small', onChange: setPage, showTotal: (t) => `共 ${t} 张` }} />
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Table columns={columns} dataSource={prescriptions} rowKey="id"
pagination={{ current: page, total, pageSize: 10, onChange: setPage, showTotal: (t) => `共 ${t} 张` }} />
</Card>
</Spin>
......@@ -165,7 +165,7 @@ const PatientPrescriptionPage: React.FC = () => {
))}
<Divider style={{ margin: '8px 0' }} />
<div style={{ textAlign: 'right' }}>
<Text strong style={{ fontSize: 16, color: '#f5222d' }}>合计:¥{(currentRx.total_amount / 100).toFixed(2)}</Text>
<Text strong style={{ fontSize: 16, color: '#1890ff' }}>合计:¥{(currentRx.total_amount / 100).toFixed(2)}</Text>
</div>
</Card>
{currentRx.remark && (
......
This diff is collapsed.
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