fix(ui): 修复住院医生站与护士站临床医嘱若干稳定性问题

医生站:
    - 修复类型切换后编辑表单残留、blur/click竞态致选中无效、批量保存缺patientId
    - 修复filterPrescriptionList.find过滤下展开失败、popover溢出、表格塌陷
    - 提取resolveCategoryCode/getAdviceTableRef消除重复, 优化adviceTableRef类型
    - 修复adviceBaseList keyField、选中残留、TS类型声明

  护士站:
    - 校对: 新增已执行状态判定+退回拦截, 修复状态标签颜色不一致
    - 执行: 修复长期医嘱dayTimes为空被静默丢弃
    - 双模块: 新增keep-alive重激活刷新+患者列表自动加载

  配置:
    - eslint.config.js 新增 @typescript-eslint/parser 支持Vue TS解析
This commit is contained in:
wangjian963
2026-06-17 11:06:45 +08:00
parent 1c68860541
commit bdb7d978fb
8 changed files with 211 additions and 81 deletions

View File

@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
import globals from "globals"; import globals from "globals";
import pluginVue from "eslint-plugin-vue"; import pluginVue from "eslint-plugin-vue";
import parserVue from "vue-eslint-parser"; import parserVue from "vue-eslint-parser";
import parserTs from "@typescript-eslint/parser";
import importPlugin, { createNodeResolver } from "eslint-plugin-import-x"; import importPlugin, { createNodeResolver } from "eslint-plugin-import-x";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -20,7 +21,7 @@ export default [
}, },
...pluginVue.configs["flat/recommended"], ...pluginVue.configs["flat/recommended"],
{ {
languageOptions: { languageOptions: {
globals: { globals: {
@@ -30,6 +31,9 @@ export default [
parser: parserVue, parser: parserVue,
ecmaVersion: "latest", ecmaVersion: "latest",
sourceType: "module", sourceType: "module",
parserOptions: {
parser: parserTs,
},
}, },
plugins: { plugins: {

View File

@@ -72,6 +72,7 @@
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.60.0", "@playwright/test": "^1.60.0",
"@types/node": "^25.0.1", "@types/node": "^25.0.1",
"@typescript-eslint/parser": "^8.61.1",
"@vitejs/plugin-vue": "^5.2.4", "@vitejs/plugin-vue": "^5.2.4",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"eslint": "^10.4.1", "eslint": "^10.4.1",

View File

@@ -8,7 +8,7 @@
:table-height="400" :table-height="400"
:max-height="400" :max-height="400"
:loading="loading" :loading="loading"
:row-config="{ keyField: 'patientId' }" :row-config="{ keyField: 'adviceDefinitionId' }"
@cell-click="handleRowClick" @cell-click="handleRowClick"
> >
<template #quantity="{ row }"> <template #quantity="{ row }">
@@ -26,15 +26,16 @@
import {computed, nextTick, ref} from 'vue'; import {computed, nextTick, ref} from 'vue';
import {throttle} from 'lodash-es'; import {throttle} from 'lodash-es';
import Table from '@/components/TableLayout/Table.vue'; import Table from '@/components/TableLayout/Table.vue';
// @ts-ignore: api.js 无类型声明
import {getAdviceBaseInfo} from './api'; import {getAdviceBaseInfo} from './api';
import type {TableColumn} from '@/components/types/TableLayout'; import {TableColumn} from '@/components/types/TableLayout';
interface Props { type Props = {
patientInfo: { patientInfo: {
inHospitalOrgId?: string; inHospitalOrgId?: string;
[key: string]: any; [key: string]: any;
}; };
} };
const props = defineProps<Props>(); const props = defineProps<Props>();
@@ -42,13 +43,23 @@ const emit = defineEmits<{
selectAdviceBase: [row: any]; selectAdviceBase: [row: any];
}>(); }>();
interface QueryParams {
pageSize: number;
pageNo: number;
adviceTypes: number[];
searchKey?: string;
organizationId?: string;
categoryCode?: string;
dischargeFlag?: number;
}
const total = ref<number>(0); const total = ref<number>(0);
const loading = ref<boolean>(false); const loading = ref<boolean>(false);
const adviceBaseRef = ref<InstanceType<typeof Table> | null>(null); const adviceBaseRef = ref<InstanceType<typeof Table> | null>(null);
const tableWrapper = ref<HTMLDivElement | null>(null); const tableWrapper = ref<HTMLDivElement | null>(null);
const currentIndex = ref<number>(0); const currentIndex = ref<number>(0);
const currentSelectRow = ref<any>({}); const currentSelectRow = ref<any>({});
const queryParams = ref({ const queryParams = ref<QueryParams>({
pageSize: 30, pageSize: 30,
pageNo: 1, pageNo: 1,
adviceTypes: [1, 2, 3, 6], adviceTypes: [1, 2, 3, 6],
@@ -86,6 +97,9 @@ const tableColumns = computed<TableColumn[]>(() => [
* @param searchKey 搜索关键词 * @param searchKey 搜索关键词
*/ */
function refresh(adviceType: any, categoryCode: string, searchKey: string) { function refresh(adviceType: any, categoryCode: string, searchKey: string) {
// 每次刷新时重置选中状态,防止上次选中的药品在新列表中残留
currentIndex.value = 0;
currentSelectRow.value = {};
// 有搜索词时跨类型搜索,避免用户输入"级护理"但因当前adviceType为药品而搜不到诊疗类护理项目 // 有搜索词时跨类型搜索,避免用户输入"级护理"但因当前adviceType为药品而搜不到诊疗类护理项目
if (searchKey) { if (searchKey) {
queryParams.value.adviceTypes = [1, 2, 3, 6]; queryParams.value.adviceTypes = [1, 2, 3, 6];
@@ -118,7 +132,7 @@ function getList() {
} }
getAdviceBaseInfo(queryParams.value) getAdviceBaseInfo(queryParams.value)
.then((res) => { .then((res: any) => {
const records = res.data?.records || []; const records = res.data?.records || [];
// 药品/耗材需要有库存才显示,诊疗/手术直接显示 // 药品/耗材需要有库存才显示,诊疗/手术直接显示
@@ -144,7 +158,7 @@ function getList() {
} }
}); });
}) })
.catch((err) => { .catch((err: any) => {
console.warn('医嘱基础信息加载失败:', err); console.warn('医嘱基础信息加载失败:', err);
adviceBaseList.value = []; adviceBaseList.value = [];
}) })

View File

@@ -174,6 +174,11 @@
:disabled="!isCategoryLoaded" :disabled="!isCategoryLoaded"
@change=" @change="
(value) => { (value) => {
// 切换类型时强制收起编辑区,防止残留字段(如药品→诊疗时 dose/rateCode 未清理)
// 注意:模板内联表达式中 ref 自动解包expandOrder 即数组,不要加 .value
if (expandOrder.length > 0) {
collapseAllExpanded();
}
filterPrescriptionList[scope.rowIndex].adviceName = undefined; filterPrescriptionList[scope.rowIndex].adviceName = undefined;
// 根据选中的医嘱类型,设置对应的 categoryCode // 根据选中的医嘱类型,设置对应的 categoryCode
const selectedItem = adviceTypeList.find(item => item.value === value); const selectedItem = adviceTypeList.find(item => item.value === value);
@@ -196,7 +201,7 @@
searchKey: adviceQueryParams.value?.searchKey || '', searchKey: adviceQueryParams.value?.searchKey || '',
}; };
// 直接调用子组件 refresh 方法,立即刷新列表(用 scope.rowIndex 找到当前行的子组件) // 直接调用子组件 refresh 方法,立即刷新列表(用 scope.rowIndex 找到当前行的子组件)
const tableRef = Array.isArray(adviceTableRef.value) ? adviceTableRef.value[scope.rowIndex] : adviceTableRef.value; const tableRef = getAdviceTableRef();
if (tableRef && tableRef.refresh) { if (tableRef && tableRef.refresh) {
tableRef.refresh(newAdviceType, newCategoryCode, ''); tableRef.refresh(newAdviceType, newCategoryCode, '');
} }
@@ -221,7 +226,7 @@
popper-class="order-advice-popper" popper-class="order-advice-popper"
:offset="0" :offset="0"
:visible="scope.row.showPopover" :visible="scope.row.showPopover"
:width="1200" :width="advicePopperWidth"
:teleported="true" :teleported="true"
:popper-options="advicePopperOptions" :popper-options="advicePopperOptions"
> >
@@ -246,7 +251,7 @@
if (['ArrowUp', 'ArrowDown', 'Enter'].includes(e.key)) { if (['ArrowUp', 'ArrowDown', 'Enter'].includes(e.key)) {
e.preventDefault(); e.preventDefault();
// 传递事件到弹窗容器 // 传递事件到弹窗容器
const tableRef = Array.isArray(adviceTableRef.value) ? adviceTableRef.value[scope.rowIndex] : adviceTableRef.value; const tableRef = getAdviceTableRef();
if (tableRef && tableRef.handleKeyDown) tableRef.handleKeyDown(e); if (tableRef && tableRef.handleKeyDown) tableRef.handleKeyDown(e);
} }
} }
@@ -461,7 +466,7 @@ import {
} from '../api'; } from '../api';
import adviceBaseList from '../adviceBaseList'; import adviceBaseList from '../adviceBaseList';
import {calculateQuantityByDays} from '@/utils/his'; import {calculateQuantityByDays} from '@/utils/his';
import {localPatientInfo as patientInfo} from '../../store/localPatient.js'; import {localPatientInfo as localPatient} from '../../store/localPatient.js';
import OrderGroupDrawer from '@/views/doctorstation/components/prescription/orderGroupDrawer.vue'; import OrderGroupDrawer from '@/views/doctorstation/components/prescription/orderGroupDrawer.vue';
import PrescriptionHistory from '@/views/doctorstation/components/prescription/prescriptionHistory.vue'; import PrescriptionHistory from '@/views/doctorstation/components/prescription/prescriptionHistory.vue';
import Decimal from 'decimal.js'; import Decimal from 'decimal.js';
@@ -492,7 +497,7 @@ const groupIndexList = ref([]);
const diagnosisList = ref([]); const diagnosisList = ref([]);
const nextId = ref(1); const nextId = ref(1);
const unitCodeList = ref([]); const unitCodeList = ref([]);
const adviceTableRef = ref([]); const adviceTableRef = ref(null);
const organization = ref([]); const organization = ref([]);
const conditionDefinitionId = ref(''); const conditionDefinitionId = ref('');
const encounterDiagnosisId = ref(''); const encounterDiagnosisId = ref('');
@@ -505,6 +510,11 @@ const popoverJustClosedByKey = ref(null);
// 医嘱检索下拉浮框对齐:跟踪表格水平滚动偏移与主体区域边界限制 // 医嘱检索下拉浮框对齐:跟踪表格水平滚动偏移与主体区域边界限制
const tableScrollLeft = ref(0); const tableScrollLeft = ref(0);
const mainBoundary = ref(null); const mainBoundary = ref(null);
const advicePopperWidth = computed(() => {
// 取主体区域宽度与 900px 中较小值,避免小屏幕溢出;下限 500px
const mainWidth = mainBoundary.value?.clientWidth || window.innerWidth - 300;
return Math.max(500, Math.min(mainWidth - 24, 900));
});
const advicePopperStyle = computed(() => ({ const advicePopperStyle = computed(() => ({
padding: '0', padding: '0',
marginLeft: `-${tableScrollLeft.value}px`, marginLeft: `-${tableScrollLeft.value}px`,
@@ -549,10 +559,10 @@ const unitMap = ref({
unit: 'unit', unit: 'unit',
}); });
const buttonDisabled = computed(() => { const buttonDisabled = computed(() => {
return !patientInfo.value; return !localPatient.value;
}); });
const isSaveDisabled = computed(() => { const isSaveDisabled = computed(() => {
return !patientInfo.value || prescriptionList.value.length === 0; return !localPatient.value || prescriptionList.value.length === 0;
}); });
const props = defineProps({ const props = defineProps({
patientInfo: { patientInfo: {
@@ -763,7 +773,7 @@ function getListInfo(addNewRow) {
const orgTreePromise = getOrgTree().then((res) => { const orgTreePromise = getOrgTree().then((res) => {
organization.value = res?.data?.records ?? res?.data ?? []; organization.value = res?.data?.records ?? res?.data ?? [];
}); });
getPrescriptionList(patientInfo.value.encounterId).then((res) => { getPrescriptionList(localPatient.value.encounterId).then((res) => {
// 等待科室树加载完成后再处理处方数据,确保 resolveOrgId 能正确匹配 // 等待科室树加载完成后再处理处方数据,确保 resolveOrgId 能正确匹配
orgTreePromise.then(() => { orgTreePromise.then(() => {
loading.value = false; loading.value = false;
@@ -830,10 +840,10 @@ function getListInfo(addNewRow) {
} }
}); });
}).catch(() => { loading.value = false; }); }).catch(() => { loading.value = false; });
getContract({ encounterId: patientInfo.value.encounterId }).then((res) => { getContract({ encounterId: localPatient.value.encounterId }).then((res) => {
contractList.value = res.data; contractList.value = res.data;
}); });
accountId.value = patientInfo.value.accountId; accountId.value = localPatient.value.accountId;
// 加载已配置的药品类别 // 加载已配置的药品类别
loadConfiguredCategories(); loadConfiguredCategories();
@@ -843,7 +853,7 @@ function getListInfo(addNewRow) {
* 加载已配置的药品类别 * 加载已配置的药品类别
*/ */
function loadConfiguredCategories() { function loadConfiguredCategories() {
const orgId = patientInfo.value?.inHospitalOrgId; const orgId = localPatient.value?.inHospitalOrgId;
if (!orgId) { if (!orgId) {
isCategoryLoaded.value = true; // 标记已加载即使没有科室ID isCategoryLoaded.value = true; // 标记已加载即使没有科室ID
return; return;
@@ -917,7 +927,7 @@ const filterPrescriptionList = computed(() => {
}); });
function getDiagnosisInfo() { function getDiagnosisInfo() {
getEncounterDiagnosis(patientInfo.value.encounterId).then((res) => { getEncounterDiagnosis(localPatient.value.encounterId).then((res) => {
diagnosisList.value = res.data; diagnosisList.value = res.data;
let diagnosisInfo = diagnosisList.value.filter((item) => { let diagnosisInfo = diagnosisList.value.filter((item) => {
return item.maindiseFlag == 1; return item.maindiseFlag == 1;
@@ -937,6 +947,7 @@ function getRowDisabled(row) {
/** /**
* 将行的 adviceType + categoryCode 映射为 el-select 的选中值 * 将行的 adviceType + categoryCode 映射为 el-select 的选中值
* 药品子分类使用复合值如 '1-2'adviceType-categoryCode诊疗/手术/全部使用原始值 * 药品子分类使用复合值如 '1-2'adviceType-categoryCode诊疗/手术/全部使用原始值
* 注意el-select 使用严格匹配,返回值类型需与 adviceTypeList option value 一致
*/ */
function getRowSelectValue(row) { function getRowSelectValue(row) {
if (row.adviceType == 1 && row.categoryCode) { if (row.adviceType == 1 && row.categoryCode) {
@@ -945,13 +956,17 @@ function getRowSelectValue(row) {
if (row.adviceType == 7) { if (row.adviceType == 7) {
return 7; return 7;
} }
// adviceType == 1 但没有 categoryCode 时无匹配项,返回 undefined 让 select 显示空
if (row.adviceType == 1) {
return undefined;
}
return row.adviceType; return row.adviceType;
} }
// 新增医嘱 // 新增医嘱
function handleAddPrescription() { function handleAddPrescription() {
// 校验是否选中患者 // 校验是否选中患者
if (!patientInfo.value || !patientInfo.value.encounterId) { if (!localPatient.value || !localPatient.value.encounterId) {
proxy.$modal.msgWarning('请先选择患者'); proxy.$modal.msgWarning('请先选择患者');
return; return;
} }
@@ -1002,9 +1017,9 @@ function checkUnit(item, row) {
* @returns {boolean} true=通过, false=不通过 * @returns {boolean} true=通过, false=不通过
*/ */
function validateStartTime(startTime) { function validateStartTime(startTime) {
if (!startTime || !patientInfo.value?.inHospitalTime) return true; if (!startTime || !localPatient.value?.inHospitalTime) return true;
const startDate = new Date(startTime); const startDate = new Date(startTime);
const inHospitalDate = new Date(patientInfo.value.inHospitalTime); const inHospitalDate = new Date(localPatient.value.inHospitalTime);
if (startDate < inHospitalDate) { if (startDate < inHospitalDate) {
const pad = (n) => String(n).padStart(2, '0'); const pad = (n) => String(n).padStart(2, '0');
const d = inHospitalDate; const d = inHospitalDate;
@@ -1168,25 +1183,40 @@ function handleFocus(row, index) {
return; return;
} }
row.showPopover = true; row.showPopover = true;
// Bug #555: handleFocus 初始化查询参数并加载初始数据searchKey 为空,无竞态风险 // 初始化查询参数searchKey 使用当前输入框已有文本,避免 @input 二次触发导致竞态
let categoryCode = ''; const searchKey = row.adviceName || '';
if (row.adviceType !== undefined) { adviceQueryParams.value = { adviceType, categoryCode: resolveCategoryCode(row, adviceType), searchKey };
const selectValue = (adviceType == 1 && row.categoryCode) ? '1-' + row.categoryCode : adviceType; const tableRef = getAdviceTableRef();
const selectedItem = adviceTypeList.value.find(item => item.value === selectValue) || adviceTypeList.value.find(item => item.adviceType === adviceType);
categoryCode = selectedItem ? selectedItem.categoryCode : (row.categoryCode || '');
}
adviceQueryParams.value = { adviceType, categoryCode, searchKey: '' };
// 弹窗首次打开时加载初始数据
const tableRef = Array.isArray(adviceTableRef.value) ? adviceTableRef.value[index] : adviceTableRef.value;
if (tableRef && tableRef.refresh) { if (tableRef && tableRef.refresh) {
tableRef.refresh(adviceType, categoryCode, ''); tableRef.refresh(adviceType, adviceQueryParams.value.categoryCode, searchKey);
} }
} }
/** 从行数据和 adviceType 解析 categoryCode */
function resolveCategoryCode(row, adviceType) {
if (row.adviceType !== undefined) {
const selectValue = (adviceType == 1 && row.categoryCode) ? '1-' + row.categoryCode : adviceType;
const selectedItem = adviceTypeList.value.find(item => item.value === selectValue) || adviceTypeList.value.find(item => item.adviceType === adviceType);
return selectedItem ? selectedItem.categoryCode : (row.categoryCode || '');
}
return '';
}
/** 获取 adviceTableRef单实例不在 v-for 中) */
function getAdviceTableRef() {
return adviceTableRef.value;
}
function handleBlur(row) { function handleBlur(row) {
row.showPopover = false; // 延迟关闭弹窗,等待 click 事件在弹出层内容上正常触发
// Bug #587: 标记弹窗刚关闭,防止点击空白处时触发行展开 // 原因:浏览器 blur 事件先于 click 触发;同步关闭会移除 DOM导致 click 丢失
popoverJustClosedByKey.value = row.uniqueKey; // 如果用户在弹出层内点击药品行selectAdviceBase 会在 150ms 内设 showPopover=false
// 此处的赋值变为 no-opfalse→false弹窗正常关闭且数据正确填充
setTimeout(() => {
row.showPopover = false;
// Bug #587: 标记弹窗刚关闭,防止点击空白处时触发行展开
popoverJustClosedByKey.value = row.uniqueKey;
}, 150);
} }
function handleChange(value, row, index) { function handleChange(value, row, index) {
@@ -1197,21 +1227,13 @@ function handleChange(value, row, index) {
return; return;
} }
adviceQueryParams.value.searchKey = value; adviceQueryParams.value.searchKey = value;
// @focus 已先于 @input 执行rowIndex 必定有效
// popover 被 blur 关闭后,用户继续输入时自行打开 // popover 被 blur 关闭后,用户继续输入时自行打开
if (!row.showPopover) { if (!row.showPopover) {
row.showPopover = true; row.showPopover = true;
} }
const tableRef = Array.isArray(adviceTableRef.value) ? adviceTableRef.value[index] : adviceTableRef.value; const tableRef = getAdviceTableRef();
if (tableRef && tableRef.refresh) { if (tableRef && tableRef.refresh) {
const adviceType = row?.adviceType !== undefined ? row.adviceType : adviceQueryParams.value.adviceType; tableRef.refresh(adviceType, resolveCategoryCode(row, adviceType), value);
let categoryCode = '';
if (row?.adviceType !== undefined) {
const selectValue = (adviceType == 1 && row?.categoryCode) ? '1-' + row.categoryCode : adviceType;
const selectedItem = adviceTypeList.value.find(item => item.value === selectValue) || adviceTypeList.value.find(item => item.adviceType === adviceType);
categoryCode = selectedItem ? selectedItem.categoryCode : (adviceQueryParams.value.categoryCode || '');
}
tableRef.refresh(adviceType, categoryCode, value);
} }
} }
@@ -1279,9 +1301,10 @@ function selectAdviceBase(key, row) {
prescriptionList.value[rowIndex.value].skinTestFlag = 1; prescriptionList.value[rowIndex.value].skinTestFlag = 1;
prescriptionList.value[rowIndex.value].skinTestFlag_enumText = '是'; prescriptionList.value[rowIndex.value].skinTestFlag_enumText = '是';
expandOrder.value = [currentUniqueKey]; expandOrder.value = [currentUniqueKey];
const expandRow = filterPrescriptionList.value.find(item => item.uniqueKey === currentUniqueKey); // 使用 prescriptionList 而非 filterPrescriptionList避免过滤后找不到行导致展开失败
if (expandRow && prescriptionRef.value?.setRowExpand) { const rowObj = prescriptionList.value.find(item => item.uniqueKey === currentUniqueKey);
prescriptionRef.value.setRowExpand([expandRow], true); if (rowObj && prescriptionRef.value?.setRowExpand) {
prescriptionRef.value.setRowExpand([rowObj], true);
} }
expandOrderAndFocus(currentUniqueKey, row); expandOrderAndFocus(currentUniqueKey, row);
}) })
@@ -1290,17 +1313,18 @@ function selectAdviceBase(key, row) {
prescriptionList.value[rowIndex.value].skinTestFlag = 0; prescriptionList.value[rowIndex.value].skinTestFlag = 0;
prescriptionList.value[rowIndex.value].skinTestFlag_enumText = '否'; prescriptionList.value[rowIndex.value].skinTestFlag_enumText = '否';
expandOrder.value = [currentUniqueKey]; expandOrder.value = [currentUniqueKey];
const expandRow = filterPrescriptionList.value.find(item => item.uniqueKey === currentUniqueKey); // 使用 prescriptionList 而非 filterPrescriptionList避免过滤后找不到行导致展开失败
if (expandRow && prescriptionRef.value?.setRowExpand) { const rowObj = prescriptionList.value.find(item => item.uniqueKey === currentUniqueKey);
prescriptionRef.value.setRowExpand([expandRow], true); if (rowObj && prescriptionRef.value?.setRowExpand) {
prescriptionRef.value.setRowExpand([rowObj], true);
} }
expandOrderAndFocus(currentUniqueKey, row); expandOrderAndFocus(currentUniqueKey, row);
}); });
} else { } else {
// 选完药品后展开编辑区域,供医生编辑剂量等字段 // 选完药品后展开编辑区域,供医生编辑剂量等字段
expandOrder.value = [currentUniqueKey]; expandOrder.value = [currentUniqueKey];
// 直接调 vxe-table 实例方法展开expandRowKeys 仅在初始化时生效) // 使用 prescriptionList 而非 filterPrescriptionList避免过滤后找不到行导致展开失败
const rowObj = filterPrescriptionList.value.find(item => item.uniqueKey === currentUniqueKey); const rowObj = prescriptionList.value.find(item => item.uniqueKey === currentUniqueKey);
if (rowObj && prescriptionRef.value?.setRowExpand) { if (rowObj && prescriptionRef.value?.setRowExpand) {
prescriptionRef.value.setRowExpand([rowObj], true); prescriptionRef.value.setRowExpand([rowObj], true);
} }
@@ -1539,9 +1563,9 @@ function handleSave() {
// 签发处理逻辑 // 签发处理逻辑
function executeSaveLogic() { function executeSaveLogic() {
saveList.forEach((item) => { saveList.forEach((item) => {
item.patientId = patientInfo.value.patientId; item.patientId = localPatient.value.patientId;
item.encounterId = patientInfo.value.encounterId; item.encounterId = localPatient.value.encounterId;
item.accountId = patientInfo.value.accountId; item.accountId = localPatient.value.accountId;
item.dbOpType = '1'; item.dbOpType = '1';
// Bug #589: 出院带药保存时转为药品类型 // Bug #589: 出院带药保存时转为药品类型
if (item.adviceType == 7) { if (item.adviceType == 7) {
@@ -1570,11 +1594,11 @@ function handleSave() {
// 保存签发按钮 // 保存签发按钮
isSaving.value = true; isSaving.value = true;
console.log('签发处方参数:', { console.log('签发处方参数:', {
organizationId: patientInfo.value.inHospitalOrgId, organizationId: localPatient.value.inHospitalOrgId,
adviceSaveList: list, adviceSaveList: list,
}); });
savePrescriptionSign({ savePrescriptionSign({
organizationId: patientInfo.value.inHospitalOrgId, organizationId: localPatient.value.inHospitalOrgId,
regAdviceSaveList: list, regAdviceSaveList: list,
}) })
.then((res) => { .then((res) => {
@@ -1636,8 +1660,8 @@ function handleOrderBindInfo(bindIdInfo) {
const newRow = { const newRow = {
...prescriptionList.value[rowIndex.value], ...prescriptionList.value[rowIndex.value],
uniqueKey: nextId.value++, uniqueKey: nextId.value++,
patientId: patientInfo.value.patientId, patientId: localPatient.value.patientId,
encounterId: patientInfo.value.encounterId, encounterId: localPatient.value.encounterId,
accountId: accountId.value, accountId: accountId.value,
quantity: item.quantity, quantity: item.quantity,
methodCode: item.methodCode, methodCode: item.methodCode,
@@ -1742,8 +1766,8 @@ function handleSaveSign(row, index) {
// 执行保存 // 执行保存
row.contentJson = undefined; row.contentJson = undefined;
row.patientId = patientInfo.value.patientId; row.patientId = localPatient.value.patientId;
row.encounterId = patientInfo.value.encounterId; row.encounterId = localPatient.value.encounterId;
row.accountId = accountId.value; row.accountId = accountId.value;
// 🔧 文字医嘱(type=8)跳过计费逻辑总金额为0 // 🔧 文字医嘱(type=8)跳过计费逻辑总金额为0
@@ -1867,6 +1891,11 @@ function handleSaveBatch() {
const therapyEnum = item.therapyEnum || '1'; const therapyEnum = item.therapyEnum || '1';
const result = { const result = {
...item, ...item,
// 确保 patientId/encounterId/accountId 始终存在,防止新增行未保存时这些字段为 null
// (新增行由 handleAddPrescription 创建时不携带患者信息,与其他保存路径保持一致)
patientId: item.patientId || localPatient.value?.patientId,
encounterId: item.encounterId || localPatient.value?.encounterId,
accountId: item.accountId || accountId.value,
therapyEnum: therapyEnum, therapyEnum: therapyEnum,
dbOpType: item.requestId ? '2' : '1', dbOpType: item.requestId ? '2' : '1',
// 确保 skinTestFlag 是数字类型1 或 0 // 确保 skinTestFlag 是数字类型1 或 0
@@ -1992,15 +2021,17 @@ function setValue(row) {
// 2. 诊疗类型优先使用项目维护的所属科室(row.orgId)其次positionId // 2. 诊疗类型优先使用项目维护的所属科室(row.orgId)其次positionId
// 3. 如果都为空,回退到患者当前所在科室(patientInfo.orgId) // 3. 如果都为空,回退到患者当前所在科室(patientInfo.orgId)
// 4. 使用 resolveOrgId 从组织树中匹配正确的 String id解决大 Long 精度丢失问题 // 4. 使用 resolveOrgId 从组织树中匹配正确的 String id解决大 Long 精度丢失问题
orgId: row.adviceType != 3 ? undefined : (resolveOrgId(row.orgId || row.positionId || patientInfo.value?.inHospitalOrgId) || ''), orgId: row.adviceType != 3 ? undefined : (resolveOrgId(row.orgId || row.positionId || localPatient.value?.inHospitalOrgId) || ''),
// 🔧 修复:同时保存 orgName当 orgId 在科室树中匹配不到时作为兜底显示 // 🔧 修复:同时保存 orgName当 orgId 在科室树中匹配不到时作为兜底显示
orgName: row.adviceType != 3 ? undefined : (findOrgName(row.orgId || row.positionId || patientInfo.value?.inHospitalOrgId) || row.orgName || patientInfo.value?.inHospitalOrgName || ''), orgName: row.adviceType != 3 ? undefined : (findOrgName(row.orgId || row.positionId || localPatient.value?.inHospitalOrgId) || row.orgName || localPatient.value?.inHospitalOrgName || ''),
// dose: undefined, Removed to preserve dose value from group package // dose: undefined, Removed to preserve dose value from group package
unitCodeList: unitCodeList.value, unitCodeList: unitCodeList.value,
doseUnitCode: row.doseUnitCode, doseUnitCode: row.doseUnitCode,
minUnitCode: String(row.minUnitCode), minUnitCode: String(row.minUnitCode),
unitCode: row.partAttributeEnum == 1 ? String(row.minUnitCode) : String(row.unitCode), unitCode: row.partAttributeEnum == 1 ? String(row.minUnitCode) : String(row.unitCode),
categoryEnum: row.categoryCode, // 显式保留 categoryCode防止 API 未返回时类型下拉框显示空白
categoryCode: row.categoryCode || baseRow.categoryCode || prevRow.categoryCode || '',
categoryEnum: row.categoryCode || baseRow.categoryCode || prevRow.categoryEnum || '',
// 确保 skinTestFlag 是数字类型1 或 0如果未定义则默认为 0 // 确保 skinTestFlag 是数字类型1 或 0如果未定义则默认为 0
skinTestFlag: row.skinTestFlag !== undefined && row.skinTestFlag !== null skinTestFlag: row.skinTestFlag !== undefined && row.skinTestFlag !== null
? (typeof row.skinTestFlag === 'number' ? row.skinTestFlag : (row.skinTestFlag ? 1 : 0)) ? (typeof row.skinTestFlag === 'number' ? row.skinTestFlag : (row.skinTestFlag ? 1 : 0))
@@ -2100,8 +2131,8 @@ function handleSaveGroup(orderGroupList) {
...prescriptionList.value[tempIndex], ...prescriptionList.value[tempIndex],
requesterId_dictText: userStore.nickName, requesterId_dictText: userStore.nickName,
requesterId: userStore.id, requesterId: userStore.id,
patientId: patientInfo.value.patientId, patientId: localPatient.value.patientId,
encounterId: patientInfo.value.encounterId, encounterId: localPatient.value.encounterId,
accountId: accountId.value, accountId: accountId.value,
// 🔧 修复 Bug #403从 mergedDetail 读取明细字段,而非直接从 item 取 // 🔧 修复 Bug #403从 mergedDetail 读取明细字段,而非直接从 item 取
// item.dose 等字段可能为 nullmergedDetail 已做 ?? 兜底 // item.dose 等字段可能为 nullmergedDetail 已做 ?? 兜底
@@ -2115,9 +2146,9 @@ function handleSaveGroup(orderGroupList) {
unitCode: mergedDetail.unitCode ?? item.unitCode, unitCode: mergedDetail.unitCode ?? item.unitCode,
unitCode_dictText: item.unitCodeName || mergedDetail.unitCodeName || '', unitCode_dictText: item.unitCodeName || mergedDetail.unitCodeName || '',
statusEnum: 1, statusEnum: 1,
orgId: resolveOrgId(mergedDetail.orgId || patientInfo.value?.inHospitalOrgId) || '', orgId: resolveOrgId(mergedDetail.orgId || localPatient.value?.inHospitalOrgId) || '',
// 🔧 修复:同时存储 orgName确保树匹配不到时仍有中文名称可显示 // 🔧 修复:同时存储 orgName确保树匹配不到时仍有中文名称可显示
orgName: findOrgName(mergedDetail.orgId || patientInfo.value?.inHospitalOrgId) || mergedDetail.orgName || patientInfo.value?.inHospitalOrgName || '', orgName: findOrgName(mergedDetail.orgId || localPatient.value?.inHospitalOrgId) || mergedDetail.orgName || localPatient.value?.inHospitalOrgName || '',
startTime: mergedDetail.startTime || defaultStartTimeFn(), startTime: mergedDetail.startTime || defaultStartTimeFn(),
dbOpType: prescriptionList.value[tempIndex].requestId ? '2' : '1', dbOpType: prescriptionList.value[tempIndex].requestId ? '2' : '1',
conditionId: conditionId.value, conditionId: conditionId.value,
@@ -2161,8 +2192,8 @@ function handleSaveHistory(value) {
...value, ...value,
requesterId_dictText: userStore.nickName, requesterId_dictText: userStore.nickName,
requesterId: userStore.id, requesterId: userStore.id,
patientId: patientInfo.value.patientId, patientId: localPatient.value.patientId,
encounterId: patientInfo.value.encounterId, encounterId: localPatient.value.encounterId,
accountId: accountId.value, accountId: accountId.value,
uniqueKey: undefined, uniqueKey: undefined,
startTime: defaultStartTimeFn(), startTime: defaultStartTimeFn(),
@@ -2386,9 +2417,9 @@ function confirmStopAdvice() {
return; return;
} }
// 校验:停嘱时间不能早于患者入院时间 // 校验:停嘱时间不能早于患者入院时间
if (patientInfo.value?.inHospitalTime) { if (localPatient.value?.inHospitalTime) {
const stopDate = new Date(stopForm.stopTime); const stopDate = new Date(stopForm.stopTime);
const inHospitalDate = new Date(patientInfo.value.inHospitalTime); const inHospitalDate = new Date(localPatient.value.inHospitalTime);
if (stopDate < inHospitalDate) { if (stopDate < inHospitalDate) {
const pad = (n) => String(n).padStart(2, '0'); const pad = (n) => String(n).padStart(2, '0');
const d = inHospitalDate; const d = inHospitalDate;
@@ -2903,7 +2934,7 @@ function sortPrescriptionList() {
} }
function handleLeaveHospital() { function handleLeaveHospital() {
if (!patientInfo.value) { if (!localPatient.value) {
proxy.$modal.msgWarning('请先选择患者'); proxy.$modal.msgWarning('请先选择患者');
return; return;
} }
@@ -3053,7 +3084,7 @@ defineExpose({ getListInfo, getDiagnosisInfo });
.inpatientDoctor-order-table { .inpatientDoctor-order-table {
flex: auto; flex: auto;
width: 100%; width: 100%;
min-height: 0; min-height: 300px;
overflow: auto; overflow: auto;
} }
.applicationForm-bottom-btn { .applicationForm-bottom-btn {

View File

@@ -451,6 +451,17 @@ function handleGetPrescription(skipAutoSelectAll = false) {
.map((x) => x.trim()) .map((x) => x.trim())
.filter((x) => x !== '') .filter((x) => x !== '')
.map((x) => normalizeDayTimeHm(x)); .map((x) => normalizeDayTimeHm(x));
// 如果频次表(adm_frequency)中 day_times 为空,降级使用 executeNum 生成默认时间点
// 避免长期医嘱因缺失 dayTimes 被静默过滤(不显示在待执行列表)
if (!rate || rate.length === 0) {
const execCount = prescription.executeNum || 1;
// 生成默认时间点:从 08:00 开始,按 executeNum 均分到 20:00
rate = Array.from({ length: execCount }, (_, i) => {
const h = 8 + Math.floor((12 / execCount) * i);
const m = Math.round(((12 / execCount) * i) % 1 * 60);
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
});
}
// 用截止时间和医嘱签发时间算出全部执行日期 // 用截止时间和医嘱签发时间算出全部执行日期
times = getDateRange(prescription.requestTime, deadline.value); times = getDateRange(prescription.requestTime, deadline.value);
} else { } else {

View File

@@ -63,9 +63,10 @@
</template> </template>
<script setup> <script setup>
import {getCurrentInstance, ref, nextTick, provide} from 'vue'; import {getCurrentInstance, ref, nextTick, provide, watch, onActivated} from 'vue';
import PatientList from '../components/patientList.vue'; import PatientList from '../components/patientList.vue';
import PrescriptionList from './components/prescriptionList.vue'; import PrescriptionList from './components/prescriptionList.vue';
import { patientInfoList } from '../components/store/patient.js';
import { RequestStatus } from '@/utils/medicalConstants'; import { RequestStatus } from '@/utils/medicalConstants';
const activeName = ref('preparation'); const activeName = ref('preparation');
@@ -129,6 +130,31 @@ function handleClick(tabName) {
provide('handleGetPrescription', (value) => { provide('handleGetPrescription', (value) => {
prescriptionRefs.value[activeName.value].handleGetPrescription(); prescriptionRefs.value[activeName.value].handleGetPrescription();
}); });
// 监听患者列表变化自动触发当前tab数据加载
// 解决从校对页面切换过来时lazy tab 未挂载导致数据不刷新的问题)
const autoFetchDone = ref(false);
watch(patientInfoList, async (newVal) => {
if (!autoFetchDone.value && newVal.length > 0) {
autoFetchDone.value = true;
// 等待组件挂载完成、模板refprescriptionRefs填充后再触发数据加载
// immediate 回调在 setup 阶段同步执行此时模板尚未渲染prescriptionRefs 为空
await nextTick();
nextTick(() => {
handleClick(activeName.value);
});
}
}, { immediate: true });
// keep-alive 缓存后重新激活时,强制刷新当前 tab 数据
// 校对通过长期医嘱后切换到执行页面autoFetchDone 已为 true 不会再触发)
onActivated(() => {
if (patientInfoList.value.length > 0) {
nextTick(() => {
handleClick(activeName.value);
});
}
});
</script> </script>
<style scoped> <style scoped>

View File

@@ -207,7 +207,7 @@
> >
<template #default="scope"> <template #default="scope">
<el-tag <el-tag
:type="getStatusType(scope.row.requestStatus)" :type="getStatusType(scope.row)"
size="small" size="small"
> >
{{ getStatusDisplayText(scope.row) }} {{ getStatusDisplayText(scope.row) }}
@@ -430,6 +430,11 @@ const getStatusDisplayText = (row) => {
if (DISPENSE_STATUS_DISPLAY[dispenseCode]) { if (DISPENSE_STATUS_DISPLAY[dispenseCode]) {
return DISPENSE_STATUS_DISPLAY[dispenseCode]; return DISPENSE_STATUS_DISPLAY[dispenseCode];
} }
// 2.5 兼容后端未更新 dispenseStatus 的情况:通过执行记录列表判断是否已执行
const exeRecords = row?.exePerformRecordList;
if (exeRecords && Array.isArray(exeRecords) && exeRecords.length > 0) {
return '已执行';
}
// 3. 最后回退到其他请求状态 // 3. 最后回退到其他请求状态
if (REQUEST_STATUS_DISPLAY[requestCode]) { if (REQUEST_STATUS_DISPLAY[requestCode]) {
return REQUEST_STATUS_DISPLAY[requestCode]; return REQUEST_STATUS_DISPLAY[requestCode];
@@ -439,7 +444,13 @@ const getStatusDisplayText = (row) => {
}; };
const getStatusType = (status) => { const getStatusType = (row) => {
// '已执行'状态优先用 success 色,与 getStatusDisplayText 保持一致
const displayText = getStatusDisplayText(row);
if (displayText === '已执行') {
return 'success';
}
const status = row?.requestStatus;
const map = { const map = {
[RequestStatus.DRAFT]: 'info', [RequestStatus.DRAFT]: 'info',
[RequestStatus.ACTIVE]: 'primary', [RequestStatus.ACTIVE]: 'primary',
@@ -452,9 +463,10 @@ const getStatusType = (status) => {
}; };
return map[status] || 'info'; return map[status] || 'info';
}; };
/** 选中医嘱中是否包含已执行或已发药记录 — 用于禁用退回按钮 */
const hasDispensedSelected = computed(() => { const hasDispensedSelected = computed(() => {
selectionTrigger.value; selectionTrigger.value;
return getSelectRows().some(item => item.dispenseStatus === 4); return getSelectRows().some(item => item.dispenseStatus === 11 || item.dispenseStatus === 4);
}); });
const props = defineProps({ const props = defineProps({
requestStatus: { requestStatus: {
@@ -591,6 +603,12 @@ function handleCheck() {
function handleCancel() { function handleCancel() {
let list = getSelectRows(); let list = getSelectRows();
if (list.length > 0) { if (list.length > 0) {
// 校验已执行的医嘱不允许直接退回
let executedItems = list.filter(item => item.dispenseStatus === 11);
if (executedItems.length > 0) {
proxy.$message.error('选中医嘱已执行,请先在"医嘱执行"界面取消执行后再执行退回操作');
return;
}
// 校验已发药的医嘱不允许退回 // 校验已发药的医嘱不允许退回
let dispensedItems = list.filter(item => item.dispenseStatus === 4); let dispensedItems = list.filter(item => item.dispenseStatus === 4);
if (dispensedItems.length > 0) { if (dispensedItems.length > 0) {

View File

@@ -66,6 +66,7 @@
import PatientList from '../components/patientList.vue'; import PatientList from '../components/patientList.vue';
import PrescriptionList from './components/prescriptionList.vue'; import PrescriptionList from './components/prescriptionList.vue';
import { RequestStatus } from '@/utils/medicalConstants'; import { RequestStatus } from '@/utils/medicalConstants';
import { patientInfoList } from '../components/store/patient.js';
const activeName = ref('unverified'); const activeName = ref('unverified');
const active = ref('first'); const active = ref('first');
@@ -123,6 +124,30 @@ function handleTabClick(tabName) {
provide('handleGetPrescription', (value) => { provide('handleGetPrescription', (value) => {
prescriptionRefs.value[activeName.value].handleGetPrescription(); prescriptionRefs.value[activeName.value].handleGetPrescription();
}); });
// 监听患者列表变化自动触发当前tab数据加载
// (解决切换医嘱校对/医嘱执行tab后组件重建时数据不刷新的问题
const autoFetchDone = ref(false);
watch(patientInfoList, async (newVal) => {
if (!autoFetchDone.value && newVal.length > 0) {
autoFetchDone.value = true;
// 等待组件挂载完成、模板refprescriptionRefs填充后再触发数据加载
// immediate 回调在 setup 阶段同步执行此时模板尚未渲染prescriptionRefs 为空
await nextTick();
nextTick(() => {
handleTabClick(activeName.value);
});
}
}, { immediate: true });
// keep-alive 缓存后重新激活时,强制刷新当前 tab 数据
onActivated(() => {
if (patientInfoList.value.length > 0) {
nextTick(() => {
handleTabClick(activeName.value);
});
}
});
</script> </script>
<style scoped> <style scoped>