Files
his/openhis-ui-vue3/src/views/inpatientDoctor/home/components/applicationShow/surgeryApplication.vue
zhaoyun 137f3109a7 fix(#581): 请修复 Bug #581:[一般] 【住院医生站-临床医嘱-手术】手术申请单缺失多项核心业务字段与强拦截逻辑,导致医疗安全制度无法落地且阻断手术室排班闭环
根因:
- Bug #请修复 Bug #581 存在的问题

修复:
- 变更摘要
- ### 修改文件
- 1. `src/views/inpatientDoctor/home/components/order/applicationForm/surgery.vue`**
- 在"发往科室"字段之后,依次新增了以下 9 个业务字段:
- | 字段 | 控件类型 | 必填 | 数据来源 |
- |---|---|---|---|
- | 手术等级 | `el-select` 下拉 |  | 字典 `surgery_level` |
- | 麻醉方式 | `el-select` 下拉 |  | 字典 `anesthesia_type` |
- | 手术部位 | `el-select` 下拉 |  | 字典 `surgery_site` |
- | 切口类别 | `el-select` 下拉 |  | 字典 `incision_level` |
- | 手术性质 | `el-select` 下拉 |  | 字典 `surgery_type` |
- | 主刀医生 | `el-select` 可搜索 |  | `listUser` API,默认当前登录医生 |
- | 第一助手 | `el-select` 可搜索 |  | `listUser` API |
- | 第二助手 | `el-select` 可搜索 |  | `listUser` API |
- | 预定手术时间 | `el-date-picker` datetime |  | 无默认值 |
- 新增逻辑:
- `loadDictOptions()`** — 并行加载 5 个字典选项
- `loadDoctorOptions()`** — 加载医生列表,自动设当前登录用户为主刀医生默认值
- `submit()` 新增强拦截校验** — 手术等级、麻醉方式、手术部位、主刀医生、预定手术时间为必填,为空时阻断提交并提示
- 2. `src/views/inpatientDoctor/home/components/applicationShow/surgeryApplication.vue`**
- `labelMap` 新增 9 条标签映射,确保详情弹窗能正确显示新字段的中文标签。
- ### 全链路完整性
- 录入  前端弹窗增加输入控件
- 保存  通过 `descJson: JSON.stringify(form)` 序列化,后端无需改动
- 查询  详情展示组件新增 labelMap 映射
- 修改 ⏸ 申请单编辑功能不在本轮范围(后续迭代可复用 submit 逻辑)
- 删除  不影响
- 关联  门诊手术申请走独立 API,不共享 descJson,无需修改
- ### 验证
- `npm run lint` —  通过,无错误
2026-05-29 02:42:13 +08:00

603 lines
15 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.

<!--
* @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="2"
/>
<el-option
label="已执行"
value="3"
/>
<el-option
label="已安排"
value="4"
/>
<el-option
label="已完成"
value="5"
/>
<el-option
label="已作废"
value="7"
/>
</el-select>
</el-form-item>
<el-form-item label="关键字">
<el-input
v-model="filterForm.keyword"
placeholder="请输入手术单号/名称"
clearable
style="width: 220px"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:loading="loading"
@click="handleSearch"
>
<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%"
>
<el-table-column
type="index"
label="序号"
width="60"
align="center"
/>
<el-table-column
label="手术单号"
width="160"
align="center"
>
<template #default="scope">
<el-link
type="primary"
@click="handleViewDetail(scope.row)"
>
{{ scope.row.prescriptionNo || '-' }}
</el-link>
</template>
</el-table-column>
<el-table-column
prop="patientName"
label="患者姓名"
width="120"
/>
<el-table-column
prop="name"
label="申请单名称"
width="140"
/>
<el-table-column
prop="createTime"
label="创建时间"
width="160"
/>
<el-table-column
prop="requesterId_dictText"
label="申请者"
width="120"
/>
<el-table-column
label="操作"
align="center"
fixed="right"
>
<template #default="scope">
<el-button
link
type="primary"
icon="View"
@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
icon="Close"
@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 {getSurgery} 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([]);
// 获取默认日期范围近7天
const getDefaultDateRange = () => {
const now = new Date();
const endDate = now.toISOString().split('T')[0];
const startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
return [startDate, endDate];
};
// 筛选表单数据
const filterForm = ref({
dateRange: getDefaultDateRange(), // 默认近一周
status: '', // 申请状态
keyword: '', // 关键字搜索
});
/**
* 查询按钮处理
*/
const handleSearch = async () => {
if (!patientInfo.value?.encounterId) {
proxy.$modal?.msgWarning?.('请先选择患者');
return;
}
await fetchData();
};
/**
* 重置按钮处理
*/
const handleReset = () => {
filterForm.value.dateRange = getDefaultDateRange();
filterForm.value.status = '';
filterForm.value.keyword = '';
fetchData();
};
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 getSurgery(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 labelMap = {
categoryType: '项目类别',
targetDepartment: '发往科室',
surgeryLevel: '手术等级',
anesthesiaType: '麻醉方式',
surgerySite: '手术部位',
incisionLevel: '切口类别',
surgeryNature: '手术性质',
mainSurgeonId: '主刀医生',
assistant1Id: '第一助手',
assistant2Id: '第二助手',
plannedTime: '预定手术时间',
symptom: '症状',
sign: '体征',
clinicalDiagnosis: '临床诊断',
otherDiagnosis: '其他诊断',
relatedResult: '相关结果',
attention: '注意事项',
};
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 || !orgOptions.value || orgOptions.value.length === 0) {
return '';
}
let name = '';
// 统一处理:扁平列表和树形结构都适用
const findInList = (list) => {
for (const node of list) {
if (String(node.id) === String(targetDepartment)) {
name = node.name;
return true;
}
// 树形结构:递归查找 children
if (node.children && node.children.length > 0) {
if (findInList(node.children)) {
return true;
}
}
}
return false;
};
findInList(orgOptions.value);
return name;
};
const handleViewDetail = async (row) => {
// 确保科室数据已加载,以便将 ID 解析为名称
if (!orgOptions.value || orgOptions.value.length === 0) {
await getLocationInfo();
}
currentDetail.value = row;
// 解析 descJson
if (row.descJson) {
try {
// descJsonData.value = JSON.parse(row.descJson);
const obj = JSON.parse(row.descJson);
obj.targetDepartment = recursionFun(obj.targetDepartment);
descJsonData.value = obj;
} catch (e) {
console.error('解析 descJson 失败:', e);
descJsonData.value = null;
}
} else {
descJsonData.value = null;
}
detailDialogVisible.value = true;
};
watch(
() => patientInfo.value?.encounterId,
async (val) => {
if (val) {
await Promise.all([fetchData(), getLocationInfo()]);
} else {
tableData.value = [];
}
},
{ 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: 0 8px;
margin-bottom: 8px;
}
.filter-form-content {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0;
}
:deep(.filter-form-content .el-form-item) {
margin-bottom: 0;
margin-right: 16px;
}
.report-table-wrapper {
flex: 1;
min-height: 0;
overflow: auto;
padding: 0 8px;
}
.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>