feat: 病历模块优化

This commit is contained in:
李琦
2026-08-05 17:50:32 +08:00
parent bd5b0b2de1
commit 9c629ff8ee
10 changed files with 1961 additions and 949 deletions

View File

@@ -62,7 +62,7 @@ const downloadQRCode = async (name: string, isQr = false) => {
};
</script>
<template>
<Modal :title="`${previewTitle}二维码`" class="w-[50%]">
<Modal :title="`${previewTitle}二维码`">
<div class="qrBox" style="text-align: center">
<div ref="htmlToImage" class="qrModal">
<div id="qrcode" ref="qrCodeUrl" class="qrcode">

View File

@@ -1,12 +1,15 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { useVbenModal, useVbenDrawer } from '@vben/common-ui';
import {
BookOutlined,
ClearOutlined,
DeleteTwoTone,
LeftOutlined,
RightOutlined,
FileTextOutlined,
HistoryOutlined,
MedicineBoxOutlined,
MinusOutlined,
PlusOutlined,
SaveOutlined,
@@ -17,7 +20,6 @@ import {
Button,
Card,
Col,
Descriptions,
Empty,
Image,
ImagePreviewGroup,
@@ -34,9 +36,6 @@ import {
Tabs,
TabPane,
Textarea,
Timeline,
TimelineItem,
Tooltip,
} from 'ant-design-vue';
import {
@@ -48,11 +47,15 @@ import { hasVipPermission } from '#/utils/vip';
import MedicalRecordPanel from '#/views/doctor/medical-record/MedicalRecordPanel.vue';
import EntryKeywordBubble from '#/views/doctor/medical-record/components/EntryKeywordBubble.vue';
import CommonDxOrderChips from '#/views/doctor/medical-record/components/CommonDxOrderChips.vue';
import PatientMedicalRecordDrawer from '#/views/doctor/medical-record/components/PatientMedicalRecordDrawer.vue';
import { saveMedicalRecord } from '#/views/doctor/medical-record/api';
import {
loadRxMrTab,
saveRxMrTab,
} from '#/views/doctor/medical-record/utils/localDraft';
import PatientBriefBar from '#/views/doctor/doctor-reception/components/PatientBriefBar.vue';
import PatientInfoDrawer from '#/views/doctor/doctor-reception/components/PatientInfoDrawer.vue';
import PatientPrescriptionHistoryDrawer from '#/views/doctor/doctor-reception/components/PatientPrescriptionHistoryDrawer.vue';
// import { getTraditionalChineseMedicineAllApi } from '#/views/doctor/doctor-reception/api';
// 导入子组件
import PrescriptionDetail from '#/views/doctor/doctor-reception/components/PrescriptionDetail.vue';
@@ -66,14 +69,13 @@ import InfoModal from '#/components/modal/InfoModal.vue';
import { DrugSearchSelect } from '#/components/drug-search-select';
import { formatPriceDisplay } from '#/utils/formatPrice';
import ChineseMedicineConfig from '#/views/doctor/doctor-reception/components/ChineseMedicineConfig.vue';
import { getDeliveryWarehouseOptionsByDrugs } from '#/views/doctor/doctor-reception/api';
import { getDeliveryWarehouseOptionsByDrugs, getPrescriptionInfoApi } from '#/views/doctor/doctor-reception/api';
import WarehouseSelectModal from './WarehouseSelectModal.vue';
import { detailDoctorOrderToText } from '#/utils/prescriptionDoctorOrder';
const splitString = (input: string) => input.split(',');
const modalData = ref<any>();
// 修改:默认折叠 (true)
const isPatientInfoCollapsed = ref(true);
const visible = ref(false);
const previewImage = ref([]);
const showNewDrugModal = ref(false);
@@ -617,9 +619,167 @@ const checkAndShowTransferTip = async () => {
}
};
const togglePatientInfo = () => {
isPatientInfoCollapsed.value = !isPatientInfoCollapsed.value;
};
const [PatientInfoDrawerComp, patientInfoDrawerApi] = useVbenDrawer({
connectedComponent: PatientInfoDrawer,
});
const [PatientMrDrawerComp, patientMrDrawerApi] = useVbenDrawer({
connectedComponent: PatientMedicalRecordDrawer,
});
const [PatientRxHistoryDrawerComp, patientRxHistoryDrawerApi] = useVbenDrawer({
connectedComponent: PatientPrescriptionHistoryDrawer,
});
function openPatientInfoDrawer() {
patientInfoDrawerApi.setData({
patient: prescriptionStore.activePatient,
register: prescriptionStore.patientInfo,
healthInquiry: prescriptionStore.userPatientHealthInquiry,
patientName: prescriptionStore.activePatient?.name,
});
patientInfoDrawerApi.open();
}
function openPatientMrCiteDrawer() {
const patientId = medicalRecordPatientId.value;
if (!patientId) {
message.warning('无就诊人信息');
return;
}
if (canUseMedicalRecord.value) {
rxMrTab.value = 'mr';
}
const panel = medicalRecordPanelRef.value as any;
if (panel?.openCiteHistory) {
panel.openCiteHistory();
return;
}
patientMrDrawerApi.setData({
mode: 'cite',
userPatientId: patientId,
storeId: medicalRecordStoreId.value || undefined,
patientName: prescriptionStore.activePatient?.name,
excludeRegisterId: medicalRecordRegisterId.value,
onCite: (record: Record<string, any>) => {
panel?.applyCitedRecord?.(record);
if (record?.diagnosis) prescriptionStore.diagnosis = String(record.diagnosis);
if (record?.doctor_order) prescriptionStore.medicalAdvice = String(record.doctor_order);
},
});
patientMrDrawerApi.open();
}
/**
* 在线开方复用历史处方:拉取详情写入 store 草稿
*/
async function reusePrescriptionFromList(prescriptionId: number) {
const go = async () => {
try {
const detail = await getPrescriptionInfoApi(prescriptionId);
const content =
typeof detail?.content === 'string'
? JSON.parse(detail.content || '{}')
: detail?.content || detail || {};
const pt = Number(detail.prescription_type);
if (![1, 2, 3, 5, 6, 7].includes(pt)) {
message.warning('该处方类型暂不支持复用');
return;
}
prescriptionStore.activeCategory = pt;
const repiceList = Array.isArray(content.repice) ? content.repice : [];
if (pt === 1) {
const rep0 = repiceList[0];
let rows: any[] = [];
try {
const raw = rep0?.content;
rows = typeof raw === 'string' ? JSON.parse(raw) : raw || [];
} catch {
message.error('解析中药处方失败');
return;
}
prescriptionStore.updateCurrentDrugs(
(rows || []).map((d: any) => {
const drugId = Number(d.drug_id ?? d.id);
return {
index_id: d.id ?? drugId,
id: drugId,
drug_name: d.name || d.drug_name || '',
number: Number(d.number ?? d.amount ?? 0),
way_id: d.way_id ?? d.use_way?.id ?? d.order,
price: Number(d.price ?? d.sell_price ?? 0),
unit: d.unit,
};
}),
);
if (rep0?.dosage != null) prescriptionStore.dosage = Number(rep0.dosage) || prescriptionStore.dosage;
if (rep0?.day_dosage != null) {
prescriptionStore.dayDosage = Number(rep0.day_dosage) || prescriptionStore.dayDosage;
}
} else {
const mapped: any[] = [];
for (const rep of repiceList) {
let drugs: any[] = [];
try {
const raw = rep?.content;
drugs = typeof raw === 'string' ? JSON.parse(raw) : raw || [];
} catch {
continue;
}
for (const d of drugs) {
mapped.push({
index_id: d.id ?? d.drug_id,
id: Number(d.drug_id ?? d.id),
drug_name: d.name || d.drug_name || '',
number: Number(d.number ?? 1),
price: Number(d.price ?? d.sell_price ?? 0),
unit: d.unit,
use_num: d.use_num,
use_type: d.use_type,
use_frequency: d.use_frequency,
time_id: d.time_id,
type_id: d.type_id,
frequency_id: d.frequency_id,
unit_id: d.unit_id,
select_number: d.select_number ?? 1,
});
}
}
prescriptionStore.updateCurrentDrugs(mapped);
}
prescriptionStore.diagnosis =
detail.clinical_diagnose ?? content.clinical_diagnose ?? '';
prescriptionStore.medicalAdvice =
content.doctor_order ?? detailDoctorOrderToText(detail.doctor_order) ?? '';
prescriptionStore.category =
detail.category === 2 || detail.category === '2' || content.category === '医保'
? 2
: 1;
message.success('已复用到开方区,请核对后发送');
} catch (e) {
console.error(e);
message.error('加载处方详情失败');
}
};
if (prescriptionStore.currentDrugs.length > 0) {
AntModal.confirm({
title: '覆盖当前草稿?',
content: '当前处方清单中有药品,复用将替换为所选历史处方内容。',
onOk: go,
});
} else {
await go();
}
}
function openPatientRxHistoryDrawer() {
patientRxHistoryDrawerApi.setData({
registerId: medicalRecordRegisterId.value,
userPatientId: medicalRecordPatientId.value,
patientName: prescriptionStore.activePatient?.name,
allowWithdraw: false,
onReuse: reusePrescriptionFromList,
});
patientRxHistoryDrawerApi.open();
}
// 打开西药模态框
const openWesternModal = () => {
@@ -978,233 +1138,149 @@ const cancelSaveCommonPrescription = () => {
</ImagePreviewGroup>
</div>
<!-- 左侧患者信息区域 (支持折叠) -->
<div
class="left-panel flex flex-col border-r pr-2 transition-all duration-300 ease-in-out"
:class="isPatientInfoCollapsed ? 'w-12 items-center' : 'w-80'"
>
<!-- 切换按钮栏 -->
<div class="flex w-full items-center justify-between border-b pb-2 mb-2" :class="{'flex-col gap-2 border-b-0': isPatientInfoCollapsed}">
<h2 v-if="!isPatientInfoCollapsed" class="text-lg font-semibold whitespace-nowrap overflow-hidden">
患者信息
<span v-if="prescriptionStore.activePatient" class="ml-2 text-xs text-green-500">(已加载)</span>
</h2>
<Tooltip :title="isPatientInfoCollapsed ? '展开患者信息' : '折叠'">
<Button type="text" size="small" @click="togglePatientInfo" class="hover:bg-gray-100 rounded">
<RightOutlined v-if="isPatientInfoCollapsed" />
<LeftOutlined v-else />
</Button>
</Tooltip>
</div>
<!-- 折叠状态下的垂直文字/图标 -->
<div v-if="isPatientInfoCollapsed" class="flex flex-col items-center gap-4 mt-4 h-full cursor-pointer" @click="togglePatientInfo">
<UserOutlined class="text-xl text-gray-500"/>
<div class="vertical-text text-gray-500 font-medium tracking-widest text-sm">患者详情</div>
</div>
<!-- 展开状态下的内容 -->
<div v-show="!isPatientInfoCollapsed" class="flex-1 overflow-y-auto pr-1">
<!-- 调试信息 -->
<div
v-if="!prescriptionStore.activePatient"
class="mb-4 rounded border border-yellow-200 bg-yellow-50 p-4 text-xs"
>
<p class="text-yellow-800 font-bold mb-1">调试信息</p>
<p>ID: {{ prescriptionStore.currentRegisterId }}</p>
<p>状态: {{ prescriptionStore.activePatient ? '已加载' : '未加载' }}</p>
</div>
<!-- 患者信息 (改为单列显示以适应侧边栏) -->
<Descriptions
v-if="prescriptionStore.activePatient"
:column="1"
bordered
size="small"
class="mb-4"
>
<Descriptions.Item label="姓名">{{ prescriptionStore.activePatient.name || '未填写' }}</Descriptions.Item>
<Descriptions.Item label="年龄">{{ prescriptionStore.activePatient.age || '未填写' }}</Descriptions.Item>
<Descriptions.Item label="性别">
{{
prescriptionStore.activePatient.sex === 1
? '男'
: prescriptionStore.activePatient.sex === 2
? '女'
: '未填写'
}}
</Descriptions.Item>
</Descriptions>
<!-- 健康信息 -->
<Descriptions
v-if="prescriptionStore.userPatientHealthInquiry"
:column="1"
bordered
size="small"
class="mb-4"
title="健康信息"
>
<Descriptions.Item label="肝功能">
<span :class="{'text-red-500': prescriptionStore.userPatientHealthInquiry.liver_function === 1}">
{{ prescriptionStore.userPatientHealthInquiry.liver_function === 0 ? '正常' : '异常' }}
</span>
<div v-if="prescriptionStore.userPatientHealthInquiry.liver_function === 1 && prescriptionStore.userPatientHealthInquiry.liver_index" class="mt-1 flex flex-wrap gap-1">
<Tag v-for="item in splitString(prescriptionStore.userPatientHealthInquiry.liver_index)" :key="item" color="#455cda" class="mr-0 mb-1 text-xs">{{ item }}</Tag>
</div>
</Descriptions.Item>
<Descriptions.Item label="肾功能">
<span :class="{'text-red-500': prescriptionStore.userPatientHealthInquiry.renal_function === 1}">
{{ prescriptionStore.userPatientHealthInquiry.renal_function === 0 ? '正常' : '异常' }}
</span>
<div v-if="prescriptionStore.userPatientHealthInquiry.renal_function === 1 && prescriptionStore.userPatientHealthInquiry.renal_index" class="mt-1 flex flex-wrap gap-1">
<Tag v-for="item in splitString(prescriptionStore.userPatientHealthInquiry.renal_index)" :key="item" color="#455cda" class="mr-0 mb-1 text-xs">{{ item }}</Tag>
</div>
</Descriptions.Item>
<Descriptions.Item label="既往史">
{{ prescriptionStore.userPatientHealthInquiry.person_status === 0 ? '无' : '有' }}
<div v-if="prescriptionStore.userPatientHealthInquiry.person_status === 1" class="mt-1 flex flex-wrap gap-1">
<Tag v-for="item in splitString(prescriptionStore.userPatientHealthInquiry.person_history)" :key="item" color="#455cda" class="mr-0 mb-1 text-xs">{{ item }}</Tag>
</div>
</Descriptions.Item>
<Descriptions.Item label="过敏史">
{{ prescriptionStore.userPatientHealthInquiry.allergic_status === 0 ? '无' : '有' }}
<div v-if="prescriptionStore.userPatientHealthInquiry.allergic_status === 1" class="mt-1 flex flex-wrap gap-1">
<Tag v-for="item in splitString(prescriptionStore.userPatientHealthInquiry.allergic_history)" :key="item" color="#455cda" class="mr-0 mb-1 text-xs">{{ item }}</Tag>
</div>
</Descriptions.Item>
<Descriptions.Item label="家族史">
{{ prescriptionStore.userPatientHealthInquiry.family_status === 0 ? '无' : '有' }}
<div v-if="prescriptionStore.userPatientHealthInquiry.family_status === 1" class="mt-1 flex flex-wrap gap-1">
<Tag v-for="item in splitString(prescriptionStore.userPatientHealthInquiry.family_history)" :key="item" color="#455cda" class="mr-0 mb-1 text-xs">{{ item }}</Tag>
</div>
</Descriptions.Item>
</Descriptions>
<!-- 处方记录 -->
<div class="mb-5 mt-5">
<div class="font-bold mb-2 text-base">历史处方</div>
<div
v-if="
prescriptionStore.prescriptionList &&
prescriptionStore.prescriptionList.length > 0
"
>
<Timeline>
<TimelineItem
v-for="item in prescriptionStore.prescriptionList"
:key="item.id"
>
<div class="flex flex-col gap-1 text-xs">
<div class="flex justify-between items-center">
<Tag color="#455cda">{{ item.prescription_no }}</Tag>
<Button type="link" size="small" @click="openPrescriptionDetail(item)" class="p-0 h-auto">查看</Button>
</div>
<span class="text-gray-400">{{ item.created_at }}</span>
<div>
<Tag :color="item.category === 1 ? '' : '#455cda'" class="mr-1 scale-90 origin-left">
{{
item.prescription_type === 1
? '中药'
: item.prescription_type === 3
? '保健'
: item.prescription_type === 5
? '服务包'
: item.prescription_type === 6
? '非药'
: item.prescription_type === 7
? '器械'
: '西药'
}}
</Tag>
<!-- 状态标签简化 -->
<Tag v-if="item.status === 0" color="warning" class="scale-90">待审</Tag>
<Tag v-else-if="item.status === 1" color="success" class="scale-90">通过</Tag>
<Tag v-else-if="item.status === 2" color="error" class="scale-90">拒</Tag>
</div>
</div>
</TimelineItem>
</Timeline>
</div>
<div v-else class="py-4 text-center text-gray-400 text-sm">
暂无处方记录
</div>
</div>
</div>
</div>
<!-- 右侧:处方开具区域 (占据剩余空间) -->
<div class="right-panel flex-1 h-full overflow-y-auto px-2">
<!-- 开方工作台顶栏固定下方内容独立滚动 -->
<div class="right-panel rx-workspace flex-1 h-full px-2">
<div class="rx-workspace-sticky">
<PatientBriefBar
:register="prescriptionStore.patientInfo"
:patient="prescriptionStore.activePatient"
:category="prescriptionStore.category"
/>
<Tabs
v-if="canUseMedicalRecord"
v-model:active-key="rxMrTab"
class="mb-2"
class="mb-0 rx-primary-tabs"
>
<TabPane key="rx" tab="处方" />
<TabPane key="mr" tab="病历" />
<TabPane v-if="canUseMedicalRecord" key="mr" tab="病历" />
<template #rightExtra>
<div class="rx-tab-extra-actions">
<Button type="link" size="small" @click="openPatientInfoDrawer">
<UserOutlined />
患者信息
</Button>
<Button type="link" size="small" @click="openPatientMrCiteDrawer">
<FileTextOutlined />
患者病历
</Button>
<Button type="link" size="small" @click="openPatientRxHistoryDrawer">
<MedicineBoxOutlined />
处方记录
</Button>
</div>
</template>
</Tabs>
<div v-show="!canUseMedicalRecord || rxMrTab === 'rx'">
<!-- 药品分类导航 -->
<div class="drug-categories sticky top-0 bg-white dark:bg-[#151515] z-10 py-2 border-b mb-4">
<button
v-for="categoryItem in categories"
:id="`rx-modal-tab-${categoryItem.value}`"
:key="categoryItem.value"
:class="{
active: prescriptionStore.activeCategory === categoryItem.value,
}"
@click="tabChange(categoryItem.value)"
<!-- 处方工具栏仅处方态吸顶 -->
<div
v-if="!canUseMedicalRecord || rxMrTab === 'rx'"
class="mb-1 flex flex-wrap items-center gap-1"
>
<RadioGroup
v-if="allowInsuranceCategory"
v-model:value="prescriptionStore.category"
size="small"
>
<img
v-if="categoryItem.icon"
class="tab-type-icon"
:src="categoryItem.icon"
alt=""
/>
<span>{{ categoryItem.label }}</span>
<span v-if="categoryItem.icon_text" class="tab-type-badge">{{ categoryItem.icon_text }}</span>
</button>
</div>
<div class="rx-swipe-panel">
<!-- 费用类型选择和添加商品按钮 -->
<div class="mb-4 flex items-center justify-between">
<RadioGroup v-if="allowInsuranceCategory" v-model:value="prescriptionStore.category">
<RadioButton :value="1">自费</RadioButton>
<RadioButton :value="2">医保</RadioButton>
</RadioGroup>
<div class="flex gap-2">
<!-- 选择常用方按钮 -->
<Button
v-if="canUseCommonPrescription"
type="default"
@click="openCommonPrescriptionModal"
>
选择常用方
</Button>
<!-- 保存为常用方按钮 -->
<Button
v-if="canUseCommonPrescription"
type="default"
@click="openSaveCommonPrescriptionModal"
>
<SaveOutlined />
保存为常用方
</Button>
<Button
v-if="prescriptionStore.activeCategory === 2"
type="primary"
@click="openWesternModal"
>
<PlusOutlined />
添加商品
</Button>
</div>
<Button
v-if="canUseCommonPrescription"
type="link"
size="small"
@click="openCommonPrescriptionModal"
>
<BookOutlined />
选择常用方
</Button>
<Button
v-if="canUseCommonPrescription"
type="link"
size="small"
@click="openSaveCommonPrescriptionModal"
>
<SaveOutlined />
保存常用方
</Button>
<Button
v-if="prescriptionStore.activeCategory === 2"
type="link"
size="small"
@click="openWesternModal"
>
<PlusOutlined />
添加商品
</Button>
</div>
<!-- 病历工具栏仅病历态吸顶 -->
<div
v-if="canUseMedicalRecord && rxMrTab === 'mr'"
class="mb-1 flex flex-wrap items-center gap-1"
>
<Button
type="link"
size="small"
danger
@click="medicalRecordPanelRef?.clear?.()"
>
<ClearOutlined />
清空病历
</Button>
<Button
type="link"
size="small"
@click="medicalRecordPanelRef?.openCiteHistory?.()"
>
<HistoryOutlined />
引用历史病历
</Button>
<Button
type="link"
size="small"
@click="medicalRecordPanelRef?.save?.()"
>
<SaveOutlined />
保存病历
</Button>
</div>
</div>
<!-- /rx-workspace-sticky -->
<div class="rx-workspace-scroll">
<div v-show="!canUseMedicalRecord || rxMrTab === 'rx'">
<!-- 二级 Tab处方类型随内容滚动病历不展示 -->
<Tabs
:active-key="String(prescriptionStore.activeCategory)"
class="rx-secondary-tabs mb-2"
size="small"
@change="(key) => tabChange(Number(key))"
>
<TabPane
v-for="categoryItem in categories"
:key="String(categoryItem.value)"
>
<template #tab>
<span
:id="`rx-modal-tab-${categoryItem.value}`"
class="rx-secondary-tab-label"
>
<img
v-if="categoryItem.icon"
class="tab-type-icon"
:src="categoryItem.icon"
alt=""
/>
<span>{{ categoryItem.label }}</span>
<span
v-if="categoryItem.icon_text"
class="tab-type-badge"
>{{ categoryItem.icon_text }}</span>
</span>
</template>
</TabPane>
</Tabs>
<div class="rx-swipe-panel">
<!-- 中药顶部固定展示已选种数 -->
<div
v-if="prescriptionStore.activeCategory === 1"
@@ -1641,21 +1717,23 @@ const cancelSaveCommonPrescription = () => {
show-search
/>
</div>
<!-- 诊断输入:搜索气泡 + 常用标签,与病历同源 -->
<!-- 诊断输入:搜索贴 label 后,与病历同源 -->
<div class="mb-3">
<div class="mb-2">
<label class="font-medium">诊断</label>
<div class="mr-field-head mb-2">
<label class="mr-field-label font-medium">诊断</label>
<div class="mr-field-search">
<EntryKeywordBubble
source="diagnosis"
placeholder="诊断关键字"
compact
@select="(item) => onPickDiagnosis(item.content || item.title)"
/>
</div>
</div>
<EntryKeywordBubble
class="mb-2"
source="diagnosis"
placeholder="诊断关键字搜索"
@select="(item) => onPickDiagnosis(item.content || item.title)"
/>
<Textarea
v-model:value="prescriptionStore.diagnosis"
disabled
placeholder="请通过上方搜索或下方常用诊断选择..."
placeholder="请通过搜索或下方常用诊断选择..."
:rows="2"
/>
<CommonDxOrderChips
@@ -1666,15 +1744,17 @@ const cancelSaveCommonPrescription = () => {
<!-- 医嘱输入 -->
<div class="mb-3">
<div class="mb-2">
<label class="font-medium">医嘱</label>
<div class="mr-field-head mb-2">
<label class="mr-field-label font-medium">医嘱</label>
<div class="mr-field-search">
<EntryKeywordBubble
source="doctor_order"
placeholder="医嘱关键字"
compact
@select="(item) => onPickDoctorOrder(item.content || item.title)"
/>
</div>
</div>
<EntryKeywordBubble
class="mb-2"
source="doctor_order"
placeholder="医嘱关键字搜索"
@select="(item) => onPickDoctorOrder(item.content || item.title)"
/>
<Textarea
v-model:value="prescriptionStore.medicalAdvice"
placeholder="输入医嘱..."
@@ -1750,9 +1830,14 @@ const cancelSaveCommonPrescription = () => {
:register-id="medicalRecordRegisterId"
:store-id="medicalRecordStoreId"
:user-patient-id="medicalRecordPatientId"
:patient-sex="Number(prescriptionStore.activePatient?.sex || 0)"
:patient-age="Number(prescriptionStore.activePatient?.age || 0)"
:storage-prefix="medicalRecordStoragePrefix"
:show-toolbar="false"
/>
</div>
</div>
<!-- /rx-workspace-scroll -->
</div>
<!-- 以下是各种弹窗组件,位置不动 -->
@@ -1831,11 +1916,75 @@ const cancelSaveCommonPrescription = () => {
<CommonPrescriptionModals />
<!-- 药店开方选配送仓库 -->
<WarehouseSelectModals @confirm="onWarehouseSelected" />
<PatientInfoDrawerComp />
<PatientMrDrawerComp />
<PatientRxHistoryDrawerComp />
</Modal>
</template>
<style scoped>
/* 保持原有样式,新增一些布局微调 */
.mr-field-head {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
align-items: center;
}
.mr-field-label {
flex-shrink: 0;
line-height: 24px;
}
.mr-field-search {
min-width: 0;
flex: 1 1 180px;
max-width: 280px;
}
.rx-tab-extra-actions {
display: inline-flex;
flex-wrap: wrap;
gap: 0 2px;
align-items: center;
}
.rx-tab-extra-actions :deep(.ant-btn) {
padding-inline: 6px;
height: 28px;
}
.rx-primary-tabs :deep(.ant-tabs-nav) {
margin-bottom: 8px;
}
.rx-workspace {
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.rx-workspace-sticky {
flex-shrink: 0;
z-index: 12;
background: hsl(var(--background));
padding-bottom: 4px;
border-bottom: 1px solid hsl(var(--border) / 0.6);
}
/* 二级处方类型 Tab仅作切换导航内容仍由下方业务区渲染 */
.rx-secondary-tabs :deep(.ant-tabs-nav) {
margin-bottom: 0;
}
.rx-secondary-tabs :deep(.ant-tabs-content-holder) {
display: none;
}
.rx-secondary-tab-label {
display: inline-flex;
align-items: center;
gap: 6px;
}
.rx-workspace-scroll {
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
padding-top: 8px;
padding-bottom: 12px;
}
.doctor-second-sign-title {
font-size: 16px;
margin: 10px 0;
@@ -2007,12 +2156,15 @@ const cancelSaveCommonPrescription = () => {
}
.right-panel {
overflow: hidden;
}
.rx-workspace-scroll {
overflow: visible;
}
.drug-categories {
flex-wrap: wrap;
position: static; /* 移动端取消固定 */
}
.table-header,

View File

@@ -144,6 +144,31 @@ export async function withdrawPrescriptionApi(data: {
return requestClient.post<any>(`${prefix}withdraw-prescription`, data);
}
/**
* 本次挂号处方列表(分页,支持 keyword 按订单号/订单ID筛选
*/
export async function getRegisterPrescriptionListApi(params: {
register_id: number;
page?: number;
page_size?: number;
keyword?: string;
}) {
return requestClient.get<any>(`${prefix}register-prescription-list`, { params });
}
/**
* 患者历史处方(排除当前挂号),支持 keyword 按订单号/订单ID筛选
*/
export async function getPatientPrescriptionHistoryApi(params: {
patient_id: number;
page?: number;
page_size?: number;
exclude_register_id?: number;
keyword?: string;
}) {
return requestClient.get<any>(`${prefix}patient-prescription-history`, { params });
}
/**
* 接诊
*/

View File

@@ -0,0 +1,127 @@
<script lang="ts" setup>
/**
* 接诊台顶部患者简要信息条
* 只展示门诊号/日期/费别/姓名/性别/年龄/手机(诊断只在处方/病历编辑区一处维护)
*/
import { computed } from 'vue';
import { Tag } from 'ant-design-vue';
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
defineOptions({ name: 'PatientBriefBar' });
const props = withDefaults(
defineProps<{
/** 挂号订单(含 order_no、created_at */
register?: Record<string, any> | null;
/** 就诊人 */
patient?: Record<string, any> | null;
/** 当前费别1 自费 2 医保 */
category?: number;
}>(),
{
register: null,
patient: null,
category: 1,
},
);
const sexText = computed(() => {
const sex = Number(props.patient?.sex || 0);
if (sex === 1) return '男';
if (sex === 2) return '女';
return '未填';
});
const categoryText = computed(() => {
return Number(props.category) === 2 ? '医保' : '自费';
});
const visitDate = computed(() => {
const raw = props.register?.created_at;
if (!raw) return '—';
// 后端查询器已格式化为字符串时直接展示
if (typeof raw === 'string') return raw;
if (typeof raw === 'number' && raw > 0) {
const d = new Date(raw * 1000);
const pad = (n: number) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
return '—';
});
</script>
<template>
<div class="patient-brief-bar">
<div class="brief-item">
<span class="brief-label">门诊号</span>
<span class="brief-value">{{ register?.order_no || '—' }}</span>
</div>
<div class="brief-item">
<span class="brief-label">日期</span>
<span class="brief-value">{{ visitDate }}</span>
</div>
<div class="brief-item">
<span class="brief-label">费别</span>
<Tag :color="Number(category) === 2 ? 'blue' : 'default'" class="m-0">
{{ categoryText }}
</Tag>
</div>
<div class="brief-item">
<span class="brief-label">姓名</span>
<span class="brief-value brief-name">{{ patient?.name || '—' }}</span>
</div>
<div class="brief-item">
<span class="brief-label">性别</span>
<span class="brief-value">{{ sexText }}</span>
</div>
<div class="brief-item">
<span class="brief-label">年龄</span>
<span class="brief-value">{{ patient?.age ?? '—' }}</span>
</div>
<div class="brief-item">
<span class="brief-label">手机</span>
<span class="brief-value">
<SensitiveText
:record="patient"
field="mobile"
:show-eye="false"
/>
</span>
</div>
</div>
</template>
<style scoped>
.patient-brief-bar {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
align-items: center;
padding: 10px 14px;
margin-bottom: 12px;
background: hsl(var(--muted) / 0.35);
border: 1px solid hsl(var(--border));
border-radius: 8px;
}
.brief-item {
display: inline-flex;
gap: 6px;
align-items: center;
min-width: 0;
font-size: 13px;
line-height: 1.4;
}
.brief-label {
flex-shrink: 0;
color: hsl(var(--muted-foreground));
}
.brief-value {
color: hsl(var(--foreground));
word-break: break-all;
}
.brief-name {
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,171 @@
<script lang="ts" setup>
/**
* 查看患者信息抽屉:完整患者字段 + 健康问诊信息(不进顶栏)
* 特色方/转诊等业务入口由父级通过 slot 注入,避免抽屉耦合开方逻辑
*/
import { computed, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { Descriptions, Empty, Tag } from 'ant-design-vue';
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
defineOptions({ name: 'PatientInfoDrawer' });
type DrawerData = {
patient?: Record<string, any> | null;
register?: Record<string, any> | null;
healthInquiry?: Record<string, any> | null;
patientName?: string;
};
const meta = ref<DrawerData>({});
const sexText = computed(() => {
const sex = Number(meta.value.patient?.sex || 0);
if (sex === 1) return '男';
if (sex === 2) return '女';
return '未填写';
});
/** 逗号分隔标签拆分(与接诊页 splitString 一致) */
function splitString(str: unknown): string[] {
if (!str || typeof str !== 'string') return [];
return str.split(/[,]/).map((s) => s.trim()).filter(Boolean);
}
const [Drawer, drawerApi] = useVbenDrawer({
class: 'w-[720px]',
title: '患者信息',
cancelText: '关闭',
showConfirmButton: false,
onOpenChange(isOpen: boolean) {
if (!isOpen) {
meta.value = {};
return;
}
const data = drawerApi.getData<DrawerData>() || {};
meta.value = data;
const name = data.patientName || data.patient?.name || '';
drawerApi.setState({
title: name ? `患者信息 - ${name}` : '患者信息',
});
},
});
defineExpose({ drawerApi });
</script>
<template>
<Drawer>
<Descriptions
v-if="meta.patient"
:column="2"
bordered
size="small"
title="基本信息"
class="mb-4"
>
<Descriptions.Item label="姓名">
{{ meta.patient.name || '—' }}
</Descriptions.Item>
<Descriptions.Item label="性别">
{{ sexText }}
</Descriptions.Item>
<Descriptions.Item label="年龄">
{{ meta.patient.age ?? '—' }}
</Descriptions.Item>
<Descriptions.Item label="手机">
<SensitiveText
:record="meta.patient"
field="mobile"
:show-eye="false"
/>
</Descriptions.Item>
<Descriptions.Item label="门诊号">
{{ meta.register?.order_no || '—' }}
</Descriptions.Item>
<Descriptions.Item label="就诊时间">
{{ meta.register?.created_at || '—' }}
</Descriptions.Item>
</Descriptions>
<Empty v-else description="暂无患者信息" class="py-6" />
<Descriptions
v-if="meta.healthInquiry"
:column="2"
bordered
size="small"
title="健康信息"
class="mb-4"
>
<Descriptions.Item label="肝功能">
<span>{{
meta.healthInquiry.liver_function === 0 ? '正常' : '异常'
}}</span>
<div v-if="meta.healthInquiry.liver_function === 1" class="mt-2">
<Tag
v-for="(item, idx) in splitString(meta.healthInquiry.liver_index)"
:key="`liver-${idx}`"
color="#455cda"
>
{{ item }}
</Tag>
</div>
</Descriptions.Item>
<Descriptions.Item label="肾功能">
<span>{{
meta.healthInquiry.renal_function === 0 ? '正常' : '异常'
}}</span>
<div v-if="meta.healthInquiry.renal_function === 1" class="mt-2">
<Tag
v-for="(item, idx) in splitString(meta.healthInquiry.renal_index)"
:key="`renal-${idx}`"
color="#455cda"
>
{{ item }}
</Tag>
</div>
</Descriptions.Item>
<Descriptions.Item label="既往史" :span="2">
<span>{{
meta.healthInquiry.person_status === 0 ? '无' : '有'
}}</span>
<p v-if="meta.healthInquiry.person_status === 1" class="mt-2 mb-0">
{{ meta.healthInquiry.person_history || '' }}
</p>
</Descriptions.Item>
<Descriptions.Item label="过敏史" :span="2">
<span>{{
meta.healthInquiry.allergic_status === 0 ? '无' : '有'
}}</span>
<div v-if="meta.healthInquiry.allergic_status === 1" class="mt-2">
<Tag
v-for="(item, idx) in splitString(
meta.healthInquiry.allergic_history,
)"
:key="`allergy-${idx}`"
color="orange"
>
{{ item }}
</Tag>
</div>
</Descriptions.Item>
<Descriptions.Item label="家庭遗传史" :span="2">
<span>{{
meta.healthInquiry.family_status === 0 ? '无' : '有'
}}</span>
<p v-if="meta.healthInquiry.family_status === 1" class="mt-2 mb-0">
{{ meta.healthInquiry.family_history || '—' }}
</p>
</Descriptions.Item>
</Descriptions>
<Empty
v-else
description="暂无健康问诊信息"
class="py-4"
/>
<slot name="extra" />
</Drawer>
</template>

View File

@@ -0,0 +1,307 @@
<script lang="ts" setup>
/**
* 患者处方记录抽屉:本次 | 历史,支持按订单号/订单ID搜索、展示支付态、复用/撤回
*/
import { computed, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import {
Button,
Empty,
Input,
Spin,
Tabs,
Tag,
message,
} from 'ant-design-vue';
import {
getPatientPrescriptionHistoryApi,
getRegisterPrescriptionListApi,
withdrawPrescriptionApi,
} from '../api';
defineOptions({ name: 'PatientPrescriptionHistoryDrawer' });
const TabPane = Tabs.TabPane;
type DrawerData = {
registerId?: number;
userPatientId?: number;
patientName?: string;
/** 复用回调:传入处方 id */
onReuse?: (prescriptionId: number) => void | Promise<void>;
/** 是否允许撤回(线下接诊) */
allowWithdraw?: boolean;
/** 撤回成功后刷新 */
onWithdrawn?: () => void;
};
const meta = ref<DrawerData>({});
const loading = ref(false);
const withdrawingId = ref(0);
const listTab = ref<'current' | 'history'>('current');
const keyword = ref('');
const list = ref<Record<string, any>[]>([]);
const page = ref(1);
const hasMore = ref(false);
const pageSize = 20;
const categoryLabel = (c: number) => (Number(c) === 2 ? '医保' : '自费');
const typeLabel = (t: number) => {
const map: Record<number, string> = {
1: '中药',
2: '西药',
3: '简单产品',
5: '简单产品',
6: '简单产品',
7: '简单产品',
};
return map[Number(t)] || `类型${t}`;
};
const payColor = (row: Record<string, any>) => {
const text = String(row.order_pay_text || '');
if (text.includes('待支付')) return 'orange';
if (text.includes('已支付')) return 'green';
if (text.includes('取消')) return 'default';
return 'blue';
};
async function loadList(reset = true) {
if (reset) {
page.value = 1;
list.value = [];
}
loading.value = true;
try {
const kw = keyword.value.trim();
let res: any;
if (listTab.value === 'current') {
const rid = Number(meta.value.registerId || 0);
if (!rid) {
list.value = [];
return;
}
res = await getRegisterPrescriptionListApi({
register_id: rid,
page: page.value,
page_size: pageSize,
keyword: kw || undefined,
});
} else {
const pid = Number(meta.value.userPatientId || 0);
if (!pid) {
list.value = [];
return;
}
res = await getPatientPrescriptionHistoryApi({
patient_id: pid,
page: page.value,
page_size: pageSize,
exclude_register_id: Number(meta.value.registerId || 0) || undefined,
keyword: kw || undefined,
});
}
const rows = Array.isArray(res?.list) ? res.list : [];
list.value = reset ? rows : [...list.value, ...rows];
hasMore.value = !!res?.has_more;
} catch (e: any) {
if (reset) list.value = [];
message.error(e?.message || '加载处方失败');
} finally {
loading.value = false;
}
}
function onSearch() {
loadList(true);
}
function onTabChange(key: string | number) {
listTab.value = key === 'history' ? 'history' : 'current';
loadList(true);
}
function loadMore() {
if (!hasMore.value || loading.value) return;
page.value += 1;
loadList(false);
}
async function handleReuse(row: Record<string, any>) {
const id = Number(row.id || 0);
if (!id) return;
try {
await meta.value.onReuse?.(id);
} catch (e: any) {
message.error(e?.message || '复用失败');
}
}
async function handleWithdraw(row: Record<string, any>) {
const prescriptionId = Number(row.id || 0);
const registerId = Number(row.register_id || meta.value.registerId || 0);
if (!prescriptionId || !registerId) return;
withdrawingId.value = prescriptionId;
try {
await withdrawPrescriptionApi({
prescription_id: prescriptionId,
register_id: registerId,
});
message.success('撤回成功');
meta.value.onWithdrawn?.();
await loadList(true);
} catch (e: any) {
message.error(e?.message || '撤回失败');
} finally {
withdrawingId.value = 0;
}
}
const titleText = computed(() => {
const name = meta.value.patientName ? ` - ${meta.value.patientName}` : '';
return `患者处方记录${name}`;
});
const [Drawer, drawerApi] = useVbenDrawer({
class: 'w-[820px]',
title: '患者处方记录',
cancelText: '关闭',
showConfirmButton: false,
onOpenChange(isOpen: boolean) {
if (!isOpen) {
meta.value = {};
list.value = [];
keyword.value = '';
listTab.value = 'current';
page.value = 1;
return;
}
const data = drawerApi.getData<DrawerData>() || {};
meta.value = data;
drawerApi.setState({ title: titleText.value });
loadList(true);
},
});
defineExpose({ drawerApi });
</script>
<template>
<Drawer>
<div class="mb-3 flex flex-wrap items-center gap-2">
<Input.Search
v-model:value="keyword"
allow-clear
placeholder="按订单号或订单ID搜索"
style="max-width: 280px"
@search="onSearch"
/>
<Button @click="onSearch">查询</Button>
</div>
<Tabs :active-key="listTab" @change="onTabChange">
<TabPane key="current" tab="本次" />
<TabPane key="history" tab="历史" />
</Tabs>
<Spin :spinning="loading">
<div v-if="list.length === 0 && !loading" class="py-10">
<Empty description="暂无处方记录" />
</div>
<div v-else class="rx-history-list">
<div
v-for="row in list"
:key="row.id"
class="rx-history-item"
>
<div class="rx-history-main">
<div class="rx-history-title">
<span class="font-medium">{{ row.prescription_no || `#${row.id}` }}</span>
<Tag class="ml-2">{{ typeLabel(row.prescription_type) }}</Tag>
<Tag>{{ categoryLabel(row.category) }}</Tag>
<Tag :color="payColor(row)">{{ row.order_pay_text || '—' }}</Tag>
</div>
<div class="rx-history-meta text-muted-foreground">
<span>开方时间{{ row.created_at || '—' }}</span>
<span class="ml-3">金额¥{{ row.total_pay_price ?? '—' }}</span>
<span v-if="row.order_no" class="ml-3">订单号{{ row.order_no }}</span>
<span v-if="row.order_id" class="ml-3">订单ID{{ row.order_id }}</span>
<span
v-if="listTab === 'history' && row.register?.order_no"
class="ml-3"
>
挂号{{ row.register.order_no }}
</span>
</div>
</div>
<div class="rx-history-actions">
<Button
type="link"
size="small"
@click="handleReuse(row)"
>
复用
</Button>
<Button
v-if="meta.allowWithdraw && row.can_withdraw && listTab === 'current'"
type="link"
size="small"
danger
:loading="withdrawingId === row.id"
@click="handleWithdraw(row)"
>
撤回
</Button>
</div>
</div>
<div v-if="hasMore" class="mt-3 text-center">
<Button type="link" :loading="loading" @click="loadMore">加载更多</Button>
</div>
</div>
</Spin>
</Drawer>
</template>
<style scoped>
.rx-history-list {
display: flex;
flex-direction: column;
gap: 10px;
max-height: calc(100vh - 260px);
overflow: auto;
}
.rx-history-item {
display: flex;
gap: 12px;
align-items: flex-start;
justify-content: space-between;
padding: 12px;
border: 1px solid hsl(var(--border));
border-radius: 8px;
}
.rx-history-main {
min-width: 0;
flex: 1;
}
.rx-history-title {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
margin-bottom: 6px;
}
.rx-history-meta {
font-size: 12px;
line-height: 1.6;
}
.rx-history-actions {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.text-muted-foreground {
color: hsl(var(--muted-foreground));
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -11,13 +11,16 @@ import { useDebounceFn } from '@vueuse/core';
import { useVbenDrawer, useVbenModal } from '@vben/common-ui';
import {
Button,
Col,
Input,
InputNumber,
Row,
Textarea,
message,
} from 'ant-design-vue';
import {
ClearOutlined,
HistoryOutlined,
SaveOutlined,
} from '@ant-design/icons-vue';
import { getSystemConfigByKeys } from '#/views/system/system-config/api';
@@ -32,7 +35,7 @@ import EntryKeywordBubble, {
import CommonDxOrderChips from './components/CommonDxOrderChips.vue';
import PatientMedicalRecordDrawer from './components/PatientMedicalRecordDrawer.vue';
import TextareaExpandModal from './components/TextareaExpandModal.vue';
import { emptyMedicalRecord } from './config/constants';
import { emptyMedicalRecord, isHistoryFieldVisible } from './config/constants';
import {
clearMedicalRecordDraft,
loadMedicalRecordDraft,
@@ -55,12 +58,24 @@ const props = withDefaults(
diagnosisReadonly?: boolean;
/** localStorage 键前缀chat-shop 用 storagePrefix 隔离) */
storagePrefix?: string;
/** 就诊人性别 1男2女用于月经史/婚育史显隐 */
patientSex?: number;
/** 就诊人年龄,用于婚育史显隐 */
patientAge?: number;
/**
* 是否在面板内展示操作工具栏
* 接诊台/弹窗已把按钮提到一级 Tab 下吸顶工具栏时传 false
*/
showToolbar?: boolean;
}>(),
{
storeId: 0,
userPatientId: 0,
diagnosisReadonly: false,
storagePrefix: '',
patientSex: 0,
patientAge: 0,
showToolbar: true,
},
);
@@ -86,27 +101,29 @@ const showCommonInBubble = computed(
const commonChipsTick = ref(0);
/**
* 病历模块焦点顺序:双栏同行先左后右,再换下一行
* 「中医病案」内:舌象→脉象→辨证/病案→证候→疾病→治法,再进入右栏过敏史
* 病历模块焦点顺序:双列从左到右、从上到下填满;中医病案整块在最底部
* 「中医病案」内:舌象→脉象→辨证/病案→证候→疾病→治法
*/
const MODULE_ORDER = [
'diagnosis',
'family_history',
'chief_complaint',
'epidemic_history',
'family_history',
'present_illness',
'epidemic_history',
'past_history',
'menstrual_history',
'marital_history',
'personal_history',
'allergy_history',
'treatment_advice',
'doctor_order',
'physical_exam',
'auxiliary_exam',
'tongue',
'pulse',
'tcm_case',
'tcm_syndrome',
'tcm_disease',
'tcm_method',
'allergy_history',
'treatment_advice',
'physical_exam',
'doctor_order',
'auxiliary_exam',
] as const;
type MrModuleKey = (typeof MODULE_ORDER)[number];
@@ -616,7 +633,11 @@ function applyCitedRecord(data: Record<string, any>) {
defineExpose({
save: handleSave,
clear: handleClear,
loadRecord,
applyCitedRecord,
openCiteHistory: handleCiteHistory,
loading,
getPayload: () => ({
...form,
user_patient_id: effectivePatientId.value,
@@ -656,21 +677,32 @@ const tcmCaseSubRows = [
[tcmCaseSubFields[2], null],
] as const;
const leftFields = [
/**
* 常规字段统一按阅读顺序排列CSS 双列自动从左到右填满
* (中医病案单独置底全宽,不放进网格)
*/
const allNormalFields = [
fieldBlock('chief_complaint', '主诉'),
fieldBlock('present_illness', '现病史'),
fieldBlock('tcm_case', '中医病案'),
fieldBlock('treatment_advice', '治疗意见'),
fieldBlock('doctor_order', '医嘱'),
];
const rightFields = [
fieldBlock('family_history', '家族史'),
fieldBlock('present_illness', '现病史'),
fieldBlock('epidemic_history', '流行病学史'),
fieldBlock('past_history', '既往史'),
fieldBlock('menstrual_history', '月经史'),
fieldBlock('marital_history', '婚育史'),
fieldBlock('personal_history', '个人史'),
fieldBlock('allergy_history', '过敏史'),
fieldBlock('treatment_advice', '治疗意见'),
fieldBlock('doctor_order', '医嘱'),
fieldBlock('physical_exam', '体征检查'),
fieldBlock('auxiliary_exam', '辅助检查'),
];
/** 月经史/婚育史按性别年龄过滤后的可见字段 */
const visibleNormalFields = computed(() =>
allNormalFields.filter((f) =>
isHistoryFieldVisible(f.fieldCode, props.patientSex, props.patientAge),
),
);
</script>
<template>
<div
@@ -679,208 +711,45 @@ const rightFields = [
:class="{ 'opacity-60': loading }"
@keydown.capture="onPanelKeydownCapture"
>
<div class="mb-3 flex flex-wrap gap-2">
<Button danger @click="handleClear">清空病历</Button>
<Button @click="handleCiteHistory">引用历史病历</Button>
<Button type="primary" :loading="loading" @click="handleSave">保存病历</Button>
<!-- 面板内工具栏接诊台吸顶场景通过 showToolbar=false 隐藏 -->
<div
v-if="showToolbar"
class="mr-toolbar mb-2 flex flex-wrap items-center gap-1"
>
<Button type="link" size="small" danger @click="handleClear">
<ClearOutlined />
清空病历
</Button>
<Button type="link" size="small" @click="handleCiteHistory">
<HistoryOutlined />
引用历史病历
</Button>
<Button
type="link"
size="small"
:loading="loading"
@click="handleSave"
>
<SaveOutlined />
保存病历
</Button>
</div>
<div class="mb-2 text-xs text-muted-foreground">
提示双击文本域可放大编辑Ctrl+Enter 下一项Ctrl+Shift+Enter 上一项
</div>
<Row :gutter="16">
<Col :span="12">
<div class="mb-4">
<div class="mb-1 font-medium">诊断与处方同步</div>
<div class="mb-1" data-mr-module="diagnosis" data-mr-role="search">
<EntryKeywordBubble
:ref="(el) => setSearchRef('diagnosis', el)"
source="diagnosis"
placeholder="诊断关键字"
:disabled="diagnosisReadonly"
:show-common-in-bubble="showCommonInBubble"
@select="(item) => onEntryPick('diagnosis', item)"
@common-changed="commonChipsTick += 1"
/>
</div>
<div data-mr-module="diagnosis" data-mr-role="textarea">
<Textarea
:ref="(el) => setTextareaRef('diagnosis', el)"
v-model:value="diagnosisModel"
:rows="2"
:disabled="diagnosisReadonly"
:placeholder="diagnosisReadonly ? '请通过上方关键字选择诊断' : '临床诊断(双击放大)'"
@dblclick="openDiagnosisExpand"
/>
</div>
<CommonDxOrderChips
v-if="showCommonTags"
:key="`dx_chips_${commonChipsTick}`"
source="diagnosis"
:field-value="diagnosisModel"
:disabled="diagnosisReadonly"
@select="(text) => onEntryPick('diagnosis', { id: text, title: text, content: text, remark: '' })"
/>
</div>
<div v-for="f in leftFields" :key="f.fieldCode" class="mb-4">
<div class="mb-1 font-medium">{{ f.label }}</div>
<template v-if="f.fieldCode === 'tcm_case'">
<!-- 1) 舌象 | 脉象 -->
<div class="mb-2 flex gap-2">
<div class="min-w-0 flex-1">
<div class="mb-1 text-sm text-muted-foreground">舌象</div>
<div class="mb-1" data-mr-module="tongue" data-mr-role="search">
<EntryKeywordBubble
:ref="(el) => setSearchRef('tongue', el)"
field-code="tongue"
:store-id="storeId"
placeholder="舌象词条"
:show-common-in-bubble="showCommonInBubble"
@select="(item) => onEntryPick('tongue', item)"
@common-changed="commonChipsTick += 1"
/>
</div>
<div data-mr-module="tongue" data-mr-role="textarea">
<Input
:ref="(el) => setTextareaRef('tongue', el)"
v-model:value="form.tongue"
placeholder="舌象"
@dblclick="openFieldExpand('tongue', '舌象')"
/>
</div>
<CommonDxOrderChips
v-if="showCommonTags"
:key="`entry_chips_tongue_${commonChipsTick}`"
source="entry"
field-code="tongue"
:field-value="form.tongue"
@select="
(text) =>
onEntryPick('tongue', {
id: text,
title: text,
content: text,
remark: '',
})
"
/>
</div>
<div class="min-w-0 flex-1">
<div class="mb-1 text-sm text-muted-foreground">脉象</div>
<div class="mb-1" data-mr-module="pulse" data-mr-role="search">
<EntryKeywordBubble
:ref="(el) => setSearchRef('pulse', el)"
field-code="pulse"
:store-id="storeId"
placeholder="脉象词条"
:show-common-in-bubble="showCommonInBubble"
@select="(item) => onEntryPick('pulse', item)"
@common-changed="commonChipsTick += 1"
/>
</div>
<div data-mr-module="pulse" data-mr-role="textarea">
<Input
:ref="(el) => setTextareaRef('pulse', el)"
v-model:value="form.pulse"
placeholder="脉象"
@dblclick="openFieldExpand('pulse', '脉象')"
/>
</div>
<CommonDxOrderChips
v-if="showCommonTags"
:key="`entry_chips_pulse_${commonChipsTick}`"
source="entry"
field-code="pulse"
:field-value="form.pulse"
@select="
(text) =>
onEntryPick('pulse', {
id: text,
title: text,
content: text,
remark: '',
})
"
/>
</div>
</div>
<!-- 2) 辨证 / 病案 -->
<div class="mb-1 text-sm text-muted-foreground">辨证 / 病案</div>
<div class="mb-1" data-mr-module="tcm_case" data-mr-role="search">
<EntryKeywordBubble
:ref="(el) => setSearchRef('tcm_case', el)"
field-code="tcm_case"
:store-id="storeId"
:show-common-in-bubble="showCommonInBubble"
@select="(item) => onEntryPick('tcm_case', item)"
@common-changed="commonChipsTick += 1"
/>
</div>
<div class="mb-3" data-mr-module="tcm_case" data-mr-role="textarea">
<Textarea
:ref="(el) => setTextareaRef('tcm_case', el)"
v-model:value="form.tcm_case"
:rows="2"
placeholder="[选择] 辨证/病案(双击放大)"
@dblclick="openFieldExpand('tcm_case', '辨证/病案')"
/>
</div>
<CommonDxOrderChips
v-if="showCommonTags"
:key="`entry_chips_tcm_case_${commonChipsTick}`"
class="mb-3"
source="entry"
field-code="tcm_case"
:field-value="form.tcm_case"
@select="
(text) =>
onEntryPick('tcm_case', {
id: text,
title: text,
content: text,
remark: '',
})
"
/>
<!-- 3) 证候|疾病治法|与舌脉同为一行两列 -->
<div
v-for="(row, rowIdx) in tcmCaseSubRows"
:key="`tcm_row_${rowIdx}`"
class="mb-2 flex gap-2"
>
<div
v-for="(sub, colIdx) in row"
:key="sub ? sub.fieldCode : `empty_${rowIdx}_${colIdx}`"
class="min-w-0 flex-1"
>
<template v-if="sub">
<div class="mb-1 text-sm text-muted-foreground">{{ sub.label }}</div>
<div
class="mb-1"
:data-mr-module="sub.fieldCode"
data-mr-role="search"
>
<EntryKeywordBubble
:ref="(el) => setSearchRef(sub.fieldCode, el)"
:source="sub.source"
:placeholder="`${sub.label}关键字/首拼/全拼`"
@select="(item) => onEntryPick(sub.fieldCode, item)"
/>
</div>
<div :data-mr-module="sub.fieldCode" data-mr-role="textarea">
<Input
:ref="(el) => setTextareaRef(sub.fieldCode, el)"
v-model:value="(form as any)[sub.textKey]"
:placeholder="sub.label"
@dblclick="openFieldExpand(sub.textKey, sub.label)"
/>
</div>
</template>
</div>
</div>
</template>
<!-- 常规字段双列自动填满避免一侧留白 -->
<div class="mr-fields-grid">
<div
v-for="f in visibleNormalFields"
:key="f.fieldCode"
class="mr-field-block"
>
<div class="mr-field-head mb-1">
<span class="mr-field-label">{{ f.label }}</span>
<div
v-else-if="f.fieldCode === 'doctor_order'"
class="mb-1"
v-if="f.fieldCode === 'doctor_order'"
class="mr-field-search"
data-mr-module="doctor_order"
data-mr-role="search"
>
@@ -888,6 +757,7 @@ const rightFields = [
:ref="(el) => setSearchRef('doctor_order', el)"
source="doctor_order"
placeholder="医嘱关键字"
compact
:show-common-in-bubble="showCommonInBubble"
@select="(item) => onEntryPick('doctor_order', item)"
@common-changed="commonChipsTick += 1"
@@ -895,7 +765,7 @@ const rightFields = [
</div>
<div
v-else
class="mb-1"
class="mr-field-search"
:data-mr-module="f.fieldCode"
data-mr-role="search"
>
@@ -903,34 +773,109 @@ const rightFields = [
:ref="(el) => setSearchRef(f.fieldCode, el)"
:field-code="f.fieldCode"
:store-id="storeId"
compact
:show-common-in-bubble="showCommonInBubble"
@select="(item) => onEntryPick(f.fieldCode, item)"
@common-changed="commonChipsTick += 1"
/>
</div>
<template v-if="f.fieldCode === 'doctor_order'">
<div data-mr-module="doctor_order" data-mr-role="textarea">
<Textarea
:ref="(el) => setTextareaRef('doctor_order', el)"
v-model:value="medicalAdviceModel"
:rows="2"
placeholder="医嘱(与处方同步,双击放大)"
@dblclick="openAdviceExpand"
/>
</div>
<CommonDxOrderChips
v-if="showCommonTags"
:key="`order_chips_${commonChipsTick}`"
source="doctor_order"
:field-value="medicalAdviceModel"
@select="(text) => onEntryPick('doctor_order', { id: text, title: text, content: text, remark: '' })"
</div>
<div
v-if="f.fieldCode === 'physical_exam'"
class="vitals-para mb-2"
>
<span class="vital-chunk">
<span class="vital-label">体温(°C)</span>
<InputNumber
v-model:value="form.temperature"
:min="0"
:step="0.1"
:bordered="false"
class="vital-input"
placeholder=" "
/>
</template>
<div
v-else-if="f.fieldCode !== 'tcm_case'"
:data-mr-module="f.fieldCode"
data-mr-role="textarea"
>
</span>
<span class="vital-chunk">
<span class="vital-label">身高(cm)</span>
<InputNumber
v-model:value="form.height"
:min="0"
:bordered="false"
class="vital-input"
placeholder=" "
/>
</span>
<span class="vital-chunk">
<span class="vital-label">体重(KG)</span>
<InputNumber
v-model:value="form.weight"
:min="0"
:step="0.1"
:bordered="false"
class="vital-input"
placeholder=" "
/>
</span>
<span class="vital-chunk">
<span class="vital-label">呼吸(/)</span>
<InputNumber
v-model:value="form.respiratory_rate"
:min="0"
:bordered="false"
class="vital-input vital-input--sm"
placeholder=" "
/>
</span>
<span class="vital-chunk">
<span class="vital-label">高压 mmHg</span>
<InputNumber
v-model:value="form.bp_systolic"
:min="0"
:bordered="false"
class="vital-input vital-input--sm"
placeholder=" "
/>
</span>
<span class="vital-sep">/</span>
<span class="vital-chunk">
<span class="vital-label">低压 mmHg</span>
<InputNumber
v-model:value="form.bp_diastolic"
:min="0"
:bordered="false"
class="vital-input vital-input--sm"
placeholder=" "
/>
</span>
</div>
<template v-if="f.fieldCode === 'doctor_order'">
<div data-mr-module="doctor_order" data-mr-role="textarea">
<Textarea
:ref="(el) => setTextareaRef('doctor_order', el)"
v-model:value="medicalAdviceModel"
:rows="2"
placeholder="医嘱(与处方同步,双击放大)"
@dblclick="openAdviceExpand"
/>
</div>
<CommonDxOrderChips
v-if="showCommonTags"
:key="`order_chips_${commonChipsTick}`"
source="doctor_order"
:field-value="medicalAdviceModel"
@select="
(text) =>
onEntryPick('doctor_order', {
id: text,
title: text,
content: text,
remark: '',
})
"
/>
</template>
<template v-else>
<div :data-mr-module="f.fieldCode" data-mr-role="textarea">
<Textarea
:ref="(el) => setTextareaRef(f.fieldCode, el)"
v-model:value="(form as any)[f.textKey]"
@@ -940,7 +885,7 @@ const rightFields = [
/>
</div>
<CommonDxOrderChips
v-if="showCommonTags && f.fieldCode !== 'tcm_case' && f.fieldCode !== 'doctor_order'"
v-if="showCommonTags"
:key="`entry_chips_${f.fieldCode}_${commonChipsTick}`"
source="entry"
:field-code="f.fieldCode"
@@ -955,112 +900,49 @@ const rightFields = [
})
"
/>
</div>
</Col>
<Col :span="12">
<div v-for="f in rightFields" :key="f.fieldCode" class="mb-4">
<div class="mb-1 font-medium">{{ f.label }}</div>
<div
class="mb-1"
:data-mr-module="f.fieldCode"
data-mr-role="search"
>
<EntryKeywordBubble
:ref="(el) => setSearchRef(f.fieldCode, el)"
:field-code="f.fieldCode"
:store-id="storeId"
:show-common-in-bubble="showCommonInBubble"
@select="(item) => onEntryPick(f.fieldCode, item)"
@common-changed="commonChipsTick += 1"
/>
</template>
</div>
</div>
<!-- 中医病案置底全宽 -->
<div class="mr-tcm-block mb-4">
<div class="mr-field-head mb-2">
<span class="mr-field-label mr-field-label--section">中医病案</span>
</div>
<div class="mb-2 flex gap-2">
<div class="min-w-0 flex-1">
<div class="mr-field-head mb-1">
<span class="mr-field-label mr-field-label--sub">舌象</span>
<div class="mr-field-search" data-mr-module="tongue" data-mr-role="search">
<EntryKeywordBubble
:ref="(el) => setSearchRef('tongue', el)"
field-code="tongue"
:store-id="storeId"
placeholder="舌象词条"
compact
:show-common-in-bubble="showCommonInBubble"
@select="(item) => onEntryPick('tongue', item)"
@common-changed="commonChipsTick += 1"
/>
</div>
</div>
<!-- 体征一段话内联书写标签含单位 + 下划线输入 -->
<div
v-if="f.fieldCode === 'physical_exam'"
class="vitals-para mb-2"
>
<span class="vital-chunk">
<span class="vital-label">体温(°C)</span>
<InputNumber
v-model:value="form.temperature"
:min="0"
:step="0.1"
:bordered="false"
class="vital-input"
placeholder=" "
/>
</span>
<span class="vital-chunk">
<span class="vital-label">身高(cm)</span>
<InputNumber
v-model:value="form.height"
:min="0"
:bordered="false"
class="vital-input"
placeholder=" "
/>
</span>
<span class="vital-chunk">
<span class="vital-label">体重(KG)</span>
<InputNumber
v-model:value="form.weight"
:min="0"
:step="0.1"
:bordered="false"
class="vital-input"
placeholder=" "
/>
</span>
<span class="vital-chunk">
<span class="vital-label">呼吸(/)</span>
<InputNumber
v-model:value="form.respiratory_rate"
:min="0"
:bordered="false"
class="vital-input vital-input--sm"
placeholder=" "
/>
</span>
<span class="vital-chunk">
<span class="vital-label">高压 mmHg</span>
<InputNumber
v-model:value="form.bp_systolic"
:min="0"
:bordered="false"
class="vital-input vital-input--sm"
placeholder=" "
/>
</span>
<span class="vital-sep">/</span>
<span class="vital-chunk">
<span class="vital-label">低压 mmHg</span>
<InputNumber
v-model:value="form.bp_diastolic"
:min="0"
:bordered="false"
class="vital-input vital-input--sm"
placeholder=" "
/>
</span>
</div>
<div :data-mr-module="f.fieldCode" data-mr-role="textarea">
<Textarea
:ref="(el) => setTextareaRef(f.fieldCode, el)"
v-model:value="(form as any)[f.textKey]"
:rows="2"
:placeholder="`[选择] ${f.label}(双击放大)`"
@dblclick="openFieldExpand(f.textKey, f.label)"
<div data-mr-module="tongue" data-mr-role="textarea">
<Input
:ref="(el) => setTextareaRef('tongue', el)"
v-model:value="form.tongue"
placeholder="舌象"
@dblclick="openFieldExpand('tongue', '舌象')"
/>
</div>
<CommonDxOrderChips
v-if="showCommonTags"
:key="`entry_chips_r_${f.fieldCode}_${commonChipsTick}`"
:key="`entry_chips_tongue_${commonChipsTick}`"
source="entry"
:field-code="f.fieldCode"
:field-value="String((form as any)[f.textKey] || '')"
field-code="tongue"
:field-value="form.tongue"
@select="
(text) =>
onEntryPick(f.fieldCode, {
onEntryPick('tongue', {
id: text,
title: text,
content: text,
@@ -1069,8 +951,128 @@ const rightFields = [
"
/>
</div>
</Col>
</Row>
<div class="min-w-0 flex-1">
<div class="mr-field-head mb-1">
<span class="mr-field-label mr-field-label--sub">脉象</span>
<div class="mr-field-search" data-mr-module="pulse" data-mr-role="search">
<EntryKeywordBubble
:ref="(el) => setSearchRef('pulse', el)"
field-code="pulse"
:store-id="storeId"
placeholder="脉象词条"
compact
:show-common-in-bubble="showCommonInBubble"
@select="(item) => onEntryPick('pulse', item)"
@common-changed="commonChipsTick += 1"
/>
</div>
</div>
<div data-mr-module="pulse" data-mr-role="textarea">
<Input
:ref="(el) => setTextareaRef('pulse', el)"
v-model:value="form.pulse"
placeholder="脉象"
@dblclick="openFieldExpand('pulse', '脉象')"
/>
</div>
<CommonDxOrderChips
v-if="showCommonTags"
:key="`entry_chips_pulse_${commonChipsTick}`"
source="entry"
field-code="pulse"
:field-value="form.pulse"
@select="
(text) =>
onEntryPick('pulse', {
id: text,
title: text,
content: text,
remark: '',
})
"
/>
</div>
</div>
<div class="mr-field-head mb-1">
<span class="mr-field-label mr-field-label--sub">辨证 / 病案</span>
<div class="mr-field-search" data-mr-module="tcm_case" data-mr-role="search">
<EntryKeywordBubble
:ref="(el) => setSearchRef('tcm_case', el)"
field-code="tcm_case"
:store-id="storeId"
compact
:show-common-in-bubble="showCommonInBubble"
@select="(item) => onEntryPick('tcm_case', item)"
@common-changed="commonChipsTick += 1"
/>
</div>
</div>
<div class="mb-3" data-mr-module="tcm_case" data-mr-role="textarea">
<Textarea
:ref="(el) => setTextareaRef('tcm_case', el)"
v-model:value="form.tcm_case"
:rows="2"
placeholder="[选择] 辨证/病案(双击放大)"
@dblclick="openFieldExpand('tcm_case', '辨证/病案')"
/>
</div>
<CommonDxOrderChips
v-if="showCommonTags"
:key="`entry_chips_tcm_case_${commonChipsTick}`"
class="mb-3"
source="entry"
field-code="tcm_case"
:field-value="form.tcm_case"
@select="
(text) =>
onEntryPick('tcm_case', {
id: text,
title: text,
content: text,
remark: '',
})
"
/>
<div
v-for="(tcmRow, tcmRowIdx) in tcmCaseSubRows"
:key="`tcm_row_${tcmRowIdx}`"
class="mb-2 flex gap-2"
>
<div
v-for="(sub, colIdx) in tcmRow"
:key="sub ? sub.fieldCode : `empty_${tcmRowIdx}_${colIdx}`"
class="min-w-0 flex-1"
>
<template v-if="sub">
<div class="mr-field-head mb-1">
<span class="mr-field-label mr-field-label--sub">{{ sub.label }}</span>
<div
class="mr-field-search"
:data-mr-module="sub.fieldCode"
data-mr-role="search"
>
<EntryKeywordBubble
:ref="(el) => setSearchRef(sub.fieldCode, el)"
:source="sub.source"
:placeholder="`${sub.label}关键字`"
compact
@select="(item) => onEntryPick(sub.fieldCode, item)"
/>
</div>
</div>
<div :data-mr-module="sub.fieldCode" data-mr-role="textarea">
<Input
:ref="(el) => setTextareaRef(sub.fieldCode, el)"
v-model:value="(form as any)[sub.textKey]"
:placeholder="sub.label"
@dblclick="openFieldExpand(sub.textKey, sub.label)"
/>
</div>
</template>
</div>
</div>
</div>
<ExpandModal />
<CiteHistoryDrawer />
</div>
@@ -1079,6 +1081,83 @@ const rightFields = [
.medical-record-panel {
padding: 8px 0 24px;
}
.mr-toolbar :deep(.ant-btn) {
padding-inline: 6px;
height: 28px;
}
/* 双列流式排布:字段从左到右、从上到下填满,不再一侧空一大块 */
.mr-fields-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
align-items: start;
margin-bottom: 8px;
}
.mr-field-block {
min-width: 0;
}
.mr-field-head {
display: flex;
flex-wrap: nowrap;
gap: 8px 12px;
align-items: center;
min-height: 28px;
}
.mr-field-label {
display: inline-flex;
align-items: center;
gap: 6px;
flex: 0 0 auto;
min-width: 5.5em;
font-size: 14px;
font-weight: 600;
line-height: 24px;
white-space: nowrap;
color: hsl(var(--foreground));
}
/* 一级 label 前置色条:醒目分区 */
.mr-field-label::before {
content: '';
flex-shrink: 0;
width: 3px;
height: 14px;
border-radius: 2px;
background: hsl(var(--primary));
}
/* 区块标题(如中医病案):更大、更醒目 */
.mr-field-label--section {
font-size: 15px;
font-weight: 700;
letter-spacing: 0.02em;
}
.mr-field-label--section::before {
width: 4px;
height: 16px;
background: hsl(173 58% 39%);
}
/* 二级 label圆点 + 更小更淡,一眼能区分层级 */
.mr-field-label--sub {
min-width: 5em;
font-size: 12px;
font-weight: 500;
color: hsl(var(--muted-foreground));
}
.mr-field-label--sub::before {
width: 6px;
height: 6px;
border-radius: 50%;
background: hsl(173 45% 45%);
opacity: 0.9;
}
.mr-field-search {
min-width: 0;
flex: 1 1 auto;
}
.mr-tcm-block {
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed hsl(var(--border));
}
/* 体征一段话:标签 + 下划线输入横向流式排布 */
.vitals-para {
display: flex;

View File

@@ -74,6 +74,10 @@ const props = withDefaults(
*(由系统配置 mr_common_display_mode=bubble|both 控制)
*/
showCommonInBubble?: boolean;
/**
* 贴在 label 右侧时用紧凑输入框(气泡仍按最小宽度展开,不跟随输入框变窄)
*/
compact?: boolean;
}>(),
{
fieldCode: '',
@@ -83,6 +87,7 @@ const props = withDefaults(
disabled: false,
loadOnFocus: true,
showCommonInBubble: false,
compact: false,
},
);
@@ -275,7 +280,11 @@ function updatePanelPlacement() {
const panelMax = Math.min(PREFERRED_PANEL, avail);
listViewportH.value = Math.max(100, panelMax - HINT_HEIGHT - HEAD_HEIGHT);
const width = Math.min(Math.max(rect.width, 560), vw - PANEL_PAD * 2);
const PANEL_MIN_WIDTH = 560;
const width = Math.min(
Math.max(rect.width, PANEL_MIN_WIDTH),
vw - PANEL_PAD * 2,
);
let left = rect.left;
if (left + width > vw - PANEL_PAD) {
left = vw - PANEL_PAD - width;
@@ -861,13 +870,18 @@ defineExpose({
});
</script>
<template>
<div ref="containerRef" class="entry-keyword-bubble">
<div
ref="containerRef"
class="entry-keyword-bubble"
:class="{ 'entry-keyword-bubble--compact': compact }"
>
<Input
ref="inputRef"
v-model:value="keyword"
:placeholder="placeholder"
:disabled="disabled"
allow-clear
size="small"
@input="handleInput"
@focus="handleFocus"
@keydown="handleKeydown"
@@ -1092,6 +1106,11 @@ defineExpose({
position: relative;
width: 100%;
}
.entry-keyword-bubble--compact {
width: 220px;
max-width: 100%;
flex: 1 1 180px;
}
.entry-keyword-bubble__panel {
/* position/尺寸由 updatePanelPlacement 以 fixed 写入,避免被 Modal overflow 裁切 */
overflow: hidden;

View File

@@ -10,6 +10,9 @@ export const MEDICAL_RECORD_FIELD_OPTIONS = [
{ label: '家族史', value: 'family_history' },
{ label: '流行病学史', value: 'epidemic_history' },
{ label: '既往史', value: 'past_history' },
{ label: '月经史', value: 'menstrual_history' },
{ label: '婚育史', value: 'marital_history' },
{ label: '个人史', value: 'personal_history' },
{ label: '过敏史', value: 'allergy_history' },
{ label: '体征检查', value: 'physical_exam' },
{ label: '辅助检查', value: 'auxiliary_exam' },
@@ -31,6 +34,9 @@ export const MEDICAL_RECORD_VIEW_FIELDS = [
{ label: '家族史', value: 'family_history' },
{ label: '流行病学史', value: 'epidemic_history' },
{ label: '既往史', value: 'past_history' },
{ label: '月经史', value: 'menstrual_history' },
{ label: '婚育史', value: 'marital_history' },
{ label: '个人史', value: 'personal_history' },
{ label: '过敏史', value: 'allergy_history' },
{ label: '体征检查', value: 'physical_exam' },
{ label: '辅助检查', value: 'auxiliary_exam' },
@@ -41,6 +47,29 @@ export const STATUS_OPTIONS = [
{ label: '禁用', value: 0 },
];
/**
* 按性别/年龄判断病史字段是否展示
* 月经史:仅女;婚育史:男>22 或 女>20个人史始终
*/
export function isHistoryFieldVisible(
fieldCode: string,
sex: number,
age: number,
): boolean {
if (fieldCode === 'menstrual_history') {
return Number(sex) === 2;
}
if (fieldCode === 'marital_history') {
const s = Number(sex);
const a = Number(age) || 0;
return (s === 1 && a > 22) || (s === 2 && a > 20);
}
if (fieldCode === 'personal_history') {
return true;
}
return true;
}
/** 空病历结构 */
export function emptyMedicalRecord(registerId = 0, storeId = 0) {
return {
@@ -63,6 +92,9 @@ export function emptyMedicalRecord(registerId = 0, storeId = 0) {
family_history: '',
epidemic_history: '',
past_history: '',
menstrual_history: '',
marital_history: '',
personal_history: '',
allergy_history: '',
physical_exam: '',
auxiliary_exam: '',