280 lines
7.9 KiB
Vue
280 lines
7.9 KiB
Vue
<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">日常记录</div>
|
||
<DailyMatrix
|
||
:diagnosis-id="Number(diagnosisIdNum)"
|
||
:patient-id="Number(diag?.patient_id ?? 0)"
|
||
:age="diag?.age ?? null"
|
||
:patient-name="diag?.patient_name || ''"
|
||
:read-only="true"
|
||
/>
|
||
</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 DailyMatrix from './components/DailyMatrix.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 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>
|