Commit 859fb6ac authored by yuguo's avatar yuguo

fix

parent 6e652bf1
...@@ -37,7 +37,9 @@ ...@@ -37,7 +37,9 @@
"Bash(bash:*)", "Bash(bash:*)",
"Bash(make run:*)", "Bash(make run:*)",
"Bash(rm:*)", "Bash(rm:*)",
"Bash(gh pr:*)" "Bash(gh pr:*)",
"Bash(claude mcp:*)",
"WebSearch"
] ]
} }
} }
{
"mcpServers": {
"playwright": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@anthropic-ai/claude-code-mcp-playwright@latest"
],
"env": {}
},
"antd-components": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"antd-components-mcp@latest"
],
"env": {}
},
"figma": {
"type": "http",
"url": "https://mcp.figma.com/mcp"
},
"context7": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@upstash/context7-mcp@latest"
],
"env": {}
},
"21st-magic": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@21st-dev/magic@latest"
],
"env": {}
}
}
}
...@@ -14,14 +14,14 @@ func defaultAgentDefinitions() []model.AgentDefinition { ...@@ -14,14 +14,14 @@ func defaultAgentDefinitions() []model.AgentDefinition {
"query_symptom_knowledge", "recommend_department", "query_symptom_knowledge", "recommend_department",
"search_medical_knowledge", "query_drug", "search_medical_knowledge", "query_drug",
"query_medical_record", "generate_follow_up_plan", "query_medical_record", "generate_follow_up_plan",
"send_notification", "send_notification", "navigate_page",
}) })
// 医生通用智能体 — 合并 diagnosis + prescription + follow_up 能力 // 医生通用智能体 — 合并 diagnosis + prescription + follow_up 能力
doctorTools, _ := json.Marshal([]string{ doctorTools, _ := json.Marshal([]string{
"query_medical_record", "query_symptom_knowledge", "search_medical_knowledge", "query_medical_record", "query_symptom_knowledge", "search_medical_knowledge",
"query_drug", "check_drug_interaction", "check_contraindication", "calculate_dosage", "query_drug", "check_drug_interaction", "check_contraindication", "calculate_dosage",
"generate_follow_up_plan", "send_notification", "generate_follow_up_plan", "send_notification", "navigate_page",
}) })
// 管理员通用智能体 — 合并 admin_assistant + general 管理能力 // 管理员通用智能体 — 合并 admin_assistant + general 管理能力
...@@ -29,7 +29,7 @@ func defaultAgentDefinitions() []model.AgentDefinition { ...@@ -29,7 +29,7 @@ func defaultAgentDefinitions() []model.AgentDefinition {
"eval_expression", "query_workflow_status", "call_agent", "eval_expression", "query_workflow_status", "call_agent",
"trigger_workflow", "request_human_review", "trigger_workflow", "request_human_review",
"list_knowledge_collections", "send_notification", "list_knowledge_collections", "send_notification",
"query_drug", "search_medical_knowledge", "query_drug", "search_medical_knowledge", "navigate_page",
}) })
return []model.AgentDefinition{ return []model.AgentDefinition{
...@@ -52,7 +52,11 @@ func defaultAgentDefinitions() []model.AgentDefinition { ...@@ -52,7 +52,11 @@ func defaultAgentDefinitions() []model.AgentDefinition {
- 主动使用工具获取真实数据,不要凭空回答 - 主动使用工具获取真实数据,不要凭空回答
- 不做确定性诊断,只提供参考建议 - 不做确定性诊断,只提供参考建议
- 关注患者的用药依从性和健康状况变化 - 关注患者的用药依从性和健康状况变化
- 所有医疗建议仅供参考,请以专业医生判断为准`, - 所有医疗建议仅供参考,请以专业医生判断为准
页面导航能力:
- 你可以使用 navigate_page 工具打开系统页面,如找医生、我的问诊、处方、健康档案等
- 当用户想查看某个页面时,直接调用 navigate_page 工具导航到对应页面`,
Tools: string(patientTools), Tools: string(patientTools),
Config: "{}", Config: "{}",
MaxIterations: 10, MaxIterations: 10,
...@@ -80,7 +84,11 @@ func defaultAgentDefinitions() []model.AgentDefinition { ...@@ -80,7 +84,11 @@ func defaultAgentDefinitions() []model.AgentDefinition {
- 基于循证医学原则提供建议 - 基于循证医学原则提供建议
- 主动使用工具获取真实数据 - 主动使用工具获取真实数据
- 对存在风险的处方要明确指出 - 对存在风险的处方要明确指出
- 所有建议仅供医生参考,请结合临床实际情况`, - 所有建议仅供医生参考,请结合临床实际情况
页面导航能力:
- 你可以使用 navigate_page 工具打开系统页面,如工作台、问诊大厅、患者档案、排班管理等
- 当医生想查看某个页面时,直接调用 navigate_page 工具导航到对应页面`,
Tools: string(doctorTools), Tools: string(doctorTools),
Config: "{}", Config: "{}",
MaxIterations: 10, MaxIterations: 10,
...@@ -106,7 +114,12 @@ func defaultAgentDefinitions() []model.AgentDefinition { ...@@ -106,7 +114,12 @@ func defaultAgentDefinitions() []model.AgentDefinition {
- 以简洁专业的方式回答管理员的问题 - 以简洁专业的方式回答管理员的问题
- 主动使用工具获取真实数据 - 主动使用工具获取真实数据
- 提供可操作的建议和方案 - 提供可操作的建议和方案
- 用中文回答`, - 用中文回答
页面导航能力:
- 你可以使用 navigate_page 工具打开系统页面,如医生管理、患者管理、科室管理、问诊管理等
- 当管理员想查看或操作某个页面时,直接调用 navigate_page 工具导航到对应页面
- 支持 open_add 操作自动打开新增弹窗(如新增医生、新增科室等)`,
Tools: string(adminTools), Tools: string(adminTools),
Config: "{}", Config: "{}",
MaxIterations: 10, MaxIterations: 10,
......
...@@ -150,11 +150,7 @@ func (t *NavigatePageTool) Execute(ctx context.Context, params map[string]interf ...@@ -150,11 +150,7 @@ func (t *NavigatePageTool) Execute(ctx context.Context, params map[string]interf
route = fmt.Sprintf("%s/edit/%s", basePath, id) route = fmt.Sprintf("%s/edit/%s", basePath, id)
} }
case "open_add": case "open_add":
parentID := id route = fmt.Sprintf("%s?action=add", basePath)
if parentID == "" {
parentID = "0"
}
route = fmt.Sprintf("%s/add/%s", basePath, parentID)
} }
// 返回标准化导航指令 // 返回标准化导航指令
......
...@@ -333,7 +333,7 @@ export const adminApi = { ...@@ -333,7 +333,7 @@ export const adminApi = {
post<null>(`/admin/departments/${id}/delete`), post<null>(`/admin/departments/${id}/delete`),
// === 系统日志 === // === 系统日志 ===
getSystemLogs: (params: { page?: number; page_size?: number }) => getSystemLogs: (params: { keyword?: string; start_date?: string; end_date?: string; page?: number; page_size?: number }) =>
get<PaginatedResponse<SystemLog>>('/admin/logs', { params }), get<PaginatedResponse<SystemLog>>('/admin/logs', { params }),
// === 问诊管理 === // === 问诊管理 ===
......
...@@ -281,7 +281,7 @@ export const workflowApi = { ...@@ -281,7 +281,7 @@ export const workflowApi = {
update: (id: number, data: Partial<WorkflowCreateParams>) => update: (id: number, data: Partial<WorkflowCreateParams>) =>
put<unknown>(`/admin/workflows/${id}`, data), put<unknown>(`/admin/workflows/${id}`, data),
delete: (id: number) => del<null>(`/admin/workflows/${id}`), delete: (workflowId: string | number) => del<null>(`/workflow/${workflowId}`),
publish: (id: number) => put<null>(`/admin/workflows/${id}/publish`, {}), publish: (id: number) => put<null>(`/admin/workflows/${id}/publish`, {}),
......
import { get, post, put, del } from './request'; import { get, post, put, del } from './request';
import type { Consultation, ConsultMessage } from './consult'; import type { Consultation, ConsultMessage } from './consult';
import type { ToolCall } from './agent'; import type { ToolCall } from './agent';
import type { Prescription } from './prescription';
// ==================== 医生端 API ==================== // ==================== 医生端 API ====================
...@@ -190,7 +191,21 @@ export const doctorPortalApi = { ...@@ -190,7 +191,21 @@ export const doctorPortalApi = {
getPatientDetail: (patientId: string) => getPatientDetail: (patientId: string) =>
get<PatientDetail>(`/doctor-portal/patient/${patientId}/detail`), get<PatientDetail>(`/doctor-portal/patient/${patientId}/detail`),
// === 处方安全审核 === // === 处方管理 ===
createPrescription: (data: {
consult_id: string;
patient_id: string;
diagnosis: string;
remark?: string;
items: { medicine_name: string; specification: string; dosage: string; usage: string; frequency: string; days: number; quantity: number; unit: string; amount: number }[];
}) => post<Prescription>('/doctor-portal/prescription/create', data),
getDoctorPrescriptions: (params?: { page?: number; page_size?: number }) =>
get<{ list: Prescription[]; total: number }>('/doctor-portal/prescriptions', { params }),
getPrescriptionDetail: (id: string) =>
get<Prescription>(`/doctor-portal/prescription/${id}`),
checkPrescriptionSafety: (params: { patient_id: string; drugs: string[] }) => checkPrescriptionSafety: (params: { patient_id: string; drugs: string[] }) =>
post<{ post<{
report: string; report: string;
......
import request from './request'; import { get, put } from './request';
export interface Notification { export interface Notification {
id: string; id: string;
...@@ -13,14 +13,14 @@ export interface Notification { ...@@ -13,14 +13,14 @@ export interface Notification {
export const notificationApi = { export const notificationApi = {
list: (params?: { page?: number; page_size?: number }) => list: (params?: { page?: number; page_size?: number }) =>
request.get<{ list: Notification[]; total: number }>('/notifications', { params }), get<{ list: Notification[]; total: number }>('/notifications', { params }),
markRead: (id: string) => markRead: (id: string) =>
request.put(`/notifications/${id}/read`, {}), put(`/notifications/${id}/read`, {}),
markAllRead: () => markAllRead: () =>
request.put('/notifications/read-all', {}), put('/notifications/read-all', {}),
getUnreadCount: () => getUnreadCount: () =>
request.get<{ count: number }>('/notifications/unread-count'), get<{ count: number }>('/notifications/unread-count'),
}; };
// 支付相关API // 支付相关API
import request from './request'; import { get, post } from './request';
export interface PaymentOrder { export interface PaymentOrder {
id: string; id: string;
...@@ -60,57 +60,57 @@ export interface WithdrawalRecord { ...@@ -60,57 +60,57 @@ export interface WithdrawalRecord {
export const paymentApi = { export const paymentApi = {
// 创建支付订单 // 创建支付订单
createOrder: (data: { order_type: string; related_id: string; amount: number }) => createOrder: (data: { order_type: string; related_id: string; amount: number }) =>
request.post<PaymentOrder>('/payment/order/create', data), post<PaymentOrder>('/payment/order/create', data),
// 获取订单详情 // 获取订单详情
getOrderDetail: (orderId: string) => getOrderDetail: (orderId: string) =>
request.get<PaymentOrder>(`/payment/order/${orderId}`), get<PaymentOrder>(`/payment/order/${orderId}`),
// 获取订单列表 // 获取订单列表
getOrderList: (params?: { status?: string; page?: number; page_size?: number }) => getOrderList: (params?: { status?: string; page?: number; page_size?: number }) =>
request.get<{ list: PaymentOrder[]; total: number }>('/payment/orders', { params }), get<{ list: PaymentOrder[]; total: number }>('/payment/orders', { params }),
// 支付订单 // 支付订单
payOrder: (orderId: string, paymentMethod: string) => payOrder: (orderId: string, paymentMethod: string) =>
request.post<{ order_id: string; status: string; transaction_id: string; paid_at: string }>( post<{ order_id: string; status: string; transaction_id: string; paid_at: string }>(
`/payment/order/${orderId}/pay`, `/payment/order/${orderId}/pay`,
{ payment_method: paymentMethod } { payment_method: paymentMethod }
), ),
// 查询支付状态 // 查询支付状态
getPaymentStatus: (orderId: string) => getPaymentStatus: (orderId: string) =>
request.get<{ status: string }>(`/payment/order/${orderId}/status`), get<{ status: string }>(`/payment/order/${orderId}/status`),
// 取消订单 // 取消订单
cancelOrder: (orderId: string) => cancelOrder: (orderId: string) =>
request.post(`/payment/order/${orderId}/cancel`), post(`/payment/order/${orderId}/cancel`),
// 确认收货 // 确认收货
confirmDelivery: (orderId: string) => confirmDelivery: (orderId: string) =>
request.post(`/payment/order/${orderId}/confirm-delivery`, {}), post(`/payment/order/${orderId}/confirm-delivery`, {}),
}; };
// 医生端收入API // 医生端收入API
export const incomeApi = { export const incomeApi = {
// 获取收入统计 // 获取收入统计
getStats: () => getStats: () =>
request.get<IncomeStats>('/doctor/income/stats'), get<IncomeStats>('/doctor/income/stats'),
// 获取收入记录 // 获取收入记录
getRecords: (params?: { start_date?: string; end_date?: string; page?: number; page_size?: number }) => getRecords: (params?: { start_date?: string; end_date?: string; page?: number; page_size?: number }) =>
request.get<{ list: IncomeRecord[]; total: number }>('/doctor/income/records', { params }), get<{ list: IncomeRecord[]; total: number }>('/doctor/income/records', { params }),
// 获取月度账单 // 获取月度账单
getMonthlyBills: () => getMonthlyBills: () =>
request.get<MonthlyBill[]>('/doctor/income/monthly'), get<MonthlyBill[]>('/doctor/income/monthly'),
// 申请提现 // 申请提现
requestWithdraw: (data: { amount: number; bank_account?: string; bank_name?: string }) => requestWithdraw: (data: { amount: number; bank_account?: string; bank_name?: string }) =>
request.post<WithdrawalRecord>('/doctor/income/withdraw', data), post<WithdrawalRecord>('/doctor/income/withdraw', data),
// 获取提现记录 // 获取提现记录
getWithdrawals: (params?: { page?: number; page_size?: number }) => getWithdrawals: (params?: { page?: number; page_size?: number }) =>
request.get<{ list: WithdrawalRecord[]; total: number }>('/doctor/income/withdrawals', { params }), get<{ list: WithdrawalRecord[]; total: number }>('/doctor/income/withdrawals', { params }),
}; };
export default paymentApi; export default paymentApi;
'use client'; 'use client';
import React, { useState, useEffect, useMemo, useCallback } from 'react'; import React, { Suspense, useState, useEffect, useMemo, useCallback } from 'react';
import { useRouter, usePathname } from 'next/navigation'; import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { Layout, Menu, Avatar, Dropdown, Badge, Space, Typography, Tag, Spin, Popover, List } from 'antd'; import { Layout, Menu, Avatar, Dropdown, Badge, Space, Typography, Tag, Spin, Popover, List } from 'antd';
import { import {
DashboardOutlined, UserOutlined, TeamOutlined, ApartmentOutlined, DashboardOutlined, UserOutlined, TeamOutlined, ApartmentOutlined,
...@@ -91,8 +91,18 @@ function findOpenKeys(menus: MenuType[], targetPath: string): string[] { ...@@ -91,8 +91,18 @@ function findOpenKeys(menus: MenuType[], targetPath: string): string[] {
} }
export default function AdminLayout({ children }: { children: React.ReactNode }) { export default function AdminLayout({ children }: { children: React.ReactNode }) {
return (
<Suspense fallback={<div style={{ minHeight: '100vh', background: '#f5f6fa' }} />}>
<AdminLayoutInner>{children}</AdminLayoutInner>
</Suspense>
);
}
function AdminLayoutInner({ children }: { children: React.ReactNode }) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams();
const isEmbed = searchParams.get('embed') === '1';
const { user, logout } = useUserStore(); const { user, logout } = useUserStore();
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [dynamicMenus, setDynamicMenus] = useState<MenuType[]>([]); const [dynamicMenus, setDynamicMenus] = useState<MenuType[]>([]);
...@@ -119,24 +129,31 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) ...@@ -119,24 +129,31 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
notificationApi.markAllRead().then(() => { setUnreadCount(0); setNotifications(n => n.map(i => ({ ...i, is_read: true }))); }).catch(() => {}); notificationApi.markAllRead().then(() => { setUnreadCount(0); setNotifications(n => n.map(i => ({ ...i, is_read: true }))); }).catch(() => {});
}; };
// 从 API 获取动态菜单 // 从 API 获取动态菜单(依赖 user,确保登录后才加载)
useEffect(() => { useEffect(() => {
if (!user) {
setMenuLoading(false);
return;
}
let cancelled = false; let cancelled = false;
setMenuLoading(true); setMenuLoading(true);
myMenuApi.getMenus() myMenuApi.getMenus()
.then(res => { .then(res => {
if (!cancelled) setDynamicMenus(res.data || []); if (!cancelled) setDynamicMenus(res.data || []);
}) })
.catch(() => { /* 静默失败,使用空菜单 */ }) .catch((err) => {
console.error('加载菜单失败:', err);
})
.finally(() => { if (!cancelled) setMenuLoading(false); }); .finally(() => { if (!cancelled) setMenuLoading(false); });
return () => { cancelled = true; }; return () => { cancelled = true; };
}, []); }, [user]);
// 转换为 Ant Design menu items // 转换为 Ant Design menu items
const menuItems = useMemo(() => convertMenuTree(dynamicMenus), [dynamicMenus]); const menuItems = useMemo(() => convertMenuTree(dynamicMenus), [dynamicMenus]);
// 监听 AI 助手导航事件 // 监听 AI 助手导航事件(embed 模式下跳过,避免 iframe 内拦截)
useEffect(() => { useEffect(() => {
if (isEmbed) return;
const handleAIAction = (e: Event) => { const handleAIAction = (e: Event) => {
const detail = (e as CustomEvent).detail; const detail = (e as CustomEvent).detail;
if (detail?.action === 'navigate' && typeof detail.page === 'string') { if (detail?.action === 'navigate' && typeof detail.page === 'string') {
...@@ -147,7 +164,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) ...@@ -147,7 +164,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
}; };
window.addEventListener('ai-action', handleAIAction); window.addEventListener('ai-action', handleAIAction);
return () => window.removeEventListener('ai-action', handleAIAction); return () => window.removeEventListener('ai-action', handleAIAction);
}, [router]); }, [router, isEmbed]);
const userMenuItems = [ const userMenuItems = [
{ key: 'profile', icon: <UserOutlined />, label: '个人信息' }, { key: 'profile', icon: <UserOutlined />, label: '个人信息' },
...@@ -178,6 +195,10 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) ...@@ -178,6 +195,10 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
return findOpenKeys(dynamicMenus, currentPath); return findOpenKeys(dynamicMenus, currentPath);
}, [dynamicMenus, currentPath, collapsed]); }, [dynamicMenus, currentPath, collapsed]);
if (isEmbed) {
return <div style={{ minHeight: '100vh', background: '#f5f6fa' }}>{children}</div>;
}
return ( return (
<Layout style={{ minHeight: '100vh' }}> <Layout style={{ minHeight: '100vh' }}>
<Sider <Sider
...@@ -219,6 +240,11 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) ...@@ -219,6 +240,11 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
</div> </div>
{/* Menu */} {/* Menu */}
{menuLoading ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: '40px 0' }}>
<Spin size="small" />
</div>
) : (
<Menu <Menu
mode="inline" mode="inline"
selectedKeys={getSelectedKeys()} selectedKeys={getSelectedKeys()}
...@@ -227,6 +253,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) ...@@ -227,6 +253,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
onClick={handleMenuClick} onClick={handleMenuClick}
style={{ border: 'none', padding: '8px 0' }} style={{ border: 'none', padding: '8px 0' }}
/> />
)}
</Sider> </Sider>
<Layout style={{ marginLeft: collapsed ? 64 : 220, transition: 'margin-left 0.2s' }}> <Layout style={{ marginLeft: collapsed ? 64 : 220, transition: 'margin-left 0.2s' }}>
......
...@@ -138,7 +138,7 @@ export default function SafetyPage() { ...@@ -138,7 +138,7 @@ export default function SafetyPage() {
{ {
title: '敏感词/正则', title: '敏感词/正则',
dataIndex: 'word', dataIndex: 'word',
render: (v, r) => ( render: (v: string, r: SafetyRule) => (
<Space> <Space>
<Text code>{v}</Text> <Text code>{v}</Text>
{r.is_regex && <Tag color="purple">正则</Tag>} {r.is_regex && <Tag color="purple">正则</Tag>}
...@@ -149,7 +149,7 @@ export default function SafetyPage() { ...@@ -149,7 +149,7 @@ export default function SafetyPage() {
title: '分类', title: '分类',
dataIndex: 'category', dataIndex: 'category',
width: 100, width: 100,
render: (v) => { render: (v: string) => {
const map: Record<string, string> = { const map: Record<string, string> = {
injection: 'red', medical_claim: 'orange', drug_promotion: 'yellow', injection: 'red', medical_claim: 'orange', drug_promotion: 'yellow',
privacy: 'blue', toxicity: 'red', privacy: 'blue', toxicity: 'red',
...@@ -165,7 +165,7 @@ export default function SafetyPage() { ...@@ -165,7 +165,7 @@ export default function SafetyPage() {
title: '级别', title: '级别',
dataIndex: 'level', dataIndex: 'level',
width: 80, width: 80,
render: (v) => { render: (v: string) => {
const map: Record<string, string> = { block: 'red', warn: 'orange', replace: 'blue' }; const map: Record<string, string> = { block: 'red', warn: 'orange', replace: 'blue' };
const labels: Record<string, string> = { block: '拦截', warn: '警告', replace: '替换' }; const labels: Record<string, string> = { block: '拦截', warn: '警告', replace: '替换' };
return <Tag color={map[v] ?? 'default'}>{labels[v] ?? v}</Tag>; return <Tag color={map[v] ?? 'default'}>{labels[v] ?? v}</Tag>;
...@@ -175,7 +175,7 @@ export default function SafetyPage() { ...@@ -175,7 +175,7 @@ export default function SafetyPage() {
title: '方向', title: '方向',
dataIndex: 'direction', dataIndex: 'direction',
width: 90, width: 90,
render: (v) => { render: (v: string) => {
const labels: Record<string, string> = { input: '输入', output: '输出', both: '双向' }; const labels: Record<string, string> = { input: '输入', output: '输出', both: '双向' };
return <Tag>{labels[v] ?? v}</Tag>; return <Tag>{labels[v] ?? v}</Tag>;
}, },
...@@ -184,12 +184,12 @@ export default function SafetyPage() { ...@@ -184,12 +184,12 @@ export default function SafetyPage() {
title: '状态', title: '状态',
dataIndex: 'status', dataIndex: 'status',
width: 80, width: 80,
render: (v) => <Badge status={v === 'active' ? 'success' : 'default'} text={v === 'active' ? '启用' : '禁用'} />, render: (v: string) => <Badge status={v === 'active' ? 'success' : 'default'} text={v === 'active' ? '启用' : '禁用'} />,
}, },
{ {
title: '操作', title: '操作',
width: 120, width: 120,
render: (_, record) => ( render: (_: unknown, record: SafetyRule) => (
<Space> <Space>
<Button size="small" icon={<EditOutlined />} onClick={() => openModal(record)}>编辑</Button> <Button size="small" icon={<EditOutlined />} onClick={() => openModal(record)}>编辑</Button>
<Popconfirm title="确认删除?" onConfirm={() => deleteMutation.mutate(record.id)}> <Popconfirm title="确认删除?" onConfirm={() => deleteMutation.mutate(record.id)}>
...@@ -201,21 +201,21 @@ export default function SafetyPage() { ...@@ -201,21 +201,21 @@ export default function SafetyPage() {
]; ];
const logColumns: ColumnsType<SafetyLog> = [ const logColumns: ColumnsType<SafetyLog> = [
{ title: 'TraceID', dataIndex: 'trace_id', width: 180, render: (v) => <Text code style={{ fontSize: 11 }}>{v?.slice(0, 16)}...</Text> }, { title: 'TraceID', dataIndex: 'trace_id', width: 180, render: (v: string) => <Text code style={{ fontSize: 11 }}>{v?.slice(0, 16)}...</Text> },
{ title: '方向', dataIndex: 'direction', width: 80, render: (v) => <Tag>{v === 'input' ? '输入' : '输出'}</Tag> }, { title: '方向', dataIndex: 'direction', width: 80, render: (v: string) => <Tag>{v === 'input' ? '输入' : '输出'}</Tag> },
{ {
title: '动作', title: '动作',
dataIndex: 'action', dataIndex: 'action',
width: 80, width: 80,
render: (v) => { render: (v: string) => {
const map: Record<string, string> = { blocked: 'red', replaced: 'blue', warned: 'orange', passed: 'green' }; const map: Record<string, string> = { blocked: 'red', replaced: 'blue', warned: 'orange', passed: 'green' };
const labels: Record<string, string> = { blocked: '拦截', replaced: '替换', warned: '警告', passed: '通过' }; const labels: Record<string, string> = { blocked: '拦截', replaced: '替换', warned: '警告', passed: '通过' };
return <Tag color={map[v] ?? 'default'}>{labels[v] ?? v}</Tag>; return <Tag color={map[v] ?? 'default'}>{labels[v] ?? v}</Tag>;
}, },
}, },
{ title: '原始内容', dataIndex: 'original_text', ellipsis: true }, { title: '原始内容', dataIndex: 'original_text', ellipsis: true },
{ title: '时间', dataIndex: 'created_at', width: 140, render: (v) => dayjs(v).format('MM-DD HH:mm:ss') }, { title: '时间', dataIndex: 'created_at', width: 140, render: (v: string) => dayjs(v).format('MM-DD HH:mm:ss') },
]; ] as ColumnsType<SafetyLog>;
return ( return (
<div style={{ padding: 24 }}> <div style={{ padding: 24 }}>
...@@ -270,9 +270,9 @@ export default function SafetyPage() { ...@@ -270,9 +270,9 @@ export default function SafetyPage() {
key: 'rules', key: 'rules',
label: '规则管理', label: '规则管理',
children: ( children: (
<Table <Table<SafetyRule>
columns={ruleColumns} columns={ruleColumns}
dataSource={rulesData?.list ?? []} dataSource={(rulesData?.list ?? []) as SafetyRule[]}
rowKey="id" rowKey="id"
pagination={{ pagination={{
current: rulesPage, current: rulesPage,
...@@ -288,9 +288,9 @@ export default function SafetyPage() { ...@@ -288,9 +288,9 @@ export default function SafetyPage() {
key: 'logs', key: 'logs',
label: '过滤日志', label: '过滤日志',
children: ( children: (
<Table <Table<SafetyLog>
columns={logColumns} columns={logColumns}
dataSource={logsData?.list ?? []} dataSource={(logsData?.list ?? []) as SafetyLog[]}
rowKey="id" rowKey="id"
pagination={{ pagination={{
current: logsPage, current: logsPage,
......
...@@ -17,7 +17,7 @@ import SkillsTab from './SkillsTab'; ...@@ -17,7 +17,7 @@ import SkillsTab from './SkillsTab';
const { Text } = Typography; const { Text } = Typography;
export const CATEGORY_CONFIG: Record<string, { color: string; label: string; icon: React.ReactNode }> = { const CATEGORY_CONFIG: Record<string, { color: string; label: string; icon: React.ReactNode }> = {
knowledge: { color: 'blue', label: '知识库', icon: <BookOutlined /> }, knowledge: { color: 'blue', label: '知识库', icon: <BookOutlined /> },
recommendation: { color: 'cyan', label: '智能推荐', icon: <ThunderboltOutlined /> }, recommendation: { color: 'cyan', label: '智能推荐', icon: <ThunderboltOutlined /> },
medical: { color: 'purple', label: '病历管理', icon: <HeartOutlined /> }, medical: { color: 'purple', label: '病历管理', icon: <HeartOutlined /> },
......
...@@ -142,7 +142,7 @@ export default function WorkflowsPage() { ...@@ -142,7 +142,7 @@ export default function WorkflowsPage() {
}}>激活</Button> }}>激活</Button>
)} )}
<Popconfirm title="确认删除?" onConfirm={async () => { <Popconfirm title="确认删除?" onConfirm={async () => {
await workflowApi.delete(r.id); await workflowApi.delete(r.workflow_id);
message.success('删除成功'); message.success('删除成功');
fetchWorkflows(); fetchWorkflows();
}}> }}>
......
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React, { Suspense, useState, useEffect } from 'react';
import { useRouter, usePathname } from 'next/navigation'; import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { Layout, Menu, Avatar, Dropdown, Badge, Space, Switch, Typography, Tag, Popover, List } from 'antd'; import { Layout, Menu, Avatar, Dropdown, Badge, Space, Switch, Typography, Tag, Popover, List } from 'antd';
import { import {
DashboardOutlined, UserOutlined, MessageOutlined, CalendarOutlined, DashboardOutlined, UserOutlined, MessageOutlined, CalendarOutlined,
...@@ -28,8 +28,18 @@ const menuItems = [ ...@@ -28,8 +28,18 @@ const menuItems = [
]; ];
export default function DoctorLayout({ children }: { children: React.ReactNode }) { export default function DoctorLayout({ children }: { children: React.ReactNode }) {
return (
<Suspense fallback={<div style={{ minHeight: '100vh', background: '#f5f8ff' }} />}>
<DoctorLayoutInner>{children}</DoctorLayoutInner>
</Suspense>
);
}
function DoctorLayoutInner({ children }: { children: React.ReactNode }) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams();
const isEmbed = searchParams.get('embed') === '1';
const { user, logout } = useUserStore(); const { user, logout } = useUserStore();
const [isOnline, setIsOnline] = useState(false); const [isOnline, setIsOnline] = useState(false);
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
...@@ -89,6 +99,10 @@ export default function DoctorLayout({ children }: { children: React.ReactNode } ...@@ -89,6 +99,10 @@ export default function DoctorLayout({ children }: { children: React.ReactNode }
return match ? [match.key] : []; return match ? [match.key] : [];
}; };
if (isEmbed) {
return <div style={{ minHeight: '100vh', background: '#f5f8ff' }}>{children}</div>;
}
return ( return (
<Layout style={{ minHeight: '100vh' }}> <Layout style={{ minHeight: '100vh' }}>
<Sider <Sider
......
'use client';
import { Suspense } from 'react';
import { useSearchParams } from 'next/navigation';
import GlobalAIFloat from '@/components/GlobalAIFloat'; import GlobalAIFloat from '@/components/GlobalAIFloat';
function AIFloatGuard() {
const searchParams = useSearchParams();
const isEmbed = searchParams.get('embed') === '1';
if (isEmbed) return null;
return <GlobalAIFloat />;
}
export default function MainLayout({ children }: { children: React.ReactNode }) { export default function MainLayout({ children }: { children: React.ReactNode }) {
return ( return (
<div className="compact-ui"> <div className="compact-ui">
{children} {children}
<GlobalAIFloat /> <Suspense fallback={null}>
<AIFloatGuard />
</Suspense>
</div> </div>
); );
} }
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React, { Suspense, useState, useEffect } from 'react';
import { useRouter, usePathname } from 'next/navigation'; import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { Layout, Menu, Avatar, Dropdown, Button, Badge, Space, Typography, Tag, Popover, List } from 'antd'; import { Layout, Menu, Avatar, Dropdown, Button, Badge, Space, Typography, Tag, Popover, List } from 'antd';
import { import {
HomeOutlined, UserOutlined, MedicineBoxOutlined, FileTextOutlined, HomeOutlined, UserOutlined, MedicineBoxOutlined, FileTextOutlined,
...@@ -50,8 +50,18 @@ const allRouteKeys = [ ...@@ -50,8 +50,18 @@ const allRouteKeys = [
]; ];
export default function PatientLayout({ children }: { children: React.ReactNode }) { export default function PatientLayout({ children }: { children: React.ReactNode }) {
return (
<Suspense fallback={<div style={{ minHeight: '100vh', background: '#f5f8ff' }} />}>
<PatientLayoutInner>{children}</PatientLayoutInner>
</Suspense>
);
}
function PatientLayoutInner({ children }: { children: React.ReactNode }) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams();
const isEmbed = searchParams.get('embed') === '1';
const { user, logout } = useUserStore(); const { user, logout } = useUserStore();
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [unreadCount, setUnreadCount] = useState(0); const [unreadCount, setUnreadCount] = useState(0);
...@@ -112,6 +122,10 @@ export default function PatientLayout({ children }: { children: React.ReactNode ...@@ -112,6 +122,10 @@ export default function PatientLayout({ children }: { children: React.ReactNode
return keys; return keys;
}; };
if (isEmbed) {
return <div style={{ minHeight: '100vh', background: '#f5f8ff' }}>{children}</div>;
}
return ( return (
<Layout style={{ minHeight: '100vh' }}> <Layout style={{ minHeight: '100vh' }}>
<Sider <Sider
......
'use client'; 'use client';
import React, { useCallback, useMemo, useState, useEffect } from 'react'; import React, { useCallback, useMemo, useState, useEffect } from 'react';
import { Tooltip } from 'antd'; import { Tooltip, Spin } from 'antd';
import { import {
RobotOutlined, RobotOutlined,
CloseOutlined, CloseOutlined,
...@@ -18,6 +18,7 @@ import { useAIAssistStore } from '../../store/aiAssistStore'; ...@@ -18,6 +18,7 @@ import { useAIAssistStore } from '../../store/aiAssistStore';
import ChatPanel from './ChatPanel'; import ChatPanel from './ChatPanel';
import type { WidgetRole } from './types'; import type { WidgetRole } from './types';
import { ROLE_THEME } from './types'; import { ROLE_THEME } from './types';
import { getRouteByPath } from '../../config/routes';
const DRAG_HANDLE_CLASS = 'ai-widget-drag-handle'; const DRAG_HANDLE_CLASS = 'ai-widget-drag-handle';
...@@ -31,6 +32,7 @@ const FloatContainer: React.FC = () => { ...@@ -31,6 +32,7 @@ const FloatContainer: React.FC = () => {
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
// 嵌入的业务页面URL(分屏模式) // 嵌入的业务页面URL(分屏模式)
const [embeddedUrl, setEmbeddedUrl] = useState<string | null>(null); const [embeddedUrl, setEmbeddedUrl] = useState<string | null>(null);
const [iframeLoading, setIframeLoading] = useState(false);
useEffect(() => { setMounted(true); }, []); useEffect(() => { setMounted(true); }, []);
...@@ -41,8 +43,10 @@ const FloatContainer: React.FC = () => { ...@@ -41,8 +43,10 @@ const FloatContainer: React.FC = () => {
if (detail?.action === 'navigate' && typeof detail.page === 'string') { if (detail?.action === 'navigate' && typeof detail.page === 'string') {
let path = detail.page; let path = detail.page;
if (!path.startsWith('/')) path = '/' + path; if (!path.startsWith('/')) path = '/' + path;
// 设置嵌入URL并进入全屏分屏模式 // 设置嵌入URL并进入全屏分屏模式,追加 embed=1
setEmbeddedUrl(path); const embedPath = path + (path.includes('?') ? '&' : '?') + 'embed=1';
setEmbeddedUrl(embedPath);
setIframeLoading(true);
if (!isFullscreen) { if (!isFullscreen) {
toggleFullscreen(); toggleFullscreen();
} }
...@@ -52,9 +56,19 @@ const FloatContainer: React.FC = () => { ...@@ -52,9 +56,19 @@ const FloatContainer: React.FC = () => {
return () => window.removeEventListener('ai-action', handleAIAction); return () => window.removeEventListener('ai-action', handleAIAction);
}, [isFullscreen, toggleFullscreen]); }, [isFullscreen, toggleFullscreen]);
// 解析嵌入页面的中文标题
const embeddedPageTitle = useMemo(() => {
if (!embeddedUrl) return '';
// 去掉 embed=1 等 query 参数来匹配路由
const pathOnly = embeddedUrl.split('?')[0];
const route = getRouteByPath(pathOnly);
return route?.name || pathOnly;
}, [embeddedUrl]);
// 关闭嵌入页面 // 关闭嵌入页面
const closeEmbedded = useCallback(() => { const closeEmbedded = useCallback(() => {
setEmbeddedUrl(null); setEmbeddedUrl(null);
setIframeLoading(false);
}, []); }, []);
// 键盘快捷键: Ctrl+K 打开/关闭 AI 助手 // 键盘快捷键: Ctrl+K 打开/关闭 AI 助手
...@@ -239,7 +253,7 @@ const FloatContainer: React.FC = () => { ...@@ -239,7 +253,7 @@ const FloatContainer: React.FC = () => {
justifyContent: 'space-between', justifyContent: 'space-between',
}}> }}>
<span style={{ fontSize: 13, color: '#374151', fontWeight: 500 }}> <span style={{ fontSize: 13, color: '#374151', fontWeight: 500 }}>
{embeddedUrl} {embeddedPageTitle}
</span> </span>
<div <div
onClick={closeEmbedded} onClick={closeEmbedded}
...@@ -261,17 +275,29 @@ const FloatContainer: React.FC = () => { ...@@ -261,17 +275,29 @@ const FloatContainer: React.FC = () => {
</div> </div>
</div> </div>
{/* iframe 嵌入业务系统 */} {/* iframe 嵌入业务系统 */}
<div style={{ flex: 1, position: 'relative' }}>
{iframeLoading && (
<div style={{
position: 'absolute', inset: 0, display: 'flex',
alignItems: 'center', justifyContent: 'center',
background: '#f5f6fa', zIndex: 1,
}}>
<Spin tip="页面加载中..." />
</div>
)}
<iframe <iframe
src={embeddedUrl} src={embeddedUrl}
onLoad={() => setIframeLoading(false)}
style={{ style={{
flex: 1,
width: '100%', width: '100%',
height: '100%',
border: 'none', border: 'none',
background: '#fff', background: '#fff',
}} }}
title="业务页面" title={embeddedPageTitle || '业务页面'}
/> />
</div> </div>
</div>
)} )}
</div> </div>
</div> </div>
......
...@@ -59,12 +59,6 @@ const SuggestedActions: React.FC<SuggestedActionsProps> = ({ actions, onNavigate ...@@ -59,12 +59,6 @@ const SuggestedActions: React.FC<SuggestedActionsProps> = ({ actions, onNavigate
if (path && onNavigate) { if (path && onNavigate) {
onNavigate(path); onNavigate(path);
} }
// 同时触发 ai-action 自定义事件(供 Layout 监听)
if (path) {
window.dispatchEvent(new CustomEvent('ai-action', {
detail: { action: 'navigate', page: path },
}));
}
} else if (action.type === 'chat' || action.type === 'followup') { } else if (action.type === 'chat' || action.type === 'followup') {
const prompt = action.prompt || action.label; // 如果没有 prompt,使用 label 作为提问内容 const prompt = action.prompt || action.label; // 如果没有 prompt,使用 label 作为提问内容
if (prompt && onSend) { if (prompt && onSend) {
......
export * from './useAuth'; export * from './useAuth';
export * from './useVideoCall'; export * from './useVideoCall';
export * from './useAIChat'; export * from './useAIChat';
export * from './useEmbedMode';
'use client';
import { useSearchParams } from 'next/navigation';
/** Detect if the page is loaded inside an iframe with ?embed=1 */
export function useEmbedMode(): boolean {
const searchParams = useSearchParams();
return searchParams.get('embed') === '1';
}
'use client';
import type { AppProps } from 'next/app';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { App as AntdApp, ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, refetchOnWindowFocus: false } },
});
/**
* Pages Router _app wrapper.
* The src/pages/ directory contains React components that are imported by App Router pages.
* Next.js also treats them as Pages Router pages, so we need to provide necessary context.
*/
export default function PagesApp({ Component, pageProps }: AppProps) {
return (
<QueryClientProvider client={queryClient}>
<ConfigProvider locale={zhCN}>
<AntdApp>
<Component {...pageProps} />
</AntdApp>
</ConfigProvider>
</QueryClientProvider>
);
}
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html lang="zh-CN">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
...@@ -51,7 +51,13 @@ const AdminCompliancePage: React.FC = () => { ...@@ -51,7 +51,13 @@ const AdminCompliancePage: React.FC = () => {
const fetchLogs = async (page = 1) => { const fetchLogs = async (page = 1) => {
setLogsLoading(true); setLogsLoading(true);
try { try {
const res = await adminApi.getSystemLogs({ page, page_size: 10 }); const res = await adminApi.getSystemLogs({
page,
page_size: 10,
keyword: searchKeyword || undefined,
start_date: searchDateRange?.[0]?.format('YYYY-MM-DD') || undefined,
end_date: searchDateRange?.[1]?.format('YYYY-MM-DD') || undefined,
});
setLogs(res.data.list || []); setLogs(res.data.list || []);
setLogsTotal(res.data.total || 0); setLogsTotal(res.data.total || 0);
} catch { } catch {
......
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useSearchParams } from 'next/navigation';
import { import {
Card, Table, Typography, Space, Button, Modal, Form, Input, InputNumber, message Card, Table, Typography, Space, Button, Modal, Form, Input, InputNumber, message
} from 'antd'; } from 'antd';
...@@ -12,6 +13,7 @@ import type { Department } from '../../../api/doctor'; ...@@ -12,6 +13,7 @@ import type { Department } from '../../../api/doctor';
const { Text } = Typography; const { Text } = Typography;
const AdminDepartmentsPage: React.FC = () => { const AdminDepartmentsPage: React.FC = () => {
const searchParams = useSearchParams();
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [editingDept, setEditingDept] = useState<Department | null>(null); const [editingDept, setEditingDept] = useState<Department | null>(null);
const [form] = Form.useForm(); const [form] = Form.useForm();
...@@ -35,6 +37,13 @@ const AdminDepartmentsPage: React.FC = () => { ...@@ -35,6 +37,13 @@ const AdminDepartmentsPage: React.FC = () => {
fetchDepartments(); fetchDepartments();
}, []); }, []);
// ?action=add 自动打开添加弹窗
useEffect(() => {
if (searchParams.get('action') === 'add') {
handleAdd();
}
}, [searchParams]); // eslint-disable-line react-hooks/exhaustive-deps
const handleAdd = () => { const handleAdd = () => {
setEditingDept(null); setEditingDept(null);
form.resetFields(); form.resetFields();
......
'use client'; 'use client';
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { useSearchParams } from 'next/navigation';
import { import {
Card, Table, Tag, Typography, Space, Button, Avatar, Modal, message, Card, Table, Tag, Typography, Space, Button, Avatar, Modal, message,
Input, Select, Form, Descriptions, Row, Col, Input, Select, Form, Descriptions, Row, Col,
...@@ -23,6 +24,7 @@ const statusMap: Record<string, { color: string; text: string }> = { ...@@ -23,6 +24,7 @@ const statusMap: Record<string, { color: string; text: string }> = {
}; };
const AdminDoctorsPage: React.FC = () => { const AdminDoctorsPage: React.FC = () => {
const searchParams = useSearchParams();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [doctors, setDoctors] = useState<DoctorItem[]>([]); const [doctors, setDoctors] = useState<DoctorItem[]>([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
...@@ -65,6 +67,13 @@ const AdminDoctorsPage: React.FC = () => { ...@@ -65,6 +67,13 @@ const AdminDoctorsPage: React.FC = () => {
fetchDoctors(); fetchDoctors();
}, [fetchDoctors]); }, [fetchDoctors]);
// ?action=add 自动打开添加弹窗
useEffect(() => {
if (searchParams.get('action') === 'add') {
setAddModalVisible(true);
}
}, [searchParams]);
const handleSearch = () => { const handleSearch = () => {
setPage(1); setPage(1);
fetchDoctors(); fetchDoctors();
......
'use client'; 'use client';
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { useSearchParams } from 'next/navigation';
import { import {
Card, Table, Input, Select, Button, Space, Tag, Avatar, Modal, message, Card, Table, Input, Select, Button, Space, Tag, Avatar, Modal, message,
Typography, Form, InputNumber, Row, Col, Typography, Form, InputNumber, Row, Col,
...@@ -15,6 +16,7 @@ import type { UserInfo } from '../../../api/user'; ...@@ -15,6 +16,7 @@ import type { UserInfo } from '../../../api/user';
const { Text } = Typography; const { Text } = Typography;
const AdminPatientsPage: React.FC = () => { const AdminPatientsPage: React.FC = () => {
const searchParams = useSearchParams();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [patients, setPatients] = useState<UserInfo[]>([]); const [patients, setPatients] = useState<UserInfo[]>([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
...@@ -45,6 +47,13 @@ const AdminPatientsPage: React.FC = () => { ...@@ -45,6 +47,13 @@ const AdminPatientsPage: React.FC = () => {
fetchPatients(); fetchPatients();
}, [fetchPatients]); }, [fetchPatients]);
// ?action=add 自动打开添加弹窗
useEffect(() => {
if (searchParams.get('action') === 'add') {
handleAdd();
}
}, [searchParams]); // eslint-disable-line react-hooks/exhaustive-deps
const handleSearch = () => { const handleSearch = () => {
setPage(1); setPage(1);
fetchPatients(); fetchPatients();
......
...@@ -82,8 +82,8 @@ const DoctorCertificationPage: React.FC = () => { ...@@ -82,8 +82,8 @@ const DoctorCertificationPage: React.FC = () => {
const res = await request.post('/upload', formData, { const res = await request.post('/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' }, headers: { 'Content-Type': 'multipart/form-data' },
}); });
file.url = res.data.url; file.url = res.data.data?.url || res.data.url;
onSuccess(res.data); onSuccess(res.data.data || res.data);
} catch (err) { } catch (err) {
onError(err); onError(err);
} }
......
...@@ -191,6 +191,7 @@ const PatientList: React.FC<PatientListProps> = ({ ...@@ -191,6 +191,7 @@ const PatientList: React.FC<PatientListProps> = ({
// 筛选 // 筛选
const filtered = useMemo(() => { const filtered = useMemo(() => {
if (!patients) return [];
if (!searchText.trim()) return patients; if (!searchText.trim()) return patients;
const keyword = searchText.trim().toLowerCase(); const keyword = searchText.trim().toLowerCase();
return patients.filter(p => return patients.filter(p =>
......
...@@ -25,9 +25,9 @@ const formatWaitingTime = (seconds: number) => { ...@@ -25,9 +25,9 @@ const formatWaitingTime = (seconds: number) => {
}; };
const WaitingQueue: React.FC<WaitingQueueProps> = ({ const WaitingQueue: React.FC<WaitingQueueProps> = ({
waitingList, waitingList = [],
inProgressList, inProgressList = [],
completedList, completedList = [],
activeConsultId, activeConsultId,
onAccept, onAccept,
onReject, onReject,
......
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Card, Avatar, Button, Descriptions, Tag, Typography, Row, Col, Divider, Switch, Space, Spin, message, Modal, Form, Input, InputNumber } from 'antd'; import { Card, Avatar, Button, Descriptions, Tag, Typography, Row, Col, Divider, Switch, Space, Spin, message, Modal, Form, Input, InputNumber, Select } from 'antd';
import { import {
UserOutlined, UserOutlined,
EditOutlined, EditOutlined,
...@@ -163,7 +163,7 @@ const DoctorProfilePage: React.FC = () => { ...@@ -163,7 +163,7 @@ const DoctorProfilePage: React.FC = () => {
}}> }}>
<Form form={editForm} layout="vertical"> <Form form={editForm} layout="vertical">
<Form.Item name="introduction" label="个人简介"><Input.TextArea rows={3} /></Form.Item> <Form.Item name="introduction" label="个人简介"><Input.TextArea rows={3} /></Form.Item>
<Form.Item name="specialties" label="擅长领域"><Input /></Form.Item> <Form.Item name="specialties" label="擅长领域"><Select mode="tags" placeholder="输入后回车添加" /></Form.Item>
<Form.Item name="price" label="问诊价格(元)"><InputNumber min={0} style={{ width: '100%' }} /></Form.Item> <Form.Item name="price" label="问诊价格(元)"><InputNumber min={0} style={{ width: '100%' }} /></Form.Item>
</Form> </Form>
</Modal> </Modal>
......
...@@ -67,10 +67,11 @@ const PatientHomePage: React.FC = () => { ...@@ -67,10 +67,11 @@ const PatientHomePage: React.FC = () => {
useEffect(() => { useEffect(() => {
// Try to get real platform stats from a public endpoint // Try to get real platform stats from a public endpoint
request.get('/admin/dashboard/stats').then((res: any) => { request.get('/admin/dashboard/stats').then((res: any) => {
if (res.data) { const stats = res.data?.data || res.data;
if (stats) {
setPlatformStats({ setPlatformStats({
doctors: `${res.data.total_doctors || 0}`, doctors: `${stats.total_doctors || 0}`,
patients: `${res.data.total_users || 0}`, patients: `${stats.total_users || 0}`,
response: '< 5 分钟', response: '< 5 分钟',
}); });
} }
......
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