This commit is contained in:
2026-05-05 15:02:04 +08:00
parent 0da27ed745
commit 19bfeef98f
19 changed files with 1771 additions and 272 deletions
+282
View File
@@ -0,0 +1,282 @@
<template>
<div class="readonly-page" v-loading="loading">
<!-- 顶部 hero -->
<div class="readonly-hero">
<div class="hero-left">
<el-button link size="default" @click="goBack" class="back-btn">
<el-icon><ArrowLeft /></el-icon>
<span>返回</span>
</el-button>
<span class="title">患者信息详情</span>
</div>
<div v-if="diag?.patient_name" class="hero-right">
<span class="patient-name">{{ diag.patient_name }}</span>
<span class="patient-meta">
{{ formatGender(diag.gender) }}
<template v-if="diag.age != null"> · {{ diag.age }}</template>
</span>
<el-tooltip
v-if="lastBloodAt"
:content="`最近一次血糖打卡:${lastBloodAt}`"
placement="bottom"
>
<span :class="['unserved-badge', unservedDaysClass(unservedDays)]">
未服务 {{ unservedDays ?? '—' }}
</span>
</el-tooltip>
<span v-else class="unserved-badge unserved-muted">从未打卡血糖</span>
</div>
</div>
<!-- 加载失败 / 越权 -->
<el-empty
v-if="!loading && !detail"
:description="errorMsg || '诊单不存在或无权访问'"
class="empty-state"
/>
<template v-else-if="detail">
<PatientInfoCard :apt="apt" :diag="diag" />
<PatientCaseCard :apt="apt" :diag="diag" />
<!-- 跟踪信息 -->
<div class="card tracking-card">
<div class="card-title">跟踪信息最近 30 </div>
<TrackingMatrix
:diagnosis-id="Number(diagnosisIdNum)"
:patient-id="Number(diag?.patient_id ?? 0)"
:age="diag?.age ?? null"
:blood-records="bloodRecords"
:diet-records="dietRecords"
:exercise-records="exerciseRecords"
/>
</div>
<!-- 医生备注复用 reception NoteTimeline但只读 -->
<div v-if="doctorNotes.length" class="card note-card">
<div class="card-title">医生备注 & 舌苔照片 & 检查报告</div>
<NoteTimeline :notes="doctorNotes" :diagnosis-id="diag?.id" />
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ArrowLeft } from '@element-plus/icons-vue'
import { diagnosisReadonlyDetail } from '@/api/tcm'
import { formatGender } from '@/utils/diag-display'
import PatientInfoCard from './components/PatientInfoCard.vue'
import PatientCaseCard from './components/PatientCaseCard.vue'
import TrackingMatrix from './components/TrackingMatrix.vue'
import NoteTimeline from '@/views/patient/reception/components/NoteTimeline.vue'
const route = useRoute()
const router = useRouter()
const diagnosisIdNum = computed(() => Number(route.query.id || 0))
const loading = ref(false)
const errorMsg = ref('')
const detail = ref<any>(null)
const apt = computed<any>(() => detail.value?.appointment || {})
const diag = computed<any>(() => detail.value?.diagnosis || {})
const bloodRecords = computed<any[]>(() => detail.value?.blood_records || [])
const dietRecords = computed<any[]>(() => detail.value?.diet_records || [])
const exerciseRecords = computed<any[]>(() => detail.value?.exercise_records || [])
const doctorNotes = computed<any[]>(() => detail.value?.doctor_notes || [])
const unservedDays = computed<number | null>(() => {
const v = detail.value?.unserved_days
return v === null || v === undefined ? null : Number(v)
})
const lastBloodAt = computed<string>(() => detail.value?.last_blood_record_at || '')
/**
* 「未服务天数」着色规则(与 T3 列表一致):
* null/undefined → 灰;>=7 → 红;3-6 → 橙;<=2 → 绿
*/
const unservedDaysClass = (n: unknown) => {
if (n === null || n === undefined) return 'unserved-muted'
const num = Number(n)
if (!Number.isFinite(num)) return 'unserved-muted'
if (num >= 7) return 'unserved-red'
if (num >= 3) return 'unserved-orange'
return 'unserved-green'
}
const fetchDetail = async () => {
const id = diagnosisIdNum.value
if (!id || id <= 0) {
errorMsg.value = '诊单 id 缺失'
detail.value = null
return
}
loading.value = true
errorMsg.value = ''
try {
const res: any = await diagnosisReadonlyDetail({ id })
detail.value = res || null
} catch (e: any) {
errorMsg.value = e?.message || ''
detail.value = null
} finally {
loading.value = false
}
}
const goBack = () => {
if (window.history.length > 1) {
router.back()
} else {
router.push({ path: '/tcm/diagnosis' })
}
}
onMounted(fetchDetail)
</script>
<style lang="scss" scoped>
/* 沿用接诊台的设计变量;该文件挂在管理后台,--rx-* 已在 reception/index.vue 全局可见的同款值 */
.readonly-page {
--rx-surface: #ffffff;
--rx-line: #e6ebf2;
--rx-ink: #1f2937;
--rx-muted: #4b5563;
--rx-soft: #6b7280;
--rx-accent: #2563eb;
--rx-accent-strong: #1d4ed8;
--rx-shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04);
--rx-shadow: 0 4px 12px rgba(15, 23, 42, 0.06);
--rx-font-data: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px;
}
.readonly-hero {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
background: linear-gradient(135deg, #f5f8ff 0%, #eef3ff 100%);
border: 1px solid #dde7ff;
border-radius: 12px;
padding: 12px 16px;
.hero-left {
display: flex;
align-items: center;
gap: 12px;
.back-btn {
display: inline-flex;
align-items: center;
gap: 4px;
font-weight: 500;
}
.title {
font-size: 16px;
font-weight: 700;
color: var(--rx-ink);
}
}
.hero-right {
display: flex;
align-items: center;
gap: 12px;
.patient-name {
font-size: 18px;
font-weight: 700;
color: var(--rx-ink);
}
.patient-meta {
font-size: 13px;
color: var(--rx-muted);
}
}
}
.unserved-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
background: rgba(255, 255, 255, 0.7);
border: 1px solid #dde7ff;
cursor: default;
&.unserved-muted {
color: var(--el-text-color-placeholder);
font-weight: 400;
}
&.unserved-green {
color: #16a34a;
border-color: #bbf7d0;
background: rgba(22, 163, 74, 0.06);
}
&.unserved-orange {
color: #ea580c;
border-color: #fed7aa;
background: rgba(234, 88, 12, 0.06);
}
&.unserved-red {
color: #dc2626;
border-color: #fecaca;
background: rgba(220, 38, 38, 0.08);
}
}
.empty-state {
background: var(--rx-surface);
border: 1px solid var(--rx-line);
border-radius: 14px;
padding: 40px 0;
}
.card {
background: var(--rx-surface);
border: 1px solid var(--rx-line);
border-radius: 14px;
padding: 18px;
display: flex;
flex-direction: column;
gap: 14px;
box-shadow: var(--rx-shadow-sm);
}
.card-title {
font-size: 15px;
font-weight: 700;
color: var(--rx-ink);
display: flex;
align-items: center;
gap: 10px;
padding-left: 10px;
position: relative;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 16px;
border-radius: 2px;
background: linear-gradient(180deg, #4f8cff 0%, #2563eb 100%);
}
}
</style>