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

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

View File

@@ -33,7 +33,9 @@ public class SysMenuController extends BaseController {
@GetMapping("/list") @GetMapping("/list")
public AjaxResult list(SysMenu menu) { public AjaxResult list(SysMenu menu) {
List<SysMenu> menus = menuService.selectMenuList(menu, getUserId()); List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
return success(menus); // 构建带完整路径的菜单树
List<SysMenu> menuTreeWithFullPath = menuService.buildMenuTreeWithFullPath(menus);
return success(menuTreeWithFullPath);
} }
/** /**
@@ -115,4 +117,25 @@ public class SysMenuController extends BaseController {
} }
return toAjax(menuService.deleteMenuById(menuId)); return toAjax(menuService.deleteMenuById(menuId));
} }
/**
* 获取菜单完整路径
*/
@PreAuthorize("@ss.hasPermi('system:menu:query')")
@GetMapping("/fullPath/{menuId}")
public AjaxResult getFullPath(@PathVariable("menuId") Long menuId) {
String fullPath = menuService.getMenuFullPath(menuId);
return success(fullPath);
}
/**
* 生成完整路径
*/
@PreAuthorize("@ss.hasPermi('system:menu:query')")
@PostMapping("/generateFullPath")
public AjaxResult generateFullPath(@RequestParam(required = false) Long parentId,
@RequestParam String currentPath) {
String fullPath = menuService.generateFullPath(parentId, currentPath);
return success(fullPath);
}
} }

View File

@@ -69,6 +69,9 @@ public class SysMenu extends BaseEntity {
/** 子菜单 */ /** 子菜单 */
private List<SysMenu> children = new ArrayList<SysMenu>(); private List<SysMenu> children = new ArrayList<SysMenu>();
/** 完整路径 */
private String fullPath;
public Long getMenuId() { public Long getMenuId() {
return menuId; return menuId;
} }
@@ -212,6 +215,14 @@ public class SysMenu extends BaseEntity {
this.children = children; this.children = children;
} }
public String getFullPath() {
return fullPath;
}
public void setFullPath(String fullPath) {
this.fullPath = fullPath;
}
@Override @Override
public String toString() { public String toString() {
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE).append("menuId", getMenuId()) return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE).append("menuId", getMenuId())
@@ -219,8 +230,8 @@ public class SysMenu extends BaseEntity {
.append("path", getPath()).append("component", getComponent()).append("query", getQuery()) .append("path", getPath()).append("component", getComponent()).append("query", getQuery())
.append("routeName", getRouteName()).append("isFrame", getIsFrame()).append("IsCache", getIsCache()) .append("routeName", getRouteName()).append("isFrame", getIsFrame()).append("IsCache", getIsCache())
.append("menuType", getMenuType()).append("visible", getVisible()).append("status ", getStatus()) .append("menuType", getMenuType()).append("visible", getVisible()).append("status ", getStatus())
.append("perms", getPerms()).append("icon", getIcon()).append("createBy", getCreateBy()) .append("perms", getPerms()).append("icon", getIcon()).append("fullPath", getFullPath())
.append("createTime", getCreateTime()).append("updateBy", getUpdateBy()) .append("createBy", getCreateBy()).append("createTime", getCreateTime()).append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime()).append("remark", getRemark()).toString(); .append("updateTime", getUpdateTime()).append("remark", getRemark()).toString();
} }
} }

View File

@@ -3,7 +3,6 @@ package com.core.system.service;
import com.core.common.core.domain.TreeSelect; import com.core.common.core.domain.TreeSelect;
import com.core.common.core.domain.entity.SysMenu; import com.core.common.core.domain.entity.SysMenu;
import com.core.system.domain.vo.RouterVo; import com.core.system.domain.vo.RouterVo;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@@ -22,7 +21,7 @@ public interface ISysMenuService {
public List<SysMenu> selectMenuList(Long userId); public List<SysMenu> selectMenuList(Long userId);
/** /**
* 根据用户查询系统菜单列表 * 查询系统菜单列表
* *
* @param menu 菜单信息 * @param menu 菜单信息
* @param userId 用户ID * @param userId 用户ID
@@ -50,7 +49,7 @@ public interface ISysMenuService {
* 根据用户ID查询菜单树信息 * 根据用户ID查询菜单树信息
* *
* @param userId 用户ID * @param userId 用户ID
* @return 菜单列表 * @return 菜单树信息
*/ */
public List<SysMenu> selectMenuTreeByUserId(Long userId); public List<SysMenu> selectMenuTreeByUserId(Long userId);
@@ -78,6 +77,14 @@ public interface ISysMenuService {
*/ */
public List<SysMenu> buildMenuTree(List<SysMenu> menus); public List<SysMenu> buildMenuTree(List<SysMenu> menus);
/**
* 构建前端所需要树结构(包含完整路径)
*
* @param menus 菜单列表
* @return 树结构列表
*/
public List<SysMenu> buildMenuTreeWithFullPath(List<SysMenu> menus);
/** /**
* 构建前端所需要下拉树结构 * 构建前端所需要下拉树结构
* *
@@ -98,15 +105,15 @@ public interface ISysMenuService {
* 是否存在菜单子节点 * 是否存在菜单子节点
* *
* @param menuId 菜单ID * @param menuId 菜单ID
* @return 结果 true 存在 false 不存在 * @return 结果
*/ */
public boolean hasChildByMenuId(Long menuId); public boolean hasChildByMenuId(Long menuId);
/** /**
* 查询菜单是否存在角色 * 查询菜单使用数量
* *
* @param menuId 菜单ID * @param menuId 菜单ID
* @return 结果 true 存在 false 不存在 * @return 结果
*/ */
public boolean checkMenuExistRole(Long menuId); public boolean checkMenuExistRole(Long menuId);
@@ -141,4 +148,21 @@ public interface ISysMenuService {
* @return 结果 * @return 结果
*/ */
public boolean checkMenuNameUnique(SysMenu menu); public boolean checkMenuNameUnique(SysMenu menu);
/**
* 根据菜单ID获取完整路径
*
* @param menuId 菜单ID
* @return 完整路径
*/
public String getMenuFullPath(Long menuId);
/**
* 根据路径参数生成完整路径
*
* @param parentId 父级菜单ID
* @param currentPath 当前路径
* @return 完整路径
*/
public String generateFullPath(Long parentId, String currentPath);
} }

View File

@@ -215,6 +215,36 @@ public class SysMenuServiceImpl implements ISysMenuService {
return returnList; return returnList;
} }
/**
* 构建前端所需要树结构(包含完整路径)
*
* @param menus 菜单列表
* @return 树结构列表
*/
@Override
public List<SysMenu> buildMenuTreeWithFullPath(List<SysMenu> menus) {
List<SysMenu> menuTree = buildMenuTree(menus);
// 为每个菜单项添加完整路径
addFullPathToMenuTree(menuTree);
return menuTree;
}
/**
* 为菜单树添加完整路径
*
* @param menus 菜单树
*/
private void addFullPathToMenuTree(List<SysMenu> menus) {
for (SysMenu menu : menus) {
// 计算当前菜单的完整路径
menu.setFullPath(getMenuFullPath(menu.getMenuId()));
// 递归处理子菜单
if (menu.getChildren() != null && !menu.getChildren().isEmpty()) {
addFullPathToMenuTree(menu.getChildren());
}
}
}
/** /**
* 构建前端所需要下拉树结构 * 构建前端所需要下拉树结构
* *
@@ -495,4 +525,132 @@ public class SysMenuServiceImpl implements ISysMenuService {
return StringUtils.replaceEach(path, new String[] {Constants.HTTP, Constants.HTTPS, Constants.WWW, ".", ":"}, return StringUtils.replaceEach(path, new String[] {Constants.HTTP, Constants.HTTPS, Constants.WWW, ".", ":"},
new String[] {"", "", "", "/", "/"}); new String[] {"", "", "", "/", "/"});
} }
/**
* 根据菜单ID获取完整路径
*
* @param menuId 菜单ID
* @return 完整路径
*/
@Override
public String getMenuFullPath(Long menuId) {
SysMenu menu = menuMapper.selectMenuById(menuId);
if (menu == null) {
return "";
}
StringBuilder fullPath = new StringBuilder();
buildMenuPath(menu, fullPath);
// 标准化完整路径,确保没有多余的斜杠
return normalizePath(fullPath.toString());
}
/**
* 递归构建菜单路径
*
* @param menu 菜单信息
* @param path 路径构建器
*/
private void buildMenuPath(SysMenu menu, StringBuilder path) {
if (menu == null || menu.getMenuId() == null) {
return;
}
// 如果不是根节点,则递归查找父节点
if (menu.getParentId() != null && menu.getParentId() > 0) {
SysMenu parentMenu = menuMapper.selectMenuById(menu.getParentId());
if (parentMenu != null) {
buildMenuPath(parentMenu, path);
}
}
// 添加当前菜单的路径,避免双斜杠
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);
}
}
}
/**
* 标准化路径片段,移除开头的斜杠
*
* @param path 原始路径
* @return 标准化后的路径片段
*/
private String normalizePathSegment(String path) {
if (path == null) {
return null;
}
// 移除开头的斜杠
if (path.startsWith("/")) {
path = path.substring(1);
}
return path;
}
/**
* 标准化完整路径,移除多余的斜杠
*
* @param path 原始路径
* @return 标准化后的完整路径
*/
private String normalizePath(String path) {
if (path == null) {
return null;
}
// 处理多个连续斜杠,将其替换为单个斜杠
while (path.contains("//")) {
path = path.replace("//", "/");
}
return path;
}
/**
* 根据路径参数生成完整路径
*
* @param parentId 父级菜单ID
* @param currentPath 当前路径
* @return 完整路径
*/
@Override
public String generateFullPath(Long parentId, String currentPath) {
StringBuilder fullPath = new StringBuilder();
// 如果有父级菜单,则先获取父级菜单的完整路径
if (parentId != null && parentId > 0) {
SysMenu parentMenu = menuMapper.selectMenuById(parentId);
if (parentMenu != null) {
String parentFullPath = getMenuFullPath(parentId);
if (StringUtils.isNotEmpty(parentFullPath)) {
fullPath.append(parentFullPath);
}
}
}
// 添加当前路径
if (StringUtils.isNotEmpty(currentPath)) {
if (fullPath.length() > 0) {
fullPath.append("/").append(currentPath);
} else {
fullPath.append(currentPath);
}
}
return normalizePath(fullPath.toString());
}
} }

View File

@@ -21,6 +21,7 @@
<result property="status" column="status"/> <result property="status" column="status"/>
<result property="perms" column="perms"/> <result property="perms" column="perms"/>
<result property="icon" column="icon"/> <result property="icon" column="icon"/>
<result property="fullPath" column="full_path"/>
<result property="createBy" column="create_by"/> <result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/> <result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/> <result property="updateTime" column="update_time"/>

View File

@@ -58,4 +58,28 @@ public interface IDoctorStationEmrAppService {
* @return 病历详情 * @return 病历详情
*/ */
R<?> getEmrDetail(Long encounterId); R<?> getEmrDetail(Long encounterId);
/**
* 获取待写病历列表
*
* @param doctorId 医生ID
* @return 待写病历列表
*/
R<?> getPendingEmrList(Long doctorId);
/**
* 获取待写病历数量
*
* @param doctorId 医生ID
* @return 待写病历数量
*/
R<?> getPendingEmrCount(Long doctorId);
/**
* 检查患者是否需要写病历
*
* @param encounterId 就诊ID
* @return 患者是否需要写病历
*/
R<?> checkNeedWriteEmr(Long encounterId);
} }

View File

@@ -7,7 +7,15 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R; import com.core.common.core.domain.R;
import com.core.common.utils.SecurityUtils; import com.core.common.utils.SecurityUtils;
import com.openhis.administration.domain.Encounter;
import com.openhis.administration.domain.Patient;
import com.openhis.administration.mapper.EncounterMapper;
import com.openhis.administration.mapper.PatientMapper;
import java.util.Date;
import java.sql.Timestamp;
import com.openhis.common.enums.BindingType; import com.openhis.common.enums.BindingType;
import com.openhis.common.enums.EncounterStatus;
import com.openhis.document.domain.Emr; import com.openhis.document.domain.Emr;
import com.openhis.document.domain.EmrDetail; import com.openhis.document.domain.EmrDetail;
import com.openhis.document.domain.EmrDict; import com.openhis.document.domain.EmrDict;
@@ -46,6 +54,15 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
@Resource @Resource
IEmrDictService emrDictService; IEmrDictService emrDictService;
@Resource
private EncounterMapper encounterMapper;
@Resource
private PatientMapper patientMapper;
@Resource
private com.openhis.administration.mapper.EncounterParticipantMapper encounterParticipantMapper;
/** /**
* 添加病人病历信息 * 添加病人病历信息
* *
@@ -175,4 +192,131 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
return emrTemplateService.removeById(id) ? R.ok() : R.fail(); return emrTemplateService.removeById(id) ? R.ok() : R.fail();
} }
/**
* 获取待写病历列表
*
* @param doctorId 医生ID
* @return 待写病历列表
*/
@Override
public R<?> getPendingEmrList(Long doctorId) {
// 由于Encounter实体中没有jzPractitionerUserId字段我们需要通过关联查询来获取相关信息
// 使用医生工作站的mapper来查询相关数据
// 这里我们直接使用医生工作站的查询逻辑
// 查询当前医生负责的、状态为"就诊中"但还没有写病历的患者
// 需要通过EncounterParticipant表来关联医生信息
List<Encounter> encounters = encounterMapper.selectList(
new LambdaQueryWrapper<Encounter>()
.eq(Encounter::getStatusEnum, EncounterStatus.IN_PROGRESS.getValue())
);
// 过滤出由指定医生负责且还没有写病历的就诊记录
List<Map<String, Object>> pendingEmrs = new ArrayList<>();
for (Encounter encounter : encounters) {
// 检查该就诊记录是否已经有病历
Emr existingEmr = emrService.getOne(
new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounter.getId())
);
// 检查该就诊是否由指定医生负责
boolean isAssignedToDoctor = isEncounterAssignedToDoctor(encounter.getId(), doctorId);
if (existingEmr == null && isAssignedToDoctor) {
// 如果没有病历且由该医生负责,则添加到待写病历列表
Map<String, Object> pendingEmr = new java.util.HashMap<>();
// 获取患者信息
Patient patient = patientMapper.selectById(encounter.getPatientId());
pendingEmr.put("encounterId", encounter.getId());
pendingEmr.put("patientId", encounter.getPatientId());
pendingEmr.put("patientName", patient != null ? patient.getName() : "未知");
pendingEmr.put("gender", patient != null ? patient.getGenderEnum() : null);
// 使用出生日期计算年龄
pendingEmr.put("age", patient != null && patient.getBirthDate() != null ?
calculateAge(patient.getBirthDate()) : null);
// 使用创建时间作为挂号时间
pendingEmr.put("registerTime", encounter.getCreateTime());
pendingEmr.put("busNo", encounter.getBusNo()); // 病历号
pendingEmrs.add(pendingEmr);
}
}
return R.ok(pendingEmrs);
}
/**
* 获取待写病历数量
*
* @param doctorId 医生ID
* @return 待写病历数量
*/
@Override
public R<?> getPendingEmrCount(Long doctorId) {
// 获取待写病历列表,然后返回数量
R<?> result = getPendingEmrList(doctorId);
if (result.getCode() == 200) {
List<?> pendingEmrs = (List<?>) result.getData();
return R.ok(pendingEmrs.size());
}
return R.ok(0);
}
/**
* 检查患者是否需要写病历
*
* @param encounterId 就诊ID
* @return 患者是否需要写病历
*/
@Override
public R<?> checkNeedWriteEmr(Long encounterId) {
// 检查该就诊记录是否已经有病历
Emr existingEmr = emrService.getOne(
new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounterId)
);
// 如果没有病历,则需要写病历
boolean needWrite = existingEmr == null;
return R.ok(needWrite);
}
/**
* 检查就诊是否分配给指定医生
*
* @param encounterId 就诊ID
* @param doctorId 医生ID
* @return 是否分配给指定医生
*/
private boolean isEncounterAssignedToDoctor(Long encounterId, Long doctorId) {
// 查询就诊参与者表,检查是否有指定医生的接诊记录
com.openhis.administration.domain.EncounterParticipant participant =
encounterParticipantMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.openhis.administration.domain.EncounterParticipant>()
.eq(com.openhis.administration.domain.EncounterParticipant::getEncounterId, encounterId)
.eq(com.openhis.administration.domain.EncounterParticipant::getPractitionerId, doctorId)
);
return participant != null;
}
/**
* 根据出生日期计算年龄
*
* @param birthDate 出生日期
* @return 年龄
*/
private String calculateAge(Date birthDate) {
if (birthDate == null) {
return null;
}
// 将java.util.Date转换为java.time.LocalDate
java.time.LocalDate birthLocalDate = new java.sql.Timestamp(birthDate.getTime()).toLocalDateTime().toLocalDate();
java.time.LocalDate currentDate = java.time.LocalDate.now();
int age = java.time.Period.between(birthLocalDate, currentDate).getYears();
return String.valueOf(age);
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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