From 996c6ce7505a462365e1e9a4bbffba872e0c33f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=AD=90=E7=90=A6?= <95332614+wangziqi0409@users.noreply.github.com> Date: Wed, 21 Jan 2026 10:23:12 +0800 Subject: [PATCH] addd --- .gitignore | 31 + README.md | 57 + backend/pom.xml | 66 + backend/schema.sql | 48 + .../community/app/CommunityApplication.java | 13 + .../app/config/GlobalExceptionHandler.java | 33 + .../community/app/config/SaTokenConfig.java | 32 + .../com/community/app/config/WebConfig.java | 35 + .../app/controller/ActivityController.java | 105 ++ .../app/controller/AdminController.java | 44 + .../app/controller/AuthController.java | 59 + .../app/controller/PublicController.java | 36 + .../app/controller/UserController.java | 37 + .../community/app/dto/ActivityRequest.java | 124 ++ .../com/community/app/dto/ActivityView.java | 132 ++ .../community/app/dto/AdminSignupView.java | 105 ++ .../com/community/app/dto/ApiResponse.java | 52 + .../com/community/app/dto/AuthRequest.java | 26 + .../com/community/app/dto/AuthResponse.java | 49 + .../com/community/app/dto/PageResponse.java | 32 + .../community/app/dto/RegisterRequest.java | 45 + .../com/community/app/dto/SignupView.java | 114 ++ .../com/community/app/entity/Activity.java | 150 +++ .../community/app/entity/ActivitySignup.java | 78 ++ .../java/com/community/app/entity/User.java | 80 ++ .../community/app/mapper/ActivityMapper.java | 19 + .../community/app/mapper/SignupMapper.java | 28 + .../com/community/app/mapper/UserMapper.java | 16 + .../app/service/ActivityService.java | 110 ++ .../community/app/service/SignupService.java | 93 ++ .../community/app/service/UserService.java | 48 + backend/src/main/resources/application.yml | 25 + .../main/resources/mapper/ActivityMapper.xml | 92 ++ .../main/resources/mapper/SignupMapper.xml | 92 ++ .../src/main/resources/mapper/UserMapper.xml | 31 + frontend/index.html | 12 + frontend/package.json | 23 + frontend/pnpm-lock.yaml | 1160 +++++++++++++++++ frontend/src/App.vue | 110 ++ frontend/src/api/activities.js | 12 + frontend/src/api/admin.js | 5 + frontend/src/api/auth.js | 6 + frontend/src/api/http.js | 32 + frontend/src/api/me.js | 3 + frontend/src/main.js | 16 + frontend/src/router/index.js | 41 + frontend/src/store/auth.js | 37 + frontend/src/store/index.js | 5 + frontend/src/styles/base.css | 90 ++ frontend/src/styles/fullcalendar.css | 83 ++ frontend/src/views/ActivityDetail.vue | 139 ++ frontend/src/views/ActivityList.vue | 131 ++ frontend/src/views/AdminActivities.vue | 534 ++++++++ frontend/src/views/AdminSignups.vue | 63 + frontend/src/views/LoginView.vue | 68 + frontend/src/views/MySignups.vue | 61 + frontend/src/views/RegisterView.vue | 76 ++ frontend/vite.config.js | 9 + 张家贝-开题报告.md | 23 + 59 files changed, 4876 insertions(+) create mode 100644 .gitignore create mode 100644 backend/pom.xml create mode 100644 backend/schema.sql create mode 100644 backend/src/main/java/com/community/app/CommunityApplication.java create mode 100644 backend/src/main/java/com/community/app/config/GlobalExceptionHandler.java create mode 100644 backend/src/main/java/com/community/app/config/SaTokenConfig.java create mode 100644 backend/src/main/java/com/community/app/config/WebConfig.java create mode 100644 backend/src/main/java/com/community/app/controller/ActivityController.java create mode 100644 backend/src/main/java/com/community/app/controller/AdminController.java create mode 100644 backend/src/main/java/com/community/app/controller/AuthController.java create mode 100644 backend/src/main/java/com/community/app/controller/PublicController.java create mode 100644 backend/src/main/java/com/community/app/controller/UserController.java create mode 100644 backend/src/main/java/com/community/app/dto/ActivityRequest.java create mode 100644 backend/src/main/java/com/community/app/dto/ActivityView.java create mode 100644 backend/src/main/java/com/community/app/dto/AdminSignupView.java create mode 100644 backend/src/main/java/com/community/app/dto/ApiResponse.java create mode 100644 backend/src/main/java/com/community/app/dto/AuthRequest.java create mode 100644 backend/src/main/java/com/community/app/dto/AuthResponse.java create mode 100644 backend/src/main/java/com/community/app/dto/PageResponse.java create mode 100644 backend/src/main/java/com/community/app/dto/RegisterRequest.java create mode 100644 backend/src/main/java/com/community/app/dto/SignupView.java create mode 100644 backend/src/main/java/com/community/app/entity/Activity.java create mode 100644 backend/src/main/java/com/community/app/entity/ActivitySignup.java create mode 100644 backend/src/main/java/com/community/app/entity/User.java create mode 100644 backend/src/main/java/com/community/app/mapper/ActivityMapper.java create mode 100644 backend/src/main/java/com/community/app/mapper/SignupMapper.java create mode 100644 backend/src/main/java/com/community/app/mapper/UserMapper.java create mode 100644 backend/src/main/java/com/community/app/service/ActivityService.java create mode 100644 backend/src/main/java/com/community/app/service/SignupService.java create mode 100644 backend/src/main/java/com/community/app/service/UserService.java create mode 100644 backend/src/main/resources/application.yml create mode 100644 backend/src/main/resources/mapper/ActivityMapper.xml create mode 100644 backend/src/main/resources/mapper/SignupMapper.xml create mode 100644 backend/src/main/resources/mapper/UserMapper.xml create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/pnpm-lock.yaml create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/api/activities.js create mode 100644 frontend/src/api/admin.js create mode 100644 frontend/src/api/auth.js create mode 100644 frontend/src/api/http.js create mode 100644 frontend/src/api/me.js create mode 100644 frontend/src/main.js create mode 100644 frontend/src/router/index.js create mode 100644 frontend/src/store/auth.js create mode 100644 frontend/src/store/index.js create mode 100644 frontend/src/styles/base.css create mode 100644 frontend/src/styles/fullcalendar.css create mode 100644 frontend/src/views/ActivityDetail.vue create mode 100644 frontend/src/views/ActivityList.vue create mode 100644 frontend/src/views/AdminActivities.vue create mode 100644 frontend/src/views/AdminSignups.vue create mode 100644 frontend/src/views/LoginView.vue create mode 100644 frontend/src/views/MySignups.vue create mode 100644 frontend/src/views/RegisterView.vue create mode 100644 frontend/vite.config.js create mode 100644 张家贝-开题报告.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5458942 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Node / Frontend +frontend/node_modules/ +frontend/dist/ +frontend/.vite/ +frontend/.npm/ +frontend/*.log +frontend/.env +frontend/.env.* + +# Java / Backend +backend/target/ +backend/.classpath +backend/.project +backend/.settings/ +backend/*.log + +# OS +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.iml + +# Logs +*.log + +# Misc +*.tmp +*.swp diff --git a/README.md b/README.md index e69de29..06df326 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,57 @@ +# 社区节气文化活动发布与报名系统 + +## 后端技术栈 +- Spring Boot 3 + MyBatis + Sa-Token +- MySQL 8 + +## 前端技术栈 +- Vue 3 + Vite +- Axios +- Arco Design Vue + +## 本地运行 + +### 1. 初始化数据库 + +1) 创建数据库并导入表结构: + +``` +mysql -u root -p < backend/schema.sql +``` + +2) 修改 `backend/src/main/resources/application.yml` 中的数据库账号与密码。 + +### 2. 启动后端 + +``` +cd backend +mvn spring-boot:run +``` + +### 3. 启动前端 + +``` +cd frontend +npm install +npm run dev +``` + +前端地址:`http://localhost:5173` +后端地址:`http://localhost:8080` + +## 角色说明 + +- 普通用户注册后默认角色为 `user`。 +- 管理员功能需要角色为 `admin` 的账号。可以在数据库中手动修改: + +``` +update sys_user set role = 'admin' where username = '你的用户名'; +``` + +## 功能概览 + +- 活动发布与管理:创建、编辑、发布、结束活动 +- 活动报名与名额控制:报名、取消、签到 +- 用户端:活动广场、活动详情、我的报名 +- 管理端:报名名单与签到 + diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 0000000..5e269d5 --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,66 @@ + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + com.community + community-activities + 0.0.1-SNAPSHOT + community-activities + Community solar-term activity system + + 17 + 17 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 3.0.3 + + + com.mysql + mysql-connector-j + runtime + + + cn.dev33 + sa-token-spring-boot3-starter + 1.38.0 + + + org.springframework.security + spring-security-crypto + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/backend/schema.sql b/backend/schema.sql new file mode 100644 index 0000000..f3c0fa1 --- /dev/null +++ b/backend/schema.sql @@ -0,0 +1,48 @@ +create database if not exists community_activity default character set utf8mb4 collate utf8mb4_general_ci; +use community_activity; + +create table if not exists sys_user ( + id bigint primary key auto_increment, + username varchar(64) not null unique, + password_hash varchar(255) not null, + nickname varchar(64) not null, + phone varchar(32), + role varchar(32) not null default 'user', + created_at datetime not null, + updated_at datetime not null +); + +create table if not exists activity ( + id bigint primary key auto_increment, + title varchar(255) not null, + term varchar(64) not null, + summary varchar(255), + content text, + location varchar(255) not null, + start_time datetime not null, + end_time datetime not null, + signup_start datetime not null, + signup_end datetime not null, + quota int not null, + status varchar(32) not null, + cover_url varchar(255), + created_by bigint, + created_at datetime not null, + updated_at datetime not null, + index idx_activity_status (status), + index idx_activity_time (start_time) +); + +create table if not exists activity_signup ( + id bigint primary key auto_increment, + activity_id bigint not null, + user_id bigint not null, + status varchar(32) not null, + checkin_status varchar(32) not null, + signed_at datetime not null, + canceled_at datetime, + checkin_at datetime, + unique key uk_activity_user (activity_id, user_id), + index idx_activity (activity_id), + index idx_user (user_id) +); diff --git a/backend/src/main/java/com/community/app/CommunityApplication.java b/backend/src/main/java/com/community/app/CommunityApplication.java new file mode 100644 index 0000000..55efced --- /dev/null +++ b/backend/src/main/java/com/community/app/CommunityApplication.java @@ -0,0 +1,13 @@ +package com.community.app; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@MapperScan("com.community.app.mapper") +public class CommunityApplication { + public static void main(String[] args) { + SpringApplication.run(CommunityApplication.class, args); + } +} diff --git a/backend/src/main/java/com/community/app/config/GlobalExceptionHandler.java b/backend/src/main/java/com/community/app/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..1986988 --- /dev/null +++ b/backend/src/main/java/com/community/app/config/GlobalExceptionHandler.java @@ -0,0 +1,33 @@ +package com.community.app.config; + +import com.community.app.dto.ApiResponse; +import cn.dev33.satoken.exception.NotLoginException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(IllegalStateException.class) + public ApiResponse handleIllegalState(IllegalStateException ex) { + return ApiResponse.fail(ex.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ApiResponse handleValidation(MethodArgumentNotValidException ex) { + String message = ex.getBindingResult().getFieldErrors().isEmpty() + ? "参数错误" + : ex.getBindingResult().getFieldErrors().get(0).getDefaultMessage(); + return ApiResponse.fail(message); + } + + @ExceptionHandler(NotLoginException.class) + public ApiResponse handleNotLogin(NotLoginException ex) { + return ApiResponse.fail("未登录或登录已失效"); + } + + @ExceptionHandler(Exception.class) + public ApiResponse handleOther(Exception ex) { + return ApiResponse.fail("服务器异常"); + } +} diff --git a/backend/src/main/java/com/community/app/config/SaTokenConfig.java b/backend/src/main/java/com/community/app/config/SaTokenConfig.java new file mode 100644 index 0000000..0d9d5d6 --- /dev/null +++ b/backend/src/main/java/com/community/app/config/SaTokenConfig.java @@ -0,0 +1,32 @@ +package com.community.app.config; + +import cn.dev33.satoken.stp.StpInterface; +import com.community.app.entity.User; +import com.community.app.service.UserService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Collections; +import java.util.List; + +@Configuration +public class SaTokenConfig { + @Bean + public StpInterface stpInterface(UserService userService) { + return new StpInterface() { + @Override + public List getPermissionList(Object loginId, String loginType) { + return Collections.emptyList(); + } + + @Override + public List getRoleList(Object loginId, String loginType) { + User user = userService.findById(Long.valueOf(loginId.toString())); + if (user == null) { + return Collections.emptyList(); + } + return Collections.singletonList(user.getRole()); + } + }; + } +} diff --git a/backend/src/main/java/com/community/app/config/WebConfig.java b/backend/src/main/java/com/community/app/config/WebConfig.java new file mode 100644 index 0000000..84cb146 --- /dev/null +++ b/backend/src/main/java/com/community/app/config/WebConfig.java @@ -0,0 +1,35 @@ +package com.community.app.config; + +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.context.SaHolder; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(false) + .maxAge(3600); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new SaInterceptor(handle -> { + String method = SaHolder.getRequest().getMethod(); + if ("OPTIONS".equalsIgnoreCase(method)) { + return; + } + StpUtil.checkLogin(); + })) + .addPathPatterns("/api/**") + .excludePathPatterns("/api/auth/**", "/api/public/**"); + } +} diff --git a/backend/src/main/java/com/community/app/controller/ActivityController.java b/backend/src/main/java/com/community/app/controller/ActivityController.java new file mode 100644 index 0000000..3b6add5 --- /dev/null +++ b/backend/src/main/java/com/community/app/controller/ActivityController.java @@ -0,0 +1,105 @@ +package com.community.app.controller; + +import cn.dev33.satoken.annotation.SaCheckRole; +import cn.dev33.satoken.stp.StpUtil; +import com.community.app.dto.ActivityRequest; +import com.community.app.dto.ActivityView; +import com.community.app.dto.ApiResponse; +import com.community.app.entity.User; +import com.community.app.service.ActivityService; +import com.community.app.service.SignupService; +import com.community.app.service.UserService; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/activities") +public class ActivityController { + private final ActivityService activityService; + private final SignupService signupService; + private final UserService userService; + + public ActivityController(ActivityService activityService, SignupService signupService, UserService userService) { + this.activityService = activityService; + this.signupService = signupService; + this.userService = userService; + } + + @GetMapping + public ApiResponse> list(@RequestParam(required = false) String status, + @RequestParam(required = false) String keyword) { + User user = currentUser(); + if (user == null || !"admin".equals(user.getRole())) { + status = "published"; + } + return ApiResponse.ok(activityService.list(status, keyword)); + } + + @GetMapping("/{id}") + public ApiResponse detail(@PathVariable Long id) { + ActivityView view = activityService.getView(id); + if (view == null) { + return ApiResponse.fail("活动不存在"); + } + User user = currentUser(); + if (user == null || !"admin".equals(user.getRole())) { + if (!"published".equals(view.getStatus())) { + return ApiResponse.fail("活动未发布"); + } + } + return ApiResponse.ok(view); + } + + @SaCheckRole("admin") + @PostMapping + public ApiResponse create(@Valid @RequestBody ActivityRequest request) { + Long userId = Long.valueOf(StpUtil.getLoginId().toString()); + activityService.create(request, userId); + return ApiResponse.ok("创建成功", null); + } + + @SaCheckRole("admin") + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @Valid @RequestBody ActivityRequest request) { + activityService.update(id, request); + return ApiResponse.ok("更新成功", null); + } + + @SaCheckRole("admin") + @PostMapping("/{id}/publish") + public ApiResponse publish(@PathVariable Long id) { + activityService.publish(id); + return ApiResponse.ok("已发布", null); + } + + @SaCheckRole("admin") + @PostMapping("/{id}/close") + public ApiResponse close(@PathVariable Long id) { + activityService.close(id); + return ApiResponse.ok("已结束", null); + } + + @PostMapping("/{id}/signup") + public ApiResponse signup(@PathVariable Long id) { + Long userId = Long.valueOf(StpUtil.getLoginId().toString()); + signupService.signup(id, userId); + return ApiResponse.ok("报名成功", null); + } + + @PostMapping("/{id}/cancel") + public ApiResponse cancel(@PathVariable Long id) { + Long userId = Long.valueOf(StpUtil.getLoginId().toString()); + signupService.cancel(id, userId); + return ApiResponse.ok("已取消报名", null); + } + + private User currentUser() { + if (!StpUtil.isLogin()) { + return null; + } + Long userId = Long.valueOf(StpUtil.getLoginId().toString()); + return userService.findById(userId); + } +} diff --git a/backend/src/main/java/com/community/app/controller/AdminController.java b/backend/src/main/java/com/community/app/controller/AdminController.java new file mode 100644 index 0000000..b789592 --- /dev/null +++ b/backend/src/main/java/com/community/app/controller/AdminController.java @@ -0,0 +1,44 @@ +package com.community.app.controller; + +import cn.dev33.satoken.annotation.SaCheckRole; +import com.community.app.dto.AdminSignupView; +import com.community.app.dto.ApiResponse; +import com.community.app.entity.User; +import com.community.app.service.SignupService; +import com.community.app.service.UserService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/admin") +@SaCheckRole("admin") +public class AdminController { + private final SignupService signupService; + private final UserService userService; + + public AdminController(SignupService signupService, UserService userService) { + this.signupService = signupService; + this.userService = userService; + } + + @GetMapping("/activities/{id}/signups") + public ApiResponse> listSignups(@PathVariable Long id) { + return ApiResponse.ok(signupService.listByActivity(id)); + } + + @PostMapping("/signups/{id}/checkin") + public ApiResponse checkin(@PathVariable Long id) { + signupService.checkin(id); + return ApiResponse.ok("签到成功", null); + } + + @GetMapping("/users") + public ApiResponse> listUsers() { + return ApiResponse.ok(userService.listAll()); + } +} diff --git a/backend/src/main/java/com/community/app/controller/AuthController.java b/backend/src/main/java/com/community/app/controller/AuthController.java new file mode 100644 index 0000000..4c97586 --- /dev/null +++ b/backend/src/main/java/com/community/app/controller/AuthController.java @@ -0,0 +1,59 @@ +package com.community.app.controller; + +import cn.dev33.satoken.stp.StpUtil; +import com.community.app.dto.ApiResponse; +import com.community.app.dto.AuthRequest; +import com.community.app.dto.AuthResponse; +import com.community.app.dto.RegisterRequest; +import com.community.app.entity.User; +import com.community.app.service.UserService; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/auth") +public class AuthController { + private final UserService userService; + + public AuthController(UserService userService) { + this.userService = userService; + } + + @PostMapping("/login") + public ApiResponse login(@Valid @RequestBody AuthRequest request) { + User user = userService.findByUsername(request.getUsername()); + if (user == null || !userService.verifyPassword(user, request.getPassword())) { + return ApiResponse.fail("用户名或密码错误"); + } + StpUtil.login(user.getId()); + AuthResponse response = new AuthResponse(); + response.setToken(StpUtil.getTokenValue()); + response.setUserId(user.getId()); + response.setUsername(user.getUsername()); + response.setNickname(user.getNickname()); + response.setRole(user.getRole()); + return ApiResponse.ok("登录成功", response); + } + + @PostMapping("/register") + public ApiResponse register(@Valid @RequestBody RegisterRequest request) { + User user = userService.register(request.getUsername(), request.getPassword(), request.getNickname(), request.getPhone()); + StpUtil.login(user.getId()); + AuthResponse response = new AuthResponse(); + response.setToken(StpUtil.getTokenValue()); + response.setUserId(user.getId()); + response.setUsername(user.getUsername()); + response.setNickname(user.getNickname()); + response.setRole(user.getRole()); + return ApiResponse.ok("注册成功", response); + } + + @PostMapping("/logout") + public ApiResponse logout() { + StpUtil.logout(); + return ApiResponse.ok("已退出", null); + } +} diff --git a/backend/src/main/java/com/community/app/controller/PublicController.java b/backend/src/main/java/com/community/app/controller/PublicController.java new file mode 100644 index 0000000..2da63f3 --- /dev/null +++ b/backend/src/main/java/com/community/app/controller/PublicController.java @@ -0,0 +1,36 @@ +package com.community.app.controller; + +import com.community.app.dto.ActivityView; +import com.community.app.dto.ApiResponse; +import com.community.app.service.ActivityService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/public") +public class PublicController { + private final ActivityService activityService; + + public PublicController(ActivityService activityService) { + this.activityService = activityService; + } + + @GetMapping("/activities") + public ApiResponse> list(@RequestParam(required = false) String keyword) { + return ApiResponse.ok(activityService.list("published", keyword)); + } + + @GetMapping("/activities/{id}") + public ApiResponse detail(@PathVariable Long id) { + ActivityView view = activityService.getView(id); + if (view == null || !"published".equals(view.getStatus())) { + return ApiResponse.fail("活动不存在或未发布"); + } + return ApiResponse.ok(view); + } +} diff --git a/backend/src/main/java/com/community/app/controller/UserController.java b/backend/src/main/java/com/community/app/controller/UserController.java new file mode 100644 index 0000000..5dff631 --- /dev/null +++ b/backend/src/main/java/com/community/app/controller/UserController.java @@ -0,0 +1,37 @@ +package com.community.app.controller; + +import cn.dev33.satoken.stp.StpUtil; +import com.community.app.dto.ApiResponse; +import com.community.app.dto.SignupView; +import com.community.app.entity.User; +import com.community.app.service.SignupService; +import com.community.app.service.UserService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/me") +public class UserController { + private final UserService userService; + private final SignupService signupService; + + public UserController(UserService userService, SignupService signupService) { + this.userService = userService; + this.signupService = signupService; + } + + @GetMapping + public ApiResponse me() { + Long userId = Long.valueOf(StpUtil.getLoginId().toString()); + return ApiResponse.ok(userService.findById(userId)); + } + + @GetMapping("/signups") + public ApiResponse> mySignups() { + Long userId = Long.valueOf(StpUtil.getLoginId().toString()); + return ApiResponse.ok(signupService.listByUser(userId)); + } +} diff --git a/backend/src/main/java/com/community/app/dto/ActivityRequest.java b/backend/src/main/java/com/community/app/dto/ActivityRequest.java new file mode 100644 index 0000000..dfefae6 --- /dev/null +++ b/backend/src/main/java/com/community/app/dto/ActivityRequest.java @@ -0,0 +1,124 @@ +package com.community.app.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; + +public class ActivityRequest { + @NotBlank + private String title; + @NotBlank + private String term; + private String summary; + private String content; + @NotBlank + private String location; + @NotNull + private LocalDateTime startTime; + @NotNull + private LocalDateTime endTime; + @NotNull + private LocalDateTime signupStart; + @NotNull + private LocalDateTime signupEnd; + @NotNull + private Integer quota; + private String status; + private String coverUrl; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getTerm() { + return term; + } + + public void setTerm(String term) { + this.term = term; + } + + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public LocalDateTime getStartTime() { + return startTime; + } + + public void setStartTime(LocalDateTime startTime) { + this.startTime = startTime; + } + + public LocalDateTime getEndTime() { + return endTime; + } + + public void setEndTime(LocalDateTime endTime) { + this.endTime = endTime; + } + + public LocalDateTime getSignupStart() { + return signupStart; + } + + public void setSignupStart(LocalDateTime signupStart) { + this.signupStart = signupStart; + } + + public LocalDateTime getSignupEnd() { + return signupEnd; + } + + public void setSignupEnd(LocalDateTime signupEnd) { + this.signupEnd = signupEnd; + } + + public Integer getQuota() { + return quota; + } + + public void setQuota(Integer quota) { + this.quota = quota; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getCoverUrl() { + return coverUrl; + } + + public void setCoverUrl(String coverUrl) { + this.coverUrl = coverUrl; + } +} diff --git a/backend/src/main/java/com/community/app/dto/ActivityView.java b/backend/src/main/java/com/community/app/dto/ActivityView.java new file mode 100644 index 0000000..cc7694f --- /dev/null +++ b/backend/src/main/java/com/community/app/dto/ActivityView.java @@ -0,0 +1,132 @@ +package com.community.app.dto; + +import java.time.LocalDateTime; + +public class ActivityView { + private Long id; + private String title; + private String term; + private String summary; + private String content; + private String location; + private LocalDateTime startTime; + private LocalDateTime endTime; + private LocalDateTime signupStart; + private LocalDateTime signupEnd; + private Integer quota; + private String status; + private String coverUrl; + private Long signupCount; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getTerm() { + return term; + } + + public void setTerm(String term) { + this.term = term; + } + + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public LocalDateTime getStartTime() { + return startTime; + } + + public void setStartTime(LocalDateTime startTime) { + this.startTime = startTime; + } + + public LocalDateTime getEndTime() { + return endTime; + } + + public void setEndTime(LocalDateTime endTime) { + this.endTime = endTime; + } + + public LocalDateTime getSignupStart() { + return signupStart; + } + + public void setSignupStart(LocalDateTime signupStart) { + this.signupStart = signupStart; + } + + public LocalDateTime getSignupEnd() { + return signupEnd; + } + + public void setSignupEnd(LocalDateTime signupEnd) { + this.signupEnd = signupEnd; + } + + public Integer getQuota() { + return quota; + } + + public void setQuota(Integer quota) { + this.quota = quota; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getCoverUrl() { + return coverUrl; + } + + public void setCoverUrl(String coverUrl) { + this.coverUrl = coverUrl; + } + + public Long getSignupCount() { + return signupCount; + } + + public void setSignupCount(Long signupCount) { + this.signupCount = signupCount; + } +} diff --git a/backend/src/main/java/com/community/app/dto/AdminSignupView.java b/backend/src/main/java/com/community/app/dto/AdminSignupView.java new file mode 100644 index 0000000..9704a7b --- /dev/null +++ b/backend/src/main/java/com/community/app/dto/AdminSignupView.java @@ -0,0 +1,105 @@ +package com.community.app.dto; + +import java.time.LocalDateTime; + +public class AdminSignupView { + private Long id; + private Long activityId; + private Long userId; + private String username; + private String nickname; + private String phone; + private String status; + private String checkinStatus; + private LocalDateTime signedAt; + private LocalDateTime canceledAt; + private LocalDateTime checkinAt; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getActivityId() { + return activityId; + } + + public void setActivityId(Long activityId) { + this.activityId = activityId; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getCheckinStatus() { + return checkinStatus; + } + + public void setCheckinStatus(String checkinStatus) { + this.checkinStatus = checkinStatus; + } + + public LocalDateTime getSignedAt() { + return signedAt; + } + + public void setSignedAt(LocalDateTime signedAt) { + this.signedAt = signedAt; + } + + public LocalDateTime getCanceledAt() { + return canceledAt; + } + + public void setCanceledAt(LocalDateTime canceledAt) { + this.canceledAt = canceledAt; + } + + public LocalDateTime getCheckinAt() { + return checkinAt; + } + + public void setCheckinAt(LocalDateTime checkinAt) { + this.checkinAt = checkinAt; + } +} diff --git a/backend/src/main/java/com/community/app/dto/ApiResponse.java b/backend/src/main/java/com/community/app/dto/ApiResponse.java new file mode 100644 index 0000000..3660e2d --- /dev/null +++ b/backend/src/main/java/com/community/app/dto/ApiResponse.java @@ -0,0 +1,52 @@ +package com.community.app.dto; + +public class ApiResponse { + private boolean success; + private String message; + private T data; + + public ApiResponse() { + } + + public ApiResponse(boolean success, String message, T data) { + this.success = success; + this.message = message; + this.data = data; + } + + public static ApiResponse ok(T data) { + return new ApiResponse<>(true, "success", data); + } + + public static ApiResponse ok(String message, T data) { + return new ApiResponse<>(true, message, data); + } + + public static ApiResponse fail(String message) { + return new ApiResponse<>(false, message, null); + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } +} diff --git a/backend/src/main/java/com/community/app/dto/AuthRequest.java b/backend/src/main/java/com/community/app/dto/AuthRequest.java new file mode 100644 index 0000000..02665b1 --- /dev/null +++ b/backend/src/main/java/com/community/app/dto/AuthRequest.java @@ -0,0 +1,26 @@ +package com.community.app.dto; + +import jakarta.validation.constraints.NotBlank; + +public class AuthRequest { + @NotBlank + private String username; + @NotBlank + private String password; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/backend/src/main/java/com/community/app/dto/AuthResponse.java b/backend/src/main/java/com/community/app/dto/AuthResponse.java new file mode 100644 index 0000000..a1f11bf --- /dev/null +++ b/backend/src/main/java/com/community/app/dto/AuthResponse.java @@ -0,0 +1,49 @@ +package com.community.app.dto; + +public class AuthResponse { + private String token; + private Long userId; + private String username; + private String nickname; + private String role; + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } +} diff --git a/backend/src/main/java/com/community/app/dto/PageResponse.java b/backend/src/main/java/com/community/app/dto/PageResponse.java new file mode 100644 index 0000000..ccc6c63 --- /dev/null +++ b/backend/src/main/java/com/community/app/dto/PageResponse.java @@ -0,0 +1,32 @@ +package com.community.app.dto; + +import java.util.List; + +public class PageResponse { + private long total; + private List list; + + public PageResponse() { + } + + public PageResponse(long total, List list) { + this.total = total; + this.list = list; + } + + public long getTotal() { + return total; + } + + public void setTotal(long total) { + this.total = total; + } + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } +} diff --git a/backend/src/main/java/com/community/app/dto/RegisterRequest.java b/backend/src/main/java/com/community/app/dto/RegisterRequest.java new file mode 100644 index 0000000..d6abbd2 --- /dev/null +++ b/backend/src/main/java/com/community/app/dto/RegisterRequest.java @@ -0,0 +1,45 @@ +package com.community.app.dto; + +import jakarta.validation.constraints.NotBlank; + +public class RegisterRequest { + @NotBlank + private String username; + @NotBlank + private String password; + @NotBlank + private String nickname; + private String phone; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } +} diff --git a/backend/src/main/java/com/community/app/dto/SignupView.java b/backend/src/main/java/com/community/app/dto/SignupView.java new file mode 100644 index 0000000..51be201 --- /dev/null +++ b/backend/src/main/java/com/community/app/dto/SignupView.java @@ -0,0 +1,114 @@ +package com.community.app.dto; + +import java.time.LocalDateTime; + +public class SignupView { + private Long id; + private Long activityId; + private String activityTitle; + private String term; + private String location; + private LocalDateTime startTime; + private LocalDateTime endTime; + private String status; + private String checkinStatus; + private LocalDateTime signedAt; + private LocalDateTime canceledAt; + private LocalDateTime checkinAt; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getActivityId() { + return activityId; + } + + public void setActivityId(Long activityId) { + this.activityId = activityId; + } + + public String getActivityTitle() { + return activityTitle; + } + + public void setActivityTitle(String activityTitle) { + this.activityTitle = activityTitle; + } + + public String getTerm() { + return term; + } + + public void setTerm(String term) { + this.term = term; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public LocalDateTime getStartTime() { + return startTime; + } + + public void setStartTime(LocalDateTime startTime) { + this.startTime = startTime; + } + + public LocalDateTime getEndTime() { + return endTime; + } + + public void setEndTime(LocalDateTime endTime) { + this.endTime = endTime; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getCheckinStatus() { + return checkinStatus; + } + + public void setCheckinStatus(String checkinStatus) { + this.checkinStatus = checkinStatus; + } + + public LocalDateTime getSignedAt() { + return signedAt; + } + + public void setSignedAt(LocalDateTime signedAt) { + this.signedAt = signedAt; + } + + public LocalDateTime getCanceledAt() { + return canceledAt; + } + + public void setCanceledAt(LocalDateTime canceledAt) { + this.canceledAt = canceledAt; + } + + public LocalDateTime getCheckinAt() { + return checkinAt; + } + + public void setCheckinAt(LocalDateTime checkinAt) { + this.checkinAt = checkinAt; + } +} diff --git a/backend/src/main/java/com/community/app/entity/Activity.java b/backend/src/main/java/com/community/app/entity/Activity.java new file mode 100644 index 0000000..be4bb17 --- /dev/null +++ b/backend/src/main/java/com/community/app/entity/Activity.java @@ -0,0 +1,150 @@ +package com.community.app.entity; + +import java.time.LocalDateTime; + +public class Activity { + private Long id; + private String title; + private String term; + private String summary; + private String content; + private String location; + private LocalDateTime startTime; + private LocalDateTime endTime; + private LocalDateTime signupStart; + private LocalDateTime signupEnd; + private Integer quota; + private String status; + private String coverUrl; + private Long createdBy; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getTerm() { + return term; + } + + public void setTerm(String term) { + this.term = term; + } + + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public LocalDateTime getStartTime() { + return startTime; + } + + public void setStartTime(LocalDateTime startTime) { + this.startTime = startTime; + } + + public LocalDateTime getEndTime() { + return endTime; + } + + public void setEndTime(LocalDateTime endTime) { + this.endTime = endTime; + } + + public LocalDateTime getSignupStart() { + return signupStart; + } + + public void setSignupStart(LocalDateTime signupStart) { + this.signupStart = signupStart; + } + + public LocalDateTime getSignupEnd() { + return signupEnd; + } + + public void setSignupEnd(LocalDateTime signupEnd) { + this.signupEnd = signupEnd; + } + + public Integer getQuota() { + return quota; + } + + public void setQuota(Integer quota) { + this.quota = quota; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getCoverUrl() { + return coverUrl; + } + + public void setCoverUrl(String coverUrl) { + this.coverUrl = coverUrl; + } + + public Long getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(Long createdBy) { + this.createdBy = createdBy; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/backend/src/main/java/com/community/app/entity/ActivitySignup.java b/backend/src/main/java/com/community/app/entity/ActivitySignup.java new file mode 100644 index 0000000..bd9f769 --- /dev/null +++ b/backend/src/main/java/com/community/app/entity/ActivitySignup.java @@ -0,0 +1,78 @@ +package com.community.app.entity; + +import java.time.LocalDateTime; + +public class ActivitySignup { + private Long id; + private Long activityId; + private Long userId; + private String status; + private String checkinStatus; + private LocalDateTime signedAt; + private LocalDateTime canceledAt; + private LocalDateTime checkinAt; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getActivityId() { + return activityId; + } + + public void setActivityId(Long activityId) { + this.activityId = activityId; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getCheckinStatus() { + return checkinStatus; + } + + public void setCheckinStatus(String checkinStatus) { + this.checkinStatus = checkinStatus; + } + + public LocalDateTime getSignedAt() { + return signedAt; + } + + public void setSignedAt(LocalDateTime signedAt) { + this.signedAt = signedAt; + } + + public LocalDateTime getCanceledAt() { + return canceledAt; + } + + public void setCanceledAt(LocalDateTime canceledAt) { + this.canceledAt = canceledAt; + } + + public LocalDateTime getCheckinAt() { + return checkinAt; + } + + public void setCheckinAt(LocalDateTime checkinAt) { + this.checkinAt = checkinAt; + } +} diff --git a/backend/src/main/java/com/community/app/entity/User.java b/backend/src/main/java/com/community/app/entity/User.java new file mode 100644 index 0000000..383b925 --- /dev/null +++ b/backend/src/main/java/com/community/app/entity/User.java @@ -0,0 +1,80 @@ +package com.community.app.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.time.LocalDateTime; + +public class User { + private Long id; + private String username; + @JsonIgnore + private String passwordHash; + private String nickname; + private String phone; + private String role; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPasswordHash() { + return passwordHash; + } + + public void setPasswordHash(String passwordHash) { + this.passwordHash = passwordHash; + } + + public String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/backend/src/main/java/com/community/app/mapper/ActivityMapper.java b/backend/src/main/java/com/community/app/mapper/ActivityMapper.java new file mode 100644 index 0000000..7acc38c --- /dev/null +++ b/backend/src/main/java/com/community/app/mapper/ActivityMapper.java @@ -0,0 +1,19 @@ +package com.community.app.mapper; + +import com.community.app.dto.ActivityView; +import com.community.app.entity.Activity; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface ActivityMapper { + int insert(Activity activity); + + int update(Activity activity); + + Activity findById(@Param("id") Long id); + + ActivityView findViewById(@Param("id") Long id); + + List list(@Param("status") String status, @Param("keyword") String keyword); +} diff --git a/backend/src/main/java/com/community/app/mapper/SignupMapper.java b/backend/src/main/java/com/community/app/mapper/SignupMapper.java new file mode 100644 index 0000000..62f0488 --- /dev/null +++ b/backend/src/main/java/com/community/app/mapper/SignupMapper.java @@ -0,0 +1,28 @@ +package com.community.app.mapper; + +import com.community.app.dto.AdminSignupView; +import com.community.app.dto.SignupView; +import com.community.app.entity.ActivitySignup; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface SignupMapper { + ActivitySignup findByActivityAndUser(@Param("activityId") Long activityId, @Param("userId") Long userId); + + ActivitySignup findById(@Param("id") Long id); + + int insert(ActivitySignup signup); + + int updateStatus(@Param("id") Long id, @Param("status") String status, @Param("canceledAt") java.time.LocalDateTime canceledAt); + + int resign(@Param("id") Long id, @Param("status") String status, @Param("signedAt") java.time.LocalDateTime signedAt); + + int checkin(@Param("id") Long id, @Param("checkinStatus") String checkinStatus, @Param("checkinAt") java.time.LocalDateTime checkinAt); + + long countSignedByActivity(@Param("activityId") Long activityId); + + List listByUser(@Param("userId") Long userId); + + List listByActivity(@Param("activityId") Long activityId); +} diff --git a/backend/src/main/java/com/community/app/mapper/UserMapper.java b/backend/src/main/java/com/community/app/mapper/UserMapper.java new file mode 100644 index 0000000..0447be1 --- /dev/null +++ b/backend/src/main/java/com/community/app/mapper/UserMapper.java @@ -0,0 +1,16 @@ +package com.community.app.mapper; + +import com.community.app.entity.User; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface UserMapper { + User findById(@Param("id") Long id); + + User findByUsername(@Param("username") String username); + + int insert(User user); + + List listAll(); +} diff --git a/backend/src/main/java/com/community/app/service/ActivityService.java b/backend/src/main/java/com/community/app/service/ActivityService.java new file mode 100644 index 0000000..ad5fc05 --- /dev/null +++ b/backend/src/main/java/com/community/app/service/ActivityService.java @@ -0,0 +1,110 @@ +package com.community.app.service; + +import com.community.app.dto.ActivityRequest; +import com.community.app.dto.ActivityView; +import com.community.app.entity.Activity; +import com.community.app.mapper.ActivityMapper; +import com.community.app.mapper.SignupMapper; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class ActivityService { + private final ActivityMapper activityMapper; + private final SignupMapper signupMapper; + + public ActivityService(ActivityMapper activityMapper, SignupMapper signupMapper) { + this.activityMapper = activityMapper; + this.signupMapper = signupMapper; + } + + public ActivityView getView(Long id) { + return activityMapper.findViewById(id); + } + + public Activity getEntity(Long id) { + return activityMapper.findById(id); + } + + public List list(String status, String keyword) { + return activityMapper.list(status, keyword); + } + + public Activity create(ActivityRequest request, Long creatorId) { + validateTimes(request); + Activity activity = new Activity(); + applyRequest(activity, request); + activity.setCreatedBy(creatorId); + if (activity.getStatus() == null || activity.getStatus().isEmpty()) { + activity.setStatus("draft"); + } + activityMapper.insert(activity); + return activity; + } + + public void update(Long id, ActivityRequest request) { + validateTimes(request); + Activity activity = activityMapper.findById(id); + if (activity == null) { + throw new IllegalStateException("活动不存在"); + } + activity.setId(id); + applyRequest(activity, request); + if (activity.getStatus() == null || activity.getStatus().isEmpty()) { + activity.setStatus("draft"); + } + activityMapper.update(activity); + } + + public void publish(Long id) { + Activity activity = activityMapper.findById(id); + if (activity == null) { + throw new IllegalStateException("活动不存在"); + } + activity.setStatus("published"); + activityMapper.update(activity); + } + + public void close(Long id) { + Activity activity = activityMapper.findById(id); + if (activity == null) { + throw new IllegalStateException("活动不存在"); + } + activity.setStatus("closed"); + activityMapper.update(activity); + } + + public long countSigned(Long activityId) { + return signupMapper.countSignedByActivity(activityId); + } + + private void validateTimes(ActivityRequest request) { + LocalDateTime start = request.getStartTime(); + LocalDateTime end = request.getEndTime(); + LocalDateTime signupStart = request.getSignupStart(); + LocalDateTime signupEnd = request.getSignupEnd(); + if (start != null && end != null && end.isBefore(start)) { + throw new IllegalStateException("活动结束时间不能早于开始时间"); + } + if (signupStart != null && signupEnd != null && signupEnd.isBefore(signupStart)) { + throw new IllegalStateException("报名结束时间不能早于开始时间"); + } + } + + private void applyRequest(Activity activity, ActivityRequest request) { + activity.setTitle(request.getTitle()); + activity.setTerm(request.getTerm()); + activity.setSummary(request.getSummary()); + activity.setContent(request.getContent()); + activity.setLocation(request.getLocation()); + activity.setStartTime(request.getStartTime()); + activity.setEndTime(request.getEndTime()); + activity.setSignupStart(request.getSignupStart()); + activity.setSignupEnd(request.getSignupEnd()); + activity.setQuota(request.getQuota()); + activity.setStatus(request.getStatus()); + activity.setCoverUrl(request.getCoverUrl()); + } +} diff --git a/backend/src/main/java/com/community/app/service/SignupService.java b/backend/src/main/java/com/community/app/service/SignupService.java new file mode 100644 index 0000000..d7d0f83 --- /dev/null +++ b/backend/src/main/java/com/community/app/service/SignupService.java @@ -0,0 +1,93 @@ +package com.community.app.service; + +import com.community.app.dto.AdminSignupView; +import com.community.app.dto.SignupView; +import com.community.app.entity.Activity; +import com.community.app.entity.ActivitySignup; +import com.community.app.mapper.ActivityMapper; +import com.community.app.mapper.SignupMapper; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class SignupService { + private final SignupMapper signupMapper; + private final ActivityMapper activityMapper; + + public SignupService(SignupMapper signupMapper, ActivityMapper activityMapper) { + this.signupMapper = signupMapper; + this.activityMapper = activityMapper; + } + + public void signup(Long activityId, Long userId) { + Activity activity = activityMapper.findById(activityId); + if (activity == null) { + throw new IllegalStateException("活动不存在"); + } + if (!"published".equals(activity.getStatus())) { + throw new IllegalStateException("活动未发布或已结束"); + } + LocalDateTime now = LocalDateTime.now(); + if (activity.getSignupStart() != null && now.isBefore(activity.getSignupStart())) { + throw new IllegalStateException("报名尚未开始"); + } + if (activity.getSignupEnd() != null && now.isAfter(activity.getSignupEnd())) { + throw new IllegalStateException("报名已结束"); + } + long count = signupMapper.countSignedByActivity(activityId); + if (activity.getQuota() != null && count >= activity.getQuota()) { + throw new IllegalStateException("报名名额已满"); + } + + ActivitySignup existing = signupMapper.findByActivityAndUser(activityId, userId); + if (existing == null) { + ActivitySignup signup = new ActivitySignup(); + signup.setActivityId(activityId); + signup.setUserId(userId); + signup.setStatus("SIGNED"); + signup.setCheckinStatus("NOT"); + signup.setSignedAt(now); + signupMapper.insert(signup); + return; + } + if ("SIGNED".equals(existing.getStatus())) { + throw new IllegalStateException("已报名该活动"); + } + signupMapper.resign(existing.getId(), "SIGNED", now); + } + + public void cancel(Long activityId, Long userId) { + ActivitySignup existing = signupMapper.findByActivityAndUser(activityId, userId); + if (existing == null || !"SIGNED".equals(existing.getStatus())) { + throw new IllegalStateException("未找到可取消的报名"); + } + if ("CHECKED".equals(existing.getCheckinStatus())) { + throw new IllegalStateException("已签到的报名不可取消"); + } + signupMapper.updateStatus(existing.getId(), "CANCELED", LocalDateTime.now()); + } + + public void checkin(Long signupId) { + ActivitySignup existing = signupMapper.findById(signupId); + if (existing == null) { + throw new IllegalStateException("报名记录不存在"); + } + if (!"SIGNED".equals(existing.getStatus())) { + throw new IllegalStateException("该报名已取消,无法签到"); + } + if ("CHECKED".equals(existing.getCheckinStatus())) { + throw new IllegalStateException("已完成签到"); + } + signupMapper.checkin(signupId, "CHECKED", LocalDateTime.now()); + } + + public List listByUser(Long userId) { + return signupMapper.listByUser(userId); + } + + public List listByActivity(Long activityId) { + return signupMapper.listByActivity(activityId); + } +} diff --git a/backend/src/main/java/com/community/app/service/UserService.java b/backend/src/main/java/com/community/app/service/UserService.java new file mode 100644 index 0000000..28082b9 --- /dev/null +++ b/backend/src/main/java/com/community/app/service/UserService.java @@ -0,0 +1,48 @@ +package com.community.app.service; + +import com.community.app.entity.User; +import com.community.app.mapper.UserMapper; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class UserService { + private final UserMapper userMapper; + private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + + public UserService(UserMapper userMapper) { + this.userMapper = userMapper; + } + + public User findById(Long id) { + return userMapper.findById(id); + } + + public User findByUsername(String username) { + return userMapper.findByUsername(username); + } + + public User register(String username, String rawPassword, String nickname, String phone) { + if (userMapper.findByUsername(username) != null) { + throw new IllegalStateException("用户名已存在"); + } + User user = new User(); + user.setUsername(username); + user.setPasswordHash(encoder.encode(rawPassword)); + user.setNickname(nickname); + user.setPhone(phone); + user.setRole("user"); + userMapper.insert(user); + return user; + } + + public boolean verifyPassword(User user, String rawPassword) { + return encoder.matches(rawPassword, user.getPasswordHash()); + } + + public List listAll() { + return userMapper.listAll(); + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000..3f3cb71 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,25 @@ +server: + port: 8080 + +spring: + datasource: + url: jdbc:mysql://localhost:3307/community_activity?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + username: root + password: qq5211314 + driver-class-name: com.mysql.cj.jdbc.Driver + jackson: + time-zone: Asia/Shanghai + date-format: yyyy-MM-dd HH:mm:ss + +mybatis: + mapper-locations: classpath:mapper/*.xml + configuration: + map-underscore-to-camel-case: true + +sa-token: + token-name: satoken + timeout: 2592000 + is-concurrent: true + is-share: true + token-style: uuid + active-timeout: 1800 diff --git a/backend/src/main/resources/mapper/ActivityMapper.xml b/backend/src/main/resources/mapper/ActivityMapper.xml new file mode 100644 index 0000000..b3c1614 --- /dev/null +++ b/backend/src/main/resources/mapper/ActivityMapper.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + insert into activity (title, term, summary, content, location, start_time, end_time, + signup_start, signup_end, quota, status, cover_url, created_by, created_at, updated_at) + values (#{title}, #{term}, #{summary}, #{content}, #{location}, #{startTime}, #{endTime}, + #{signupStart}, #{signupEnd}, #{quota}, #{status}, #{coverUrl}, #{createdBy}, now(), now()) + + + + update activity + set title = #{title}, + term = #{term}, + summary = #{summary}, + content = #{content}, + location = #{location}, + start_time = #{startTime}, + end_time = #{endTime}, + signup_start = #{signupStart}, + signup_end = #{signupEnd}, + quota = #{quota}, + status = #{status}, + cover_url = #{coverUrl}, + updated_at = now() + where id = #{id} + + + + + + + + diff --git a/backend/src/main/resources/mapper/SignupMapper.xml b/backend/src/main/resources/mapper/SignupMapper.xml new file mode 100644 index 0000000..7fa0bac --- /dev/null +++ b/backend/src/main/resources/mapper/SignupMapper.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + insert into activity_signup (activity_id, user_id, status, checkin_status, signed_at) + values (#{activityId}, #{userId}, #{status}, #{checkinStatus}, #{signedAt}) + + + + update activity_signup set status = #{status}, canceled_at = #{canceledAt} where id = #{id} + + + + update activity_signup set status = #{status}, signed_at = #{signedAt}, canceled_at = null + where id = #{id} + + + + update activity_signup set checkin_status = #{checkinStatus}, checkin_at = #{checkinAt} + where id = #{id} + + + + + + + + diff --git a/backend/src/main/resources/mapper/UserMapper.xml b/backend/src/main/resources/mapper/UserMapper.xml new file mode 100644 index 0000000..3966d27 --- /dev/null +++ b/backend/src/main/resources/mapper/UserMapper.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + insert into sys_user (username, password_hash, nickname, phone, role, created_at, updated_at) + values (#{username}, #{passwordHash}, #{nickname}, #{phone}, #{role}, now(), now()) + + + + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..8d07570 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + 社区节气活动系统 + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..9e49ac1 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,23 @@ +{ + "name": "community-activities-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@arco-design/web-vue": "^2.55.1", + "axios": "^1.7.2", + "lunar-javascript": "^1.6.13", + "pinia": "^2.1.7", + "vue": "^3.4.29", + "vue-router": "^4.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.5", + "vite": "^5.3.5" + } +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..d55777d --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,1160 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@arco-design/web-vue': + specifier: ^2.55.1 + version: 2.57.0(vue@3.5.27) + '@fullcalendar/core': + specifier: ^6.1.15 + version: 6.1.20 + '@fullcalendar/daygrid': + specifier: ^6.1.15 + version: 6.1.20(@fullcalendar/core@6.1.20) + '@fullcalendar/interaction': + specifier: ^6.1.15 + version: 6.1.20(@fullcalendar/core@6.1.20) + '@fullcalendar/vue3': + specifier: ^6.1.15 + version: 6.1.20(@fullcalendar/core@6.1.20)(vue@3.5.27) + axios: + specifier: ^1.7.2 + version: 1.13.2 + lunar-javascript: + specifier: ^1.6.13 + version: 1.7.7 + pinia: + specifier: ^2.1.7 + version: 2.3.1(vue@3.5.27) + vue: + specifier: ^3.4.29 + version: 3.5.27 + vue-router: + specifier: ^4.4.0 + version: 4.6.4(vue@3.5.27) + devDependencies: + '@vitejs/plugin-vue': + specifier: ^5.0.5 + version: 5.2.4(vite@5.4.21)(vue@3.5.27) + vite: + specifier: ^5.3.5 + version: 5.4.21 + +packages: + + '@arco-design/color@0.4.0': + resolution: {integrity: sha512-s7p9MSwJgHeL8DwcATaXvWT3m2SigKpxx4JA1BGPHL4gfvaQsmQfrLBDpjOJFJuJ2jG2dMt3R3P8Pm9E65q18g==} + + '@arco-design/web-vue@2.57.0': + resolution: {integrity: sha512-R5YReC3C2sG3Jv0+YuR3B7kzkq2KdhhQNCGXD8T11xAoa0zMt6SWTP1xJQOdZcM9du+q3z6tk5mRvh4qkieRJw==} + peerDependencies: + vue: ^3.1.0 + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.6': + resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.28.6': + resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@fullcalendar/core@6.1.20': + resolution: {integrity: sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==} + + '@fullcalendar/daygrid@6.1.20': + resolution: {integrity: sha512-AO9vqhkLP77EesmJzuU+IGXgxNulsA8mgQHynclJ8U70vSwAVnbcLG9qftiTAFSlZjiY/NvhE7sflve6cJelyQ==} + peerDependencies: + '@fullcalendar/core': ~6.1.20 + + '@fullcalendar/interaction@6.1.20': + resolution: {integrity: sha512-p6txmc5txL0bMiPaJxe2ip6o0T384TyoD2KGdsU6UjZ5yoBlaY+dg7kxfnYKpYMzEJLG58n+URrHr2PgNL2fyA==} + peerDependencies: + '@fullcalendar/core': ~6.1.20 + + '@fullcalendar/vue3@6.1.20': + resolution: {integrity: sha512-8qg6pS27II9QBwFkkJC+7SfflMpWqOe7i3ii5ODq9KpLAjwQAd/zjfq8RvKR1Yryoh5UmMCmvRbMB7i4RGtqog==} + peerDependencies: + '@fullcalendar/core': ~6.1.20 + vue: ^3.0.11 + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@rollup/rollup-android-arm-eabi@4.55.2': + resolution: {integrity: sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.55.2': + resolution: {integrity: sha512-eXBg7ibkNUZ+sTwbFiDKou0BAckeV6kIigK7y5Ko4mB/5A1KLhuzEKovsmfvsL8mQorkoincMFGnQuIT92SKqA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.55.2': + resolution: {integrity: sha512-UCbaTklREjrc5U47ypLulAgg4njaqfOVLU18VrCrI+6E5MQjuG0lSWaqLlAJwsD7NpFV249XgB0Bi37Zh5Sz4g==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.55.2': + resolution: {integrity: sha512-dP67MA0cCMHFT2g5XyjtpVOtp7y4UyUxN3dhLdt11at5cPKnSm4lY+EhwNvDXIMzAMIo2KU+mc9wxaAQJTn7sQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.55.2': + resolution: {integrity: sha512-WDUPLUwfYV9G1yxNRJdXcvISW15mpvod1Wv3ok+Ws93w1HjIVmCIFxsG2DquO+3usMNCpJQ0wqO+3GhFdl6Fow==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.55.2': + resolution: {integrity: sha512-Ng95wtHVEulRwn7R0tMrlUuiLVL/HXA8Lt/MYVpy88+s5ikpntzZba1qEulTuPnPIZuOPcW9wNEiqvZxZmgmqQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.55.2': + resolution: {integrity: sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.55.2': + resolution: {integrity: sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.55.2': + resolution: {integrity: sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.55.2': + resolution: {integrity: sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.55.2': + resolution: {integrity: sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.55.2': + resolution: {integrity: sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.55.2': + resolution: {integrity: sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.55.2': + resolution: {integrity: sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.55.2': + resolution: {integrity: sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.55.2': + resolution: {integrity: sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.55.2': + resolution: {integrity: sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.55.2': + resolution: {integrity: sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.55.2': + resolution: {integrity: sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.55.2': + resolution: {integrity: sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.55.2': + resolution: {integrity: sha512-sZnyUgGkuzIXaK3jNMPmUIyJrxu/PjmATQrocpGA1WbCPX8H5tfGgRSuYtqBYAvLuIGp8SPRb1O4d1Fkb5fXaQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.55.2': + resolution: {integrity: sha512-sDpFbenhmWjNcEbBcoTV0PWvW5rPJFvu+P7XoTY0YLGRupgLbFY0XPfwIbJOObzO7QgkRDANh65RjhPmgSaAjQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.55.2': + resolution: {integrity: sha512-GvJ03TqqaweWCigtKQVBErw2bEhu1tyfNQbarwr94wCGnczA9HF8wqEe3U/Lfu6EdeNP0p6R+APeHVwEqVxpUQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.55.2': + resolution: {integrity: sha512-KvXsBvp13oZz9JGe5NYS7FNizLe99Ny+W8ETsuCyjXiKdiGrcz2/J/N8qxZ/RSwivqjQguug07NLHqrIHrqfYw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.55.2': + resolution: {integrity: sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@vue/compiler-core@3.5.27': + resolution: {integrity: sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==} + + '@vue/compiler-dom@3.5.27': + resolution: {integrity: sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==} + + '@vue/compiler-sfc@3.5.27': + resolution: {integrity: sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==} + + '@vue/compiler-ssr@3.5.27': + resolution: {integrity: sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/reactivity@3.5.27': + resolution: {integrity: sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==} + + '@vue/runtime-core@3.5.27': + resolution: {integrity: sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==} + + '@vue/runtime-dom@3.5.27': + resolution: {integrity: sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==} + + '@vue/server-renderer@3.5.27': + resolution: {integrity: sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==} + peerDependencies: + vue: 3.5.27 + + '@vue/shared@3.5.27': + resolution: {integrity: sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + + b-tween@0.3.3: + resolution: {integrity: sha512-oEHegcRpA7fAuc9KC4nktucuZn2aS8htymCPcP3qkEGPqiBH+GfqtqoG2l7LxHngg6O0HFM7hOeOYExl1Oz4ZA==} + + b-validate@1.5.3: + resolution: {integrity: sha512-iCvCkGFskbaYtfQ0a3GmcQCHl/Sv1GufXFGuUQ+FE+WJa7A/espLOuFIn09B944V8/ImPj71T4+rTASxO2PAuA==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + compute-scroll-into-view@1.0.20: + resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + entities@7.0.0: + resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + + lunar-javascript@1.7.7: + resolution: {integrity: sha512-u/KYiwPIBo/0bT+WWfU7qO1d+aqeB90Tuy4ErXenr2Gam0QcWeezUvtiOIyXR7HbVnW2I1DKfU0NBvzMZhbVQw==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + number-precision@1.6.0: + resolution: {integrity: sha512-05OLPgbgmnixJw+VvEh18yNPUo3iyp4BEWJcrLu4X9W05KmMifN7Mu5exYvQXqxxeNWhvIF+j3Rij+HmddM/hQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + pinia@2.3.1: + resolution: {integrity: sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==} + peerDependencies: + typescript: '>=4.4.4' + vue: ^2.7.0 || ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + preact@10.12.1: + resolution: {integrity: sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + + rollup@4.55.2: + resolution: {integrity: sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + scroll-into-view-if-needed@2.2.31: + resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==} + + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} + peerDependencies: + vue: ^3.5.0 + + vue@3.5.27: + resolution: {integrity: sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + +snapshots: + + '@arco-design/color@0.4.0': + dependencies: + color: 3.2.1 + + '@arco-design/web-vue@2.57.0(vue@3.5.27)': + dependencies: + '@arco-design/color': 0.4.0 + b-tween: 0.3.3 + b-validate: 1.5.3 + compute-scroll-into-view: 1.0.20 + dayjs: 1.11.19 + number-precision: 1.6.0 + resize-observer-polyfill: 1.5.1 + scroll-into-view-if-needed: 2.2.31 + vue: 3.5.27 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.28.6': + dependencies: + '@babel/types': 7.28.6 + + '@babel/types@7.28.6': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@fullcalendar/core@6.1.20': + dependencies: + preact: 10.12.1 + + '@fullcalendar/daygrid@6.1.20(@fullcalendar/core@6.1.20)': + dependencies: + '@fullcalendar/core': 6.1.20 + + '@fullcalendar/interaction@6.1.20(@fullcalendar/core@6.1.20)': + dependencies: + '@fullcalendar/core': 6.1.20 + + '@fullcalendar/vue3@6.1.20(@fullcalendar/core@6.1.20)(vue@3.5.27)': + dependencies: + '@fullcalendar/core': 6.1.20 + vue: 3.5.27 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@rollup/rollup-android-arm-eabi@4.55.2': + optional: true + + '@rollup/rollup-android-arm64@4.55.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.55.2': + optional: true + + '@rollup/rollup-darwin-x64@4.55.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.55.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.55.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.55.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.55.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.55.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.55.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.55.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.55.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.55.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.55.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.55.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.55.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.55.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.55.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.55.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.55.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.55.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.55.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.55.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.55.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.55.2': + optional: true + + '@types/estree@1.0.8': {} + + '@vitejs/plugin-vue@5.2.4(vite@5.4.21)(vue@3.5.27)': + dependencies: + vite: 5.4.21 + vue: 3.5.27 + + '@vue/compiler-core@3.5.27': + dependencies: + '@babel/parser': 7.28.6 + '@vue/shared': 3.5.27 + entities: 7.0.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.27': + dependencies: + '@vue/compiler-core': 3.5.27 + '@vue/shared': 3.5.27 + + '@vue/compiler-sfc@3.5.27': + dependencies: + '@babel/parser': 7.28.6 + '@vue/compiler-core': 3.5.27 + '@vue/compiler-dom': 3.5.27 + '@vue/compiler-ssr': 3.5.27 + '@vue/shared': 3.5.27 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.27': + dependencies: + '@vue/compiler-dom': 3.5.27 + '@vue/shared': 3.5.27 + + '@vue/devtools-api@6.6.4': {} + + '@vue/reactivity@3.5.27': + dependencies: + '@vue/shared': 3.5.27 + + '@vue/runtime-core@3.5.27': + dependencies: + '@vue/reactivity': 3.5.27 + '@vue/shared': 3.5.27 + + '@vue/runtime-dom@3.5.27': + dependencies: + '@vue/reactivity': 3.5.27 + '@vue/runtime-core': 3.5.27 + '@vue/shared': 3.5.27 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.27(vue@3.5.27)': + dependencies: + '@vue/compiler-ssr': 3.5.27 + '@vue/shared': 3.5.27 + vue: 3.5.27 + + '@vue/shared@3.5.27': {} + + asynckit@0.4.0: {} + + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + b-tween@0.3.3: {} + + b-validate@1.5.3: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.4 + + color@3.2.1: + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + compute-scroll-into-view@1.0.20: {} + + csstype@3.2.3: {} + + dayjs@1.11.19: {} + + delayed-stream@1.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + entities@7.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + estree-walker@2.0.2: {} + + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + is-arrayish@0.3.4: {} + + lunar-javascript@1.7.7: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + nanoid@3.3.11: {} + + number-precision@1.6.0: {} + + picocolors@1.1.1: {} + + pinia@2.3.1(vue@3.5.27): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.27 + vue-demi: 0.14.10(vue@3.5.27) + transitivePeerDependencies: + - '@vue/composition-api' + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + preact@10.12.1: {} + + proxy-from-env@1.1.0: {} + + resize-observer-polyfill@1.5.1: {} + + rollup@4.55.2: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.55.2 + '@rollup/rollup-android-arm64': 4.55.2 + '@rollup/rollup-darwin-arm64': 4.55.2 + '@rollup/rollup-darwin-x64': 4.55.2 + '@rollup/rollup-freebsd-arm64': 4.55.2 + '@rollup/rollup-freebsd-x64': 4.55.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.55.2 + '@rollup/rollup-linux-arm-musleabihf': 4.55.2 + '@rollup/rollup-linux-arm64-gnu': 4.55.2 + '@rollup/rollup-linux-arm64-musl': 4.55.2 + '@rollup/rollup-linux-loong64-gnu': 4.55.2 + '@rollup/rollup-linux-loong64-musl': 4.55.2 + '@rollup/rollup-linux-ppc64-gnu': 4.55.2 + '@rollup/rollup-linux-ppc64-musl': 4.55.2 + '@rollup/rollup-linux-riscv64-gnu': 4.55.2 + '@rollup/rollup-linux-riscv64-musl': 4.55.2 + '@rollup/rollup-linux-s390x-gnu': 4.55.2 + '@rollup/rollup-linux-x64-gnu': 4.55.2 + '@rollup/rollup-linux-x64-musl': 4.55.2 + '@rollup/rollup-openbsd-x64': 4.55.2 + '@rollup/rollup-openharmony-arm64': 4.55.2 + '@rollup/rollup-win32-arm64-msvc': 4.55.2 + '@rollup/rollup-win32-ia32-msvc': 4.55.2 + '@rollup/rollup-win32-x64-gnu': 4.55.2 + '@rollup/rollup-win32-x64-msvc': 4.55.2 + fsevents: 2.3.3 + + scroll-into-view-if-needed@2.2.31: + dependencies: + compute-scroll-into-view: 1.0.20 + + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + + source-map-js@1.2.1: {} + + vite@5.4.21: + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.55.2 + optionalDependencies: + fsevents: 2.3.3 + + vue-demi@0.14.10(vue@3.5.27): + dependencies: + vue: 3.5.27 + + vue-router@4.6.4(vue@3.5.27): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.27 + + vue@3.5.27: + dependencies: + '@vue/compiler-dom': 3.5.27 + '@vue/compiler-sfc': 3.5.27 + '@vue/runtime-dom': 3.5.27 + '@vue/server-renderer': 3.5.27(vue@3.5.27) + '@vue/shared': 3.5.27 diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..ef36bc1 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/frontend/src/api/activities.js b/frontend/src/api/activities.js new file mode 100644 index 0000000..0aa4fde --- /dev/null +++ b/frontend/src/api/activities.js @@ -0,0 +1,12 @@ +import http from './http'; + +export const listActivities = (params) => http.get('/api/public/activities', { params }); +export const getActivity = (id) => http.get(`/api/public/activities/${id}`); +export const listActivitiesAdmin = (params) => http.get('/api/activities', { params }); +export const getActivityAdmin = (id) => http.get(`/api/activities/${id}`); +export const createActivity = (data) => http.post('/api/activities', data); +export const updateActivity = (id, data) => http.put(`/api/activities/${id}`, data); +export const publishActivity = (id) => http.post(`/api/activities/${id}/publish`); +export const closeActivity = (id) => http.post(`/api/activities/${id}/close`); +export const signupActivity = (id) => http.post(`/api/activities/${id}/signup`); +export const cancelSignup = (id) => http.post(`/api/activities/${id}/cancel`); diff --git a/frontend/src/api/admin.js b/frontend/src/api/admin.js new file mode 100644 index 0000000..8f79b10 --- /dev/null +++ b/frontend/src/api/admin.js @@ -0,0 +1,5 @@ +import http from './http'; + +export const listSignupsByActivity = (id) => http.get(`/api/admin/activities/${id}/signups`); +export const checkinSignup = (id) => http.post(`/api/admin/signups/${id}/checkin`); +export const listUsers = () => http.get('/api/admin/users'); diff --git a/frontend/src/api/auth.js b/frontend/src/api/auth.js new file mode 100644 index 0000000..2a22119 --- /dev/null +++ b/frontend/src/api/auth.js @@ -0,0 +1,6 @@ +import http from './http'; + +export const login = (data) => http.post('/api/auth/login', data); +export const register = (data) => http.post('/api/auth/register', data); +export const logout = () => http.post('/api/auth/logout'); +export const me = () => http.get('/api/me'); diff --git a/frontend/src/api/http.js b/frontend/src/api/http.js new file mode 100644 index 0000000..c03cf32 --- /dev/null +++ b/frontend/src/api/http.js @@ -0,0 +1,32 @@ +import axios from 'axios'; +import { Message } from '@arco-design/web-vue'; + +const http = axios.create({ + baseURL: 'http://localhost:8080', + timeout: 10000 +}); + +http.interceptors.request.use((config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.satoken = token; + } + return config; +}); + +http.interceptors.response.use( + (response) => { + const payload = response.data; + if (payload && payload.success === false) { + Message.error(payload.message || '请求失败'); + return Promise.reject(payload); + } + return payload; + }, + (error) => { + Message.error('网络或服务器异常'); + return Promise.reject(error); + } +); + +export default http; diff --git a/frontend/src/api/me.js b/frontend/src/api/me.js new file mode 100644 index 0000000..e399245 --- /dev/null +++ b/frontend/src/api/me.js @@ -0,0 +1,3 @@ +import http from './http'; + +export const mySignups = () => http.get('/api/me/signups'); diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..bbe44e3 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,16 @@ +import { createApp } from 'vue'; +import ArcoVue from '@arco-design/web-vue'; +import ArcoVueIcon from '@arco-design/web-vue/es/icon'; +import zhCN from '@arco-design/web-vue/es/locale/lang/zh-cn'; +import App from './App.vue'; +import router from './router'; +import pinia from './store'; +import './styles/base.css'; +import '@arco-design/web-vue/dist/arco.css'; + +const app = createApp(App); +app.use(ArcoVue, { locale: zhCN }); +app.use(ArcoVueIcon); +app.use(pinia); +app.use(router); +app.mount('#app'); diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..cd3b9c8 --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,41 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import LoginView from '../views/LoginView.vue'; +import RegisterView from '../views/RegisterView.vue'; +import ActivityList from '../views/ActivityList.vue'; +import ActivityDetail from '../views/ActivityDetail.vue'; +import MySignups from '../views/MySignups.vue'; +import AdminActivities from '../views/AdminActivities.vue'; +import AdminSignups from '../views/AdminSignups.vue'; +import pinia from '../store'; +import { useAuthStore } from '../store/auth'; + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { path: '/', redirect: '/activities' }, + { path: '/login', component: LoginView }, + { path: '/register', component: RegisterView }, + { path: '/activities', component: ActivityList }, + { path: '/activities/:id', component: ActivityDetail }, + { path: '/me/signups', component: MySignups }, + { path: '/admin/activities', component: AdminActivities }, + { path: '/admin/activities/:id/signups', component: AdminSignups } + ] +}); + +router.beforeEach((to, from, next) => { + const auth = useAuthStore(pinia); + const token = auth.token; + if (to.path.startsWith('/login') || to.path.startsWith('/register')) { + return next(); + } + if (!token && to.path !== '/activities') { + return next('/login'); + } + if (to.path.startsWith('/admin') && !auth.isAdmin) { + return next('/activities'); + } + return next(); +}); + +export default router; diff --git a/frontend/src/store/auth.js b/frontend/src/store/auth.js new file mode 100644 index 0000000..df98e18 --- /dev/null +++ b/frontend/src/store/auth.js @@ -0,0 +1,37 @@ +import { defineStore } from 'pinia'; + +const STORAGE_TOKEN = 'token'; +const STORAGE_USER = 'user'; + +export const useAuthStore = defineStore('auth', { + state: () => ({ + token: localStorage.getItem(STORAGE_TOKEN) || '', + user: (() => { + const raw = localStorage.getItem(STORAGE_USER); + return raw ? JSON.parse(raw) : null; + })() + }), + getters: { + isLogin: (state) => Boolean(state.token), + isAdmin: (state) => state.user && state.user.role === 'admin' + }, + actions: { + setAuth(token, user) { + this.token = token || ''; + this.user = user || null; + if (this.token) { + localStorage.setItem(STORAGE_TOKEN, this.token); + } else { + localStorage.removeItem(STORAGE_TOKEN); + } + if (this.user) { + localStorage.setItem(STORAGE_USER, JSON.stringify(this.user)); + } else { + localStorage.removeItem(STORAGE_USER); + } + }, + logout() { + this.setAuth('', null); + } + } +}); diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js new file mode 100644 index 0000000..7889400 --- /dev/null +++ b/frontend/src/store/index.js @@ -0,0 +1,5 @@ +import { createPinia } from 'pinia'; + +const pinia = createPinia(); + +export default pinia; diff --git a/frontend/src/styles/base.css b/frontend/src/styles/base.css new file mode 100644 index 0000000..ccdd6aa --- /dev/null +++ b/frontend/src/styles/base.css @@ -0,0 +1,90 @@ +@import url('https://fonts.googleapis.com/css2?family=ZCOOL+XiaoWei&family=Noto+Serif+SC:wght@400;600;700&display=swap'); + +:root { + color-scheme: light; + --brand-ink: #1a1b24; + --brand-ember: #e24a2d; + --brand-ocean: #0f4c5c; + --brand-sand: #f6efe6; + --brand-mist: #e5eff1; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: 'Noto Serif SC', 'ZCOOL XiaoWei', serif; + color: var(--brand-ink); + background: + radial-gradient(1200px circle at 10% 10%, rgba(226, 74, 45, 0.15), transparent 50%), + radial-gradient(900px circle at 90% 20%, rgba(15, 76, 92, 0.18), transparent 50%), + linear-gradient(180deg, #fffdfb 0%, #f6efe6 45%, #eef5f6 100%); + min-height: 100vh; +} + +#app { + min-height: 100vh; +} + +.page-shell { + max-width: 1200px; + margin: 0 auto; + padding: 32px 20px 64px; +} + +.section-title { + font-size: 28px; + font-weight: 700; + margin: 0 0 12px; + letter-spacing: 1px; +} + +.section-subtitle { + margin: 0 0 24px; + color: rgba(26, 27, 36, 0.7); +} + +.hero-panel { + background: linear-gradient(135deg, rgba(15, 76, 92, 0.9), rgba(226, 74, 45, 0.85)); + color: #fff; + border-radius: 20px; + padding: 28px; + box-shadow: 0 20px 40px rgba(26, 27, 36, 0.18); +} + +.hero-panel h1 { + margin: 0 0 8px; + font-size: 32px; +} + +.hero-panel p { + margin: 0; + opacity: 0.85; +} + +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 18px; +} + +.fade-in { + animation: fadeInUp 0.6s ease both; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(16px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + + + + diff --git a/frontend/src/styles/fullcalendar.css b/frontend/src/styles/fullcalendar.css new file mode 100644 index 0000000..853b295 --- /dev/null +++ b/frontend/src/styles/fullcalendar.css @@ -0,0 +1,83 @@ +/* Minimal FullCalendar daygrid styles for offline usage */ +.fc { + --fc-border-color: rgba(26, 27, 36, 0.12); + --fc-page-bg-color: transparent; + --fc-today-bg-color: rgba(226, 74, 45, 0.08); + font-size: 14px; +} + +.fc .fc-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.fc .fc-toolbar-title { + font-weight: 700; + font-size: 16px; +} + +.fc .fc-button { + border: 1px solid rgba(26, 27, 36, 0.18); + background: #fff; + color: #1a1b24; + padding: 4px 10px; + border-radius: 8px; + cursor: pointer; +} + +.fc .fc-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.fc .fc-daygrid { + border: 1px solid var(--fc-border-color); + border-radius: 12px; + overflow: hidden; +} + +.fc .fc-scrollgrid, +.fc .fc-scrollgrid table { + border-collapse: collapse; + width: 100%; +} + +.fc .fc-scrollgrid-section > td { + border: none; +} + +.fc .fc-col-header-cell { + background: rgba(246, 239, 230, 0.6); + padding: 6px 0; + border-bottom: 1px solid var(--fc-border-color); + text-align: center; + font-weight: 600; +} + +.fc .fc-daygrid-day { + border-right: 1px solid var(--fc-border-color); + border-bottom: 1px solid var(--fc-border-color); + min-height: 82px; +} + +.fc .fc-daygrid-day:last-child { + border-right: none; +} + +.fc .fc-daygrid-day-frame { + padding: 8px 6px; +} + +.fc .fc-day-today { + background: var(--fc-today-bg-color); +} + +.fc .fc-day-other { + color: rgba(26, 27, 36, 0.35); +} + +.fc .fc-daygrid-day-number { + display: none; +} diff --git a/frontend/src/views/ActivityDetail.vue b/frontend/src/views/ActivityDetail.vue new file mode 100644 index 0000000..1209718 --- /dev/null +++ b/frontend/src/views/ActivityDetail.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/frontend/src/views/ActivityList.vue b/frontend/src/views/ActivityList.vue new file mode 100644 index 0000000..473812c --- /dev/null +++ b/frontend/src/views/ActivityList.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/frontend/src/views/AdminActivities.vue b/frontend/src/views/AdminActivities.vue new file mode 100644 index 0000000..8860a85 --- /dev/null +++ b/frontend/src/views/AdminActivities.vue @@ -0,0 +1,534 @@ + + + + + diff --git a/frontend/src/views/AdminSignups.vue b/frontend/src/views/AdminSignups.vue new file mode 100644 index 0000000..092f886 --- /dev/null +++ b/frontend/src/views/AdminSignups.vue @@ -0,0 +1,63 @@ + + + diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue new file mode 100644 index 0000000..bbdedcb --- /dev/null +++ b/frontend/src/views/LoginView.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/frontend/src/views/MySignups.vue b/frontend/src/views/MySignups.vue new file mode 100644 index 0000000..002ea76 --- /dev/null +++ b/frontend/src/views/MySignups.vue @@ -0,0 +1,61 @@ + + + diff --git a/frontend/src/views/RegisterView.vue b/frontend/src/views/RegisterView.vue new file mode 100644 index 0000000..27449f0 --- /dev/null +++ b/frontend/src/views/RegisterView.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..c18a1ae --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; + +export default defineConfig({ + plugins: [vue()], + server: { + port: 5173 + } +}); diff --git a/张家贝-开题报告.md b/张家贝-开题报告.md new file mode 100644 index 0000000..e7739a9 --- /dev/null +++ b/张家贝-开题报告.md @@ -0,0 +1,23 @@ +# 河南开封科技传媒学院2026届本科生毕业论文(设计) + +# 开题报告 + +
论文(设计)题目面向社区的传统节气文化活动发布与报名系统的设计与实现
学院信息工程学院专业数据科学与大数据技术
姓名张家贝学号2236091064
一、本课题研究意义 + 助力中华优秀传统文化的传承与创新:通过数字化手段,将深奥的节气知识转化为社区居民可参与的互动活动(例如:春分立蛋、冬至包饺子),使传统文化在现代都市生活中活跃起来。 + 提升社区数字化治理与管理效率:传统的社区活动通知往往通过微信群或线下告示栏,信息容易被覆盖而且报名统计繁琐。该系统提供了文化活动的发布平台和报名统计等功能,降低社区工作人员的管理成本。 + 构建和谐社区与增强居民社交联结:以节气活动为纽带,吸引不同年龄段的居民(如年轻人带小孩参加科普,老人参加民俗制作)走出家门。
二、国内外有关本课题的研究动态 + 国内研究现状:国内目前存在大量智慧社区相关的平台,这些平台目前已经实现了智慧社区相关的一部分基础功能,但是在文化活动管理,尤其是传统文化和社区活动相结合的系统比较少见,另外,目前二十四节气文化,以及社区相关活动的信息发布,多依赖于日常聊天工具(如:微信),缺乏一个科普-发布-报名为一体的闭环管理系统。
国外研究现状:国外并没有二十四节气的概念,但是也存在一部分活动管理系统,国外拥有如 Eventbrite、Meetup 等一系列比较成熟的的活动发布与报名的平台,尤其是在用户体验、自动化通知提醒方面具有比较高的研究程度。 + 总结:目前国内外存在的不足之处基本为通用性过强,缺乏一些专业性,系统无法承载二十四节气的文化内涵。
三、本课题研究的基本内容 + 本课题重点研究如何将传统的二十四节气文化与现代社区治理相结合。通过构建一套基于 SpringBoot 的后台支撑系统,实现活动名额的精确控制与居民报名的数字化处理。同时,利用 Vue 框架研究界面的时令化动态渲染技术,旨在解决社区活动管理混乱、文化传播碎片化的问题,最终交付一个具备高易用性、高稳定性的数字化社区服务平台。
四、本课题拟解决的主要问题 + 解决社区文化活动信息“碎片化”与“不对称”的问题:目前社区活动信息散落在微信群、朋友圈或线下公示栏,居民容易错过报名时间,信息查阅不便。 + 解决传统报名方式流程相对繁琐以及信息统计低效的问题:传统的纸质登记或简单的在线收集表缺乏名额限制、撤销报名、签到统计等逻辑,导致社区工作人员在后台整理 Excel 时工作量极大。 + 解决传统文化教育脱离生活的问题:很多居民对二十四节气的了解仅停留在课本上,缺乏实践参与感。
五、研究方法 + 文献研究法:首先通过阅读知网、维基百科相关文献,聚焦于技术栈的底层调研与业务域驱动建模,对当前主流的前后端分离架构进行比对,评估 SpringBoot 框架的生态优势,以及Vue.js 在构建交互界面时的优点。
面向对象分析与设计法:在系统开发阶段,采用面向对象的软件开发思想。利用UML建模工具,针对社区管理人员与普通居民的不同需求,绘制用例图、时序图与类图。基于SpringBoot框架的特性,对系统进行模块化拆解,将复杂的活动报名逻辑抽象为具体的对象与服务。
六、主要创新点 + 在技术实现层面,本系统的核心创新在于采用了完全解耦的前后端分离架构。通过将后端SpringBoot逻辑与前端Vue表现层彻底切分,利用RESTful API建立了数据交互链路。这种架构设计降低了系统模块间的耦合度,还允许前后端独立进行版本更替与性能优化。 + 在业务逻辑方面,本系统的创新点在于构建社区服务新模式。与市面上功能单一的管理系统不同,本系统通过将传统民俗知识与社区实际活动挂钩,嵌入“文化科普—实地参与—互动反馈”的完整闭环,让居民在数字化的交互中产生实时的文化共鸣,并解决传统报名方式流程相对繁琐以及信息统计低效的问题,
七、主要参考文献 + [1]栗梁.基于SSM框架的汽车租赁管理系统设计与实现[J].电脑编程技巧与维护,2024,32(1):43-45,52. + [2]武卫翔,吴雪宁,童欣,秦睿,陈海燕.基于Java的第三方物流协同订单管理系统的设计与实现[J].物流科技,2024,42(12):77-81. + [3]丁禹钧,朱一龙,王雪静.基于SpringBoot和Vue的高校学生社团管理系统[J].电脑编程技巧与维护,2025,(9):110-112,165. + [4]丁子木,刘美彤,韩梦杰,曹严,赵礼扬.Vue框架中的MVVM思想的实践与优化[J].电脑编程技巧与维护,2025,(4):76-78.
[5]柳伟卫.Vue.js+Spring Boot全栈开发实战[M].人民邮电出版社,2023.
[6]刘靓丽 HTML5与CSS3在网页前端设计优化中的应用研究[J].电脑知识与技术,2025,21(22),51-53.
[7]翟宝峰,邓明亮 HTML5+CSS3网页设计基础与实战[M].人民邮电出版社,2024,250.
[8]张晓颖,石磊.Web交互界面设计与制作[M].人民邮电出版社:2024,449.
[9]Shangguan S ,Chen W .HTML5-Powered Causal Explanations: Fueling D eep Science Learning[J].World Journal of Innovation and Modern Technology, 2025,12.
[10]K. Nandhini. Veterinary Hospital Management System Using AI Integra +ted Advisory[J]. International Journal of Science, Engineering and Technolog +y, 2025, 13(2).
具体时间及写作进度安排
起止日期主要工作内容
指导教师对开题报告的意见
指导教师签名: 年月日
系部对本课题开题的意见
\ No newline at end of file