Files
his/openhis-ui-vue3/src/views/inpatientNurse/inOut/components/bedAllocation.vue
zhangfei 9c3e603b94 Fix Bug #443: 手术计费:点击签发耗材时异常报错
当手术计费弹窗中点击"签发"耗材时,因耗材的locationId(发放库房)为空导致后端异常。
在DoctorStationAdviceAppServiceImpl.handDevice方法中,当locationId为null时,使用登录用户的科室ID作为默认值,
与NurseBillingAppService中的处理方式保持一致。
2026-05-08 09:14:18 +08:00

907 lines
22 KiB
Vue
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="bedAllocation-container">
<div class="bedAllocation-main">
<PendingPatientList
:list="patientList"
:active-id="activePatientId"
@item-click="handleCardClick"
@item-dblclick="handleCardDblClick"
@dragstart="handleDragStart"
@dragend="handleDragEnd"
/>
<div class="disabled-wrapper right-panel" @dragover="handleDragOver" @drop="handleDrop">
<div class="right-filter">
<Filter
:query-params="queryParams"
:form-items="filterItems"
@query="handleQuery"
@reset="resetQuery"
>
<template #bedStatus>
<el-select v-model="bedStatusFilter" clearable style="width: 240px">
<el-option label="全部" value="" />
<el-option
v-for="item in initInfoOptions.bedStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</Filter>
</div>
<el-scrollbar class="right-scrollbar">
<div class="bedAllocation-search">
<div
v-for="item in filteredBadList"
:key="item.id"
class="search-item"
:class="{ 'drag-over': draggedOverBedId === item.bedId }"
@dragover="handleBedDragOver($event, item)"
@dragenter="handleBedDragEnter(item)"
@dragleave="handleBedDragLeave"
@drop="handleBedDrop($event, item)"
>
<div class="bed-card__body">
<div>
<div class="bed-card__title" :title="(item.houseName || '-') + '-' + (item.bedName || '-')">
{{ (item.houseName || '-') + '-' + (item.bedName || '-') }}
</div>
<div class="bed-tag" :class="getBedTagClass(item)">
{{ item.bedStatus_enumText }}
</div>
</div>
<div class="bed-card__sub">
<span class="bed-card__patient" :title="item.patientName || ''">
{{ item.patientName || '—' }}
</span>
<el-tag
v-if="item.encounterId"
size="small"
class="bed-card__age-tag"
effect="plain"
:class="{
'bed-card__age-tag-female': item.genderEnum_enumText === '女性',
'bed-card__age-tag-male': item.genderEnum_enumText === '男性',
}"
>
{{ item.genderEnum_enumText || '-' }}
<span v-if="item.age"> · {{ item.age }}</span>
</el-tag>
</div>
</div>
</div>
</div>
</el-scrollbar>
</div>
</div>
<TransferInDialog
v-model:visible="transferInDialogVisible"
:pendingInfo="pendingInfo"
:priorityOptions="priorityOptions"
@okAct="handleTransferInOk"
/>
<SignEntryDialog v-model:visible="signEntryDialogVisible" />
</div>
</template>
<script setup lang="ts">
import Filter from '@/components/TableLayout/Filter.vue';
import {computed, onBeforeMount, onMounted, reactive, ref} from 'vue';
import TransferInDialog from './transferInDialog.vue';
import SignEntryDialog from './signEntryDialog.vue';
import {childLocationList, getBedInfo, getInit, getPendingInfo, getPractitionerWard} from './api';
import {ElLoading, ElMessage, ElMessageBox} from 'element-plus';
import PendingPatientList from '@/components/PendingPatientList/index.vue';
// 定义相关类型
interface OptionItem {
id: string | number;
name: string;
}
interface OptionItemTwo {
value: string | number;
label: string;
}
interface InitInfoOptions {
encounterStatusOptions: OptionItemTwo[];
bedStatusOptions: OptionItemTwo[];
priorityOptions: OptionItem[];
wardListOptions: OptionItemTwo[];
}
const transferInDialogVisible = ref(false);
const signEntryDialogVisible = ref(false);
const state = reactive({});
const loading = ref(false);
const total = ref();
const badList = ref<any[]>([]);
const patientList = ref<any[]>([]);
const pendingInfo = ref({});
const draggedPatient = ref<any>(null);
const wardLocationList = ref<OptionItem[]>([]);
const draggedOverBedId = ref<number | null>(null);
const priorityOptions = ref<OptionItem[]>([]);
const wardListOptions = ref<OptionItem[]>([]);
const initInfoOptions = ref<InitInfoOptions>({
encounterStatusOptions: [],
bedStatusOptions: [],
priorityOptions: [],
wardListOptions: [],
});
// 用于处理单击/双击冲突
let clickTimer = null;
let loadingInstance: any = undefined;
// 入院病区loading
const selectHosLoding = ref(true);
// 入院病房loading
const selectHoouseLoding = ref(true);
// 新增床状态筛选字段
const bedStatusFilter = ref('');
const activePatientId = computed(() => {
const active = patientList.value?.find?.((it) => it?.active);
return active?.id || '';
});
const filterItems = computed(() => [
{
type: 'select',
label: '入院病区',
prop: 'wardId',
style: { width: '240px' },
multiple: false,
filterable: false,
options: (initInfoOptions.value.wardListOptions || []).map((i) => ({
label: i.name,
value: i.id,
})),
extraprops: { loading: selectHosLoding.value, clearable: false },
onChange: (value) => {
changeWardLocationId(value);
},
},
{
type: 'select',
label: '入院病房',
prop: 'houseId',
style: { width: '240px' },
multiple: false,
filterable: false,
options: (wardLocationList.value || []).map((i) => ({ label: i.name, value: i.id })),
extraprops: { loading: selectHoouseLoding.value, clearable: true },
onChange: () => onHosHouse(),
},
{
type: 'select',
label: '住院状态',
prop: 'encounterStatus',
style: { width: '240px' },
multiple: false,
filterable: false,
options: [
{ label: '全部', value: '' },
...(initInfoOptions.value.encounterStatusOptions || []).map((i) => ({
label: i.label,
value: i.value,
})),
],
extraprops: { clearable: true },
onChange: () => onHosStatus(),
},
{
type: 'custom',
label: '床位状态',
prop: 'bedStatus',
slot: 'bedStatus',
style: { width: '240px' },
},
{
type: 'input',
label: '住院号',
prop: 'searchKey',
style: { width: '240px' },
placeholder: '请输入住院号',
clearable: true,
},
]);
onBeforeMount(() => {});
const queryParams = ref<{
pageNo: number;
pageSize: number;
searchKey: string;
wardId?: string;
houseId: string;
encounterStatus: string;
bedStatus: string;
}>({
pageNo: 1,
pageSize: 50,
searchKey: '',
wardId: '',
houseId: '',
encounterStatus: '',
bedStatus: '', // 这个字段现在只用于床位查询,不再用于患者列表查询
});
const ininData = async () => {
try {
// 先获取初始化数据
const initRes = await getInit();
initInfoOptions.value = initRes.data;
priorityOptions.value = initRes.data.priorityOptions || [];
// 然后获取病区数据(使用与病区管理页面相同的接口)
const wardRes = await getPractitionerWard();
const wardList = wardRes.data || [];
selectHosLoding.value = false;
queryParams.value.wardId = wardList[0]?.id || '';
initInfoOptions.value.wardListOptions = wardList;
// 获取病房数据
if (wardList[0]?.id) {
await changeWardLocationId(wardList[0].id, true); // 传入 true 表示初始化阶段,不调用 getList
}
// 最后获取列表数据
getList();
} catch (error) {
console.error('初始化数据失败:', error);
// 即使某个请求失败,也要尝试加载列表
getList();
}
};
onMounted(() => {
ininData();
});
const refreshTap = async () => {
await ininData();
};
defineExpose({ state, refreshTap });
// 计算属性:根据床状态筛选条件过滤床位列表
const filteredBadList = computed(() => {
if (!bedStatusFilter.value) {
return badList.value;
}
return badList.value.filter((item) => item.bedStatus == bedStatusFilter.value);
});
const getList = async () => {
loadingInstance = ElLoading.service({ fullscreen: true });
try {
// 并行获取患者列表和床位列表
const [patientData, bedRes] = await Promise.all([
getPatientList(),
getBedInfo({
...queryParams.value,
encounterStatus: undefined, // 移除encounterStatus确保不影响床位列表查询
})
]);
// 更新床位列表
badList.value = bedRes.data.records;
} catch (error) {
console.error('获取列表数据失败:', error);
} finally {
if (loadingInstance) {
loadingInstance.close();
}
}
};
// 重置查询条件
async function resetQuery() {
// 不重置入院病区
const resetParams = {
...queryParams.value,
};
queryParams.value = {
...resetParams,
pageNo: 1,
pageSize: 50,
searchKey: '',
houseId: '',
encounterStatus: '',
bedStatus: '',
};
bedStatusFilter.value = '';
await getList();
}
// 入院病区下拉选
function changeWardLocationId(id: string | number, isInit = false) {
let params = {
locationId: id,
locationForm: 10,
};
queryParams.value.houseId = '';
selectHoouseLoding.value = true;
return childLocationList(params).then((res) => {
selectHoouseLoding.value = false;
wardLocationList.value = res;
if (!isInit) {
// 在非初始化情况下,需要同时更新患者列表和床位列表
getPatientList();
// 床位查询不使用encounterStatus参数只使用基本的查询参数
const bedQueryParams = {
...queryParams.value,
encounterStatus: undefined, // 移除encounterStatus确保不影响床位列表查询
};
getBedInfo(bedQueryParams).then((bedRes) => {
badList.value = bedRes.data.records;
});
}
return res;
});
}
// 入院病房下拉选
const onHosHouse = async () => {
await getList();
};
// 住院状态下拉选
const onHosStatus = async () => {
await getList();
};
// 获新入院患者列表
async function getPatientList() {
// 为患者列表查询创建一个新的参数对象不包含bedStatus
const patientQueryParams = {
...queryParams.value,
bedStatus: undefined, // 移除bedStatus确保不影响患者列表查询
};
try {
const res = await getPendingInfo(patientQueryParams);
loading.value = false;
patientList.value = res.data.records;
total.value = res.data.total;
return res;
} catch (error) {
console.error('获取患者列表失败:', error);
throw error;
}
}
const handleTransferInOk = async () => {
transferInDialogVisible.value = false;
await getList();
};
// 单击患者卡片事件 - 直接触发入科选床界面
function handleCardClick(item: any, index: number) {
if (item.encounterStatus == 2) {
// 显示提示信息,指导用户如何分配床位
ElMessage({
message: '该患者尚未分配病床,请通过拖拽操作将患者分配到右侧床位',
type: 'warning',
grouping: true,
showClose: true,
});
} else {
pendingInfo.value = {
...item,
entranceType: 1,
};
transferInDialogVisible.value = true;
}
}
// 双击患者卡片事件 - 保持原有逻辑
function handleCardDblClick(item: any) {
if (item.encounterStatus == 2) {
// 显示提示信息,指导用户如何分配床位
ElMessage({
message: '该患者尚未分配病床,请通过拖拽操作将患者分配到右侧床位',
type: 'warning',
grouping: true,
showClose: true,
});
} else {
pendingInfo.value = {
...item,
entranceType: 1,
};
transferInDialogVisible.value = true;
}
}
// 拖拽开始事件
function handleDragStart(event: DragEvent, item: any) {
if (event.dataTransfer) {
event.dataTransfer.setData('text/plain', JSON.stringify(item));
draggedPatient.value = item;
// 设置拖拽样式
if (event.target instanceof HTMLElement) {
event.target.classList.add('dragging');
}
}
}
async function handleQuery() {
await getList();
}
// 拖拽结束事件
function handleDragEnd(event: DragEvent) {
// 清除拖拽样式
if (event.target instanceof HTMLElement) {
event.target.classList.remove('dragging');
}
// 清除高亮
draggedOverBedId.value = null;
}
// 拖拽过程中阻止默认行为
function handleDragOver(event: DragEvent) {
event.preventDefault();
}
// 拖拽放置事件
function handleDrop(event: DragEvent) {
event.preventDefault();
// 清除高亮
draggedOverBedId.value = null;
}
// 床位拖拽事件
function handleBedDragOver(event: DragEvent, bed: any) {
event.preventDefault();
}
// 床位拖拽进入事件
function handleBedDragEnter(bed: any) {
draggedOverBedId.value = bed.bedId;
}
// 床位拖拽离开事件
function handleBedDragLeave(event: DragEvent) {
// 避免子元素触发的dragleave事件清除高亮
if (event.target === event.currentTarget) {
draggedOverBedId.value = null;
}
}
// 床位放置事件
function handleBedDrop(event: DragEvent, bed: any) {
if (draggedPatient.value.encounterStatus == 2 && bed.bedStatus == 5) {
ElMessage({
message: '该床位已被占用!',
type: 'error',
grouping: true,
showClose: true,
});
} else {
if (draggedPatient.value.encounterStatus == 5) {
ElMessageBox.confirm('是否确认换床?')
.then(() => {
event.preventDefault();
// 清除高亮
draggedOverBedId.value = null;
if (draggedPatient.value) {
// 合并患者信息和床位信息
pendingInfo.value = {
...draggedPatient.value,
bedName: bed.bedName,
bedId: bed.bedId,
targetHouseId: bed.houseId,
entranceType: 2,
targetEncounterId: bed.encounterId,
houseName: bed.houseName,
};
// 显示TransferInDialog对话框
transferInDialogVisible.value = true;
// 清空拖拽的患者信息
draggedPatient.value = null;
}
})
.catch(() => {});
} else {
event.preventDefault();
// 清除高亮
draggedOverBedId.value = null;
if (draggedPatient.value) {
// 合并患者信息和床位信息
pendingInfo.value = {
...draggedPatient.value,
bedName: bed.bedName,
bedId: bed.bedId,
targetHouseId: bed.houseId,
entranceType: 2,
targetEncounterId: bed.encounterId,
houseName: bed.houseName,
};
// 显示TransferInDialog对话框
transferInDialogVisible.value = true;
// 清空拖拽的患者信息
draggedPatient.value = null;
}
}
}
}
// 根据bedStatus获取床位标签类
function getBedTagClass(item: any) {
if (item.bedStatus == 6) {
return 'blue-tag';
} else if (item.bedStatus == 5) {
return 'green-tag';
}
return '';
}
function getPatientTagClass(item: any) {
if (item.encounterStatus == 2) {
return 'blue-tag';
} else if (item.encounterStatus == 5) {
return 'green-tag';
}
return '';
}
const addSigns = (row: any) => {
signEntryDialogVisible.value = true;
};
const selectBed = (row: any) => {
pendingInfo.value = row;
transferInDialogVisible.value = true;
};
</script>
<style lang="scss" scoped>
.bedAllocation-container {
height: 100%;
display: flex;
flex-direction: column;
.bedAllocation-main {
flex: 1 1 auto;
min-height: 0;
display: flex;
}
.right-panel {
border: 1px solid #eee;
position: relative;
display: flex;
flex-direction: column;
min-height: 0;
}
.right-filter {
flex: none;
padding: 8px;
}
.right-scrollbar {
flex: 1 1 auto;
min-height: 0;
}
.bedAllocation-search {
height: 100px;
width: 100%;
display: flex;
flex-wrap: wrap;
align-items: center;
.search-item {
width: 200px;
height: 100px;
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 12px 10px;
margin: 8px;
border: 1px solid #eee;
border-radius: 8px;
background: #fff;
box-shadow: 0 2px 6px rgba(58, 69, 86, 0.12);
position: relative;
transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease,
transform 0.2s ease;
&:hover {
border-color: rgba(24, 144, 255, 0.35);
box-shadow: 0 6px 14px rgba(58, 69, 86, 0.16);
transform: translateY(-1px);
}
&.drag-over {
background-color: #e6f7ff;
border-color: #1890ff;
box-shadow: 0 0 8px rgba(24, 144, 255, 0.6);
transform: scale(1.03);
}
&.blue-bed {
background-color: #f0f9ff; // 蓝色背景
border-color: #91d5ff;
}
&.green-bed {
background-color: #f6ffed; // 绿色背景
border-color: #b7eb8f;
}
.bed-tag {
position: absolute;
top: 12px;
right: 10px;
font-size: 12px;
line-height: 18px;
height: 18px;
padding: 0 8px;
border-radius: 999px;
font-weight: 600;
letter-spacing: 0.2px;
&.blue-tag {
background-color: #1890ff; // 蓝色标签
color: white;
}
&.green-tag {
background-color: #52c41a; // 绿色标签
color: white;
}
}
.bed-card__body {
flex: 1 1 auto;
height: 100%;
min-width: 0;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.bed-card__title {
color: #1f2d3d;
font-size: 14px;
font-weight: 600;
line-height: 20px;
white-space: normal;
overflow: visible;
word-wrap: break-word;
margin-right: 40px;
}
.bed-card__sub {
margin-top: 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-width: 0;
}
.bed-card__patient {
flex: 1 1 auto;
min-width: 0;
color: #111827;
font-size: 14px;
font-weight: 600;
line-height: 18px;
white-space: normal;
overflow: visible;
word-break: break-word;
}
.bed-card__age-tag {
flex: none;
border-radius: 999px;
padding: 0 8px;
}
.bed-card__age-tag-female {
border-color: rgb(255, 55, 158);
color: rgb(255, 126, 184);
}
.bed-card__age-tag-male {
border-color: #91d5ff;
color: #1677ff;
}
}
}
.bedAllocation-table {
flex: auto;
}
}
.patient-card {
width: 100%;
overflow: hidden;
background-color: #fff;
border: 1px solid;
border-color: #eee;
border-radius: 4px;
box-shadow: 0 2px 2px 0 rgba(57.55, 69.04, 86.28, 20%);
margin-bottom: 10px;
cursor: pointer;
transition: all 0.2s ease;
&.actived {
background-color: rgb(7, 155, 140, 5%);
border-color: var(--el-color-primary);
}
&.dragging {
opacity: 0.5;
transform: rotate(5deg);
}
.cross-dept {
height: 24px;
padding: 0 16px;
color: #fff;
font-size: 14px;
line-height: 24px;
background-color: #256d95;
}
.doctor-parent-line {
margin: 0 16px;
border-bottom: 1px dashed #ddd;
}
.personal-info-container {
display: flex;
align-items: center;
justify-content: space-between;
margin: 8px 0;
padding: 0 16px;
.name-container {
display: flex;
align-items: center;
.name {
color: #333;
font-size: 14px;
}
.age {
margin-left: 10px;
color: #666;
font-size: 14px;
}
}
.change-department {
width: 58px;
height: 24px;
color: #5585e3;
font-size: 14px;
line-height: 24px;
text-align: center;
background: #e6edfb;
border-radius: 4px;
}
}
.dept {
margin-bottom: 4px;
padding: 0 16px;
display: flex;
justify-content: space-between;
align-items: center;
.doctor {
display: flex;
align-items: center;
height: 32px;
line-height: 32px;
.doctor_name {
display: flex;
align-items: center;
margin-left: 4px;
color: #333;
}
}
.deptNurseName {
display: flex;
align-items: center;
height: 32px;
color: #256d95;
line-height: 32px;
}
}
}
// 拖拽时的全局样式
.patient-card.dragging {
opacity: 0.6;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
transform: rotate(3deg);
z-index: 100;
}
.main-info-container {
display: flex;
align-items: center;
justify-content: space-between;
height: 32px;
margin: 7px 0;
padding: 0 16px;
margin-right: 48px;
position: relative;
.patient-tag {
position: absolute;
top: -7px;
right: -48px;
font-size: 14px;
padding: 2px 8px;
border-radius: 4px;
&.blue-tag {
background-color: #1890ff; // 蓝色标签
color: white;
}
&.green-tag {
background-color: #52c41a; // 绿色标签
color: white;
}
}
.bed-container {
display: flex;
flex: 1;
align-items: center;
min-width: 0;
.bed {
flex-grow: 0;
flex-shrink: 1;
min-width: 0;
:deep(.bed-font) {
color: #333;
font-weight: 600;
font-size: 16px;
}
}
.bed_new {
flex-shrink: 0;
width: 10px;
height: 10px;
margin-left: 4px;
background: #29af6f;
border-radius: 50%;
}
}
.indepatient-code-container {
display: flex;
flex-shrink: 0;
align-items: center;
padding-left: 6px;
color: #666;
font-size: 14px;
.sign {
width: 24px;
height: 24px;
color: white;
line-height: 24px;
text-align: center;
border-radius: 50%;
user-select: none;
}
}
}
.pagination-container {
padding: 0px 20px !important;
}
.commentDisply {
display: flex;
align-items: center;
justify-content: center;
}
</style>