|
|
@@ -247,6 +247,9 @@ export default {
|
|
|
|
|
|
const isInDepartmentFollowup = ref(false)
|
|
|
|
|
|
+ /** 软刷新时自增,用于忽略刷新前未结束的 SSE/异步回调,避免旧数据写回已清空的对话 */
|
|
|
+ const chatResetEpoch = ref(0)
|
|
|
+
|
|
|
/** 机器人追问病情/症状时出现的关键词,用于标记「科室推荐后的追问阶段」 */
|
|
|
const FOLLOWUP_HINT_REGEX = /(症状|主诉|不适|多久|多长时间|部位|程度|具体|哪些|诱因|加重|减轻|伴随|发作|持续|反复|既往|过敏|用药|体温|疼痛|请补充|请描述|详细说说|还有其他|进一步|请问)/
|
|
|
|
|
|
@@ -376,6 +379,7 @@ export default {
|
|
|
const sendMessage = async () => {
|
|
|
const query = inputMessage.value.trim()
|
|
|
if (!query || isLoading.value) return
|
|
|
+ const opEpoch = chatResetEpoch.value
|
|
|
|
|
|
disableWelcomePlaybackForSession()
|
|
|
stopAllTtsPlayback()
|
|
|
@@ -414,6 +418,9 @@ export default {
|
|
|
let timedOut = false
|
|
|
const timeoutMs = 30000
|
|
|
const timeoutId = setTimeout(() => {
|
|
|
+ if (opEpoch !== chatResetEpoch.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
timedOut = true
|
|
|
// 超时后停止界面 loading,且忽略后续 SSE 推送
|
|
|
if (isLoading.value) isLoading.value = false
|
|
|
@@ -422,6 +429,9 @@ export default {
|
|
|
|
|
|
try {
|
|
|
await chatApi.sendMessage(query, conversationId.value, (data) => {
|
|
|
+ if (opEpoch !== chatResetEpoch.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
if (timedOut) return
|
|
|
console.log('收到SSE消息:', data)
|
|
|
if (data.type === 'text') {
|
|
|
@@ -444,6 +454,9 @@ export default {
|
|
|
}
|
|
|
})
|
|
|
} catch (error) {
|
|
|
+ if (opEpoch !== chatResetEpoch.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
if (timedOut) return
|
|
|
console.error('发送消息失败:', error)
|
|
|
addMessage('ai', '抱歉,服务暂时不可用,请稍后重试。')
|
|
|
@@ -467,6 +480,7 @@ export default {
|
|
|
}
|
|
|
|
|
|
if (action.type === 'select-department') {
|
|
|
+ const opEpoch = chatResetEpoch.value
|
|
|
stopAllTtsPlayback()
|
|
|
isLoading.value = true
|
|
|
try {
|
|
|
@@ -474,6 +488,9 @@ export default {
|
|
|
conversationId.value,
|
|
|
action.departmentId,
|
|
|
(data) => {
|
|
|
+ if (opEpoch !== chatResetEpoch.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
if (data.type === 'text') {
|
|
|
addMessage('ai', data.content)
|
|
|
} else if (data.type === 'card') {
|
|
|
@@ -490,6 +507,7 @@ export default {
|
|
|
}
|
|
|
|
|
|
if (action.type === 'select-doctor') {
|
|
|
+ const opEpoch = chatResetEpoch.value
|
|
|
stopAllTtsPlayback()
|
|
|
isLoading.value = true
|
|
|
try {
|
|
|
@@ -498,6 +516,9 @@ export default {
|
|
|
action.doctorId,
|
|
|
action.date,
|
|
|
(data) => {
|
|
|
+ if (opEpoch !== chatResetEpoch.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
if (data.type === 'card_update') {
|
|
|
// 更新医生卡片数据
|
|
|
const lastCardMessage = messages.value
|
|
|
@@ -522,6 +543,7 @@ export default {
|
|
|
}
|
|
|
|
|
|
if (action.type === 'create-appointment') {
|
|
|
+ const opEpoch = chatResetEpoch.value
|
|
|
stopAllTtsPlayback()
|
|
|
isLoading.value = true
|
|
|
try {
|
|
|
@@ -535,6 +557,9 @@ export default {
|
|
|
conversationId: conversationId.value
|
|
|
},
|
|
|
(data) => {
|
|
|
+ if (opEpoch !== chatResetEpoch.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
if (data.type === 'text') {
|
|
|
addMessage('ai', data.content)
|
|
|
} else if (data.type === 'card') {
|
|
|
@@ -552,6 +577,7 @@ export default {
|
|
|
|
|
|
// 处理身份证拍摄
|
|
|
if (action.type === 'idcard-capture') {
|
|
|
+ const opEpoch = chatResetEpoch.value
|
|
|
const instanceId = message?.card?.instanceId
|
|
|
processingIdCardInstanceId.value = instanceId
|
|
|
stopAllTtsPlayback()
|
|
|
@@ -561,6 +587,9 @@ export default {
|
|
|
conversationId.value,
|
|
|
action.image,
|
|
|
(data) => {
|
|
|
+ if (opEpoch !== chatResetEpoch.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
if (data.type === 'text') {
|
|
|
addMessage('ai', data.content)
|
|
|
} else if (data.type === 'card') {
|
|
|
@@ -588,10 +617,14 @@ export default {
|
|
|
}
|
|
|
isLoading.value = true
|
|
|
try {
|
|
|
+ const opEpoch = chatResetEpoch.value
|
|
|
await chatApi.submitRecord(
|
|
|
conversationId.value,
|
|
|
action.patient,
|
|
|
(data) => {
|
|
|
+ if (opEpoch !== chatResetEpoch.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
if (data.type === 'text') {
|
|
|
const t = String(data.content || '').trim()
|
|
|
if (t) addMessage('ai', data.content)
|
|
|
@@ -619,6 +652,7 @@ export default {
|
|
|
|
|
|
// 处理报告拍摄
|
|
|
if (action.type === 'report-capture') {
|
|
|
+ const opEpoch = chatResetEpoch.value
|
|
|
const instanceId = message?.card?.instanceId
|
|
|
processingReportInstanceId.value = instanceId
|
|
|
stopAllTtsPlayback()
|
|
|
@@ -629,6 +663,9 @@ export default {
|
|
|
action.reportType,
|
|
|
action.image,
|
|
|
(data) => {
|
|
|
+ if (opEpoch !== chatResetEpoch.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
if (data.type === 'text') {
|
|
|
addMessage('ai', data.content)
|
|
|
} else if (data.type === 'card') {
|
|
|
@@ -642,9 +679,12 @@ export default {
|
|
|
} catch (error) {
|
|
|
console.error('报告解读失败:', error)
|
|
|
} finally {
|
|
|
- // SSE 被网关/服务端提前断开时可能收不到 message_end,避免界面永久卡在「解读中」
|
|
|
- processingReportInstanceId.value = null
|
|
|
- isLoading.value = false
|
|
|
+ // 软刷新后忽略旧请求的收尾,避免把新对话的 loading 清掉
|
|
|
+ if (opEpoch === chatResetEpoch.value) {
|
|
|
+ // SSE 被网关/服务端提前断开时可能收不到 message_end,避免界面永久卡在「解读中」
|
|
|
+ processingReportInstanceId.value = null
|
|
|
+ isLoading.value = false
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -672,6 +712,7 @@ export default {
|
|
|
stopAllTtsPlayback()
|
|
|
isLoading.value = true
|
|
|
try {
|
|
|
+ const opEpoch = chatResetEpoch.value
|
|
|
const tongueCaptureData = message?.card?.data || {}
|
|
|
const result = await chatApi.analyzeTongueDiagnosis({
|
|
|
imageBase64: action.image,
|
|
|
@@ -682,6 +723,9 @@ export default {
|
|
|
patientAge: tongueCaptureData.patientAge || 0,
|
|
|
patientGender: tongueCaptureData.patientGender || 0
|
|
|
})
|
|
|
+ if (opEpoch !== chatResetEpoch.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
const isRediagnosis = !!action.isRediagnosis
|
|
|
|
|
|
if (isRediagnosis) {
|
|
|
@@ -889,8 +933,12 @@ export default {
|
|
|
}
|
|
|
|
|
|
const uploadVoiceAudio = async (audioBlob, fileName) => {
|
|
|
+ const uEpoch = chatResetEpoch.value
|
|
|
try {
|
|
|
const text = await chatApi.uploadVoiceAudio(audioBlob, fileName)
|
|
|
+ if (uEpoch !== chatResetEpoch.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
const cleaned = (text || '').trim()
|
|
|
const ignoreText = '请不吝点赞 订阅 转发 打赏支持明镜与点点栏目'
|
|
|
const shouldIgnore = !cleaned || cleaned === ignoreText
|
|
|
@@ -975,11 +1023,6 @@ export default {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 顶部按钮:重新加载整个页面(重新打开当前路由)
|
|
|
- const refreshPage = () => {
|
|
|
- window.location.reload()
|
|
|
- }
|
|
|
-
|
|
|
const disableWelcomePlaybackForSession = () => {
|
|
|
stopAllTtsPlayback()
|
|
|
resetSequentialTts()
|
|
|
@@ -991,6 +1034,34 @@ export default {
|
|
|
sequentialEnqueueText(welcomeText.value)
|
|
|
}
|
|
|
|
|
|
+ // 顶部「刷新」:仅重置导诊对话与相关状态,不重载整页(避免 triageOpen 丢失回到假桌面)
|
|
|
+ const refreshPage = async () => {
|
|
|
+ chatResetEpoch.value++
|
|
|
+ stopAllTtsPlayback()
|
|
|
+ resetSequentialTts()
|
|
|
+ clearWelcomeTtsPlaybackFlag()
|
|
|
+ if (isVoiceRecording.value) {
|
|
|
+ stopVoiceRecording()
|
|
|
+ }
|
|
|
+ isVoiceUploading.value = false
|
|
|
+ androidFileRecorderActive = false
|
|
|
+ if (voiceRecordTimeout) {
|
|
|
+ clearTimeout(voiceRecordTimeout)
|
|
|
+ voiceRecordTimeout = null
|
|
|
+ }
|
|
|
+ messages.value = []
|
|
|
+ inputMessage.value = ''
|
|
|
+ isLoading.value = false
|
|
|
+ conversationId.value = ''
|
|
|
+ processingReportInstanceId.value = null
|
|
|
+ processingIdCardInstanceId.value = null
|
|
|
+ isInDepartmentFollowup.value = false
|
|
|
+ await applyBranding()
|
|
|
+ addMessage('ai', welcomeText.value, null, true)
|
|
|
+ playWelcomeTtsOnOpen()
|
|
|
+ scrollToBottom()
|
|
|
+ }
|
|
|
+
|
|
|
// 初始化欢迎消息、语音支持检测
|
|
|
onMounted(async () => {
|
|
|
clearWelcomeTtsPlaybackFlag()
|