1
This commit is contained in:
@@ -5,6 +5,11 @@ export function tcmDiagnosisLists(params: any) {
|
||||
return request.get({ url: '/tcm.diagnosis/lists', params })
|
||||
}
|
||||
|
||||
/** 二诊只读病例详情(T1+T2):返回 appointment + diagnosis + tracking + 未服务天数 */
|
||||
export function diagnosisReadonlyDetail(params: { id: number }) {
|
||||
return request.get({ url: '/tcm.diagnosis/readonlyDetail', params })
|
||||
}
|
||||
|
||||
// 医助理诊单统计(按部门、按人)
|
||||
export function assistantDiagnosisStats(params?: {
|
||||
start_time?: string
|
||||
|
||||
@@ -82,6 +82,24 @@ export const constantRoutes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/tcm/diagnosis/h5',
|
||||
component: () => import('@/views/tcm/diagnosis/index_h5.vue')
|
||||
},
|
||||
{
|
||||
// T1+T2:患者信息详情页(隐藏路由 — 不在侧边栏出现,由诊单列表的「查看」/双击行驱动跳转)
|
||||
path: '/tcm/diagnosis-readonly',
|
||||
component: LAYOUT,
|
||||
meta: { hidden: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: () => import('@/views/tcm/diagnosis/readonly.vue'),
|
||||
name: 'tcmDiagnosisReadonly',
|
||||
meta: {
|
||||
title: '患者信息详情',
|
||||
hidden: true,
|
||||
activeMenu: '/tcm/diagnosis'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
// {
|
||||
// path: '/dev_tools',
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 血糖 / 血压 阈值与异常判定工具
|
||||
*
|
||||
* 阈值规则(与 reception/index.vue 抽离前一致):
|
||||
* - 年龄 < 50:空腹血糖 ≥ 7、餐后/其他血糖 ≥ 9 视为偏高
|
||||
* - 年龄 ≥ 50:空腹血糖 ≥ 8、餐后/其他血糖 ≥ 10 视为偏高
|
||||
* - 收缩压 > 140 / 舒张压 > 90 视为偏高
|
||||
*
|
||||
* 任何字段缺失或非数值 → 视为「不偏高」(避免误报红色 ↑)。
|
||||
*/
|
||||
|
||||
/**
|
||||
* 把任意值转成有限数字;空 / 非法 → null
|
||||
*/
|
||||
export function toNumeric(value: unknown): number | null {
|
||||
if (value === null || value === undefined || value === '') return null
|
||||
const num = Number(value as any)
|
||||
return Number.isFinite(num) ? num : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据年龄返回血糖阈值;年龄缺失/非法 → null(=不判定偏高)
|
||||
*/
|
||||
export function getBloodSugarThresholds(
|
||||
ageValue: unknown
|
||||
): { fasting: number; postprandial: number } | null {
|
||||
const age = toNumeric(ageValue)
|
||||
if (age === null) return null
|
||||
if (age < 50) return { fasting: 7, postprandial: 9 }
|
||||
return { fasting: 8, postprandial: 10 }
|
||||
}
|
||||
|
||||
export function isHighFastingBloodSugar(value: unknown, ageValue: unknown): boolean {
|
||||
const num = toNumeric(value)
|
||||
const thresholds = getBloodSugarThresholds(ageValue)
|
||||
return num !== null && thresholds !== null && num >= thresholds.fasting
|
||||
}
|
||||
|
||||
export function isHighPostprandialBloodSugar(value: unknown, ageValue: unknown): boolean {
|
||||
const num = toNumeric(value)
|
||||
const thresholds = getBloodSugarThresholds(ageValue)
|
||||
return num !== null && thresholds !== null && num >= thresholds.postprandial
|
||||
}
|
||||
|
||||
/**
|
||||
* 「其他血糖」按餐后阈值判定(与 reception 原实现一致)
|
||||
*/
|
||||
export function isHighOtherBloodSugar(value: unknown, ageValue: unknown): boolean {
|
||||
const num = toNumeric(value)
|
||||
const thresholds = getBloodSugarThresholds(ageValue)
|
||||
return num !== null && thresholds !== null && num >= thresholds.postprandial
|
||||
}
|
||||
|
||||
export function isHighSystolicPressure(value: unknown): boolean {
|
||||
const num = toNumeric(value)
|
||||
return num !== null && num > 140
|
||||
}
|
||||
|
||||
export function isHighDiastolicPressure(value: unknown): boolean {
|
||||
const num = toNumeric(value)
|
||||
return num !== null && num > 90
|
||||
}
|
||||
|
||||
/**
|
||||
* 一行血糖血压记录是否有任一压偏高
|
||||
*/
|
||||
export function isHighBloodPressure(row: {
|
||||
systolic_pressure?: unknown
|
||||
diastolic_pressure?: unknown
|
||||
} | null | undefined): boolean {
|
||||
if (!row) return false
|
||||
return isHighSystolicPressure(row.systolic_pressure) || isHighDiastolicPressure(row.diastolic_pressure)
|
||||
}
|
||||
|
||||
/**
|
||||
* 把一行记录的血压两值格式化为 `xxx / yyy`,缺值 → `—`
|
||||
*/
|
||||
export function formatBp(row: {
|
||||
systolic_pressure?: unknown
|
||||
diastolic_pressure?: unknown
|
||||
} | null | undefined): string {
|
||||
const sys = row?.systolic_pressure
|
||||
const dia = row?.diastolic_pressure
|
||||
if (sys == null && dia == null) return '—'
|
||||
return `${sys ?? '—'} / ${dia ?? '—'}`
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 病例 / 接诊页通用展示辅助
|
||||
*
|
||||
* - maskPhone:手机号脱敏 138****1234
|
||||
* - formatGender:0/1 → 女/男
|
||||
* - formatPeriod:morning/afternoon → 上午/下午
|
||||
* - joinArray:数组 → 顿号拼接,非数组 → toString
|
||||
* - makeTextOf:工厂,传入 diag ref/getter,返回闭包后的 textOf 函数
|
||||
*
|
||||
* 翻译规则(textOf):诊单字典字段由后端 `AppointmentLogic::enrichDiagnosisLabels`
|
||||
* 预翻译为 `<field>_text`,前端优先读 `*_text`;找不到时回退原值。
|
||||
*/
|
||||
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
export function maskPhone(phone?: string | null): string {
|
||||
if (!phone) return '—'
|
||||
if (phone.length < 11) return phone
|
||||
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||
}
|
||||
|
||||
export function formatGender(g?: number | null): string {
|
||||
if (g === 1) return '男'
|
||||
if (g === 0) return '女'
|
||||
return '—'
|
||||
}
|
||||
|
||||
export function formatPeriod(period?: string | null): string {
|
||||
if (period === 'morning') return '上午'
|
||||
if (period === 'afternoon') return '下午'
|
||||
return '全天'
|
||||
}
|
||||
|
||||
export function joinArray(arr: unknown): string {
|
||||
if (!arr) return ''
|
||||
if (Array.isArray(arr)) return arr.filter(Boolean).join('、')
|
||||
return String(arr)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建闭包了具体 diag 的 textOf 取值器。
|
||||
* 同时支持 `Ref` / `() => any` / 普通对象,避免在调用方处反复样板。
|
||||
*/
|
||||
export function makeTextOf(diagSource: Ref<any> | (() => any) | Record<string, any>) {
|
||||
const resolve = (): Record<string, any> => {
|
||||
if (typeof diagSource === 'function') {
|
||||
return (diagSource as () => any)() || {}
|
||||
}
|
||||
if (diagSource && typeof diagSource === 'object' && 'value' in diagSource) {
|
||||
return ((diagSource as Ref<any>).value ?? {}) as Record<string, any>
|
||||
}
|
||||
return (diagSource as Record<string, any>) || {}
|
||||
}
|
||||
|
||||
return function textOf(field: string): string {
|
||||
const d = resolve()
|
||||
const t = d[field + '_text']
|
||||
if (t !== undefined && t !== null && t !== '') return String(t)
|
||||
const raw = d[field]
|
||||
if (raw === undefined || raw === null || raw === '') return ''
|
||||
return Array.isArray(raw) ? raw.join('、') : String(raw)
|
||||
}
|
||||
}
|
||||
@@ -111,126 +111,10 @@
|
||||
|
||||
<template v-else-if="detail">
|
||||
<!-- 患者信息 -->
|
||||
<div class="card patient-card">
|
||||
<div class="card-title">患者信息</div>
|
||||
<div class="patient-hero">
|
||||
<div class="patient-name">{{ apt.patient_name || '—' }}</div>
|
||||
<div class="patient-meta">
|
||||
<div>{{ maskPhone(apt.patient_phone) }} · {{ formatGender(diag.gender) }} · {{ diag.age != null ? diag.age + '岁' : '—' }}</div>
|
||||
<div>{{ diag.height ? diag.height + 'cm' : '—' }} / {{ diag.weight ? diag.weight + 'kg' : '—' }} · {{ diag.region || '—' }}</div>
|
||||
<div>预约:{{ apt.appointment_date }} {{ apt.appointment_time }} · {{ formatPeriod(apt.period) }}</div>
|
||||
<div>医生:{{ apt.doctor_name || '—' }} 医助:{{ apt.assistant_name || '—' }}</div>
|
||||
<div>状态:{{ apt.status_desc || '—' }} · {{ apt.has_prescription ? '已开方' : '未开方' }}</div>
|
||||
<div v-if="apt.remark">备注:{{ apt.remark }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PatientInfoCard :apt="apt" :diag="diag" />
|
||||
|
||||
<!-- 患者病例(完整、字段与 tcm/diagnosis/edit.vue 保持同步,只读展示) -->
|
||||
<div class="card case-card">
|
||||
<div class="card-title">
|
||||
患者病例
|
||||
<span class="case-sub">{{ caseTypeLabel }} · 诊断日期 {{ diag.diagnosis_date || apt.appointment_date || '—' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<div class="grid-block">
|
||||
<div class="grid-title">基本信息</div>
|
||||
<div class="kv-grid four">
|
||||
<KVItem label="诊单ID" :value="diag.patient_id" />
|
||||
<KVItem label="姓名" :value="diag.patient_name" />
|
||||
<KVItem label="身份证号" :value="diag.id_card" />
|
||||
<KVItem label="手机号" :value="maskPhone(diag.phone || apt.patient_phone)" />
|
||||
<KVItem label="性别" :value="textOf('gender')" />
|
||||
<KVItem label="年龄" :value="diag.age != null ? diag.age + '岁' : ''" />
|
||||
<KVItem label="婚姻状态" :value="textOf('marital_status')" />
|
||||
<KVItem label="地区" :value="diag.region" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 生命体征 -->
|
||||
<div class="grid-block">
|
||||
<div class="grid-title">生命体征</div>
|
||||
<div class="kv-grid four">
|
||||
<KVItem label="身高" :value="diag.height ? diag.height + ' cm' : ''" />
|
||||
<KVItem label="体重" :value="diag.weight ? diag.weight + ' kg' : ''" />
|
||||
<MetricKVItem label="高压" :value="diag.systolic_pressure" unit=" mmHg" :high="isHighSystolicPressure(diag.systolic_pressure)" />
|
||||
<MetricKVItem label="低压" :value="diag.diastolic_pressure" unit=" mmHg" :high="isHighDiastolicPressure(diag.diastolic_pressure)" />
|
||||
<KVItem label="诊断类型" :value="textOf('diagnosis_type')" />
|
||||
<MetricKVItem label="空腹血糖" :value="diag.fasting_blood_sugar" unit=" mmol/L" :high="isHighFastingBloodSugar(diag.fasting_blood_sugar, diag.age)" />
|
||||
<KVItem label="在用药物" :value="diag.current_medications" multiline />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主诉 -->
|
||||
<div class="grid-block">
|
||||
<div class="grid-title">主诉</div>
|
||||
<div class="kv-grid four">
|
||||
<KVItem label="当地医院诊断日期" :value="diag.diagnosis_date" />
|
||||
<KVItem label="发现糖尿病病史" :value="formatDiabetesDiscoveryDisplay ? formatDiabetesDiscoveryDisplay(diag.diabetes_discovery_year) : diag.diabetes_discovery_year" />
|
||||
<KVItem label="当地就诊医院" :value="diag.local_hospital_name" />
|
||||
<KVItem label="当地医院诊断结果" :value="joinArray(diag.local_hospital_diagnosis)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 现病史(后端已翻译 *_text) -->
|
||||
<div class="grid-block">
|
||||
<div class="grid-title">现病史</div>
|
||||
<div class="kv-grid three">
|
||||
<KVItem label="口腔感觉" :value="textOf('appetite')" />
|
||||
<KVItem label="每日饮水量" :value="textOf('water_intake')" />
|
||||
<KVItem label="近月体重变化" :value="textOf('weight_change')" />
|
||||
<KVItem label="脂肪肝程度" :value="textOf('fatty_liver_degree')" />
|
||||
<KVItem label="饮食情况" :value="textOf('diet_condition')" span="2" />
|
||||
<KVItem label="肢体感觉" :value="textOf('body_feeling')" />
|
||||
<KVItem label="睡眠情况" :value="textOf('sleep_condition')" />
|
||||
<KVItem label="眼睛情况" :value="textOf('eye_condition')" />
|
||||
<KVItem label="头部感觉" :value="textOf('head_feeling')" />
|
||||
<KVItem label="出汗情况" :value="textOf('sweat_condition')" />
|
||||
<KVItem label="皮肤情况" :value="textOf('skin_condition')" />
|
||||
<KVItem label="小便情况" :value="textOf('urine_condition')" />
|
||||
<KVItem label="大便情况" :value="textOf('stool_condition')" />
|
||||
<KVItem label="腰肾情况" :value="textOf('kidney_condition')" />
|
||||
<KVItem label="其他补充" :value="diag.symptoms" span="3" multiline />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 既往史 -->
|
||||
<div class="grid-block">
|
||||
<div class="grid-title">既往史</div>
|
||||
<div class="kv-grid three">
|
||||
<KVItem label="既往病史" :value="textOf('past_history')" span="3" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 其他病史 -->
|
||||
<div class="grid-block">
|
||||
<div class="grid-title">其他病史</div>
|
||||
<div class="kv-grid three">
|
||||
<KVItem label="外伤史" :value="textOf('trauma_history')" />
|
||||
<KVItem label="手术史" :value="textOf('surgery_history')" />
|
||||
<KVItem label="过敏史" :value="textOf('allergy_history')" />
|
||||
<KVItem label="家族病史" :value="textOf('family_history')" />
|
||||
<KVItem label="妊娠哺乳史" :value="textOf('pregnancy_history')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 病史补充 -->
|
||||
<div class="grid-block" v-if="diag.remark">
|
||||
<div class="grid-title">病史补充</div>
|
||||
<div class="kv-grid one">
|
||||
<KVItem label="" :value="diag.remark" multiline />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 处方意见 -->
|
||||
<div class="grid-block">
|
||||
<div class="grid-title">处方意见</div>
|
||||
<div class="rx-note">
|
||||
{{ apt.has_prescription ? '已开方。' : '当前未开方。' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 患者病例(与 tcm/diagnosis/readonly 共用同一组件,确保两处字段一致) -->
|
||||
<PatientCaseCard :apt="apt" :diag="diag" />
|
||||
|
||||
<!-- 医生备注 & 舌苔照片 -->
|
||||
<div class="card note-card">
|
||||
@@ -353,13 +237,23 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, onMounted, onUnmounted, h } from 'vue'
|
||||
import { defineAsyncComponent, onMounted, onUnmounted } from 'vue'
|
||||
import { receptionQueue, receptionDetail, notifyAssistant, addDoctorNote } from '@/api/patient'
|
||||
import NoteTimeline from './components/NoteTimeline.vue'
|
||||
import PatientInfoCard from '@/views/tcm/diagnosis/components/PatientInfoCard.vue'
|
||||
import PatientCaseCard from '@/views/tcm/diagnosis/components/PatientCaseCard.vue'
|
||||
import { completeAppointment } from '@/api/doctor'
|
||||
import { getCallSignature } from '@/api/tcm'
|
||||
import feedback from '@/utils/feedback'
|
||||
import { formatDiabetesDiscoveryDisplay } from '@/utils/diabetes-discovery-display'
|
||||
import { formatGender } from '@/utils/diag-display'
|
||||
import {
|
||||
isHighFastingBloodSugar,
|
||||
isHighPostprandialBloodSugar,
|
||||
isHighOtherBloodSugar,
|
||||
isHighBloodPressure,
|
||||
formatBp
|
||||
} from '@/utils/blood-thresholds'
|
||||
import {
|
||||
Phone,
|
||||
Search,
|
||||
@@ -370,55 +264,6 @@ import {
|
||||
const ChatDialog = defineAsyncComponent(() => import('@/components/chat-dialog/index.vue'))
|
||||
const DiagnosisEdit = defineAsyncComponent(() => import('@/views/tcm/diagnosis/edit.vue'))
|
||||
|
||||
/**
|
||||
* 局部渲染组件:左侧 label + 右侧 value,支持 span / 换行
|
||||
*/
|
||||
const KVItem = (props: { label: string; value: any; span?: string | number; multiline?: boolean }) => {
|
||||
const val = props.value
|
||||
const empty = val === undefined || val === null || val === '' || (Array.isArray(val) && val.length === 0)
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: ['kv-item', props.multiline ? 'multiline' : '', empty ? 'empty' : ''],
|
||||
style: props.span ? `grid-column: span ${props.span}` : ''
|
||||
},
|
||||
[
|
||||
h('span', { class: 'kv-label' }, props.label),
|
||||
h('span', { class: 'kv-value' }, empty ? '—' : String(val))
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
const MetricKVItem = (props: {
|
||||
label: string
|
||||
value: any
|
||||
unit?: string
|
||||
span?: string | number
|
||||
high?: boolean
|
||||
}) => {
|
||||
const empty = props.value === undefined || props.value === null || props.value === ''
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: ['kv-item', 'metric-kv-item', empty ? 'empty' : ''],
|
||||
style: props.span ? `grid-column: span ${props.span}` : ''
|
||||
},
|
||||
[
|
||||
h('span', { class: 'kv-label' }, props.label),
|
||||
h(
|
||||
'span',
|
||||
{ class: ['kv-value', 'metric-kv-value', props.high ? 'high' : ''] },
|
||||
empty
|
||||
? '—'
|
||||
: [
|
||||
`${props.value}${props.unit || ''}`,
|
||||
props.high ? h('span', { class: 'metric-up' }, '↑') : null
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
const POLL_MS = 5_000
|
||||
const PAGE_SIZE = 15
|
||||
|
||||
@@ -489,38 +334,12 @@ const todayMeta = computed(() => {
|
||||
})
|
||||
|
||||
|
||||
const caseTypeLabel = computed(() => {
|
||||
if (!diag.value) return '初诊'
|
||||
return diag.value.consultation_type === 'follow_up' ? '复诊' : '初诊'
|
||||
})
|
||||
|
||||
/**
|
||||
* 诊单字典字段由后端 AppointmentLogic::enrichDiagnosisLabels 预翻译,
|
||||
* 前端直接读 `<field>_text`;找不到时回退原值。
|
||||
*/
|
||||
const textOf = (field: string): string => {
|
||||
const d: any = diag.value || {}
|
||||
const t = d[field + '_text']
|
||||
if (t !== undefined && t !== null && t !== '') return String(t)
|
||||
const raw = d[field]
|
||||
if (raw === undefined || raw === null || raw === '') return ''
|
||||
return Array.isArray(raw) ? raw.join('、') : String(raw)
|
||||
}
|
||||
|
||||
const todayStr = () => {
|
||||
const d = new Date()
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
||||
}
|
||||
|
||||
const maskPhone = (phone?: string) => {
|
||||
if (!phone) return '—'
|
||||
if (phone.length < 11) return phone
|
||||
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||
}
|
||||
|
||||
const formatGender = (g?: number) => (g === 1 ? '男' : g === 0 ? '女' : '—')
|
||||
|
||||
const formatGenderAge = (row: QueueRow) => {
|
||||
const parts = [formatGender(row.gender), row.age != null ? row.age + '岁' : '']
|
||||
return parts.filter((p) => p && p !== '—').join(' ')
|
||||
@@ -531,73 +350,6 @@ const formatTimeHM = (time?: string) => {
|
||||
return time.replace(/^(\d{2}:\d{2})(:\d{2})?$/, '$1')
|
||||
}
|
||||
|
||||
const formatPeriod = (period?: string) => {
|
||||
if (period === 'morning') return '上午'
|
||||
if (period === 'afternoon') return '下午'
|
||||
return '全天'
|
||||
}
|
||||
|
||||
const formatBp = (row: any) => {
|
||||
const sys = row?.systolic_pressure
|
||||
const dia = row?.diastolic_pressure
|
||||
if (sys == null && dia == null) return '—'
|
||||
return `${sys ?? '—'} / ${dia ?? '—'}`
|
||||
}
|
||||
|
||||
const toNumeric = (value: any) => {
|
||||
if (value === null || value === undefined || value === '') return null
|
||||
const num = Number(value)
|
||||
return Number.isFinite(num) ? num : null
|
||||
}
|
||||
|
||||
const getBloodSugarThresholds = (ageValue: any) => {
|
||||
const age = toNumeric(ageValue)
|
||||
if (age === null) return null
|
||||
if (age < 50) return { fasting: 7, postprandial: 9 }
|
||||
if (age >= 50) return { fasting: 8, postprandial: 10 }
|
||||
return null
|
||||
}
|
||||
|
||||
const isHighFastingBloodSugar = (value: any, ageValue: any) => {
|
||||
const num = toNumeric(value)
|
||||
const thresholds = getBloodSugarThresholds(ageValue)
|
||||
return num !== null && thresholds !== null && num >= thresholds.fasting
|
||||
}
|
||||
|
||||
const isHighPostprandialBloodSugar = (value: any, ageValue: any) => {
|
||||
const num = toNumeric(value)
|
||||
const thresholds = getBloodSugarThresholds(ageValue)
|
||||
return num !== null && thresholds !== null && num >= thresholds.postprandial
|
||||
}
|
||||
|
||||
const isHighOtherBloodSugar = (value: any, ageValue: any) => {
|
||||
const num = toNumeric(value)
|
||||
const thresholds = getBloodSugarThresholds(ageValue)
|
||||
return num !== null && thresholds !== null && num >= thresholds.postprandial
|
||||
}
|
||||
|
||||
const isHighBloodPressure = (row: any) => {
|
||||
const sys = toNumeric(row?.systolic_pressure)
|
||||
const dia = toNumeric(row?.diastolic_pressure)
|
||||
return (sys !== null && sys > 140) || (dia !== null && dia > 90)
|
||||
}
|
||||
|
||||
const isHighSystolicPressure = (value: any) => {
|
||||
const num = toNumeric(value)
|
||||
return num !== null && num > 140
|
||||
}
|
||||
|
||||
const isHighDiastolicPressure = (value: any) => {
|
||||
const num = toNumeric(value)
|
||||
return num !== null && num > 90
|
||||
}
|
||||
|
||||
const joinArray = (arr: any): string => {
|
||||
if (!arr) return ''
|
||||
if (Array.isArray(arr)) return arr.filter(Boolean).join('、')
|
||||
return String(arr)
|
||||
}
|
||||
|
||||
const fetchQueuePage = async (pageNo: number, status?: number, patientName?: string) => {
|
||||
const today = todayStr()
|
||||
const res: any = await receptionQueue({
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
<template>
|
||||
<div class="card case-card">
|
||||
<div class="card-title">
|
||||
患者病例
|
||||
<span class="case-sub">{{ caseTypeLabel }} · 诊断日期 {{ diag?.diagnosis_date || apt?.appointment_date || '—' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<div class="grid-block">
|
||||
<div class="grid-title">基本信息</div>
|
||||
<div class="kv-grid four">
|
||||
<KVItem label="诊单ID" :value="diag?.patient_id" />
|
||||
<KVItem label="姓名" :value="diag?.patient_name" />
|
||||
<KVItem label="身份证号" :value="diag?.id_card" />
|
||||
<KVItem label="手机号" :value="maskPhone(diag?.phone || apt?.patient_phone)" />
|
||||
<KVItem label="性别" :value="textOf('gender')" />
|
||||
<KVItem label="年龄" :value="diag?.age != null ? diag.age + '岁' : ''" />
|
||||
<KVItem label="婚姻状态" :value="textOf('marital_status')" />
|
||||
<KVItem label="地区" :value="diag?.region" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 生命体征 -->
|
||||
<div class="grid-block">
|
||||
<div class="grid-title">生命体征</div>
|
||||
<div class="kv-grid four">
|
||||
<KVItem label="身高" :value="diag?.height ? diag.height + ' cm' : ''" />
|
||||
<KVItem label="体重" :value="diag?.weight ? diag.weight + ' kg' : ''" />
|
||||
<MetricKVItem
|
||||
label="高压"
|
||||
:value="diag?.systolic_pressure"
|
||||
unit=" mmHg"
|
||||
:high="isHighSystolicPressure(diag?.systolic_pressure)"
|
||||
/>
|
||||
<MetricKVItem
|
||||
label="低压"
|
||||
:value="diag?.diastolic_pressure"
|
||||
unit=" mmHg"
|
||||
:high="isHighDiastolicPressure(diag?.diastolic_pressure)"
|
||||
/>
|
||||
<KVItem label="诊断类型" :value="textOf('diagnosis_type')" />
|
||||
<MetricKVItem
|
||||
label="空腹血糖"
|
||||
:value="diag?.fasting_blood_sugar"
|
||||
unit=" mmol/L"
|
||||
:high="isHighFastingBloodSugar(diag?.fasting_blood_sugar, diag?.age)"
|
||||
/>
|
||||
<KVItem label="在用药物" :value="diag?.current_medications" multiline />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主诉 -->
|
||||
<div class="grid-block">
|
||||
<div class="grid-title">主诉</div>
|
||||
<div class="kv-grid four">
|
||||
<KVItem label="当地医院诊断日期" :value="diag?.diagnosis_date" />
|
||||
<KVItem
|
||||
label="发现糖尿病病史"
|
||||
:value="formatDiabetesDiscoveryDisplay
|
||||
? formatDiabetesDiscoveryDisplay(diag?.diabetes_discovery_year)
|
||||
: diag?.diabetes_discovery_year"
|
||||
/>
|
||||
<KVItem label="当地就诊医院" :value="diag?.local_hospital_name" />
|
||||
<KVItem label="当地医院诊断结果" :value="joinArray(diag?.local_hospital_diagnosis)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 现病史(后端已翻译 *_text) -->
|
||||
<div class="grid-block">
|
||||
<div class="grid-title">现病史</div>
|
||||
<div class="kv-grid three">
|
||||
<KVItem label="口腔感觉" :value="textOf('appetite')" />
|
||||
<KVItem label="每日饮水量" :value="textOf('water_intake')" />
|
||||
<KVItem label="近月体重变化" :value="textOf('weight_change')" />
|
||||
<KVItem label="脂肪肝程度" :value="textOf('fatty_liver_degree')" />
|
||||
<KVItem label="饮食情况" :value="textOf('diet_condition')" span="2" />
|
||||
<KVItem label="肢体感觉" :value="textOf('body_feeling')" />
|
||||
<KVItem label="睡眠情况" :value="textOf('sleep_condition')" />
|
||||
<KVItem label="眼睛情况" :value="textOf('eye_condition')" />
|
||||
<KVItem label="头部感觉" :value="textOf('head_feeling')" />
|
||||
<KVItem label="出汗情况" :value="textOf('sweat_condition')" />
|
||||
<KVItem label="皮肤情况" :value="textOf('skin_condition')" />
|
||||
<KVItem label="小便情况" :value="textOf('urine_condition')" />
|
||||
<KVItem label="大便情况" :value="textOf('stool_condition')" />
|
||||
<KVItem label="腰肾情况" :value="textOf('kidney_condition')" />
|
||||
<KVItem label="其他补充" :value="diag?.symptoms" span="3" multiline />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 既往史 -->
|
||||
<div class="grid-block">
|
||||
<div class="grid-title">既往史</div>
|
||||
<div class="kv-grid three">
|
||||
<KVItem label="既往病史" :value="textOf('past_history')" span="3" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 其他病史 -->
|
||||
<div class="grid-block">
|
||||
<div class="grid-title">其他病史</div>
|
||||
<div class="kv-grid three">
|
||||
<KVItem label="外伤史" :value="textOf('trauma_history')" />
|
||||
<KVItem label="手术史" :value="textOf('surgery_history')" />
|
||||
<KVItem label="过敏史" :value="textOf('allergy_history')" />
|
||||
<KVItem label="家族病史" :value="textOf('family_history')" />
|
||||
<KVItem label="妊娠哺乳史" :value="textOf('pregnancy_history')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 病史补充 -->
|
||||
<div class="grid-block" v-if="diag?.remark">
|
||||
<div class="grid-title">病史补充</div>
|
||||
<div class="kv-grid one">
|
||||
<KVItem label="" :value="diag.remark" multiline />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 处方意见 -->
|
||||
<div class="grid-block">
|
||||
<div class="grid-title">处方意见</div>
|
||||
<div class="rx-note">
|
||||
{{ apt?.has_prescription ? '已开方。' : '当前未开方。' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h } from 'vue'
|
||||
import {
|
||||
isHighSystolicPressure,
|
||||
isHighDiastolicPressure,
|
||||
isHighFastingBloodSugar
|
||||
} from '@/utils/blood-thresholds'
|
||||
import { maskPhone, joinArray, makeTextOf } from '@/utils/diag-display'
|
||||
import { formatDiabetesDiscoveryDisplay } from '@/utils/diabetes-discovery-display'
|
||||
|
||||
interface Props {
|
||||
apt?: Record<string, any> | null
|
||||
diag?: Record<string, any> | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
apt: () => ({}),
|
||||
diag: () => ({})
|
||||
})
|
||||
|
||||
const caseTypeLabel = computed(() => {
|
||||
const d: any = props.diag || {}
|
||||
return d.consultation_type === 'follow_up' ? '复诊' : '初诊'
|
||||
})
|
||||
|
||||
const textOf = makeTextOf(() => props.diag || {})
|
||||
|
||||
// ========== 本地函数式子组件 ==========
|
||||
|
||||
/**
|
||||
* 局部渲染组件:左侧 label + 右侧 value,支持 span / 换行(与抽离前 reception 内联实现一致)
|
||||
*/
|
||||
const KVItem = (kvProps: { label: string; value: any; span?: string | number; multiline?: boolean }) => {
|
||||
const val = kvProps.value
|
||||
const empty =
|
||||
val === undefined || val === null || val === '' || (Array.isArray(val) && val.length === 0)
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: ['kv-item', kvProps.multiline ? 'multiline' : '', empty ? 'empty' : ''],
|
||||
style: kvProps.span ? `grid-column: span ${kvProps.span}` : ''
|
||||
},
|
||||
[
|
||||
h('span', { class: 'kv-label' }, kvProps.label),
|
||||
h('span', { class: 'kv-value' }, empty ? '—' : String(val))
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 数值型 KV:偏高时红色 + ↑ 箭头(与抽离前 reception 内联实现一致)
|
||||
*/
|
||||
const MetricKVItem = (mkvProps: {
|
||||
label: string
|
||||
value: any
|
||||
unit?: string
|
||||
span?: string | number
|
||||
high?: boolean
|
||||
}) => {
|
||||
const empty = mkvProps.value === undefined || mkvProps.value === null || mkvProps.value === ''
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: ['kv-item', 'metric-kv-item', empty ? 'empty' : ''],
|
||||
style: mkvProps.span ? `grid-column: span ${mkvProps.span}` : ''
|
||||
},
|
||||
[
|
||||
h('span', { class: 'kv-label' }, mkvProps.label),
|
||||
h(
|
||||
'span',
|
||||
{ class: ['kv-value', 'metric-kv-value', mkvProps.high ? 'high' : ''] },
|
||||
empty
|
||||
? '—'
|
||||
: [
|
||||
`${mkvProps.value}${mkvProps.unit || ''}`,
|
||||
mkvProps.high ? h('span', { class: 'metric-up' }, '↑') : null
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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);
|
||||
transition: box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--rx-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.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%);
|
||||
}
|
||||
}
|
||||
|
||||
.case-sub {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--rx-soft);
|
||||
font-family: var(--rx-font-data);
|
||||
}
|
||||
|
||||
.grid-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px dashed var(--rx-line);
|
||||
|
||||
&:first-of-type {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--rx-accent-strong);
|
||||
letter-spacing: 0.02em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: var(--rx-accent);
|
||||
}
|
||||
}
|
||||
|
||||
.kv-grid {
|
||||
display: grid;
|
||||
gap: 8px 16px;
|
||||
|
||||
&.one {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
&.two {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
&.three {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
&.four {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.kv-item) {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
min-width: 0;
|
||||
|
||||
&.multiline {
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.kv-label {
|
||||
color: var(--rx-soft);
|
||||
flex-shrink: 0;
|
||||
min-width: 58px;
|
||||
}
|
||||
|
||||
.kv-value {
|
||||
color: var(--rx-ink);
|
||||
word-break: break-word;
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.empty .kv-value {
|
||||
color: #c0c4cc;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&.multiline .kv-value {
|
||||
white-space: pre-wrap;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.metric-kv-item .metric-kv-value) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
&.high {
|
||||
color: #dc2626;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.metric-up) {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.rx-note {
|
||||
font-size: 13px;
|
||||
color: var(--rx-muted);
|
||||
line-height: 1.6;
|
||||
padding: 12px 14px;
|
||||
background: linear-gradient(135deg, #fafbfd 0%, #f5f7fb 100%);
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid var(--rx-accent);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div class="card patient-card">
|
||||
<div class="card-title">患者信息</div>
|
||||
<div class="patient-hero">
|
||||
<div class="patient-name">{{ apt?.patient_name || '—' }}</div>
|
||||
<div class="patient-meta">
|
||||
<div>
|
||||
{{ maskPhone(apt?.patient_phone) }} · {{ formatGender(diag?.gender) }} ·
|
||||
{{ diag?.age != null ? diag.age + '岁' : '—' }}
|
||||
</div>
|
||||
<div>
|
||||
{{ diag?.height ? diag.height + 'cm' : '—' }} /
|
||||
{{ diag?.weight ? diag.weight + 'kg' : '—' }} · {{ diag?.region || '—' }}
|
||||
</div>
|
||||
<div>
|
||||
预约:{{ apt?.appointment_date }} {{ apt?.appointment_time }} ·
|
||||
{{ formatPeriod(apt?.period) }}
|
||||
</div>
|
||||
<div>医生:{{ apt?.doctor_name || '—' }} 医助:{{ apt?.assistant_name || '—' }}</div>
|
||||
<div>
|
||||
状态:{{ apt?.status_desc || '—' }} ·
|
||||
{{ apt?.has_prescription ? '已开方' : '未开方' }}
|
||||
</div>
|
||||
<div v-if="apt?.remark">备注:{{ apt.remark }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { maskPhone, formatGender, formatPeriod } from '@/utils/diag-display'
|
||||
|
||||
interface Props {
|
||||
apt?: Record<string, any> | null
|
||||
diag?: Record<string, any> | null
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
apt: () => ({}),
|
||||
diag: () => ({})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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);
|
||||
transition: box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--rx-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.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%);
|
||||
}
|
||||
}
|
||||
|
||||
.patient-hero {
|
||||
background: linear-gradient(135deg, #f5f8ff 0%, #eef3ff 100%);
|
||||
border: 1px solid #dde7ff;
|
||||
border-radius: 12px;
|
||||
padding: 16px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
top: -20px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(79, 140, 255, 0.08) 0%, transparent 70%);
|
||||
}
|
||||
}
|
||||
|
||||
.patient-name {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--rx-ink);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.patient-meta {
|
||||
color: var(--rx-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,387 @@
|
||||
<template>
|
||||
<div class="tracking-matrix">
|
||||
<div class="toolbar">
|
||||
<el-radio-group v-model="windowDays" @change="onWindowChange">
|
||||
<el-radio-button :value="7">最近 7 天</el-radio-button>
|
||||
<el-radio-button :value="30">最近 30 天</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span class="hint">同一天血糖三段挤一格;超阈值红字↑(年龄分段:<50:空7/餐9,≥50:空8/餐10)</span>
|
||||
</div>
|
||||
|
||||
<el-empty
|
||||
v-if="visibleDates.length === 0"
|
||||
description="近期无任何打卡记录"
|
||||
:image-size="80"
|
||||
/>
|
||||
|
||||
<el-table
|
||||
v-else
|
||||
:data="rows"
|
||||
border
|
||||
stripe
|
||||
class="matrix-table"
|
||||
empty-text="暂无数据"
|
||||
row-key="date"
|
||||
max-height="640"
|
||||
>
|
||||
<el-table-column prop="dateLabel" label="日期" width="120" fixed="left" />
|
||||
<el-table-column label="血糖 (mmol/L)" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.bloodAny" class="cell-blood">
|
||||
<span :class="{ 'cell-high': row.fastingHigh }">
|
||||
空
|
||||
<span class="value">{{ row.fasting ?? '—' }}</span>
|
||||
<span v-if="row.fastingHigh" class="cell-up">↑</span>
|
||||
</span>
|
||||
<span :class="{ 'cell-high': row.postprandialHigh }">
|
||||
餐
|
||||
<span class="value">{{ row.postprandial ?? '—' }}</span>
|
||||
<span v-if="row.postprandialHigh" class="cell-up">↑</span>
|
||||
</span>
|
||||
<span :class="{ 'cell-high': row.otherHigh }">
|
||||
其他
|
||||
<span class="value">{{ row.other ?? '—' }}</span>
|
||||
<span v-if="row.otherHigh" class="cell-up">↑</span>
|
||||
</span>
|
||||
</div>
|
||||
<span v-else class="cell-empty">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="血压 (mmHg)" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.bp" :class="{ 'cell-high': row.bpHigh }">
|
||||
{{ row.bp }}
|
||||
<span v-if="row.bpHigh" class="cell-up">↑</span>
|
||||
</span>
|
||||
<span v-else class="cell-empty">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="西药" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.westernMedicine">{{ row.westernMedicine }}</span>
|
||||
<span v-else class="cell-empty">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="胰岛素" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.insulin">{{ row.insulin }}</span>
|
||||
<span v-else class="cell-empty">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="饮食打卡" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip
|
||||
v-if="row.diet"
|
||||
placement="top"
|
||||
:disabled="!row.diet"
|
||||
effect="light"
|
||||
>
|
||||
<template #content>
|
||||
<div class="diet-tip">
|
||||
<div>早:{{ row.diet.breakfast || '—' }}</div>
|
||||
<div>午:{{ row.diet.lunch || '—' }}</div>
|
||||
<div>晚:{{ row.diet.dinner || '—' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<span class="cell-checked">已打卡</span>
|
||||
</el-tooltip>
|
||||
<span v-else class="cell-empty">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="运动打卡" min-width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip v-if="row.exercise" placement="top" effect="light">
|
||||
<template #content>
|
||||
<div class="exercise-tip">
|
||||
<div>强度:{{ row.exercise.intensityText || '—' }}</div>
|
||||
<div>时长:{{ row.exercise.duration ? row.exercise.duration + ' min' : '—' }}</div>
|
||||
<div v-if="row.exercise.remark">备注:{{ row.exercise.remark }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<span class="cell-checked">
|
||||
已打卡
|
||||
<span v-if="row.exercise.duration" class="meta">· {{ row.exercise.duration }}min</span>
|
||||
<span v-if="row.exercise.intensityText" class="meta">· {{ row.exercise.intensityText }}</span>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<span v-else class="cell-empty">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import {
|
||||
isHighFastingBloodSugar,
|
||||
isHighPostprandialBloodSugar,
|
||||
isHighOtherBloodSugar,
|
||||
isHighBloodPressure,
|
||||
formatBp
|
||||
} from '@/utils/blood-thresholds'
|
||||
|
||||
interface BloodRecord {
|
||||
record_date?: string
|
||||
record_date_ts?: number
|
||||
fasting_blood_sugar?: any
|
||||
postprandial_blood_sugar?: any
|
||||
other_blood_sugar?: any
|
||||
systolic_pressure?: any
|
||||
diastolic_pressure?: any
|
||||
western_medicine?: string
|
||||
insulin?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface DietRecord {
|
||||
record_date?: string
|
||||
record_date_ts?: number
|
||||
breakfast?: string
|
||||
lunch?: string
|
||||
dinner?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface ExerciseRecord {
|
||||
record_date?: string
|
||||
record_date_ts?: number
|
||||
duration?: number
|
||||
intensity?: number
|
||||
intensity_text?: string
|
||||
remark?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface Props {
|
||||
diagnosisId?: number
|
||||
patientId?: number | null
|
||||
age?: number | null
|
||||
bloodRecords: BloodRecord[]
|
||||
dietRecords: DietRecord[]
|
||||
exerciseRecords: ExerciseRecord[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
diagnosisId: 0,
|
||||
patientId: null,
|
||||
age: null,
|
||||
bloodRecords: () => [],
|
||||
dietRecords: () => [],
|
||||
exerciseRecords: () => []
|
||||
})
|
||||
|
||||
const windowDays = ref<7 | 30>(30)
|
||||
|
||||
const onWindowChange = () => {
|
||||
// 触发 computed 重算;预留扩展点:未来可在此处通知父组件重拉
|
||||
}
|
||||
|
||||
/**
|
||||
* 把同一天可能多条的血糖记录聚合:每段取第一条非空值(与 BloodRecordLogic 趋势图一致)
|
||||
*/
|
||||
const bloodByDate = computed(() => {
|
||||
const map = new Map<string, BloodRecord>()
|
||||
for (const r of props.bloodRecords || []) {
|
||||
const d = String(r.record_date || '')
|
||||
if (!d) continue
|
||||
if (!map.has(d)) {
|
||||
map.set(d, {
|
||||
record_date: d,
|
||||
record_date_ts: r.record_date_ts
|
||||
})
|
||||
}
|
||||
const acc = map.get(d)!
|
||||
const fields: Array<keyof BloodRecord> = [
|
||||
'fasting_blood_sugar',
|
||||
'postprandial_blood_sugar',
|
||||
'other_blood_sugar',
|
||||
'systolic_pressure',
|
||||
'diastolic_pressure',
|
||||
'western_medicine',
|
||||
'insulin'
|
||||
]
|
||||
for (const f of fields) {
|
||||
if ((acc[f] === undefined || acc[f] === null || acc[f] === '') && r[f] !== undefined && r[f] !== null && r[f] !== '') {
|
||||
;(acc as any)[f] = r[f]
|
||||
}
|
||||
}
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const dietByDate = computed(() => {
|
||||
const map = new Map<string, DietRecord>()
|
||||
for (const r of props.dietRecords || []) {
|
||||
const d = String(r.record_date || '')
|
||||
if (!d) continue
|
||||
if (!map.has(d)) map.set(d, r)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const exerciseByDate = computed(() => {
|
||||
const map = new Map<string, ExerciseRecord>()
|
||||
for (const r of props.exerciseRecords || []) {
|
||||
const d = String(r.record_date || '')
|
||||
if (!d) continue
|
||||
if (!map.has(d)) map.set(d, r)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
/**
|
||||
* 取三类记录的日期并集,按 desc 排序并去重,限制最大 windowDays 条
|
||||
*/
|
||||
const visibleDates = computed<string[]>(() => {
|
||||
const set = new Set<string>()
|
||||
for (const r of props.bloodRecords || []) if (r.record_date) set.add(String(r.record_date))
|
||||
for (const r of props.dietRecords || []) if (r.record_date) set.add(String(r.record_date))
|
||||
for (const r of props.exerciseRecords || []) if (r.record_date) set.add(String(r.record_date))
|
||||
const list = Array.from(set).filter((d) => d.length === 10)
|
||||
list.sort((a, b) => (a < b ? 1 : a > b ? -1 : 0))
|
||||
return list.slice(0, windowDays.value)
|
||||
})
|
||||
|
||||
const weekText = (d: string) => {
|
||||
const ts = Date.parse(d + 'T00:00:00')
|
||||
if (!Number.isFinite(ts)) return ''
|
||||
const w = new Date(ts).getDay()
|
||||
return ['日', '一', '二', '三', '四', '五', '六'][w]
|
||||
}
|
||||
|
||||
const rows = computed(() =>
|
||||
visibleDates.value.map((date) => {
|
||||
const blood = bloodByDate.value.get(date)
|
||||
const diet = dietByDate.value.get(date)
|
||||
const exercise = exerciseByDate.value.get(date)
|
||||
|
||||
const fasting = blood?.fasting_blood_sugar ?? null
|
||||
const postprandial = blood?.postprandial_blood_sugar ?? null
|
||||
const other = blood?.other_blood_sugar ?? null
|
||||
const sys = blood?.systolic_pressure ?? null
|
||||
const dia = blood?.diastolic_pressure ?? null
|
||||
|
||||
const bloodAny =
|
||||
fasting !== null && fasting !== '' ||
|
||||
postprandial !== null && postprandial !== '' ||
|
||||
other !== null && other !== ''
|
||||
|
||||
const bp = sys || dia ? formatBp({ systolic_pressure: sys, diastolic_pressure: dia }) : ''
|
||||
|
||||
return {
|
||||
date,
|
||||
dateLabel: `${date.slice(5)}(周${weekText(date)})`,
|
||||
// 血糖
|
||||
bloodAny,
|
||||
fasting: fasting ?? '—',
|
||||
postprandial: postprandial ?? '—',
|
||||
other: other ?? '—',
|
||||
fastingHigh: isHighFastingBloodSugar(fasting, props.age),
|
||||
postprandialHigh: isHighPostprandialBloodSugar(postprandial, props.age),
|
||||
otherHigh: isHighOtherBloodSugar(other, props.age),
|
||||
// 血压
|
||||
bp,
|
||||
bpHigh: isHighBloodPressure({ systolic_pressure: sys, diastolic_pressure: dia }),
|
||||
// 用药
|
||||
westernMedicine: blood?.western_medicine || '',
|
||||
insulin: blood?.insulin || '',
|
||||
// 饮食
|
||||
diet: diet
|
||||
? {
|
||||
breakfast: diet.breakfast || '',
|
||||
lunch: diet.lunch || '',
|
||||
dinner: diet.dinner || ''
|
||||
}
|
||||
: null,
|
||||
// 运动
|
||||
exercise: exercise
|
||||
? {
|
||||
duration: exercise.duration || 0,
|
||||
intensityText: exercise.intensity_text || '',
|
||||
remark: exercise.remark || ''
|
||||
}
|
||||
: null
|
||||
}
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tracking-matrix {
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.matrix-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cell-blood {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
|
||||
> span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.value {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cell-high {
|
||||
color: #dc2626;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cell-up {
|
||||
font-size: 13px;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.cell-empty {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.cell-checked {
|
||||
color: #2563eb;
|
||||
font-weight: 500;
|
||||
|
||||
.meta {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-weight: 400;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.diet-tip,
|
||||
.exercise-tip {
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
|
||||
> div {
|
||||
margin-bottom: 2px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -116,11 +116,12 @@
|
||||
<div v-if="selectedIds.length > 0" class="list-selected">已选 {{ selectedIds.length }} 条</div>
|
||||
</div>
|
||||
<div class="table-wrap px-4">
|
||||
<el-table
|
||||
:data="pager.lists"
|
||||
<el-table
|
||||
:data="pager.lists"
|
||||
size="default"
|
||||
v-loading="pager.loading"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="goReadonly"
|
||||
:row-class-name="getRowClassName"
|
||||
class="diagnosis-table"
|
||||
stripe
|
||||
@@ -215,6 +216,20 @@
|
||||
<span v-else class="status-unprescribed">未开方</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="未服务天数" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip
|
||||
v-if="row.last_blood_record_at"
|
||||
:content="`最近一次血糖打卡:${row.last_blood_record_at}`"
|
||||
placement="top"
|
||||
>
|
||||
<span class="unserved-days" :class="unservedDaysClass(row.unserved_days)">
|
||||
{{ row.unserved_days ?? '—' }}
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<span v-else class="unserved-days unserved-muted">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="视频旁观" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="video-watch-col">
|
||||
@@ -239,9 +254,18 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300" fixed="right" align="left">
|
||||
<el-table-column label="操作" width="340" fixed="right" align="left">
|
||||
<template #default="{ row }">
|
||||
<div class="action-cell">
|
||||
<div class="action-cell" @click.stop @dblclick.stop>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="goReadonly(row)"
|
||||
v-perms="['tcm.diagnosis/readonlyDetail']"
|
||||
>
|
||||
查看
|
||||
</el-button>
|
||||
<el-button type="primary" link size="small" @click="handleEdit(row.id)" v-perms="['tcm.diagnosis/edit']">诊单</el-button>
|
||||
<el-button
|
||||
v-perms="['tcm.diagnosis/kaifang']"
|
||||
@@ -616,7 +640,7 @@ import feedback from '@/utils/feedback'
|
||||
import { hasPermission } from '@/utils/perm'
|
||||
import { Loading, Warning, List, CircleCheck, Clock, ArrowDown, Picture, User, Document, Delete, CircleClose, Remove } from '@element-plus/icons-vue'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, watch } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
@@ -944,6 +968,19 @@ const isDiagnosisConfirmed = (row: any) => {
|
||||
return records.length > 0 && records.some((r: any) => r.is_confirmed == 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 「未服务天数」着色规则(计划 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 getRowClassName = ({ row }: { row: any }) => {
|
||||
if (!isDiagnosisConfirmed(row)) return 'row-unconfirmed'
|
||||
@@ -1706,6 +1743,17 @@ const handleDeleteRecord = async (id: number) => {
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
/** 跳到只读病例页(T1+T2):双击行 / 点「查看」按钮均触发 */
|
||||
const goReadonly = (row: any) => {
|
||||
// 没分配 readonlyDetail 权限的管理员:双击行也不跳转(按钮已用 v-perms 隐藏)
|
||||
if (!hasPermission(['tcm.diagnosis/readonlyDetail'])) return
|
||||
const id = Number(row?.id)
|
||||
if (!id || id <= 0) return
|
||||
router.push({ path: '/tcm/diagnosis-readonly', query: { id: String(id) } })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 默认展示当天挂号,突出重点
|
||||
formData.appointment_date = todayStr.value
|
||||
@@ -2091,6 +2139,33 @@ onUnmounted(() => {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
/* 未服务天数(T3):null=灰;>=7=红;3-6=橙;<=2=绿 */
|
||||
.unserved-days {
|
||||
display: inline-block;
|
||||
min-width: 28px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.unserved-muted {
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.unserved-green {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.unserved-orange {
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
.unserved-red {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.diagnosis-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user