简易记事本开发-(SSM+Vue)
目录
前言
一、项目需求分析
二、项目环境搭建
1.创建MavenWeb项目:
2.配置 Spring、SpringMVC 和 MyBatis
SpringMVC 配置文件 (spring-mvc.xml): 配置视图解析器、处理器映射器,配置了CORS(跨源资源共享),允许来自http://localhost:5173的跨域请求。
spring-mybatis:
三、数据库设计:
四、功能模块实现
用户管理:
UserController:
User实体:
UserService:
UserServiceImp:
UserMapper:
文件管理:
FileController:
事件管理:
EventsConroller:
Events实体:
EventsService:
EventsServiceImp:
EventsMapper:
EventCategories都是同理,后面就不放了
拦截器:
五、前端界面(Vue):
登录界面:
用户注册:
个人信息:
首页:
事件分类:
事件管理:
登出:
项目目录参考:
六:运行界面
登录:
首页:
分类:
事件:
前言
这次博客续在上次的SSM框架的简易记事本,更新了前端,我的博客里面一直以来都不会把完整代码放出来,假如CSDN的文章质量跟代码图片这些没关联的话,说不定我连部分代码都不会放,写博客的目的更多的是想分享我的思路,而不是把代码放出来让别人抄,这种对自己对其他人都不尊重——我是这样想的
一、项目需求分析
开发一个基于 SSM框架+Vue的简易记事本项目,主要功能包括:
- 用户注册
- 用户登录与退出
- 事件分类的增删改查管理
- 事件管理的增删改查管理
二、项目环境搭建
1.创建MavenWeb项目:
- 使用 IDEA 创建 Maven Web 工程,设置打包方式为
war
。 - 添加 SSM 框架依赖到
pom.xml
文件中:
pom文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.flowerfog</groupId>
<artifactId>SSM</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- junit -->
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.16</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.1.12</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.1.12</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>6.1.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.23</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
2.配置 Spring、SpringMVC 和 MyBatis
SpringMVC 配置文件 (spring-mvc.xml
): 配置视图解析器、处理器映射器,配置了CORS(跨源资源共享),允许来自http://localhost:5173
的跨域请求。
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
">
<mvc:annotation-driven/>
<context:component-scan base-package="org.flowerfog"/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
<mvc:default-servlet-handler/>
<mvc:interceptors>
<bean class="org.flowerfog.intercept.LoginInterceptors"/>
</mvc:interceptors>
<mvc:cors>
<mvc:mapping path="/**"
allowed-origins="http://localhost:5173"
allowed-methods="POST, GET, OPTIONS, DELETE, PUT"
allowed-headers="Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With"
allow-credentials="true" />
</mvc:cors>
</beans>
spring-mybatis:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="useGeneratedKeys" value="true"/>
<setting name="autoMappingBehavior" value="FULL"/>
</settings>
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="helperDialect" value="mysql"/>
</plugin>
</plugins>
</configuration>
三、数据库设计:
这里不谈,后续需要sql的可以联系我
四、功能模块实现
用户管理:
UserController:
package org.flowerfog.controller;
import org.flowerfog.pojo.entity.Users;
import org.flowerfog.pojo.vo.UserInfoVO;
import org.flowerfog.pojo.vo.UserLoginVO;
import org.flowerfog.service.UsersService;
import org.flowerfog.utils.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @author flowerfog
* @version 1.0
* @description: TODO
* @date 2024/12/7 20:32
*/
@RestController
@CrossOrigin
@RequestMapping("/user")
public class UsersController {
@Autowired
private UsersService usersService;
//登录
@PostMapping("/login")
public Result login(@RequestBody UserLoginVO user) {
System.out.println(user.getUsername());
boolean flag = usersService.login(user.getUsername(), user.getPassword());
if (!flag) {
return Result.error("用户名或密码错误");
}
return Result.success("登陆成功");
}
//注册
@PostMapping("/register")
public Result register(@RequestBody Users users) {
usersService.register(users);
return Result.success();
}
@GetMapping("/findbyid")
public Result findById() {
UserInfoVO users = usersService.findById();
return Result.success(users);
}
//修改
@PostMapping("/update")
public Result update(@RequestBody Users users) {
usersService.update(users);
return Result.success();
}
//查所有用户
@GetMapping("/findall")
public Result findAll() {
return Result.success(usersService.findAll());
}
//退出登录
@GetMapping("/logout")
public Result logout() {
usersService.logout();
return Result.success();
}
}
User实体:
package org.flowerfog.pojo.entity;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用户表
* users
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Users {
private Integer id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 昵称
*/
private String nickname;
/**
* 年龄
*/
private Integer age;
/**
* 邮箱
*/
private String email;
/**
* 手机号
*/
private String phone;
/**
* 个人简介
*/
private String bio;
/**
* 头像URL
*/
private String avatar;
/**
* 创建时间
*/
private Date createdAt;
/**
* 更新时间
*/
private Date updatedAt;
private static final long serialVersionUID = 1L;
}
UserService:
package org.flowerfog.service;
import org.flowerfog.pojo.entity.Users;
import org.flowerfog.pojo.vo.UserInfoVO;
import java.util.List;
public interface UsersService {
UserInfoVO findById();
Boolean login(String username, String password);
Boolean register(Users user);
Boolean update(Users users);
List<Users> findAll();
void logout();
}
UserServiceImp:
package org.flowerfog.service.impl;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.flowerfog.exception.LoginException;
import org.flowerfog.mapper.UsersMapper;
import org.flowerfog.pojo.entity.Users;
import org.flowerfog.pojo.vo.UserInfoVO;
import org.flowerfog.service.UsersService;
import org.flowerfog.utils.Md5Util;
import org.flowerfog.utils.ThreadLocalUtil;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author flowerfog
* @version 1.0
* @date 2024/12/7 20:53
*/
@Service
@RequiredArgsConstructor
public class UsersServiceimpl implements UsersService {
@Autowired
private UsersMapper usersMapper;
private final HttpSession session; // 注入HttpSession
public UserInfoVO findById() {
Integer id = ThreadLocalUtil.get().getId();
Users users = usersMapper.selectByPrimaryKey(id);
UserInfoVO userInfoVO = new UserInfoVO();
BeanUtils.copyProperties(users, userInfoVO);
return userInfoVO;
}
@Override
public Boolean login(String username, String password) {
password = Md5Util.getMD5String(password);
Users flag = usersMapper.login(username, password);
if (flag!=null) {
// 存入session
session.setAttribute("user", flag);
return true;
}
return false;
}
@Override
public Boolean register(Users user) {
user.setPassword(Md5Util.getMD5String(user.getPassword()));
Users flag = usersMapper.findByUsername(user.getUsername());
if(flag!=null){
throw new LoginException("该账号已存在");
}
return usersMapper.insertSelective(user)>0;
}
@Override
public Boolean update(Users users) {
users.setId(ThreadLocalUtil.get().getId());
if(users.getPassword()!=null)
users.setPassword(Md5Util.getMD5String(users.getPassword()));
return usersMapper.updateByPrimaryKeySelective(users)>0;
}
@Override
public List<Users> findAll() {
return usersMapper.findAll();
}
@Override
public void logout() {
session.removeAttribute("user");
}
}
UserMapper:
package org.flowerfog.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.flowerfog.pojo.entity.Users;
import java.util.List;
//@Repository
@Mapper
public interface UsersMapper {
int deleteByPrimaryKey(Integer id);
int insert(Users record);
int insertSelective(Users record);
Users selectByPrimaryKey(Integer id);
int updateByPrimaryKeySelective(Users record);
int updateByPrimaryKey(Users record);
@Select("select * from users where username=#{username} and password=#{password}")
Users login(@Param("username") String username, @Param("password") String password);
@Select("select * from users")
List<Users> findAll();
@Select("select * from users where username=#{username}")
Users findByUsername(String username);
}
mapper.xml就不放了
文件管理:
FileController:
package org.flowerfog.controller;
import org.flowerfog.pojo.entity.Users;
import org.flowerfog.service.UsersService;
import org.flowerfog.utils.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.UUID;
/**
* @author flowerfog
* @version 1.0
* @description: TODO
* @date 2024/12/5 20:31
*/
@RestController
@RequestMapping("/file")
@CrossOrigin
public class FileController {
public static final String PATH = "d:/tmp/";
@Autowired
private UsersService usersService;
@RequestMapping("/upload")
public Result<String> upload(@RequestParam("imgfile") MultipartFile file) throws IOException {
String fileName = UUID.randomUUID().toString();
file.transferTo(new File(PATH + fileName));
Users users = new Users();
users.setAvatar(fileName);
usersService.update(users);
return Result.success(fileName);
}
@RequestMapping("/download/{fileName}")
public void download(@PathVariable("fileName") String fileName, HttpServletResponse response) throws IOException {
FileInputStream fis = new FileInputStream(PATH + fileName);
response.setContentType("application/octet-stream");
OutputStream os = response.getOutputStream();
byte[] buffer = new byte[1024];
int length;
while((length = fis.read(buffer)) > 0){
os.write(buffer, 0, length);
}
fis.close();
}
}
事件管理:
EventsConroller:
package org.flowerfog.controller;
import org.flowerfog.pojo.entity.Events;
import org.flowerfog.service.EventsService;
import org.flowerfog.utils.Result;
import org.flowerfog.utils.ThreadLocalUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @author flowerfog
* @version 1.0
* @description: TODO
* @date 2024/12/7 20:33
*/
@RestController
@CrossOrigin
@RequestMapping("/events")
public class EventsController {
@Autowired
private EventsService eventsService;
// 添加事件
@PostMapping("/add")
public Result add(@RequestBody Events events){
eventsService.add(events);
return Result.success();
}
// 删除事件
@DeleteMapping("/delete")
public Result delete(@RequestParam("id") Integer id){
eventsService.delete(id);
return Result.success();
}
// 查询该用户所有事件
@GetMapping("/findall")
public Result findAll(){
return Result.success(eventsService.findAll(ThreadLocalUtil.get().getId()));
}
//修改事件
@PostMapping("/update")
public Result update(@RequestBody Events events){
eventsService.update(events);
return Result.success();
}
// 查询该用户事件总数
@GetMapping("/count")
public Result count(){
return Result.success(eventsService.findAll(ThreadLocalUtil.get().getId()).size());
}
// 查询该用户已完成的个数
@GetMapping("/countcomplete")
public Result countComplete(){
return Result.success(eventsService.findAll(ThreadLocalUtil.get().getId()).stream().filter(events -> events.getStatus().equals("completed")).count());
}
// 查询该用户待处理事件的个数
@GetMapping("/countuncomplete")
public Result countUnComplete(){
return Result.success(eventsService.findAll(ThreadLocalUtil.get().getId()).size()-eventsService.findAll(ThreadLocalUtil.get().getId()).stream().filter(events -> events.getStatus().equals("completed")).count());
}
//数量前5个分类的事件个数几分类名称
@GetMapping("/countcategory")
public Result countCategory(){
return Result.success(eventsService.findFive());
}
//距离当前时间最接近的5条事件
@GetMapping("/findfive")
public Result findFive(){
return Result.success(eventsService.findFiveevent());
}
//首页数据的接口
@GetMapping("/findhome")
public Result findUnStart(){
return Result.success(eventsService.findhome());
}
}
Events实体:
package org.flowerfog.pojo.entity;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 事件表
* events
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Events {
private Integer id;
/**
* 用户ID
*/
private Integer userId;
/**
* 分类ID
*/
private Integer categoryId;
/**
* 事件标题
*/
private String title;
/**
* 事件描述
*/
private String description;
/**
* 优先级
*/
private Object priority;
/**
* 状态
*/
private Object status;
/**
* 开始时间
*/
private Date startDate;
/**
* 结束时间
*/
private Date endDate;
/**
* 创建时间
*/
private Date createdAt;
/**
* 更新时间
*/
private Date updatedAt;
private static final long serialVersionUID = 1L;
}
EventsService:
package org.flowerfog.service;
import org.flowerfog.pojo.entity.Events;
import org.flowerfog.pojo.vo.CategoryStatVO;
import org.flowerfog.pojo.vo.DashboardVO;
import org.flowerfog.pojo.vo.EventStatVO;
import java.util.List;
public interface EventsService {
void add(Events events);
void delete(Integer id);
List<Events> findAll(Integer userId);
void update(Events events);
List<CategoryStatVO> findFive();
List<DashboardVO> findFiveevent();
EventStatVO findhome();
}
EventsServiceImp:
package org.flowerfog.service.impl;
import org.flowerfog.mapper.EventCategoriesMapper;
import org.flowerfog.mapper.EventsMapper;
import org.flowerfog.pojo.entity.Events;
import org.flowerfog.pojo.entity.Users;
import org.flowerfog.pojo.vo.CategoryStatVO;
import org.flowerfog.pojo.vo.DashboardVO;
import org.flowerfog.pojo.vo.EventStatVO;
import org.flowerfog.pojo.vo.EventsWeekVO;
import org.flowerfog.service.EventsService;
import org.flowerfog.utils.ThreadLocalUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author qinbo
* @version 1.0
* @description: TODO
* @date 2024/12/8 20:38
*/
@Service
public class EventsServiceimpl implements EventsService {
@Autowired
private EventsMapper eventsMapper;
@Autowired
private EventCategoriesMapper eventCategoriesMapper;
@Override
public void add(Events events) {
Users user = ThreadLocalUtil.get();
events.setUserId(user.getId());
events.setStatus("pending");
eventsMapper.insertSelective(events);
}
@Override
public void delete(Integer id) {
eventsMapper.deleteByPrimaryKey(id);
}
@Override
public List<Events> findAll(Integer userId) {
return eventsMapper.findAll(userId);
}
@Override
public void update(Events events) {
eventsMapper.updateByPrimaryKeySelective(events);
}
@Override
public List<CategoryStatVO> findFive() {
Integer id = ThreadLocalUtil.get().getId();
return eventsMapper.findFive(id);
}
@Override
public List<DashboardVO> findFiveevent() {
Integer id = ThreadLocalUtil.get().getId();
return eventsMapper.findFiveenvt(id);
}
@Override
public EventStatVO findhome() {
Integer totalEvents = eventsMapper.findAll(ThreadLocalUtil.get().getId()).size();
Integer pendingEvents = eventsMapper.findpending(ThreadLocalUtil.get().getId());
Integer completedEvents = totalEvents - pendingEvents;
Integer eventstotal = eventCategoriesMapper.count(ThreadLocalUtil.get().getId());
List<CategoryStatVO> categoryStats = eventsMapper.findFive(ThreadLocalUtil.get().getId());
List<EventsWeekVO> eventweek = eventsMapper.eventweek(ThreadLocalUtil.get().getId());
return new EventStatVO(totalEvents, pendingEvents, completedEvents, eventstotal, categoryStats, eventweek);
}
}
EventsMapper:
package org.flowerfog.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.flowerfog.pojo.entity.Events;
import org.flowerfog.pojo.vo.CategoryStatVO;
import org.flowerfog.pojo.vo.DashboardVO;
import org.flowerfog.pojo.vo.EventsWeekVO;
import java.util.List;
//@Repository
@Mapper
public interface EventsMapper {
int deleteByPrimaryKey(Integer id);
int insert(Events record);
int insertSelective(Events record);
Events selectByPrimaryKey(Integer id);
int updateByPrimaryKeySelective(Events record);
int updateByPrimaryKey(Events record);
@Select("select * from events where user_id=#{userId}")
List<Events> findAll(@Param("userId") Integer userId);
@Select("SELECT ec.name ,COUNT(e.id) as totalCount FROM events e INNER JOIN event_categories ec ON e.category_id = ec.id INNER JOIN users u ON e.user_id = u.id WHERE u.id = #{id} GROUP BY u.id, ec.name ORDER BY COUNT(e.id) DESC LIMIT 5")
List<CategoryStatVO> findFive(@Param("id") Integer id);
@Select("SELECT\n" +
" e.title,\n" +
" c.name,\n" +
" e.status,\n" +
" e.end_date\n" +
"FROM\n" +
" events e\n" +
" JOIN\n" +
" event_categories c ON e.category_id = c.id\n" +
"WHERE\n" +
" e.user_id = #{id} AND\n" +
" e.status <> 'completed' AND\n" +
" e.end_Date > NOW()\n" +
"ORDER BY\n" +
" e.end_Date ASC\n" +
"LIMIT\n" +
" 5;")
List<DashboardVO> findFiveenvt(Integer id);
@Select("select count(*) from events where user_id=#{id} and (status='pending'||status='inProgress')")
Integer findpending(Integer id);
@Select("SELECT\n" +
" CASE days.day_of_week_index\n" +
" WHEN 0 THEN '星期一'\n" +
" WHEN 1 THEN '星期二'\n" +
" WHEN 2 THEN '星期三'\n" +
" WHEN 3 THEN '星期四'\n" +
" WHEN 4 THEN '星期五'\n" +
" WHEN 5 THEN '星期六'\n" +
" WHEN 6 THEN '星期日'\n" +
" END AS week,\n" +
" COALESCE(completed_events.count, 0) AS count\n" +
"FROM (\n" +
" SELECT 1 AS day_of_week_index UNION ALL\n" +
" SELECT 2 AS day_of_week_index UNION ALL\n" +
" SELECT 3 AS day_of_week_index UNION ALL\n" +
" SELECT 4 AS day_of_week_index UNION ALL\n" +
" SELECT 5 AS day_of_week_index UNION ALL\n" +
" SELECT 6 AS day_of_week_index UNION ALL\n" +
" SELECT 0 AS day_of_week_index\n" +
" ) AS days\n" +
" LEFT JOIN (\n" +
" SELECT\n" +
" WEEKDAY(start_date) AS day_of_week_index,\n" +
" COUNT(*) AS count\n" +
" FROM\n" +
" events\n" +
" WHERE\n" +
" status = 'completed' AND\n" +
" user_id = #{id} AND\n" +
" start_date BETWEEN DATE_SUB(NOW() - INTERVAL 1 WEEK, INTERVAL WEEKDAY(NOW() - INTERVAL 1 WEEK) + 1 DAY) AND DATE_SUB(NOW() - INTERVAL 1 WEEK, INTERVAL WEEKDAY(NOW() - INTERVAL 1 WEEK) - 6 DAY)\n" +
" GROUP BY\n" +
" WEEKDAY(start_date)\n" +
") AS completed_events ON days.day_of_week_index = completed_events.day_of_week_index\n" +
"ORDER BY\n" +
" days.day_of_week_index;")
List<EventsWeekVO> eventweek(Integer id);
}
xml不放出来了
EventCategories都是同理,后面就不放了
拦截器:
package org.flowerfog.intercept;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.flowerfog.exception.LoginException;
import org.flowerfog.pojo.entity.Users;
import org.flowerfog.utils.ThreadLocalUtil;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* 登录拦截器
*/
public class LoginInterceptors implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 1. 从session获取用户信息
HttpSession session = request.getSession();
Users user = (Users) session.getAttribute("user");
System.out.println("拦截器用户信息:" + user);
// 登录 URL
String loginUrl = request.getContextPath() + "/user/login";
//注册
String registerUrl = request.getContextPath() + "/user/register";
// 如果是登录请求,直接放行
if (request.getRequestURI().equals(loginUrl)||request.getRequestURI().equals(registerUrl)) {
return true;
}
if (user == null) {
System.out.println("用户未登录:"+request.getRequestURI());
throw new LoginException("请登录!!!");
}
// 2. 设置到ThreadLocal
ThreadLocalUtil.set(user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 3. 请求结束后清理
ThreadLocalUtil.remove();
}
}
………………
这里把项目文件目录放在这里作为参考:
五、前端界面(Vue):
登录界面:
<template>
<div class="login-container">
<div class="login-box">
<div class="login-header">
<div class="logo-container">
<svg class="logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<path fill="#1890ff" d="M64 0C28.7 0 0 28.7 0 64s28.7 64 64 64 64-28.7 64-64S99.3 0 64 0zm32 98.7c0 3.5-2.8 6.3-6.3 6.3H38.3c-3.5 0-6.3-2.8-6.3-6.3V29.3c0-3.5 2.8-6.3 6.3-6.3h51.4c3.5 0 6.3 2.8 6.3 6.3v69.4z"/>
<path fill="#fff" d="M45 41h38v6H45zm0 20h38v6H45zm0 20h38v6H45z"/>
</svg>
</div>
<h2>日记月累</h2>
<p class="subtitle">记录生活,规划未来</p>
</div>
<el-form :model="loginFormData" :rules="rules" ref="loginForm">
<el-form-item prop="username">
<el-input v-model="loginFormData.username" placeholder="请输入账户名" size="large" />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginFormData.password" type="password" placeholder="请输入密码" size="large" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin" style="width: 100%" size="large">
登录
</el-button>
</el-form-item>
</el-form>
<div class="register-link">
<router-link to="/register">没有账户?点击注册</router-link>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import axios from 'axios'
import { BASE_URL } from '../config/api'
const router = useRouter()
const loginForm = ref(null)
const loginFormData = reactive({
username: '',
password: ''
})
const rules = {
username: [
{ required: true, message: '请输入账户名', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能小于6位', trigger: 'blur' }
]
}
const handleLogin = () => {
if (!loginForm.value) return
loginForm.value.validate(async (valid) => {
if (valid) {
try {
const response = await axios.post(`${BASE_URL}/user/login`, {
username: loginFormData.username,
password: loginFormData.password
})
if (response.data.code === 0) {
ElMessage.success(response.data.data || '登录成功!')
router.push('/dashboard')
} else {
ElMessage.error(response.data.message || '登录失败')
}
} catch (error) {
console.error('登录错误:', error)
ElMessage.error('登录失败,请检查网络连接或稍后重试')
}
}
})
}
</script>
<style scoped>
.login-container {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #1890ff 0%, #36cfc9 100%);
}
.login-box {
width: 420px;
padding: 40px;
background: rgba(255, 255, 255, 0.9);
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}
.login-header {
text-align: center;
margin-bottom: 40px;
}
h2 {
margin: 0;
font-size: 28px;
color: #1890ff;
margin-bottom: 8px;
}
.subtitle {
margin: 0;
color: #666;
font-size: 16px;
}
:deep(.el-input__wrapper) {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
:deep(.el-button) {
border-radius: 8px;
font-size: 16px;
height: 44px;
}
:deep(.el-form-item) {
margin-bottom: 24px;
}
.register-link {
text-align: center;
margin-top: 20px;
}
.register-link a {
color: #1890ff;
text-decoration: none;
}
.logo-container {
display: flex;
justify-content: center;
margin-bottom: 16px;
}
.logo {
width: 80px;
height: 80px;
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
100% {
transform: translateY(0px);
}
}
</style>
用户注册:
<template>
<div class="register-container">
<div class="register-box">
<div class="register-header">
<h2>用户注册</h2>
<p class="subtitle">创建一个新账户</p>
</div>
<el-form :model="registerFormData" :rules="rules" ref="registerForm">
<el-form-item prop="username">
<el-input v-model="registerFormData.username" placeholder="请输入账户名" size="large" />
</el-form-item>
<el-form-item prop="name">
<el-input v-model="registerFormData.name" placeholder="请输入姓名" size="large" />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="registerFormData.password" type="password" placeholder="请输入密码" size="large" />
</el-form-item>
<el-form-item prop="age">
<el-input v-model="registerFormData.age" placeholder="请输入年龄" size="large" type="number" />
</el-form-item>
<el-form-item prop="phone">
<el-input v-model="registerFormData.phone" placeholder="请输入手机号" size="large" />
</el-form-item>
<el-form-item prop="email">
<el-input v-model="registerFormData.email" placeholder="请输入邮箱" size="large" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleRegister" style="width: 100%" size="large">
注册
</el-button>
</el-form-item>
</el-form>
<div class="login-link">
<router-link to="/login">已有账户?点击登录</router-link>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import axios from 'axios'
import { BASE_URL } from '../config/api'
const router = useRouter()
const registerForm = ref(null)
const registerFormData = reactive({
username: '',
name: '',
password: '',
age: '',
phone: '',
email: ''
})
const rules = {
username: [
{ required: true, message: '请输入账户名', trigger: 'blur' }
],
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能小于6位', trigger: 'blur' }
],
age: [
{ required: true, message: '请输入年龄', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
]
}
const handleRegister = async () => {
if (!registerForm.value) return
registerForm.value.validate(async (valid) => {
if (valid) {
try {
const requestData = {
username: registerFormData.username,
nickname: registerFormData.name,
password: registerFormData.password,
age: registerFormData.age,
email: registerFormData.email,
phone: registerFormData.phone
}
const response = await axios.post(`${BASE_URL}/user/register`, requestData)
if (response.data.code === 0) {
ElMessage.success('注册成功!')
router.push('/login')
} else {
ElMessage.error(response.data.message || '注册失败,请重试')
}
} catch (error) {
console.error('注册错误:', error)
ElMessage.error(error.response?.data?.message || '注册失败,请检查网络连接后重试')
}
}
})
}
</script>
<style scoped>
.register-container {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #1890ff 0%, #36cfc9 100%);
}
.register-box {
width: 420px;
padding: 40px;
background: rgba(255, 255, 255, 0.9);
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}
.register-header {
text-align: center;
margin-bottom: 40px;
}
h2 {
margin: 0;
font-size: 28px;
color: #1890ff;
margin-bottom: 8px;
}
.subtitle {
margin: 0;
color: #666;
font-size: 16px;
}
:deep(.el-input__wrapper) {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
:deep(.el-button) {
border-radius: 8px;
font-size: 16px;
height: 44px;
}
:deep(.el-form-item) {
margin-bottom: 24px;
}
.login-link {
text-align: center;
margin-top: 20px;
}
.login-link a {
color: #1890ff;
text-decoration: none;
}
</style>
个人信息:
<template>
<div class="profile-container">
<el-card class="profile-card">
<template #header>
<div class="card-header">
<span class="header-title">个人信息</span>
<el-button type="primary" @click="enableEdit" v-if="!isEditing">编辑</el-button>
<div v-else class="action-buttons">
<el-button type="success" @click="saveChanges">保存</el-button>
<el-button @click="cancelEdit">取消</el-button>
</div>
</div>
</template>
<div class="profile-content">
<div class="avatar-section">
<el-avatar
:size="120"
:src="userForm.avatarUrl || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'"
/>
<el-upload
v-if="isEditing"
class="avatar-uploader"
:http-request="customUpload"
:show-file-list="false"
:before-upload="beforeAvatarUpload"
>
<el-button size="small" type="primary">更换头像</el-button>
</el-upload>
</div>
<el-form
ref="formRef"
:model="userForm"
:rules="rules"
:disabled="!isEditing"
label-width="100px"
class="profile-form">
<el-form-item label="用户名">
<el-input v-model="userForm.username" disabled />
<span class="form-tip">用户名不可修改</span>
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input v-model="userForm.nickname" placeholder="请输入昵称" />
</el-form-item>
<el-form-item label="密码" prop="password" v-if="isEditing">
<el-input
v-model="userForm.password"
type="password"
placeholder="不修改请留空"
show-password
/>
<span class="form-tip">密码长度至少6位</span>
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input-number
v-model="userForm.age"
:min="1"
:max="120"
controls-position="right"
/>
</el-form-item>
<el-form-item label="手机号码" prop="phone">
<el-input v-model="userForm.phone" placeholder="请输入手机号码" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="userForm.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="个人简介" prop="bio">
<el-input
v-model="userForm.bio"
type="textarea"
:rows="4"
placeholder="介绍一下自己吧"
/>
</el-form-item>
</el-form>
</div>
</el-card>
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue'
import { useStore } from 'vuex'
import { ElMessage } from 'element-plus'
import axios from 'axios'
// import axios from '../config/axios'
import { BASE_URL } from '../config/api'
import eventBus from '../utils/eventBus'
import { useRouter } from 'vue-router'
// 在setup中获取router实例
const router = useRouter()
export default {
name: 'Profile',
setup() {
const store = useStore()
const isEditing = ref(false)
const originalUserData = ref(null)
const formRef = ref(null)
// 表单验证规则
const rules = {
nickname: [
{ required: true, message: '请输入昵称', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到20 个字符', trigger: 'blur' }
],
password: [
{ min: 6, message: '密码长度至少6位', trigger: 'blur' }
],
phone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
email: [
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
],
age: [
{ type: 'number', message: '年龄必须为数字', trigger: 'blur' },
{ type: 'number', min: 1, max: 120, message: '年龄必须在1-120之间', trigger: 'blur' }
]
}
// 用户表单数据
const userForm = reactive({
username: '',
nickname: '',
password: '',
age: null,
email: '',
phone: '',
bio: '',
avatar: '',
avatarUrl: ''
})
// 设置axios认允跨域请求时发送凭证
// axios.defaults.withCredentials = true;
// 获取用户信息
const fetchUserData = async () => {
try {
const response = await axios.get(`${BASE_URL}/user/findbyid`)
// const response = await axios.get('/user/findbyid')
console.log('用户信息:', response.data)
if (response.data.code === 0) {
Object.assign(userForm, response.data.data)
// 如果有头像,获取图片数据
if (userForm.avatar) {
try {
const imageResponse = await axios.get(`${BASE_URL}/file/download/${userForm.avatar}`, {
// const imageResponse = await axios.get('/file/download/${userForm.avatar}', {
responseType: 'arraybuffer' // 重要:设置响应类型为 arraybuffer
})
const base64 = btoa(
new Uint8Array(imageResponse.data)
.reduce((data, byte) => data + String.fromCharCode(byte), '')
)
// 设置为 base64 格式的图片
userForm.avatarUrl = `data:image/jpeg;base64,${base64}`
} catch (error) {
console.error('获取头像失败:', error)
}
}
} else {
ElMessage.error(response.data.message)
// 添加延时,让错误消息显示后再跳转
setTimeout(() => {
router.push('/login')
}, 1500)
}
} catch (error) {
console.error('获取用户信息错误:', error)
ElMessage.error('获取用户信息失败,请稍后重试')
}
}
// 开启编辑模式
const enableEdit = () => {
isEditing.value = true
originalUserData.value = JSON.parse(JSON.stringify(userForm))
userForm.password = '' // 清空密码字段
}
// 保存更改
const saveChanges = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
const submitData = { ...userForm }
if (!submitData.password) {
delete submitData.password
}
const response = await axios.post(`${BASE_URL}/user/update`, submitData)
if (response.data.code === 0) {
isEditing.value = false
eventBus.emit('userInfoUpdated') // 发送更新事件
ElMessage.success('保存成功')
} else {
ElMessage.error(response.data.message)
}
} catch (error) {
ElMessage.error('表单验证失败,请检查输入')
}
}
// 取消编辑
const cancelEdit = () => {
isEditing.value = false
Object.assign(userForm, originalUserData.value)
formRef.value?.clearValidate()
}
// 头像上传
const handleAvatarSuccess = async (response) => {
console.log('上传响应:', response)
if (response.code === 0) {
userForm.avatar = response.data
try {
const imageResponse = await axios.get(`${BASE_URL}/file/download/${response.data}`, {
responseType: 'arraybuffer'
})
const base64 = btoa(
new Uint8Array(imageResponse.data)
.reduce((data, byte) => data + String.fromCharCode(byte), '')
)
userForm.avatarUrl = `data:image/jpeg;base64,${base64}`
// 更新 store 中的用户信息
store.commit('UPDATE_USER', {
...store.state.user,
name: userForm.nickname, // Layout 中使用 name 显示用户名
avatar: userForm.avatarUrl // Layout 中直接使用 avatar 作为头像 URL
})
eventBus.emit('userInfoUpdated') // 发送更新事件
ElMessage.success('头像上传成功')
} catch (error) {
console.error('获取新头像失败:', error)
ElMessage.error('头像上传成功但显示失败')
}
} else {
ElMessage.error(response.message || '头像上传失败')
}
}
// 头像上传前的验证
const beforeAvatarUpload = (file) => {
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png';
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isJPG) {
ElMessage.error('头像只能是 JPG 或 PNG 格式!');
}
if (!isLt2M) {
ElMessage.error('头像大小不能超过 2MB!');
}
return isJPG && isLt2M;
}
// 添加自定义上传方法
const customUpload = async (options) => {
try {
const formData = new FormData()
formData.append('imgfile', options.file)
const response = await axios.post(`${BASE_URL}/file/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
withCredentials: true // 确保发送 cookies
})
// 调用原来的成功处理函数
handleAvatarSuccess(response.data)
} catch (error) {
console.error('上传失败:', error)
ElMessage.error('上��失败,请重试')
}
}
// 在组件挂载时获取用户数据
onMounted(() => {
fetchUserData()
})
return {
isEditing,
userForm,
formRef,
rules,
enableEdit,
saveChanges,
cancelEdit,
beforeAvatarUpload,
handleAvatarSuccess,
customUpload
}
}
}
</script>
<style scoped>
.profile-container {
padding: 20px;
background-color: #f5f7fa;
min-height: 100%;
}
.profile-card {
max-width: 800px;
margin: 0 auto;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
font-size: 18px;
font-weight: 600;
color: #303133;
}
.action-buttons {
display: flex;
gap: 12px;
}
.profile-content {
display: flex;
gap: 40px;
padding: 20px 0;
}
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.profile-form {
flex: 1;
}
.avatar-uploader {
margin-top: 10px;
}
.form-tip {
margin-left: 8px;
font-size: 12px;
color: #909399;
}
:deep(.el-input.is-disabled .el-input__wrapper) {
background-color: #f5f7fa;
}
:deep(.el-form-item) {
margin-bottom: 22px;
}
:deep(.el-input__wrapper),
:deep(.el-textarea__inner) {
box-shadow: 0 0 0 1px #dcdfe6 inset;
}
:deep(.el-input__wrapper:hover),
:deep(.el-textarea__inner:hover) {
box-shadow: 0 0 0 1px #c0c4cc inset;
}
:deep(.el-input__wrapper.is-focus),
:deep(.el-textarea__inner:focus) {
box-shadow: 0 0 0 1px #409eff inset;
}
:deep(.el-form-item__label) {
font-weight: 500;
}
</style>
首页:
<template>
<div class="home-container">
<!-- 数据概览卡片 -->
<el-row :gutter="20">
<el-col :span="6" v-for="card in dataCards" :key="card.title">
<el-card class="data-card" shadow="hover">
<div class="data-header">
<div class="data-title">
<el-icon class="icon"><component :is="card.icon" /></el-icon>
<span>{{ card.title }}</span>
</div>
<div class="data-value">{{ card.value }}</div>
</div>
<div class="data-footer">
<span>{{ card.footerLabel }}</span>
<span :class="card.trend">{{ card.footerValue }}</span>
</div>
</el-card>
</el-col>
</el-row>
<!-- 图表区域 -->
<el-row :gutter="20" class="charts-container">
<el-col :span="12">
<el-card class="chart-card" shadow="hover">
<template #header>
<div class="chart-header">
<span>事件分类统计</span>
</div>
</template>
<div class="pie-chart chart"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card class="chart-card" shadow="hover">
<template #header>
<div class="chart-header">
<span>上一周完成事件趋势</span>
</div>
</template>
<div class="line-chart chart"></div>
</el-card>
</el-col>
</el-row>
<!-- 最近事件列表 -->
<el-card class="recent-events" shadow="hover">
<template #header>
<div class="recent-header">
<span>最近事件</span>
<el-button type="primary" link @click="$router.push('/dashboard/event-management')">
查看更多<el-icon><ArrowRight /></el-icon>
</el-button>
</div>
</template>
<el-table
:data="recentEvents"
style="width: 100%"
:row-class-name="tableRowClassName">
<el-table-column prop="title" label="事件标题">
<template #default="scope">
<div class="event-title">{{ scope.row.title }}</div>
</template>
</el-table-column>
<el-table-column prop="category" label="分类" width="120">
<template #default="scope">
<el-tag size="small" effect="plain">{{ scope.row.category }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="120">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)" size="small">
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="结束时间" width="180" />
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
import { Calendar, Check, Warning, Folder, ArrowRight } from '@element-plus/icons-vue'
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { BASE_URL } from '../config/api'
import { useRouter } from 'vue-router'
const router = useRouter()
// 数据卡片
const dataCards = ref([
{
title: '总事件数',
value: 0,
icon: 'Calendar',
footerLabel: '总计',
footerValue: '0',
trend: 'up'
},
{
title: '已完成事件',
value: 0,
icon: 'Check',
footerLabel: '完成率',
footerValue: '0%',
trend: 'up'
},
{
title: '待处理事件',
value: 0,
icon: 'Warning',
footerLabel: '待处理',
footerValue: '0',
trend: 'warning'
},
{
title: '事件分类',
value: 0,
icon: 'Folder',
footerLabel: '分类总数',
footerValue: '0',
trend: 'up'
}
])
// 最近事件
const recentEvents = ref([])
// 图表实例
let pieChartInstance = null
let lineChartInstance = null
// 获取首页数据
const fetchHomeData = async () => {
try {
const response = await axios.get(`${BASE_URL}/events/findhome`)
if (response.data.code === 0) {
const data = response.data.data
// 更新数据卡片
dataCards.value[0].value = data.totalEvents
dataCards.value[0].footerValue = `${data.totalEvents}`
dataCards.value[1].value = data.completedEvents
dataCards.value[1].footerValue = data.totalEvents > 0
? `${Math.round((data.completedEvents / data.totalEvents) * 100)}%`
: '0%'
dataCards.value[2].value = data.pendingEvents
dataCards.value[2].footerValue = `${data.pendingEvents}`
dataCards.value[3].value = data.eventstotal
dataCards.value[3].footerValue = `${data.eventstotal}`
// 更新饼图数据
if (pieChartInstance) {
pieChartInstance.setOption({
series: [{
data: data.categoryStats.map(item => ({
value: item.totalCount,
name: item.name
}))
}]
})
}
// 更新折线图数据
if (lineChartInstance) {
lineChartInstance.setOption({
xAxis: {
data: data.eventweek.map(item => item.week)
},
series: [{
data: data.eventweek.map(item => item.count)
}]
})
}
}
} catch (error) {
console.error('获取首页数据失败:', error)
}
}
// 修改获取最近事件的函数
const fetchRecentEvents = async () => {
try {
const response = await axios.get(`${BASE_URL}/events/findfive`)
if (response.data.code === 0) {
recentEvents.value = response.data.data.map(event => ({
title: event.title,
category: event.name,
status: getStatusText(event.status),
createTime: new Date(event.endDate).toLocaleString()
}))
} else {
ElMessage.error(response.data.message || '获取数据失败')
// 添加延时,让错误消息显示后再跳转
setTimeout(() => {
router.push('/login')
}, 1500)
}
} catch (error) {
console.error('获取最近事件失败:', error)
ElMessage.error('获取数据失败')
setTimeout(() => {
router.push('/login')
}, 1500)
}
}
// 添加状态转换函数(与事件管理相同的状态转换函数)
const getStatusText = (status) => {
const texts = {
pending: '待开始',
inProgress: '进行中',
completed: '已完成',
cancelled: '已取消',
delayed: '已延期'
}
return texts[status] || '未知'
}
// 获取状态类型(用于标签颜色)
const getStatusType = (status) => {
const types = {
'待开始': 'info',
'进行中': 'warning',
'已完成': 'success',
'已取消': 'danger',
'已延期': 'warning'
}
return types[status] || 'info'
}
// 初始化图表
onMounted(() => {
// 初始化饼图
const pieChart = document.querySelector('.pie-chart')
if (pieChart) {
pieChartInstance = echarts.init(pieChart)
pieChartInstance.setOption({
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [{
type: 'pie',
radius: '50%',
data: [],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
})
}
// 初始化折线图
const lineChart = document.querySelector('.line-chart')
if (lineChart) {
lineChartInstance = echarts.init(lineChart)
lineChartInstance.setOption({
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: []
},
yAxis: {
type: 'value'
},
series: [{
data: [],
type: 'line',
smooth: true,
areaStyle: {
opacity: 0.3
}
}]
})
}
// 获取数据
fetchHomeData()
fetchRecentEvents()
// 监听窗口大小变化
window.addEventListener('resize', () => {
pieChartInstance?.resize()
lineChartInstance?.resize()
})
})
const tableRowClassName = ({ rowIndex }) => {
return 'table-row-' + rowIndex
}
</script>
<style scoped>
.home-container {
padding: 20px;
}
.data-card {
height: 120px;
margin-bottom: 20px;
}
.data-header {
display: flex;
flex-direction: column;
gap: 10px;
}
.data-title {
display: flex;
align-items: center;
gap: 8px;
color: #666;
}
.icon {
font-size: 20px;
}
.data-value {
font-size: 24px;
font-weight: bold;
color: #303133;
}
.data-footer {
margin-top: 10px;
display: flex;
justify-content: space-between;
color: #909399;
font-size: 14px;
}
.up {
color: #67c23a;
}
.down {
color: #f56c6c;
}
.warning {
color: #e6a23c;
}
.charts-container {
margin-top: 20px;
}
.chart-card {
margin-bottom: 20px;
}
.chart {
height: 300px;
}
.chart-header {
font-size: 16px;
font-weight: 500;
}
.recent-header {
display: flex;
justify-content: space-between;
align-items: center;
}
:deep(.el-card__header) {
padding: 15px 20px;
border-bottom: 1px solid #ebeef5;
}
.el-row {
margin-bottom: 20px;
}
.recent-events {
transition: all 0.3s;
}
.recent-events:hover {
transform: translateY(-5px);
}
:deep(.el-table) {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
:deep(.el-table__header) {
background-color: #f5f7fa;
}
:deep(.el-table__row) {
transition: all 0.3s;
}
:deep(.el-table__row:hover) {
transform: translateZ(20px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.event-title {
font-weight: 500;
color: #303133;
}
:deep(.table-row-0) {
background-color: rgba(24, 144, 255, 0.05);
}
:deep(.table-row-1) {
background-color: rgba(54, 207, 201, 0.05);
}
:deep(.el-card) {
border-radius: 12px;
overflow: hidden;
}
.recent-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
}
:deep(.el-card__header) {
border-bottom: 1px solid #f0f0f0;
padding: 0 20px;
}
</style>
事件分类:
<template>
<div class="category-container">
<div class="category-header">
<h3>事件分类管理</h3>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增分类
</el-button>
</div>
<el-table :data="categories" style="width: 100%" border stripe>
<el-table-column label="序号" width="80" align="center">
<template #default="scope">
{{ scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="name" label="分类名称" />
<el-table-column prop="count" label="事件数量" width="100" align="center" />
<el-table-column prop="createTime" label="创建时间" width="180" align="center" />
<el-table-column label="操作" width="200" align="center">
<template #default="scope">
<div class="operation-buttons">
<el-button type="primary" size="small" @click="handleEdit(scope.row)">
<el-icon><Edit /></el-icon>
<span>编辑</span>
</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.row)">
<el-icon><Delete /></el-icon>
<span>删除</span>
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 新增/编辑对话框 -->
<el-dialog
:title="dialogType === 'add' ? '新增分类' : '编辑分类'"
v-model="dialogVisible"
width="30%">
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
<el-form-item label="分类名称" prop="name">
<el-input v-model="form.name" placeholder="请输入分类名称"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Edit, Delete } from '@element-plus/icons-vue'
import axios from 'axios'
import { BASE_URL } from '../config/api'
import { useRouter } from 'vue-router'
// 在setup中获取router实例
const router = useRouter()
// 分类数据
const categories = ref([])
// 获取所有分类
const fetchCategories = async () => {
try {
const response = await axios.get(`${BASE_URL}/categor/findall`)
if (response.data.code === 0) {
// 处理时间格式
categories.value = response.data.data.map(item => ({
...item,
createTime: new Date(item.createdAt).toLocaleString(),
}))
} else {
ElMessage.error(response.data.message || '获取分类失败')
// 添加延时,让错误消息显示后再跳转
setTimeout(() => {
router.push('/login')
}, 1500)
}
} catch (error) {
console.error('获取分类失败:', error)
ElMessage.error('获取分类失败,请检查网络连接')
}
}
const dialogVisible = ref(false)
const dialogType = ref('add')
const formRef = ref(null)
const currentId = ref(null)
const form = reactive({
name: ''
})
const rules = {
name: [
{ required: true, message: '请输入分类名称', trigger: 'blur' },
{ min: 2, max: 10, message: '长度在 2 到 10 个字符', trigger: 'blur' }
]
}
// 新增分类
const handleAdd = () => {
dialogType.value = 'add'
form.name = ''
dialogVisible.value = true
}
// 编辑分类
const handleEdit = (row) => {
dialogType.value = 'edit'
currentId.value = row.id
form.name = row.name
dialogVisible.value = true
}
// 删除分类
const handleDelete = (row) => {
ElMessageBox.confirm(
'此操作将永久删除该分类,是否继续?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
try {
const response = await axios.delete(`${BASE_URL}/categor/delete?id=${row.id}`)
if (response.data.code === 0) {
ElMessage.success('删除成功')
fetchCategories() // 重新获取列表
} else {
ElMessage.error(response.data.message || '删除失败')
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error('删除失败,请检查网络连接')
}
})
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (dialogType.value === 'add') {
// 新增分类
const response = await axios.post(`${BASE_URL}/categor/add`, {
name: form.name
})
if (response.data.code === 0) {
ElMessage.success('新增成功')
dialogVisible.value = false
fetchCategories() // 重新获取列表
} else {
ElMessage.error(response.data.message || '新增失败')
}
} else {
// 修改分类
const response = await axios.post(`${BASE_URL}/categor/update`, {
id: currentId.value,
name: form.name
})
if (response.data.code === 0) {
ElMessage.success('编辑成功')
dialogVisible.value = false
fetchCategories() // 重新获取列表
} else {
ElMessage.error(response.data.message || '编辑失败')
}
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error('操作失败,请检查网络连接')
}
}
// 在组件挂载时获取分类列表
onMounted(() => {
fetchCategories()
})
</script>
<style scoped>
.category-container {
padding: 20px;
background-color: #fff;
border-radius: 4px;
}
.category-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.category-header h3 {
margin: 0;
font-size: 20px;
font-weight: 500;
}
.el-button {
display: flex;
align-items: center;
gap: 5px;
}
.operation-buttons {
display: flex;
justify-content: center;
gap: 8px;
}
.el-button {
display: inline-flex;
align-items: center;
gap: 4px;
}
:deep(.el-button .el-icon) {
margin: 0;
}
</style>
事件管理:
<template>
<div class="event-management">
<div class="header">
<h2>事件管理</h2>
<el-button type="primary" @click="handleAdd">新增事件</el-button>
</div>
<!-- 搜索和筛选区域 -->
<div class="search-bar">
<el-input
v-model="searchQuery"
placeholder="搜索事件标题或描述"
class="search-input"
clearable
@input="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-select
v-model="filterPriority"
placeholder="优先级筛选"
clearable
@change="handleFilter"
>
<el-option
v-for="item in priorities"
:key="item.value"
:label="item.label"
:value="item.value"
>
<el-tag
:type="getPriorityType(item.value)"
effect="dark"
size="small"
>
{{ item.label }}
</el-tag>
</el-option>
</el-select>
<el-select
v-model="filterStatus"
placeholder="状态筛选"
clearable
@change="handleFilter"
>
<el-option
v-for="item in statusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
>
<el-tag
:type="getStatusType(item.value)"
size="small"
>
{{ item.label }}
</el-tag>
</el-option>
</el-select>
<div class="sort-buttons">
<el-tooltip content="按结束时间排序" placement="top">
<el-button
:type="sortBy === 'time' ? 'primary' : 'default'"
@click="handleSort('time')"
>
<el-icon><Timer /></el-icon>
结束时间
<el-icon v-if="sortBy === 'time'">
<component :is="sortOrder === 'asc' ? 'ArrowUp' : 'ArrowDown'" />
</el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="按优先级排序" placement="top">
<el-button
:type="sortBy === 'priority' ? 'primary' : 'default'"
@click="handleSort('priority')"
>
<el-icon><Sort /></el-icon>
优先级
<el-icon v-if="sortBy === 'priority'">
<component :is="sortOrder === 'asc' ? 'ArrowUp' : 'ArrowDown'" />
</el-icon>
</el-button>
</el-tooltip>
</div>
</div>
<el-table :data="paginatedEvents" style="width: 100%" border>
<el-table-column prop="title" label="事件标题" />
<el-table-column prop="categoryName" label="所属分类" />
<el-table-column prop="priority" label="优先级" width="100">
<template #default="{ row }">
<el-tag
:type="getPriorityType(row.priority)"
effect="dark"
size="small"
>
{{ getPriorityText(row.priority) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="起止时间" width="340">
<template #default="{ row }">
<div class="date-range">
<span class="date-label">开始:</span>
<span class="date-value">{{ formatDate(row.startDate) }}</span>
<el-divider direction="vertical" />
<span class="date-label">结束:</span>
<span class="date-value">{{ formatDate(row.endDate) }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="120">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="280">
<template #default="{ row }">
<el-button
type="primary"
size="small"
:disabled="row.status !== 'pending'"
@click="handleStart(row)"
>
开始
</el-button>
<el-button
type="success"
size="small"
:disabled="row.status !== 'inProgress'"
@click="handleComplete(row)"
>
完成
</el-button>
<el-button
type="warning"
size="small"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 新增/编辑话框 -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="500px"
>
<el-form
ref="formRef"
:model="eventForm"
:rules="rules"
label-width="100px"
>
<el-form-item label="事件标题" prop="title">
<el-input v-model="eventForm.title" placeholder="请输入事件标题" />
</el-form-item>
<el-form-item label="所属分类" prop="category">
<el-select
v-model="eventForm.category"
placeholder="请选择分类"
:loading="!categories.length"
>
<el-option
v-for="item in categories"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="事件描述" prop="description">
<el-input
v-model="eventForm.description"
type="textarea"
:rows="4"
placeholder="请输入事件描述"
/>
</el-form-item>
<el-form-item label="优先级" prop="priority">
<el-select v-model="eventForm.priority" placeholder="请选择优先级">
<el-option
v-for="item in priorities"
:key="item.value"
:label="item.label"
:value="item.value"
>
<template #default="{ label }">
<el-tag
:type="getPriorityType(item.value)"
effect="dark"
size="small"
style="margin-right: 8px"
>
{{ label }}
</el-tag>
{{ label }}
</template>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="起止时间" prop="dateRange" required>
<el-date-picker
v-model="eventForm.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:shortcuts="dateShortcuts"
value-format="YYYY-MM-DD HH:mm:ss"
:default-time="defaultTime"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 删除确认框 -->
<el-dialog
v-model="deleteDialogVisible"
title="确认删除"
width="300px"
>
<p>确定要删除事件吗?此操作不可恢复。</p>
<template #footer>
<span class="dialog-footer">
<el-button @click="deleteDialogVisible = false">取消</el-button>
<el-button type="danger" @click="confirmDelete">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 添加分页器 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[7, 14, 21, 28]"
:total="filteredAndSortedEvents.length"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script>
import { ref, reactive, computed, watch, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Search, Timer, Sort, ArrowUp, ArrowDown } from '@element-plus/icons-vue'
import { formatDate } from '../utils/date'
import axios from 'axios'
import { BASE_URL } from '../config/api'
import { useRouter } from 'vue-router'
// 在setup中获取router实例
const router = useRouter()
export default {
name: 'EventManagement',
components: {
Search,
Timer,
Sort,
ArrowUp,
ArrowDown
},
setup() {
// 事件列表数据 - 添加模拟数据
const eventList = ref([
{
id: 1,
title: '完成项目文档',
category: 'work',
description: '编写项目需求文档和技术方案说明',
status: 'completed',
priority: 'high',
startDate: '2024-03-15 09:00:00',
endDate: '2024-03-16 18:00:00',
createTime: '2024-03-15 09:00:00'
},
{
id: 2,
title: '学习Vue3新特性',
category: 'study',
description: '学习Vue3的Composition API和新的响应式系统',
status: 'inProgress',
priority: 'medium',
startDate: '2024-03-16 14:30:00',
endDate: '2024-03-17 14:30:00',
createTime: '2024-03-16 14:30:00'
},
{
id: 3,
title: '每日健身计划',
category: 'life',
description: '进行30分钟有氧运动和力量训练',
status: 'pending',
priority: 'low',
startDate: '2024-03-17 08:00:00',
endDate: '2024-03-17 08:30:00',
createTime: '2024-03-17 08:00:00'
},
{
id: 4,
title: '团队周会',
category: 'work',
description: '讨论本周工作进展和下周计划',
status: 'pending',
priority: 'low',
startDate: '2024-03-17 10:00:00',
endDate: '2024-03-17 12:00:00',
createTime: '2024-03-17 10:00:00'
},
{
id: 5,
title: '阅读技术书籍',
category: 'study',
description: '阅读《深入浅出Vue.js第三章节',
status: 'inProgress',
priority: 'medium',
startDate: '2024-03-17 15:30:00',
endDate: '2024-03-17 17:30:00',
createTime: '2024-03-17 15:30:00'
},
{
id: 6,
title: '整理房间',
category: 'life',
description: '打扫卫生,整理衣物和书籍',
status: 'completed',
priority: 'high',
startDate: '2024-03-16 16:00:00',
endDate: '2024-03-16 18:00:00',
createTime: '2024-03-16 16:00:00'
},
{
id: 7,
title: '代码评审',
category: 'work',
description: '评审团队成员提交的代码,提供修改建议',
status: 'pending',
priority: 'low',
startDate: '2024-03-17 11:30:00',
endDate: '2024-03-17 13:30:00',
createTime: '2024-03-17 11:30:00'
},
{
id: 8,
title: '准备晚餐',
category: 'life',
description: '购买食材并准备健康的晚餐',
status: 'pending',
priority: 'low',
startDate: '2024-03-17 17:00:00',
endDate: '2024-03-17 19:00:00',
createTime: '2024-03-17 17:00:00'
}
])
// 表单相关
const dialogVisible = ref(false)
const deleteDialogVisible = ref(false)
const dialogTitle = ref('新增事件')
const formRef = ref(null)
const currentEvent = ref(null)
// 表单数据
const eventForm = reactive({
title: '',
category: '',
description: '',
priority: 'medium',
dateRange: null
})
// 表单验证规则
const rules = {
title: [
{ required: true, message: '请输入事件标题', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
category: [
{ required: true, message: '请选择所属分类', trigger: 'change' }
],
priority: [
{ required: true, message: '请选择优先级', trigger: 'change' }
],
dateRange: [
{
type: 'array',
required: true,
message: '请选择起止时间',
trigger: 'change'
}
]
}
// 分类选项 - 添加更多分类
const categories = ref([])
// 优先级选项
const priorities = [
// { value: 'high', label: '高' },
// { value: 'medium', label: '中' },
// { value: 'low', label: '低' }
{ value: 'high', label: '高' },
{ value: 'medium', label: '中' },
{ value: 'low', label: '低' }
]
// 获取状态类型 - 添加更多状态样式
const getStatusType = (status) => {
const types = {
pending: 'info',
inProgress: 'warning',
completed: 'success',
cancelled: 'danger', // 预留状态
delayed: 'warning' // 预留状态
}
return types[status] || 'info'
}
// 获取状态文本 - 添加更多状态描述
const getStatusText = (status) => {
const texts = {
pending: '待开始',
inProgress: '进行中',
completed: '已完成',
cancelled: '已取消', // 预留状态
delayed: '已延期' // 预留状态
}
return texts[status] || '未知'
}
// 获取优先级类型
const getPriorityType = (priority) => {
const types = {
high: 'danger',
medium: 'warning',
low: 'info'
}
return types[priority] || 'info'
}
// 获取优先级文本
const getPriorityText = (priority) => {
const texts = {
high: '高',
medium: '中',
low: '低'
}
return texts[priority] || '未知'
}
// 新增事件
const handleAdd = async () => {
dialogTitle.value = '新增事件'
eventForm.title = ''
eventForm.category = ''
eventForm.description = ''
eventForm.priority = 'medium'
eventForm.dateRange = null
await fetchCategories() // 刷新分类列表
dialogVisible.value = true
currentEvent.value = null
}
// 编辑事件
const handleEdit = async (row) => {
dialogTitle.value = '编辑事件'
await fetchCategories() // 刷新分类列表
eventForm.title = row.title
eventForm.category = row.category
eventForm.description = row.description
eventForm.priority = row.priority
eventForm.dateRange = [row.startDate, row.endDate]
dialogVisible.value = true
currentEvent.value = row
}
// 开始事件
const handleStart = async (row) => {
try {
const response = await axios.post(`${BASE_URL}/events/update`, {
id: row.id,
categoryId: parseInt(row.category),
title: row.title,
description: row.description,
priority: row.priority,
status: 'inProgress',
startDate: new Date(row.startDate).getTime(),
endDate: new Date(row.endDate).getTime()
})
if (response.data.code === 0) {
ElMessage.success('事件已开始')
fetchEvents() // 重新获取列表
} else {
ElMessage.error(response.data.message || '操作失败')
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error('操作失败,请检查网络连接')
}
}
// 完成事件
const handleComplete = async (row) => {
try {
const response = await axios.post(`${BASE_URL}/events/update`, {
id: row.id,
categoryId: parseInt(row.category),
title: row.title,
description: row.description,
priority: row.priority,
status: 'completed',
startDate: new Date(row.startDate).getTime(),
endDate: new Date(row.endDate).getTime()
})
if (response.data.code === 0) {
ElMessage.success('事件已完成')
fetchEvents() // 重新获取列表
} else {
ElMessage.error(response.data.message || '操作失败')
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error('操作失败,请检查网络连接')
}
}
// 删除事件
const handleDelete = (row) => {
currentEvent.value = row
deleteDialogVisible.value = true
}
// 确认删除
const confirmDelete = async () => {
try {
const response = await axios.delete(`${BASE_URL}/events/delete?id=${currentEvent.value.id}`)
if (response.data.code === 0) {
deleteDialogVisible.value = false
ElMessage.success('删除成功')
fetchEvents() // 重新获取列表
} else {
ElMessage.error(response.data.message || '删除失败')
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error('删除失败,请检查网络连接')
}
}
// 修改提交表单逻辑
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
try {
const formData = {
categoryId: parseInt(eventForm.category),
title: eventForm.title,
description: eventForm.description,
priority: eventForm.priority,
startDate: new Date(eventForm.dateRange[0]).getTime(),
endDate: new Date(eventForm.dateRange[1]).getTime()
}
if (currentEvent.value) {
// 编辑模式
const response = await axios.post(`${BASE_URL}/events/update`, {
...formData,
id: currentEvent.value.id,
status: currentEvent.value.status
})
if (response.data.code === 0) {
dialogVisible.value = false
ElMessage.success('编辑成功')
fetchEvents() // 重新获取列表
} else {
ElMessage.error(response.data.message || '编辑失败')
}
} else {
// 新增模式
const response = await axios.post(`${BASE_URL}/events/add`, formData)
if (response.data.code === 0) {
dialogVisible.value = false
ElMessage.success('添加成功')
fetchEvents() // 重新获取列表
} else {
ElMessage.error(response.data.message || '添加失败')
}
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error('操作失败,请检查网络连接')
}
}
})
}
// 搜索和筛选状态
const searchQuery = ref('')
const filterPriority = ref('')
const filterStatus = ref('')
const sortBy = ref('time') // 默认按时间排序
const sortOrder = ref('desc') // 默认降序
// 状态选项
const statusOptions = [
{ value: 'pending', label: '待开始' },
{ value: 'inProgress', label: '进行中' },
{ value: 'completed', label: '已完成' }
]
// 优先级权重映射
const priorityWeight = {
high: 3,
medium: 2,
low: 1
}
// 过滤和排序后的事件列表
const filteredAndSortedEvents = computed(() => {
let result = [...eventList.value]
// 搜索过滤
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
result = result.filter(event =>
event.title.toLowerCase().includes(query) ||
event.description.toLowerCase().includes(query)
)
}
// 优先级过滤
if (filterPriority.value) {
result = result.filter(event => event.priority === filterPriority.value)
}
// 状态过滤
if (filterStatus.value) {
result = result.filter(event => event.status === filterStatus.value)
}
// 排序
result.sort((a, b) => {
if (sortBy.value === 'time') {
const timeA = new Date(a.endDate).getTime() // 使用结束时间
const timeB = new Date(b.endDate).getTime() // 使用结束时间
return sortOrder.value === 'asc' ? timeA - timeB : timeB - timeA
} else if (sortBy.value === 'priority') {
const weightA = priorityWeight[a.priority]
const weightB = priorityWeight[b.priority]
return sortOrder.value === 'asc' ? weightA - weightB : weightB - weightA
}
return 0
})
return result
})
// 处理搜索
const handleSearch = () => {
// 搜索是实时的,不需要额外处理
}
// 处理筛选
const handleFilter = () => {
// 筛选是实时的,不需要额外处理
}
// 处理排序
const handleSort = (type) => {
if (sortBy.value === type) {
// 如果点击的是当前排序字段,则切换排序顺序
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
} else {
// 如果点击的是新的排序字段,则设置为该字段降序
sortBy.value = type
sortOrder.value = 'desc'
}
}
// 分页相关
const currentPage = ref(1)
const pageSize = ref(7)
// 分页后的数据
const paginatedEvents = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredAndSortedEvents.value.slice(start, end)
})
// 处理每页显示数量变化
const handleSizeChange = (val) => {
pageSize.value = val
// 当每页数量变化时,可能需要调整当前页码
if (currentPage.value * val > filteredAndSortedEvents.value.length) {
currentPage.value = Math.ceil(filteredAndSortedEvents.value.length / val)
}
}
// 处理页码变化
const handleCurrentChange = (val) => {
currentPage.value = val
}
// 监听筛选条件变化,重置页码到第一页
watch([searchQuery, filterPriority, filterStatus], () => {
currentPage.value = 1
})
// 日期快捷选项
const dateShortcuts = [
{
text: '最近一周',
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
return [start, end]
}
},
{
text: '最近一月',
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
return [start, end]
}
}
]
// 默认时间
const defaultTime = [
new Date(2000, 1, 1, 0, 0, 0),
new Date(2000, 1, 1, 23, 59, 59)
]
// 修改获取事件列表的函数
const fetchEvents = async () => {
try {
const response = await axios.get(`${BASE_URL}/events/findall`)
if (response.data.code === 0) {
// 处理后端返回的数据,转换成前端需要的格式
eventList.value = response.data.data.map(event => ({
id: event.id,
title: event.title,
category: event.categoryId.toString(), // 保留 categoryId 用于表单编辑
categoryName: getCategoryName(event.categoryId), // 添加 categoryName 用于显示
description: event.description,
status: event.status,
priority: event.priority,
startDate: formatDate(event.startDate),
endDate: formatDate(event.endDate),
createTime: formatDate(event.createdAt),
updateTime: formatDate(event.updatedAt)
}))
} else {
ElMessage.error(response.data.message || '获取事件列表失败')
// 添加延时,让错误消息显示后再跳转
setTimeout(() => {
router.push('/login')
}, 1500)
}
} catch (error) {
console.error('获取事件列表失败:', error)
ElMessage.error('获取事件列表失败,请检查网络连接')
}
}
// 添获取分类名称的函数
const getCategoryName = (categoryId) => {
const category = categories.value.find(c => c.value === categoryId.toString())
return category ? category.label : '未知分类'
}
// 添加获取分类列表的函数
const fetchCategories = async () => {
try {
const response = await axios.get(`${BASE_URL}/categor/findall`)
if (response.data.code === 0) {
// 将后端返回的分类数据转换为选项格式
categories.value = response.data.data.map(category => ({
value: category.id.toString(),
label: category.name
}))
}
} catch (error) {
console.error('获取分类列表失败:', error)
ElMessage.error('获取分类列表失败,请检查网络连接')
}
}
// 在组件挂载时获取事件列表
onMounted(() => {
fetchCategories() // 获取分类列表
fetchEvents() // 获取事件列表
})
return {
eventList,
dialogVisible,
deleteDialogVisible,
dialogTitle,
formRef,
eventForm,
rules,
categories,
handleAdd,
handleEdit,
handleStart,
handleComplete,
handleDelete,
confirmDelete,
submitForm,
getStatusType,
getStatusText,
priorities,
getPriorityType,
getPriorityText,
searchQuery,
filterPriority,
filterStatus,
sortBy,
sortOrder,
statusOptions,
filteredAndSortedEvents,
handleSearch,
handleFilter,
handleSort,
currentPage,
pageSize,
paginatedEvents,
handleSizeChange,
handleCurrentChange,
dateShortcuts,
defaultTime,
formatDate
}
}
}
</script>
<style scoped>
.event-management {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header h2 {
margin: 0;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
:deep(.el-button) {
margin-left: 8px;
}
:deep(.el-tag) {
min-width: 60px;
text-align: center;
}
:deep(.el-select-dropdown__item) {
display: flex;
align-items: center;
padding: 5px 12px;
height: 32px;
}
:deep(.el-tag) {
min-width: 45px;
text-align: center;
font-size: 12px;
padding: 0 8px;
height: 22px;
line-height: 20px;
}
:deep(.el-select) {
width: 120px;
}
.search-bar {
margin-bottom: 20px;
display: flex;
gap: 16px;
align-items: center;
}
.search-input {
width: 250px;
}
.sort-buttons {
display: flex;
gap: 8px;
}
:deep(.el-button .el-icon) {
margin-right: 4px;
}
:deep(.el-button .el-icon:last-child) {
margin-left: 4px;
margin-right: 0;
}
:deep(.el-select) {
width: 120px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
/* 优化分页器样式 */
:deep(.el-pagination) {
padding: 0;
margin: 0;
}
:deep(.el-pagination .el-select .el-input) {
width: 110px;
}
.date-range {
display: flex;
align-items: center;
gap: 8px;
}
.date-label {
color: #909399;
font-size: 14px;
}
.date-value {
color: #606266;
font-size: 14px;
}
:deep(.el-date-editor.el-input__wrapper) {
width: 100%;
}
:deep(.el-date-editor--daterange) {
width: 100%;
}
</style>
登出:
<template>
<div class="layout-container">
<!-- 顶部导航栏 -->
<div class="header">
<div class="header-gradient">
<div class="header-content">
<div class="left">
<div class="logo-container">
<img src="../assets/logo.png" alt="日记月累" class="logo-img">
</div>
<div class="weather-info">
<div class="weather-main">
<div class="weather-icon" :class="weatherIconClass">
<el-icon v-if="weather.type === 'sunny'"><Sunny /></el-icon>
<el-icon v-else-if="weather.type === 'cloudy'"><Cloudy /></el-icon>
<el-icon v-else-if="weather.type === 'rainy'"><Lightning /></el-icon>
<el-icon v-else><Sunny /></el-icon>
</div>
<span class="temperature">{{ weather.temperature }}°C</span>
</div>
<div class="weather-details">
<div class="description">{{ weather.description }}</div>
<div class="weather-extra">
<span>{{ weather.wind }}</span>
<el-divider direction="vertical" />
<span>{{ weather.humidity }}</span>
</div>
<span class="location">{{ weather.city }}</span>
</div>
</div>
</div>
<div class="right">
<el-dropdown @command="handleCommand">
<span class="user-info">
<el-avatar :size="32" :src="userAvatar" />
<span>{{ userName }}</span>
<el-icon><CaretBottom /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">个人信息</el-dropdown-item>
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
</div>
<!-- 主要内容区域 -->
<div class="page-container">
<!-- 侧边导航栏 -->
<div class="sidebar">
<el-menu
:default-active="activeMenu"
router
class="menu-container">
<el-menu-item index="/dashboard/home">
<el-icon><HomeFilled /></el-icon>
<span>首页</span>
</el-menu-item>
<el-menu-item index="/dashboard/event-category">
<el-icon><Folder /></el-icon>
<span>事件分类</span>
</el-menu-item>
<el-menu-item index="/dashboard/event-management">
<el-icon><Document /></el-icon>
<span>事件管理</span>
</el-menu-item>
<el-menu-item index="/dashboard/profile">
<el-icon><User /></el-icon>
<span>个人中心</span>
</el-menu-item>
</el-menu>
</div>
<!-- 右侧内容区 -->
<div class="main-content">
<router-view />
</div>
</div>
<!-- 修改看板娘容器 -->
<div class="live2d-container">
<div class="pio-container left">
<div class="pio-action">
<!-- 自定义菜单 -->
<div class="custom-menu" v-show="showMenu">
<div class="menu-item" @click="navigateTo('/dashboard/home')">首页</div>
<div class="menu-item" @click="navigateTo('/dashboard/event-category')">事件分类</div>
<div class="menu-item" @click="navigateTo('/dashboard/event-management')">事件管理</div>
<div class="menu-item" @click="navigateTo('/dashboard/profile')">个人中心</div>
</div>
</div>
<!-- 调整看板娘大小 -->
<canvas id="pio" width="220" height="360" @click="toggleMenu"></canvas>
</div>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted, nextTick } from 'vue'
import { useStore } from 'vuex'
import { useRouter, useRoute } from 'vue-router'
import { User } from '@element-plus/icons-vue'
import axios from 'axios'
import eventBus from '../utils/eventBus'
import { BASE_URL } from '../config/api'
import { ElMessage } from 'element-plus'
export default {
name: 'Layout',
setup() {
const store = useStore()
const router = useRouter()
const route = useRoute()
const weather = ref({
temperature: '--',
description: '获取中...',
type: 'sunny',
wind: '',
humidity: '',
city: '重庆市巴南区'
})
// 修改用户信息的响应式引用
const userAvatar = ref('https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png')
const userName = ref('')
// 添加获取用户信息的方法
const fetchUserInfo = async () => {
try {
const response = await axios.get(`${BASE_URL}/user/findbyid`)
if (response.data.code === 0) {
const userData = response.data.data
userName.value = userData.nickname
// 如果有头像,获取头像数据
if (userData.avatar) {
try {
const imageResponse = await axios.get(`${BASE_URL}/file/download/${userData.avatar}`, {
responseType: 'arraybuffer'
})
const base64 = btoa(
new Uint8Array(imageResponse.data)
.reduce((data, byte) => data + String.fromCharCode(byte), '')
)
userAvatar.value = `data:image/jpeg;base64,${base64}`
} catch (error) {
console.error('获取头像失败:', error)
}
}
}
} catch (error) {
console.error('获取用户信息失败:', error)
}
}
// 在组件挂载时获取用户信息
onMounted(() => {
fetchUserInfo()
// 监听用户信息更新事件
eventBus.on('userInfoUpdated', () => {
fetchUserInfo()
})
})
// 添加刷新用户信息的方法
const refreshUserInfo = () => {
fetchUserInfo()
}
// 修改 handleCommand 方法
const handleCommand = async (command) => {
if (command === 'logout') {
try {
const response = await axios.get('http://localhost:8080/SSM/user/logout')
if (response.data.code === 0) {
// 清除本地存储的用户信息
store.dispatch('logout')
// 跳转到登录页
router.push('/login')
} else {
ElMessage.error(response.data.msg || '退出失败')
}
} catch (error) {
console.error('退出失败:', error)
ElMessage.error('退出失败,请稍后重试')
}
} else if (command === 'profile') {
router.push('/dashboard/profile')
}
}
// 当前激活的菜单项
const activeMenu = computed(() => route.path)
// 天气图标的样式类
const weatherIconClass = computed(() => ({
'weather-sunny': weather.value.type === 'sunny',
'weather-cloudy': weather.value.type === 'cloudy',
'weather-rainy': weather.value.type === 'rainy'
}))
// 根据天气状况设置图标类型
const setWeatherType = (text) => {
if (text.includes('晴')) return 'sunny'
if (text.includes('云') || text.includes('阴')) return 'cloudy'
if (text.includes('雨') || text.includes('雪')) return 'rainy'
return 'sunny'
}
// 获取天气信息
const getWeather = async () => {
try {
const response = await fetch(
'https://devapi.qweather.com/v7/weather/now?location=101040900&key=60754b24070c4925bb63ce660f48614c'
)
const data = await response.json()
if (data.code === '200') {
const now = data.now
weather.value = {
temperature: now.temp,
description: now.text,
type: setWeatherType(now.text),
wind: `${now.windDir} ${now.windScale}级`,
humidity: `湿度 ${now.humidity}%`,
city: '重庆市巴南区'
}
} else {
console.error('获取气数据失败:', data)
}
} catch (error) {
console.error('请天气数据失败:', error)
}
}
// 添加菜单控制
const showMenu = ref(false)
// 切换菜单显示状态
const toggleMenu = () => {
showMenu.value = !showMenu.value
}
// 导航函数
const navigateTo = (path) => {
showMenu.value = false
router.push(path)
}
// 修改看板娘初始化配置
onMounted(() => {
nextTick(() => {
// 加载看板娘样式
const link = document.createElement('link')
link.rel = 'stylesheet'
link.type = 'text/css'
link.href = 'https://cdn.jsdelivr.net/gh/xiaoyanu/file-test@2021.12.1-2/kbn/pio.css'
document.head.appendChild(link)
// 等待一小段时间确保模型加载完成
setTimeout(() => {
try {
const pio = new Paul_Pio({
"mode": "fixed",
"hidden": false,
"referer": "欢迎来到日记月累!",
"content": {
"welcome": ["欢迎来到日记月累!"],
// "touch": ["想要去哪个页面呢?"],
"skin": ["想要切换看板娘吗?"],
"home": ["点击这里回到首页!"],
"events": ["去看看待办事项吧!"],
"profile": ["要修改个人信息吗?"]
},
"model": [
"https://cdn.jsdelivr.net/gh/xiaoyanu/file-test@2021.12.1/kbn/xiaomai/model.json"
],
"tips": true,
"click": true,
"night": "single",
"method": "click",
"selector": "pio",
"onClickStart": () => {
if (window.pio) {
const messages = [
"哎呀,你点到我了!",
"想去别的页面看看吗?",
"有什么需要帮忙的吗?",
"点击我可以打开导航菜单哦~"
]
const randomMessage = messages[Math.floor(Math.random() * messages.length)]
window.pio.render(randomMessage)
}
}
})
window.pio = pio
} catch (error) {
console.error('看板娘初始化失败:', error)
}
}, 3000)
})
// 获取天气信息
getWeather()
setInterval(getWeather, 30 * 60 * 1000)
})
return {
weather,
weatherIconClass,
userAvatar,
userName,
handleCommand,
activeMenu,
showMenu,
toggleMenu,
navigateTo,
refreshUserInfo
}
}
}
</script>
<style scoped>
.layout-container {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
width: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 1000;
}
.header-gradient {
background: linear-gradient(135deg, #1e90ff 0%, #70a1ff 50%, #97c1ff 100%);
padding: 0 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
}
.header-gradient::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.2) 100%);
pointer-events: none;
}
.header-content {
height: 64px;
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
}
.left {
display: flex;
align-items: center;
}
.logo-container {
display: flex;
align-items: center;
margin-right: 20px;
}
.logo-img {
height: 40px;
width: auto;
object-fit: contain;
}
.title {
display: none;
}
.weather-info {
display: flex;
align-items: center;
gap: 16px;
padding: 8px 16px;
border-radius: 15px;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
}
.weather-info:hover {
transform: translateY(-1px);
}
.weather-main {
display: flex;
align-items: center;
gap: 12px;
padding-right: 16px;
border-right: 1px solid rgba(255, 255, 255, 0.15);
}
.weather-icon {
display: flex;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
}
.weather-icon:hover {
background: rgba(255, 255, 255, 0.15);
transform: scale(1.02);
}
.weather-icon .el-icon {
font-size: 26px;
color: #fff;
}
.weather-details {
display: flex;
flex-direction: column;
gap: 3px;
}
.temperature {
font-size: 22px;
font-weight: 600;
color: #fff;
}
.description {
font-size: 14px;
color: #fff;
font-weight: 500;
}
.weather-extra {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: rgba(255, 255, 255, 0.9);
}
.weather-extra :deep(.el-divider--vertical) {
border-color: rgba(255, 255, 255, 0.2);
margin: 0;
height: 10px;
}
.location {
font-size: 12px;
color: rgba(255, 255, 255, 0.85);
display: flex;
align-items: center;
gap: 4px;
}
.location::before {
content: '';
display: inline-block;
width: 3px;
height: 3px;
background-color: rgba(255, 255, 255, 0.7);
border-radius: 50%;
}
.right {
display: flex;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
color: white;
cursor: pointer;
}
.page-container {
display: flex;
margin-top: 64px;
height: calc(100vh - 64px);
}
.sidebar {
width: 200px;
background-color: #fff;
border-right: 1px solid #e6e6e6;
height: calc(100vh - 64px);
position: fixed;
top: 64px;
left: 0;
overflow-y: auto;
}
.menu-container {
height: 100%;
border-right: none;
}
.main-content {
flex: 1;
padding: 20px;
background-color: #f5f5f5;
margin-left: 200px;
min-height: calc(100vh - 64px);
overflow-y: auto;
box-sizing: border-box;
}
:deep(.el-menu-item) {
display: flex;
align-items: center;
}
:deep(.el-menu-item .el-icon) {
margin-right: 8px;
}
/* 修改天气图标动画 */
.weather-sunny .el-icon {
animation: shine 4s ease-in-out infinite;
}
.weather-cloudy .el-icon {
animation: float 5s ease-in-out infinite;
}
.weather-rainy .el-icon {
animation: rain 2s ease-in-out infinite;
}
@keyframes shine {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-2px);
}
}
@keyframes rain {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(2px);
}
}
/* 优化滚动样式 */
.main-content::-webkit-scrollbar {
width: 6px;
}
.main-content::-webkit-scrollbar-thumb {
background-color: #ddd;
border-radius: 3px;
}
.main-content::-webkit-scrollbar-track {
background-color: #f5f5f5;
}
.sidebar::-webkit-scrollbar {
width: 6px;
}
.sidebar::-webkit-scrollbar-thumb {
background-color: #ddd;
border-radius: 3px;
}
.sidebar::-webkit-scrollbar-track {
background-color: #fff;
}
/* 自定义菜单样式 */
.custom-menu {
position: absolute;
left: 120%;
bottom: 30%;
transform: translateY(50%);
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
padding: 8px 0;
margin-left: 10px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
z-index: 1002;
}
.menu-item {
padding: 8px 20px;
color: #333;
cursor: pointer;
transition: all 0.3s;
white-space: nowrap;
}
.menu-item:hover {
background: rgba(0, 0, 0, 0.05);
color: #409EFF;
}
/* 确保看板娘和菜单可以正常点击 */
.live2d-container {
position: fixed;
left: 20px;
bottom: 30px;
z-index: 999;
}
.pio-container {
position: relative;
transform: scale(0.8);
transform-origin: bottom left;
}
#pio {
cursor: pointer;
}
/* 添加房子图标样式 */
.home-icon {
position: absolute;
top: -30px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.home-icon:hover {
transform: translateX(-50%) scale(1.1);
background: #409EFF;
color: white;
}
.home-icon .el-icon {
font-size: 18px;
}
</style>
项目目录参考:
六:运行界面
登录:
首页:
分类:
事件:
至此,简易记事本的项目展示结束。