8 Commits

Author SHA1 Message Date
RuoYi
0017cc8862 首页新增通知公告消息提醒 2026-03-22 00:15:34 +08:00
RuoYi
88c2e4979a 新增锁定屏幕功能 2026-03-21 23:29:22 +08:00
RuoYi
b43048a589 菜单管理支持批量保存排序 2026-03-21 22:54:21 +08:00
RuoYi
0025941824 保存排序添加编辑权限 2026-03-21 22:41:49 +08:00
RuoYi
784fe5c399 优化定时任务详情页展示&补充执行时间字段 2026-03-21 22:23:32 +08:00
RuoYi
af2d446a5f 字典类型列表新增抽屉效果详细信息 2026-03-21 22:07:16 +08:00
RuoYi
1f8dd5feb5 操作日志详细页面优化 2026-03-21 21:11:40 +08:00
RuoYi
d3b5e93952 优化RightToolbar搜索栏切换动画 2026-03-21 20:51:34 +08:00
51 changed files with 2296 additions and 269 deletions

View File

@@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.auth.form.LoginBody;
import com.ruoyi.auth.form.RegisterBody;
import com.ruoyi.auth.form.UnLockBody;
import com.ruoyi.auth.service.SysLoginService;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.utils.JwtUtils;
@@ -75,4 +76,14 @@ public class TokenController
sysLoginService.register(registerBody.getUsername(), registerBody.getPassword());
return R.ok();
}
/**
* 解锁屏幕
*/
@PostMapping("/unlockscreen")
public R<?> unlockScreen(@RequestBody UnLockBody unLockBody)
{
sysLoginService.unlock(unLockBody.getPassword());
return R.ok();
}
}

View File

@@ -0,0 +1,24 @@
package com.ruoyi.auth.form;
/**
* 系统解锁对象
*
* @author ruoyi
*/
public class UnLockBody
{
/**
* 用户密码
*/
private String password;
public String getPassword()
{
return password;
}
public void setPassword(String password)
{
this.password = password;
}
}

View File

@@ -113,11 +113,40 @@ public class SysLoginService
remoteUserService.recordUserLogin(sysUser, SecurityConstants.INNER);
}
/**
* 退出
*/
public void logout(String loginName)
{
recordLogService.recordLogininfor(loginName, Constants.LOGOUT, "退出成功");
}
/**
* 解锁
*/
public void unlock(String password)
{
String username = SecurityUtils.getUsername();
// 或密码为空 错误
if (StringUtils.isEmpty(password))
{
throw new ServiceException("密码不能为空");
}
// 查询用户信息
R<LoginUser> userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER);
if (R.FAIL == userResult.getCode())
{
throw new ServiceException(userResult.getMsg());
}
SysUser user = userResult.getData().getSysUser();
if (!SecurityUtils.matchesPassword(password, user.getPassword()))
{
throw new ServiceException("密码错误,请重新输入");
}
}
/**
* 注册
*/

View File

@@ -3,6 +3,7 @@ package com.ruoyi.job.domain;
import java.util.Date;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.annotation.Excel;
import com.ruoyi.common.core.web.domain.BaseEntity;
@@ -44,10 +45,12 @@ public class SysJobLog extends BaseEntity
private String exceptionInfo;
/** 开始时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
/** 停止时间 */
private Date stopTime;
/** 结束时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
public Long getJobLogId()
{
@@ -129,14 +132,14 @@ public class SysJobLog extends BaseEntity
this.startTime = startTime;
}
public Date getStopTime()
public Date getEndTime()
{
return stopTime;
return endTime;
}
public void setStopTime(Date stopTime)
public void setEndTime(Date endTime)
{
this.stopTime = stopTime;
this.endTime = endTime;
}
@Override
@@ -149,7 +152,7 @@ public class SysJobLog extends BaseEntity
.append("status", getStatus())
.append("exceptionInfo", getExceptionInfo())
.append("startTime", getStartTime())
.append("stopTime", getStopTime())
.append("stopTime", getEndTime())
.toString();
}
}

View File

@@ -76,8 +76,8 @@ public abstract class AbstractQuartzJob implements Job
sysJobLog.setJobGroup(sysJob.getJobGroup());
sysJobLog.setInvokeTarget(sysJob.getInvokeTarget());
sysJobLog.setStartTime(startTime);
sysJobLog.setStopTime(new Date());
long runMs = sysJobLog.getStopTime().getTime() - sysJobLog.getStartTime().getTime();
sysJobLog.setEndTime(new Date());
long runMs = sysJobLog.getEndTime().getTime() - sysJobLog.getStartTime().getTime();
sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒");
if (e != null)
{

View File

@@ -12,11 +12,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="jobMessage" column="job_message" />
<result property="status" column="status" />
<result property="exceptionInfo" column="exception_info" />
<result property="startTime" column="start_time" />
<result property="endTime" column="end_time" />
<result property="createTime" column="create_time" />
</resultMap>
<sql id="selectJobLogVo">
select job_log_id, job_name, job_group, invoke_target, job_message, status, exception_info, create_time
select job_log_id, job_name, job_group, invoke_target, job_message, status, exception_info, start_time, end_time, create_time
from sys_job_log
</sql>
@@ -78,6 +80,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="jobMessage != null and jobMessage != ''">job_message,</if>
<if test="status != null and status != ''">status,</if>
<if test="exceptionInfo != null and exceptionInfo != ''">exception_info,</if>
<if test="startTime != null">start_time,</if>
<if test="endTime != null">end_time,</if>
create_time
)values(
<if test="jobLogId != null and jobLogId != 0">#{jobLogId},</if>
@@ -87,6 +91,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="jobMessage != null and jobMessage != ''">#{jobMessage},</if>
<if test="status != null and status != ''">#{status},</if>
<if test="exceptionInfo != null and exceptionInfo != ''">#{exceptionInfo},</if>
<if test="startTime != null">#{startTime},</if>
<if test="endTime != null">#{endTime},</if>
sysdate()
)
</insert>

View File

@@ -21,7 +21,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<sql id="selectJobVo">
select job_id, job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark
select job_id, job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, update_by, update_time, remark
from sys_job
</sql>

View File

@@ -1,6 +1,7 @@
package com.ruoyi.system.controller;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
@@ -111,6 +112,20 @@ public class SysDeptController extends BaseController
return toAjax(deptService.updateDept(dept));
}
/**
* 保存部门排序
*/
@RequiresPermissions("system:dept:edit")
@Log(title = "保存部门排序", businessType = BusinessType.UPDATE)
@PutMapping("/updateSort")
public AjaxResult updateSort(@RequestBody Map<String, String> params)
{
String[] deptIds = params.get("deptIds").split(",");
String[] orderNums = params.get("orderNums").split(",");
deptService.updateDeptSort(deptIds, orderNums);
return success();
}
/**
* 删除部门
*/

View File

@@ -1,6 +1,7 @@
package com.ruoyi.system.controller;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
@@ -133,6 +134,20 @@ public class SysMenuController extends BaseController
return toAjax(menuService.updateMenu(menu));
}
/**
* 保存菜单排序
*/
@RequiresPermissions("system:menu:edit")
@Log(title = "保存菜单排序", businessType = BusinessType.UPDATE)
@PutMapping("/updateSort")
public AjaxResult updateSort(@RequestBody Map<String, String> params)
{
String[] menuIds = params.get("menuIds").split(",");
String[] orderNums = params.get("orderNums").split(",");
menuService.updateMenuSort(menuIds, orderNums);
return success();
}
/**
* 删除菜单
*/

View File

@@ -10,7 +10,9 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.core.web.controller.BaseController;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.TableDataInfo;
@@ -19,6 +21,7 @@ import com.ruoyi.common.log.enums.BusinessType;
import com.ruoyi.common.security.annotation.RequiresPermissions;
import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.system.domain.SysNotice;
import com.ruoyi.system.service.ISysNoticeReadService;
import com.ruoyi.system.service.ISysNoticeService;
/**
@@ -33,6 +36,9 @@ public class SysNoticeController extends BaseController
@Autowired
private ISysNoticeService noticeService;
@Autowired
private ISysNoticeReadService noticeReadService;
/**
* 获取通知公告列表
*/
@@ -79,6 +85,46 @@ public class SysNoticeController extends BaseController
return toAjax(noticeService.updateNotice(notice));
}
/**
* 首页顶部公告列表返回全部正常公告带当前用户已读标记最多5条
*/
@GetMapping("/listTop")
@ResponseBody
public AjaxResult listTop()
{
Long userId = SecurityUtils.getUserId();
List<SysNotice> list = noticeReadService.selectNoticeListWithReadStatus(userId, 5);
long unreadCount = list.stream().filter(n -> !n.getIsRead()).count();
AjaxResult result = AjaxResult.success(list);
result.put("unreadCount", unreadCount);
return result;
}
/**
* 标记公告已读
*/
@PostMapping("/markRead")
@ResponseBody
public AjaxResult markRead(Long noticeId)
{
Long userId = SecurityUtils.getUserId();
noticeReadService.markRead(noticeId, userId);
return success();
}
/**
* 批量标记已读
*/
@PostMapping("/markReadAll")
@ResponseBody
public AjaxResult markReadAll(String ids)
{
Long userId = SecurityUtils.getUserId();
Long[] noticeIds = Convert.toLongArray(ids);
noticeReadService.markReadBatch(userId, noticeIds);
return success();
}
/**
* 删除通知公告
*/
@@ -87,6 +133,7 @@ public class SysNoticeController extends BaseController
@DeleteMapping("/{noticeIds}")
public AjaxResult remove(@PathVariable Long[] noticeIds)
{
noticeReadService.deleteByNoticeIds(noticeIds);
return toAjax(noticeService.deleteNoticeByIds(noticeIds));
}
}

View File

@@ -4,6 +4,7 @@ import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.ruoyi.common.core.web.domain.BaseEntity;
import com.ruoyi.common.core.xss.Xss;
@@ -31,6 +32,10 @@ public class SysNotice extends BaseEntity
/** 公告状态0正常 1关闭 */
private String status;
/** 是否已读 */
@JsonProperty("isRead")
private boolean isRead;
public Long getNoticeId()
{
return noticeId;
@@ -84,6 +89,16 @@ public class SysNotice extends BaseEntity
return status;
}
public boolean getIsRead()
{
return isRead;
}
public void setIsRead(boolean isRead)
{
this.isRead = isRead;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)

View File

@@ -0,0 +1,76 @@
package com.ruoyi.system.domain;
import java.util.Date;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
/**
* 公告已读记录表 sys_notice_read
*
* @author ruoyi
*/
public class SysNoticeRead
{
/** 主键 */
private Long readId;
/** 公告ID */
private Long noticeId;
/** 用户ID */
private Long userId;
/** 阅读时间 */
private Date readTime;
public Long getReadId()
{
return readId;
}
public void setReadId(Long readId)
{
this.readId = readId;
}
public Long getNoticeId()
{
return noticeId;
}
public void setNoticeId(Long noticeId)
{
this.noticeId = noticeId;
}
public Long getUserId()
{
return userId;
}
public void setUserId(Long userId)
{
this.userId = userId;
}
public Date getReadTime()
{
return readTime;
}
public void setReadTime(Date readTime)
{
this.readTime = readTime;
}
@Override
public String toString()
{
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("readId", getReadId())
.append("noticeId", getNoticeId())
.append("userId", getUserId())
.append("readTime", getReadTime())
.toString();
}
}

View File

@@ -108,6 +108,13 @@ public interface SysDeptMapper
*/
public int updateDeptChildren(@Param("depts") List<SysDept> depts);
/**
* 保存部门排序
*
* @param dept 部门信息
*/
public void updateDeptSort(SysDept dept);
/**
* 删除部门管理信息
*

View File

@@ -106,6 +106,13 @@ public interface SysMenuMapper
*/
public int updateMenu(SysMenu menu);
/**
* 保存菜单排序
*
* @param menu 菜单信息
*/
public void updateMenuSort(SysMenu menu);
/**
* 删除菜单管理信息
*

View File

@@ -0,0 +1,65 @@
package com.ruoyi.system.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.system.domain.SysNoticeRead;
import com.ruoyi.system.domain.SysNotice;
/**
* 公告已读记录 数据层
*
* @author ruoyi
*/
public interface SysNoticeReadMapper
{
/**
* 新增已读记录(忽略重复)
*
* @param noticeRead 已读记录
* @return 结果
*/
public int insertNoticeRead(SysNoticeRead noticeRead);
/**
* 查询某用户未读公告数量
*
* @param userId 用户ID
* @return 未读数量
*/
public int selectUnreadCount(@Param("userId") Long userId);
/**
* 查询某用户是否已读某公告
*
* @param noticeId 公告ID
* @param userId 用户ID
* @return 已读记录数0未读 1已读
*/
public int selectIsRead(@Param("noticeId") Long noticeId, @Param("userId") Long userId);
/**
* 批量标记已读
*
* @param userId 用户ID
* @param noticeIds 公告ID数组
* @return 结果
*/
public int insertNoticeReadBatch(@Param("userId") Long userId, @Param("noticeIds") Long[] noticeIds);
/**
* 查询带已读状态的公告列表SQL层限制条数一次查询完成
*
* @param userId 用户ID
* @param limit 最多返回条数
* @return 带 isRead 标记的公告列表
*/
public List<SysNotice> selectNoticeListWithReadStatus(@Param("userId") Long userId, @Param("limit") int limit);
/**
* 公告删除时清理对应已读记录
*
* @param noticeIds 公告ID数组
* @return 结果
*/
public int deleteByNoticeIds(@Param("noticeIds") Long[] noticeIds);
}

View File

@@ -114,6 +114,14 @@ public interface ISysDeptService
*/
public int updateDept(SysDept dept);
/**
* 保存部门排序
*
* @param deptIds 部门ID数组
* @param orderNums 排序数组
*/
public void updateDeptSort(String[] deptIds, String[] orderNums);
/**
* 删除部门管理信息
*

View File

@@ -126,6 +126,14 @@ public interface ISysMenuService
*/
public int updateMenu(SysMenu menu);
/**
* 保存菜单排序
*
* @param menuIds 菜单ID
* @param orderNums 排序ID
*/
public void updateMenuSort(String[] menuIds, String[] orderNums);
/**
* 删除菜单管理信息
*

View File

@@ -0,0 +1,52 @@
package com.ruoyi.system.service;
import java.util.List;
import com.ruoyi.system.domain.SysNotice;
/**
* 公告已读记录 服务层
*
* @author ruoyi
*/
public interface ISysNoticeReadService
{
/**
* 标记已读(幂等,重复调用不报错)
*
* @param noticeId 公告ID
* @param userId 用户ID
*/
public void markRead(Long noticeId, Long userId);
/**
* 查询某用户未读公告数量
*
* @param userId 用户ID
* @return 未读数量
*/
public int selectUnreadCount(Long userId);
/**
* 查询公告列表并标记当前用户已读状态(用于首页展示)
*
* @param userId 用户ID
* @param limit 最多返回条数
* @return 带 isRead 标记的公告列表
*/
public List<SysNotice> selectNoticeListWithReadStatus(Long userId, int limit);
/**
* 批量标记已读
*
* @param userId 用户ID
* @param noticeIds 公告ID数组
*/
public void markReadBatch(Long userId, Long[] noticeIds);
/**
* 删除公告时清理对应已读记录
*
* @param noticeIds 公告ID数组
*/
public void deleteByNoticeIds(Long[] noticeIds);
}

View File

@@ -6,6 +6,7 @@ import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.core.constant.UserConstants;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.text.Convert;
@@ -280,6 +281,32 @@ public class SysDeptServiceImpl implements ISysDeptService
}
}
/**
* 保存部门排序
*
* @param deptIds 部门ID数组
* @param orderNums 排序数组
*/
@Override
@Transactional
public void updateDeptSort(String[] deptIds, String[] orderNums)
{
try
{
for (int i = 0; i < deptIds.length; i++)
{
SysDept dept = new SysDept();
dept.setDeptId(Convert.toLong(deptIds[i]));
dept.setOrderNum(Convert.toInt(orderNums[i]));
deptMapper.updateDeptSort(dept);
}
}
catch (Exception e)
{
throw new ServiceException("保存排序异常,请联系管理员");
}
}
/**
* 删除部门管理信息
*

View File

@@ -12,8 +12,11 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.constant.UserConstants;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.system.api.domain.SysRole;
@@ -321,6 +324,32 @@ public class SysMenuServiceImpl implements ISysMenuService
return menuMapper.updateMenu(menu);
}
/**
* 保存菜单排序
*
* @param menuIds 菜单ID
* @param orderNums 排序ID
*/
@Override
@Transactional
public void updateMenuSort(String[] menuIds, String[] orderNums)
{
try
{
for (int i = 0; i < menuIds.length; i++)
{
SysMenu menu = new SysMenu();
menu.setMenuId(Convert.toLong(menuIds[i]));
menu.setOrderNum(Convert.toInt(orderNums[i]));
menuMapper.updateMenuSort(menu);
}
}
catch (Exception e)
{
throw new ServiceException("保存排序异常,请联系管理员");
}
}
/**
* 删除菜单管理信息
*

View File

@@ -0,0 +1,73 @@
package com.ruoyi.system.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.system.domain.SysNoticeRead;
import com.ruoyi.system.domain.SysNotice;
import com.ruoyi.system.mapper.SysNoticeReadMapper;
import com.ruoyi.system.service.ISysNoticeReadService;
/**
* 公告已读记录 服务层实现
*
* @author ruoyi
*/
@Service
public class SysNoticeReadServiceImpl implements ISysNoticeReadService
{
@Autowired
private SysNoticeReadMapper noticeReadMapper;
/**
* 标记已读
*/
@Override
public void markRead(Long noticeId, Long userId)
{
SysNoticeRead record = new SysNoticeRead();
record.setNoticeId(noticeId);
record.setUserId(userId);
noticeReadMapper.insertNoticeRead(record);
}
/**
* 查询某用户未读公告数量
*/
@Override
public int selectUnreadCount(Long userId)
{
return noticeReadMapper.selectUnreadCount(userId);
}
/**
* 查询公告列表并标记当前用户已读状态
*/
@Override
public List<SysNotice> selectNoticeListWithReadStatus(Long userId, int limit)
{
return noticeReadMapper.selectNoticeListWithReadStatus(userId, limit);
}
/**
* 批量标记已读
*/
@Override
public void markReadBatch(Long userId, Long[] noticeIds)
{
if (noticeIds == null || noticeIds.length == 0)
{
return;
}
noticeReadMapper.insertNoticeReadBatch(userId, noticeIds);
}
/**
* 删除公告时清理对应已读记录
*/
@Override
public void deleteByNoticeIds(Long[] noticeIds)
{
noticeReadMapper.deleteByNoticeIds(noticeIds);
}
}

View File

@@ -150,8 +150,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</foreach>
</update>
<update id="updateDeptSort" parameterType="SysDept">
update sys_dept set order_num = #{orderNum} where dept_id = #{deptId}
</update>
<delete id="deleteDeptById" parameterType="Long">
update sys_dept set del_flag = '2' where dept_id = #{deptId}
</delete>
</mapper>
</mapper>

View File

@@ -204,6 +204,10 @@
)
</insert>
<update id="updateMenuSort" parameterType="SysMenu">
update sys_menu set order_num = #{orderNum} where menu_id = #{menuId}
</update>
<delete id="deleteMenuById" parameterType="Long">
delete from sys_menu where menu_id = #{menuId}
</delete>

View File

@@ -40,6 +40,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
AND create_by like concat('%', #{createBy}, '%')
</if>
</where>
order by notice_id desc
</select>
<insert id="insertNotice" parameterType="SysNotice">

View File

@@ -0,0 +1,66 @@
<?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.ruoyi.system.mapper.SysNoticeReadMapper">
<resultMap type="SysNoticeRead" id="SysNoticeReadResult">
<id property="readId" column="read_id" />
<result property="noticeId" column="notice_id" />
<result property="userId" column="user_id" />
<result property="readTime" column="read_time" />
</resultMap>
<!-- 新增已读记录 -->
<insert id="insertNoticeRead" parameterType="SysNoticeRead">
insert ignore into sys_notice_read (notice_id, user_id, read_time)
values (#{noticeId}, #{userId}, sysdate())
</insert>
<!-- 查询未读数量:正常状态公告 减去 当前用户已读数 -->
<select id="selectUnreadCount" resultType="int">
select count(*) from sys_notice n
where n.status = '0' and not exists (select 1 from sys_notice_read r where r.notice_id = n.notice_id and r.user_id = #{userId})
</select>
<!-- 查询是否已读 -->
<select id="selectIsRead" resultType="int">
select count(*) from sys_notice_read where notice_id = #{noticeId} and user_id = #{userId}
</select>
<!-- 查询带已读状态的公告列表直接在SQL中限制条数 -->
<select id="selectNoticeListWithReadStatus" resultType="SysNotice">
select
n.notice_id as noticeId,
n.notice_title as noticeTitle,
n.notice_type as noticeType,
n.status,
n.create_by as createBy,
n.create_time as createTime,
case when r.notice_id is not null then true else false end as isRead
from sys_notice n
left join sys_notice_read r
on r.notice_id = n.notice_id and r.user_id = #{userId}
where n.status = '0'
order by n.notice_id desc
limit #{limit}
</select>
<!-- 批量标记已读 -->
<insert id="insertNoticeReadBatch">
insert ignore into sys_notice_read (notice_id, user_id, read_time)
values
<foreach collection="noticeIds" item="noticeId" separator=",">
(#{noticeId}, #{userId}, sysdate())
</foreach>
</insert>
<!-- 删除公告时清理已读记录 -->
<delete id="deleteByNoticeIds">
delete from sys_notice_read where notice_id in
<foreach collection="noticeIds" item="noticeId" open="(" separator="," close=")">
#{noticeId}
</foreach>
</delete>
</mapper>

View File

@@ -25,6 +25,15 @@ export function register(data) {
})
}
// 解锁屏幕
export function unlockScreen(password) {
return request({
url: '/auth/unlockscreen',
method: 'post',
data: { password }
})
}
// 刷新方法
export function refreshToken() {
return request({

View File

@@ -43,6 +43,15 @@ export function updateDept(data) {
})
}
// 保存部门排序
export function updateDeptSort(data) {
return request({
url: '/system/dept/updateSort',
method: 'put',
data: data
})
}
// 删除部门
export function delDept(deptId) {
return request({

View File

@@ -51,6 +51,15 @@ export function updateMenu(data) {
})
}
// 保存菜单排序
export function updateMenuSort(data) {
return request({
url: '/system/menu/updateSort',
method: 'put',
data: data
})
}
// 删除菜单
export function delMenu(menuId) {
return request({

View File

@@ -41,4 +41,30 @@ export function delNotice(noticeId) {
url: '/system/notice/' + noticeId,
method: 'delete'
})
}
}
// 首页顶部公告列表(带已读状态)
export function listNoticeTop() {
return request({
url: '/system/notice/listTop',
method: 'get'
})
}
// 标记公告已读
export function markNoticeRead(noticeId) {
return request({
url: '/system/notice/markRead',
method: 'post',
params: { noticeId }
})
}
// 批量标记已读
export function markNoticeReadAll(ids) {
return request({
url: '/system/notice/markReadAll',
method: 'post',
params: { ids }
})
}

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1773923748724" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5930" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 212l48.8 12c101.6 24.8 176 117.6 176 220.8v254.4l18.4 18.4 24.8 25.6h-536l24.8-25.6 18.4-18.4V444.8c0-103.2 73.6-196.8 176-220.8l48.8-12M512 64c-36.8 0-64 30.4-64 68v30.4C320.8 192 223.2 307.2 223.2 444.8v228.8L136 763.2v44.8h752v-44.8l-87.2-89.6V444.8c0-137.6-97.6-252.8-224.8-283.2v-28.8c0-32-17.6-60.8-48-67.2-5.6-1.6-11.2-1.6-16-1.6z m88 808H424c0 49.6 38.4 88 88 88s88-38.4 88-88z" p-id="5931"></path></svg>

After

Width:  |  Height:  |  Size: 750 B

View File

@@ -228,6 +228,120 @@
color: #FFFFFF;
}
/** 详细卡片样式 */
.detail-wrap { padding: 0 4px; }
.detail-card {
border: 1px solid #ebeef5;
border-radius: 6px;
margin-bottom: 14px;
overflow: hidden;
}
.detail-card-title {
background: #f7f9fb;
padding: 8px 16px;
font-size: 13px;
font-weight: 600;
color: #333;
border-bottom: 1px solid #ebeef5;
}
.detail-card-title i { margin-right: 5px; color: #409EFF; }
.detail-row { padding: 0 8px; }
.detail-item {
display: flex;
align-items: flex-start;
padding: 10px 8px;
font-size: 13px;
border-bottom: 1px solid #f5f7fa;
}
.detail-item:last-child { border-bottom: none; }
.detail-label {
flex-shrink: 0;
width: 72px;
color: #909399;
margin-right: 12px;
}
.detail-value { color: #303133; flex: 1; word-break: break-all; }
.detail-location { color: #999; font-size: 12px; }
.method-tag {
display: inline-block;
padding: 1px 7px;
border-radius: 3px;
font-size: 11px;
font-weight: 700;
margin-right: 6px;
vertical-align: middle;
}
.mono { font-family: Consolas, 'SFMono-Regular', monospace; font-size: 12px; }
.code-body { padding: 14px; }
.code-wrap {
background: #f7f9fb;
border: 1px solid #e8ecf0;
border-radius: 4px;
overflow: hidden;
max-height: 260px;
position: relative;
}
.code-action {
position: absolute;
top: 8px;
right: 8px;
z-index: 10;
margin: 0;
padding: 0;
}
.code-action .el-button {
height: 24px;
font-size: 12px;
padding: 4px 8px;
background: rgba(255, 255, 255, 0.9);
border: 1px solid #dcdcdc;
}
.code-action .el-button:hover {
background: #ffffff;
border-color: #409EFF;
}
.code-pre {
margin: 0;
padding: 12px 14px;
font-size: 12px;
line-height: 1.6;
font-family: Consolas, 'SFMono-Regular', monospace;
color: #444;
white-space: pre-wrap;
word-break: break-all;
overflow: auto;
max-height: 240px;
display: block;
}
/* error */
.error-title { color: #c0392b !important; }
.error-title i { color: #c0392b !important; }
.error-body { padding: 12px 16px; }
.error-msg {
background: #fff8f8;
border-left: 3px solid #e74c3c;
border-radius: 3px;
padding: 8px 12px;
color: #c0392b;
font-size: 12px;
line-height: 1.7;
word-break: break-all;
white-space: pre-wrap;
}
/* http method */
.method-GET { background: #e8f5e9; color: #27ae60; }
.method-POST { background: #e3f2fd; color: #1565c0; }
.method-PUT { background: #fff3e0; color: #e65100; }
.method-DELETE { background: #ffebee; color: #c62828; }
/* text color */
.text-navy {
color: #1ab394;

View File

@@ -123,7 +123,34 @@ export default {
methods: {
// 搜索
toggleSearch() {
this.$emit("update:showSearch", !this.showSearch)
let el = this.$el
let formEl = null
while ((el = el.parentElement) && el !== document.body) {
if ((formEl = el.querySelector('.el-form'))) break
}
if (!formEl) return this.$emit('update:showSearch', !this.showSearch)
this._animateSearch(formEl, this.showSearch)
},
// 搜索栏动画
_animateSearch(el, isHide) {
const DURATION = 260
const TRANSITION = 'max-height 0.25s ease, opacity 0.2s ease'
const clear = () => Object.assign(el.style, { transition: '', maxHeight: '', opacity: '', overflow: '' })
Object.assign(el.style, { overflow: 'hidden', transition: '' })
if (isHide) {
Object.assign(el.style, { maxHeight: el.scrollHeight + 'px', opacity: '1', transition: TRANSITION })
requestAnimationFrame(() => Object.assign(el.style, { maxHeight: '0', opacity: '0' }))
setTimeout(() => { this.$emit('update:showSearch', false); clear() }, DURATION)
} else {
this.$emit('update:showSearch', true)
this.$nextTick(() => {
Object.assign(el.style, { maxHeight: '0', opacity: '0' })
requestAnimationFrame(() => requestAnimationFrame(() => {
Object.assign(el.style, { transition: TRANSITION, maxHeight: el.scrollHeight + 'px', opacity: '1' })
}))
setTimeout(clear, DURATION)
})
}
},
// 刷新
refresh() {

View File

@@ -0,0 +1,229 @@
<template>
<div>
<el-popover ref="noticePopover" placement="bottom-end" width="320" trigger="manual" :value="noticeVisible" popper-class="notice-popover">
<div class="notice-header">
<span class="notice-title">通知公告</span>
<span class="notice-mark-all" @click="markAllRead">全部已读</span>
</div>
<div v-if="noticeLoading" class="notice-loading"><i class="el-icon-loading"></i> 加载中...</div>
<div v-else-if="noticeList.length === 0" class="notice-empty"><i class="el-icon-inbox"></i><br>暂无公告</div>
<div v-else>
<div v-for="item in noticeList" :key="item.noticeId" class="notice-item" :class="{ 'is-read': item.isRead }" @click="previewNotice(item)">
<el-tag size="mini" :type="item.noticeType === '1' ? 'warning' : 'success'" class="notice-tag">
{{ item.noticeType === '1' ? '通知' : '公告' }}
</el-tag>
<span class="notice-item-title">{{ item.noticeTitle }}</span>
<span class="notice-item-date">{{ item.createTime }}</span>
</div>
</div>
</el-popover>
<div v-popover:noticePopover class="right-menu-item hover-effect notice-trigger" @mouseenter="onNoticeEnter" @mouseleave="onNoticeLeave">
<svg-icon icon-class="bell" />
<span v-if="unreadCount > 0" class="notice-badge">{{ unreadCount }}</span>
</div>
<el-dialog :title="previewTitle" :visible.sync="previewVisible" width="680px" append-to-body custom-class="notice-preview-dialog">
<div class="notice-preview-meta">
<el-tag size="small" :type="previewNoticeType === '1' ? 'warning' : 'success'">
{{ previewNoticeType === '1' ? '通知' : '公告' }}
</el-tag>
<span class="notice-preview-info"><i class="el-icon-user"></i> {{ previewCreateBy }}</span>
<span class="notice-preview-info"><i class="el-icon-time"></i> {{ previewCreateTime }}</span>
</div>
<div class="notice-preview-divider"></div>
<div class="notice-preview-content" v-html="previewContent"></div>
</el-dialog>
</div>
</template>
<script>
import { listNoticeTop, markNoticeRead, markNoticeReadAll, getNotice } from '@/api/system/notice'
export default {
name: 'HeaderNotice',
data() {
return {
noticeList: [], // 通知列表
unreadCount: 0, // 未读数量
noticeLoading: false, // 加载状态
noticeVisible: false, // 弹出层显示状态
noticeLeaveTimer: null, // 鼠标离开计时器
previewVisible: false, // 预览弹窗显示状态
previewTitle: '', // 预览弹窗标题
previewContent: '', // 预览弹窗内容
previewNoticeType: '', // 预览弹窗类型
previewCreateBy: '', // 预览弹窗创建人
previewCreateTime: '' // 预览弹窗创建时间
}
},
mounted() {
this.loadNoticeTop()
},
methods: {
// 鼠标移入铃铛区域
onNoticeEnter() {
clearTimeout(this.noticeLeaveTimer)
this.noticeVisible = true
this.$nextTick(() => {
const popper = this.$refs.noticePopover.$refs.popper
if (popper && !popper._noticeBound) {
popper._noticeBound = true
popper.addEventListener('mouseenter', () => clearTimeout(this.noticeLeaveTimer))
popper.addEventListener('mouseleave', () => {
this.noticeLeaveTimer = setTimeout(() => { this.noticeVisible = false }, 100)
})
}
})
},
// 鼠标离开铃铛区域
onNoticeLeave() {
this.noticeLeaveTimer = setTimeout(() => { this.noticeVisible = false }, 150)
},
// 加载顶部公告列表
loadNoticeTop() {
this.noticeLoading = true
listNoticeTop().then(res => {
this.noticeList = res.data || []
this.unreadCount = res.unreadCount !== undefined ? res.unreadCount : this.noticeList.filter(n => !n.isRead).length
}).finally(() => {
this.noticeLoading = false
})
},
// 预览公告详情
previewNotice(item) {
if (!item.isRead) {
markNoticeRead(item.noticeId).catch(() => {})
item.isRead = true
const idx = this.noticeList.indexOf(item)
if (idx !== -1) this.$set(this.noticeList, idx, { ...item, isRead: true })
this.unreadCount = Math.max(0, this.unreadCount - 1)
}
getNotice(item.noticeId).then(res => {
const notice = res.data
this.previewTitle = notice.noticeTitle
this.previewContent = notice.noticeContent
this.previewNoticeType = notice.noticeType
this.previewCreateBy = notice.createBy
this.previewCreateTime = notice.createTime
this.previewVisible = true
})
},
// 全部已读
markAllRead() {
const ids = this.noticeList.map(n => n.noticeId).join(',')
if (!ids) return
markNoticeReadAll(ids).catch(() => {})
this.noticeList = this.noticeList.map(n => ({ ...n, isRead: true }))
this.unreadCount = 0
}
}
}
</script>
<style lang="scss" scoped>
.notice-trigger {
position: relative;
transform: translateX(-6px);
.svg-icon { width: 1.2em; height: 1.2em; vertical-align: -0.2em; }
.notice-badge {
position: absolute;
top: 7px;
right: -3px;
background: #f56c6c;
color: #fff;
border-radius: 10px;
font-size: 10px;
height: 16px;
line-height: 16px;
padding: 0 4px;
min-width: 16px;
text-align: center;
white-space: nowrap;
pointer-events: none;
}
}
.notice-popover {
padding: 0 !important;
}
.notice-popover .notice-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
background: #f7f9fb;
border-bottom: 1px solid #eee;
font-size: 13px;
font-weight: 600;
color: #333;
}
.notice-popover .notice-mark-all {
font-size: 12px;
color: #409EFF;
font-weight: normal;
cursor: pointer;
}
.notice-popover .notice-mark-all:hover { color: #2b7cc1; }
.notice-popover .notice-loading,
.notice-popover .notice-empty {
padding: 24px;
text-align: center;
color: #bbb;
font-size: 12px;
line-height: 1.8;
}
.notice-popover .notice-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
border-bottom: 1px solid #f5f5f5;
cursor: pointer;
transition: background 0.15s;
}
.notice-popover .notice-item:last-child { border-bottom: none; }
.notice-popover .notice-item:hover { background: #f7f9fb; }
.notice-popover .notice-item.is-read .notice-tag,
.notice-popover .notice-item.is-read .notice-item-title,
.notice-popover .notice-item.is-read .notice-item-date { opacity: 0.45; filter: grayscale(1); color: #999; }
.notice-popover .notice-tag { flex-shrink: 0; }
.notice-popover .notice-item-title {
flex: 1;
font-size: 12px;
color: #333;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.notice-popover .notice-item-date {
flex-shrink: 0;
font-size: 11px;
color: #bbb;
}
::v-deep .notice-preview-dialog {
.el-dialog__body { padding: 0 20px 20px; }
.notice-preview-meta {
display: flex;
align-items: center;
gap: 14px;
padding: 12px 0;
font-size: 12px;
color: #888;
.notice-preview-info { display: flex; align-items: center; gap: 4px; }
}
.notice-preview-divider {
height: 1px;
background: linear-gradient(to right, transparent, #e2e8f0, transparent);
margin-bottom: 16px;
}
.notice-preview-content {
font-size: 14px;
line-height: 1.85;
color: #2d3748;
word-break: break-word;
img { max-width: 100%; border-radius: 4px; }
p { margin: 0 0 1em; }
a { color: #409EFF; text-decoration: underline; }
}
}
</style>

View File

@@ -26,6 +26,10 @@
<size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip content="消息通知" effect="dark" placement="bottom">
<header-notice id="header-notice" class="right-menu-item hover-effect" />
</el-tooltip>
</template>
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="hover">
@@ -40,6 +44,9 @@
<el-dropdown-item @click.native="setLayout" v-if="setting">
<span>布局设置</span>
</el-dropdown-item>
<el-dropdown-item @click.native="lockScreen">
<span>锁定屏幕</span>
</el-dropdown-item>
<el-dropdown-item divided @click.native="logout">
<span>退出登录</span>
</el-dropdown-item>
@@ -61,9 +68,9 @@ import SizeSelect from '@/components/SizeSelect'
import Search from '@/components/HeaderSearch'
import RuoYiGit from '@/components/RuoYi/Git'
import RuoYiDoc from '@/components/RuoYi/Doc'
import HeaderNotice from './HeaderNotice'
export default {
emits: ['setLayout'],
components: {
Breadcrumb,
Logo,
@@ -74,7 +81,8 @@ export default {
SizeSelect,
Search,
RuoYiGit,
RuoYiDoc
RuoYiDoc,
HeaderNotice
},
computed: {
...mapGetters([
@@ -106,6 +114,12 @@ export default {
setLayout(event) {
this.$emit('setLayout')
},
lockScreen() {
const currentPath = this.$route.fullPath
this.$store.dispatch('lock/lockScreen', currentPath).then(() => {
this.$router.push('/lock')
})
},
logout() {
this.$confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
@@ -173,11 +187,6 @@ export default {
margin-left: 8px;
}
.errLog-container {
display: inline-block;
vertical-align: top;
}
.right-menu {
height: 100%;
line-height: 50px;

View File

@@ -19,12 +19,19 @@ router.beforeEach((to, from, next) => {
NProgress.start()
if (getToken()) {
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
const isLock = store.getters.isLock
/* has token*/
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else if (isWhiteList(to.path)) {
next()
} else if (isLock && to.path !== '/lock') {
next({ path: '/lock' })
NProgress.done()
} else if (!isLock && to.path === '/lock') {
next({ path: '/' })
NProgress.done()
} else {
if (store.getters.roles.length === 0) {
isRelogin.show = true

View File

@@ -74,6 +74,12 @@ export const constantRoutes = [
}
]
},
{
path: '/lock',
component: () => import('@/views/lock'),
hidden: true,
meta: { title: '锁定屏幕' }
},
{
path: '/user',
component: Layout,

View File

@@ -3,6 +3,8 @@ const getters = {
size: state => state.app.size,
device: state => state.app.device,
dict: state => state.dict.dict,
isLock: state => state.lock.isLock,
lockPath: state => state.lock.lockPath,
visitedViews: state => state.tagsView.visitedViews,
cachedViews: state => state.tagsView.cachedViews,
token: state => state.user.token,

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'
import Vuex from 'vuex'
import app from './modules/app'
import lock from './modules/lock'
import dict from './modules/dict'
import user from './modules/user'
import tagsView from './modules/tagsView'
@@ -13,6 +14,7 @@ Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
app,
lock,
dict,
user,
tagsView,

View File

@@ -0,0 +1,34 @@
const LOCK_KEY = 'screen-lock'
const LOCK_PATH_KEY = 'screen-lock-path'
const lock = {
namespaced: true,
state: {
isLock: JSON.parse(localStorage.getItem(LOCK_KEY) || 'false'),
lockPath: localStorage.getItem(LOCK_PATH_KEY) || '/index'
},
mutations: {
SET_LOCK(state, status) {
state.isLock = status
localStorage.setItem(LOCK_KEY, JSON.stringify(status))
},
SET_LOCK_PATH(state, path) {
state.lockPath = path
localStorage.setItem(LOCK_PATH_KEY, path)
}
},
actions: {
// 锁定屏幕,同时记录当前路径
lockScreen({ commit }, currentPath) {
commit('SET_LOCK_PATH', currentPath || '/index')
commit('SET_LOCK', true)
},
// 解锁屏幕,清除路径
unlockScreen({ commit }) {
commit('SET_LOCK', false)
commit('SET_LOCK_PATH', '/index')
}
}
}
export default lock

View File

@@ -1,3 +1,4 @@
import store from '@/store'
import router from '@/router'
import { MessageBox, } from 'element-ui'
import { login, logout, getInfo, refreshToken } from '@/api/login'
@@ -57,6 +58,7 @@ const user = {
commit('SET_TOKEN', data.access_token)
setExpiresIn(data.expires_in)
commit('SET_EXPIRES_IN', data.expires_in)
store.dispatch('lock/unlockScreen')
resolve()
}).catch(error => {
reject(error)

375
ruoyi-ui/src/views/lock.vue Normal file
View File

@@ -0,0 +1,375 @@
<template>
<div class="lock-container">
<!-- 动态粒子背景 -->
<canvas ref="particleCanvas" class="particle-bg"></canvas>
<!-- 时钟 -->
<div class="lock-time">{{ currentTime }}</div>
<div class="lock-date">{{ currentDate }}</div>
<!-- 锁屏卡片 -->
<div class="lock-card">
<div class="avatar-wrap">
<img :src="avatar" class="lock-avatar" @error="onAvatarError" />
<div class="lock-icon">🔒</div>
</div>
<div class="lock-username">{{ nickName }}</div>
<div class="lock-hint">系统已锁定请输入密码解锁</div>
<div class="input-wrap" :class="{ shake: isShaking }">
<input ref="passwordInput" v-model="password" type="password" placeholder="请输入登录密码" class="lock-input" @keydown.enter="handleUnlock" autocomplete="off" />
<button class="unlock-btn" @click="handleUnlock" :disabled="loading">
<span v-if="!loading"></span>
<span v-else class="loading-dot">···</span>
</button>
</div>
<div v-if="errorMsg" class="error-msg">{{ errorMsg }}</div>
<div class="lock-footer">
<a href="/login" @click.prevent="goLogin">退出重新登录</a>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { unlockScreen } from '@/api/login'
import defAva from '@/assets/images/profile.jpg'
export default {
name: 'LockScreen',
data() {
return {
password: '',
loading: false,
errorMsg: '',
isShaking: false,
currentTime: '',
currentDate: '',
timer: null,
animationId: null,
particles: []
}
},
computed: {
...mapGetters(['avatar', 'nickName'])
},
mounted() {
this.startClock()
this.initParticles()
this.$nextTick(() => {
this.$refs.passwordInput && this.$refs.passwordInput.focus()
})
},
beforeDestroy() {
clearInterval(this.timer)
cancelAnimationFrame(this.animationId)
},
methods: {
onAvatarError(e) {
e.target.src = defAva
},
startClock() {
const update = () => {
const now = new Date()
const h = String(now.getHours()).padStart(2, '0')
const m = String(now.getMinutes()).padStart(2, '0')
const s = String(now.getSeconds()).padStart(2, '0')
this.currentTime = `${h}:${m}:${s}`
const days = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六']
this.currentDate = `${now.getFullYear()}${now.getMonth()+1}${now.getDate()}${days[now.getDay()]}`
}
update()
this.timer = setInterval(update, 1000)
},
async handleUnlock() {
if (!this.password) {
this.showError('请输入密码')
return
}
this.loading = true
this.errorMsg = ''
try {
await unlockScreen(this.password)
const lockPath = this.$store.getters.lockPath // 取锁屏前的路径
await this.$store.dispatch('lock/unlockScreen')
this.$router.replace(lockPath)
} catch (err) {
const msg = err.message || err.toString()
this.showError(msg)
this.password = ''
this.$refs.passwordInput && this.$refs.passwordInput.focus()
} finally {
this.loading = false
}
},
showError(msg) {
this.errorMsg = msg
this.isShaking = true
setTimeout(() => { this.isShaking = false }, 600)
},
goLogin() {
this.$store.dispatch('lock/unlockScreen')
this.$store.dispatch('LogOut').then(() => {
this.$router.push('/login')
})
},
// 粒子背景
initParticles() {
const canvas = this.$refs.particleCanvas
if (!canvas) return
const ctx = canvas.getContext('2d')
const resize = () => {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
}
resize()
window.addEventListener('resize', resize)
const count = 80
for (let i = 0; i < count; i++) {
this.particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
r: Math.random() * 2 + 1,
dx: (Math.random() - 0.5) * 0.6,
dy: (Math.random() - 0.5) * 0.6,
alpha: Math.random() * 0.5 + 0.2
})
}
const draw = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height)
this.particles.forEach(p => {
ctx.beginPath()
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2)
ctx.fillStyle = `rgba(255,255,255,${p.alpha})`
ctx.fill()
p.x += p.dx
p.y += p.dy
if (p.x < 0 || p.x > canvas.width) p.dx *= -1
if (p.y < 0 || p.y > canvas.height) p.dy *= -1
})
// 连线
for (let i = 0; i < this.particles.length; i++) {
for (let j = i + 1; j < this.particles.length; j++) {
const a = this.particles[i], b = this.particles[j]
const dist = Math.hypot(a.x - b.x, a.y - b.y)
if (dist < 120) {
ctx.beginPath()
ctx.moveTo(a.x, a.y)
ctx.lineTo(b.x, b.y)
ctx.strokeStyle = `rgba(255,255,255,${0.15 * (1 - dist / 120)})`
ctx.lineWidth = 0.5
ctx.stroke()
}
}
}
this.animationId = requestAnimationFrame(draw)
}
draw()
}
}
}
</script>
<style scoped>
.lock-container {
position: fixed;
inset: 0;
background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
overflow: hidden;
}
.particle-bg {
position: absolute;
inset: 0;
z-index: 0;
}
.lock-time {
position: relative;
z-index: 1;
font-size: 72px;
font-weight: 200;
color: #fff;
letter-spacing: 4px;
text-shadow: 0 0 40px rgba(255,255,255,0.3);
margin-bottom: 8px;
font-variant-numeric: tabular-nums;
}
.lock-date {
position: relative;
z-index: 1;
font-size: 15px;
color: rgba(255,255,255,0.6);
margin-bottom: 48px;
letter-spacing: 2px;
}
.lock-card {
position: relative;
z-index: 1;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 24px;
padding: 40px 48px;
width: 360px;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 25px 60px rgba(0,0,0,0.4);
}
.avatar-wrap {
position: relative;
margin-bottom: 16px;
}
.lock-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
border: 3px solid rgba(255,255,255,0.3);
object-fit: cover;
display: block;
}
.lock-icon {
position: absolute;
bottom: -4px;
right: -4px;
background: rgba(255,255,255,0.15);
border-radius: 50%;
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
backdrop-filter: blur(8px);
}
.lock-username {
color: #fff;
font-size: 18px;
font-weight: 600;
margin-bottom: 6px;
letter-spacing: 1px;
}
.lock-hint {
color: rgba(255,255,255,0.5);
font-size: 13px;
margin-bottom: 28px;
}
.input-wrap {
width: 100%;
display: flex;
align-items: center;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 50px;
padding: 4px 4px 4px 20px;
transition: border-color 0.3s;
}
.input-wrap:focus-within {
border-color: rgba(255,255,255,0.6);
background: rgba(255,255,255,0.13);
}
.input-wrap.shake {
animation: shake 0.5s ease;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-8px); }
40% { transform: translateX(8px); }
60% { transform: translateX(-6px); }
80% { transform: translateX(6px); }
}
.lock-input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: #fff;
font-size: 15px;
padding: 10px 0;
}
.lock-input::placeholder {
color: rgba(255,255,255,0.35);
}
.unlock-btn {
width: 42px;
height: 42px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
border: none;
color: #fff;
font-size: 18px;
cursor: pointer;
transition: transform 0.2s, opacity 0.2s;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.unlock-btn:hover:not(:disabled) {
transform: scale(1.08);
}
.unlock-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.loading-dot {
font-size: 13px;
letter-spacing: 1px;
}
.error-msg {
margin-top: 14px;
color: #ff7675;
font-size: 13px;
text-align: center;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
.lock-footer {
margin-top: 24px;
}
.lock-footer a {
color: rgba(255,255,255,0.4);
font-size: 13px;
text-decoration: none;
transition: color 0.2s;
}
.lock-footer a:hover {
color: rgba(255,255,255,0.8);
}
</style>

View File

@@ -0,0 +1,197 @@
<template>
<el-dialog :title="type === 'log' ? '调度日志详细' : '任务详细'" :visible.sync="visible" width="780px" append-to-body @close="$emit('update:visible', false)">
<div class="detail-wrap">
<template v-if="type === 'log'">
<!-- 基本信息 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-info"></i> 基本信息</div>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">日志编号</span><span class="detail-value">{{ form.jobLogId }}</span></div>
</el-col>
<el-col :span="12">
<div class="detail-item">
<span class="detail-label">执行状态</span>
<el-tag v-if="form.status == 0" type="success" size="small">正常</el-tag>
<el-tag v-else type="danger" size="small">失败</el-tag>
</div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">开始时间</span><span class="detail-value">{{ form.startTime }}</span></div>
</el-col>
<el-col :span="12">
<div class="detail-item"><span class="detail-label">结束时间</span><span class="detail-value">{{ form.endTime }}</span></div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">记录时间</span><span class="detail-value">{{ form.createTime }}</span></div>
</el-col>
<el-col :span="12" v-if="form.status == 0 && form.startTime && form.endTime">
<div class="detail-item"><span class="detail-label">执行耗时</span><span class="detail-value">{{ costTime }} 毫秒</span></div>
</el-col>
</el-row>
</div>
<!-- 任务信息 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-time"></i> 任务信息</div>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">任务名称</span><span class="detail-value">{{ form.jobName }}</span></div>
</el-col>
<el-col :span="12">
<div class="detail-item">
<span class="detail-label">任务分组</span>
<dict-tag :options="dict.type.sys_job_group" :value="form.jobGroup" />
</div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="24">
<div class="detail-item"><span class="detail-label">日志信息</span><span class="detail-value">{{ form.jobMessage }}</span></div>
</el-col>
</el-row>
</div>
<!-- 调用目标 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-s-operation"></i> 调用目标</div>
<div class="code-body">
<div class="code-wrap"><pre class="code-pre">{{ form.invokeTarget || '(无)' }}</pre></div>
</div>
</div>
<!-- 异常信息 -->
<div class="detail-card" v-if="form.status == 1">
<div class="detail-card-title error-title"><i class="el-icon-warning"></i> 异常信息</div>
<div class="error-body"><div class="error-msg">{{ form.exceptionInfo }}</div></div>
</div>
</template>
<template v-else>
<!-- 任务配置 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-setting"></i> 任务配置</div>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">任务编号</span><span class="detail-value">{{ form.jobId }}</span></div>
</el-col>
<el-col :span="12">
<div class="detail-item"><span class="detail-label">任务名称</span><span class="detail-value">{{ form.jobName }}</span></div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item">
<span class="detail-label">任务分组</span>
<dict-tag :options="dict.type.sys_job_group" :value="form.jobGroup" />
</div>
</el-col>
<el-col :span="12">
<div class="detail-item">
<span class="detail-label">执行状态</span>
<el-tag v-if="form.status == 0" type="success" size="small">正常</el-tag>
<el-tag v-else type="info" size="small">暂停</el-tag>
</div>
</el-col>
</el-row>
</div>
<!-- 调度信息 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-date"></i> 调度信息</div>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">cron 表达式</span><span class="detail-value mono">{{ form.cronExpression }}</span></div>
</el-col>
<el-col :span="12">
<div class="detail-item"><span class="detail-label">下次执行时间</span><span class="detail-value">{{ parseTime(form.nextValidTime) }}</span></div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item">
<span class="detail-label">执行策略</span>
<el-tag v-if="form.misfirePolicy == 0" type="info" size="small">默认策略</el-tag>
<el-tag v-else-if="form.misfirePolicy == 1" type="warning" size="small">立即执行</el-tag>
<el-tag v-else-if="form.misfirePolicy == 2" type="primary" size="small">执行一次</el-tag>
<el-tag v-else-if="form.misfirePolicy == 3" type="danger" size="small">放弃执行</el-tag>
</div>
</el-col>
<el-col :span="12">
<div class="detail-item">
<span class="detail-label">并发执行</span>
<el-tag v-if="form.concurrent == 0" type="success" size="small">允许</el-tag>
<el-tag v-else type="danger" size="small">禁止</el-tag>
</div>
</el-col>
</el-row>
</div>
<!-- 执行方法 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-s-operation"></i> 执行方法</div>
<div class="code-body">
<div class="code-wrap"><pre class="code-pre">{{ form.invokeTarget || '(无)' }}</pre></div>
</div>
</div>
<!-- 元信息 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-document"></i> 元信息</div>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">创建人</span><span class="detail-value">{{ form.createBy || '-' }}</span></div>
</el-col>
<el-col :span="12">
<div class="detail-item"><span class="detail-label">创建时间</span><span class="detail-value">{{ form.createTime }}</span></div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">更新人</span><span class="detail-value">{{ form.updateBy || '-' }}</span></div>
</el-col>
<el-col :span="12">
<div class="detail-item"><span class="detail-label">更新时间</span><span class="detail-value">{{ form.updateTime || '-' }}</span></div>
</el-col>
</el-row>
<el-row class="detail-row" v-if="form.remark">
<el-col :span="24">
<div class="detail-item"><span class="detail-label">备注</span><span class="detail-value">{{ form.remark }}</span></div>
</el-col>
</el-row>
</div>
</template>
</div>
</el-dialog>
</template>
<script>
export default {
dicts: ['sys_job_group'],
props: {
visible: { type: Boolean, default: false },
row: { type: Object, default: () => ({}) },
// 'job' 任务详细 | 'log' 调度日志详细
type: { type: String, default: 'job' }
},
computed: {
form() { return this.row || {} },
costTime() {
if (!this.form.startTime || !this.form.endTime) return 0
return new Date(this.form.endTime).getTime() - new Date(this.form.startTime).getTime()
}
}
}
</script>
<style scoped>
.detail-label {
width: 80px;
}
</style>

View File

@@ -94,7 +94,11 @@
<el-table v-loading="loading" :data="jobList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="任务编号" width="100" align="center" prop="jobId" />
<el-table-column label="任务名称" align="center" prop="jobName" :show-overflow-tooltip="true" />
<el-table-column label="任务名称" align="center" :show-overflow-tooltip="true">
<template slot-scope="scope">
<a class="link-type" style="cursor:pointer" @click="handleView(scope.row)">{{ scope.row.jobName }}</a>
</template>
</el-table-column>
<el-table-column label="任务组名" align="center" prop="jobGroup">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_job_group" :value="scope.row.jobGroup"/>
@@ -133,8 +137,6 @@
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="handleRun" icon="el-icon-caret-right"
v-hasPermi="['monitor:job:changeStatus']">执行一次</el-dropdown-item>
<el-dropdown-item command="handleView" icon="el-icon-view"
v-hasPermi="['monitor:job:query']">任务详细</el-dropdown-item>
<el-dropdown-item command="handleJobLog" icon="el-icon-s-operation"
v-hasPermi="['monitor:job:query']">调度日志</el-dropdown-item>
</el-dropdown-menu>
@@ -241,61 +243,17 @@
</el-dialog>
<!-- 任务日志详细 -->
<el-dialog title="任务详细" :visible.sync="openView" width="700px" append-to-body>
<el-form ref="form" :model="form" label-width="120px" size="mini">
<el-row>
<el-col :span="12">
<el-form-item label="任务编号:">{{ form.jobId }}</el-form-item>
<el-form-item label="任务名称:">{{ form.jobName }}</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="任务分组:">{{ jobGroupFormat(form) }}</el-form-item>
<el-form-item label="创建时间:">{{ form.createTime }}</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="cron表达式">{{ form.cronExpression }}</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="下次执行时间:">{{ parseTime(form.nextValidTime) }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="调用目标方法:">{{ form.invokeTarget }}</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="任务状态:">
<div v-if="form.status == 0">正常</div>
<div v-else-if="form.status == 1">暂停</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否并发:">
<div v-if="form.concurrent == 0">允许</div>
<div v-else-if="form.concurrent == 1">禁止</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="执行策略:">
<div v-if="form.misfirePolicy == 0">默认策略</div>
<div v-else-if="form.misfirePolicy == 1">立即执行</div>
<div v-else-if="form.misfirePolicy == 2">执行一次</div>
<div v-else-if="form.misfirePolicy == 3">放弃执行</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="openView = false"> </el-button>
</div>
</el-dialog>
<job-detail :visible.sync="openView" :row="form" type="job" />
</div>
</template>
<script>
import { listJob, getJob, delJob, addJob, updateJob, runJob, changeJobStatus } from "@/api/monitor/job";
import { listJob, getJob, delJob, addJob, updateJob, runJob, changeJobStatus } from "@/api/monitor/job"
import JobDetail from './detail'
import Crontab from '@/components/Crontab'
export default {
components: { Crontab },
components: { Crontab, JobDetail },
name: "Job",
dicts: ['sys_job_group', 'sys_job_status'],
data() {
@@ -346,29 +304,29 @@ export default {
{ required: true, message: "cron执行表达式不能为空", trigger: "blur" }
]
}
};
}
},
created() {
this.getList();
this.getList()
},
methods: {
/** 查询定时任务列表 */
getList() {
this.loading = true;
this.loading = true
listJob(this.queryParams).then(response => {
this.jobList = response.rows;
this.total = response.total;
this.loading = false;
});
this.jobList = response.rows
this.total = response.total
this.loading = false
})
},
// 任务组名字典翻译
jobGroupFormat(row, column) {
return this.selectDictLabel(this.dict.type.sys_job_group, row.jobGroup);
return this.selectDictLabel(this.dict.type.sys_job_group, row.jobGroup)
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
this.open = false
this.reset()
},
// 表单重置
reset() {
@@ -381,96 +339,96 @@ export default {
misfirePolicy: 1,
concurrent: 1,
status: "0"
};
this.resetForm("form");
}
this.resetForm("form")
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
this.resetForm("queryForm")
this.handleQuery()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.jobId);
this.single = selection.length != 1;
this.multiple = !selection.length;
this.ids = selection.map(item => item.jobId)
this.single = selection.length != 1
this.multiple = !selection.length
},
// 更多操作触发
handleCommand(command, row) {
switch (command) {
case "handleRun":
this.handleRun(row);
break;
this.handleRun(row)
break
case "handleView":
this.handleView(row);
break;
this.handleView(row)
break
case "handleJobLog":
this.handleJobLog(row);
break;
this.handleJobLog(row)
break
default:
break;
break
}
},
// 任务状态修改
handleStatusChange(row) {
let text = row.status === "0" ? "启用" : "停用";
let text = row.status === "0" ? "启用" : "停用"
this.$modal.confirm('确认要"' + text + '""' + row.jobName + '"任务吗?').then(function() {
return changeJobStatus(row.jobId, row.status);
return changeJobStatus(row.jobId, row.status)
}).then(() => {
this.$modal.msgSuccess(text + "成功");
this.$modal.msgSuccess(text + "成功")
}).catch(function() {
row.status = row.status === "0" ? "1" : "0";
});
row.status = row.status === "0" ? "1" : "0"
})
},
/* 立即执行一次 */
handleRun(row) {
this.$modal.confirm('确认要立即执行一次"' + row.jobName + '"任务吗?').then(function() {
return runJob(row.jobId, row.jobGroup);
return runJob(row.jobId, row.jobGroup)
}).then(() => {
this.$modal.msgSuccess("执行成功");
}).catch(() => {});
this.$modal.msgSuccess("执行成功")
}).catch(() => {})
},
/** 任务详细信息 */
handleView(row) {
getJob(row.jobId).then(response => {
this.form = response.data;
this.openView = true;
});
this.form = response.data
this.openView = true
})
},
/** cron表达式按钮操作 */
handleShowCron() {
this.expression = this.form.cronExpression;
this.openCron = true;
this.expression = this.form.cronExpression
this.openCron = true
},
/** 确定后回传值 */
crontabFill(value) {
this.form.cronExpression = value;
this.form.cronExpression = value
},
/** 任务日志列表查询 */
handleJobLog(row) {
const jobId = row.jobId || 0;
const jobId = row.jobId || 0
this.$router.push('/monitor/job-log/index/' + jobId)
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加任务";
this.reset()
this.open = true
this.title = "添加任务"
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
const jobId = row.jobId || this.ids;
this.reset()
const jobId = row.jobId || this.ids
getJob(jobId).then(response => {
this.form = response.data;
this.open = true;
this.title = "修改任务";
});
this.form = response.data
this.open = true
this.title = "修改任务"
})
},
/** 提交按钮 */
submitForm: function() {
@@ -478,29 +436,29 @@ export default {
if (valid) {
if (this.form.jobId != undefined) {
updateJob(this.form).then(() => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
});
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
})
} else {
addJob(this.form).then(() => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
});
this.$modal.msgSuccess("新增成功")
this.open = false
this.getList()
})
}
}
});
})
},
/** 删除按钮操作 */
handleDelete(row) {
const jobIds = row.jobId || this.ids;
const jobIds = row.jobId || this.ids
this.$modal.confirm('是否确认删除定时任务编号为"' + jobIds + '"的数据项?').then(function() {
return delJob(jobIds);
return delJob(jobIds)
}).then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
},
/** 导出按钮操作 */
handleExport() {
@@ -509,5 +467,5 @@ export default {
}, `job_${new Date().getTime()}.xlsx`)
}
}
};
}
</script>

View File

@@ -143,48 +143,18 @@
@pagination="getList"
/>
<!-- 调度日志详细 -->
<el-dialog title="调度日志详细" :visible.sync="open" width="700px" append-to-body>
<el-form ref="form" :model="form" label-width="100px" size="mini">
<el-row>
<el-col :span="12">
<el-form-item label="日志序号:">{{ form.jobLogId }}</el-form-item>
<el-form-item label="任务名称:">{{ form.jobName }}</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="任务分组:">{{ form.jobGroup }}</el-form-item>
<el-form-item label="执行时间:">{{ form.createTime }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="调用方法:">{{ form.invokeTarget }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="日志信息:">{{ form.jobMessage }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="执行状态:">
<div v-if="form.status == 0">正常</div>
<div v-else-if="form.status == 1">失败</div>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="异常信息:" v-if="form.status == 1">{{ form.exceptionInfo }}</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="open = false"> </el-button>
</div>
</el-dialog>
<job-log-detail :visible.sync="open" :row="form" type="log" />
</div>
</template>
<script>
import { getJob} from "@/api/monitor/job";
import { listJobLog, delJobLog, cleanJobLog } from "@/api/monitor/jobLog";
import { getJob} from "@/api/monitor/job"
import { listJobLog, delJobLog, cleanJobLog } from "@/api/monitor/jobLog"
import JobLogDetail from './detail'
export default {
name: "JobLog",
components: { JobLogDetail },
dicts: ['sys_common_status', 'sys_job_group'],
data() {
return {
@@ -214,75 +184,75 @@ export default {
jobGroup: undefined,
status: undefined
}
};
}
},
created() {
const jobId = this.$route.params && this.$route.params.jobId;
const jobId = this.$route.params && this.$route.params.jobId
if (jobId !== undefined && jobId != 0) {
getJob(jobId).then(response => {
this.queryParams.jobName = response.data.jobName;
this.queryParams.jobGroup = response.data.jobGroup;
this.getList();
});
this.queryParams.jobName = response.data.jobName
this.queryParams.jobGroup = response.data.jobGroup
this.getList()
})
} else {
this.getList();
this.getList()
}
},
methods: {
/** 查询调度日志列表 */
getList() {
this.loading = true;
this.loading = true
listJobLog(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
this.jobLogList = response.rows;
this.total = response.total;
this.loading = false;
this.jobLogList = response.rows
this.total = response.total
this.loading = false
}
);
)
},
// 返回按钮
handleClose() {
const obj = { path: "/monitor/job" };
this.$tab.closeOpenPage(obj);
const obj = { path: "/monitor/job" }
this.$tab.closeOpenPage(obj)
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = [];
this.resetForm("queryForm");
this.handleQuery();
this.dateRange = []
this.resetForm("queryForm")
this.handleQuery()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.jobLogId);
this.multiple = !selection.length;
this.ids = selection.map(item => item.jobLogId)
this.multiple = !selection.length
},
/** 详细按钮操作 */
handleView(row) {
this.open = true;
this.form = row;
this.open = true
this.form = row
},
/** 删除按钮操作 */
handleDelete(row) {
const jobLogIds = this.ids;
const jobLogIds = this.ids
this.$modal.confirm('是否确认删除调度日志编号为"' + jobLogIds + '"的数据项?').then(function() {
return delJobLog(jobLogIds);
return delJobLog(jobLogIds)
}).then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
},
/** 清空按钮操作 */
handleClean() {
this.$modal.confirm('是否确认清空所有调度日志数据项?').then(function() {
return cleanJobLog();
return cleanJobLog()
}).then(() => {
this.getList();
this.$modal.msgSuccess("清空成功");
}).catch(() => {});
this.getList()
this.$modal.msgSuccess("清空成功")
}).catch(() => {})
},
/** 导出按钮操作 */
handleExport() {
@@ -291,5 +261,5 @@ export default {
}, `log_${new Date().getTime()}.xlsx`)
}
}
};
</script>
}
</script>

View File

@@ -36,6 +36,16 @@
v-hasPermi="['system:dept:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-check"
size="mini"
@click="handleSaveSort"
v-hasPermi="['system:dept:edit']"
>保存排序</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
@@ -57,7 +67,11 @@
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
>
<el-table-column prop="deptName" label="部门名称" width="260"></el-table-column>
<el-table-column prop="orderNum" label="排序" width="200"></el-table-column>
<el-table-column prop="orderNum" label="排序" width="200">
<template slot-scope="scope">
<el-input-number v-model="scope.row.orderNum" controls-position="right" :min="0" size="mini" style="width: 88px" />
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status"/>
@@ -158,7 +172,7 @@
</template>
<script>
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept"
import { listDept, getDept, delDept, addDept, updateDept, updateDeptSort, listDeptExcludeChild } from "@/api/system/dept"
import Treeselect from "@riophae/vue-treeselect"
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
@@ -184,6 +198,8 @@ export default {
isExpandAll: true,
// 重新渲染表格状态
refreshTable: true,
// 记录原始排序,用于对比变更
originalOrders: {},
// 查询参数
queryParams: {
deptName: undefined,
@@ -228,6 +244,8 @@ export default {
this.loading = true
listDept(this.queryParams).then(response => {
this.deptList = this.handleTree(response.data, "deptId")
// 记录原始排序值
this.recordOriginalOrders(this.deptList)
this.loading = false
})
},
@@ -326,6 +344,40 @@ export default {
}
})
},
/** 递归记录原始排序 */
recordOriginalOrders(list) {
list.forEach(item => {
this.originalOrders[item.deptId] = item.orderNum
if (item.children && item.children.length) {
this.recordOriginalOrders(item.children)
}
})
},
/** 保存排序 */
handleSaveSort() {
const changedDeptIds = []
const changedOrderNums = []
const collectChanged = (list) => {
list.forEach(item => {
if (String(this.originalOrders[item.deptId]) !== String(item.orderNum)) {
changedDeptIds.push(item.deptId)
changedOrderNums.push(item.orderNum)
}
if (item.children && item.children.length) {
collectChanged(item.children)
}
})
}
collectChanged(this.deptList)
if (changedDeptIds.length === 0) {
this.$modal.msgWarning("未检测到排序修改")
return
}
updateDeptSort({ deptIds: changedDeptIds.join(","), orderNums: changedOrderNums.join(",") }).then(() => {
this.$modal.msgSuccess("排序保存成功")
this.recordOriginalOrders(this.deptList)
})
},
/** 删除按钮操作 */
handleDelete(row) {
this.$modal.confirm('是否确认删除名称为"' + row.deptName + '"的数据项?').then(function() {

View File

@@ -0,0 +1,203 @@
<template>
<el-drawer :title="drawerTitle" :visible.sync="localVisible" direction="rtl" size="700px" append-to-body>
<div class="drawer-wrap">
<div v-if="loading" class="drawer-loading">
<i class="el-icon-loading"></i>
<span>加载中...</span>
</div>
<div v-else-if="!dataList.length" class="drawer-empty">
<i class="el-icon-document"></i>
<div>暂无字典数据</div>
</div>
<template v-else>
<el-row :gutter="12" class="stat-row">
<el-col :span="disabledCount > 0 ? 8 : 12">
<div class="stat-card">
<div class="stat-num">{{ dataList.length }}</div>
<div class="stat-label">共计条目</div>
</div>
</el-col>
<el-col :span="disabledCount > 0 ? 8 : 12">
<div class="stat-card">
<div class="stat-num success">{{ normalCount }}</div>
<div class="stat-label">正常</div>
</div>
</el-col>
<el-col :span="8" v-if="disabledCount > 0">
<div class="stat-card">
<div class="stat-num danger">{{ disabledCount }}</div>
<div class="stat-label">停用</div>
</div>
</el-col>
</el-row>
<div v-for="item in dataList" :key="item.dictCode" class="dict-item">
<div class="dict-cell">
<div class="dict-cell-key">标签</div>
<div class="dict-cell-val">
<el-tag v-if="item.listClass && item.listClass !== 'default'" :type="item.listClass === 'primary' ? '' : item.listClass" size="small">{{ item.dictLabel }}</el-tag>
<span v-else>{{ item.dictLabel }}</span>
</div>
</div>
<div class="dict-cell">
<div class="dict-cell-key">键值</div>
<div class="dict-cell-val">{{ item.dictValue }}</div>
</div>
<div class="dict-cell">
<div class="dict-cell-key">状态</div>
<div class="dict-cell-val">
<el-tag :type="item.status === '0' ? 'success' : 'danger'" size="small">
{{ item.status === '0' ? '正常' : '停用' }}
</el-tag>
</div>
</div>
</div>
</template>
</div>
</el-drawer>
</template>
<script>
import { listData } from '@/api/system/dict/data'
export default {
props: {
visible: { type: Boolean, default: false },
row: { type: Object, default: () => ({}) }
},
data() {
return {
localVisible: false,
loading: false,
dataList: []
}
},
computed: {
drawerTitle() {
return this.row.dictName + ' ' + (this.row.dictType || '')
},
normalCount() {
return this.dataList.filter(r => r.status === '0').length
},
disabledCount() {
return this.dataList.filter(r => r.status !== '0').length
}
},
watch: {
visible(val) {
this.localVisible = val
},
localVisible(val) {
this.$emit('update:visible', val)
if (val) {
this.loadData()
} else {
this.dataList = []
}
}
},
methods: {
loadData() {
if (!this.row || !this.row.dictType) return
this.loading = true
this.dataList = []
listData({ dictType: this.row.dictType, pageSize: 100, pageNum: 1 }).then(response => {
this.dataList = response.rows || []
}).catch(() => {}).finally(() => {
this.loading = false
})
}
}
}
</script>
<style scoped>
.drawer-wrap {
padding: 0 20px 20px;
}
.drawer-loading {
display: flex;
align-items: center;
justify-content: center;
height: 120px;
color: #aaa;
font-size: 13px;
gap: 8px;
}
.drawer-empty {
text-align: center;
color: #bbb;
padding: 60px 0;
font-size: 13px;
}
.drawer-empty i {
font-size: 36px;
display: block;
margin-bottom: 8px;
}
.stat-row {
margin-bottom: 16px;
}
.stat-card {
background: #f7f9fb;
border: 1px solid #e8ecf0;
border-radius: 6px;
padding: 10px 14px;
text-align: center;
}
.stat-num {
font-size: 22px;
font-weight: 700;
color: #2c3e50;
}
.stat-num.success { color: #27ae60; }
.stat-num.danger { color: #e74c3c; }
.stat-label {
font-size: 11px;
color: #95a5a6;
margin-top: 4px;
}
.dict-item {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
border: 1px solid #e8ecf0;
border-radius: 6px;
overflow: hidden;
margin-bottom: 8px;
}
.dict-cell {
display: grid;
grid-template-columns: 70px 1fr;
border-right: 1px solid #f0f4f8;
}
.dict-cell:last-child {
border-right: 0;
}
.dict-cell-key {
padding: 9px 14px;
font-size: 12px;
color: #888;
background: #f7f9fb;
border-right: 1px solid #f0f4f8;
}
.dict-cell-val {
padding: 9px 14px;
font-size: 13px;
color: #2c3e50;
word-break: break-all;
display: flex;
align-items: center;
}
</style>

View File

@@ -113,9 +113,7 @@
<el-table-column label="字典名称" align="center" prop="dictName" :show-overflow-tooltip="true" />
<el-table-column label="字典类型" align="center" :show-overflow-tooltip="true">
<template slot-scope="scope">
<router-link :to="'/system/dict-data/index/' + scope.row.dictId" class="link-type">
<span>{{ scope.row.dictType }}</span>
</router-link>
<a class="link-type" style="cursor:pointer" @click="handleViewData(scope.row)">{{ scope.row.dictType }}</a>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status">
@@ -138,6 +136,13 @@
@click="handleUpdate(scope.row)"
v-hasPermi="['system:dict:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-s-operation"
@click="handleDataList(scope.row)"
v-hasPermi="['system:dict:edit']"
>列表</el-button>
<el-button
size="mini"
type="text"
@@ -190,14 +195,18 @@
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
<dict-data-drawer :visible.sync="drawerVisible" :row="drawerRow" />
</div>
</template>
<script>
import DictDataDrawer from './detail'
import { listType, getType, delType, addType, updateType, refreshCache } from "@/api/system/dict/type"
export default {
name: "Dict",
components: { DictDataDrawer },
dicts: ['sys_normal_disable'],
data() {
return {
@@ -219,6 +228,10 @@ export default {
title: "",
// 是否显示弹出层
open: false,
// 字典数据抽屉状态
drawerVisible: false,
// 字典数据信息
drawerRow: {},
// 日期范围
dateRange: [],
// 查询参数
@@ -295,6 +308,15 @@ export default {
this.single = selection.length!=1
this.multiple = !selection.length
},
/** 字典数据抽屉显示信息 */
handleViewData(row) {
this.drawerRow = row
this.drawerVisible = true
},
/** 字典数据列表页面 */
handleDataList(row) {
this.$tab.openPage("字典数据", '/system/dict-data/index/' + row.dictId)
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()

View File

@@ -36,6 +36,16 @@
v-hasPermi="['system:menu:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-check"
size="mini"
@click="handleSaveSort"
v-hasPermi="['system:menu:edit']"
>保存排序</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
@@ -62,7 +72,11 @@
<svg-icon :icon-class="scope.row.icon" />
</template>
</el-table-column>
<el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
<el-table-column prop="orderNum" label="排序" width="200">
<template slot-scope="scope">
<el-input-number v-model="scope.row.orderNum" controls-position="right" :min="0" size="mini" style="width: 88px" />
</template>
</el-table-column>
<el-table-column prop="perms" 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">
@@ -299,7 +313,7 @@
</template>
<script>
import { listMenu, getMenu, delMenu, addMenu, updateMenu } from "@/api/system/menu"
import { listMenu, getMenu, delMenu, addMenu, updateMenu, updateMenuSort } from "@/api/system/menu"
import Treeselect from "@riophae/vue-treeselect"
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
import IconSelect from "@/components/IconSelect"
@@ -326,6 +340,8 @@ export default {
isExpandAll: false,
// 重新渲染表格状态
refreshTable: true,
// 记录原始排序,用于对比变更
originalOrders: {},
// 查询参数
queryParams: {
menuName: undefined,
@@ -360,6 +376,8 @@ export default {
this.loading = true
listMenu(this.queryParams).then(response => {
this.menuList = this.handleTree(response.data, "menuId")
// 记录原始排序值
this.recordOriginalOrders(this.menuList)
this.loading = false
})
},
@@ -463,6 +481,40 @@ export default {
}
})
},
/** 递归记录原始排序 */
recordOriginalOrders(list) {
list.forEach(item => {
this.originalOrders[item.menuId] = item.orderNum
if (item.children && item.children.length) {
this.recordOriginalOrders(item.children)
}
})
},
/** 保存排序 */
handleSaveSort() {
const changedMenuIds = []
const changedOrderNums = []
const collectChanged = (list) => {
list.forEach(item => {
if (String(this.originalOrders[item.menuId]) !== String(item.orderNum)) {
changedMenuIds.push(item.menuId)
changedOrderNums.push(item.orderNum)
}
if (item.children && item.children.length) {
collectChanged(item.children)
}
})
}
collectChanged(this.menuList)
if (changedMenuIds.length === 0) {
this.$modal.msgWarning("未检测到排序修改")
return
}
updateMenuSort({ menuIds: changedMenuIds.join(","), orderNums: changedOrderNums.join(",") }).then(() => {
this.$modal.msgSuccess("排序保存成功")
this.recordOriginalOrders(this.menuList)
})
},
/** 删除按钮操作 */
handleDelete(row) {
this.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?').then(function() {

View File

@@ -0,0 +1,147 @@
<template>
<el-dialog title="操作日志详细" :visible.sync="visible" width="780px" append-to-body @close="$emit('update:visible', false)">
<div class="detail-wrap">
<!-- 基本信息 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-info"></i> 基本信息</div>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">操作模块</span><span class="detail-value">{{ form.title }}</span></div>
</el-col>
<el-col :span="12">
<div class="detail-item"><span class="detail-label">业务类型</span><span class="detail-value">{{ typeLabel }}</span></div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">操作时间</span><span class="detail-value">{{ form.operTime }}</span></div>
</el-col>
<el-col :span="12">
<div class="detail-item">
<span class="detail-label">执行状态</span>
<el-tag v-if="form.status === 0" type="success" size="small"><i class="el-icon-check"></i> 正常</el-tag>
<el-tag v-else type="danger" size="small"><i class="el-icon-close"></i> 异常</el-tag>
</div>
</el-col>
</el-row>
</div>
<!-- 操作人员 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-user"></i> 操作人员</div>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">操作人员</span><span class="detail-value">{{ form.operName }}</span></div>
</el-col>
<el-col :span="12" v-if="form.deptName">
<div class="detail-item"><span class="detail-label">所属部门</span><span class="detail-value">{{ form.deptName }}</span></div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="24">
<div class="detail-item">
<span class="detail-label">操作地址</span>
<span class="detail-value">{{ form.operIp }}&nbsp;&nbsp;<span class="detail-location">{{ form.operLocation }}</span></span>
</div>
</el-col>
</el-row>
</div>
<!-- 请求信息 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-sort"></i> 请求信息</div>
<el-row class="detail-row">
<el-col :span="24">
<div class="detail-item">
<span class="detail-label">请求地址</span>
<span class="detail-value">
<span :class="'method-tag method-' + form.requestMethod">{{ form.requestMethod }}</span>
{{ form.operUrl }}
</span>
</div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="24">
<div class="detail-item"><span class="detail-label">操作方法</span><span class="detail-value mono">{{ form.method }}</span></div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">消耗时间</span><span class="detail-value">{{ form.costTime }} 毫秒</span></div>
</el-col>
</el-row>
</div>
<!-- 请求参数 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-upload2"></i> 请求参数</div>
<div class="code-body">
<div class="code-wrap">
<div class="code-action">
<el-button size="mini" icon="el-icon-copy-document" @click="copyText(form.operParam)">复制</el-button>
</div>
<pre class="code-pre">{{ formatJson(form.operParam) }}</pre>
</div>
</div>
</div>
<!-- 返回参数 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-download"></i> 返回参数</div>
<div class="code-body">
<div class="code-wrap">
<div class="code-action">
<el-button size="mini" icon="el-icon-copy-document" @click="copyText(form.jsonResult)">复制</el-button>
</div>
<pre class="code-pre">{{ formatJson(form.jsonResult) }}</pre>
</div>
</div>
</div>
<!-- 异常信息 -->
<div class="detail-card" v-if="form.status !== 0">
<div class="detail-card-title error-title"><i class="el-icon-warning"></i> 异常信息</div>
<div class="error-body">
<div class="error-msg">{{ form.errorMsg }}</div>
</div>
</div>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'OperlogDetail',
dicts: ['sys_oper_type'],
props: {
visible: { type: Boolean, default: false },
row: { type: Object, default: () => ({}) }
},
computed: {
form() { return this.row || {} },
typeLabel() { return this.selectDictLabel(this.dict.type.sys_oper_type, this.form.businessType) || '-' }
},
methods: {
formatJson(str) {
if (!str) return '(无数据)'
try { return JSON.stringify(JSON.parse(str), null, 2) } catch { return str }
},
copyText(str) {
const text = this.formatJson(str)
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => this.$message({ message: '已复制', type: 'success', duration: 1500 }))
} else {
const ta = document.createElement('textarea')
ta.value = text
document.body.appendChild(ta)
ta.select()
document.execCommand('copy')
document.body.removeChild(ta)
this.$message({ message: '已复制', type: 'success', duration: 1500 })
}
}
}
}
</script>

View File

@@ -144,7 +144,7 @@
size="mini"
type="text"
icon="el-icon-view"
@click="handleView(scope.row,scope.index)"
@click="handleDetail(scope.row,scope.index)"
v-hasPermi="['system:operlog:query']"
>详细</el-button>
</template>
@@ -159,58 +159,17 @@
@pagination="getList"
/>
<!-- 操作日志详细 -->
<el-dialog title="操作日志详细" :visible.sync="open" width="800px" append-to-body>
<el-form ref="form" :model="form" label-width="100px" size="mini">
<el-row>
<el-col :span="12">
<el-form-item label="操作模块:">{{ form.title }} / {{ typeFormat(form) }}</el-form-item>
<el-form-item
label="登录信息:"
>{{ form.operName }} / {{ form.operIp }}</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="请求地址:">{{ form.operUrl }}</el-form-item>
<el-form-item label="请求方式:">{{ form.requestMethod }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="操作方法:">{{ form.method }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="请求参数:">{{ form.operParam }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="返回参数:">{{ form.jsonResult }}</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="操作状态:">
<div v-if="form.status === 0">正常</div>
<div v-else-if="form.status === 1">失败</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="消耗时间:">{{ form.costTime }}毫秒</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="操作时间:">{{ parseTime(form.operTime) }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="异常信息:" v-if="form.status === 1">{{ form.errorMsg }}</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="open = false"> </el-button>
</div>
</el-dialog>
<operlog-detail :visible.sync="detailVisible" :row="detailRow" />
</div>
</template>
<script>
import OperlogDetail from './detail'
import { list, delOperlog, cleanOperlog } from "@/api/system/operlog"
export default {
name: "Operlog",
components: { OperlogDetail },
dicts: ['sys_oper_type', 'sys_common_status'],
data() {
return {
@@ -227,7 +186,8 @@ export default {
// 表格数据
list: [],
// 是否显示弹出层
open: false,
detailVisible: false,
detailRow: {},
// 日期范围
dateRange: [],
// 默认排序
@@ -260,9 +220,10 @@ export default {
}
)
},
// 操作日志类型字典翻译
typeFormat(row, column) {
return this.selectDictLabel(this.dict.type.sys_oper_type, row.businessType)
// 详细按钮操作
handleDetail(row) {
this.detailRow = row
this.detailVisible = true
},
/** 搜索按钮操作 */
handleQuery() {
@@ -287,11 +248,6 @@ export default {
this.queryParams.isAsc = column.order
this.getList()
},
/** 详细按钮操作 */
handleView(row) {
this.open = true
this.form = row
},
/** 删除按钮操作 */
handleDelete(row) {
const operIds = row.operId || this.ids

View File

@@ -609,6 +609,8 @@ create table sys_job_log (
job_message varchar(500) comment '日志信息',
status char(1) default '0' comment '执行状态0正常 1失败',
exception_info varchar(2000) default '' comment '异常信息',
start_time datetime comment '执行开始时间',
end_time datetime comment '执行结束时间',
create_time datetime comment '创建时间',
primary key (job_log_id)
) engine=innodb comment = '定时任务调度日志表';
@@ -637,10 +639,25 @@ create table sys_notice (
-- ----------------------------
insert into sys_notice values('1', '温馨提醒2018-07-01 若依新版本发布啦', '2', '新版本内容', '0', 'admin', sysdate(), '', null, '管理员');
insert into sys_notice values('2', '维护通知2018-07-01 若依系统凌晨维护', '1', '维护内容', '0', 'admin', sysdate(), '', null, '管理员');
insert into sys_notice values('3', '若依开源框架介绍', '1', '<p><span style=\"color: rgb(230, 0, 0);\">项目介绍</span></p><p><font color=\"#333333\">RuoYi开源项目是为企业用户定制的后台脚手架框架为企业打造的一站式解决方案降低企业开发成本提升开发效率。主要包括用户管理、角色管理、部门管理、菜单管理、参数管理、字典管理、</font><span style=\"color: rgb(51, 51, 51);\">岗位管理</span><span style=\"color: rgb(51, 51, 51);\">、定时任务</span><span style=\"color: rgb(51, 51, 51);\">、</span><span style=\"color: rgb(51, 51, 51);\">服务监控、登录日志、操作日志、代码生成等功能。其中还支持多数据源、数据权限、国际化、Redis缓存、Docker部署、滑动验证码、第三方认证登录、分布式事务、</span><font color=\"#333333\">分布式文件存储</font><span style=\"color: rgb(51, 51, 51);\">、分库分表处理等技术特点。</span></p><p><img src=\"https://foruda.gitee.com/images/1773931848342439032/a4d22313_1815095.png\" style=\"width: 64px;\"><br></p><p><span style=\"color: rgb(230, 0, 0);\">官网及演示</span></p><p><span style=\"color: rgb(51, 51, 51);\">若依官网地址:&nbsp;</span><a href=\"http://ruoyi.vip\" target=\"_blank\">http://ruoyi.vip</a><a href=\"http://ruoyi.vip\" target=\"_blank\"></a></p><p><span style=\"color: rgb(51, 51, 51);\">若依文档地址:&nbsp;</span><a href=\"http://doc.ruoyi.vip\" target=\"_blank\">http://doc.ruoyi.vip</a><br></p><p><span style=\"color: rgb(51, 51, 51);\">演示地址【不分离版】:&nbsp;</span><a href=\"http://demo.ruoyi.vip\" target=\"_blank\">http://demo.ruoyi.vip</a></p><p><span style=\"color: rgb(51, 51, 51);\">演示地址【分离版本】:&nbsp;</span><a href=\"http://vue.ruoyi.vip\" target=\"_blank\">http://vue.ruoyi.vip</a></p><p><span style=\"color: rgb(51, 51, 51);\">演示地址【微服务版】:&nbsp;</span><a href=\"http://cloud.ruoyi.vip\" target=\"_blank\">http://cloud.ruoyi.vip</a></p><p><span style=\"color: rgb(51, 51, 51);\">演示地址【移动端版】:&nbsp;</span><a href=\"http://h5.ruoyi.vip\" target=\"_blank\">http://h5.ruoyi.vip</a></p><p><br style=\"color: rgb(48, 49, 51); font-family: &quot;Helvetica Neue&quot;, Helvetica, Arial, sans-serif; font-size: 12px;\"></p>', '0', 'admin', sysdate(), '', null, '管理员');
-- ----------------------------
-- 18、代码生成业务
-- 18、公告已读记录
-- ----------------------------
drop table if exists sys_notice_read;
create table sys_notice_read (
read_id bigint(20) not null auto_increment comment '已读主键',
notice_id int(4) not null comment '公告id',
user_id bigint(20) not null comment '用户id',
read_time datetime not null comment '阅读时间',
primary key (read_id),
unique key uk_user_notice (user_id, notice_id) comment '同一用户同一公告只记录一次'
) engine=innodb auto_increment=1 comment='公告已读记录表';
-- ----------------------------
-- 19、代码生成业务表
-- ----------------------------
drop table if exists gen_table;
create table gen_table (
@@ -670,7 +687,7 @@ create table gen_table (
-- ----------------------------
-- 19、代码生成业务表字段
-- 20、代码生成业务表字段
-- ----------------------------
drop table if exists gen_table_column;
create table gen_table_column (