feat(router): 添加医生工作站等功能模块路由配置
- 新增医生工作站路由,包含待写病历功能 - 添加全部功能模块路由,支持功能列表和配置页面 - 集成待办事项模块路由,完善工作流功能 - 配置相关API接口和服务类,实现用户配置管理 - 实现待写病历列表展示和相关业务逻辑 - 完善首页统计数据显示功能
This commit is contained in:
@@ -0,0 +1,113 @@
|
|||||||
|
package com.core.system.controller;
|
||||||
|
|
||||||
|
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.system.domain.SysUserConfig;
|
||||||
|
import com.core.system.service.ISysUserConfigService;
|
||||||
|
import com.core.common.utils.SecurityUtils;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户配置Controller
|
||||||
|
*
|
||||||
|
* @author
|
||||||
|
* @date 2026-01-30
|
||||||
|
*/
|
||||||
|
@Api(tags = "用户配置")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/system/userConfig")
|
||||||
|
public class SysUserConfigController extends BaseController
|
||||||
|
{
|
||||||
|
@Autowired
|
||||||
|
private ISysUserConfigService sysUserConfigService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户配置列表
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:userConfig:list')")
|
||||||
|
@GetMapping("/list")
|
||||||
|
public TableDataInfo list(SysUserConfig sysUserConfig)
|
||||||
|
{
|
||||||
|
startPage();
|
||||||
|
List<SysUserConfig> list = sysUserConfigService.selectSysUserConfigList(sysUserConfig);
|
||||||
|
return getDataTable(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户配置详细信息
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:userConfig:query')")
|
||||||
|
@GetMapping(value = "/{configId}")
|
||||||
|
public AjaxResult getInfo(@PathVariable("configId") Long configId)
|
||||||
|
{
|
||||||
|
return AjaxResult.success(sysUserConfigService.selectSysUserConfigById(configId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增用户配置
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:userConfig:add')")
|
||||||
|
@Log(title = "用户配置", businessType = BusinessType.INSERT)
|
||||||
|
@PostMapping
|
||||||
|
public AjaxResult add(@RequestBody SysUserConfig sysUserConfig)
|
||||||
|
{
|
||||||
|
return AjaxResult.success(sysUserConfigService.insertSysUserConfig(sysUserConfig));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改用户配置
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:userConfig:edit')")
|
||||||
|
@Log(title = "用户配置", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping
|
||||||
|
public AjaxResult edit(@RequestBody SysUserConfig sysUserConfig)
|
||||||
|
{
|
||||||
|
return toAjax(sysUserConfigService.updateSysUserConfig(sysUserConfig));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除用户配置
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:userConfig:remove')")
|
||||||
|
@Log(title = "用户配置", businessType = BusinessType.DELETE)
|
||||||
|
@DeleteMapping("/{configIds}")
|
||||||
|
public AjaxResult remove(@PathVariable Long[] configIds)
|
||||||
|
{
|
||||||
|
return toAjax(sysUserConfigService.deleteSysUserConfigByIds(configIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户的指定配置
|
||||||
|
*/
|
||||||
|
@ApiOperation("获取当前用户的指定配置")
|
||||||
|
@GetMapping("/currentUserConfig")
|
||||||
|
public AjaxResult getCurrentUserConfig(@RequestParam String configKey)
|
||||||
|
{
|
||||||
|
Long userId = SecurityUtils.getUserId();
|
||||||
|
String configValue = sysUserConfigService.selectConfigValueByUserIdAndKey(userId, configKey);
|
||||||
|
// 返回原始配置值,不需要额外编码,由前端处理
|
||||||
|
return AjaxResult.success(configValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存当前用户的配置
|
||||||
|
*/
|
||||||
|
@ApiOperation("保存当前用户的配置")
|
||||||
|
@Log(title = "用户配置", businessType = BusinessType.UPDATE)
|
||||||
|
@PostMapping("/saveCurrentUserConfig")
|
||||||
|
public AjaxResult saveCurrentUserConfig(@RequestParam String configKey, @RequestParam String configValue)
|
||||||
|
{
|
||||||
|
Long userId = SecurityUtils.getUserId();
|
||||||
|
int result = sysUserConfigService.saveConfigValueByUserIdAndKey(userId, configKey, configValue);
|
||||||
|
return result > 0 ? AjaxResult.success() : AjaxResult.error("保存失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.core.system.domain;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.core.common.core.domain.BaseEntity;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户配置对象 sys_user_config
|
||||||
|
*
|
||||||
|
* @author
|
||||||
|
* @date 2026-01-30
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("sys_user_config")
|
||||||
|
public class SysUserConfig extends BaseEntity
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 配置ID */
|
||||||
|
@TableId
|
||||||
|
private Long configId;
|
||||||
|
|
||||||
|
/** 用户ID */
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/** 配置键名 */
|
||||||
|
private String configKey;
|
||||||
|
|
||||||
|
/** 配置值 */
|
||||||
|
private String configValue;
|
||||||
|
|
||||||
|
/** 备注 */
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
/** 创建者 - 标记为非数据库字段 */
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String createBy;
|
||||||
|
|
||||||
|
/** 创建时间 - 使用自动填充 */
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
/** 更新者 - 标记为非数据库字段 */
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String updateBy;
|
||||||
|
|
||||||
|
/** 更新时间 - 使用自动填充 */
|
||||||
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date updateTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.core.system.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.core.system.domain.SysUserConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户配置Mapper接口
|
||||||
|
*
|
||||||
|
* @author
|
||||||
|
* @date 2026-01-30
|
||||||
|
*/
|
||||||
|
public interface SysUserConfigMapper extends BaseMapper<SysUserConfig>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package com.core.system.service;
|
||||||
|
|
||||||
|
import com.core.system.domain.SysUserConfig;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户配置Service接口
|
||||||
|
*
|
||||||
|
* @author
|
||||||
|
* @date 2026-01-30
|
||||||
|
*/
|
||||||
|
public interface ISysUserConfigService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 查询用户配置
|
||||||
|
*
|
||||||
|
* @param configId 用户配置ID
|
||||||
|
* @return 用户配置
|
||||||
|
*/
|
||||||
|
public SysUserConfig selectSysUserConfigById(Long configId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户配置列表
|
||||||
|
*
|
||||||
|
* @param sysUserConfig 用户配置
|
||||||
|
* @return 用户配置集合
|
||||||
|
*/
|
||||||
|
public List<SysUserConfig> selectSysUserConfigList(SysUserConfig sysUserConfig);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增用户配置
|
||||||
|
*
|
||||||
|
* @param sysUserConfig 用户配置
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
public int insertSysUserConfig(SysUserConfig sysUserConfig);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改用户配置
|
||||||
|
*
|
||||||
|
* @param sysUserConfig 用户配置
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
public int updateSysUserConfig(SysUserConfig sysUserConfig);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除用户配置
|
||||||
|
*
|
||||||
|
* @param configIds 需要删除的用户配置ID
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
public int deleteSysUserConfigByIds(Long[] configIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除用户配置信息
|
||||||
|
*
|
||||||
|
* @param configId 用户配置ID
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
public int deleteSysUserConfigById(Long configId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID和配置键获取配置值
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param configKey 配置键
|
||||||
|
* @return 配置值
|
||||||
|
*/
|
||||||
|
public String selectConfigValueByUserIdAndKey(Long userId, String configKey);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID和配置键保存配置
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param configKey 配置键
|
||||||
|
* @param configValue 配置值
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
public int saveConfigValueByUserIdAndKey(Long userId, String configKey, String configValue);
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
package com.core.system.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.core.system.domain.SysUserConfig;
|
||||||
|
import com.core.common.utils.StringUtils;
|
||||||
|
import com.core.system.mapper.SysUserConfigMapper;
|
||||||
|
import com.core.system.service.ISysUserConfigService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户配置Service业务层处理
|
||||||
|
*
|
||||||
|
* @author
|
||||||
|
* @date 2026-01-30
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class SysUserConfigServiceImpl extends ServiceImpl<SysUserConfigMapper, SysUserConfig> implements ISysUserConfigService
|
||||||
|
{
|
||||||
|
@Autowired
|
||||||
|
private SysUserConfigMapper sysUserConfigMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户配置
|
||||||
|
*
|
||||||
|
* @param configId 用户配置ID
|
||||||
|
* @return 用户配置
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public SysUserConfig selectSysUserConfigById(Long configId)
|
||||||
|
{
|
||||||
|
return sysUserConfigMapper.selectById(configId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户配置列表
|
||||||
|
*
|
||||||
|
* @param sysUserConfig 用户配置
|
||||||
|
* @return 用户配置集合
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<SysUserConfig> selectSysUserConfigList(SysUserConfig sysUserConfig)
|
||||||
|
{
|
||||||
|
LambdaQueryWrapper<SysUserConfig> lqw = new LambdaQueryWrapper<>();
|
||||||
|
lqw.eq(StringUtils.isNotEmpty(sysUserConfig.getConfigKey()), SysUserConfig::getConfigKey, sysUserConfig.getConfigKey())
|
||||||
|
.eq(sysUserConfig.getUserId() != null, SysUserConfig::getUserId, sysUserConfig.getUserId());
|
||||||
|
return sysUserConfigMapper.selectList(lqw);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增用户配置
|
||||||
|
*
|
||||||
|
* @param sysUserConfig 用户配置
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int insertSysUserConfig(SysUserConfig sysUserConfig)
|
||||||
|
{
|
||||||
|
return sysUserConfigMapper.insert(sysUserConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改用户配置
|
||||||
|
*
|
||||||
|
* @param sysUserConfig 用户配置
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int updateSysUserConfig(SysUserConfig sysUserConfig)
|
||||||
|
{
|
||||||
|
return sysUserConfigMapper.updateById(sysUserConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID和配置键更新配置值
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param configKey 配置键
|
||||||
|
* @param configValue 配置值
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
private int updateConfigValueByUserIdAndKey(Long userId, String configKey, String configValue)
|
||||||
|
{
|
||||||
|
SysUserConfig config = new SysUserConfig();
|
||||||
|
config.setConfigValue(configValue);
|
||||||
|
|
||||||
|
LambdaQueryWrapper<SysUserConfig> lqw = new LambdaQueryWrapper<>();
|
||||||
|
lqw.eq(SysUserConfig::getUserId, userId)
|
||||||
|
.eq(SysUserConfig::getConfigKey, configKey);
|
||||||
|
|
||||||
|
return sysUserConfigMapper.update(config, lqw);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除用户配置
|
||||||
|
*
|
||||||
|
* @param configIds 需要删除的用户配置ID
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int deleteSysUserConfigByIds(Long[] configIds)
|
||||||
|
{
|
||||||
|
return sysUserConfigMapper.deleteBatchIds(java.util.Arrays.asList(configIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除用户配置信息
|
||||||
|
*
|
||||||
|
* @param configId 用户配置ID
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int deleteSysUserConfigById(Long configId)
|
||||||
|
{
|
||||||
|
return sysUserConfigMapper.deleteById(configId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID和配置键获取配置值
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param configKey 配置键
|
||||||
|
* @return 配置值
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String selectConfigValueByUserIdAndKey(Long userId, String configKey)
|
||||||
|
{
|
||||||
|
LambdaQueryWrapper<SysUserConfig> lqw = new LambdaQueryWrapper<>();
|
||||||
|
lqw.eq(SysUserConfig::getUserId, userId)
|
||||||
|
.eq(SysUserConfig::getConfigKey, configKey);
|
||||||
|
SysUserConfig config = sysUserConfigMapper.selectOne(lqw);
|
||||||
|
return config != null ? config.getConfigValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID和配置键保存配置
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param configKey 配置键
|
||||||
|
* @param configValue 配置值
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int saveConfigValueByUserIdAndKey(Long userId, String configKey, String configValue)
|
||||||
|
{
|
||||||
|
// 参数验证
|
||||||
|
if (userId == null || configKey == null) {
|
||||||
|
throw new IllegalArgumentException("用户ID和配置键不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
LambdaQueryWrapper<SysUserConfig> lqw = new LambdaQueryWrapper<>();
|
||||||
|
lqw.eq(SysUserConfig::getUserId, userId)
|
||||||
|
.eq(SysUserConfig::getConfigKey, configKey);
|
||||||
|
SysUserConfig config = sysUserConfigMapper.selectOne(lqw);
|
||||||
|
|
||||||
|
if (config != null) {
|
||||||
|
// 更新现有配置,只更新配置值,避免更新审计字段
|
||||||
|
return updateConfigValueByUserIdAndKey(userId, configKey, configValue);
|
||||||
|
} else {
|
||||||
|
// 插入新配置
|
||||||
|
SysUserConfig newConfig = new SysUserConfig();
|
||||||
|
newConfig.setUserId(userId);
|
||||||
|
newConfig.setConfigKey(configKey);
|
||||||
|
newConfig.setConfigValue(configValue);
|
||||||
|
return sysUserConfigMapper.insert(newConfig);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 记录错误日志以便调试
|
||||||
|
System.err.println("保存用户配置时发生错误: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
throw e; // 重新抛出异常让上层处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper
|
||||||
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.core.system.mapper.SysUserConfigMapper">
|
||||||
|
|
||||||
|
<resultMap type="com.core.system.domain.SysUserConfig" id="SysUserConfigResult">
|
||||||
|
<result property="configId" column="config_id" />
|
||||||
|
<result property="userId" column="user_id" />
|
||||||
|
<result property="configKey" column="config_key" />
|
||||||
|
<result property="configValue" column="config_value" />
|
||||||
|
<result property="remark" column="remark" />
|
||||||
|
<result property="createTime" column="create_time" />
|
||||||
|
<result property="updateTime" column="update_time" />
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<!-- 根据用户ID和配置键名查询配置值 -->
|
||||||
|
<select id="selectConfigValueByUserIdAndKey" resultType="String">
|
||||||
|
SELECT config_value
|
||||||
|
FROM sys_user_config
|
||||||
|
WHERE user_id = #{userId} AND config_key = #{configKey}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 根据用户ID和配置键名查询完整配置 -->
|
||||||
|
<select id="selectByUserIdAndKey" resultMap="SysUserConfigResult">
|
||||||
|
SELECT config_id, user_id, config_key, config_value, remark, create_time, update_time
|
||||||
|
FROM sys_user_config
|
||||||
|
WHERE user_id = #{userId} AND config_key = #{configKey}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 根据用户ID和配置键更新配置值 -->
|
||||||
|
<update id="updateConfigValueByUserIdAndKey">
|
||||||
|
UPDATE sys_user_config
|
||||||
|
SET config_value = #{configValue}, update_time = CURRENT_TIMESTAMP
|
||||||
|
WHERE user_id = #{userId} AND config_key = #{configKey}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.openhis.web.doctorstation.controller;
|
||||||
|
|
||||||
|
import com.core.common.core.domain.R;
|
||||||
|
import com.openhis.web.doctorstation.appservice.IDoctorStationEmrAppService;
|
||||||
|
import com.openhis.web.doctorstation.dto.PatientEmrDto;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待写病历控制器
|
||||||
|
* 用于处理医生待写病历的相关操作
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/doctor-station/pending-emr")
|
||||||
|
@Slf4j
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PendingEmrController {
|
||||||
|
|
||||||
|
private final IDoctorStationEmrAppService iDoctorStationEmrAppService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取待写病历列表
|
||||||
|
*
|
||||||
|
* @param doctorId 医生ID
|
||||||
|
* @return 待写病历列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/pending-list")
|
||||||
|
public R<?> getPendingEmrList(@RequestParam(required = false) Long doctorId) {
|
||||||
|
// 如果没有传递医生ID,则使用当前登录用户ID
|
||||||
|
if (doctorId == null) {
|
||||||
|
doctorId = com.core.common.utils.SecurityUtils.getLoginUser().getUserId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用服务获取待写病历列表
|
||||||
|
return iDoctorStationEmrAppService.getPendingEmrList(doctorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取待写病历数量
|
||||||
|
*
|
||||||
|
* @param doctorId 医生ID
|
||||||
|
* @return 待写病历数量
|
||||||
|
*/
|
||||||
|
@GetMapping("/pending-count")
|
||||||
|
public R<?> getPendingEmrCount(@RequestParam(required = false) Long doctorId) {
|
||||||
|
// 如果没有传递医生ID,则使用当前登录用户ID
|
||||||
|
if (doctorId == null) {
|
||||||
|
doctorId = com.core.common.utils.SecurityUtils.getLoginUser().getUserId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用服务获取待写病历数量
|
||||||
|
return iDoctorStationEmrAppService.getPendingEmrCount(doctorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查患者是否需要写病历
|
||||||
|
*
|
||||||
|
* @param encounterId 就诊ID
|
||||||
|
* @return 患者是否需要写病历
|
||||||
|
*/
|
||||||
|
@GetMapping("/need-write-emr")
|
||||||
|
public R<?> checkNeedWriteEmr(@RequestParam Long encounterId) {
|
||||||
|
return iDoctorStationEmrAppService.checkNeedWriteEmr(encounterId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.openhis.web.system.controller;
|
||||||
|
|
||||||
|
import com.core.common.core.domain.R;
|
||||||
|
import com.core.common.utils.SecurityUtils;
|
||||||
|
import com.openhis.web.doctorstation.appservice.IDoctorStationEmrAppService;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统首页控制器
|
||||||
|
*/
|
||||||
|
@Api(tags = "系统首页")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/system/home")
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class HomeController {
|
||||||
|
|
||||||
|
private final IDoctorStationEmrAppService doctorStationEmrAppService;
|
||||||
|
|
||||||
|
@ApiOperation("获取首页统计数据")
|
||||||
|
@GetMapping("/statistics")
|
||||||
|
public R<?> getStatistics() {
|
||||||
|
// 这里可以返回各种统计数据
|
||||||
|
// 为了简化,我们只返回待写病历数量
|
||||||
|
Long userId = SecurityUtils.getLoginUser().getUserId();
|
||||||
|
R<?> pendingEmrCount = doctorStationEmrAppService.getPendingEmrCount(userId);
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
java.util.Map<String, Object> data = new java.util.HashMap<>();
|
||||||
|
data.put("pendingEmr", pendingEmrCount.getData());
|
||||||
|
|
||||||
|
return R.ok(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
openhis-ui-vue3/src/api/system/userConfig.js
Normal file
62
openhis-ui-vue3/src/api/system/userConfig.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询用户配置列表
|
||||||
|
export function listUserConfig(query) {
|
||||||
|
return request({
|
||||||
|
url: '/system/userConfig/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询用户配置详细
|
||||||
|
export function getUserConfig(configId) {
|
||||||
|
return request({
|
||||||
|
url: '/system/userConfig/' + configId,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增用户配置
|
||||||
|
export function addUserConfig(data) {
|
||||||
|
return request({
|
||||||
|
url: '/system/userConfig',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改用户配置
|
||||||
|
export function updateUserConfig(data) {
|
||||||
|
return request({
|
||||||
|
url: '/system/userConfig',
|
||||||
|
method: 'put',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户配置
|
||||||
|
export function delUserConfig(configId) {
|
||||||
|
return request({
|
||||||
|
url: '/system/userConfig/' + configId,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户的指定配置
|
||||||
|
export function getCurrentUserConfig(configKey) {
|
||||||
|
return request({
|
||||||
|
url: '/system/userConfig/currentUserConfig',
|
||||||
|
method: 'get',
|
||||||
|
params: { configKey }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存当前用户的配置
|
||||||
|
export function saveCurrentUserConfig(configKey, configValue) {
|
||||||
|
return request({
|
||||||
|
url: '/system/userConfig/saveCurrentUserConfig',
|
||||||
|
method: 'post',
|
||||||
|
params: { configKey, configValue }
|
||||||
|
})
|
||||||
|
}
|
||||||
153
openhis-ui-vue3/src/api/workflow/task.js
Normal file
153
openhis-ui-vue3/src/api/workflow/task.js
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询待办任务列表
|
||||||
|
export function listTodo(query) {
|
||||||
|
return request({
|
||||||
|
url: '/flowable/task/todoList',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询已办任务列表
|
||||||
|
export function listFinished(query) {
|
||||||
|
return request({
|
||||||
|
url: '/flowable/task/finishedList',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询我发起的流程
|
||||||
|
export function myListProcess(query) {
|
||||||
|
return request({
|
||||||
|
url: '/flowable/task/myProcess',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成任务
|
||||||
|
export function completeTask(data) {
|
||||||
|
return request({
|
||||||
|
url: '/flowable/task/complete',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 驳回任务
|
||||||
|
export function rejectTask(data) {
|
||||||
|
return request({
|
||||||
|
url: '/flowable/task/reject',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退回任务
|
||||||
|
export function returnTask(data) {
|
||||||
|
return request({
|
||||||
|
url: '/flowable/task/return',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 认领/签收任务
|
||||||
|
export function claimTask(data) {
|
||||||
|
return request({
|
||||||
|
url: '/flowable/task/claim',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消认领/签收任务
|
||||||
|
export function unClaimTask(data) {
|
||||||
|
return request({
|
||||||
|
url: '/flowable/task/unClaim',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 委派任务
|
||||||
|
export function delegateTask(data) {
|
||||||
|
return request({
|
||||||
|
url: '/flowable/task/delegateTask',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 任务归还
|
||||||
|
export function resolveTask(data) {
|
||||||
|
return request({
|
||||||
|
url: '/flowable/task/resolveTask',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转办任务
|
||||||
|
export function assignTask(data) {
|
||||||
|
return request({
|
||||||
|
url: '/flowable/task/assignTask',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消申请
|
||||||
|
export function stopProcess(data) {
|
||||||
|
return request({
|
||||||
|
url: '/flowable/task/stopProcess',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 撤回流程
|
||||||
|
export function revokeProcess(data) {
|
||||||
|
return request({
|
||||||
|
url: '/flowable/task/revokeProcess',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除任务
|
||||||
|
export function deleteTask(data) {
|
||||||
|
return request({
|
||||||
|
url: '/flowable/task/delete',
|
||||||
|
method: 'delete',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取流程变量
|
||||||
|
export function getProcessVariables(taskId) {
|
||||||
|
return request({
|
||||||
|
url: `/flowable/task/processVariables/${taskId}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取下一节点
|
||||||
|
export function getNextFlowNode(data) {
|
||||||
|
return request({
|
||||||
|
url: '/flowable/task/nextFlowNode',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取任务表单
|
||||||
|
export function getTaskForm(taskId) {
|
||||||
|
return request({
|
||||||
|
url: '/flowable/task/getTaskForm',
|
||||||
|
method: 'get',
|
||||||
|
params: { taskId }
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -237,6 +237,55 @@ export const dynamicRoutes = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/doctorstation',
|
||||||
|
component: Layout,
|
||||||
|
redirect: '/doctorstation/index',
|
||||||
|
name: 'DoctorStation',
|
||||||
|
meta: { title: '医生工作站', icon: 'operation' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'pending-emr',
|
||||||
|
component: () => import('@/views/doctorstation/pendingEmr.vue'),
|
||||||
|
name: 'PendingEmr',
|
||||||
|
meta: { title: '待写病历', icon: 'document', permissions: ['doctorstation:pending-emr:view'] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/features',
|
||||||
|
component: Layout,
|
||||||
|
name: 'Features',
|
||||||
|
meta: { title: '全部功能', icon: 'menu' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: () => import('@/views/features/index.vue'),
|
||||||
|
name: 'FeaturesIndex',
|
||||||
|
meta: { title: '功能列表', icon: 'menu' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'config',
|
||||||
|
component: () => import('@/views/features/config.vue'),
|
||||||
|
name: 'FeaturesConfig',
|
||||||
|
meta: { title: '功能配置', icon: 'setting' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/todo',
|
||||||
|
component: Layout,
|
||||||
|
name: 'Todo',
|
||||||
|
meta: { title: '待办事项', icon: 'todo' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: () => import('@/views/todo/index.vue'),
|
||||||
|
name: 'TodoIndex',
|
||||||
|
meta: { title: '待办列表', icon: 'todo' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 合并常量路由和动态路由,确保所有路由都能被访问
|
// 合并常量路由和动态路由,确保所有路由都能被访问
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
<template>
|
||||||
|
<div class="pending-emr-container">
|
||||||
|
<div class="header">
|
||||||
|
<h2>待写病历</h2>
|
||||||
|
<div class="actions">
|
||||||
|
<el-button type="primary" @click="refreshList">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-card class="search-card">
|
||||||
|
<el-form :model="queryParams" inline>
|
||||||
|
<el-form-item label="患者姓名">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.patientName"
|
||||||
|
placeholder="请输入患者姓名"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button @click="resetQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
:data="emrList"
|
||||||
|
v-loading="loading"
|
||||||
|
style="width: 100%"
|
||||||
|
highlight-current-row
|
||||||
|
@row-click="handleRowClick"
|
||||||
|
>
|
||||||
|
<el-table-column prop="patientName" label="患者姓名" width="120" />
|
||||||
|
<el-table-column prop="busNo" label="病历号" width="150" />
|
||||||
|
<el-table-column prop="gender" label="性别" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ getGenderText(row.gender) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="age" label="年龄" width="80" />
|
||||||
|
<el-table-column prop="registerTime" label="挂号时间" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ parseTime(row.registerTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="150" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click.stop="handleWriteEmr(row)">写病历</el-button>
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
<el-button link type="primary" @click.stop="handleViewPatient(row)">查看患者</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="total > 0"
|
||||||
|
:total="total"
|
||||||
|
v-model:page="queryParams.pageNum"
|
||||||
|
v-model:size="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="PendingEmr">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { listPendingEmr, getPendingEmrCount } from '@/views/doctorstation/components/api.js'
|
||||||
|
import { parseTime } from '@/utils/index.js'
|
||||||
|
import Pagination from '@/components/Pagination'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(true)
|
||||||
|
const total = ref(0)
|
||||||
|
const emrList = ref([])
|
||||||
|
|
||||||
|
// 查询参数
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
patientName: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取待写病历列表
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await listPendingEmr(queryParams)
|
||||||
|
// 根据后端返回的数据结构调整
|
||||||
|
if (response.code === 200) {
|
||||||
|
emrList.value = response.data || []
|
||||||
|
total.value = Array.isArray(response.data) ? response.data.length : 0
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.msg || '获取待写病历列表失败')
|
||||||
|
emrList.value = []
|
||||||
|
total.value = 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取待写病历列表失败:', error)
|
||||||
|
ElMessage.error('获取待写病历列表失败')
|
||||||
|
emrList.value = []
|
||||||
|
total.value = 0
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNum = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryParams.patientName = undefined
|
||||||
|
queryParams.pageNum = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新列表
|
||||||
|
const refreshList = () => {
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 行点击事件
|
||||||
|
const handleRowClick = (row) => {
|
||||||
|
// 可以在这里处理行点击事件
|
||||||
|
console.log('点击行:', row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写病历
|
||||||
|
const handleWriteEmr = (row) => {
|
||||||
|
console.log('写病历:', row)
|
||||||
|
// 触发写病历事件,通知父组件切换到病历页面
|
||||||
|
emit('writeEmr', row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看患者
|
||||||
|
const handleViewPatient = (row) => {
|
||||||
|
console.log('查看患者:', row)
|
||||||
|
// 触发查看患者事件
|
||||||
|
emit('viewPatient', row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取性别文本
|
||||||
|
const getGenderText = (genderValue) => {
|
||||||
|
const genderMap = {
|
||||||
|
1: '男',
|
||||||
|
2: '女'
|
||||||
|
}
|
||||||
|
return genderMap[genderValue] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
getList()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义emit
|
||||||
|
const emit = defineEmits(['writeEmr', 'viewPatient'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.pending-emr-container {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
219
openhis-ui-vue3/src/views/doctorstation/pendingEmr.vue
Normal file
219
openhis-ui-vue3/src/views/doctorstation/pendingEmr.vue
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
<template>
|
||||||
|
<div class="pending-emr-page">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>
|
||||||
|
<el-icon style="margin-right: 8px;">
|
||||||
|
<Document />
|
||||||
|
</el-icon>
|
||||||
|
待写病历
|
||||||
|
</h2>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button type="primary" @click="refreshList">
|
||||||
|
<el-icon>
|
||||||
|
<Refresh />
|
||||||
|
</el-icon>
|
||||||
|
刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索区域 -->
|
||||||
|
<el-card class="search-card">
|
||||||
|
<el-form :model="queryParams" inline>
|
||||||
|
<el-form-item label="患者姓名">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.patientName"
|
||||||
|
placeholder="请输入患者姓名"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleQuery">
|
||||||
|
<el-icon>
|
||||||
|
<Search />
|
||||||
|
</el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetQuery">
|
||||||
|
<el-icon>
|
||||||
|
<Delete />
|
||||||
|
</el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<el-table
|
||||||
|
:data="emrList"
|
||||||
|
v-loading="loading"
|
||||||
|
style="width: 100%"
|
||||||
|
highlight-current-row
|
||||||
|
@row-click="handleRowClick"
|
||||||
|
>
|
||||||
|
<el-table-column prop="patientName" label="患者姓名" width="120" />
|
||||||
|
<el-table-column prop="busNo" label="病历号" width="150" />
|
||||||
|
<el-table-column prop="gender" label="性别" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ getGenderText(row.gender) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="age" label="年龄" width="80" />
|
||||||
|
<el-table-column prop="registerTime" label="挂号时间" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ parseTime(row.registerTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="150" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click.stop="handleWriteEmr(row)">写病历</el-button>
|
||||||
|
<span class="table-divider">|</span>
|
||||||
|
<el-button link type="primary" @click.stop="handleViewPatient(row)">查看患者</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<pagination
|
||||||
|
v-show="total > 0"
|
||||||
|
:total="total"
|
||||||
|
v-model:page="queryParams.pageNum"
|
||||||
|
v-model:size="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { listPendingEmr, getPendingEmrCount } from '@/views/doctorstation/components/api.js'
|
||||||
|
import { parseTime } from '@/utils/index.js'
|
||||||
|
import Pagination from '@/components/Pagination'
|
||||||
|
import { Document, Refresh, Search, Delete } from '@element-plus/icons-vue'
|
||||||
|
import { ElDivider } from 'element-plus'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(true)
|
||||||
|
const total = ref(0)
|
||||||
|
const emrList = ref([])
|
||||||
|
|
||||||
|
// 查询参数
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
patientName: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取待写病历列表
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await listPendingEmr(queryParams)
|
||||||
|
// 根据后端返回的数据结构调整
|
||||||
|
if (response.code === 200) {
|
||||||
|
emrList.value = response.data || []
|
||||||
|
total.value = Array.isArray(response.data) ? response.data.length : 0
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.msg || '获取待写病历列表失败')
|
||||||
|
emrList.value = []
|
||||||
|
total.value = 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取待写病历列表失败:', error)
|
||||||
|
ElMessage.error('获取待写病历列表失败')
|
||||||
|
emrList.value = []
|
||||||
|
total.value = 0
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNum = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryParams.patientName = undefined
|
||||||
|
queryParams.pageNum = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新列表
|
||||||
|
const refreshList = () => {
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 行点击事件
|
||||||
|
const handleRowClick = (row) => {
|
||||||
|
// 可以在这里处理行点击事件
|
||||||
|
console.log('点击行:', row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写病历
|
||||||
|
const handleWriteEmr = (row) => {
|
||||||
|
console.log('写病历:', row)
|
||||||
|
// 这里可以触发写病历事件
|
||||||
|
// 可能需要跳转到病历编辑页面
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看患者
|
||||||
|
const handleViewPatient = (row) => {
|
||||||
|
console.log('查看患者:', row)
|
||||||
|
// 这里可以触发查看患者事件
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取性别文本
|
||||||
|
const getGenderText = (genderValue) => {
|
||||||
|
const genderMap = {
|
||||||
|
1: '男',
|
||||||
|
2: '女'
|
||||||
|
}
|
||||||
|
return genderMap[genderValue] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
getList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.pending-emr-page {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-divider {
|
||||||
|
margin: 0 8px;
|
||||||
|
color: #dcdfe6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
746
openhis-ui-vue3/src/views/features/config.vue
Normal file
746
openhis-ui-vue3/src/views/features/config.vue
Normal file
@@ -0,0 +1,746 @@
|
|||||||
|
<template>
|
||||||
|
<div class="config-container">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>首页功能配置</h2>
|
||||||
|
<p>选择要在首页快捷功能区域显示的功能</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-content">
|
||||||
|
<el-card class="config-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>功能选择</span>
|
||||||
|
<el-button class="button" type="primary" @click="saveConfig">保存配置</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="config-layout">
|
||||||
|
<div class="menu-tree-section">
|
||||||
|
<el-input
|
||||||
|
v-model="filterText"
|
||||||
|
placeholder="输入关键字进行过滤"
|
||||||
|
size="default"
|
||||||
|
style="margin-bottom: 16px;"
|
||||||
|
/>
|
||||||
|
<el-card v-loading="loading" class="tree-card">
|
||||||
|
<el-tree
|
||||||
|
ref="treeRef"
|
||||||
|
:data="menuTree"
|
||||||
|
:props="treeProps"
|
||||||
|
show-checkbox
|
||||||
|
node-key="menuId"
|
||||||
|
:default-expanded-keys="expandedKeys"
|
||||||
|
:default-checked-keys="checkedKeys"
|
||||||
|
:filter-node-method="filterNode"
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
@check="handleCheckChange"
|
||||||
|
>
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<span class="custom-tree-node">
|
||||||
|
<div class="tree-node-info">
|
||||||
|
<div class="node-main">
|
||||||
|
<el-icon :size="16" :color="getIconColor(data)">
|
||||||
|
<component :is="getIconComponent(data.icon)" />
|
||||||
|
</el-icon>
|
||||||
|
<span class="menu-label" style="margin-left: 8px;">{{ node.label }}</span>
|
||||||
|
<el-tag v-if="data.fullPath" type="info" size="small" effect="plain" class="path-tag-inline">
|
||||||
|
{{ data.fullPath }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-else-if="data.path" type="info" size="small" effect="plain" class="path-tag-inline">
|
||||||
|
{{ data.path }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
<el-tag
|
||||||
|
v-if="data.menuType === 'M'"
|
||||||
|
type="info"
|
||||||
|
size="small"
|
||||||
|
style="margin-right: 8px;">
|
||||||
|
目录
|
||||||
|
</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-if="data.menuType === 'C'"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
style="margin-right: 8px;">
|
||||||
|
菜单
|
||||||
|
</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-if="data.menuType === 'F'"
|
||||||
|
type="warning"
|
||||||
|
size="small">
|
||||||
|
按钮
|
||||||
|
</el-tag>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="selected-functions-section">
|
||||||
|
<h4>已选择的功能</h4>
|
||||||
|
<div class="selected-functions-list">
|
||||||
|
<div
|
||||||
|
v-for="item in selectedFunctions"
|
||||||
|
:key="item.menuId"
|
||||||
|
class="selected-function-item"
|
||||||
|
>
|
||||||
|
<div class="function-info">
|
||||||
|
<el-icon :size="16" :color="getIconColor(item)">
|
||||||
|
<component :is="getIconComponent(item.icon)" />
|
||||||
|
</el-icon>
|
||||||
|
<div class="function-details">
|
||||||
|
<span class="function-name">{{ item.menuName }}</span>
|
||||||
|
<el-tag v-if="item.fullPath" type="info" size="small" class="function-path-below">
|
||||||
|
{{ item.fullPath }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-else-if="item.path" type="info" size="small" class="function-path-below">
|
||||||
|
{{ item.path }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
icon="Delete"
|
||||||
|
circle
|
||||||
|
@click="removeSelected(item.menuId)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedFunctions.length === 0" class="no-selected">
|
||||||
|
暂无选择功能
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, nextTick, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import useUserStore from '@/store/modules/user'
|
||||||
|
import { listMenu } from '@/api/system/menu'
|
||||||
|
import { getMenuFullPath } from '@/api/system/menu'
|
||||||
|
import { saveCurrentUserConfig, getCurrentUserConfig } from '@/api/system/userConfig'
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
Grid,
|
||||||
|
Folder,
|
||||||
|
Tickets,
|
||||||
|
Document,
|
||||||
|
Setting,
|
||||||
|
User,
|
||||||
|
Goods,
|
||||||
|
ChatDotSquare,
|
||||||
|
Histogram,
|
||||||
|
Wallet,
|
||||||
|
OfficeBuilding,
|
||||||
|
Postcard,
|
||||||
|
Collection,
|
||||||
|
VideoPlay,
|
||||||
|
Camera,
|
||||||
|
Headset,
|
||||||
|
Phone,
|
||||||
|
Message,
|
||||||
|
ChatLineSquare,
|
||||||
|
ChatRound,
|
||||||
|
Guide,
|
||||||
|
Help,
|
||||||
|
InfoFilled,
|
||||||
|
CircleCheck,
|
||||||
|
CircleClose,
|
||||||
|
Warning,
|
||||||
|
QuestionFilled,
|
||||||
|
Star,
|
||||||
|
Link,
|
||||||
|
Position,
|
||||||
|
Picture,
|
||||||
|
Upload,
|
||||||
|
Download,
|
||||||
|
CaretLeft,
|
||||||
|
CaretRight,
|
||||||
|
More,
|
||||||
|
Close,
|
||||||
|
Check,
|
||||||
|
ArrowUp,
|
||||||
|
ArrowDown,
|
||||||
|
ArrowLeft,
|
||||||
|
ArrowRight,
|
||||||
|
Plus,
|
||||||
|
Minus,
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
|
Refresh,
|
||||||
|
Search,
|
||||||
|
Edit,
|
||||||
|
Delete,
|
||||||
|
Share,
|
||||||
|
View,
|
||||||
|
SwitchButton,
|
||||||
|
Hide,
|
||||||
|
Finished,
|
||||||
|
CirclePlus,
|
||||||
|
Remove,
|
||||||
|
CircleCheckFilled,
|
||||||
|
CircleCloseFilled,
|
||||||
|
WarningFilled,
|
||||||
|
InfoFilled as InfoFilledIcon,
|
||||||
|
SuccessFilled,
|
||||||
|
QuestionFilled as QuestionFilledIcon
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
// 添加 loading 状态
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const treeRef = ref()
|
||||||
|
const menuTree = ref([])
|
||||||
|
const expandedKeys = ref([])
|
||||||
|
const checkedKeys = ref([])
|
||||||
|
const selectedFunctions = ref([]) // 已选择的功能
|
||||||
|
const filterText = ref('') // 过滤文本
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const treeProps = {
|
||||||
|
children: 'children',
|
||||||
|
label: 'menuName'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听过滤文本变化
|
||||||
|
watch(filterText, (val) => {
|
||||||
|
treeRef.value?.filter(val)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 图标映射
|
||||||
|
const iconMap = {
|
||||||
|
'menu': Menu,
|
||||||
|
'grid': Grid,
|
||||||
|
'folder': Folder,
|
||||||
|
'tickets': Tickets,
|
||||||
|
'document': Document,
|
||||||
|
'setting': Setting,
|
||||||
|
'user': User,
|
||||||
|
'goods': Goods,
|
||||||
|
'chat-dot-square': ChatDotSquare,
|
||||||
|
'histogram': Histogram,
|
||||||
|
'wallet': Wallet,
|
||||||
|
'office-building': OfficeBuilding,
|
||||||
|
'postcard': Postcard,
|
||||||
|
'collection': Collection,
|
||||||
|
'video-play': VideoPlay,
|
||||||
|
'camera': Camera,
|
||||||
|
'headset': Headset,
|
||||||
|
'phone': Phone,
|
||||||
|
'message': Message,
|
||||||
|
'chat-line-square': ChatLineSquare,
|
||||||
|
'chat-round': ChatRound,
|
||||||
|
'guide': Guide,
|
||||||
|
'help': Help,
|
||||||
|
'info-filled': InfoFilled,
|
||||||
|
'circle-check': CircleCheck,
|
||||||
|
'circle-close': CircleClose,
|
||||||
|
'warning': Warning,
|
||||||
|
'question-filled': QuestionFilled,
|
||||||
|
'star': Star,
|
||||||
|
'link': Link,
|
||||||
|
'position': Position,
|
||||||
|
'picture': Picture,
|
||||||
|
'upload': Upload,
|
||||||
|
'download': Download,
|
||||||
|
'caret-left': CaretLeft,
|
||||||
|
'caret-right': CaretRight,
|
||||||
|
'more': More,
|
||||||
|
'close': Close,
|
||||||
|
'check': Check,
|
||||||
|
'arrow-up': ArrowUp,
|
||||||
|
'arrow-down': ArrowDown,
|
||||||
|
'arrow-left': ArrowLeft,
|
||||||
|
'arrow-right': ArrowRight,
|
||||||
|
'plus': Plus,
|
||||||
|
'minus': Minus,
|
||||||
|
'zoom-in': ZoomIn,
|
||||||
|
'zoom-out': ZoomOut,
|
||||||
|
'refresh': Refresh,
|
||||||
|
'search': Search,
|
||||||
|
'edit': Edit,
|
||||||
|
'delete': Delete,
|
||||||
|
'share': Share,
|
||||||
|
'view': View,
|
||||||
|
'switch-button': SwitchButton,
|
||||||
|
'hide': Hide,
|
||||||
|
'finished': Finished,
|
||||||
|
'circle-plus': CirclePlus,
|
||||||
|
'remove': Remove,
|
||||||
|
'circle-check-filled': CircleCheckFilled,
|
||||||
|
'circle-close-filled': CircleCloseFilled,
|
||||||
|
'warning-filled': WarningFilled,
|
||||||
|
'info-filled-icon': InfoFilledIcon,
|
||||||
|
'success-filled': SuccessFilled,
|
||||||
|
'question-filled-icon': QuestionFilledIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图标组件
|
||||||
|
const getIconComponent = (iconName) => {
|
||||||
|
if (!iconName) return Document
|
||||||
|
// 移除前缀,如 fa-, el-icon-
|
||||||
|
const cleanIconName = iconName.replace(/^(fa-|el-icon-)/, '').toLowerCase()
|
||||||
|
return iconMap[cleanIconName] || Document
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图标颜色
|
||||||
|
const getIconColor = (data) => {
|
||||||
|
if (data.menuType === 'M') return '#409EFF' // 目录蓝色
|
||||||
|
if (data.menuType === 'C') return '#67C23A' // 菜单绿色
|
||||||
|
if (data.menuType === 'F') return '#E6A23C' // 按钮橙色
|
||||||
|
return '#909399' // 默认灰色
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载菜单数据
|
||||||
|
const loadMenuData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await listMenu({})
|
||||||
|
if (response.code === 200) {
|
||||||
|
// 过滤掉隐藏的菜单项、目录和按钮类型的菜单,只保留当前角色可访问的菜单项
|
||||||
|
const filteredMenus = filterVisibleMenus(response.data)
|
||||||
|
menuTree.value = filteredMenus
|
||||||
|
|
||||||
|
// 展开所有节点
|
||||||
|
expandedKeys.value = getAllNodeIds(filteredMenus)
|
||||||
|
|
||||||
|
// 获取已保存的配置
|
||||||
|
await loadSavedConfig()
|
||||||
|
} else {
|
||||||
|
ElMessage.error('获取菜单数据失败: ' + response.msg)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载菜单数据失败:', error)
|
||||||
|
ElMessage.error('加载菜单数据失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤可见的菜单项(非隐藏且非按钮类型,仅显示当前角色可访问的菜单)
|
||||||
|
const filterVisibleMenus = (menus) => {
|
||||||
|
return menus.filter(menu => {
|
||||||
|
// 过滤掉隐藏的菜单项和按钮类型的菜单,保留目录(M类型)和菜单(C类型)
|
||||||
|
// visible为'0'表示可见,'1'表示隐藏
|
||||||
|
return menu.visible !== '1' && menu.menuType !== 'F'
|
||||||
|
}).map(menu => {
|
||||||
|
// 保留完整路径信息
|
||||||
|
if (menu.fullPath) {
|
||||||
|
menu.fullPath = menu.fullPath;
|
||||||
|
}
|
||||||
|
// 递归处理子菜单
|
||||||
|
if (menu.children && menu.children.length > 0) {
|
||||||
|
menu.children = filterVisibleMenus(menu.children)
|
||||||
|
}
|
||||||
|
return menu
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有节点ID
|
||||||
|
const getAllNodeIds = (nodes) => {
|
||||||
|
const ids = []
|
||||||
|
nodes.forEach(node => {
|
||||||
|
ids.push(node.menuId)
|
||||||
|
if (node.children) {
|
||||||
|
ids.push(...getAllNodeIds(node.children))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
const saveConfig = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 获取所有选中的节点,包括半选状态的节点
|
||||||
|
const checkedNodes = treeRef.value.getCheckedNodes()
|
||||||
|
// 只保留菜单类型(C类型)的节点
|
||||||
|
const validMenuNodes = checkedNodes.filter(node => node.menuType === 'C')
|
||||||
|
|
||||||
|
// 创建包含菜单ID和完整路径的对象数组
|
||||||
|
const menuPromises = validMenuNodes.map(async (node) => {
|
||||||
|
try {
|
||||||
|
console.log(`开始获取菜单 ${node.menuName} (ID: ${node.menuId}) 的完整路径`);
|
||||||
|
console.log(`节点对象:`, node);
|
||||||
|
console.log(`节点的 path 属性:`, node.path);
|
||||||
|
|
||||||
|
// 获取菜单的完整路径
|
||||||
|
const fullPathResponse = await getMenuFullPath(node.menuId);
|
||||||
|
console.log(`菜单 ${node.menuName} 的完整路径响应:`, fullPathResponse);
|
||||||
|
|
||||||
|
let fullPath = fullPathResponse.code === 200 ? (fullPathResponse.data || fullPathResponse.msg) : node.path;
|
||||||
|
// 确保路径格式正确,去除多余的斜杠
|
||||||
|
if (fullPath && typeof fullPath === 'string') {
|
||||||
|
// 将多个连续的斜杠替换为单个斜杠,但保留协议部分的双斜杠(如 http://)
|
||||||
|
fullPath = fullPath.replace(/([^:])\/{2,}/g, '$1/');
|
||||||
|
}
|
||||||
|
console.log(`菜单 ${node.menuName} 的完整路径:`, fullPath);
|
||||||
|
|
||||||
|
const menuItem = {
|
||||||
|
menuId: node.menuId,
|
||||||
|
fullPath: fullPath,
|
||||||
|
menuName: node.menuName,
|
||||||
|
path: node.path,
|
||||||
|
icon: node.icon, // 保存图标信息
|
||||||
|
menuType: node.menuType // 保存菜单类型信息
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`构造的菜单项对象:`, menuItem);
|
||||||
|
return menuItem;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`获取菜单 ${node.menuName} 的完整路径失败:`, error);
|
||||||
|
console.error(`错误堆栈:`, error.stack);
|
||||||
|
|
||||||
|
// 如果获取完整路径失败,使用现有路径作为备选
|
||||||
|
console.log(`在错误处理中,节点的 path 属性:`, node.path);
|
||||||
|
const menuItem = {
|
||||||
|
menuId: node.menuId,
|
||||||
|
fullPath: node.path,
|
||||||
|
menuName: node.menuName,
|
||||||
|
path: node.path,
|
||||||
|
icon: node.icon, // 保存图标信息
|
||||||
|
menuType: node.menuType // 保存菜单类型信息
|
||||||
|
};
|
||||||
|
console.log(`构造的菜单项对象(错误处理):`, menuItem);
|
||||||
|
return menuItem;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 等待所有完整路径获取完成
|
||||||
|
const menuDataWithPaths = await Promise.all(menuPromises);
|
||||||
|
|
||||||
|
// 添加调试信息
|
||||||
|
console.log('准备保存的菜单数据:', menuDataWithPaths);
|
||||||
|
|
||||||
|
// 检查每个对象是否包含 fullPath 属性
|
||||||
|
menuDataWithPaths.forEach((item, index) => {
|
||||||
|
console.log(`菜单项 ${index} 包含 fullPath:`, item.hasOwnProperty('fullPath'), '值为:', item.fullPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 对配置值进行URL编码以避免特殊字符问题
|
||||||
|
const encodedConfigValue = encodeURIComponent(JSON.stringify(menuDataWithPaths))
|
||||||
|
console.log('编码后的配置值:', encodedConfigValue);
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
const saveResult = await saveCurrentUserConfig('homeFeaturesConfig', encodedConfigValue)
|
||||||
|
|
||||||
|
if (saveResult.code === 200) {
|
||||||
|
// 只有在数据库保存成功后,才保存到本地存储
|
||||||
|
localStorage.setItem('homeFeaturesConfig', JSON.stringify(menuDataWithPaths))
|
||||||
|
ElMessage.success('配置保存成功')
|
||||||
|
// 触发全局事件,通知首页更新快捷功能
|
||||||
|
window.dispatchEvent(new Event('homeFeaturesConfigUpdated'));
|
||||||
|
} else {
|
||||||
|
console.error('保存到数据库失败:', saveResult);
|
||||||
|
ElMessage.error('保存到数据库失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存配置失败:', error)
|
||||||
|
ElMessage.error('保存配置失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理复选框变化
|
||||||
|
const handleCheckChange = () => {
|
||||||
|
// 获取当前选中的节点
|
||||||
|
const checkedNodes = treeRef.value.getCheckedNodes()
|
||||||
|
// 只保留菜单类型(C)的节点,排除目录(M)和按钮(F)
|
||||||
|
selectedFunctions.value = checkedNodes.filter(node => node.menuType === 'C')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除已选择的功能
|
||||||
|
const removeSelected = (menuId) => {
|
||||||
|
// 从树中取消勾选该节点
|
||||||
|
treeRef.value.setChecked(menuId, false, false)
|
||||||
|
// 更新已选择的功能列表
|
||||||
|
selectedFunctions.value = selectedFunctions.value.filter(item => item.menuId !== menuId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取默认选择项(前几个菜单项)
|
||||||
|
const getDefaultSelections = (nodes, count = 8) => {
|
||||||
|
const selections = []
|
||||||
|
const traverse = (items) => {
|
||||||
|
for (const item of items) {
|
||||||
|
if (selections.length >= count) break
|
||||||
|
if (item.menuType === 'C') { // 只选择菜单类型(C),不选择目录(M)或按钮(F)
|
||||||
|
selections.push(item.menuId)
|
||||||
|
}
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
traverse(item.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
traverse(nodes)
|
||||||
|
return selections.slice(0, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤节点方法
|
||||||
|
const filterNode = (value, data) => {
|
||||||
|
if (!value) return true
|
||||||
|
return data.menuName.indexOf(value) !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载已保存的配置
|
||||||
|
const loadSavedConfig = async () => {
|
||||||
|
try {
|
||||||
|
// 优先从数据库获取已保存的配置
|
||||||
|
const response = await getCurrentUserConfig('homeFeaturesConfig')
|
||||||
|
let savedConfig = null;
|
||||||
|
|
||||||
|
if (response.code === 200) {
|
||||||
|
savedConfig = response.data; // 从数据库获取的配置
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果数据库中没有配置,尝试从本地存储获取
|
||||||
|
if (!savedConfig) {
|
||||||
|
savedConfig = localStorage.getItem('homeFeaturesConfig')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedConfig) {
|
||||||
|
// 尝试解码配置值(如果是URL编码的)
|
||||||
|
let parsedConfig;
|
||||||
|
try {
|
||||||
|
// 先尝试解码
|
||||||
|
const decodedConfig = decodeURIComponent(savedConfig);
|
||||||
|
parsedConfig = JSON.parse(decodedConfig);
|
||||||
|
} catch (e) {
|
||||||
|
// 如果解码失败,尝试直接解析(兼容旧数据)
|
||||||
|
try {
|
||||||
|
parsedConfig = JSON.parse(savedConfig);
|
||||||
|
} catch (e2) {
|
||||||
|
console.error('解析配置失败:', e2);
|
||||||
|
parsedConfig = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedConfig && Array.isArray(parsedConfig)) {
|
||||||
|
// 检查数据格式,如果是包含对象的数组(新格式),提取菜单ID
|
||||||
|
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
|
||||||
|
} else {
|
||||||
|
// 如果解析失败,使用默认配置
|
||||||
|
const defaultSelections = getDefaultSelections(menuTree.value)
|
||||||
|
checkedKeys.value = defaultSelections
|
||||||
|
// 初始化已选择的功能
|
||||||
|
const allNodes = getAllNodes(menuTree.value)
|
||||||
|
const checkedNodes = allNodes.filter(node =>
|
||||||
|
defaultSelections.includes(node.menuId) && node.menuType === 'C'
|
||||||
|
)
|
||||||
|
selectedFunctions.value = checkedNodes
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有配置,使用默认配置
|
||||||
|
const defaultSelections = getDefaultSelections(menuTree.value)
|
||||||
|
checkedKeys.value = defaultSelections
|
||||||
|
// 初始化已选择的功能
|
||||||
|
const allNodes = getAllNodes(menuTree.value)
|
||||||
|
const checkedNodes = allNodes.filter(node =>
|
||||||
|
defaultSelections.includes(node.menuId) && node.menuType === 'C'
|
||||||
|
)
|
||||||
|
selectedFunctions.value = checkedNodes
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载配置失败:', error)
|
||||||
|
// 如果加载失败,使用默认配置
|
||||||
|
const defaultSelections = getDefaultSelections(menuTree.value)
|
||||||
|
checkedKeys.value = defaultSelections
|
||||||
|
// 初始化已选择的功能
|
||||||
|
const allNodes = getAllNodes(menuTree.value)
|
||||||
|
const checkedNodes = allNodes.filter(node =>
|
||||||
|
defaultSelections.includes(node.menuId)
|
||||||
|
)
|
||||||
|
selectedFunctions.value = checkedNodes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有节点(递归)
|
||||||
|
const getAllNodes = (nodes) => {
|
||||||
|
if (!nodes || !Array.isArray(nodes)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = []
|
||||||
|
nodes.forEach(node => {
|
||||||
|
result.push(node)
|
||||||
|
if (node.children && Array.isArray(node.children)) {
|
||||||
|
result = result.concat(getAllNodes(node.children))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadMenuData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.config-container {
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
min-height: calc(100vh - 120px);
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-content {
|
||||||
|
.config-card {
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-tree {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 14px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0; /* 允许flex项目收缩到其内容的固有尺寸以下 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0; /* 允许收缩 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-label {
|
||||||
|
margin: 0 8px;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-tag-inline {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 8px;
|
||||||
|
flex-shrink: 0; /* 防止路径标签被压缩 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-tree-section {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-card {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-functions-section {
|
||||||
|
width: 400px; /* 增加宽度 */
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-functions-list {
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-function-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start; /* 改为flex-start以适应多行内容 */
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0 8px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-name {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-path-below {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
align-self: flex-start; /* 让路径标签左对齐 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-selected {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
386
openhis-ui-vue3/src/views/features/index.vue
Normal file
386
openhis-ui-vue3/src/views/features/index.vue
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
<template>
|
||||||
|
<div class="features-container">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>快捷功能</h2>
|
||||||
|
<p>这里展示了您配置的快捷功能模块</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-loading="loading" element-loading-text="正在加载快捷功能...">
|
||||||
|
<div class="features-grid">
|
||||||
|
<div
|
||||||
|
v-for="feature in userFeatures"
|
||||||
|
:key="feature.menuId"
|
||||||
|
class="feature-card"
|
||||||
|
@click="goToFeature(feature.fullPath)"
|
||||||
|
>
|
||||||
|
<div class="feature-icon">
|
||||||
|
<el-icon :size="32" :color="getIconColor(feature)">
|
||||||
|
<component :is="getIconComponent(feature.icon)" />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="feature-title">{{ feature.menuName }}</div>
|
||||||
|
<div class="feature-path" v-if="feature.fullPath">{{ feature.fullPath }}</div>
|
||||||
|
<div class="feature-path" v-else-if="feature.path">{{ feature.path }}</div>
|
||||||
|
<div class="feature-desc">{{ feature.remark || '功能描述未设置' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="userFeatures.length === 0 && !loading" class="no-features">
|
||||||
|
暂无配置的快捷功能,请前往 <el-link type="primary" @click="goToConfig">功能配置</el-link> 页面进行设置
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { listMenu, getMenuFullPath } from '@/api/system/menu'
|
||||||
|
import { getCurrentUserConfig } from '@/api/system/userConfig'
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
Grid,
|
||||||
|
Folder,
|
||||||
|
Tickets,
|
||||||
|
Document,
|
||||||
|
Setting,
|
||||||
|
User,
|
||||||
|
Goods,
|
||||||
|
ChatDotSquare,
|
||||||
|
Histogram,
|
||||||
|
Wallet,
|
||||||
|
OfficeBuilding,
|
||||||
|
Postcard,
|
||||||
|
Collection,
|
||||||
|
VideoPlay,
|
||||||
|
Camera,
|
||||||
|
Headset,
|
||||||
|
Phone,
|
||||||
|
Message,
|
||||||
|
ChatLineSquare,
|
||||||
|
ChatRound,
|
||||||
|
Guide,
|
||||||
|
Help,
|
||||||
|
InfoFilled,
|
||||||
|
CircleCheck,
|
||||||
|
CircleClose,
|
||||||
|
Warning,
|
||||||
|
QuestionFilled,
|
||||||
|
Star,
|
||||||
|
Link,
|
||||||
|
Position,
|
||||||
|
Picture,
|
||||||
|
Upload,
|
||||||
|
Download,
|
||||||
|
CaretLeft,
|
||||||
|
CaretRight,
|
||||||
|
More,
|
||||||
|
Close,
|
||||||
|
Check,
|
||||||
|
ArrowUp,
|
||||||
|
ArrowDown,
|
||||||
|
ArrowLeft,
|
||||||
|
ArrowRight,
|
||||||
|
Plus,
|
||||||
|
Minus,
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
|
Refresh,
|
||||||
|
Search,
|
||||||
|
Edit,
|
||||||
|
Delete,
|
||||||
|
Share,
|
||||||
|
View,
|
||||||
|
SwitchButton,
|
||||||
|
Hide,
|
||||||
|
Finished,
|
||||||
|
CirclePlus,
|
||||||
|
Remove,
|
||||||
|
CircleCheckFilled,
|
||||||
|
CircleCloseFilled,
|
||||||
|
WarningFilled,
|
||||||
|
InfoFilled as InfoFilledIcon,
|
||||||
|
SuccessFilled,
|
||||||
|
QuestionFilled as QuestionFilledIcon
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
// 添加 loading 状态
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const userFeatures = ref([])
|
||||||
|
|
||||||
|
// 图标映射
|
||||||
|
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) => {
|
||||||
|
if (data.menuType === 'M') return '#409EFF' // 目录蓝色
|
||||||
|
if (data.menuType === 'C') return '#67C23A' // 菜单绿色
|
||||||
|
if (data.menuType === 'F') return '#E6A23C' // 按钮橙色
|
||||||
|
return '#909399' // 默认灰色
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载用户配置的快捷功能
|
||||||
|
const loadUserFeatures = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
// 获取用户配置的快捷功能数据
|
||||||
|
const configResponse = await getCurrentUserConfig('homeFeaturesConfig')
|
||||||
|
let menuDataWithPaths = [];
|
||||||
|
|
||||||
|
if (configResponse.code === 200 && configResponse.data) {
|
||||||
|
// 解析配置数据
|
||||||
|
try {
|
||||||
|
const decodedConfig = decodeURIComponent(configResponse.data);
|
||||||
|
menuDataWithPaths = JSON.parse(decodedConfig);
|
||||||
|
} catch (e) {
|
||||||
|
// 如果解码失败,尝试直接解析
|
||||||
|
try {
|
||||||
|
menuDataWithPaths = JSON.parse(configResponse.data);
|
||||||
|
} catch (e2) {
|
||||||
|
console.error('解析用户配置失败:', e2);
|
||||||
|
menuDataWithPaths = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有配置,从本地存储获取
|
||||||
|
const localConfig = localStorage.getItem('homeFeaturesConfig');
|
||||||
|
if (localConfig) {
|
||||||
|
menuDataWithPaths = JSON.parse(localConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接使用保存的完整路径数据,无需再次调用API
|
||||||
|
userFeatures.value = menuDataWithPaths;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载用户快捷功能失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将树形菜单结构扁平化
|
||||||
|
const flattenMenuTree = (menuTree) => {
|
||||||
|
const result = [];
|
||||||
|
const traverse = (items) => {
|
||||||
|
for (const item of items) {
|
||||||
|
result.push(item);
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
traverse(item.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
traverse(menuTree);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到功能页面
|
||||||
|
const goToFeature = (path) => {
|
||||||
|
if (path) {
|
||||||
|
// 检查是否为外部链接
|
||||||
|
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||||
|
// 如果是外部链接,使用 window.open 打开
|
||||||
|
window.open(path, '_blank');
|
||||||
|
} else {
|
||||||
|
// 确保内部路径以 / 开头,以保证正确的路由跳转
|
||||||
|
const normalizedPath = path.startsWith('/') ? path : '/' + path;
|
||||||
|
router.push(normalizedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到配置页面
|
||||||
|
const goToConfig = () => {
|
||||||
|
router.push('/features/config')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadUserFeatures()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.features-container {
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
min-height: calc(100vh - 120px);
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||||
|
border-color: #c6e2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-path {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #409EFF;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
word-break: break-all;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background-color: #ecf5ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-features {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
.el-link {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.features-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.feature-title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-path {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-desc {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
418
openhis-ui-vue3/src/views/todo/index.vue
Normal file
418
openhis-ui-vue3/src/views/todo/index.vue
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
<template>
|
||||||
|
<div class="todo-container">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>全部待办事项</h2>
|
||||||
|
<p>这里展示了您的所有待办事项</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-section">
|
||||||
|
<el-form :model="queryParams" ref="queryRef" :inline="true">
|
||||||
|
<el-form-item label="优先级" prop="priority">
|
||||||
|
<el-select v-model="queryParams.priority" placeholder="请选择优先级" clearable>
|
||||||
|
<el-option label="高" value="high"></el-option>
|
||||||
|
<el-option label="中" value="medium"></el-option>
|
||||||
|
<el-option label="低" value="low"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
|
||||||
|
<el-option label="未处理" value="pending"></el-option>
|
||||||
|
<el-option label="处理中" value="processing"></el-option>
|
||||||
|
<el-option label="已完成" value="completed"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="todo-list">
|
||||||
|
<el-card
|
||||||
|
v-for="todo in todoList"
|
||||||
|
:key="todo.id"
|
||||||
|
class="todo-item"
|
||||||
|
:class="`priority-${todo.priority} status-${todo.status}`"
|
||||||
|
@click="handleTodoClick(todo)"
|
||||||
|
>
|
||||||
|
<div class="todo-header">
|
||||||
|
<div class="todo-title">
|
||||||
|
<el-icon :size="16" :color="getPriorityColor(todo.priority)">
|
||||||
|
<component :is="todo.icon" />
|
||||||
|
</el-icon>
|
||||||
|
<span class="title-text">{{ todo.title }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="todo-status">
|
||||||
|
<el-tag :type="getStatusTagType(todo.status)">{{ getStatusText(todo.status) }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="todo-content">
|
||||||
|
<p class="todo-desc">{{ todo.desc }}</p>
|
||||||
|
<div class="todo-meta">
|
||||||
|
<span class="todo-time">{{ todo.time }}</span>
|
||||||
|
<span class="todo-priority" :style="{ color: getPriorityColor(todo.priority) }">
|
||||||
|
{{ getPriorityText(todo.priority) }}优先级
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="todo-actions">
|
||||||
|
<el-button type="primary" size="small" @click.stop="handleProcess(todo)">立即处理</el-button>
|
||||||
|
<el-button size="small" @click.stop="handleDetails(todo)">查看详情</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<div v-if="todoList.length === 0 && !loading" class="empty-state">
|
||||||
|
<el-empty description="暂无待办事项" />
|
||||||
|
</div>
|
||||||
|
<div v-if="loading" class="loading-state">
|
||||||
|
<el-skeleton :rows="6" animated />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-pagination
|
||||||
|
class="pagination"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
:current-page="queryParams.pageNum"
|
||||||
|
:page-sizes="[5, 10, 20, 50]"
|
||||||
|
:page-size="queryParams.pageSize"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="total"
|
||||||
|
>
|
||||||
|
</el-pagination>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { listTodo } from '@/api/workflow/task.js'
|
||||||
|
import { markRaw } from 'vue'
|
||||||
|
import {
|
||||||
|
Document,
|
||||||
|
User,
|
||||||
|
ChatDotRound,
|
||||||
|
Warning,
|
||||||
|
Files,
|
||||||
|
DataLine,
|
||||||
|
Operation,
|
||||||
|
Setting
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 待办事项数据
|
||||||
|
const todoList = ref([])
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
// 查询参数
|
||||||
|
const queryParams = ref({
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
priority: '',
|
||||||
|
status: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
const total = ref(0)
|
||||||
|
|
||||||
|
// 获取待办事项列表
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await listTodo(queryParams.value)
|
||||||
|
if (response.code === 200) {
|
||||||
|
// 将工作流任务数据转换为待办事项格式
|
||||||
|
const rows = response.rows || [];
|
||||||
|
todoList.value = rows.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: formatDate(task.createTime || task.createTimeStr),
|
||||||
|
taskInfo: task // 保存原始任务信息,便于后续处理
|
||||||
|
}))
|
||||||
|
total.value = response.total || 0
|
||||||
|
} else {
|
||||||
|
console.error('获取待办事项失败:', response.msg)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取待办事项异常:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据任务信息确定优先级
|
||||||
|
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) // 默认图标
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateStr) => {
|
||||||
|
if (!dateStr) return '刚刚'
|
||||||
|
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const now = new Date()
|
||||||
|
const diffMs = now - date
|
||||||
|
const diffMins = Math.floor(diffMs / 60000)
|
||||||
|
const diffHours = Math.floor(diffMins / 60)
|
||||||
|
const diffDays = Math.floor(diffHours / 24)
|
||||||
|
|
||||||
|
if (diffMins < 1) return '刚刚'
|
||||||
|
if (diffMins < 60) return `${diffMins}分钟前`
|
||||||
|
if (diffHours < 24) return `${diffHours}小时前`
|
||||||
|
if (diffDays < 7) return `${diffDays}天前`
|
||||||
|
|
||||||
|
// 返回具体日期
|
||||||
|
return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.value.pageNum = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置查询
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryParams.value = {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
priority: '',
|
||||||
|
status: ''
|
||||||
|
}
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页大小改变
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
queryParams.value.pageSize = size
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前页改变
|
||||||
|
const handleCurrentChange = (page) => {
|
||||||
|
queryParams.value.pageNum = page
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getList()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取优先级颜色
|
||||||
|
const getPriorityColor = (priority) => {
|
||||||
|
switch (priority) {
|
||||||
|
case 'high': return '#F56C6C'
|
||||||
|
case 'medium': return '#E6A23C'
|
||||||
|
case 'low': return '#67C23A'
|
||||||
|
default: return '#909399'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取优先级文本
|
||||||
|
const getPriorityText = (priority) => {
|
||||||
|
switch (priority) {
|
||||||
|
case 'high': return '高'
|
||||||
|
case 'medium': return '中'
|
||||||
|
case 'low': return '低'
|
||||||
|
default: return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态标签类型
|
||||||
|
const getStatusTagType = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending': return 'info'
|
||||||
|
case 'processing': return 'warning'
|
||||||
|
case 'completed': return 'success'
|
||||||
|
default: return 'info'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending': return '未处理'
|
||||||
|
case 'processing': return '处理中'
|
||||||
|
case 'completed': return '已完成'
|
||||||
|
default: return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理待办事项点击
|
||||||
|
const handleTodoClick = (todo) => {
|
||||||
|
handleDetails(todo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理立即处理按钮
|
||||||
|
const handleProcess = (todo) => {
|
||||||
|
console.log('处理待办事项:', todo)
|
||||||
|
// 这里可以添加具体的处理逻辑
|
||||||
|
router.push(`/todo/process/${todo.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理查看详情按钮
|
||||||
|
const handleDetails = (todo) => {
|
||||||
|
console.log('查看详情:', todo)
|
||||||
|
// 这里可以添加查看详细信息的逻辑
|
||||||
|
router.push(`/todo/detail/${todo.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getList()
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.todo-container {
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
min-height: calc(100vh - 120px);
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list {
|
||||||
|
.todo-item {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border-left: 4px solid #409EFF;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateX(4px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.priority-high {
|
||||||
|
border-left-color: #F56C6C;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.priority-medium {
|
||||||
|
border-left-color: #E6A23C;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.priority-low {
|
||||||
|
border-left-color: #67C23A;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-completed {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.todo-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-content {
|
||||||
|
.todo-desc {
|
||||||
|
color: #606266;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
|
||||||
|
.todo-priority {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-actions {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user