简易记事本开发-(SSM+Vue)

发布于:2024-12-21 ⋅ 阅读:(13) ⋅ 点赞:(0)

目录

前言

一、项目需求分析

二、项目环境搭建 

 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. 用户注册
  2. 用户登录与退出
  3. 事件分类的增删改查管理
  4. 事件管理的增删改查管理

二、项目环境搭建 

 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> 

项目目录参考:

 

 六:运行界面

登录:

 首页:

分类:

 

事件:

 

 

至此,简易记事本的项目展示结束。