|
|
@@ -0,0 +1,407 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import { h, ref } from 'vue';
|
|
|
+
|
|
|
+import { VxeUI } from 'vxe-pc-ui';
|
|
|
+import { ArrowDownOutlined, ArrowUpOutlined, EditOutlined } from '@ant-design/icons-vue';
|
|
|
+
|
|
|
+import { useWatcher } from 'alova/client';
|
|
|
+import { getPatientTagsMethod, patientMethod } from '@/request/api/patient.api';
|
|
|
+import { getPatientDiagnosisReportMethod, getPatientHealthIndicatorMethod, getPatientHealthRecordMethod } from '@/request/api/report.api';
|
|
|
+import type { PatientModel, PatientTagVO, ReportModel } from '@/model';
|
|
|
+
|
|
|
+import UseDict from '@/core/dictionary/component';
|
|
|
+import HealthReportAnalysisWidget from '@/widgets/HealthReportAnalysisWidget.vue';
|
|
|
+
|
|
|
+const props = defineProps<{
|
|
|
+ patient: Partial<PatientModel>;
|
|
|
+ report: Partial<ReportModel>;
|
|
|
+}>();
|
|
|
+
|
|
|
+const emits = defineEmits<{
|
|
|
+ refresh: [];
|
|
|
+ destroy: [];
|
|
|
+}>();
|
|
|
+
|
|
|
+// 患者基本信息
|
|
|
+const { data: patient } = useWatcher(() => patientMethod(props.patient?.id!), [() => props.patient.id], {
|
|
|
+ initialData: { ...props.patient },
|
|
|
+ immediate: true,
|
|
|
+ middleware: (_, next) => {
|
|
|
+ if (props.patient.id) next();
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+const { data: patientTags, loading: loadPatientTagsPending } = useWatcher(() => getPatientTagsMethod(props.patient?.id!), [() => props.patient.id], {
|
|
|
+ initialData: [],
|
|
|
+ immediate: true,
|
|
|
+ middleware: (_, next) => {
|
|
|
+ if (props.patient.id) next();
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+// 患者最后一次就诊记录
|
|
|
+const { data: diagnosisRecord } = useWatcher(() => getPatientDiagnosisReportMethod(0, props.patient?.id!), [() => props.patient.id], {
|
|
|
+ initialData: { disease: {}, symptom: {} },
|
|
|
+ immediate: true,
|
|
|
+ middleware: (_, next) => {
|
|
|
+ if (props.patient.id) next();
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+// 患者最后一次健康分析报告
|
|
|
+const { data: healthRecord } = useWatcher(() => getPatientHealthRecordMethod(props.report?.id!), [() => props.report.id], {
|
|
|
+ initialData: {},
|
|
|
+ immediate: true,
|
|
|
+ middleware: (_, next) => {
|
|
|
+ if (props.report.id) next();
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+const { data: indicator } = useWatcher(() => getPatientHealthIndicatorMethod(props.patient?.id!), [() => props.patient.id], {
|
|
|
+ initialData: [],
|
|
|
+ immediate: true,
|
|
|
+ middleware: (_, next) => {
|
|
|
+ if (props.patient.id) next();
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+const panels = shallowReactive([
|
|
|
+ {
|
|
|
+ id: 'patient-diagnosis-records',
|
|
|
+ title: '就诊记录',
|
|
|
+ component: defineAsyncComponent(() => import('@/widgets/PatientDiagnosisRecordsWidget.vue')),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'patient-health-records',
|
|
|
+ title: '健康分析记录',
|
|
|
+ component: defineAsyncComponent(() => import('@/widgets/PatientHealthRecordsWidget.vue')),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'patient-physicalSign-records',
|
|
|
+ title: '生理指标',
|
|
|
+ component: defineAsyncComponent(() => import('@/widgets/PatientPhysicalSignRecordsWidget.vue')),
|
|
|
+ },
|
|
|
+]);
|
|
|
+
|
|
|
+const activePanel = ref(panels[0].id);
|
|
|
+const activePanelChange = (event: Event) => {
|
|
|
+ (event.target as HTMLElement)?.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
|
|
+};
|
|
|
+
|
|
|
+function openPatientTagEdit(event: MouseEvent) {
|
|
|
+ const width = 500;
|
|
|
+ const offset = 32;
|
|
|
+ const component = defineAsyncComponent(() => import('@/components/PatientTagEdit.vue'));
|
|
|
+ const id = `PatientTagEdit`;
|
|
|
+ VxeUI.modal.open({
|
|
|
+ id,
|
|
|
+ title: '标签',
|
|
|
+ type: 'modal',
|
|
|
+ position: {
|
|
|
+ top: event.pageY + offset,
|
|
|
+ left: event.pageX - width,
|
|
|
+ },
|
|
|
+ escClosable: true,
|
|
|
+ resize: true,
|
|
|
+ width,
|
|
|
+ minWidth: width,
|
|
|
+ mask: false,
|
|
|
+ slots: {
|
|
|
+ default() {
|
|
|
+ return h(component, {
|
|
|
+ id: patient.value.id,
|
|
|
+ tags: patientTags.value,
|
|
|
+ onDestroy(values?: PatientTagVO[]) {
|
|
|
+ if (values) {
|
|
|
+ patientTags.value = values;
|
|
|
+ emits('refresh');
|
|
|
+ }
|
|
|
+ VxeUI.modal.close(id);
|
|
|
+ },
|
|
|
+ });
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function openPatientRecordsPreview() {
|
|
|
+ const component = defineAsyncComponent(() => import('@/components/RecordsPatientPreview.vue'));
|
|
|
+ const id = `modal:record-patient:preview`;
|
|
|
+ const onDestroy = () => { VxeUI.modal.close(id); };
|
|
|
+ onDestroy();
|
|
|
+ VxeUI.modal.open({
|
|
|
+ id, remember: true,
|
|
|
+ showMaximize: true, mask: false, lockView: false, padding: false,
|
|
|
+ resize: true, width: Math.floor(window.innerWidth * 0.5), height: Math.floor(window.innerHeight * 0.5),
|
|
|
+ escClosable: true, maskClosable: true,
|
|
|
+ title: `基础信息更新记录`,
|
|
|
+ slots: {
|
|
|
+ default() {
|
|
|
+ return h(component, {
|
|
|
+ patient: patient.value,
|
|
|
+ onDestroy,
|
|
|
+ });
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function openIndicatorRecordsPreview() {
|
|
|
+ const component = defineAsyncComponent(() => import('@/components/RecordsIndicatorPreview.vue'));
|
|
|
+ const id = `modal:record-indicator:preview`;
|
|
|
+ const onDestroy = () => { VxeUI.modal.close(id); };
|
|
|
+ onDestroy();
|
|
|
+ VxeUI.modal.open({
|
|
|
+ id, remember: true,
|
|
|
+ showMaximize: true, mask: false, lockView: false, padding: false,
|
|
|
+ resize: true, width: Math.floor(window.innerWidth * 0.5), height: Math.floor(window.innerHeight * 0.5),
|
|
|
+ escClosable: true, maskClosable: true,
|
|
|
+ title: `指标信息更新记录`,
|
|
|
+ slots: {
|
|
|
+ default() {
|
|
|
+ return h(component, {
|
|
|
+ patient: patient.value,
|
|
|
+ onDestroy,
|
|
|
+ });
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="p-6">
|
|
|
+ <div class="flex">
|
|
|
+ <section class="flex-auto">
|
|
|
+ <header class="flex items-center">
|
|
|
+ <div class="title">基本信息</div>
|
|
|
+ <a-button type="link" @click="openPatientRecordsPreview">更新记录</a-button>
|
|
|
+ </header>
|
|
|
+ <main>
|
|
|
+ <div class="row">
|
|
|
+ <a-space class="separate" :size="0">
|
|
|
+ <span>{{ patient.name }}</span>
|
|
|
+ <UseDict v-slot="{ value }" sign="sys_user_sex" :raw="patient.gender">
|
|
|
+ <span v-if="value">{{ value }}</span>
|
|
|
+ </UseDict>
|
|
|
+ <span v-if="patient.age">{{ patient.age }} 岁</span>
|
|
|
+ <UseDict v-slot="{ value }" sign="women_special_period" :raw="patient.womenSpecialPeriod">
|
|
|
+ <span v-if="value && value !== '无'">{{ value }}</span>
|
|
|
+ </UseDict>
|
|
|
+ <UseDict v-slot="{ value }" sign="job" :raw="patient.job">
|
|
|
+ <span v-if="value && value !== '无'">{{ value }}</span>
|
|
|
+ </UseDict>
|
|
|
+ <span v-if="patient.phone"><label>手机号</label> {{ patient.phone }}</span>
|
|
|
+ <span v-if="patient.cardno"><label>身份证号</label> {{ patient.cardno }}</span>
|
|
|
+ </a-space>
|
|
|
+ </div>
|
|
|
+ <div class="row">
|
|
|
+ <a-space :size="40">
|
|
|
+ <span v-if="patient.height"><label>身高</label> {{ patient.height }} cm</span>
|
|
|
+ <span v-if="patient.weight"><label>体重</label> {{ patient.weight }} kg</span>
|
|
|
+ <UseDict v-slot="{ value }" sign="sys_yes_no" :raw="patient.drinkState">
|
|
|
+ <span v-if="value"><label>饮酒</label> {{ value }}</span>
|
|
|
+ </UseDict>
|
|
|
+ <UseDict v-slot="{ value }" sign="sys_yes_no" :raw="patient.smokeState">
|
|
|
+ <span v-if="value"><label>抽烟</label> {{ value }}</span>
|
|
|
+ </UseDict>
|
|
|
+ <UseDict v-slot="{ value }" sign="food_allergy" :raw="patient.foodAllergy2" :multiple="true" :separator="[',', '、']">
|
|
|
+ <span v-if="value"><label>食物过敏</label> {{ value }}</span>
|
|
|
+ </UseDict>
|
|
|
+ <UseDict v-slot="{ value }" sign="hobby_flavor" :raw="patient.hobbyFlavor" :multiple="true" :separator="[',', '、']">
|
|
|
+ <span v-if="value"><label>喜好口味</label> {{ value }}</span>
|
|
|
+ </UseDict>
|
|
|
+ </a-space>
|
|
|
+ </div>
|
|
|
+ </main>
|
|
|
+ </section>
|
|
|
+ <section class="flex-none min-w-100px max-w-400px">
|
|
|
+ <label>标签:</label>
|
|
|
+ <a-spin v-if="loadPatientTagsPending"></a-spin>
|
|
|
+ <template v-else>
|
|
|
+ <a-tag v-for="tag in patientTags" :key="tag.id">{{ tag.name }}</a-tag>
|
|
|
+ <a-button type="link" @click="openPatientTagEdit($event)">
|
|
|
+ <template #icon>
|
|
|
+ <EditOutlined />
|
|
|
+ </template>
|
|
|
+ </a-button>
|
|
|
+ </template>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ <section class="mt-4">
|
|
|
+ <header class="flex items-center">
|
|
|
+ <div class="title">健康状况</div>
|
|
|
+ </header>
|
|
|
+ <main>
|
|
|
+ <div class="row" v-if="diagnosisRecord">
|
|
|
+ <header>
|
|
|
+ <label>诊断</label>
|
|
|
+ <span>{{ diagnosisRecord.disease?.name }} - {{ diagnosisRecord.symptom?.name }}</span>
|
|
|
+ <span v-if="diagnosisRecord.date">({{ diagnosisRecord.date }})</span>
|
|
|
+ </header>
|
|
|
+ </div>
|
|
|
+ <div class="row">
|
|
|
+ <header>
|
|
|
+ <label>健康状态</label>
|
|
|
+ <span>{{ healthRecord.result?.status }}</span>
|
|
|
+ <span v-if="healthRecord.date">({{ healthRecord.date }})</span>
|
|
|
+ </header>
|
|
|
+ <main>
|
|
|
+ <div class="row">
|
|
|
+ <a-space :size="24">
|
|
|
+ <a-space direction="vertical" :size="12">
|
|
|
+ <span><label>程度</label>{{ healthRecord.result?.level }}</span>
|
|
|
+ <span><label>表现</label>{{ healthRecord.result?.description }}</span>
|
|
|
+ <span><label>症素</label>{{ healthRecord.syndromeElement?.label }}</span>
|
|
|
+ </a-space>
|
|
|
+ <a-space direction="vertical" :size="12">
|
|
|
+ <span><label>类型</label>{{ healthRecord.result?.category }}</span>
|
|
|
+ <span><label>体质</label>{{ healthRecord.physique?.label }}</span>
|
|
|
+ <span><label>证型</label>{{ healthRecord.syndrome?.label }}</span>
|
|
|
+ </a-space>
|
|
|
+ </a-space>
|
|
|
+ </div>
|
|
|
+ </main>
|
|
|
+ </div>
|
|
|
+ <div class="row">
|
|
|
+ <header>
|
|
|
+ <label>症状</label>
|
|
|
+ <span>{{ healthRecord.symptom?.value || ' - ' }}</span>
|
|
|
+ <span v-if="healthRecord.symptom?.duration">,{{ healthRecord.symptom?.duration }}</span>
|
|
|
+ <span v-if="healthRecord.symptom?.influence">,{{ healthRecord.symptom?.influence }}</span>
|
|
|
+ </header>
|
|
|
+ </div>
|
|
|
+ <HealthReportAnalysisWidget class="row" category="tongue" :analysis="healthRecord.analysis"></HealthReportAnalysisWidget>
|
|
|
+ <HealthReportAnalysisWidget class="row" category="face" :analysis="healthRecord.analysis"></HealthReportAnalysisWidget>
|
|
|
+ <div class="row" v-if="indicator.length">
|
|
|
+ <header>
|
|
|
+ <label>生理指标</label>
|
|
|
+ <a-button type="link" @click="openIndicatorRecordsPreview">更新记录</a-button>
|
|
|
+ </header>
|
|
|
+ <main>
|
|
|
+ <div class="flex flex-wrap">
|
|
|
+ <div class="text-center w-260px row" v-for="item in indicator" :key="item.id">
|
|
|
+ <div class="flex justify-center">
|
|
|
+ <span><label>{{ item.name }}</label>{{ item.value }}{{ item.unit }}</span>
|
|
|
+ <div class="inline-block ml-2 size-24px">
|
|
|
+ <a-button v-if="item.trend > 0" :icon="h(ArrowUpOutlined)" shape="circle" size="small" class="trend-up" />
|
|
|
+ <a-button v-else-if="item.trend < 0" :icon="h(ArrowDownOutlined)" shape="circle" size="small" class="trend-down" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="text-center mt-1" style="font-size: 14px; color: rgba(0, 0, 0, 0.45)">{{ item.date }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </main>
|
|
|
+ </div>
|
|
|
+ </main>
|
|
|
+ </section>
|
|
|
+ <a-tabs class="panel-wrapper" v-model:activeKey="activePanel">
|
|
|
+ <a-tab-pane v-for="panel in panels" :key="panel.id">
|
|
|
+ <component :is="panel.component" :patient="patient"></component>
|
|
|
+ </a-tab-pane>
|
|
|
+ <template #renderTabBar>
|
|
|
+ <a-radio-group v-model:value="activePanel" @change="activePanelChange($event.nativeEvent)">
|
|
|
+ <a-radio-button v-for="panel in panels" :key="panel.id" :value="panel.id" :disabled="panel.disabled">
|
|
|
+ {{ panel.title }}
|
|
|
+ </a-radio-button>
|
|
|
+ </a-radio-group>
|
|
|
+ </template>
|
|
|
+ </a-tabs>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+section {
|
|
|
+ color: rgba(0, 0, 0, 0.85);
|
|
|
+
|
|
|
+ > header {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 700;
|
|
|
+
|
|
|
+ :deep(.ant-btn-link) {
|
|
|
+ padding-block: 0;
|
|
|
+ font-size: 18px;
|
|
|
+ border: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ > main {
|
|
|
+ margin-left: 18px * 4;
|
|
|
+ padding: 0 15px;
|
|
|
+ font-size: 16px;
|
|
|
+ color: rgba(0, 0, 0, 0.85);
|
|
|
+
|
|
|
+ > .row > .ant-space {
|
|
|
+ font-size: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .row {
|
|
|
+ padding: 12px 0;
|
|
|
+
|
|
|
+ span > label {
|
|
|
+ color: rgba(0, 0, 0, 0.45);
|
|
|
+ }
|
|
|
+
|
|
|
+ label::after {
|
|
|
+ margin-left: 2px;
|
|
|
+ margin-right: 8px;
|
|
|
+ content: ':';
|
|
|
+ }
|
|
|
+
|
|
|
+ > header::before {
|
|
|
+ $size: 10px;
|
|
|
+ content: '';
|
|
|
+ display: inline-block;
|
|
|
+ margin-right: 12px;
|
|
|
+ width: $size;
|
|
|
+ height: $size;
|
|
|
+ border: 2px solid #1d6ff6;
|
|
|
+ border-radius: 50%;
|
|
|
+ }
|
|
|
+
|
|
|
+ > main {
|
|
|
+ margin-left: 18px * 2;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .ant-tag {
|
|
|
+ margin-top: 6px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.separate {
|
|
|
+ :deep(.ant-space-item) {
|
|
|
+ & + .ant-space-item::before {
|
|
|
+ content: ',';
|
|
|
+ margin-right: 2px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ span + span::before {
|
|
|
+ content: ',';
|
|
|
+ margin-right: 2px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.panel-wrapper {
|
|
|
+ :deep(.ant-tabs-content-holder) {
|
|
|
+ padding-top: 12px;
|
|
|
+ height: calc(100vh - 60px - 24px - 32px);
|
|
|
+
|
|
|
+ .ant-tabs-content {
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.trend-up {
|
|
|
+ color: #ff4d4f;
|
|
|
+ border-color: #ff4d4f;
|
|
|
+}
|
|
|
+
|
|
|
+.trend-down {
|
|
|
+ color: #87d068;
|
|
|
+ border-color: #87d068;
|
|
|
+}
|
|
|
+</style>
|