Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
2026-06-26 08:58:35 +08:00
13 changed files with 253 additions and 36 deletions

View File

@@ -130,5 +130,14 @@ public interface IATDManageAppService {
* @return 转科筛选选项
*/
R<?> getTransferOptions();
/**
* 换床 (指定目标床位)
*
* @param encounterId 住院患者id
* @param targetBedId 目标床位id
* @return 结果
*/
R<?> changeBedAssginment(Long encounterId, Long targetBedId);
}

View File

@@ -16,6 +16,7 @@ import com.healthlink.his.administration.domain.Encounter;
import com.healthlink.his.administration.domain.ChargeItem;
import com.healthlink.his.administration.service.IChargeItemService;
import com.healthlink.his.administration.domain.EncounterLocation;
import com.healthlink.his.administration.domain.Location;
import com.healthlink.his.administration.domain.EncounterParticipant;
import com.healthlink.his.administration.domain.Location;
import com.healthlink.his.administration.domain.Organization;
@@ -224,7 +225,7 @@ public class ATDManageAppServiceImpl implements IATDManageAppService {
public R<?> getAdmissionBedPage(AdmissionPageParam admissionPageParam, Integer pageNo, Integer pageSize) {
// 获取当前登录用户的科室 ID
Long currentUserOrgId = SecurityUtils.getLoginUser().getOrgId();
// 构建查询条件
QueryWrapper<AdmissionPageParam> queryWrapper
= HisQueryUtils.buildQueryWrapper(admissionPageParam, null, null, null);
@@ -528,7 +529,7 @@ public class ATDManageAppServiceImpl implements IATDManageAppService {
List<EncounterParticipant> savedParticipants = encounterParticipantService.getEncounterParticipantList(encounterId);
log.info("保存后查询参与者 - encounterId: {}, 数量: {}", encounterId, savedParticipants.size());
for (EncounterParticipant ep : savedParticipants) {
log.info("参与者详情 - typeCode: {}, practitionerId: {}, statusEnum: {}",
log.info("参与者详情 - typeCode: {}, practitionerId: {}, statusEnum: {}",
ep.getTypeCode(), ep.getPractitionerId(), ep.getStatusEnum());
}
// 更新入院体征(在事务外执行,避免影响参与者数据保存)
@@ -983,7 +984,7 @@ public class ATDManageAppServiceImpl implements IATDManageAppService {
= ((List<DocStatisticsDto>) docStatisticsAppService.queryByEncounterId(encounterId).getData()).stream()
.filter(item -> DocDefinitionEnum.ADMISSION_VITAL_SIGNS.getValue().equals(item.getSource())).toList();
List<DocStatisticsDto> list = new ArrayList<>(data);
// 先删除所有已有的入院体征记录(重新保存最新数据)
for (DocStatisticsDto existingItem : data) {
if (existingItem.getId() != null) {
@@ -991,7 +992,7 @@ public class ATDManageAppServiceImpl implements IATDManageAppService {
}
}
list.clear();
map.keySet().forEach(key -> {
String value = map.get(key);
// 只保存非空值
@@ -1188,5 +1189,143 @@ public class ATDManageAppServiceImpl implements IATDManageAppService {
return R.ok("退床成功");
}
/**
* 换床
*
* @param encounterId 住院患者id
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> changeBedAssginment(Long encounterId, Long targetBedId) {
if (encounterId == null) {
return R.fail("换床失败,请选择有效的就诊记录");
}
if (targetBedId == null) {
return R.fail("换床失败,请选择目标床位");
}
Encounter encounter = encounterService.getById(encounterId);
if (encounter == null) {
return R.fail("未找到就诊记录");
}
// 仅已入院状态允许换床
if (!EncounterZyStatus.ADMITTED_TO_THE_HOSPITAL.getValue().equals(encounter.getStatusEnum())) {
return R.fail("该患者未在科,无法办理换床");
}
// 查询目标床位
Location targetBed = locationService.getById(targetBedId);
if (targetBed == null) {
return R.fail("目标床位不存在");
}
if (!LocationForm.BED.getValue().equals(targetBed.getFormEnum())) {
return R.fail("所选位置不是床位");
}
// 根据目标床位的 busNo 获取其父级房间 (house)
String bedBusNo = targetBed.getBusNo();
if (bedBusNo == null || !bedBusNo.contains(".")) {
return R.fail("目标床位编码异常");
}
String[] parts = bedBusNo.split("\\.");
if (parts.length < 2) {
return R.fail("目标床位编码层级异常");
}
String houseBusNo = parts[0] + "." + parts[1];
Location targetHouse = locationService.lambdaQuery()
.eq(Location::getBusNo, houseBusNo)
.eq(Location::getFormEnum, LocationForm.HOUSE.getValue())
.eq(Location::getDeleteFlag, "0")
.one();
if (targetHouse == null) {
return R.fail("未找到目标床位所属的病房");
}
Date now = new Date();
// 检查目标床位是否已经被占用
List<EncounterLocation> occupiedBedLocs = encounterLocationService.lambdaQuery()
.eq(EncounterLocation::getLocationId, targetBedId)
.eq(EncounterLocation::getFormEnum, LocationForm.BED.getValue())
.eq(EncounterLocation::getStatusEnum, EncounterActivityStatus.ACTIVE.getValue())
.eq(EncounterLocation::getDeleteFlag, "0")
.list();
if (occupiedBedLocs != null && !occupiedBedLocs.isEmpty()) {
// Target bed is occupied! This is a bed swap (床位互换)
Long targetEncounterId = occupiedBedLocs.get(0).getEncounterId();
Encounter targetEncounter = encounterService.getById(targetEncounterId);
if (targetEncounter == null) {
return R.fail("目标床位占用患者就诊记录异常");
}
if (!EncounterZyStatus.ADMITTED_TO_THE_HOSPITAL.getValue().equals(targetEncounter.getStatusEnum())) {
return R.fail("目标床位占用患者已不在科,无法办理换床");
}
// 获取当前患者的原床位和原病房
List<EncounterLocation> currentBedLocs = encounterLocationService.getEncounterLocationList(encounterId,
LocationForm.BED, EncounterActivityStatus.ACTIVE);
if (currentBedLocs == null || currentBedLocs.isEmpty()) {
return R.fail("当前患者未分配床位,无法进行换床互换");
}
Long currentBedId = currentBedLocs.get(0).getLocationId();
List<EncounterLocation> currentHouseLocs = encounterLocationService.getEncounterLocationList(encounterId,
LocationForm.HOUSE, EncounterActivityStatus.ACTIVE);
if (currentHouseLocs == null || currentHouseLocs.isEmpty()) {
return R.fail("当前患者原病房记录不存在");
}
Long currentHouseId = currentHouseLocs.get(0).getLocationId();
// 获取被交换患者的原开始时间,保证其床位历史记录连贯性
Date targetStartTime = occupiedBedLocs.get(0).getStartTime();
if (targetStartTime == null) {
targetStartTime = now;
}
// 1. 将两位患者现有的 BED 和 HOUSE 位置状态设为 COMPLETED (false)
Integer res1 = encounterLocationService.updateEncounterLocationStatus(encounterId, false);
Integer res2 = encounterLocationService.updateEncounterLocationStatus(targetEncounterId, false);
if (res1 == 0 || res2 == 0) {
throw new RuntimeException("更新原就诊位置状态失败");
}
// 2. 为当前患者创建新位置 (目标病房和目标床位)
encounterLocationService.creatEncounterLocation(encounterId, now, targetHouse.getId(), LocationForm.HOUSE.getValue());
encounterLocationService.creatEncounterLocation(encounterId, now, targetBedId, LocationForm.BED.getValue());
// 3. 为被交换患者创建新位置 (当前患者的原病房和原床位)
encounterLocationService.creatEncounterLocation(targetEncounterId, targetStartTime, currentHouseId, LocationForm.HOUSE.getValue());
encounterLocationService.creatEncounterLocation(targetEncounterId, targetStartTime, currentBedId, LocationForm.BED.getValue());
return R.ok("床位互换成功");
} else {
// Target bed is vacant! Normal bed change
// 获取当前患者原床位
List<EncounterLocation> currentBedLocs = encounterLocationService.getEncounterLocationList(encounterId,
LocationForm.BED, EncounterActivityStatus.ACTIVE);
// 1. 将当前患者现有的 BED 和 HOUSE 位置状态设为 COMPLETED (false)
encounterLocationService.updateEncounterLocationStatus(encounterId, false);
// 2. 将原床位状态更新为空闲 (LocationStatus.IDLE)
if (currentBedLocs != null && !currentBedLocs.isEmpty()) {
for (EncounterLocation bedLoc : currentBedLocs) {
locationService.updateStatusById(bedLoc.getLocationId(), LocationStatus.IDLE.getValue());
}
}
// 3. 为当前患者创建新位置 (目标病房和目标床位)
encounterLocationService.creatEncounterLocation(encounterId, now, targetHouse.getId(), LocationForm.HOUSE.getValue());
encounterLocationService.creatEncounterLocation(encounterId, now, targetBedId, LocationForm.BED.getValue());
// 4. 将目标床位状态更新为占用 (LocationStatus.OCCUPY)
locationService.updateStatusById(targetBedId, LocationStatus.OCCUPY.getValue());
return R.ok("换床成功");
}
}
}

View File

@@ -14,6 +14,8 @@ import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* 入出转管理 controller
@@ -187,5 +189,19 @@ public class ATDManageController {
public R<?> getTransferOptions() {
return atdManageAppService.getTransferOptions();
}
/**
* 换床
*
* @param encounterId 住院患者id
* @return 结果
*/
@PutMapping(value = "/change-bed-assignment")
public R<?> changeBedAssignment(Long encounterId){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String targetBedIdStr = request.getParameter("targetBedId");
Long targetBedId = (targetBedIdStr == null || targetBedIdStr.trim().isEmpty()) ? null : Long.valueOf(targetBedIdStr);
return atdManageAppService.changeBedAssginment(encounterId, targetBedId);
}
}

View File

@@ -113,7 +113,7 @@
AND ao_target.delete_flag = '0'
WHERE ae.delete_flag = '0'
AND ae.class_enum = #{imp}
AND ae.status_enum != #{toBeRegistered}
AND ae.status_enum IN (2, 3, 5, 6)
AND ae.organization_id = #{currentUserOrgId}
GROUP BY ae.tenant_id,
ae.id,

View File

@@ -124,11 +124,14 @@ public class EncounterLocationServiceImpl extends ServiceImpl<EncounterLocationM
if (isTransfer) {
locationForms.add(LocationForm.WARD.getValue());
}
// 更新状态为已完成
// 更新状态为已完成 — 仅针对当前 ACTIVE 且未删除的记录
return baseMapper.update(null,
new LambdaUpdateWrapper<EncounterLocation>()
.set(EncounterLocation::getStatusEnum, EncounterActivityStatus.COMPLETED.getValue())
.eq(EncounterLocation::getEncounterId, encounterId).in(EncounterLocation::getFormEnum, locationForms));
.eq(EncounterLocation::getEncounterId, encounterId)
.in(EncounterLocation::getFormEnum, locationForms)
.eq(EncounterLocation::getStatusEnum, EncounterActivityStatus.ACTIVE.getValue())
.eq(EncounterLocation::getDeleteFlag, DelFlag.NO.getCode()));
}
/*

View File

@@ -65,7 +65,7 @@
"vue": "^3.5.25",
"vue-area-linkage": "^5.1.0",
"vue-cropper": "^1.1.1",
"vue-i18n": "^11.4.6",
"vue-i18n": "^9.14.5",
"vue-plugin-hiprint": "^0.0.60",
"vue-router": "^4.6.4",
"vxe-pc-ui": "^4.14.26",

View File

@@ -271,7 +271,8 @@
>
<el-input
v-model="form.medicalrecordNumber"
readonly
size="small"
placeholder="请输入就诊卡号"
/>
</el-form-item>
</el-col>
@@ -1759,12 +1760,14 @@ watch(selectedMethods, () => {
}, { deep: true });
// 监听患者变化
watch(() => props.patientInfo, (newVal) => {
if (newVal?.encounterId) {
initPatientForm(newVal);
// 🔧 Bug #767 同类修复:移除 deep: true避免深层属性变化时重置用户输入
// 父组件通过整体替换 patientInfo 对象来切换患者watch 引用变化即可,无需 deep
watch(() => props.patientInfo?.encounterId, (newEncounterId) => {
if (newEncounterId && props.patientInfo?.encounterId) {
initPatientForm(props.patientInfo);
getList();
}
}, { immediate: true, deep: true });
}, { immediate: true });
watch(() => props.activeTab, async (val) => {
if (val === 'examination') {

View File

@@ -2395,9 +2395,9 @@ const handleCurrentChange = (page) => {
getInspectionList()
}
// 选择框变化
const handleSelectionChange = (selection) => {
selectedRows.value = selection
// 选择框变化vxe-table v4 事件参数为 { records, reserves, row, rowIndex, checked, $event }
const handleSelectionChange = ({ records }) => {
selectedRows.value = records || []
}
const handlePrint = (row) => {
@@ -2456,11 +2456,11 @@ const handleDelete = (row) => {
})
}
// 单元格点击 - 点击表格行时加载申请单详情
const handleCellClick = (row, column) => {
// 如果点击的是操作列展开列,不触发数据填充
// 单元格点击 - 点击表格行时加载申请单详情vxe-table v4 事件参数为 { row, column, ... }
const handleCellClick = ({ row, column }) => {
// 如果点击的是操作列展开列或选择列,不触发数据填充
if (column.property === '操作' || column.label === '操作' ||
column.type === 'expand' || column.type === 'selection') {
column.type === 'expand' || column.type === 'selection' || column.type === 'checkbox') {
return;
}
// 点击表格行时,将该申请单的数据加载到表单中
@@ -2470,8 +2470,8 @@ const handleCellClick = (row, column) => {
}
}
// 行点击事件处理
const handleRowClick = (currentRow, oldRow) => {
// 行点击事件处理vxe-table v4 事件参数为 { row, rowIndex, $event }
const handleRowClick = ({ row: currentRow }) => {
// 点击表格行时,将该申请单的数据加载到表单中
// 使用 applyNo 判断是否有效,同时检查是否处于删除状态
if (currentRow && currentRow.applyNo && !isDeleting.value) {
@@ -2618,24 +2618,21 @@ watch(() => props.activeTab, async (newVal) => {
})
// 监听patientInfo变化,确保encounterId及时更新并重新加载数据
watch(() => props.patientInfo, async (newVal) => {
if (newVal && newVal.encounterId) {
const oldEncounterId = queryParams.encounterId
queryParams.encounterId = newVal.encounterId
// 🔧 Bug #767 修复:移除 deep: true避免深层属性变化时重置就诊卡号等用户输入
// 父组件通过整体替换 patientInfo 对象来切换患者watch 引用变化即可,无需 deep
watch(() => props.patientInfo?.encounterId, async (newEncounterId, oldEncounterId) => {
if (newEncounterId) {
queryParams.encounterId = newEncounterId
// 初始化数据
await initData();
// 如果encounterId发生变化重新加载检验申请单列表
if (oldEncounterId !== newVal.encounterId) {
if (oldEncounterId !== newEncounterId) {
getInspectionList()
}
// 更新科室编码
// const currentDeptCode = await getCurrentDeptCode();
// formData.applyDeptCode = currentDeptCode || '';
}
}, { deep: true, immediate: true })
}, { immediate: true })
// Bug #329: 监听已选择的检验项目,自动更新检验项目文本并设置默认执行科室
watch(() => selectedInspectionItems.value, async (newVal) => {

View File

@@ -104,7 +104,7 @@
:type="row.disposition==='ADMIT'?'warning':row.disposition==='DEATH'?'danger':'success'"
size="small"
>
{{ {$t('emergency.observation.dispAdmit'):t('emergency.observation.dispAdmit'),$t('emergency.observation.dispDischarge'):t('emergency.observation.dispDischarge'),$t('emergency.observation.dispTransfer'):t('emergency.observation.dispTransfer'),$t('emergency.observation.dispDeath'):t('emergency.observation.dispDeath')}[row.disposition]||row.disposition }}
{{ { ADMIT: $t('emergency.observation.dispAdmit'), DISCHARGE: $t('emergency.observation.dispDischarge'), TRANSFER: $t('emergency.observation.dispTransfer'), DEATH: $t('emergency.observation.dispDeath') }[row.disposition] || row.disposition }}
</el-tag>
<el-tag
v-else

View File

@@ -178,7 +178,7 @@
:type="row.status==='WAITING'?'danger':row.status==='IN_TREATMENT'?'warning':'success'"
size="small"
>
{{ {$t('emergency.triage.statusWaiting'):'待诊',$t('emergency.triage.statusInTreatment'):'就诊中',$t('emergency.triage.statusCompleted'):'已完成'}[row.status]||row.status }}
{{ { WAITING: $t('emergency.triage.statusWaiting'), IN_TREATMENT: $t('emergency.triage.statusInTreatment'), COMPLETED: $t('emergency.triage.statusCompleted') }[row.status] || row.status }}
</el-tag>
</template>
</el-table-column>

View File

@@ -167,6 +167,18 @@ export function cancelBedAssignment(encounterId) {
});
}
// 换床/对换
export function changeBedAssignment(encounterId, targetBedId) {
return request({
url: '/nurse-station/atd-manage/change-bed-assignment',
method: 'put',
params: {
encounterId: encounterId,
targetBedId: targetBedId,
},
});
}
/**
* 获取病区列表(与病区管理页面相同的接口)

View File

@@ -41,6 +41,14 @@
</el-select>
</template>
<template #extra-buttons>
<el-button
type="primary"
plain
style="margin-right: 8px;"
@click="handleChangeBed"
>
换床
</el-button>
<el-button
type="danger"
plain
@@ -112,6 +120,11 @@
@ok-act="handleTransferInOk"
/>
<SignEntryDialog v-model:visible="signEntryDialogVisible" />
<ChangeBedDialog
v-model:visible="changeBedDialogVisible"
:bad-list="badList"
@ok-act="handleTransferInOk"
/>
</div>
</template>
<script setup lang="ts">
@@ -119,6 +132,7 @@ 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 ChangeBedDialog from './changeBedDialog.vue';
import {childLocationList, getBedInfo, getInit, getPendingInfo, getPractitionerWard, cancelBedAssignment} from './api';
import {ElLoading, ElMessage, ElMessageBox} from 'element-plus';
import PendingPatientList from '@/components/PendingPatientList/index.vue';
@@ -143,6 +157,7 @@ interface InitInfoOptions {
const transferInDialogVisible = ref(false);
const signEntryDialogVisible = ref(false);
const changeBedDialogVisible = ref(false);
const state = reactive({});
const loading = ref(false);
const total = ref();
@@ -426,6 +441,11 @@ function handleCardDblClick(item: any) {
}
}
// 换床操作
function handleChangeBed() {
changeBedDialogVisible.value = true;
}
// 退床操作 (取消分床)
const handleCancelBedAssignment = async () => {
const activePatient = patientList.value?.find?.((it) => it?.active);

View File

@@ -1,4 +1,4 @@
<template>
<template>
<div class="app-container">
<el-form
ref="queryRef"
@@ -129,7 +129,7 @@
<vxe-table
v-loading="loading"
:data="dataList"
height="calc(100vh - 250px)"
height="auto"
@checkbox-change="handleSelectionChange"
>
<vxe-column
@@ -388,3 +388,21 @@ onMounted(() => {
getList();
});
</script>
<style lang="scss" scoped>
.app-container {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
padding: 20px;
}
.vxe-table {
flex: 1;
min-height: 0;
width: 100%;
}
</style>