Files
vueapp/pages/chat/chat.vue
T
2026-06-03 16:01:03 +08:00

1295 lines
34 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>
<DiagnosisPage
v-if="showDiagnosisPage"
:case-item="caseItem"
@open-settings="emit('open-settings')"
@open-profile="emit('open-profile')"
@go-home="emit('go-home')"
/>
<view v-else class="chat-page">
<view class="chat-shell">
<view class="top-nav">
<button class="icon-button" aria-label="设置" @click="emit('open-settings')">
<view class="settings-icon"></view>
</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="emit('open-profile')">
<view class="account-icon"></view>
</button>
</view>
<view class="case-header">
<view class="case-title-row">
<text class="case-heading">患者{{ session.patient.name }} ({{ complaintShort }})</text>
<button class="finish-button" @click="showDiagnosisPage = true">
<view class="check-icon"></view>
<text>完成采集</text>
</button>
</view>
<view class="patient-meta">
<text>姓名{{ session.patient.name }}</text>
<text>性别{{ session.patient.gender }}</text>
<text>年龄{{ session.patient.age }}</text>
<text>科室{{ session.patient.department }}</text>
</view>
</view>
<view class="stage-bar">
<view class="stage-line"></view>
<view class="stage-line-active"></view>
<view
v-for="stage in session.stages"
:key="stage.key"
class="stage-item"
:class="{ active: stage.active }"
>
<view class="stage-dot">
<view class="stage-icon" :class="`stage-icon-${stage.key}`"></view>
</view>
<text>{{ stage.label }}</text>
</view>
</view>
<scroll-view class="chat-body" scroll-y :scroll-top="scrollTop">
<view class="message-list">
<view
v-for="message in session.messages"
:key="message.id"
class="message-row"
:class="[`role-${message.role}`]"
>
<view v-if="message.role === 'patient'" class="avatar patient-avatar">
<text>{{ session.patient.name.slice(0, 1) }}</text>
</view>
<view class="message-bubble" :class="`${message.role}-bubble`">
<text class="message-content">"{{ message.content }}"</text>
<view class="message-label" :class="{ mentor: message.role === 'mentor' }">
<view v-if="message.role === 'mentor'" class="star-icon"></view>
<text>{{ message.label }}</text>
</view>
</view>
<view v-if="message.role === 'doctor'" class="avatar doctor-avatar">
<view class="person-icon"></view>
</view>
</view>
</view>
</scroll-view>
<view class="mentor-float">
<text class="mentor-badge">王主任</text>
<view class="mentor-avatar">
<image src="/static/config-doctor.png" mode="aspectFill"></image>
</view>
</view>
<view class="input-panel">
<scroll-view class="quick-actions" scroll-x>
<view class="quick-row">
<button class="quick-chip" @click="openPhysicalPanel">体格检查</button>
<button class="quick-chip" @click="openExamPanel">辅助检查</button>
</view>
</scroll-view>
<view class="input-row">
<input
class="chat-input"
v-model="draft"
type="text"
placeholder="输入你对患者的提问..."
placeholder-class="input-placeholder"
@confirm="handleSend"
/>
<button class="send-button" :disabled="sending" @click="handleSend">
<view class="send-icon"></view>
</button>
</view>
</view>
</view>
<view v-if="physicalPanelVisible" class="exam-mask" @click="physicalPanelVisible = false">
<view class="physical-panel" @click.stop>
<view class="exam-header">
<text class="exam-title">录入体格检查</text>
<button class="exam-close" aria-label="关闭" @click="physicalPanelVisible = false">
<view class="close-icon"></view>
</button>
</view>
<scroll-view class="physical-form" scroll-y>
<view class="vital-grid">
<view class="form-field">
<text class="field-label">体温</text>
<input class="field-input" v-model="physicalForm.temperature" type="digit" placeholder="36.8" placeholder-class="field-placeholder" />
<text class="field-unit"></text>
</view>
<view class="form-field">
<text class="field-label">心率</text>
<input class="field-input" v-model="physicalForm.pulse" type="number" placeholder="98" placeholder-class="field-placeholder" />
<text class="field-unit">/</text>
</view>
<view class="form-field">
<text class="field-label">呼吸</text>
<input class="field-input" v-model="physicalForm.respiration" type="number" placeholder="22" placeholder-class="field-placeholder" />
<text class="field-unit">/</text>
</view>
<view class="form-field">
<text class="field-label">血氧</text>
<input class="field-input" v-model="physicalForm.spo2" type="number" placeholder="96" placeholder-class="field-placeholder" />
<text class="field-unit">%</text>
</view>
</view>
<view class="form-field full">
<text class="field-label">血压</text>
<input class="field-input" v-model="physicalForm.bloodPressure" type="text" placeholder="138/86" placeholder-class="field-placeholder" />
<text class="field-unit">mmHg</text>
</view>
<view class="form-field full">
<text class="field-label">意识/面色</text>
<input class="field-input" v-model="physicalForm.complexion" type="text" placeholder="清醒,面色苍白,出汗" placeholder-class="field-placeholder" />
</view>
<view class="form-field textarea-field">
<text class="field-label">心肺/腹部查体</text>
<textarea class="field-textarea" v-model="physicalForm.examFinding" placeholder="心音、肺部啰音、腹部压痛等" placeholder-class="field-placeholder"></textarea>
</view>
<view class="form-field textarea-field">
<text class="field-label">其他发现</text>
<textarea class="field-textarea" v-model="physicalForm.otherFinding" placeholder="如双侧血压差、下肢水肿、神经系统体征等" placeholder-class="field-placeholder"></textarea>
</view>
</scroll-view>
<view class="physical-actions">
<button class="physical-cancel" @click="physicalPanelVisible = false">取消</button>
<button class="physical-submit" @click="submitPhysicalExam">提交检查结果</button>
</view>
</view>
</view>
<view v-if="examPanelVisible" class="exam-mask" @click="examPanelVisible = false">
<view class="exam-panel" @click.stop>
<view class="exam-header">
<text class="exam-title">选择辅助检查</text>
<button class="exam-close" aria-label="关闭" @click="examPanelVisible = false">
<view class="close-icon"></view>
</button>
</view>
<view class="exam-list">
<button
v-for="exam in auxiliaryExams"
:key="exam.name"
class="exam-item"
@click="selectAuxiliaryExam(exam)"
>
<text class="exam-name">{{ exam.name }}</text>
<view class="chevron-icon"></view>
</button>
</view>
</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 { createMockChatSession, sendMockChatMessage, type ChatMessage, type ChatSession } from '../../api/chat'
import DiagnosisPage from '../diagnosis/diagnosis.vue'
const props = defineProps<{
caseItem: ClinicalCase | null
}>()
const emit = defineEmits<{
(event: 'open-settings'): void
(event: 'open-profile'): void
(event: 'go-home'): void
}>()
const session = reactive<ChatSession>({
patient: {
name: '陈先生',
gender: '男',
age: 60,
department: '心血管内科',
chiefComplaint: '持续胸痛3小时'
},
stages: [
{ key: 'history', label: '病史采集', active: true },
{ key: 'diagnosis', label: '初步诊断', active: false },
{ key: 'treatment', label: '治疗方案', active: false }
],
messages: []
})
const draft = ref('')
const sending = 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)
let toastTimer: ReturnType<typeof setTimeout> | null = null
type AuxiliaryExam = {
name: string
result: string
}
const auxiliaryExams: AuxiliaryExam[] = [
{
name: '心电图',
result: '检查结果:床边12导联心电图提示窦性心律,II、III、aVF 导联 ST 段抬高,提示下壁急性心肌梗死可能。'
},
{
name: '胸部X线',
result: '检查结果:胸部X线未见明显气胸或纵隔明显增宽,心影大小基本正常,不能排除急性冠脉综合征。'
},
{
name: '心脏超声',
result: '检查结果:心脏超声提示左室下壁节段性运动减低,未见大量心包积液,需结合心电图及心肌标志物判断。'
},
{
name: '冠脉CTA',
result: '检查结果:冠脉CTA提示右冠状动脉近段重度狭窄/闭塞可能,建议结合急诊介入评估。'
}
]
const physicalForm = reactive({
temperature: '',
pulse: '',
respiration: '',
bloodPressure: '',
spo2: '',
complexion: '',
examFinding: '',
otherFinding: ''
})
const complaintShort = computed(() => {
if (session.patient.chiefComplaint.includes('胸痛')) return '胸痛'
return session.patient.chiefComplaint.slice(0, 6)
})
function loadSession() {
createMockChatSession(props.caseItem).then(result => {
Object.assign(session.patient, result.patient)
session.stages = result.stages
session.messages = result.messages
scrollToBottom()
})
}
function sendQuickAction(content: string) {
draft.value = content
handleSend()
}
function openExamPanel() {
physicalPanelVisible.value = false
examPanelVisible.value = true
}
function openPhysicalPanel() {
examPanelVisible.value = false
physicalPanelVisible.value = true
}
function selectAuxiliaryExam(exam: AuxiliaryExam) {
examPanelVisible.value = false
const timestamp = Date.now()
const messages: ChatMessage[] = [
{
id: `doctor-exam-${timestamp}`,
role: 'doctor',
content: `选择辅助检查:${exam.name}`,
label: '我'
},
{
id: `mentor-exam-${timestamp + 1}`,
role: 'mentor',
content: exam.result,
label: 'AI助手'
}
]
session.messages.push(...messages)
scrollToBottom()
}
function submitPhysicalExam() {
const items = [
physicalForm.temperature.trim() ? `体温 ${physicalForm.temperature.trim()}` : '',
physicalForm.pulse.trim() ? `心率 ${physicalForm.pulse.trim()}次/分` : '',
physicalForm.respiration.trim() ? `呼吸 ${physicalForm.respiration.trim()}次/分` : '',
physicalForm.bloodPressure.trim() ? `血压 ${physicalForm.bloodPressure.trim()}mmHg` : '',
physicalForm.spo2.trim() ? `血氧 ${physicalForm.spo2.trim()}%` : '',
physicalForm.complexion.trim() ? `意识/面色:${physicalForm.complexion.trim()}` : '',
physicalForm.examFinding.trim() ? `心肺/腹部查体:${physicalForm.examFinding.trim()}` : '',
physicalForm.otherFinding.trim() ? `其他发现:${physicalForm.otherFinding.trim()}` : ''
].filter(Boolean)
if (items.length === 0) {
showToast('请至少录入一项体格检查')
return
}
const timestamp = Date.now()
const summary = items.join('')
const messages: ChatMessage[] = [
{
id: `doctor-physical-${timestamp}`,
role: 'doctor',
content: `录入体格检查:${summary}`,
label: '我'
},
{
id: `mentor-physical-${timestamp + 1}`,
role: 'mentor',
content: buildPhysicalFeedback(),
label: 'AI助手'
}
]
session.messages.push(...messages)
physicalPanelVisible.value = false
resetPhysicalForm()
scrollToBottom()
}
function resetPhysicalForm() {
physicalForm.temperature = ''
physicalForm.pulse = ''
physicalForm.respiration = ''
physicalForm.bloodPressure = ''
physicalForm.spo2 = ''
physicalForm.complexion = ''
physicalForm.examFinding = ''
physicalForm.otherFinding = ''
}
function buildPhysicalFeedback() {
const pulse = Number(physicalForm.pulse)
const respiration = Number(physicalForm.respiration)
const spo2 = Number(physicalForm.spo2)
const temperature = Number(physicalForm.temperature)
const tips: string[] = []
if (pulse >= 100) tips.push('心率偏快')
if (respiration >= 22) tips.push('呼吸频率偏快')
if (spo2 > 0 && spo2 < 95) tips.push('血氧偏低')
if (temperature >= 37.3) tips.push('体温偏高')
if (physicalForm.complexion.includes('苍白') || physicalForm.complexion.includes('出汗')) tips.push('面色/出汗提示急性病容')
if (physicalForm.otherFinding.includes('血压差') || physicalForm.otherFinding.includes('双侧')) tips.push('双侧血压或脉搏差异需警惕主动脉夹层')
const prefix = tips.length ? `已记录体格检查。当前提示:${tips.join('、')}` : '已记录体格检查,暂未见明确异常体征。'
return `${prefix}建议结合胸痛性质、心电图及心肌标志物进一步判断,并持续监测生命体征变化。`
}
function handleSend() {
const content = draft.value.trim()
if (!content || sending.value) return
sending.value = true
draft.value = ''
sendMockChatMessage(content).then(messages => {
session.messages.push(...messages)
scrollToBottom()
}).finally(() => {
sending.value = false
})
}
function scrollToBottom() {
setTimeout(() => {
scrollTop.value += 1000
}, 60)
}
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(loadSession)
onUnmounted(() => {
if (toastTimer) clearTimeout(toastTimer)
})
</script>
<style scoped>
page {
min-height: 100%;
background: #f9f9ff;
}
.chat-page {
min-height: 100vh;
background: #f9f9ff;
color: #191c21;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
-webkit-tap-highlight-color: transparent;
}
.chat-page view,
.chat-page text,
.chat-page button,
.chat-page input,
.chat-page scroll-view {
box-sizing: border-box;
}
.chat-shell {
position: relative;
height: 100vh;
overflow: hidden;
background: #f9f9ff;
display: flex;
flex-direction: column;
}
.top-nav {
position: fixed;
left: 0;
right: 0;
top: 0;
z-index: 50;
box-sizing: border-box;
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;
padding: 0;
border-radius: 50%;
background: transparent;
display: flex;
align-items: center;
justify-content: center;
}
.home-button {
margin-left: 4px;
}
.icon-button::after,
.finish-button::after,
.quick-chip::after,
.send-button::after {
border: 0;
}
.icon-button:active {
background: rgba(25, 28, 33, 0.05);
}
.settings-icon,
.home-icon,
.account-icon,
.check-icon,
.person-icon,
.star-icon,
.send-icon {
background: #424752;
}
.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 {
width: 23px;
height: 23px;
-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 {
width: 24px;
height: 24px;
-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 {
position: fixed;
left: 0;
right: 0;
top: 56px;
z-index: 40;
box-sizing: border-box;
height: 100px;
padding: 16px 20px;
background: #f9f9ff;
box-shadow: 0 2px 8px rgba(25, 28, 33, 0.04);
}
.case-title-row {
position: relative;
height: 32px;
display: block;
}
.case-heading {
display: block;
box-sizing: border-box;
width: 100%;
padding-right: 116px;
color: #191c21;
font-size: 18px;
line-height: 28px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.finish-button {
position: absolute;
right: 0;
top: 0;
z-index: 2;
width: 112px;
height: 32px;
min-height: 32px;
margin: 0;
padding: 0 14px;
border: 0;
border-radius: 999px;
background: #00478d;
box-shadow: 0 4px 10px rgba(0, 71, 141, 0.22);
color: #ffffff;
font-size: 14px;
line-height: 20px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.finish-button text {
white-space: nowrap;
}
.check-icon {
width: 18px;
height: 18px;
background: #ffffff;
-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-20zm-1.2%2014.4-4-4L8.2%2011l2.6%202.6L16.8%207.6%2018.2%209l-7.4%207.4z'/%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-20zm-1.2%2014.4-4-4L8.2%2011l2.6%202.6L16.8%207.6%2018.2%209l-7.4%207.4z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.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;
}
.stage-bar {
position: fixed;
left: 0;
right: 0;
top: 156px;
z-index: 30;
box-sizing: border-box;
height: 72px;
padding: 8px 24px;
border-bottom: 1px solid rgba(194, 198, 212, 0.3);
background: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
}
.stage-line,
.stage-line-active {
position: absolute;
left: 24px;
right: 24px;
top: 28px;
height: 2px;
background: #e1e2ea;
}
.stage-line-active {
right: auto;
width: 10%;
background: #00478d;
}
.stage-item {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
color: #c2c6d4;
font-size: 11px;
line-height: 16px;
}
.stage-item.active {
color: #00478d;
font-weight: 700;
}
.stage-dot {
width: 24px;
height: 24px;
border-radius: 50%;
background: #e1e2ea;
display: flex;
align-items: center;
justify-content: center;
}
.stage-item.active .stage-dot {
background: #00478d;
box-shadow: 0 0 0 4px rgba(200, 218, 255, 0.5);
}
.stage-icon {
width: 14px;
height: 14px;
background: currentColor;
}
.stage-icon-history {
-webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M19%203H5a2%202%200%200%200-2%202v14a2%202%200%200%200%202%202h14a2%202%200%200%200%202-2V5a2%202%200%200%200-2-2zm-7%203a3%203%200%201%201%200%206%203%203%200%200%201%200-6zm6%2012H6v-1.2c0-2%204-3.1%206-3.1s6%201.1%206%203.1V18z'/%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%203H5a2%202%200%200%200-2%202v14a2%202%200%200%200%202%202h14a2%202%200%200%200%202-2V5a2%202%200%200%200-2-2zm-7%203a3%203%200%201%201%200%206%203%203%200%200%201%200-6zm6%2012H6v-1.2c0-2%204-3.1%206-3.1s6%201.1%206%203.1V18z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.stage-icon-diagnosis {
-webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M19%203h-1V1h-2v2H8V1H6v2H5a2%202%200%200%200-2%202v14a2%202%200%200%200%202%202h14a2%202%200%200%200%202-2V5a2%202%200%200%200-2-2zM8%207h8v2H8V7zm0%204h8v2H8v-2zm0%204h5v2H8v-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='M19%203h-1V1h-2v2H8V1H6v2H5a2%202%200%200%200-2%202v14a2%202%200%200%200%202%202h14a2%202%200%200%200%202-2V5a2%202%200%200%200-2-2zM8%207h8v2H8V7zm0%204h8v2H8v-2zm0%204h5v2H8v-2z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.stage-icon-treatment {
-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;
}
.stage-item.active .stage-icon {
background: #ffffff;
}
.chat-body {
box-sizing: border-box;
height: 100vh;
padding: 244px 20px 188px;
}
.message-list {
display: flex;
flex-direction: column;
gap: 24px;
}
.message-row {
display: flex;
align-items: flex-start;
gap: 16px;
max-width: 85%;
}
.role-mentor {
align-self: flex-end;
}
.role-doctor {
align-self: flex-end;
flex-direction: row-reverse;
}
.avatar {
flex: 0 0 auto;
width: 40px;
height: 40px;
border: 1px solid #c2c6d4;
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.patient-avatar {
background: linear-gradient(135deg, #ffb691, #9f4300);
color: #ffffff;
font-size: 18px;
font-weight: 700;
}
.doctor-avatar {
background: #7af1fc;
}
.person-icon {
width: 24px;
height: 24px;
background: #006e75;
-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;
}
.message-bubble {
position: relative;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(25, 28, 33, 0.04);
}
.message-bubble::after {
content: '';
position: absolute;
top: 12px;
width: 14px;
height: 14px;
transform: rotate(45deg);
}
.patient-bubble {
border: 1px solid #c2c6d4;
background: #f9f9ff;
}
.patient-bubble::after {
left: -8px;
border-left: 1px solid #c2c6d4;
border-bottom: 1px solid #c2c6d4;
background: #f9f9ff;
}
.mentor-bubble {
border: 1px solid rgba(0, 71, 141, 0.2);
background: #e7e8f0;
}
.mentor-bubble::after {
right: -8px;
border-right: 1px solid rgba(0, 71, 141, 0.2);
border-top: 1px solid rgba(0, 71, 141, 0.2);
background: #e7e8f0;
}
.doctor-bubble {
border: 1px solid rgba(0, 71, 141, 0.1);
background: #005eb8;
}
.doctor-bubble::after {
right: -8px;
background: #005eb8;
}
.message-content {
color: #191c21;
font-size: 16px;
line-height: 24px;
}
.mentor-bubble .message-content {
color: #00478d;
font-weight: 600;
font-style: italic;
}
.doctor-bubble .message-content {
color: #c8daff;
}
.message-label {
margin-top: 8px;
color: #727783;
font-size: 12px;
line-height: 16px;
font-weight: 500;
letter-spacing: 0;
text-transform: uppercase;
display: flex;
align-items: center;
gap: 4px;
}
.message-label.mentor {
color: #00478d;
font-weight: 700;
}
.doctor-bubble .message-label {
justify-content: flex-end;
color: rgba(200, 218, 255, 0.72);
}
.star-icon {
width: 16px;
height: 16px;
background: #00478d;
-webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='m12%202%202.7%206.6%207.1.6-5.4%204.6%201.7%206.9L12%2017l-6.1%203.7%201.7-6.9-5.4-4.6%207.1-.6L12%202z'/%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%202%202.7%206.6%207.1.6-5.4%204.6%201.7%206.9L12%2017l-6.1%203.7%201.7-6.9-5.4-4.6%207.1-.6L12%202z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.mentor-float {
position: fixed;
right: 20px;
bottom: 140px;
z-index: 45;
display: flex;
flex-direction: column;
align-items: center;
pointer-events: none;
}
.mentor-badge {
margin-bottom: 8px;
padding: 4px 12px;
border-radius: 999px;
background: #00478d;
box-shadow: 0 3px 8px rgba(0, 71, 141, 0.22);
color: #ffffff;
font-size: 12px;
line-height: 16px;
font-weight: 700;
animation: pulse-soft 1.6s ease-in-out infinite;
}
.mentor-avatar {
width: 96px;
height: 96px;
border: 2px solid #00478d;
border-radius: 50%;
box-shadow: 0 8px 18px rgba(25, 28, 33, 0.18);
overflow: hidden;
background: #ffffff;
animation: breathe 3s ease-in-out infinite;
}
.mentor-avatar image {
width: 100%;
height: 100%;
}
.input-panel {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 50;
box-sizing: border-box;
padding: 12px;
border-top: 1px solid #c2c6d4;
background: #f9f9ff;
display: flex;
flex-direction: column;
gap: 16px;
}
.quick-actions {
width: 100%;
white-space: nowrap;
}
.quick-row {
display: flex;
gap: 8px;
}
.quick-chip {
flex: 0 0 auto;
padding: 8px 16px;
border: 1px solid #006970;
border-radius: 999px;
background: #ffffff;
color: #006970;
font-size: 14px;
line-height: 20px;
font-weight: 600;
}
.input-row {
min-height: 48px;
padding: 4px;
border: 1px solid #c2c6d4;
border-radius: 12px;
background: #ecedf6;
display: flex;
align-items: center;
gap: 8px;
}
.chat-input {
flex: 1;
min-width: 0;
height: 40px;
padding: 0 8px;
border: 0;
background: transparent;
color: #191c21;
font-size: 16px;
line-height: 24px;
}
.input-placeholder {
color: #c2c6d4;
}
.send-button {
flex: 0 0 auto;
width: 40px;
height: 40px;
border-radius: 8px;
background: #00478d;
display: flex;
align-items: center;
justify-content: center;
}
.send-button:active {
transform: scale(0.95);
}
.send-icon {
width: 22px;
height: 22px;
background: #ffffff;
-webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M2%2021 23%2012 2%203v7l15%202-15%202v7z'/%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='M2%2021 23%2012 2%203v7l15%202-15%202v7z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.exam-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 120;
background: rgba(25, 28, 33, 0.36);
display: flex;
align-items: flex-end;
justify-content: center;
}
.exam-panel {
width: 100%;
max-width: 448px;
max-height: 62vh;
border-radius: 20px 20px 0 0;
background: #ffffff;
box-shadow: 0 -12px 30px rgba(25, 28, 33, 0.18);
display: flex;
flex-direction: column;
overflow: hidden;
}
.physical-panel {
width: 100%;
max-width: 448px;
max-height: 78vh;
border-radius: 20px 20px 0 0;
background: #ffffff;
box-shadow: 0 -12px 30px rgba(25, 28, 33, 0.18);
display: flex;
flex-direction: column;
overflow: hidden;
}
.exam-header {
box-sizing: border-box;
flex: 0 0 auto;
height: 64px;
padding: 0 20px;
border-bottom: 1px solid #e7e8f0;
display: flex;
align-items: center;
justify-content: space-between;
}
.exam-title {
color: #191c21;
font-size: 20px;
line-height: 28px;
font-weight: 700;
}
.exam-close {
width: 40px;
height: 40px;
padding: 0;
border-radius: 50%;
background: transparent;
display: flex;
align-items: center;
justify-content: center;
}
.exam-close::after,
.exam-item::after {
border: 0;
}
.close-icon {
position: relative;
width: 24px;
height: 24px;
}
.close-icon::before,
.close-icon::after {
content: '';
position: absolute;
left: 3px;
top: 11px;
width: 20px;
height: 2px;
border-radius: 999px;
background: #2e3037;
}
.close-icon::before {
transform: rotate(45deg);
}
.close-icon::after {
transform: rotate(-45deg);
}
.exam-list {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.exam-item {
box-sizing: border-box;
width: 100%;
height: 76px;
padding: 0 20px;
border-bottom: 1px solid #e7e8f0;
border-radius: 0;
background: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
text-align: left;
}
.exam-item:active {
background: #f2f3fb;
}
.exam-name {
color: #191c21;
font-size: 18px;
line-height: 26px;
font-weight: 500;
}
.chevron-icon {
width: 13px;
height: 13px;
border-top: 2px solid #191c21;
border-right: 2px solid #191c21;
transform: rotate(45deg);
}
.physical-form {
flex: 1;
min-height: 0;
padding: 16px 20px;
background: #f9f9ff;
}
.vital-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.form-field {
position: relative;
box-sizing: border-box;
min-height: 72px;
padding: 10px 12px;
border: 1px solid #c2c6d4;
border-radius: 12px;
background: #ffffff;
display: flex;
align-items: flex-end;
}
.form-field.full,
.form-field.textarea-field {
margin-top: 12px;
}
.field-label {
position: absolute;
left: 12px;
top: 8px;
color: #424752;
font-size: 12px;
line-height: 16px;
font-weight: 600;
}
.field-input {
flex: 1;
min-width: 0;
height: 32px;
padding: 12px 4px 0 0;
border: 0;
background: transparent;
color: #191c21;
font-size: 16px;
line-height: 24px;
}
.field-unit {
flex: 0 0 auto;
margin-left: 4px;
color: #727783;
font-size: 12px;
line-height: 24px;
font-weight: 600;
}
.field-placeholder {
color: rgba(114, 119, 131, 0.55);
}
.textarea-field {
min-height: 104px;
align-items: stretch;
}
.field-textarea {
width: 100%;
min-height: 76px;
margin-top: 18px;
padding: 0;
border: 0;
background: transparent;
color: #191c21;
font-size: 15px;
line-height: 22px;
}
.physical-actions {
flex: 0 0 auto;
padding: 12px 20px calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #e7e8f0;
background: #ffffff;
display: flex;
gap: 12px;
}
.physical-cancel,
.physical-submit {
flex: 1;
height: 48px;
border-radius: 12px;
font-size: 16px;
line-height: 48px;
font-weight: 700;
}
.physical-cancel::after,
.physical-submit::after {
border: 0;
}
.physical-cancel {
border: 1px solid rgba(0, 71, 141, 0.22);
background: #ffffff;
color: #00478d;
}
.physical-submit {
background: #00478d;
color: #ffffff;
box-shadow: 0 4px 12px rgba(0, 71, 141, 0.18);
}
.toast {
position: fixed;
left: 50%;
bottom: 148px;
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 breathe {
0%,
100% {
transform: translateY(0) scale(1);
}
50% {
transform: translateY(-4px) scale(1.02);
}
}
@keyframes pulse-soft {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.72;
}
}
</style>