Files
his/openhis-ui-vue3/src/views/inpatientDoctor/home/components/applicationShow/testApplication.vue
荀彧 fd0132ba80 Fix Bug #467: [住院医生工作站-检验申请] 列表显示信息不规范:标题术语错误且单据名称未展示具体检验项目
1. 详情弹窗中"处方号"改为"申请单号",符合住院检验业务术语规范
2. 列表"申请单名称"列改为从 requestFormDetailList 动态构建:
   - 单一项目:显示"项目名称+数量"
   - 多个项目:显示"首项目名称+数量等X项"
   解决此前统一显示"检验申请单"无法区分单据内容的问题

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 03:15:10 +08:00

631 lines
18 KiB
Vue
Executable File

<!--
* @Author: sjjh
* @Date: 2025-09-05 21:16:06
* @Description: 检验申请
-->
<template>
<div class="report-container">
<div class="report-section">
<div class="report-title">
<span>检验申请</span>
<el-icon
class="report-refresh-icon"
:class="{ 'is-loading': loading }"
@click="handleRefresh"
>
<Refresh />
</el-icon>
</div>
<!-- 筛选表单 -->
<div class="filter-form">
<el-form :inline="true" :model="filterForm" class="filter-form-content">
<el-form-item label="申请日期">
<el-date-picker
v-model="filterForm.dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
clearable
style="width: 240px"
/>
</el-form-item>
<el-form-item label="单据状态">
<el-select
v-model="filterForm.status"
placeholder="请选择"
clearable
style="width: 150px"
>
<el-option label="全部" value="" />
<el-option label="待签发" value="0" />
<el-option label="已签发" value="1" />
<el-option label="报告已出" value="4" />
<el-option label="已作废" value="5" />
</el-select>
</el-form-item>
<el-form-item label="关键字">
<el-input
v-model="filterForm.keyword"
placeholder="申请单号/检验项目"
clearable
style="width: 200px"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch" :loading="loading">
<el-icon><Search /></el-icon>
查询
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-form-item>
</el-form>
</div>
<div class="report-table-wrapper">
<el-table
v-loading="loading"
:data="tableData"
border
size="small"
height="100%"
style="width: 100%"
>
<template #empty>
<div class="empty-data">
<el-empty description="暂无匹配记录" :image-size="80" />
</div>
</template>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column label="申请单名称" width="140">
<template #default="scope">
<span>{{ buildApplicationName(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column prop="prescriptionNo" label="申请单号" width="140" />
<el-table-column label="单据状态" width="100" align="center">
<template #default="scope">
<span>{{ parseBillStatus(scope.row.billStatus ?? scope.row.status) }}</span>
</template>
</el-table-column>
<el-table-column label="申请类型" width="100" align="center">
<template #default="scope">
<span>{{ parsePriorityCode(scope.row.descJson) }}</span>
</template>
</el-table-column>
<el-table-column label="标本类型" width="120" align="center">
<template #default="scope">
<span>{{ parseSpecimenType(scope.row.descJson) }}</span>
</template>
</el-table-column>
<el-table-column prop="requesterId_dictText" label="申请者" width="120" />
<el-table-column label="操作" align="center" fixed="right" width="160">
<template #default="scope">
<template v-if="scope.row.status == 0">
<el-button link type="primary" @click="handleEdit(scope.row)">修改</el-button>
<el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
<template v-else-if="scope.row.status == 1">
<el-button link type="warning" @click="handleWithdraw(scope.row)">撤回</el-button>
</template>
<el-button link type="primary" @click="handleViewDetail(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 详情弹窗 -->
<el-dialog
v-model="detailDialogVisible"
title="检验申请详情"
width="800px"
destroy-on-close
top="5vh"
:close-on-click-modal="false"
>
<div v-if="currentDetail" class="applicationShow-container">
<div class="applicationShow-container-content">
<el-descriptions title="基本信息" :column="2">
<el-descriptions-item label="患者姓名">{{
currentDetail.patientName || '-'
}}</el-descriptions-item>
<el-descriptions-item label="申请单名称">{{
currentDetail.name || '-'
}}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{
currentDetail.createTime || '-'
}}</el-descriptions-item>
<el-descriptions-item label="申请单号">{{
currentDetail.prescriptionNo || '-'
}}</el-descriptions-item>
<el-descriptions-item label="申请者">{{
currentDetail.requesterId_dictText || '-'
}}</el-descriptions-item>
<el-descriptions-item label="就诊ID">{{
currentDetail.encounterId || '-'
}}</el-descriptions-item>
<el-descriptions-item label="申请单ID">{{
currentDetail.requestFormId || '-'
}}</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="descJsonData && hasMatchedFields" class="applicationShow-container-content">
<el-descriptions title="申请单描述" :column="2">
<template v-for="(value, key) in descJsonData" :key="key">
<el-descriptions-item v-if="isFieldMatched(key)" :label="getFieldLabel(key)">
{{ value || '-' }}
</el-descriptions-item>
</template>
</el-descriptions>
</div>
<div
v-if="currentDetail.requestFormDetailList && currentDetail.requestFormDetailList.length"
class="applicationShow-container-table"
>
<el-table :data="currentDetail.requestFormDetailList" border>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="adviceName" label="医嘱名称" />
<el-table-column prop="quantity" label="数量" width="80" align="center" />
<el-table-column prop="unitCode_dictText" label="单位" width="100" />
<el-table-column prop="totalPrice" label="总价" width="100" align="right" />
</el-table>
</div>
</div>
<template #footer>
<el-button @click="detailDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {computed, getCurrentInstance, ref, watch} from 'vue';
import {Refresh, Search} from '@element-plus/icons-vue';
import {patientInfo} from '../../store/patient.js';
import {getInspection, deleteRequestForm, withdrawRequestForm} from './api';
import {getDepartmentList} from '@/api/public.js';
const { proxy } = getCurrentInstance();
const tableData = ref([]);
const loading = ref(false);
const detailDialogVisible = ref(false);
const currentDetail = ref(null);
const descJsonData = ref(null);
const orgOptions = ref([]);
// 筛选表单数据
const filterForm = ref({
dateRange: [], // [startDate, endDate]
status: '', // 单据状态
keyword: '', // 关键字搜索
});
const fetchData = async () => {
if (!patientInfo.value?.encounterId) {
tableData.value = [];
loading.value = false;
return;
}
loading.value = true;
try {
// 构建查询参数
const params = { encounterId: patientInfo.value.encounterId };
// 添加日期范围筛选
if (filterForm.value.dateRange && filterForm.value.dateRange.length === 2) {
params.startDate = filterForm.value.dateRange[0];
params.endDate = filterForm.value.dateRange[1];
}
// 添加状态筛选
if (filterForm.value.status !== '' && filterForm.value.status !== undefined) {
params.status = filterForm.value.status;
}
// 添加关键字搜索
if (filterForm.value.keyword && filterForm.value.keyword.trim()) {
params.keyword = filterForm.value.keyword.trim();
}
const res = await getInspection(params);
if (res.code === 200 && res.data) {
const raw = res.data?.records || res.data;
const list = Array.isArray(raw) ? raw : [raw];
tableData.value = list.filter(Boolean);
} else {
tableData.value = [];
}
} catch (e) {
proxy.$modal?.msgError?.(e.message || '查询检验申请失败');
tableData.value = [];
} finally {
loading.value = false;
}
};
const handleRefresh = async () => {
if (loading.value || !patientInfo.value?.encounterId) return;
await fetchData();
};
/**
* 查询按钮处理
*/
const handleSearch = async () => {
if (!patientInfo.value?.encounterId) {
proxy.$modal?.msgWarning?.('请先选择患者');
return;
}
await fetchData();
};
/**
* 重置按钮处理
*/
const handleReset = () => {
// 重置筛选条件为默认值
filterForm.value.dateRange = [];
filterForm.value.status = '';
filterForm.value.keyword = '';
// 重新加载数据
fetchData();
};
const labelMap = {
categoryType: '项目类别',
targetDepartment: '发往科室',
symptom: '症状',
sign: '体征',
clinicalDiagnosis: '临床诊断',
otherDiagnosis: '其他诊断',
relatedResult: '相关结果',
attention: '注意事项',
applicationType: '申请类型',
specimenName: '标本类型',
executeTime: '执行时间',
};
/**
* 解析单据状态
* @param {string|number} status - 状态码
* @returns {string} 状态文本
*/
const parseBillStatus = (status) => {
const statusMap = {
'0': '待签发',
'1': '已签发',
'4': '报告已出',
'5': '已作废',
};
return statusMap[String(status)] || '-';
};
/**
* 解析申请类型(优先级代码)
* @param {string} descJson - JSON字符串
* @returns {string} 申请类型文本
*/
const parsePriorityCode = (descJson) => {
if (!descJson) return '-';
try {
const obj = JSON.parse(descJson);
// applicationType: 0-普通, 1-急诊
return obj.applicationType === 1 ? '急' : '普通';
} catch (e) {
console.error('解析 descJson 失败:', e);
return '-';
}
};
/**
* 解析标本类型
* @param {string} descJson - JSON字符串
* @returns {string} 标本类型名称
*/
const parseSpecimenType = (descJson) => {
if (!descJson) return '-';
try {
const obj = JSON.parse(descJson);
// specimenName 或 sampleType 字段
return obj.specimenName || obj.sampleType || '-';
} catch (e) {
console.error('解析 descJson 失败:', e);
return '-';
}
};
/**
* 根据申请单详情构建申请单名称
* 单一项目:显示项目名称+数量
* 多个项目:显示首个项目名称+数量+"等X项"
*/
const buildApplicationName = (row) => {
const details = row.requestFormDetailList;
if (!details || details.length === 0) {
return row.name || '-';
}
if (details.length === 1) {
const item = details[0];
return `${item.adviceName}${item.quantity || ''}`;
}
const first = details[0];
return `${first.adviceName}${first.quantity || ''}${details.length}`;
};
const isFieldMatched = (key) => {
return key in labelMap;
};
const getFieldLabel = (key) => {
return labelMap[key] || key;
};
const hasMatchedFields = computed(() => {
if (!descJsonData.value) return false;
return Object.keys(descJsonData.value).some((key) => isFieldMatched(key));
});
/** 查询科室 */
const getLocationInfo = async () => {
const res = await getDepartmentList();
orgOptions.value = res.data || [];
};
const recursionFun = (targetDepartment) => {
if (!targetDepartment) return '';
let name = '';
for (let index = 0; index < orgOptions.value.length; index++) {
const obj = orgOptions.value[index];
if (obj.id == targetDepartment) {
name = obj.name;
break;
}
const subObjArray = obj['children'];
if (subObjArray && subObjArray.length > 0) {
for (let i = 0; i < subObjArray.length; i++) {
const item = subObjArray[i];
if (item.id == targetDepartment) {
name = item.name;
break;
}
}
}
if (name) break;
}
return name;
};
const handleViewDetail = async (row) => {
// 确保科室数据已加载,以便将 ID 解析为名称
if (!orgOptions.value || orgOptions.value.length === 0) {
await getLocationInfo();
}
currentDetail.value = row;
// 解析 descJson
if (row.descJson) {
try {
const obj = JSON.parse(row.descJson);
obj.targetDepartment = recursionFun(obj.targetDepartment);
// 转换申请类型编码为可读文本
if (obj.applicationType === 0) obj.applicationType = '普通';
else if (obj.applicationType === 1) obj.applicationType = '急诊';
descJsonData.value = obj;
} catch (e) {
console.error('解析 descJson 失败:', e);
descJsonData.value = null;
}
} else {
descJsonData.value = null;
}
detailDialogVisible.value = true;
};
/**
* 修改检验申请单(待签发状态)
*/
const handleEdit = (row) => {
// 复用详情查看逻辑,后续可扩展为打开编辑弹窗
handleViewDetail(row);
proxy.$modal?.msgInfo?.('修改功能待接入,请通过详情弹窗查看后重新开立');
};
/**
* 删除检验申请单(仅待签发状态可删除)
*/
const handleDelete = async (row) => {
try {
await proxy.$modal?.confirm?.(`确定要删除申请单 "${row.prescriptionNo}" 吗?此操作不可恢复。`);
} catch {
return; // 用户取消
}
try {
const res = await deleteRequestForm({ prescriptionNo: row.prescriptionNo });
if (res?.code === 200) {
proxy.$modal?.msgSuccess?.('删除成功');
await fetchData();
} else {
proxy.$modal?.msgError?.(res?.msg || '删除失败');
}
} catch (e) {
proxy.$modal?.msgError?.(e.message || '删除异常');
}
};
/**
* 撤回检验申请单(已签发状态撤回至待签发)
*/
const handleWithdraw = async (row) => {
try {
await proxy.$modal?.confirm?.(`确定要撤回申请单 "${row.prescriptionNo}" 吗?撤回后将恢复为待签发状态。`);
} catch {
return; // 用户取消
}
try {
const res = await withdrawRequestForm({ prescriptionNo: row.prescriptionNo });
if (res?.code === 200) {
proxy.$modal?.msgSuccess?.('撤回成功');
await fetchData();
} else {
proxy.$modal?.msgError?.(res?.msg || '撤回失败');
}
} catch (e) {
proxy.$modal?.msgError?.(e.message || '撤回异常');
}
};
watch(
() => patientInfo.value?.encounterId,
async (val) => {
if (val) {
// 设置默认日期范围为近7天
const today = new Date();
const sevenDaysAgo = new Date(today);
sevenDaysAgo.setDate(today.getDate() - 6); // 包含今天共7天
// 格式化为 YYYY-MM-DD
const formatDate = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
filterForm.value.dateRange = [
formatDate(sevenDaysAgo),
formatDate(today)
];
await Promise.all([fetchData(), getLocationInfo()]);
} else {
tableData.value = [];
// 重置筛选条件
filterForm.value.dateRange = [];
filterForm.value.status = '';
filterForm.value.keyword = '';
}
},
{ immediate: true }
);
defineExpose({
refresh: fetchData,
});
</script>
<style scoped lang="scss">
.report-container {
display: flex;
flex-direction: column;
gap: 12px;
padding: 8px 0;
height: 100%;
}
.report-section {
background: #fff;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.report-title {
font-weight: 600;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8px;
}
.filter-form {
padding: 12px 8px;
border-bottom: 1px solid #ebeef5;
background-color: #fafafa;
}
.filter-form-content {
:deep(.el-form-item) {
margin-bottom: 0;
margin-right: 16px;
}
:deep(.el-form-item__label) {
font-weight: 500;
}
}
.report-table-wrapper {
flex: 1;
min-height: 0;
overflow: auto;
padding: 0 8px;
}
.empty-data {
padding: 40px 0;
display: flex;
justify-content: center;
align-items: center;
}
.report-refresh-icon {
cursor: pointer;
color: #909399;
transition: color 0.2s;
font-size: 18px;
}
.report-refresh-icon:hover {
color: #409eff;
}
.report-refresh-icon.is-loading {
animation: rotating 2s linear infinite;
}
@keyframes rotating {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
:deep(.el-dialog__body) {
padding-top: 0 !important;
}
.applicationShow-container {
display: flex;
flex-direction: column;
max-height: 70vh;
width: 100%;
overflow-y: auto;
.applicationShow-container-content {
flex-shrink: 0;
margin-bottom: 0px;
}
.applicationShow-container-table {
flex-shrink: 0;
max-height: 300px;
overflow: auto;
}
}
</style>