v0.3.5 个人页面对接
parent
c8969aaefd
commit
e26e7bfc4e
|
|
@ -14,6 +14,7 @@ declare module 'vue' {
|
|||
AdminSidebar: typeof import('./src/components/admin/AdminSidebar.vue')['default']
|
||||
ApprovalDetailDialog: typeof import('./src/components/admin/ApprovalDetailDialog.vue')['default']
|
||||
BannerCarousel: typeof import('./src/components/student/BannerCarousel.vue')['default']
|
||||
ChangePasswordDialog: typeof import('./src/components/student/ChangePasswordDialog.vue')['default']
|
||||
ClubCard: typeof import('./src/components/student/ClubCard.vue')['default']
|
||||
ClubCreateDialog: typeof import('./src/components/admin/ClubCreateDialog.vue')['default']
|
||||
ClubEditDialog: typeof import('./src/components/admin/ClubEditDialog.vue')['default']
|
||||
|
|
@ -52,6 +53,7 @@ declare module 'vue' {
|
|||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
||||
Home: typeof import('./src/views/student/Home.vue')['default']
|
||||
IconCommunity: typeof import('./src/components/icons/IconCommunity.vue')['default']
|
||||
|
|
@ -65,6 +67,7 @@ declare module 'vue' {
|
|||
StudentFooter: typeof import('./src/components/common/StudentFooter.vue')['default']
|
||||
StudentHeader: typeof import('./src/components/common/StudentHeader.vue')['default']
|
||||
TheWelcome: typeof import('./src/components/TheWelcome.vue')['default']
|
||||
UserEditDialog: typeof import('./src/components/student/UserEditDialog.vue')['default']
|
||||
WelcomeItem: typeof import('./src/components/WelcomeItem.vue')['default']
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取我发起的活动列表
|
||||
*/
|
||||
export function getMyCreatedActivities() {
|
||||
return request({
|
||||
url: '/activity/my/created',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我参与的活动列表
|
||||
*/
|
||||
export function getMyJoinedActivities() {
|
||||
return request({
|
||||
url: '/activity/my/joined',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
|
@ -5,7 +5,42 @@ import request from '@/utils/request'
|
|||
*/
|
||||
export function getUserProfile() {
|
||||
return request({
|
||||
url: '/user/profile',
|
||||
url: '/api/user/profile',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传头像
|
||||
*/
|
||||
export function uploadAvatar(file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return request({
|
||||
url: '/api/user/avatar',
|
||||
method: 'post',
|
||||
data: formData
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
export function updateUserInfo(data) {
|
||||
return request({
|
||||
url: '/api/user/update',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
export function changePassword(data) {
|
||||
return request({
|
||||
url: '/api/user/change-password',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
<el-menu-item index="/student/home">首页</el-menu-item>
|
||||
<el-menu-item index="/student/clubs">社团中心</el-menu-item>
|
||||
<el-menu-item index="/student/activities">活动</el-menu-item>
|
||||
<el-menu-item index="/student/announcements">公告</el-menu-item>
|
||||
<el-menu-item index="/student/profile">个人中心</el-menu-item>
|
||||
</el-menu>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<el-dialog v-model="visible" title="修改密码" width="400px" :close-on-click-modal="false">
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-form-item label="原密码">
|
||||
<el-input type="password" v-model="form.oldPassword" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码">
|
||||
<el-input type="password" v-model="form.newPassword" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码">
|
||||
<el-input type="password" v-model="form.confirmPassword" show-password />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submit">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { changePassword } from '@/api/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const visible = defineModel()
|
||||
const form = ref({
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
const submit = async () => {
|
||||
if (form.value.newPassword !== form.value.confirmPassword) {
|
||||
ElMessage.warning('两次输入的新密码不一致')
|
||||
return
|
||||
}
|
||||
await changePassword(form.value)
|
||||
ElMessage.success('修改成功')
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
<template>
|
||||
<el-dialog v-model="visible" title="编辑个人资料" width="400px" :close-on-click-modal="false">
|
||||
<el-form :model="form" label-width="80px" ref="formRef">
|
||||
<el-form-item label="昵称">
|
||||
<el-input v-model="form.nick_name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="form.email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="头像">
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:on-success="onUploadSuccess"
|
||||
:action="uploadUrl"
|
||||
name="file"
|
||||
>
|
||||
<el-avatar :src="form.avatar" class="avatar-upload" />
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { updateUserInfo } from '@/api/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const visible = defineModel()
|
||||
const userData = defineProps({ user: Object })
|
||||
const emit = defineEmits(['updated'])
|
||||
|
||||
const form = ref({ ...userData.user })
|
||||
watch(() => userData.user, (newVal) => {
|
||||
form.value = { ...newVal }
|
||||
})
|
||||
|
||||
const uploadUrl = '/api/user/avatar'
|
||||
|
||||
const onUploadSuccess = (res) => {
|
||||
form.value.avatar = res.data
|
||||
ElMessage.success('头像上传成功')
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
await updateUserInfo(form.value)
|
||||
ElMessage.success('保存成功')
|
||||
visible.value = false
|
||||
emit('updated')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.avatar-upload {
|
||||
cursor: pointer;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<div class="activity-detail">
|
||||
<StudentHeader />
|
||||
|
||||
<el-card class="detail-card" v-if="activity">
|
||||
<div class="title">{{ activity.title }}</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<div class="activity-list">
|
||||
<StudentHeader />
|
||||
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="keyword" placeholder="搜索活动标题" clearable class="search" @keyup.enter="fetchActivities" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<div class="club-center">
|
||||
<StudentHeader />
|
||||
|
||||
<section class="section">
|
||||
<h2 class="section-title">🎯 我加入的社团</h2>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
<template>
|
||||
<div class="profile-page">
|
||||
<StudentHeader />
|
||||
|
||||
<el-card class="profile-card">
|
||||
<div class="user-info">
|
||||
<el-avatar :src="user.avatar" size="large" />
|
||||
<div class="info">
|
||||
<h2 class="name">{{ user.name }}</h2>
|
||||
<p class="school-id">学号:{{ user.schoolId }}</p>
|
||||
<h2 class="name">姓名:{{ user.nickName }}</h2>
|
||||
<p class="detail">学号:{{ user.schoolId }}</p>
|
||||
<p class="detail">班级:{{ user.className || '暂无信息' }}</p>
|
||||
<p class="detail">学院:{{ user.collegeName || '暂无信息' }}</p>
|
||||
</div>
|
||||
<div class="btns-inline">
|
||||
<el-button class="btn" type="primary" plain size="small" @click="editVisible = true">编辑资料</el-button>
|
||||
<el-button class="btn" type="warning" plain size="small" @click="pwdVisible = true">修改密码</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats">
|
||||
|
|
@ -19,10 +23,6 @@
|
|||
<div class="stat-value">{{ user.activityCount }}</div>
|
||||
<div class="stat-label">参与活动</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-value">{{ user.credits }}</div>
|
||||
<div class="stat-label">信用分</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
|
|
@ -30,49 +30,70 @@
|
|||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="我发起的活动" name="created">
|
||||
<ul class="act-list">
|
||||
<li v-for="item in createdActivities" :key="item.actId">{{ item.title }}</li>
|
||||
<li v-for="item in createdActivities" :key="item.actId">
|
||||
<a @click="goToActivity(item.actId)">{{ item.title }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="我参与的活动" name="joined">
|
||||
<ul class="act-list">
|
||||
<li v-for="item in joinedActivities" :key="item.actId">{{ item.title }}</li>
|
||||
<li v-for="item in joinedActivities" :key="item.actId">
|
||||
<a @click="goToActivity(item.actId)">{{ item.title }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
|
||||
<!-- 弹窗 -->
|
||||
<UserEditDialog v-model="editVisible" :user="user" @updated="loadProfile" />
|
||||
<ChangePasswordDialog v-model="pwdVisible" />
|
||||
|
||||
<StudentFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import StudentHeader from '@/components/common/StudentHeader.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import StudentFooter from '@/components/common/StudentFooter.vue'
|
||||
import request from '@/utils/request'
|
||||
import UserEditDialog from '@/components/student/UserEditDialog.vue'
|
||||
import ChangePasswordDialog from '@/components/student/ChangePasswordDialog.vue'
|
||||
import { getUserProfile } from '@/api/user'
|
||||
import { getMyCreatedActivities, getMyJoinedActivities } from '@/api/activity'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const user = ref({
|
||||
name: '',
|
||||
nickName: '',
|
||||
avatar: '',
|
||||
schoolId: '',
|
||||
className: '',
|
||||
collegeName: '',
|
||||
clubCount: 0,
|
||||
activityCount: 0,
|
||||
credits: 0
|
||||
activityCount: 0
|
||||
})
|
||||
|
||||
const editVisible = ref(false)
|
||||
const pwdVisible = ref(false)
|
||||
|
||||
const activeTab = ref('created')
|
||||
const createdActivities = ref([])
|
||||
const joinedActivities = ref([])
|
||||
|
||||
const loadProfile = async () => {
|
||||
const { data } = await getUserProfile()
|
||||
user.value = data
|
||||
}
|
||||
|
||||
const goToActivity = (id) => {
|
||||
router.push(`/student/activity/${id}`)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const profileRes = await request({ url: '/user/profile' })
|
||||
user.value = profileRes.data
|
||||
|
||||
const res1 = await request({ url: '/activity/my/created' })
|
||||
createdActivities.value = res1.data || []
|
||||
|
||||
const res2 = await request({ url: '/activity/my/joined' })
|
||||
joinedActivities.value = res2.data || []
|
||||
await loadProfile()
|
||||
createdActivities.value = (await getMyCreatedActivities()).data || []
|
||||
joinedActivities.value = (await getMyJoinedActivities()).data || []
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -85,20 +106,34 @@ onMounted(async () => {
|
|||
}
|
||||
.user-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
.name {
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
.info {
|
||||
flex: 1;
|
||||
}
|
||||
.school-id {
|
||||
.name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.detail {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
margin: 2px 0;
|
||||
}
|
||||
.btns-inline {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.btn {
|
||||
width: 100px;
|
||||
}
|
||||
.stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.stat-box {
|
||||
|
|
@ -120,4 +155,8 @@ onMounted(async () => {
|
|||
padding-left: 20px;
|
||||
line-height: 2;
|
||||
}
|
||||
.act-list a {
|
||||
cursor: pointer;
|
||||
color: #409eff;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ public class SecurityConfig {
|
|||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/login").permitAll()
|
||||
.requestMatchers("/api/auth/me").hasAnyRole("COLLEGE_ADMIN")
|
||||
.requestMatchers("/api/user/**").authenticated()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
|||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* 用户端 - 个人信息管理控制器
|
||||
|
|
@ -22,69 +24,57 @@ public class UserController {
|
|||
private UserService userService;
|
||||
|
||||
/**
|
||||
* 用户修改密码
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param oldPassword 旧密码
|
||||
* @param newPassword 新密码
|
||||
* @return AjaxResult
|
||||
* 获取当前用户个人信息(前端调用 /user/profile,无需传 userId)
|
||||
*/
|
||||
@PutMapping("/change-password")
|
||||
public AjaxResult changePassword(@RequestParam Long userId, @RequestParam String oldPassword, @RequestParam String newPassword) {
|
||||
userService.changePassword(userId, oldPassword, newPassword);
|
||||
return AjaxResult.success("密码修改成功");
|
||||
@GetMapping("/profile")
|
||||
public AjaxResult getUserProfile() {
|
||||
System.out.println("运行中");
|
||||
User user = getCurrentUser();
|
||||
return AjaxResult.success(userService.getUserProfile(user.getUserId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户修改个人信息(昵称、邮箱、头像)
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param updatedUser 用户提交的新信息(昵称、邮箱、头像)
|
||||
* @return AjaxResult
|
||||
* 修改个人资料(昵称、邮箱、头像)=> PUT /user/update
|
||||
*/
|
||||
@PutMapping("/profile")
|
||||
public AjaxResult updateProfile(@RequestParam Long userId, @RequestBody User updatedUser) {
|
||||
userService.updateProfile(userId, updatedUser);
|
||||
@PutMapping("/update")
|
||||
public AjaxResult updateProfile(@RequestBody User updatedUser) {
|
||||
User user = getCurrentUser();
|
||||
userService.updateProfile(user.getUserId(), updatedUser);
|
||||
return AjaxResult.success("个人信息更新成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户详情(查看个人信息)
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return AjaxResult
|
||||
* 修改密码 => PUT /user/change-password
|
||||
*/
|
||||
@GetMapping("/{userId}")
|
||||
public AjaxResult getUserProfile(@PathVariable Long userId) {
|
||||
return AjaxResult.success(userService.getUserProfile(userId));
|
||||
@PutMapping("/change-password")
|
||||
public AjaxResult changePassword(@RequestBody Map<String, String> body) {
|
||||
User user = getCurrentUser();
|
||||
String oldPassword = body.get("oldPassword");
|
||||
String newPassword = body.get("newPassword");
|
||||
userService.changePassword(user.getUserId(), oldPassword, newPassword);
|
||||
return AjaxResult.success("密码修改成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传头像
|
||||
*
|
||||
* @param file 头像信息
|
||||
* @return AjaxResult
|
||||
* 上传头像 => POST /user/avatar
|
||||
*/
|
||||
@PostMapping("/avatar")
|
||||
public String uploadAvatar(@RequestParam("file") MultipartFile file) {
|
||||
// 获取当前认证信息
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
throw new RuntimeException("未认证用户");
|
||||
}
|
||||
|
||||
// 获取当前用户对象
|
||||
User userDetails = (User) authentication.getPrincipal();
|
||||
Long userId = userDetails.getUserId(); // 获取 userId
|
||||
|
||||
// 上传文件
|
||||
User user = getCurrentUser();
|
||||
String avatarUrl = FileUploadUtil.uploadFile(file, "avatars/");
|
||||
|
||||
// 更新用户头像
|
||||
userService.updateUserAvatar(userId, avatarUrl);
|
||||
|
||||
userService.updateUserAvatar(user.getUserId(), avatarUrl);
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户
|
||||
*/
|
||||
private User getCurrentUser() {
|
||||
System.out.println("✔ Controller 已进入!");
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
throw new RuntimeException("用户未认证");
|
||||
}
|
||||
return (User) authentication.getPrincipal();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.bruce.sams.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bruce.sams.common.enums.UserStatus;
|
||||
import com.bruce.sams.common.exception.PasswordIncorrectException;
|
||||
|
|
@ -135,23 +136,22 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
|
|||
}
|
||||
|
||||
/**
|
||||
* 用户修改个人信息(用户端)
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param updatedUser 用户提交的新信息(昵称、邮箱、头像)
|
||||
* 更新用户信息(昵称、邮箱、头像)
|
||||
*/
|
||||
@Override
|
||||
public void updateProfile(Long userId, User updatedUser) {
|
||||
|
||||
User user = userMapper.selectById(userId);
|
||||
if (user == null) {
|
||||
throw new UserNotFoundException();
|
||||
}
|
||||
LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
|
||||
updateWrapper.eq(User::getUserId, userId)
|
||||
.set(User::getNickName, updatedUser.getNickName())
|
||||
.set(User::getEmail, updatedUser.getEmail())
|
||||
.set(User::getAvatar, updatedUser.getAvatar());
|
||||
|
||||
// 仅允许修改部分字段
|
||||
user.setNickName(updatedUser.getNickName());
|
||||
user.setEmail(updatedUser.getEmail());
|
||||
user.setAvatar(updatedUser.getAvatar());
|
||||
|
||||
userMapper.updateById(user);
|
||||
this.update(updateWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -168,6 +168,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
|
|||
return user;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*
|
||||
|
|
@ -198,13 +199,15 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
|
|||
}
|
||||
|
||||
/**
|
||||
* 更新用户头像
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param avatarUrl 头像存储路径
|
||||
* 更新头像地址
|
||||
*/
|
||||
@Override
|
||||
public void updateUserAvatar(Long userId, String avatarUrl) {
|
||||
userMapper.updateAvatar(userId, avatarUrl);
|
||||
LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
|
||||
updateWrapper.eq(User::getUserId, userId)
|
||||
.set(User::getAvatar, avatarUrl);
|
||||
|
||||
this.update(updateWrapper);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue