Compare commits

...

6 Commits

Author SHA1 Message Date
赵云
4c083cc698 Fix Bug #464: [目录管理-诊疗目录] 新增项目时"零售价"未与"诊疗子项"合计总价自动同步
根因:calculateTotalPrice中form.value.retailPrice赋值被nextTick包裹,
在多调用方(watcher/selectRow/addItem)并发时产生竞态,导致零售价更新丢失
修复:移除nextTick,改为同步赋值确保零售价实时同步总价

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 13:03:25 +08:00
Ranyunqiao
6367654ada 476 住院医生工作-检查申请单界面缺失核心临床字段(紧急程度、过敏史、检查目的等) 2026-05-14 12:56:04 +08:00
关羽
360256e589 Fix Bug #465: [住院医生工作站-检验申请] 检验项目选择列表被限制为500项,导致医生无法检索并开立其余800多项
问题根因:
- 前端使用 pageSize=500 分页拉取数据,el-transfer 组件客户端过滤在 1400+ 条数据下存在渲染和搜索性能问题
- 数据库实际有 1400 项已启用的检验类诊疗项目,但仅加载了 500 项

修复方案:
1. 改用 pageSize=9999 一次性拉取全部数据,消除分页导致的 500 项截断
2. 新增顶部搜索框,支持按项目名称/拼音首拼/业务编号实时过滤
3. 使用 computed 属性动态生成 transfer 组件数据,搜索时自动过滤
4. 显示总数统计(未搜索时显示总数,搜索时显示匹配数/总数)
5. 移除不再需要的 applicationList 变量引用和 onBeforeMount 空调用

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 12:23:13 +08:00
荀彧
feb033b857 Fix Bug #462: [目录管理-诊疗目录] 编辑弹窗中"所需标本"下拉框数据加载失败,显示为"无数据" Fix: selectDictDataByType方法移除Redis缓存读取逻辑,直接查询数据库避免缓存为空数据导致前端下拉框无数据 2026-05-14 12:15:47 +08:00
wangjian963
79cce458ee Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	openhis-server-new/openhis-application/src/main/java/com/openhis/web/clinicalmanage/dto/OpScheduleDto.java
2026-05-14 12:01:33 +08:00
wangjian963
1140912f3a Fix Bug #437: 【门诊手术计费】保存签章TOCTOU竞态致重复提交,且耗材计费项目缺失/重复、手术单号未关联
Fix: 频次总量计算改用字典store动态读取,el-input-number新增@input实时计算
2026-05-14 12:00:18 +08:00
10 changed files with 425 additions and 462 deletions

View File

@@ -85,18 +85,13 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService {
String trimmedKey = searchKey.trim(); String trimmedKey = searchKey.trim();
return dictDataMapper.selectDictDataByTypeWithSearch(dictType, trimmedKey); return dictDataMapper.selectDictDataByTypeWithSearch(dictType, trimmedKey);
} }
// 否则使用原有方法(带缓存) // 直接查询数据库,避免缓存中为空数据导致前端下拉框显示"无数据"
List<SysDictData> dictDatas = DictUtils.getDictCache(dictType); List<SysDictData> dictDatas = dictDataMapper.selectDictDataByType(dictType);
if (StringUtils.isNotEmpty(dictDatas)) {
return dictDatas;
}
dictDatas = dictDataMapper.selectDictDataByType(dictType);
if (StringUtils.isNotEmpty(dictDatas)) { if (StringUtils.isNotEmpty(dictDatas)) {
DictUtils.setDictCache(dictType, dictDatas); DictUtils.setDictCache(dictType, dictDatas);
return dictDatas;
} }
return null; return dictDatas;
} }
/** /**

View File

@@ -17,7 +17,6 @@ import java.time.LocalDate;
* @date 2026-01-28 * @date 2026-01-28
*/ */
@Data @Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class OpScheduleDto extends OpSchedule { public class OpScheduleDto extends OpSchedule {
@@ -114,4 +113,5 @@ public class OpScheduleDto extends OpSchedule {
* 创建人名称 * 创建人名称
*/ */
private String createByName; private String createByName;
} }

View File

@@ -1543,6 +1543,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
deviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), 4)); deviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), 4));
} }
deviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源 deviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
deviceRequest.setPrescriptionNo(adviceSaveDto.getSourceBillNo()); // 来源业务单据号(手术单号)
deviceRequest.setQuantity(adviceSaveDto.getQuantity()); // 请求数量 deviceRequest.setQuantity(adviceSaveDto.getQuantity()); // 请求数量
deviceRequest.setUnitCode(adviceSaveDto.getUnitCode()); // 请求单位编码 deviceRequest.setUnitCode(adviceSaveDto.getUnitCode()); // 请求单位编码
deviceRequest.setLotNumber(adviceSaveDto.getLotNumber());// 产品批号 deviceRequest.setLotNumber(adviceSaveDto.getLotNumber());// 产品批号
@@ -1906,6 +1907,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
serviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4)); serviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4));
} }
serviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源 serviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
serviceRequest.setPrescriptionNo(adviceSaveDto.getSourceBillNo()); // 来源业务单据号(手术单号)
serviceRequest.setQuantity(adviceSaveDto.getQuantity()); // 请求数量 serviceRequest.setQuantity(adviceSaveDto.getQuantity()); // 请求数量
serviceRequest.setUnitCode(adviceSaveDto.getUnitCode()); // 请求单位编码 serviceRequest.setUnitCode(adviceSaveDto.getUnitCode()); // 请求单位编码
@@ -2101,10 +2103,10 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
CommonConstants.TableName.MED_MEDICATION_REQUEST, CommonConstants.TableName.WOR_DEVICE_REQUEST, CommonConstants.TableName.MED_MEDICATION_REQUEST, CommonConstants.TableName.WOR_DEVICE_REQUEST,
CommonConstants.TableName.WOR_SERVICE_REQUEST, practitionerId, Whether.NO.getCode(), CommonConstants.TableName.WOR_SERVICE_REQUEST, practitionerId, Whether.NO.getCode(),
sourceEnum, sourceBillNo); sourceEnum, sourceBillNo);
// 手术计费场景sourceBillNo 不为空时,只保留诊疗请求3/6过滤掉药品1耗材2 // 手术计费场景sourceBillNo 不为空时过滤掉药品1,保留耗材2和诊疗3/6
if (sourceBillNo != null && !sourceBillNo.isEmpty()) { if (sourceBillNo != null && !sourceBillNo.isEmpty()) {
requestBaseInfo.removeIf(dto -> dto.getAdviceType() != null requestBaseInfo.removeIf(dto -> dto.getAdviceType() != null
&& (dto.getAdviceType() == 1 || dto.getAdviceType() == 2)); && dto.getAdviceType() == 1);
} }
for (RequestBaseDto requestBaseDto : requestBaseInfo) { for (RequestBaseDto requestBaseDto : requestBaseInfo) {
// 请求状态 // 请求状态

View File

@@ -539,7 +539,8 @@
AND T1.refund_medicine_id IS NULL AND T1.refund_medicine_id IS NULL
ORDER BY T1.status_enum,T1.sort_number) ORDER BY T1.status_enum,T1.sort_number)
UNION ALL UNION ALL
-- 🔧 新增:查询门诊术中计费生成的耗材数据(这些数据存在于 adm_charge_item 和 wor_device_request -- 🔧 查询仅存在于 adm_charge_item 的"孤儿"耗材数据DeviceRequest 缺失或 generate_source_enum 未设置
-- 正常 DeviceRequestgenerate_source_enum 已赋值)由下方 Part 3 统一负责,此处不做重复覆盖避免 UNION ALL 重复行
(SELECT 2 AS advice_type, (SELECT 2 AS advice_type,
CI.service_id AS request_id, CI.service_id AS request_id,
CI.service_id || '-ci-dev' AS unique_key, CI.service_id || '-ci-dev' AS unique_key,
@@ -584,7 +585,7 @@
WHERE CI.delete_flag = '0' WHERE CI.delete_flag = '0'
AND CI.service_table = 'wor_device_request' AND CI.service_table = 'wor_device_request'
<if test="generateSourceEnum != null"> <if test="generateSourceEnum != null">
AND (DR.generate_source_enum IS NULL OR DR.generate_source_enum = #{generateSourceEnum}) AND DR.generate_source_enum IS NULL <!-- 仅匹配孤儿记录normal DeviceRequest 由 Part 3 负责,避免 UNION ALL 重复 -->
</if> </if>
<if test="historyFlag == '0'.toString()"> <if test="historyFlag == '0'.toString()">
AND CI.encounter_id = #{encounterId} AND CI.encounter_id = #{encounterId}

View File

@@ -1,3 +1,5 @@
import useDictStore from '@/store/modules/dict';
// 日期格式化 // 日期格式化
export function parseTime(time, pattern) { export function parseTime(time, pattern) {
if (arguments.length === 0 || !time) { if (arguments.length === 0 || !time) {
@@ -275,30 +277,13 @@ export function blobValidate(data) {
// 按照频次天数计算总数量 // 按照频次天数计算总数量
export function calculateQuantityByDays(frequency, days) { export function calculateQuantityByDays(frequency, days) {
// const dict = useDict('rate_code').rate_code.value const dicts = useDictStore().getDict('rate_code');
// const rate = dict.find(item => item.value === frequency).remark if (!dicts) return;
// if(rate){ const dict = dicts.find(item => item.value === frequency);
// return Math.floor(Number(rate) * days) if (!dict?.remark) return;
// } else { const rate = Number(dict.remark);
// return undefined if (isNaN(rate) || !rate) return;
// } const quantity = rate * days;
const frequencyMap = {
ST: 1,
QD: 1, // 每日一次
BID: 2, // 每日两次
TID: 3, // 每日三次
QID: 4, // 每日四次
QN: 1, // 每晚一次
QOD: 1 / 2, // 每隔一日一次
QW: 1 / 7, // 每周一次
BIW: 2 / 7, // 每周两次
TIW: 3 / 7, // 每周三次
QOW: 1 / 14, // 隔周一次
};
if (!frequencyMap[frequency]) {
return;
}
const quantity = frequencyMap[frequency] * days;
return quantity < 1 ? 1 : Math.ceil(quantity); return quantity < 1 ? 1 : Math.ceil(quantity);
} }

View File

@@ -473,15 +473,12 @@ function calculateTotalPrice() {
} }
}); });
totalPrice.value = sum.toFixed(2); totalPrice.value = sum.toFixed(2);
// Bug #464: 零售价与诊疗子项合计总价实时同步 // Bug #464: 零售价与诊疗子项合计总价实时同步直接赋值不使用nextTick避免多调用方竞争
const hasValidItem = treatmentItems.value.some( const hasValidItem = treatmentItems.value.some(
(item) => item.adviceDefinitionId && item.adviceDefinitionId !== '' (item) => item.adviceDefinitionId && item.adviceDefinitionId !== ''
); );
if (hasValidItem) { if (hasValidItem) {
// 使用 nextTick 确保总价更新后零售价才更新,避免 Vue 响应式时序问题 form.value.retailPrice = parseFloat(totalPrice.value) || 0;
nextTick(() => {
form.value.retailPrice = parseFloat(totalPrice.value) || 0;
});
} else { } else {
form.value.retailPrice = undefined; form.value.retailPrice = undefined;
} }
@@ -763,10 +760,7 @@ function selectRow(row, index) {
treatmentItems.value[index].adviceDefinitionId = row.id; treatmentItems.value[index].adviceDefinitionId = row.id;
treatmentItems.value[index].retailPrice = row.retailPrice || 0; treatmentItems.value[index].retailPrice = row.retailPrice || 0;
medicineSearchKey.value = ''; medicineSearchKey.value = '';
// 使用 nextTick 确保 DOM 更新后再计算总价 calculateTotalPrice();
nextTick(() => {
calculateTotalPrice();
});
} }
// 清空诊疗子项 // 清空诊疗子项

View File

@@ -461,7 +461,7 @@ watch(
console.log(prescriptionList.value,"prescriptionList.value") console.log(prescriptionList.value,"prescriptionList.value")
if(newValue&&newValue.length>0){ if(newValue&&newValue.length>0){
let saveList = prescriptionList.value.filter((item) => { let saveList = prescriptionList.value.filter((item) => {
return item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag) return item.check && item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag)
}) })
console.log(saveList,"prescriptionList.value") console.log(saveList,"prescriptionList.value")
if (saveList.length == 0) { if (saveList.length == 0) {
@@ -1015,7 +1015,7 @@ function handleSave() {
return; return;
} }
let saveList = prescriptionList.value.filter((item) => { let saveList = prescriptionList.value.filter((item) => {
return item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag) return item.check && item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag)
}); });
// let saveList = prescriptionList.value // let saveList = prescriptionList.value
// .filter((item) => { // .filter((item) => {
@@ -1080,42 +1080,44 @@ function handleSaveSign(row, index) {
proxy.$modal.msgWarning('诊疗项目必须选择执行科室'); proxy.$modal.msgWarning('诊疗项目必须选择执行科室');
return; return;
} }
isSaving.value = true; // #437 立即加锁,消除 TOCTOU 竞态
proxy.$refs['formRef' + index].validate((valid) => { proxy.$refs['formRef' + index].validate((valid) => {
if (valid) { if (!valid) {
isSaving.value = true; // #437 加 isSaving.value = false; // 验证失败释放
row.isEdit = false; return;
isAdding.value = false;
expandOrder.value = [];
row.patientId = props.patientInfo.patientId;
row.encounterId = props.patientInfo.encounterId;
row.accountId = props.patientInfo.accountId;
const cleanRow = JSON.parse(JSON.stringify(row));
cleanRow.contentJson = JSON.stringify(cleanRow);
cleanRow.dbOpType = cleanRow.requestId ? '2' : '1';
cleanRow.minUnitQuantity = cleanRow.quantity * cleanRow.partPercent;
cleanRow.categoryEnum = cleanRow.adviceType
// 如果是手术计费,设置生成来源和来源业务单据号
if (props.patientInfo.sourceBillNo) {
cleanRow.generateSourceEnum = 6; // 手术计费
cleanRow.sourceBillNo = props.patientInfo.sourceBillNo;
}
console.log('cleanRow', cleanRow)
savePrescription({ adviceSaveList: [cleanRow] }).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功');
getListInfo(false);
nextId.value = 1;
// 🔧 Bug Fix #238: 如果诊疗项目缺少执行科室,标记为需要修复的脏数据
if (row.adviceType === 3 && !row.orgId) {
console.warn('Bug #238: 检测到诊疗项目保存时缺少执行科室,请手动编辑修正:', cleanRow);
proxy.$modal.msgWarning('诊疗项目执行科室信息不完整,请编辑后重新保存');
}
}
}).finally(() => {
isSaving.value = false; // #437 释放锁
});
} }
}); row.isEdit = false;
isAdding.value = false;
expandOrder.value = [];
row.patientId = props.patientInfo.patientId;
row.encounterId = props.patientInfo.encounterId;
row.accountId = props.patientInfo.accountId;
const cleanRow = JSON.parse(JSON.stringify(row));
cleanRow.contentJson = JSON.stringify(cleanRow);
cleanRow.dbOpType = cleanRow.requestId ? '2' : '1';
cleanRow.minUnitQuantity = cleanRow.quantity * cleanRow.partPercent;
cleanRow.categoryEnum = cleanRow.adviceType
// 如果是手术计费,设置生成来源和来源业务单据号
if (props.patientInfo.sourceBillNo) {
cleanRow.generateSourceEnum = 6; // 手术计费
cleanRow.sourceBillNo = props.patientInfo.sourceBillNo;
}
console.log('cleanRow', cleanRow)
savePrescription({ adviceSaveList: [cleanRow] }, '1').then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功');
getListInfo(false);
nextId.value = 1;
// 🔧 Bug Fix #238: 如果诊疗项目缺少执行科室,标记为需要修复的脏数据
if (row.adviceType === 3 && !row.orgId) {
console.warn('Bug #238: 检测到诊疗项目保存时缺少执行科室,请手动编辑修正:', cleanRow);
proxy.$modal.msgWarning('诊疗项目执行科室信息不完整,请编辑后重新保存');
}
}
}).finally(() => {
isSaving.value = false; // #437 释放锁
});
})
} }
// 签退 // 签退

View File

@@ -315,6 +315,7 @@
data-prop="dispensePerDuration"> data-prop="dispensePerDuration">
<el-input-number v-model="scope.row.dispensePerDuration" style="width: 80px" :min="1" <el-input-number v-model="scope.row.dispensePerDuration" style="width: 80px" :min="1"
controls-position="right" :controls="false" :ref="(el) => (inputRefs.dispensePerDuration = el)" controls-position="right" :controls="false" :ref="(el) => (inputRefs.dispensePerDuration = el)"
@input="calculateTotalAmount(scope.row, scope.$index)"
@change="calculateTotalAmount(scope.row, scope.$index)" @change="calculateTotalAmount(scope.row, scope.$index)"
@keyup.enter.prevent=" @keyup.enter.prevent="
handleEnter('dispensePerDuration', scope.row, scope.$index) handleEnter('dispensePerDuration', scope.row, scope.$index)

View File

@@ -6,9 +6,26 @@
<template> <template>
<div class="LaboratoryTests-container"> <div class="LaboratoryTests-container">
<div v-loading="loading" class="transfer-wrapper"> <div v-loading="loading" class="transfer-wrapper">
<!-- 远程搜索框 -->
<div class="search-bar">
<el-input
v-model="searchKey"
placeholder="输入项目代码/名称搜索"
clearable
@keyup.enter="handleSearch"
@clear="handleSearch"
style="width: 300px; margin-bottom: 10px"
>
<template #append>
<el-button @click="handleSearch">搜索</el-button>
</template>
</el-input>
<span v-if="!searchKey" class="total-count"> {{ totalCount }} </span>
<span v-else class="total-count">搜索到 {{ filteredCount }} / {{ totalCount }} </span>
</div>
<el-transfer <el-transfer
v-model="transferValue" v-model="transferValue"
:data="applicationList" :data="transferData"
filter-placeholder="项目代码/名称" filter-placeholder="项目代码/名称"
filterable filterable
:titles="['未选择', '已选择']" :titles="['未选择', '已选择']"
@@ -117,7 +134,7 @@
</div> </div>
</template> </template>
<script setup name="LaboratoryTests"> <script setup name="LaboratoryTests">
import {getCurrentInstance, onBeforeMount, onMounted, reactive, watch} from 'vue'; import {getCurrentInstance, onMounted, reactive, ref, watch, computed} from 'vue';
import {patientInfo} from '../../../store/patient.js'; import {patientInfo} from '../../../store/patient.js';
import {getApplicationList, saveInspection} from './api'; import {getApplicationList, saveInspection} from './api';
import {getOrgList} from '@/views/doctorstation/components/api.js'; import {getOrgList} from '@/views/doctorstation/components/api.js';
@@ -140,61 +157,85 @@ const findTreeItem = (list, id) => {
const emits = defineEmits(['submitOk']); const emits = defineEmits(['submitOk']);
const props = defineProps({}); const props = defineProps({});
const state = reactive({}); const state = reactive({});
const applicationListAll = ref(); const applicationListAll = ref([]);
const applicationList = ref();
const loading = ref(false); const loading = ref(false);
const orgOptions = ref([]); // 科室选项 const orgOptions = ref([]);
const getList = async () => { const searchKey = ref('');
const totalCount = ref(0);
// 将已加载的全部数据转为 transfer 组件所需的格式
const buildTransferData = (records) => {
return records.map((item) => {
const priceInfo = item.priceList?.[0] || {};
const price = priceInfo.price != null ? Number(priceInfo.price).toFixed(2) : '0.00';
const unit = item.unitCode_dictText || item.unitCode || '';
return {
adviceDefinitionId: item.adviceDefinitionId,
orgId: item.orgId,
label: item.adviceName + ' (¥' + price + '/' + unit + ')',
key: item.adviceDefinitionId,
};
});
};
// 加载全部数据(不分页,一次性拉取)
const loadAllData = async () => {
if (!patientInfo.value?.inHospitalOrgId) { if (!patientInfo.value?.inHospitalOrgId) {
applicationList.value = []; applicationListAll.value = [];
return; return;
} }
loading.value = true; loading.value = true;
try { try {
const allRecords = []; // 使用大 pageSize 一次性拉取所有启用状态的检验类诊疗项目
let currentPage = 1; const res = await getApplicationList({
const pageSize = 500; pageSize: 9999,
pageNo: 1,
// 分页拉取全部数据后端单页最多500条 categoryCode: '22',
while (true) { organizationId: patientInfo.value.inHospitalOrgId,
const res = await getApplicationList({ adviceTypes: [3], // 1 药品 2 耗材 3 诊疗
pageSize,
pageNo: currentPage,
categoryCode: '22',
organizationId: patientInfo.value.inHospitalOrgId,
adviceTypes: [3], // 1 药品 2 耗材 3 诊疗
});
if (res.code !== 200) {
proxy.$message.error(res.message);
applicationList.value = [];
return;
}
const records = res.data?.records || [];
allRecords.push(...records);
// 当前页不足 pageSize 或已无数据,说明已全部拉取
if (records.length < pageSize) break;
currentPage++;
}
applicationListAll.value = allRecords;
applicationList.value = allRecords.map((item) => {
const priceInfo = item.priceList?.[0] || {};
const price = priceInfo.price != null ? Number(priceInfo.price).toFixed(2) : '0.00';
const unit = item.unitCode_dictText || item.unitCode || '';
return {
adviceDefinitionId: item.adviceDefinitionId,
orgId: item.orgId,
label: item.adviceName + ' (¥' + price + '/' + unit + ')',
key: item.adviceDefinitionId,
};
}); });
if (res.code !== 200) {
proxy.$message.error(res.message);
applicationListAll.value = [];
return;
}
applicationListAll.value = res.data?.records || [];
totalCount.value = res.data?.total || 0;
} catch (e) { } catch (e) {
proxy.$message.error('获取检验项目列表失败'); proxy.$message.error('获取检验项目列表失败');
applicationList.value = []; applicationListAll.value = [];
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; };
// 根据搜索关键词过滤数据
const filterData = (key) => {
if (!key || key.trim() === '') {
return applicationListAll.value;
}
const lowerKey = key.toLowerCase().trim();
return applicationListAll.value.filter((item) => {
return (
item.adviceName?.toLowerCase().includes(lowerKey) ||
item.pyStr?.toLowerCase().includes(lowerKey) ||
item.adviceBusNo?.toLowerCase().includes(lowerKey)
);
});
};
// transfer 组件实际显示的数据(受搜索词影响)
const transferData = computed(() => buildTransferData(filterData(searchKey.value)));
// 当前显示的条数
const filteredCount = computed(() => filterData(searchKey.value).length);
const getList = async () => {
await loadAllData();
};
const handleSearch = () => {
// 搜索时保持已选中的项目不受影响
};
const transferValue = ref([]); const transferValue = ref([]);
const form = reactive({ const form = reactive({
// categoryType: '', // 项目类别 // categoryType: '', // 项目类别
@@ -212,7 +253,6 @@ const form = reactive({
otherDiagnosisList: [], //其他断目录 otherDiagnosisList: [], //其他断目录
}); });
const rules = reactive({}); const rules = reactive({});
onBeforeMount(() => {});
onMounted(() => { onMounted(() => {
getList(); getList();
}); });
@@ -225,12 +265,22 @@ const projectWithDepartment = (selectProjectIds, type) => {
let isRelease = true; let isRelease = true;
// 选中项目的数组 // 选中项目的数组
const arr = []; const arr = [];
// 根据选中的项目id查找对应的项目 // 根据选中的项目id查找对应的项目(从全部原始数据中查找)
selectProjectIds.forEach((element) => { selectProjectIds.forEach((element) => {
const searchData = applicationList.value.find((item) => { const searchData = applicationListAll.value.find((item) => {
return element == item.adviceDefinitionId; return element == item.adviceDefinitionId;
}); });
arr.push(searchData); if (searchData) {
const priceInfo = searchData.priceList?.[0] || {};
const price = priceInfo.price != null ? Number(priceInfo.price).toFixed(2) : '0.00';
const unit = searchData.unitCode_dictText || searchData.unitCode || '';
arr.push({
adviceDefinitionId: searchData.adviceDefinitionId,
orgId: searchData.orgId,
label: searchData.adviceName + ' (¥' + price + '/' + unit + ')',
key: searchData.adviceDefinitionId,
});
}
}); });
// 保存用户手动选择的发往科室(提交时需要保留) // 保存用户手动选择的发往科室(提交时需要保留)
const manualDept = type === 2 ? form.targetDepartment : ''; const manualDept = type === 2 ? form.targetDepartment : '';
@@ -321,7 +371,7 @@ const submit = () => {
saveInspection(params).then((res) => { saveInspection(params).then((res) => {
if (res.code === 200) { if (res.code === 200) {
proxy.$message.success(res.msg); proxy.$message.success(res.msg);
applicationList.value = []; transferValue.value = [];
emits('submitOk'); emits('submitOk');
} else { } else {
proxy.$message.error(res.message); proxy.$message.error(res.message);
@@ -378,6 +428,19 @@ defineExpose({ state, submit, getLocationInfo, getDiagnosisList, getList });
.transfer-wrapper { .transfer-wrapper {
position: relative; position: relative;
min-height: 300px; min-height: 300px;
.search-bar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
.total-count {
font-size: 13px;
color: #909399;
white-space: nowrap;
}
}
} }
.el-transfer { .el-transfer {

View File

@@ -5,36 +5,25 @@
--> -->
<template> <template>
<div class="medicalExaminations-container"> <div class="medicalExaminations-container">
<!-- 顶部标题栏 --> <!-- 主体内容 -->
<div class="form-header"> <div class="form-body">
<div class="header-left"> <!-- 右上角紧急程度 -->
<el-icon class="header-icon"><Files /></el-icon> <div class="urgency-bar">
<span class="header-title">检查申请单</span> <span class="urgency-bar-label">紧急程度</span>
</div> <el-radio-group v-model="form.urgencyLevel" @change="handleUrgencyChange" size="small">
<div class="header-right">
<span class="urgency-label">紧急程度</span>
<el-radio-group v-model="form.urgencyLevel" @change="handleUrgencyChange" class="urgency-radio-group">
<el-radio-button label="routine">普通</el-radio-button> <el-radio-button label="routine">普通</el-radio-button>
<el-radio-button label="emergency">急诊</el-radio-button> <el-radio-button label="emergency">急诊</el-radio-button>
</el-radio-group> </el-radio-group>
<transition name="el-fade-in-linear"> <transition name="el-fade-in-linear">
<span v-if="form.urgencyLevel === 'emergency'" class="emergency-tip"> <span v-if="form.urgencyLevel === 'emergency'" class="emergency-tip-inline">
<el-icon><WarningFilled /></el-icon> <el-icon><WarningFilled /></el-icon>
急诊单将进入绿色通道 绿色通道
</span> </span>
</transition> </transition>
</div> </div>
</div>
<!-- 主体内容区 -->
<div class="form-body">
<!-- 选择检查项目 --> <!-- 选择检查项目 -->
<div class="section-card"> <div class="section-card">
<div class="section-header"> <div class="transfer-wrapper">
<el-icon><Document /></el-icon>
<span>选择检查项目</span>
</div>
<div v-loading="loading" class="transfer-wrapper">
<el-transfer <el-transfer
v-model="transferValue" v-model="transferValue"
:data="applicationList" :data="applicationList"
@@ -45,165 +34,150 @@
</div> </div>
</div> </div>
<!-- 申请信息 --> <el-form :model="form" :rules="rules" ref="formRef" label-position="top" class="info-form">
<div class="section-card"> <!-- 第一行发往科室 + 紧急程度 + 期望检查时间 -->
<div class="section-header"> <el-row :gutter="16">
<el-icon><EditPen /></el-icon> <el-col :span="8">
<span>申请信息</span> <el-form-item label="发往科室" prop="targetDepartment">
</div> <el-tree-select
clearable
style="width: 100%"
v-model="form.targetDepartment"
filterable
:data="orgOptions"
:props="{ value: 'id', label: 'name', children: 'children' }"
value-key="id"
check-strictly
placeholder="请选择执行科室"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="期望检查时间">
<el-date-picker
v-model="form.expectedExaminationTime"
type="datetime"
placeholder="默认当前时间"
style="width: 100%"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm"
:disabled-date="disabledFutureDate"
:default-value="new Date()"
/>
</el-form-item>
</el-col>
</el-row>
<el-form :model="form" :rules="rules" ref="formRef" label-position="top" class="info-form"> <!-- 第二行症状 + 体征 -->
<!-- 第一行发往科室 + 期望检查时间 --> <el-row :gutter="16">
<el-row :gutter="16"> <el-col :span="12">
<el-col :span="12"> <el-form-item label="症状">
<el-form-item label="发往科室" prop="targetDepartment"> <el-input v-model="form.symptom" autocomplete="off" type="textarea" :rows="2" placeholder="请输入患者症状" />
<el-tree-select </el-form-item>
clearable </el-col>
style="width: 100%" <el-col :span="12">
v-model="form.targetDepartment" <el-form-item label="体征">
filterable <el-input v-model="form.sign" autocomplete="off" type="textarea" :rows="2" placeholder="请输入患者体征" />
:data="orgOptions" </el-form-item>
:props="{ value: 'id', label: 'name', children: 'children' }" </el-col>
value-key="id" </el-row>
check-strictly
placeholder="请选择执行科室"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="期望检查时间">
<el-date-picker
v-model="form.expectedExaminationTime"
type="datetime"
placeholder="默认当前时间"
style="width: 100%"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm"
:disabled-date="disabledFutureDate"
:default-value="new Date()"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 症状 + 体征 --> <!-- 临床诊断 + 其他诊断 -->
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="症状"> <el-form-item label="临床诊断">
<el-input v-model="form.symptom" autocomplete="off" type="textarea" :rows="2" placeholder="请输入患者症状" /> <el-input disabled v-model="form.clinicalDiagnosis" placeholder="自动带入主诊断" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="体征"> <el-form-item label="其他诊断">
<el-input v-model="form.sign" autocomplete="off" type="textarea" :rows="2" placeholder="请输入患者体征" /> <el-input disabled v-model="form.otherDiagnosis" placeholder="自动带入其他诊断" />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<!-- 临床诊断 + 其他诊断 --> <!-- 相关结果 + 注意事项 -->
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="临床诊断"> <el-form-item label="相关结果">
<el-input disabled v-model="form.clinicalDiagnosis" placeholder="自动带入主诊断" /> <el-input v-model="form.relatedResult" autocomplete="off" type="textarea" :rows="2" placeholder="请输入相关检验结果" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="其他诊断"> <el-form-item label="注意事项">
<el-input disabled v-model="form.otherDiagnosis" placeholder="自动带入其他诊断" /> <el-input v-model="form.attention" autocomplete="off" type="textarea" :rows="2" placeholder="请输入检查注意事项" />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<!-- 相关结果 + 注意事项 --> <!-- 检查目的 + 病史摘要 -->
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="相关结果"> <el-form-item label="检查目的" prop="examinationPurpose">
<el-input v-model="form.relatedResult" autocomplete="off" type="textarea" :rows="2" placeholder="请输入相关检验结果" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="注意事项">
<el-input v-model="form.attention" autocomplete="off" type="textarea" :rows="2" placeholder="请输入检查注意事项" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- 过敏史卡片 -->
<div class="section-card allergy-card">
<div class="section-header">
<el-icon><Warning /></el-icon>
<span>过敏史</span>
<span v-if="form.allergyHistory" class="header-count">{{ form.allergyHistory.length }}</span>
</div>
<div class="allergy-content">
<div class="allergy-input-row">
<el-input <el-input
v-model="form.allergyHistory" v-model="form.examinationPurpose"
autocomplete="off" autocomplete="off"
type="textarea" type="textarea"
:rows="2" :rows="2"
:class="{ 'allergy-danger': isSevereAllergy }" maxlength="200"
placeholder="如:造影剂过敏史等(系统将自动从患者档案带入)" show-word-limit
placeholder="请输入检查目的,如:明确诊断、术后复查、疗效评估等"
/> />
<span v-if="isSevereAllergy" class="allergy-severe-tag"> </el-form-item>
<el-icon><WarningFilled /></el-icon> </el-col>
严重过敏 <el-col :span="12">
</span> <el-form-item label="病史摘要" prop="medicalHistorySummary">
</div> <div class="history-field-wrapper">
<div class="allergy-confirm"> <el-input
<el-checkbox v-model="form.allergyConfirmed" size="small"> v-model="form.medicalHistorySummary"
已通过口头询问确认无过敏史 autocomplete="off"
</el-checkbox> type="textarea"
</div> :rows="2"
</div> placeholder="请简要描述患者病史摘要"
</div> />
<el-button
type="primary"
plain
size="small"
class="history-sync-btn"
@click="handleSyncHistory"
:loading="syncingHistory"
>
<el-icon><Refresh /></el-icon>
同步
</el-button>
</div>
</el-form-item>
</el-col>
</el-row>
<!-- 检查目的卡片 --> <!-- 第六行过敏史 -->
<div class="section-card purpose-card"> <el-row :gutter="16">
<div class="section-header"> <el-col :span="24">
<el-icon><Aim /></el-icon> <el-form-item label="过敏史">
<span>检查目的</span> <div class="allergy-wrapper">
<span class="required-mark">必填</span> <el-input
</div> v-model="form.allergyHistory"
<el-input autocomplete="off"
v-model="form.examinationPurpose" type="textarea"
autocomplete="off" :rows="1"
type="textarea" :class="{ 'allergy-danger': isSevereAllergy }"
:rows="2" placeholder="如:造影剂过敏史等(系统将自动从患者档案带入)"
maxlength="200" />
show-word-limit <div class="allergy-actions">
placeholder="请输入检查目的,如:明确诊断、术后复查、疗效评估等" <span v-if="isSevereAllergy" class="allergy-severe-tag-inline">
/> <el-icon><WarningFilled /></el-icon>
</div> 严重过敏
</span>
<!-- 病史摘要卡片 --> <el-checkbox v-model="form.allergyConfirmed" size="small">
<div class="section-card history-card"> 已通过口头询问确认无过敏史
<div class="section-header"> </el-checkbox>
<el-icon><DocumentCopy /></el-icon> </div>
<span>病史摘要</span> </div>
<span class="required-mark">必填</span> </el-form-item>
<el-button </el-col>
type="primary" </el-row>
plain </el-form>
size="small"
class="sync-btn"
@click="handleSyncHistory"
:loading="syncingHistory"
>
<el-icon><Refresh /></el-icon>
同步现病史/体征
</el-button>
</div>
<el-input
v-model="form.medicalHistorySummary"
autocomplete="off"
type="textarea"
:rows="3"
placeholder="请简要描述患者病史摘要"
/>
</div>
</div>
</div> </div>
<!-- 急诊确认弹窗 --> <!-- 急诊确认弹窗 -->
@@ -228,6 +202,7 @@
<script setup name="MedicalExaminations"> <script setup name="MedicalExaminations">
import {getCurrentInstance, onMounted, reactive, ref, watch, computed, nextTick} from 'vue'; import {getCurrentInstance, onMounted, reactive, ref, watch, computed, nextTick} from 'vue';
import dayjs from 'dayjs';
import {patientInfo} from '../../../store/patient.js'; import {patientInfo} from '../../../store/patient.js';
import {getDepartmentList} from '@/api/public.js'; import {getDepartmentList} from '@/api/public.js';
import {getEncounterDiagnosis} from '../../api.js'; import {getEncounterDiagnosis} from '../../api.js';
@@ -355,7 +330,7 @@ const form = reactive({
allergyHistory: '', allergyHistory: '',
examinationPurpose: '', examinationPurpose: '',
medicalHistorySummary: '', medicalHistorySummary: '',
expectedExaminationTime: '', expectedExaminationTime: dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss'),
symptom: '', symptom: '',
sign: '', sign: '',
clinicalDiagnosis: '', clinicalDiagnosis: '',
@@ -622,7 +597,7 @@ const resetForm = () => {
form.allergyHistory = ''; form.allergyHistory = '';
form.examinationPurpose = ''; form.examinationPurpose = '';
form.medicalHistorySummary = ''; form.medicalHistorySummary = '';
form.expectedExaminationTime = ''; form.expectedExaminationTime = dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss');
form.symptom = ''; form.symptom = '';
form.sign = ''; form.sign = '';
form.clinicalDiagnosis = ''; form.clinicalDiagnosis = '';
@@ -705,81 +680,13 @@ $bg-color: #f5f7fa;
background: $bg-color; background: $bg-color;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
// 顶部标题栏 // 主体内容区 - 紧凑布局
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 20px;
background: linear-gradient(135deg, #fff 0%, #f0f7ff 100%);
border-bottom: 1px solid $border-color;
.header-left {
display: flex;
align-items: center;
gap: 10px;
.header-icon {
font-size: 24px;
color: $primary-color;
}
.header-title {
font-size: 18px;
font-weight: 600;
color: $text-primary;
letter-spacing: 1px;
}
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
.urgency-label {
font-size: 13px;
color: $text-secondary;
font-weight: 500;
}
.urgency-radio-group {
:deep(.el-radio-button__inner) {
border-radius: 4px;
margin: 0;
}
:deep(.el-radio-button:first-child .el-radio-button__inner) {
border-radius: 4px;
}
:deep(.el-radio-button:last-child .el-radio-button__inner) {
border-radius: 4px;
}
}
.emergency-tip {
display: flex;
align-items: center;
gap: 4px;
color: $danger-color;
font-size: 13px;
font-weight: 500;
background: #fef0f0;
padding: 4px 10px;
border-radius: 4px;
border: 1px solid #fde2e2;
}
}
}
// 主体内容区
.form-body { .form-body {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 8px;
padding: 16px; padding: 8px 12px;
overflow-y: auto; overflow-y: auto;
&::-webkit-scrollbar { &::-webkit-scrollbar {
@@ -796,47 +703,30 @@ $bg-color: #f5f7fa;
} }
} }
// 卡片通用样式 // 紧急程度栏 - 右上角
.urgency-bar {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding: 4px 0;
margin-bottom: 4px;
}
.urgency-bar-label {
font-size: 13px;
font-weight: 500;
color: $text-regular;
white-space: nowrap;
}
// 卡片通用样式 - 紧凑
.section-card { .section-card {
background: #fff; background: #fff;
border-radius: 8px; border-radius: 6px;
padding: 16px; padding: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); border: 1px solid #e4e7ed;
border: 1px solid rgba(0, 0, 0, 0.04); margin-bottom: 4px;
.section-header {
display: flex;
align-items: center;
gap: 8px;
padding-bottom: 12px;
margin-bottom: 12px;
border-bottom: 1px dashed $border-color;
font-size: 14px;
font-weight: 600;
color: $text-primary;
> i {
font-size: 16px;
color: $primary-color;
}
.header-count {
margin-left: auto;
font-size: 12px;
font-weight: 400;
color: $text-secondary;
}
.required-mark {
font-size: 12px;
font-weight: 500;
color: #fff;
background: $danger-color;
padding: 2px 8px;
border-radius: 10px;
margin-left: 4px;
}
}
} }
.transfer-wrapper { .transfer-wrapper {
@@ -850,10 +740,23 @@ $bg-color: #f5f7fa;
display: flex !important; display: flex !important;
flex-direction: row !important; flex-direction: row !important;
} }
// 信息表单
// 穿梭框按钮垂直居中
:deep(.el-transfer__buttons) {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0 4px;
}
:deep(.el-transfer__button) {
margin: 4px 0;
}
// 信息表单 - 紧凑
.info-form { .info-form {
:deep(.el-form-item) { :deep(.el-form-item) {
margin-bottom: 14px; margin-bottom: 6px;
.el-form-item__label { .el-form-item__label {
font-size: 13px; font-size: 13px;
@@ -883,53 +786,10 @@ $bg-color: #f5f7fa;
} }
} }
// 过敏史卡片 // 过敏史危险输入样式
.allergy-card { :deep(.el-textarea__inner.allergy-danger) {
.allergy-content { border-color: $danger-color !important;
.allergy-input-row { background-color: #fef0f0;
position: relative;
:deep(.el-textarea) {
.el-textarea__inner.allergy-danger {
border-color: $danger-color !important;
background-color: #fef0f0;
}
}
}
.allergy-severe-tag {
position: absolute;
right: 12px;
top: 8px;
display: flex;
align-items: center;
gap: 4px;
color: $danger-color;
font-size: 13px;
font-weight: 600;
background: #fef0f0;
padding: 3px 10px;
border-radius: 12px;
border: 1px solid #fde2e2;
}
.allergy-confirm {
margin-top: 10px;
padding-left: 4px;
}
}
}
// 病史摘要卡片
.history-card {
.section-header {
.sync-btn {
margin-left: auto;
font-size: 12px;
padding: 6px 12px;
border-radius: 16px;
}
}
} }
// 急诊确认弹窗 // 急诊确认弹窗
@@ -968,4 +828,64 @@ $bg-color: #f5f7fa;
.fade-in-linear-leave-to { .fade-in-linear-leave-to {
opacity: 0; opacity: 0;
} }
/* 紧急程度行内布局 */
.urgency-inline {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.emergency-tip-inline {
display: inline-flex;
align-items: center;
gap: 2px;
color: $danger-color;
font-size: 11px;
font-weight: 500;
background: #fef0f0;
padding: 2px 6px;
border-radius: 3px;
white-space: nowrap;
}
/* 过敏史包装 */
.allergy-wrapper {
width: 100%;
}
.allergy-actions {
display: flex;
align-items: center;
gap: 12px;
margin-top: 4px;
}
.allergy-severe-tag-inline {
display: inline-flex;
align-items: center;
gap: 2px;
color: $danger-color;
font-size: 11px;
font-weight: 600;
background: #fef0f0;
padding: 2px 8px;
border-radius: 3px;
}
/* 病史摘要同步按钮 */
.history-field-wrapper {
position: relative;
width: 100%;
}
.history-sync-btn {
position: absolute;
right: 4px;
top: -28px;
font-size: 11px;
padding: 2px 8px;
height: 24px;
}
</style> </style>