feat: 新增体格检查
This commit is contained in:
+7
-1
@@ -1,4 +1,8 @@
|
||||
const apiBaseUrl = 'http://127.0.0.1:8000/api'
|
||||
let apiBaseUrl = 'http://192.168.2.76:8000/api'
|
||||
|
||||
// #ifdef H5
|
||||
apiBaseUrl = '/backend-api'
|
||||
// #endif
|
||||
|
||||
export const API_BASE_URL = apiBaseUrl
|
||||
|
||||
@@ -14,6 +18,8 @@ export type SendCodeResponse = {
|
||||
export type LoginCodePayload = {
|
||||
phone: string
|
||||
code: string
|
||||
institution_code: string
|
||||
institution_name: string
|
||||
}
|
||||
|
||||
export type BackendUser = {
|
||||
|
||||
+2
-1
@@ -34,7 +34,8 @@ const defaultCase: ClinicalCase = {
|
||||
department: '心血管内科',
|
||||
scene: '住院部',
|
||||
caseNo: '1006004',
|
||||
tone: 'orange'
|
||||
tone: 'orange',
|
||||
mode: 'training'
|
||||
}
|
||||
|
||||
export function createMockChatSession(caseItem?: ClinicalCase | null): Promise<ChatSession> {
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@
|
||||
"devServer" : {
|
||||
"proxy" : {
|
||||
"/backend-api" : {
|
||||
"target" : "http://127.0.0.1:8000",
|
||||
"target" : "http://192.168.2.76:8000",
|
||||
"changeOrigin" : true,
|
||||
"pathRewrite" : {
|
||||
"^/backend-api" : "/api"
|
||||
|
||||
+476
-3
@@ -88,8 +88,8 @@
|
||||
<view class="input-panel">
|
||||
<scroll-view class="quick-actions" scroll-x>
|
||||
<view class="quick-row">
|
||||
<button class="quick-chip" @click="sendQuickAction('体格检查')">体格检查</button>
|
||||
<button class="quick-chip" @click="sendQuickAction('辅助检查')">辅助检查</button>
|
||||
<button class="quick-chip" @click="openPhysicalPanel">体格检查</button>
|
||||
<button class="quick-chip" @click="openExamPanel">辅助检查</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="input-row">
|
||||
@@ -108,6 +108,85 @@
|
||||
</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>
|
||||
@@ -115,7 +194,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
import type { ClinicalCase } from '../../api/cases'
|
||||
import { createMockChatSession, sendMockChatMessage, type ChatSession } from '../../api/chat'
|
||||
import { createMockChatSession, sendMockChatMessage, type ChatMessage, type ChatSession } from '../../api/chat'
|
||||
import DiagnosisPage from '../diagnosis/diagnosis.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -150,9 +229,46 @@ 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)
|
||||
@@ -172,6 +288,107 @@ function sendQuickAction(content: string) {
|
||||
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
|
||||
@@ -774,6 +991,262 @@ page {
|
||||
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%;
|
||||
|
||||
+2
-1
@@ -2,11 +2,12 @@ import { defineConfig } from 'vite'
|
||||
import uni from '@dcloudio/vite-plugin-uni'
|
||||
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [uni()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/backend-api': {
|
||||
target: 'http://127.0.0.1:8000',
|
||||
target: 'http://192.168.2.76:8000',
|
||||
changeOrigin: true,
|
||||
rewrite: path => path.replace(/^\/backend-api/, '/api')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user