feat: 联调登录+对话
This commit is contained in:
+155
-5
@@ -3,7 +3,7 @@
|
||||
v-if="showDiagnosisPage"
|
||||
:case-item="caseItem"
|
||||
@open-settings="emit('open-settings')"
|
||||
@open-profile="emit('open-profile')"
|
||||
@open-profile="openProfile"
|
||||
@go-home="emit('go-home')"
|
||||
/>
|
||||
<view v-else class="chat-page">
|
||||
@@ -16,7 +16,7 @@
|
||||
<view class="home-icon"></view>
|
||||
</button>
|
||||
<view class="nav-spacer"></view>
|
||||
<button class="icon-button" aria-label="个人中心" @click="emit('open-profile')">
|
||||
<button class="icon-button" aria-label="个人中心" @click="openProfile">
|
||||
<view class="account-icon"></view>
|
||||
</button>
|
||||
</view>
|
||||
@@ -78,9 +78,9 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="mentor-float">
|
||||
<view class="mentor-float" :class="{ loading: hinting }" @click.stop="handleMentorHint" @tap.stop="handleMentorHint">
|
||||
<text class="mentor-badge">王主任</text>
|
||||
<view class="mentor-avatar">
|
||||
<view class="mentor-avatar" @click.stop="handleMentorHint" @tap.stop="handleMentorHint">
|
||||
<image src="/static/config-doctor.png" mode="aspectFill"></image>
|
||||
</view>
|
||||
</view>
|
||||
@@ -195,6 +195,8 @@
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
import type { ClinicalCase } from '../../api/cases'
|
||||
import { createMockChatSession, sendMockChatMessage, type ChatMessage, type ChatSession } from '../../api/chat'
|
||||
import { createProfileOpener } from '../../api/navigation'
|
||||
import { streamSessionChat, streamSessionHint, type TrainingSession } from '../../api/session'
|
||||
import DiagnosisPage from '../diagnosis/diagnosis.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -207,6 +209,8 @@ const emit = defineEmits<{
|
||||
(event: 'go-home'): void
|
||||
}>()
|
||||
|
||||
const openProfile = createProfileOpener(emit)
|
||||
|
||||
const session = reactive<ChatSession>({
|
||||
patient: {
|
||||
name: '陈先生',
|
||||
@@ -225,14 +229,22 @@ const session = reactive<ChatSession>({
|
||||
|
||||
const draft = ref('')
|
||||
const sending = ref(false)
|
||||
const hinting = ref(false)
|
||||
const scrollTop = ref(0)
|
||||
const toastMessage = ref('')
|
||||
const toastVisible = ref(false)
|
||||
const showDiagnosisPage = ref(false)
|
||||
const examPanelVisible = ref(false)
|
||||
const physicalPanelVisible = ref(false)
|
||||
const activeSessionId = ref<number | null>(null)
|
||||
|
||||
let toastTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let activeStreamController: AbortController | null = null
|
||||
let activeHintController: AbortController | null = null
|
||||
|
||||
type StoredScenario = {
|
||||
session?: TrainingSession
|
||||
}
|
||||
|
||||
type AuxiliaryExam = {
|
||||
name: string
|
||||
@@ -278,11 +290,33 @@ function loadSession() {
|
||||
createMockChatSession(props.caseItem).then(result => {
|
||||
Object.assign(session.patient, result.patient)
|
||||
session.stages = result.stages
|
||||
const scenario = readStoredScenario()
|
||||
if (scenario?.session?.session_id) {
|
||||
activeSessionId.value = scenario.session.session_id
|
||||
session.messages = scenario.session.patient_opening ? [
|
||||
{
|
||||
id: `patient-opening-${scenario.session.session_id}`,
|
||||
role: 'patient',
|
||||
content: scenario.session.patient_opening,
|
||||
label: '患者'
|
||||
}
|
||||
] : []
|
||||
scrollToBottom()
|
||||
void streamPatientReply('开始问诊')
|
||||
return
|
||||
}
|
||||
|
||||
session.messages = result.messages
|
||||
scrollToBottom()
|
||||
})
|
||||
}
|
||||
|
||||
function readStoredScenario() {
|
||||
const value = uni.getStorageSync('clinical-thinking-scenario')
|
||||
if (value && typeof value === 'object') return value as StoredScenario
|
||||
return null
|
||||
}
|
||||
|
||||
function sendQuickAction(content: string) {
|
||||
draft.value = content
|
||||
handleSend()
|
||||
@@ -393,8 +427,20 @@ function handleSend() {
|
||||
const content = draft.value.trim()
|
||||
if (!content || sending.value) return
|
||||
|
||||
sending.value = true
|
||||
draft.value = ''
|
||||
if (activeSessionId.value) {
|
||||
session.messages.push({
|
||||
id: `doctor-${Date.now()}`,
|
||||
role: 'doctor',
|
||||
content,
|
||||
label: '我'
|
||||
})
|
||||
scrollToBottom()
|
||||
void streamPatientReply(content)
|
||||
return
|
||||
}
|
||||
|
||||
sending.value = true
|
||||
sendMockChatMessage(content).then(messages => {
|
||||
session.messages.push(...messages)
|
||||
scrollToBottom()
|
||||
@@ -403,6 +449,97 @@ function handleSend() {
|
||||
})
|
||||
}
|
||||
|
||||
async function streamPatientReply(message: string) {
|
||||
const sessionId = activeSessionId.value
|
||||
if (!sessionId) return
|
||||
|
||||
sending.value = true
|
||||
activeStreamController?.abort()
|
||||
activeStreamController = new AbortController()
|
||||
|
||||
const streamedMessageIndex = session.messages.length
|
||||
session.messages.push({
|
||||
id: `patient-stream-${Date.now()}`,
|
||||
role: 'patient',
|
||||
content: '',
|
||||
label: '患者'
|
||||
})
|
||||
scrollToBottom()
|
||||
|
||||
try {
|
||||
await streamSessionChat(sessionId, message, {
|
||||
onDelta(delta) {
|
||||
session.messages[streamedMessageIndex].content += delta
|
||||
scrollToBottom()
|
||||
}
|
||||
}, activeStreamController.signal)
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === 'AbortError') return
|
||||
if (!session.messages[streamedMessageIndex].content.trim()) {
|
||||
session.messages[streamedMessageIndex].content = 'AI 病人回复失败,请重试。'
|
||||
}
|
||||
showToast(error instanceof Error ? error.message : 'AI 流式回复异常')
|
||||
} finally {
|
||||
sending.value = false
|
||||
activeStreamController = null
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
function findLastUserMessage() {
|
||||
const lastDoctorMessage = [...session.messages].reverse().find(message => message.role === 'doctor')
|
||||
return lastDoctorMessage?.content || '开始问诊'
|
||||
}
|
||||
|
||||
async function handleMentorHint() {
|
||||
const sessionId = activeSessionId.value
|
||||
if (!sessionId) {
|
||||
showToast('请先生成模拟场景')
|
||||
return
|
||||
}
|
||||
if (hinting.value) return
|
||||
|
||||
hinting.value = true
|
||||
activeHintController?.abort()
|
||||
activeHintController = new AbortController()
|
||||
|
||||
const streamedMessageIndex = session.messages.length
|
||||
session.messages.push({
|
||||
id: `mentor-hint-${Date.now()}`,
|
||||
role: 'mentor',
|
||||
content: '王主任正在生成提示...',
|
||||
label: '王主任'
|
||||
})
|
||||
scrollToBottom()
|
||||
|
||||
try {
|
||||
let receivedFirstDelta = false
|
||||
await streamSessionHint(sessionId, findLastUserMessage(), {
|
||||
onDelta(delta) {
|
||||
if (!receivedFirstDelta) {
|
||||
session.messages[streamedMessageIndex].content = ''
|
||||
receivedFirstDelta = true
|
||||
}
|
||||
session.messages[streamedMessageIndex].content += delta
|
||||
scrollToBottom()
|
||||
}
|
||||
}, activeHintController.signal)
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === 'AbortError') return
|
||||
if (
|
||||
!session.messages[streamedMessageIndex].content.trim()
|
||||
|| session.messages[streamedMessageIndex].content === '王主任正在生成提示...'
|
||||
) {
|
||||
session.messages[streamedMessageIndex].content = '练习提示生成失败,请稍后重试。'
|
||||
}
|
||||
showToast(error instanceof Error ? error.message : '练习提示生成失败,请稍后重试')
|
||||
} finally {
|
||||
hinting.value = false
|
||||
activeHintController = null
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
setTimeout(() => {
|
||||
scrollTop.value += 1000
|
||||
@@ -425,6 +562,8 @@ function showToast(message: string) {
|
||||
onMounted(loadSession)
|
||||
|
||||
onUnmounted(() => {
|
||||
activeStreamController?.abort()
|
||||
activeHintController?.abort()
|
||||
if (toastTimer) clearTimeout(toastTimer)
|
||||
})
|
||||
</script>
|
||||
@@ -872,6 +1011,17 @@ page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transition: transform 0.18s ease, opacity 0.18s ease;
|
||||
}
|
||||
|
||||
.mentor-float:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.mentor-float.loading {
|
||||
opacity: 0.72;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user