Files
vueapp/pages/diagnosis/diagnosis.vue
T
2026-06-05 15:27:29 +08:00

714 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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-shell">
<view class="top-nav">
<button class="icon-button" aria-label="设置" @click="emit('open-settings')">
</button>
<button class="icon-button home-button" aria-label="首页" @click="emit('go-home')">
<view class="home-icon"></view>
</button>
<view class="nav-spacer"></view>
<button class="icon-button" aria-label="个人中心" @click="openProfile">
<view class="account-icon"></view>
</button>
</view>
<view class="case-header">
<text class="case-heading">患者{{ patientName }} ({{ complaintShort }})</text>
<view class="patient-meta">
<text>姓名{{ patientName }}</text>
<text>性别{{ patientGender }}</text>
<text>年龄{{ patientAge }}</text>
<text>科室{{ patientDepartment }}</text>
</view>
</view>
<view class="diagnosis-content">
<view class="stepper">
<view class="step-line">
<view class="step-line-active"></view>
</view>
<view class="step done">
<view class="step-dot">
<view class="check-icon"></view>
</view>
<text>问诊</text>
</view>
<view class="step active">
<view class="step-dot">
<view class="stethoscope-icon"></view>
</view>
<text>临床诊断</text>
</view>
<view class="step">
<view class="step-dot">
<view class="pill-icon"></view>
</view>
<text>治疗计划</text>
</view>
</view>
<view class="mentor-card">
<view class="mentor-avatar">
<image src="/static/config-doctor.png" mode="aspectFill"></image>
</view>
<view class="mentor-bubble">
<text>{{ mentorAdvice }}</text>
</view>
</view>
<view class="form-area">
<view class="field-block">
<view class="field-label primary">
<view class="priority-icon"></view>
<text>主要诊断</text>
</view>
<view class="input-wrap">
<input
class="diagnosis-input"
v-model="form.primaryDiagnosis"
type="text"
placeholder="请输入初步诊断..."
placeholder-class="input-placeholder"
/>
</view>
</view>
<view class="field-block">
<view class="field-label">
<view class="checklist-icon"></view>
<text>鉴别诊断</text>
</view>
<view class="diff-list">
<view
v-for="(_, index) in form.differentialDiagnosis"
:key="index"
class="diff-row"
>
<text class="diff-index">{{ index + 1 }}</text>
<input
class="diff-input"
v-model="form.differentialDiagnosis[index]"
type="text"
:placeholder="`备选诊断 ${index + 1}`"
placeholder-class="input-placeholder"
/>
</view>
</view>
</view>
<view class="field-block">
<view class="field-label">
<view class="description-icon"></view>
<text>诊断依据</text>
</view>
<textarea
class="evidence-input"
v-model="form.evidence"
placeholder="请简述诊断依据,如:患者高龄、剧烈撕裂样胸痛、血压双侧不对称等..."
placeholder-class="input-placeholder"
></textarea>
</view>
</view>
<button class="next-button" :class="{ submitted: submitState === 'submitted' }" :disabled="submitting" @click="handleNext">
<view v-if="submitting" class="spinner"></view>
<text>{{ buttonText }}</text>
<view v-if="!submitting && submitState !== 'submitted'" class="arrow-icon"></view>
<view v-if="submitState === 'submitted'" class="check-small-icon"></view>
</button>
</view>
</view>
<view class="toast" :class="{ visible: toastVisible }">{{ toastMessage }}</view>
</view>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
import type { ClinicalCase } from '../../api/cases'
import { fetchDiagnosisContext, submitDiagnosis, type DiagnosisDraft } from '../../api/diagnosis'
import { createProfileOpener } from '../../api/navigation'
import TreatmentPage from '../treatment/treatment.vue'
const props = defineProps<{
caseItem: ClinicalCase | null
}>()
const emit = defineEmits<{
(event: 'open-settings'): void
(event: 'open-profile'): void
(event: 'go-home'): void
}>()
const openProfile = createProfileOpener(emit)
type SubmitState = 'idle' | 'submitted'
const form = reactive<DiagnosisDraft>({
primaryDiagnosis: '',
differentialDiagnosis: ['', ''],
evidence: ''
})
const mentorAdvice = ref('王主任建议:请结合患者主诉和问诊信息,完成主要诊断、鉴别诊断和诊断依据。')
const submitting = ref(false)
const submitState = ref<SubmitState>('idle')
const toastMessage = ref('')
const toastVisible = ref(false)
const showTreatmentPage = ref(false)
let toastTimer: ReturnType<typeof setTimeout> | null = null
const patientName = computed(() => '陈先生')
const patientGender = computed(() => '男')
const patientAge = computed(() => 60)
const patientDepartment = computed(() => '心血管内科')
const complaintShort = computed(() => '胸痛')
const buttonText = computed(() => {
if (submitting.value) return '提交中...'
if (submitState.value === 'submitted') return '已提交'
return '下一步'
})
function loadDiagnosisContext() {
fetchDiagnosisContext(props.caseItem).then(result => {
mentorAdvice.value = result.mentorAdvice
form.primaryDiagnosis = ''
form.differentialDiagnosis = ['', '']
form.evidence = ''
})
}
function handleNext() {
if (submitting.value) 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 => {
uni.setStorageSync('clinical-thinking-diagnosis', result)
showTreatmentPage.value = true
}).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)
onUnmounted(() => {
if (toastTimer) clearTimeout(toastTimer)
})
</script>
<style scoped>
page {
min-height: 100%;
background: #f9f9ff;
}
.diagnosis-page {
width: 390px;
max-width: 100vw;
height: 884px;
min-height: 884px;
margin: 0 auto;
overflow-x: hidden;
background: #f9f9ff;
color: #191c21;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
-webkit-tap-highlight-color: transparent;
}
.diagnosis-page view,
.diagnosis-page text,
.diagnosis-page button,
.diagnosis-page input,
.diagnosis-page textarea,
.diagnosis-page scroll-view {
box-sizing: border-box;
}
.diagnosis-shell {
position: relative;
width: 390px;
max-width: 100vw;
height: 884px;
min-height: 884px;
background: #f9f9ff;
overflow-x: hidden;
}
.top-nav {
position: fixed;
left: 0;
right: 0;
top: 0;
transform: none;
z-index: 50;
box-sizing: border-box;
width: 100%;
max-width: 100vw;
height: 56px;
padding: 0 20px;
border-bottom: 1px solid rgba(194, 198, 212, 0.3);
background: #ffffff;
box-shadow: 0 2px 8px rgba(25, 28, 33, 0.04);
display: flex;
align-items: center;
}
.nav-spacer {
flex: 1;
}
.icon-button {
width: 40px;
height: 40px;
min-height: 40px;
margin: 0;
padding: 0;
border: 0;
border-radius: 50%;
background: transparent;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
.home-button {
margin-left: 4px;
}
.top-nav > .icon-button:first-child {
display: flex;
}
.icon-button::after,
.next-button::after {
border: 0;
}
.home-icon,
.account-icon,
.check-icon,
.stethoscope-icon,
.pill-icon,
.priority-icon,
.checklist-icon,
.description-icon,
.arrow-icon,
.check-small-icon {
background: #424752;
}
.home-icon,
.account-icon {
width: 24px;
height: 24px;
}
.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;
}
.account-icon {
-webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M12%202a10%2010%200%201%200%200%2020%2010%2010%200%200%200%200-20zm0%203a3.5%203.5%200%201%201%200%207%203.5%203.5%200%200%201%200-7zm0%2015a8%208%200%200%201-6.4-3.2c1.18-2.02%203.57-3.3%206.4-3.3s5.22%201.28%206.4%203.3A8%208%200%200%201%2012%2020z'/%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='M12%202a10%2010%200%201%200%200%2020%2010%2010%200%200%200%200-20zm0%203a3.5%203.5%200%201%201%200%207%203.5%203.5%200%200%201%200-7zm0%2015a8%208%200%200%201-6.4-3.2c1.18-2.02%203.57-3.3%206.4-3.3s5.22%201.28%206.4%203.3A8%208%200%200%201%2012%2020z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.case-header {
margin-top: 56px;
padding: 16px 20px;
background: #f9f9ff;
box-shadow: 0 2px 8px rgba(25, 28, 33, 0.04);
}
.case-heading {
color: #191c21;
font-size: 20px;
line-height: 28px;
font-weight: 600;
}
.patient-meta {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid rgba(194, 198, 212, 0.3);
display: flex;
flex-wrap: wrap;
column-gap: 24px;
row-gap: 4px;
color: #424752;
font-size: 13px;
line-height: 20px;
}
.diagnosis-content {
box-sizing: border-box;
padding: 24px 20px 32px;
display: flex;
flex-direction: column;
gap: 24px;
}
.stepper {
position: relative;
padding: 8px 0;
display: flex;
align-items: center;
justify-content: space-between;
}
.step-line {
position: absolute;
left: 40px;
right: 40px;
top: 20px;
height: 4px;
background: #e1e2ea;
z-index: 0;
}
.step-line-active {
width: 50%;
height: 100%;
background: #00478d;
}
.step {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
color: #727783;
font-size: 12px;
line-height: 16px;
font-weight: 500;
}
.step.active {
color: #00478d;
font-weight: 700;
}
.step.done {
color: #006970;
}
.step-dot {
width: 32px;
height: 32px;
border-radius: 50%;
background: #e1e2ea;
display: flex;
align-items: center;
justify-content: center;
}
.step.active .step-dot {
width: 40px;
height: 40px;
border: 4px solid #d6e3ff;
border-radius: 50%;
background: #00478d;
box-shadow: 0 0 0 2px #00478d;
}
.step.done .step-dot {
background: #006970;
}
.check-icon,
.stethoscope-icon,
.pill-icon {
width: 18px;
height: 18px;
background: currentColor;
}
.step.done .check-icon,
.step.active .stethoscope-icon {
background: #ffffff;
}
.check-icon {
-webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M9%2016.2%204.8%2012l-1.4%201.4L9%2019%2021%207l-1.4-1.4L9%2016.2z'/%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='M9%2016.2%204.8%2012l-1.4%201.4L9%2019%2021%207l-1.4-1.4L9%2016.2z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.stethoscope-icon {
width: 20px;
height: 20px;
-webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M19%208h-1V4h-3v2h1v5a4%204%200%200%201-8%200V6h1V4H6v4H5a1%201%200%200%200%200%202h1v1a6%206%200%200%200%205%205.92V19a3%203%200%200%200%206%200v-1.1A5%205%200%200%200%2019%208zm-5%2012a1%201%200%200%201-1-1v-2.08a6%206%200%200%200%203-1.2V19a1%201%200%200%201-1%201h-1zm5-4a3%203%200%201%201%200-6%203%203%200%200%201%200%206z'/%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%208h-1V4h-3v2h1v5a4%204%200%200%201-8%200V6h1V4H6v4H5a1%201%200%200%200%200%202h1v1a6%206%200%200%200%205%205.92V19a3%203%200%200%200%206%200v-1.1A5%205%200%200%200%2019%208zm-5%2012a1%201%200%200%201-1-1v-2.08a6%206%200%200%200%203-1.2V19a1%201%200%200%201-1%201h-1zm5-4a3%203%200%201%201%200-6%203%203%200%200%201%200%206z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.pill-icon {
-webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='m4.22%2019.78-.01-.01a5.5%205.5%200%200%201%200-7.78L12%204.22a5.5%205.5%200%200%201%207.78%207.78L12%2019.78a5.5%205.5%200%200%201-7.78%200zM13.41%205.64%2010%209.05%2014.95%2014%2018.36%2010.59a3.5%203.5%200%200%200-4.95-4.95zM5.64%2013.41a3.5%203.5%200%200%200%204.95%204.95L14%2014.95%209.05%2010%205.64%2013.41z'/%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='m4.22%2019.78-.01-.01a5.5%205.5%200%200%201%200-7.78L12%204.22a5.5%205.5%200%200%201%207.78%207.78L12%2019.78a5.5%205.5%200%200%201-7.78%200zM13.41%205.64%2010%209.05%2014.95%2014%2018.36%2010.59a3.5%203.5%200%200%200-4.95-4.95zM5.64%2013.41a3.5%203.5%200%200%200%204.95%204.95L14%2014.95%209.05%2010%205.64%2013.41z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.mentor-card {
padding: 12px;
border: 1px solid rgba(0, 71, 141, 0.1);
border-radius: 12px;
background: rgba(0, 71, 141, 0.05);
display: flex;
align-items: flex-start;
gap: 12px;
}
.mentor-avatar {
flex: 0 0 auto;
width: 48px;
height: 48px;
border: 1px solid rgba(0, 71, 141, 0.2);
border-radius: 50%;
overflow: hidden;
}
.mentor-avatar image {
width: 100%;
height: 100%;
}
.mentor-bubble {
flex: 1;
padding: 12px;
border: 1px solid rgba(194, 198, 212, 0.3);
border-radius: 8px;
border-top-left-radius: 0;
background: #ffffff;
box-shadow: 0 2px 8px rgba(25, 28, 33, 0.04);
color: #191c21;
font-size: 14px;
line-height: 20px;
}
.form-area {
display: flex;
flex-direction: column;
gap: 20px;
}
.field-block {
display: flex;
flex-direction: column;
gap: 8px;
}
.field-label {
display: flex;
align-items: center;
gap: 4px;
color: #424752;
font-size: 14px;
line-height: 20px;
font-weight: 600;
}
.field-label.primary {
color: #00478d;
}
.priority-icon,
.checklist-icon,
.description-icon {
width: 16px;
height: 16px;
background: currentColor;
}
.priority-icon {
-webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M11%2018h2v-2h-2v2zm0-4h2V6h-2v8zm1%208a10%2010%200%201%201%200-20%2010%2010%200%200%201%200%2020z'/%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='M11%2018h2v-2h-2v2zm0-4h2V6h-2v8zm1%208a10%2010%200%201%201%200-20%2010%2010%200%200%201%200%2020z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.checklist-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%2017h10v-2H10v2zm0-4h10v-2H10v2zm0-6v2h10V7H10zM4%207h4v4H4V7zm1%201v2h2V8H5zm-1%205h4v4H4v-4zm1%201v2h2v-2H5z'/%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%2017h10v-2H10v2zm0-4h10v-2H10v2zm0-6v2h10V7H10zM4%207h4v4H4V7zm1%201v2h2V8H5zm-1%205h4v4H4v-4zm1%201v2h2v-2H5z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.description-icon {
-webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M14%202H6a2%202%200%200%200-2%202v16a2%202%200%200%200%202%202h12a2%202%200%200%200%202-2V8l-6-6zm-1%207V3.5L18.5%209H13zm-5%204h8v2H8v-2zm0%204h8v2H8v-2z'/%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='M14%202H6a2%202%200%200%200-2%202v16a2%202%200%200%200%202%202h12a2%202%200%200%200%202-2V8l-6-6zm-1%207V3.5L18.5%209H13zm-5%204h8v2H8v-2zm0%204h8v2H8v-2z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.diagnosis-input,
.diff-input,
.evidence-input {
box-sizing: border-box;
width: 100%;
border: 1px solid #c2c6d4;
border-radius: 8px;
background: #ffffff;
color: #191c21;
font-size: 16px;
line-height: 24px;
}
.input-wrap {
position: relative;
}
.diagnosis-input {
height: 56px;
padding: 0 16px;
}
.diff-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.diff-row {
display: flex;
align-items: center;
gap: 8px;
}
.diff-index {
flex: 0 0 auto;
width: 24px;
height: 24px;
border-radius: 4px;
background: #e7e8f0;
color: #424752;
font-size: 12px;
line-height: 24px;
font-weight: 600;
text-align: center;
}
.diff-input {
flex: 1;
height: 40px;
padding: 0 12px;
font-size: 14px;
line-height: 20px;
}
.evidence-input {
height: 116px;
padding: 14px 16px;
font-size: 14px;
line-height: 20px;
}
.input-placeholder {
color: #c2c6d4;
}
.next-button {
width: 100%;
min-height: 56px;
margin-top: 8px;
padding: 0;
border: 0;
border-radius: 999px;
background: #00478d;
box-shadow: 0 8px 18px rgba(0, 71, 141, 0.22);
color: #ffffff;
font-size: 16px;
line-height: 24px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.next-button.submitted {
background: #006970;
}
.arrow-icon,
.check-small-icon {
width: 20px;
height: 20px;
background: #ffffff;
}
.arrow-icon {
-webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='m12%204-1.41%201.41L16.17%2011H4v2h12.17l-5.58%205.59L12%2020l8-8-8-8z'/%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='m12%204-1.41%201.41L16.17%2011H4v2h12.17l-5.58%205.59L12%2020l8-8-8-8z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.check-small-icon {
-webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M9%2016.2 4.8%2012l-1.4%201.4L9%2019 21%207l-1.4-1.4L9%2016.2z'/%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='M9%2016.2 4.8%2012l-1.4%201.4L9%2019 21%207l-1.4-1.4L9%2016.2z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.36);
border-top-color: #ffffff;
border-radius: 50%;
box-sizing: border-box;
animation: spin 1s linear infinite;
}
.toast {
position: fixed;
left: 50%;
bottom: 96px;
z-index: 100;
max-width: 320px;
padding: 12px 24px;
border-radius: 999px;
background: #2e3037;
color: #eff0f8;
font-size: 14px;
line-height: 20px;
font-weight: 600;
text-align: center;
pointer-events: none;
opacity: 0;
transform: translate(-50%, 16px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.toast.visible {
opacity: 1;
transform: translate(-50%, 0);
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>