Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
2026-01-28 14:29:29 +08:00
42 changed files with 1389 additions and 563 deletions

View File

@@ -12,7 +12,7 @@
@keyup.enter="handleSearch"
:prefix-icon="Search"
/>
<el-button class="icon-btn" circle @click="handleRefresh" type="text" plain>
<el-button class="icon-btn" circle @click="handleRefresh" link plain>
<el-icon icon-class="Refresh" size="24" :class="{ 'is-rotating': refreshing }">
<Refresh />
</el-icon>
@@ -36,7 +36,7 @@
<!-- 第1行姓名 -->
<div class="info-row name-row">
<div class="name">
<el-text :text="item.patientName" tclass="name" width="auto">
<el-text :text="item.patientName" class="name" :truncated="true">
{{ item.patientName || '-' }}
</el-text>
</div>

View File

@@ -23,7 +23,7 @@
<el-form-item v-if="showDefaultButtons" style="margin-left: 20px">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
<el-button v-if="needCollapse" type="text" @click="toggleExpand" style="margin-left: 16px">
<el-button v-if="needCollapse" link @click="toggleExpand" style="margin-left: 16px">
{{ isExpanded ? '收起' : '展开' }}
<el-icon class="el-icon--right">
<DArrowLeft v-if="isExpanded" class="collapse-arrow collapse-arrow--up" />

View File

@@ -565,6 +565,26 @@ const rules = reactive({
chiefComplaint: [{ required: true, message: '请填写主诉', trigger: ['blur', 'submit'] }],
});
// 监听 patientInfo 变化,更新表单数据
watch(
() => props.patientInfo,
(newPatientInfo) => {
if (newPatientInfo) {
// 更新基础信息
formData.patientName = newPatientInfo.patientName || newPatientInfo.name || '';
formData.hospitalNo = newPatientInfo.busNo || newPatientInfo.hospitalNo || '';
formData.gender = newPatientInfo.genderEnum_enumText || newPatientInfo.gender || '';
formData.age = newPatientInfo.age || '';
formData.nation = newPatientInfo.nation || '';
formData.occupation = newPatientInfo.profession || '';
formData.marriage = newPatientInfo.maritalStatus || '';
formData.birthplace = newPatientInfo.birthPlace || '';
// 可以根据需要更新更多字段
}
},
{ deep: true, immediate: true }
);
// 生命周期
onMounted(() => {
// 初始化记录时间为当前时间
@@ -578,16 +598,16 @@ onMounted(() => {
formData.signDate = formatDateTime(new Date());
}
if (!formData.patientName) {
formData.patientName = patient?.patientName || '';
formData.patientName = patient?.patientName || patient?.name || '';
}
if (!formData.gender) {
formData.gender = patient?.genderEnum_enumText || '';
formData.gender = patient?.genderEnum_enumText || patient?.gender || '';
}
if (!formData.age) {
formData.age = patient?.age || '';
}
if (!formData.hospitalNo) {
formData.hospitalNo = patient?.busNo || '';
formData.hospitalNo = patient?.busNo || patient?.hospitalNo || '';
}
});

View File

@@ -334,11 +334,13 @@ export function handleColor(a, b, status) {
*/
export function getPrinterList() {
try {
const printerList =
window.hiprint && window.hiprint.hiwebSocket
? window.hiprint.hiwebSocket.getPrinterList()
: [];
return printerList || [];
if (window.hiprint && window.hiprint.hiwebSocket && window.hiprint.hiwebSocket.connected) {
const printerList = window.hiprint.hiwebSocket.getPrinterList();
return printerList || [];
} else {
console.warn('打印服务未连接,返回空打印机列表');
return [];
}
} catch (error) {
console.error('获取打印机列表失败:', error);
return [];

View File

@@ -151,11 +151,13 @@ export const PRINT_TEMPLATE = {
*/
export function getPrinterList() {
try {
const printerList =
window.hiprint && window.hiprint.hiwebSocket
? window.hiprint.hiwebSocket.getPrinterList()
: [];
return printerList || [];
if (window.hiprint && window.hiprint.hiwebSocket && window.hiprint.hiwebSocket.connected) {
const printerList = window.hiprint.hiwebSocket.getPrinterList();
return printerList || [];
} else {
console.warn('打印服务未连接,返回空打印机列表');
return [];
}
} catch (error) {
console.error('获取打印机列表失败:', error);
return [];

View File

@@ -225,7 +225,15 @@ function getList() {
loading.value = true;
getDiagnosisTreatmentList(queryParams.value).then((res) => {
loading.value = false;
catagoryList.value = res.data.records;
catagoryList.value = res.data.records.map(record => {
// 为每一行初始化 filteredOptions确保显示框能正确显示项目名称
const filteredOptions = allImplementDepartmentList.value.slice(0, 100);
return {
...record,
loading: false,
filteredOptions: filteredOptions
};
});
total.value = res.data.total;
});
}

View File

@@ -52,8 +52,8 @@
</el-table-column>
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-button type="text" @click="editTemplate(row)">编辑</el-button>
<el-button type="text" @click="deleteTemplate(row.id)" style="color: #f56c6c">
<el-button link @click="editTemplate(row)">编辑</el-button>
<el-button link @click="deleteTemplate(row.id)" style="color: #f56c6c">
删除
</el-button>
</template>

View File

@@ -776,7 +776,8 @@ const transformFormData = (form) => {
extraDetails,
comment,
busNo,
},
practitionerId: form.value.practitionerId,
},
chargeItemDefinitionFormData: {
id,
chargeName,
@@ -833,7 +834,8 @@ const transformFormEditData = (form) => {
extraDetails,
comment,
busNo,
},
practitionerId: form.value.practitionerId,
},
};
};
getRegistrationfeeTypeList();

View File

@@ -42,7 +42,6 @@ import {defineEmits, ref, unref} from 'vue';
import {deleteRecord, getRecordByEncounterIdList} from '../api';
import {ElMessage} from 'element-plus';
import {patientInfo} from '../../store/patient.js';
import apiRequestManager from '@/utils/apiRequestManager.js';
const emits = defineEmits(['historyClick']);
const props = defineProps({
@@ -68,30 +67,15 @@ const queryParams = ref({
isPage: 0,
});
const historyData = ref([]);
// 防止重复加载的标志
let isLoadingHistory = false;
const queryList = async () => {
// 防止重复加载
if (isLoadingHistory) {
console.log('History data is already loading, skipping duplicate call');
return;
}
isLoadingHistory = true;
try {
if (patientInfo.value.encounterId && unref(definitionId) && unref(definitionId) !== '') {
const res = await apiRequestManager.execute(
getRecordByEncounterIdList,
'/document/record/getRecordByEncounterIdList',
{
isPage: 0, // 确保参数一致,便于去重
encounterId: patientInfo.value.encounterId,
patientId: patientInfo.value.patientId,
definitionId: unref(definitionId),
}
);
const res = await getRecordByEncounterIdList({
...queryParams.value,
encounterId: patientInfo.value.encounterId,
patientId: patientInfo.value.patientId,
definitionId: unref(definitionId),
});
historyData.value = res.data || [];
} else {
historyData.value = [];
@@ -99,8 +83,6 @@ const queryList = async () => {
} catch (error) {
// ElMessage.error(' 获取模板树失败 ');
historyData.value = [];
} finally {
isLoadingHistory = false; // 重置加载标志
}
};
const handleNodeClick = (data) => {

View File

@@ -103,7 +103,6 @@ import dayjs from 'dayjs';
// 打印工具
import {PRINT_TEMPLATE, simplePrint} from '@/utils/printUtils.js';
import {getEncounterDiagnosis} from '../api';
import apiRequestManager from '@/utils/apiRequestManager.js';
import History from './components/history';
import Template from './components/template';
import TemplateEdit from './components/templateEdit.vue';
@@ -206,7 +205,7 @@ const handleNodeClick = (data, node) => {
// 选择任何病历模板后,都加载该病历类型的最新历史记录
if (node.isLeaf && props.patientInfo && props.patientInfo.patientId) {
debouncedLoadLatestMedicalRecord();
loadLatestMedicalRecord();
}
}, 100);
});
@@ -280,7 +279,7 @@ const handleSubmitOk = async (data) => {
// 等待历史记录列表更新后,重新加载最新病历并更新选中状态
setTimeout(() => {
debouncedLoadLatestMedicalRecord();
loadLatestMedicalRecord();
}, 100);
} catch (error) {
ElMessage.error('提交失败');
@@ -411,7 +410,7 @@ const selectOutpatientMedicalRecordTemplate = async () => {
// 等待模板加载完成,然后获取并回显最新病历数据
setTimeout(() => {
historyRef.value?.queryList();
debouncedLoadLatestMedicalRecord();
loadLatestMedicalRecord();
}, 500);
});
} else {
@@ -422,36 +421,19 @@ const selectOutpatientMedicalRecordTemplate = async () => {
// 当前选中的历史病历ID用于在History组件中高亮显示
const selectedHistoryRecordId = ref('');
import { debounce } from 'lodash-es';
// 防止重复加载的标志
let isLoadingLatestRecord = false;
// 加载最新的病历数据并回显
const loadLatestMedicalRecord = async () => {
if (!patientInfo.value.encounterId || !currentSelectTemplate.value.id) return;
// 防止重复加载
if (isLoadingLatestRecord) {
console.log('Latest medical record is already loading, skipping duplicate call');
return;
}
isLoadingLatestRecord = true;
loading.value = true;
try {
// 获取患者的历史病历记录
const res = await apiRequestManager.execute(
getRecordByEncounterIdList,
'/document/record/getRecordByEncounterIdList',
{
isPage: 0,
encounterId: patientInfo.value.encounterId,
patientId: patientInfo.value.patientId,
definitionId: currentSelectTemplate.value.id,
}
);
const res = await getRecordByEncounterIdList({
isPage: 0,
encounterId: patientInfo.value.encounterId,
patientId: patientInfo.value.patientId,
definitionId: currentSelectTemplate.value.id,
});
const historyRecords = res.data || [];
if (historyRecords.length > 0) {
@@ -537,12 +519,8 @@ const loadLatestMedicalRecord = async () => {
});
} finally {
loading.value = false;
isLoadingLatestRecord = false; // 重置加载标志
}
};
// 防抖版本的加载最新病历数据函数
const debouncedLoadLatestMedicalRecord = debounce(loadLatestMedicalRecord, 300);
const templateRef = ref(null);
const handleTemplateClick = (data) => {
@@ -772,7 +750,7 @@ const selectDefaultTemplate = () => {
// 直接加载最新病历数据不再使用额外的setTimeout延迟
// 因为handleNodeClick中已经有nextTick和setTimeout处理组件渲染
debouncedLoadLatestMedicalRecord();
loadLatestMedicalRecord();
});
} else {
console.log('未找到门诊病历模板');

View File

@@ -59,8 +59,8 @@
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template #default="scope">
<el-button type="text" size="small" @click="handlePrint(scope.row)">打印</el-button>
<el-button type="text" size="small" style="color: #f56c6c" @click="handleDelete(scope.row)">删除</el-button>
<el-button link size="small" @click="handlePrint(scope.row)">打印</el-button>
<el-button link size="small" style="color: #f56c6c" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -379,7 +379,7 @@
<!-- 标题栏 -->
<div class="selected-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #eee;">
<span style="font-weight: bold; color: #1a2b6d">已选择</span>
<el-button type="text" @click="clearAllSelected" style="color: #f56c6c">清空</el-button>
<el-button link @click="clearAllSelected" style="color: #f56c6c">清空</el-button>
</div>
<!-- 已选项目列表 -->
@@ -393,7 +393,7 @@
<span class="item-itemName">{{ item.itemName }}</span>
<span class="item-price">¥{{ item.itemPrice }}</span>
<el-button
type="text"
link
size="small"
style="color: #f56c6c; margin-left: auto"
@click="removeInspectionItem(item)"

View File

@@ -268,14 +268,15 @@ const validatePhraseName = (phraseName, excludeId = null) => {
// 所有数据(用于客户端分页处理)
const allData = ref([])
// 获取医生常用语列表数据
// 获取医生常用语列表数据
const fetchDoctorPhraseList = async () => {
try {
const response = await getDoctorPhraseList()
// 处理后端返回的数据结构data.data
if (response.code === 200 && response.data && response.data.data) {
// 【关键修改】去掉 response.data.data直接取 response.data
if (response.code === 200 && response.data) {
// 按照sortNo由小到大排序保证列表顺序正确
allData.value = response.data.data.sort((a, b) => a.sortNo - b.sortNo)
allData.value = response.data.sort((a, b) => a.sortNo - b.sortNo)
total.value = allData.value.length
// 执行客户端分页逻辑
applyPagination()
@@ -285,7 +286,7 @@ const fetchDoctorPhraseList = async () => {
total.value = 0
}
} catch (error) {
console.error('获取列表失败:', error) // 增加控制台日志便于调试
console.error('获取列表失败:', error)
ElMessage.error('获取数据失败: 网络请求错误')
allData.value = []
total.value = 0
@@ -322,19 +323,18 @@ const handleCurrentChange = (val) => {
applyPagination()
}
// 搜索功能核心方法
// 搜索功能核心方法
const handleSearch = async () => {
try {
// searchScope可能是null未选择、1=个人2=科室3=全院
const phraseType = searchScope.value === null ? undefined : searchScope.value
// 调用搜索接口phraseName, phraseType
const response = await searchDoctorPhraseList(searchKeyword.value, phraseType)
if (response.code === 200 && response.data && response.data.data) {
// 按照sortNo由小到大排序
allData.value = response.data.data.sort((a, b) => a.sortNo - b.sortNo)
// 【关键修改】去掉 response.data.data直接取 response.data
if (response.code === 200 && response.data) {
allData.value = response.data.sort((a, b) => a.sortNo - b.sortNo)
total.value = allData.value.length
currentPage.value = 1 // 搜索后重置到第一页
applyPagination() // 应用分页
currentPage.value = 1
applyPagination()
} else {
ElMessage.error('搜索失败: ' + (response.msg || '未知错误'))
allData.value = []
@@ -349,20 +349,30 @@ const handleSearch = async () => {
}
// 打开新增模态框方法
// index.vue
const showAddDialog = () => {
// 重置表单数据
// 1. 算出当前最大的排序号
// 如果列表是空的,就从 1 开始;如果不空,取第一条(因为我们排过序了)或遍历找最大值
let maxSortNo = 0
if (allData.value && allData.value.length > 0) {
// 既然 allData 已经按 sortNo 排序了,那最后一个就是最大的?
// 或者保险起见,用 Math.max 算一下
maxSortNo = Math.max(...allData.value.map(item => item.sortNo || 0))
}
// 2. 重置表单,并将排序号设为 最大值 + 1
addForm.value = {
phraseName: '',
phraseContent: '',
sortNo: 1,
sortNo: maxSortNo + 1, // <--- 这样每次打开就是 2, 3, 4...
phraseType: 1,
phraseCategory: ''
}
// 重置表单验证状态
if (addFormRef.value) {
addFormRef.value.clearValidate()
}
// 打开模态框
addDialogVisible.value = true
}
@@ -434,7 +444,6 @@ const handleDelete = async (row) => {
// 用户取消删除时不提示错误
if (error !== 'cancel') {
console.error('删除失败:', error)
ElMessage.error('删除操作失败: 网络异常或权限不足')
}
}
}
@@ -455,39 +464,41 @@ const showEditDialog = (row) => {
}
// 编辑表单提交保存方法
// 修改 index.vue 中的 handleEditSave 方法
const handleEditSave = async () => {
try {
// 先执行表单验证
// 1. 表单校验
const validateResult = await editFormRef.value.validate()
if (!validateResult) return
// 名称唯一性校验排除当前编辑的这条记录ID
// 2. 名称唯一性校验
const nameValidation = validatePhraseName(editForm.value.phraseName, editForm.value.id)
if (!nameValidation.valid) {
ElMessage.error(nameValidation.message)
return
}
// 准备更新数据修复时间格式为ISO字符串适配后端LocalDateTime
// 3. 准备数据
const updateData = {
...editForm.value,
enableFlag: 1,
updateTime: new Date().toISOString() // 前端临时赋值,后端最终以自己的为准
updateTime: new Date().toISOString()
}
// 调用更新接口
// 4. 调用接口
const response = await updateDoctorPhrase(updateData)
// 【核心修改】直接判断 code === 200 即可
// 因为后端现在失败会返回 R.fail (code!=200),所以只要是 200 就是成功
if (response.code === 200) {
ElMessage.success('更新成功')
ElMessage.success(response.msg || '更新成功') // 优先显示后端返回的消息
editDialogVisible.value = false
// 重新拉取数据,保证列表数据最新
fetchDoctorPhraseList()
} else {
ElMessage.error('更新失败: ' + (response.msg || '未知错误'))
ElMessage.error(response.msg || '更新失败')
}
} catch (error) {
console.error('更新失败:', error)
ElMessage.error('更新操作失败: 网络请求错误')
}
}

View File

@@ -99,6 +99,10 @@
{{ userStore.nickName }}
</el-descriptions-item>
<el-descriptions-item label="" width="300">
<el-radio-group v-model="firstEnum">
<el-radio :label="1">初诊</el-radio>
<el-radio :label="2">复诊</el-radio>
</el-radio-group>
<el-button type="primary" plain @click.stop="handleFinish(patientInfo.encounterId)">
完诊
</el-button>
@@ -209,7 +213,6 @@ import useUserStore from '@/store/modules/user';
import { nextTick } from 'vue';
import { updatePatientInfo } from './components/store/patient.js';
import { ElMessage, ElMessageBox } from 'element-plus';
import { debounce } from 'lodash-es';
// // 监听路由离开事件
// onBeforeRouteLeave((to, from, next) => {
@@ -276,6 +279,7 @@ const loading = ref(false);
const { proxy } = getCurrentInstance();
const visitType = ref('');
const firstVisitDate = ref('');
const firstEnum = ref(1); // 初复诊标识1=初诊2=复诊
const disabled = computed(() => {
// 只有在有患者信息但某些条件不满足时才启用覆盖层
// 当前逻辑保持不变,但我们将在按钮级别处理禁用状态
@@ -488,8 +492,7 @@ function handleOpen() {
patientDrawerRef.value.refreshList();
}
// 原始的handleCardClick函数
function handleCardClickOriginal(item, index) {
function handleCardClick(item, index) {
console.log('handleCardClick 被调用');
console.log('点击的患者项目:', item);
console.log('患者项目中的encounterId:', item.encounterId);
@@ -506,6 +509,15 @@ function handleCardClickOriginal(item, index) {
console.log('patientInfo.value 设置为:', patientInfo.value);
console.log('patientInfo.value.encounterId:', patientInfo.value?.encounterId);
// 根据患者信息设置初复诊标识
const backendValue = item.firstEnum ?? item.first_enum;
if (backendValue !== undefined && backendValue !== null) {
firstEnum.value = Number(backendValue); // 确保是数字类型
} else {
firstEnum.value = 1;
}
// 确保患者信息包含必要的字段
if (!patientInfo.value.encounterId) {
console.error('患者信息缺少encounterId字段:', patientInfo.value);
@@ -546,9 +558,6 @@ function handleCardClickOriginal(item, index) {
});
}
// 使用防抖的handleCardClick函数防止短时间内多次点击
const handleCardClick = debounce(handleCardClickOriginal, 500);
function handleLeave(encounterId) {
leaveEncounter(encounterId).then((res) => {
if (res.code == 200) {
@@ -566,11 +575,18 @@ function handleFinish(encounterId) {
patientInfo.value = {};
visitType.value = ''; // 重置初复诊标识
visitTypeDisabled.value = false; // 重置禁用状态
firstEnum.value = 1; // 重置为初诊
getPatientList();
}
});
}
// 监听初复诊标识变化
watch(firstEnum, (newValue) => {
// 这里可以添加更新后端的逻辑,如果需要实时同步到后端
// 例如updateEncounterFirstEnum(patientInfo.value.encounterId, newValue)
});
function handleTimeChange(value) {
queryParams.value.registerTimeSTime = value + ' 00:00:00';
queryParams.value.registerTimeETime = value + ' 23:59:59';
@@ -594,7 +610,7 @@ function handleHospitalizationClick() {
// 接诊回调
function handleReceive(row) {
handleCardClickOriginal(row);
handleCardClick(row);
currentEncounterId.value = row.encounterId;
drawer.value = false;
getPatientList();
@@ -781,7 +797,7 @@ const markSeen = async () => {
currentCallPatient.value = {};
};
const callThis = (row) => {
handleCardClickOriginal(row);
handleCardClick(row);
currentCallPatient.value = row;
dialogVisible.value = false;
// 刷新患者列表和候诊列表

View File

@@ -699,7 +699,12 @@ const getDom = () => {
};
const setData = (data) => {
console.log('设置数据=========>', JSON.stringify(data));
Object.assign(formData, data);
// 仅更新传入的数据,保留未传入字段的原有值
Object.keys(data).forEach(key => {
if (formData.hasOwnProperty(key)) {
formData[key] = data[key];
}
});
};
defineExpose({
setData,

View File

@@ -424,8 +424,8 @@ function handleWardClick(item) {
function getInitOptions() {
getOrgList().then((res) => {
// organization.value = res.data.records
organization.value = res.data.records[0].children.filter(
// 直接从records中过滤而不是从records[0].children
organization.value = res.data.records.filter(
(record) => record.typeEnum === 2 && checkClassEnumValue(record.classEnum, 2)
);
});

View File

@@ -106,7 +106,7 @@ watch(
getList();
function getList() {
loading.value = true;
queryParams.value.organizationId = props.patientInfo.inHospitalOrgId;
queryParams.value.organizationId = props.patientInfo?.inHospitalOrgId || '';
getAdviceBaseInfo(queryParams.value)
.then((res) => {
console.log(res.data.records);

View File

@@ -203,7 +203,8 @@ const form = ref({
const props = defineProps({
patientInfo: {
type: Object,
required: true,
required: false,
default: () => ({}),
},
});
const emits = defineEmits(['diagnosisSave']);

View File

@@ -10,25 +10,25 @@
<el-button
type="primary"
@click="showApplicationFormDialog('LaboratoryTests')"
:disabled="!patientInfo?.inHospitalOrgId"
:disabled="!props.patientInfo?.inHospitalOrgId"
>检验</el-button
>
<el-button
type="primary"
@click="showApplicationFormDialog('MedicalExaminations')"
:disabled="!patientInfo?.inHospitalOrgId"
:disabled="!props.patientInfo?.inHospitalOrgId"
>检查</el-button
>
<el-button
type="primary"
@click="showApplicationFormDialog('BloodTransfusion')"
:disabled="!patientInfo?.inHospitalOrgId"
:disabled="!props.patientInfo?.inHospitalOrgId"
>输血</el-button
>
<el-button
type="primary"
@click="showApplicationFormDialog('Surgery')"
:disabled="!patientInfo?.inHospitalOrgId"
:disabled="!props.patientInfo?.inHospitalOrgId"
>手术</el-button
>
</el-button-group>
@@ -58,14 +58,19 @@
<script setup>
import {computed, getCurrentInstance, nextTick, onBeforeMount, onMounted, reactive, ref,} from 'vue';
import BloodTransfusion from './bloodTransfusion.vue';
import {patientInfo} from '../../../store/patient.js';
import Surgery from './surgery.vue';
import LaboratoryTests from './laboratoryTests.vue';
import MedicalExaminations from './medicalExaminations.vue';
const { proxy } = getCurrentInstance();
const emits = defineEmits(['refResh']);
const props = defineProps({});
const props = defineProps({
patientInfo: {
type: Object,
required: false,
default: () => ({}),
},
});
const state = reactive({});
const components = ref({
BloodTransfusion,

View File

@@ -297,16 +297,17 @@
</el-table>
</div>
<!-- // 底部按钮 -->
<application-form-bottom-btn @refResh="refresh" />
<application-form-bottom-btn :patientInfo="patientInfo" @refResh="refresh" />
<OrderGroupDrawer
ref="orderFroupRef"
:diagnosis="diagnosisInfo"
:organizationId="patientInfo?.orgId || ''"
@useOrderGroup="handleSaveGroup"
/>
<PrescriptionHistory
ref="prescriptionHistoryRef"
:diagnosis="diagnosisInfo"
:patientInfo="patientInfo"
:patientInfo="patientInfo || {}"
@userPrescriptionHistory="handleSaveHistory"
/>
<LeaveHospitalDialog
@@ -405,7 +406,8 @@ const buttonDisabled = computed(() => {
const props = defineProps({
patientInfo: {
type: Object,
required: true,
required: false,
default: () => ({}),
},
activeTab: {
type: String,
@@ -525,33 +527,19 @@ function getList() {
function refresh() {
getListInfo(false);
}
// 防止重复请求的标志
let listInfoRequestPromise = null;
// 获取列表信息
function getListInfo(addNewRow) {
// 如果已经有正在进行的请求则返回该请求的Promise
if (listInfoRequestPromise) {
return listInfoRequestPromise;
}
loadingInstance = ElLoading.service({ fullscreen: true });
setTimeout(() => {
if (loadingInstance) {
loadingInstance.close();
}
loadingInstance.close();
}, 180);
isAdding.value = false;
expandOrder.value = [];
getPrescriptionList(patientInfo.value.encounterId).then((res) => {
console.log('getListInfo==========>', JSON.stringify(res.data));
// 并行请求两个API并将结果合并处理
listInfoRequestPromise = Promise.all([
getPrescriptionList(patientInfo.value.encounterId),
getContract({ encounterId: patientInfo.value.encounterId })
])
.then(([prescriptionRes, contractRes]) => {
// 处理处方列表
prescriptionList.value = prescriptionRes.data
loadingInstance.close();
prescriptionList.value = res.data
.map((item) => {
return {
...JSON.parse(item.contentJson),
@@ -563,35 +551,15 @@ function getListInfo(addNewRow) {
.sort((a, b) => {
return new Date(b.requestTime) - new Date(a.requestTime);
});
// 处理合同列表
contractList.value = contractRes.data;
// 更新账户ID
accountId.value = patientInfo.value.accountId;
// 更新标记
getGroupMarkers();
getGroupMarkers(); // 更新标记
if (props.activeTab == 'prescription' && addNewRow) {
handleAddPrescription();
}
console.log('getListInfo==========>', JSON.stringify(prescriptionRes.data));
})
.catch(error => {
console.error('获取列表信息失败:', error);
ElMessage.error('获取列表信息失败');
})
.finally(() => {
if (loadingInstance) {
loadingInstance.close();
}
// 请求完成后清除Promise引用
listInfoRequestPromise = null;
});
return listInfoRequestPromise;
getContract({ encounterId: patientInfo.value.encounterId }).then((res) => {
contractList.value = res.data;
});
accountId.value = patientInfo.value.accountId;
}
// 数据过滤
const filterPrescriptionList = computed(() => {
@@ -605,37 +573,18 @@ const filterPrescriptionList = computed(() => {
return pList;
});
// 防止诊断信息重复请求的标志
let diagnosisInfoRequestPromise = null;
function getDiagnosisInfo() {
// 如果已经有正在进行的请求则返回该请求的Promise
if (diagnosisInfoRequestPromise) {
return diagnosisInfoRequestPromise;
}
diagnosisInfoRequestPromise = getEncounterDiagnosis(patientInfo.value.encounterId)
.then((res) => {
diagnosisList.value = res.data;
let diagnosisInfo = diagnosisList.value.filter((item) => {
return item.maindiseFlag == 1;
});
diagnosisInfo.value = diagnosisInfo[0];
conditionDefinitionId.value = diagnosisInfo[0].definitionId;
conditionId.value = diagnosisInfo[0].conditionId;
encounterDiagnosisId.value = diagnosisInfo[0].encounterDiagnosisId;
diagnosisName.value = diagnosisInfo[0].name;
})
.catch(error => {
console.error('获取诊断信息失败:', error);
ElMessage.error('获取诊断信息失败');
})
.finally(() => {
// 请求完成后清除Promise引用
diagnosisInfoRequestPromise = null;
getEncounterDiagnosis(patientInfo.value.encounterId).then((res) => {
diagnosisList.value = res.data;
let diagnosisInfo = diagnosisList.value.filter((item) => {
return item.maindiseFlag == 1;
});
return diagnosisInfoRequestPromise;
diagnosisInfo.value = diagnosisInfo[0];
conditionDefinitionId.value = diagnosisInfo[0].definitionId;
conditionId.value = diagnosisInfo[0].conditionId;
encounterDiagnosisId.value = diagnosisInfo[0].encounterDiagnosisId;
diagnosisName.value = diagnosisInfo[0].name;
});
}
function getRowDisabled(row) {

View File

@@ -65,40 +65,22 @@ const queryParams = ref({
isPage: 0,
});
const historyData = ref([]);
// 防止重复请求的标志
let queryListPromise = null;
const queryList = async () => {
// 如果已经有正在进行的请求则返回该请求的Promise
if (queryListPromise) {
return queryListPromise;
}
try {
if (patientInfo.value.encounterId && unref(definitionId) && unref(definitionId) !== '') {
queryListPromise = getRecordByEncounterIdList({
const res = await getRecordByEncounterIdList({
...queryParams.value,
encounterId: patientInfo.value.encounterId,
patientId: patientInfo.value.patientId,
definitionId: unref(definitionId),
})
.then(res => {
historyData.value = res.data || [];
})
.finally(() => {
// 请求完成后清除Promise引用
queryListPromise = null;
});
return queryListPromise;
historyData.value = res.data || [];
} else {
historyData.value = [];
}
} catch (error) {
// 不显示错误消息,避免干扰用户体验
historyData.value = [];
// 请求完成后清除Promise引用
queryListPromise = null;
}
};
@@ -121,7 +103,7 @@ const handleDelete = async (item) => {
await deleteRecord([item.id]);
ElMessage.success('删除成功');
queryList();
await queryList();
} catch (error) {
ElMessage.error('删除失败');
}

View File

@@ -211,11 +211,11 @@ const handleNodeClick = (data, node) => {
};
// 先清空当前组件,再设置新组件,确保组件完全重新渲染
currentComponent.value = '';
currentComponent.value = undefined;
// 使用 nextTick 确保 DOM 更新后再设置新组件
nextTick(() => {
currentComponent.value = currentSelectTemplate.value.vueRouter || '';
currentComponent.value = currentSelectTemplate.value.vueRouter;
});
} else {
currentSelectTemplate.value = {
@@ -241,7 +241,7 @@ const handleNodeClick = (data, node) => {
const newEmr = () => {
if (currentSelectTemplate.value) {
currentComponent.value = currentSelectTemplate.value.vueRouter || '';
currentComponent.value = currentSelectTemplate.value.vueRouter;
return;
}
ElMessage.error('请选择模版!');
@@ -305,10 +305,9 @@ const handleSubmitOk = async (data) => {
// templateRef.value?.queryList();
// 等待历史记录列表更新后,重新加载最新病历并更新选中状态
// 增加延迟时间以确保数据库更新完成
setTimeout(() => {
loadLatestMedicalRecord();
}, 300);
}, 100);
ElMessage.success('保存成功');
} catch (error) {
ElMessage.error('提交失败');
@@ -504,7 +503,7 @@ const resetForm = async () => {
// 先将组件设置为空,强制卸载
const currentComponentName = currentComponent.value;
if (currentComponentName) {
currentComponent.value = '';
currentComponent.value = undefined;
// 等待DOM更新
await nextTick();
@@ -554,94 +553,54 @@ const selectOutpatientMedicalRecordTemplate = async () => {
selectDefaultTemplate();
};
// 防止重复请求的标志
let loadLatestMedicalRecordPromise = null;
// 加载最新的病历数据并回显
const loadLatestMedicalRecord = async () => {
// 如果已经有正在进行的请求则返回该请求的Promise
if (loadLatestMedicalRecordPromise) {
return loadLatestMedicalRecordPromise;
}
if (!patientInfo.value?.encounterId || !currentSelectTemplate.value.id) return;
editForm.value.id = '';
loading.value = true;
try {
// 获取患者的历史病历记录
// const res = await getRecordByEncounterIdList({
// isPage: 0,
// encounterId: patientInfo.value.encounterId,
// patientId: patientInfo.value.patientId,
// definitionId: currentSelectTemplate.value.id,
// });
// 创建一个新的Promise来处理请求
loadLatestMedicalRecordPromise = new Promise(async (resolve, reject) => {
try {
// 获取患者的历史病历记录
const res = await getRecordByEncounterIdList({
isPage: 0,
encounterId: patientInfo.value.encounterId,
patientId: patientInfo.value.patientId,
definitionId: currentSelectTemplate.value.id,
const historyRecords = res.data || [];
if (historyRecords.length > 0) {
// 按时间排序,获取最新的病历记录
historyRecords.sort((a, b) => new Date(b.recordTime) - new Date(a.recordTime));
const latestRecord = historyRecords[0];
// 保存最新病历ID用于在History组件中高亮显示
selectedHistoryRecordId.value = latestRecord.id;
// 自动回显最新病历数据到模板
editForm.value = latestRecord;
nextTick(() => {
if (emrComponentRef.value && latestRecord.contentJson) {
try {
const parsedData = JSON.parse(latestRecord.contentJson);
emrComponentRef.value.setFormData(parsedData);
} catch (parseError) {
console.error('解析病历数据失败:', parseError);
// 解析失败时仍然尝试设置空数据以清空之前的残留数据
emrComponentRef.value.setFormData({});
}
} else {
// 如果没有内容数据,也要清空组件中的数据
emrComponentRef.value.setFormData({});
}
// 通知History组件更新选中状态
if (historyRef.value && typeof historyRef.value.updateSelectedRecord === 'function') {
historyRef.value.updateSelectedRecord(latestRecord.id);
}
});
const historyRecords = res.data || [];
if (historyRecords.length > 0) {
// 按时间排序,获取最新的病历记录
historyRecords.sort((a, b) => new Date(b.recordTime) - new Date(a.recordTime));
const latestRecord = historyRecords[0];
// 保存最新病历ID用于在History组件中高亮显示
selectedHistoryRecordId.value = latestRecord.id;
// 自动回显最新病历数据到模板
editForm.value = latestRecord;
nextTick(() => {
if (emrComponentRef.value && latestRecord.contentJson) {
try {
const parsedData = JSON.parse(latestRecord.contentJson);
emrComponentRef.value.setFormData(parsedData);
} catch (parseError) {
console.error('解析病历数据失败:', parseError);
// 解析失败时仍然尝试设置空数据以清空之前的残留数据
emrComponentRef.value.setFormData({});
}
} else {
// 如果没有内容数据,也要清空组件中的数据
emrComponentRef.value.setFormData({});
}
// 通知History组件更新选中状态
if (historyRef.value && typeof historyRef.value.updateSelectedRecord === 'function') {
historyRef.value.updateSelectedRecord(latestRecord.id);
}
resolve(); // 成功完成
});
} else {
// 清空选中状态
selectedHistoryRecordId.value = '';
// 当没有历史记录时,也要清空当前表单数据,避免显示之前患者的数据
editForm.value = {
id: '',
definitionId: '',
definitionBusNo: '',
contentJson: '',
statusEnum: 1,
organizationId: 0,
encounterId: '',
patientId: '',
recordTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
createBy: '',
source: '',
};
nextTick(() => {
if (emrComponentRef.value) {
emrComponentRef.value.setFormData({});
}
resolve(); // 成功完成
});
loading.value = false;
}
} catch (error) {
ElMessage.error('加载最新病历数据失败=====>', error);
// 出错时也清空选中状态
} else {
// 清空选中状态
selectedHistoryRecordId.value = '';
// 出错时也要清空表单数据,避免显示之前患者的数据
// 当没有历史记录时,也要清空当前表单数据,避免显示之前患者的数据
editForm.value = {
id: '',
definitionId: '',
@@ -660,17 +619,37 @@ const loadLatestMedicalRecord = async () => {
if (emrComponentRef.value) {
emrComponentRef.value.setFormData({});
}
reject(error); // 错误完成
});
loading.value = false;
} finally {
loading.value = false;
// 请求完成后清除Promise引用
loadLatestMedicalRecordPromise = null;
}
});
} catch (error) {
ElMessage.error('加载最新病历数据失败=====>', error);
// 出错时也清空选中状态
selectedHistoryRecordId.value = '';
// 出错时也要清空表单数据,避免显示之前患者的数据
editForm.value = {
id: '',
definitionId: '',
definitionBusNo: '',
contentJson: '',
statusEnum: 1,
organizationId: 0,
encounterId: '',
patientId: '',
recordTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
createBy: '',
source: '',
};
return loadLatestMedicalRecordPromise;
nextTick(() => {
if (emrComponentRef.value) {
emrComponentRef.value.setFormData({});
}
});
loading.value = false;
} finally {
loading.value = false;
}
};
// 选择默认模板 - 获取住院病历分类下的第一个模板
@@ -830,7 +809,7 @@ watch(
}
} else {
// 如果没有患者信息,也要重置组件和表单数据
currentComponent.value = '';
currentComponent.value = undefined;
editForm.value = {
id: '',
definitionId: '',
@@ -856,41 +835,6 @@ watch(
{ deep: true, immediate: true }
);
// 合并两个监听器,避免重复触发
let patientChangeProcessing = false; // 防止重复处理
watch(
() => [patientInfo.value?.encounterId, currentSelectTemplate.value?.id],
([newEncounterId, newTemplateId]) => {
// 当患者就诊ID或模板ID变化时加载最新病历数据
if (newEncounterId && newTemplateId && !patientChangeProcessing) {
patientChangeProcessing = true;
// 添加延迟以确保模板数据已更新
nextTick(() => {
loadLatestMedicalRecord().finally(() => {
// 重置处理标志
patientChangeProcessing = false;
});
});
}
},
{ immediate: true }
);
// 监听模板选择变化,当模板选择变化时加载最新病历数据
watch(
() => currentSelectTemplate.value.id,
(newTemplateId) => {
// 当模板选择变化时,加载该模板的最新病历数据
if (newTemplateId) {
// 只要有模板ID就尝试加载数据不管之前是否有患者信息
// 因为可能是在切换患者后才选择模板
loadLatestMedicalRecord();
}
}
);
onMounted(async () => {
// 移除日志
await queryTemplateTree();
@@ -914,7 +858,43 @@ const onPrint = () => {
}
};
defineExpose({ state });
// 添加一个方法供父组件调用,处理患者切换
const handlePatientChange = (patient) => {
// 更新患者信息
// 注意这里我们不直接修改patientInfo.value因为它是由父组件通过watch管理的
// 我们只需要确保当前组件响应patientInfo的变化
// 重置当前组件,确保在切换患者时重新加载
// 但我们需要确保在组件重新加载后能正确设置患者信息
const selectedTemplateId = currentSelectTemplate.value?.id;
if (selectedTemplateId) {
// 先清空当前组件
currentComponent.value = undefined;
// 使用nextTick确保DOM更新后再重新加载组件和数据
nextTick(() => {
// 重新设置组件
const template = templateData.value?.find(t => t.document && t.document.id === selectedTemplateId);
if (template && template.document?.vueRouter) {
currentComponent.value = template.document.vueRouter;
// 再次使用nextTick确保组件已加载后再加载数据
nextTick(() => {
loadLatestMedicalRecord();
});
} else {
// 如果找不到对应模板,尝试选择默认模板
selectDefaultTemplate();
}
});
} else {
// 如果没有选中模板,尝试选择默认模板
selectDefaultTemplate();
}
};
defineExpose({ state, handlePatientChange });
const disNode = () => {
leftShow.value = !leftShow.value;

View File

@@ -109,25 +109,16 @@ const getList = () => {
});
};
// 标记是否已经手动选择过患者,防止后续自动选择
const hasManuallySelectedPatient = ref(false);
// 添加一个变量来跟踪当前期望的患者ID
let expectedPatientId = null;
watch(
() => filteredCardData.value,
(newData) => {
// 如果有数据且当前没有选中患者,且是首次加载,默认选择第一条
// 只有在从未手动选择过患者的情况下才自动选择
// 并且确保当前没有正在处理的患者切换操作
if (
newData &&
newData.length > 0 &&
!cardId.value &&
isFirstLoad.value &&
!patientInfo.value?.encounterId &&
!hasManuallySelectedPatient.value
!patientInfo.value?.encounterId
) {
const firstPatient = newData[0];
if (firstPatient?.encounterId) {
@@ -139,81 +130,34 @@ watch(
debounceTimer = setTimeout(() => {
handleItemClick(firstPatient);
isFirstLoad.value = false;
hasManuallySelectedPatient.value = true; // 标记已手动选择过
}, 100);
}
} else if (expectedPatientId && cardId.value && cardId.value !== expectedPatientId) {
// 如果当前cardId与期望的不一致且不是初始状态这可能意味着发生了意外的重置
// 这种情况下,我们不希望自动选择第一个患者
console.debug(`期望的患者ID: ${expectedPatientId}, 当前cardId: ${cardId.value}`);
}
},
{ immediate: true }
);
// 更新handleItemClick函数设置期望的患者ID
// 防抖函数,防止快速点击导致状态冲突
let debounceTimer = null;
const handleItemClick = (node) => {
// 设置期望的患者ID
expectedPatientId = node.encounterId;
// 清除之前的计时器
if (debounceTimer) {
clearTimeout(debounceTimer);
}
// 取消之前未完成的患者加载操作
if (currentPatientPromise) {
// 注意这里无法真正取消Promise但我们可以标记当前操作已过期
currentPatientPromise.cancelled = true;
}
// 设置新的计时器
debounceTimer = setTimeout(async () => {
// 检查是否已被取消
if (currentPatientPromise?.cancelled) {
return;
}
debounceTimer = setTimeout(() => {
cardId.value = node.encounterId;
// 同时更新本地和全局状态,确保模块内组件和跨模块组件都能正确响应
updatePatientInfo(node);
updateLocalPatientInfo(node);
// 标记已手动选择患者,防止自动选择第一条
hasManuallySelectedPatient.value = true;
// 创建一个新的Promise来追踪这次加载操作
currentPatientPromise = Promise.all([
// 并行调用医嘱相关的API避免重复请求
adviceRef.value?.getListInfo().catch(error => {
console.error('获取医嘱信息失败:', error);
return null;
}),
adviceRef.value?.getDiagnosisInfo().catch(error => {
console.error('获取诊断信息失败:', error);
return null;
}),
// 获取诊断信息
diagnosisRef.value?.getList?.().catch(error => {
console.error('获取诊断信息失败:', error);
return null;
})
]);
try {
await currentPatientPromise;
// 检查在此期间是否选择了其他患者
if (currentPatientPromise?.cancelled) {
return;
}
} catch (error) {
console.error('加载患者信息时出错:', error);
}
diagnosisRef.value?.getList();
adviceRef.value?.getListInfo();
adviceRef.value?.getDiagnosisInfo();
}, 100); // 100ms 防抖延迟
};
// 防抖函数,防止快速点击导致状态冲突
const handleSearch = (keyword) => {
searchData.keyword = keyword;
getList();

View File

@@ -206,7 +206,7 @@
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template #default="scope">
<el-button type="text" size="small" @click="feeItemsList.splice(scope.$index, 1)">
<el-button link size="small" @click="feeItemsList.splice(scope.$index, 1)">
删除
</el-button>
</template>

View File

@@ -200,7 +200,7 @@
<el-table-column label="金额" prop="amount" width="100" align="center" />
<el-table-column label="退费审核" width="80" align="center">
<template #default="scope">
<el-button type="text" style="color: #409eff">退费申请</el-button>
<el-button link style="color: #409eff">退费申请</el-button>
</template>
</el-table-column>
</el-table>

View File

@@ -74,7 +74,7 @@
</span>
</div>
<!-- <div>
<el-button size="small" type="text" @click="exportData">
<el-button size="small" link @click="exportData">
<el-icon><Download /></el-icon> 导出数据
</el-button>
</div> -->

View File

@@ -1,9 +1,14 @@
<template>
<div class="call-number-display">
<div class="call-number-display" ref="screenContainer">
<!-- 头部区域 -->
<div class="header">
<h1>{{ departmentName }}</h1>
<div class="time">{{ currentTime }}</div>
<div class="header-right">
<button class="fullscreen-btn" @click="toggleFullscreen">
{{ isFullscreen ? '退出全屏' : '全屏' }}
</button>
<div class="time">{{ currentTime }}</div>
</div>
</div>
<!-- 当前呼叫区 -->
@@ -31,19 +36,19 @@
</tr>
</thead>
<tbody>
<template v-for="doctorName in paginatedDoctors" :key="doctorName">
<template v-for="doctorName in paginatedDoctors">
<template v-if="groupedPatients[doctorName]">
<!-- 医生分组标题 -->
<tr class="doctor-header">
<tr class="doctor-header" :key="`doctor-${doctorName}`">
<td colspan="4">{{ doctorName }} 医生 (诊室: {{ getDoctorRoom(doctorName) }})</td>
</tr>
<!-- 患者列表 -->
<tr v-for="(patient, index) in groupedPatients[doctorName]" :key="patient.id">
<tr v-for="(patient, index) in groupedPatients[doctorName]" :key="`${doctorName}-${patient.id}`">
<td>{{ index + 1 }}</td>
<td>{{ formatPatientName(patient.name) }}</td>
<td>{{ patient.name }}</td>
<td>{{ getDoctorRoom(doctorName) }}</td>
<td :style="{ color: index === 0 ? '#e74c3c' : '#27ae60' }">
{{ index === 0 ? '就诊中' : '等待' }}
<td :style="{ color: patient.status === 'CALLING' ? '#e74c3c' : '#27ae60' }">
{{ patient.status === 'CALLING' ? '就诊中' : '等待' }}
</td>
</tr>
</template>
@@ -88,49 +93,67 @@
<script setup>
import { ref, reactive, computed, onMounted, onUnmounted, nextTick, watchEffect } from 'vue'
import { storeToRefs } from 'pinia'
import { ElMessage } from 'element-plus'
import dayjs from 'dayjs'
import request from '@/utils/request'
import useUserStore from '@/store/modules/user'
// ========== 配置参数 ==========
const userStore = useUserStore()
const { orgId: userOrgId, tenantId: userTenantId } = storeToRefs(userStore)
// 从登录用户获取科室ID避免硬编码后端已确保 orgId 以字符串返回)
const ORGANIZATION_ID = computed(() => (userOrgId.value ? String(userOrgId.value) : ''))
const TENANT_ID = computed(() => (userTenantId.value ? Number(userTenantId.value) : 1))
const API_BASE_URL = '/triage/queue'
// SSE 地址(走后端 API 代理)
const SSE_URL = computed(() => {
const baseApi = import.meta.env.VITE_APP_BASE_API || ''
const orgId = ORGANIZATION_ID.value
const tenantId = TENANT_ID.value
return `${baseApi}${API_BASE_URL}/display/stream?organizationId=${encodeURIComponent(orgId)}&tenantId=${tenantId}`
})
// 响应式数据
const currentTime = ref('')
const currentCall = ref({
number: '1',
name: '李*四',
room: '3号'
})
const currentCall = ref(null)
const patients = ref([])
const loading = ref(false)
const currentPage = ref(1)
const patientsPerPage = 5
const autoScrollInterval = ref(null)
const scrollInterval = 5000 // 5秒自动翻页
const sseConnection = ref(null) // SSE 连接
const timeInterval = ref(null)
const isFullscreen = ref(false)
const screenContainer = ref(null)
let tableContainer = null
// 科室名称
const departmentName = ref('心内科叫号显示屏幕')
const departmentName = ref('叫号显示屏幕')
// 计算属性
const applyDefaultDepartmentName = () => {
if (userStore.orgName) {
departmentName.value = `${userStore.orgName} 叫号显示屏`
}
}
// 等待总人数(从后端返回)
const waitingCount = ref(0)
// 计算属性:按医生分组的患者列表
const groupedPatients = computed(() => {
const grouped = {}
patients.value.forEach(patient => {
if (!grouped[patient.doctor]) {
grouped[patient.doctor] = []
}
grouped[patient.doctor].push(patient)
patients.value.forEach(doctorGroup => {
grouped[doctorGroup.doctorName] = doctorGroup.patients || []
})
return grouped
})
const waitingCount = computed(() => {
let count = 0
Object.values(groupedPatients.value).forEach(group => {
count += Math.max(0, group.length - 1) // 排除每个医生组中第一个就诊中的患者
})
return count
})
// 获取排序后的医生列表
const sortedDoctors = computed(() => {
return Object.keys(groupedPatients.value).sort()
return patients.value.map(group => group.doctorName)
})
// 按医生分组的分页逻辑
@@ -159,6 +182,24 @@ const updateTime = () => {
currentTime.value = now.format('YYYY-MM-DD HH:mm')
}
const updateFullscreenState = () => {
const isActive = !!document.fullscreenElement
isFullscreen.value = isActive
document.body.classList.toggle('call-screen-fullscreen', isActive)
}
const toggleFullscreen = async () => {
try {
if (document.fullscreenElement) {
await document.exitFullscreen()
} else if (screenContainer.value && screenContainer.value.requestFullscreen) {
await screenContainer.value.requestFullscreen()
}
} catch (error) {
console.error('切换全屏失败:', error)
}
}
const formatPatientName = (name) => {
if (!name || typeof name !== 'string') return '-'
if (name.length === 0) return '-'
@@ -166,49 +207,98 @@ const formatPatientName = (name) => {
}
const getDoctorRoom = (doctorName) => {
// 根据医生获取固定诊室
const doctorRooms = {
'张医生': '3号',
'李医生': '1号',
'王医生': '2号'
}
return doctorRooms[doctorName] || '1号'
// 从后端数据中查找医生的诊室
const doctorGroup = patients.value.find(group => group.doctorName === doctorName)
return doctorGroup?.roomNo || '1号'
}
const generateWaitingData = async () => {
const ensureUserInfo = async () => {
if (!userStore.orgId) {
try {
await userStore.getInfo()
} catch (error) {
console.error('获取用户信息失败:', error)
}
}
applyDefaultDepartmentName()
}
/**
* 获取显示屏数据从后端API
*/
const fetchDisplayData = async () => {
try {
if (!ORGANIZATION_ID.value) {
ElMessage.warning('未获取到登录用户科室信息')
return
}
loading.value = true
// 确保数组已正确初始化
if (!Array.isArray(patients.value)) {
patients.value = []
console.log('正在获取显示屏数据...', {
url: `${API_BASE_URL}/display`,
organizationId: ORGANIZATION_ID.value,
tenantId: TENANT_ID.value
})
const response = await request({
url: `${API_BASE_URL}/display`,
method: 'get',
params: {
organizationId: ORGANIZATION_ID.value,
tenantId: TENANT_ID.value
}
})
console.log('后端响应:', response)
if (response.code === 200 && response.data) {
const data = response.data
// 更新科室名称
if (data.departmentName && data.departmentName !== '叫号显示屏') {
departmentName.value = data.departmentName
} else {
applyDefaultDepartmentName()
}
// 更新当前叫号信息
if (data.currentCall) {
currentCall.value = data.currentCall
} else {
currentCall.value = {
number: null,
name: '-',
room: '-',
doctor: '-'
}
}
// 更新等候队列(按医生分组)
if (data.waitingList && Array.isArray(data.waitingList)) {
patients.value = data.waitingList
console.log('等候队列数据:', data.waitingList)
} else {
patients.value = []
console.log('等候队列为空')
}
// 更新等待人数
waitingCount.value = data.waitingCount || 0
console.log('显示屏数据更新成功', data)
ElMessage.success('数据加载成功')
} else {
throw new Error(response.msg || '获取数据失败')
}
// 模拟API调用获取候诊数据
// 实际项目中这里应该调用真实API
const mockData = [
{ id: 13, name: '李四', type: '专家', doctor: '张医生', status: '就诊中' },
{ id: 14, name: '王五', type: '普通', doctor: '李医生', status: '候诊中' },
{ id: 15, name: '赵六', type: '专家', doctor: '张医生', status: '候诊中' },
{ id: 16, name: '钱七', type: '普通', doctor: '王医生', status: '候诊中' },
{ id: 17, name: '孙八', type: '专家', doctor: '李医生', status: '候诊中' },
{ id: 18, name: '周九', type: '普通', doctor: '王医生', status: '候诊中' },
{ id: 19, name: '吴十', type: '专家', doctor: '张医生', status: '候诊中' },
{ id: 20, name: '郑一', type: '普通', doctor: '李医生', status: '候诊中' },
{ id: 21, name: '王二', type: '专家', doctor: '王医生', status: '候诊中' },
{ id: 22, name: '李三', type: '普通', doctor: '张医生', status: '候诊中' },
{ id: 23, name: '赵四', type: '专家', doctor: '李医生', status: '候诊中' },
{ id: 24, name: '钱五', type: '普通', doctor: '王医生', status: '候诊中' }
]
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 500))
patients.value = mockData
} catch (error) {
console.error('获取候诊数据失败:', error)
ElMessage.error('获取候诊数据失败')
// 出错时设置为空数组
console.error('获取显示屏数据失败:', error)
ElMessage.error('获取显示屏数据失败' + (error.message || '未知错误'))
// 出错时设置默认值
patients.value = []
currentCall.value = { number: null, name: '-', room: '-', doctor: '-' }
waitingCount.value = 0
applyDefaultDepartmentName()
} finally {
loading.value = false
}
@@ -259,40 +349,158 @@ const stopAutoScroll = () => {
}
}
/**
* 初始化 SSE 连接
*/
const initSse = () => {
try {
if (!ORGANIZATION_ID.value) {
console.warn('未获取到科室ID跳过 SSE 连接')
return
}
if (sseConnection.value) {
sseConnection.value.close()
}
console.log('正在连接 SSE:', SSE_URL.value)
sseConnection.value = new EventSource(SSE_URL.value)
sseConnection.value.onopen = () => {
console.log('SSE 连接成功')
ElMessage.success('实时连接已建立')
}
sseConnection.value.onmessage = (event) => {
try {
const message = JSON.parse(event.data)
console.log('收到 SSE 消息:', message)
if (message.type === 'init') {
handleSseUpdate(message.data)
} else if (message.type === 'update') {
handleSseUpdate(message.data)
}
} catch (error) {
console.error('解析 SSE 消息失败:', error)
}
}
sseConnection.value.onerror = (error) => {
console.error('SSE 错误:', error)
ElMessage.error('实时连接出现错误')
}
} catch (error) {
console.error('初始化 SSE 失败:', error)
}
}
/**
* 处理 SSE 推送的更新数据
*/
const handleSseUpdate = (data) => {
if (!data) return
// 更新科室名称
if (data.departmentName) {
departmentName.value = data.departmentName
}
// 更新当前叫号信息
if (data.currentCall) {
currentCall.value = data.currentCall
}
// 更新等候队列
if (data.waitingList && Array.isArray(data.waitingList)) {
patients.value = data.waitingList
}
// 更新等待人数
if (data.waitingCount !== undefined) {
waitingCount.value = data.waitingCount
}
console.log('显示屏数据已更新(来自 SSE')
// 播放语音(如果有新的叫号)
if (data.currentCall && data.currentCall.number) {
playVoiceNotification(data.currentCall)
}
}
/**
* 播放语音通知
*/
const playVoiceNotification = (callInfo) => {
if (!callInfo || !callInfo.number) return
try {
// 使用 Web Speech API 播放语音
const utterance = new SpeechSynthesisUtterance(
`${callInfo.number}${callInfo.name}${callInfo.room}诊室就诊`
)
utterance.lang = 'zh-CN'
utterance.rate = 0.9 // 语速
utterance.pitch = 1.0 // 音调
utterance.volume = 1.0 // 音量
window.speechSynthesis.speak(utterance)
} catch (error) {
console.error('语音播放失败:', error)
}
}
/**
* 关闭 SSE 连接
*/
const closeSse = () => {
if (sseConnection.value) {
sseConnection.value.close()
sseConnection.value = null
console.log('SSE 连接已关闭')
}
}
// 生命周期钩子
onMounted(async () => {
document.addEventListener('fullscreenchange', updateFullscreenState)
await ensureUserInfo()
// 初始化时间
updateTime()
// 每分钟更新时间
const timeInterval = setInterval(updateTime, 60000)
timeInterval.value = setInterval(updateTime, 60000)
// 获取候诊数据
await generateWaitingData()
// ✅ 获取初始数据(从后端 API
await fetchDisplayData()
// ✅ 初始化 SSE 连接(实时推送)
initSse()
// 启动自动滚动
startAutoScroll()
// 鼠标悬停时暂停自动滚动
const tableContainer = document.querySelector('.table-container')
tableContainer = document.querySelector('.table-container')
if (tableContainer) {
tableContainer.addEventListener('mouseenter', stopAutoScroll)
tableContainer.addEventListener('mouseleave', startAutoScroll)
}
// 组件卸载时清理
onUnmounted(() => {
clearInterval(timeInterval)
stopAutoScroll()
if (tableContainer) {
tableContainer.removeEventListener('mouseenter', stopAutoScroll)
tableContainer.removeEventListener('mouseleave', startAutoScroll)
}
})
})
onUnmounted(() => {
// 组件卸载时的清理工作
if (timeInterval.value) {
clearInterval(timeInterval.value)
timeInterval.value = null
}
stopAutoScroll()
closeSse() // ✅ 关闭 SSE 连接
if (tableContainer) {
tableContainer.removeEventListener('mouseenter', stopAutoScroll)
tableContainer.removeEventListener('mouseleave', startAutoScroll)
tableContainer = null
}
document.removeEventListener('fullscreenchange', updateFullscreenState)
document.body.classList.remove('call-screen-fullscreen')
})
// 监听页面变化,重置滚动位置
@@ -317,6 +525,43 @@ watchEffect(() => {
flex-direction: column;
}
:global(body.call-screen-fullscreen) {
overflow: hidden;
background: #f5f7fa;
}
:global(body.call-screen-fullscreen .sidebar-wrapper),
:global(body.call-screen-fullscreen .navbar),
:global(body.call-screen-fullscreen .tags-view-container),
:global(body.call-screen-fullscreen #tags-view-container),
:global(body.call-screen-fullscreen .drawer-bg) {
display: none !important;
}
:global(body.call-screen-fullscreen .app-wrapper),
:global(body.call-screen-fullscreen .main-wrapper),
:global(body.call-screen-fullscreen .content-wrapper),
:global(body.call-screen-fullscreen .app-main) {
width: 100% !important;
height: 100% !important;
margin: 0 !important;
padding: 0 !important;
}
:global(body.call-screen-fullscreen .app-wrapper) {
height: 100vh !important;
}
:global(body.call-screen-fullscreen .call-number-display) {
max-width: none;
width: 100vw;
height: 100vh;
min-height: 100vh;
margin: 0;
border-radius: 0;
box-shadow: none;
}
/* 头部样式 */
.header {
background: linear-gradient(135deg, #4a90e2, #5fa3e8);
@@ -342,6 +587,28 @@ watchEffect(() => {
padding: 5px 15px;
border-radius: 30px;
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
.fullscreen-btn {
border: 1px solid rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.15);
color: #fff;
padding: 6px 14px;
border-radius: 20px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s ease, border-color 0.2s ease;
}
.fullscreen-btn:hover {
background: rgba(255, 255, 255, 0.28);
border-color: rgba(255, 255, 255, 0.9);
}
}
/* 当前呼叫区 */

View File

@@ -815,6 +815,8 @@ const loadDataFromApi = async () => {
appointmentType: item.healthcareName ?? '普通',
room: item.organizationName ? `${item.organizationName}` : '-',
doctor: item.practitionerName ?? '-',
// 当前接口返回的是 practitionerUserId保存为 practitionerId 供入队使用
practitionerId: item.practitionerUserId ?? null,
matchingRule: '-' // 这里先不做智能规则匹配
}))
console.log('【心内科】候选池已加载', originalCandidatePoolList.value.length, '条今天的数据')
@@ -1031,7 +1033,9 @@ const handleAddToQueue = async () => {
patientId: c.patientId,
patientName: c.patientName,
healthcareName: c.appointmentType,
practitionerName: c.doctor
practitionerName: c.doctor,
practitionerId: c.practitionerId ?? null,
roomNo: c.roomNo ?? c.room ?? null
})
})
@@ -1144,7 +1148,9 @@ const handleAddAllToQueue = async () => {
patientId: c.patientId,
patientName: c.patientName,
healthcareName: c.appointmentType,
practitionerName: c.doctor
practitionerName: c.doctor,
practitionerId: c.practitionerId ?? null,
roomNo: c.roomNo ?? c.room ?? null
})
})