v0.3.5 个人页面对接

master
bruce 2025-04-24 22:31:28 +08:00
parent c8969aaefd
commit e26e7bfc4e
13 changed files with 286 additions and 93 deletions

View File

@ -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']
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
<template>
<div class="activity-detail">
<StudentHeader />
<el-card class="detail-card" v-if="activity">
<div class="title">{{ activity.title }}</div>

View File

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

View File

@ -1,6 +1,5 @@
<template>
<div class="club-center">
<StudentHeader />
<section class="section">
<h2 class="section-title">🎯 我加入的社团</h2>

View File

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

View File

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

View File

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

View File

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