feat: 病历模块优化
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 接诊
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
Reference in New Issue
Block a user