diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 00000000..c749844f --- /dev/null +++ b/doc/README.md @@ -0,0 +1,20 @@ +# SAMS系统文档 + +本目录包含SAMS系统的相关文档。 + +## 文档结构 + +- `api.md`: API接口文档,包含所有后端接口的详细说明 + - 认证接口 + - 系统管理接口 + - 活动管理接口 + - 社团管理接口 + - 通用说明 + +## 文档更新 + +本文档会随着系统开发持续更新。如果您发现文档中的任何问题或有改进建议,请及时反馈。 + +## 文档格式 + +所有文档均使用Markdown格式编写,可以使用任何支持Markdown的编辑器或查看器阅读。 \ No newline at end of file diff --git a/doc/api.md b/doc/api.md new file mode 100644 index 00000000..0a1224d6 --- /dev/null +++ b/doc/api.md @@ -0,0 +1,961 @@ +# SAMS系统接口文档 + +## 1. 认证接口 (AuthController) + +### 1.1 用户登录 +- **接口路径**: `/login` +- **请求方式**: POST +- **请求参数**: +```json +{ + "username": "string", // 用户名/学号/邮箱 + "password": "string" // 密码 +} +``` +- **响应结果**: +```json +{ + "code": 200, + "msg": "登录成功", + "data": "JWT令牌" +} +``` + +## 2. 系统管理接口 (sys) + +### 2.1 用户管理 (UserController) +- **接口路径**: `/api/user` +- **功能**: 普通用户管理相关接口 + +#### 2.1.1 修改密码 +- **接口路径**: `/api/user/change-password` +- **请求方式**: PUT +- **请求参数**: +```json +{ + "userId": "number", // 用户ID + "oldPassword": "string", // 旧密码 + "newPassword": "string" // 新密码 +} +``` +- **响应结果**: +```json +{ + "code": 200, + "msg": "密码修改成功", + "data": null +} +``` + +#### 2.1.2 更新个人信息 +- **接口路径**: `/api/user/profile` +- **请求方式**: PUT +- **请求参数**: +```json +{ + "userId": "number", // 用户ID + "nickname": "string", // 昵称 + "email": "string", // 邮箱 + "avatar": "string" // 头像URL +} +``` +- **响应结果**: +```json +{ + "code": 200, + "msg": "个人信息更新成功", + "data": null +} +``` + +#### 2.1.3 获取用户详情 +- **接口路径**: `/api/user/{userId}` +- **请求方式**: GET +- **路径参数**: + - userId: 用户ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "success", + "data": { + "userId": "number", + "username": "string", + "nickname": "string", + "email": "string", + "avatar": "string" + } +} +``` + +#### 2.1.4 上传头像 +- **接口路径**: `/api/user/avatar` +- **请求方式**: POST +- **请求参数**: + - file: 头像文件(MultipartFile) +- **响应结果**: 返回上传后的头像URL字符串 + +### 2.2 管理员用户管理 (AdminUserController) +- **接口路径**: `/api/admin/user` +- **功能**: 管理员用户管理相关接口 + +#### 2.2.1 批量导入用户 +- **接口路径**: `/api/admin/user/import` +- **请求方式**: POST +- **请求参数**: +```json +[ + { + "username": "string", + "password": "string", + "email": "string", + "role": "string" + } +] +``` +- **响应结果**: +```json +{ + "code": 200, + "msg": "用户导入成功", + "data": null +} +``` + +#### 2.2.2 查询用户列表 +- **接口路径**: `/api/admin/user/list` +- **请求方式**: GET +- **请求参数**: + - keyword: 关键字(可选,用于搜索用户名、学号、邮箱) +- **响应结果**: +```json +{ + "code": 200, + "msg": "success", + "data": [ + { + "userId": "number", + "username": "string", + "email": "string", + "role": "string", + "status": "string" + } + ] +} +``` + +#### 2.2.3 更新用户信息 +- **接口路径**: `/api/admin/user/update` +- **请求方式**: PUT +- **请求参数**: +```json +{ + "userId": "number", + "role": "string", + "status": "string", + "password": "string" +} +``` +- **响应结果**: +```json +{ + "code": 200, + "msg": "用户信息更新成功", + "data": null +} +``` + +#### 2.2.4 重置用户密码 +- **接口路径**: `/api/admin/user/reset-password/{userId}` +- **请求方式**: PUT +- **路径参数**: + - userId: 用户ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "密码已重置为 123456", + "data": null +} +``` + +#### 2.2.5 封禁用户 +- **接口路径**: `/api/admin/user/ban/{userId}` +- **请求方式**: PUT +- **路径参数**: + - userId: 用户ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "用户已封禁", + "data": null +} +``` + +#### 2.2.6 删除用户 +- **接口路径**: `/api/admin/user/{userId}` +- **请求方式**: DELETE +- **路径参数**: + - userId: 用户ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "用户已删除", + "data": null +} +``` + +### 2.3 文件管理 (FileController) +- **接口路径**: `/api/file` +- **功能**: 文件上传下载相关接口 +- **说明**: 该模块目前为空,待实现 + +## 3. 活动管理接口 (ams) + +### 3.1 活动管理 (ActivityController) +- **接口路径**: `/api/admin/activity` +- **功能**: 活动创建、修改、查询等 +- **权限要求**: 需要相应角色权限 + +#### 3.1.1 创建活动 +- **接口路径**: `/api/admin/activity/create` +- **请求方式**: POST +- **权限要求**: USER, CLUB_ADMIN +- **请求参数**: +```json +{ + "title": "string", // 活动标题 + "description": "string", // 活动描述 + "startTime": "string", // 开始时间 + "endTime": "string", // 结束时间 + "location": "string", // 活动地点 + "maxParticipants": "number", // 最大参与人数 + "clubId": "number" // 主办社团ID +} +``` +- **响应结果**: +```json +{ + "code": 200, + "msg": "活动创建成功,等待审批", + "data": null +} +``` + +#### 3.1.2 取消活动 +- **接口路径**: `/api/admin/activity/cancel` +- **请求方式**: PUT +- **权限要求**: CLUB_ADMIN +- **请求参数**: + - actId: 活动ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "活动已取消", + "data": null +} +``` + +### 3.2 审批管理 (ApprovalController) +- **接口路径**: `/api/admin/approval` +- **功能**: 活动审批相关接口 +- **权限要求**: 需要相应角色权限 + +#### 3.2.1 审批通过 +- **接口路径**: `/api/admin/approval/approve` +- **请求方式**: POST +- **权限要求**: CLUB_ADMIN, DEPARTMENT_ADMIN, COLLEGE_ADMIN +- **请求参数**: + - apprId: 审批ID + - approverId: 审批人ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "审批已通过", + "data": null +} +``` + +#### 3.2.2 审批拒绝 +- **接口路径**: `/api/admin/approval/reject` +- **请求方式**: POST +- **权限要求**: CLUB_ADMIN, DEPARTMENT_ADMIN, COLLEGE_ADMIN +- **请求参数**: + - apprId: 审批ID + - reason: 拒绝原因 + - approverId: 审批人ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "审批已拒绝", + "data": null +} +``` + +### 3.3 评论管理 (CommentController) +- **接口路径**: `/api/comment` +- **功能**: 活动评论相关接口 +- **权限要求**: 需要相应角色权限 + +#### 3.3.1 发表评论 +- **接口路径**: `/api/comment/add` +- **请求方式**: POST +- **权限要求**: USER +- **请求参数**: + - userId: 用户ID + - actId: 活动ID + - content: 评论内容 + - parentCommentId: 父评论ID(可选,用于回复) +- **响应结果**: +```json +{ + "code": 200, + "msg": "评论成功", + "data": null +} +``` + +#### 3.3.2 删除评论 +- **接口路径**: `/api/comment/delete` +- **请求方式**: DELETE +- **权限要求**: USER, ADMIN +- **请求参数**: + - commentId: 评论ID + - userId: 用户ID + - isAdmin: 是否为管理员 +- **响应结果**: +```json +{ + "code": 200, + "msg": "评论已删除", + "data": null +} +``` + +#### 3.3.3 获取活动评论列表 +- **接口路径**: `/api/comment/list` +- **请求方式**: GET +- **请求参数**: + - actId: 活动ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "success", + "data": [ + { + "commentId": "number", + "userId": "number", + "content": "string", + "createTime": "string" + } + ] +} +``` + +#### 3.3.4 获取评论回复 +- **接口路径**: `/api/comment/replies` +- **请求方式**: GET +- **请求参数**: + - parentCommentId: 父评论ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "success", + "data": [ + { + "commentId": "number", + "userId": "number", + "content": "string", + "createTime": "string" + } + ] +} +``` + +### 3.4 反应管理 (ReactionController) +- **接口路径**: `/api/reaction` +- **功能**: 活动反应(点赞等)相关接口 +- **权限要求**: 需要相应角色权限 + +#### 3.4.1 添加反应 +- **接口路径**: `/api/reaction/add` +- **请求方式**: POST +- **权限要求**: USER +- **请求参数**: + - actId: 活动ID + - userId: 用户ID + - reactionType: 反应类型(LIKE/DISLIKE) +- **响应结果**: +```json +{ + "code": 200, + "msg": "操作成功", + "data": null +} +``` + +#### 3.4.2 取消反应 +- **接口路径**: `/api/reaction/remove` +- **请求方式**: DELETE +- **权限要求**: USER +- **请求参数**: + - actId: 活动ID + - userId: 用户ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "已取消点赞/点踩", + "data": null +} +``` + +#### 3.4.3 获取反应统计 +- **接口路径**: `/api/reaction/stats` +- **请求方式**: GET +- **请求参数**: + - actId: 活动ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "success", + "data": { + "likes": "number", + "dislikes": "number" + } +} +``` + +#### 3.4.4 获取活动反应列表 +- **接口路径**: `/api/reaction/list` +- **请求方式**: GET +- **权限要求**: ADMIN +- **请求参数**: + - actId: 活动ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "success", + "data": [ + { + "reactionId": "number", + "userId": "number", + "reactionType": "string", + "createTime": "string" + } + ] +} +``` + +### 3.5 报名管理 (RegistrationController) +- **接口路径**: `/api/registration` +- **功能**: 活动报名相关接口 +- **权限要求**: 需要相应角色权限 + +#### 3.5.1 报名活动 +- **接口路径**: `/api/registration/register` +- **请求方式**: POST +- **权限要求**: USER +- **请求参数**: + - actId: 活动ID + - userId: 用户ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "报名成功", + "data": null +} +``` + +#### 3.5.2 取消报名 +- **接口路径**: `/api/registration/cancel` +- **请求方式**: PUT +- **权限要求**: USER +- **请求参数**: + - actId: 活动ID + - userId: 用户ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "报名已取消", + "data": null +} +``` + +#### 3.5.3 活动签到 +- **接口路径**: `/api/registration/attend` +- **请求方式**: PUT +- **权限要求**: USER +- **请求参数**: + - actId: 活动ID + - userId: 用户ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "签到成功", + "data": null +} +``` + +#### 3.5.4 获取我的报名列表 +- **接口路径**: `/api/registration/my-registrations` +- **请求方式**: GET +- **权限要求**: USER +- **请求参数**: + - userId: 用户ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "success", + "data": [ + { + "registrationId": "number", + "actId": "number", + "userId": "number", + "status": "string", + "createTime": "string" + } + ] +} +``` + +#### 3.5.5 获取活动报名列表 +- **接口路径**: `/api/registration/list` +- **请求方式**: GET +- **权限要求**: ADMIN, COLLEGE_ADMIN, DEPARTMENT_ADMIN +- **请求参数**: + - actId: 活动ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "success", + "data": [ + { + "registrationId": "number", + "actId": "number", + "userId": "number", + "status": "string", + "createTime": "string" + } + ] +} +``` + +## 4. 社团管理接口 (sms) + +### 4.1 社团管理 (ClubController) +- **接口路径**: `/api/admin/club` +- **功能**: 社团基本信息管理 +- **权限要求**: 需要相应角色权限 + +#### 4.1.1 添加社团 +- **接口路径**: `/api/admin/club/add` +- **请求方式**: POST +- **权限要求**: ADMIN, COLLEGE_ADMIN, DEPARTMENT_ADMIN +- **请求参数**: +```json +{ + "name": "string", // 社团名称 + "description": "string", // 社团描述 + "collegeId": "number", // 所属学院ID + "type": "string" // 社团类型 +} +``` +- **响应结果**: +```json +{ + "code": 200, + "msg": "社团添加成功", + "data": null +} +``` + +#### 4.1.2 更新社团信息 +- **接口路径**: `/api/admin/club/update` +- **请求方式**: PUT +- **权限要求**: ADMIN, CLUB_ADMIN +- **请求参数**: +```json +{ + "clubId": "number", // 社团ID + "name": "string", // 社团名称 + "description": "string", // 社团描述 + "type": "string" // 社团类型 +} +``` +- **响应结果**: +```json +{ + "code": 200, + "msg": "社团信息更新成功", + "data": null +} +``` + +#### 4.1.3 删除社团 +- **接口路径**: `/api/admin/club/delete/{clubId}` +- **请求方式**: DELETE +- **权限要求**: ADMIN, COLLEGE_ADMIN, DEPARTMENT_ADMIN +- **路径参数**: + - clubId: 社团ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "社团删除成功", + "data": null +} +``` + +#### 4.1.4 获取社团详情 +- **接口路径**: `/api/admin/club/{clubId}` +- **请求方式**: GET +- **权限要求**: ADMIN, COLLEGE_ADMIN, DEPARTMENT_ADMIN, CLUB_ADMIN +- **路径参数**: + - clubId: 社团ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "success", + "data": { + "clubId": "number", + "name": "string", + "description": "string", + "collegeId": "number", + "type": "string" + } +} +``` + +#### 4.1.5 查询社团列表 +- **接口路径**: `/api/admin/club/list` +- **请求方式**: GET +- **权限要求**: ADMIN, COLLEGE_ADMIN, DEPARTMENT_ADMIN, CLUB_ADMIN +- **请求参数**: + - keyword: 关键字(可选,用于搜索) + - userId: 用户ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "success", + "data": [ + { + "clubId": "number", + "name": "string", + "description": "string", + "collegeId": "number", + "type": "string" + } + ] +} +``` + +### 4.2 社团成员管理 (ClubUserController) +- **接口路径**: `/api/admin/club/user` +- **功能**: 社团成员管理相关接口 +- **权限要求**: 需要相应角色权限 + +#### 4.2.1 添加社团成员 +- **接口路径**: `/api/admin/club/user/add` +- **请求方式**: POST +- **权限要求**: CLUB_ADMIN +- **请求参数**: + - clubId: 社团ID + - userId: 用户ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "成员已加入社团", + "data": null +} +``` + +#### 4.2.2 移除社团成员 +- **接口路径**: `/api/admin/club/user/remove` +- **请求方式**: DELETE +- **权限要求**: CLUB_ADMIN, COLLEGE_ADMIN +- **请求参数**: + - clubId: 社团ID + - userId: 用户ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "成员已移除", + "data": null +} +``` + +#### 4.2.3 查询社团成员列表 +- **接口路径**: `/api/admin/club/user/list` +- **请求方式**: GET +- **权限要求**: ADMIN, COLLEGE_ADMIN, DEPARTMENT_ADMIN +- **请求参数**: + - clubId: 社团ID + - userId: 用户ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "success", + "data": [ + { + "userId": "number", + "username": "string", + "role": "string" + } + ] +} +``` + +#### 4.2.4 查看我的社团成员 +- **接口路径**: `/api/admin/club/user/my-club-members` +- **请求方式**: GET +- **权限要求**: CLUB_ADMIN +- **请求参数**: + - clubId: 社团ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "success", + "data": [ + { + "userId": "number", + "username": "string", + "role": "string" + } + ] +} +``` + +### 4.3 学院管理 (CollegeController) +- **接口路径**: `/api/admin/college` +- **功能**: 学院信息管理相关接口 +- **权限要求**: 需要相应角色权限 + +#### 4.3.1 添加学院 +- **接口路径**: `/api/admin/college/add` +- **请求方式**: POST +- **权限要求**: ADMIN, COLLEGE_ADMIN +- **请求参数**: +```json +{ + "name": "string", // 学院名称 + "description": "string", // 学院描述 + "code": "string" // 学院代码 +} +``` +- **响应结果**: +```json +{ + "code": 200, + "msg": "学院添加成功", + "data": null +} +``` + +#### 4.3.2 更新学院信息 +- **接口路径**: `/api/admin/college/update` +- **请求方式**: PUT +- **权限要求**: ADMIN, COLLEGE_ADMIN +- **请求参数**: +```json +{ + "collegeId": "number", // 学院ID + "name": "string", // 学院名称 + "description": "string", // 学院描述 + "code": "string" // 学院代码 +} +``` +- **响应结果**: +```json +{ + "code": 200, + "msg": "学院更新成功", + "data": null +} +``` + +#### 4.3.3 删除学院 +- **接口路径**: `/api/admin/college/delete/{collegeId}` +- **请求方式**: DELETE +- **权限要求**: ADMIN, COLLEGE_ADMIN +- **路径参数**: + - collegeId: 学院ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "学院删除成功", + "data": null +} +``` + +#### 4.3.4 获取学院详情 +- **接口路径**: `/api/admin/college/{collegeId}` +- **请求方式**: GET +- **权限要求**: ADMIN, COLLEGE_ADMIN +- **路径参数**: + - collegeId: 学院ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "success", + "data": { + "collegeId": "number", + "name": "string", + "description": "string", + "code": "string" + } +} +``` + +#### 4.3.5 获取我的学院信息 +- **接口路径**: `/api/admin/college/my-college` +- **请求方式**: GET +- **权限要求**: DEPARTMENT_ADMIN +- **请求参数**: + - userId: 用户ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "success", + "data": { + "collegeId": "number", + "name": "string", + "description": "string", + "code": "string" + } +} +``` + +#### 4.3.6 查询学院列表 +- **接口路径**: `/api/admin/college/list` +- **请求方式**: GET +- **权限要求**: ADMIN, COLLEGE_ADMIN, DEPARTMENT_ADMIN +- **请求参数**: + - keyword: 关键字(可选,用于搜索) + - userId: 用户ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "success", + "data": [ + { + "collegeId": "number", + "name": "string", + "description": "string", + "code": "string" + } + ] +} +``` + +#### 4.3.7 指派学院负责人 +- **接口路径**: `/api/admin/college/assign` +- **请求方式**: POST +- **权限要求**: ADMIN, COLLEGE_ADMIN +- **请求参数**: + - collegeId: 学院ID + - userId: 用户ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "负责人指派成功", + "data": null +} +``` + +#### 4.3.8 移除学院负责人 +- **接口路径**: `/api/admin/college/remove` +- **请求方式**: DELETE +- **权限要求**: ADMIN, COLLEGE_ADMIN +- **请求参数**: + - collegeId: 学院ID + - userId: 用户ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "负责人移除成功", + "data": null +} +``` + +#### 4.3.9 获取学院负责人 +- **接口路径**: `/api/admin/college/getleader/{collegeId}` +- **请求方式**: GET +- **权限要求**: ADMIN, COLLEGE_ADMIN, DEPARTMENT_ADMIN +- **路径参数**: + - collegeId: 学院ID +- **请求参数**: + - userId: 用户ID +- **响应结果**: +```json +{ + "code": 200, + "msg": "success", + "data": { + "userId": "number", + "username": "string", + "role": "string" + } +} +``` + +## 通用说明 + +1. **认证方式**: + - 除登录接口外,所有接口都需要在请求头中携带 JWT Token + - Token 格式:`Authorization: Bearer ` + +2. **响应格式**: + - 所有接口统一使用 `AjaxResult` 封装响应 + - 成功响应示例: + ```json + { + "code": 200, + "msg": "操作成功", + "data": {} + } + ``` + - 失败响应示例: + ```json + { + "code": 500, + "msg": "操作失败", + "data": null + } + ``` + +3. **请求方式**: + - 查询类接口使用 GET + - 创建类接口使用 POST + - 更新类接口使用 PUT + - 删除类接口使用 DELETE + +4. **数据格式**: + - 请求和响应均使用 JSON 格式 + - 日期时间格式统一使用 ISO 8601 标准 \ No newline at end of file diff --git a/pic/学生端界面/个人中心@1x.jpg b/pic/学生端界面/个人中心@1x.jpg new file mode 100644 index 00000000..f0a9927d Binary files /dev/null and b/pic/学生端界面/个人中心@1x.jpg differ diff --git a/pic/学生端界面/活动@1x.jpg b/pic/学生端界面/活动@1x.jpg new file mode 100644 index 00000000..e69de29b diff --git a/pic/学生端界面/社团中心@1x.jpg b/pic/学生端界面/社团中心@1x.jpg new file mode 100644 index 00000000..e69de29b diff --git a/pic/学生端界面/首页@1x.jpg b/pic/学生端界面/首页@1x.jpg new file mode 100644 index 00000000..a34f2b51 Binary files /dev/null and b/pic/学生端界面/首页@1x.jpg differ diff --git a/pic/管理端界面/审批列表@1x.jpg b/pic/管理端界面/审批列表@1x.jpg new file mode 100644 index 00000000..e69de29b diff --git a/pic/管理端界面/成员管理@1x.jpg b/pic/管理端界面/成员管理@1x.jpg new file mode 100644 index 00000000..e69de29b diff --git a/pic/管理端界面/活动管理@1x.jpg b/pic/管理端界面/活动管理@1x.jpg new file mode 100644 index 00000000..b84f67ed Binary files /dev/null and b/pic/管理端界面/活动管理@1x.jpg differ diff --git a/pic/管理端界面/活动管理(待审批@1x.jpg b/pic/管理端界面/活动管理(待审批@1x.jpg new file mode 100644 index 00000000..5df56c1d Binary files /dev/null and b/pic/管理端界面/活动管理(待审批@1x.jpg differ diff --git a/pic/管理端界面/社团列表@1x.jpg b/pic/管理端界面/社团列表@1x.jpg new file mode 100644 index 00000000..e69de29b diff --git a/pic/管理端界面/首页@1x.jpg b/pic/管理端界面/首页@1x.jpg new file mode 100644 index 00000000..4b0c42ee Binary files /dev/null and b/pic/管理端界面/首页@1x.jpg differ diff --git a/sams-ui/package.json b/sams-ui/package.json index bcafc86d..163e8ecf 100644 --- a/sams-ui/package.json +++ b/sams-ui/package.json @@ -9,8 +9,10 @@ "preview": "vite preview" }, "dependencies": { + "@element-plus/icons-vue": "^2.3.1", "axios": "^1.8.4", "element-plus": "^2.9.7", + "icons-vue": "^1.0.9", "jwt-decode": "^4.0.0", "pinia": "^3.0.1", "vue": "^3.5.13", diff --git a/sams-ui/pnpm-lock.yaml b/sams-ui/pnpm-lock.yaml index 4057dd75..276c236c 100644 --- a/sams-ui/pnpm-lock.yaml +++ b/sams-ui/pnpm-lock.yaml @@ -8,28 +8,34 @@ importers: .: dependencies: + '@element-plus/icons-vue': + specifier: ^2.3.1 + version: 2.3.1(vue@3.5.13(typescript@4.9.5)) axios: specifier: ^1.8.4 version: 1.8.4 element-plus: specifier: ^2.9.7 - version: 2.9.7(vue@3.5.13) + version: 2.9.7(vue@3.5.13(typescript@4.9.5)) + icons-vue: + specifier: ^1.0.9 + version: 1.0.9(vue@3.5.13(typescript@4.9.5)) jwt-decode: specifier: ^4.0.0 version: 4.0.0 pinia: specifier: ^3.0.1 - version: 3.0.1(vue@3.5.13) + version: 3.0.1(typescript@4.9.5)(vue@3.5.13(typescript@4.9.5)) vue: specifier: ^3.5.13 - version: 3.5.13 + version: 3.5.13(typescript@4.9.5) vue-router: specifier: '4' - version: 4.5.0(vue@3.5.13) + version: 4.5.0(vue@3.5.13(typescript@4.9.5)) devDependencies: '@vitejs/plugin-vue': specifier: ^5.2.1 - version: 5.2.3(vite@6.2.2)(vue@3.5.13) + version: 5.2.3(vite@6.2.2)(vue@3.5.13(typescript@4.9.5)) vite: specifier: ^6.2.0 version: 6.2.2 @@ -513,6 +519,11 @@ packages: hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + icons-vue@1.0.9: + resolution: {integrity: sha512-9/yH38wV4fRmPq3vncy5pofA0jfNlT8UDzXJOtRLGn8ap8Gy6B9YBO4Unj6pz5jg4MdmN49D6IiqDzqr5Ec2IQ==} + peerDependencies: + vue: '>=3.0.3' + is-what@4.1.16: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} engines: {node: '>=12.13'} @@ -605,6 +616,11 @@ packages: resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} engines: {node: '>=16'} + typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + vite@6.2.2: resolution: {integrity: sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -686,9 +702,9 @@ snapshots: '@ctrl/tinycolor@3.6.1': {} - '@element-plus/icons-vue@2.3.1(vue@3.5.13)': + '@element-plus/icons-vue@2.3.1(vue@3.5.13(typescript@4.9.5))': dependencies: - vue: 3.5.13 + vue: 3.5.13(typescript@4.9.5) '@esbuild/aix-ppc64@0.25.1': optional: true @@ -847,10 +863,10 @@ snapshots: '@types/web-bluetooth@0.0.16': {} - '@vitejs/plugin-vue@5.2.3(vite@6.2.2)(vue@3.5.13)': + '@vitejs/plugin-vue@5.2.3(vite@6.2.2)(vue@3.5.13(typescript@4.9.5))': dependencies: vite: 6.2.2 - vue: 3.5.13 + vue: 3.5.13(typescript@4.9.5) '@vue/compiler-core@3.5.13': dependencies: @@ -918,29 +934,29 @@ snapshots: '@vue/shared': 3.5.13 csstype: 3.1.3 - '@vue/server-renderer@3.5.13(vue@3.5.13)': + '@vue/server-renderer@3.5.13(vue@3.5.13(typescript@4.9.5))': dependencies: '@vue/compiler-ssr': 3.5.13 '@vue/shared': 3.5.13 - vue: 3.5.13 + vue: 3.5.13(typescript@4.9.5) '@vue/shared@3.5.13': {} - '@vueuse/core@9.13.0(vue@3.5.13)': + '@vueuse/core@9.13.0(vue@3.5.13(typescript@4.9.5))': dependencies: '@types/web-bluetooth': 0.0.16 '@vueuse/metadata': 9.13.0 - '@vueuse/shared': 9.13.0(vue@3.5.13) - vue-demi: 0.14.10(vue@3.5.13) + '@vueuse/shared': 9.13.0(vue@3.5.13(typescript@4.9.5)) + vue-demi: 0.14.10(vue@3.5.13(typescript@4.9.5)) transitivePeerDependencies: - '@vue/composition-api' - vue '@vueuse/metadata@9.13.0': {} - '@vueuse/shared@9.13.0(vue@3.5.13)': + '@vueuse/shared@9.13.0(vue@3.5.13(typescript@4.9.5))': dependencies: - vue-demi: 0.14.10(vue@3.5.13) + vue-demi: 0.14.10(vue@3.5.13(typescript@4.9.5)) transitivePeerDependencies: - '@vue/composition-api' - vue @@ -984,15 +1000,15 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - element-plus@2.9.7(vue@3.5.13): + element-plus@2.9.7(vue@3.5.13(typescript@4.9.5)): dependencies: '@ctrl/tinycolor': 3.6.1 - '@element-plus/icons-vue': 2.3.1(vue@3.5.13) + '@element-plus/icons-vue': 2.3.1(vue@3.5.13(typescript@4.9.5)) '@floating-ui/dom': 1.6.13 '@popperjs/core': '@sxzz/popperjs-es@2.11.7' '@types/lodash': 4.17.16 '@types/lodash-es': 4.17.12 - '@vueuse/core': 9.13.0(vue@3.5.13) + '@vueuse/core': 9.13.0(vue@3.5.13(typescript@4.9.5)) async-validator: 4.2.5 dayjs: 1.11.13 escape-html: 1.0.3 @@ -1001,7 +1017,7 @@ snapshots: lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21) memoize-one: 6.0.0 normalize-wheel-es: 1.2.0 - vue: 3.5.13 + vue: 3.5.13(typescript@4.9.5) transitivePeerDependencies: - '@vue/composition-api' @@ -1100,6 +1116,11 @@ snapshots: hookable@5.5.3: {} + icons-vue@1.0.9(vue@3.5.13(typescript@4.9.5)): + dependencies: + typescript: 4.9.5 + vue: 3.5.13(typescript@4.9.5) + is-what@4.1.16: {} jwt-decode@4.0.0: {} @@ -1138,10 +1159,12 @@ snapshots: picocolors@1.1.1: {} - pinia@3.0.1(vue@3.5.13): + pinia@3.0.1(typescript@4.9.5)(vue@3.5.13(typescript@4.9.5)): dependencies: '@vue/devtools-api': 7.7.2 - vue: 3.5.13 + vue: 3.5.13(typescript@4.9.5) + optionalDependencies: + typescript: 4.9.5 postcss@8.5.3: dependencies: @@ -1186,6 +1209,8 @@ snapshots: dependencies: copy-anything: 3.0.5 + typescript@4.9.5: {} + vite@6.2.2: dependencies: esbuild: 0.25.1 @@ -1194,19 +1219,21 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - vue-demi@0.14.10(vue@3.5.13): + vue-demi@0.14.10(vue@3.5.13(typescript@4.9.5)): dependencies: - vue: 3.5.13 + vue: 3.5.13(typescript@4.9.5) - vue-router@4.5.0(vue@3.5.13): + vue-router@4.5.0(vue@3.5.13(typescript@4.9.5)): dependencies: '@vue/devtools-api': 6.6.4 - vue: 3.5.13 + vue: 3.5.13(typescript@4.9.5) - vue@3.5.13: + vue@3.5.13(typescript@4.9.5): dependencies: '@vue/compiler-dom': 3.5.13 '@vue/compiler-sfc': 3.5.13 '@vue/runtime-dom': 3.5.13 - '@vue/server-renderer': 3.5.13(vue@3.5.13) + '@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@4.9.5)) '@vue/shared': 3.5.13 + optionalDependencies: + typescript: 4.9.5 diff --git a/sams-ui/src/components/student/ActivityList.vue b/sams-ui/src/components/student/ActivityList.vue new file mode 100644 index 00000000..dccff42d --- /dev/null +++ b/sams-ui/src/components/student/ActivityList.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/sams-ui/src/components/student/ClubCarousel.vue b/sams-ui/src/components/student/ClubCarousel.vue new file mode 100644 index 00000000..3f6029b3 --- /dev/null +++ b/sams-ui/src/components/student/ClubCarousel.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/sams-ui/src/components/student/NewsList.vue b/sams-ui/src/components/student/NewsList.vue new file mode 100644 index 00000000..6cd2490d --- /dev/null +++ b/sams-ui/src/components/student/NewsList.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/sams-ui/src/layout/AdminLayout.vue b/sams-ui/src/layout/AdminLayout.vue new file mode 100644 index 00000000..59b9afdc --- /dev/null +++ b/sams-ui/src/layout/AdminLayout.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/sams-ui/src/layout/StudentLayout.vue b/sams-ui/src/layout/StudentLayout.vue new file mode 100644 index 00000000..87bf8335 --- /dev/null +++ b/sams-ui/src/layout/StudentLayout.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/sams-ui/src/router/index.js b/sams-ui/src/router/index.js index db437b71..ba5a3061 100644 --- a/sams-ui/src/router/index.js +++ b/sams-ui/src/router/index.js @@ -1,19 +1,52 @@ import { createRouter, createWebHistory } from 'vue-router' import Login from '@/views/login/index.vue' +import UserHome from '@/views/student/home/index.vue' +import AdminLayout from '@/layout/AdminLayout.vue' +import AdminDashboard from '@/views/admin/dashboard.vue' + +// 获取用户角色(从 token 中或 localStorage 中) +function getUserRole() { + // 示例:从 localStorage 模拟获取 + const role = localStorage.getItem('userRole') + return role || '' +} const routes = [ { path: '/', - redirect: '/login' // 默认重定向到登录页 + redirect: '/login' }, { path: '/login', + name: 'Login', component: Login }, + + // 学生端主页(角色为 PARTICIPANT) { - path: '/user/home', - component: () => import('@/views/home/UserHome.vue'), + path: '/student/home', + name: 'UserHome', + component: UserHome, meta: { roles: ['PARTICIPANT'] } + }, + + // 管理端页面(角色为 COLLEGE_ADMIN / DEPARTMENT_ADMIN / CLUB_ADMIN) + { + path: '/admin', + component: AdminLayout, + meta: { roles: ['COLLEGE_ADMIN', 'DEPARTMENT_ADMIN', 'CLUB_ADMIN'] }, + children: [ + { + path: 'dashboard', + name: 'AdminDashboard', + component: AdminDashboard + } + ] + }, + { + path: '/403', + name: 'Forbidden', + component: () => import('@/views/error/403.vue') } ] @@ -22,4 +55,37 @@ const router = createRouter({ routes }) +// 全局路由守卫:根据角色权限控制访问页面 +router.beforeEach((to, from, next) => { + const token = localStorage.getItem('token') + const raw = localStorage.getItem('userRoles') + let roles = [] + try { + roles = JSON.parse(raw) || [] + } catch { + roles = [] + } + + if (to.path === '/login') return next() + + if (!token) return next('/login') + + // 权限校验失败时避免无限重定向 + if (to.meta.roles && !roles.some(r => to.meta.roles.includes(r))) { + if (roles.includes('PARTICIPANT') && to.path !== '/student/home') { + return next('/student/home') + } else if ( + roles.some(r => ['COLLEGE_ADMIN', 'DEPARTMENT_ADMIN', 'CLUB_ADMIN'].includes(r)) && + to.path !== '/admin/dashboard' + ) { + return next('/admin/dashboard') + } else { + return next('/403') + } + } + + next() +}) + + export default router diff --git a/sams-ui/src/views/admin/dashboard.vue b/sams-ui/src/views/admin/dashboard.vue new file mode 100644 index 00000000..a364cc69 --- /dev/null +++ b/sams-ui/src/views/admin/dashboard.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/sams-ui/src/views/error/403.vue b/sams-ui/src/views/error/403.vue new file mode 100644 index 00000000..78c94ded --- /dev/null +++ b/sams-ui/src/views/error/403.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/sams-ui/src/views/home/UserHome.vue b/sams-ui/src/views/home/UserHome.vue deleted file mode 100644 index 5a6e1b3f..00000000 --- a/sams-ui/src/views/home/UserHome.vue +++ /dev/null @@ -1,177 +0,0 @@ - - - - - diff --git a/sams-ui/src/views/login/index.vue b/sams-ui/src/views/login/index.vue index 91ebdedd..a3aef5d4 100644 --- a/sams-ui/src/views/login/index.vue +++ b/sams-ui/src/views/login/index.vue @@ -33,6 +33,8 @@ import { ref } from 'vue' import { useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import { login } from '@/api/auth' +import { jwtDecode } from 'jwt-decode' +import { nextTick } from 'vue' const router = useRouter() const loginRef = ref(null) @@ -56,17 +58,28 @@ const submitLogin = () => { try { const token = await login(loginForm.value) localStorage.setItem('token', token) - ElMessage.success('登录成功') - // 登录成功后 + const payload = jwtDecode(token) const roles = Array.isArray(payload.role) ? payload.role : [payload.role] + localStorage.setItem('userRoles', JSON.stringify(roles)) - if (roles.includes('PARTICIPANT')) { - router.push('/user/home') - } else if (roles.includes('COLLEGE_ADMIN') ||roles.includes('DEPARTMENT_ADMIN') ||roles.includes('CLUB_ADMIN') ) { - router.push('/admin/home') - } else { - router.push('/403') // 无匹配权限 + ElMessage.success('登录成功') + + + await nextTick() + + + const currentPath = router.currentRoute.value.path + + if (roles.includes('PARTICIPANT') && currentPath !== '/student/home') { + router.push('/student/home') + } else if ( + roles.some(r => ['COLLEGE_ADMIN', 'DEPARTMENT_ADMIN', 'CLUB_ADMIN'].includes(r)) && + currentPath !== '/admin/dashboard' + ) { + router.push('/admin/dashboard') + } else if (currentPath !== '/403') { + router.push('/403') } } catch (e) { ElMessage.error(e.message || '登录失败') diff --git a/sams-ui/src/views/student/home/index.vue b/sams-ui/src/views/student/home/index.vue new file mode 100644 index 00000000..874bf35a --- /dev/null +++ b/sams-ui/src/views/student/home/index.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/src/test/java/com/bruce/sams/service/impl/AuthServiceImplTest.java b/src/test/java/com/bruce/sams/service/impl/AuthServiceImplTest.java index 68516180..938dabe6 100644 --- a/src/test/java/com/bruce/sams/service/impl/AuthServiceImplTest.java +++ b/src/test/java/com/bruce/sams/service/impl/AuthServiceImplTest.java @@ -1,6 +1,5 @@ package com.bruce.sams.service.impl; -import com.bruce.sams.common.utils.PasswordUtil; import com.bruce.sams.domain.entity.LoginRequest; import com.bruce.sams.service.AuthService; import org.junit.jupiter.api.Test; @@ -8,7 +7,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import static org.junit.jupiter.api.Assertions.*; @SpringBootTest class AuthServiceImplTest { @@ -30,4 +28,4 @@ class AuthServiceImplTest { // authService.authenticate(loginRequest); } -} \ No newline at end of file +} diff --git a/src/test/java/com/bruce/sams/service/impl/TokenUtilTest.java b/src/test/java/com/bruce/sams/service/impl/TokenUtilTest.java new file mode 100644 index 00000000..d3e007fb --- /dev/null +++ b/src/test/java/com/bruce/sams/service/impl/TokenUtilTest.java @@ -0,0 +1,30 @@ +package com.bruce.sams.service.impl; + +import com.bruce.sams.common.utils.TokenUtil; +import com.bruce.sams.domain.sys.User; +import com.bruce.sams.service.UserService; +import io.jsonwebtoken.Claims; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.Arrays; +import java.util.List; + +/** + * TokenUtil 工具类单元测试 + */ +public class TokenUtilTest { + + + @Test + public void testGetRoleFromToken() { + + + String token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2Iiwicm9sZSI6WyJQQVJUSUNJUEFOVCJdLCJpYXQiOjE3NDI5MTg2MzAsImV4cCI6MTc0MjkyMjIzMH0.si-0JKvdzxxXHWPFRozc0EOSa6Vp5lF4pE1KWKD2fRY"; + + String roleStr = TokenUtil.getRoleFromToken(token); + System.out.println(roleStr); + } +} \ No newline at end of file