412 lines
13 KiB
Vue
412 lines
13 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 v-if="hasDailyRecordPermission" class="card tracking-card">
|
||
<div class="card-title">日常记录</div>
|
||
<div class="card-body">
|
||
<DailyMatrix
|
||
:diagnosis-id="diagnosisNumericId"
|
||
:patient-id="patientNumericId"
|
||
:age="diag?.age ?? null"
|
||
:patient-name="diag?.patient_name || ''"
|
||
:read-only="true"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 医生备注、舌苔照片、检查报告(只读;与编辑页「医生备注」同源) -->
|
||
<div class="card note-card">
|
||
<div class="card-title">医生备注 & 舌苔照片 & 检查报告</div>
|
||
<div class="card-body card-body--matrix-align">
|
||
<el-empty v-if="diagnosisNumericId <= 0" description="诊单数据不完整" />
|
||
<el-empty
|
||
v-else-if="!doctorNotesLocal.length"
|
||
description="暂无医生备注"
|
||
/>
|
||
<NoteTimeline
|
||
v-else
|
||
:notes="doctorNotesLocal"
|
||
:diagnosis-id="diagnosisNumericId"
|
||
:readonly="true"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 业务订单 -->
|
||
<div
|
||
v-if="hasPermission(['tcm.diagnosis/patientOrders'])"
|
||
class="card section-card"
|
||
>
|
||
<div class="card-title">业务订单</div>
|
||
<div class="card-body card-body--matrix-align">
|
||
<patient-order-list
|
||
v-if="diagnosisNumericId > 0"
|
||
:diagnosis-id="diagnosisNumericId"
|
||
:patient-id="patientNumericId || 0"
|
||
/>
|
||
<el-empty v-else description="诊单数据不完整" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 视频录制回放记录 -->
|
||
<div v-if="hasPermission(['tcm.diagnosis/huifang'])" class="card section-card">
|
||
<div class="card-title">视频录制回放记录</div>
|
||
<div class="card-body card-body--matrix-align">
|
||
<call-record-panel
|
||
v-if="diagnosisNumericId > 0"
|
||
:diagnosis-id="diagnosisNumericId"
|
||
:read-only="true"
|
||
/>
|
||
<el-empty v-else description="诊单数据不完整" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 聊天记录 -->
|
||
<div v-if="hasPermission(['tcm.diagnosis/chat'])" class="card section-card">
|
||
<div class="card-title">聊天记录</div>
|
||
<div class="card-body card-body--matrix-align">
|
||
<im-chat-record-panel
|
||
v-if="diagnosisNumericId > 0"
|
||
:diagnosis-id="diagnosisNumericId"
|
||
/>
|
||
<el-empty v-else description="诊单数据不完整" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 指派医助记录 -->
|
||
<div
|
||
v-if="
|
||
hasPermission(['tcm.diagnosis/assign']) ||
|
||
hasPermission(['tcm.diagnosis/detail'])
|
||
"
|
||
class="card section-card"
|
||
>
|
||
<div class="card-title">指派医助记录</div>
|
||
<div class="card-body card-body--matrix-align">
|
||
<assign-log-panel
|
||
v-if="diagnosisNumericId > 0"
|
||
:diagnosis-id="diagnosisNumericId"
|
||
/>
|
||
<el-empty v-else description="诊单数据不完整" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 挂号记录 -->
|
||
<div
|
||
v-if="hasPermission(['doctor.appointment/lists'])"
|
||
class="card section-card"
|
||
>
|
||
<div class="card-title">挂号记录</div>
|
||
<div class="card-body card-body--matrix-align">
|
||
<appointment-record-panel
|
||
v-if="diagnosisNumericId > 0"
|
||
:diagnosis-id="diagnosisNumericId"
|
||
/>
|
||
<el-empty v-else description="诊单数据不完整" />
|
||
</div>
|
||
</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 { getDoctorNotes } from '@/api/patient'
|
||
import { formatGender } from '@/utils/diag-display'
|
||
import { hasPermission } from '@/utils/perm'
|
||
import PatientInfoCard from './components/PatientInfoCard.vue'
|
||
import PatientCaseCard from './components/PatientCaseCard.vue'
|
||
import DailyMatrix from './components/DailyMatrix.vue'
|
||
import CallRecordPanel from './components/CallRecordPanel.vue'
|
||
import ImChatRecordPanel from './components/ImChatRecordPanel.vue'
|
||
import AssignLogPanel from './components/AssignLogPanel.vue'
|
||
import AppointmentRecordPanel from './components/AppointmentRecordPanel.vue'
|
||
import PatientOrderList from './components/PatientOrderList.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)
|
||
/** 医生备注:readonlyDetail 返回 + getDoctorNotes 对齐接诊台结构 */
|
||
const doctorNotesLocal = ref<any[]>([])
|
||
|
||
const apt = computed<any>(() => detail.value?.appointment || {})
|
||
const diag = computed<any>(() => detail.value?.diagnosis || {})
|
||
|
||
const hasDailyRecordPermission = computed(() => hasPermission(['tcm.diagnosis/dailyRecord']))
|
||
|
||
const diagnosisNumericId = computed(() =>
|
||
Number(diag.value?.id ?? diagnosisIdNum.value ?? 0)
|
||
)
|
||
const patientNumericId = computed(() => Number(diag.value?.patient_id ?? 0))
|
||
|
||
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 fetchDoctorNotes = async () => {
|
||
const id = diagnosisNumericId.value
|
||
if (!id) return
|
||
try {
|
||
const data = (await getDoctorNotes({ diagnosis_id: id })) || []
|
||
doctorNotesLocal.value = data
|
||
} catch {
|
||
/* 失败时保留 readonlyDetail 已下发的 doctor_notes,避免整块备注空白 */
|
||
}
|
||
}
|
||
|
||
const fetchDetail = async () => {
|
||
const id = diagnosisIdNum.value
|
||
if (!id || id <= 0) {
|
||
errorMsg.value = '诊单 id 缺失'
|
||
detail.value = null
|
||
doctorNotesLocal.value = []
|
||
return
|
||
}
|
||
loading.value = true
|
||
errorMsg.value = ''
|
||
try {
|
||
const res: any = await diagnosisReadonlyDetail({ id })
|
||
detail.value = res || null
|
||
doctorNotesLocal.value = Array.isArray(res?.doctor_notes) ? res.doctor_notes : []
|
||
await fetchDoctorNotes()
|
||
} catch (e: any) {
|
||
errorMsg.value = e?.message || ''
|
||
detail.value = null
|
||
doctorNotesLocal.value = []
|
||
} 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%);
|
||
}
|
||
}
|
||
|
||
.card-body {
|
||
min-width: 0;
|
||
}
|
||
|
||
/* 与 DailyMatrix 根节点 .daily-matrix { padding: 16px } 对齐,使下方区块与「日常记录 / 待办」主内容区左右留白一致 */
|
||
.card-body--matrix-align {
|
||
padding: 16px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.section-card .card-title {
|
||
margin-bottom: 0;
|
||
}
|
||
</style>
|