Files
zyt/admin/src/views/tcm/diagnosis/readonly.vue
T
long 9df6330d41 update
修改未服务天数判断条件
2026-05-18 17:42:24 +08:00

412 lines
13 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>
<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>