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

news2024/12/20 19:02:00

目录

前言

一、项目需求分析

二、项目环境搭建 

 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> 

项目目录参考:

 

 六:运行界面

登录:

 首页:

分类:

 

事件:

 

 

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2262860.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vsCode 报错[vue/no-v-model-argument]e‘v-model‘ directives require no argument

在vue3中使用ui库中的组件语法v-model:value时会提示[vue/no-multiple-template-root]The template root requires exactly one element. 引入组件使用单标签时会提示[vue/no-multiple-template-root]“The template root requires exactly one element. 原因&#xff1a; 1.可…

初学stm32 -- SysTick定时器

以delay延时函数来介绍SysTick定时器的配置与使用 首先是delay_init()延时初始化函数&#xff0c;这个函数主要是去初始化SysTick定时器&#xff1b; void delay_init() {SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); //选择外部时钟 HCLK/8fac_usSystemCoreCloc…

Gitlab 数据备份全攻略:命令、方法与注意事项

文章目录 1、备份命令2、备份目录名称说明3、手工备份配置文件3.1 备份配置文件3.2 备份ssh文件 4、备份注意事项4.1 停止puma和sicdekiq组件4.2 copy策略需要更多磁盘空间 5、数据备份方法5.1 docker命令备份5.2 kubectl命令备份5.3 参数说明5.4、选择性备份5.5、非tar备份5.6…

selenium工作原理

原文链接&#xff1a;https://blog.csdn.net/weixin_67603503/article/details/143226557 启动浏览器和绑定端口 当你创建一个 WebDriver 实例&#xff08;如 webdriver.Chrome()&#xff09;时&#xff0c;Selenium 会启动一个新的浏览器实例&#xff0c;并为其分配一个特定的…

Docker--Docker Registry(镜像仓库)

什么是Docker Registry&#xff1f; 镜像仓库&#xff08;Docker Registry&#xff09;是Docker生态系统中用于存储、管理和分发Docker镜像的关键组件。 镜像仓库主要负责存储Docker镜像&#xff0c;这些镜像包含了应用程序及其相关的依赖项和配置&#xff0c;是构建和运行Doc…

OpenEuler Linux上怎么测试Nvidia显卡安装情况

当安装好显卡驱动后怎么样知道驱动程序安装好了,这里以T400 OpenEuler 正常情况下,我们只要看一下nvidia-smi 状态就可以确定他已经正常了 如图: 这里就已经确定是可以正常使用了,这里只是没有运行对应的程序,那接来下我们就写一个测试程序来测试一下:以下代码通过AI给出然后…

【python虚拟环境安装】linux centos 下的python虚拟环境配置

linux centos 下的python虚拟环境配置 在 CentOS 环境中处理 pip 安装警告的方法1. 创建并使用虚拟环境2. 忽略警告并继续使用 root 用户安装&#xff08;不推荐&#xff09;报错问题处理 在 CentOS 环境中处理 pip 安装警告的方法 当在 CentOS 环境中遇到 pip 安装警告时&…

Excel根据身份证号,计算退休日期和剩余天数!

大家好&#xff0c;我是小鱼。 日常工作中&#xff0c;有时我们需要使用Excel表格统计男女员工退休日期或者退休剩余天数&#xff0c;很多新手小伙伴可能不知道如何下手。今天就跟大家分享一下WPS中的Excel表格数据如果根据身份证号&#xff0c;自动批量计算退休日期和剩余天数…

排序算法(3)——归并排序、计数排序

目录 1. 归并排序 1.1 递归实现 1.2 非递归实现 1.3 归并排序特性总结 2. 计数排序 代码实现 3. 总结 1. 归并排序 基本思想&#xff1a; 归并排序&#xff08;merge sort&#xff09;是建立在归并操作上的一种有效的排序算法&#xff0c;该算法是采用分治法&#xff0…

Electron-Vue 开发下 dev/prod/webpack server各种路径设置汇总

背景 在实际开发中&#xff0c;我发现团队对于这几个路径的设置上是纯靠猜的&#xff0c;通过一点点地尝试来找到可行的路径&#xff0c;这是不应该的&#xff0c;我们应该很清晰地了解这几个概念&#xff0c;以下通过截图和代码进行细节讲解。 npm run dev 下的路径如何处理&…

HTML零基础入门教学

目录 一. HTML语言 二. HTML结构 三. HTML文件基本结构 四. 准备开发环境 五. 快速生成代码框架 六. HTML常见标签 6.1 注释标签 6.2 标题标签&#xff1a;h1-h6 6.3 段落标签&#xff1a;p 6.4 换行标签&#xff1a;br 6.5 格式化标签 6.6 图片标签&a…

晶闸管-直流电动机调速系统设计【MATLAB源码+Word文档】

1.1.设计指标及要求 某双闭环直流调速系统采用晶闸管三相桥式整流电路供电&#xff0c;基本数据为:直流他励电动机&#xff0c; 设计要求主要技术指标&#xff1a; 1.2 目录 2. 硬件电路设计 3. 控制电路 4. MATLAB系统仿真 转速输出波形 硬件电路图 Word文档MATLAB仿真源…

Andriod Studio | 项目构建成功,依赖无报错的情况下,却无法启动App?

启动App时出现问题&#xff08;Error running app&#xff09;&#xff1a; &#xff08;1&#xff09; Emulator failed to connect within 5 minutes 原因&#xff1a;App(模拟器)超过5分钟未响应&#xff0c;连接失败 解决办法&#xff1a;可能是因为电脑磁盘不足&#…

UE5中实现Billboard公告板渲染

公告板&#xff08;Billboard&#xff09;通常指永远面向摄像机的面片&#xff0c;游戏中许多技术都基于公告板&#xff0c;例如提示拾取图标、敌人血槽信息等&#xff0c;本文将使用UE5和材质节点制作一个公告板。 Gif效果&#xff1a; 网格效果&#xff1a; 1.思路 通过…

中宇联与亚马逊云科技共同推出Well-Architected联合解决方案

数字化转型正如火如荼地进行&#xff0c;云计算已逐渐成为企业发展的核心动力。亚马逊云科技积极承担起数字经济时代基础设施提供者及企业成长的高质量伙伴角色&#xff0c;全心全意深化客户服务&#xff0c;赋能企业迈向成功之路。基于多年服务各行各业客户的经验总结&#xf…

React+Vite从零搭建项目及配置详解

相信很多React初学者第一次搭建自己的项目&#xff0c;搭建时会无从下手&#xff0c;本篇适合快速实现功能&#xff0c;熟悉React项目搭建流程。 目录 一、创建项目react-item 二、调整项目目录结构 三、使用scss预处理器 四、组件库Ant Design 五、配置基础路由 六、配置…

JDK21 虚拟线程:能完全代替传统线程吗?聊聊 Web 应用中的场景适配

虚拟线程到底是个什么东西&#xff1f; 虚拟线程的出现&#xff0c;可以说是 Java 并发编程的一次“大手术”。本质上&#xff0c;它是对 线程模型的抽象和轻量化&#xff1a; 传统线程&#xff1a;由操作系统管理&#xff0c;每个线程需要分配较大的栈空间&#xff08;通常 …

《Vue3实战教程》13:Vue3侦听器

如果您有疑问&#xff0c;请观看视频教程《Vue3实战教程》 侦听器​ 基本示例​ 计算属性允许我们声明性地计算衍生值。然而在有些情况下&#xff0c;我们需要在状态变化时执行一些“副作用”&#xff1a;例如更改 DOM&#xff0c;或是根据异步操作的结果去修改另一处的状态。…

Intel(R) Iris(R) Xe Graphics安装Anaconda、Pytorch(CPU版本)

一、Intel(R) Iris(R) Xe Graphics安装Anaconda 下载网址&#xff1a;https://repo.anaconda.com/archive/ 双击Anaconda3-2024.10-1-Windows-x86_64&#xff0c;一直下一步&#xff0c;选择安装的路径位置&#xff0c;一直下一步就安装完成了。打开Anaconda PowerShell Promp…

如何在NGINX中实现基于IP的访问控制(IP黑白名单)?

大家好&#xff0c;我是锋哥。今天分享关于【如何在NGINX中实现基于IP的访问控制&#xff08;IP黑白名单&#xff09;&#xff1f;】面试题。希望对大家有帮助&#xff1b; 如何在NGINX中实现基于IP的访问控制&#xff08;IP黑白名单&#xff09;&#xff1f; 1000道 互联网大…