Commit 4ae56601 authored by yuguo's avatar yuguo

fix

parent f350fadd
...@@ -12,7 +12,9 @@ ...@@ -12,7 +12,9 @@
"Bash(node:*)", "Bash(node:*)",
"Bash(ls:*)", "Bash(ls:*)",
"Bash(npx tailwindcss:*)", "Bash(npx tailwindcss:*)",
"Bash(npx next:*)" "Bash(npx next:*)",
"Bash(PGPASSWORD=T5sSfTZ6XYTD9bfC psql:*)",
"Bash(npm install:*)"
] ]
} }
} }
File added
...@@ -5,12 +5,14 @@ server: ...@@ -5,12 +5,14 @@ server:
version: 1.0.0 version: 1.0.0
database: database:
host: localhost host: 10.10.0.102
port: 5432 port: 5432
user: postgres user: postgres
password: your_password password: T5sSfTZ6XYTD9bfC
dbname: internet_hospital dbname: xxxx
sslmode: disable sslmode: disable
schema: public
timezone: Asia/Shanghai
redis: redis:
host: localhost host: localhost
......
...@@ -35,6 +35,8 @@ type DatabaseConfig struct { ...@@ -35,6 +35,8 @@ type DatabaseConfig struct {
Password string `mapstructure:"password"` Password string `mapstructure:"password"`
DBName string `mapstructure:"dbname"` DBName string `mapstructure:"dbname"`
SSLMode string `mapstructure:"sslmode"` SSLMode string `mapstructure:"sslmode"`
Schema string `mapstructure:"schema"`
Timezone string `mapstructure:"timezone"`
} }
type RedisConfig struct { type RedisConfig struct {
......
...@@ -17,6 +17,12 @@ func InitPostgres(cfg *config.DatabaseConfig) (*gorm.DB, error) { ...@@ -17,6 +17,12 @@ func InitPostgres(cfg *config.DatabaseConfig) (*gorm.DB, error) {
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode, cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode,
) )
if cfg.Schema != "" {
dsn += fmt.Sprintf(" search_path=%s", cfg.Schema)
}
if cfg.Timezone != "" {
dsn += fmt.Sprintf(" TimeZone=%s", cfg.Timezone)
}
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info), Logger: logger.Default.LogMode(logger.Info),
......
...@@ -12,7 +12,7 @@ import ( ...@@ -12,7 +12,7 @@ import (
func main() { func main() {
// 数据库连接配置 // 数据库连接配置
dsn := "host=localhost port=5432 user=postgres password=123456 dbname=internet_hospital sslmode=disable" dsn := "host=10.10.0.102 port=5432 user=postgres password=T5sSfTZ6XYTD9bfC dbname=xxxx sslmode=disable search_path=public TimeZone=Asia/Shanghai"
// 连接数据库 // 连接数据库
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.6.1", "@ant-design/icons": "^5.6.1",
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@tanstack/react-query": "^5.80.6", "@tanstack/react-query": "^5.80.6",
"antd": "^5.25.3", "antd": "^5.25.3",
...@@ -26,7 +27,7 @@ ...@@ -26,7 +27,7 @@
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"iconv-lite": "^0.7.2", "iconv-lite": "^0.7.2",
"typescript": "~5.9.3" "typescript": "5.9.3"
} }
}, },
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
...@@ -138,6 +139,20 @@ ...@@ -138,6 +139,20 @@
"react": ">=16.9.0" "react": ">=16.9.0"
} }
}, },
"node_modules/@ant-design/v5-patch-for-react-19": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@ant-design/v5-patch-for-react-19/-/v5-patch-for-react-19-1.0.3.tgz",
"integrity": "sha512-iWfZuSUl5kuhqLUw7jJXUQFMMkM7XpW7apmKzQBQHU0cpifYW4A79xIBt9YVO5IBajKpPG5UKP87Ft7Yrw1p/w==",
"license": "MIT",
"engines": {
"node": ">=12.x"
},
"peerDependencies": {
"antd": ">=5.22.6",
"react": ">=19.0.0",
"react-dom": ">=19.0.0"
}
},
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.28.6", "version": "7.28.6",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.6.tgz", "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.6.tgz",
...@@ -4020,7 +4035,7 @@ ...@@ -4020,7 +4035,7 @@
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
......
...@@ -3,13 +3,14 @@ ...@@ -3,13 +3,14 @@
"private": true, "private": true,
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev --turbopack --hostname 0.0.0.0",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint ." "lint": "eslint ."
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.6.1", "@ant-design/icons": "^5.6.1",
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@tanstack/react-query": "^5.80.6", "@tanstack/react-query": "^5.80.6",
"antd": "^5.25.3", "antd": "^5.25.3",
...@@ -27,6 +28,6 @@ ...@@ -27,6 +28,6 @@
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"iconv-lite": "^0.7.2", "iconv-lite": "^0.7.2",
"typescript": "~5.9.3" "typescript": "5.9.3"
} }
} }
...@@ -33,6 +33,25 @@ export interface DoctorReviewItem { ...@@ -33,6 +33,25 @@ export interface DoctorReviewItem {
qualification_image?: string; qualification_image?: string;
} }
// 医生管理列表项(管理端用)
export interface DoctorItem {
id: string;
user_id: string;
name: string;
phone: string;
avatar: string;
title: string;
department_id: string;
department_name: string;
hospital: string;
license_no: string;
introduction: string;
specialties: string[];
review_status: string;
review_id: string;
user_status: string;
}
// 用户管理查询参数 // 用户管理查询参数
export interface UserListParams { export interface UserListParams {
keyword?: string; keyword?: string;
...@@ -237,6 +256,9 @@ export const adminApi = { ...@@ -237,6 +256,9 @@ export const adminApi = {
post<null>(`/admin/patients/${patientId}/reset-password`), post<null>(`/admin/patients/${patientId}/reset-password`),
// === 医生管理 === // === 医生管理 ===
getDoctorList: (params: UserListParams) =>
get<PaginatedResponse<DoctorItem>>('/admin/doctors', { params }),
getDoctorManageList: (params: UserListParams) => getDoctorManageList: (params: UserListParams) =>
get<PaginatedResponse<DoctorReviewItem>>('/admin/doctors', { params }), get<PaginatedResponse<DoctorReviewItem>>('/admin/doctors', { params }),
......
import { get, post } from './request'; import { get, post } from './request';
import type { DoctorWorkbenchStats } from './doctorPortal';
export type { DoctorWorkbenchStats };
// 问诊相关类型定义 // 问诊相关类型定义
export type ConsultType = 'text' | 'video'; export type ConsultType = 'text' | 'video';
...@@ -149,14 +151,3 @@ export const consultApi = { ...@@ -149,14 +151,3 @@ export const consultApi = {
post<null>(`/consult/${id}/reject`, { reason }), post<null>(`/consult/${id}/reject`, { reason }),
}; };
// 医生工作台统计
export interface DoctorWorkbenchStats {
today_consult_count: number;
waiting_count: number;
in_progress_count: number;
completed_today: number;
total_patients: number;
rating: number;
income_today: number;
income_month: number;
}
...@@ -3,4 +3,13 @@ export * from './user'; ...@@ -3,4 +3,13 @@ export * from './user';
export * from './doctor'; export * from './doctor';
export * from './consult'; export * from './consult';
export * from './admin'; export * from './admin';
export * from './doctorPortal'; // doctorPortal 中的 DoctorWorkbenchStats, WaitingPatient, PatientListItem 与 consult.ts 重名,需选择性导出
export {
doctorPortalApi,
type DoctorProfile,
type ScheduleSlot,
type PatientSummary,
type PatientConsultRecord,
type PatientPrescriptionRecord,
type PatientDetail,
} from './doctorPortal';
...@@ -66,7 +66,7 @@ export interface ChatDoneInfo { ...@@ -66,7 +66,7 @@ export interface ChatDoneInfo {
// ====== SSE 流式对话工具函数 ====== // ====== SSE 流式对话工具函数 ======
const getApiBaseURL = () => { const getApiBaseURL = () => {
const envUrl = import.meta.env.VITE_API_BASE_URL; const envUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
if (envUrl && envUrl.includes('/api/v1')) return envUrl; if (envUrl && envUrl.includes('/api/v1')) return envUrl;
return (envUrl || 'http://localhost:8080') + '/api/v1'; return (envUrl || 'http://localhost:8080') + '/api/v1';
}; };
......
This diff is collapsed.
This diff is collapsed.
...@@ -10,7 +10,7 @@ import { ...@@ -10,7 +10,7 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useUserStore } from '@/store/userStore'; import { useUserStore } from '@/store/userStore';
const { Header, Content } = Layout; const { Content } = Layout;
const { Text } = Typography; const { Text } = Typography;
const menuItems = [ const menuItems = [
...@@ -35,6 +35,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) ...@@ -35,6 +35,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const { user, logout } = useUserStore(); const { user, logout } = useUserStore();
const currentPath = pathname || '';
const userMenuItems = [ const userMenuItems = [
{ key: 'profile', icon: <UserOutlined />, label: '个人信息' }, { key: 'profile', icon: <UserOutlined />, label: '个人信息' },
...@@ -43,56 +44,72 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) ...@@ -43,56 +44,72 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
{ key: 'logout', icon: <LogoutOutlined />, label: '退出登录', danger: true }, { key: 'logout', icon: <LogoutOutlined />, label: '退出登录', danger: true },
]; ];
const handleMenuClick = ({ key }: { key: string }) => {
if (key.startsWith('/')) {
router.push(key);
}
};
const handleUserMenuClick = ({ key }: { key: string }) => { const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') { logout(); router.push('/login'); } if (key === 'logout') { logout(); router.push('/login'); }
}; };
const getOpenKeys = () => { // 选中的菜单项(支持子路径匹配)
if (pathname.startsWith('/admin/patients') || pathname.startsWith('/admin/doctors') || pathname.startsWith('/admin/admins')) { const getSelectedKeys = () => {
return ['user-mgmt']; const allKeys = ['/admin/dashboard', '/admin/patients', '/admin/doctors', '/admin/admins',
} '/admin/departments', '/admin/consultations', '/admin/prescription', '/admin/pharmacy',
return []; '/admin/ai-config', '/admin/compliance'];
const match = allKeys.find(k => currentPath.startsWith(k));
return match ? [match] : [];
}; };
return ( return (
<Layout style={{ minHeight: '100vh' }}> <Layout style={{ minHeight: '100vh' }}>
<Header className="fixed! top-0! left-0! right-0! z-100! flex! items-center! h-12! px-4! leading-12!" <header
style={{ background: 'linear-gradient(135deg, #001529 0%, #003a8c 50%, #0050b3 100%)', borderBottom: 'none', boxShadow: '0 2px 8px rgba(0,21,41,0.15)', padding: '0 16px' }}> style={{
<div className="flex items-center gap-2 cursor-pointer mr-4 shrink-0" position: 'fixed', top: 0, left: 0, right: 0, zIndex: 1000,
onClick={() => router.push('/admin/dashboard')}> height: 48, display: 'flex', alignItems: 'center', padding: '0 16px',
<MedicineBoxOutlined className="text-lg text-white" /> background: 'linear-gradient(135deg, #001529 0%, #003a8c 50%, #0050b3 100%)',
<span className="text-sm font-bold text-white">互联网医院</span> boxShadow: '0 2px 8px rgba(0,21,41,0.15)',
<Tag color="geekblue" className="ml-1! text-[10px]! leading-4! px-1! border-blue-400/30!">管理后台</Tag> }}
>
<div
style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', marginRight: 16, 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> </div>
<Menu <Menu
mode="horizontal" mode="horizontal"
selectedKeys={[pathname]} selectedKeys={getSelectedKeys()}
defaultOpenKeys={getOpenKeys()}
items={menuItems} items={menuItems}
onClick={({ key }) => router.push(key)} onClick={handleMenuClick}
theme="dark" 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!" className="top-nav-menu"
style={{ flex: 1, minWidth: 0 }}
/> />
<div className="flex items-center gap-3 shrink-0"> <div style={{ display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0 }}>
<Badge count={2} size="small"> <Badge count={2} size="small">
<BellOutlined className="text-base text-white/70 cursor-pointer hover:text-white" /> <BellOutlined style={{ fontSize: 16, color: 'rgba(255,255,255,0.7)', cursor: 'pointer' }} />
</Badge> </Badge>
{user ? ( {user ? (
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}> <Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}>
<Space className="cursor-pointer"> <Space style={{ cursor: 'pointer' }}>
<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: '#722ed1' }} /> <Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: '#722ed1' }} />
<Text className="text-white/90! text-xs!">{user.real_name || '管理员'}</Text> <Text style={{ color: 'rgba(255,255,255,0.9)', fontSize: 12 }}>{user.real_name || '管理员'}</Text>
</Space> </Space>
</Dropdown> </Dropdown>
) : ( ) : (
<Button size="small" type="primary" onClick={() => router.push('/login')}>登录</Button> <Button size="small" type="primary" onClick={() => router.push('/login')}>登录</Button>
)} )}
</div> </div>
</Header> </header>
<Content className="mt-12! p-3! min-h-[calc(100vh-48px)]! bg-bg-base!"> <Content style={{ marginTop: 48, padding: 16, minHeight: 'calc(100vh - 48px)', background: '#f0f5ff' }}>
{children} {children}
</Content> </Content>
</Layout> </Layout>
......
...@@ -10,7 +10,7 @@ import { ...@@ -10,7 +10,7 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useUserStore } from '@/store/userStore'; import { useUserStore } from '@/store/userStore';
const { Header, Content } = Layout; const { Content } = Layout;
const { Text } = Typography; const { Text } = Typography;
const menuItems = [ const menuItems = [
...@@ -29,6 +29,7 @@ export default function DoctorLayout({ children }: { children: React.ReactNode } ...@@ -29,6 +29,7 @@ export default function DoctorLayout({ children }: { children: React.ReactNode }
const pathname = usePathname(); const pathname = usePathname();
const { user, logout } = useUserStore(); const { user, logout } = useUserStore();
const [isOnline, setIsOnline] = useState(false); const [isOnline, setIsOnline] = useState(false);
const currentPath = pathname || '';
const userMenuItems = [ const userMenuItems = [
{ key: 'profile', icon: <UserOutlined />, label: '个人信息' }, { key: 'profile', icon: <UserOutlined />, label: '个人信息' },
...@@ -37,50 +38,70 @@ export default function DoctorLayout({ children }: { children: React.ReactNode } ...@@ -37,50 +38,70 @@ export default function DoctorLayout({ children }: { children: React.ReactNode }
{ key: 'logout', icon: <LogoutOutlined />, label: '退出登录', danger: true }, { key: 'logout', icon: <LogoutOutlined />, label: '退出登录', danger: true },
]; ];
const handleMenuClick = ({ key }: { key: string }) => {
if (key.startsWith('/')) {
router.push(key);
}
};
const handleUserMenuClick = ({ key }: { key: string }) => { const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') { logout(); router.push('/login'); } if (key === 'logout') { logout(); router.push('/login'); }
else if (key === 'profile') router.push('/doctor/profile'); else if (key === 'profile') router.push('/doctor/profile');
}; };
const getSelectedKeys = () => {
const match = menuItems.find(item => currentPath.startsWith(item.key));
return match ? [match.key] : [];
};
return ( return (
<Layout style={{ minHeight: '100vh' }}> <Layout style={{ minHeight: '100vh' }}>
<Header className="fixed! top-0! left-0! right-0! z-100! flex! items-center! h-12! px-4! leading-12!" <header
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' }}> style={{
<div className="flex items-center gap-2 cursor-pointer mr-4 shrink-0" position: 'fixed', top: 0, left: 0, right: 0, zIndex: 1000,
onClick={() => router.push('/doctor/workbench')}> height: 48, display: 'flex', alignItems: 'center', padding: '0 16px',
<MedicineBoxOutlined className="text-lg text-white" /> background: 'linear-gradient(135deg, #002140 0%, #003a8c 50%, #0958d9 100%)',
<span className="text-sm font-bold text-white">互联网医院</span> boxShadow: '0 2px 8px rgba(0,21,41,0.15)',
<Tag color="green" className="ml-1! text-[10px]! leading-4! px-1!">医生工作站</Tag> }}
>
<div
style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', marginRight: 16, 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> </div>
<Menu <Menu
mode="horizontal" mode="horizontal"
selectedKeys={[pathname]} selectedKeys={getSelectedKeys()}
items={menuItems} items={menuItems}
onClick={({ key }) => router.push(key)} onClick={handleMenuClick}
theme="dark" 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!" className="top-nav-menu"
style={{ flex: 1, minWidth: 0 }}
/> />
<div className="flex items-center gap-3 shrink-0"> <div style={{ display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0 }}>
<Switch checked={isOnline} onChange={setIsOnline} checkedChildren="在线" unCheckedChildren="离线" size="small" /> <Switch checked={isOnline} onChange={setIsOnline} checkedChildren="在线" unCheckedChildren="离线" size="small" />
<Badge count={5} size="small"> <Badge count={5} size="small">
<BellOutlined className="text-base text-white/70 cursor-pointer hover:text-white" /> <BellOutlined style={{ fontSize: 16, color: 'rgba(255,255,255,0.7)', cursor: 'pointer' }} />
</Badge> </Badge>
{user ? ( {user ? (
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}> <Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}>
<Space className="cursor-pointer"> <Space style={{ cursor: 'pointer' }}>
<Avatar size="small" src={user.avatar} icon={<UserOutlined />} style={{ backgroundColor: '#52c41a' }} /> <Avatar size="small" src={user.avatar} icon={<UserOutlined />} style={{ backgroundColor: '#52c41a' }} />
<Text className="text-white/90! text-xs!">{user.real_name || user.phone}</Text> <Text style={{ color: 'rgba(255,255,255,0.9)', fontSize: 12 }}>{user.real_name || user.phone}</Text>
</Space> </Space>
</Dropdown> </Dropdown>
) : ( ) : (
<Button size="small" type="primary" onClick={() => router.push('/login')}>登录</Button> <Button size="small" type="primary" onClick={() => router.push('/login')}>登录</Button>
)} )}
</div> </div>
</Header> </header>
<Content className="mt-12! p-3! min-h-[calc(100vh-48px)]! bg-bg-base!"> <Content style={{ marginTop: 48, padding: 16, minHeight: 'calc(100vh - 48px)', background: '#f0f5ff' }}>
{children} {children}
</Content> </Content>
</Layout> </Layout>
......
export default function MainLayout({ children }: { children: React.ReactNode }) { export default function MainLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>; return <div className="compact-ui">{children}</div>;
} }
...@@ -11,7 +11,7 @@ import { ...@@ -11,7 +11,7 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useUserStore } from '@/store/userStore'; import { useUserStore } from '@/store/userStore';
const { Header, Content } = Layout; const { Content } = Layout;
const { Text } = Typography; const { Text } = Typography;
const menuItems = [ const menuItems = [
...@@ -31,6 +31,7 @@ export default function PatientLayout({ children }: { children: React.ReactNode ...@@ -31,6 +31,7 @@ export default function PatientLayout({ children }: { children: React.ReactNode
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const { user, logout } = useUserStore(); const { user, logout } = useUserStore();
const currentPath = pathname || '';
const userMenuItems = [ const userMenuItems = [
{ key: 'profile', icon: <UserOutlined />, label: '个人中心' }, { key: 'profile', icon: <UserOutlined />, label: '个人中心' },
...@@ -39,49 +40,69 @@ export default function PatientLayout({ children }: { children: React.ReactNode ...@@ -39,49 +40,69 @@ export default function PatientLayout({ children }: { children: React.ReactNode
{ key: 'logout', icon: <LogoutOutlined />, label: '退出登录', danger: true }, { key: 'logout', icon: <LogoutOutlined />, label: '退出登录', danger: true },
]; ];
const handleMenuClick = ({ key }: { key: string }) => {
if (key.startsWith('/')) {
router.push(key);
}
};
const handleUserMenuClick = ({ key }: { key: string }) => { const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') { logout(); router.push('/login'); } if (key === 'logout') { logout(); router.push('/login'); }
else if (key === 'profile') router.push('/patient/profile'); else if (key === 'profile') router.push('/patient/profile');
}; };
const getSelectedKeys = () => {
const match = menuItems.find(item => currentPath.startsWith(item.key));
return match ? [match.key] : [];
};
return ( return (
<Layout style={{ minHeight: '100vh' }}> <Layout style={{ minHeight: '100vh' }}>
<Header className="fixed! top-0! left-0! right-0! z-100! flex! items-center! h-12! px-4! leading-12!" <header
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' }}> style={{
<div className="flex items-center gap-2 cursor-pointer mr-4 shrink-0" position: 'fixed', top: 0, left: 0, right: 0, zIndex: 1000,
onClick={() => router.push('/patient/home')}> height: 48, display: 'flex', alignItems: 'center', padding: '0 16px',
<MedicineBoxOutlined className="text-lg text-white" /> background: 'linear-gradient(135deg, #003a8c 0%, #0050b3 50%, #1890ff 100%)',
<span className="text-sm font-bold text-white">互联网医院</span> boxShadow: '0 2px 8px rgba(0,21,41,0.12)',
<Tag color="blue" className="ml-1! text-[10px]! leading-4! px-1! border-blue-300/30!">患者端</Tag> }}
>
<div
style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', marginRight: 16, 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> </div>
<Menu <Menu
mode="horizontal" mode="horizontal"
selectedKeys={[pathname]} selectedKeys={getSelectedKeys()}
items={menuItems} items={menuItems}
onClick={({ key }) => router.push(key)} onClick={handleMenuClick}
theme="dark" 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!" className="top-nav-menu"
style={{ flex: 1, minWidth: 0 }}
/> />
<div className="flex items-center gap-3 shrink-0"> <div style={{ display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0 }}>
<Badge count={3} size="small"> <Badge count={3} size="small">
<BellOutlined className="text-base text-white/70 cursor-pointer hover:text-white" /> <BellOutlined style={{ fontSize: 16, color: 'rgba(255,255,255,0.7)', cursor: 'pointer' }} />
</Badge> </Badge>
{user ? ( {user ? (
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}> <Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}>
<Space className="cursor-pointer"> <Space style={{ cursor: 'pointer' }}>
<Avatar size="small" src={user.avatar} icon={<UserOutlined />} style={{ backgroundColor: '#1890ff' }} /> <Avatar size="small" src={user.avatar} icon={<UserOutlined />} style={{ backgroundColor: '#1890ff' }} />
<Text className="text-white/90! text-xs!">{user.real_name || user.phone}</Text> <Text style={{ color: 'rgba(255,255,255,0.9)', fontSize: 12 }}>{user.real_name || user.phone}</Text>
</Space> </Space>
</Dropdown> </Dropdown>
) : ( ) : (
<Button size="small" type="primary" onClick={() => router.push('/login')}>登录 / 注册</Button> <Button size="small" type="primary" onClick={() => router.push('/login')}>登录 / 注册</Button>
)} )}
</div> </div>
</Header> </header>
<Content className="mt-12! p-3! min-h-[calc(100vh-48px)]! bg-bg-base!"> <Content style={{ marginTop: 48, padding: 16, minHeight: 'calc(100vh - 48px)', background: '#f0f5ff' }}>
{children} {children}
</Content> </Content>
</Layout> </Layout>
......
/* 将 antd CSS-in-JS 层声明在最前,使其优先级低于 Tailwind utilities */ /*
@layer antd; * CSS Layer 顺序: base(preflight) < antd(组件样式) < utilities(工具类)
*
@import "tailwindcss"; * 关键: antd 层在 base 和 utilities 之间,确保:
* - Antd 组件样式能覆盖 Tailwind 的 CSS 重置 (preflight)
* - Tailwind 工具类能覆盖 Antd 默认样式
*/
@layer base, antd, utilities;
@import "tailwindcss/preflight" layer(base);
@import "tailwindcss/utilities" layer(utilities);
@theme inline { @theme inline {
--color-primary: #1890ff; --color-primary: #1890ff;
...@@ -20,13 +27,9 @@ ...@@ -20,13 +27,9 @@
--color-border: #d9e4f5; --color-border: #d9e4f5;
} }
* { /* ===== 全局基础样式(无层级,最高优先级)===== */
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #__next { html, body {
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: 100vh; min-height: 100vh;
...@@ -36,7 +39,7 @@ body { ...@@ -36,7 +39,7 @@ body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji'; 'Noto Color Emoji';
font-size: 13px; font-size: 14px;
line-height: 1.5; line-height: 1.5;
color: #1d2129; color: #1d2129;
background-color: #f0f5ff; background-color: #f0f5ff;
...@@ -53,121 +56,145 @@ a:hover { ...@@ -53,121 +56,145 @@ a:hover {
color: #40a9ff; color: #40a9ff;
} }
/* ===== Ant Design 全局紧凑覆盖 ===== */ /* ===== 滚动条 ===== */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f0f5ff;
}
::-webkit-scrollbar-thumb {
background: #bbd4f0;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #91c2f0;
}
/* ===== 顶部导航栏菜单样式 ===== */
.top-nav-menu.ant-menu-horizontal {
border-bottom: none !important;
background: transparent !important;
line-height: 48px !important;
}
.top-nav-menu .ant-menu-item,
.top-nav-menu .ant-menu-submenu-title {
color: rgba(187, 213, 255, 0.8) !important;
font-size: 13px !important;
padding: 0 12px !important;
margin: 0 2px !important;
border-radius: 6px !important;
}
.top-nav-menu .ant-menu-item:hover,
.top-nav-menu .ant-menu-submenu-title:hover,
.top-nav-menu .ant-menu-item-active,
.top-nav-menu .ant-menu-submenu-active > .ant-menu-submenu-title {
color: #fff !important;
background: rgba(255, 255, 255, 0.1) !important;
}
.top-nav-menu .ant-menu-item-selected {
color: #fff !important;
background: rgba(255, 255, 255, 0.15) !important;
}
.top-nav-menu .ant-menu-item::after,
.top-nav-menu .ant-menu-submenu::after {
display: none !important;
}
/* 子菜单弹出样式 */
.ant-menu-submenu-popup .ant-menu {
border-radius: 8px !important;
}
/* ===== Ant Design 紧凑覆盖(仅作用于 .compact-ui 内部) ===== */
.ant-card { .compact-ui .ant-card {
border-radius: 8px !important; border-radius: 8px !important;
box-shadow: 0 1px 4px rgba(24, 144, 255, 0.06) !important; box-shadow: 0 1px 4px rgba(24, 144, 255, 0.06) !important;
border: 1px solid #e6f0fa !important; border: 1px solid #e6f0fa !important;
} }
.ant-card .ant-card-head { .compact-ui .ant-card .ant-card-head {
min-height: 36px !important; min-height: 40px !important;
padding: 0 12px !important; padding: 0 16px !important;
font-size: 13px !important; font-size: 14px !important;
border-bottom: 1px solid #e6f0fa !important; border-bottom: 1px solid #e6f0fa !important;
} }
.ant-card .ant-card-body { .compact-ui .ant-card .ant-card-body {
padding: 12px !important; padding: 16px !important;
} }
.ant-table { .compact-ui .ant-table {
font-size: 13px !important; font-size: 13px !important;
} }
.ant-table-thead > tr > th, .compact-ui .ant-table-thead > tr > th,
.ant-table-thead > tr > td { .compact-ui .ant-table-thead > tr > td {
padding: 8px 12px !important; padding: 10px 12px !important;
background: #f0f7ff !important; background: #f0f7ff !important;
font-weight: 600 !important; font-weight: 600 !important;
font-size: 12px !important; font-size: 13px !important;
color: #1d2129 !important; color: #1d2129 !important;
} }
.ant-table-tbody > tr > td { .compact-ui .ant-table-tbody > tr > td {
padding: 6px 12px !important; padding: 8px 12px !important;
} }
.ant-table-tbody > tr:hover > td { .compact-ui .ant-table-tbody > tr:hover > td {
background: #e6f7ff !important; background: #e6f7ff !important;
} }
.ant-form-item { .compact-ui .ant-form-item {
margin-bottom: 12px !important; margin-bottom: 16px !important;
} }
.ant-form-item-label > label { .compact-ui .ant-form-item-label > label {
font-size: 12px !important; font-size: 13px !important;
color: #4e5969 !important; color: #4e5969 !important;
} }
.ant-modal .ant-modal-header { .compact-ui .ant-modal .ant-modal-header {
padding: 12px 16px !important; padding: 16px 20px !important;
border-bottom: 1px solid #e6f0fa !important; border-bottom: 1px solid #e6f0fa !important;
} }
.ant-modal .ant-modal-body { .compact-ui .ant-modal .ant-modal-body {
padding: 12px 16px !important; padding: 16px 20px !important;
} }
.ant-modal .ant-modal-footer { .compact-ui .ant-modal .ant-modal-footer {
padding: 8px 16px !important; padding: 12px 20px !important;
} }
.ant-tabs .ant-tabs-tab { .compact-ui .ant-statistic .ant-statistic-title {
padding: 8px 12px !important;
font-size: 13px !important; font-size: 13px !important;
margin-bottom: 4px !important;
} }
.compact-ui .ant-statistic .ant-statistic-content {
.ant-statistic .ant-statistic-title { font-size: 22px !important;
font-size: 12px !important;
margin-bottom: 2px !important;
}
.ant-statistic .ant-statistic-content {
font-size: 20px !important;
} }
.ant-descriptions .ant-descriptions-item-label { .compact-ui .ant-descriptions .ant-descriptions-item-label {
font-size: 12px !important; font-size: 13px !important;
padding: 6px 12px !important; padding: 8px 12px !important;
background: #f0f7ff !important; background: #f0f7ff !important;
} }
.ant-descriptions .ant-descriptions-item-content { .compact-ui .ant-descriptions .ant-descriptions-item-content {
font-size: 13px !important; font-size: 13px !important;
padding: 6px 12px !important; padding: 8px 12px !important;
} }
.ant-tag { .compact-ui .ant-tag {
font-size: 11px !important;
padding: 0 6px !important;
line-height: 20px !important;
border-radius: 4px !important; border-radius: 4px !important;
} }
.ant-badge .ant-badge-count { .compact-ui .ant-input,
font-size: 10px !important; .compact-ui .ant-input-affix-wrapper,
} .compact-ui .ant-select-selector {
.ant-btn-sm {
font-size: 12px !important;
padding: 0 8px !important;
height: 26px !important;
}
.ant-input, .ant-input-affix-wrapper, .ant-select-selector {
border-radius: 6px !important; border-radius: 6px !important;
} }
.ant-alert { .compact-ui .ant-alert {
padding: 6px 12px !important;
border-radius: 6px !important; border-radius: 6px !important;
font-size: 12px !important;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f0f5ff;
}
::-webkit-scrollbar-thumb {
background: #bbd4f0;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #91c2f0;
} }
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="8" fill="#1677ff"/>
<path d="M16 6v20M6 16h20" stroke="#fff" stroke-width="4" stroke-linecap="round"/>
</svg>
'use client'; 'use client';
import '@ant-design/v5-patch-for-react-19';
import React from 'react'; import React from 'react';
import { ConfigProvider, App as AntdApp } from 'antd'; import { ConfigProvider, App as AntdApp } from 'antd';
import { StyleProvider } from '@ant-design/cssinjs'; import { StyleProvider } from '@ant-design/cssinjs';
......
import React from 'react'; 'use client';
import { Navigate, useLocation } from 'react-router-dom';
import React, { useEffect } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { useUserStore } from '../../store/userStore'; import { useUserStore } from '../../store/userStore';
import type { UserRole } from '../../api/user'; import type { UserRole } from '../../api/user';
...@@ -15,24 +17,41 @@ interface AuthGuardProps { ...@@ -15,24 +17,41 @@ interface AuthGuardProps {
*/ */
const AuthGuard: React.FC<AuthGuardProps> = ({ children, requiredRole }) => { const AuthGuard: React.FC<AuthGuardProps> = ({ children, requiredRole }) => {
const { isAuthenticated, user } = useUserStore(); const { isAuthenticated, user } = useUserStore();
const location = useLocation(); const router = useRouter();
const pathname = usePathname();
useEffect(() => {
if (!isAuthenticated || !user) { if (!isAuthenticated || !user) {
return <Navigate to="/login" state={{ from: location }} replace />; router.replace('/login');
return;
} }
if (requiredRole) { if (requiredRole) {
const roles = Array.isArray(requiredRole) ? requiredRole : [requiredRole]; const roles = Array.isArray(requiredRole) ? requiredRole : [requiredRole];
if (!roles.includes(user.role as UserRole)) { if (!roles.includes(user.role as UserRole)) {
// 角色不匹配,重定向到其对应端首页
switch (user.role) { switch (user.role) {
case 'doctor': case 'doctor':
return <Navigate to="/doctor/workbench" replace />; router.replace('/doctor/workbench');
break;
case 'admin': case 'admin':
return <Navigate to="/admin/dashboard" replace />; router.replace('/admin/dashboard');
break;
default: default:
return <Navigate to="/patient/home" replace />; router.replace('/patient/home');
break;
}
}
}
}, [isAuthenticated, user, requiredRole, router, pathname]);
if (!isAuthenticated || !user) {
return null;
} }
if (requiredRole) {
const roles = Array.isArray(requiredRole) ? requiredRole : [requiredRole];
if (!roles.includes(user.role as UserRole)) {
return null;
} }
} }
......
'use client';
import React from 'react'; import React from 'react';
import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { useRouter, usePathname } from 'next/navigation';
import { Layout, Menu, Avatar, Dropdown, Button, Badge, Space, Typography, Tag } from 'antd'; import { Layout, Menu, Avatar, Dropdown, Button, Badge, Space, Typography, Tag } from 'antd';
import { import {
DashboardOutlined, UserOutlined, TeamOutlined, ApartmentOutlined, DashboardOutlined, UserOutlined, TeamOutlined, ApartmentOutlined,
...@@ -29,9 +31,9 @@ const menuItems = [ ...@@ -29,9 +31,9 @@ const menuItems = [
{ key: '/admin/compliance', icon: <SafetyCertificateOutlined />, label: '合规报表' }, { key: '/admin/compliance', icon: <SafetyCertificateOutlined />, label: '合规报表' },
]; ];
const AdminLayout: React.FC = () => { const AdminLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const navigate = useNavigate(); const router = useRouter();
const location = useLocation(); const pathname = usePathname();
const { user, logout } = useUserStore(); const { user, logout } = useUserStore();
const userMenuItems = [ const userMenuItems = [
...@@ -42,13 +44,13 @@ const AdminLayout: React.FC = () => { ...@@ -42,13 +44,13 @@ const AdminLayout: React.FC = () => {
]; ];
const handleUserMenuClick = ({ key }: { key: string }) => { const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') { logout(); navigate('/login'); } if (key === 'logout') { logout(); router.push('/login'); }
}; };
const getSelectedKeys = () => [location.pathname]; const currentPath = pathname || '';
const getSelectedKeys = () => [currentPath];
const getOpenKeys = () => { const getOpenKeys = () => {
const p = location.pathname; if (currentPath.startsWith('/admin/patients') || currentPath.startsWith('/admin/doctors') || currentPath.startsWith('/admin/admins')) {
if (p.startsWith('/admin/patients') || p.startsWith('/admin/doctors') || p.startsWith('/admin/admins')) {
return ['user-mgmt']; return ['user-mgmt'];
} }
return []; return [];
...@@ -59,7 +61,7 @@ const AdminLayout: React.FC = () => { ...@@ -59,7 +61,7 @@ const AdminLayout: React.FC = () => {
<Header className="fixed! top-0! left-0! right-0! z-100! flex! items-center! h-12! px-4! leading-12!" <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, #001529 0%, #003a8c 50%, #0050b3 100%)', borderBottom: 'none', boxShadow: '0 2px 8px rgba(0,21,41,0.15)', padding: '0 16px' }}> style={{ background: 'linear-gradient(135deg, #001529 0%, #003a8c 50%, #0050b3 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" <div className="flex items-center gap-2 cursor-pointer mr-4 shrink-0"
onClick={() => navigate('/admin/dashboard')}> onClick={() => router.push('/admin/dashboard')}>
<MedicineBoxOutlined className="text-lg text-white" /> <MedicineBoxOutlined className="text-lg text-white" />
<span className="text-sm font-bold text-white">互联网医院</span> <span className="text-sm font-bold text-white">互联网医院</span>
<Tag color="geekblue" className="ml-1! text-[10px]! leading-4! px-1! border-blue-400/30!">管理后台</Tag> <Tag color="geekblue" className="ml-1! text-[10px]! leading-4! px-1! border-blue-400/30!">管理后台</Tag>
...@@ -70,7 +72,7 @@ const AdminLayout: React.FC = () => { ...@@ -70,7 +72,7 @@ const AdminLayout: React.FC = () => {
selectedKeys={getSelectedKeys()} selectedKeys={getSelectedKeys()}
defaultOpenKeys={getOpenKeys()} defaultOpenKeys={getOpenKeys()}
items={menuItems} items={menuItems}
onClick={({ key }) => navigate(key)} onClick={({ key }) => router.push(key)}
theme="dark" 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!" 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!"
/> />
...@@ -87,13 +89,13 @@ const AdminLayout: React.FC = () => { ...@@ -87,13 +89,13 @@ const AdminLayout: React.FC = () => {
</Space> </Space>
</Dropdown> </Dropdown>
) : ( ) : (
<Button size="small" type="primary" onClick={() => navigate('/login')}>登录</Button> <Button size="small" type="primary" onClick={() => router.push('/login')}>登录</Button>
)} )}
</div> </div>
</Header> </Header>
<Content className="mt-12! p-3! min-h-[calc(100vh-48px)]! bg-[#f0f5ff]!"> <Content className="mt-12! p-3! min-h-[calc(100vh-48px)]! bg-[#f0f5ff]!">
<Outlet /> {children}
</Content> </Content>
</Layout> </Layout>
); );
......
'use client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { useRouter, usePathname } from 'next/navigation';
import { Layout, Menu, Avatar, Dropdown, Button, Badge, Space, Switch, Typography, Tag } from 'antd'; import { Layout, Menu, Avatar, Dropdown, Button, Badge, Space, Switch, Typography, Tag } from 'antd';
import { import {
DashboardOutlined, UserOutlined, MessageOutlined, CalendarOutlined, DashboardOutlined, UserOutlined, MessageOutlined, CalendarOutlined,
...@@ -22,9 +24,9 @@ const menuItems = [ ...@@ -22,9 +24,9 @@ const menuItems = [
{ key: '/doctor/profile', icon: <UserOutlined />, label: '个人信息' }, { key: '/doctor/profile', icon: <UserOutlined />, label: '个人信息' },
]; ];
const DoctorLayout: React.FC = () => { const DoctorLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const navigate = useNavigate(); const router = useRouter();
const location = useLocation(); const pathname = usePathname();
const { user, logout } = useUserStore(); const { user, logout } = useUserStore();
const [isOnline, setIsOnline] = useState(false); const [isOnline, setIsOnline] = useState(false);
...@@ -36,8 +38,8 @@ const DoctorLayout: React.FC = () => { ...@@ -36,8 +38,8 @@ const DoctorLayout: React.FC = () => {
]; ];
const handleUserMenuClick = ({ key }: { key: string }) => { const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') { logout(); navigate('/login'); } if (key === 'logout') { logout(); router.push('/login'); }
else if (key === 'profile') navigate('/doctor/profile'); else if (key === 'profile') router.push('/doctor/profile');
}; };
return ( return (
...@@ -45,7 +47,7 @@ const DoctorLayout: React.FC = () => { ...@@ -45,7 +47,7 @@ const DoctorLayout: React.FC = () => {
<Header className="fixed! top-0! left-0! right-0! z-100! flex! items-center! h-12! px-4! leading-12!" <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' }}> 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" <div className="flex items-center gap-2 cursor-pointer mr-4 shrink-0"
onClick={() => navigate('/doctor/workbench')}> onClick={() => router.push('/doctor/workbench')}>
<MedicineBoxOutlined className="text-lg text-white" /> <MedicineBoxOutlined className="text-lg text-white" />
<span className="text-sm font-bold text-white">互联网医院</span> <span className="text-sm font-bold text-white">互联网医院</span>
<Tag color="green" className="ml-1! text-[10px]! leading-4! px-1!">医生工作站</Tag> <Tag color="green" className="ml-1! text-[10px]! leading-4! px-1!">医生工作站</Tag>
...@@ -53,9 +55,9 @@ const DoctorLayout: React.FC = () => { ...@@ -53,9 +55,9 @@ const DoctorLayout: React.FC = () => {
<Menu <Menu
mode="horizontal" mode="horizontal"
selectedKeys={[location.pathname]} selectedKeys={[pathname || '']}
items={menuItems} items={menuItems}
onClick={({ key }) => navigate(key)} onClick={({ key }) => router.push(key)}
theme="dark" 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!" 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!"
/> />
...@@ -73,13 +75,13 @@ const DoctorLayout: React.FC = () => { ...@@ -73,13 +75,13 @@ const DoctorLayout: React.FC = () => {
</Space> </Space>
</Dropdown> </Dropdown>
) : ( ) : (
<Button size="small" type="primary" onClick={() => navigate('/login')}>登录</Button> <Button size="small" type="primary" onClick={() => router.push('/login')}>登录</Button>
)} )}
</div> </div>
</Header> </Header>
<Content className="mt-12! p-3! min-h-[calc(100vh-48px)]! bg-[#f0f5ff]!"> <Content className="mt-12! p-3! min-h-[calc(100vh-48px)]! bg-[#f0f5ff]!">
<Outlet /> {children}
</Content> </Content>
</Layout> </Layout>
); );
......
'use client';
import React from 'react'; import React from 'react';
import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { useRouter, usePathname } from 'next/navigation';
import { Layout, Menu, Avatar, Dropdown, Button, Space, Badge } from 'antd'; import { Layout, Menu, Avatar, Dropdown, Button, Space, Badge } from 'antd';
import { import {
HomeOutlined, HomeOutlined,
...@@ -16,9 +18,9 @@ import { useUserStore } from '../../store/userStore'; ...@@ -16,9 +18,9 @@ import { useUserStore } from '../../store/userStore';
const { Header, Sider, Content } = Layout; const { Header, Sider, Content } = Layout;
const MainLayout: React.FC = () => { const MainLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const navigate = useNavigate(); const router = useRouter();
const location = useLocation(); const pathname = usePathname();
const { user, logout } = useUserStore(); const { user, logout } = useUserStore();
const menuItems = [ const menuItems = [
...@@ -40,11 +42,11 @@ const MainLayout: React.FC = () => { ...@@ -40,11 +42,11 @@ const MainLayout: React.FC = () => {
const handleUserMenuClick = ({ key }: { key: string }) => { const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') { if (key === 'logout') {
logout(); logout();
navigate('/login'); router.push('/login');
} else if (key === 'profile') { } else if (key === 'profile') {
navigate('/profile'); router.push('/profile');
} else if (key === 'settings') { } else if (key === 'settings') {
navigate('/settings'); router.push('/settings');
} }
}; };
...@@ -68,7 +70,7 @@ const MainLayout: React.FC = () => { ...@@ -68,7 +70,7 @@ const MainLayout: React.FC = () => {
> >
<div <div
style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }} style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}
onClick={() => navigate('/home')} onClick={() => router.push('/home')}
> >
<MedicineBoxOutlined style={{ fontSize: 26, color: '#1677ff' }} /> <MedicineBoxOutlined style={{ fontSize: 26, color: '#1677ff' }} />
<span style={{ fontSize: 17, fontWeight: 700, color: '#1677ff', letterSpacing: 1 }}> <span style={{ fontSize: 17, fontWeight: 700, color: '#1677ff', letterSpacing: 1 }}>
...@@ -89,8 +91,8 @@ const MainLayout: React.FC = () => { ...@@ -89,8 +91,8 @@ const MainLayout: React.FC = () => {
</Dropdown> </Dropdown>
) : ( ) : (
<Space> <Space>
<Button onClick={() => navigate('/login')}>登录</Button> <Button onClick={() => router.push('/login')}>登录</Button>
<Button type="primary" onClick={() => navigate('/register')}>注册</Button> <Button type="primary" onClick={() => router.push('/register')}>注册</Button>
</Space> </Space>
)} )}
</Space> </Space>
...@@ -110,10 +112,10 @@ const MainLayout: React.FC = () => { ...@@ -110,10 +112,10 @@ const MainLayout: React.FC = () => {
> >
<Menu <Menu
mode="inline" mode="inline"
selectedKeys={[location.pathname]} selectedKeys={[pathname || '']}
style={{ height: '100%', borderRight: 0, paddingTop: 8 }} style={{ height: '100%', borderRight: 0, paddingTop: 8 }}
items={menuItems} items={menuItems}
onClick={({ key }) => navigate(key)} onClick={({ key }) => router.push(key)}
/> />
</Sider> </Sider>
...@@ -126,7 +128,7 @@ const MainLayout: React.FC = () => { ...@@ -126,7 +128,7 @@ const MainLayout: React.FC = () => {
overflow: 'auto', overflow: 'auto',
}} }}
> >
<Outlet /> {children}
</Content> </Content>
</Layout> </Layout>
</Layout> </Layout>
......
'use client';
import React from 'react'; import React from 'react';
import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { useRouter, usePathname } from 'next/navigation';
import { Layout, Menu, Avatar, Dropdown, Button, Badge, Space, Typography, Tag } from 'antd'; import { Layout, Menu, Avatar, Dropdown, Button, Badge, Space, Typography, Tag } from 'antd';
import { import {
HomeOutlined, UserOutlined, MedicineBoxOutlined, FileTextOutlined, HomeOutlined, UserOutlined, MedicineBoxOutlined, FileTextOutlined,
...@@ -25,9 +27,9 @@ const menuItems = [ ...@@ -25,9 +27,9 @@ const menuItems = [
{ key: '/patient/profile', icon: <UserOutlined />, label: '个人中心' }, { key: '/patient/profile', icon: <UserOutlined />, label: '个人中心' },
]; ];
const PatientLayout: React.FC = () => { const PatientLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const navigate = useNavigate(); const router = useRouter();
const location = useLocation(); const pathname = usePathname();
const { user, logout } = useUserStore(); const { user, logout } = useUserStore();
const userMenuItems = [ const userMenuItems = [
...@@ -38,8 +40,8 @@ const PatientLayout: React.FC = () => { ...@@ -38,8 +40,8 @@ const PatientLayout: React.FC = () => {
]; ];
const handleUserMenuClick = ({ key }: { key: string }) => { const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') { logout(); navigate('/login'); } if (key === 'logout') { logout(); router.push('/login'); }
else if (key === 'profile') navigate('/patient/profile'); else if (key === 'profile') router.push('/patient/profile');
}; };
return ( return (
...@@ -47,7 +49,7 @@ const PatientLayout: React.FC = () => { ...@@ -47,7 +49,7 @@ const PatientLayout: React.FC = () => {
<Header className="fixed! top-0! left-0! right-0! z-100! flex! items-center! h-12! px-4! leading-12!" <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' }}> 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" <div className="flex items-center gap-2 cursor-pointer mr-4 shrink-0"
onClick={() => navigate('/patient/home')}> onClick={() => router.push('/patient/home')}>
<MedicineBoxOutlined className="text-lg text-white" /> <MedicineBoxOutlined className="text-lg text-white" />
<span className="text-sm font-bold text-white">互联网医院</span> <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> <Tag color="blue" className="ml-1! text-[10px]! leading-4! px-1! border-blue-300/30!">患者端</Tag>
...@@ -55,9 +57,9 @@ const PatientLayout: React.FC = () => { ...@@ -55,9 +57,9 @@ const PatientLayout: React.FC = () => {
<Menu <Menu
mode="horizontal" mode="horizontal"
selectedKeys={[location.pathname]} selectedKeys={[pathname || '']}
items={menuItems} items={menuItems}
onClick={({ key }) => navigate(key)} onClick={({ key }) => router.push(key)}
theme="dark" 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!" 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!"
/> />
...@@ -74,13 +76,13 @@ const PatientLayout: React.FC = () => { ...@@ -74,13 +76,13 @@ const PatientLayout: React.FC = () => {
</Space> </Space>
</Dropdown> </Dropdown>
) : ( ) : (
<Button size="small" type="primary" onClick={() => navigate('/login')}>登录 / 注册</Button> <Button size="small" type="primary" onClick={() => router.push('/login')}>登录 / 注册</Button>
)} )}
</div> </div>
</Header> </Header>
<Content className="mt-12! p-3! min-h-[calc(100vh-48px)]! bg-[#f0f5ff]!"> <Content className="mt-12! p-3! min-h-[calc(100vh-48px)]! bg-[#f0f5ff]!">
<Outlet /> {children}
</Content> </Content>
</Layout> </Layout>
); );
......
import React from 'react'; 'use client';
import { Navigate } from 'react-router-dom';
import React, { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useUserStore } from '../../store/userStore'; import { useUserStore } from '../../store/userStore';
interface RoleRedirectProps { interface RoleRedirectProps {
...@@ -12,19 +14,28 @@ interface RoleRedirectProps { ...@@ -12,19 +14,28 @@ interface RoleRedirectProps {
*/ */
const RoleRedirect: React.FC<RoleRedirectProps> = () => { const RoleRedirect: React.FC<RoleRedirectProps> = () => {
const { isAuthenticated, user } = useUserStore(); const { isAuthenticated, user } = useUserStore();
const router = useRouter();
useEffect(() => {
if (!isAuthenticated || !user) { if (!isAuthenticated || !user) {
return <Navigate to="/login" replace />; router.replace('/login');
return;
} }
switch (user.role) { switch (user.role) {
case 'doctor': case 'doctor':
return <Navigate to="/doctor/workbench" replace />; router.replace('/doctor/workbench');
break;
case 'admin': case 'admin':
return <Navigate to="/admin/dashboard" replace />; router.replace('/admin/dashboard');
break;
default: default:
return <Navigate to="/patient/home" replace />; router.replace('/patient/home');
break;
} }
}, [isAuthenticated, user, router]);
return null;
}; };
export default RoleRedirect; export default RoleRedirect;
'use client';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Card, Table, Input, Button, Space, Tag, Avatar, Modal, message, Typography, Form } from 'antd'; import { Card, Table, Input, Button, Space, Tag, Avatar, Modal, message, Typography, Form } from 'antd';
import { SearchOutlined, UserOutlined, ReloadOutlined, PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; import { SearchOutlined, UserOutlined, ReloadOutlined, PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
......
This diff is collapsed.
'use client';
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { Card, Table, Tag, Typography, Space, Input, Select, Button, Avatar, DatePicker, message } from 'antd'; import { Card, Table, Tag, Typography, Space, Input, Select, Button, Avatar, DatePicker, message } from 'antd';
import { import {
......
'use client';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Card, Row, Col, Statistic, Typography, List, Avatar, Tag, Space, Progress } from 'antd'; import { Card, Row, Col, Statistic, Typography, List, Avatar, Tag, Space, Progress } from 'antd';
import { import {
......
'use client';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Card, Table, Tag, Typography, Space, Button, Avatar, Modal, message, Descriptions } from 'antd'; import { Card, Table, Tag, Typography, Space, Button, Avatar, Modal, message, Descriptions } from 'antd';
import { UserOutlined, CheckCircleOutlined, CloseCircleOutlined, EyeOutlined } from '@ant-design/icons'; import { UserOutlined, CheckCircleOutlined, CloseCircleOutlined, EyeOutlined } from '@ant-design/icons';
......
'use client';
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { import {
Card, Row, Col, Table, Tag, Typography, Space, Tabs, Statistic, Card, Row, Col, Table, Tag, Typography, Space, Tabs, Statistic,
......
This diff is collapsed.
'use client';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Card, Table, Tag, Typography, Space, Input, Select, Button, Avatar, Modal, message } from 'antd'; import { Card, Table, Tag, Typography, Space, Input, Select, Button, Avatar, Modal, message } from 'antd';
import { SearchOutlined, UserOutlined, LockOutlined, StopOutlined, EditOutlined } from '@ant-design/icons'; import { SearchOutlined, UserOutlined, LockOutlined, StopOutlined, EditOutlined } from '@ant-design/icons';
...@@ -8,9 +10,7 @@ import type { UserInfo } from '../../../api/user'; ...@@ -8,9 +10,7 @@ import type { UserInfo } from '../../../api/user';
const { Title, Text } = Typography; const { Title, Text } = Typography;
interface UserRecord extends UserInfo { type UserRecord = UserInfo;
status?: 'active' | 'disabled';
}
const roleMap: Record<string, { text: string; color: string }> = { const roleMap: Record<string, { text: string; color: string }> = {
patient: { text: '患者', color: 'blue' }, patient: { text: '患者', color: 'blue' },
......
'use client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Card, Row, Col, Input, Button, List, Tag, Typography, Tabs, Empty, Spin, Divider, Alert } from 'antd'; import { Card, Row, Col, Input, Button, List, Tag, Typography, Tabs, Empty, Spin, Divider, Alert } from 'antd';
import { import {
......
'use client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { import {
Card, Row, Col, Table, Button, Tag, Typography, Space, Modal, Form, Card, Row, Col, Table, Button, Tag, Typography, Space, Modal, Form,
......
...@@ -21,7 +21,7 @@ const formatWaitingTime = (seconds: number) => { ...@@ -21,7 +21,7 @@ const formatWaitingTime = (seconds: number) => {
if (seconds < 60) return '刚刚'; if (seconds < 60) return '刚刚';
const minutes = Math.floor(seconds / 60); const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}分钟`; if (minutes < 60) return `${minutes}分钟`;
return `${Math.floor(minutes / 60)}小时${minutes % 60}分; return `${Math.floor(minutes / 60)}小时${minutes % 60}钟`;
}; };
const WaitingQueue: React.FC<WaitingQueueProps> = ({ const WaitingQueue: React.FC<WaitingQueueProps> = ({
...@@ -47,9 +47,9 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({ ...@@ -47,9 +47,9 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 8 }}>
<Space> <Space>
<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: '#87d068' }} /> <Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: '#87d068' }} />
<Text strong>{patient.patient_name || '患者}</Text> <Text strong>{patient.patient_name || '患者'}</Text>
<Tag color={patient.type === 'video' ? 'blue' : 'green'} style={{ fontSize: 11 }}> <Tag color={patient.type === 'video' ? 'blue' : 'green'} style={{ fontSize: 11 }}>
{patient.type === 'video' ? '视频' : '图文'} {patient.type === 'video' ? '视频' : '图文'}
</Tag> </Tag>
</Space> </Space>
</div> </div>
...@@ -58,7 +58,7 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({ ...@@ -58,7 +58,7 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({
? patient.chief_complaint.length > 30 ? patient.chief_complaint.length > 30
? patient.chief_complaint.substring(0, 30) + '...' ? patient.chief_complaint.substring(0, 30) + '...'
: patient.chief_complaint : patient.chief_complaint
: '鏈~鍐欎富璇} : '未填写主诉'}
</Text> </Text>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Tag icon={<ClockCircleOutlined />} color="orange" style={{ fontSize: 11 }}> <Tag icon={<ClockCircleOutlined />} color="orange" style={{ fontSize: 11 }}>
...@@ -92,9 +92,9 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({ ...@@ -92,9 +92,9 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 8 }}>
<Space> <Space>
<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: '#1890ff' }} /> <Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: '#1890ff' }} />
<Text strong>{consult.patient_name || '患者}</Text> <Text strong>{consult.patient_name || '患者'}</Text>
<Tag color={consult.type === 'video' ? 'blue' : 'green'} style={{ fontSize: 11 }}> <Tag color={consult.type === 'video' ? 'blue' : 'green'} style={{ fontSize: 11 }}>
{consult.type === 'video' ? '视频' : '图文'} {consult.type === 'video' ? '视频' : '图文'}
</Tag> </Tag>
</Space> </Space>
</div> </div>
...@@ -103,7 +103,7 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({ ...@@ -103,7 +103,7 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({
? consult.chief_complaint.length > 30 ? consult.chief_complaint.length > 30
? consult.chief_complaint.substring(0, 30) + '...' ? consult.chief_complaint.substring(0, 30) + '...'
: consult.chief_complaint : consult.chief_complaint
: '鏈~鍐欎富璇} : '未填写主诉'}
</Text> </Text>
</Card> </Card>
); );
...@@ -113,12 +113,12 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({ ...@@ -113,12 +113,12 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({
key: 'waiting', key: 'waiting',
label: ( label: (
<Space> <Space>
<span>待接诊/span> <span>待接诊</span>
<Badge count={waitingList.length} style={{ backgroundColor: '#fa8c16' }} /> <Badge count={waitingList.length} style={{ backgroundColor: '#fa8c16' }} />
</Space> </Space>
), ),
children: waitingList.length === 0 ? ( children: waitingList.length === 0 ? (
<Empty description="暂无等待患者 style={{ marginTop: 40 }} /> <Empty description="暂无等待患者" style={{ marginTop: 40 }} />
) : ( ) : (
<div>{waitingList.map(renderWaitingItem)}</div> <div>{waitingList.map(renderWaitingItem)}</div>
), ),
...@@ -128,12 +128,12 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({ ...@@ -128,12 +128,12 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({
label: ( label: (
<Space> <Space>
<MessageOutlined /> <MessageOutlined />
<span>杩涜涓</span> <span>进行中</span>
<Badge count={inProgressList.length} style={{ backgroundColor: '#52c41a' }} /> <Badge count={inProgressList.length} style={{ backgroundColor: '#52c41a' }} />
</Space> </Space>
), ),
children: inProgressList.length === 0 ? ( children: inProgressList.length === 0 ? (
<Empty description="暂无杩涜涓棶璇 style={{ marginTop: 40 }} /> <Empty description="暂无进行中问诊" style={{ marginTop: 40 }} />
) : ( ) : (
<div>{inProgressList.map(renderInProgressItem)}</div> <div>{inProgressList.map(renderInProgressItem)}</div>
), ),
...@@ -143,12 +143,12 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({ ...@@ -143,12 +143,12 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({
label: ( label: (
<Space> <Space>
<CheckCircleOutlined /> <CheckCircleOutlined />
<span>已完成/span> <span>已完成</span>
<Badge count={completedList.length} style={{ backgroundColor: '#999' }} /> <Badge count={completedList.length} style={{ backgroundColor: '#999' }} />
</Space> </Space>
), ),
children: completedList.length === 0 ? ( children: completedList.length === 0 ? (
<Empty description="暂无已完成愰棶璇 style={{ marginTop: 40 }} /> <Empty description="暂无已完成问诊" style={{ marginTop: 40 }} />
) : ( ) : (
<div>{completedList.map((consult) => ( <div>{completedList.map((consult) => (
<Card <Card
...@@ -167,8 +167,8 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({ ...@@ -167,8 +167,8 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 8 }}>
<Space> <Space>
<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: '#999' }} /> <Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: '#999' }} />
<Text strong>{consult.patient_name || '患者}</Text> <Text strong>{consult.patient_name || '患者'}</Text>
<Tag color="default" style={{ fontSize: 11 }}>已完成/Tag> <Tag color="default" style={{ fontSize: 11 }}>已完成</Tag>
</Space> </Space>
</div> </div>
<Text type="secondary" style={{ fontSize: 12, display: 'block' }}> <Text type="secondary" style={{ fontSize: 12, display: 'block' }}>
...@@ -176,7 +176,7 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({ ...@@ -176,7 +176,7 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({
? consult.chief_complaint.length > 30 ? consult.chief_complaint.length > 30
? consult.chief_complaint.substring(0, 30) + '...' ? consult.chief_complaint.substring(0, 30) + '...'
: consult.chief_complaint : consult.chief_complaint
: '鏈~鍐欎富璇} : '未填写主诉'}
</Text> </Text>
</Card> </Card>
))}</div> ))}</div>
......
'use client';
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { Card, Row, Col, Input, Button, Avatar, Typography, Space, Descriptions, Tag, Divider } from 'antd'; import { Card, Row, Col, Input, Button, Avatar, Typography, Space, Descriptions, Tag, Divider } from 'antd';
import { import {
...@@ -8,52 +10,36 @@ import { ...@@ -8,52 +10,36 @@ import {
MedicineBoxOutlined, MedicineBoxOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
const { Text, Title } = Typography; const { Text } = Typography;
const { TextArea } = Input; const { TextArea } = Input;
const DoctorConsultRoomPage: React.FC = () => { const DoctorConsultRoomPage: React.FC = () => {
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
// TODO: 从API获取真实数据
const currentConsult = { const currentConsult = {
id: 'c1', id: 'c1',
patient_name: '张三', patient_name: '张三',
patient_avatar: '', patient_avatar: '',
type: 'text', type: 'text',
status: 'in_progress', status: 'in_progress',
chief_complaint: '头痛、乏力,持续3天, chief_complaint: '头痛、乏力,持续3天',
medical_history: '高血压病史5年,长期服用降压药物', medical_history: '高血压病史5年,长期服用降压药物',
}; };
const messages = [ const messages = [
{ { id: '1', sender_type: 'system', content: '问诊已开始', created_at: '2026-02-25T13:00:00Z' },
id: '1', { id: '2', sender_type: 'patient', content: '医生您好,我最近三天一直头痛,感觉很乏力,精神也不好。', created_at: '2026-02-25T13:01:00Z' },
sender_type: 'system', { id: '3', sender_type: 'patient', content: '我有高血压,一直在吃药控制。', created_at: '2026-02-25T13:01:30Z' },
content: '问诊已开始,
created_at: '2026-02-25T13:00:00Z',
},
{
id: '2',
sender_type: 'patient',
content: '医生您好,我最近三天一直头痛,感觉很乏力,精神也不好评,
created_at: '2026-02-25T13:01:00Z',
},
{
id: '3',
sender_type: 'patient',
content: '我有高血压,一直在吃药控制定,
created_at: '2026-02-25T13:01:30Z',
},
]; ];
const patientInfo = { const patientInfo = {
name: '张三', name: '张三',
age: 45, age: 45,
gender: '?, gender: '',
phone: '138****8888', phone: '138****8888',
medical_history: '高血压病史5, medical_history: '高血压病史5年',
allergy_history: '青霉素过敏, allergy_history: '青霉素过敏',
consultation_count: 3, consultation_count: 3,
}; };
...@@ -63,7 +49,6 @@ const DoctorConsultRoomPage: React.FC = () => { ...@@ -63,7 +49,6 @@ const DoctorConsultRoomPage: React.FC = () => {
const handleSend = () => { const handleSend = () => {
if (!inputValue.trim()) return; if (!inputValue.trim()) return;
// TODO: 发送消息到API
setInputValue(''); setInputValue('');
}; };
...@@ -135,7 +120,7 @@ const DoctorConsultRoomPage: React.FC = () => { ...@@ -135,7 +120,7 @@ const DoctorConsultRoomPage: React.FC = () => {
<Avatar size={48} icon={<UserOutlined />} style={{ backgroundColor: '#87d068' }} /> <Avatar size={48} icon={<UserOutlined />} style={{ backgroundColor: '#87d068' }} />
<div className="mt-1"> <div className="mt-1">
<div className="text-sm font-bold">{patientInfo.name}</div> <div className="text-sm font-bold">{patientInfo.name}</div>
<Text type="secondary" className="text-xs!">{patientInfo.age}?· {patientInfo.gender}</Text> <Text type="secondary" className="text-xs!">{patientInfo.age}· {patientInfo.gender}</Text>
</div> </div>
</div> </div>
<Divider className="my-1!" /> <Divider className="my-1!" />
...@@ -143,7 +128,7 @@ const DoctorConsultRoomPage: React.FC = () => { ...@@ -143,7 +128,7 @@ const DoctorConsultRoomPage: React.FC = () => {
<Descriptions.Item label="电话">{patientInfo.phone}</Descriptions.Item> <Descriptions.Item label="电话">{patientInfo.phone}</Descriptions.Item>
<Descriptions.Item label="病史">{patientInfo.medical_history}</Descriptions.Item> <Descriptions.Item label="病史">{patientInfo.medical_history}</Descriptions.Item>
<Descriptions.Item label="过敏"><Tag color="red">{patientInfo.allergy_history}</Tag></Descriptions.Item> <Descriptions.Item label="过敏"><Tag color="red">{patientInfo.allergy_history}</Tag></Descriptions.Item>
<Descriptions.Item label="就诊">{patientInfo.consultation_count} ?/Descriptions.Item> <Descriptions.Item label="就诊">{patientInfo.consultation_count} </Descriptions.Item>
</Descriptions> </Descriptions>
<Divider className="my-1!" /> <Divider className="my-1!" />
<div className="text-xs font-semibold mb-1">本次主诉</div> <div className="text-xs font-semibold mb-1">本次主诉</div>
......
'use client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Card, Table, Tag, Typography, Space, Input, Select, DatePicker, Button, Avatar } from 'antd'; import { Card, Table, Tag, Typography, Space, Input, Select, DatePicker, Button, Avatar } from 'antd';
import { SearchOutlined, UserOutlined, MessageOutlined, VideoCameraOutlined, EyeOutlined } from '@ant-design/icons'; import { SearchOutlined, UserOutlined, MessageOutlined, VideoCameraOutlined, EyeOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
const { Title, Text } = Typography; const { Text } = Typography;
const { RangePicker } = DatePicker; const { RangePicker } = DatePicker;
interface ConsultRecord { interface ConsultRecord {
...@@ -19,15 +21,14 @@ interface ConsultRecord { ...@@ -19,15 +21,14 @@ interface ConsultRecord {
} }
const statusMap: Record<string, { text: string; color: string }> = { const statusMap: Record<string, { text: string; color: string }> = {
completed: { text: '已完成, color: 'green' }, completed: { text: '已完成', color: 'green' },
cancelled: { text: '已取消, color: 'red' }, cancelled: { text: '已取消', color: 'red' },
in_progress: { text: '进行中, color: 'blue' }, in_progress: { text: '进行中', color: 'blue' },
}; };
const DoctorHistoryPage: React.FC = () => { const DoctorHistoryPage: React.FC = () => {
const [keyword, setKeyword] = useState(''); const [keyword, setKeyword] = useState('');
// TODO: 从API获取真实数据
const historyData: ConsultRecord[] = [ const historyData: ConsultRecord[] = [
{ {
id: '1', id: '1',
...@@ -35,7 +36,7 @@ const DoctorHistoryPage: React.FC = () => { ...@@ -35,7 +36,7 @@ const DoctorHistoryPage: React.FC = () => {
patient_avatar: '', patient_avatar: '',
type: 'text', type: 'text',
status: 'completed', status: 'completed',
chief_complaint: '头痛、乏力, chief_complaint: '头痛、乏力',
created_at: '2026-02-24T10:00:00Z', created_at: '2026-02-24T10:00:00Z',
ended_at: '2026-02-24T10:30:00Z', ended_at: '2026-02-24T10:30:00Z',
}, },
...@@ -55,7 +56,7 @@ const DoctorHistoryPage: React.FC = () => { ...@@ -55,7 +56,7 @@ const DoctorHistoryPage: React.FC = () => {
patient_avatar: '', patient_avatar: '',
type: 'text', type: 'text',
status: 'cancelled', status: 'cancelled',
chief_complaint: '复诊开, chief_complaint: '复诊开',
created_at: '2026-02-23T09:00:00Z', created_at: '2026-02-23T09:00:00Z',
ended_at: null, ended_at: null,
}, },
...@@ -63,7 +64,7 @@ const DoctorHistoryPage: React.FC = () => { ...@@ -63,7 +64,7 @@ const DoctorHistoryPage: React.FC = () => {
const columns: ColumnsType<ConsultRecord> = [ const columns: ColumnsType<ConsultRecord> = [
{ {
title: '患者, title: '患者',
dataIndex: 'patient_name', dataIndex: 'patient_name',
key: 'patient_name', key: 'patient_name',
render: (name: string, record) => ( render: (name: string, record) => (
...@@ -93,7 +94,7 @@ const DoctorHistoryPage: React.FC = () => { ...@@ -93,7 +94,7 @@ const DoctorHistoryPage: React.FC = () => {
ellipsis: true, ellipsis: true,
}, },
{ {
title: '状态, title: '状态',
dataIndex: 'status', dataIndex: 'status',
key: 'status', key: 'status',
render: (status: string) => { render: (status: string) => {
...@@ -123,7 +124,7 @@ const DoctorHistoryPage: React.FC = () => { ...@@ -123,7 +124,7 @@ const DoctorHistoryPage: React.FC = () => {
<Card size="small"> <Card size="small">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Input <Input
placeholder="搜索患者姓名 placeholder="搜索患者姓名"
prefix={<SearchOutlined />} prefix={<SearchOutlined />}
value={keyword} value={keyword}
onChange={(e) => setKeyword(e.target.value)} onChange={(e) => setKeyword(e.target.value)}
...@@ -132,8 +133,8 @@ const DoctorHistoryPage: React.FC = () => { ...@@ -132,8 +133,8 @@ const DoctorHistoryPage: React.FC = () => {
/> />
<Select placeholder="类型" style={{ width: 100 }} size="small" allowClear <Select placeholder="类型" style={{ width: 100 }} size="small" allowClear
options={[{ label: '图文', value: 'text' }, { label: '视频', value: 'video' }]} /> options={[{ label: '图文', value: 'text' }, { label: '视频', value: 'video' }]} />
<Select placeholder="状态 style={{ width: 100 }} size="small" allowClear <Select placeholder="状态" style={{ width: 100 }} size="small" allowClear
options={[{ label: '已完成, value: 'completed' }, { label: '已取消, value: 'cancelled' }]} /> options={[{ label: '已完成', value: 'completed' }, { label: '已取消', value: 'cancelled' }]} />
<RangePicker size="small" /> <RangePicker size="small" />
</div> </div>
</Card> </Card>
...@@ -144,7 +145,7 @@ const DoctorHistoryPage: React.FC = () => { ...@@ -144,7 +145,7 @@ const DoctorHistoryPage: React.FC = () => {
dataSource={historyData} dataSource={historyData}
rowKey="id" rowKey="id"
size="small" size="small"
pagination={{ pageSize: 10, size: 'small', showSizeChanger: true, showTotal: (total) => `?${total} 条` }} pagination={{ pageSize: 10, size: 'small', showSizeChanger: true, showTotal: (total) => `${total} 条` }}
/> />
</Card> </Card>
</div> </div>
......
'use client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { import {
Card, Row, Col, Table, Button, Tag, Typography, Space, Statistic, Card, Row, Col, Table, Button, Tag, Typography, Space, Statistic,
...@@ -12,7 +14,7 @@ import { ...@@ -12,7 +14,7 @@ import {
FileTextOutlined, FileTextOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
const { Title, Text } = Typography; const { Text } = Typography;
const { RangePicker } = DatePicker; const { RangePicker } = DatePicker;
interface IncomeRecord { interface IncomeRecord {
...@@ -50,7 +52,7 @@ const mockMonthlyBills: MonthlyBill[] = [ ...@@ -50,7 +52,7 @@ const mockMonthlyBills: MonthlyBill[] = [
const IncomePage: React.FC = () => { const IncomePage: React.FC = () => {
const [withdrawModalVisible, setWithdrawModalVisible] = useState(false); const [withdrawModalVisible, setWithdrawModalVisible] = useState(false);
const totalBalance = 32500; // 可提现余额(分) const totalBalance = 32500;
const monthIncome = 156000; const monthIncome = 156000;
const monthConsults = 42; const monthConsults = 42;
...@@ -61,7 +63,7 @@ const IncomePage: React.FC = () => { ...@@ -61,7 +63,7 @@ const IncomePage: React.FC = () => {
const incomeColumns = [ const incomeColumns = [
{ title: '时间', dataIndex: 'date', key: 'date', width: 160 }, { title: '时间', dataIndex: 'date', key: 'date', width: 160 },
{ title: '患者, dataIndex: 'patient_name', key: 'patient_name', width: 100 }, { title: '患者', dataIndex: 'patient_name', key: 'patient_name', width: 100 },
{ {
title: '类型', title: '类型',
dataIndex: 'type', dataIndex: 'type',
...@@ -80,13 +82,13 @@ const IncomePage: React.FC = () => { ...@@ -80,13 +82,13 @@ const IncomePage: React.FC = () => {
render: (v: number) => <Text strong style={{ color: '#52c41a' }}>¥{(v / 100).toFixed(2)}</Text>, render: (v: number) => <Text strong style={{ color: '#52c41a' }}>¥{(v / 100).toFixed(2)}</Text>,
}, },
{ {
title: '状态, title: '状态',
dataIndex: 'status', dataIndex: 'status',
key: 'status', key: 'status',
width: 80, width: 80,
render: (v: string) => ( render: (v: string) => (
<Tag color={v === 'settled' ? 'green' : 'orange'}> <Tag color={v === 'settled' ? 'green' : 'orange'}>
{v === 'settled' ? '已结算 : '待结} {v === 'settled' ? '已结算' : '待结算'}
</Tag> </Tag>
), ),
}, },
...@@ -99,11 +101,10 @@ const IncomePage: React.FC = () => { ...@@ -99,11 +101,10 @@ const IncomePage: React.FC = () => {
收入统计 收入统计
</h4> </h4>
{/* 概览卡片 */}
<Row gutter={[8, 8]}> <Row gutter={[8, 8]}>
<Col span={8}> <Col span={8}>
<div className="rounded-lg p-3 text-white" style={{ background: 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)' }}> <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-xs text-white/80 mb-1">可提现余</div>
<div className="text-2xl font-bold">¥{(totalBalance / 100).toFixed(2)}</div> <div className="text-2xl font-bold">¥{(totalBalance / 100).toFixed(2)}</div>
<Button ghost size="small" className="mt-2 text-white! border-white/60!" icon={<WalletOutlined />} <Button ghost size="small" className="mt-2 text-white! border-white/60!" icon={<WalletOutlined />}
onClick={() => setWithdrawModalVisible(true)}>申请提现</Button> onClick={() => setWithdrawModalVisible(true)}>申请提现</Button>
...@@ -114,15 +115,15 @@ const IncomePage: React.FC = () => { ...@@ -114,15 +115,15 @@ const IncomePage: React.FC = () => {
<div className="text-xs text-gray-500 mb-1">本月收入</div> <div className="text-xs text-gray-500 mb-1">本月收入</div>
<div className="text-xl font-bold text-green-500">¥{(monthIncome / 100).toFixed(2)}</div> <div className="text-xl font-bold text-green-500">¥{(monthIncome / 100).toFixed(2)}</div>
<div className="mt-1 text-[11px] text-gray-400"> <div className="mt-1 text-[11px] text-gray-400">
<RiseOutlined className="text-green-500 mr-0.5" />较上<span className="text-green-500">+12.5%</span> <RiseOutlined className="text-green-500 mr-0.5" />较上<span className="text-green-500">+12.5%</span>
</div> </div>
</div> </div>
</Col> </Col>
<Col span={8}> <Col span={8}>
<div className="bg-white rounded-lg p-3 border border-[#e6f0fa]"> <div className="bg-white rounded-lg p-3 border border-[#e6f0fa]">
<div className="text-xs text-gray-500 mb-1">本月问诊</div> <div className="text-xs text-gray-500 mb-1">本月问诊</div>
<div className="text-xl font-bold text-[#1890ff]">{monthConsults} <span className="text-xs font-normal text-gray-400">?/span></div> <div className="text-xl font-bold text-[#1890ff]">{monthConsults} <span className="text-xs font-normal text-gray-400"></span></div>
<div className="mt-1 text-[11px] text-gray-400">日均 {(monthConsults / 25).toFixed(1)} ?/div> <div className="mt-1 text-[11px] text-gray-400">日均 {(monthConsults / 25).toFixed(1)} </div>
</div> </div>
</Col> </Col>
</Row> </Row>
...@@ -165,7 +166,7 @@ const IncomePage: React.FC = () => { ...@@ -165,7 +166,7 @@ const IncomePage: React.FC = () => {
</Col> </Col>
<Col span={5}> <Col span={5}>
<Statistic <Statistic
title="总收入 title="总收入"
value={bill.total_income / 100} value={bill.total_income / 100}
precision={2} precision={2}
prefix="¥" prefix="¥"
...@@ -173,7 +174,7 @@ const IncomePage: React.FC = () => { ...@@ -173,7 +174,7 @@ const IncomePage: React.FC = () => {
/> />
</Col> </Col>
<Col span={3}> <Col span={3}>
<Statistic title="问诊量 value={bill.consult_count} suffix="? valueStyle={{ fontSize: 16 }} /> <Statistic title="问诊量" value={bill.consult_count} suffix="次" valueStyle={{ fontSize: 16 }} />
</Col> </Col>
<Col span={12}> <Col span={12}>
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 8 }}>
...@@ -220,7 +221,7 @@ const IncomePage: React.FC = () => { ...@@ -220,7 +221,7 @@ const IncomePage: React.FC = () => {
{ title: '申请日期', dataIndex: 'date', key: 'date' }, { title: '申请日期', dataIndex: 'date', key: 'date' },
{ title: '金额', dataIndex: 'amount', key: 'amount', render: (v: number) => `¥${(v / 100).toFixed(2)}` }, { title: '金额', dataIndex: 'amount', key: 'amount', render: (v: number) => `¥${(v / 100).toFixed(2)}` },
{ title: '提现账户', dataIndex: 'bank', key: 'bank' }, { title: '提现账户', dataIndex: 'bank', key: 'bank' },
{ title: '状态, dataIndex: 'status', key: 'status', render: () => <Tag color="green">已到</Tag> }, { title: '状态', dataIndex: 'status', key: 'status', render: () => <Tag color="green">已到账</Tag> },
]} ]}
/> />
</Card> </Card>
...@@ -237,20 +238,20 @@ const IncomePage: React.FC = () => { ...@@ -237,20 +238,20 @@ const IncomePage: React.FC = () => {
okText="确认提现" okText="确认提现"
> >
<Form layout="vertical"> <Form layout="vertical">
<Form.Item label="可提现金> <Form.Item label="可提现金额">
<Text strong style={{ fontSize: 24, color: '#52c41a' }}>¥{(totalBalance / 100).toFixed(2)}</Text> <Text strong style={{ fontSize: 24, color: '#52c41a' }}>¥{(totalBalance / 100).toFixed(2)}</Text>
</Form.Item> </Form.Item>
<Form.Item label="提现金额" required> <Form.Item label="提现金额" required>
<InputNumber <InputNumber
style={{ width: '100%' }} style={{ width: '100%' }}
placeholder="请输入提现金额 placeholder="请输入提现金额"
min={1} min={1}
max={totalBalance / 100} max={totalBalance / 100}
precision={2} precision={2}
prefix="¥" prefix="¥"
/> />
</Form.Item> </Form.Item>
<Form.Item label="到账银行> <Form.Item label="到账银行卡">
<Input value="招商银行 ****5678" disabled /> <Input value="招商银行 ****5678" disabled />
</Form.Item> </Form.Item>
</Form> </Form>
......
'use client';
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback } from 'react';
import { import {
Card, Row, Col, Input, Typography, Tag, Space, Tabs, Card, Row, Col, Input, Typography, Tag, Space, Tabs,
...@@ -15,7 +17,7 @@ import { ...@@ -15,7 +17,7 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { doctorPortalApi, type PatientListItem, type PatientDetail, type PatientConsultRecord, type PatientPrescriptionRecord } from '../../../api/doctorPortal'; import { doctorPortalApi, type PatientListItem, type PatientDetail, type PatientConsultRecord, type PatientPrescriptionRecord } from '../../../api/doctorPortal';
const { Title, Text } = Typography; const { Text } = Typography;
const consultTypeLabel: Record<string, string> = { const consultTypeLabel: Record<string, string> = {
text: '图文问诊', text: '图文问诊',
...@@ -30,10 +32,10 @@ const consultStatusColor: Record<string, string> = { ...@@ -30,10 +32,10 @@ const consultStatusColor: Record<string, string> = {
}; };
const consultStatusLabel: Record<string, string> = { const consultStatusLabel: Record<string, string> = {
completed: '已完成, completed: '已完成',
in_progress: '进行中, in_progress: '进行中',
pending: '待接诊, pending: '待接诊',
cancelled: '已取消, cancelled: '已取消',
}; };
const prescriptionStatusColor: Record<string, string> = { const prescriptionStatusColor: Record<string, string> = {
...@@ -46,12 +48,18 @@ const prescriptionStatusColor: Record<string, string> = { ...@@ -46,12 +48,18 @@ const prescriptionStatusColor: Record<string, string> = {
}; };
const prescriptionStatusLabel: Record<string, string> = { const prescriptionStatusLabel: Record<string, string> = {
completed: '已完成, completed: '已完成',
dispensed: '已取消, dispensed: '已发药',
approved: '已审核, approved: '已审核',
signed: '已签名, signed: '已签名',
pending: '待审核, pending: '待审核',
rejected: '已拒绝, rejected: '已拒绝',
};
const genderLabel = (g: string) => {
if (g === 'male') return '';
if (g === 'female') return '';
return g;
}; };
const PatientRecordPage: React.FC = () => { const PatientRecordPage: React.FC = () => {
...@@ -70,13 +78,12 @@ const PatientRecordPage: React.FC = () => { ...@@ -70,13 +78,12 @@ const PatientRecordPage: React.FC = () => {
setPatients(res.data.list || []); setPatients(res.data.list || []);
setTotal(res.data.total || 0); setTotal(res.data.total || 0);
} catch { } catch {
message.error('获取患者列表失); message.error('获取患者列表失');
} finally { } finally {
setListLoading(false); setListLoading(false);
} }
}, []); }, []);
// 初始加载
React.useEffect(() => { React.useEffect(() => {
fetchPatients(); fetchPatients();
}, [fetchPatients]); }, [fetchPatients]);
...@@ -93,7 +100,7 @@ const PatientRecordPage: React.FC = () => { ...@@ -93,7 +100,7 @@ const PatientRecordPage: React.FC = () => {
const res = await doctorPortalApi.getPatientDetail(patientId); const res = await doctorPortalApi.getPatientDetail(patientId);
setSelectedPatient(res.data); setSelectedPatient(res.data);
} catch { } catch {
message.error('获取患者详情失); message.error('获取患者详情失');
} finally { } finally {
setDetailLoading(false); setDetailLoading(false);
} }
...@@ -107,7 +114,7 @@ const PatientRecordPage: React.FC = () => { ...@@ -107,7 +114,7 @@ const PatientRecordPage: React.FC = () => {
}, },
{ title: '主诉', dataIndex: 'chief_complaint', key: 'chief_complaint', ellipsis: true }, { title: '主诉', dataIndex: 'chief_complaint', key: 'chief_complaint', ellipsis: true },
{ {
title: '状态, dataIndex: 'status', key: 'status', width: 80, title: '状态', dataIndex: 'status', key: 'status', width: 80,
render: (v: string) => <Tag color={consultStatusColor[v] ?? 'default'}>{consultStatusLabel[v] ?? v}</Tag>, render: (v: string) => <Tag color={consultStatusColor[v] ?? 'default'}>{consultStatusLabel[v] ?? v}</Tag>,
}, },
]; ];
...@@ -126,7 +133,7 @@ const PatientRecordPage: React.FC = () => { ...@@ -126,7 +133,7 @@ const PatientRecordPage: React.FC = () => {
), ),
}, },
{ {
title: '状态, dataIndex: 'status', key: 'status', width: 80, title: '状态', dataIndex: 'status', key: 'status', width: 80,
render: (v: string) => <Tag color={prescriptionStatusColor[v] ?? 'default'}>{prescriptionStatusLabel[v] ?? v}</Tag>, render: (v: string) => <Tag color={prescriptionStatusColor[v] ?? 'default'}>{prescriptionStatusLabel[v] ?? v}</Tag>,
}, },
]; ];
...@@ -134,12 +141,13 @@ const PatientRecordPage: React.FC = () => { ...@@ -134,12 +141,13 @@ const PatientRecordPage: React.FC = () => {
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0"> <h4 className="text-sm font-bold text-gray-800 m-0">
<UserOutlined className="mr-1" />患者档 </h4> <UserOutlined className="mr-1" />患者档案
</h4>
<Row gutter={8}> <Row gutter={8}>
<Col span={8}> <Col span={8}>
<Card title={<span className="text-xs font-semibold">患者列表</span>} size="small" <Card title={<span className="text-xs font-semibold">患者列表</span>} size="small"
extra={<Text type="secondary" className="text-[11px]!">{total} ?/Text>}> extra={<Text type="secondary" className="text-[11px]!">{total} </Text>}>
<Input prefix={<SearchOutlined />} placeholder="搜索姓名/手机" size="small" <Input prefix={<SearchOutlined />} placeholder="搜索姓名/手机" size="small"
value={searchKeyword} onChange={(e) => setSearchKeyword(e.target.value)} value={searchKeyword} onChange={(e) => setSearchKeyword(e.target.value)}
onPressEnter={(e) => handleSearch((e.target as HTMLInputElement).value)} onPressEnter={(e) => handleSearch((e.target as HTMLInputElement).value)}
...@@ -147,7 +155,7 @@ const PatientRecordPage: React.FC = () => { ...@@ -147,7 +155,7 @@ const PatientRecordPage: React.FC = () => {
className="mb-2" /> className="mb-2" />
<Spin spinning={listLoading}> <Spin spinning={listLoading}>
<List size="small" dataSource={patients} <List size="small" dataSource={patients}
locale={{ emptyText: <Empty description="暂无患者 /> }} locale={{ emptyText: <Empty description="暂无患者" /> }}
pagination={total > 20 ? { current: page, pageSize: 20, total, size: 'small', pagination={total > 20 ? { current: page, pageSize: 20, total, size: 'small',
onChange: (p) => { setPage(p); fetchPatients(searchKeyword, p); } } : false} onChange: (p) => { setPage(p); fetchPatients(searchKeyword, p); } } : false}
renderItem={(patient) => ( renderItem={(patient) => (
...@@ -159,11 +167,11 @@ const PatientRecordPage: React.FC = () => { ...@@ -159,11 +167,11 @@ const PatientRecordPage: React.FC = () => {
<List.Item.Meta <List.Item.Meta
avatar={patient.avatar ? <Avatar src={patient.avatar} size={32} /> : 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>} <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">{patient.gender === 'male' ? '? : patient.gender === 'female' ? '? : patient.gender}/{patient.age}?/Text></span>} title={<span className="text-xs"><Text strong>{patient.name}</Text> <Text type="secondary">{genderLabel(patient.gender)}/{patient.age}</Text></span>}
description={ description={
<div className="text-[11px]"> <div className="text-[11px]">
<div className="text-gray-400"><PhoneOutlined className="mr-0.5" />{patient.phone}</div> <div className="text-gray-400"><PhoneOutlined className="mr-0.5" />{patient.phone}</div>
{patient.allergy_history && patient.allergy_history !== '? && ( {patient.allergy_history && patient.allergy_history !== '' && (
<Tag color="red" className="text-[10px]!"><SafetyOutlined /> {patient.allergy_history}</Tag> <Tag color="red" className="text-[10px]!"><SafetyOutlined /> {patient.allergy_history}</Tag>
)} )}
<div className="text-gray-400">{patient.consultation_count}次问诊{patient.last_consult_at && ` · ${patient.last_consult_at}`}</div> <div className="text-gray-400">{patient.consultation_count}次问诊{patient.last_consult_at && ` · ${patient.last_consult_at}`}</div>
...@@ -184,15 +192,17 @@ const PatientRecordPage: React.FC = () => { ...@@ -184,15 +192,17 @@ const PatientRecordPage: React.FC = () => {
<Card size="small"> <Card size="small">
<Descriptions title={<span className="text-xs font-semibold">基本信息</span>} bordered size="small" column={3}> <Descriptions title={<span className="text-xs font-semibold">基本信息</span>} bordered size="small" column={3}>
<Descriptions.Item label="姓名">{selectedPatient.name}</Descriptions.Item> <Descriptions.Item label="姓名">{selectedPatient.name}</Descriptions.Item>
<Descriptions.Item label="性别">{selectedPatient.gender === 'male' ? '? : selectedPatient.gender === 'female' ? '? : selectedPatient.gender}</Descriptions.Item> <Descriptions.Item label="性别">{genderLabel(selectedPatient.gender)}</Descriptions.Item>
<Descriptions.Item label="年龄">{selectedPatient.age}?/Descriptions.Item> <Descriptions.Item label="年龄">{selectedPatient.age}</Descriptions.Item>
<Descriptions.Item label="手机">{selectedPatient.phone}</Descriptions.Item> <Descriptions.Item label="手机">{selectedPatient.phone}</Descriptions.Item>
<Descriptions.Item label="紧急联>{selectedPatient.emergency_contact || <Text type="secondary">未填</Text>}</Descriptions.Item> <Descriptions.Item label="紧急联系人">{selectedPatient.emergency_contact || <Text type="secondary">未填写</Text>}</Descriptions.Item>
<Descriptions.Item label="医保">{selectedPatient.insurance_type || <Text type="secondary">未填</Text>}</Descriptions.Item> <Descriptions.Item label="医保类型">{selectedPatient.insurance_type || <Text type="secondary">未填写</Text>}</Descriptions.Item>
<Descriptions.Item label="过敏感 span={3}> <Descriptions.Item label="过敏史" span={3}>
{selectedPatient.allergy_history ? <Tag color={selectedPatient.allergy_history === '? ? 'green' : 'red'}>{selectedPatient.allergy_history}</Tag> : <Tag color="green">?/Tag>} {selectedPatient.allergy_history
? <Tag color={selectedPatient.allergy_history === '' ? 'green' : 'red'}>{selectedPatient.allergy_history}</Tag>
: <Tag color="green"></Tag>}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="病史" span={3}>{selectedPatient.medical_history || <Text type="secondary">?/Text>}</Descriptions.Item> <Descriptions.Item label="既往病史" span={3}>{selectedPatient.medical_history || <Text type="secondary"></Text>}</Descriptions.Item>
</Descriptions> </Descriptions>
</Card> </Card>
...@@ -224,7 +234,7 @@ const PatientRecordPage: React.FC = () => { ...@@ -224,7 +234,7 @@ const PatientRecordPage: React.FC = () => {
</div> </div>
) : ( ) : (
<Card size="small" className="text-center py-10"> <Card size="small" className="text-center py-10">
<Empty description="请从左侧选择患者查看档 /> <Empty description="请从左侧选择患者查看档案" />
</Card> </Card>
)} )}
</Spin> </Spin>
......
'use client';
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback } from 'react';
import { import {
Input, Button, Table, Form, Select, InputNumber, Input, Button, Table, Form, Select, InputNumber,
......
import React from 'react'; 'use client';
import { Card, Avatar, Button, Descriptions, Tag, Typography, Row, Col, Divider, Switch, Space } from 'antd';
import React, { useState, useEffect } from 'react';
import { Card, Avatar, Button, Descriptions, Tag, Typography, Row, Col, Divider, Switch, Space, Spin, message } from 'antd';
import { import {
UserOutlined, UserOutlined,
EditOutlined, EditOutlined,
SafetyCertificateOutlined, SafetyCertificateOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useUserStore } from '../../../store/userStore'; import { useUserStore } from '../../../store/userStore';
import { doctorPortalApi, type DoctorProfile } from '../../../api/doctorPortal';
const { Title, Text } = Typography; const { Text } = Typography;
const DoctorProfilePage: React.FC = () => { const DoctorProfilePage: React.FC = () => {
const { user } = useUserStore(); const { user } = useUserStore();
const [loading, setLoading] = useState(true);
const [profile, setProfile] = useState<DoctorProfile | null>(null);
// TODO: 从API获取医生详细信息 useEffect(() => {
const doctorProfile = { const fetchProfile = async () => {
try {
const res = await doctorPortalApi.getProfile();
setProfile(res.data);
} catch {
// 使用默认数据
setProfile({
id: user?.id || '',
name: user?.real_name || '医生', name: user?.real_name || '医生',
avatar: user?.avatar || '', avatar: user?.avatar || '',
title: '主任医师', title: '主任医师',
department_id: '',
department_name: '内科', department_name: '内科',
hospital: '北京协和医院', hospital: '北京协和医院',
license_no: 'XXXXX', introduction: '从事内科临床工作20余年,擅长高血压、糖尿病等慢性病的诊治。',
introduction: '从事内科临床工作20余年,擅长高血压、糖尿病等慢性病的诊治, specialties: ['高血压', '糖尿病', '冠心病', '心律失常'],
specialties: ['高血压, '糖尿, '冠心, '心律失常'],
price: 5000, price: 5000,
is_online: false, is_online: false,
ai_assist_enabled: true, ai_assist_enabled: true,
});
} finally {
setLoading(false);
}
};
fetchProfile();
}, [user]);
const handleToggleAI = async (checked: boolean) => {
if (!profile) return;
try {
await doctorPortalApi.updateProfile({ ai_assist_enabled: checked });
setProfile({ ...profile, ai_assist_enabled: checked });
message.success(checked ? 'AI辅助已开启' : 'AI辅助已关闭');
} catch {
message.error('操作失败');
}
}; };
if (loading) {
return <div className="text-center py-10"><Spin size="large" /></div>;
}
if (!profile) return null;
return ( return (
<div className="max-w-[800px] mx-auto space-y-2"> <div className="max-w-[800px] mx-auto space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">个人信息</h4> <h4 className="text-sm font-bold text-gray-800 m-0">个人信息</h4>
...@@ -34,17 +69,16 @@ const DoctorProfilePage: React.FC = () => { ...@@ -34,17 +69,16 @@ const DoctorProfilePage: React.FC = () => {
<Card size="small"> <Card size="small">
<Row gutter={12} align="middle"> <Row gutter={12} align="middle">
<Col> <Col>
<Avatar size={64} src={doctorProfile.avatar} icon={<UserOutlined />} /> <Avatar size={64} src={profile.avatar} icon={<UserOutlined />} />
</Col> </Col>
<Col flex="auto"> <Col flex="auto">
<div className="flex items-center gap-2 mb-0.5"> <div className="flex items-center gap-2 mb-0.5">
<span className="text-base font-bold">{doctorProfile.name}</span> <span className="text-base font-bold">{profile.name}</span>
<Tag color="blue">{doctorProfile.title}</Tag> <Tag color="blue">{profile.title}</Tag>
</div> </div>
<div className="text-xs text-gray-500 mb-1">{doctorProfile.hospital} · {doctorProfile.department_name}</div> <div className="text-xs text-gray-500 mb-1">{profile.hospital} · {profile.department_name}</div>
<Space size={4}> <Space size={4}>
<Tag icon={<SafetyCertificateOutlined />} color="success">已认证</Tag> <Tag icon={<SafetyCertificateOutlined />} color="success">已认证</Tag>
<Tag>证号:{doctorProfile.license_no}</Tag>
</Space> </Space>
</Col> </Col>
<Col> <Col>
...@@ -55,25 +89,24 @@ const DoctorProfilePage: React.FC = () => { ...@@ -55,25 +89,24 @@ const DoctorProfilePage: React.FC = () => {
<Card title={<span className="text-xs font-semibold">详细信息</span>} size="small"> <Card title={<span className="text-xs font-semibold">详细信息</span>} size="small">
<Descriptions column={2} size="small"> <Descriptions column={2} size="small">
<Descriptions.Item label="姓名">{doctorProfile.name}</Descriptions.Item> <Descriptions.Item label="姓名">{profile.name}</Descriptions.Item>
<Descriptions.Item label="职称">{doctorProfile.title}</Descriptions.Item> <Descriptions.Item label="职称">{profile.title}</Descriptions.Item>
<Descriptions.Item label="医院">{doctorProfile.hospital}</Descriptions.Item> <Descriptions.Item label="医院">{profile.hospital}</Descriptions.Item>
<Descriptions.Item label="科室">{doctorProfile.department_name}</Descriptions.Item> <Descriptions.Item label="科室">{profile.department_name}</Descriptions.Item>
<Descriptions.Item label="价格">¥{(doctorProfile.price / 100).toFixed(0)}/?/Descriptions.Item> <Descriptions.Item label="价格">¥{(profile.price / 100).toFixed(0)}/次</Descriptions.Item>
<Descriptions.Item label="证号">{doctorProfile.license_no}</Descriptions.Item>
</Descriptions> </Descriptions>
</Card> </Card>
<Card title={<span className="text-xs font-semibold">擅长领域</span>} size="small"> <Card title={<span className="text-xs font-semibold">擅长领域</span>} size="small">
<Space wrap size={4}> <Space wrap size={4}>
{doctorProfile.specialties.map((s, i) => ( {profile.specialties.map((s, i) => (
<Tag key={i} color="processing">{s}</Tag> <Tag key={i} color="processing">{s}</Tag>
))} ))}
</Space> </Space>
</Card> </Card>
<Card title={<span className="text-xs font-semibold">个人简</span>} size="small"> <Card title={<span className="text-xs font-semibold">个人简</span>} size="small">
<Text className="text-xs!">{doctorProfile.introduction}</Text> <Text className="text-xs!">{profile.introduction}</Text>
</Card> </Card>
<Card title={<span className="text-xs font-semibold">接诊设置</span>} size="small"> <Card title={<span className="text-xs font-semibold">接诊设置</span>} size="small">
...@@ -81,9 +114,9 @@ const DoctorProfilePage: React.FC = () => { ...@@ -81,9 +114,9 @@ const DoctorProfilePage: React.FC = () => {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<Text strong className="text-xs!">AI 辅助接诊</Text> <Text strong className="text-xs!">AI 辅助接诊</Text>
<div className="text-[11px] text-gray-400">开启后 AI 将协助分析患者症</div> <div className="text-[11px] text-gray-400">开启后 AI 将协助分析患者症</div>
</div> </div>
<Switch size="small" checked={doctorProfile.ai_assist_enabled} /> <Switch size="small" checked={profile.ai_assist_enabled} onChange={handleToggleAI} />
</div> </div>
<Divider className="my-0!" /> <Divider className="my-0!" />
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
......
...@@ -10,19 +10,18 @@ import { ...@@ -10,19 +10,18 @@ import {
ClockCircleOutlined, ClockCircleOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
const { Text, Title } = Typography; const { Text } = Typography;
const DoctorQueuePage: React.FC = () => { const DoctorQueuePage: React.FC = () => {
const router = useRouter(); const router = useRouter();
// TODO: 从API获取真实数据
const waitingPatients = [ const waitingPatients = [
{ {
id: '1', id: '1',
consult_id: 'c1', consult_id: 'c1',
patient_name: '张三', patient_name: '张三',
patient_avatar: '', patient_avatar: '',
chief_complaint: '头痛、乏力,持续3天。既往有高血压病史, chief_complaint: '头痛、乏力,持续3天。既往有高血压病史',
type: 'text' as const, type: 'text' as const,
waiting_time: 300, waiting_time: 300,
created_at: '2026-02-25T13:00:00Z', created_at: '2026-02-25T13:00:00Z',
...@@ -32,7 +31,7 @@ const DoctorQueuePage: React.FC = () => { ...@@ -32,7 +31,7 @@ const DoctorQueuePage: React.FC = () => {
consult_id: 'c2', consult_id: 'c2',
patient_name: '李四', patient_name: '李四',
patient_avatar: '', patient_avatar: '',
chief_complaint: '咳嗽有痰,偶尔发热,已持续一周, chief_complaint: '咳嗽有痰,偶尔发热,已持续一周',
type: 'video' as const, type: 'video' as const,
waiting_time: 180, waiting_time: 180,
created_at: '2026-02-25T13:05:00Z', created_at: '2026-02-25T13:05:00Z',
...@@ -42,7 +41,7 @@ const DoctorQueuePage: React.FC = () => { ...@@ -42,7 +41,7 @@ const DoctorQueuePage: React.FC = () => {
consult_id: 'c3', consult_id: 'c3',
patient_name: '王五', patient_name: '王五',
patient_avatar: '', patient_avatar: '',
chief_complaint: '复诊开药,糖尿病用药续方, chief_complaint: '复诊开药,糖尿病用药续方',
type: 'text' as const, type: 'text' as const,
waiting_time: 60, waiting_time: 60,
created_at: '2026-02-25T13:10:00Z', created_at: '2026-02-25T13:10:00Z',
...@@ -52,7 +51,7 @@ const DoctorQueuePage: React.FC = () => { ...@@ -52,7 +51,7 @@ const DoctorQueuePage: React.FC = () => {
consult_id: 'c4', consult_id: 'c4',
patient_name: '赵六', patient_name: '赵六',
patient_avatar: '', patient_avatar: '',
chief_complaint: '皮肤瘙痒红肿,疑似过敏, chief_complaint: '皮肤瘙痒红肿,疑似过敏',
type: 'video' as const, type: 'video' as const,
waiting_time: 30, waiting_time: 30,
created_at: '2026-02-25T13:12:00Z', created_at: '2026-02-25T13:12:00Z',
...@@ -66,7 +65,6 @@ const DoctorQueuePage: React.FC = () => { ...@@ -66,7 +65,6 @@ const DoctorQueuePage: React.FC = () => {
}; };
const handleAccept = (_consultId: string) => { const handleAccept = (_consultId: string) => {
// TODO: 调用接诊API
router.push('/doctor/consult-room'); router.push('/doctor/consult-room');
}; };
...@@ -79,7 +77,7 @@ const DoctorQueuePage: React.FC = () => { ...@@ -79,7 +77,7 @@ const DoctorQueuePage: React.FC = () => {
<Card size="small"> <Card size="small">
{waitingPatients.length === 0 ? ( {waitingPatients.length === 0 ? (
<Empty description="暂无等待患者 /> <Empty description="暂无等待患者" />
) : ( ) : (
<List <List
itemLayout="horizontal" itemLayout="horizontal"
...@@ -122,4 +120,3 @@ const DoctorQueuePage: React.FC = () => { ...@@ -122,4 +120,3 @@ const DoctorQueuePage: React.FC = () => {
}; };
export default DoctorQueuePage; export default DoctorQueuePage;
import React, { useState } from 'react'; 'use client';
import { Card, Calendar, Button, Modal, Form, TimePicker, InputNumber, Tag, Typography, Space, Row, Col, Badge, List } from 'antd';
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'; import { PlusOutlined, DeleteOutlined, CalendarOutlined } from '@ant-design/icons';
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import { doctorPortalApi, type ScheduleSlot } from '../../../api/doctorPortal';
const { Title, Text } = Typography; const { Text } = Typography;
interface ScheduleSlot {
id: string;
date: string;
start_time: string;
end_time: string;
max_count: number;
remaining: number;
}
const DoctorSchedulePage: React.FC = () => { const DoctorSchedulePage: React.FC = () => {
const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs()); const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [scheduleData, setScheduleData] = useState<Record<string, ScheduleSlot[]>>({});
const [form] = Form.useForm(); const [form] = Form.useForm();
// TODO: 从API获取真实数据 const fetchSchedule = async () => {
const scheduleData: Record<string, ScheduleSlot[]> = { setLoading(true);
'2026-02-25': [ try {
{ id: '1', date: '2026-02-25', start_time: '09:00', end_time: '12:00', max_count: 10, remaining: 5 }, const start = selectedDate.startOf('month').format('YYYY-MM-DD');
{ id: '2', date: '2026-02-25', start_time: '14:00', end_time: '17:00', max_count: 8, remaining: 3 }, const end = selectedDate.endOf('month').format('YYYY-MM-DD');
], const res = await doctorPortalApi.getMySchedule(start, end);
'2026-02-26': [ const grouped: Record<string, ScheduleSlot[]> = {};
{ id: '3', date: '2026-02-26', start_time: '09:00', end_time: '12:00', max_count: 10, remaining: 10 }, (res.data || []).forEach((slot) => {
], if (!grouped[slot.date]) grouped[slot.date] = [];
'2026-02-27': [ grouped[slot.date].push(slot);
{ id: '4', date: '2026-02-27', start_time: '14:00', end_time: '17:00', max_count: 6, remaining: 6 }, });
], setScheduleData(grouped);
} catch {
// 使用默认空数据
} finally {
setLoading(false);
}
}; };
useEffect(() => {
fetchSchedule();
}, [selectedDate.format('YYYY-MM')]);
const dateKey = selectedDate.format('YYYY-MM-DD'); const dateKey = selectedDate.format('YYYY-MM-DD');
const todaySchedule = scheduleData[dateKey] || []; const todaySchedule = scheduleData[dateKey] || [];
...@@ -43,7 +48,7 @@ const DoctorSchedulePage: React.FC = () => { ...@@ -43,7 +48,7 @@ const DoctorSchedulePage: React.FC = () => {
return ( return (
<Badge <Badge
status="success" status="success"
text={`${slots.length}涓椂娈礰} text={`${slots.length}个时段`}
style={{ fontSize: 10 }} style={{ fontSize: 10 }}
/> />
); );
...@@ -57,23 +62,33 @@ const DoctorSchedulePage: React.FC = () => { ...@@ -57,23 +62,33 @@ const DoctorSchedulePage: React.FC = () => {
}; };
const handleSaveSchedule = async (values: any) => { const handleSaveSchedule = async (values: any) => {
// TODO: 调用API保存排班 try {
console.log('保存排班:', { await doctorPortalApi.createSchedule([{
date: selectedDate.format('YYYY-MM-DD'), date: selectedDate.format('YYYY-MM-DD'),
start_time: values.time_range[0].format('HH:mm'), start_time: values.time_range[0].format('HH:mm'),
end_time: values.time_range[1].format('HH:mm'), end_time: values.time_range[1].format('HH:mm'),
max_count: values.max_count, max_count: values.max_count,
}); }]);
message.success('排班添加成功');
setIsModalOpen(false); setIsModalOpen(false);
fetchSchedule();
} catch {
message.error('添加排班失败');
}
}; };
const handleDeleteSlot = (slotId: string) => { const handleDeleteSlot = (slotId: string) => {
// TODO: 调用API删除排班
Modal.confirm({ Modal.confirm({
title: '确删除', title: '确认删除',
content: '确畾瑕佸垹闄ゆ排班无舵鍚楋紵', content: '确定要删除该排班吗?',
onOk: () => { onOk: async () => {
console.log('删除排班:', slotId); try {
await doctorPortalApi.deleteSchedule(slotId);
message.success('删除成功');
fetchSchedule();
} catch {
message.error('删除失败');
}
}, },
}); });
}; };
...@@ -85,6 +100,7 @@ const DoctorSchedulePage: React.FC = () => { ...@@ -85,6 +100,7 @@ const DoctorSchedulePage: React.FC = () => {
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={handleAddSchedule}>添加排班</Button> <Button size="small" type="primary" icon={<PlusOutlined />} onClick={handleAddSchedule}>添加排班</Button>
</div> </div>
<Spin spinning={loading}>
<Row gutter={12}> <Row gutter={12}>
<Col span={16}> <Col span={16}>
<Card size="small"> <Card size="small">
...@@ -99,7 +115,7 @@ const DoctorSchedulePage: React.FC = () => { ...@@ -99,7 +115,7 @@ const DoctorSchedulePage: React.FC = () => {
<Col span={8}> <Col span={8}>
<Card <Card
title={<span className="text-xs font-semibold"><CalendarOutlined className="mr-1" />{selectedDate.format('MM月DD日)} 排班</span>} 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>} extra={<Button type="link" size="small" icon={<PlusOutlined />} onClick={handleAddSchedule}>添加</Button>}
size="small" size="small"
> >
...@@ -116,7 +132,7 @@ const DoctorSchedulePage: React.FC = () => { ...@@ -116,7 +132,7 @@ const DoctorSchedulePage: React.FC = () => {
renderItem={(slot) => ( renderItem={(slot) => (
<List.Item <List.Item
actions={[ actions={[
<Button type="text" danger size="small" icon={<DeleteOutlined />} onClick={() => handleDeleteSlot(slot.id)} />, <Button type="text" danger size="small" icon={<DeleteOutlined />} onClick={() => handleDeleteSlot(slot.id!)} />,
]} ]}
> >
<div> <div>
...@@ -125,7 +141,7 @@ const DoctorSchedulePage: React.FC = () => { ...@@ -125,7 +141,7 @@ const DoctorSchedulePage: React.FC = () => {
</div> </div>
<Space size={4}> <Space size={4}>
<Tag color="blue">最大 {slot.max_count}</Tag> <Tag color="blue">最大 {slot.max_count}</Tag>
<Tag color={slot.remaining > 0 ? 'green' : 'red'}>剩余 {slot.remaining}</Tag> <Tag color={(slot.remaining ?? slot.max_count) > 0 ? 'green' : 'red'}>剩余 {slot.remaining ?? slot.max_count}</Tag>
</Space> </Space>
</div> </div>
</List.Item> </List.Item>
...@@ -135,6 +151,7 @@ const DoctorSchedulePage: React.FC = () => { ...@@ -135,6 +151,7 @@ const DoctorSchedulePage: React.FC = () => {
</Card> </Card>
</Col> </Col>
</Row> </Row>
</Spin>
<Modal <Modal
title={`添加排班 - ${selectedDate.format('YYYY-MM-DD')}`} title={`添加排班 - ${selectedDate.format('YYYY-MM-DD')}`}
...@@ -144,7 +161,7 @@ const DoctorSchedulePage: React.FC = () => { ...@@ -144,7 +161,7 @@ const DoctorSchedulePage: React.FC = () => {
width={400} width={400}
> >
<Form form={form} onFinish={handleSaveSchedule} layout="vertical" size="small"> <Form form={form} onFinish={handleSaveSchedule} layout="vertical" size="small">
<Form.Item name="time_range" label="时间段 rules={[{ required: true, message: '请选择时间段 }]}> <Form.Item name="time_range" label="时间段" rules={[{ required: true, message: '请选择时间段' }]}>
<TimePicker.RangePicker format="HH:mm" minuteStep={30} style={{ width: '100%' }} /> <TimePicker.RangePicker format="HH:mm" minuteStep={30} style={{ width: '100%' }} />
</Form.Item> </Form.Item>
<Form.Item name="max_count" label="最大接诊数" rules={[{ required: true, message: '请输入最大接诊数' }]} initialValue={10}> <Form.Item name="max_count" label="最大接诊数" rules={[{ required: true, message: '请输入最大接诊数' }]} initialValue={10}>
......
This diff is collapsed.
...@@ -32,26 +32,23 @@ const ConsultCreatePage: React.FC = () => { ...@@ -32,26 +32,23 @@ const ConsultCreatePage: React.FC = () => {
const [preConsult, setPreConsult] = useState<PreConsultResponse | null>(null); const [preConsult, setPreConsult] = useState<PreConsultResponse | null>(null);
const [form] = Form.useForm(); const [form] = Form.useForm();
const doctorId = searchParams.get('doctor_id') || ''; const doctorId = searchParams?.get('doctor_id') || '';
const consultType = (searchParams.get('type') || 'text') as 'text' | 'video'; const consultType = (searchParams?.get('type') || 'text') as 'text' | 'video';
const preConsultId = searchParams.get('pre_consult_id') || ''; const preConsultId = searchParams?.get('pre_consult_id') || '';
const chiefComplaintParam = searchParams.get('chief_complaint') || ''; const chiefComplaintParam = searchParams?.get('chief_complaint') || '';
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
setLoading(true); setLoading(true);
try { try {
// 获取医生信息
if (doctorId) { if (doctorId) {
const res = await doctorApi.getDoctorDetail(doctorId); const res = await doctorApi.getDoctorDetail(doctorId);
setDoctor(res.data); setDoctor(res.data);
} }
// 如果有预问诊ID,获取预问诊详情
if (preConsultId) { if (preConsultId) {
const res = await preConsultApi.getDetail(preConsultId); const res = await preConsultApi.getDetail(preConsultId);
setPreConsult(res.data); setPreConsult(res.data);
//
form.setFieldsValue({ form.setFieldsValue({
chief_complaint: res.data.chief_complaint || chiefComplaintParam, chief_complaint: res.data.chief_complaint || chiefComplaintParam,
}); });
...@@ -80,9 +77,8 @@ const ConsultCreatePage: React.FC = () => { ...@@ -80,9 +77,8 @@ const ConsultCreatePage: React.FC = () => {
pre_consult_id: preConsultId || undefined, pre_consult_id: preConsultId || undefined,
}); });
message.success('问诊创建成功!); message.success('问诊创建成功!');
//
if (consultType === 'video') { if (consultType === 'video') {
router.push(`/patient/consult/video/${res.data.id}`); router.push(`/patient/consult/video/${res.data.id}`);
} else { } else {
...@@ -90,7 +86,7 @@ const ConsultCreatePage: React.FC = () => { ...@@ -90,7 +86,7 @@ const ConsultCreatePage: React.FC = () => {
} }
} catch (error: any) { } catch (error: any) {
if (error?.errorFields) return; if (error?.errorFields) return;
message.error('创建问诊失败: ' + (error?.response?.data?.message || error?.message || '请稍后重试)); message.error('创建问诊失败: ' + (error?.response?.data?.message || error?.message || '请稍后重试'));
} finally { } finally {
setSubmitting(false); setSubmitting(false);
} }
...@@ -99,7 +95,7 @@ const ConsultCreatePage: React.FC = () => { ...@@ -99,7 +95,7 @@ const ConsultCreatePage: React.FC = () => {
if (loading) { if (loading) {
return ( return (
<div style={{ textAlign: 'center', padding: 80 }}> <div style={{ textAlign: 'center', padding: 80 }}>
<Spin size="large" tip="加载中.." /> <Spin size="large" tip="加载中..." />
</div> </div>
); );
} }
...@@ -127,7 +123,7 @@ const ConsultCreatePage: React.FC = () => { ...@@ -127,7 +123,7 @@ const ConsultCreatePage: React.FC = () => {
<Text type="secondary" className="text-xs!">{doctor.hospital}</Text> <Text type="secondary" className="text-xs!">{doctor.hospital}</Text>
<div className="mt-0.5"> <div className="mt-0.5">
<Tag color="orange">¥{(doctor.price / 100).toFixed(0)}/次</Tag> <Tag color="orange">¥{(doctor.price / 100).toFixed(0)}/次</Tag>
<Text type="secondary" className="text-xs!">评分 {doctor.rating} · {doctor.consult_count}?/Text> <Text type="secondary" className="text-xs!">评分 {doctor.rating} · {doctor.consult_count} 次问诊</Text>
</div> </div>
</div> </div>
</Space> </Space>
...@@ -154,8 +150,8 @@ const ConsultCreatePage: React.FC = () => { ...@@ -154,8 +150,8 @@ const ConsultCreatePage: React.FC = () => {
<Card size="small"> <Card size="small">
<Form form={form} layout="vertical" size="small"> <Form form={form} layout="vertical" size="small">
<Form.Item label="主诉(症状描述)" name="chief_complaint" rules={[{ required: true, message: '请描述您的症状 }]}> <Form.Item label="主诉(症状描述)" name="chief_complaint" rules={[{ required: true, message: '请描述您的症状' }]}>
<TextArea rows={3} placeholder="请详细描述您的症状.." maxLength={500} showCount /> <TextArea rows={3} placeholder="请详细描述您的症状..." maxLength={500} showCount />
</Form.Item> </Form.Item>
<Form.Item label="既往病史" name="medical_history"> <Form.Item label="既往病史" name="medical_history">
<TextArea rows={2} placeholder="如有慢性病、手术史等请填写" /> <TextArea rows={2} placeholder="如有慢性病、手术史等请填写" />
...@@ -165,7 +161,7 @@ const ConsultCreatePage: React.FC = () => { ...@@ -165,7 +161,7 @@ const ConsultCreatePage: React.FC = () => {
<Space size={8}> <Space size={8}>
<Button size="small" onClick={() => router.back()}>取消</Button> <Button size="small" onClick={() => router.back()}>取消</Button>
<Button type="primary" size="small" onClick={handleSubmit} loading={submitting}> <Button type="primary" size="small" onClick={handleSubmit} loading={submitting}>
{submitting ? '提交中..' : `发起${consultType === 'video' ? '视频' : '图文'}问诊`} {submitting ? '提交中...' : `发起${consultType === 'video' ? '视频' : '图文'}问诊`}
</Button> </Button>
</Space> </Space>
</Form.Item> </Form.Item>
...@@ -176,4 +172,3 @@ const ConsultCreatePage: React.FC = () => { ...@@ -176,4 +172,3 @@ const ConsultCreatePage: React.FC = () => {
}; };
export default ConsultCreatePage; export default ConsultCreatePage;
...@@ -14,10 +14,11 @@ import { useQuery } from '@tanstack/react-query'; ...@@ -14,10 +14,11 @@ import { useQuery } from '@tanstack/react-query';
import { doctorApi } from '../../../api/doctor'; import { doctorApi } from '../../../api/doctor';
import ConsultCreateModal from '../../../components/ConsultCreateModal'; import ConsultCreateModal from '../../../components/ConsultCreateModal';
const { Title, Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const DoctorDetailPage: React.FC = () => { const DoctorDetailPage: React.FC = () => {
const { id } = useParams<{ id: string }>(); const params = useParams<{ id: string }>();
const id = params?.id;
const [consultModalOpen, setConsultModalOpen] = useState(false); const [consultModalOpen, setConsultModalOpen] = useState(false);
const [consultType, setConsultType] = useState<'text' | 'video'>('text'); const [consultType, setConsultType] = useState<'text' | 'video'>('text');
...@@ -53,7 +54,7 @@ const DoctorDetailPage: React.FC = () => { ...@@ -53,7 +54,7 @@ const DoctorDetailPage: React.FC = () => {
<Rate disabled value={doctor.rating} style={{ fontSize: 12 }} /> <Rate disabled value={doctor.rating} style={{ fontSize: 12 }} />
<Text type="secondary" className="text-xs!">({doctor.rating.toFixed(1)})</Text> <Text type="secondary" className="text-xs!">({doctor.rating.toFixed(1)})</Text>
<Divider type="vertical" /> <Divider type="vertical" />
<Text type="secondary" className="text-xs!">问诊 {doctor.consult_count}</Text> <Text type="secondary" className="text-xs!">问诊 {doctor.consult_count}</Text>
</Space> </Space>
</Col> </Col>
<Col> <Col>
...@@ -82,13 +83,13 @@ const DoctorDetailPage: React.FC = () => { ...@@ -82,13 +83,13 @@ const DoctorDetailPage: React.FC = () => {
</Card> </Card>
<Card title={<span className="text-xs font-semibold">医生简介</span>} size="small"> <Card title={<span className="text-xs font-semibold">医生简介</span>} size="small">
<Paragraph className="text-xs! mb-0!">{doctor.introduction || '暂无简介}</Paragraph> <Paragraph className="text-xs! mb-0!">{doctor.introduction || '暂无简介'}</Paragraph>
</Card> </Card>
<Card title={<span className="text-xs font-semibold">排班信息</span>} <Card title={<span className="text-xs font-semibold">排班信息</span>}
extra={<Button type="link" size="small" icon={<CalendarOutlined />}>更多</Button>} size="small"> extra={<Button type="link" size="small" icon={<CalendarOutlined />}>更多</Button>} size="small">
<Descriptions column={2} size="small"> <Descriptions column={2} size="small">
<Descriptions.Item label="出诊状> <Descriptions.Item label="出诊状态">
{doctor.is_online ? <Tag color="green">在线</Tag> : <Tag color="default">未排班</Tag>} {doctor.is_online ? <Tag color="green">在线</Tag> : <Tag color="default">未排班</Tag>}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="资质认证"> <Descriptions.Item label="资质认证">
...@@ -97,7 +98,6 @@ const DoctorDetailPage: React.FC = () => { ...@@ -97,7 +98,6 @@ const DoctorDetailPage: React.FC = () => {
</Descriptions> </Descriptions>
</Card> </Card>
{/* 创建问诊弹窗 */}
<ConsultCreateModal <ConsultCreateModal
open={consultModalOpen} open={consultModalOpen}
onCancel={() => setConsultModalOpen(false)} onCancel={() => setConsultModalOpen(false)}
...@@ -109,4 +109,3 @@ const DoctorDetailPage: React.FC = () => { ...@@ -109,4 +109,3 @@ const DoctorDetailPage: React.FC = () => {
}; };
export default DoctorDetailPage; export default DoctorDetailPage;
...@@ -13,8 +13,8 @@ const { Text } = Typography; ...@@ -13,8 +13,8 @@ const { Text } = Typography;
const PatientDoctorsPage: React.FC = () => { const PatientDoctorsPage: React.FC = () => {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [keyword, setKeyword] = useState(searchParams.get('keyword') || ''); const [keyword, setKeyword] = useState(searchParams?.get('keyword') || '');
const [departmentId, setDepartmentId] = useState(searchParams.get('department') || ''); const [departmentId, setDepartmentId] = useState(searchParams?.get('department') || '');
const [sortBy, setSortBy] = useState<'rating' | 'consult_count' | 'price'>('rating'); const [sortBy, setSortBy] = useState<'rating' | 'consult_count' | 'price'>('rating');
const [consultModalOpen, setConsultModalOpen] = useState(false); const [consultModalOpen, setConsultModalOpen] = useState(false);
const [selectedDoctor, setSelectedDoctor] = useState<{ id: string; type: 'text' | 'video' } | null>(null); const [selectedDoctor, setSelectedDoctor] = useState<{ id: string; type: 'text' | 'video' } | null>(null);
...@@ -68,9 +68,9 @@ const PatientDoctorsPage: React.FC = () => { ...@@ -68,9 +68,9 @@ const PatientDoctorsPage: React.FC = () => {
style={{ width: 110 }} style={{ width: 110 }}
size="small" size="small"
options={[ options={[
{ label: '评分最高, value: 'rating' }, { label: '评分最高', value: 'rating' },
{ label: '问诊最, value: 'consult_count' }, { label: '问诊最', value: 'consult_count' },
{ label: '价格最, value: 'price' }, { label: '价格最', value: 'price' },
]} ]}
/> />
</div> </div>
...@@ -95,10 +95,10 @@ const PatientDoctorsPage: React.FC = () => { ...@@ -95,10 +95,10 @@ const PatientDoctorsPage: React.FC = () => {
<div className="text-xs text-gray-400 mb-1">{doctor.hospital}</div> <div className="text-xs text-gray-400 mb-1">{doctor.hospital}</div>
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<Rate disabled defaultValue={doctor.rating} className="text-xs!" /> <Rate disabled defaultValue={doctor.rating} className="text-xs!" />
<span className="text-[11px] text-gray-400">问诊 {doctor.consult_count}</span> <span className="text-[11px] text-gray-400">问诊 {doctor.consult_count}</span>
</div> </div>
<div className="text-xs text-gray-500 line-clamp-1"> <div className="text-xs text-gray-500 line-clamp-1">
擅长:{doctor.specialties?.join('?) || doctor.introduction} 擅长:{doctor.specialties?.join('') || doctor.introduction}
</div> </div>
</Col> </Col>
<Col> <Col>
...@@ -117,7 +117,7 @@ const PatientDoctorsPage: React.FC = () => { ...@@ -117,7 +117,7 @@ const PatientDoctorsPage: React.FC = () => {
</Space> </Space>
<div> <div>
<Button type="link" size="small" className="text-xs!" onClick={() => router.push(`/patient/doctors/${doctor.id}`)}> <Button type="link" size="small" className="text-xs!" onClick={() => router.push(`/patient/doctors/${doctor.id}`)}>
详情 ? 查看详情 &gt;
</Button> </Button>
</div> </div>
</div> </div>
...@@ -140,4 +140,3 @@ const PatientDoctorsPage: React.FC = () => { ...@@ -140,4 +140,3 @@ const PatientDoctorsPage: React.FC = () => {
}; };
export default PatientDoctorsPage; export default PatientDoctorsPage;
'use client'; 'use client';
import React from 'react'; import React, { useState, useEffect } from 'react';
import { Card, Typography, Button, Row, Col } from 'antd'; import { Card, Typography, Button, Row, Col, Table, Tag, Space, Empty, Tabs, Statistic, message } from 'antd';
import { PayCircleOutlined, WechatOutlined, AlipayOutlined, FileTextOutlined, HistoryOutlined } from '@ant-design/icons'; import {
PayCircleOutlined,
WalletOutlined,
FileTextOutlined,
HistoryOutlined,
CheckCircleOutlined,
ClockCircleOutlined,
} from '@ant-design/icons';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { consultApi, type Consultation } from '../../../api/consult';
import { prescriptionPatientApi, type Prescription } from '../../../api/prescription';
import dayjs from 'dayjs';
const { Text } = Typography; const { Text } = Typography;
const features = [ interface PaymentRecord {
{ icon: <WechatOutlined style={{ fontSize: 28, color: '#07c160' }} />, title: '微信/支付宝支付', desc: '便捷的在线支付方式' }, id: string;
{ icon: <PayCircleOutlined style={{ fontSize: 28, color: '#1677ff' }} />, title: '医保扣费', desc: '支持医保在线结算' }, type: string;
{ icon: <FileTextOutlined style={{ fontSize: 28, color: '#fa8c16' }} />, title: '账单明细', desc: '详细的费用清单' }, description: string;
{ icon: <HistoryOutlined style={{ fontSize: 28, color: '#722ed1' }} />, title: '支付记录', desc: '历史支付记录查询' }, amount: number;
]; status: 'paid' | 'pending' | 'refunded';
created_at: string;
}
const PatientPaymentPage: React.FC = () => { const PatientPaymentPage: React.FC = () => {
const router = useRouter(); const router = useRouter();
const [loading, setLoading] = useState(false);
const [consults, setConsults] = useState<Consultation[]>([]);
const [prescriptions, setPrescriptions] = useState<Prescription[]>([]);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const [consultRes, rxRes] = await Promise.allSettled([
consultApi.getConsultList(),
prescriptionPatientApi.getList({ page: 1, page_size: 20 }),
]);
if (consultRes.status === 'fulfilled') setConsults(consultRes.value.data || []);
if (rxRes.status === 'fulfilled') setPrescriptions(rxRes.value.data?.list || []);
} catch {
// 静默处理
} finally {
setLoading(false);
}
};
fetchData();
}, []);
const paymentRecords: PaymentRecord[] = [
...consults.map((c) => ({
id: c.id,
type: c.type === 'video' ? '视频问诊' : '图文问诊',
description: `${c.doctor_name} - ${c.chief_complaint?.substring(0, 20) || '问诊'}`,
amount: c.type === 'video' ? 10000 : 5000,
status: 'paid' as const,
created_at: c.created_at,
})),
...prescriptions.map((p) => ({
id: p.id,
type: '处方购药',
description: `处方 ${p.prescription_no} - ${p.diagnosis}`,
amount: p.total_amount,
status: (p.status === 'dispensed' || p.status === 'completed' ? 'paid' : 'pending') as 'paid' | 'pending',
created_at: p.created_at,
})),
].sort((a, b) => dayjs(b.created_at).unix() - dayjs(a.created_at).unix());
const totalPaid = paymentRecords.filter(r => r.status === 'paid').reduce((sum, r) => sum + r.amount, 0);
const totalPending = paymentRecords.filter(r => r.status === 'pending').reduce((sum, r) => sum + r.amount, 0);
const statusMap: Record<string, { color: string; label: string; icon: React.ReactNode }> = {
paid: { color: 'green', label: '已支付', icon: <CheckCircleOutlined /> },
pending: { color: 'orange', label: '待支付', icon: <ClockCircleOutlined /> },
refunded: { color: 'red', label: '已退款', icon: <PayCircleOutlined /> },
};
const columns = [
{
title: '时间', dataIndex: 'created_at', key: 'created_at', width: 140,
render: (v: string) => <Text className="text-xs!">{dayjs(v).format('MM-DD HH:mm')}</Text>,
},
{ title: '类型', dataIndex: 'type', key: 'type', width: 100, render: (v: string) => <Tag color="blue">{v}</Tag> },
{ title: '描述', dataIndex: 'description', key: 'description', ellipsis: true },
{
title: '金额', dataIndex: 'amount', key: 'amount', width: 100,
render: (v: number) => <Text strong style={{ color: '#f5222d' }}>¥{(v / 100).toFixed(2)}</Text>,
},
{
title: '状态', dataIndex: 'status', key: 'status', width: 90,
render: (v: string) => {
const s = statusMap[v];
return s ? <Tag color={s.color} icon={s.icon}>{s.label}</Tag> : <Tag>{v}</Tag>;
},
},
{
title: '操作', key: 'action', width: 80,
render: (_: any, r: PaymentRecord) => (
r.status === 'pending'
? <Button type="primary" size="small" onClick={() => message.info('支付功能对接中')}>去支付</Button>
: <Button type="link" size="small">详情</Button>
),
},
];
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-gray-800 m-0">在线支付</h4> <h4 className="text-sm font-bold text-gray-800 m-0">
<WalletOutlined className="mr-1" />支付管理
</h4>
<Button type="primary" size="small" onClick={() => router.push('/patient/consult')}>查看问诊</Button> <Button type="primary" size="small" onClick={() => router.push('/patient/consult')}>查看问诊</Button>
</div> </div>
<Row gutter={[8, 8]}>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #52c41a' }}>
<Statistic title="已支付总额" value={totalPaid / 100} precision={2} prefix="¥" valueStyle={{ color: '#52c41a' }} />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #fa8c16' }}>
<Statistic title="待支付" value={totalPending / 100} precision={2} prefix="¥" valueStyle={{ color: '#fa8c16' }} />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #1890ff' }}>
<Statistic title="问诊支付" value={consults.length} suffix="笔" />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #722ed1' }}>
<Statistic title="购药支付" value={prescriptions.length} suffix="笔" />
</Card>
</Col>
</Row>
<Card size="small"> <Card size="small">
<div className="text-center py-4"> <Tabs defaultActiveKey="all" size="small" items={[
<PayCircleOutlined className="text-4xl text-yellow-400 mb-2" /> {
<div className="text-xs text-gray-400">支付方式说明</div> key: 'all',
</div> label: <span><FileTextOutlined /> 全部记录</span>,
children: (
<Table
columns={columns}
dataSource={paymentRecords}
rowKey="id"
size="small"
loading={loading}
pagination={{ pageSize: 10, size: 'small' }}
locale={{ emptyText: <Empty description="暂无支付记录" /> }}
/>
),
},
{
key: 'pending',
label: <span><ClockCircleOutlined /> 待支付</span>,
children: (
<Table
columns={columns}
dataSource={paymentRecords.filter(r => r.status === 'pending')}
rowKey="id"
size="small"
loading={loading}
pagination={false}
locale={{ emptyText: <Empty description="暂无待支付订单" /> }}
/>
),
},
{
key: 'paid',
label: <span><CheckCircleOutlined /> 已支付</span>,
children: (
<Table
columns={columns}
dataSource={paymentRecords.filter(r => r.status === 'paid')}
rowKey="id"
size="small"
loading={loading}
pagination={{ pageSize: 10, size: 'small' }}
locale={{ emptyText: <Empty description="暂无支付记录" /> }}
/>
),
},
]} />
</Card>
<Card size="small" title={<span className="text-xs font-semibold">支付方式</span>}>
<Row gutter={[8, 8]}> <Row gutter={[8, 8]}>
{features.map((f) => ( {[
<Col xs={24} sm={12} key={f.title}> { icon: <PayCircleOutlined style={{ fontSize: 24, color: '#07c160' }} />, title: '微信支付', desc: '推荐' },
<Card size="small" className="bg-gray-50"> { icon: <PayCircleOutlined style={{ fontSize: 24, color: '#1677ff' }} />, title: '支付宝', desc: '便捷支付' },
<div className="flex items-center gap-2"> { icon: <WalletOutlined style={{ fontSize: 24, color: '#fa8c16' }} />, title: '医保支付', desc: '在线结算' },
{f.icon} ].map((item) => (
<div> <Col span={8} key={item.title}>
<div className="text-xs font-medium">{f.title}</div> <Card size="small" hoverable className="text-center">
<Text type="secondary" className="text-[11px]!">{f.desc}</Text> <div className="mb-1">{item.icon}</div>
</div> <div className="text-xs font-medium">{item.title}</div>
</div> <Text type="secondary" className="text-[11px]!">{item.desc}</Text>
</Card> </Card>
</Col> </Col>
))} ))}
...@@ -48,4 +211,3 @@ const PatientPaymentPage: React.FC = () => { ...@@ -48,4 +211,3 @@ const PatientPaymentPage: React.FC = () => {
}; };
export default PatientPaymentPage; export default PatientPaymentPage;
...@@ -24,7 +24,8 @@ const statusMap: Record<string, { text: string; color: string }> = { ...@@ -24,7 +24,8 @@ const statusMap: Record<string, { text: string; color: string }> = {
}; };
const PatientTextConsultPage: React.FC = () => { const PatientTextConsultPage: React.FC = () => {
const { id } = useParams<{ id: string }>(); const params = useParams<{ id: string }>();
const id = params?.id;
const router = useRouter(); const router = useRouter();
const { user } = useUserStore(); const { user } = useUserStore();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
......
...@@ -15,7 +15,8 @@ import { useVideoCall } from '../../../hooks/useVideoCall'; ...@@ -15,7 +15,8 @@ import { useVideoCall } from '../../../hooks/useVideoCall';
const { Title } = Typography; const { Title } = Typography;
const PatientVideoConsultPage: React.FC = () => { const PatientVideoConsultPage: React.FC = () => {
const { id } = useParams<{ id: string }>(); const params = useParams<{ id: string }>();
const id = params?.id;
const router = useRouter(); const router = useRouter();
const localVideoRef = useRef<HTMLVideoElement>(null); const localVideoRef = useRef<HTMLVideoElement>(null);
const remoteVideoRef = useRef<HTMLVideoElement>(null); const remoteVideoRef = useRef<HTMLVideoElement>(null);
......
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