fix:问题修复

This commit is contained in:
huxuejian 2026-09-21 17:15:54 +08:00
parent 5b316c61b0
commit 8138458b3b
6 changed files with 307 additions and 25 deletions

View File

@ -350,6 +350,14 @@ export async function importHlwDrugInfo(data) {
});
}
export async function getHlwSchedule(data) {
return await useHlw({ type: 'getHlwSchedule', ...data });
}
export async function saveHlwSchedule(data) {
return await useHlw({ type: 'saveHlwSchedule', ...data });
}
// 将互联网医院药品信息同步至 HIS
export async function updateHisDrugInfo(data) {
return await useHlw({

View File

@ -47,10 +47,10 @@
<div class="w-3 border-t border-gray-500 border-dashed"></div>
<div class="rp-symbol mx-2">Rp</div>
<div class="flex-grow border-t border-gray-500 border-dashed"></div>
<!-- <div class="absolute right-0 -top-2">
<div class="border-2 border-red-500 px-2 py-1 text-red-500 transform rotate-15">处方外购专用章</div>
<div class="absolute right-0 -top-2">
<!-- <div class="border-2 border-red-500 px-2 py-1 text-red-500 transform rotate-15">处方外购专用章</div> -->
<img class="w-30 h-30 mt-3" :src="stampPng" alt="">
</div> -->
</div>
</div>
<div class="medicine-list">
<div v-for="(i, idx) in drugList" :key="i.insurance_code" class="medicine-item">
@ -76,17 +76,20 @@
</div>
</div>
<div class="flex pt-3">
<div class="flex-shrink-0 w-1/3">
审核:
</div>
<div class="flex-shrink-0 w-1/3">
调配:
</div>
<div class="flex-shrink-0 w-1/3">
核对:
</div>
<div class="flex-shrink-0 w-1/3">
发药:
核对/发药:
</div>
</div>
<div class="notice">注意:请勿丢失,处方当日有效,因特殊情况,该处方有效为<span class="write_line"></span> 天(签名:<span class="write_line"></span> )</div>
<div class="footer-notice footer-notice--first leading-normal text-sm">1.根据《处方管理办法》规定,本处方开具三天有效;</div>
<div class="footer-notice leading-normal text-sm">2.本处方仅供患者{{ order?.drugStoreName||'' }}使用,本处方金额以实际药店成交金额为准;</div>
<div class="footer-notice leading-normal text-sm">3.请仔细阅读药品使用说明,服药谨遵医嘱,如有不适请立即停药并前往医院就诊。</div>
<!-- <div class="notice">注意:请勿丢失,处方当日有效,因特殊情况,该处方有效为<span class="write_line"></span> 天(签名:<span class="write_line"></span> )</div> -->
</div>
</div>
</layout-main>
@ -114,7 +117,7 @@
</template>
<script setup>
import { computed, nextTick, ref, watch } from 'vue';
import { getSealImage } from '@/api/hlw';
import { getConsultOrder, getSealImage } from '@/api/hlw';
import validate from "@/utils/validate";
import dayjs from 'dayjs';
@ -141,6 +144,9 @@ const printPaperSize = ref('A5')
const printing = ref(false)
const doctorCode = ref('')
const pharmacistNo = ref('')
const order = ref(null);
let orderLoadingId = '';
let orderLoadingPromise = null;
const diagnosis = computed(() => props.data && Array.isArray(props.data.diagnosisList) ? props.data.diagnosisList : [])
const diagnosisStr = computed(() => diagnosis.value.map((item) => item.name).join('+'))
const drugList = computed(() => {
@ -211,8 +217,65 @@ function createPrintPageStyle(paperSize) {
return style;
}
function currentPrescriptionOrderId() {
return typeof props.data?.orderId === 'string' ? props.data.orderId.trim() : '';
}
async function ensureOrder() {
const prescriptionOrderId = currentPrescriptionOrderId();
if (!prescriptionOrderId) {
order.value = null;
return;
}
// 避免复用上一张处方的订单信息,导致打印出错误的门店名称。
if (order.value?.orderId === prescriptionOrderId) return;
// 抽屉打开后立即查询;若紧接着点击打印,则复用同一次请求。
if (orderLoadingId === prescriptionOrderId && orderLoadingPromise) {
return orderLoadingPromise;
}
order.value = null;
orderLoadingId = prescriptionOrderId;
orderLoadingPromise = getConsultOrder({ orderId: prescriptionOrderId, page: 1, pageSize: 1 })
.then((res) => {
const payload = res?.data?.data || res?.data || [];
const orders = Array.isArray(payload) ? payload : [];
// 请求期间若已切换处方,不允许旧请求覆盖当前处方的店铺名称。
if (props.visible && currentPrescriptionOrderId() === prescriptionOrderId) {
order.value = orders.find((item) => item?.orderId === prescriptionOrderId) || null;
}
})
.catch(() => {
// 查询失败时保留空名称,展示与打印流程均不被阻断。
if (props.visible && currentPrescriptionOrderId() === prescriptionOrderId) order.value = null;
})
.finally(() => {
if (orderLoadingId === prescriptionOrderId) {
orderLoadingId = '';
orderLoadingPromise = null;
}
});
return orderLoadingPromise;
}
watch(
() => [props.visible, props.data?.orderId],
([visible]) => {
if (visible) {
void ensureOrder();
} else {
order.value = null;
}
},
{ immediate: true }
);
async function print(paperSize = 'A5') {
if (!paperRef.value) return;
await ensureOrder();
await nextTick();
// Element Plus 抽屉带有定位、变换和裁剪样式,直接打印抽屉内节点会得到空白页。
@ -412,15 +475,18 @@ defineExpose({ print: openPrintDialog });
.no {
margin-top: 5px;
}
.write_line {
position: relative;
display: inline-block;
height: 1em;
width: 32px;
}
.write_line+.write_line {
width: 52px;
}
.write_line::after {
content: '';
position: absolute;
@ -429,7 +495,6 @@ defineExpose({ print: openPrintDialog });
bottom: -4px;
border-bottom: 1px solid currentColor;
}
</style>
<style>
.rx-print-root {
@ -456,17 +521,20 @@ defineExpose({ print: openPrintDialog });
display: flex !important;
flex-direction: column !important;
width: 100% !important;
min-height: 281mm !important;
height: 281mm !important;
max-height: 281mm !important;
box-sizing: border-box !important;
padding: 0 !important;
box-shadow: none !important;
overflow: hidden !important;
/* 打印时提高可读性,同时收紧行距,避免放大字体后版面变松散。 */
font-size: 15px !important;
font-size: 14px !important;
line-height: 1.28 !important;
}
body.printing-rx>.rx-print-root--a5 .rx-print-area {
min-height: 194mm !important;
height: 194mm !important;
max-height: 194mm !important;
}
body.printing-rx>.rx-print-root .header {
@ -480,7 +548,7 @@ defineExpose({ print: openPrintDialog });
body.printing-rx>.rx-print-root .hospital {
margin: 3px 0 !important;
font-size: 18px !important;
font-size: 17px !important;
}
body.printing-rx>.rx-print-root .patient-info,
@ -498,7 +566,7 @@ defineExpose({ print: openPrintDialog });
}
body.printing-rx>.rx-print-root .rp-symbol {
font-size: 21px !important;
font-size: 20px !important;
}
body.printing-rx>.rx-print-root .medicine-item {
@ -510,8 +578,8 @@ defineExpose({ print: openPrintDialog });
}
body.printing-rx>.rx-print-root .medicine-section img {
width: 86px !important;
height: 86px !important;
width: 120px !important;
height: 120px !important;
margin-top: 2px !important;
}
@ -524,6 +592,17 @@ defineExpose({ print: openPrintDialog });
padding-top: 4px !important;
}
/* 页尾三条使用说明比正文再小一级,并优先保持单行展示。 */
body.printing-rx>.rx-print-root .footer-notice {
font-size: 12px !important;
line-height: 1.25 !important;
white-space: nowrap !important;
}
body.printing-rx>.rx-print-root .footer-notice--first {
margin-top: 7px !important;
}
body.printing-rx>.rx-print-root .signImage {
width: 76px !important;
height: 30px !important;
@ -543,7 +622,11 @@ defineExpose({ print: openPrintDialog });
margin-bottom: 4px !important;
}
body.printing-rx>.rx-print-root--a5 .medicine-section img,
body.printing-rx>.rx-print-root--a5 .medicine-section img {
width: 120px !important;
height: 120px !important;
}
body.printing-rx>.rx-print-root--dense .medicine-section img {
width: 72px !important;
height: 72px !important;

View File

@ -255,4 +255,13 @@ export default [
title: "配送药品库",
},
},
{
name: "HNHLWSCHEDULE",
path: "/HNHLWSCHEDULE",
component: () => import("../views/hlw-schedule/hlw-schedule"),
meta: {
keepAlive: false,
title: "互联网医院排班",
},
},
];

View File

@ -17,7 +17,7 @@ export const drugFieldDict = [
// 放宽分类校验,允许任意字符串;前端选择仍可用固定下拉,导入/编辑不会因为新分类报错
{ name: '药品分类', field: 'category', verify: (val, label) => verifyString(val, label) },
{ name: '药品拼音码', field: 'pinyin_code', verify: (val, label) => verifyString(val, label) },
{ name: 'ERP ID', field: 'erpId', required: true, libType: 'hlw', verify: (val, label) => verifyString(val, label, 1) },
{ name: 'ERP ID', field: 'erpId', libType: 'hlw', verify: (val, label) => verifyString(val, label) },
{ name: '药库货品ID', field: 'product_id', libType: 'online', verify: (val, label) => validateNumber(val, label) },
{ name: '通用名', field: 'generic_name', verify: (val, label) => verifyString(val, label) },
{ name: '通用名拼音码', field: 'generic_pinyin', verify: (val, label) => verifyString(val, label) },

View File

@ -15,7 +15,9 @@
:label="getOptionLabel(option)" :value="getOptionValue(option)" />
</el-select>
<el-input v-else clearable :model-value="form[item.prop]" :maxlength="item.maxlength"
:placeholder="`请输入${item.label}`" @update:model-value="changeValue($event, item.prop)" />
:placeholder="item.placeholder || `请输入${item.label}`"
:readonly="item.readonly"
@update:model-value="changeValue($event, item.prop)" />
</el-form-item>
</el-col>
</el-row>
@ -176,7 +178,7 @@ const onlineBasicInfoExtra = [
const basicInfoList = computed(() => {
const idField = props.libType === 'online'
? { label: '药库货品ID', prop: "product_id", maxlength: 100 }
: { label: 'ERP ID', prop: "erpId", maxlength: 100 };
: { label: 'ERP ID', readonly: true, prop: "erpId", maxlength: 100, placeholder: '系统自动生成' };
const fields = [...basicInfo.value.slice(0, 9), idField, ...basicInfo.value.slice(9)];
return props.libType === 'online' ? [...fields, ...onlineBasicInfoExtra] : fields;
})

View File

@ -0,0 +1,180 @@
<template>
<div v-loading="loading" class="schedule-page">
<el-calendar v-model="calendarDate">
<template #header>
<div class="calendar-header">
<el-button text @click="changeMonth(-1)">‹ 上个月</el-button>
<div class="calendar-title">{{ currentMonthLabel }}</div>
<div>
<el-button text @click="goToday">本月</el-button>
<el-button text @click="changeMonth(1)">下个月 ›</el-button>
</div>
</div>
</template>
<template #date-cell="{ data }">
<div class="schedule-cell" :class="{ 'is-other-month': data.type !== 'current-month', 'is-past': isPastDate(data.day) }"
@click="openSchedule(data)">
<div class="schedule-day">{{ dayNumber(data.day) }}</div>
<template v-if="data.type === 'current-month'">
<div v-if="scheduleByDate[data.day]?.doctors?.length" class="schedule-line doctor-line">
<span class="role-label">医生</span>
<el-tooltip :content="peopleNames(scheduleByDate[data.day].doctors)" placement="top" :show-after="300">
<span class="role-names">{{ displayPeople(scheduleByDate[data.day].doctors) }}</span>
</el-tooltip>
<span class="person-count">{{ scheduleByDate[data.day].doctors.length }}人</span>
</div>
<div v-if="scheduleByDate[data.day]?.pharmacists?.length" class="schedule-line pharmacist-line">
<span class="role-label">药师</span>
<el-tooltip :content="peopleNames(scheduleByDate[data.day].pharmacists)" placement="top" :show-after="300">
<span class="role-names">{{ displayPeople(scheduleByDate[data.day].pharmacists) }}</span>
</el-tooltip>
<span class="person-count">{{ scheduleByDate[data.day].pharmacists.length }}人</span>
</div>
<div v-if="!hasSchedule(data.day)" class="empty-schedule">暂无排班</div>
</template>
</div>
</template>
</el-calendar>
<el-dialog v-model="dialogVisible" :title="`${selectedDate} 排班`" width="560px" destroy-on-close>
<el-alert v-if="selectedDate && isPastDate(selectedDate)" title="过去日期的排班仅可查看,不能修改。" type="info" :closable="false" class="mb-16px" />
<el-form label-width="80px">
<el-form-item label="值班医生">
<el-select v-model="editingDoctorNos" multiple filterable clearable class="w-full" placeholder="请选择值班医生" :disabled="isSelectedDatePast">
<el-option v-for="person in doctors" :key="person.doctorNo" :label="person.doctorName" :value="person.doctorNo" />
</el-select>
</el-form-item>
<el-form-item label="值班药师">
<el-select v-model="editingPharmacistNos" multiple filterable clearable class="w-full" placeholder="请选择值班药师" :disabled="isSelectedDatePast">
<el-option v-for="person in pharmacists" :key="person.doctorNo" :label="person.doctorName" :value="person.doctorNo" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">{{ isSelectedDatePast ? '关闭' : '取消' }}</el-button>
<el-button v-if="!isSelectedDatePast" type="primary" :loading="saving" @click="save">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { computed, onMounted, ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { getAllDoctorNos, getHlwSchedule, saveHlwSchedule } from '@/api/hlw';
import { memberStore } from '@/store/member';
import { storeToRefs } from 'pinia';
const { memberInfo } = storeToRefs(memberStore());
const calendarDate = ref(startOfMonth(new Date()));
const loading = ref(false);
const saving = ref(false);
const scheduleByDate = ref({});
const doctors = ref([]);
const pharmacists = ref([]);
const dialogVisible = ref(false);
const selectedDate = ref('');
const editingDoctorNos = ref([]);
const editingPharmacistNos = ref([]);
const currentMonth = computed(() => formatMonth(calendarDate.value));
const currentMonthLabel = computed(() => `${calendarDate.value.getFullYear()}年${calendarDate.value.getMonth() + 1}月`);
const isSelectedDatePast = computed(() => isPastDate(selectedDate.value));
function startOfMonth(date) { return new Date(date.getFullYear(), date.getMonth(), 1); }
function formatMonth(date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; }
function formatDate(date) { return `${formatMonth(date)}-${String(date.getDate()).padStart(2, '0')}`; }
function dayNumber(date) { return Number(date.slice(-2)); }
function isPastDate(date) { return Boolean(date) && date < formatDate(new Date()); }
function hasSchedule(date) {
const schedule = scheduleByDate.value[date];
return Boolean(schedule?.doctors?.length || schedule?.pharmacists?.length);
}
function peopleNames(people) { return people.map(person => person.doctorName).join('、'); }
function displayPeople(people) {
const names = people.map(person => person.doctorName).filter(Boolean);
return names.length > 2 ? `${names.slice(0, 2).join('、')}等` : names.join('、');
}
function changeMonth(offset) { calendarDate.value = new Date(calendarDate.value.getFullYear(), calendarDate.value.getMonth() + offset, 1); }
function goToday() { calendarDate.value = startOfMonth(new Date()); }
function openSchedule(data) {
if (data.type !== 'current-month') return;
const schedule = scheduleByDate.value[data.day] || {};
selectedDate.value = data.day;
editingDoctorNos.value = (schedule.doctors || []).map(person => person.doctorNo);
editingPharmacistNos.value = (schedule.pharmacists || []).map(person => person.doctorNo);
dialogVisible.value = true;
}
async function loadSchedule() {
const corpId = memberInfo.value.corpId;
if (!corpId) return;
loading.value = true;
const res = await getHlwSchedule({ corpId, month: currentMonth.value });
loading.value = false;
if (!res.success) return ElMessage.error(res.message || '排班加载失败');
const schedules = Array.isArray(res.data?.data)
? res.data.data
: (Array.isArray(res.data) ? res.data : []);
scheduleByDate.value = schedules.reduce((map, item) => {
map[item.date] = item;
return map;
}, {});
}
async function loadPeople() {
const res = await getAllDoctorNos({ corpId: memberInfo.value.corpId, jobs: ['doctor', 'pharmacist'] });
if (!res.success) return ElMessage.error(res.message || '人员加载失败');
// useHlw responses wrap the service payload once, so personnel is normally
// in res.data.data. Keep the direct-array fallback for callers without it.
const people = Array.isArray(res.data?.data)
? res.data.data
: (Array.isArray(res.data) ? res.data : []);
doctors.value = people.filter(person => person.job === 'doctor');
pharmacists.value = people.filter(person => person.job === 'pharmacist');
}
async function save() {
if (saving.value || isSelectedDatePast.value) return;
saving.value = true;
const res = await saveHlwSchedule({
corpId: memberInfo.value.corpId,
date: selectedDate.value,
doctorNos: editingDoctorNos.value,
pharmacistNos: editingPharmacistNos.value,
operator: memberInfo.value.anotherName,
operatorId: memberInfo.value._id || memberInfo.value.userid,
});
saving.value = false;
if (!res.success) return ElMessage.error(res.message || '保存排班失败');
ElMessage.success(res.message || '排班保存成功');
dialogVisible.value = false;
loadSchedule();
}
watch(currentMonth, loadSchedule);
onMounted(() => {
loadPeople();
loadSchedule();
});
</script>
<style scoped>
.schedule-page { height: 100%; padding: 16px; overflow: auto; background: var(--el-bg-color-page); }
.schedule-page :deep(.el-calendar) { min-width: 900px; background: var(--el-bg-color); }
.calendar-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.calendar-title { font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); }
.schedule-page :deep(.el-calendar-day) { height: 118px; padding: 8px; }
.schedule-cell { height: 100%; cursor: pointer; }
.schedule-cell.is-other-month { cursor: default; opacity: .45; }
.schedule-cell.is-past:not(.is-other-month) { background: var(--el-fill-color-lighter); }
.schedule-day { margin-bottom: 5px; font-size: 14px; font-weight: 600; }
.schedule-line { display: flex; align-items: center; gap: 4px; min-width: 0; margin-top: 5px; font-size: 12px; line-height: 20px; }
.role-label { flex-shrink: 0; width: 28px; font-weight: 600; }
.doctor-line .role-label { color: var(--el-color-primary); }
.pharmacist-line .role-label { color: var(--el-color-success); }
.role-names { min-width: 0; flex: 1; overflow: hidden; color: var(--el-text-color-regular); text-overflow: ellipsis; white-space: nowrap; }
.person-count { flex-shrink: 0; padding: 0 4px; border-radius: 8px; background: var(--el-fill-color); color: var(--el-text-color-secondary); font-size: 11px; }
.empty-schedule { margin-top: 18px; color: var(--el-text-color-placeholder); font-size: 12px; }
</style>