Files
his/healthlink-his-ui/src/views/inpatientDoctor/home/components/applicationShow/surgeryApplication.vue
wangjian963 cfb1ea1b3c fix(手术申请): 修复手术部位未保存到cli_surgery表及详情展示为编码的问题
- 后端:保存手术申请单时,从descJson解析surgerySite字段,写入
  cli_surgery.body_site和wor_service_request.content_json,解决
  手术部位数据未持久化到手术主表的问题
- 前端:手术申请详情弹窗加载字典数据(手术等级、麻醉方式、手术
  部位、切口类别、手术性质),将descJson中的字典编码翻译为中文
  标签展示,解决详情中显示原始编码(如"1")而非实际名称的问题
2026-06-05 15:32:21 +08:00

890 lines
24 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="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="6"
/>
<el-option
label="已作废"
value="10"
/>
</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">
<vxe-table
v-loading="loading"
:data="tableData"
border
size="small"
height="100%"
style="width: 100%"
>
<vxe-column
type="seq"
title="序号"
width="60"
align="center"
/>
<vxe-column
title="手术单号"
min-width="160"
align="center"
>
<template #default="scope">
<el-link
type="primary"
@click="handleViewDetail(scope.row)"
>
{{ scope.row.prescriptionNo || '-' }}
</el-link>
</template>
</vxe-column>
<vxe-column
field="patientName"
title="患者姓名"
min-width="100"
/>
<vxe-column
field="name"
title="申请单名称"
min-width="140"
/>
<vxe-column
field="createTime"
title="创建时间"
min-width="160"
/>
<vxe-column
field="requesterId_dictText"
title="申请者"
min-width="100"
/>
<vxe-column
title="状态"
min-width="100"
align="center"
>
<template #default="scope">
<el-tag
:type="getStatusType(scope.row.status)"
size="small"
>
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</vxe-column>
<vxe-column
title="操作"
min-width="220"
align="center"
fixed="right"
>
<template #default="scope">
<!-- 待签发编辑 + 详情 + 删除 -->
<template v-if="canManageRow(scope.row) && isPendingStatus(scope.row)">
<el-button
link
type="primary"
@click="handleEdit(scope.row)"
>
编辑
</el-button>
<el-button
link
type="primary"
@click="handleViewDetail(scope.row)"
>
详情
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row)"
>
删除
</el-button>
</template>
<!-- 已签发撤回 + 详情 -->
<template v-else-if="canManageRow(scope.row) && isWithdrawableStatus(scope.row)">
<el-button
link
type="warning"
@click="handleWithdraw(scope.row)"
>
撤回
</el-button>
<el-button
link
type="primary"
@click="handleViewDetail(scope.row)"
>
详情
</el-button>
</template>
<!-- 已校对/已执行/已安排/已完成详情 + 打印 -->
<template v-else-if="isPrintableStatus(scope.row)">
<el-button
link
type="primary"
@click="handleViewDetail(scope.row)"
>
详情
</el-button>
<el-button
link
type="success"
@click="handlePrint(scope.row)"
>
打印
</el-button>
</template>
<!-- 已作废/其他状态仅详情 -->
<template v-else>
<el-button
link
type="primary"
@click="handleViewDetail(scope.row)"
>
详情
</el-button>
</template>
</template>
</vxe-column>
</vxe-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)"
>
{{ getFieldValue(key, value) }}
</el-descriptions-item>
</template>
</el-descriptions>
</div>
<div
v-if="currentDetail.requestFormDetailList && currentDetail.requestFormDetailList.length"
class="applicationShow-container-table"
>
<vxe-table
:data="currentDetail.requestFormDetailList"
border
>
<vxe-column
type="seq"
title="序号"
width="60"
align="center"
/>
<vxe-column
field="adviceName"
title="医嘱名称"
/>
<vxe-column
field="quantity"
title="数量"
width="80"
align="center"
/>
<vxe-column
field="unitCode_dictText"
title="单位"
width="100"
/>
<vxe-column
field="totalPrice"
title="总价"
width="100"
align="right"
/>
</vxe-table>
</div>
</div>
<template #footer>
<el-button
icon="Close"
@click="detailDialogVisible = false"
>
关闭
</el-button>
</template>
</el-dialog>
<!-- 编辑弹窗 -->
<el-dialog
v-model="editDialogVisible"
title="编辑手术申请单"
width="1200px"
destroy-on-close
:close-on-click-modal="false"
@closed="editRowData = null"
>
<SurgeryForm
ref="editFormRef"
:edit-data="editRowData"
@submit-ok="handleEditSubmitOk"
/>
<template #footer>
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button
type="primary"
@click="submitEditForm"
>
确认修改
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {computed, getCurrentInstance, nextTick, onMounted, ref, watch} from 'vue';
import {Refresh, Search} from '@element-plus/icons-vue';
import {patientInfo} from '../../store/patient.js';
import {getSurgery, deleteRequestForm, withdrawRequestForm} from './api';
import {getDepartmentList} from '@/api/public.js';
import {getDicts} from '@/api/system/dict/data';
import SurgeryForm from '../order/applicationForm/surgery.vue';
import useUserStore from '@/store/modules/user';
import auth from '@/plugins/auth';
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
const tableData = ref([]);
const loading = ref(false);
const detailDialogVisible = ref(false);
const currentDetail = ref(null);
const descJsonData = ref(null);
const orgOptions = ref([]);
const editDialogVisible = ref(false);
const editRowData = ref(null);
const editFormRef = ref(null);
// 字典选项(用于详情展示时将编码转为中文标签)
const surgeryLevelDictMap = ref({});
const anesthesiaTypeDictMap = ref({});
const surgerySiteDictMap = ref({});
const incisionLevelDictMap = ref({});
const surgeryNatureDictMap = ref({});
/** 字段 → 字典映射表getFieldValue 根据此表将字典编码翻译为标签 */
const dictFieldMap = {
surgeryLevel: surgeryLevelDictMap,
anesthesiaType: anesthesiaTypeDictMap,
surgerySite: surgerySiteDictMap,
incisionLevel: incisionLevelDictMap,
surgeryNature: surgeryNatureDictMap,
};
/** 加载字典选项 */
const loadDictOptions = async () => {
try {
const res = await Promise.all([
getDicts('surgery_level'),
getDicts('anesthesia_type'),
getDicts('surgical_site'),
getDicts('incision_level'),
getDicts('surgery_type'),
]);
const toMap = (arr) => {
const m = {};
(arr || []).forEach(item => { m[item.dictValue] = item.dictLabel; });
return m;
};
surgeryLevelDictMap.value = toMap(res[0]?.data);
anesthesiaTypeDictMap.value = toMap(res[1]?.data);
surgerySiteDictMap.value = toMap(res[2]?.data);
incisionLevelDictMap.value = toMap(res[3]?.data);
surgeryNatureDictMap.value = toMap(res[4]?.data);
} catch (e) {
console.error('加载手术字典数据失败:', e);
}
};
onMounted(() => {
loadDictOptions();
});
// 获取默认日期范围近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 isPendingStatus = (row) => row.status === 1;
/** 已签发(可撤回) */
const isWithdrawableStatus = (row) => row.status === 2;
/** 已校对/已执行/已安排/已完成(可打印) */
const isPrintableStatus = (row) => [3, 4, 5, 6].includes(row.status);
/** 是否可管理该申请单:申请者本人或管理员 */
const canManageRow = (row) => {
if (auth.hasRole('admin')) return true;
const currentPractitionerId = userStore.practitionerId;
const requesterId = row?.requesterId;
if (!currentPractitionerId || !requesterId) return false;
return String(currentPractitionerId) === String(requesterId);
};
/**
* 编辑手术申请单(待签发状态)
*/
const handleEdit = async (row) => {
editRowData.value = row;
editDialogVisible.value = true;
await nextTick();
editFormRef.value?.getLocationInfo?.();
editFormRef.value?.getDiagnosisList?.();
editFormRef.value?.loadDoctorOptions?.();
if (row.requestFormDetailList?.length > 0) {
editFormRef.value?.fillForm?.(
JSON.parse(row.descJson || '{}'),
row.requestFormDetailList,
row.requestFormId
);
}
};
const handleEditSubmitOk = async () => {
editDialogVisible.value = false;
editRowData.value = null;
proxy.$modal?.msgSuccess?.('修改成功');
await fetchData();
};
const submitEditForm = () => {
if (editFormRef.value?.submit) {
editFormRef.value.submit();
}
};
/**
* 删除手术申请单(仅待签发状态可删除)
*/
const handleDelete = async (row) => {
try {
await proxy.$modal?.confirm?.('确认删除该笔手术申请单吗?删除后数据将无法恢复。');
} catch { return; }
try {
const res = await deleteRequestForm({ requestFormId: row.requestFormId });
if (res?.code === 200) {
proxy.$modal?.msgSuccess?.('删除成功');
await fetchData();
} else {
proxy.$modal?.msgError?.(res?.msg || '删除失败');
}
} catch { /* 响应拦截器已处理错误提示 */ }
};
/**
* 撤回手术申请单(已签发状态可撤回)
*/
const handleWithdraw = async (row) => {
try {
await proxy.$modal?.confirm?.(
'确认撤回该手术申请吗?撤回后将恢复为待签发状态,护士站医嘱校对将同步更新。'
);
} catch { return; }
try {
const res = await withdrawRequestForm({ requestFormId: row.requestFormId });
if (res?.code === 200) {
proxy.$modal?.msgSuccess?.('撤回成功');
await fetchData();
} else {
proxy.$modal?.msgError?.(res?.msg || '撤回失败');
}
} catch { /* 响应拦截器已处理错误提示 */ }
};
/**
* 打印手术申请单:打开详情弹窗后触发浏览器打印
*/
const handlePrint = async (row) => {
await handleViewDetail(row);
nextTick(() => {
window.print();
});
};
/** 手术申请单状态映射 (与后端 SurgeryAppStatusEnum 对齐) */
const statusMap = {
1: { text: '待签发', type: 'info' },
2: { text: '已签发', type: 'primary' },
3: { text: '已校对', type: 'success' },
4: { text: '已执行', type: 'warning' },
5: { text: '已安排', type: 'warning' },
6: { text: '已完成', type: 'success' },
10: { text: '已作废', type: 'danger' },
};
const getStatusText = (status) => {
return statusMap[status]?.text || '未知';
};
const getStatusType = (status) => {
return statusMap[status]?.type || 'info';
};
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 getFieldValue = (key, value) => {
// 主刀医生/助手优先显示姓名兜底显示ID
if (key === 'mainSurgeonId' && descJsonData.value?.mainSurgeonName) {
return descJsonData.value.mainSurgeonName;
}
if (key === 'assistant1Id' && descJsonData.value?.assistant1Name) {
return descJsonData.value.assistant1Name;
}
if (key === 'assistant2Id' && descJsonData.value?.assistant2Name) {
return descJsonData.value.assistant2Name;
}
// 字典字段:将编码翻译为中文标签(如 surgerySite="1" → "头部"
if (dictFieldMap[key]) {
const dictMap = dictFieldMap[key].value;
if (dictMap && dictMap[value] !== undefined) {
return dictMap[value];
}
}
return value || '-';
};
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: #64748B;
transition: color 0.2s;
font-size: 18px;
}
.report-refresh-icon:hover {
color: #3B82F6;
}
.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>