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
9033450a
Commit
9033450a
authored
Mar 02, 2026
by
yuguo
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix
parent
f5410548
Changes
10
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
1251 additions
and
499 deletions
+1251
-499
.claude/settings.local.json
.claude/settings.local.json
+2
-1
web/src/app/(main)/admin/agents/page.tsx
web/src/app/(main)/admin/agents/page.tsx
+156
-24
web/src/app/(main)/admin/workflows/page.tsx
web/src/app/(main)/admin/workflows/page.tsx
+20
-9
web/src/components/GlobalAIFloat/FloatContainer.tsx
web/src/components/GlobalAIFloat/FloatContainer.tsx
+5
-0
web/src/components/workflow/VisualWorkflowEditor.tsx
web/src/components/workflow/VisualWorkflowEditor.tsx
+1
-0
web/src/pages/doctor/Consult/AIPanel.tsx
web/src/pages/doctor/Consult/AIPanel.tsx
+224
-139
web/src/pages/doctor/Consult/ChatPanel.tsx
web/src/pages/doctor/Consult/ChatPanel.tsx
+244
-96
web/src/pages/doctor/Consult/PatientList.tsx
web/src/pages/doctor/Consult/PatientList.tsx
+214
-99
web/src/pages/doctor/Consult/index.tsx
web/src/pages/doctor/Consult/index.tsx
+91
-60
web/src/pages/doctor/Prescription/index.tsx
web/src/pages/doctor/Prescription/index.tsx
+294
-71
No files found.
.claude/settings.local.json
View file @
9033450a
...
@@ -23,7 +23,8 @@
...
@@ -23,7 +23,8 @@
"Bash(npm:*)"
,
"Bash(npm:*)"
,
"Bash(./api.exe:*)"
,
"Bash(./api.exe:*)"
,
"Bash(PGPASSWORD=123456 psql:*)"
,
"Bash(PGPASSWORD=123456 psql:*)"
,
"Bash(go mod:*)"
"Bash(go mod:*)"
,
"Bash(go vet:*)"
]
]
}
}
}
}
web/src/app/(main)/admin/agents/page.tsx
View file @
9033450a
'
use client
'
;
'
use client
'
;
import
{
useState
}
from
'
react
'
;
import
{
useState
,
useEffect
}
from
'
react
'
;
import
{
Card
,
Table
,
Tag
,
Button
,
Modal
,
Input
,
message
,
Space
,
Collapse
,
Timeline
,
Typography
}
from
'
antd
'
;
import
{
Card
,
Table
,
Tag
,
Button
,
Modal
,
Input
,
message
,
Space
,
Collapse
,
Timeline
,
Typography
,
Tabs
,
DatePicker
,
Select
,
Badge
,
Tooltip
}
from
'
antd
'
;
import
{
RobotOutlined
,
PlayCircleOutlined
,
ToolOutlined
,
CheckCircleOutlined
,
CloseCircleOutlined
}
from
'
@ant-design/icons
'
;
import
{
RobotOutlined
,
PlayCircleOutlined
,
ToolOutlined
,
CheckCircleOutlined
,
CloseCircleOutlined
,
HistoryOutlined
,
ThunderboltOutlined
}
from
'
@ant-design/icons
'
;
import
{
agentApi
}
from
'
@/api/agent
'
;
import
{
agentApi
}
from
'
@/api/agent
'
;
import
type
{
ToolCall
}
from
'
@/api/agent
'
;
import
type
{
ToolCall
,
AgentExecutionLog
}
from
'
@/api/agent
'
;
const
{
Text
}
=
Typography
;
const
{
Text
}
=
Typography
;
const
{
RangePicker
}
=
DatePicker
;
const
BUILTIN_AGENTS
=
[
const
BUILTIN_AGENTS
=
[
{
id
:
'
pre_consult_agent
'
,
name
:
'
预问诊智能助手
'
,
description
:
'
通过多轮对话收集患者症状,生成预问诊报告
'
,
category
:
'
pre_consult
'
,
tools
:
[
'
query_symptom_knowledge
'
,
'
recommend_department
'
],
max_iterations
:
5
},
{
id
:
'
pre_consult_agent
'
,
name
:
'
预问诊智能助手
'
,
description
:
'
通过多轮对话收集患者症状,生成预问诊报告
'
,
category
:
'
pre_consult
'
,
tools
:
[
'
query_symptom_knowledge
'
,
'
recommend_department
'
],
max_iterations
:
5
},
...
@@ -16,17 +17,10 @@ const BUILTIN_AGENTS = [
...
@@ -16,17 +17,10 @@ const BUILTIN_AGENTS = [
];
];
const
categoryColor
:
Record
<
string
,
string
>
=
{
const
categoryColor
:
Record
<
string
,
string
>
=
{
pre_consult
:
'
blue
'
,
pre_consult
:
'
blue
'
,
diagnosis
:
'
purple
'
,
prescription
:
'
orange
'
,
follow_up
:
'
green
'
,
diagnosis
:
'
purple
'
,
prescription
:
'
orange
'
,
follow_up
:
'
green
'
,
};
};
const
categoryLabel
:
Record
<
string
,
string
>
=
{
const
categoryLabel
:
Record
<
string
,
string
>
=
{
pre_consult
:
'
预问诊
'
,
pre_consult
:
'
预问诊
'
,
diagnosis
:
'
诊断辅助
'
,
prescription
:
'
处方审核
'
,
follow_up
:
'
随访管理
'
,
diagnosis
:
'
诊断辅助
'
,
prescription
:
'
处方审核
'
,
follow_up
:
'
随访管理
'
,
};
};
interface
AgentResponse
{
interface
AgentResponse
{
...
@@ -34,7 +28,6 @@ interface AgentResponse {
...
@@ -34,7 +28,6 @@ interface AgentResponse {
tool_calls
?:
ToolCall
[];
tool_calls
?:
ToolCall
[];
iterations
?:
number
;
iterations
?:
number
;
total_tokens
?:
number
;
total_tokens
?:
number
;
finish_reason
?:
string
;
}
}
export
default
function
AgentsPage
()
{
export
default
function
AgentsPage
()
{
...
@@ -44,6 +37,24 @@ export default function AgentsPage() {
...
@@ -44,6 +37,24 @@ export default function AgentsPage() {
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
sessionId
,
setSessionId
]
=
useState
(
''
);
const
[
sessionId
,
setSessionId
]
=
useState
(
''
);
// 执行日志
const
[
logs
,
setLogs
]
=
useState
<
AgentExecutionLog
[]
>
([]);
const
[
logTotal
,
setLogTotal
]
=
useState
(
0
);
const
[
logLoading
,
setLogLoading
]
=
useState
(
false
);
const
[
logFilter
,
setLogFilter
]
=
useState
<
{
agent_id
?:
string
;
page
:
number
;
page_size
:
number
}
>
({
page
:
1
,
page_size
:
10
});
const
[
expandedLog
,
setExpandedLog
]
=
useState
<
AgentExecutionLog
|
null
>
(
null
);
const
fetchLogs
=
async
(
filter
=
logFilter
)
=>
{
setLogLoading
(
true
);
try
{
const
res
=
await
agentApi
.
getExecutionLogs
(
filter
);
setLogs
(
res
.
data
?.
list
||
[]);
setLogTotal
(
res
.
data
?.
total
||
0
);
}
catch
{}
finally
{
setLogLoading
(
false
);
}
};
useEffect
(()
=>
{
fetchLogs
();
},
[]);
const
openTest
=
(
agentId
:
string
,
agentName
:
string
)
=>
{
const
openTest
=
(
agentId
:
string
,
agentName
:
string
)
=>
{
setTestModal
({
open
:
true
,
agentId
,
agentName
});
setTestModal
({
open
:
true
,
agentId
,
agentName
});
setTestMessages
([]);
setTestMessages
([]);
...
@@ -59,10 +70,9 @@ export default function AgentsPage() {
...
@@ -59,10 +70,9 @@ export default function AgentsPage() {
try
{
try
{
const
res
=
await
agentApi
.
chat
(
testModal
.
agentId
,
{
session_id
:
sessionId
,
message
:
userMsg
});
const
res
=
await
agentApi
.
chat
(
testModal
.
agentId
,
{
session_id
:
sessionId
,
message
:
userMsg
});
const
agentData
=
res
.
data
as
AgentResponse
;
const
agentData
=
res
.
data
as
AgentResponse
;
const
reply
=
agentData
?.
response
||
'
无响应
'
;
setTestMessages
(
prev
=>
[...
prev
,
{
setTestMessages
(
prev
=>
[...
prev
,
{
role
:
'
assistant
'
,
role
:
'
assistant
'
,
content
:
reply
,
content
:
agentData
?.
response
||
'
无响应
'
,
toolCalls
:
agentData
?.
tool_calls
,
toolCalls
:
agentData
?.
tool_calls
,
meta
:
{
iterations
:
agentData
?.
iterations
,
tokens
:
agentData
?.
total_tokens
},
meta
:
{
iterations
:
agentData
?.
iterations
,
tokens
:
agentData
?.
total_tokens
},
}]);
}]);
...
@@ -89,8 +99,7 @@ export default function AgentsPage() {
...
@@ -89,8 +99,7 @@ export default function AgentsPage() {
<
div
style=
{
{
color
:
'
#8c8c8c
'
}
}
>
参数:
{
tc
.
arguments
}
</
div
>
<
div
style=
{
{
color
:
'
#8c8c8c
'
}
}
>
参数:
{
tc
.
arguments
}
</
div
>
{
tc
.
result
&&
(
{
tc
.
result
&&
(
<
div
style=
{
{
color
:
tc
.
success
?
'
#52c41a
'
:
'
#ff4d4f
'
}
}
>
<
div
style=
{
{
color
:
tc
.
success
?
'
#52c41a
'
:
'
#ff4d4f
'
}
}
>
结果:
{
tc
.
success
?
JSON
.
stringify
(
tc
.
result
.
data
).
slice
(
0
,
100
)
:
tc
.
result
.
error
}
{
tc
.
success
?
JSON
.
stringify
(
tc
.
result
.
data
).
slice
(
0
,
100
)
+
(
JSON
.
stringify
(
tc
.
result
.
data
).
length
>
100
?
'
...
'
:
''
)
:
tc
.
result
.
error
}
{
JSON
.
stringify
(
tc
.
result
.
data
).
length
>
100
&&
'
...
'
}
</
div
>
</
div
>
)
}
)
}
</
div
>
</
div
>
...
@@ -101,7 +110,7 @@ export default function AgentsPage() {
...
@@ -101,7 +110,7 @@ export default function AgentsPage() {
);
);
};
};
const
c
olumns
=
[
const
agentC
olumns
=
[
{
{
title
:
'
智能体名称
'
,
key
:
'
name
'
,
title
:
'
智能体名称
'
,
key
:
'
name
'
,
render
:
(
_
:
unknown
,
r
:
typeof
BUILTIN_AGENTS
[
0
])
=>
(
render
:
(
_
:
unknown
,
r
:
typeof
BUILTIN_AGENTS
[
0
])
=>
(
...
@@ -122,9 +131,7 @@ export default function AgentsPage() {
...
@@ -122,9 +131,7 @@ export default function AgentsPage() {
},
},
{
{
title
:
'
工具
'
,
dataIndex
:
'
tools
'
,
key
:
'
tools
'
,
title
:
'
工具
'
,
dataIndex
:
'
tools
'
,
key
:
'
tools
'
,
render
:
(
v
:
string
[])
=>
(
render
:
(
v
:
string
[])
=>
<
Space
size=
{
4
}
wrap
>
{
v
.
map
(
t
=>
<
Tag
key=
{
t
}
style=
{
{
fontSize
:
11
,
margin
:
0
}
}
>
{
t
}
</
Tag
>)
}
</
Space
>,
<
Space
size=
{
4
}
wrap
>
{
v
.
map
(
t
=>
<
Tag
key=
{
t
}
style=
{
{
fontSize
:
11
,
margin
:
0
}
}
>
{
t
}
</
Tag
>)
}
</
Space
>
),
},
},
{
title
:
'
最大迭代
'
,
dataIndex
:
'
max_iterations
'
,
key
:
'
max_iterations
'
,
width
:
90
,
render
:
(
v
:
number
)
=>
<
Tag
>
{
v
}
次
</
Tag
>
},
{
title
:
'
最大迭代
'
,
dataIndex
:
'
max_iterations
'
,
key
:
'
max_iterations
'
,
width
:
90
,
render
:
(
v
:
number
)
=>
<
Tag
>
{
v
}
次
</
Tag
>
},
{
{
...
@@ -135,17 +142,105 @@ export default function AgentsPage() {
...
@@ -135,17 +142,105 @@ export default function AgentsPage() {
},
},
];
];
const
logColumns
=
[
{
title
:
'
时间
'
,
dataIndex
:
'
created_at
'
,
key
:
'
created_at
'
,
width
:
160
,
render
:
(
v
:
string
)
=>
<
Text
style=
{
{
fontSize
:
12
}
}
>
{
new
Date
(
v
).
toLocaleString
(
'
zh-CN
'
)
}
</
Text
>,
},
{
title
:
'
智能体
'
,
dataIndex
:
'
agent_id
'
,
key
:
'
agent_id
'
,
width
:
160
,
render
:
(
v
:
string
)
=>
<
Tag
color=
{
categoryColor
[
v
?.
replace
(
'
_agent
'
,
''
)]
||
'
default
'
}
>
{
v
}
</
Tag
>,
},
{
title
:
'
用户ID
'
,
dataIndex
:
'
user_id
'
,
key
:
'
user_id
'
,
width
:
130
,
ellipsis
:
true
,
render
:
(
v
:
string
)
=>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
12
}
}
>
{
v
}
</
Text
>
},
{
title
:
'
输入摘要
'
,
dataIndex
:
'
input
'
,
key
:
'
input
'
,
ellipsis
:
true
,
render
:
(
v
:
string
)
=>
{
try
{
const
obj
=
JSON
.
parse
(
v
);
return
<
Tooltip
title=
{
obj
.
message
}
><
Text
style=
{
{
fontSize
:
12
}
}
>
{
(
obj
.
message
||
''
).
slice
(
0
,
30
)
}
...
</
Text
></
Tooltip
>;
}
catch
{
return
v
;
}
},
},
{
title
:
'
迭代
'
,
dataIndex
:
'
iterations
'
,
key
:
'
iterations
'
,
width
:
70
,
render
:
(
v
:
number
)
=>
<
Badge
count=
{
v
}
style=
{
{
backgroundColor
:
'
#722ed1
'
}
}
/>
},
{
title
:
'
Tokens
'
,
dataIndex
:
'
total_tokens
'
,
key
:
'
total_tokens
'
,
width
:
80
,
render
:
(
v
:
number
)
=>
<
Text
style=
{
{
fontSize
:
12
}
}
>
{
v
}
</
Text
>
},
{
title
:
'
耗时(ms)
'
,
dataIndex
:
'
duration_ms
'
,
key
:
'
duration_ms
'
,
width
:
90
,
render
:
(
v
:
number
)
=>
<
Text
style=
{
{
fontSize
:
12
}
}
>
{
v
}
</
Text
>
},
{
title
:
'
状态
'
,
dataIndex
:
'
success
'
,
key
:
'
success
'
,
width
:
80
,
render
:
(
v
:
boolean
)
=>
v
?
<
Badge
status=
"success"
text=
"成功"
/>
:
<
Badge
status=
"error"
text=
"失败"
/>,
},
{
title
:
'
操作
'
,
key
:
'
action
'
,
width
:
80
,
render
:
(
_
:
unknown
,
record
:
AgentExecutionLog
)
=>
(
<
Button
type=
"link"
size=
"small"
icon=
{
<
HistoryOutlined
/>
}
onClick=
{
()
=>
setExpandedLog
(
record
)
}
>
详情
</
Button
>
),
},
];
return
(
return
(
<
div
style=
{
{
padding
:
'
20px 24px
'
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
16
}
}
>
<
div
style=
{
{
padding
:
'
20px 24px
'
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
16
}
}
>
<
div
>
<
div
>
<
h2
style=
{
{
fontSize
:
20
,
fontWeight
:
700
,
color
:
'
#1d2129
'
,
margin
:
0
}
}
>
智能体管理
</
h2
>
<
h2
style=
{
{
fontSize
:
20
,
fontWeight
:
700
,
color
:
'
#1d2129
'
,
margin
:
0
}
}
>
智能体管理
</
h2
>
<
div
style=
{
{
fontSize
:
13
,
color
:
'
#8c8c8c
'
,
marginTop
:
2
}
}
>
管理平台内置 AI 智能体,支持对话测试与
工具调用
查看
</
div
>
<
div
style=
{
{
fontSize
:
13
,
color
:
'
#8c8c8c
'
,
marginTop
:
2
}
}
>
管理平台内置 AI 智能体,支持对话测试与
执行日志
查看
</
div
>
</
div
>
</
div
>
<
Card
style=
{
{
borderRadius
:
12
,
border
:
'
1px solid #edf2fc
'
}
}
>
<
Card
style=
{
{
borderRadius
:
12
,
border
:
'
1px solid #edf2fc
'
}
}
>
<
Table
dataSource=
{
BUILTIN_AGENTS
}
columns=
{
columns
}
rowKey=
"id"
pagination=
{
false
}
size=
"small"
/>
<
Tabs
items=
{
[
{
key
:
'
agents
'
,
label
:
<
Space
><
RobotOutlined
/>
智能体列表
</
Space
>,
children
:
<
Table
dataSource=
{
BUILTIN_AGENTS
}
columns=
{
agentColumns
}
rowKey=
"id"
pagination=
{
false
}
size=
"small"
/>,
},
{
key
:
'
logs
'
,
label
:
<
Space
><
HistoryOutlined
/>
执行日志
</
Space
>,
children
:
(
<
div
style=
{
{
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
12
}
}
>
<
div
style=
{
{
display
:
'
flex
'
,
gap
:
8
,
alignItems
:
'
center
'
,
flexWrap
:
'
wrap
'
}
}
>
<
Select
placeholder=
"筛选智能体"
allowClear
style=
{
{
width
:
200
}
}
options=
{
BUILTIN_AGENTS
.
map
(
a
=>
({
value
:
a
.
id
,
label
:
a
.
name
}))
}
onChange=
{
v
=>
{
const
newFilter
=
{
...
logFilter
,
agent_id
:
v
,
page
:
1
};
setLogFilter
(
newFilter
);
fetchLogs
(
newFilter
);
}
}
/>
<
RangePicker
onChange=
{
(
_
,
strs
)
=>
{
const
newFilter
=
{
...
logFilter
,
...(
strs
[
0
]
?
{
start
:
strs
[
0
]
}
:
{}),
...(
strs
[
1
]
?
{
end
:
strs
[
1
]
}
:
{}),
page
:
1
};
setLogFilter
(
newFilter
);
fetchLogs
(
newFilter
);
}
}
/>
<
Button
icon=
{
<
ThunderboltOutlined
/>
}
onClick=
{
()
=>
fetchLogs
()
}
>
刷新
</
Button
>
</
div
>
<
Table
dataSource=
{
logs
}
columns=
{
logColumns
}
rowKey=
"id"
loading=
{
logLoading
}
size=
"small"
pagination=
{
{
current
:
logFilter
.
page
,
pageSize
:
logFilter
.
page_size
,
total
:
logTotal
,
size
:
'
small
'
,
showTotal
:
(
t
)
=>
`共 ${t} 条`
,
onChange
:
(
page
,
pageSize
)
=>
{
const
newFilter
=
{
...
logFilter
,
page
,
page_size
:
pageSize
};
setLogFilter
(
newFilter
);
fetchLogs
(
newFilter
);
},
}
}
/>
</
div
>
),
},
]
}
/>
</
Card
>
</
Card
>
{
/* 测试对话 Modal */
}
<
Modal
<
Modal
title=
{
`测试 · ${testModal.agentName}`
}
title=
{
`测试 · ${testModal.agentName}`
}
open=
{
testModal
.
open
}
open=
{
testModal
.
open
}
...
@@ -179,6 +274,43 @@ export default function AgentsPage() {
...
@@ -179,6 +274,43 @@ export default function AgentsPage() {
<
Button
type=
"primary"
onClick=
{
sendMessage
}
loading=
{
loading
}
>
发送
</
Button
>
<
Button
type=
"primary"
onClick=
{
sendMessage
}
loading=
{
loading
}
>
发送
</
Button
>
</
Space
.
Compact
>
</
Space
.
Compact
>
</
Modal
>
</
Modal
>
{
/* 执行日志详情 Modal */
}
<
Modal
title=
{
<
Space
><
HistoryOutlined
/>
执行日志详情
</
Space
>
}
open=
{
!!
expandedLog
}
onCancel=
{
()
=>
setExpandedLog
(
null
)
}
footer=
{
null
}
width=
{
700
}
>
{
expandedLog
&&
(()
=>
{
let
toolCalls
:
ToolCall
[]
=
[];
try
{
toolCalls
=
JSON
.
parse
(
expandedLog
.
tool_calls
||
'
[]
'
);
}
catch
{}
let
inputObj
:
Record
<
string
,
unknown
>
=
{};
try
{
inputObj
=
JSON
.
parse
(
expandedLog
.
input
||
'
{}
'
);
}
catch
{}
let
outputObj
:
Record
<
string
,
unknown
>
=
{};
try
{
outputObj
=
JSON
.
parse
(
expandedLog
.
output
||
'
{}
'
);
}
catch
{}
return
(
<
div
style=
{
{
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
12
}
}
>
<
div
style=
{
{
display
:
'
grid
'
,
gridTemplateColumns
:
'
1fr 1fr
'
,
gap
:
8
,
fontSize
:
13
}
}
>
<
div
><
Text
type=
"secondary"
>
智能体:
</
Text
><
Tag
>
{
expandedLog
.
agent_id
}
</
Tag
></
div
>
<
div
><
Text
type=
"secondary"
>
状态:
</
Text
><
Badge
status=
{
expandedLog
.
success
?
'
success
'
:
'
error
'
}
text=
{
expandedLog
.
success
?
'
成功
'
:
'
失败
'
}
/></
div
>
<
div
><
Text
type=
"secondary"
>
迭代次数:
</
Text
>
{
expandedLog
.
iterations
}
</
div
>
<
div
><
Text
type=
"secondary"
>
耗时:
</
Text
>
{
expandedLog
.
duration_ms
}
ms
</
div
>
<
div
><
Text
type=
"secondary"
>
Tokens:
</
Text
>
{
expandedLog
.
total_tokens
}
</
div
>
<
div
><
Text
type=
"secondary"
>
完成原因:
</
Text
>
{
expandedLog
.
finish_reason
||
'
-
'
}
</
div
>
</
div
>
<
Card
size=
"small"
title=
"用户输入"
>
<
Text
style=
{
{
fontSize
:
12
}
}
>
{
(
inputObj
.
message
as
string
)
||
expandedLog
.
input
}
</
Text
>
</
Card
>
<
Card
size=
"small"
title=
"AI 回复"
>
<
Text
style=
{
{
fontSize
:
12
}
}
>
{
(
outputObj
.
response
as
string
)
||
expandedLog
.
output
}
</
Text
>
</
Card
>
{
renderToolCalls
(
toolCalls
)
}
</
div
>
);
})()
}
</
Modal
>
</
div
>
</
div
>
);
);
}
}
web/src/app/(main)/admin/workflows/page.tsx
View file @
9033450a
...
@@ -5,7 +5,6 @@ import { Card, Table, Tag, Button, Modal, Form, Input, Select, message, Space, B
...
@@ -5,7 +5,6 @@ import { Card, Table, Tag, Button, Modal, Form, Input, Select, message, Space, B
import
{
DeploymentUnitOutlined
,
PlayCircleOutlined
,
PlusOutlined
,
EditOutlined
}
from
'
@ant-design/icons
'
;
import
{
DeploymentUnitOutlined
,
PlayCircleOutlined
,
PlusOutlined
,
EditOutlined
}
from
'
@ant-design/icons
'
;
import
{
workflowApi
}
from
'
@/api/agent
'
;
import
{
workflowApi
}
from
'
@/api/agent
'
;
import
VisualWorkflowEditor
from
'
@/components/workflow/VisualWorkflowEditor
'
;
import
VisualWorkflowEditor
from
'
@/components/workflow/VisualWorkflowEditor
'
;
import
type
{
Node
,
Edge
}
from
'
@xyflow/react
'
;
interface
Workflow
{
interface
Workflow
{
id
:
number
;
id
:
number
;
...
@@ -60,7 +59,13 @@ export default function WorkflowsPage() {
...
@@ -60,7 +59,13 @@ export default function WorkflowsPage() {
},
},
edges
:
[{
id
:
'
e1
'
,
source_node
:
'
start
'
,
target_node
:
'
end
'
}],
edges
:
[{
id
:
'
e1
'
,
source_node
:
'
start
'
,
target_node
:
'
end
'
}],
};
};
await
workflowApi
.
create
({
...
values
,
definition
:
JSON
.
stringify
(
definition
)
});
await
workflowApi
.
create
({
workflow_id
:
values
.
workflow_id
,
name
:
values
.
name
,
description
:
values
.
description
,
category
:
values
.
category
,
definition
:
JSON
.
stringify
(
definition
),
});
message
.
success
(
'
创建成功
'
);
message
.
success
(
'
创建成功
'
);
setCreateModal
(
false
);
setCreateModal
(
false
);
form
.
resetFields
();
form
.
resetFields
();
...
@@ -72,7 +77,8 @@ export default function WorkflowsPage() {
...
@@ -72,7 +77,8 @@ export default function WorkflowsPage() {
}
}
};
};
const
handleSaveWorkflow
=
useCallback
(
async
(
nodes
:
Node
[],
edges
:
Edge
[])
=>
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const
handleSaveWorkflow
=
useCallback
(
async
(
nodes
:
any
[],
edges
:
any
[])
=>
{
if
(
!
editingWorkflow
)
return
;
if
(
!
editingWorkflow
)
return
;
try
{
try
{
await
workflowApi
.
update
(
editingWorkflow
.
id
,
{
definition
:
JSON
.
stringify
({
nodes
,
edges
})
});
await
workflowApi
.
update
(
editingWorkflow
.
id
,
{
definition
:
JSON
.
stringify
({
nodes
,
edges
})
});
...
@@ -81,7 +87,8 @@ export default function WorkflowsPage() {
...
@@ -81,7 +87,8 @@ export default function WorkflowsPage() {
}
catch
{
message
.
error
(
'
保存失败
'
);
}
}
catch
{
message
.
error
(
'
保存失败
'
);
}
},
[
editingWorkflow
]);
},
[
editingWorkflow
]);
const
handleExecuteFromEditor
=
useCallback
(
async
(
nodes
:
Node
[],
edges
:
Edge
[])
=>
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const
handleExecuteFromEditor
=
useCallback
(
async
(
nodes
:
any
[],
edges
:
any
[])
=>
{
if
(
!
editingWorkflow
)
return
;
if
(
!
editingWorkflow
)
return
;
try
{
try
{
const
result
=
await
workflowApi
.
execute
(
editingWorkflow
.
workflow_id
,
{
workflow_data
:
{
nodes
,
edges
}
});
const
result
=
await
workflowApi
.
execute
(
editingWorkflow
.
workflow_id
,
{
workflow_data
:
{
nodes
,
edges
}
});
...
@@ -96,7 +103,7 @@ export default function WorkflowsPage() {
...
@@ -96,7 +103,7 @@ export default function WorkflowsPage() {
} catch { message.error('执行失败'); }
} catch { message.error('执行失败'); }
};
};
const getEditorInitialData = (): { nodes?:
Node[]; edges?: Edge
[] } | undefined => {
const getEditorInitialData = (): { nodes?:
unknown[]; edges?: unknown
[] } | undefined => {
if (!editingWorkflow?.definition) return undefined;
if (!editingWorkflow?.definition) return undefined;
try { return JSON.parse(editingWorkflow.definition); } catch { return undefined; }
try { return JSON.parse(editingWorkflow.definition); } catch { return undefined; }
};
};
...
@@ -193,10 +200,14 @@ export default function WorkflowsPage() {
...
@@ -193,10 +200,14 @@ export default function WorkflowsPage() {
<div style={{ height: 650 }}>
<div style={{ height: 650 }}>
<VisualWorkflowEditor
<VisualWorkflowEditor
workflowName={editingWorkflow?.name || '编辑工作流'}
workflowName={editingWorkflow?.name || '编辑工作流'}
initialNodes={getEditorInitialData()?.nodes}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
initialEdges={getEditorInitialData()?.edges}
initialNodes={getEditorInitialData()?.nodes as any}
onSave={handleSaveWorkflow}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onExecute={handleExecuteFromEditor}
initialEdges={getEditorInitialData()?.edges as any}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onSave={handleSaveWorkflow as any}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onExecute={handleExecuteFromEditor as any}
/>
/>
</div>
</div>
</Modal>
</Modal>
...
...
web/src/components/GlobalAIFloat/FloatContainer.tsx
View file @
9033450a
...
@@ -8,6 +8,7 @@ import {
...
@@ -8,6 +8,7 @@ import {
HeartOutlined
,
HeartOutlined
,
MessageOutlined
,
MessageOutlined
,
}
from
'
@ant-design/icons
'
;
}
from
'
@ant-design/icons
'
;
import
{
usePathname
}
from
'
next/navigation
'
;
import
{
useUserStore
}
from
'
../../store/userStore
'
;
import
{
useUserStore
}
from
'
../../store/userStore
'
;
import
{
useAIAssistStore
}
from
'
../../store/aiAssistStore
'
;
import
{
useAIAssistStore
}
from
'
../../store/aiAssistStore
'
;
import
ChatPanel
from
'
./ChatPanel
'
;
import
ChatPanel
from
'
./ChatPanel
'
;
...
@@ -15,6 +16,7 @@ import ChatPanel from './ChatPanel';
...
@@ -15,6 +16,7 @@ import ChatPanel from './ChatPanel';
const
FloatContainer
:
React
.
FC
=
()
=>
{
const
FloatContainer
:
React
.
FC
=
()
=>
{
const
{
user
}
=
useUserStore
();
const
{
user
}
=
useUserStore
();
const
{
isOpen
,
patientContext
,
openDrawer
,
closeDrawer
}
=
useAIAssistStore
();
const
{
isOpen
,
patientContext
,
openDrawer
,
closeDrawer
}
=
useAIAssistStore
();
const
pathname
=
usePathname
();
if
(
!
user
)
return
null
;
if
(
!
user
)
return
null
;
...
@@ -22,6 +24,9 @@ const FloatContainer: React.FC = () => {
...
@@ -22,6 +24,9 @@ const FloatContainer: React.FC = () => {
const
isPatient
=
user
.
role
===
'
patient
'
;
const
isPatient
=
user
.
role
===
'
patient
'
;
if
(
!
isDoctor
&&
!
isPatient
)
return
null
;
if
(
!
isDoctor
&&
!
isPatient
)
return
null
;
// Hide on doctor consult page — it has its own integrated AI panel
if
(
isDoctor
&&
pathname
?.
includes
(
'
/doctor/consult
'
))
return
null
;
const
primaryColor
=
isPatient
const
primaryColor
=
isPatient
?
'
linear-gradient(135deg, #22c55e, #06b6d4)
'
?
'
linear-gradient(135deg, #22c55e, #06b6d4)
'
:
'
linear-gradient(135deg, #3b82f6, #8b5cf6)
'
;
:
'
linear-gradient(135deg, #3b82f6, #8b5cf6)
'
;
...
...
web/src/components/workflow/VisualWorkflowEditor.tsx
View file @
9033450a
...
@@ -40,6 +40,7 @@ interface NodeData {
...
@@ -40,6 +40,7 @@ interface NodeData {
label
:
string
;
label
:
string
;
nodeType
:
NodeType
;
nodeType
:
NodeType
;
config
?:
Record
<
string
,
unknown
>
;
config
?:
Record
<
string
,
unknown
>
;
[
key
:
string
]:
unknown
;
}
}
const
NODE_CONFIGS
:
{
type
:
NodeType
;
label
:
string
;
icon
:
React
.
ReactNode
;
color
:
string
;
bgColor
:
string
}[]
=
[
const
NODE_CONFIGS
:
{
type
:
NodeType
;
label
:
string
;
icon
:
React
.
ReactNode
;
color
:
string
;
bgColor
:
string
}[]
=
[
...
...
web/src/pages/doctor/Consult/AIPanel.tsx
View file @
9033450a
import
React
,
{
useState
}
from
'
react
'
;
import
React
,
{
useState
}
from
'
react
'
;
import
{
import
{
Card
,
Tabs
,
Typography
,
Space
,
Empty
,
Tag
,
Divider
,
Alert
,
Spin
,
Badge
,
Button
,
Tabs
,
Typography
,
Space
,
Empty
,
Tag
,
Alert
,
Spin
,
Badge
,
Button
,
Collapse
,
Timeline
,
Collapse
,
Timeline
,
Divider
,
}
from
'
antd
'
;
}
from
'
antd
'
;
import
{
import
{
RobotOutlined
,
FileTextOutlined
,
UserOutlined
,
RobotOutlined
,
FileTextOutlined
,
UserOutlined
,
...
@@ -20,6 +20,8 @@ interface AIPanelProps {
...
@@ -20,6 +20,8 @@ interface AIPanelProps {
activeConsultId
?:
string
;
activeConsultId
?:
string
;
preConsultReport
:
PreConsultResponse
|
null
;
preConsultReport
:
PreConsultResponse
|
null
;
preConsultLoading
:
boolean
;
preConsultLoading
:
boolean
;
onDiagnosisChange
?:
(
text
:
string
)
=>
void
;
onMedicationChange
?:
(
text
:
string
)
=>
void
;
}
}
const
AIPanel
:
React
.
FC
<
AIPanelProps
>
=
({
const
AIPanel
:
React
.
FC
<
AIPanelProps
>
=
({
...
@@ -27,6 +29,8 @@ const AIPanel: React.FC<AIPanelProps> = ({
...
@@ -27,6 +29,8 @@ const AIPanel: React.FC<AIPanelProps> = ({
activeConsultId
,
activeConsultId
,
preConsultReport
,
preConsultReport
,
preConsultLoading
,
preConsultLoading
,
onDiagnosisChange
,
onMedicationChange
,
})
=>
{
})
=>
{
const
[
diagnosisContent
,
setDiagnosisContent
]
=
useState
(
''
);
const
[
diagnosisContent
,
setDiagnosisContent
]
=
useState
(
''
);
const
[
diagnosisLoading
,
setDiagnosisLoading
]
=
useState
(
false
);
const
[
diagnosisLoading
,
setDiagnosisLoading
]
=
useState
(
false
);
...
@@ -38,16 +42,22 @@ const AIPanel: React.FC<AIPanelProps> = ({
...
@@ -38,16 +42,22 @@ const AIPanel: React.FC<AIPanelProps> = ({
const
handleAIAssist
=
async
(
scene
:
'
consult_diagnosis
'
|
'
consult_medication
'
)
=>
{
const
handleAIAssist
=
async
(
scene
:
'
consult_diagnosis
'
|
'
consult_medication
'
)
=>
{
if
(
!
activeConsultId
)
return
;
if
(
!
activeConsultId
)
return
;
const
setLoading
=
scene
===
'
consult_diagnosis
'
?
setDiagnosisLoading
:
setMedicationLoading
;
const
isDiag
=
scene
===
'
consult_diagnosis
'
;
const
setContent
=
scene
===
'
consult_diagnosis
'
?
setDiagnosisContent
:
setMedicationContent
;
const
setLoading
=
isDiag
?
setDiagnosisLoading
:
setMedicationLoading
;
const
setToolCalls
=
scene
===
'
consult_diagnosis
'
?
setDiagnosisToolCalls
:
setMedicationToolCalls
;
const
setContent
=
isDiag
?
setDiagnosisContent
:
setMedicationContent
;
const
setToolCalls
=
isDiag
?
setDiagnosisToolCalls
:
setMedicationToolCalls
;
const
onChange
=
isDiag
?
onDiagnosisChange
:
onMedicationChange
;
setLoading
(
true
);
setLoading
(
true
);
try
{
try
{
const
res
=
await
consultApi
.
aiAssist
(
activeConsultId
,
scene
);
const
res
=
await
consultApi
.
aiAssist
(
activeConsultId
,
scene
);
setContent
(
res
.
data
?.
response
||
'
暂无分析结果
'
);
const
text
=
res
.
data
?.
response
||
'
暂无分析结果
'
;
setContent
(
text
);
setToolCalls
(
res
.
data
?.
tool_calls
||
[]);
setToolCalls
(
res
.
data
?.
tool_calls
||
[]);
}
catch
(
err
:
any
)
{
onChange
?.(
text
);
setContent
(
'
AI分析失败:
'
+
(
err
?.
message
||
'
请稍后重试
'
));
}
catch
(
err
:
unknown
)
{
const
text
=
'
AI分析失败:
'
+
((
err
as
Error
)?.
message
||
'
请稍后重试
'
);
setContent
(
text
);
setToolCalls
([]);
setToolCalls
([]);
}
finally
{
}
finally
{
setLoading
(
false
);
setLoading
(
false
);
...
@@ -59,7 +69,7 @@ const AIPanel: React.FC<AIPanelProps> = ({
...
@@ -59,7 +69,7 @@ const AIPanel: React.FC<AIPanelProps> = ({
return
(
return
(
<
Collapse
<
Collapse
size=
"small"
size=
"small"
style=
{
{
marginTop
:
8
,
b
ackground
:
'
#f9fafb
'
,
borderRadius
:
8
}
}
style=
{
{
marginTop
:
8
,
b
orderRadius
:
6
}
}
items=
{
[{
items=
{
[{
key
:
'
tools
'
,
key
:
'
tools
'
,
label
:
(
label
:
(
...
@@ -79,7 +89,10 @@ const AIPanel: React.FC<AIPanelProps> = ({
...
@@ -79,7 +89,10 @@ const AIPanel: React.FC<AIPanelProps> = ({
children
:
(
children
:
(
<
div
key=
{
idx
}
style=
{
{
fontSize
:
12
}
}
>
<
div
key=
{
idx
}
style=
{
{
fontSize
:
12
}
}
>
<
div
style=
{
{
fontWeight
:
500
,
color
:
'
#374151
'
}
}
>
{
tc
.
tool_name
}
</
div
>
<
div
style=
{
{
fontWeight
:
500
,
color
:
'
#374151
'
}
}
>
{
tc
.
tool_name
}
</
div
>
<
div
style=
{
{
color
:
'
#9ca3af
'
,
overflow
:
'
hidden
'
,
textOverflow
:
'
ellipsis
'
,
whiteSpace
:
'
nowrap
'
,
maxWidth
:
260
}
}
>
<
div
style=
{
{
color
:
'
#9ca3af
'
,
overflow
:
'
hidden
'
,
textOverflow
:
'
ellipsis
'
,
whiteSpace
:
'
nowrap
'
,
}
}
>
参数:
{
tc
.
arguments
}
参数:
{
tc
.
arguments
}
</
div
>
</
div
>
{
tc
.
result
&&
(
{
tc
.
result
&&
(
...
@@ -97,6 +110,80 @@ const AIPanel: React.FC<AIPanelProps> = ({
...
@@ -97,6 +110,80 @@ const AIPanel: React.FC<AIPanelProps> = ({
);
);
};
};
const
renderAIAssistContent
=
(
loading
:
boolean
,
content
:
string
,
toolCalls
:
ToolCall
[],
scene
:
'
consult_diagnosis
'
|
'
consult_medication
'
,
)
=>
{
if
(
loading
)
{
return
(
<
div
style=
{
{
textAlign
:
'
center
'
,
padding
:
'
32px 16px
'
}
}
>
<
Spin
tip=
"AI分析中,请稍候..."
/>
</
div
>
);
}
if
(
content
)
{
return
(
<
div
>
<
div
style=
{
{
maxHeight
:
360
,
overflow
:
'
auto
'
,
padding
:
'
4px 0
'
}
}
>
<
MarkdownRenderer
content=
{
content
}
fontSize=
{
12
}
lineHeight=
{
1.6
}
/>
</
div
>
{
renderToolCalls
(
toolCalls
)
}
{
toolCalls
.
length
>
0
&&
(
<
div
style=
{
{
fontSize
:
11
,
color
:
'
#9ca3af
'
,
marginTop
:
6
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
3
}
}
>
<
ThunderboltOutlined
/>
通过
{
toolCalls
.
length
}
次工具调用生成
</
div
>
)
}
<
Divider
style=
{
{
margin
:
'
10px 0
'
}
}
/>
<
Space
size=
{
6
}
>
<
Button
size=
"small"
icon=
{
<
ThunderboltOutlined
/>
}
onClick=
{
()
=>
handleAIAssist
(
scene
)
}
>
重新分析
</
Button
>
{
scene
===
'
consult_medication
'
&&
(
<
Button
size=
"small"
type=
"primary"
ghost
icon=
{
<
MedicineBoxOutlined
/>
}
onClick=
{
()
=>
{
// Medication content is already synced to parent via onMedicationChange
// The prescription modal reads it from aiMedication prop
}
}
>
已同步至处方
</
Button
>
)
}
</
Space
>
</
div
>
);
}
return
(
<
div
style=
{
{
textAlign
:
'
center
'
,
padding
:
'
28px 16px
'
}
}
>
<
RobotOutlined
style=
{
{
fontSize
:
36
,
color
:
'
#d9d9d9
'
,
display
:
'
block
'
,
marginBottom
:
12
}
}
/>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
12
,
display
:
'
block
'
,
marginBottom
:
14
,
lineHeight
:
1.6
}
}
>
{
scene
===
'
consult_diagnosis
'
?
'
基于本次问诊对话内容,AI将辅助生成鉴别诊断建议
'
:
'
基于问诊对话和患者信息,AI将辅助生成安全用药建议
'
}
</
Text
>
<
Button
type=
"primary"
size=
"small"
icon=
{
scene
===
'
consult_diagnosis
'
?
<
ThunderboltOutlined
/>
:
<
MedicineBoxOutlined
/>
}
onClick=
{
()
=>
handleAIAssist
(
scene
)
}
style=
{
scene
===
'
consult_medication
'
?
{
background
:
'
#722ed1
'
,
borderColor
:
'
#722ed1
'
}
:
{}
}
>
{
scene
===
'
consult_diagnosis
'
?
'
生成鉴别诊断
'
:
'
生成用药建议
'
}
</
Button
>
</
div
>
);
};
const
hasPreConsultData
=
preConsultReport
&&
(
const
hasPreConsultData
=
preConsultReport
&&
(
preConsultReport
.
ai_analysis
||
(
preConsultReport
.
chat_messages
&&
preConsultReport
.
chat_messages
.
length
>
0
)
preConsultReport
.
ai_analysis
||
(
preConsultReport
.
chat_messages
&&
preConsultReport
.
chat_messages
.
length
>
0
)
);
);
...
@@ -106,17 +193,23 @@ const AIPanel: React.FC<AIPanelProps> = ({
...
@@ -106,17 +193,23 @@ const AIPanel: React.FC<AIPanelProps> = ({
return
<
Alert
message=
"暂无对话记录"
type=
"info"
showIcon
style=
{
{
fontSize
:
12
}
}
/>;
return
<
Alert
message=
"暂无对话记录"
type=
"info"
showIcon
style=
{
{
fontSize
:
12
}
}
/>;
}
}
return
(
return
(
<
div
style=
{
{
maxHeight
:
3
0
0
,
overflow
:
'
auto
'
,
padding
:
8
,
background
:
'
#f9f9f9
'
,
borderRadius
:
6
}
}
>
<
div
style=
{
{
maxHeight
:
3
2
0
,
overflow
:
'
auto
'
,
padding
:
8
,
background
:
'
#f9f9f9
'
,
borderRadius
:
6
}
}
>
<
Text
st
rong
style=
{
{
fontSize
:
12
,
color
:
'
#666
'
,
marginBottom
:
8
,
display
:
'
block
'
}
}
>
<
Text
st
yle=
{
{
fontSize
:
11
,
color
:
'
#8c8c8c
'
,
marginBottom
:
8
,
display
:
'
block
'
}
}
>
<
MessageOutlined
style=
{
{
marginRight
:
4
}
}
/>
<
MessageOutlined
style=
{
{
marginRight
:
4
}
}
/>
对话记录(共
{
Math
.
floor
(
chatMsgs
.
length
/
2
)
}
轮)
对话记录(共
{
Math
.
floor
(
chatMsgs
.
length
/
2
)
}
轮)
</
Text
>
</
Text
>
{
chatMsgs
.
map
((
msg
,
i
)
=>
(
{
chatMsgs
.
map
((
msg
,
i
)
=>
(
<
div
key=
{
i
}
style=
{
{
fontSize
:
12
,
marginTop
:
6
,
paddingLeft
:
8
,
borderLeft
:
msg
.
role
===
'
user
'
?
'
2px solid #1890ff
'
:
'
2px solid #52c41a
'
}
}
>
<
div
key=
{
i
}
style=
{
{
fontSize
:
12
,
marginTop
:
6
,
paddingLeft
:
8
,
borderLeft
:
msg
.
role
===
'
user
'
?
'
2px solid #1890ff
'
:
'
2px solid #52c41a
'
,
}
}
>
<
Text
strong
style=
{
{
fontSize
:
12
,
color
:
msg
.
role
===
'
user
'
?
'
#1890ff
'
:
'
#52c41a
'
}
}
>
<
Text
strong
style=
{
{
fontSize
:
12
,
color
:
msg
.
role
===
'
user
'
?
'
#1890ff
'
:
'
#52c41a
'
}
}
>
{
msg
.
role
===
'
user
'
?
'
患者
'
:
'
AI
'
}
{
msg
.
role
===
'
user
'
?
'
患者
'
:
'
AI
助手
'
}
</
Text
>
</
Text
>
<
div
style=
{
{
marginTop
:
2
,
color
:
'
#333
'
}
}
>
{
msg
.
content
}
</
div
>
<
div
style=
{
{
marginTop
:
2
,
color
:
'
#333
'
,
lineHeight
:
1.5
}
}
>
{
msg
.
content
}
</
div
>
</
div
>
</
div
>
))
}
))
}
</
div
>
</
div
>
...
@@ -125,38 +218,47 @@ const AIPanel: React.FC<AIPanelProps> = ({
...
@@ -125,38 +218,47 @@ const AIPanel: React.FC<AIPanelProps> = ({
const
renderPreConsultContent
=
()
=>
{
const
renderPreConsultContent
=
()
=>
{
if
(
!
preConsultReport
)
return
null
;
if
(
!
preConsultReport
)
return
null
;
const
patientInfo
=
(
return
(
<
div
style=
{
{
padding
:
8
,
background
:
'
#f9f0ff
'
,
borderRadius
:
6
,
marginBottom
:
8
}
}
>
<
div
>
{
/* 患者信息 */
}
<
div
style=
{
{
padding
:
'
8px 10px
'
,
background
:
'
#f9f0ff
'
,
borderRadius
:
6
,
marginBottom
:
8
}
}
>
<
Text
strong
style=
{
{
fontSize
:
13
,
color
:
'
#722ed1
'
}
}
>
<
Text
strong
style=
{
{
fontSize
:
13
,
color
:
'
#722ed1
'
}
}
>
<
UserOutlined
style=
{
{
marginRight
:
4
}
}
/>
<
UserOutlined
style=
{
{
marginRight
:
4
}
}
/>
{
preConsultReport
.
patient_name
||
'
患者
'
}
{
preConsultReport
.
patient_name
||
'
患者
'
}
</
Text
>
</
Text
>
{
(
preConsultReport
.
patient_gender
||
preConsultReport
.
patient_age
)
&&
(
{
(
preConsultReport
.
patient_gender
||
preConsultReport
.
patient_age
)
&&
(
<
Text
type=
"secondary"
style=
{
{
fontSize
:
12
,
marginLeft
:
8
}
}
>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
12
,
marginLeft
:
8
}
}
>
{
preConsultReport
.
patient_gender
}
{
preConsultReport
.
patient_age
?
`· ${preConsultReport.patient_age}岁`
:
''
}
{
preConsultReport
.
patient_gender
}
{
preConsultReport
.
patient_age
?
` · ${preConsultReport.patient_age}岁`
:
''
}
</
Text
>
</
Text
>
)
}
)
}
</
div
>
</
div
>
);
const
tags
=
(
preConsultReport
.
ai_severity
||
preConsultReport
.
ai_department
)
&&
(
{
/* 严重程度 + 推荐科室 */
}
<
div
style=
{
{
display
:
'
flex
'
,
gap
:
4
,
marginBottom
:
8
,
flexWrap
:
'
wrap
'
}
}
>
{
(
preConsultReport
.
ai_severity
||
preConsultReport
.
ai_department
)
&&
(
<
Space
size=
{
4
}
style=
{
{
marginBottom
:
8
,
flexWrap
:
'
wrap
'
}
}
>
{
preConsultReport
.
ai_severity
&&
(
{
preConsultReport
.
ai_severity
&&
(
<
Tag
color=
{
preConsultReport
.
ai_severity
===
'
severe
'
?
'
red
'
:
preConsultReport
.
ai_severity
===
'
moderate
'
?
'
orange
'
:
'
green
'
}
>
<
Tag
color=
{
{
preConsultReport
.
ai_severity
===
'
severe
'
?
'
重度
'
:
preConsultReport
.
ai_severity
===
'
moderate
'
?
'
中度
'
:
'
轻度
'
}
preConsultReport
.
ai_severity
===
'
severe
'
?
'
red
'
:
preConsultReport
.
ai_severity
===
'
moderate
'
?
'
orange
'
:
'
green
'
}
>
{
preConsultReport
.
ai_severity
===
'
severe
'
?
'
重度
'
:
preConsultReport
.
ai_severity
===
'
moderate
'
?
'
中度
'
:
'
轻度
'
}
</
Tag
>
</
Tag
>
)
}
)
}
{
preConsultReport
.
ai_department
&&
<
Tag
color=
"blue"
>
{
preConsultReport
.
ai_department
}
</
Tag
>
}
{
preConsultReport
.
ai_department
&&
<
Tag
color=
"blue"
>
{
preConsultReport
.
ai_department
}
</
Tag
>
}
</
div
>
</
Space
>
);
)
}
return
(
<
div
>
{
/* 主诉 */
}
{
patientInfo
}
{
tags
}
{
preConsultReport
.
chief_complaint
&&
(
{
preConsultReport
.
chief_complaint
&&
(
<
div
style=
{
{
fontSize
:
12
,
marginBottom
:
8
}
}
>
<
div
style=
{
{
fontSize
:
12
,
marginBottom
:
10
}
}
>
<
Text
strong
>
主诉
</
Text
>
{
preConsultReport
.
chief_complaint
}
<
Text
type=
"secondary"
>
主诉:
</
Text
>
{
preConsultReport
.
chief_complaint
}
</
div
>
</
div
>
)
}
)
}
{
/* 子标签:对话 vs 报告 */
}
<
Tabs
<
Tabs
size=
"small"
size=
"small"
activeKey=
{
preConsultSubTab
}
activeKey=
{
preConsultSubTab
}
...
@@ -171,7 +273,7 @@ const AIPanel: React.FC<AIPanelProps> = ({
...
@@ -171,7 +273,7 @@ const AIPanel: React.FC<AIPanelProps> = ({
key
:
'
analysis
'
,
key
:
'
analysis
'
,
label
:
<
span
><
FileTextOutlined
/>
AI分析报告
</
span
>,
label
:
<
span
><
FileTextOutlined
/>
AI分析报告
</
span
>,
children
:
preConsultReport
.
ai_analysis
?
(
children
:
preConsultReport
.
ai_analysis
?
(
<
div
style=
{
{
maxHeight
:
3
0
0
,
overflow
:
'
auto
'
,
padding
:
8
,
background
:
'
#f6ffed
'
,
borderRadius
:
6
}
}
>
<
div
style=
{
{
maxHeight
:
3
2
0
,
overflow
:
'
auto
'
,
padding
:
8
,
background
:
'
#f6ffed
'
,
borderRadius
:
6
}
}
>
<
MarkdownRenderer
content=
{
preConsultReport
.
ai_analysis
}
fontSize=
{
12
}
lineHeight=
{
1.6
}
/>
<
MarkdownRenderer
content=
{
preConsultReport
.
ai_analysis
}
fontSize=
{
12
}
lineHeight=
{
1.6
}
/>
</
div
>
</
div
>
)
:
(
)
:
(
...
@@ -184,81 +286,51 @@ const AIPanel: React.FC<AIPanelProps> = ({
...
@@ -184,81 +286,51 @@ const AIPanel: React.FC<AIPanelProps> = ({
);
);
};
};
const
renderAIAssistContent
=
(
loading
:
boolean
,
content
:
string
,
toolCalls
:
ToolCall
[],
scene
:
'
consult_diagnosis
'
|
'
consult_medication
'
,
)
=>
{
if
(
loading
)
return
<
div
style=
{
{
textAlign
:
'
center
'
,
padding
:
20
}
}
><
Spin
tip=
"AI分析.."
/></
div
>;
if
(
content
)
{
return
(
return
(
<
div
>
<
div
style=
{
{
<
div
style=
{
{
maxHeight
:
400
,
overflow
:
'
auto
'
}
}
>
height
:
'
100%
'
,
<
MarkdownRenderer
content=
{
content
}
fontSize=
{
12
}
lineHeight=
{
1.6
}
/>
borderRadius
:
12
,
</
div
>
border
:
'
1px solid #edf2fc
'
,
{
renderToolCalls
(
toolCalls
)
}
background
:
'
#fff
'
,
{
toolCalls
.
length
>
0
&&
(
display
:
'
flex
'
,
<
div
style=
{
{
fontSize
:
11
,
color
:
'
#9ca3af
'
,
marginTop
:
4
}
}
>
flexDirection
:
'
column
'
,
<
ThunderboltOutlined
style=
{
{
marginRight
:
2
}
}
/>
overflow
:
'
hidden
'
,
通过
{
toolCalls
.
length
}
次工具调用生成
}
}
>
</
div
>
{
/* 标题 */
}
<
div
style=
{
{
padding
:
'
10px 14px
'
,
borderBottom
:
'
1px solid #f0f0f0
'
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
8
,
flexShrink
:
0
,
}
}
>
<
RobotOutlined
style=
{
{
color
:
'
#52c41a
'
,
fontSize
:
16
}
}
/>
<
span
style=
{
{
fontWeight
:
600
,
fontSize
:
14
}
}
>
AI 辅助
</
span
>
{
hasActiveConsult
&&
(
<
Badge
status=
"processing"
style=
{
{
marginLeft
:
2
}
}
/>
)
}
)
}
<
Divider
style=
{
{
margin
:
'
8px 0
'
}
}
/>
<
Button
size=
"small"
icon=
{
scene
===
'
consult_diagnosis
'
?
<
ThunderboltOutlined
/>
:
<
MedicineBoxOutlined
/>
}
onClick=
{
()
=>
handleAIAssist
(
scene
)
}
>
重新分析
</
Button
>
</
div
>
</
div
>
);
}
return
(
<
div
style=
{
{
textAlign
:
'
center
'
,
padding
:
16
}
}
>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
12
,
display
:
'
block
'
,
marginBottom
:
12
}
}
>
{
scene
===
'
consult_diagnosis
'
?
'
基于问诊对话内容,AI将辅助生成鉴别诊断
'
:
'
基于问诊对话内容,AI将辅助生成用药建议
'
}
</
Text
>
<
Button
type=
"primary"
size=
"small"
icon=
{
scene
===
'
consult_diagnosis
'
?
<
ThunderboltOutlined
/>
:
<
MedicineBoxOutlined
/>
}
onClick=
{
()
=>
handleAIAssist
(
scene
)
}
>
{
scene
===
'
consult_diagnosis
'
?
'
生成鉴别诊断
'
:
'
生成用药建议
'
}
</
Button
>
</
div
>
);
};
return
(
{
/* 内容区 */
}
<
Card
<
div
style=
{
{
flex
:
1
,
overflow
:
'
auto
'
,
padding
:
'
0 4px
'
}
}
>
title=
{
<
Space
>
<
RobotOutlined
style=
{
{
color
:
'
#52c41a
'
}
}
/>
<
span
>
AI 辅助
</
span
>
</
Space
>
}
style=
{
{
borderRadius
:
12
,
height
:
'
100%
'
}
}
styles=
{
{
body
:
{
padding
:
12
}
}
}
size=
"small"
>
{
hasActiveConsult
?
(
{
hasActiveConsult
?
(
<
Tabs
<
Tabs
size=
"small"
size=
"small"
defaultActiveKey=
{
hasPreConsultData
?
'
preConsult
'
:
'
diagnosis
'
}
defaultActiveKey=
{
hasPreConsultData
?
'
preConsult
'
:
'
diagnosis
'
}
style=
{
{
padding
:
'
0 8px
'
}
}
items=
{
[
items=
{
[
{
{
key
:
'
preConsult
'
,
key
:
'
preConsult
'
,
label
:
(
label
:
(
<
span
>
<
span
>
<
FileTextOutlined
/>
<
FileTextOutlined
/>
{
'
预问诊
'
}{
hasPreConsultData
&&
<
Badge
dot
offset=
{
[
4
,
-
2
]
}
/>
}
{
'
预问诊
'
}
{
hasPreConsultData
&&
<
Badge
dot
offset=
{
[
4
,
-
2
]
}
/>
}
</
span
>
</
span
>
),
),
children
:
preConsultLoading
?
(
children
:
preConsultLoading
?
(
<
div
style=
{
{
textAlign
:
'
center
'
,
padding
:
20
}
}
><
Spin
tip=
"加载中
.."
/></
div
>
<
div
style=
{
{
textAlign
:
'
center
'
,
padding
:
24
}
}
><
Spin
tip=
"加载中.
.."
/></
div
>
)
:
hasPreConsultData
?
(
)
:
hasPreConsultData
?
(
renderPreConsultContent
()
renderPreConsultContent
()
)
:
(
)
:
(
...
@@ -268,19 +340,32 @@ const AIPanel: React.FC<AIPanelProps> = ({
...
@@ -268,19 +340,32 @@ const AIPanel: React.FC<AIPanelProps> = ({
{
{
key
:
'
diagnosis
'
,
key
:
'
diagnosis
'
,
label
:
'
鉴别诊断
'
,
label
:
'
鉴别诊断
'
,
children
:
renderAIAssistContent
(
diagnosisLoading
,
diagnosisContent
,
diagnosisToolCalls
,
'
consult_diagnosis
'
),
children
:
renderAIAssistContent
(
diagnosisLoading
,
diagnosisContent
,
diagnosisToolCalls
,
'
consult_diagnosis
'
),
},
},
{
{
key
:
'
drugs
'
,
key
:
'
drugs
'
,
label
:
'
用药建议
'
,
label
:
(
children
:
renderAIAssistContent
(
medicationLoading
,
medicationContent
,
medicationToolCalls
,
'
consult_medication
'
),
<
span
>
用药建议
{
medicationContent
&&
<
Badge
dot
offset=
{
[
3
,
-
2
]
}
style=
{
{
backgroundColor
:
'
#722ed1
'
}
}
/>
}
</
span
>
),
children
:
renderAIAssistContent
(
medicationLoading
,
medicationContent
,
medicationToolCalls
,
'
consult_medication
'
),
},
},
]
}
]
}
/>
/>
)
:
(
)
:
(
<
Empty
description=
"接诊后自动开始AI分析"
style=
{
{
marginTop
:
40
}
}
/>
<
div
style=
{
{
textAlign
:
'
center
'
,
paddingTop
:
60
}
}
>
<
RobotOutlined
style=
{
{
fontSize
:
40
,
color
:
'
#d9d9d9
'
,
display
:
'
block
'
,
marginBottom
:
14
}
}
/>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
13
}
}
>
接诊后自动开始AI辅助
</
Text
>
</
div
>
)
}
)
}
</
Card
>
</
div
>
</
div
>
);
);
};
};
...
...
web/src/pages/doctor/Consult/ChatPanel.tsx
View file @
9033450a
import
React
,
{
useState
,
useEffect
,
useRef
}
from
'
react
'
;
import
React
,
{
useState
,
useEffect
,
useRef
}
from
'
react
'
;
import
{
import
{
Card
,
Avatar
,
Tag
,
Button
,
Typography
,
Space
,
Empty
,
Input
,
Modal
,
Avatar
,
Tag
,
Button
,
Typography
,
Space
,
Empty
,
Input
,
Modal
,
Tooltip
,
Popover
,
}
from
'
antd
'
;
}
from
'
antd
'
;
import
{
import
{
UserOutlined
,
VideoCameraOutlined
,
SendOutlined
,
StopOutlined
,
UserOutlined
,
VideoCameraOutlined
,
SendOutlined
,
StopOutlined
,
FileTextOutlined
,
Robot
Outlined
,
FileTextOutlined
,
ThunderboltOutlined
,
Smile
Outlined
,
}
from
'
@ant-design/icons
'
;
}
from
'
@ant-design/icons
'
;
import
type
{
ConsultMessage
}
from
'
../../../api/consult
'
;
import
type
{
ConsultMessage
}
from
'
../../../api/consult
'
;
import
PrescriptionModal
from
'
../Prescription
'
;
import
PrescriptionModal
from
'
../Prescription
'
;
...
@@ -12,38 +12,53 @@ import PrescriptionModal from '../Prescription';
...
@@ -12,38 +12,53 @@ import PrescriptionModal from '../Prescription';
const
{
Text
}
=
Typography
;
const
{
Text
}
=
Typography
;
const
{
TextArea
}
=
Input
;
const
{
TextArea
}
=
Input
;
const
QUICK_REPLIES
=
[
'
请问您现在的主要症状是什么?
'
,
'
症状持续多久了?
'
,
'
有没有发烧、咳嗽等其他伴随症状?
'
,
'
既往有什么基础疾病吗?
'
,
'
目前正在服用什么药物?
'
,
'
您好,请详细描述一下您的不适情况。
'
,
'
根据您的描述,初步考虑以下方向...
'
,
'
建议您先做以下检查,结果出来后我再为您进一步诊疗。
'
,
'
您的情况不严重,注意休息、多喝水,一般3-5天会好转。
'
,
'
如果症状明显加重,请及时前往线下医院就诊。
'
,
'
我这边为您开具处方,请到附近药房取药并按时服用。
'
,
];
interface
ActiveConsult
{
interface
ActiveConsult
{
consult_id
:
string
;
consult_id
:
string
;
patient_id
?:
string
;
patient_id
?:
string
;
patient_name
:
string
;
patient_name
:
string
;
patient_gender
?:
string
;
patient_gender
?:
string
;
patient_age
?:
number
;
patient_age
?:
number
;
chief_complaint
?:
string
;
type
:
'
text
'
|
'
video
'
;
type
:
'
text
'
|
'
video
'
;
status
?:
string
;
}
}
interface
ChatPanelProps
{
interface
ChatPanelProps
{
activeConsult
:
ActiveConsult
|
null
;
activeConsult
:
ActiveConsult
|
null
;
consultStatus
?:
string
;
messages
:
ConsultMessage
[];
messages
:
ConsultMessage
[];
onSend
:
(
content
:
string
)
=>
void
;
onSend
:
(
content
:
string
)
=>
void
;
onEndConsult
:
()
=>
void
;
onEndConsult
:
()
=>
void
;
onToggleAI
:
()
=>
void
;
aiPanelVisible
:
boolean
;
sending
:
boolean
;
sending
:
boolean
;
aiDiagnosis
?:
string
;
aiMedication
?:
string
;
}
}
const
ChatPanel
:
React
.
FC
<
ChatPanelProps
>
=
({
const
ChatPanel
:
React
.
FC
<
ChatPanelProps
>
=
({
activeConsult
,
activeConsult
,
consultStatus
,
messages
,
messages
,
onSend
,
onSend
,
onEndConsult
,
onEndConsult
,
onToggleAI
,
aiPanelVisible
,
sending
,
sending
,
aiDiagnosis
,
aiMedication
,
})
=>
{
})
=>
{
const
[
inputValue
,
setInputValue
]
=
useState
(
''
);
const
[
inputValue
,
setInputValue
]
=
useState
(
''
);
const
[
prescriptionOpen
,
setPrescriptionOpen
]
=
useState
(
false
);
const
[
prescriptionOpen
,
setPrescriptionOpen
]
=
useState
(
false
);
const
[
quickReplyOpen
,
setQuickReplyOpen
]
=
useState
(
false
);
const
messagesEndRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
messagesEndRef
=
useRef
<
HTMLDivElement
>
(
null
);
useEffect
(()
=>
{
useEffect
(()
=>
{
...
@@ -56,6 +71,11 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
...
@@ -56,6 +71,11 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
setInputValue
(
''
);
setInputValue
(
''
);
};
};
const
handleQuickReply
=
(
text
:
string
)
=>
{
setInputValue
(
prev
=>
prev
?
`
${
prev
}
\n
${
text
}
`
:
text
);
setQuickReplyOpen
(
false
);
};
const
handleEndConsult
=
()
=>
{
const
handleEndConsult
=
()
=>
{
Modal
.
confirm
({
Modal
.
confirm
({
title
:
'
确认结束问诊
'
,
title
:
'
确认结束问诊
'
,
...
@@ -75,88 +95,172 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
...
@@ -75,88 +95,172 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
}
}
};
};
const
isCompleted
=
consultStatus
===
'
completed
'
;
const
isCompleted
=
activeConsult
?.
status
===
'
completed
'
;
const
quickReplyContent
=
(
<
div
style=
{
{
width
:
290
}
}
>
<
div
style=
{
{
fontSize
:
12
,
color
:
'
#8c8c8c
'
,
marginBottom
:
8
,
fontWeight
:
500
}
}
>
快捷回复模板
</
div
>
<
div
style=
{
{
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
4
}
}
>
{
QUICK_REPLIES
.
map
((
text
,
i
)
=>
(
<
div
key=
{
i
}
onClick=
{
()
=>
handleQuickReply
(
text
)
}
style=
{
{
padding
:
'
7px 10px
'
,
borderRadius
:
6
,
fontSize
:
12
,
cursor
:
'
pointer
'
,
color
:
'
#374151
'
,
background
:
'
#f9fafb
'
,
border
:
'
1px solid #f0f0f0
'
,
lineHeight
:
1.5
,
transition
:
'
background 0.15s
'
,
}
}
onMouseEnter=
{
e
=>
{
(
e
.
currentTarget
as
HTMLDivElement
).
style
.
background
=
'
#e6f7ff
'
;
}
}
onMouseLeave=
{
e
=>
{
(
e
.
currentTarget
as
HTMLDivElement
).
style
.
background
=
'
#f9fafb
'
;
}
}
>
{
text
}
</
div
>
))
}
</
div
>
</
div
>
);
return
(
return
(
<>
<>
<
Card
<
div
style=
{
{
title=
{
height
:
'
100%
'
,
activeConsult
?
(
display
:
'
flex
'
,
<
Space
>
flexDirection
:
'
column
'
,
<
Avatar
size=
"small"
icon=
{
<
UserOutlined
/>
}
style=
{
{
backgroundColor
:
'
#87d068
'
}
}
/>
borderRadius
:
12
,
<
Text
strong
>
{
activeConsult
.
patient_name
}
</
Text
>
border
:
'
1px solid #edf2fc
'
,
<
Tag
color=
{
activeConsult
.
type
===
'
video
'
?
'
blue
'
:
'
green
'
}
>
background
:
'
#fff
'
,
overflow
:
'
hidden
'
,
}
}
>
{
/* 顶部患者信息栏 */
}
<
div
style=
{
{
padding
:
'
10px 16px
'
,
borderBottom
:
'
1px solid #f0f0f0
'
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
10
,
flexShrink
:
0
,
background
:
'
#fff
'
,
}
}
>
{
activeConsult
?
(
<>
<
Avatar
size=
{
38
}
icon=
{
<
UserOutlined
/>
}
style=
{
{
backgroundColor
:
'
#87d068
'
,
flexShrink
:
0
}
}
/>
<
div
style=
{
{
flex
:
1
,
minWidth
:
0
}
}
>
<
div
style=
{
{
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
6
,
marginBottom
:
2
}
}
>
<
Text
strong
style=
{
{
fontSize
:
15
}
}
>
{
activeConsult
.
patient_name
}
</
Text
>
<
Tag
color=
{
activeConsult
.
type
===
'
video
'
?
'
purple
'
:
'
green
'
}
style=
{
{
fontSize
:
11
,
margin
:
0
,
lineHeight
:
'
18px
'
}
}
>
{
activeConsult
.
type
===
'
video
'
?
'
视频问诊
'
:
'
图文问诊
'
}
{
activeConsult
.
type
===
'
video
'
?
'
视频问诊
'
:
'
图文问诊
'
}
</
Tag
>
</
Tag
>
{
isCompleted
&&
<
Tag
color=
"default"
>
已结束
</
Tag
>
}
{
isCompleted
</
Space
>
?
<
Tag
color=
"default"
style=
{
{
fontSize
:
11
,
margin
:
0
,
lineHeight
:
'
18px
'
}
}
>
已结束
</
Tag
>
)
:
'
问诊对话
'
:
<
Tag
color=
"success"
style=
{
{
fontSize
:
11
,
margin
:
0
,
lineHeight
:
'
18px
'
}
}
>
进行中
</
Tag
>
}
}
style=
{
{
borderRadius
:
12
,
height
:
'
100%
'
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
}
}
</
div
>
styles=
{
{
body
:
{
flex
:
1
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
padding
:
0
}
}
}
<
div
style=
{
{
fontSize
:
12
,
color
:
'
#8c8c8c
'
}
}
>
extra=
{
{
activeConsult
.
patient_gender
&&
`${activeConsult.patient_gender} · `
}
activeConsult
&&
(
{
activeConsult
.
patient_age
&&
`${activeConsult.patient_age}岁 · `
}
<
Space
>
{
activeConsult
.
chief_complaint
<
Button
?
`主诉:${activeConsult.chief_complaint.slice(0, 28)}${activeConsult.chief_complaint.length > 28 ? '...' : ''}`
type=
"text"
:
'
暂无主诉
'
icon=
{
<
RobotOutlined
/>
}
}
onClick=
{
onToggleAI
}
</
div
>
style=
{
{
color
:
aiPanelVisible
?
'
#52c41a
'
:
undefined
}
}
</
div
>
>
<
Space
size=
{
6
}
>
AI面板
</
Button
>
{
activeConsult
.
type
===
'
video
'
&&
!
isCompleted
&&
(
{
activeConsult
.
type
===
'
video
'
&&
!
isCompleted
&&
(
<
Button
type=
"primary"
icon=
{
<
VideoCameraOutlined
/>
}
>
<
Button
size=
"small"
type=
"primary"
ghost
icon=
{
<
VideoCameraOutlined
/>
}
>
发起视频
发起视频
</
Button
>
</
Button
>
)
}
)
}
<
Button
<
Button
size=
"small"
icon=
{
<
FileTextOutlined
/>
}
icon=
{
<
FileTextOutlined
/>
}
onClick=
{
()
=>
setPrescriptionOpen
(
true
)
}
onClick=
{
()
=>
setPrescriptionOpen
(
true
)
}
>
>
开处方
开处方
</
Button
>
</
Button
>
{
!
isCompleted
&&
(
{
!
isCompleted
&&
(
<
Button
danger
icon=
{
<
StopOutlined
/>
}
onClick=
{
handleEndConsult
}
>
<
Button
size=
"small"
danger
icon=
{
<
StopOutlined
/>
}
onClick=
{
handleEndConsult
}
>
结束问诊
结束问诊
</
Button
>
</
Button
>
)
}
)
}
</
Space
>
</
Space
>
)
</>
}
)
:
(
>
<
Text
style=
{
{
color
:
'
#8c8c8c
'
,
fontSize
:
14
}
}
>
<
FileTextOutlined
style=
{
{
marginRight
:
6
}
}
/>
问诊对话
</
Text
>
)
}
</
div
>
{
/* 消息区域 */
}
<
div
style=
{
{
flex
:
1
,
overflow
:
'
auto
'
,
padding
:
16
,
background
:
'
#f5f7fb
'
}
}
>
{
activeConsult
?
(
{
activeConsult
?
(
<>
<>
<
div
style=
{
{
flex
:
1
,
overflow
:
'
auto
'
,
padding
:
16
,
background
:
'
#f9f9f9
'
}
}
>
{
messages
.
length
===
0
&&
(
{
messages
.
map
((
msg
)
=>
(
<
div
style=
{
{
textAlign
:
'
center
'
,
paddingTop
:
60
,
color
:
'
#bbb
'
,
fontSize
:
13
}
}
>
等待患者发送消息...
</
div
>
)
}
{
messages
.
map
(
msg
=>
(
<
div
<
div
key=
{
msg
.
id
}
key=
{
msg
.
id
}
style=
{
{
style=
{
{
display
:
'
flex
'
,
display
:
'
flex
'
,
justifyContent
:
msg
.
sender_type
===
'
doctor
'
?
'
flex-end
'
:
msg
.
sender_type
===
'
system
'
?
'
center
'
:
'
flex-start
'
,
justifyContent
:
msg
.
sender_type
===
'
doctor
'
marginBottom
:
12
,
?
'
flex-end
'
:
msg
.
sender_type
===
'
system
'
?
'
center
'
:
'
flex-start
'
,
marginBottom
:
16
,
}
}
}
}
>
>
{
msg
.
sender_type
===
'
system
'
?
(
{
msg
.
sender_type
===
'
system
'
?
(
<
Tag
color=
"default"
style=
{
{
fontSize
:
12
}
}
>
{
msg
.
content
}
</
Tag
>
<
div
style=
{
{
background
:
'
rgba(0,0,0,0.04)
'
,
borderRadius
:
12
,
padding
:
'
3px 14px
'
,
fontSize
:
11
,
color
:
'
#8c8c8c
'
,
}
}
>
{
msg
.
content
}
</
div
>
)
:
(
)
:
(
<
div
style=
{
{
maxWidth
:
'
70%
'
}
}
>
<
div
style=
{
{
<
div
style=
{
{
textAlign
:
msg
.
sender_type
===
'
doctor
'
?
'
right
'
:
'
left
'
,
maxWidth
:
'
72%
'
,
marginBottom
:
4
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
alignItems
:
msg
.
sender_type
===
'
doctor
'
?
'
flex-end
'
:
'
flex-start
'
,
}
}
>
}
}
>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
12
}
}
>
<
div
style=
{
{
fontSize
:
11
,
color
:
'
#aaa
'
,
marginBottom
:
5
}
}
>
{
msg
.
sender_type
===
'
doctor
'
?
'
我
'
:
activeConsult
.
patient_name
}
{
formatTime
(
msg
.
created_at
)
}
{
msg
.
sender_type
===
'
doctor
'
?
'
我
'
:
activeConsult
.
patient_name
}
</
Text
>
{
'
·
'
}
{
formatTime
(
msg
.
created_at
)
}
</
div
>
</
div
>
<
div
style=
{
{
<
div
style=
{
{
padding
:
'
8px 12px
'
,
padding
:
'
10px 14px
'
,
borderRadius
:
8
,
borderRadius
:
msg
.
sender_type
===
'
doctor
'
?
'
12px 4px 12px 12px
'
:
'
4px 12px 12px 12px
'
,
background
:
msg
.
sender_type
===
'
doctor
'
?
'
#52c41a
'
:
'
#fff
'
,
background
:
msg
.
sender_type
===
'
doctor
'
?
'
#52c41a
'
:
'
#fff
'
,
color
:
msg
.
sender_type
===
'
doctor
'
?
'
#fff
'
:
'
#
333
'
,
color
:
msg
.
sender_type
===
'
doctor
'
?
'
#fff
'
:
'
#
1d2129
'
,
boxShadow
:
'
0 1px
2px rgba(0,0,0,0.1
)
'
,
boxShadow
:
'
0 1px
3px rgba(0,0,0,0.08
)
'
,
whiteSpace
:
'
pre-wrap
'
,
whiteSpace
:
'
pre-wrap
'
,
wordBreak
:
'
break-word
'
,
wordBreak
:
'
break-word
'
,
fontSize
:
14
,
lineHeight
:
1.6
,
}
}
>
}
}
>
{
msg
.
content
}
{
msg
.
content
}
</
div
>
</
div
>
...
@@ -165,53 +269,95 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
...
@@ -165,53 +269,95 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
</
div
>
</
div
>
))
}
))
}
<
div
ref=
{
messagesEndRef
}
/>
<
div
ref=
{
messagesEndRef
}
/>
</>
)
:
(
<
Empty
description=
{
<
span
style=
{
{
color
:
'
#8c8c8c
'
,
fontSize
:
14
}
}
>
请从左侧列表中选择患者接诊
</
span
>
}
style=
{
{
marginTop
:
80
}
}
/>
)
}
</
div
>
</
div
>
{
/* 已结束时显示提示,进行中时显示输入框 */
}
{
/* 输入区域 */
}
{
activeConsult
&&
(
<
div
style=
{
{
borderTop
:
'
1px solid #f0f0f0
'
,
background
:
'
#fff
'
,
flexShrink
:
0
}
}
>
{
isCompleted
?
(
{
isCompleted
?
(
<
div
style=
{
{
padding
:
'
12px 16px
'
,
textAlign
:
'
center
'
,
color
:
'
#8c8c8c
'
,
fontSize
:
13
}
}
>
本次问诊已结束
</
div
>
)
:
(
<>
{
/* 工具栏 */
}
<
div
style=
{
{
<
div
style=
{
{
padding
:
'
12px 16px
'
,
padding
:
'
6px 12px 0
'
,
borderTop
:
'
1px solid #f0f0f0
'
,
display
:
'
flex
'
,
textAlign
:
'
center
'
,
gap
:
2
,
background
:
'
#fafafa
'
,
borderBottom
:
'
1px solid #f5f5f5
'
,
}
}
>
}
}
>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
13
}
}
>
本次问诊已结束
</
Text
>
<
Popover
content=
{
quickReplyContent
}
trigger=
"click"
open=
{
quickReplyOpen
}
onOpenChange=
{
setQuickReplyOpen
}
placement=
"topLeft"
>
<
Tooltip
title=
"快捷回复"
>
<
Button
type=
"text"
size=
"small"
icon=
{
<
ThunderboltOutlined
/>
}
style=
{
{
color
:
'
#fa8c16
'
,
fontSize
:
13
}
}
/>
</
Tooltip
>
</
Popover
>
<
Tooltip
title=
"表情(开发中)"
>
<
Button
type=
"text"
size=
"small"
icon=
{
<
SmileOutlined
/>
}
style=
{
{
color
:
'
#8c8c8c
'
}
}
/>
</
Tooltip
>
</
div
>
</
div
>
)
:
(
<
div
style=
{
{
padding
:
12
,
borderTop
:
'
1px solid #f0f0f0
'
}
}
>
{
/* 输入框 + 发送 */
}
<
Space
.
Compact
style=
{
{
width
:
'
100%
'
}
}
>
<
div
style=
{
{
padding
:
'
8px 12px 10px
'
,
display
:
'
flex
'
,
gap
:
8
,
alignItems
:
'
flex-end
'
}
}
>
<
TextArea
<
TextArea
value=
{
inputValue
}
value=
{
inputValue
}
onChange=
{
(
e
)
=>
setInputValue
(
e
.
target
.
value
)
}
onChange=
{
e
=>
setInputValue
(
e
.
target
.
value
)
}
placeholder=
"输入消息..."
placeholder=
"输入消息
,Enter 发送,Shift+Enter 换行
..."
autoSize=
{
{
minRows
:
1
,
maxRows
:
3
}
}
autoSize=
{
{
minRows
:
2
,
maxRows
:
5
}
}
onPressEnter=
{
(
e
)
=>
{
onPressEnter=
{
e
=>
{
if
(
!
e
.
shiftKey
)
{
if
(
!
e
.
shiftKey
)
{
e
.
preventDefault
();
e
.
preventDefault
();
handleSend
();
handleSend
();
}
}
}
}
}
}
style=
{
{
borderRadius
:
'
8px 0 0 8px
'
}
}
style=
{
{
flex
:
1
,
borderRadius
:
8
,
resize
:
'
none
'
}
}
/>
/>
<
Button
<
Button
type=
"primary"
type=
"primary"
icon=
{
<
SendOutlined
/>
}
icon=
{
<
SendOutlined
/>
}
onClick=
{
handleSend
}
onClick=
{
handleSend
}
loading=
{
sending
}
loading=
{
sending
}
style=
{
{
height
:
'
auto
'
,
borderRadius
:
'
0 8px 8px 0
'
,
background
:
'
#52c41a
'
,
borderColor
:
'
#52c41a
'
}
}
style=
{
{
background
:
'
#52c41a
'
,
borderColor
:
'
#52c41a
'
,
borderRadius
:
8
,
height
:
'
auto
'
,
alignSelf
:
'
flex-end
'
,
paddingBottom
:
8
,
paddingTop
:
8
,
}
}
>
>
发送
发送
</
Button
>
</
Button
>
</
Space
.
Compact
>
</
div
>
</
div
>
)
}
</>
</>
)
:
(
)
}
<
div
style=
{
{
flex
:
1
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
justifyContent
:
'
center
'
}
}
>
<
Empty
description=
"请从左侧列表中选择患者接诊"
/>
</
div
>
</
div
>
)
}
)
}
</
Card
>
</
div
>
<
PrescriptionModal
<
PrescriptionModal
open=
{
prescriptionOpen
}
open=
{
prescriptionOpen
}
...
@@ -221,6 +367,8 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
...
@@ -221,6 +367,8 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
patientName=
{
activeConsult
?.
patient_name
}
patientName=
{
activeConsult
?.
patient_name
}
patientGender=
{
activeConsult
?.
patient_gender
}
patientGender=
{
activeConsult
?.
patient_gender
}
patientAge=
{
activeConsult
?.
patient_age
}
patientAge=
{
activeConsult
?.
patient_age
}
diagnosis=
{
aiDiagnosis
}
medicationSuggestion=
{
aiMedication
}
/>
/>
</>
</>
);
);
...
...
web/src/pages/doctor/Consult/PatientList.tsx
View file @
9033450a
import
React
from
'
react
'
;
import
React
from
'
react
'
;
import
{
List
,
Typography
,
Tag
,
Button
,
Card
}
from
'
antd
'
;
import
{
Typography
,
Tag
,
Button
,
Badge
}
from
'
antd
'
;
import
{
UserOutlined
,
MessageOutlined
,
VideoCameraOutlined
,
CheckOutlined
,
CloseOutlined
}
from
'
@ant-design/icons
'
;
import
{
UserOutlined
,
MessageOutlined
,
VideoCameraOutlined
,
CheckOutlined
,
CloseOutlined
,
ClockCircleOutlined
,
}
from
'
@ant-design/icons
'
;
import
type
{
PatientListItem
}
from
'
../../../api/consult
'
;
import
type
{
PatientListItem
}
from
'
../../../api/consult
'
;
const
{
Text
}
=
Typography
;
const
{
Text
}
=
Typography
;
const
formatWaitTime
=
(
seconds
:
number
):
string
=>
{
if
(
!
seconds
||
seconds
<=
0
)
return
''
;
const
m
=
Math
.
floor
(
seconds
/
60
);
if
(
m
>=
60
)
return
`
${
Math
.
floor
(
m
/
60
)}
小时
${
m
%
60
}
分`
;
if
(
m
>
0
)
return
`
${
m
}
分
${
seconds
%
60
}
秒`
;
return
`
${
seconds
}
秒`
;
};
interface
PatientListProps
{
interface
PatientListProps
{
patients
:
PatientListItem
[];
patients
:
PatientListItem
[];
onSelectPatient
:
(
patient
:
PatientListItem
)
=>
void
;
onSelectPatient
:
(
patient
:
PatientListItem
)
=>
void
;
...
@@ -13,89 +24,91 @@ interface PatientListProps {
...
@@ -13,89 +24,91 @@ interface PatientListProps {
selectedConsultId
?:
string
;
selectedConsultId
?:
string
;
}
}
const
statusConfig
:
Record
<
string
,
{
color
:
string
;
bgColor
:
string
;
text
:
string
}
>
=
{
const
PatientCard
:
React
.
FC
<
{
in_progress
:
{
color
:
'
#52c41a
'
,
bgColor
:
'
#f6ffed
'
,
text
:
'
进行中
'
},
patient
:
PatientListItem
;
pending
:
{
color
:
'
#1890ff
'
,
bgColor
:
'
#e6f7ff
'
,
text
:
'
待接诊
'
},
isSelected
:
boolean
;
waiting
:
{
color
:
'
#1890ff
'
,
bgColor
:
'
#e6f7ff
'
,
text
:
'
待接诊
'
},
onSelect
:
()
=>
void
;
completed
:
{
color
:
'
#8c8c8c
'
,
bgColor
:
'
#fafafa
'
,
text
:
'
已完成
'
},
onAccept
:
()
=>
void
;
};
onReject
:
()
=>
void
;
}
>
=
({
patient
,
isSelected
,
onSelect
,
onAccept
,
onReject
})
=>
{
const
PatientList
:
React
.
FC
<
PatientListProps
>
=
({
patients
,
onSelectPatient
,
onAccept
,
onReject
,
selectedConsultId
})
=>
{
const
isWaiting
=
patient
.
status
===
'
pending
'
||
patient
.
status
===
'
waiting
'
;
const
isWaiting
=
(
status
:
string
)
=>
status
===
'
pending
'
||
status
===
'
waiting
'
;
const
isInProgress
=
patient
.
status
===
'
in_progress
'
;
const
isCompleted
=
patient
.
status
===
'
completed
'
;
return
(
const
avatarColor
=
isInProgress
?
'
#52c41a
'
:
isWaiting
?
'
#fa8c16
'
:
'
#d9d9d9
'
;
<
Card
const
avatarBg
=
isInProgress
?
'
#f6ffed
'
:
isWaiting
?
'
#fff7e6
'
:
'
#fafafa
'
;
title=
{
<
span
className=
"text-xs"
>
<
UserOutlined
className=
"text-[#1890ff] mr-1"
/>
<
Text
strong
className=
"text-xs!"
>
患者列表
</
Text
>
<
Tag
className=
"ml-2 text-[10px]!"
>
{
patients
.
length
}
</
Tag
>
</
span
>
}
style=
{
{
height
:
'
100%
'
}
}
styles=
{
{
body
:
{
padding
:
0
,
height
:
'
calc(100% - 40px)
'
,
overflow
:
'
auto
'
}
}
}
size=
"small"
>
<
List
dataSource=
{
patients
}
locale=
{
{
emptyText
:
(
<
div
className=
"text-center py-8 text-gray-400"
>
<
UserOutlined
className=
"text-3xl text-gray-300 mb-2 block"
/>
<
div
className=
"text-xs"
>
暂无患者
</
div
>
</
div
>
),
}
}
renderItem=
{
(
patient
:
PatientListItem
)
=>
{
const
config
=
statusConfig
[
patient
.
status
]
||
statusConfig
.
pending
;
const
isSelected
=
patient
.
consult_id
===
selectedConsultId
;
return
(
return
(
<
List
.
Item
<
div
key=
{
patient
.
consult_id
}
onClick=
{
()
=>
!
isWaiting
&&
onSelect
()
}
onClick=
{
()
=>
{
if
(
!
isWaiting
(
patient
.
status
))
onSelectPatient
(
patient
);
}
}
style=
{
{
style=
{
{
cursor
:
isWaiting
(
patient
.
status
)
?
'
default
'
:
'
pointer
'
,
padding
:
'
10px 12px
'
,
padding
:
'
8px 12px
'
,
background
:
isSelected
?
'
#e6f7ff
'
:
'
#fff
'
,
background
:
isSelected
?
'
#e6f7ff
'
:
'
#fff
'
,
borderLeft
:
isSelected
?
'
3px solid #1890ff
'
:
'
3px solid transparent
'
,
borderLeft
:
`3px solid ${isSelected ? '#1890ff' : 'transparent'}`
,
borderBottom
:
'
1px solid #f0f0f0
'
,
borderBottom
:
'
1px solid #f5f5f5
'
,
transition
:
'
all 0.2s
'
,
cursor
:
isWaiting
?
'
default
'
:
'
pointer
'
,
transition
:
'
background 0.15s
'
,
}
}
onMouseEnter=
{
e
=>
{
if
(
!
isSelected
&&
!
isWaiting
)
(
e
.
currentTarget
as
HTMLDivElement
).
style
.
background
=
'
#fafafa
'
;
}
}
onMouseLeave=
{
e
=>
{
if
(
!
isSelected
)
(
e
.
currentTarget
as
HTMLDivElement
).
style
.
background
=
'
#fff
'
;
}
}
}
}
>
>
<
div
className=
"w-full"
>
<
div
style=
{
{
display
:
'
flex
'
,
gap
:
9
,
alignItems
:
'
flex-start
'
}
}
>
<
div
className=
"flex justify-between items-center mb-1"
>
{
/* Avatar */
}
<
span
className=
"text-xs"
>
<
div
style=
{
{
<
UserOutlined
className=
"text-[#1890ff] mr-1"
/>
width
:
34
,
height
:
34
,
borderRadius
:
'
50%
'
,
flexShrink
:
0
,
<
Text
strong
className=
"text-xs!"
>
{
patient
.
patient_name
}
</
Text
>
background
:
avatarBg
,
<
Tag
color=
{
config
.
color
}
className=
"ml-1 text-[10px]! leading-4! px-1!"
>
{
config
.
text
}
</
Tag
>
border
:
`2px solid ${avatarColor}`
,
</
span
>
display
:
'
flex
'
,
alignItems
:
'
center
'
,
justifyContent
:
'
center
'
,
{
patient
.
type
===
'
video
'
?
(
}
}
>
<
VideoCameraOutlined
className=
"text-purple-500 text-xs"
/>
<
UserOutlined
style=
{
{
fontSize
:
15
,
color
:
avatarColor
}
}
/>
)
:
(
</
div
>
<
MessageOutlined
className=
"text-green-500 text-xs"
/>
{
/* Info */
}
<
div
style=
{
{
flex
:
1
,
minWidth
:
0
}
}
>
<
div
style=
{
{
display
:
'
flex
'
,
alignItems
:
'
center
'
,
justifyContent
:
'
space-between
'
,
marginBottom
:
3
}
}
>
<
Text
strong
style=
{
{
fontSize
:
13
}
}
>
{
patient
.
patient_name
||
'
患者
'
}
</
Text
>
{
patient
.
type
===
'
video
'
?
<
VideoCameraOutlined
style=
{
{
fontSize
:
12
,
color
:
'
#722ed1
'
}
}
/>
:
<
MessageOutlined
style=
{
{
fontSize
:
12
,
color
:
'
#52c41a
'
}
}
/>
}
</
div
>
<
div
style=
{
{
fontSize
:
11
,
color
:
'
#8c8c8c
'
,
marginBottom
:
3
}
}
>
{
patient
.
patient_gender
&&
`${patient.patient_gender} · `
}
{
patient
.
patient_age
?
`${patient.patient_age}岁`
:
''
}
</
div
>
<
div
style=
{
{
fontSize
:
11
,
color
:
'
#666
'
,
overflow
:
'
hidden
'
,
whiteSpace
:
'
nowrap
'
,
textOverflow
:
'
ellipsis
'
,
}
}
>
{
patient
.
chief_complaint
||
'
暂无主诉
'
}
</
div
>
{
isWaiting
&&
patient
.
waiting_seconds
>
0
&&
(
<
div
style=
{
{
fontSize
:
11
,
color
:
'
#fa8c16
'
,
marginTop
:
4
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
3
}
}
>
<
ClockCircleOutlined
/>
等待
{
formatWaitTime
(
patient
.
waiting_seconds
)
}
</
div
>
)
}
)
}
{
isCompleted
&&
(
<
div
style=
{
{
fontSize
:
11
,
color
:
'
#8c8c8c
'
,
marginTop
:
4
}
}
>
已完成
</
div
>
</
div
>
<
div
className=
"text-[11px] text-gray-500 mb-1"
>
)
}
<
Text
ellipsis
className=
"text-[11px]! text-gray-500!"
>
{
'
主诉:
'
}{
patient
.
chief_complaint
||
'
暂无
'
}
</
Text
>
</
div
>
</
div
>
<
div
className=
"flex justify-between items-center"
>
<
span
className=
"text-[10px] text-gray-400"
>
{
patient
.
patient_gender
}{
'
·
'
}{
patient
.
patient_age
}{
'
岁
'
}
</
span
>
</
div
>
</
div
>
{
isWaiting
(
patient
.
status
)
&&
(
<
div
className=
"mt-1.5 flex gap-1.5"
>
{
/* 接诊/拒诊按钮 */
}
{
isWaiting
&&
(
<
div
style=
{
{
display
:
'
flex
'
,
gap
:
6
,
marginTop
:
8
}
}
>
<
Button
<
Button
type=
"primary"
type=
"primary"
size=
"small"
size=
"small"
icon=
{
<
CheckOutlined
/>
}
icon=
{
<
CheckOutlined
/>
}
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
onAccept
(
patient
);
}
}
style=
{
{
flex
:
1
,
fontSize
:
11
,
height
:
26
}
}
className=
"flex-1 text-[11px]!"
onClick=
{
e
=>
{
e
.
stopPropagation
();
onAccept
();
}
}
>
>
接诊
接诊
</
Button
>
</
Button
>
...
@@ -103,19 +116,121 @@ const PatientList: React.FC<PatientListProps> = ({ patients, onSelectPatient, on
...
@@ -103,19 +116,121 @@ const PatientList: React.FC<PatientListProps> = ({ patients, onSelectPatient, on
danger
danger
size=
"small"
size=
"small"
icon=
{
<
CloseOutlined
/>
}
icon=
{
<
CloseOutlined
/>
}
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
onReject
(
patient
);
}
}
style=
{
{
flex
:
1
,
fontSize
:
11
,
height
:
26
}
}
className=
"flex-1 text-[11px]!"
onClick=
{
e
=>
{
e
.
stopPropagation
();
onReject
();
}
}
>
>
拒绝
拒诊
</
Button
>
</
Button
>
</
div
>
</
div
>
)
}
)
}
</
div
>
</
div
>
</
List
.
Item
>
);
);
}
}
};
interface
SectionProps
{
label
:
string
;
count
:
number
;
color
:
string
}
const
SectionHeader
:
React
.
FC
<
SectionProps
>
=
({
label
,
count
,
color
})
=>
(
<
div
style=
{
{
padding
:
'
5px 12px
'
,
background
:
'
#fafafa
'
,
borderBottom
:
'
1px solid #f0f0f0
'
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
6
,
}
}
>
<
Badge
count=
{
count
}
style=
{
{
backgroundColor
:
color
}
}
showZero
/>
<
Text
style=
{
{
fontSize
:
11
,
color
:
'
#8c8c8c
'
,
fontWeight
:
500
}
}
>
{
label
}
</
Text
>
</
div
>
);
const
PatientList
:
React
.
FC
<
PatientListProps
>
=
({
patients
,
onSelectPatient
,
onAccept
,
onReject
,
selectedConsultId
,
})
=>
{
const
waiting
=
patients
.
filter
(
p
=>
p
.
status
===
'
pending
'
||
p
.
status
===
'
waiting
'
);
const
inProgress
=
patients
.
filter
(
p
=>
p
.
status
===
'
in_progress
'
);
const
completed
=
patients
.
filter
(
p
=>
p
.
status
===
'
completed
'
);
return
(
<
div
style=
{
{
height
:
'
100%
'
,
borderRadius
:
12
,
border
:
'
1px solid #edf2fc
'
,
background
:
'
#fff
'
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
overflow
:
'
hidden
'
,
}
}
>
{
/* 标题栏 */
}
<
div
style=
{
{
padding
:
'
10px 14px
'
,
borderBottom
:
'
1px solid #f0f0f0
'
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
8
,
flexShrink
:
0
,
}
}
>
<
UserOutlined
style=
{
{
color
:
'
#1890ff
'
}
}
/>
<
Text
strong
style=
{
{
fontSize
:
13
}
}
>
患者列表
</
Text
>
<
Tag
style=
{
{
marginLeft
:
'
auto
'
,
fontSize
:
11
,
margin
:
0
}
}
>
{
patients
.
length
}
人
</
Tag
>
</
div
>
{
/* 列表区域 */
}
<
div
style=
{
{
flex
:
1
,
overflow
:
'
auto
'
}
}
>
{
patients
.
length
===
0
?
(
<
div
style=
{
{
textAlign
:
'
center
'
,
padding
:
'
48px 0
'
,
color
:
'
#bbb
'
}
}
>
<
UserOutlined
style=
{
{
fontSize
:
30
,
display
:
'
block
'
,
marginBottom
:
10
}
}
/>
<
div
style=
{
{
fontSize
:
12
}
}
>
暂无患者
</
div
>
</
div
>
)
:
(
<>
{
waiting
.
length
>
0
&&
(
<>
<
SectionHeader
label=
"候诊中"
count=
{
waiting
.
length
}
color=
"#fa8c16"
/>
{
waiting
.
map
(
p
=>
(
<
PatientCard
key=
{
p
.
consult_id
}
patient=
{
p
}
isSelected=
{
p
.
consult_id
===
selectedConsultId
}
onSelect=
{
()
=>
onSelectPatient
(
p
)
}
onAccept=
{
()
=>
onAccept
(
p
)
}
onReject=
{
()
=>
onReject
(
p
)
}
/>
/>
</
Card
>
))
}
</>
)
}
{
inProgress
.
length
>
0
&&
(
<>
<
SectionHeader
label=
"接诊中"
count=
{
inProgress
.
length
}
color=
"#52c41a"
/>
{
inProgress
.
map
(
p
=>
(
<
PatientCard
key=
{
p
.
consult_id
}
patient=
{
p
}
isSelected=
{
p
.
consult_id
===
selectedConsultId
}
onSelect=
{
()
=>
onSelectPatient
(
p
)
}
onAccept=
{
()
=>
onAccept
(
p
)
}
onReject=
{
()
=>
onReject
(
p
)
}
/>
))
}
</>
)
}
{
completed
.
length
>
0
&&
(
<>
<
SectionHeader
label=
"已完成"
count=
{
completed
.
length
}
color=
"#8c8c8c"
/>
{
completed
.
map
(
p
=>
(
<
PatientCard
key=
{
p
.
consult_id
}
patient=
{
p
}
isSelected=
{
p
.
consult_id
===
selectedConsultId
}
onSelect=
{
()
=>
onSelectPatient
(
p
)
}
onAccept=
{
()
=>
onAccept
(
p
)
}
onReject=
{
()
=>
onReject
(
p
)
}
/>
))
}
</>
)
}
</>
)
}
</
div
>
</
div
>
);
);
};
};
...
...
web/src/pages/doctor/Consult/index.tsx
View file @
9033450a
'
use client
'
;
'
use client
'
;
import
React
,
{
useState
,
useEffect
,
useCallback
}
from
'
react
'
;
import
React
,
{
useState
,
useEffect
,
useCallback
}
from
'
react
'
;
import
{
Row
,
Col
,
Typography
,
message
,
Modal
}
from
'
antd
'
;
import
{
message
,
Modal
}
from
'
antd
'
;
import
{
Messag
eOutlined
}
from
'
@ant-design/icons
'
;
import
{
ClockCircleOutlined
,
MessageOutlined
,
CheckCircl
eOutlined
}
from
'
@ant-design/icons
'
;
import
{
consultApi
,
type
PatientListItem
,
type
ConsultMessage
}
from
'
../../../api/consult
'
;
import
{
consultApi
,
type
PatientListItem
,
type
ConsultMessage
}
from
'
../../../api/consult
'
;
import
{
preConsultApi
}
from
'
../../../api/preConsult
'
;
import
{
preConsultApi
}
from
'
../../../api/preConsult
'
;
import
type
{
PreConsultResponse
}
from
'
../../../api/preConsult
'
;
import
type
{
PreConsultResponse
}
from
'
../../../api/preConsult
'
;
...
@@ -10,8 +10,6 @@ import PatientList from './PatientList';
...
@@ -10,8 +10,6 @@ import PatientList from './PatientList';
import
ChatPanel
from
'
./ChatPanel
'
;
import
ChatPanel
from
'
./ChatPanel
'
;
import
AIPanel
from
'
./AIPanel
'
;
import
AIPanel
from
'
./AIPanel
'
;
const
{
Title
}
=
Typography
;
interface
ActiveConsult
{
interface
ActiveConsult
{
consult_id
:
string
;
consult_id
:
string
;
patient_id
?:
string
;
patient_id
?:
string
;
...
@@ -27,11 +25,18 @@ const ConsultPage: React.FC = () => {
...
@@ -27,11 +25,18 @@ const ConsultPage: React.FC = () => {
const
[
patientList
,
setPatientList
]
=
useState
<
PatientListItem
[]
>
([]);
const
[
patientList
,
setPatientList
]
=
useState
<
PatientListItem
[]
>
([]);
const
[
activeConsult
,
setActiveConsult
]
=
useState
<
ActiveConsult
|
null
>
(
null
);
const
[
activeConsult
,
setActiveConsult
]
=
useState
<
ActiveConsult
|
null
>
(
null
);
const
[
messages
,
setMessages
]
=
useState
<
ConsultMessage
[]
>
([]);
const
[
messages
,
setMessages
]
=
useState
<
ConsultMessage
[]
>
([]);
const
[
aiPanelVisible
,
setAiPanelVisible
]
=
useState
(
true
);
const
[
preConsultReport
,
setPreConsultReport
]
=
useState
<
PreConsultResponse
|
null
>
(
null
);
const
[
preConsultReport
,
setPreConsultReport
]
=
useState
<
PreConsultResponse
|
null
>
(
null
);
const
[
preConsultLoading
,
setPreConsultLoading
]
=
useState
(
false
);
const
[
preConsultLoading
,
setPreConsultLoading
]
=
useState
(
false
);
const
[
sending
,
setSending
]
=
useState
(
false
);
const
[
sending
,
setSending
]
=
useState
(
false
);
// AI state lifted up for prescription data integration
const
[
aiDiagnosis
,
setAiDiagnosis
]
=
useState
(
''
);
const
[
aiMedication
,
setAiMedication
]
=
useState
(
''
);
const
waitingCount
=
patientList
.
filter
(
p
=>
p
.
status
===
'
pending
'
||
p
.
status
===
'
waiting
'
).
length
;
const
inProgressCount
=
patientList
.
filter
(
p
=>
p
.
status
===
'
in_progress
'
).
length
;
const
completedCount
=
patientList
.
filter
(
p
=>
p
.
status
===
'
completed
'
).
length
;
const
fetchPatientList
=
useCallback
(
async
()
=>
{
const
fetchPatientList
=
useCallback
(
async
()
=>
{
try
{
try
{
const
res
=
await
consultApi
.
getPatientList
();
const
res
=
await
consultApi
.
getPatientList
();
...
@@ -41,7 +46,6 @@ const ConsultPage: React.FC = () => {
...
@@ -41,7 +46,6 @@ const ConsultPage: React.FC = () => {
}
}
},
[]);
},
[]);
// 获取消息列表
const
fetchMessages
=
useCallback
(
async
(
consultId
:
string
)
=>
{
const
fetchMessages
=
useCallback
(
async
(
consultId
:
string
)
=>
{
try
{
try
{
const
res
=
await
consultApi
.
getConsultMessages
(
consultId
);
const
res
=
await
consultApi
.
getConsultMessages
(
consultId
);
...
@@ -53,13 +57,10 @@ const ConsultPage: React.FC = () => {
...
@@ -53,13 +57,10 @@ const ConsultPage: React.FC = () => {
useEffect
(()
=>
{
useEffect
(()
=>
{
fetchPatientList
();
fetchPatientList
();
const
timer
=
setInterval
(()
=>
{
const
timer
=
setInterval
(()
=>
fetchPatientList
(),
5000
);
fetchPatientList
();
},
5000
);
return
()
=>
clearInterval
(
timer
);
return
()
=>
clearInterval
(
timer
);
},
[
fetchPatientList
]);
},
[
fetchPatientList
]);
// 定时刷新消息
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
!
activeConsult
)
return
;
if
(
!
activeConsult
)
return
;
fetchMessages
(
activeConsult
.
consult_id
);
fetchMessages
(
activeConsult
.
consult_id
);
...
@@ -67,14 +68,12 @@ const ConsultPage: React.FC = () => {
...
@@ -67,14 +68,12 @@ const ConsultPage: React.FC = () => {
return
()
=>
clearInterval
(
timer
);
return
()
=>
clearInterval
(
timer
);
},
[
activeConsult
,
fetchMessages
]);
},
[
activeConsult
,
fetchMessages
]);
// 加载预问诊报告(先通过问诊ID查,查不到再通过患者ID查)
const
fetchPreConsultReport
=
async
(
consultId
:
string
,
patientId
?:
string
)
=>
{
const
fetchPreConsultReport
=
async
(
consultId
:
string
,
patientId
?:
string
)
=>
{
setPreConsultLoading
(
true
);
setPreConsultLoading
(
true
);
try
{
try
{
const
res
=
await
preConsultApi
.
getByConsultationId
(
consultId
);
const
res
=
await
preConsultApi
.
getByConsultationId
(
consultId
);
setPreConsultReport
(
res
.
data
);
setPreConsultReport
(
res
.
data
);
}
catch
{
}
catch
{
// 问诊没有关联预问诊,尝试通过患者ID获取最新预问诊
if
(
patientId
)
{
if
(
patientId
)
{
try
{
try
{
const
res
=
await
preConsultApi
.
getByPatientId
(
patientId
);
const
res
=
await
preConsultApi
.
getByPatientId
(
patientId
);
...
@@ -90,13 +89,17 @@ const ConsultPage: React.FC = () => {
...
@@ -90,13 +89,17 @@ const ConsultPage: React.FC = () => {
}
}
};
};
// 接诊
const
resetConsultState
=
()
=>
{
setAiDiagnosis
(
''
);
setAiMedication
(
''
);
setPreConsultReport
(
null
);
setMessages
([]);
};
const
handleAccept
=
async
(
patient
:
PatientListItem
)
=>
{
const
handleAccept
=
async
(
patient
:
PatientListItem
)
=>
{
console
.
log
(
'
handleAccept 被调用
'
,
patient
);
try
{
try
{
console
.
log
(
'
调用 acceptConsult API
'
,
patient
.
consult_id
);
await
consultApi
.
acceptConsult
(
patient
.
consult_id
);
await
consultApi
.
acceptConsult
(
patient
.
consult_id
);
console
.
log
(
'
acceptConsult API 调用成功
'
);
resetConsultState
(
);
setActiveConsult
({
setActiveConsult
({
consult_id
:
patient
.
consult_id
,
consult_id
:
patient
.
consult_id
,
patient_id
:
patient
.
patient_id
,
patient_id
:
patient
.
patient_id
,
...
@@ -107,19 +110,17 @@ const ConsultPage: React.FC = () => {
...
@@ -107,19 +110,17 @@ const ConsultPage: React.FC = () => {
type
:
patient
.
type
,
type
:
patient
.
type
,
status
:
'
in_progress
'
,
status
:
'
in_progress
'
,
});
});
setPreConsultReport
(
null
);
message
.
success
(
`已接诊患者
${
patient
.
patient_name
||
'
未知
'
}
`
);
message
.
success
(
`已接诊患者
${
patient
.
patient_name
||
'
未知
'
}
`
);
fetchPreConsultReport
(
patient
.
consult_id
,
patient
.
patient_id
);
fetchPreConsultReport
(
patient
.
consult_id
,
patient
.
patient_id
);
fetchPatientList
();
fetchPatientList
();
fetchMessages
(
patient
.
consult_id
);
fetchMessages
(
patient
.
consult_id
);
}
catch
(
err
:
any
)
{
}
catch
(
err
:
unknown
)
{
console
.
error
(
'
接诊失败
'
,
err
);
message
.
error
((
err
as
Error
)?.
message
||
'
接诊失败
'
);
message
.
error
(
err
?.
message
||
'
接诊失败
'
);
}
}
};
};
// 选择患者(仅用于进行中和已完成的问诊)
const
handleSelectPatient
=
(
patient
:
PatientListItem
)
=>
{
const
handleSelectPatient
=
(
patient
:
PatientListItem
)
=>
{
resetConsultState
();
setActiveConsult
({
setActiveConsult
({
consult_id
:
patient
.
consult_id
,
consult_id
:
patient
.
consult_id
,
patient_id
:
patient
.
patient_id
,
patient_id
:
patient
.
patient_id
,
...
@@ -130,33 +131,26 @@ const ConsultPage: React.FC = () => {
...
@@ -130,33 +131,26 @@ const ConsultPage: React.FC = () => {
type
:
patient
.
type
,
type
:
patient
.
type
,
status
:
patient
.
status
,
status
:
patient
.
status
,
});
});
setPreConsultReport
(
null
);
fetchPreConsultReport
(
patient
.
consult_id
,
patient
.
patient_id
);
fetchPreConsultReport
(
patient
.
consult_id
,
patient
.
patient_id
);
fetchMessages
(
patient
.
consult_id
);
fetchMessages
(
patient
.
consult_id
);
};
};
// 拒诊
const
handleReject
=
(
patient
:
PatientListItem
)
=>
{
const
handleReject
=
(
patient
:
PatientListItem
)
=>
{
console
.
log
(
'
handleReject 被调用
'
,
patient
);
Modal
.
confirm
({
Modal
.
confirm
({
title
:
'
确认拒诊
'
,
title
:
'
确认拒诊
'
,
content
:
`确定拒绝接诊患者
${
patient
.
patient_name
||
'
未知
'
}
?`
,
content
:
`确定拒绝接诊患者
${
patient
.
patient_name
||
'
未知
'
}
?`
,
onOk
:
async
()
=>
{
onOk
:
async
()
=>
{
try
{
try
{
console
.
log
(
'
调用 rejectConsult API
'
,
patient
.
consult_id
);
await
consultApi
.
rejectConsult
(
patient
.
consult_id
);
await
consultApi
.
rejectConsult
(
patient
.
consult_id
);
console
.
log
(
'
rejectConsult API 调用成功
'
);
message
.
info
(
'
已拒诊
'
);
message
.
info
(
'
已拒诊
'
);
fetchPatientList
();
fetchPatientList
();
}
catch
(
err
:
any
)
{
}
catch
{
console
.
error
(
'
拒诊失败
'
,
err
);
message
.
error
(
'
拒诊失败
'
);
message
.
error
(
'
拒诊失败
'
);
}
}
},
},
});
});
};
};
const
handleSendMessage
=
async
(
content
:
string
)
=>
{
const
handleSendMessage
=
async
(
content
:
string
)
=>
{
if
(
!
activeConsult
)
return
;
if
(
!
activeConsult
)
return
;
setSending
(
true
);
setSending
(
true
);
...
@@ -170,31 +164,66 @@ const ConsultPage: React.FC = () => {
...
@@ -170,31 +164,66 @@ const ConsultPage: React.FC = () => {
}
}
};
};
// 结束问诊
const
handleEndConsult
=
async
()
=>
{
const
handleEndConsult
=
async
()
=>
{
if
(
!
activeConsult
)
return
;
if
(
!
activeConsult
)
return
;
try
{
try
{
await
consultApi
.
endConsult
(
activeConsult
.
consult_id
);
await
consultApi
.
endConsult
(
activeConsult
.
consult_id
);
message
.
success
(
'
问诊已结束
'
);
message
.
success
(
'
问诊已结束
'
);
setActiveConsult
(
null
);
setActiveConsult
(
null
);
setMessages
([]);
resetConsultState
();
setPreConsultReport
(
null
);
fetchPatientList
();
fetchPatientList
();
}
catch
(
err
:
any
)
{
}
catch
(
err
:
unknown
)
{
message
.
error
(
'
结束问诊失败:
'
+
(
err
?.
message
||
'
未知错误
'
));
message
.
error
(
'
结束问诊失败:
'
+
(
(
err
as
Error
)
?.
message
||
'
未知错误
'
));
}
}
};
};
const
statItems
=
[
{
label
:
'
候诊中
'
,
value
:
waitingCount
,
color
:
'
#fa8c16
'
,
icon
:
<
ClockCircleOutlined
/>
},
{
label
:
'
接诊中
'
,
value
:
inProgressCount
,
color
:
'
#52c41a
'
,
icon
:
<
MessageOutlined
/>
},
{
label
:
'
今日完成
'
,
value
:
completedCount
,
color
:
'
#1890ff
'
,
icon
:
<
CheckCircleOutlined
/>
},
];
return
(
return
(
<
div
style=
{
{
height
:
'
calc(100vh - 72px)
'
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
12
}
}
>
{
/* 顶部标题 + 统计卡片 */
}
<
div
style=
{
{
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
16
,
flexShrink
:
0
}
}
>
<
div
>
<
div
>
<
Title
level=
{
4
}
style=
{
{
marginBottom
:
16
}
}
>
<
div
style=
{
{
fontSize
:
20
,
fontWeight
:
700
,
color
:
'
#1d2129
'
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
8
}
}
>
<
MessageOutlined
style=
{
{
marginRight
:
8
}
}
/>
<
MessageOutlined
style=
{
{
color
:
'
#1890ff
'
}
}
/>
问诊大厅
问诊大厅
</
Title
>
</
div
>
<
div
style=
{
{
fontSize
:
12
,
color
:
'
#8c8c8c
'
,
marginTop
:
2
}
}
>
实时接诊管理,AI辅助诊断
</
div
>
</
div
>
<
div
style=
{
{
marginLeft
:
'
auto
'
,
display
:
'
flex
'
,
gap
:
10
}
}
>
{
statItems
.
map
(({
label
,
value
,
color
,
icon
})
=>
(
<
div
key=
{
label
}
style=
{
{
background
:
'
#fff
'
,
borderRadius
:
10
,
padding
:
'
8px 18px
'
,
border
:
'
1px solid #edf2fc
'
,
minWidth
:
110
,
boxShadow
:
'
0 1px 4px rgba(0,0,0,0.04)
'
,
}
}
>
<
div
style=
{
{
fontSize
:
11
,
color
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
4
,
marginBottom
:
2
}
}
>
{
icon
}
{
label
}
</
div
>
<
div
style=
{
{
fontSize
:
24
,
fontWeight
:
700
,
color
,
lineHeight
:
1.2
}
}
>
{
value
}
<
span
style=
{
{
fontSize
:
12
,
fontWeight
:
400
,
marginLeft
:
2
}
}
>
人
</
span
>
</
div
>
</
div
>
))
}
</
div
>
</
div
>
<
Row
gutter=
{
16
}
style=
{
{
height
:
'
calc(100vh - 160px)
'
}
}
>
{
/* 主体三栏 */
}
<
Col
span=
{
6
}
>
<
div
style=
{
{
flex
:
1
,
display
:
'
flex
'
,
gap
:
12
,
overflow
:
'
hidden
'
,
minHeight
:
0
}
}
>
{
/* 左:患者列表 */
}
<
div
style=
{
{
width
:
272
,
flexShrink
:
0
}
}
>
<
PatientList
<
PatientList
patients=
{
patientList
}
patients=
{
patientList
}
onSelectPatient=
{
handleSelectPatient
}
onSelectPatient=
{
handleSelectPatient
}
...
@@ -202,31 +231,33 @@ const ConsultPage: React.FC = () => {
...
@@ -202,31 +231,33 @@ const ConsultPage: React.FC = () => {
onReject=
{
handleReject
}
onReject=
{
handleReject
}
selectedConsultId=
{
activeConsult
?.
consult_id
}
selectedConsultId=
{
activeConsult
?.
consult_id
}
/>
/>
</
Col
>
</
div
>
<
Col
span=
{
aiPanelVisible
?
12
:
18
}
>
{
/* 中:聊天区域 */
}
<
div
style=
{
{
flex
:
1
,
minWidth
:
0
}
}
>
<
ChatPanel
<
ChatPanel
activeConsult=
{
activeConsult
}
activeConsult=
{
activeConsult
}
messages=
{
messages
}
messages=
{
messages
}
onSend=
{
handleSendMessage
}
onSend=
{
handleSendMessage
}
onEndConsult=
{
handleEndConsult
}
onEndConsult=
{
handleEndConsult
}
onToggleAI=
{
()
=>
setAiPanelVisible
(
!
aiPanelVisible
)
}
aiPanelVisible=
{
aiPanelVisible
}
sending=
{
sending
}
sending=
{
sending
}
aiDiagnosis=
{
aiDiagnosis
}
aiMedication=
{
aiMedication
}
/>
/>
</
Col
>
</
div
>
{
aiPanelVisible
&&
(
{
/* 右:AI辅助面板 */
}
<
Col
span=
{
6
}
>
<
div
style=
{
{
width
:
340
,
flexShrink
:
0
}
}
>
<
AIPanel
<
AIPanel
hasActiveConsult=
{
!!
activeConsult
}
hasActiveConsult=
{
!!
activeConsult
}
activeConsultId=
{
activeConsult
?.
consult_id
}
activeConsultId=
{
activeConsult
?.
consult_id
}
preConsultReport=
{
preConsultReport
}
preConsultReport=
{
preConsultReport
}
preConsultLoading=
{
preConsultLoading
}
preConsultLoading=
{
preConsultLoading
}
onDiagnosisChange=
{
setAiDiagnosis
}
onMedicationChange=
{
setAiMedication
}
/>
/>
</
Col
>
</
div
>
)
}
</
div
>
</
Row
>
</
div
>
</
div
>
);
);
};
};
...
...
web/src/pages/doctor/Prescription/index.tsx
View file @
9033450a
...
@@ -4,16 +4,17 @@ import React, { useState, useCallback } from 'react';
...
@@ -4,16 +4,17 @@ import React, { useState, useCallback } from 'react';
import
{
import
{
Input
,
Button
,
Table
,
Form
,
Select
,
InputNumber
,
Input
,
Button
,
Table
,
Form
,
Select
,
InputNumber
,
Typography
,
Tag
,
Space
,
Modal
,
message
,
AutoComplete
,
Empty
,
Typography
,
Tag
,
Space
,
Modal
,
message
,
AutoComplete
,
Empty
,
Alert
,
Collapse
,
Timeline
,
}
from
'
antd
'
;
}
from
'
antd
'
;
import
{
import
{
DeleteOutlined
,
DeleteOutlined
,
SearchOutlined
,
SafetyCertificateOutlined
,
SearchOutlined
,
PrinterOutlined
,
MedicineBoxOutlined
,
RobotOutlined
,
SafetyCertificateOutlined
,
CheckCircleOutlined
,
CloseCircleOutlined
,
WarningOutlined
,
ThunderboltOutlined
,
PrinterOutlined
,
MedicineBoxOutlined
,
}
from
'
@ant-design/icons
'
;
}
from
'
@ant-design/icons
'
;
import
{
medicineApi
,
prescriptionDoctorApi
}
from
'
../../../api/prescription
'
;
import
{
medicineApi
,
prescriptionDoctorApi
}
from
'
../../../api/prescription
'
;
import
type
{
Medicine
}
from
'
../../../api/prescription
'
;
import
type
{
Medicine
}
from
'
../../../api/prescription
'
;
import
{
doctorPortalApi
}
from
'
../../../api/doctorPortal
'
;
import
type
{
ToolCall
}
from
'
../../../api/agent
'
;
const
{
Text
}
=
Typography
;
const
{
Text
}
=
Typography
;
const
{
TextArea
}
=
Input
;
const
{
TextArea
}
=
Input
;
...
@@ -33,6 +34,13 @@ interface DrugItem {
...
@@ -33,6 +34,13 @@ interface DrugItem {
note
:
string
;
note
:
string
;
}
}
interface
SafetyResult
{
report
:
string
;
tool_calls
?:
ToolCall
[];
has_warning
:
boolean
;
has_contraindication
:
boolean
;
}
interface
PrescriptionModalProps
{
interface
PrescriptionModalProps
{
open
:
boolean
;
open
:
boolean
;
onClose
:
()
=>
void
;
onClose
:
()
=>
void
;
...
@@ -41,10 +49,15 @@ interface PrescriptionModalProps {
...
@@ -41,10 +49,15 @@ interface PrescriptionModalProps {
patientName
?:
string
;
patientName
?:
string
;
patientGender
?:
string
;
patientGender
?:
string
;
patientAge
?:
number
;
patientAge
?:
number
;
/** AI鉴别诊断内容 — 用于预填诊断字段 */
diagnosis
?:
string
;
/** AI用药建议内容 — 作为开方参考 */
medicationSuggestion
?:
string
;
}
}
const
PrescriptionModal
:
React
.
FC
<
PrescriptionModalProps
>
=
({
const
PrescriptionModal
:
React
.
FC
<
PrescriptionModalProps
>
=
({
open
,
onClose
,
consultId
,
patientId
,
patientName
,
patientGender
,
patientAge
,
open
,
onClose
,
consultId
,
patientId
,
patientName
,
patientGender
,
patientAge
,
diagnosis
,
medicationSuggestion
,
})
=>
{
})
=>
{
const
[
drugs
,
setDrugs
]
=
useState
<
DrugItem
[]
>
([]);
const
[
drugs
,
setDrugs
]
=
useState
<
DrugItem
[]
>
([]);
const
[
searchValue
,
setSearchValue
]
=
useState
(
''
);
const
[
searchValue
,
setSearchValue
]
=
useState
(
''
);
...
@@ -52,23 +65,40 @@ const PrescriptionModal: React.FC<PrescriptionModalProps> = ({
...
@@ -52,23 +65,40 @@ const PrescriptionModal: React.FC<PrescriptionModalProps> = ({
const
[
signModalVisible
,
setSignModalVisible
]
=
useState
(
false
);
const
[
signModalVisible
,
setSignModalVisible
]
=
useState
(
false
);
const
[
submitting
,
setSubmitting
]
=
useState
(
false
);
const
[
submitting
,
setSubmitting
]
=
useState
(
false
);
const
[
remarkText
,
setRemarkText
]
=
useState
(
''
);
const
[
remarkText
,
setRemarkText
]
=
useState
(
''
);
const
[
safetyResult
,
setSafetyResult
]
=
useState
<
SafetyResult
|
null
>
(
null
);
const
[
safetyLoading
,
setSafetyLoading
]
=
useState
(
false
);
const
[
form
]
=
Form
.
useForm
();
const
[
form
]
=
Form
.
useForm
();
// Extract a brief diagnosis summary from AI text (first line or first 60 chars)
const
extractDiagnosisSummary
=
(
aiText
:
string
):
string
=>
{
if
(
!
aiText
)
return
''
;
const
lines
=
aiText
.
split
(
'
\n
'
).
filter
(
l
=>
l
.
trim
());
const
firstMeaningful
=
lines
.
find
(
l
=>
!
l
.
startsWith
(
'
#
'
)
&&
l
.
trim
().
length
>
0
);
if
(
firstMeaningful
)
{
const
clean
=
firstMeaningful
.
replace
(
/
[
#*`
]
/g
,
''
).
trim
();
return
clean
.
length
>
60
?
clean
.
slice
(
0
,
60
)
+
'
...
'
:
clean
;
}
return
aiText
.
slice
(
0
,
60
)
+
(
aiText
.
length
>
60
?
'
...
'
:
''
);
};
React
.
useEffect
(()
=>
{
React
.
useEffect
(()
=>
{
if
(
open
)
{
if
(
open
)
{
form
.
setFieldsValue
({
form
.
setFieldsValue
({
patient_name
:
patientName
||
''
,
patient_name
:
patientName
||
''
,
patient_gender
:
patientGender
||
undefined
,
patient_gender
:
patientGender
||
undefined
,
patient_age
:
patientAge
||
undefined
,
patient_age
:
patientAge
||
undefined
,
diagnosis
:
diagnosis
?
extractDiagnosisSummary
(
diagnosis
)
:
''
,
});
});
setSafetyResult
(
null
);
}
}
},
[
open
,
patientName
,
patientGender
,
patientAge
,
form
]);
},
[
open
,
patientName
,
patientGender
,
patientAge
,
diagnosis
,
form
]);
const
handleClose
=
()
=>
{
const
handleClose
=
()
=>
{
setDrugs
([]);
setDrugs
([]);
setSearchValue
(
''
);
setSearchValue
(
''
);
setSearchResults
([]);
setSearchResults
([]);
setRemarkText
(
''
);
setRemarkText
(
''
);
setSafetyResult
(
null
);
form
.
resetFields
();
form
.
resetFields
();
onClose
();
onClose
();
};
};
...
@@ -83,9 +113,9 @@ const PrescriptionModal: React.FC<PrescriptionModalProps> = ({
...
@@ -83,9 +113,9 @@ const PrescriptionModal: React.FC<PrescriptionModalProps> = ({
},
[]);
},
[]);
const
handleAddDrug
=
(
medicineId
:
string
)
=>
{
const
handleAddDrug
=
(
medicineId
:
string
)
=>
{
const
med
=
searchResults
.
find
(
(
m
)
=>
m
.
id
===
medicineId
);
const
med
=
searchResults
.
find
(
m
=>
m
.
id
===
medicineId
);
if
(
!
med
)
return
;
if
(
!
med
)
return
;
if
(
drugs
.
find
(
(
d
)
=>
d
.
medicine_id
===
med
.
id
))
{
if
(
drugs
.
find
(
d
=>
d
.
medicine_id
===
med
.
id
))
{
message
.
warning
(
'
该药品已添加
'
);
message
.
warning
(
'
该药品已添加
'
);
return
;
return
;
}
}
...
@@ -103,22 +133,48 @@ const PrescriptionModal: React.FC<PrescriptionModalProps> = ({
...
@@ -103,22 +133,48 @@ const PrescriptionModal: React.FC<PrescriptionModalProps> = ({
price
:
med
.
price
,
price
:
med
.
price
,
note
:
''
,
note
:
''
,
}]);
}]);
setSafetyResult
(
null
);
// Reset safety check when drugs change
setSearchValue
(
''
);
setSearchValue
(
''
);
setSearchResults
([]);
setSearchResults
([]);
};
};
const
handleRemoveDrug
=
(
key
:
string
)
=>
setDrugs
(
drugs
.
filter
((
d
)
=>
d
.
key
!==
key
));
const
handleRemoveDrug
=
(
key
:
string
)
=>
{
setDrugs
(
drugs
.
filter
(
d
=>
d
.
key
!==
key
));
setSafetyResult
(
null
);
};
const
handleDrugChange
=
(
key
:
string
,
field
:
string
,
value
:
any
)
=>
const
handleDrugChange
=
(
key
:
string
,
field
:
string
,
value
:
unknown
)
=>
setDrugs
(
drugs
.
map
(
(
d
)
=>
(
d
.
key
===
key
?
{
...
d
,
[
field
]:
value
}
:
d
)
));
setDrugs
(
drugs
.
map
(
d
=>
d
.
key
===
key
?
{
...
d
,
[
field
]:
value
}
:
d
));
const
totalAmount
=
drugs
.
reduce
((
sum
,
d
)
=>
sum
+
d
.
price
*
d
.
quantity
,
0
);
const
totalAmount
=
drugs
.
reduce
((
sum
,
d
)
=>
sum
+
d
.
price
*
d
.
quantity
,
0
);
const
handleSign
=
async
()
=>
{
// AI safety check
if
(
drugs
.
length
===
0
)
{
const
handleSafetyCheck
=
async
()
=>
{
message
.
warning
(
'
请至少添加一种药品
'
);
if
(
!
patientId
)
{
message
.
warning
(
'
无法获取患者信息,请确认已接诊
'
);
return
;
}
return
;
if
(
drugs
.
length
===
0
)
{
message
.
warning
(
'
请先添加药品
'
);
return
;
}
setSafetyLoading
(
true
);
try
{
const
res
=
await
doctorPortalApi
.
checkPrescriptionSafety
({
patient_id
:
patientId
,
drugs
:
drugs
.
map
(
d
=>
d
.
name
),
});
setSafetyResult
(
res
.
data
);
if
(
res
.
data
?.
has_contraindication
)
{
message
.
error
(
'
检测到禁忌症,请重新审核处方
'
);
}
else
if
(
res
.
data
?.
has_warning
)
{
message
.
warning
(
'
存在药物相互作用风险,请注意
'
);
}
else
{
message
.
success
(
'
处方安全审核通过
'
);
}
}
}
catch
(
err
:
unknown
)
{
message
.
error
(
'
安全审核失败:
'
+
((
err
as
Error
)?.
message
||
'
请稍后重试
'
));
}
finally
{
setSafetyLoading
(
false
);
}
};
const
handleSign
=
async
()
=>
{
if
(
drugs
.
length
===
0
)
{
message
.
warning
(
'
请至少添加一种药品
'
);
return
;
}
try
{
await
form
.
validateFields
();
}
catch
{
return
;
}
try
{
await
form
.
validateFields
();
}
catch
{
return
;
}
setSignModalVisible
(
true
);
setSignModalVisible
(
true
);
};
};
...
@@ -136,7 +192,7 @@ const PrescriptionModal: React.FC<PrescriptionModalProps> = ({
...
@@ -136,7 +192,7 @@ const PrescriptionModal: React.FC<PrescriptionModalProps> = ({
diagnosis
:
values
.
diagnosis
,
diagnosis
:
values
.
diagnosis
,
allergy_history
:
values
.
allergy
,
allergy_history
:
values
.
allergy
,
remark
:
remarkText
,
remark
:
remarkText
,
items
:
drugs
.
map
(
(
d
)
=>
({
items
:
drugs
.
map
(
d
=>
({
medicine_id
:
d
.
medicine_id
,
medicine_id
:
d
.
medicine_id
,
medicine_name
:
d
.
name
,
medicine_name
:
d
.
name
,
specification
:
d
.
specification
,
specification
:
d
.
specification
,
...
@@ -153,26 +209,97 @@ const PrescriptionModal: React.FC<PrescriptionModalProps> = ({
...
@@ -153,26 +209,97 @@ const PrescriptionModal: React.FC<PrescriptionModalProps> = ({
message
.
success
(
'
处方已签名并提交
'
);
message
.
success
(
'
处方已签名并提交
'
);
setSignModalVisible
(
false
);
setSignModalVisible
(
false
);
handleClose
();
handleClose
();
}
catch
(
err
:
any
)
{
}
catch
(
err
:
unknown
)
{
message
.
error
(
err
?.
message
||
'
提交处方失败
'
);
message
.
error
(
(
err
as
Error
)
?.
message
||
'
提交处方失败
'
);
}
finally
{
}
finally
{
setSubmitting
(
false
);
setSubmitting
(
false
);
}
}
};
};
const
renderSafetyResult
=
()
=>
{
if
(
!
safetyResult
)
return
null
;
return
(
<
div
style=
{
{
marginBottom
:
12
}
}
>
{
safetyResult
.
has_contraindication
?
(
<
Alert
type=
"error"
showIcon
message=
"存在禁忌症"
description=
"该患者对处方中的某些药物存在禁忌,请重新审查处方。"
style=
{
{
marginBottom
:
6
}
}
/>
)
:
safetyResult
.
has_warning
?
(
<
Alert
type=
"warning"
showIcon
message=
"存在药物相互作用风险"
description=
"处方中存在可能的药物相互作用,建议调整用药方案或注意监测。"
style=
{
{
marginBottom
:
6
}
}
/>
)
:
(
<
Alert
type=
"success"
showIcon
message=
"处方安全审核通过"
description=
"未检测到明显的禁忌症或药物相互作用风险。"
style=
{
{
marginBottom
:
6
}
}
/>
)
}
{
safetyResult
.
report
&&
(
<
Collapse
size=
"small"
items=
{
[{
key
:
'
report
'
,
label
:
<
span
style=
{
{
fontSize
:
12
}
}
><
RobotOutlined
style=
{
{
marginRight
:
4
}
}
/>
AI审核报告
</
span
>,
children
:
<
div
style=
{
{
fontSize
:
12
,
whiteSpace
:
'
pre-wrap
'
,
lineHeight
:
1.6
}
}
>
{
safetyResult
.
report
}
</
div
>,
}]
}
/>
)
}
{
safetyResult
.
tool_calls
&&
safetyResult
.
tool_calls
.
length
>
0
&&
(
<
Collapse
size=
"small"
style=
{
{
marginTop
:
4
}
}
items=
{
[{
key
:
'
tools
'
,
label
:
<
span
style=
{
{
fontSize
:
12
}
}
>
工具调用记录 (
{
safetyResult
.
tool_calls
.
length
}
)
</
span
>,
children
:
(
<
Timeline
items=
{
safetyResult
.
tool_calls
.
map
((
tc
,
i
)
=>
({
key
:
i
,
color
:
tc
.
success
?
'
green
'
:
'
red
'
,
dot
:
tc
.
success
?
<
CheckCircleOutlined
style=
{
{
fontSize
:
11
}
}
/>
:
<
CloseCircleOutlined
style=
{
{
fontSize
:
11
}
}
/>,
children
:
(
<
div
style=
{
{
fontSize
:
11
}
}
>
<
strong
>
{
tc
.
tool_name
}
</
strong
>
<
div
style=
{
{
color
:
'
#8c8c8c
'
}
}
>
{
tc
.
arguments
}
</
div
>
</
div
>
),
}))
}
/>
),
}]
}
/>
)
}
</
div
>
);
};
const
columns
=
[
const
columns
=
[
{
title
:
'
药品名称
'
,
dataIndex
:
'
name
'
,
key
:
'
name
'
,
width
:
1
40
,
render
:
(
t
:
string
)
=>
<
Text
strong
>
{
t
}
</
Text
>
},
{
title
:
'
药品名称
'
,
dataIndex
:
'
name
'
,
key
:
'
name
'
,
width
:
1
30
,
render
:
(
t
:
string
)
=>
<
Text
strong
style=
{
{
fontSize
:
13
}
}
>
{
t
}
</
Text
>
},
{
title
:
'
规格
'
,
dataIndex
:
'
specification
'
,
key
:
'
specification
'
,
width
:
90
},
{
title
:
'
规格
'
,
dataIndex
:
'
specification
'
,
key
:
'
specification
'
,
width
:
90
,
render
:
(
t
:
string
)
=>
<
Text
style=
{
{
fontSize
:
12
}
}
>
{
t
}
</
Text
>
},
{
{
title
:
'
用法
'
,
dataIndex
:
'
usage
'
,
key
:
'
usage
'
,
width
:
100
,
title
:
'
用法
'
,
dataIndex
:
'
usage
'
,
key
:
'
usage
'
,
width
:
100
,
render
:
(
_
:
string
,
r
:
DrugItem
)
=>
(
render
:
(
_
:
string
,
r
:
DrugItem
)
=>
(
<
Select
value=
{
r
.
usage
}
size=
"small"
style=
{
{
width
:
90
}
}
<
Select
onChange=
{
(
v
)
=>
handleDrugChange
(
r
.
key
,
'
usage
'
,
v
)
}
value=
{
r
.
usage
}
size=
"small"
style=
{
{
width
:
90
}
}
onChange=
{
v
=>
handleDrugChange
(
r
.
key
,
'
usage
'
,
v
)
}
options=
{
[
options=
{
[
{
label
:
'
口服
'
,
value
:
'
口服
'
},
{
label
:
'
口服
'
,
value
:
'
口服
'
},
{
label
:
'
外用
'
,
value
:
'
外用
'
},
{
label
:
'
外用
'
,
value
:
'
外用
'
},
{
label
:
'
静脉注射
'
,
value
:
'
静脉注射
'
},
{
label
:
'
肌肉注射
'
,
value
:
'
肌肉注射
'
},
{
label
:
'
静脉注射
'
,
value
:
'
静脉注射
'
},
{
label
:
'
肌肉注射
'
,
value
:
'
肌肉注射
'
},
{
label
:
'
雾化吸入
'
,
value
:
'
雾化吸入
'
},
{
label
:
'
雾化吸入
'
,
value
:
'
雾化吸入
'
},
]
}
]
}
/>
/>
...
@@ -182,19 +309,20 @@ const PrescriptionModal: React.FC<PrescriptionModalProps> = ({
...
@@ -182,19 +309,20 @@ const PrescriptionModal: React.FC<PrescriptionModalProps> = ({
title
:
'
单次剂量
'
,
dataIndex
:
'
dosage
'
,
key
:
'
dosage
'
,
width
:
110
,
title
:
'
单次剂量
'
,
dataIndex
:
'
dosage
'
,
key
:
'
dosage
'
,
width
:
110
,
render
:
(
_
:
string
,
r
:
DrugItem
)
=>
(
render
:
(
_
:
string
,
r
:
DrugItem
)
=>
(
<
Input
size=
"small"
placeholder=
"如 1粒"
value=
{
r
.
dosage
}
<
Input
size=
"small"
placeholder=
"如 1粒"
value=
{
r
.
dosage
}
onChange=
{
(
e
)
=>
handleDrugChange
(
r
.
key
,
'
dosage
'
,
e
.
target
.
value
)
}
/>
onChange=
{
e
=>
handleDrugChange
(
r
.
key
,
'
dosage
'
,
e
.
target
.
value
)
}
/>
),
),
},
},
{
{
title
:
'
频次
'
,
dataIndex
:
'
frequency
'
,
key
:
'
frequency
'
,
width
:
110
,
title
:
'
频次
'
,
dataIndex
:
'
frequency
'
,
key
:
'
frequency
'
,
width
:
110
,
render
:
(
_
:
string
,
r
:
DrugItem
)
=>
(
render
:
(
_
:
string
,
r
:
DrugItem
)
=>
(
<
Select
value=
{
r
.
frequency
}
size=
"small"
style=
{
{
width
:
100
}
}
<
Select
onChange=
{
(
v
)
=>
handleDrugChange
(
r
.
key
,
'
frequency
'
,
v
)
}
value=
{
r
.
frequency
}
size=
"small"
style=
{
{
width
:
100
}
}
onChange=
{
v
=>
handleDrugChange
(
r
.
key
,
'
frequency
'
,
v
)
}
options=
{
[
options=
{
[
{
label
:
'
每日1次
'
,
value
:
'
每日1次
'
},
{
label
:
'
每日1次
'
,
value
:
'
每日1次
'
},
{
label
:
'
每日2次
'
,
value
:
'
每日2次
'
},
{
label
:
'
每日2次
'
,
value
:
'
每日2次
'
},
{
label
:
'
每日3次
'
,
value
:
'
每日3次
'
},
{
label
:
'
必要时
'
,
value
:
'
必要时
'
},
{
label
:
'
每日3次
'
,
value
:
'
每日3次
'
},
{
label
:
'
必要时
'
,
value
:
'
必要时
'
},
{
label
:
'
睡前
'
,
value
:
'
睡前
'
},
{
label
:
'
睡前
'
,
value
:
'
睡前
'
},
]
}
]
}
/>
/>
...
@@ -203,28 +331,29 @@ const PrescriptionModal: React.FC<PrescriptionModalProps> = ({
...
@@ -203,28 +331,29 @@ const PrescriptionModal: React.FC<PrescriptionModalProps> = ({
{
{
title
:
'
天数
'
,
dataIndex
:
'
days
'
,
key
:
'
days
'
,
width
:
70
,
title
:
'
天数
'
,
dataIndex
:
'
days
'
,
key
:
'
days
'
,
width
:
70
,
render
:
(
_
:
number
,
r
:
DrugItem
)
=>
(
render
:
(
_
:
number
,
r
:
DrugItem
)
=>
(
<
InputNumber
size=
"small"
min=
{
1
}
max=
{
3
0
}
value=
{
r
.
days
}
<
InputNumber
size=
"small"
min=
{
1
}
max=
{
9
0
}
value=
{
r
.
days
}
onChange=
{
(
v
)
=>
handleDrugChange
(
r
.
key
,
'
days
'
,
v
)
}
style=
{
{
width
:
60
}
}
/>
onChange=
{
v
=>
handleDrugChange
(
r
.
key
,
'
days
'
,
v
)
}
style=
{
{
width
:
58
}
}
/>
),
),
},
},
{
{
title
:
'
数量
'
,
dataIndex
:
'
quantity
'
,
key
:
'
quantity
'
,
width
:
10
0
,
title
:
'
数量
'
,
dataIndex
:
'
quantity
'
,
key
:
'
quantity
'
,
width
:
9
0
,
render
:
(
_
:
number
,
r
:
DrugItem
)
=>
(
render
:
(
_
:
number
,
r
:
DrugItem
)
=>
(
<
Space
size=
{
4
}
>
<
Space
size=
{
4
}
>
<
InputNumber
size=
"small"
min=
{
1
}
value=
{
r
.
quantity
}
<
InputNumber
size=
"small"
min=
{
1
}
value=
{
r
.
quantity
}
onChange=
{
(
v
)
=>
handleDrugChange
(
r
.
key
,
'
quantity
'
,
v
)
}
style=
{
{
width
:
55
}
}
/>
onChange=
{
v
=>
handleDrugChange
(
r
.
key
,
'
quantity
'
,
v
)
}
style=
{
{
width
:
50
}
}
/>
<
Text
type=
"secondary"
>
{
r
.
unit
}
</
Text
>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
12
}
}
>
{
r
.
unit
}
</
Text
>
</
Space
>
</
Space
>
),
),
},
},
{
{
title
:
'
单价
'
,
dataIndex
:
'
price
'
,
key
:
'
price
'
,
width
:
70
,
title
:
'
单价
'
,
dataIndex
:
'
price
'
,
key
:
'
price
'
,
width
:
70
,
render
:
(
v
:
number
)
=>
`¥
${(
v
/
100
).
toFixed
(
2
)}
`
,
render
:
(
v
:
number
)
=>
<
Text
style=
{
{
fontSize
:
12
}
}
>
¥
{
(
v
/
100
).
toFixed
(
2
)
}
</
Text
>
,
},
},
{
{
title
:
'
操作
'
,
key
:
'
action
'
,
width
:
50
,
title
:
'
操作
'
,
key
:
'
action
'
,
width
:
48
,
render
:
(
_
:
any
,
r
:
DrugItem
)
=>
(
render
:
(
_
:
unknown
,
r
:
DrugItem
)
=>
(
<
Button
type=
"text"
danger
icon=
{
<
DeleteOutlined
/>
}
onClick=
{
()
=>
handleRemoveDrug
(
r
.
key
)
}
/>
<
Button
type=
"text"
danger
size=
"small"
icon=
{
<
DeleteOutlined
/>
}
onClick=
{
()
=>
handleRemoveDrug
(
r
.
key
)
}
/>
),
),
},
},
];
];
...
@@ -232,73 +361,161 @@ const PrescriptionModal: React.FC<PrescriptionModalProps> = ({
...
@@ -232,73 +361,161 @@ const PrescriptionModal: React.FC<PrescriptionModalProps> = ({
return
(
return
(
<>
<>
<
Modal
<
Modal
title=
{
<
span
><
MedicineBoxOutlined
className=
"text-green-500 mr-1"
/>
开具处方
</
span
>
}
title=
{
<
span
><
MedicineBoxOutlined
style=
{
{
color
:
'
#52c41a
'
,
marginRight
:
6
}
}
/>
开具处方
</
span
>
}
open=
{
open
}
onCancel=
{
handleClose
}
width=
{
960
}
footer=
{
null
}
destroyOnClose
open=
{
open
}
onCancel=
{
handleClose
}
width=
{
980
}
footer=
{
null
}
destroyOnClose
>
>
<
Form
layout=
"inline"
form=
{
form
}
size=
"small"
className=
"mb-2 p-2 bg-gray-50 rounded"
>
{
/* AI用药建议参考 */
}
{
medicationSuggestion
&&
(
<
Collapse
size=
"small"
defaultActiveKey=
{
[
'
suggestion
'
]
}
style=
{
{
marginBottom
:
12
,
borderRadius
:
8
}
}
items=
{
[{
key
:
'
suggestion
'
,
label
:
(
<
span
style=
{
{
fontSize
:
12
,
color
:
'
#722ed1
'
}
}
>
<
RobotOutlined
style=
{
{
marginRight
:
4
}
}
/>
AI用药建议参考(点击展开/折叠)
</
span
>
),
children
:
(
<
div
style=
{
{
maxHeight
:
140
,
overflow
:
'
auto
'
,
fontSize
:
12
,
lineHeight
:
1.6
,
color
:
'
#333
'
,
whiteSpace
:
'
pre-wrap
'
,
background
:
'
#faf5ff
'
,
padding
:
8
,
borderRadius
:
6
,
}
}
>
{
medicationSuggestion
}
</
div
>
),
}]
}
/>
)
}
{
/* 患者基本信息 */
}
<
Form
layout=
"inline"
form=
{
form
}
size=
"small"
style=
{
{
marginBottom
:
10
,
padding
:
'
8px 12px
'
,
background
:
'
#f9fafb
'
,
borderRadius
:
8
}
}
>
<
Form
.
Item
label=
"姓名"
name=
"patient_name"
rules=
{
[{
required
:
true
,
message
:
'
请输入姓名
'
}]
}
>
<
Form
.
Item
label=
"姓名"
name=
"patient_name"
rules=
{
[{
required
:
true
,
message
:
'
请输入姓名
'
}]
}
>
<
Input
placeholder=
"请输入姓名"
style=
{
{
width
:
100
}
}
/>
<
Input
placeholder=
"请输入姓名"
style=
{
{
width
:
96
}
}
/>
</
Form
.
Item
>
</
Form
.
Item
>
<
Form
.
Item
label=
"性别"
name=
"patient_gender"
>
<
Form
.
Item
label=
"性别"
name=
"patient_gender"
>
<
Select
placeholder=
"性别"
style=
{
{
width
:
70
}
}
<
Select
placeholder=
"性别"
style=
{
{
width
:
68
}
}
options=
{
[{
label
:
'
男
'
,
value
:
'
男
'
},
{
label
:
'
女
'
,
value
:
'
女
'
}]
}
allowClear
/>
options=
{
[{
label
:
'
男
'
,
value
:
'
男
'
},
{
label
:
'
女
'
,
value
:
'
女
'
}]
}
allowClear
/>
</
Form
.
Item
>
</
Form
.
Item
>
<
Form
.
Item
label=
"年龄"
name=
"patient_age"
>
<
Form
.
Item
label=
"年龄"
name=
"patient_age"
>
<
InputNumber
placeholder=
"岁"
style=
{
{
width
:
60
}
}
min=
{
0
}
max=
{
150
}
/>
<
InputNumber
placeholder=
"岁"
style=
{
{
width
:
60
}
}
min=
{
0
}
max=
{
150
}
/>
</
Form
.
Item
>
</
Form
.
Item
>
<
Form
.
Item
label=
"诊断"
name=
"diagnosis"
rules=
{
[{
required
:
true
,
message
:
'
请输入诊断
'
}]
}
>
<
Form
.
Item
<
Input
placeholder=
"诊断"
style=
{
{
width
:
160
}
}
/>
label=
"诊断"
name=
"diagnosis"
rules=
{
[{
required
:
true
,
message
:
'
请输入诊断
'
}]
}
tooltip=
{
diagnosis
?
'
AI已根据鉴别诊断结果预填,可修改
'
:
undefined
}
>
<
Input
placeholder=
"诊断(必填)"
style=
{
{
width
:
200
}
}
suffix=
{
diagnosis
?
<
RobotOutlined
style=
{
{
color
:
'
#722ed1
'
,
fontSize
:
11
}
}
/>
:
undefined
}
/>
</
Form
.
Item
>
</
Form
.
Item
>
<
Form
.
Item
label=
"过敏"
name=
"allergy"
>
<
Form
.
Item
label=
"过敏"
name=
"allergy"
>
<
Input
placeholder=
"过敏史"
style=
{
{
width
:
1
1
0
}
}
/>
<
Input
placeholder=
"过敏史"
style=
{
{
width
:
1
2
0
}
}
/>
</
Form
.
Item
>
</
Form
.
Item
>
</
Form
>
</
Form
>
<
div
className=
"flex items-center gap-2 mb-2"
>
{
/* 药品列表 */
}
<
div
style=
{
{
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
8
,
marginBottom
:
8
}
}
>
<
Space
size=
{
4
}
>
<
Space
size=
{
4
}
>
<
MedicineBoxOutlined
/>
<
MedicineBoxOutlined
/>
<
Text
strong
>
药品
</
Text
>
<
Text
strong
>
药品
清单
</
Text
>
<
Tag
color=
"blue"
>
{
drugs
.
length
}
种
</
Tag
>
<
Tag
color=
"blue"
>
{
drugs
.
length
}
种
</
Tag
>
</
Space
>
</
Space
>
<
AutoComplete
<
AutoComplete
value=
{
searchValue
}
value=
{
searchValue
}
options=
{
searchResults
.
map
(
(
m
)
=>
({
options=
{
searchResults
.
map
(
m
=>
({
value
:
m
.
id
,
value
:
m
.
id
,
label
:
`${m.name} (${m.specification}) 库存:${m.stock}`
,
label
:
`${m.name} (${m.specification}) 库存:${m.stock}`
,
}))
}
}))
}
onSelect=
{
handleAddDrug
}
onSelect=
{
handleAddDrug
}
onSearch=
{
handleSearch
}
onSearch=
{
handleSearch
}
onChange=
{
setSearchValue
}
onChange=
{
setSearchValue
}
style=
{
{
width
:
2
60
}
}
style=
{
{
width
:
2
80
,
marginLeft
:
8
}
}
>
>
<
Input
prefix=
{
<
SearchOutlined
/>
}
placeholder=
"搜索药品..."
allowClear
size=
"small"
/>
<
Input
prefix=
{
<
SearchOutlined
/>
}
placeholder=
"搜索药品
名称
..."
allowClear
size=
"small"
/>
</
AutoComplete
>
</
AutoComplete
>
{
drugs
.
length
>
0
&&
(
<
Button
size=
"small"
icon=
{
<
ThunderboltOutlined
style=
{
{
color
:
'
#52c41a
'
}
}
/>
}
loading=
{
safetyLoading
}
onClick=
{
handleSafetyCheck
}
style=
{
{
marginLeft
:
'
auto
'
}
}
>
AI安全审核
</
Button
>
)
}
</
div
>
</
div
>
{
/* 安全审核结果 */
}
{
renderSafetyResult
()
}
{
drugs
.
length
>
0
?
(
{
drugs
.
length
>
0
?
(
<
Table
columns=
{
columns
}
dataSource=
{
drugs
}
pagination=
{
false
}
size=
"small"
<
Table
rowKey=
"key"
scroll=
{
{
x
:
800
}
}
className=
"mb-2"
/>
columns=
{
columns
}
dataSource=
{
drugs
}
pagination=
{
false
}
size=
"small"
rowKey=
"key"
scroll=
{
{
x
:
800
}
}
style=
{
{
marginBottom
:
8
}
}
rowClassName=
{
safetyResult
?.
has_contraindication
?
()
=>
'
prescription-warning-row
'
:
undefined
}
/>
)
:
(
)
:
(
<
Empty
description=
"请搜索并添加药品"
className=
"my-3"
/>
<
Empty
description=
"请搜索并添加药品"
style=
{
{
margin
:
'
12px 0
'
}
}
/>
)
}
)
}
{
/* 合计 */
}
{
drugs
.
length
>
0
&&
(
{
drugs
.
length
>
0
&&
(
<
div
className=
"text-right mb-1"
>
<
div
style=
{
{
textAlign
:
'
right
'
,
marginBottom
:
8
}
}
>
<
Text
strong
className=
"text-red-500"
>
合计:¥
{
(
totalAmount
/
100
).
toFixed
(
2
)
}
</
Text
>
<
Text
strong
style=
{
{
color
:
'
#ff4d4f
'
,
fontSize
:
14
}
}
>
合计:¥
{
(
totalAmount
/
100
).
toFixed
(
2
)
}
</
Text
>
</
div
>
</
div
>
)
}
)
}
<
TextArea
rows=
{
2
}
placeholder=
"医嘱备注..."
size=
"small"
{
/* 医嘱 */
}
value=
{
remarkText
}
onChange=
{
(
e
)
=>
setRemarkText
(
e
.
target
.
value
)
}
className=
"mb-2"
/>
<
TextArea
rows=
{
2
}
placeholder=
"医嘱备注..."
size=
"small"
value=
{
remarkText
}
onChange=
{
e
=>
setRemarkText
(
e
.
target
.
value
)
}
style=
{
{
marginBottom
:
10
}
}
/>
<
div
className=
"flex justify-end gap-1"
>
{
/* 底部操作 */
}
<
div
style=
{
{
display
:
'
flex
'
,
justifyContent
:
'
flex-end
'
,
gap
:
8
}
}
>
<
Button
size=
"small"
icon=
{
<
PrinterOutlined
/>
}
onClick=
{
()
=>
message
.
info
(
'
打印功能开发中
'
)
}
>
打印
</
Button
>
<
Button
size=
"small"
icon=
{
<
PrinterOutlined
/>
}
onClick=
{
()
=>
message
.
info
(
'
打印功能开发中
'
)
}
>
打印
</
Button
>
<
Button
size=
"small"
onClick=
{
handleClose
}
>
取消
</
Button
>
<
Button
size=
"small"
onClick=
{
handleClose
}
>
取消
</
Button
>
<
Button
type=
"primary"
size=
"small"
icon=
{
<
SafetyCertificateOutlined
/>
}
{
safetyResult
?.
has_contraindication
&&
(
style=
{
{
background
:
'
#52c41a
'
,
borderColor
:
'
#52c41a
'
}
}
onClick=
{
handleSign
}
>
签名提交
</
Button
>
<
Tag
color=
"error"
icon=
{
<
WarningOutlined
/>
}
style=
{
{
display
:
'
flex
'
,
alignItems
:
'
center
'
}
}
>
存在禁忌症
</
Tag
>
)
}
<
Button
type=
"primary"
size=
"small"
icon=
{
<
SafetyCertificateOutlined
/>
}
style=
{
{
background
:
'
#52c41a
'
,
borderColor
:
'
#52c41a
'
}
}
onClick=
{
handleSign
}
disabled=
{
safetyResult
?.
has_contraindication
===
true
}
>
签名提交
</
Button
>
</
div
>
</
div
>
</
Modal
>
</
Modal
>
{
/* CA签名确认 */
}
<
Modal
<
Modal
title=
"CA 签名确认"
title=
"CA 签名确认"
open=
{
signModalVisible
}
open=
{
signModalVisible
}
...
@@ -309,13 +526,19 @@ const PrescriptionModal: React.FC<PrescriptionModalProps> = ({
...
@@ -309,13 +526,19 @@ const PrescriptionModal: React.FC<PrescriptionModalProps> = ({
confirmLoading=
{
submitting
}
confirmLoading=
{
submitting
}
width=
{
380
}
width=
{
380
}
>
>
<
div
className=
"text-center py-3"
>
<
div
style=
{
{
textAlign
:
'
center
'
,
padding
:
'
16px 0
'
}
}
>
<
SafetyCertificateOutlined
className=
"text-3xl text-green-500 mb-2"
/>
<
SafetyCertificateOutlined
style=
{
{
fontSize
:
36
,
color
:
'
#52c41a
'
,
marginBottom
:
12
,
display
:
'
block
'
}
}
/>
<
div
className=
"font-semibold mb-1"
>
确认使用 CA 数字证书签署此处方?
</
div
>
<
div
style=
{
{
fontWeight
:
600
,
marginBottom
:
6
}
}
>
确认使用 CA 数字证书签署此处方?
</
div
>
<
Text
type=
"secondary"
>
签署后处方将具有法律效力
</
Text
>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
13
}
}
>
签署后处方将具有法律效力
</
Text
>
<
div
className=
"mt-3 p-2 bg-green-50 rounded"
>
<
div
style=
{
{
marginTop
:
14
,
padding
:
'
10px 16px
'
,
background
:
'
#f6ffed
'
,
borderRadius
:
8
,
border
:
'
1px solid #b7eb8f
'
}
}
>
<
div
>
药品:
<
Text
strong
>
{
drugs
.
length
}
</
Text
>
种
</
div
>
<
div
>
药品:
<
Text
strong
>
{
drugs
.
length
}
</
Text
>
种
</
div
>
<
div
>
金额:
<
Text
strong
className=
"text-red-500"
>
¥
{
(
totalAmount
/
100
).
toFixed
(
2
)
}
</
Text
></
div
>
<
div
style=
{
{
marginTop
:
4
}
}
>
金额:
<
Text
strong
style=
{
{
color
:
'
#ff4d4f
'
}
}
>
¥
{
(
totalAmount
/
100
).
toFixed
(
2
)
}
</
Text
></
div
>
{
safetyResult
&&
!
safetyResult
.
has_contraindication
&&
!
safetyResult
.
has_warning
&&
(
<
div
style=
{
{
marginTop
:
4
,
color
:
'
#52c41a
'
,
fontSize
:
12
}
}
>
<
CheckCircleOutlined
style=
{
{
marginRight
:
4
}
}
/>
已通过AI安全审核
</
div
>
)
}
</
div
>
</
div
>
</
div
>
</
div
>
</
Modal
>
</
Modal
>
...
...
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