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

@@ -140,4 +140,11 @@ public interface SysMenuMapper {
* @return 结果 * @return 结果
*/ */
public SysMenu checkMenuNameUnique(@Param("menuName") String menuName, @Param("parentId") Long parentId); public SysMenu checkMenuNameUnique(@Param("menuName") String menuName, @Param("parentId") Long parentId);
/**
* 查询所有菜单信息
*
* @return 菜单列表
*/
public List<SysMenu> selectAllMenus();
} }

View File

@@ -59,6 +59,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
* @return 菜单列表 * @return 菜单列表
*/ */
@Override @Override
@org.springframework.cache.annotation.Cacheable(value = "menu", key = "'menuList:' + #userId + ':' + (#menu == null ? 'all' : #menu.menuName)")
public List<SysMenu> selectMenuList(SysMenu menu, Long userId) { public List<SysMenu> selectMenuList(SysMenu menu, Long userId) {
List<SysMenu> menuList = null; List<SysMenu> menuList = null;
// 管理员显示所有菜单信息 // 管理员显示所有菜单信息
@@ -224,23 +225,133 @@ public class SysMenuServiceImpl implements ISysMenuService {
@Override @Override
public List<SysMenu> buildMenuTreeWithFullPath(List<SysMenu> menus) { public List<SysMenu> buildMenuTreeWithFullPath(List<SysMenu> menus) {
List<SysMenu> menuTree = buildMenuTree(menus); List<SysMenu> menuTree = buildMenuTree(menus);
// 为每个菜单项添加完整路径 // 一次性获取所有菜单信息避免N+1查询问题
addFullPathToMenuTree(menuTree); List<SysMenu> allMenus = menuMapper.selectAllMenus();
Map<Long, SysMenu> menuMap = allMenus.stream()
.collect(Collectors.toMap(SysMenu::getMenuId, menu -> menu));
// 为每个菜单项添加完整路径优化版本避免N+1查询问题
addFullPathsToMenuTreeOptimized(menuTree, menuMap);
return menuTree; return menuTree;
} }
/** /**
* 为菜单树添加完整路径 * 为菜单树添加完整路径(优化版本)
* *
* @param menus 菜单树 * @param menus 菜单树
* @param menuMap 菜单映射
*/ */
private void addFullPathToMenuTree(List<SysMenu> menus) { private void addFullPathsToMenuTreeOptimized(List<SysMenu> menus, Map<Long, SysMenu> menuMap) {
for (SysMenu menu : menus) { for (SysMenu menu : menus) {
// 计算当前菜单的完整路径 // 使用优化的路径计算方法
menu.setFullPath(getMenuFullPath(menu.getMenuId())); menu.setFullPath(computeMenuFullPathOptimized(menu, menuMap));
// 递归处理子菜单 // 递归处理子菜单
if (menu.getChildren() != null && !menu.getChildren().isEmpty()) { if (menu.getChildren() != null && !menu.getChildren().isEmpty()) {
addFullPathToMenuTree(menu.getChildren()); addFullPathsToMenuTreeOptimized(menu.getChildren(), menuMap);
}
}
}
/**
* 优化的计算菜单完整路径方法
*
* @param menu 菜单
* @param menuMap 菜单映射
* @return 完整路径
*/
private String computeMenuFullPathOptimized(SysMenu menu, Map<Long, SysMenu> menuMap) {
if (menu == null || menu.getMenuId() == null) {
return "";
}
StringBuilder fullPath = new StringBuilder();
buildMenuPathOptimized(menu, fullPath, menuMap);
return normalizePath(fullPath.toString());
}
/**
* 优化的递归构建菜单路径
*
* @param menu 菜单信息
* @param path 路径构建器
* @param menuMap 菜单映射
*/
private void buildMenuPathOptimized(SysMenu menu, StringBuilder path, Map<Long, SysMenu> menuMap) {
if (menu == null || menu.getMenuId() == null) {
return;
}
// 如果不是根节点,则递归查找父节点
if (menu.getParentId() != null && menu.getParentId() > 0) {
SysMenu parentMenu = menuMap.get(menu.getParentId());
if (parentMenu != null) {
buildMenuPathOptimized(parentMenu, path, menuMap);
}
}
// 添加当前菜单的路径,避免双斜杠
String currentPath = normalizePathSegment(menu.getPath());
if (currentPath != null && !currentPath.isEmpty()) {
if (path.length() > 0) {
// 确保路径之间只有一个斜杠分隔符
if (path.charAt(path.length() - 1) != '/') {
path.append("/").append(currentPath);
} else {
path.append(currentPath);
}
} else {
// 对于第一个路径,直接追加
path.append(currentPath);
}
}
}
/**
* 递归收集菜单树中的所有菜单ID
*
* @param menus 菜单树
* @return 菜单ID列表
*/
private List<Long> collectMenuIds(List<SysMenu> menus) {
List<Long> menuIds = new ArrayList<>();
for (SysMenu menu : menus) {
menuIds.add(menu.getMenuId());
if (menu.getChildren() != null && !menu.getChildren().isEmpty()) {
menuIds.addAll(collectMenuIds(menu.getChildren()));
}
}
return menuIds;
}
/**
* 批量获取菜单完整路径
*
* @param menuIds 菜单ID列表
* @return 菜单ID到完整路径的映射
*/
private Map<Long, String> batchGetMenuFullPaths(List<Long> menuIds) {
Map<Long, String> fullPathMap = new HashMap<>();
for (Long menuId : menuIds) {
// 使用缓存的getMenuFullPath方法
fullPathMap.put(menuId, getMenuFullPath(menuId));
}
return fullPathMap;
}
/**
* 为菜单树设置完整路径
*
* @param menus 菜单树
* @param fullPathMap 完整路径映射
*/
private void setFullPathsToMenuTree(List<SysMenu> menus, Map<Long, String> fullPathMap) {
for (SysMenu menu : menus) {
// 设置当前菜单的完整路径
menu.setFullPath(fullPathMap.get(menu.getMenuId()));
// 递归处理子菜单
if (menu.getChildren() != null && !menu.getChildren().isEmpty()) {
setFullPathsToMenuTree(menu.getChildren(), fullPathMap);
} }
} }
} }
@@ -299,6 +410,9 @@ public class SysMenuServiceImpl implements ISysMenuService {
* @return 结果 * @return 结果
*/ */
@Override @Override
@org.springframework.cache.annotation.Caching(evict = {
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true)
})
public int insertMenu(SysMenu menu) { public int insertMenu(SysMenu menu) {
//路径Path唯一性判断 //路径Path唯一性判断
SysMenu sysMenu = menuMapper.selectMenuByPath(menu.getPath()); SysMenu sysMenu = menuMapper.selectMenuByPath(menu.getPath());
@@ -315,6 +429,10 @@ public class SysMenuServiceImpl implements ISysMenuService {
* @return 结果 * @return 结果
*/ */
@Override @Override
@org.springframework.cache.annotation.Caching(evict = {
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true),
@org.springframework.cache.annotation.CacheEvict(value = "menu", key = "'fullPath:' + #menu.menuId")
})
public int updateMenu(SysMenu menu) { public int updateMenu(SysMenu menu) {
//路径Path唯一性判断排除当前菜单本身 //路径Path唯一性判断排除当前菜单本身
String path = menu.getPath(); String path = menu.getPath();
@@ -337,6 +455,10 @@ public class SysMenuServiceImpl implements ISysMenuService {
* @return 结果 * @return 结果
*/ */
@Override @Override
@org.springframework.cache.annotation.Caching(evict = {
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true),
@org.springframework.cache.annotation.CacheEvict(value = "menu", key = "'fullPath:' + #menuId")
})
public int deleteMenuById(Long menuId) { public int deleteMenuById(Long menuId) {
return menuMapper.deleteMenuById(menuId); return menuMapper.deleteMenuById(menuId);
} }
@@ -533,6 +655,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
* @return 完整路径 * @return 完整路径
*/ */
@Override @Override
@org.springframework.cache.annotation.Cacheable(value = "menu", key = "'fullPath:' + #menuId", unless = "#result == null || #result.isEmpty()")
public String getMenuFullPath(Long menuId) { public String getMenuFullPath(Long menuId) {
SysMenu menu = menuMapper.selectMenuById(menuId); SysMenu menu = menuMapper.selectMenuById(menuId);
if (menu == null) { if (menu == null) {

View File

@@ -274,4 +274,27 @@
where menu_id = #{menuId} where menu_id = #{menuId}
</delete> </delete>
<select id="selectAllMenus" resultMap="SysMenuResult">
select menu_id,
parent_id,
menu_name,
path,
component,
"query",
route_name,
is_frame,
is_cache,
menu_type,
visible,
status,
perms,
icon,
order_num,
create_time,
update_time,
remark
from sys_menu
order by parent_id, order_num
</select>
</mapper> </mapper>

View File

@@ -7,6 +7,8 @@ public interface IDoctorScheduleAppService {
R<?> getDoctorScheduleList(); R<?> getDoctorScheduleList();
R<?> getTodayDoctorScheduleList();
R<?> addDoctorSchedule(DoctorSchedule doctorSchedule); R<?> addDoctorSchedule(DoctorSchedule doctorSchedule);
R<?> removeDoctorSchedule(Integer doctorScheduleId); R<?> removeDoctorSchedule(Integer doctorScheduleId);

View File

@@ -1,6 +1,7 @@
package com.openhis.web.appointmentmanage.appservice.impl; package com.openhis.web.appointmentmanage.appservice.impl;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.core.common.core.domain.R; import com.core.common.core.domain.R;
import com.openhis.appointmentmanage.domain.DoctorSchedule; import com.openhis.appointmentmanage.domain.DoctorSchedule;
import com.openhis.appointmentmanage.mapper.DoctorScheduleMapper; import com.openhis.appointmentmanage.mapper.DoctorScheduleMapper;
@@ -9,6 +10,8 @@ import com.openhis.web.appointmentmanage.appservice.IDoctorScheduleAppService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.List; import java.util.List;
@Service @Service
@@ -26,6 +29,50 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
return R.ok(list); return R.ok(list);
} }
@Override
public R<?> getTodayDoctorScheduleList() {
// 获取今天的日期
LocalDate today = LocalDate.now();
DayOfWeek dayOfWeek = today.getDayOfWeek();
// 将 Java 的 DayOfWeek 转换为字符串表示
String weekdayStr = convertDayOfWeekToString(dayOfWeek);
// 查询今天排班的医生
LambdaQueryWrapper<DoctorSchedule> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DoctorSchedule::getWeekday, weekdayStr) // 根据星期几查询
.eq(DoctorSchedule::getIsStopped, false); // 只查询未停止的排班
List<DoctorSchedule> list = doctorScheduleService.list(queryWrapper);
return R.ok(list);
}
/**
* 将 DayOfWeek 转换为字符串表示
* @param dayOfWeek DayOfWeek枚举
* @return 对应的字符串表示
*/
private String convertDayOfWeekToString(DayOfWeek dayOfWeek) {
switch (dayOfWeek) {
case MONDAY:
return "1"; // 或者 "星期一" 或 "Monday",取决于数据库中的实际存储格式
case TUESDAY:
return "2";
case WEDNESDAY:
return "3";
case THURSDAY:
return "4";
case FRIDAY:
return "5";
case SATURDAY:
return "6";
case SUNDAY:
return "7";
default:
return "1"; // 默认为星期一
}
}
@Override @Override
public R<?> addDoctorSchedule(DoctorSchedule doctorSchedule) { public R<?> addDoctorSchedule(DoctorSchedule doctorSchedule) {
if (ObjectUtil.isEmpty(doctorSchedule)) { if (ObjectUtil.isEmpty(doctorSchedule)) {

View File

@@ -40,4 +40,13 @@ public class DoctorScheduleController {
return R.ok(doctorScheduleAppService.removeDoctorSchedule(doctorScheduleId)); return R.ok(doctorScheduleAppService.removeDoctorSchedule(doctorScheduleId));
} }
/*
* 获取今日医生排班List
*
* */
@GetMapping("/today")
public R<?> getTodayDoctorScheduleList() {
return R.ok(doctorScheduleAppService.getTodayDoctorScheduleList());
}
} }

View File

@@ -0,0 +1,88 @@
package com.openhis.web.medicationmanagement.controller;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.core.common.annotation.Log;
import com.core.common.core.controller.BaseController;
import com.core.common.core.domain.AjaxResult;
import com.core.common.core.page.TableDataInfo;
import com.core.common.enums.BusinessType;
import com.core.common.utils.poi.ExcelUtil;
import com.openhis.domain.DayEndSettlement;
import com.openhis.service.IDayEndSettlementService;
import com.core.common.core.page.PageDomain;
import com.core.common.utils.StringUtils;
import com.github.pagehelper.PageHelper;
/**
* 日结结算单Controller
*
* @author openhis
* @date 2025-02-01
*/
@RestController
@RequestMapping("/medication/dayEndSettlement")
public class DayEndSettlementController extends BaseController {
@Autowired
private IDayEndSettlementService dayEndSettlementService;
/**
* 查询日结结算单列表
*/
@GetMapping("/list")
public TableDataInfo list(DayEndSettlement dayEndSettlement, PageDomain pageDomain) {
// 使用PageHelper进行分页
PageHelper.startPage(pageDomain.getPageNum(), pageDomain.getPageSize());
List<DayEndSettlement> list = dayEndSettlementService.lambdaQuery()
.like(StringUtils.isNotBlank(dayEndSettlement.getSettlementNo()), DayEndSettlement::getSettlementNo, dayEndSettlement.getSettlementNo())
.eq(StringUtils.isNotBlank(dayEndSettlement.getSettlementType()), DayEndSettlement::getSettlementType, dayEndSettlement.getSettlementType())
.eq(dayEndSettlement.getSettlementDate() != null, DayEndSettlement::getSettlementDate, dayEndSettlement.getSettlementDate())
.orderByDesc(DayEndSettlement::getId)
.list();
return getDataTable(list);
}
/**
* 获取日结结算单详细信息
*/
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id) {
return AjaxResult.success(dayEndSettlementService.getById(id));
}
/**
* 新增日结结算单
*/
@Log(title = "日结结算单", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody DayEndSettlement dayEndSettlement) {
return toAjax(dayEndSettlementService.save(dayEndSettlement));
}
/**
* 修改日结结算单
*/
@Log(title = "日结结算单", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody DayEndSettlement dayEndSettlement) {
return toAjax(dayEndSettlementService.updateById(dayEndSettlement));
}
/**
* 删除日结结算单
*/
@Log(title = "日结结算单", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(dayEndSettlementService.removeByIds(List.of(ids)));
}
}

View File

@@ -0,0 +1,60 @@
package com.openhis.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 日结结算单对象 medication_day_end_settlement
*
* @author openhis
* @date 2025-02-01
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("medication_day_end_settlement")
public class DayEndSettlement extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 结算单号
*/
private String settlementNo;
/**
* 结算日期
*/
private LocalDate settlementDate;
/**
* 结算类型 (daily, weekly, monthly)
*/
private String settlementType;
/**
* 总金额
*/
private BigDecimal totalAmount;
/**
* 状态 (0正常 1停用)
*/
private String status;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,14 @@
package com.openhis.mapper;
import com.openhis.domain.DayEndSettlement;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* 日结结算单Mapper接口
*
* @author openhis
* @date 2025-02-01
*/
public interface DayEndSettlementMapper extends BaseMapper<DayEndSettlement> {
}

View File

@@ -0,0 +1,14 @@
package com.openhis.service;
import com.openhis.domain.DayEndSettlement;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* 日结结算单Service接口
*
* @author openhis
* @date 2025-02-01
*/
public interface IDayEndSettlementService extends IService<DayEndSettlement> {
}

View File

@@ -0,0 +1,18 @@
package com.openhis.service.impl;
import com.openhis.domain.DayEndSettlement;
import com.openhis.mapper.DayEndSettlementMapper;
import com.openhis.service.IDayEndSettlementService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* 日结结算单Service业务层处理
*
* @author openhis
* @date 2025-02-01
*/
@Service
public class DayEndSettlementServiceImpl extends ServiceImpl<DayEndSettlementMapper, DayEndSettlement> implements IDayEndSettlementService {
}

View File

@@ -0,0 +1,52 @@
-- 创建日结结算单表
CREATE TABLE IF NOT EXISTS medication_day_end_settlement (
id BIGSERIAL PRIMARY KEY,
settlement_no VARCHAR(64) NOT NULL,
settlement_date DATE NOT NULL,
settlement_type VARCHAR(20) DEFAULT 'daily',
total_amount DECIMAL(15,2) DEFAULT 0.00,
status CHAR(1) DEFAULT '0',
remark VARCHAR(500),
create_by VARCHAR(64) DEFAULT '',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64) DEFAULT '',
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 添加注释
COMMENT ON TABLE medication_day_end_settlement IS '日结结算单表';
COMMENT ON COLUMN medication_day_end_settlement.settlement_no IS '结算单号';
COMMENT ON COLUMN medication_day_end_settlement.settlement_date IS '结算日期';
COMMENT ON COLUMN medication_day_end_settlement.settlement_type IS '结算类型 (daily, weekly, monthly)';
COMMENT ON COLUMN medication_day_end_settlement.total_amount IS '总金额';
COMMENT ON COLUMN medication_day_end_settlement.status IS '状态 (0正常 1停用)';
COMMENT ON COLUMN medication_day_end_settlement.remark IS '备注';
COMMENT ON COLUMN medication_day_end_settlement.create_by IS '创建者';
COMMENT ON COLUMN medication_day_end_settlement.create_time IS '创建时间';
COMMENT ON COLUMN medication_day_end_settlement.update_by IS '更新者';
COMMENT ON COLUMN medication_day_end_settlement.update_time IS '更新时间';
-- 添加索引
CREATE INDEX IF NOT EXISTS idx_settlement_date ON medication_day_end_settlement(settlement_date);
CREATE INDEX IF NOT EXISTS idx_settlement_no ON medication_day_end_settlement(settlement_no);
-- 创建更新时间触发器函数
CREATE OR REPLACE FUNCTION update_modified_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.update_time = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- 创建更新时间触发器
DROP TRIGGER IF EXISTS update_medication_day_end_settlement_modtime ON medication_day_end_settlement;
CREATE TRIGGER update_medication_day_end_settlement_modtime
BEFORE UPDATE ON medication_day_end_settlement
FOR EACH ROW
EXECUTE FUNCTION update_modified_column();
-- 插入初始数据
INSERT INTO medication_day_end_settlement (settlement_no, settlement_date, settlement_type, total_amount, status, remark, create_by) VALUES
('DS20250201001', '2025-02-01', 'daily', 15000.00, '0', '2025年2月1日日结', 'admin'),
('DS20250201002', '2025-02-01', 'daily', 8500.50, '0', '药房日结', 'admin');

View File

@@ -0,0 +1,38 @@
CREATE TABLE IF NOT EXISTS sys_user_config (
config_id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
config_key VARCHAR(100) NOT NULL,
config_value TEXT,
remark VARCHAR(500),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 添加注释
COMMENT ON TABLE sys_user_config IS '用户配置表';
COMMENT ON COLUMN sys_user_config.config_id IS '配置ID';
COMMENT ON COLUMN sys_user_config.user_id IS '用户ID';
COMMENT ON COLUMN sys_user_config.config_key IS '配置键名';
COMMENT ON COLUMN sys_user_config.config_value IS '配置值';
COMMENT ON COLUMN sys_user_config.remark IS '备注';
COMMENT ON COLUMN sys_user_config.create_time IS '创建时间';
COMMENT ON COLUMN sys_user_config.update_time IS '更新时间';
-- 创建唯一索引
CREATE UNIQUE INDEX IF NOT EXISTS uk_user_config ON sys_user_config (user_id, config_key);
CREATE INDEX IF NOT EXISTS idx_user_id ON sys_user_config (user_id);
-- 创建更新时间触发器函数
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.update_time = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- 创建触发器
CREATE TRIGGER update_sys_user_config_updated_at
BEFORE UPDATE ON sys_user_config
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

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"> <span class="custom-tree-node">
<div class="tree-node-info"> <div class="tree-node-info">
<div class="node-main"> <div class="node-main">
<el-icon :size="16" :color="getIconColor(data)"> <svg-icon :icon-class="data.icon" :style="{ color: getIconColor(data) }" />
<component :is="getIconComponent(data.icon)" />
</el-icon>
<span class="menu-label" style="margin-left: 8px;">{{ node.label }}</span> <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"> <el-tag v-if="data.fullPath" type="info" size="small" effect="plain" class="path-tag-inline">
{{ data.fullPath }} {{ data.fullPath }}
@@ -88,9 +86,7 @@
class="selected-function-item" class="selected-function-item"
> >
<div class="function-info"> <div class="function-info">
<el-icon :size="16" :color="getIconColor(item)"> <svg-icon :icon-class="item.icon" :style="{ color: getIconColor(item) }" />
<component :is="getIconComponent(item.icon)" />
</el-icon>
<div class="function-details"> <div class="function-details">
<span class="function-name">{{ item.menuName }}</span> <span class="function-name">{{ item.menuName }}</span>
<el-tag v-if="item.fullPath" type="info" size="small" class="function-path-below"> <el-tag v-if="item.fullPath" type="info" size="small" class="function-path-below">
@@ -193,6 +189,7 @@ import {
SuccessFilled, SuccessFilled,
QuestionFilled as QuestionFilledIcon QuestionFilled as QuestionFilledIcon
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import SvgIcon from '@/components/SvgIcon'
// 添加 loading 状态 // 添加 loading 状态
const loading = ref(false) const loading = ref(false)
@@ -214,81 +211,6 @@ watch(filterText, (val) => {
treeRef.value?.filter(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) => { const getIconColor = (data) => {
@@ -302,6 +224,24 @@ const getIconColor = (data) => {
const loadMenuData = async () => { const loadMenuData = async () => {
loading.value = true loading.value = true
try { 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({}) const response = await listMenu({})
if (response.code === 200) { if (response.code === 200) {
// 过滤掉隐藏的菜单项、目录和按钮类型的菜单,只保留当前角色可访问的菜单项 // 过滤掉隐藏的菜单项、目录和按钮类型的菜单,只保留当前角色可访问的菜单项
@@ -311,6 +251,10 @@ const loadMenuData = async () => {
// 展开所有节点 // 展开所有节点
expandedKeys.value = getAllNodeIds(filteredMenus) expandedKeys.value = getAllNodeIds(filteredMenus)
// 将菜单数据缓存到本地存储
localStorage.setItem('menuTreeCache', JSON.stringify(filteredMenus));
localStorage.setItem('menuTreeCacheTimestamp', Date.now().toString());
// 获取已保存的配置 // 获取已保存的配置
await loadSavedConfig() await loadSavedConfig()
} else { } else {
@@ -389,7 +333,7 @@ const saveConfig = async () => {
fullPath: fullPath, fullPath: fullPath,
menuName: node.menuName, menuName: node.menuName,
path: node.path, path: node.path,
icon: node.icon, // 保存图标信息 icon: node.icon, // 保存数据库中的图标类名
menuType: node.menuType // 保存菜单类型信息 menuType: node.menuType // 保存菜单类型信息
}; };
@@ -406,7 +350,7 @@ const saveConfig = async () => {
fullPath: node.path, fullPath: node.path,
menuName: node.menuName, menuName: node.menuName,
path: node.path, path: node.path,
icon: node.icon, // 保存图标信息 icon: node.icon, // 保存数据库中的图标类名
menuType: node.menuType // 保存菜单类型信息 menuType: node.menuType // 保存菜单类型信息
}; };
console.log(`构造的菜单项对象(错误处理):`, menuItem); console.log(`构造的菜单项对象(错误处理):`, menuItem);
@@ -435,6 +379,9 @@ const saveConfig = async () => {
if (saveResult.code === 200) { if (saveResult.code === 200) {
// 只有在数据库保存成功后,才保存到本地存储 // 只有在数据库保存成功后,才保存到本地存储
localStorage.setItem('homeFeaturesConfig', JSON.stringify(menuDataWithPaths)) localStorage.setItem('homeFeaturesConfig', JSON.stringify(menuDataWithPaths))
// 清除菜单树缓存,以便下次加载最新数据
localStorage.removeItem('menuTreeCache');
localStorage.removeItem('menuTreeCacheTimestamp');
ElMessage.success('配置保存成功') ElMessage.success('配置保存成功')
// 触发全局事件,通知首页更新快捷功能 // 触发全局事件,通知首页更新快捷功能
window.dispatchEvent(new Event('homeFeaturesConfigUpdated')); window.dispatchEvent(new Event('homeFeaturesConfigUpdated'));
@@ -493,6 +440,36 @@ const filterNode = (value, data) => {
// 加载已保存的配置 // 加载已保存的配置
const loadSavedConfig = async () => { const loadSavedConfig = async () => {
try { 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') const response = await getCurrentUserConfig('homeFeaturesConfig')
let savedConfig = null; let savedConfig = null;
@@ -539,6 +516,10 @@ const loadSavedConfig = async () => {
checkedKeys.value.includes(node.menuId) && node.menuType === 'C' checkedKeys.value.includes(node.menuId) && node.menuType === 'C'
) )
selectedFunctions.value = checkedNodes selectedFunctions.value = checkedNodes
// 将配置缓存到本地存储
localStorage.setItem('homeFeaturesConfigCache', JSON.stringify(parsedConfig));
localStorage.setItem('homeFeaturesConfigCacheTimestamp', Date.now().toString());
} else { } else {
// 如果解析失败,使用默认配置 // 如果解析失败,使用默认配置
const defaultSelections = getDefaultSelections(menuTree.value) const defaultSelections = getDefaultSelections(menuTree.value)

View File

@@ -59,9 +59,7 @@
@click="handleQuickAccess(func)" @click="handleQuickAccess(func)"
> >
<div class="quick-icon"> <div class="quick-icon">
<el-icon :size="28" :color="func.iconColor"> <svg-icon :icon-class="func.icon" :style="{ fontSize: '28px', color: func.iconColor }" />
<component :is="func.icon" />
</el-icon>
</div> </div>
<div class="quick-label">{{ func.label }}</div> <div class="quick-label">{{ func.label }}</div>
</div> </div>
@@ -138,6 +136,7 @@ import { getHomeStatistics, getPendingEmrCount } from '@/api/home'
import { listTodo } from '@/api/workflow/task.js' import { listTodo } from '@/api/workflow/task.js'
import { getCurrentUserConfig } from '@/api/system/userConfig' import { getCurrentUserConfig } from '@/api/system/userConfig'
import { listMenu, getMenuFullPath } from '@/api/system/menu' import { listMenu, getMenuFullPath } from '@/api/system/menu'
import { getTodayDoctorScheduleList } from '@/api/appointmentmanage/doctorSchedule'
import { ElDivider } from 'element-plus' import { ElDivider } from 'element-plus'
import { import {
User, User,
@@ -220,6 +219,7 @@ import {
// 为别名单独导入 // 为别名单独导入
import { InfoFilled as InfoFilledIcon, QuestionFilled as QuestionFilledIcon, SuccessFilled } from '@element-plus/icons-vue' import { InfoFilled as InfoFilledIcon, QuestionFilled as QuestionFilledIcon, SuccessFilled } from '@element-plus/icons-vue'
import SvgIcon from '@/components/SvgIcon'
const userStore = useUserStore() const userStore = useUserStore()
const router = useRouter() const router = useRouter()
@@ -400,8 +400,8 @@ const convertMenuIdsToQuickAccess = async (menuIds) => {
return { return {
key: menuItem.menuId, key: menuItem.menuId,
label: menuItem.menuName, label: menuItem.menuName,
icon: getIconComponent(menuItem.icon || 'Document'), // 使用菜单项的图标,如果没有则使用默认图标 icon: menuItem.icon || 'document', // 使用数据库中的图标类名
iconColor: getIconColorByMenuType(menuItem.menuType) || '#67C23A', // 使用菜单类型的颜色,如果没有则使用默认颜色 iconColor: menuItem.iconColor || getIconColorByMenuType(menuItem.menuType) || '#67C23A', // 优先使用数据库中的颜色,否则使用菜单类型的颜色
route: route route: route
}; };
}).filter(item => item.route); // 过滤掉 route 为空的项 }).filter(item => item.route); // 过滤掉 route 为空的项
@@ -436,8 +436,8 @@ const convertMenuIdsToQuickAccess = async (menuIds) => {
return { return {
key: matchedMenu.perms || matchedMenu.path || `menu_${matchedMenu.menuId}`, key: matchedMenu.perms || matchedMenu.path || `menu_${matchedMenu.menuId}`,
label: matchedMenu.menuName, label: matchedMenu.menuName,
icon: getIconComponent(matchedMenu.icon), icon: matchedMenu.icon || 'document', // 使用数据库中的图标类名
iconColor: getIconColorByMenuType(matchedMenu.menuType), iconColor: matchedMenu.iconColor || getIconColorByMenuType(matchedMenu.menuType),
route: fullPath || matchedMenu.path // 确保 route 不为空 route: fullPath || matchedMenu.path // 确保 route 不为空
}; };
} catch (error) { } catch (error) {
@@ -446,8 +446,8 @@ const convertMenuIdsToQuickAccess = async (menuIds) => {
return { return {
key: matchedMenu.perms || matchedMenu.path || `menu_${matchedMenu.menuId}`, key: matchedMenu.perms || matchedMenu.path || `menu_${matchedMenu.menuId}`,
label: matchedMenu.menuName, label: matchedMenu.menuName,
icon: getIconComponent(matchedMenu.icon), icon: matchedMenu.icon || 'document', // 使用数据库中的图标类名
iconColor: getIconColorByMenuType(matchedMenu.menuType), iconColor: matchedMenu.iconColor || getIconColorByMenuType(matchedMenu.menuType),
route: matchedMenu.path || matchedMenu.fullPath || '/' // 确保 route 不为空 route: matchedMenu.path || matchedMenu.fullPath || '/' // 确保 route 不为空
}; };
} }
@@ -490,81 +490,6 @@ const flattenMenuTree = (menuTree) => {
return result; 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) => { const getIconColorByMenuType = (menuType) => {
@@ -581,48 +506,48 @@ const getDefaultQuickAccessConfig = () => {
switch (role) { switch (role) {
case 'doctor': case 'doctor':
return [ return [
{ key: 'outpatient', label: '门诊接诊', icon: markRaw(ChatDotRound), iconColor: '#409eff', route: '/doctorstation' }, { key: 'outpatient', label: '门诊接诊', icon: 'chat-dot-round', iconColor: '#409eff', route: '/doctorstation' },
{ key: 'emr', label: '病历管理', icon: markRaw(Document), iconColor: '#67c23a', route: '/doctorstation/doctorphrase' }, { key: 'emr', label: '病历管理', icon: 'document', iconColor: '#67c23a', route: '/doctorstation/doctorphrase' },
{ key: 'prescription', label: '开立处方', icon: markRaw(Box), iconColor: '#e6a23c', route: '/clinicmanagement/ePrescribing' }, { key: 'prescription', label: '开立处方', icon: 'box', iconColor: '#e6a23c', route: '/clinicmanagement/ePrescribing' },
{ key: 'history', label: '历史处方', icon: markRaw(Clock), iconColor: '#f56c6c', route: '/clinicmanagement/historicalPrescription' }, { key: 'history', label: '历史处方', icon: 'clock', iconColor: '#f56c6c', route: '/clinicmanagement/historicalPrescription' },
{ key: 'schedule', label: '排班管理', icon: markRaw(Calendar), iconColor: '#909399', route: '/appoinmentmanage/deptManage' }, { key: 'schedule', label: '排班管理', icon: 'calendar', iconColor: '#909399', route: '/appoinmentmanage/deptManage' },
{ key: 'inquiry', label: '患者查询', icon: markRaw(Search), iconColor: '#409eff', route: '/patientmanagement' } { key: 'inquiry', label: '患者查询', icon: 'search', iconColor: '#409eff', route: '/patientmanagement' }
]; ];
case 'nurse': case 'nurse':
return [ return [
{ key: 'ward', label: '病房管理', icon: markRaw(User), iconColor: '#409eff', route: '/inpatientNurse/inpatientNurseStation' }, { key: 'ward', label: '病房管理', icon: 'user', iconColor: '#409eff', route: '/inpatientNurse/inpatientNurseStation' },
{ key: 'execution', label: '医嘱执行', icon: markRaw(Operation), iconColor: '#67c23a', route: '/inpatientNurse/medicalOrderExecution' }, { key: 'execution', label: '医嘱执行', icon: 'operation', iconColor: '#67c23a', route: '/inpatientNurse/medicalOrderExecution' },
{ key: 'proofread', label: '医嘱核对', icon: markRaw(Document), iconColor: '#e6a23c', route: '/inpatientNurse/medicalOrderProofread' }, { key: 'proofread', label: '医嘱核对', icon: 'document', iconColor: '#e6a23c', route: '/inpatientNurse/medicalOrderProofread' },
{ key: 'drugCollect', label: '领药管理', icon: markRaw(Box), iconColor: '#f56c6c', route: '/inpatientNurse/medicineCollect' }, { key: 'drugCollect', label: '领药管理', icon: 'box', iconColor: '#f56c6c', route: '/inpatientNurse/medicineCollect' },
{ key: 'tpr', label: '体温单', icon: markRaw(Monitor), iconColor: '#909399', route: '/inpatientNurse/tprsheet' }, { key: 'tpr', label: '体温单', icon: 'monitor', iconColor: '#909399', route: '/inpatientNurse/tprsheet' },
{ key: 'nursing', label: '护理记录', icon: markRaw(ChatDotRound), iconColor: '#409eff', route: '/inpatientNurse/nursingRecord' } { key: 'nursing', label: '护理记录', icon: 'chat-dot-round', iconColor: '#409eff', route: '/inpatientNurse/nursingRecord' }
]; ];
case 'pharmacist': case 'pharmacist':
return [ return [
{ key: 'dispensing', label: '发药管理', icon: markRaw(Box), iconColor: '#409eff', route: '/pharmacymanagement' }, { key: 'dispensing', label: '发药管理', icon: 'box', iconColor: '#409eff', route: '/pharmacymanagement' },
{ key: 'prescription', label: '处方审核', icon: markRaw(Document), iconColor: '#67c23a', route: '/pharmacymanagement' }, { key: 'prescription', label: '处方审核', icon: 'document', iconColor: '#67c23a', route: '/pharmacymanagement' },
{ key: 'inventory', label: '库存管理', icon: markRaw(Van), iconColor: '#e6a23c', route: '/medicineStorage' }, { key: 'inventory', label: '库存管理', icon: 'van', iconColor: '#e6a23c', route: '/medicineStorage' },
{ key: 'purchase', label: '采购管理', icon: markRaw(ShoppingCart), iconColor: '#f56c6c', route: '/medicineStorage' }, { key: 'purchase', label: '采购管理', icon: 'shopping-cart', iconColor: '#f56c6c', route: '/medicineStorage' },
{ key: 'warning', label: '效期预警', icon: markRaw(Warning), iconColor: '#f56c6c', route: '/medicationmanagement/statisticalManagement/statisticalManagement' }, { key: 'warning', label: '效期预警', icon: 'warning', iconColor: '#f56c6c', route: '/medicationmanagement/statisticalManagement/statisticalManagement' },
{ key: 'statistics', label: '用药统计', icon: markRaw(DataLine), iconColor: '#909399', route: '/monitor' } { key: 'statistics', label: '用药统计', icon: 'data-line', iconColor: '#909399', route: '/monitor' }
]; ];
case 'cashier': case 'cashier':
return [ return [
{ key: 'registration', label: '挂号收费', icon: markRaw(Money), iconColor: '#409eff', route: '/charge/outpatientregistration' }, { key: 'registration', label: '挂号收费', icon: 'money', iconColor: '#409eff', route: '/charge/outpatientregistration' },
{ key: 'clinicCharge', label: '门诊收费', icon: markRaw(Wallet), iconColor: '#67c23a', route: '/charge/cliniccharge' }, { key: 'clinicCharge', label: '门诊收费', icon: 'wallet', iconColor: '#67c23a', route: '/charge/cliniccharge' },
{ key: 'refund', label: '退费管理', icon: markRaw(Document), iconColor: '#e6a23c', route: '/charge/clinicrefund' }, { key: 'refund', label: '退费管理', icon: 'document', iconColor: '#e6a23c', route: '/charge/clinicrefund' },
{ key: 'invoice', label: '发票打印', icon: markRaw(Files), iconColor: '#f56c6c', route: '/basicmanage/InvoiceManagement' }, { key: 'invoice', label: '发票打印', icon: 'files', iconColor: '#f56c6c', route: '/basicmanage/InvoiceManagement' },
{ key: 'record', label: '收费记录', icon: markRaw(Clock), iconColor: '#909399', route: '/charge/clinicRecord' }, { key: 'record', label: '收费记录', icon: 'clock', iconColor: '#909399', route: '/charge/clinicRecord' },
{ key: 'insurance', label: '医保结算', icon: markRaw(Bell), iconColor: '#409eff', route: '/ybmanagement' } { key: 'insurance', label: '医保结算', icon: 'bell', iconColor: '#409eff', route: '/ybmanagement' }
]; ];
default: // admin default: // admin
return [ return [
{ key: 'patient', label: '患者管理', icon: markRaw(User), iconColor: '#409eff', route: '/patient/patientmgr' }, { key: 'patient', label: '患者管理', icon: 'user', iconColor: '#409eff', route: '/patient/patientmgr' },
{ key: 'appointment', label: '预约管理', icon: markRaw(Calendar), iconColor: '#67c23a', route: '/appoinmentmanage' }, { key: 'appointment', label: '预约管理', icon: 'calendar', iconColor: '#67c23a', route: '/appoinmentmanage' },
{ key: 'doctor', label: '医生管理', icon: markRaw(User), iconColor: '#e6a23c', route: '/doctorstation' }, { key: 'doctor', label: '医生管理', icon: 'user', iconColor: '#e6a23c', route: '/doctorstation' },
{ key: 'surgery', label: '手术管理', icon: markRaw(Operation), iconColor: '#f56c6c', route: '/surgerymanage' }, { key: 'surgery', label: '手术管理', icon: 'operation', iconColor: '#f56c6c', route: '/surgerymanage' },
{ key: 'drug', label: '药品管理', icon: markRaw(Box), iconColor: '#909399', route: '/pharmacymanagement' }, { key: 'drug', label: '药品管理', icon: 'box', iconColor: '#909399', route: '/pharmacymanagement' },
{ key: 'statistic', label: '数据统计', icon: markRaw(TrendCharts), iconColor: '#409eff', route: '/monitor' } { key: 'statistic', label: '数据统计', icon: 'trend-charts', iconColor: '#409eff', route: '/monitor' }
]; ];
} }
}; };
@@ -639,12 +564,7 @@ const updatePendingEmrTodo = () => {
} }
// 今日日程 // 今日日程
const scheduleList = ref([ 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 getGreeting = () => { const getGreeting = () => {
@@ -980,8 +900,58 @@ const getTaskIcon = (category) => {
// 获取日程数据实际应用中应该从API获取 // 获取日程数据实际应用中应该从API获取
const fetchScheduleList = async () => { const fetchScheduleList = async () => {
// TODO: 调用API获取真实数据 try {
console.log('Fetching schedule list...') 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-col :span="24">
<el-form-item label="菜单类型" prop="menuType"> <el-form-item label="菜单类型" prop="menuType">
<el-radio-group v-model="form.menuType"> <el-radio-group v-model="form.menuType">
<el-radio label="M">目录</el-radio> <el-radio value="M">目录</el-radio>
<el-radio label="C">菜单</el-radio> <el-radio value="C">菜单</el-radio>
<el-radio label="F">按钮</el-radio> <el-radio value="F">按钮</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -174,8 +174,8 @@
</span> </span>
</template> </template>
<el-radio-group v-model="form.isFrame"> <el-radio-group v-model="form.isFrame">
<el-radio label="0"></el-radio> <el-radio value="0"></el-radio>
<el-radio label="1"></el-radio> <el-radio value="1"></el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -247,8 +247,8 @@
</span> </span>
</template> </template>
<el-radio-group v-model="form.isCache"> <el-radio-group v-model="form.isCache">
<el-radio label="0">缓存</el-radio> <el-radio value="0">缓存</el-radio>
<el-radio label="1">不缓存</el-radio> <el-radio value="1">不缓存</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -266,7 +266,7 @@
<el-radio <el-radio
v-for="dict in sys_show_hide" v-for="dict in sys_show_hide"
:key="dict.value" :key="dict.value"
:label="dict.value" :value="dict.value"
>{{ dict.label }}</el-radio> >{{ dict.label }}</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
@@ -285,7 +285,7 @@
<el-radio <el-radio
v-for="dict in sys_normal_disable" v-for="dict in sys_normal_disable"
:key="dict.value" :key="dict.value"
:label="dict.value" :value="dict.value"
>{{ dict.label }}</el-radio> >{{ dict.label }}</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
@@ -303,7 +303,7 @@
</template> </template>
<script setup name="Menu"> <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 SvgIcon from "@/components/SvgIcon";
import IconSelect from "@/components/IconSelect"; import IconSelect from "@/components/IconSelect";
@@ -351,10 +351,28 @@ async function getList() {
/** 查询菜单下拉树结构 */ /** 查询菜单下拉树结构 */
function getTreeselect() { function getTreeselect() {
menuOptions.value = []; menuOptions.value = [];
listMenu().then(response => { // 使用专门的treeselect API它返回预构建的树形结构
const menu = { menuId: 0, menuName: "主类目", children: [] }; treeselect().then(response => {
menu.children = proxy.handleTree(response.data, "menuId"); // TreeSelect对象使用id、label、children字段但el-tree-select组件期望menuId、menuName、children字段
menuOptions.value.push(menu); // 需要将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); const response = await getMenu(row.menuId);
form.value = response.data; form.value = response.data;
// 使用后端返回的完整路径 // 使用后端返回的完整路径
form.value.fullPath = row.fullPath || row.path; form.value.fullPath = response.data.fullPath || response.data.path;
open.value = true; open.value = true;
title.value = "修改菜单"; title.value = "修改菜单";
} catch (error) { } catch (error) {