| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451 |
- <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>;
- source?: string;
- sourceInsName?: string;
- }>();
- 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-followUp-records',
- title: '就诊随访',
- component: defineAsyncComponent(() => import('@/widgets/PatientFollowUpRecordsWidget.vue')),
- },
- {
- id: 'patient-service-records',
- title: '调养记录',
- component: defineAsyncComponent(() => import('@/widgets/PatientCareRecordsWidget.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`;
- console.log(patient.value, 'patient======');
- 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,
- records: indicator.value,
- onDestroy,
- });
- },
- },
- });
- }
- </script>
- <template>
- <div class="p-6">
- <div class="flex">
- <section class="flex-auto">
- <div class="row mb-5" v-if="source || sourceInsName">
- <span class="text-lg mr-12 font-bold">来源</span>
- <a-tag color="pink">{{ source }}</a-tag>
- <a-tag color="red">{{ sourceInsName }}</a-tag>
- </div>
- <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" :color="tag.color">{{ 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">
- .title-text {
- font-size: 16px;
- color: rgba(0, 0, 0, 0.45);
- }
- 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>
|