docs(release-notes): 添加住院护士站划价功能说明和发版记录

- 新增住院护士站划价服务流程说明文档,详细描述了从参数预处理到结果响应的五大阶段流程
- 包含耗材类医嘱和诊疗活动类医嘱的差异化处理逻辑
- 添加完整的发版内容记录,涵盖新增菜单功能和各模块优化点
- 记录了住院相关功能的新增和门诊业务流程的修复
```
This commit is contained in:
2025-12-25 14:13:14 +08:00
parent 85fcb7c2e2
commit abc0674531
920 changed files with 107068 additions and 14495 deletions

View File

@@ -0,0 +1,874 @@
<template>
<div style="height: calc(100vh - 126px)">
<!-- 操作工具栏 -->
<div
style="
height: 51px;
border-bottom: 2px solid #e4e7ed;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
"
>
<div style="display: flex; align-items: center">
<!-- 日期选择tabs -->
<el-tabs
v-model="dateRange"
type="card"
class="date-tabs"
@tab-click="handleDateTabClick"
style="margin-right: 20px"
>
<el-tab-pane label="今日" name="today"></el-tab-pane>
<el-tab-pane label="昨日" name="yesterday"></el-tab-pane>
<!-- <el-tab-pane label="其他" name="other"></el-tab-pane> -->
</el-tabs>
<!-- 日期选择器 -->
<el-date-picker
v-model="dateRangeValue"
type="daterange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="handleDatePickerChange"
@clear="onClear"
style="width: 240px; margin-right: 20px"
/>
<!-- 费用类型选择 -->
<el-select
v-model="formParams.chargeItemEnum"
placeholder="请选择费用类型"
clearable
style="width: 150px; margin-right: 20px"
>
<el-option
v-for="type in med_chrgitm_type"
:key="type.value"
:label="type.label"
:value="type.value"
/>
</el-select>
<!-- 执行科室选择 -->
<el-select
v-model="formParams.orgId"
placeholder="请选择执行科室"
clearable
style="width: 150px; margin-right: 20px"
>
<el-option
v-for="dept in orgOptions"
:key="dept.id"
:label="dept.name"
:value="dept.id"
/>
</el-select>
<!-- 查询按钮 -->
<el-button type="primary" @click="onReset">重置</el-button>
<el-button type="primary" @click="handleQuery">查询</el-button>
</div>
<div style="display: flex; align-items: center">
<!-- 导出按钮 -->
<el-button @click="handleExport">导出</el-button>
<!-- 打印按钮 -->
<!-- <el-button @click="handlePrint" style="margin-left: 15px">打印</el-button> -->
</div>
</div>
<!-- 费用明细列表区域 -->
<div
style="padding: 10px; background-color: #eef9fd; height: 100%; overflow-y: auto"
v-loading="loading"
>
<!-- 费用汇总信息 -->
<div style="background-color: white; padding: 15px; margin-bottom: 10px; border-radius: 4px">
<div style="display: flex; justify-content: space-between; align-items: center">
<div>
<h3 style="margin: 0; font-weight: normal; color: #303133">费用汇总</h3>
<p style="margin: 5px 0; color: #606266; font-size: 14px">
费用周期{{ formatDateRange() }}
</p>
</div>
<div style="text-align: right">
<p style="margin: 0; font-size: 18px; font-weight: bold; color: #ff4d4f">
合计金额¥{{ formatNumber(totalAmount, 4) }}
</p>
<p style="margin: 5px 0; color: #606266; font-size: 14px">
明细项数{{ groupedPrescriptionList.length }}
</p>
</div>
</div>
</div>
<!-- 列表内容容器 -->
<div
v-if="groupedPrescriptionList.length > 0"
style="
flex: 1;
overflow-y: auto;
padding: 10px;
background-color: white;
border-radius: 4px;
"
>
<!-- 患者医嘱折叠面板 -->
<div class="prescription-collapse-container">
<el-collapse
v-model="activeCollapseNames"
accordion
border
style="--el-collapse-border-color: #e4e7ed"
@change="onCollapasChange"
>
<!-- 按encounterId分组渲染患者折叠项 -->
<el-collapse-item
v-for="(patientGroup, index) in groupedPrescriptionList"
:key="`patient-${index}-${safeGet(patientGroup[0], 'encounterId', index)}`"
:name="`patient-${index}`"
class="patient-collapse-item"
>
<!-- 折叠面板头部 - 患者信息 -->
<template #title>
<div class="patient-header">
<div class="patient-basic-info">
<el-avatar :icon="User" size="small" style="margin-right: 10px"></el-avatar>
<div>
<span class="patient-name">{{
safeGet(patientGroup[0], 'patientName', '未知患者')
}}</span>
<span class="patient-info-tag"
>{{ safeGet(patientGroup[0], 'genderEnum_enumText', '未知') }} /
{{ safeGet(patientGroup[0], 'age', '未知') }}</span
>
<span class="patient-info-tag"
>{{ safeGet(patientGroup[0], 'bedName', '无床位') }}{{
safeGet(patientGroup[0], 'busNo', '未知编号')
}}</span
>
</div>
</div>
<div class="patient-ext-info">
<div class="ext-item">
<span class="label">住院医生</span>
<span class="value">{{
safeGet(patientGroup[0], 'admittingDoctorName', '未知')
}}</span>
</div>
<div class="ext-item">
<span class="label">预交金余额</span>
<span class="value amount">{{
formatNumber(safeGet(patientGroup[0], 'balanceAmount', 0), 4)
}}</span>
</div>
<div class="ext-item">
<span class="label">诊断</span>
<span class="value" :title="safeGet(patientGroup[0], 'conditionNames', '无')">
{{ truncateText(safeGet(patientGroup[0], 'conditionNames', '无'), 20) }}
</span>
</div>
<div class="ext-item">
<el-tag size="small">{{
safeGet(patientGroup[0], 'contractName', '未知')
}}</el-tag>
</div>
<div class="patient-amount-preview">
小计<span class="amount">{{ calculatePatientTotal(patientGroup) }}</span>
</div>
</div>
</div>
</template>
<!-- 折叠面板内容 - 医嘱表格 -->
<div class="prescription-table-container">
<el-table
:data="safeArray(patientGroup)"
border
size="small"
style="width: 100%; margin-top: 10px"
@sort-change="handleSortChange"
>
<el-table-column label="项目名称" prop="chargeName" min-width="200" />
<el-table-column
label="费用类型"
prop="chargeItemEnum_enumText"
width="120"
align="center"
/>
<el-table-column
label="单价"
prop="unitPrice"
width="100"
align="center"
sortable
>
<template #default="scope">
{{ formatNumber(scope.row.unitPrice, 4) }}
</template>
</el-table-column>
<el-table-column
label="数量"
prop="quantityValue"
width="100"
align="center"
sortable
/>
<el-table-column
label="金额"
prop="totalPrice"
width="100"
align="center"
sortable
>
<template #default="scope">
<span style="color: #ff4d4f">{{
formatNumber(scope.row.totalPrice, 4)
}}</span>
</template>
</el-table-column>
<el-table-column :prop="orgId" label="执行科室" width="120" align="center">
<template #default="scope">
{{ selectOrg(scope.row.orgId) }}
</template>
</el-table-column>
<el-table-column label="执行人" prop="practitioner" width="100" align="center" />
<el-table-column label="执行日期" prop="recordedTime" width="120" align="center">
<template #default="scope">
{{ moment(scope.row?.recordedTime).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
<el-table-column
label="医保类型"
prop="chrgitmLv_enumText"
width="100"
align="center"
>
</el-table-column>
<el-table-column label="备注" prop="remark" min-width="150" />
</el-table>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
<!-- 无数据提示 -->
<el-empty
v-if="!loading && groupedPrescriptionList.length === 0"
description="暂无费用明细数据"
/>
</div>
<!-- 打印预览弹窗 -->
<el-dialog
v-model="printDialogVisible"
title="打印预览"
width="80%"
:close-on-click-modal="false"
>
<div id="print-content">
<div style="text-align: center; margin-bottom: 20px">
<h2 style="margin: 0">费用明细清单</h2>
<p style="margin: 5px 0">患者姓名{{ patientInfo || '未选择患者' }}</p>
<p style="margin: 5px 0">费用周期{{ formatDateRange() }}</p>
</div>
<table style="width: 100%; border-collapse: collapse">
<thead>
<tr style="background-color: #eef9fd">
<th style="border: 1px solid #e4e7ed; padding: 8px">项目名称</th>
<th style="border: 1px solid #e4e7ed; padding: 8px">费用类型</th>
<th style="border: 1px solid #e4e7ed; padding: 8px">单价</th>
<th style="border: 1px solid #e4e7ed; padding: 8px">数量</th>
<th style="border: 1px solid #e4e7ed; padding: 8px">金额</th>
<th style="border: 1px solid #e4e7ed; padding: 8px">执行科室</th>
<th style="border: 1px solid #e4e7ed; padding: 8px">执行日期</th>
</tr>
</thead>
<tbody>
<tr v-for="item in feeDetailList" :key="item.id">
<td style="border: 1px solid #e4e7ed; padding: 8px">{{ item.itemName }}</td>
<td style="border: 1px solid #e4e7ed; padding: 8px">{{ item.feeTypeName }}</td>
<td style="border: 1px solid #e4e7ed; padding: 8px">
{{ item.unitPrice.toFixed(2) }}
</td>
<td style="border: 1px solid #e4e7ed; padding: 8px">{{ item.quantity }}</td>
<td style="border: 1px solid #e4e7ed; padding: 8px">{{ item.amount.toFixed(2) }}</td>
<td style="border: 1px solid #e4e7ed; padding: 8px">{{ item.execDept }}</td>
<td style="border: 1px solid #e4e7ed; padding: 8px">{{ item.executeDate }}</td>
</tr>
</tbody>
<tfoot>
<tr style="background-color: #f5f7fa">
<td
colspan="4"
style="
border: 1px solid #e4e7ed;
padding: 8px;
text-align: right;
font-weight: bold;
"
>
合计
</td>
<td
colspan="3"
style="border: 1px solid #e4e7ed; padding: 8px; color: #ff4d4f; font-weight: bold"
>
¥{{ totalAmount.toFixed(2) }}
</td>
</tr>
</tfoot>
</table>
</div>
<template #footer>
<el-button @click="printDialogVisible = false">关闭</el-button>
<el-button type="primary" @click="doPrint">打印</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch, reactive } from 'vue';
import moment from 'moment';
import { ElMessage } from 'element-plus';
import { patientInfoList } from '../../components/store/patient.js';
import { formatDateStr } from '@/utils/index';
import { getCostDetail } from './api.js';
import { getOrgList } from '../../../basicmanage/ward/components/api.js';
import { User } from '@element-plus/icons-vue';
const { proxy } = getCurrentInstance();
const { med_chrgitm_type } = proxy.useDict('med_chrgitm_type');
const groupedPrescriptionList = ref([]); // 按encounterId分组后的数据
const activeCollapseNames = ref(['patient-0']); // Collapse激活状态
// 响应式数据
const loading = ref(false);
const feeDetailList = ref([]);
const dateRange = ref('today'); // today, yesterday, other
const dateRangeValue = ref([]);
const execDepartment = ref('');
const feeTypeOptions = ref([]);
const patientInfo = ref('');
const orgOptions = ref([]);
const selectIndex = ref(0);
const formParams = reactive({
chargeItemEnum: undefined,
orgId: undefined,
recordedTimeSTime: undefined,
recordedTimeETime: undefined,
pageSize: 10,
encounterIds: '',
pageNo: 1,
});
const props = defineProps({
activeTab: {
type: String,
},
});
// 分页相关
const total = ref(0);
// 打印相关
const printDialogVisible = ref(false);
// 计算总金额
const totalAmount = computed(() => {
console.log('feeDetailList========>', feeDetailList.value);
return feeDetailList?.value?.reduce((sum, item) => {
return sum + (item.totalPrice || 0);
}, 0);
});
// 初始化
onMounted(() => {
// 设置默认日期
const today = new Date();
dateRangeValue.value = [formatDateStr(today, 'YYYY-MM-DD'), formatDateStr(today, 'YYYY-MM-DD')];
});
watch(
() => patientInfoList,
(newValue) => {
if (newValue.value.length > 0) {
if (!(dateRangeValue.value == null || dateRangeValue.value == undefined)) {
formParams.recordedTimeSTime = dateRangeValue.value[0] + ' ' + '00:00:00';
formParams.recordedTimeETime = dateRangeValue.value[1] + ' ' + '23:59:59';
}
const encounterIds = newValue.value
.map((item) => {
return item.encounterId;
})
.join(',');
formParams.encounterIds = encounterIds;
if (props.activeTab === 'expenseDetail') {
getTableList();
}
}
if (newValue.value.length <= 0) {
feeDetailList.value = [];
groupedPrescriptionList.value = [];
}
},
{ deep: true }
);
// 获取列表信息
const getTableList = async () => {
const params = formParams;
try {
const res = await getCostDetail(params);
feeDetailList.value = res.data;
total.value = res.data?.total;
// 核心按encounterId分组数据
groupedPrescriptionList.value = groupByEncounterId(res.data);
// 默认展开第一个患者面板
if (groupedPrescriptionList.value.length > 0 && activeCollapseNames.value.length === 0) {
activeCollapseNames.value = ['patient-0'];
}
} catch (error) {}
};
// 监听患者选择变化
function watchPatientSelection() {
// 定期检查患者选择状态变化
setInterval(() => {
if (patientInfoList.value && patientInfoList.value.length > 0) {
const selectedPatient = patientInfoList.value.find((patient) => patient.selected === true);
if (selectedPatient) {
patientInfo.value = selectedPatient.patientName || '';
} else {
patientInfo.value = '未选择患者';
}
} else {
patientInfo.value = '未选择患者';
}
}, 300);
}
/** 查询科室 */
const getLocationInfo = () => {
getOrgList().then((res) => {
orgOptions.value = res.data?.records[0]?.children;
});
};
getLocationInfo();
// 映射
const selectOrg = (itemid) => {
const item = orgOptions.value.find((item) => {
return item.id == itemid;
});
return item?.name;
};
// 重置
const onReset = () => {
const today = new Date();
dateRangeValue.value = [formatDateStr(today, 'YYYY-MM-DD'), formatDateStr(today, 'YYYY-MM-DD')];
formParams.orgId = undefined;
formParams.chargeItemEnum = undefined;
formParams.recordedTimeSTime = dateRangeValue.value[0] + ' ' + '00:00:00';
formParams.recordedTimeETime = dateRangeValue.value[1] + ' ' + '23:59:59';
dateRange.value = 'today';
getTableList();
};
// 加载费用类型选项
function loadFeeTypeOptions() {
// 模拟费用类型数据
feeTypeOptions.value = [
{ label: '检查费', value: 'examine' },
{ label: '治疗费', value: 'treatment' },
{ label: '药品费', value: 'medicine' },
{ label: '材料费', value: 'material' },
{ label: '床位费', value: 'bed' },
{ label: '其他费用', value: 'others' },
];
}
// 处理日期tabs点击
function handleDateTabClick(tab) {
const rangeType = tab.paneName;
const today = new Date();
const yesterday = new Date();
yesterday.setDate(today.getDate() - 1);
switch (rangeType) {
case 'today':
dateRangeValue.value = [
formatDateStr(today, 'YYYY-MM-DD'),
formatDateStr(today, 'YYYY-MM-DD'),
];
break;
case 'yesterday':
dateRangeValue.value = [
formatDateStr(yesterday, 'YYYY-MM-DD'),
formatDateStr(yesterday, 'YYYY-MM-DD'),
];
break;
// other 情况保持用户选择的值
}
if (!(dateRangeValue.value == null || dateRangeValue.value == undefined)) {
formParams.recordedTimeSTime = dateRangeValue.value[0] + ' ' + '00:00:00';
formParams.recordedTimeETime = dateRangeValue.value[1] + ' ' + '23:59:59';
}
getTableList();
}
// 处理日期选择器变化
function handleDatePickerChange() {
if (dateRangeValue?.value?.length > 0) {
// dateRange.value = 'other';
formParams.recordedTimeSTime = dateRangeValue.value[0] + ' ' + '00:00:00';
formParams.recordedTimeETime = dateRangeValue.value[1] + ' ' + '23:59:59';
} else {
formParams.recordedTimeSTime = undefined;
formParams.recordedTimeETime = undefined;
}
}
// 清空
const onClear = () => {
console.log('1111111111');
const today = new Date();
dateRangeValue.value = [formatDateStr(today, 'YYYY-MM-DD'), formatDateStr(today, 'YYYY-MM-DD')];
formParams.orgId = undefined;
formParams.chargeItemEnum = undefined;
formParams.recordedTimeSTime = dateRangeValue.value[0] + ' ' + '00:00:00';
formParams.recordedTimeETime = dateRangeValue.value[1] + ' ' + '23:59:59';
dateRange.value = 'today';
getTableList();
};
// 格式化日期范围显示
function formatDateRange() {
if (dateRangeValue.value && dateRangeValue.value.length === 2) {
return `${dateRangeValue.value[0]}${dateRangeValue.value[1]}`;
}
return '';
}
// 查询按钮点击
function handleQuery() {
console.log('params=======>', formParams);
getTableList();
}
// 处理排序变化
function handleSortChange({ prop, order }) {
// const sortedData = [...feeDetailList.value];
const selectData = groupedPrescriptionList.value[selectIndex.value];
const sortedData = [...selectData];
if (order === 'ascending') {
sortedData.sort((a, b) => (a[prop] > b[prop] ? 1 : -1));
} else if (order === 'descending') {
sortedData.sort((a, b) => (a[prop] < b[prop] ? 1 : -1));
}
groupedPrescriptionList.value[selectIndex.value] = sortedData;
// feeDetailList.value = sortedData;
}
// 处理分页大小变化
function handleSizeChange(newSize) {
formParams.pageSize = newSize;
formParams.pageNo = 1;
getTableList();
}
// 处理当前页变化
function handleCurrentChange(newCurrent) {
formParams.pageNo = newCurrent;
getTableList();
}
// 导出
async function handleExport() {
if (groupedPrescriptionList.value.length === 0) {
ElMessage.warning('暂无数据可导出');
return;
}
try {
// 实际项目中这里应该调用导出API或使用Excel库生成文件
await proxy.$download.downloadGet(
'/inhospitalnursestation/nursebilling/excel-out',
{
...formParams,
},
`dict_${new Date().getTime()}.xlsx`
);
} catch (error) {}
}
// 打印预览
function handlePrint() {
if (feeDetailList.value.length === 0) {
ElMessage.warning('暂无数据可打印');
return;
}
printDialogVisible.value = true;
}
// 执行打印
function doPrint() {
try {
// 获取要打印的内容
const printContent = document.getElementById('print-content').innerHTML;
// 创建临时窗口
const printWindow = window.open('', '_blank');
// 写入内容
printWindow.document.write(`
<html>
<head>
<title>费用明细清单</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ccc; padding: 8px; }
th { background-color: #f2f2f2; }
tfoot { font-weight: bold; }
.total-row { background-color: #f5f5f5; }
</style>
</head>
<body>
${printContent}
</body>
</html>
`);
// 打印
printWindow.document.close();
printWindow.focus();
printWindow.print();
} catch (e) {
ElMessage.error('打印失败');
console.error('Print error:', e);
}
}
// 获取当前展开折叠板索引
function onCollapasChange(select) {
if (select) {
const selectArr = select.split('-');
if (selectArr && selectArr.length > 1) {
const idx = selectArr[1];
selectIndex.value = Number(idx);
}
}
}
// ========== 核心工具函数 ==========
/**
* 安全获取对象属性,避免空值报错
* @param {Object} obj - 源对象
* @param {String} path - 属性路径
* @param {Any} defaultValue - 默认值
* @returns {Any}
*/
const safeGet = (obj, path, defaultValue = '') => {
// 1. 前置校验:如果源对象不是对象类型,直接返回默认值
if (!obj || typeof obj !== 'object') return defaultValue;
// 2. 拆分路径:把 "info.basic.name" 拆成 ["info", "basic", "name"]
const paths = path.split('.');
// 3. 初始化结果为源对象
let result = obj;
// 4. 循环遍历路径数组,逐层访问属性
for (const p of paths) {
// 5. 关键如果当前层属性不存在undefined/null直接返回默认值
if (result[p] === undefined || result[p] === null) return defaultValue;
// 6. 存在则继续访问下一层
result = result[p];
}
// 7. 所有层级都存在,返回最终属性值
return result;
};
/**
* 安全转换为数组
* @param {Any} data - 待转换数据
* @returns {Array}
*/
const safeArray = (data) => {
return Array.isArray(data) ? data : [];
};
/**
* 格式化数字保留4位小数金额专用
* @param {Number} num - 数字
* @param {Number} decimal - 小数位数默认4位
* @returns {String}
*/
const formatNumber = (num, decimal = 4) => {
if (isNaN(Number(num))) return '0.0000';
// 保留指定小数位不足补0
return Number(num).toFixed(decimal);
};
/**
* 文本截断(超出长度显示省略号)
* @param {String} text - 待处理文本
* @param {Number} length - 最大长度
* @returns {String}
*/
const truncateText = (text, length = 20) => {
if (!text || text.length <= length) return text;
return `${text.slice(0, length)}...`;
};
/**
* 按encounterId分组数据
* @param {Array} data - 原始数据
* @returns {Array} 分组后的数据(二维数组)
*/
const groupByEncounterId = (data) => {
const grouped = {};
safeArray(data).forEach((item) => {
const encounterId = safeGet(item, 'encounterId', 'unknown');
if (!grouped[encounterId]) {
grouped[encounterId] = [];
}
grouped[encounterId].push({
...item,
});
});
// 转换为数组格式
return Object.values(grouped);
};
/**
* 计算单个患者的总金额保留4位小数
* @param {Array} patientGroup - 患者医嘱列表
* @returns {String} 格式化后的金额字符串
*/
const calculatePatientTotal = (patientGroup) => {
const total = safeArray(patientGroup).reduce((sum, item) => {
return Math.round((sum + Number(safeGet(item, 'totalPrice', 0))) * 10000) / 10000;
}, 0);
return formatNumber(total, 4);
};
// 暴露方法供父组件调用
defineExpose({
handleQuery,
});
</script>
<style scoped>
/* 日期tabs样式 */
.date-tabs .el-tabs__header {
margin-bottom: 0;
}
.date-tabs .el-tabs__content {
display: none;
}
/* :deep(.el-table__header th) {
background-color: #eef9fd !important;
} */
:deep(.el-table__row:hover > td) {
background-color: #eef9fd !important;
}
/* 折叠面板容器 */
.prescription-collapse-container {
--el-collapse-item-border-radius: 8px;
}
/* 患者折叠项 */
.patient-collapse-item {
margin-bottom: 12px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
/* 患者头部 */
.patient-header {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
padding-left: 10px;
flex-wrap: wrap;
gap: 8px;
}
.patient-basic-info {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.patient-name {
font-weight: 600;
font-size: 15px;
margin-right: 10px;
}
.patient-info-tag {
color: #666;
font-size: 13px;
margin-right: 8px;
}
.patient-ext-info {
display: flex;
align-items: center;
gap: 15px;
flex-wrap: wrap;
}
.ext-item {
display: flex;
align-items: center;
font-size: 13px;
flex-wrap: wrap;
}
.ext-item .label {
color: #999;
margin-right: 4px;
}
.ext-item .amount {
color: #e6a23c;
font-weight: 600;
}
.patient-amount-preview {
font-size: 13px;
color: #333;
margin-left: 10px;
}
.patient-amount-preview .amount {
color: #e6a23c;
font-weight: 600;
}
.prescription-table-container {
padding: 10px 0;
}
</style>