feat(menu): 优化菜单服务性能并新增医生排班功能

- 添加菜单缓存注解以提升查询性能
- 实现菜单完整路径计算优化,解决 N+1 查询问题
- 新增 selectAllMenus 方法供路径计算使用
- 添加今日医生排班查询功能
- 重构前端图标显示逻辑,使用 SVG 图标替代 Element 图标
- 添加前端菜单数据本地缓存机制
- 更新菜单管理界面的表单组件绑定方式
- 新增预约管理、门诊管理和药房管理路由配置
This commit is contained in:
2026-02-02 08:46:33 +08:00
parent 669d669422
commit 5534a71c7d
20 changed files with 1156 additions and 228 deletions

View File

@@ -0,0 +1,43 @@
import request from '@/utils/request'
// 获取今日医生排班列表
export function getTodayDoctorScheduleList() {
return request({
url: '/doctor-schedule/today',
method: 'get'
})
}
// 获取医生排班列表
export function getDoctorScheduleList() {
return request({
url: '/appointment/doctor-schedule/list',
method: 'get'
})
}
// 添加医生排班
export function addDoctorSchedule(data) {
return request({
url: '/appointment/doctor-schedule',
method: 'post',
data: data
})
}
// 更新医生排班
export function updateDoctorSchedule(data) {
return request({
url: '/appointment/doctor-schedule',
method: 'put',
data: data
})
}
// 删除医生排班
export function deleteDoctorSchedule(id) {
return request({
url: '/appointment/doctor-schedule/delete/' + id,
method: 'delete'
})
}

View File

@@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询日结结算单列表
export function listDayEndSettlement(query) {
return request({
url: '/medication/dayEndSettlement/list',
method: 'get',
params: query
})
}
// 查询日结结算单详细
export function getDayEndSettlement(settlementId) {
return request({
url: '/medication/dayEndSettlement/' + settlementId,
method: 'get'
})
}
// 新增日结结算单
export function addDayEndSettlement(data) {
return request({
url: '/medication/dayEndSettlement',
method: 'post',
data: data
})
}
// 修改日结结算单
export function updateDayEndSettlement(data) {
return request({
url: '/medication/dayEndSettlement',
method: 'put',
data: data
})
}
// 删除日结结算单
export function delDayEndSettlement(settlementId) {
return request({
url: '/medication/dayEndSettlement/' + settlementId,
method: 'delete'
})
}

View File

@@ -286,6 +286,48 @@ export const dynamicRoutes = [
}
]
},
{
path: '/appoinmentmanage',
component: Layout,
name: 'AppoinmentManage',
meta: { title: '预约管理', icon: 'appointment' },
children: [
{
path: 'deptManage',
component: () => import('@/views/appoinmentmanage/deptManage/index.vue'),
name: 'DeptManage',
meta: { title: '科室排班管理', icon: 'calendar' }
}
]
},
{
path: '/clinicmanagement',
component: Layout,
name: 'ClinicManagement',
meta: { title: '门诊管理', icon: 'operation' },
children: [
{
path: 'dayEnd',
component: () => import('@/views/clinicmanagement/dayEnd/index.vue'),
name: 'DayEnd',
meta: { title: '门诊日结', icon: 'document' }
}
]
},
{
path: '/medicationmanagement',
component: Layout,
name: 'MedicationManagement',
meta: { title: '药房管理', icon: 'medication' },
children: [
{
path: 'dayEndSettlement',
component: () => import('@/views/medicationmanagement/dayEndSettlement/index.vue'),
name: 'DayEndSettlement',
meta: { title: '日结结算单管理', icon: 'document' }
}
]
}
];
// 合并常量路由和动态路由,确保所有路由都能被访问

View File

@@ -39,9 +39,7 @@
<span class="custom-tree-node">
<div class="tree-node-info">
<div class="node-main">
<el-icon :size="16" :color="getIconColor(data)">
<component :is="getIconComponent(data.icon)" />
</el-icon>
<svg-icon :icon-class="data.icon" :style="{ color: getIconColor(data) }" />
<span class="menu-label" style="margin-left: 8px;">{{ node.label }}</span>
<el-tag v-if="data.fullPath" type="info" size="small" effect="plain" class="path-tag-inline">
{{ data.fullPath }}
@@ -88,9 +86,7 @@
class="selected-function-item"
>
<div class="function-info">
<el-icon :size="16" :color="getIconColor(item)">
<component :is="getIconComponent(item.icon)" />
</el-icon>
<svg-icon :icon-class="item.icon" :style="{ color: getIconColor(item) }" />
<div class="function-details">
<span class="function-name">{{ item.menuName }}</span>
<el-tag v-if="item.fullPath" type="info" size="small" class="function-path-below">
@@ -193,6 +189,7 @@ import {
SuccessFilled,
QuestionFilled as QuestionFilledIcon
} from '@element-plus/icons-vue'
import SvgIcon from '@/components/SvgIcon'
// 添加 loading 状态
const loading = ref(false)
@@ -214,81 +211,6 @@ watch(filterText, (val) => {
treeRef.value?.filter(val)
})
// 图标映射
const iconMap = {
'menu': Menu,
'grid': Grid,
'folder': Folder,
'tickets': Tickets,
'document': Document,
'setting': Setting,
'user': User,
'goods': Goods,
'chat-dot-square': ChatDotSquare,
'histogram': Histogram,
'wallet': Wallet,
'office-building': OfficeBuilding,
'postcard': Postcard,
'collection': Collection,
'video-play': VideoPlay,
'camera': Camera,
'headset': Headset,
'phone': Phone,
'message': Message,
'chat-line-square': ChatLineSquare,
'chat-round': ChatRound,
'guide': Guide,
'help': Help,
'info-filled': InfoFilled,
'circle-check': CircleCheck,
'circle-close': CircleClose,
'warning': Warning,
'question-filled': QuestionFilled,
'star': Star,
'link': Link,
'position': Position,
'picture': Picture,
'upload': Upload,
'download': Download,
'caret-left': CaretLeft,
'caret-right': CaretRight,
'more': More,
'close': Close,
'check': Check,
'arrow-up': ArrowUp,
'arrow-down': ArrowDown,
'arrow-left': ArrowLeft,
'arrow-right': ArrowRight,
'plus': Plus,
'minus': Minus,
'zoom-in': ZoomIn,
'zoom-out': ZoomOut,
'refresh': Refresh,
'search': Search,
'edit': Edit,
'delete': Delete,
'share': Share,
'view': View,
'switch-button': SwitchButton,
'hide': Hide,
'finished': Finished,
'circle-plus': CirclePlus,
'remove': Remove,
'circle-check-filled': CircleCheckFilled,
'circle-close-filled': CircleCloseFilled,
'warning-filled': WarningFilled,
'info-filled-icon': InfoFilledIcon,
'success-filled': SuccessFilled,
'question-filled-icon': QuestionFilledIcon
}
// 获取图标组件
const getIconComponent = (iconName) => {
if (!iconName) return Document
// 移除前缀,如 fa-, el-icon-
const cleanIconName = iconName.replace(/^(fa-|el-icon-)/, '').toLowerCase()
return iconMap[cleanIconName] || Document
}
// 获取图标颜色
const getIconColor = (data) => {
@@ -302,6 +224,24 @@ const getIconColor = (data) => {
const loadMenuData = async () => {
loading.value = true
try {
// 尝试从本地缓存获取菜单数据
const cachedMenuData = localStorage.getItem('menuTreeCache');
const cacheTimestamp = localStorage.getItem('menuTreeCacheTimestamp');
// 检查缓存是否有效24小时内
if (cachedMenuData && cacheTimestamp) {
const cacheAge = Date.now() - parseInt(cacheTimestamp);
if (cacheAge < 24 * 60 * 60 * 1000) { // 24小时
menuTree.value = JSON.parse(cachedMenuData);
// 展开所有节点
expandedKeys.value = getAllNodeIds(menuTree.value);
// 获取已保存的配置
await loadSavedConfig();
loading.value = false;
return;
}
}
const response = await listMenu({})
if (response.code === 200) {
// 过滤掉隐藏的菜单项、目录和按钮类型的菜单,只保留当前角色可访问的菜单项
@@ -311,6 +251,10 @@ const loadMenuData = async () => {
// 展开所有节点
expandedKeys.value = getAllNodeIds(filteredMenus)
// 将菜单数据缓存到本地存储
localStorage.setItem('menuTreeCache', JSON.stringify(filteredMenus));
localStorage.setItem('menuTreeCacheTimestamp', Date.now().toString());
// 获取已保存的配置
await loadSavedConfig()
} else {
@@ -389,7 +333,7 @@ const saveConfig = async () => {
fullPath: fullPath,
menuName: node.menuName,
path: node.path,
icon: node.icon, // 保存图标信息
icon: node.icon, // 保存数据库中的图标类名
menuType: node.menuType // 保存菜单类型信息
};
@@ -406,7 +350,7 @@ const saveConfig = async () => {
fullPath: node.path,
menuName: node.menuName,
path: node.path,
icon: node.icon, // 保存图标信息
icon: node.icon, // 保存数据库中的图标类名
menuType: node.menuType // 保存菜单类型信息
};
console.log(`构造的菜单项对象(错误处理):`, menuItem);
@@ -435,6 +379,9 @@ const saveConfig = async () => {
if (saveResult.code === 200) {
// 只有在数据库保存成功后,才保存到本地存储
localStorage.setItem('homeFeaturesConfig', JSON.stringify(menuDataWithPaths))
// 清除菜单树缓存,以便下次加载最新数据
localStorage.removeItem('menuTreeCache');
localStorage.removeItem('menuTreeCacheTimestamp');
ElMessage.success('配置保存成功')
// 触发全局事件,通知首页更新快捷功能
window.dispatchEvent(new Event('homeFeaturesConfigUpdated'));
@@ -493,6 +440,36 @@ const filterNode = (value, data) => {
// 加载已保存的配置
const loadSavedConfig = async () => {
try {
// 尝试从本地缓存获取配置
const cachedConfig = localStorage.getItem('homeFeaturesConfigCache');
const cacheTimestamp = localStorage.getItem('homeFeaturesConfigCacheTimestamp');
// 检查缓存是否有效1小时内
if (cachedConfig && cacheTimestamp) {
const cacheAge = Date.now() - parseInt(cacheTimestamp);
if (cacheAge < 60 * 60 * 1000) { // 1小时
const parsedConfig = JSON.parse(cachedConfig);
// 检查数据格式如果是包含对象的数组新格式提取菜单ID
if (parsedConfig && Array.isArray(parsedConfig)) {
if (parsedConfig.length > 0 && typeof parsedConfig[0] === 'object' && parsedConfig[0].hasOwnProperty('menuId')) {
// 新格式:[{menuId: 1, fullPath: "...", ...}, ...]
checkedKeys.value = parsedConfig.map(item => item.menuId);
} else {
// 旧格式:[1, 2, 3, ...]
checkedKeys.value = parsedConfig;
}
// 根据保存的配置初始化已选择的功能
const allNodes = getAllNodes(menuTree.value)
const checkedNodes = allNodes.filter(node =>
checkedKeys.value.includes(node.menuId) && node.menuType === 'C'
)
selectedFunctions.value = checkedNodes
}
return; // 使用缓存数据,直接返回
}
}
// 优先从数据库获取已保存的配置
const response = await getCurrentUserConfig('homeFeaturesConfig')
let savedConfig = null;
@@ -539,6 +516,10 @@ const loadSavedConfig = async () => {
checkedKeys.value.includes(node.menuId) && node.menuType === 'C'
)
selectedFunctions.value = checkedNodes
// 将配置缓存到本地存储
localStorage.setItem('homeFeaturesConfigCache', JSON.stringify(parsedConfig));
localStorage.setItem('homeFeaturesConfigCacheTimestamp', Date.now().toString());
} else {
// 如果解析失败,使用默认配置
const defaultSelections = getDefaultSelections(menuTree.value)

View File

@@ -59,9 +59,7 @@
@click="handleQuickAccess(func)"
>
<div class="quick-icon">
<el-icon :size="28" :color="func.iconColor">
<component :is="func.icon" />
</el-icon>
<svg-icon :icon-class="func.icon" :style="{ fontSize: '28px', color: func.iconColor }" />
</div>
<div class="quick-label">{{ func.label }}</div>
</div>
@@ -138,6 +136,7 @@ import { getHomeStatistics, getPendingEmrCount } from '@/api/home'
import { listTodo } from '@/api/workflow/task.js'
import { getCurrentUserConfig } from '@/api/system/userConfig'
import { listMenu, getMenuFullPath } from '@/api/system/menu'
import { getTodayDoctorScheduleList } from '@/api/appointmentmanage/doctorSchedule'
import { ElDivider } from 'element-plus'
import {
User,
@@ -220,6 +219,7 @@ import {
// 为别名单独导入
import { InfoFilled as InfoFilledIcon, QuestionFilled as QuestionFilledIcon, SuccessFilled } from '@element-plus/icons-vue'
import SvgIcon from '@/components/SvgIcon'
const userStore = useUserStore()
const router = useRouter()
@@ -400,8 +400,8 @@ const convertMenuIdsToQuickAccess = async (menuIds) => {
return {
key: menuItem.menuId,
label: menuItem.menuName,
icon: getIconComponent(menuItem.icon || 'Document'), // 使用菜单项的图标,如果没有则使用默认图标
iconColor: getIconColorByMenuType(menuItem.menuType) || '#67C23A', // 使用菜单类型的颜色,如果没有则使用默认颜色
icon: menuItem.icon || 'document', // 使用数据库中的图标类名
iconColor: menuItem.iconColor || getIconColorByMenuType(menuItem.menuType) || '#67C23A', // 优先使用数据库中的颜色,否则使用菜单类型的颜色
route: route
};
}).filter(item => item.route); // 过滤掉 route 为空的项
@@ -436,8 +436,8 @@ const convertMenuIdsToQuickAccess = async (menuIds) => {
return {
key: matchedMenu.perms || matchedMenu.path || `menu_${matchedMenu.menuId}`,
label: matchedMenu.menuName,
icon: getIconComponent(matchedMenu.icon),
iconColor: getIconColorByMenuType(matchedMenu.menuType),
icon: matchedMenu.icon || 'document', // 使用数据库中的图标类名
iconColor: matchedMenu.iconColor || getIconColorByMenuType(matchedMenu.menuType),
route: fullPath || matchedMenu.path // 确保 route 不为空
};
} catch (error) {
@@ -446,8 +446,8 @@ const convertMenuIdsToQuickAccess = async (menuIds) => {
return {
key: matchedMenu.perms || matchedMenu.path || `menu_${matchedMenu.menuId}`,
label: matchedMenu.menuName,
icon: getIconComponent(matchedMenu.icon),
iconColor: getIconColorByMenuType(matchedMenu.menuType),
icon: matchedMenu.icon || 'document', // 使用数据库中的图标类名
iconColor: matchedMenu.iconColor || getIconColorByMenuType(matchedMenu.menuType),
route: matchedMenu.path || matchedMenu.fullPath || '/' // 确保 route 不为空
};
}
@@ -490,81 +490,6 @@ const flattenMenuTree = (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) => {
@@ -581,48 +506,48 @@ const getDefaultQuickAccessConfig = () => {
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' }
{ key: 'outpatient', label: '门诊接诊', icon: 'chat-dot-round', iconColor: '#409eff', route: '/doctorstation' },
{ key: 'emr', label: '病历管理', icon: 'document', iconColor: '#67c23a', route: '/doctorstation/doctorphrase' },
{ key: 'prescription', label: '开立处方', icon: 'box', iconColor: '#e6a23c', route: '/clinicmanagement/ePrescribing' },
{ key: 'history', label: '历史处方', icon: 'clock', iconColor: '#f56c6c', route: '/clinicmanagement/historicalPrescription' },
{ key: 'schedule', label: '排班管理', icon: 'calendar', iconColor: '#909399', route: '/appoinmentmanage/deptManage' },
{ key: 'inquiry', label: '患者查询', icon: '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' }
{ key: 'ward', label: '病房管理', icon: 'user', iconColor: '#409eff', route: '/inpatientNurse/inpatientNurseStation' },
{ key: 'execution', label: '医嘱执行', icon: 'operation', iconColor: '#67c23a', route: '/inpatientNurse/medicalOrderExecution' },
{ key: 'proofread', label: '医嘱核对', icon: 'document', iconColor: '#e6a23c', route: '/inpatientNurse/medicalOrderProofread' },
{ key: 'drugCollect', label: '领药管理', icon: 'box', iconColor: '#f56c6c', route: '/inpatientNurse/medicineCollect' },
{ key: 'tpr', label: '体温单', icon: 'monitor', iconColor: '#909399', route: '/inpatientNurse/tprsheet' },
{ key: 'nursing', label: '护理记录', icon: 'chat-dot-round', 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' }
{ key: 'dispensing', label: '发药管理', icon: 'box', iconColor: '#409eff', route: '/pharmacymanagement' },
{ key: 'prescription', label: '处方审核', icon: 'document', iconColor: '#67c23a', route: '/pharmacymanagement' },
{ key: 'inventory', label: '库存管理', icon: 'van', iconColor: '#e6a23c', route: '/medicineStorage' },
{ key: 'purchase', label: '采购管理', icon: 'shopping-cart', iconColor: '#f56c6c', route: '/medicineStorage' },
{ key: 'warning', label: '效期预警', icon: 'warning', iconColor: '#f56c6c', route: '/medicationmanagement/statisticalManagement/statisticalManagement' },
{ key: 'statistics', label: '用药统计', icon: 'data-line', 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' }
{ key: 'registration', label: '挂号收费', icon: 'money', iconColor: '#409eff', route: '/charge/outpatientregistration' },
{ key: 'clinicCharge', label: '门诊收费', icon: 'wallet', iconColor: '#67c23a', route: '/charge/cliniccharge' },
{ key: 'refund', label: '退费管理', icon: 'document', iconColor: '#e6a23c', route: '/charge/clinicrefund' },
{ key: 'invoice', label: '发票打印', icon: 'files', iconColor: '#f56c6c', route: '/basicmanage/InvoiceManagement' },
{ key: 'record', label: '收费记录', icon: 'clock', iconColor: '#909399', route: '/charge/clinicRecord' },
{ key: 'insurance', label: '医保结算', icon: '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' }
{ key: 'patient', label: '患者管理', icon: 'user', iconColor: '#409eff', route: '/patient/patientmgr' },
{ key: 'appointment', label: '预约管理', icon: 'calendar', iconColor: '#67c23a', route: '/appoinmentmanage' },
{ key: 'doctor', label: '医生管理', icon: 'user', iconColor: '#e6a23c', route: '/doctorstation' },
{ key: 'surgery', label: '手术管理', icon: 'operation', iconColor: '#f56c6c', route: '/surgerymanage' },
{ key: 'drug', label: '药品管理', icon: 'box', iconColor: '#909399', route: '/pharmacymanagement' },
{ key: 'statistic', label: '数据统计', icon: 'trend-charts', iconColor: '#409eff', route: '/monitor' }
];
}
};
@@ -639,12 +564,7 @@ const updatePendingEmrTodo = () => {
}
// 今日日程
const scheduleList = ref([
{ id: 1, time: '09:00', title: '科室晨会', location: '第一会议室', type: 'info', tag: '日常' },
{ id: 2, time: '10:30', title: '病例讨论', location: '第二会议室', type: 'primary', tag: '会议' },
{ id: 3, time: '14:00', title: '专家查房', location: '住院部3楼', type: 'warning', tag: '重要' },
{ id: 4, time: '16:00', title: '新药培训', location: '培训中心', type: 'success', tag: '培训' }
])
const scheduleList = ref([])
// 获取问候语
const getGreeting = () => {
@@ -980,8 +900,58 @@ const getTaskIcon = (category) => {
// 获取日程数据实际应用中应该从API获取
const fetchScheduleList = async () => {
// TODO: 调用API获取真实数据
console.log('Fetching schedule list...')
try {
console.log('Fetching schedule list...')
const response = await getTodayDoctorScheduleList()
if (response.code === 200) {
// 将API返回的数据转换为前端所需的格式
const scheduleData = response.data.map((schedule, index) => {
// 根据排班类型设置标签类型
let tagType = 'info'
if (schedule.weekday) {
tagType = schedule.weekday.toLowerCase()
} else if (schedule.timePeriod) {
tagType = schedule.timePeriod.toLowerCase()
}
// 确定标题
const title = schedule.doctor ? `${schedule.doctor}医生排班` : '医生排班'
// 确定位置
const location = schedule.clinic || schedule.deptId || '未知科室'
return {
id: schedule.id || index,
time: schedule.startTime || '未知时间',
title: title,
location: location,
type: tagType,
tag: schedule.timePeriod || '排班'
}
})
// 更新日程列表
scheduleList.value = scheduleData
} else {
console.error('获取排班信息失败:', response.msg)
// 如果API调用失败使用默认数据
scheduleList.value = [
{ id: 1, time: '09:00', title: '科室晨会', location: '第一会议室', type: 'info', tag: '日常' },
{ id: 2, time: '10:30', title: '病例讨论', location: '第二会议室', type: 'primary', tag: '会议' },
{ id: 3, time: '14:00', title: '专家查房', location: '住院部3楼', type: 'warning', tag: '重要' },
{ id: 4, time: '16:00', title: '新药培训', location: '培训中心', type: 'success', tag: '培训' }
]
}
} catch (error) {
console.error('获取排班信息异常:', error)
// 如果出现异常,使用默认数据
scheduleList.value = [
{ id: 1, time: '09:00', title: '科室晨会', location: '第一会议室', type: 'info', tag: '日常' },
{ id: 2, time: '10:30', title: '病例讨论', location: '第二会议室', type: 'primary', tag: '会议' },
{ id: 3, time: '14:00', title: '专家查房', location: '住院部3楼', type: 'warning', tag: '重要' },
{ id: 4, time: '16:00', title: '新药培训', location: '培训中心', type: 'success', tag: '培训' }
]
}
}
// 监听本地存储变化,以便在其他标签页或窗口中修改配置后更新当前页面

View File

@@ -0,0 +1,335 @@
<template>
<div class="app-container">
<el-form
:model="queryParams"
ref="queryRef"
:inline="true"
v-show="showSearch"
label-width="90px"
>
<el-form-item label="查询日期:">
<el-date-picker
v-model="queryTime"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 300px; margin-right: 20px"
@change="handleQuery"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="结算类型:">
<el-select
v-model="queryParams.settlementType"
placeholder="结算类型"
clearable
style="width: 150px; margin-right: 30px"
>
<el-option label="日结" value="daily" />
<el-option label="周结" value="weekly" />
<el-option label="月结" value="monthly" />
</el-select>
<el-button type="primary" plain icon="Search" @click="handleQuery">查询</el-button>
<el-button type="primary" plain icon="Printer" @click="handlePrint">打印</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
v-hasPermi="['medication:dayEndSettlement:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['medication:dayEndSettlement:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['medication:dayEndSettlement:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="Download"
@click="handleExport"
v-hasPermi="['medication:dayEndSettlement:export']"
>导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
</el-row>
<el-table v-loading="loading" :data="dayEndSettlementList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="结算单号" align="center" prop="settlementNo" />
<el-table-column label="结算日期" align="center" prop="settlementDate" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.settlementDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="结算类型" align="center" prop="settlementType" />
<el-table-column label="结算状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="总金额" align="center" prop="totalAmount" />
<el-table-column label="操作人" align="center" prop="operator" />
<el-table-column label="操作时间" align="center" prop="createTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['medication:dayEndSettlement:query']">查看</el-button>
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['medication:dayEndSettlement:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['medication:dayEndSettlement:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@size-change="getList"
@current-change="getList"
/>
<!-- 添加或修改日结结算单对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="dayEndSettlementRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="结算单号" prop="settlementNo">
<el-input v-model="form.settlementNo" placeholder="请输入结算单号" />
</el-form-item>
<el-form-item label="结算日期" prop="settlementDate">
<el-date-picker clearable
v-model="form.settlementDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择结算日期">
</el-date-picker>
</el-form-item>
<el-form-item label="结算类型" prop="settlementType">
<el-select v-model="form.settlementType" placeholder="请选择结算类型">
<el-option label="日结" value="daily" />
<el-option label="周结" value="weekly" />
<el-option label="月结" value="monthly" />
</el-select>
</el-form-item>
<el-form-item label="结算状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in sys_normal_disable"
:key="dict.value"
:label="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="总金额" prop="totalAmount">
<el-input-number v-model="form.totalAmount" placeholder="请输入总金额" style="width: 100%" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="DayEndSettlement">
import { listDayEndSettlement, getDayEndSettlement, delDayEndSettlement, addDayEndSettlement, updateDayEndSettlement } from "@/api/medicationmanagement/dayEndSettlement";
const { proxy } = getCurrentInstance();
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref("");
const open = ref(false);
const queryTime = ref([]);
const dayEndSettlementList = ref([]);
const queryFormRef = ref();
const dayEndSettlementRef = ref();
const queryParams = ref({
pageNum: 1,
pageSize: 10,
settlementNo: null,
settlementDate: null,
settlementType: null,
status: null
});
const form = ref({});
const rules = ref({
settlementNo: [
{ required: true, message: "结算单号不能为空", trigger: "blur" }
],
settlementDate: [
{ required: true, message: "结算日期不能为空", trigger: "blur" }
],
settlementType: [
{ required: true, message: "结算类型不能为空", trigger: "change" }
],
totalAmount: [
{ required: true, message: "总金额不能为空", trigger: "blur" }
]
});
const { sys_normal_disable } = proxy.useDict("sys_normal_disable");
/** 查询日结结算单列表 */
const getList = async () => {
loading.value = true;
try {
const response = await listDayEndSettlement(queryParams.value);
dayEndSettlementList.value = response.rows;
total.value = response.total;
} catch (error) {
console.error('获取日结结算单列表失败:', error);
} finally {
loading.value = false;
}
};
/** 取消按钮 */
const cancel = () => {
open.value = false;
reset();
};
/** 表单重置 */
const reset = () => {
form.value = {
id: null,
settlementNo: null,
settlementDate: null,
settlementType: null,
status: "0",
totalAmount: null,
remark: null
};
proxy.resetForm("dayEndSettlementRef");
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryTime.value = [];
proxy.resetForm("queryRef");
handleQuery();
};
/** 多择框多选 */
const handleSelectionChange = (selection) => {
ids.value = selection.map(item => item.id);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
open.value = true;
title.value = "添加日结结算单";
};
/** 修改按钮操作 */
const handleUpdate = (row) => {
reset();
const settlementId = row.id || ids.value[0];
getDayEndSettlement(settlementId).then(response => {
form.value = response.data;
open.value = true;
title.value = "修改日结结算单";
});
};
/** 提交按钮 */
const submitForm = () => {
proxy.$refs["dayEndSettlementRef"].validate(valid => {
if (valid) {
if (form.value.id != null) {
updateDayEndSettlement(form.value).then(response => {
proxy.$modal.msgSuccess("修改成功");
open.value = false;
getList();
});
} else {
addDayEndSettlement(form.value).then(response => {
proxy.$modal.msgSuccess("新增成功");
open.value = false;
getList();
});
}
}
});
};
/** 删除按钮操作 */
const handleDelete = (row) => {
const settlementIds = row.id || ids.value;
proxy.$modal.confirm('是否确认删除日结结算单编号为"' + settlementIds + '"的数据项?').then(function() {
return delDayEndSettlement(settlementIds);
}).then(() => {
getList();
proxy.$modal.msgSuccess("删除成功");
}).catch(() => {});
};
/** 导出按钮操作 */
const handleExport = () => {
proxy.download("medication/dayEndSettlement/export", {
...queryParams.value
}, `dayEndSettlement_${new Date().getTime()}.xlsx`);
};
/** 打印按钮操作 */
const handlePrint = () => {
// TODO: 实现打印功能
proxy.$modal.msgSuccess("打印功能待实现");
};
/** 查看按钮操作 */
const handleView = (row) => {
// TODO: 实现查看功能
proxy.$modal.msgSuccess("查看功能待实现");
};
/** 初始化数据 */
getList();
</script>

View File

@@ -124,9 +124,9 @@
<el-col :span="24">
<el-form-item label="菜单类型" prop="menuType">
<el-radio-group v-model="form.menuType">
<el-radio label="M">目录</el-radio>
<el-radio label="C">菜单</el-radio>
<el-radio label="F">按钮</el-radio>
<el-radio value="M">目录</el-radio>
<el-radio value="C">菜单</el-radio>
<el-radio value="F">按钮</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
@@ -174,8 +174,8 @@
</span>
</template>
<el-radio-group v-model="form.isFrame">
<el-radio label="0"></el-radio>
<el-radio label="1"></el-radio>
<el-radio value="0"></el-radio>
<el-radio value="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
@@ -247,8 +247,8 @@
</span>
</template>
<el-radio-group v-model="form.isCache">
<el-radio label="0">缓存</el-radio>
<el-radio label="1">不缓存</el-radio>
<el-radio value="0">缓存</el-radio>
<el-radio value="1">不缓存</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
@@ -266,7 +266,7 @@
<el-radio
v-for="dict in sys_show_hide"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
@@ -285,7 +285,7 @@
<el-radio
v-for="dict in sys_normal_disable"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
@@ -303,7 +303,7 @@
</template>
<script setup name="Menu">
import {addMenu, delMenu, getMenu, listMenu, updateMenu} from "@/api/system/menu";
import {addMenu, delMenu, getMenu, listMenu, updateMenu, treeselect} from "@/api/system/menu";
import SvgIcon from "@/components/SvgIcon";
import IconSelect from "@/components/IconSelect";
@@ -351,10 +351,28 @@ async function getList() {
/** 查询菜单下拉树结构 */
function getTreeselect() {
menuOptions.value = [];
listMenu().then(response => {
const menu = { menuId: 0, menuName: "主类目", children: [] };
menu.children = proxy.handleTree(response.data, "menuId");
menuOptions.value.push(menu);
// 使用专门的treeselect API它返回预构建的树形结构
treeselect().then(response => {
// TreeSelect对象使用id、label、children字段但el-tree-select组件期望menuId、menuName、children字段
// 需要将TreeSelect对象转换为el-tree-select组件期望的格式
const convertTreeSelectToMenuFormat = (treeSelectList) => {
return treeSelectList.map(item => ({
menuId: item.id,
menuName: item.label,
value: item.id,
label: item.label,
children: item.children ? convertTreeSelectToMenuFormat(item.children) : []
}));
};
const rootNode = {
menuId: 0,
menuName: "主类目",
value: 0,
label: "主类目",
children: convertTreeSelectToMenuFormat(response.data)
};
menuOptions.value.push(rootNode);
});
}
/** 取消按钮 */
@@ -426,7 +444,7 @@ async function handleUpdate(row) {
const response = await getMenu(row.menuId);
form.value = response.data;
// 使用后端返回的完整路径
form.value.fullPath = row.fullPath || row.path;
form.value.fullPath = response.data.fullPath || response.data.path;
open.value = true;
title.value = "修改菜单";
} catch (error) {