feat(menu): 添加菜单完整路径功能和待写病历管理

- 在SysMenu实体类中新增fullPath字段用于存储完整路径
- 实现buildMenuTreeWithFullPath方法构建带完整路径的菜单树
- 添加getMenuFullPath和generateFullPath服务方法获取和生成完整路径
- 在菜单控制器中增加获取完整路径的API接口
- 前端菜单组件显示完整路径并在新增修改时使用后端返回的路径
- 添加待写病历管理功能包括获取待写病历列表、数量统计和检查接口
- 在医生工作站界面集成待写病历选项卡和相关处理逻辑
- 更新首页统计数据接口路径并添加待写病历数量获取功能
- 重构首页快捷功能配置为动态从数据库获取用户自定义配置
- 优化菜单列表查询使用异步方式处理带完整路径的菜单数据
- 添加菜单完整路径的数据库映射配置和前端API调用支持
This commit is contained in:
2026-02-01 14:50:22 +08:00
parent 29ecfd90f2
commit 0a08088ada
14 changed files with 1240 additions and 163 deletions

View File

@@ -1,9 +1,17 @@
import request from '@/utils/request'
import request from '@/utils/request';
// 获取首页统计数据
export function getHomeStatistics() {
return request({
url: '/home/statistics',
url: '/system/home/statistics',
method: 'get'
})
});
}
// 获取待写病历数量
export function getPendingEmrCount() {
return request({
url: '/doctor-station/pending-emr/pending-count',
method: 'get'
});
}

View File

@@ -57,4 +57,24 @@ export function delMenu(menuId) {
url: '/system/menu/' + menuId,
method: 'delete'
})
}
// 获取菜单完整路径
export function getMenuFullPath(menuId) {
return request({
url: '/system/menu/fullPath/' + menuId,
method: 'get'
})
}
// 生成完整路径
export function generateFullPath(parentId, currentPath) {
return request({
url: '/system/menu/generateFullPath',
method: 'post',
params: {
parentId: parentId,
currentPath: currentPath
}
})
}

View File

@@ -1,4 +1,7 @@
import {parseTime} from './openhis'
import { parseTime } from './openhis'
// 导出 parseTime 函数以供其他模块使用
export { parseTime }
/**
* 表格时间格式化

View File

@@ -129,6 +129,39 @@ export function saveEmrTemplate(data) {
});
}
/**
* 获取待写病历列表
*/
export function listPendingEmr(queryParams) {
return request({
url: '/doctor-station/pending-emr/pending-list',
method: 'get',
params: queryParams,
});
}
/**
* 获取待写病历数量
*/
export function getPendingEmrCount(doctorId) {
return request({
url: '/doctor-station/pending-emr/pending-count',
method: 'get',
params: { doctorId },
});
}
/**
* 检查患者是否需要写病历
*/
export function checkNeedWriteEmr(encounterId) {
return request({
url: '/doctor-station/pending-emr/need-write-emr',
method: 'get',
params: { encounterId },
});
}
// 诊断相关接口
/**
* 保存诊断

View File

@@ -129,6 +129,9 @@
<el-tab-pane label="门诊病历" name="hospitalizationEmr">
<hospitalizationEmr :patientInfo="patientInfo" :activeTab="activeTab" @emrSaved="handleEmrSaved" />
</el-tab-pane>
<el-tab-pane label="待写病历" name="pendingEmr">
<PendingEmr @writeEmr="handleWriteEmr" @viewPatient="handleViewPatient" />
</el-tab-pane>
<!-- <el-tab-pane label="病历" name="emr">
<Emr
:patientInfo="patientInfo"
@@ -188,6 +191,7 @@
</template>
<script setup>
import hospitalizationEmr from './components/hospitalizationEmr/index.vue';
import PendingEmr from './components/pendingEmr/index.vue';
import {
completeEncounter,
getEncounterDiagnosis,
@@ -214,6 +218,8 @@ import { nextTick } from 'vue';
import { updatePatientInfo } from './components/store/patient.js';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useRoute } from 'vue-router';
// // 监听路由离开事件
// onBeforeRouteLeave((to, from, next) => {
// // 弹出确认框
@@ -228,6 +234,20 @@ defineOptions({
name: 'PatientParentCard',
});
const route = useRoute();
// 监听路由参数变化
watch(
() => route.query.tab,
(newTab) => {
if (newTab === 'pendingEmr') {
console.log('Route tab changed to pendingEmr');
activeTab.value = 'pendingEmr';
}
},
{ immediate: true }
);
const userStore = useUserStore();
const bedfont = 'bed-font';
const queryParams = ref({
@@ -321,6 +341,21 @@ onMounted(() => {
getWaitPatient();
getWaitPatientList();
getPatientList();
// 检查路由参数,如果指定了待写病历,则默认选中该选项卡
console.log('Route query:', route.query); // 调试信息
if (route.query.tab === 'pendingEmr') {
console.log('Switching to pendingEmr tab'); // 调试信息
activeTab.value = 'pendingEmr';
}
// 确保DOM更新后激活正确的选项卡
nextTick(() => {
if (route.query.tab === 'pendingEmr') {
// 强制触发选项卡切换
handleClick('pendingEmr');
}
});
});
// 获取现诊患者列表
function getPatientList() {
@@ -622,6 +657,20 @@ function handleEmrSaved(isSaved) {
outpatientEmrSaved.value = isSaved;
}
// 处理写病历事件
function handleWriteEmr(row) {
console.log('处理写病历:', row);
// 这里可以触发切换到病历页面并加载患者信息
// 可以根据需要实现具体逻辑
}
// 处理查看患者事件
function handleViewPatient(row) {
console.log('处理查看患者:', row);
// 这里可以触发查看患者详细信息的逻辑
// 可以根据需要实现具体逻辑
}
function openDrawer() {
drawer.value = true;
}

View File

@@ -46,27 +46,31 @@
<div class="quick-access-section">
<div class="section-header">
<h3>快捷功能</h3>
<el-button text type="primary" @click="showAllFunctions">查看全部</el-button>
<div>
<el-button text type="primary" @click="showConfig">配置</el-button>
</div>
</div>
<div class="quick-access-grid">
<div
v-for="func in quickAccess"
:key="func.key"
class="quick-access-card"
@click="handleQuickAccess(func)"
>
<div class="quick-icon">
<el-icon :size="28" :color="func.iconColor">
<component :is="func.icon" />
</el-icon>
<div v-loading="quickAccessLoading" element-loading-text="加载快捷功能...">
<div class="quick-access-grid">
<div
v-for="func in quickAccess"
:key="func.key"
class="quick-access-card"
@click="handleQuickAccess(func)"
>
<div class="quick-icon">
<el-icon :size="28" :color="func.iconColor">
<component :is="func.icon" />
</el-icon>
</div>
<div class="quick-label">{{ func.label }}</div>
</div>
<div class="quick-label">{{ func.label }}</div>
</div>
</div>
</div>
<!-- 待办事项 -->
<div class="todo-section" v-if="todoList.length > 0">
<div class="todo-section">
<div class="section-header">
<h3>待办事项</h3>
<el-badge :value="todoList.length" class="todo-badge">
@@ -92,6 +96,9 @@
</div>
<div class="todo-time">{{ todo.time }}</div>
</div>
<div v-if="todoList.length === 0" class="empty-todo">
<el-empty description="暂无待办事项" :image-size="60" />
</div>
</div>
</div>
@@ -123,11 +130,15 @@
</template>
<script setup name="Home">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { markRaw } from 'vue'
import useUserStore from '@/store/modules/user'
import { getHomeStatistics } from '@/api/home'
import { getHomeStatistics, getPendingEmrCount } from '@/api/home'
import { listTodo } from '@/api/workflow/task'
import { getCurrentUserConfig } from '@/api/system/userConfig'
import { listMenu, getMenuFullPath } from '@/api/system/menu'
import { ElDivider } from 'element-plus'
import {
User,
Document,
@@ -149,9 +160,67 @@ import {
Van,
Bell,
Setting,
Search
Search,
Menu,
Grid,
Folder,
Tickets,
ChatDotSquare,
Histogram,
OfficeBuilding,
Postcard,
Collection,
VideoPlay,
Camera,
Headset,
Phone,
Message,
ChatLineSquare,
ChatRound,
Guide,
Help,
InfoFilled,
CircleCheck,
CircleClose,
QuestionFilled,
Star,
Link,
Position,
Picture,
Upload,
Download,
CaretLeft,
CaretRight,
More,
Close,
Check,
ArrowUp,
ArrowDown,
ArrowLeft,
ArrowRight,
Plus,
Minus,
ZoomIn,
ZoomOut,
Refresh,
Edit,
Delete,
Share,
View,
SwitchButton,
Hide,
Finished,
CirclePlus,
Remove,
CircleCheckFilled,
CircleCloseFilled,
WarningFilled,
Goods
} from '@element-plus/icons-vue'
// 为别名单独导入
import { InfoFilled as InfoFilledIcon, QuestionFilled as QuestionFilledIcon, SuccessFilled } from '@element-plus/icons-vue'
const userStore = useUserStore()
const router = useRouter()
@@ -180,7 +249,7 @@ const roleStatsConfig = {
doctor: [
{ key: 'myPatients', label: '我的患者', icon: markRaw(User), type: 'primary', iconColor: '#409eff' },
{ key: 'todayAppointments', label: '今日门诊', icon: markRaw(Calendar), type: 'success', iconColor: '#67c23a' },
{ key: 'pendingRecords', label: '待写病历', icon: markRaw(Document), type: 'warning', iconColor: '#e6a23c' },
{ key: 'pendingEmr', label: '待写病历', icon: markRaw(Document), type: 'warning', iconColor: '#e6a23c' },
{ key: 'prescriptions', label: '今日处方', icon: markRaw(Box), type: 'info', iconColor: '#909399' }
],
nurse: [
@@ -203,60 +272,371 @@ const roleStatsConfig = {
]
}
// 不同角色的快捷功能配置
const roleQuickAccessConfig = {
admin: [
{ key: 'patient', label: '患者管理', icon: markRaw(User), iconColor: '#409eff', route: '/patientmanagement' },
{ key: 'appointment', label: '预约管理', icon: markRaw(Calendar), iconColor: '#67c23a', route: '/appoinmentmanage' },
{ key: 'doctor', label: '医生管理', icon: markRaw(User), iconColor: '#e6a23c', route: '/doctorstation' },
{ key: 'surgery', label: '手术管理', icon: markRaw(Operation), iconColor: '#f56c6c', route: '/surgerymanage' },
{ key: 'drug', label: '药品管理', icon: markRaw(Box), iconColor: '#909399', route: '/pharmacymanagement' },
{ key: 'statistic', label: '数据统计', icon: markRaw(TrendCharts), iconColor: '#409eff', route: '/monitor' },
{ key: 'invoice', label: '发票管理', icon: markRaw(Files), iconColor: '#67c23a', route: '/basicmanage/InvoiceManagement' },
{ key: 'system', label: '系统设置', icon: markRaw(Setting), iconColor: '#909399', route: '/system' }
],
doctor: [
{ key: 'outpatient', label: '门诊接诊', icon: markRaw(ChatDotRound), iconColor: '#409eff', route: '/doctorstation' },
{ key: 'emr', label: '病历管理', icon: markRaw(Document), iconColor: '#67c23a', route: '/doctorstation/doctorphrase' },
{ key: 'prescription', label: '开立处方', icon: markRaw(Box), iconColor: '#e6a23c', route: '/clinicmanagement/ePrescribing' },
{ key: 'history', label: '历史处方', icon: markRaw(Clock), iconColor: '#f56c6c', route: '/clinicmanagement/historicalPrescription' },
{ key: 'schedule', label: '排班管理', icon: markRaw(Calendar), iconColor: '#909399', route: '/appoinmentmanage/deptManage' },
{ key: 'inquiry', label: '患者查询', icon: markRaw(Search), iconColor: '#409eff', route: '/patientmanagement' }
],
nurse: [
{ key: 'ward', label: '病房管理', icon: markRaw(User), iconColor: '#409eff', route: '/inpatientNurse/inpatientNurseStation' },
{ key: 'execution', label: '医嘱执行', icon: markRaw(Operation), iconColor: '#67c23a', route: '/inpatientNurse/medicalOrderExecution' },
{ key: 'proofread', label: '医嘱核对', icon: markRaw(Document), iconColor: '#e6a23c', route: '/inpatientNurse/medicalOrderProofread' },
{ key: 'drugCollect', label: '领药管理', icon: markRaw(Box), iconColor: '#f56c6c', route: '/inpatientNurse/medicineCollect' },
{ key: 'tpr', label: '体温单', icon: markRaw(Monitor), iconColor: '#909399', route: '/inpatientNurse/tprsheet' },
{ key: 'nursing', label: '护理记录', icon: markRaw(ChatDotRound), iconColor: '#409eff', route: '/inpatientNurse/nursingRecord' }
],
pharmacist: [
{ key: 'dispensing', label: '发药管理', icon: markRaw(Box), iconColor: '#409eff', route: '/pharmacymanagement' },
{ key: 'prescription', label: '处方审核', icon: markRaw(Document), iconColor: '#67c23a', route: '/pharmacymanagement' },
{ key: 'inventory', label: '库存管理', icon: markRaw(Van), iconColor: '#e6a23c', route: '/medicineStorage' },
{ key: 'purchase', label: '采购管理', icon: markRaw(ShoppingCart), iconColor: '#f56c6c', route: '/medicineStorage' },
{ key: 'warning', label: '效期预警', icon: markRaw(Warning), iconColor: '#f56c6c', route: '/medicationmanagement/statisticalManagement/statisticalManagement' },
{ key: 'statistics', label: '用药统计', icon: markRaw(DataLine), iconColor: '#909399', route: '/monitor' }
],
cashier: [
{ key: 'registration', label: '挂号收费', icon: markRaw(Money), iconColor: '#409eff', route: '/charge/outpatientregistration' },
{ key: 'clinicCharge', label: '门诊收费', icon: markRaw(Wallet), iconColor: '#67c23a', route: '/charge/cliniccharge' },
{ key: 'refund', label: '退费管理', icon: markRaw(Document), iconColor: '#e6a23c', route: '/charge/clinicrefund' },
{ key: 'invoice', label: '发票打印', icon: markRaw(Files), iconColor: '#f56c6c', route: '/basicmanage/InvoiceManagement' },
{ key: 'record', label: '收费记录', icon: markRaw(Clock), iconColor: '#909399', route: '/charge/clinicRecord' },
{ key: 'insurance', label: '医保结算', icon: markRaw(Bell), iconColor: '#409eff', route: '/ybmanagement' }
]
}
// 从数据库获取用户配置的快捷功能ID列表
const getUserQuickAccessConfig = async () => {
try {
// 优先从数据库获取配置
const response = await getCurrentUserConfig('homeFeaturesConfig');
console.log('从数据库获取的配置响应:', response);
// 检查响应结构,数据可能在 data 或 msg 字段中
let configData = null;
if (response.code === 200) {
if (response.data) {
configData = response.data;
} else if (response.msg) {
// 如果数据在 msg 字段中可能是URL编码的
configData = response.msg;
}
}
if (configData) {
// 解码配置值如果是URL编码的
let decodedData = configData;
try {
decodedData = decodeURIComponent(configData);
} catch (decodeError) {
console.warn('解码配置数据失败,使用原始数据:', decodeError);
decodedData = configData;
}
console.log('解码后的配置数据:', decodedData);
const parsedData = JSON.parse(decodedData);
console.log('解析后的配置数据:', parsedData);
return parsedData;
}
} catch (error) {
console.error('从数据库获取用户配置失败:', error);
}
// 如果数据库中没有配置,尝试从本地存储获取
try {
const savedConfig = localStorage.getItem('homeFeaturesConfig');
console.log('从本地存储获取的配置:', savedConfig);
if (savedConfig) {
const parsedData = JSON.parse(savedConfig);
console.log('从本地存储解析的配置数据:', parsedData);
return parsedData;
}
} catch (error) {
console.error('从本地存储获取用户配置失败:', error);
}
// 如果没有配置,返回空数组
console.log('没有找到用户配置,返回空数组');
return [];
};
// 响应式数据存储快捷访问功能
const quickAccessData = ref([]);
// 根据用户配置获取快捷功能
const quickAccess = computed(() => {
return quickAccessData.value;
});
// 添加 loading 状态
const quickAccessLoading = ref(false);
// 异步加载用户配置
const loadUserQuickAccessConfig = async () => {
quickAccessLoading.value = true;
try {
console.log('开始加载用户快捷访问配置...');
const userConfig = await getUserQuickAccessConfig();
console.log('获取到的用户配置:', userConfig);
// 如果用户没有配置任何功能,返回默认配置
if (!userConfig || userConfig.length === 0) {
console.log('用户没有配置任何功能,使用默认配置');
quickAccessData.value = getDefaultQuickAccessConfig();
return;
}
// 如果用户配置了功能ID列表需要从菜单中获取详细信息
console.log('开始转换菜单ID为快捷访问格式...');
const convertedFeatures = await convertMenuIdsToQuickAccess(userConfig);
console.log('转换后的功能:', convertedFeatures);
// 如果转换后没有功能可能是因为配置的ID没有对应的菜单项返回默认配置
if (!convertedFeatures || convertedFeatures.length === 0) {
console.log('转换后没有功能,使用默认配置');
quickAccessData.value = getDefaultQuickAccessConfig();
return;
}
console.log('设置最终的快捷访问数据:', convertedFeatures);
quickAccessData.value = convertedFeatures;
} catch (error) {
console.error('加载用户快捷访问配置失败:', error);
// 出错时使用默认配置
quickAccessData.value = getDefaultQuickAccessConfig();
} finally {
quickAccessLoading.value = false;
}
};
// 将菜单ID转换为快捷访问格式
const convertMenuIdsToQuickAccess = async (menuIds) => {
if (!menuIds || menuIds.length === 0) {
return [];
}
try {
// 检查 menuIds 是否已经是包含完整路径的对象数组(新格式)
if (menuIds.length > 0 && typeof menuIds[0] === 'object') {
// 检查是否包含 fullPath 属性(新格式)或 menuId 属性(新格式但可能还没获取完整路径)
if (menuIds[0].hasOwnProperty('fullPath') || menuIds[0].hasOwnProperty('menuId')) {
// 如果是新格式,直接转换为快捷功能格式
return menuIds.map(menuItem => {
// 如果没有 fullPath使用 path 作为备选
let route = menuItem.fullPath || menuItem.path;
// 确保路径格式正确,去除多余的斜杠
if (route && typeof route === 'string') {
// 将多个连续的斜杠替换为单个斜杠,但保留协议部分的双斜杠(如 http://
route = route.replace(/([^:])\/{2,}/g, '$1/');
}
return {
key: menuItem.menuId,
label: menuItem.menuName,
icon: getIconComponent(menuItem.icon || 'Document'), // 使用菜单项的图标,如果没有则使用默认图标
iconColor: getIconColorByMenuType(menuItem.menuType) || '#67C23A', // 使用菜单类型的颜色,如果没有则使用默认颜色
route: route
};
}).filter(item => item.route); // 过滤掉 route 为空的项
}
}
// 如果是旧格式仅包含菜单ID的数组则按原方式处理
// 获取所有菜单数据
const response = await listMenu({});
if (response.code === 200) {
const allMenus = response.data;
// 扁平化菜单树结构,获取所有叶子节点(菜单项)
const flatMenus = flattenMenuTree(allMenus);
// 根据用户选择的菜单ID过滤并返回对应的功能对象
const menuPromises = menuIds.map(async (id) => {
// 查找匹配的菜单项
const matchedMenu = flatMenus.find(menu => menu.menuId == id);
if (matchedMenu) {
// 获取完整路径
try {
const fullPathResponse = await getMenuFullPath(matchedMenu.menuId);
// 确保返回的路径不为空
const fullPath = (fullPathResponse.code === 200 && fullPathResponse.data)
? fullPathResponse.data
: (matchedMenu.path || matchedMenu.fullPath);
// 将菜单数据转换为快捷功能格式
return {
key: matchedMenu.perms || matchedMenu.path || `menu_${matchedMenu.menuId}`,
label: matchedMenu.menuName,
icon: getIconComponent(matchedMenu.icon),
iconColor: getIconColorByMenuType(matchedMenu.menuType),
route: fullPath || matchedMenu.path // 确保 route 不为空
};
} catch (error) {
console.error(`获取菜单 ${matchedMenu.menuName} 的完整路径失败:`, error);
// 如果获取完整路径失败,使用现有路径作为备选
return {
key: matchedMenu.perms || matchedMenu.path || `menu_${matchedMenu.menuId}`,
label: matchedMenu.menuName,
icon: getIconComponent(matchedMenu.icon),
iconColor: getIconColorByMenuType(matchedMenu.menuType),
route: matchedMenu.path || matchedMenu.fullPath || '/' // 确保 route 不为空
};
}
}
return null;
});
// 等待所有完整路径获取完成
const convertedMenus = await Promise.all(menuPromises);
// 过滤掉 route 为空的项和未找到的菜单ID
return convertedMenus.filter(item => item !== null && item.route);
}
} catch (error) {
console.error('获取菜单数据失败:', error);
}
// 如果获取失败,返回空数组
return [];
};
// 将菜单树结构扁平化
const flattenMenuTree = (menuTree) => {
const result = [];
const flatten = (items) => {
items.forEach(item => {
// 只处理菜单类型为'C'(菜单)的项目,忽略目录('M')和按钮('F')
if (item.menuType === 'C') {
result.push(item);
}
// 递归处理子菜单
if (item.children && item.children.length > 0) {
flatten(item.children);
}
});
};
flatten(menuTree);
return result;
};
// 获取图标组件
const getIconComponent = (iconName) => {
if (!iconName) return Document;
// 移除前缀,如 fa-, el-icon-
const cleanIconName = iconName.replace(/^(fa-|el-icon-)/, '').toLowerCase();
const iconMap = {
'menu': markRaw(Menu),
'grid': markRaw(Grid),
'folder': markRaw(Folder),
'tickets': markRaw(Tickets),
'document': markRaw(Document),
'setting': markRaw(Setting),
'user': markRaw(User),
'goods': markRaw(Goods),
'chat-dot-square': markRaw(ChatDotSquare),
'histogram': markRaw(Histogram),
'wallet': markRaw(Wallet),
'office-building': markRaw(OfficeBuilding),
'postcard': markRaw(Postcard),
'collection': markRaw(Collection),
'video-play': markRaw(VideoPlay),
'camera': markRaw(Camera),
'headset': markRaw(Headset),
'phone': markRaw(Phone),
'message': markRaw(Message),
'chat-line-square': markRaw(ChatLineSquare),
'chat-round': markRaw(ChatRound),
'guide': markRaw(Guide),
'help': markRaw(Help),
'info-filled': markRaw(InfoFilled),
'circle-check': markRaw(CircleCheck),
'circle-close': markRaw(CircleClose),
'warning': markRaw(Warning),
'question-filled': markRaw(QuestionFilled),
'star': markRaw(Star),
'link': markRaw(Link),
'position': markRaw(Position),
'picture': markRaw(Picture),
'upload': markRaw(Upload),
'download': markRaw(Download),
'caret-left': markRaw(CaretLeft),
'caret-right': markRaw(CaretRight),
'more': markRaw(More),
'close': markRaw(Close),
'check': markRaw(Check),
'arrow-up': markRaw(ArrowUp),
'arrow-down': markRaw(ArrowDown),
'arrow-left': markRaw(ArrowLeft),
'arrow-right': markRaw(ArrowRight),
'plus': markRaw(Plus),
'minus': markRaw(Minus),
'zoom-in': markRaw(ZoomIn),
'zoom-out': markRaw(ZoomOut),
'refresh': markRaw(Refresh),
'search': markRaw(Search),
'edit': markRaw(Edit),
'delete': markRaw(Delete),
'share': markRaw(Share),
'view': markRaw(View),
'switch-button': markRaw(SwitchButton),
'hide': markRaw(Hide),
'finished': markRaw(Finished),
'circle-plus': markRaw(CirclePlus),
'remove': markRaw(Remove),
'circle-check-filled': markRaw(CircleCheckFilled),
'circle-close-filled': markRaw(CircleCloseFilled),
'warning-filled': markRaw(WarningFilled),
'info-filled-icon': markRaw(InfoFilledIcon),
'success-filled': markRaw(SuccessFilled),
'question-filled-icon': markRaw(QuestionFilledIcon)
};
return iconMap[cleanIconName] || markRaw(Document);
};
// 根据菜单类型获取图标颜色
const getIconColorByMenuType = (menuType) => {
if (menuType === 'M') return '#409EFF'; // 目录蓝色
if (menuType === 'C') return '#67C23A'; // 菜单绿色
if (menuType === 'F') return '#E6A23C'; // 按钮橙色
return '#909399'; // 默认灰色
};
// 获取默认快捷功能配置
const getDefaultQuickAccessConfig = () => {
// 根据不同角色返回默认配置
const role = userStore.roles[0] || 'admin';
switch (role) {
case 'doctor':
return [
{ key: 'outpatient', label: '门诊接诊', icon: markRaw(ChatDotRound), iconColor: '#409eff', route: '/doctorstation' },
{ key: 'emr', label: '病历管理', icon: markRaw(Document), iconColor: '#67c23a', route: '/doctorstation/doctorphrase' },
{ key: 'prescription', label: '开立处方', icon: markRaw(Box), iconColor: '#e6a23c', route: '/clinicmanagement/ePrescribing' },
{ key: 'history', label: '历史处方', icon: markRaw(Clock), iconColor: '#f56c6c', route: '/clinicmanagement/historicalPrescription' },
{ key: 'schedule', label: '排班管理', icon: markRaw(Calendar), iconColor: '#909399', route: '/appoinmentmanage/deptManage' },
{ key: 'inquiry', label: '患者查询', icon: markRaw(Search), iconColor: '#409eff', route: '/patientmanagement' }
];
case 'nurse':
return [
{ key: 'ward', label: '病房管理', icon: markRaw(User), iconColor: '#409eff', route: '/inpatientNurse/inpatientNurseStation' },
{ key: 'execution', label: '医嘱执行', icon: markRaw(Operation), iconColor: '#67c23a', route: '/inpatientNurse/medicalOrderExecution' },
{ key: 'proofread', label: '医嘱核对', icon: markRaw(Document), iconColor: '#e6a23c', route: '/inpatientNurse/medicalOrderProofread' },
{ key: 'drugCollect', label: '领药管理', icon: markRaw(Box), iconColor: '#f56c6c', route: '/inpatientNurse/medicineCollect' },
{ key: 'tpr', label: '体温单', icon: markRaw(Monitor), iconColor: '#909399', route: '/inpatientNurse/tprsheet' },
{ key: 'nursing', label: '护理记录', icon: markRaw(ChatDotRound), iconColor: '#409eff', route: '/inpatientNurse/nursingRecord' }
];
case 'pharmacist':
return [
{ key: 'dispensing', label: '发药管理', icon: markRaw(Box), iconColor: '#409eff', route: '/pharmacymanagement' },
{ key: 'prescription', label: '处方审核', icon: markRaw(Document), iconColor: '#67c23a', route: '/pharmacymanagement' },
{ key: 'inventory', label: '库存管理', icon: markRaw(Van), iconColor: '#e6a23c', route: '/medicineStorage' },
{ key: 'purchase', label: '采购管理', icon: markRaw(ShoppingCart), iconColor: '#f56c6c', route: '/medicineStorage' },
{ key: 'warning', label: '效期预警', icon: markRaw(Warning), iconColor: '#f56c6c', route: '/medicationmanagement/statisticalManagement/statisticalManagement' },
{ key: 'statistics', label: '用药统计', icon: markRaw(DataLine), iconColor: '#909399', route: '/monitor' }
];
case 'cashier':
return [
{ key: 'registration', label: '挂号收费', icon: markRaw(Money), iconColor: '#409eff', route: '/charge/outpatientregistration' },
{ key: 'clinicCharge', label: '门诊收费', icon: markRaw(Wallet), iconColor: '#67c23a', route: '/charge/cliniccharge' },
{ key: 'refund', label: '退费管理', icon: markRaw(Document), iconColor: '#e6a23c', route: '/charge/clinicrefund' },
{ key: 'invoice', label: '发票打印', icon: markRaw(Files), iconColor: '#f56c6c', route: '/basicmanage/InvoiceManagement' },
{ key: 'record', label: '收费记录', icon: markRaw(Clock), iconColor: '#909399', route: '/charge/clinicRecord' },
{ key: 'insurance', label: '医保结算', icon: markRaw(Bell), iconColor: '#409eff', route: '/ybmanagement' }
];
default: // admin
return [
{ key: 'patient', label: '患者管理', icon: markRaw(User), iconColor: '#409eff', route: '/patient/patientmgr' },
{ key: 'appointment', label: '预约管理', icon: markRaw(Calendar), iconColor: '#67c23a', route: '/appoinmentmanage' },
{ key: 'doctor', label: '医生管理', icon: markRaw(User), iconColor: '#e6a23c', route: '/doctorstation' },
{ key: 'surgery', label: '手术管理', icon: markRaw(Operation), iconColor: '#f56c6c', route: '/surgerymanage' },
{ key: 'drug', label: '药品管理', icon: markRaw(Box), iconColor: '#909399', route: '/pharmacymanagement' },
{ key: 'statistic', label: '数据统计', icon: markRaw(TrendCharts), iconColor: '#409eff', route: '/monitor' }
];
}
};
// 待办事项
const todoList = ref([
{ id: 1, title: '审核处方申请', desc: '张医生提交的5条处方待审核', priority: 'high', icon: markRaw(Document), time: '10分钟前' },
{ id: 2, title: '确认患者入院', desc: '李某某45岁已办理入院', priority: 'medium', icon: markRaw(User), time: '30分钟前' },
{ id: 3, title: '处理投诉反馈', desc: '患者家属对服务态度的投诉', priority: 'high', icon: markRaw(ChatDotRound), time: '1小时前' },
{ id: 4, title: '药品效期预警', desc: '阿莫西林等3种药品即将过期', priority: 'low', icon: markRaw(Warning), time: '2小时前' },
{ id: 5, title: '月度报表审核', desc: '11月份门诊收入报表待审核', priority: 'medium', icon: markRaw(Files), time: '3小时前' }
])
const todoList = ref([])
// 更新待办事项中的待写病历数量
const updatePendingEmrTodo = () => {
const pendingEmrTodo = todoList.value.find(item => item.title === '待写病历' || item.desc?.includes('患者等待写病历'));
if (pendingEmrTodo) {
pendingEmrTodo.desc = `${statisticsData.value.pendingEmr || 0}个患者等待写病历`;
}
}
// 今日日程
const scheduleList = ref([
@@ -310,95 +690,90 @@ const currentStats = computed(() => {
// 合并配置和实际数据
return baseConfig.map(stat => {
const statWith = { ...stat }
// 根据不同的 key 获取对应的值
switch (stat.key) {
case 'totalPatients':
statWith.value = statisticsData.value.totalPatients
statWith.trend = statisticsData.value.patientTrend
break
statWith.value = statisticsData.value.totalPatients;
statWith.trend = statisticsData.value.patientTrend;
break;
case 'todayRevenue':
statWith.value = statisticsData.value.todayRevenue
statWith.trend = statisticsData.value.revenueTrend
break
statWith.value = statisticsData.value.todayRevenue;
statWith.trend = statisticsData.value.revenueTrend;
break;
case 'appointments':
statWith.value = statisticsData.value.todayAppointments
statWith.trend = statisticsData.value.appointmentTrend
break
statWith.value = statisticsData.value.todayAppointments;
statWith.trend = statisticsData.value.appointmentTrend;
break;
case 'pendingApprovals':
statWith.value = statisticsData.value.pendingApprovals
break
statWith.value = statisticsData.value.pendingApprovals;
break;
case 'myPatients':
statWith.value = statisticsData.value.totalPatients
statWith.trend = statisticsData.value.patientTrend
break
statWith.value = statisticsData.value.totalPatients;
statWith.trend = statisticsData.value.patientTrend;
break;
case 'todayAppointments':
statWith.value = statisticsData.value.todayAppointments
statWith.trend = statisticsData.value.appointmentTrend
break
case 'pendingRecords':
statWith.value = statisticsData.value.pendingApprovals
break
statWith.value = statisticsData.value.todayAppointments;
statWith.trend = statisticsData.value.appointmentTrend;
break;
case 'pendingEmr':
statWith.value = statisticsData.value.pendingEmr;
break;
case 'prescriptions':
statWith.value = statisticsData.value.todayAppointments
statWith.trend = statisticsData.value.appointmentTrend
break
statWith.value = statisticsData.value.todayAppointments;
statWith.trend = statisticsData.value.appointmentTrend;
break;
case 'wardPatients':
statWith.value = statisticsData.value.totalPatients
statWith.trend = statisticsData.value.patientTrend
break
statWith.value = statisticsData.value.totalPatients;
statWith.trend = statisticsData.value.patientTrend;
break;
case 'todayTreatments':
statWith.value = statisticsData.value.todayAppointments
statWith.trend = statisticsData.value.appointmentTrend
break
statWith.value = statisticsData.value.todayAppointments;
statWith.trend = statisticsData.value.appointmentTrend;
break;
case 'vitalSigns':
statWith.value = statisticsData.value.pendingApprovals
break
statWith.value = statisticsData.value.pendingApprovals;
break;
case 'drugDistribution':
statWith.value = statisticsData.value.todayAppointments
statWith.trend = statisticsData.value.appointmentTrend
break
statWith.value = statisticsData.value.todayAppointments;
statWith.trend = statisticsData.value.appointmentTrend;
break;
case 'todayPrescriptions':
statWith.value = statisticsData.value.todayAppointments
statWith.trend = statisticsData.value.appointmentTrend
break
statWith.value = statisticsData.value.todayAppointments;
statWith.trend = statisticsData.value.appointmentTrend;
break;
case 'pendingReview':
statWith.value = statisticsData.value.pendingApprovals
break
statWith.value = statisticsData.value.pendingApprovals;
break;
case 'outOfStock':
statWith.value = statisticsData.value.pendingApprovals
break
statWith.value = statisticsData.value.pendingApprovals;
break;
case 'nearExpiry':
statWith.value = statisticsData.value.pendingApprovals
break
statWith.value = statisticsData.value.pendingApprovals;
break;
case 'todayPayments':
statWith.value = statisticsData.value.todayRevenue
statWith.trend = statisticsData.value.revenueTrend
break
statWith.value = statisticsData.value.todayRevenue;
statWith.trend = statisticsData.value.revenueTrend;
break;
case 'refundRequests':
statWith.value = statisticsData.value.pendingApprovals
break
statWith.value = statisticsData.value.pendingApprovals;
break;
case 'pendingInvoices':
statWith.value = statisticsData.value.pendingApprovals
break
statWith.value = statisticsData.value.pendingApprovals;
break;
case 'insuranceClaims':
statWith.value = statisticsData.value.todayAppointments
statWith.trend = statisticsData.value.appointmentTrend
break
statWith.value = statisticsData.value.todayAppointments;
statWith.trend = statisticsData.value.appointmentTrend;
break;
default:
statWith.value = '0'
statWith.trend = 0
statWith.value = '0';
statWith.trend = 0;
}
return statWith
})
})
// 根据角色获取快捷功能
const quickAccess = computed(() => {
const role = userStore.roles[0] || 'admin'
return roleQuickAccessConfig[role] || roleQuickAccessConfig.admin
})
// 处理统计卡片点击
const handleStatClick = (stat) => {
@@ -419,13 +794,42 @@ const handleStatClick = (stat) => {
} else if (stat.key === 'pendingApprovals' || stat.key === 'pendingReview') {
// 跳转到待审核页面
router.push('/clinicmanagement/ePrescribing')
} else if (stat.key === 'pendingEmr') {
// 跳转到待写病历页面
router.push('/doctorstation/pending-emr')
}
}
// 处理快捷功能点击
const handleQuickAccess = (func) => {
if (func.route) {
router.push(func.route)
// 检查是否为外部链接
if (func.route.startsWith('http://') || func.route.startsWith('https://')) {
// 如果是外部链接,使用 window.open 打开
window.open(func.route, '_blank');
} else {
// 确保路径格式正确,去除多余的斜杠
let normalizedPath = func.route;
if (normalizedPath && typeof normalizedPath === 'string') {
// 将多个连续的斜杠替换为单个斜杠,但保留协议部分的双斜杠(如 http://
normalizedPath = normalizedPath.replace(/([^:])\/{2,}/g, '$1/');
}
// 确保内部路径以 / 开头,以保证正确的路由跳转
if (!normalizedPath.startsWith('/')) {
normalizedPath = '/' + normalizedPath;
}
try {
router.push(normalizedPath);
} catch (error) {
console.error('路由跳转失败:', error);
// 如果路径跳转失败,尝试使用原始路径
router.push(func.route);
}
}
} else {
console.warn('快捷功能没有配置路由路径:', func);
}
}
@@ -433,16 +837,22 @@ const handleQuickAccess = (func) => {
const handleTodoClick = (todo) => {
console.log('Todo clicked:', todo)
// 跳转到相应的处理页面
if (todo.id === 6) { // 待写病历
router.push('/doctorstation?tab=pendingEmr')
}
}
// 显示全部功能
const showAllFunctions = () => {
// 跳转到功能菜单页面
// 显示功能配置
const showConfig = () => {
// 跳转到功能配置页面
router.push('/features/config')
}
// 显示全部待办
const showAllTodos = () => {
// 跳转到待办事项页面
router.push('/todo')
}
// 管理日程
@@ -461,12 +871,111 @@ const fetchStatsData = async () => {
} catch (error) {
console.error('获取统计数据失败:', error)
}
try {
// 获取待写病历数量
const pendingEmrRes = await getPendingEmrCount();
if (pendingEmrRes.code === 200) {
// 确保统计数据对象中有pendingEmr字段
if (!statisticsData.value) {
statisticsData.value = {}
}
statisticsData.value.pendingEmr = pendingEmrRes.data || 0;
} else {
statisticsData.value.pendingEmr = 0;
}
// 更新待办事项中的待写病历数量
updatePendingEmrTodo();
} catch (error) {
console.error('获取待写病历数量失败:', error)
// 确保统计数据对象中有pendingEmr字段
if (!statisticsData.value) {
statisticsData.value = {}
}
statisticsData.value.pendingEmr = 0;
// 更新待办事项中的待写病历数量
updatePendingEmrTodo();
}
}
// 获取待办事项实际应用中应该从API获取
const fetchTodoList = async () => {
// TODO: 调用API获取真实数据
console.log('Fetching todo list...')
try {
const response = await listTodo({ pageNum: 1, pageSize: 5 })
if (response.code === 200) {
// 将工作流任务数据转换为待办事项格式
const rows = response.rows || [];
const todoData = rows.slice(0, 5).map((task, index) => ({
id: task.id || index,
title: task.taskName || task.name || '待办事项',
desc: task.description || '暂无描述',
priority: getPriorityFromTask(task),
status: getTaskStatus(task.status || task.state),
icon: getTaskIcon(task.category),
time: parseTime(task.createTime || task.createTimeStr, '{y}-{m}-{d} {h}:{i}'),
taskInfo: task // 保存原始任务信息,便于后续处理
}))
// 检查是否已经有"待写病历"任务,如果没有则添加
const hasPendingEmrTask = todoData.some(task => task.title === '待写病历' || task.desc?.includes('患者等待写病历'));
if (!hasPendingEmrTask && statisticsData.value.pendingEmr > 0) {
// 添加待写病历任务
const pendingEmrTask = {
id: Date.now(), // 使用时间戳作为唯一ID
title: '待写病历',
desc: `${statisticsData.value.pendingEmr || 0}个患者等待写病历`,
priority: 'high',
icon: markRaw(Document),
time: '刚刚',
taskInfo: null
};
// 如果数组未满5个添加到末尾否则替换最后一个
if (todoData.length < 5) {
todoData.push(pendingEmrTask);
} else {
todoData[4] = pendingEmrTask;
}
}
todoList.value = todoData;
}
} catch (error) {
console.error('获取待办事项失败:', error)
// 如果获取真实数据失败,保留空数组,但模块框架仍会显示
todoList.value = [];
}
}
// 根据任务信息确定优先级
const getPriorityFromTask = (task) => {
// 根据任务的某些属性来确定优先级,这里可以根据实际业务调整
if (task.priority && task.priority > 50) return 'high'
if (task.priority && task.priority > 20) return 'medium'
return 'low'
}
// 获取任务状态
const getTaskStatus = (status) => {
// 根据实际返回的状态值映射
if (status === 'completed' || status === 'finish') return 'completed'
if (status === 'processing' || status === 'active') return 'processing'
return 'pending'
}
// 获取任务图标
const getTaskIcon = (category) => {
// 根据任务分类确定图标
if (category && category.includes('approval')) return markRaw(Document)
if (category && category.includes('patient')) return markRaw(User)
if (category && category.includes('feedback')) return markRaw(ChatDotRound)
if (category && category.includes('warning')) return markRaw(Warning)
if (category && category.includes('report')) return markRaw(Files)
if (category && category.includes('data')) return markRaw(DataLine)
if (category && category.includes('operation')) return markRaw(Operation)
if (category && category.includes('system')) return markRaw(Setting)
return markRaw(Document) // 默认图标
}
// 获取日程数据实际应用中应该从API获取
@@ -475,11 +984,37 @@ const fetchScheduleList = async () => {
console.log('Fetching schedule list...')
}
onMounted(() => {
// 监听本地存储变化,以便在其他标签页或窗口中修改配置后更新当前页面
const handleStorageChange = (event) => {
if (event.key === 'homeFeaturesConfig') {
console.log('检测到快捷功能配置更新,正在重新加载...');
loadUserQuickAccessConfig();
}
};
// 监听配置更新事件
const handleConfigUpdate = () => {
console.log('检测到快捷功能配置更新事件,正在重新加载...');
loadUserQuickAccessConfig();
};
onMounted(async () => {
fetchStatsData()
fetchTodoList()
await fetchTodoList()
fetchScheduleList()
await loadUserQuickAccessConfig()
// 添加本地存储变化监听器
window.addEventListener('storage', handleStorageChange);
// 添加配置更新事件监听器
window.addEventListener('homeFeaturesConfigUpdated', handleConfigUpdate);
})
// 在组件卸载前移除监听器
onUnmounted(() => {
window.removeEventListener('storage', handleStorageChange);
window.removeEventListener('homeFeaturesConfigUpdated', handleConfigUpdate);
});
</script>
<style scoped lang="scss">
@@ -641,6 +1176,11 @@ onMounted(() => {
color: #303133;
margin: 0;
}
.divider {
color: #d8d8d8;
margin: 0 8px;
}
}
.quick-access-grid {
@@ -702,6 +1242,11 @@ onMounted(() => {
color: #303133;
margin: 0;
}
.divider {
color: #d8d8d8;
margin: 0 8px;
}
}
.todo-list {
@@ -762,6 +1307,11 @@ onMounted(() => {
color: #c0c4cc;
}
}
.empty-todo {
padding: 20px 0;
text-align: center;
}
}
}
@@ -784,6 +1334,11 @@ onMounted(() => {
color: #303133;
margin: 0;
}
.divider {
color: #d8d8d8;
margin: 0 8px;
}
}
.schedule-list {

View File

@@ -74,6 +74,12 @@
<el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
<el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="path" label="路由地址" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="fullPath" label="完整路径" :show-overflow-tooltip="true">
<template #default="scope">
<span v-if="scope.row.fullPath">{{ scope.row.fullPath }}</span>
<span v-else-if="scope.row.path">{{ scope.row.path }}</span>
</template>
</el-table-column>
<el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="status" label="状态" width="80">
<template #default="scope">
@@ -186,6 +192,11 @@
<el-input v-model="form.path" placeholder="请输入路由地址" />
</el-form-item>
</el-col>
<el-col :span="24" v-if="form.menuType != 'F' && form.fullPath">
<el-form-item label="完整路径">
<el-input v-model="form.fullPath" readonly placeholder="完整路径" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType == 'C'">
<el-form-item prop="component">
<template #label>
@@ -325,12 +336,17 @@ const data = reactive({
const { queryParams, form, rules } = toRefs(data);
/** 查询菜单列表 */
function getList() {
async function getList() {
loading.value = true;
listMenu(queryParams.value).then(response => {
menuList.value = proxy.handleTree(response.data, "menuId");
try {
const response = await listMenu(queryParams.value);
// 后端已经返回了带完整路径的菜单树,直接使用即可
menuList.value = response.data;
} catch (error) {
console.error('获取菜单列表失败:', error);
} finally {
loading.value = false;
});
}
}
/** 查询菜单下拉树结构 */
function getTreeselect() {
@@ -380,13 +396,16 @@ function resetQuery() {
handleQuery();
}
/** 新增按钮操作 */
function handleAdd(row) {
async function handleAdd(row) {
reset();
getTreeselect();
await getTreeselect();
if (row != null && row.menuId) {
form.value.parentId = row.menuId;
// 使用后端返回的完整路径
form.value.parentFullPath = row.fullPath || row.path;
} else {
form.value.parentId = 0;
form.value.parentFullPath = '';
}
open.value = true;
title.value = "添加菜单";
@@ -403,11 +422,16 @@ function toggleExpandAll() {
async function handleUpdate(row) {
reset();
await getTreeselect();
getMenu(row.menuId).then(response => {
try {
const response = await getMenu(row.menuId);
form.value = response.data;
// 使用后端返回的完整路径
form.value.fullPath = row.fullPath || row.path;
open.value = true;
title.value = "修改菜单";
});
} catch (error) {
console.error('获取菜单信息失败:', error);
}
}
/** 提交按钮 */
function submitForm() {