v0.3.1 前端重做

master
bruce 2025-03-26 00:17:26 +08:00
parent 260a85d8f3
commit 89775e7084
27 changed files with 1711 additions and 219 deletions

20
doc/README.md 100644
View File

@ -0,0 +1,20 @@
# SAMS系统文档
本目录包含SAMS系统的相关文档。
## 文档结构
- `api.md`: API接口文档包含所有后端接口的详细说明
- 认证接口
- 系统管理接口
- 活动管理接口
- 社团管理接口
- 通用说明
## 文档更新
本文档会随着系统开发持续更新。如果您发现文档中的任何问题或有改进建议,请及时反馈。
## 文档格式
所有文档均使用Markdown格式编写可以使用任何支持Markdown的编辑器或查看器阅读。

961
doc/api.md 100644
View File

@ -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 <token>`
2. **响应格式**
- 所有接口统一使用 `AjaxResult` 封装响应
- 成功响应示例:
```json
{
"code": 200,
"msg": "操作成功",
"data": {}
}
```
- 失败响应示例:
```json
{
"code": 500,
"msg": "操作失败",
"data": null
}
```
3. **请求方式**
- 查询类接口使用 GET
- 创建类接口使用 POST
- 更新类接口使用 PUT
- 删除类接口使用 DELETE
4. **数据格式**
- 请求和响应均使用 JSON 格式
- 日期时间格式统一使用 ISO 8601 标准

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

View File

@ -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",

View File

@ -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

View File

@ -0,0 +1,56 @@
<template>
<el-row :gutter="20">
<el-col v-for="item in activities" :key="item.actId" :span="6">
<el-card class="activity-card" shadow="hover">
<h3 class="title">{{ item.title }}</h3>
<div class="meta">
<span><el-icon><Location /></el-icon> {{ item.location }}</span>
<span><el-icon><Calendar /></el-icon> {{ formatDate(item.startTime) }}</span>
</div>
<p class="desc">{{ item.description }}</p>
<el-button type="primary" size="small" @click="join(item.actId)"></el-button>
</el-card>
</el-col>
</el-row>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Location, Calendar } from '@element-plus/icons-vue'
// import { listHotActivities } from '@/api/activity'
import { ElMessage } from 'element-plus'
const activities = ref([])
onMounted(async () => {
activities.value = await listHotActivities()
})
const formatDate = (str) => new Date(str).toLocaleString()
const join = (actId) => {
ElMessage.success(`报名成功活动ID${actId}`)
}
</script>
<style scoped>
.activity-card {
margin-bottom: 20px;
}
.title {
font-size: 16px;
font-weight: bold;
}
.meta {
font-size: 12px;
color: #888;
display: flex;
justify-content: space-between;
margin: 8px 0;
}
.desc {
font-size: 14px;
color: #555;
margin: 10px 0;
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<el-carousel height="200px" :interval="4000" type="card">
<el-carousel-item v-for="club in clubs" :key="club.clubId">
<div class="club-card">
<img class="club-img" :src="club.logo || '/default-club.png'" />
<h3>{{ club.clubName }}</h3>
<p>{{ club.description }}</p>
</div>
</el-carousel-item>
</el-carousel>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// import { listTopClubs } from '@/api/club'
const clubs = ref([])
onMounted(async () => {
clubs.value = await listTopClubs()
})
</script>
<style scoped>
.club-card {
text-align: center;
padding: 20px;
}
.club-img {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
margin-bottom: 10px;
}
h3 {
font-size: 16px;
margin: 5px 0;
}
p {
font-size: 13px;
color: #777;
}
</style>

View File

@ -0,0 +1,53 @@
<template>
<el-row :gutter="20">
<el-col v-for="item in newsList" :key="item.id" :span="8">
<el-card class="news-card" shadow="hover">
<h3 class="news-title">{{ item.title }}</h3>
<p class="news-desc">{{ item.summary }}</p>
<div class="news-footer">
<el-icon><Clock /></el-icon>
<span class="time">{{ formatTime(item.createTime) }}</span>
</div>
</el-card>
</el-col>
</el-row>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Clock } from '@element-plus/icons-vue'
// import { listLatestNews } from '@/api/news'
const newsList = ref([])
onMounted(async () => {
newsList.value = await listLatestNews()
})
const formatTime = (str) => new Date(str).toLocaleDateString()
</script>
<style scoped>
.news-card {
margin-bottom: 20px;
}
.news-title {
font-weight: bold;
font-size: 16px;
margin-bottom: 10px;
}
.news-desc {
font-size: 14px;
color: #666;
}
.news-footer {
display: flex;
align-items: center;
font-size: 12px;
color: #999;
margin-top: 10px;
}
.time {
margin-left: 4px;
}
</style>

View File

@ -0,0 +1,118 @@
<template>
<el-container class="admin-layout">
<!-- 左侧菜单 -->
<el-aside width="220px" class="sidebar">
<div class="logo">社团后台管理</div>
<el-menu
router
class="el-menu-vertical-demo"
background-color="#304156"
text-color="#fff"
active-text-color="#409EFF"
:default-active="activePath"
>
<el-menu-item index="/admin/dashboard">
<el-icon><HomeFilled /></el-icon>
<span>首页</span>
</el-menu-item>
<el-menu-item index="/admin/activity">
<el-icon><List /></el-icon>
<span>活动管理</span>
</el-menu-item>
<el-menu-item index="/admin/club">
<el-icon><User /></el-icon>
<span>社团管理</span>
</el-menu-item>
<el-menu-item index="/admin/user">
<el-icon><Avatar /></el-icon>
<span>用户管理</span>
</el-menu-item>
<el-menu-item index="/admin/approval">
<el-icon><Check /></el-icon>
<span>审批中心</span>
</el-menu-item>
</el-menu>
</el-aside>
<!-- 右侧主内容 -->
<el-container>
<!-- 顶部导航栏 -->
<el-header class="top-header">
<div class="right">
<el-dropdown>
<span class="el-dropdown-link">
<el-avatar size="small" src="/avatar.png" />
<span class="username">管理员</span>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="goProfile"></el-dropdown-item>
<el-dropdown-item divided @click="logout">退</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<!-- 内容展示区 -->
<el-main class="main-content">
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import { useRoute, useRouter } from 'vue-router'
import { HomeFilled, List, User, Avatar, Check } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const router = useRouter()
const route = useRoute()
const activePath = route.path
const logout = () => {
localStorage.removeItem('token')
ElMessage.success('退出成功')
router.push('/login')
}
const goProfile = () => {
ElMessage.info('暂未开放个人中心')
}
</script>
<style scoped>
.admin-layout {
height: 100vh;
}
.logo {
height: 60px;
color: #fff;
text-align: center;
line-height: 60px;
font-size: 18px;
font-weight: bold;
background-color: #1f2d3d;
}
.sidebar {
background-color: #304156;
color: #fff;
}
.top-header {
display: flex;
justify-content: flex-end;
align-items: center;
background-color: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding-right: 30px;
}
.username {
margin-left: 10px;
}
.main-content {
padding: 20px;
background-color: #f5f6fa;
min-height: calc(100vh - 60px);
}
</style>

View File

@ -0,0 +1,58 @@
<template>
<div class="layout">
<el-header class="layout-header">
<div class="logo">社团活动系统</div>
<el-menu mode="horizontal" router>
<el-menu-item index="/">首页</el-menu-item>
<el-menu-item index="/club">社团</el-menu-item>
<el-menu-item index="/activity">活动</el-menu-item>
<el-menu-item index="/notice">公告</el-menu-item>
</el-menu>
<div class="user-area">
<el-avatar size="small" src="/avatar.png" />
<span>欢迎你张三</span>
</div>
</el-header>
<el-main class="layout-main">
<slot />
</el-main>
<el-footer class="layout-footer">
<div>© 山东建筑大学 - 校园社团活动系统</div>
</el-footer>
</div>
</template>
<script setup>
</script>
<style scoped>
.layout-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #409EFF;
color: #fff;
padding: 0 30px;
}
.logo {
font-size: 20px;
font-weight: bold;
}
.user-area {
display: flex;
align-items: center;
gap: 10px;
}
.layout-main {
padding: 40px;
background: #f8f9fa;
min-height: calc(100vh - 120px);
}
.layout-footer {
text-align: center;
padding: 20px;
color: #888;
}
</style>

View File

@ -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

View File

@ -0,0 +1,147 @@
<template>
<admin-layout>
<div class="dashboard">
<h2 class="welcome-text">你好管理员欢迎使用社团管理系统</h2>
<!-- 顶部数据卡片 -->
<el-row :gutter="20" class="stat-boxes">
<el-col :span="6" v-for="stat in stats" :key="stat.label">
<el-card>
<div class="stat-card">
<el-icon :size="28">{{ stat.icon }}</el-icon>
<div class="stat-info">
<div class="stat-label">{{ stat.label }}</div>
<div class="stat-value">{{ stat.value }}</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 活动列表 -->
<section class="section">
<div class="section-header">
<h3>活动列表</h3>
<el-input v-model="activitySearch" placeholder="搜索活动名称" size="small" class="search-box" />
<el-button type="primary" size="small"> 新建活动</el-button>
</div>
<el-table :data="filteredActivities" border>
<el-table-column prop="title" label="活动名称" />
<el-table-column prop="club" label="申请社团" />
<el-table-column prop="time" label="活动时间" />
<el-table-column prop="place" label="活动地点" />
<el-table-column prop="status" label="状态" />
<el-table-column label="操作">
<template #default>
<el-button size="small" type="primary" link>查看</el-button>
<el-button size="small" type="success" link>审批</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination layout="prev, pager, next" :total="24" background class="mt-4" />
</section>
<!-- 社团列表 -->
<section class="section">
<div class="section-header">
<h3>社团列表</h3>
<el-input v-model="clubSearch" placeholder="搜索社团名称" size="small" class="search-box" />
<el-button type="primary" size="small"> 新建社团</el-button>
</div>
<el-table :data="filteredClubs" border>
<el-table-column prop="name" label="社团名称" />
<el-table-column prop="type" label="社团类型" />
<el-table-column prop="createTime" label="成立时间" />
<el-table-column prop="leader" label="指导老师" />
<el-table-column prop="members" label="成员数量" />
<el-table-column prop="status" label="状态" />
<el-table-column label="操作">
<template #default>
<el-button size="small" type="primary" link>查看</el-button>
<el-button size="small" type="warning" link>编辑</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination layout="prev, pager, next" :total="56" background class="mt-4" />
</section>
</div>
</admin-layout>
</template>
<script setup>
import { Document, User, Check, Avatar } from '@element-plus/icons-vue'
import { ref, reactive, computed, onMounted } from 'vue'
const activitySearch = ref('')
const clubSearch = ref('')
const stats = [
{ label: '待审批活动', value: 12, icon: Document },
{ label: '进行中活动', value: 8, icon: Check },
{ label: '待审核入团', value: 25, icon: User },
{ label: '社团总数', value: 56, icon: Avatar }
]
const activities = [
{ title: '2023年秋新生欢迎会', club: '学生会', time: '2023-11-20 14:00', place: '大学生活动中心', status: '待审批' },
{ title: '摄影社作品展', club: '摄影社', time: '2023-11-25 09:00', place: '图书馆展厅', status: '进行中' },
{ title: '街舞社年度汇演', club: '街舞社', time: '2023-12-01 19:00', place: '大礼堂', status: '已结束' }
]
const filteredActivities = computed(() => {
return activities.filter(act => act.title.includes(activitySearch.value))
})
const clubs = [
{ name: '学生会', type: '学生组织', createTime: '2010-09-01', leader: '王教武', members: 126, status: '正常' },
{ name: '摄影社', type: '艺术类', createTime: '2015-03-15', leader: '李明远', members: 45, status: '正常' },
{ name: '街舞社', type: '体育类', createTime: '2018-09-10', leader: '张建国', members: 38, status: '正常' }
]
const filteredClubs = computed(() => {
return clubs.filter(c => c.name.includes(clubSearch.value))
})
</script>
<style scoped>
.dashboard {
padding: 20px;
}
.welcome-text {
font-size: 20px;
margin-bottom: 20px;
}
.stat-boxes {
margin-bottom: 30px;
}
.stat-card {
display: flex;
align-items: center;
gap: 16px;
}
.stat-info {
display: flex;
flex-direction: column;
}
.stat-label {
font-size: 14px;
color: #666;
}
.stat-value {
font-size: 22px;
font-weight: bold;
}
.section {
margin-top: 40px;
}
.section-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.search-box {
width: 200px;
}
</style>

View File

@ -0,0 +1,24 @@
<template>
<div class="error-page">
<h1>403</h1>
<p>抱歉您没有权限访问此页面</p>
<el-button type="primary" @click="$router.push('/')"></el-button>
</div>
</template>
<script setup></script>
<style scoped>
.error-page {
text-align: center;
padding: 100px 20px;
}
.error-page h1 {
font-size: 72px;
color: #f56c6c;
}
.error-page p {
font-size: 20px;
margin: 20px 0;
}
</style>

View File

@ -1,177 +0,0 @@
<template>
<div class="user-home">
<!-- 顶部导航 -->
<el-menu mode="horizontal" class="top-nav" background-color="#2c3e50" text-color="#fff" active-text-color="#ffd04b">
<div class="nav-left">🏠 社团活动管理系统</div>
<el-menu-item index="1">首页</el-menu-item>
<el-menu-item index="2">社团中心</el-menu-item>
<el-menu-item index="3">活动</el-menu-item>
<el-menu-item index="4">公告</el-menu-item>
<el-menu-item index="5">个人中心</el-menu-item>
<div class="nav-right">欢迎你{{ nickname }}</div>
</el-menu>
<!-- 轮播图 -->
<el-carousel height="360px">
<el-carousel-item v-for="item in banners" :key="item">
<img :src="item" class="banner-img" />
</el-carousel-item>
</el-carousel>
<!-- 社团资讯和活动 -->
<div class="info-section">
<el-card class="info-box">
<div class="section-header">
<h3>社团资讯</h3>
<el-link>更多</el-link>
</div>
<el-timeline>
<el-timeline-item v-for="news in newsList" :key="news.id" :timestamp="news.date">
{{ news.title }}
</el-timeline-item>
</el-timeline>
</el-card>
<el-card class="info-box">
<div class="section-header">
<h3>社团活动</h3>
<el-link>更多</el-link>
</div>
<el-timeline>
<el-timeline-item v-for="act in activityList" :key="act.id" :timestamp="act.date">
{{ act.title }}
</el-timeline-item>
</el-timeline>
</el-card>
</div>
<!-- 优秀社团 -->
<el-card class="club-section">
<h3>优秀社团</h3>
<div class="club-list">
<div class="club-item" v-for="club in clubs" :key="club.id">
<img :src="club.logo" />
<p>{{ club.name }}</p>
</div>
</div>
</el-card>
<!-- 页脚 -->
<el-footer class="footer">
<div class="footer-cols">
<div>
<h4>关于我们</h4>
<p>致力于为用户提供优质的社团活动管理服务</p>
</div>
<div>
<h4>快速链接</h4>
<p>社团中心<br />公告</p>
</div>
<div>
<h4>联系方式</h4>
<p>电话: 400-123-4567<br />邮箱: contact@example.com</p>
</div>
<div>
<h4>关注我们</h4>
<p>🌐 📱 🧧</p>
</div>
</div>
<p class="footer-note">© 2024 社团活动管理系统. 保留所有权利.</p>
</el-footer>
</div>
</template>
<script setup>
import { ref } from 'vue'
const nickname = localStorage.getItem('nickname') || '学生'
const banners = ['/login-bg.jpg', '/login-bg.jpg'] //
const newsList = [
{ id: 1, title: '乒乓社招新啦!', date: '2023-11-14' },
{ id: 2, title: '街舞社重磅来袭!', date: '2023-11-14' },
]
const activityList = [
{ id: 1, title: '小小球员,等你来战!', date: '2023-11-14' },
{ id: 2, title: '舞蹈嗨翻,敬“梦”想!', date: '2023-11-14' },
]
const clubs = [
{ id: 1, name: '乒乓社', logo: '/pingpong.png' },
{ id: 2, name: '街舞社', logo: '/street.png' },
{ id: 3, name: '书法社', logo: '/calligraphy.png' },
{ id: 4, name: '英语社', logo: '/english.png' },
{ id: 5, name: '足球社', logo: '/soccer.png' },
]
</script>
<style scoped>
.user-home {
font-family: "Microsoft YaHei", sans-serif;
}
.top-nav {
display: flex;
align-items: center;
justify-content: space-between;
}
.nav-left {
color: #fff;
font-size: 18px;
padding: 0 20px;
}
.nav-right {
padding: 0 20px;
color: #fff;
}
.banner-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.info-section {
display: flex;
gap: 20px;
padding: 20px;
}
.info-box {
flex: 1;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.club-section {
margin: 20px;
}
.club-list {
display: flex;
gap: 30px;
margin-top: 20px;
justify-content: start;
flex-wrap: wrap;
}
.club-item {
text-align: center;
}
.club-item img {
width: 70px;
height: 70px;
border-radius: 50%;
}
.footer {
background: #2c3e50;
color: #fff;
padding: 30px;
margin-top: 50px;
}
.footer-cols {
display: flex;
justify-content: space-between;
}
.footer-note {
text-align: center;
margin-top: 20px;
}
</style>

View File

@ -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 || '登录失败')

View File

@ -0,0 +1,52 @@
<template>
<student-layout>
<!-- 首页 Banner -->
<el-carousel height="400px" class="home-banner">
<el-carousel-item v-for="i in 2" :key="i">
<img :src="`/banner${i}.jpg`" class="banner-img" />
</el-carousel-item>
</el-carousel>
<!-- 社团资讯 -->
<section class="section">
<h2 class="section-title">📢 最新社团资讯</h2>
<NewsList />
</section>
<!-- 活动推荐 -->
<section class="section">
<h2 class="section-title">🔥 活动推荐</h2>
<ActivityList />
</section>
<!-- 优秀社团 -->
<section class="section">
<h2 class="section-title">🏅 优秀社团</h2>
<ClubCarousel />
</section>
</student-layout>
</template>
<script setup>
import NewsList from '@/components/student/NewsList.vue'
import ActivityList from '@/components/student/ActivityList.vue'
import ClubCarousel from '@/components/student/ClubCarousel.vue'
</script>
<style scoped>
.section {
margin: 40px auto;
max-width: 1200px;
}
.section-title {
font-size: 20px;
margin-bottom: 20px;
color: #333;
}
.banner-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 12px;
}
</style>

View File

@ -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 {

View File

@ -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);
}
}