feat: 联调对话功能

This commit is contained in:
王天骄
2026-06-09 17:00:23 +08:00
parent 3414d0662c
commit 2192b855a1
77 changed files with 1082 additions and 487 deletions
+64 -35
View File
@@ -1,17 +1,11 @@
<template>
<TreatmentPage
v-if="showTreatmentPage"
:case-item="caseItem"
@open-settings="emit('open-settings')"
@open-profile="openProfile"
@go-home="emit('go-home')"
/>
<view v-else class="diagnosis-page">
<view class="diagnosis-page">
<view class="diagnosis-shell">
<view class="top-nav">
<button class="icon-button" aria-label="设置" @click="emit('open-settings')">
<button class="icon-button" aria-label="设置" @click="openSettings">
<view class="settings-icon"></view>
</button>
<button class="icon-button home-button" aria-label="首页" @click="emit('go-home')">
<button class="icon-button home-button" aria-label="首页" @click="goHome">
<view class="home-icon"></view>
</button>
<view class="nav-spacer"></view>
@@ -132,13 +126,13 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
import type { ClinicalCase } from '../../api/cases'
import { readStoredClinicalCase, type ClinicalCase } from '../../api/cases'
import { fetchDiagnosisContext, submitDiagnosis, type DiagnosisDraft } from '../../api/diagnosis'
import { createProfileOpener } from '../../api/navigation'
import TreatmentPage from '../treatment/treatment.vue'
import { createHomeNavigator, createProfileOpener, createSettingsOpener } from '../../api/navigation'
import { readActiveSessionId } from '../../api/session'
const props = defineProps<{
caseItem: ClinicalCase | null
caseItem?: ClinicalCase | null
}>()
const emit = defineEmits<{
@@ -148,6 +142,8 @@ const emit = defineEmits<{
}>()
const openProfile = createProfileOpener(emit)
const openSettings = createSettingsOpener(emit)
const goHome = createHomeNavigator(emit)
type SubmitState = 'idle' | 'submitted'
@@ -162,15 +158,19 @@ const submitting = ref(false)
const submitState = ref<SubmitState>('idle')
const toastMessage = ref('')
const toastVisible = ref(false)
const showTreatmentPage = ref(false)
const storedCase = ref<ClinicalCase | null>(null)
let toastTimer: ReturnType<typeof setTimeout> | null = null
const patientName = computed(() => '陈先生')
const patientGender = computed(() => '男')
const patientAge = computed(() => 60)
const patientDepartment = computed(() => '心血管内科')
const complaintShort = computed(() => '胸痛')
const activeCase = computed(() => props.caseItem || storedCase.value)
const patientName = computed(() => activeCase.value?.patientName || '陈先生')
const patientGender = computed(() => activeCase.value?.gender || '男')
const patientAge = computed(() => activeCase.value?.age || 60)
const patientDepartment = computed(() => activeCase.value?.department || '心血管内科')
const complaintShort = computed(() => {
const title = activeCase.value?.title || '持续胸痛3小时'
return title.includes('胸痛') ? '胸痛' : title.slice(0, 6)
})
const buttonText = computed(() => {
if (submitting.value) return '提交中...'
@@ -179,7 +179,7 @@ const buttonText = computed(() => {
})
function loadDiagnosisContext() {
fetchDiagnosisContext(props.caseItem).then(result => {
fetchDiagnosisContext(activeCase.value).then(result => {
mentorAdvice.value = result.mentorAdvice
form.primaryDiagnosis = ''
form.differentialDiagnosis = ['', '']
@@ -187,36 +187,54 @@ function loadDiagnosisContext() {
})
}
function handleNext() {
async function handleNext() {
if (submitting.value) return
if (!form.primaryDiagnosis.trim()) {
showToast('请输入主要诊断')
return
}
if (!form.evidence.trim()) {
showToast('请输入诊断依据')
return
}
submitting.value = true
submitDiagnosis(props.caseItem?.id || 'mock-case', {
primaryDiagnosis: form.primaryDiagnosis,
differentialDiagnosis: form.differentialDiagnosis.filter(item => item.trim()),
evidence: form.evidence
}).then(result => {
try {
const sessionId = readActiveSessionId()
const result = await submitDiagnosis(sessionId, {
primaryDiagnosis: form.primaryDiagnosis,
differentialDiagnosis: form.differentialDiagnosis.filter(item => item.trim()),
evidence: form.evidence
})
uni.setStorageSync('clinical-thinking-diagnosis', result)
showTreatmentPage.value = true
}).finally(() => {
submitState.value = 'submitted'
uni.redirectTo({
url: '/pages/treatment/treatment',
fail() {
submitState.value = 'idle'
showToast('进入治疗计划失败,请重试')
}
})
} catch (error) {
showToast(error instanceof Error ? error.message : '诊断提交失败')
} finally {
submitting.value = false
})
}
}
function showToast(message: string) {
if (toastTimer) clearTimeout(toastTimer)
toastMessage.value = message
toastVisible.value = true
uni.showToast({
title: message,
icon: 'none'
})
toastTimer = setTimeout(() => {
toastVisible.value = false
}, 2200)
}
onMounted(loadDiagnosisContext)
onMounted(() => {
storedCase.value = readStoredClinicalCase()
loadDiagnosisContext()
})
onUnmounted(() => {
if (toastTimer) clearTimeout(toastTimer)
@@ -312,6 +330,7 @@ page {
border: 0;
}
.settings-icon,
.home-icon,
.account-icon,
.check-icon,
@@ -331,6 +350,13 @@ page {
height: 24px;
}
.settings-icon {
width: 22px;
height: 22px;
-webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M19.43%2012.98c.04-.32.07-.65.07-.98s-.02-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.37-.31-.6-.22l-2.49%201c-.52-.4-1.08-.73-1.69-.98L14.5%202.42C14.47%202.18%2014.25%202%2014%202h-4c-.25%200-.46.18-.5.42l-.38%202.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.08-.48%200-.6.22l-2%203.46c-.13.22-.07.49.12.64l2.11%201.65c-.04.32-.08.65-.08.98s.03.66.08.98l-2.11%201.65c-.19.15-.24.42-.12.64l2%203.46c.12.22.37.31.6.22l2.49-1c.52.4%201.08.73%201.69.98l.38%202.65c.04.24.25.42.5.42h4c.25%200%20.46-.18.5-.42l.38-2.65c.61-.25%201.17-.58%201.69-.98l2.49%201c.23.08.48%200%20.6-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12%2015.5A3.5%203.5%200%201%201%2012%208a3.5%203.5%200%200%201%200%207.5z'/%3E%3C/svg%3E") center / contain no-repeat;
mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M19.43%2012.98c.04-.32.07-.65.07-.98s-.02-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.37-.31-.6-.22l-2.49%201c-.52-.4-1.08-.73-1.69-.98L14.5%202.42C14.47%202.18%2014.25%202%2014%202h-4c-.25%200-.46.18-.5.42l-.38%202.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.08-.48%200-.6.22l-2%203.46c-.13.22-.07.49.12.64l2.11%201.65c-.04.32-.08.65-.08.98s.03.66.08.98l-2.11%201.65c-.19.15-.24.42-.12.64l2%203.46c.12.22.37.31.6.22l2.49-1c.52.4%201.08.73%201.69.98l.38%202.65c.04.24.25.42.5.42h4c.25%200%20.46-.18.5-.42l.38-2.65c.61-.25%201.17-.58%201.69-.98l2.49%201c.23.08.48%200%20.6-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12%2015.5A3.5%203.5%200%201%201%2012%208a3.5%203.5%200%200%201%200%207.5z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.home-icon {
-webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M10%2020v-6h4v6h5v-8h3L12%203%202%2012h3v8h5z'/%3E%3C/svg%3E") center / contain no-repeat;
mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M10%2020v-6h4v6h5v-8h3L12%203%202%2012h3v8h5z'/%3E%3C/svg%3E") center / contain no-repeat;
@@ -378,6 +404,9 @@ page {
.stepper {
position: relative;
z-index: 1;
flex: 0 0 auto;
min-height: 56px;
padding: 8px 0;
display: flex;
align-items: center;