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
Hide 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 @@
...
@@ -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"
]
]
}
}
}
}
.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 {
...
@@ -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
,
...
...
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
...
@@ -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
)
}
}
// 返回标准化导航指令
// 返回标准化导航指令
...
...
web/src/api/admin.ts
View file @
859fb6ac
...
@@ -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
}),
// === 问诊管理 ===
// === 问诊管理 ===
...
...
web/src/api/agent.ts
View file @
859fb6ac
...
@@ -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/
${
i
d
}
`
),
delete
:
(
workflowId
:
string
|
number
)
=>
del
<
null
>
(
`/workflow/
${
workflowI
d
}
`
),
publish
:
(
id
:
number
)
=>
put
<
null
>
(
`/admin/workflows/
${
id
}
/publish`
,
{}),
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
{
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
;
...
...
web/src/api/notification.ts
View file @
859fb6ac
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
'
),
};
};
web/src/api/payment.ts
View file @
859fb6ac
// 支付相关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
;
web/src/app/(main)/admin/layout.tsx
View file @
859fb6ac
'
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,14 +240,20 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
...
@@ -219,14 +240,20 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
</
div
>
</
div
>
{
/* Menu */
}
{
/* Menu */
}
<
Menu
{
menuLoading
?
(
mode=
"inline"
<
div
style=
{
{
display
:
'
flex
'
,
justifyContent
:
'
center
'
,
padding
:
'
40px 0
'
}
}
>
selectedKeys=
{
getSelectedKeys
()
}
<
Spin
size=
"small"
/>
defaultOpenKeys=
{
getOpenKeys
()
}
</
div
>
items=
{
menuItems
}
)
:
(
onClick=
{
handleMenuClick
}
<
Menu
style=
{
{
border
:
'
none
'
,
padding
:
'
8px 0
'
}
}
mode=
"inline"
/>
selectedKeys=
{
getSelectedKeys
()
}
defaultOpenKeys=
{
getOpenKeys
()
}
items=
{
menuItems
}
onClick=
{
handleMenuClick
}
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
'
}
}
>
...
...
web/src/app/(main)/admin/safety/page.tsx
View file @
859fb6ac
...
@@ -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,
...
...
web/src/app/(main)/admin/tools/page.tsx
View file @
859fb6ac
...
@@ -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
/>
},
...
...
web/src/app/(main)/admin/workflows/page.tsx
View file @
859fb6ac
...
@@ -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();
}}>
}}>
...
...
web/src/app/(main)/doctor/layout.tsx
View file @
859fb6ac
'
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
...
...
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
'
;
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
>
);
);
}
}
web/src/app/(main)/patient/layout.tsx
View file @
859fb6ac
'
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
...
...
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
'
;
'
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
}
}
>
{
embedded
Url
}
{
embedded
PageTitle
}
</
span
>
</
span
>
<
div
<
div
onClick=
{
closeEmbedded
}
onClick=
{
closeEmbedded
}
...
@@ -261,16 +275,28 @@ const FloatContainer: React.FC = () => {
...
@@ -261,16 +275,28 @@ const FloatContainer: React.FC = () => {
</
div
>
</
div
>
</
div
>
</
div
>
{
/* iframe 嵌入业务系统 */
}
{
/* iframe 嵌入业务系统 */
}
<
iframe
<
div
style=
{
{
flex
:
1
,
position
:
'
relative
'
}
}
>
src=
{
embeddedUrl
}
{
iframeLoading
&&
(
style=
{
{
<
div
style=
{
{
flex
:
1
,
position
:
'
absolute
'
,
inset
:
0
,
display
:
'
flex
'
,
width
:
'
100%
'
,
alignItems
:
'
center
'
,
justifyContent
:
'
center
'
,
border
:
'
none
'
,
background
:
'
#f5f6fa
'
,
zIndex
:
1
,
background
:
'
#fff
'
,
}
}
>
}
}
<
Spin
tip=
"页面加载中..."
/>
title=
"业务页面"
</
div
>
/>
)
}
<
iframe
src=
{
embeddedUrl
}
onLoad=
{
()
=>
setIframeLoading
(
false
)
}
style=
{
{
width
:
'
100%
'
,
height
:
'
100%
'
,
border
:
'
none
'
,
background
:
'
#fff
'
,
}
}
title=
{
embeddedPageTitle
||
'
业务页面
'
}
/>
</
div
>
</
div
>
</
div
>
)
}
)
}
</
div
>
</
div
>
...
...
web/src/components/GlobalAIFloat/SuggestedActions.tsx
View file @
859fb6ac
...
@@ -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
)
{
...
...
web/src/hooks/index.ts
View file @
859fb6ac
export
*
from
'
./useAuth
'
;
export
*
from
'
./useAuth
'
;
export
*
from
'
./useVideoCall
'
;
export
*
from
'
./useVideoCall
'
;
export
*
from
'
./useAIChat
'
;
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 = () => {
...
@@ -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
{
...
...
web/src/pages/admin/Departments/index.tsx
View file @
859fb6ac
'
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
();
...
...
web/src/pages/admin/Doctors/index.tsx
View file @
859fb6ac
'
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
();
...
...
web/src/pages/admin/Patients/index.tsx
View file @
859fb6ac
'
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
();
...
...
web/src/pages/doctor/Certification/index.tsx
View file @
859fb6ac
...
@@ -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
);
}
}
...
...
web/src/pages/doctor/Consult/PatientList.tsx
View file @
859fb6ac
...
@@ -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
=>
...
...
web/src/pages/doctor/Consult/WaitingQueue.tsx
View file @
859fb6ac
...
@@ -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
,
...
...
web/src/pages/doctor/Profile/index.tsx
View file @
859fb6ac
'
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
>
...
...
web/src/pages/patient/Home/index.tsx
View file @
859fb6ac
...
@@ -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 分钟
'
,
});
});
}
}
...
...
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