Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
I
Internet-hospital
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Labels
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Jobs
Commits
Open sidebar
yuguo
Internet-hospital
Commits
859fb6ac
Commit
859fb6ac
authored
Mar 04, 2026
by
yuguo
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix
parent
6e652bf1
Changes
32
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
32 changed files
with
452 additions
and
254 deletions
+452
-254
.claude/settings.local.json
.claude/settings.local.json
+3
-1
.mcp.json
.mcp.json
+44
-0
server/internal/agent/agents.go
server/internal/agent/agents.go
+19
-6
server/pkg/agent/tools/navigate_page.go
server/pkg/agent/tools/navigate_page.go
+1
-5
web/src/api/admin.ts
web/src/api/admin.ts
+1
-1
web/src/api/agent.ts
web/src/api/agent.ts
+1
-1
web/src/api/doctorPortal.ts
web/src/api/doctorPortal.ts
+16
-1
web/src/api/notification.ts
web/src/api/notification.ts
+5
-5
web/src/api/payment.ts
web/src/api/payment.ts
+13
-13
web/src/app/(main)/admin/layout.tsx
web/src/app/(main)/admin/layout.tsx
+42
-15
web/src/app/(main)/admin/safety/page.tsx
web/src/app/(main)/admin/safety/page.tsx
+15
-15
web/src/app/(main)/admin/tools/page.tsx
web/src/app/(main)/admin/tools/page.tsx
+1
-1
web/src/app/(main)/admin/workflows/page.tsx
web/src/app/(main)/admin/workflows/page.tsx
+1
-1
web/src/app/(main)/doctor/layout.tsx
web/src/app/(main)/doctor/layout.tsx
+16
-2
web/src/app/(main)/layout.tsx
web/src/app/(main)/layout.tsx
+14
-1
web/src/app/(main)/patient/layout.tsx
web/src/app/(main)/patient/layout.tsx
+16
-2
web/src/components/GlobalAIFloat/ChatPanel.tsx
web/src/components/GlobalAIFloat/ChatPanel.tsx
+108
-153
web/src/components/GlobalAIFloat/FloatContainer.tsx
web/src/components/GlobalAIFloat/FloatContainer.tsx
+40
-14
web/src/components/GlobalAIFloat/SuggestedActions.tsx
web/src/components/GlobalAIFloat/SuggestedActions.tsx
+0
-6
web/src/hooks/index.ts
web/src/hooks/index.ts
+1
-0
web/src/hooks/useEmbedMode.ts
web/src/hooks/useEmbedMode.ts
+9
-0
web/src/pages/_app.tsx
web/src/pages/_app.tsx
+27
-0
web/src/pages/_document.tsx
web/src/pages/_document.tsx
+13
-0
web/src/pages/admin/Compliance/index.tsx
web/src/pages/admin/Compliance/index.tsx
+7
-1
web/src/pages/admin/Departments/index.tsx
web/src/pages/admin/Departments/index.tsx
+9
-0
web/src/pages/admin/Doctors/index.tsx
web/src/pages/admin/Doctors/index.tsx
+9
-0
web/src/pages/admin/Patients/index.tsx
web/src/pages/admin/Patients/index.tsx
+9
-0
web/src/pages/doctor/Certification/index.tsx
web/src/pages/doctor/Certification/index.tsx
+2
-2
web/src/pages/doctor/Consult/PatientList.tsx
web/src/pages/doctor/Consult/PatientList.tsx
+1
-0
web/src/pages/doctor/Consult/WaitingQueue.tsx
web/src/pages/doctor/Consult/WaitingQueue.tsx
+3
-3
web/src/pages/doctor/Profile/index.tsx
web/src/pages/doctor/Profile/index.tsx
+2
-2
web/src/pages/patient/Home/index.tsx
web/src/pages/patient/Home/index.tsx
+4
-3
No files found.
.claude/settings.local.json
View file @
859fb6ac
...
...
@@ -37,7 +37,9 @@
"Bash(bash:*)"
,
"Bash(make run:*)"
,
"Bash(rm:*)"
,
"Bash(gh pr:*)"
"Bash(gh pr:*)"
,
"Bash(claude mcp:*)"
,
"WebSearch"
]
}
}
.mcp.json
0 → 100644
View file @
859fb6ac
{
"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"
:
{}
}
}
}
server/internal/agent/agents.go
View file @
859fb6ac
...
...
@@ -14,14 +14,14 @@ func defaultAgentDefinitions() []model.AgentDefinition {
"query_symptom_knowledge"
,
"recommend_department"
,
"search_medical_knowledge"
,
"query_drug"
,
"query_medical_record"
,
"generate_follow_up_plan"
,
"send_notification"
,
"send_notification"
,
"navigate_page"
,
})
// 医生通用智能体 — 合并 diagnosis + prescription + follow_up 能力
doctorTools
,
_
:=
json
.
Marshal
([]
string
{
"query_medical_record"
,
"query_symptom_knowledge"
,
"search_medical_knowledge"
,
"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 管理能力
...
...
@@ -29,7 +29,7 @@ func defaultAgentDefinitions() []model.AgentDefinition {
"eval_expression"
,
"query_workflow_status"
,
"call_agent"
,
"trigger_workflow"
,
"request_human_review"
,
"list_knowledge_collections"
,
"send_notification"
,
"query_drug"
,
"search_medical_knowledge"
,
"query_drug"
,
"search_medical_knowledge"
,
"navigate_page"
,
})
return
[]
model
.
AgentDefinition
{
...
...
@@ -52,7 +52,11 @@ func defaultAgentDefinitions() []model.AgentDefinition {
- 主动使用工具获取真实数据,不要凭空回答
- 不做确定性诊断,只提供参考建议
- 关注患者的用药依从性和健康状况变化
- 所有医疗建议仅供参考,请以专业医生判断为准`
,
- 所有医疗建议仅供参考,请以专业医生判断为准
页面导航能力:
- 你可以使用 navigate_page 工具打开系统页面,如找医生、我的问诊、处方、健康档案等
- 当用户想查看某个页面时,直接调用 navigate_page 工具导航到对应页面`
,
Tools
:
string
(
patientTools
),
Config
:
"{}"
,
MaxIterations
:
10
,
...
...
@@ -80,7 +84,11 @@ func defaultAgentDefinitions() []model.AgentDefinition {
- 基于循证医学原则提供建议
- 主动使用工具获取真实数据
- 对存在风险的处方要明确指出
- 所有建议仅供医生参考,请结合临床实际情况`
,
- 所有建议仅供医生参考,请结合临床实际情况
页面导航能力:
- 你可以使用 navigate_page 工具打开系统页面,如工作台、问诊大厅、患者档案、排班管理等
- 当医生想查看某个页面时,直接调用 navigate_page 工具导航到对应页面`
,
Tools
:
string
(
doctorTools
),
Config
:
"{}"
,
MaxIterations
:
10
,
...
...
@@ -106,7 +114,12 @@ func defaultAgentDefinitions() []model.AgentDefinition {
- 以简洁专业的方式回答管理员的问题
- 主动使用工具获取真实数据
- 提供可操作的建议和方案
- 用中文回答`
,
- 用中文回答
页面导航能力:
- 你可以使用 navigate_page 工具打开系统页面,如医生管理、患者管理、科室管理、问诊管理等
- 当管理员想查看或操作某个页面时,直接调用 navigate_page 工具导航到对应页面
- 支持 open_add 操作自动打开新增弹窗(如新增医生、新增科室等)`
,
Tools
:
string
(
adminTools
),
Config
:
"{}"
,
MaxIterations
:
10
,
...
...
server/pkg/agent/tools/navigate_page.go
View file @
859fb6ac
...
...
@@ -150,11 +150,7 @@ func (t *NavigatePageTool) Execute(ctx context.Context, params map[string]interf
route
=
fmt
.
Sprintf
(
"%s/edit/%s"
,
basePath
,
id
)
}
case
"open_add"
:
parentID
:=
id
if
parentID
==
""
{
parentID
=
"0"
}
route
=
fmt
.
Sprintf
(
"%s/add/%s"
,
basePath
,
parentID
)
route
=
fmt
.
Sprintf
(
"%s?action=add"
,
basePath
)
}
// 返回标准化导航指令
...
...
web/src/api/admin.ts
View file @
859fb6ac
...
...
@@ -333,7 +333,7 @@ export const adminApi = {
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
}),
// === 问诊管理 ===
...
...
web/src/api/agent.ts
View file @
859fb6ac
...
...
@@ -281,7 +281,7 @@ export const workflowApi = {
update
:
(
id
:
number
,
data
:
Partial
<
WorkflowCreateParams
>
)
=>
put
<
unknown
>
(
`/admin/workflows/
${
id
}
`
,
data
),
delete
:
(
id
:
number
)
=>
del
<
null
>
(
`/admin/workflows/
${
i
d
}
`
),
delete
:
(
workflowId
:
string
|
number
)
=>
del
<
null
>
(
`/workflow/
${
workflowI
d
}
`
),
publish
:
(
id
:
number
)
=>
put
<
null
>
(
`/admin/workflows/
${
id
}
/publish`
,
{}),
...
...
web/src/api/doctorPortal.ts
View file @
859fb6ac
import
{
get
,
post
,
put
,
del
}
from
'
./request
'
;
import
type
{
Consultation
,
ConsultMessage
}
from
'
./consult
'
;
import
type
{
ToolCall
}
from
'
./agent
'
;
import
type
{
Prescription
}
from
'
./prescription
'
;
// ==================== 医生端 API ====================
...
...
@@ -190,7 +191,21 @@ export const doctorPortalApi = {
getPatientDetail
:
(
patientId
:
string
)
=>
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
[]
})
=>
post
<
{
report
:
string
;
...
...
web/src/api/notification.ts
View file @
859fb6ac
import
request
from
'
./request
'
;
import
{
get
,
put
}
from
'
./request
'
;
export
interface
Notification
{
id
:
string
;
...
...
@@ -13,14 +13,14 @@ export interface Notification {
export
const
notificationApi
=
{
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
)
=>
request
.
put
(
`/notifications/
${
id
}
/read`
,
{}),
put
(
`/notifications/
${
id
}
/read`
,
{}),
markAllRead
:
()
=>
request
.
put
(
'
/notifications/read-all
'
,
{}),
put
(
'
/notifications/read-all
'
,
{}),
getUnreadCount
:
()
=>
request
.
get
<
{
count
:
number
}
>
(
'
/notifications/unread-count
'
),
get
<
{
count
:
number
}
>
(
'
/notifications/unread-count
'
),
};
web/src/api/payment.ts
View file @
859fb6ac
// 支付相关API
import
request
from
'
./request
'
;
import
{
get
,
post
}
from
'
./request
'
;
export
interface
PaymentOrder
{
id
:
string
;
...
...
@@ -60,57 +60,57 @@ export interface WithdrawalRecord {
export
const
paymentApi
=
{
// 创建支付订单
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
)
=>
request
.
get
<
PaymentOrder
>
(
`/payment/order/
${
orderId
}
`
),
get
<
PaymentOrder
>
(
`/payment/order/
${
orderId
}
`
),
// 获取订单列表
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
)
=>
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_method
:
paymentMethod
}
),
// 查询支付状态
getPaymentStatus
:
(
orderId
:
string
)
=>
request
.
get
<
{
status
:
string
}
>
(
`/payment/order/
${
orderId
}
/status`
),
get
<
{
status
:
string
}
>
(
`/payment/order/
${
orderId
}
/status`
),
// 取消订单
cancelOrder
:
(
orderId
:
string
)
=>
request
.
post
(
`/payment/order/
${
orderId
}
/cancel`
),
post
(
`/payment/order/
${
orderId
}
/cancel`
),
// 确认收货
confirmDelivery
:
(
orderId
:
string
)
=>
request
.
post
(
`/payment/order/
${
orderId
}
/confirm-delivery`
,
{}),
post
(
`/payment/order/
${
orderId
}
/confirm-delivery`
,
{}),
};
// 医生端收入API
export
const
incomeApi
=
{
// 获取收入统计
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
})
=>
request
.
get
<
{
list
:
IncomeRecord
[];
total
:
number
}
>
(
'
/doctor/income/records
'
,
{
params
}),
get
<
{
list
:
IncomeRecord
[];
total
:
number
}
>
(
'
/doctor/income/records
'
,
{
params
}),
// 获取月度账单
getMonthlyBills
:
()
=>
request
.
get
<
MonthlyBill
[]
>
(
'
/doctor/income/monthly
'
),
get
<
MonthlyBill
[]
>
(
'
/doctor/income/monthly
'
),
// 申请提现
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
})
=>
request
.
get
<
{
list
:
WithdrawalRecord
[];
total
:
number
}
>
(
'
/doctor/income/withdrawals
'
,
{
params
}),
get
<
{
list
:
WithdrawalRecord
[];
total
:
number
}
>
(
'
/doctor/income/withdrawals
'
,
{
params
}),
};
export
default
paymentApi
;
web/src/app/(main)/admin/layout.tsx
View file @
859fb6ac
'
use client
'
;
import
React
,
{
useState
,
useEffect
,
useMemo
,
useCallback
}
from
'
react
'
;
import
{
useRouter
,
usePathname
}
from
'
next/navigation
'
;
import
React
,
{
Suspense
,
useState
,
useEffect
,
useMemo
,
useCallback
}
from
'
react
'
;
import
{
useRouter
,
usePathname
,
useSearchParams
}
from
'
next/navigation
'
;
import
{
Layout
,
Menu
,
Avatar
,
Dropdown
,
Badge
,
Space
,
Typography
,
Tag
,
Spin
,
Popover
,
List
}
from
'
antd
'
;
import
{
DashboardOutlined
,
UserOutlined
,
TeamOutlined
,
ApartmentOutlined
,
...
...
@@ -91,8 +91,18 @@ function findOpenKeys(menus: MenuType[], targetPath: string): string[] {
}
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
pathname
=
usePathname
();
const
searchParams
=
useSearchParams
();
const
isEmbed
=
searchParams
.
get
(
'
embed
'
)
===
'
1
'
;
const
{
user
,
logout
}
=
useUserStore
();
const
[
collapsed
,
setCollapsed
]
=
useState
(
false
);
const
[
dynamicMenus
,
setDynamicMenus
]
=
useState
<
MenuType
[]
>
([]);
...
...
@@ -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
(()
=>
{});
};
// 从 API 获取动态菜单
// 从 API 获取动态菜单
(依赖 user,确保登录后才加载)
useEffect
(()
=>
{
if
(
!
user
)
{
setMenuLoading
(
false
);
return
;
}
let
cancelled
=
false
;
setMenuLoading
(
true
);
myMenuApi
.
getMenus
()
.
then
(
res
=>
{
if
(
!
cancelled
)
setDynamicMenus
(
res
.
data
||
[]);
})
.
catch
(()
=>
{
/* 静默失败,使用空菜单 */
})
.
catch
((
err
)
=>
{
console
.
error
(
'
加载菜单失败:
'
,
err
);
})
.
finally
(()
=>
{
if
(
!
cancelled
)
setMenuLoading
(
false
);
});
return
()
=>
{
cancelled
=
true
;
};
},
[]);
},
[
user
]);
// 转换为 Ant Design menu items
const
menuItems
=
useMemo
(()
=>
convertMenuTree
(
dynamicMenus
),
[
dynamicMenus
]);
// 监听 AI 助手导航事件
// 监听 AI 助手导航事件
(embed 模式下跳过,避免 iframe 内拦截)
useEffect
(()
=>
{
if
(
isEmbed
)
return
;
const
handleAIAction
=
(
e
:
Event
)
=>
{
const
detail
=
(
e
as
CustomEvent
).
detail
;
if
(
detail
?.
action
===
'
navigate
'
&&
typeof
detail
.
page
===
'
string
'
)
{
...
...
@@ -147,7 +164,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
};
window
.
addEventListener
(
'
ai-action
'
,
handleAIAction
);
return
()
=>
window
.
removeEventListener
(
'
ai-action
'
,
handleAIAction
);
},
[
router
]);
},
[
router
,
isEmbed
]);
const
userMenuItems
=
[
{
key
:
'
profile
'
,
icon
:
<
UserOutlined
/>,
label
:
'
个人信息
'
},
...
...
@@ -178,6 +195,10 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
return
findOpenKeys
(
dynamicMenus
,
currentPath
);
},
[
dynamicMenus
,
currentPath
,
collapsed
]);
if
(
isEmbed
)
{
return
<
div
style=
{
{
minHeight
:
'
100vh
'
,
background
:
'
#f5f6fa
'
}
}
>
{
children
}
</
div
>;
}
return
(
<
Layout
style=
{
{
minHeight
:
'
100vh
'
}
}
>
<
Sider
...
...
@@ -219,6 +240,11 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
</
div
>
{
/* Menu */
}
{
menuLoading
?
(
<
div
style=
{
{
display
:
'
flex
'
,
justifyContent
:
'
center
'
,
padding
:
'
40px 0
'
}
}
>
<
Spin
size=
"small"
/>
</
div
>
)
:
(
<
Menu
mode=
"inline"
selectedKeys=
{
getSelectedKeys
()
}
...
...
@@ -227,6 +253,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
onClick=
{
handleMenuClick
}
style=
{
{
border
:
'
none
'
,
padding
:
'
8px 0
'
}
}
/>
)
}
</
Sider
>
<
Layout
style=
{
{
marginLeft
:
collapsed
?
64
:
220
,
transition
:
'
margin-left 0.2s
'
}
}
>
...
...
web/src/app/(main)/admin/safety/page.tsx
View file @
859fb6ac
...
...
@@ -138,7 +138,7 @@ export default function SafetyPage() {
{
title: '敏感词/正则',
dataIndex: 'word',
render: (v
, r
) => (
render: (v
: string, r: SafetyRule
) => (
<Space>
<Text code>{v}</Text>
{r.is_regex && <Tag color="purple">正则</Tag>}
...
...
@@ -149,7 +149,7 @@ export default function SafetyPage() {
title: '分类',
dataIndex: 'category',
width: 100,
render: (v) => {
render: (v
: string
) => {
const map: Record<string, string> = {
injection: 'red', medical_claim: 'orange', drug_promotion: 'yellow',
privacy: 'blue', toxicity: 'red',
...
...
@@ -165,7 +165,7 @@ export default function SafetyPage() {
title: '级别',
dataIndex: 'level',
width: 80,
render: (v) => {
render: (v
: string
) => {
const map: Record<string, string> = { block: 'red', warn: 'orange', replace: 'blue' };
const labels: Record<string, string> = { block: '拦截', warn: '警告', replace: '替换' };
return <Tag color={map[v] ?? 'default'}>{labels[v] ?? v}</Tag>;
...
...
@@ -175,7 +175,7 @@ export default function SafetyPage() {
title: '方向',
dataIndex: 'direction',
width: 90,
render: (v) => {
render: (v
: string
) => {
const labels: Record<string, string> = { input: '输入', output: '输出', both: '双向' };
return <Tag>{labels[v] ?? v}</Tag>;
},
...
...
@@ -184,12 +184,12 @@ export default function SafetyPage() {
title: '状态',
dataIndex: 'status',
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: '操作',
width: 120,
render: (_
, record
) => (
render: (_
: unknown, record: SafetyRule
) => (
<Space>
<Button size="small" icon={<EditOutlined />} onClick={() => openModal(record)}>编辑</Button>
<Popconfirm title="确认删除?" onConfirm={() => deleteMutation.mutate(record.id)}>
...
...
@@ -201,21 +201,21 @@ export default function SafetyPage() {
];
const logColumns: ColumnsType<SafetyLog> = [
{ title: 'TraceID', dataIndex: 'trace_id', width: 180, render: (v) => <Text code style={{ fontSize: 11 }}>{v?.slice(0, 16)}...</Text> },
{ title: '方向', dataIndex: 'direction', width: 80, render: (v) => <Tag>{v === 'input' ? '输入' : '输出'}</Tag> },
{ 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
: string
) => <Tag>{v === 'input' ? '输入' : '输出'}</Tag> },
{
title: '动作',
dataIndex: 'action',
width: 80,
render: (v) => {
render: (v
: string
) => {
const map: Record<string, string> = { blocked: 'red', replaced: 'blue', warned: 'orange', passed: 'green' };
const labels: Record<string, string> = { blocked: '拦截', replaced: '替换', warned: '警告', passed: '通过' };
return <Tag color={map[v] ?? 'default'}>{labels[v] ?? v}</Tag>;
},
},
{ 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 (
<div style={{ padding: 24 }}>
...
...
@@ -270,9 +270,9 @@ export default function SafetyPage() {
key: 'rules',
label: '规则管理',
children: (
<Table
<Table
<SafetyRule>
columns={ruleColumns}
dataSource={
rulesData?.list ??
[]}
dataSource={
(rulesData?.list ?? []) as SafetyRule
[]}
rowKey="id"
pagination={{
current: rulesPage,
...
...
@@ -288,9 +288,9 @@ export default function SafetyPage() {
key: 'logs',
label: '过滤日志',
children: (
<Table
<Table
<SafetyLog>
columns={logColumns}
dataSource={
logsData?.list ??
[]}
dataSource={
(logsData?.list ?? []) as SafetyLog
[]}
rowKey="id"
pagination={{
current: logsPage,
...
...
web/src/app/(main)/admin/tools/page.tsx
View file @
859fb6ac
...
...
@@ -17,7 +17,7 @@ import SkillsTab from './SkillsTab';
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
/>
},
recommendation
:
{
color
:
'
cyan
'
,
label
:
'
智能推荐
'
,
icon
:
<
ThunderboltOutlined
/>
},
medical
:
{
color
:
'
purple
'
,
label
:
'
病历管理
'
,
icon
:
<
HeartOutlined
/>
},
...
...
web/src/app/(main)/admin/workflows/page.tsx
View file @
859fb6ac
...
...
@@ -142,7 +142,7 @@ export default function WorkflowsPage() {
}}>激活</Button>
)}
<Popconfirm title="确认删除?" onConfirm={async () => {
await workflowApi.delete(r.id);
await workflowApi.delete(r.
workflow_
id);
message.success('删除成功');
fetchWorkflows();
}}>
...
...
web/src/app/(main)/doctor/layout.tsx
View file @
859fb6ac
'
use client
'
;
import
React
,
{
useState
,
useEffect
}
from
'
react
'
;
import
{
useRouter
,
usePathname
}
from
'
next/navigation
'
;
import
React
,
{
Suspense
,
useState
,
useEffect
}
from
'
react
'
;
import
{
useRouter
,
usePathname
,
useSearchParams
}
from
'
next/navigation
'
;
import
{
Layout
,
Menu
,
Avatar
,
Dropdown
,
Badge
,
Space
,
Switch
,
Typography
,
Tag
,
Popover
,
List
}
from
'
antd
'
;
import
{
DashboardOutlined
,
UserOutlined
,
MessageOutlined
,
CalendarOutlined
,
...
...
@@ -28,8 +28,18 @@ const menuItems = [
];
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
pathname
=
usePathname
();
const
searchParams
=
useSearchParams
();
const
isEmbed
=
searchParams
.
get
(
'
embed
'
)
===
'
1
'
;
const
{
user
,
logout
}
=
useUserStore
();
const
[
isOnline
,
setIsOnline
]
=
useState
(
false
);
const
[
collapsed
,
setCollapsed
]
=
useState
(
false
);
...
...
@@ -89,6 +99,10 @@ export default function DoctorLayout({ children }: { children: React.ReactNode }
return
match
?
[
match
.
key
]
:
[];
};
if
(
isEmbed
)
{
return
<
div
style=
{
{
minHeight
:
'
100vh
'
,
background
:
'
#f5f8ff
'
}
}
>
{
children
}
</
div
>;
}
return
(
<
Layout
style=
{
{
minHeight
:
'
100vh
'
}
}
>
<
Sider
...
...
web/src/app/(main)/layout.tsx
View file @
859fb6ac
'
use client
'
;
import
{
Suspense
}
from
'
react
'
;
import
{
useSearchParams
}
from
'
next/navigation
'
;
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
})
{
return
(
<
div
className=
"compact-ui"
>
{
children
}
<
GlobalAIFloat
/>
<
Suspense
fallback=
{
null
}
>
<
AIFloatGuard
/>
</
Suspense
>
</
div
>
);
}
web/src/app/(main)/patient/layout.tsx
View file @
859fb6ac
'
use client
'
;
import
React
,
{
useState
,
useEffect
}
from
'
react
'
;
import
{
useRouter
,
usePathname
}
from
'
next/navigation
'
;
import
React
,
{
Suspense
,
useState
,
useEffect
}
from
'
react
'
;
import
{
useRouter
,
usePathname
,
useSearchParams
}
from
'
next/navigation
'
;
import
{
Layout
,
Menu
,
Avatar
,
Dropdown
,
Button
,
Badge
,
Space
,
Typography
,
Tag
,
Popover
,
List
}
from
'
antd
'
;
import
{
HomeOutlined
,
UserOutlined
,
MedicineBoxOutlined
,
FileTextOutlined
,
...
...
@@ -50,8 +50,18 @@ const allRouteKeys = [
];
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
pathname
=
usePathname
();
const
searchParams
=
useSearchParams
();
const
isEmbed
=
searchParams
.
get
(
'
embed
'
)
===
'
1
'
;
const
{
user
,
logout
}
=
useUserStore
();
const
[
collapsed
,
setCollapsed
]
=
useState
(
false
);
const
[
unreadCount
,
setUnreadCount
]
=
useState
(
0
);
...
...
@@ -112,6 +122,10 @@ export default function PatientLayout({ children }: { children: React.ReactNode
return
keys
;
};
if
(
isEmbed
)
{
return
<
div
style=
{
{
minHeight
:
'
100vh
'
,
background
:
'
#f5f8ff
'
}
}
>
{
children
}
</
div
>;
}
return
(
<
Layout
style=
{
{
minHeight
:
'
100vh
'
}
}
>
<
Sider
...
...
web/src/components/GlobalAIFloat/ChatPanel.tsx
View file @
859fb6ac
This diff is collapsed.
Click to expand it.
web/src/components/GlobalAIFloat/FloatContainer.tsx
View file @
859fb6ac
'
use client
'
;
import
React
,
{
useCallback
,
useMemo
,
useState
,
useEffect
}
from
'
react
'
;
import
{
Tooltip
}
from
'
antd
'
;
import
{
Tooltip
,
Spin
}
from
'
antd
'
;
import
{
RobotOutlined
,
CloseOutlined
,
...
...
@@ -18,6 +18,7 @@ import { useAIAssistStore } from '../../store/aiAssistStore';
import
ChatPanel
from
'
./ChatPanel
'
;
import
type
{
WidgetRole
}
from
'
./types
'
;
import
{
ROLE_THEME
}
from
'
./types
'
;
import
{
getRouteByPath
}
from
'
../../config/routes
'
;
const
DRAG_HANDLE_CLASS
=
'
ai-widget-drag-handle
'
;
...
...
@@ -31,6 +32,7 @@ const FloatContainer: React.FC = () => {
const
[
mounted
,
setMounted
]
=
useState
(
false
);
// 嵌入的业务页面URL(分屏模式)
const
[
embeddedUrl
,
setEmbeddedUrl
]
=
useState
<
string
|
null
>
(
null
);
const
[
iframeLoading
,
setIframeLoading
]
=
useState
(
false
);
useEffect
(()
=>
{
setMounted
(
true
);
},
[]);
...
...
@@ -41,8 +43,10 @@ const FloatContainer: React.FC = () => {
if
(
detail
?.
action
===
'
navigate
'
&&
typeof
detail
.
page
===
'
string
'
)
{
let
path
=
detail
.
page
;
if
(
!
path
.
startsWith
(
'
/
'
))
path
=
'
/
'
+
path
;
// 设置嵌入URL并进入全屏分屏模式
setEmbeddedUrl
(
path
);
// 设置嵌入URL并进入全屏分屏模式,追加 embed=1
const
embedPath
=
path
+
(
path
.
includes
(
'
?
'
)
?
'
&
'
:
'
?
'
)
+
'
embed=1
'
;
setEmbeddedUrl
(
embedPath
);
setIframeLoading
(
true
);
if
(
!
isFullscreen
)
{
toggleFullscreen
();
}
...
...
@@ -52,9 +56,19 @@ const FloatContainer: React.FC = () => {
return
()
=>
window
.
removeEventListener
(
'
ai-action
'
,
handleAIAction
);
},
[
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
(()
=>
{
setEmbeddedUrl
(
null
);
setIframeLoading
(
false
);
},
[]);
// 键盘快捷键: Ctrl+K 打开/关闭 AI 助手
...
...
@@ -239,7 +253,7 @@ const FloatContainer: React.FC = () => {
justifyContent
:
'
space-between
'
,
}
}
>
<
span
style=
{
{
fontSize
:
13
,
color
:
'
#374151
'
,
fontWeight
:
500
}
}
>
{
embedded
Url
}
{
embedded
PageTitle
}
</
span
>
<
div
onClick=
{
closeEmbedded
}
...
...
@@ -261,17 +275,29 @@ const FloatContainer: React.FC = () => {
</
div
>
</
div
>
{
/* 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
src=
{
embeddedUrl
}
onLoad=
{
()
=>
setIframeLoading
(
false
)
}
style=
{
{
flex
:
1
,
width
:
'
100%
'
,
height
:
'
100%
'
,
border
:
'
none
'
,
background
:
'
#fff
'
,
}
}
title=
"业务页面"
title=
{
embeddedPageTitle
||
'
业务页面
'
}
/>
</
div
>
</
div
>
)
}
</
div
>
</
div
>
...
...
web/src/components/GlobalAIFloat/SuggestedActions.tsx
View file @
859fb6ac
...
...
@@ -59,12 +59,6 @@ const SuggestedActions: React.FC<SuggestedActionsProps> = ({ actions, onNavigate
if
(
path
&&
onNavigate
)
{
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
'
)
{
const
prompt
=
action
.
prompt
||
action
.
label
;
// 如果没有 prompt,使用 label 作为提问内容
if
(
prompt
&&
onSend
)
{
...
...
web/src/hooks/index.ts
View file @
859fb6ac
export
*
from
'
./useAuth
'
;
export
*
from
'
./useVideoCall
'
;
export
*
from
'
./useAIChat
'
;
export
*
from
'
./useEmbedMode
'
;
web/src/hooks/useEmbedMode.ts
0 → 100644
View file @
859fb6ac
'
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
'
;
}
web/src/pages/_app.tsx
0 → 100644
View file @
859fb6ac
'
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
>
);
}
web/src/pages/_document.tsx
0 → 100644
View file @
859fb6ac
import
{
Html
,
Head
,
Main
,
NextScript
}
from
'
next/document
'
;
export
default
function
Document
()
{
return
(
<
Html
lang=
"zh-CN"
>
<
Head
/>
<
body
>
<
Main
/>
<
NextScript
/>
</
body
>
</
Html
>
);
}
web/src/pages/admin/Compliance/index.tsx
View file @
859fb6ac
...
...
@@ -51,7 +51,13 @@ const AdminCompliancePage: React.FC = () => {
const
fetchLogs
=
async
(
page
=
1
)
=>
{
setLogsLoading
(
true
);
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
||
[]);
setLogsTotal
(
res
.
data
.
total
||
0
);
}
catch
{
...
...
web/src/pages/admin/Departments/index.tsx
View file @
859fb6ac
'
use client
'
;
import
React
,
{
useState
,
useEffect
}
from
'
react
'
;
import
{
useSearchParams
}
from
'
next/navigation
'
;
import
{
Card
,
Table
,
Typography
,
Space
,
Button
,
Modal
,
Form
,
Input
,
InputNumber
,
message
}
from
'
antd
'
;
...
...
@@ -12,6 +13,7 @@ import type { Department } from '../../../api/doctor';
const
{
Text
}
=
Typography
;
const
AdminDepartmentsPage
:
React
.
FC
=
()
=>
{
const
searchParams
=
useSearchParams
();
const
[
isModalOpen
,
setIsModalOpen
]
=
useState
(
false
);
const
[
editingDept
,
setEditingDept
]
=
useState
<
Department
|
null
>
(
null
);
const
[
form
]
=
Form
.
useForm
();
...
...
@@ -35,6 +37,13 @@ const AdminDepartmentsPage: React.FC = () => {
fetchDepartments
();
},
[]);
// ?action=add 自动打开添加弹窗
useEffect
(()
=>
{
if
(
searchParams
.
get
(
'
action
'
)
===
'
add
'
)
{
handleAdd
();
}
},
[
searchParams
]);
// eslint-disable-line react-hooks/exhaustive-deps
const
handleAdd
=
()
=>
{
setEditingDept
(
null
);
form
.
resetFields
();
...
...
web/src/pages/admin/Doctors/index.tsx
View file @
859fb6ac
'
use client
'
;
import
React
,
{
useState
,
useEffect
,
useCallback
}
from
'
react
'
;
import
{
useSearchParams
}
from
'
next/navigation
'
;
import
{
Card
,
Table
,
Tag
,
Typography
,
Space
,
Button
,
Avatar
,
Modal
,
message
,
Input
,
Select
,
Form
,
Descriptions
,
Row
,
Col
,
...
...
@@ -23,6 +24,7 @@ const statusMap: Record<string, { color: string; text: string }> = {
};
const
AdminDoctorsPage
:
React
.
FC
=
()
=>
{
const
searchParams
=
useSearchParams
();
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
doctors
,
setDoctors
]
=
useState
<
DoctorItem
[]
>
([]);
const
[
total
,
setTotal
]
=
useState
(
0
);
...
...
@@ -65,6 +67,13 @@ const AdminDoctorsPage: React.FC = () => {
fetchDoctors
();
},
[
fetchDoctors
]);
// ?action=add 自动打开添加弹窗
useEffect
(()
=>
{
if
(
searchParams
.
get
(
'
action
'
)
===
'
add
'
)
{
setAddModalVisible
(
true
);
}
},
[
searchParams
]);
const
handleSearch
=
()
=>
{
setPage
(
1
);
fetchDoctors
();
...
...
web/src/pages/admin/Patients/index.tsx
View file @
859fb6ac
'
use client
'
;
import
React
,
{
useState
,
useEffect
,
useCallback
}
from
'
react
'
;
import
{
useSearchParams
}
from
'
next/navigation
'
;
import
{
Card
,
Table
,
Input
,
Select
,
Button
,
Space
,
Tag
,
Avatar
,
Modal
,
message
,
Typography
,
Form
,
InputNumber
,
Row
,
Col
,
...
...
@@ -15,6 +16,7 @@ import type { UserInfo } from '../../../api/user';
const
{
Text
}
=
Typography
;
const
AdminPatientsPage
:
React
.
FC
=
()
=>
{
const
searchParams
=
useSearchParams
();
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
patients
,
setPatients
]
=
useState
<
UserInfo
[]
>
([]);
const
[
total
,
setTotal
]
=
useState
(
0
);
...
...
@@ -45,6 +47,13 @@ const AdminPatientsPage: React.FC = () => {
fetchPatients
();
},
[
fetchPatients
]);
// ?action=add 自动打开添加弹窗
useEffect
(()
=>
{
if
(
searchParams
.
get
(
'
action
'
)
===
'
add
'
)
{
handleAdd
();
}
},
[
searchParams
]);
// eslint-disable-line react-hooks/exhaustive-deps
const
handleSearch
=
()
=>
{
setPage
(
1
);
fetchPatients
();
...
...
web/src/pages/doctor/Certification/index.tsx
View file @
859fb6ac
...
...
@@ -82,8 +82,8 @@ const DoctorCertificationPage: React.FC = () => {
const
res
=
await
request
.
post
(
'
/upload
'
,
formData
,
{
headers
:
{
'
Content-Type
'
:
'
multipart/form-data
'
},
});
file
.
url
=
res
.
data
.
url
;
onSuccess
(
res
.
data
);
file
.
url
=
res
.
data
.
data
?.
url
||
res
.
data
.
url
;
onSuccess
(
res
.
data
.
data
||
res
.
data
);
}
catch
(
err
)
{
onError
(
err
);
}
...
...
web/src/pages/doctor/Consult/PatientList.tsx
View file @
859fb6ac
...
...
@@ -191,6 +191,7 @@ const PatientList: React.FC<PatientListProps> = ({
// 筛选
const
filtered
=
useMemo
(()
=>
{
if
(
!
patients
)
return
[];
if
(
!
searchText
.
trim
())
return
patients
;
const
keyword
=
searchText
.
trim
().
toLowerCase
();
return
patients
.
filter
(
p
=>
...
...
web/src/pages/doctor/Consult/WaitingQueue.tsx
View file @
859fb6ac
...
...
@@ -25,9 +25,9 @@ const formatWaitingTime = (seconds: number) => {
};
const
WaitingQueue
:
React
.
FC
<
WaitingQueueProps
>
=
({
waitingList
,
inProgressList
,
completedList
,
waitingList
=
[]
,
inProgressList
=
[]
,
completedList
=
[]
,
activeConsultId
,
onAccept
,
onReject
,
...
...
web/src/pages/doctor/Profile/index.tsx
View file @
859fb6ac
'
use client
'
;
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
{
UserOutlined
,
EditOutlined
,
...
...
@@ -163,7 +163,7 @@ const DoctorProfilePage: React.FC = () => {
}
}
>
<
Form
form=
{
editForm
}
layout=
"vertical"
>
<
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
>
</
Modal
>
...
...
web/src/pages/patient/Home/index.tsx
View file @
859fb6ac
...
...
@@ -67,10 +67,11 @@ const PatientHomePage: React.FC = () => {
useEffect
(()
=>
{
// Try to get real platform stats from a public endpoint
request
.
get
(
'
/admin/dashboard/stats
'
).
then
((
res
:
any
)
=>
{
if
(
res
.
data
)
{
const
stats
=
res
.
data
?.
data
||
res
.
data
;
if
(
stats
)
{
setPlatformStats
({
doctors
:
`
${
res
.
data
.
total_doctors
||
0
}
`
,
patients
:
`
${
res
.
data
.
total_users
||
0
}
`
,
doctors
:
`
${
stats
.
total_doctors
||
0
}
`
,
patients
:
`
${
stats
.
total_users
||
0
}
`
,
response
:
'
< 5 分钟
'
,
});
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment