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` —  通过,无错误
This commit is contained in:
2026-05-29 02:42:13 +08:00
parent 6baac543c9
commit b36bf4e1be
2 changed files with 351 additions and 51 deletions

View File

@@ -400,6 +400,15 @@ const handleRefresh = async () => {
const labelMap = {
categoryType: '项目类别',
targetDepartment: '发往科室',
surgeryLevel: '手术等级',
anesthesiaType: '麻醉方式',
surgerySite: '手术部位',
incisionLevel: '切口类别',
surgeryNature: '手术性质',
mainSurgeonId: '主刀医生',
assistant1Id: '第一助手',
assistant2Id: '第二助手',
plannedTime: '预定手术时间',
symptom: '症状',
sign: '体征',
clinicalDiagnosis: '临床诊断',

View File

@@ -29,18 +29,12 @@
class="demo-ruleForm"
>
<el-row :gutter="20">
<!-- <el-col :span="12">
<el-form-item label="项目类别" prop="categoryType" style="width: 100%">
<el-input v-model="form.categoryType" autocomplete="off" />
</el-form-item>
</el-col> -->
<el-col :span="12">
<el-form-item
label="发往科室"
prop="targetDepartment"
style="width: 100%"
>
<!-- <el-input v-model="form.targetDepartment" autocomplete="off" /> -->
<el-tree-select
v-model="form.targetDepartment"
clearable
@@ -58,6 +52,201 @@
/>
</el-form-item>
</el-col>
<!-- 手术等级 -->
<el-col :span="12">
<el-form-item
label="手术等级"
prop="surgeryLevel"
style="width: 100%"
>
<el-select
v-model="form.surgeryLevel"
placeholder="请选择手术等级"
clearable
style="width: 100%"
>
<el-option
v-for="item in surgeryLevelOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<!-- 麻醉方式 -->
<el-col :span="12">
<el-form-item
label="麻醉方式"
prop="anesthesiaType"
style="width: 100%"
>
<el-select
v-model="form.anesthesiaType"
placeholder="请选择麻醉方式"
clearable
style="width: 100%"
>
<el-option
v-for="item in anesthesiaTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<!-- 手术部位 -->
<el-col :span="12">
<el-form-item
label="手术部位"
prop="surgerySite"
style="width: 100%"
>
<el-select
v-model="form.surgerySite"
placeholder="请选择手术部位"
clearable
style="width: 100%"
>
<el-option
v-for="item in surgerySiteOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<!-- 切口类别 -->
<el-col :span="12">
<el-form-item
label="切口类别"
prop="incisionLevel"
style="width: 100%"
>
<el-select
v-model="form.incisionLevel"
placeholder="请选择切口类别"
clearable
style="width: 100%"
>
<el-option
v-for="item in incisionLevelOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<!-- 手术性质 -->
<el-col :span="12">
<el-form-item
label="手术性质"
prop="surgeryNature"
style="width: 100%"
>
<el-select
v-model="form.surgeryNature"
placeholder="请选择手术性质"
clearable
style="width: 100%"
>
<el-option
v-for="item in surgeryNatureOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<!-- 主刀医生 -->
<el-col :span="12">
<el-form-item
label="主刀医生"
prop="mainSurgeonId"
style="width: 100%"
>
<el-select
v-model="form.mainSurgeonId"
filterable
clearable
placeholder="请选择主刀医生(支持搜索)"
style="width: 100%"
>
<el-option
v-for="item in doctorOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<!-- 第一助手 -->
<el-col :span="12">
<el-form-item
label="第一助手"
prop="assistant1Id"
style="width: 100%"
>
<el-select
v-model="form.assistant1Id"
filterable
clearable
placeholder="请选择第一助手(支持搜索)"
style="width: 100%"
>
<el-option
v-for="item in doctorOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<!-- 第二助手 -->
<el-col :span="12">
<el-form-item
label="第二助手"
prop="assistant2Id"
style="width: 100%"
>
<el-select
v-model="form.assistant2Id"
filterable
clearable
placeholder="请选择第二助手(支持搜索)"
style="width: 100%"
>
<el-option
v-for="item in doctorOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<!-- 预定手术时间 -->
<el-col :span="12">
<el-form-item
label="预定手术时间"
prop="plannedTime"
style="width: 100%"
>
<el-date-picker
v-model="form.plannedTime"
type="datetime"
placeholder="选择日期时间"
value-format="YYYY-MM-DD HH:mm"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="症状"
@@ -148,8 +337,12 @@ import {getDepartmentList} from '@/api/public.js';
import {getEncounterDiagnosis} from '../../api.js';
import {getSurgeryPage, saveSurgery} from './api';
import {ElMessage} from 'element-plus';
import {getDicts} from '@/api/system/dict/data';
import {listUser} from '@/api/system/user';
import useUserStore from '@/store/modules/user';
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
// 模块级缓存:避免每次打开弹窗都重新请求手术项目列表
let surgeryRecordsCache = null; // 原始 API 记录
let surgeryMappedCache = null; // 映射后的 el-transfer 数据
@@ -158,68 +351,71 @@ const dbTotal = ref(0); // 数据库中的手术项目总数
const checkedCount = computed(() => transferValue.value.length);
const leftPanelFormat = computed(() => ({
noChecked: ` 0/${dbTotal.value}`,
hasChecked: ` \${checked}/${dbTotal.value}`,
hasChecked: ` ${checked}/${dbTotal.value}`,
}));
// 递归查找树形科室节点
const findTreeItem = (list, id) => {
if (!list || list.length === 0) return null;
for (const item of list) {
if (item.id == id) return item;
if (item.children && item.children.length > 0) {
const found = findTreeItem(item.children, id);
if (!list || list.length === 0 || id == null || id === '') return null;
const strId = String(id);
for (const node of list) {
if (String(node.id) === strId) return node;
if (node.children?.length) {
const found = findTreeItem(node.children, id);
if (found) return found;
}
}
return null;
};
const emits = defineEmits(['submitOk']);
const props = defineProps({});
const state = reactive({});
const applicationListAll = ref();
const applicationList = ref([]);
const orgOptions = ref([]); // 科室选项
const loading = ref(false); // 加载状态
const mapToTransferItem = (item) => {
const price = item.price != null ? Number(item.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 flattenTree = (list) => {
if (!list) return [];
const result = [];
const walk = (nodes) => {
for (const node of nodes) {
result.push({ value: String(node.id), label: node.name });
if (node.children?.length) walk(node.children);
}
};
walk(list);
return result;
};
const getList = () => {
if (!patientInfo.value?.inHospitalOrgId) {
applicationList.value = [];
return;
const orgOptions = ref([]);
const loading = ref(false);
const applicationList = ref([]);
const applicationListAll = ref([]);
const allLoading = ref(false);
// 递归查找默认科室
const findTargetDepartment = (selectProjectIds) => {
if (!selectProjectIds || selectProjectIds.length === 0) return '';
let dep = '';
if (applicationListAll.value && applicationListAll.value.length > 0) {
const filtered = applicationListAll.value.filter((item) => {
return selectProjectIds.includes(item.adviceDefinitionId);
});
if (filtered.length > 0) {
// 判断所选项目的所属科室是否一致
const arr = filtered.map((item) => item.positionId).filter(Boolean);
if (new Set(arr).size === 1) {
dep = arr[0];
}
}
}
// 命中内存缓存时直接使用
if (surgeryMappedCache && surgeryMappedCache.length > 0) {
return dep;
};
const getList = async (key) => {
// 优先使用缓存(仅当没有搜索关键词时)
if (!key && surgeryRecordsCache && surgeryMappedCache) {
applicationList.value = surgeryMappedCache;
applicationListAll.value = surgeryRecordsCache;
loading.value = false;
dbTotal.value = surgeryRecordsCache.length;
return;
}
loadPage('');
};
/**
* 加载手术项目分页数据
* @param {string} key 搜索关键字(可选)
*/
const loadPage = (key) => {
const orgId = patientInfo.value.inHospitalOrgId;
loading.value = true;
getSurgeryPage({ organizationId: orgId, pageNo: 1, pageSize: 100, searchKey: key || '' })
getSurgeryPage({
pageSize: 1000,
keyword: key || undefined,
})
.then((res) => {
if (res.code !== 200 || !res.data?.records) {
applicationList.value = [];
dbTotal.value = 0;
loading.value = false;
return;
}
dbTotal.value = res.data.total || 0;
const records = res.data.records;
applicationListAll.value = records;
applicationList.value = records
@@ -251,10 +447,26 @@ const filterMethod = (query, item) => {
return label.includes(q) || key.includes(q);
};
const mapToTransferItem = (item) => ({
key: String(item.adviceDefinitionId),
label: `${item.adviceName} - ${item.unitCode || ''}`,
disabled: false,
});
const transferValue = ref([]);
const form = reactive({
// categoryType: '', // 项目类别
targetDepartment: '', // 发往科室
// === 新增手术业务字段 ===
surgeryLevel: '', // 手术等级
anesthesiaType: '', // 麻醉方式
surgerySite: '', // 手术部位
incisionLevel: '', // 切口类别
surgeryNature: '', // 手术性质
mainSurgeonId: '', // 主刀医生ID
assistant1Id: '', // 第一助手ID
assistant2Id: '', // 第二助手ID
plannedTime: '', // 预定手术时间
symptom: '', // 症状
sign: '', // 体征
clinicalDiagnosis: '', // 临床诊断
@@ -265,10 +477,73 @@ const form = reactive({
otherDiagnosisList: [], //其他断目录
});
const rules = reactive({});
// 字典选项
const surgeryLevelOptions = ref([]);
const anesthesiaTypeOptions = ref([]);
const surgerySiteOptions = ref([]);
const incisionLevelOptions = ref([]);
const surgeryNatureOptions = ref([]);
// 医生选项
const doctorOptions = ref([]);
onBeforeMount(() => {});
onMounted(() => {
getList();
loadDictOptions();
loadDoctorOptions();
});
/**
* 加载字典选项
*/
const loadDictOptions = async () => {
try {
const res = await Promise.all([
getDicts('surgery_level'),
getDicts('anesthesia_type'),
getDicts('surgery_site'),
getDicts('incision_level'),
getDicts('surgery_type'),
]);
surgeryLevelOptions.value = (res[0]?.data || []).map(p => ({ label: p.dictLabel, value: p.dictValue }));
anesthesiaTypeOptions.value = (res[1]?.data || []).map(p => ({ label: p.dictLabel, value: p.dictValue }));
surgerySiteOptions.value = (res[2]?.data || []).map(p => ({ label: p.dictLabel, value: p.dictValue }));
incisionLevelOptions.value = (res[3]?.data || []).map(p => ({ label: p.dictLabel, value: p.dictValue }));
surgeryNatureOptions.value = (res[4]?.data || []).map(p => ({ label: p.dictLabel, value: p.dictValue }));
} catch (e) {
console.error('加载手术字典数据失败:', e);
}
};
/**
* 加载医生选项列表
*/
const loadDoctorOptions = async () => {
try {
const res = await listUser({ pageNo: 1, pageSize: 1000 });
if (res.code === 200) {
const data = res.data?.records || res.rows || res.data || [];
if (Array.isArray(data)) {
doctorOptions.value = data.map(item => ({
id: item.practitionerId || item.id || item.userId,
name: item.nickName || item.name || item.userName,
}));
}
}
// 默认主刀医生设为当前登录用户
if (userStore.nickName) {
const currentUser = doctorOptions.value.find(
doc => doc.name === userStore.nickName
);
if (currentUser) {
form.mainSurgeonId = currentUser.id;
}
}
} catch (e) {
console.error('加载医生列表失败:', e);
}
};
/**
* type(1watch监听类型 2:点击保存类型)
* selectProjectIds(选中项目的id数组)
@@ -289,6 +564,22 @@ const submit = () => {
if (!form.targetDepartment) {
return proxy.$message.error('请选择发往科室');
}
// 新增必填校验
if (!form.surgeryLevel) {
return proxy.$message.error('请选择手术等级');
}
if (!form.anesthesiaType) {
return proxy.$message.error('请选择麻醉方式');
}
if (!form.surgerySite) {
return proxy.$message.error('请选择手术部位');
}
if (!form.mainSurgeonId) {
return proxy.$message.error('请选择主刀医生');
}
if (!form.plannedTime) {
return proxy.$message.error('请选择预定手术时间');
}
let applicationListAllFilter = applicationListAll.value.filter((item) => {
return transferValue.value.includes(item.adviceDefinitionId);
});