个人信息

czq
2991692032 4 weeks ago
parent e13486812c
commit 22065758d4

@ -18,11 +18,31 @@
- ✅ 评论时间的友好显示(几分钟前、几小时前等) - ✅ 评论时间的友好显示(几分钟前、几小时前等)
- ✅ 未登录用户会提示登录 - ✅ 未登录用户会提示登录
### 3. 用户体验优化 ### 3. 个人中心系统
- ✅ 完善了个人主页 (`/src/views/Home.vue`)
- ✅ 真实的用户统计数据API接口
- ✅ 最近发布帖子的展示
- ✅ 加载状态和错误处理
- ✅ 响应式设计
- ✅ 创建了消息中心页面 (`/src/views/MessagesView.vue`)
- ✅ 系统通知、评论回复、点赞通知分类
- ✅ 消息已读/未读状态管理
- ✅ 批量操作功能
- ✅ 创建了设置页面 (`/src/views/SettingsView.vue`)
- ✅ 通知设置
- ✅ 隐私设置
- ✅ 界面设置
- ✅ 账户安全设置
- ✅ 数据管理功能
- ✅ 完善了个人帖子管理页面 (`/src/views/forum/MyPostsView.vue`)
- ✅ 完善了账号管理页面 (`/src/views/AccountManager.vue`)
### 4. 用户体验优化
- ✅ 添加了加载状态和错误处理 - ✅ 添加了加载状态和错误处理
- ✅ 优化了UI布局和样式 - ✅ 优化了UI布局和样式
- ✅ 支持未登录用户浏览但限制互动功能 - ✅ 支持未登录用户浏览但限制互动功能
- ✅ 完善了错误提示和成功提示 - ✅ 完善了错误提示和成功提示
- ✅ 响应式设计适配移动端
## 技术实现 ## 技术实现
@ -32,16 +52,38 @@
- `POST /comments` - 发表评论 - `POST /comments` - 发表评论
- `DELETE /comments/{id}` - 删除评论 - `DELETE /comments/{id}` - 删除评论
- `POST /comments/{id}/like` - 点赞/取消点赞评论 - `POST /comments/{id}/like` - 点赞/取消点赞评论
- `GET /users/stats` - 获取用户统计数据
- `GET /users/recent-posts` - 获取用户最近帖子
### 组件结构 ### 组件结构
``` ```
PostListView.vue - 帖子列表页面 个人中心布局 (PersonalLayout.vue)
├── 个人主页 (Home.vue)
│ ├── 用户统计卡片
│ └── 最近帖子列表
├── 账号管理 (AccountManager.vue)
│ ├── 个人资料编辑
│ └── 密码修改
├── 我的帖子 (MyPostsView.vue)
│ ├── 帖子列表
│ └── 编辑/删除操作
├── 消息中心 (MessagesView.vue)
│ ├── 系统通知
│ ├── 评论回复
│ └── 点赞通知
└── 设置 (SettingsView.vue)
├── 通知设置
├── 隐私设置
├── 界面设置
└── 账户安全
帖子列表页面 (PostListView.vue)
├── 点赞按钮 ├── 点赞按钮
└── 帖子卡片交互 └── 帖子卡片交互
PostDetailView.vue - 帖子详情页面 帖子详情页面 (PostDetailView.vue)
├── 点赞按钮 ├── 点赞按钮
└── CommentSection.vue - 评论区组件 └── 评论区组件 (CommentSection.vue)
├── 评论表单 ├── 评论表单
├── 评论列表 ├── 评论列表
├── 回复功能 ├── 回复功能
@ -50,33 +92,29 @@ PostDetailView.vue - 帖子详情页面
### 状态管理 ### 状态管理
- 在PostStore中添加了`likePost`方法 - 在PostStore中添加了`likePost`方法
- 实现了帖子点赞状态的同步更新 - 在UserStore中完善了用户信息管理
- 在评论组件中管理评论数据和状态 - 评论数据通过CommentSection组件本地管理
## 功能特点
### 点赞系统
- 实时更新点赞数量
- 视觉反馈(已点赞的按钮变为主色调)
- 防重复点击loading状态
- 同步更新列表和详情页状态
### 评论系统 ## 数据流设计
- 支持多层嵌套回复 1. **用户统计数据**: API → UserStore → 个人主页显示
- 评论和回复的独立点赞 2. **帖子点赞**: 用户点击 → PostStore处理 → API调用 → 状态更新
- 用户权限控制(只能删除自己的评论) 3. **评论系统**: 用户操作 → CommentSection → API调用 → 界面更新
- 富文本内容支持 4. **个人设置**: 用户修改 → 本地状态 → API保存 → 成功提示
- 实时评论数量更新
### 安全和权限 ## 已解决的问题
- 所有交互功能都需要登录 - ✅ 个人主页假数据问题
- 后端API有用户身份验证 - ✅ 消息中心页面缺失
- 前端有相应的权限检查和提示 - ✅ 设置页面缺失
- ✅ 用户统计数据获取
- ✅ 帖子交互功能不完整
- ✅ 响应式布局适配
## 待优化项目 ## 待开发功能(建议)
- [ ] 添加评论分页功能 - 🔲 消息系统的后端API实现
- [ ] 实现评论的编辑功能 - 🔲 设置数据的持久化存储
- [ ] 添加评论的举报功能 - 🔲 两步验证功能
- [ ] 优化评论的实时更新 - 🔲 会话管理功能
- [ ] 添加富文本编辑器支持图片等 - 🔲 数据导出功能
- [ ] 实现评论的@功能 - 🔲 实时通知推送
- 🔲 好友系统
- 🔲 私信功能

@ -5,6 +5,7 @@ export interface UserInfo {
id: number; id: number;
username: string; username: string;
email: string; email: string;
nickname?: string;
avatar?: string; avatar?: string;
bio?: string; bio?: string;
gender?: number; gender?: number;
@ -57,6 +58,13 @@ export interface UpdatePasswordParams {
newPassword: string; newPassword: string;
} }
export interface UserStats {
totalPosts: number;
totalLikes: number;
totalComments: number;
totalViews: number;
}
// 用户API // 用户API
export default { export default {
// 登录 // 登录
@ -114,6 +122,20 @@ export default {
); );
}, },
// 获取用户统计数据
getUserStats() {
return get<{code: number; data: UserStats}>(
'/users/stats'
);
},
// 获取用户最近帖子
getUserRecentPosts(limit: number = 5) {
return get<{code: number; data: {list: any[]}}>(
`/users/recent-posts?limit=${limit}`
);
},
// 上传头像 // 上传头像
uploadAvatar(formData: FormData) { uploadAvatar(formData: FormData) {
return post<{code: number; data: {avatarUrl: string}}>( return post<{code: number; data: {avatarUrl: string}}>(

@ -136,13 +136,13 @@ const routes: Array<RouteRecordRaw> = [
{ {
path: 'messages', // URL: /personal/messages path: 'messages', // URL: /personal/messages
name: 'Messages', name: 'Messages',
component: () => import('../views/NotFound.vue'), // 占位符 component: () => import('../views/MessagesView.vue'),
meta: { title: '消息中心 - UniLife' } meta: { title: '消息中心 - UniLife' }
}, },
{ {
path: 'settings', // URL: /personal/settings path: 'settings', // URL: /personal/settings
name: 'Settings', name: 'Settings',
component: () => import('../views/NotFound.vue'), // 占位符 component: () => import('../views/SettingsView.vue'),
meta: { title: '设置 - UniLife' } meta: { title: '设置 - UniLife' }
}, },
// 默认重定向到个人主页 // 默认重定向到个人主页

@ -1,52 +1,116 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '../stores'; import { useUserStore } from '../stores';
import userApi from '../api/user';
import type { UserStats } from '../api/user';
import { ElMessage, ElSkeleton, ElEmpty, ElIcon, ElButton } from 'element-plus';
import { Document, Star, ChatDotRound, View, Edit, Delete } from '@element-plus/icons-vue';
const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();
// //
const userPosts = ref([ const loading = ref(true);
{ const statsLoading = ref(true);
id: 1, const postsLoading = ref(true);
title: '大学生活经验分享', const error = ref<string | null>(null);
content: '分享一些大学生活的经验和技巧...',
createTime: '2023-05-01', //
likes: 25, const userPosts = ref<any[]>([]);
comments: 10
},
{
id: 2,
title: '考研复习计划',
content: '分享我的考研复习计划和方法...',
createTime: '2023-05-15',
likes: 42,
comments: 18
},
{
id: 3,
title: '校园活动推荐',
content: '推荐几个值得参加的校园活动...',
createTime: '2023-06-01',
likes: 15,
comments: 5
}
]);
// //
const userStats = ref({ const userStats = ref<UserStats>({
totalPosts: 3, totalPosts: 0,
totalLikes: 82, totalLikes: 0,
totalComments: 33, totalComments: 0,
totalViews: 256 totalViews: 0
}); });
//
const fetchUserStats = async () => {
try {
statsLoading.value = true;
const response = await userApi.getUserStats();
if (response && response.code === 200 && response.data) {
userStats.value = response.data;
}
} catch (err: any) {
console.error('获取统计数据失败:', err);
//
} finally {
statsLoading.value = false;
}
};
//
const fetchRecentPosts = async () => {
try {
postsLoading.value = true;
const response = await userApi.getUserRecentPosts(5);
if (response && response.code === 200 && response.data) {
userPosts.value = response.data.list;
}
} catch (err: any) {
console.error('获取最近帖子失败:', err);
userPosts.value = [];
} finally {
postsLoading.value = false;
}
};
//
const formatDate = (dateString: string) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString();
};
//
const goToPost = (postId: number) => {
router.push(`/post/${postId}`);
};
//
const editPost = (postId: number) => {
router.push(`/edit-post/${postId}`);
};
//
const deletePost = async (postId: number) => {
try {
// API
ElMessage.success('删除成功');
await fetchRecentPosts(); //
} catch (err: any) {
ElMessage.error('删除失败');
}
};
//
const createNewPost = () => {
router.push('/create-post');
};
onMounted(async () => { onMounted(async () => {
try {
loading.value = true;
// //
if (!userStore.userInfo) { if (!userStore.userInfo) {
await userStore.fetchUserInfo(); await userStore.fetchUserInfo();
} }
// API //
await Promise.all([
fetchUserStats(),
fetchRecentPosts()
]);
} catch (err: any) {
error.value = '加载数据失败';
ElMessage.error('加载数据失败,请稍后重试');
} finally {
loading.value = false;
}
}); });
</script> </script>
@ -54,48 +118,80 @@ onMounted(async () => {
<div class="home-page"> <div class="home-page">
<div class="page-header"> <div class="page-header">
<h1>个人主页</h1> <h1>个人主页</h1>
<p>欢迎回来{{ userStore.userInfo?.username || '用户' }}</p> <p>欢迎回来{{ userStore.userInfo?.nickname || userStore.userInfo?.username || '用户' }}</p>
</div> </div>
<!-- 统计卡片 --> <!-- 统计卡片 -->
<div class="stats-container"> <div class="stats-container">
<div class="stat-card"> <div class="stat-card">
<div class="stat-icon"> <div class="stat-icon">
<el-icon><document /></el-icon> <el-icon><Document /></el-icon>
</div> </div>
<div class="stat-content"> <div class="stat-content">
<el-skeleton :loading="statsLoading" animated>
<template #template>
<el-skeleton-item variant="text" style="width: 40px; height: 24px;" />
<el-skeleton-item variant="text" style="width: 60px; height: 16px;" />
</template>
<template #default>
<div class="stat-value">{{ userStats.totalPosts }}</div> <div class="stat-value">{{ userStats.totalPosts }}</div>
<div class="stat-label">发布帖子</div> <div class="stat-label">发布帖子</div>
</template>
</el-skeleton>
</div> </div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="stat-icon"> <div class="stat-icon">
<el-icon><star /></el-icon> <el-icon><Star /></el-icon>
</div> </div>
<div class="stat-content"> <div class="stat-content">
<el-skeleton :loading="statsLoading" animated>
<template #template>
<el-skeleton-item variant="text" style="width: 40px; height: 24px;" />
<el-skeleton-item variant="text" style="width: 60px; height: 16px;" />
</template>
<template #default>
<div class="stat-value">{{ userStats.totalLikes }}</div> <div class="stat-value">{{ userStats.totalLikes }}</div>
<div class="stat-label">获得点赞</div> <div class="stat-label">获得点赞</div>
</template>
</el-skeleton>
</div> </div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="stat-icon"> <div class="stat-icon">
<el-icon><chat-dot-round /></el-icon> <el-icon><ChatDotRound /></el-icon>
</div> </div>
<div class="stat-content"> <div class="stat-content">
<el-skeleton :loading="statsLoading" animated>
<template #template>
<el-skeleton-item variant="text" style="width: 40px; height: 24px;" />
<el-skeleton-item variant="text" style="width: 60px; height: 16px;" />
</template>
<template #default>
<div class="stat-value">{{ userStats.totalComments }}</div> <div class="stat-value">{{ userStats.totalComments }}</div>
<div class="stat-label">收到评论</div> <div class="stat-label">收到评论</div>
</template>
</el-skeleton>
</div> </div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="stat-icon"> <div class="stat-icon">
<el-icon><view /></el-icon> <el-icon><View /></el-icon>
</div> </div>
<div class="stat-content"> <div class="stat-content">
<el-skeleton :loading="statsLoading" animated>
<template #template>
<el-skeleton-item variant="text" style="width: 40px; height: 24px;" />
<el-skeleton-item variant="text" style="width: 60px; height: 16px;" />
</template>
<template #default>
<div class="stat-value">{{ userStats.totalViews }}</div> <div class="stat-value">{{ userStats.totalViews }}</div>
<div class="stat-label">帖子浏览</div> <div class="stat-label">帖子浏览</div>
</template>
</el-skeleton>
</div> </div>
</div> </div>
</div> </div>
@ -104,39 +200,51 @@ onMounted(async () => {
<div class="recent-posts"> <div class="recent-posts">
<div class="section-header"> <div class="section-header">
<h2>最近发布的帖子</h2> <h2>最近发布的帖子</h2>
<button class="btn btn-primary">发布新帖</button> <el-button type="primary" @click="createNewPost"></el-button>
</div> </div>
<div class="posts-list"> <div class="posts-list">
<div v-if="userPosts.length === 0" class="empty-state"> <el-skeleton :loading="postsLoading" :rows="3" animated />
<p>你还没有发布过帖子</p>
<button class="btn btn-primary">立即发布</button> <el-empty v-if="!postsLoading && userPosts.length === 0" description="你还没有发布过帖子">
</div> <el-button type="primary" @click="createNewPost"></el-button>
</el-empty>
<div v-else class="post-card" v-for="post in userPosts" :key="post.id"> <div v-if="!postsLoading && userPosts.length > 0" class="post-card" v-for="post in userPosts" :key="post.id">
<div class="post-header"> <div class="post-header">
<h3 class="post-title">{{ post.title }}</h3> <h3 class="post-title clickable" @click="goToPost(post.id)">{{ post.title }}</h3>
<span class="post-date">{{ post.createTime }}</span> <span class="post-date">{{ formatDate(post.createdAt) }}</span>
</div> </div>
<p class="post-content">{{ post.content }}</p> <p class="post-content">{{ post.summary || post.content?.substring(0, 100) + '...' || '暂无摘要' }}</p>
<div class="post-footer"> <div class="post-footer">
<div class="post-stats"> <div class="post-stats">
<div class="post-stat"> <div class="post-stat">
<el-icon><star /></el-icon> <el-icon><Star /></el-icon>
<span>{{ post.likes }}</span> <span>{{ post.likeCount || 0 }}</span>
</div>
<div class="post-stat">
<el-icon><ChatDotRound /></el-icon>
<span>{{ post.commentCount || 0 }}</span>
</div> </div>
<div class="post-stat"> <div class="post-stat">
<el-icon><chat-dot-round /></el-icon> <el-icon><View /></el-icon>
<span>{{ post.comments }}</span> <span>{{ post.viewCount || 0 }}</span>
</div> </div>
</div> </div>
<div class="post-actions"> <div class="post-actions">
<button class="btn btn-text">编辑</button> <el-button link type="primary" @click="editPost(post.id)">
<button class="btn btn-text">删除</button> <el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button link type="danger" @click="deletePost(post.id)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</div> </div>
</div> </div>
</div> </div>
@ -259,6 +367,15 @@ onMounted(async () => {
margin: 0; margin: 0;
} }
.post-title.clickable {
cursor: pointer;
transition: color 0.2s;
}
.post-title.clickable:hover {
color: var(--primary-color);
}
.post-date { .post-date {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
color: var(--text-light); color: var(--text-light);

@ -0,0 +1,313 @@
<template>
<div class="messages-view">
<div class="page-header">
<h1>消息中心</h1>
<p>查看你的所有通知和私信</p>
</div>
<!-- 消息分类 -->
<div class="message-tabs">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="系统通知" name="system">
<div class="message-list">
<el-skeleton :loading="loading" :rows="3" animated />
<el-empty v-if="!loading && systemMessages.length === 0" description="暂无系统通知">
<el-button type="primary" @click="refreshMessages"></el-button>
</el-empty>
<div v-if="!loading && systemMessages.length > 0">
<div v-for="message in systemMessages" :key="message.id" class="message-item">
<div class="message-header">
<div class="message-title">{{ message.title }}</div>
<div class="message-time">{{ formatTime(message.createdAt) }}</div>
</div>
<div class="message-content">{{ message.content }}</div>
<div class="message-actions" v-if="!message.isRead">
<el-button size="small" @click="markAsRead(message.id)"></el-button>
</div>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="评论回复" name="comments">
<div class="message-list">
<el-skeleton :loading="loading" :rows="3" animated />
<el-empty v-if="!loading && commentMessages.length === 0" description="暂无评论回复">
<el-button type="primary" @click="refreshMessages"></el-button>
</el-empty>
<div v-if="!loading && commentMessages.length > 0">
<div v-for="message in commentMessages" :key="message.id" class="message-item">
<div class="message-header">
<div class="message-title">{{ message.nickname }} 回复了你的帖子</div>
<div class="message-time">{{ formatTime(message.createdAt) }}</div>
</div>
<div class="message-content">{{ message.content }}</div>
<div class="message-actions">
<el-button size="small" type="primary" @click="goToPost(message.postId)"></el-button>
<el-button size="small" @click="markAsRead(message.id)" v-if="!message.isRead"></el-button>
</div>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="点赞通知" name="likes">
<div class="message-list">
<el-skeleton :loading="loading" :rows="3" animated />
<el-empty v-if="!loading && likeMessages.length === 0" description="暂无点赞通知">
<el-button type="primary" @click="refreshMessages"></el-button>
</el-empty>
<div v-if="!loading && likeMessages.length > 0">
<div v-for="message in likeMessages" :key="message.id" class="message-item">
<div class="message-header">
<div class="message-title">{{ message.nickname }} 点赞了你的帖子</div>
<div class="message-time">{{ formatTime(message.createdAt) }}</div>
</div>
<div class="message-content">{{ message.postTitle }}</div>
<div class="message-actions">
<el-button size="small" type="primary" @click="goToPost(message.postId)"></el-button>
<el-button size="small" @click="markAsRead(message.id)" v-if="!message.isRead"></el-button>
</div>
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- 操作按钮 -->
<div class="message-actions-bar">
<el-button @click="markAllAsRead"></el-button>
<el-button @click="clearReadMessages"></el-button>
<el-button type="primary" @click="refreshMessages"></el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage, ElTabs, ElTabPane, ElSkeleton, ElEmpty, ElButton } from 'element-plus';
const router = useRouter();
//
const activeTab = ref('system');
const loading = ref(true);
//
const systemMessages = ref([
{
id: 1,
title: '欢迎使用UniLife',
content: '欢迎来到UniLife这里是你的大学生活助手。',
createdAt: '2023-12-01T10:00:00Z',
isRead: false
},
{
id: 2,
title: '系统维护通知',
content: '系统将于今晚进行维护预计持续2小时。',
createdAt: '2023-12-02T14:30:00Z',
isRead: true
}
]);
const commentMessages = ref([
{
id: 3,
nickname: '张同学',
content: '很有用的分享,谢谢!',
postId: 1,
postTitle: '大学生活经验分享',
createdAt: '2023-12-01T15:20:00Z',
isRead: false
}
]);
const likeMessages = ref([
{
id: 4,
nickname: '李同学',
postId: 1,
postTitle: '大学生活经验分享',
createdAt: '2023-12-01T12:10:00Z',
isRead: false
}
]);
//
const formatTime = (timeString: string) => {
const date = new Date(timeString);
const now = new Date();
const diff = now.getTime() - date.getTime();
if (diff < 60000) return '刚刚';
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`;
if (diff < 604800000) return `${Math.floor(diff / 86400000)}天前`;
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
};
//
const handleTabClick = (tab: any) => {
console.log('切换到标签页:', tab.props.name);
//
};
//
const markAsRead = (messageId: number) => {
//
const allMessages = [...systemMessages.value, ...commentMessages.value, ...likeMessages.value];
const message = allMessages.find(msg => msg.id === messageId);
if (message) {
message.isRead = true;
ElMessage.success('已标记为已读');
}
};
//
const markAllAsRead = () => {
systemMessages.value.forEach(msg => msg.isRead = true);
commentMessages.value.forEach(msg => msg.isRead = true);
likeMessages.value.forEach(msg => msg.isRead = true);
ElMessage.success('已全部标记为已读');
};
//
const clearReadMessages = () => {
systemMessages.value = systemMessages.value.filter(msg => !msg.isRead);
commentMessages.value = commentMessages.value.filter(msg => !msg.isRead);
likeMessages.value = likeMessages.value.filter(msg => !msg.isRead);
ElMessage.success('已清除已读消息');
};
//
const refreshMessages = () => {
loading.value = true;
// API
setTimeout(() => {
loading.value = false;
ElMessage.success('刷新成功');
}, 1000);
};
//
const goToPost = (postId: number) => {
router.push(`/post/${postId}`);
};
onMounted(() => {
//
setTimeout(() => {
loading.value = false;
}, 1000);
});
</script>
<style scoped>
.messages-view {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.page-header {
margin-bottom: 30px;
}
.page-header h1 {
font-size: 28px;
color: var(--el-text-color-primary);
margin-bottom: 8px;
}
.page-header p {
color: var(--el-text-color-secondary);
}
.message-tabs {
margin-bottom: 30px;
}
.message-list {
min-height: 300px;
}
.message-item {
padding: 16px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
margin-bottom: 12px;
background: var(--el-bg-color);
transition: box-shadow 0.2s;
}
.message-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.message-title {
font-weight: 600;
color: var(--el-text-color-primary);
}
.message-time {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.message-content {
color: var(--el-text-color-regular);
line-height: 1.6;
margin-bottom: 12px;
}
.message-actions {
display: flex;
gap: 8px;
}
.message-actions-bar {
display: flex;
gap: 12px;
justify-content: center;
padding: 20px 0;
border-top: 1px solid var(--el-border-color-lighter);
}
/* 响应式设计 */
@media (max-width: 768px) {
.messages-view {
padding: 10px;
}
.message-header {
flex-direction: column;
align-items: flex-start;
}
.message-time {
margin-top: 4px;
}
.message-actions-bar {
flex-direction: column;
align-items: center;
}
}
</style>

@ -0,0 +1,410 @@
<template>
<div class="settings-view">
<div class="page-header">
<h1>设置</h1>
<p>管理你的账户偏好和应用设置</p>
</div>
<div class="settings-content">
<!-- 通知设置 -->
<el-card class="settings-card" shadow="hover">
<template #header>
<h3>通知设置</h3>
</template>
<div class="setting-item">
<div class="setting-info">
<div class="setting-title">邮件通知</div>
<div class="setting-desc">接收新评论和点赞的邮件通知</div>
</div>
<el-switch v-model="notificationSettings.email" />
</div>
<el-divider />
<div class="setting-item">
<div class="setting-info">
<div class="setting-title">浏览器通知</div>
<div class="setting-desc">在浏览器中显示实时通知</div>
</div>
<el-switch v-model="notificationSettings.browser" />
</div>
<el-divider />
<div class="setting-item">
<div class="setting-info">
<div class="setting-title">私信通知</div>
<div class="setting-desc">接收新私信的通知</div>
</div>
<el-switch v-model="notificationSettings.message" />
</div>
</el-card>
<!-- 隐私设置 -->
<el-card class="settings-card" shadow="hover">
<template #header>
<h3>隐私设置</h3>
</template>
<div class="setting-item">
<div class="setting-info">
<div class="setting-title">个人资料可见性</div>
<div class="setting-desc">其他用户是否可以查看你的个人资料</div>
</div>
<el-select v-model="privacySettings.profileVisibility" style="width: 140px;">
<el-option label="公开" value="public" />
<el-option label="仅好友" value="friends" />
<el-option label="私密" value="private" />
</el-select>
</div>
<el-divider />
<div class="setting-item">
<div class="setting-info">
<div class="setting-title">在线状态</div>
<div class="setting-desc">是否显示你的在线状态</div>
</div>
<el-switch v-model="privacySettings.showOnlineStatus" />
</div>
<el-divider />
<div class="setting-item">
<div class="setting-info">
<div class="setting-title">活动历史</div>
<div class="setting-desc">是否保存你的活动历史记录</div>
</div>
<el-switch v-model="privacySettings.saveActivityHistory" />
</div>
</el-card>
<!-- 界面设置 -->
<el-card class="settings-card" shadow="hover">
<template #header>
<h3>界面设置</h3>
</template>
<div class="setting-item">
<div class="setting-info">
<div class="setting-title">主题模式</div>
<div class="setting-desc">选择你喜欢的界面主题</div>
</div>
<el-select v-model="interfaceSettings.theme" style="width: 120px;">
<el-option label="自动" value="auto" />
<el-option label="浅色" value="light" />
<el-option label="深色" value="dark" />
</el-select>
</div>
<el-divider />
<div class="setting-item">
<div class="setting-info">
<div class="setting-title">语言</div>
<div class="setting-desc">选择界面显示语言</div>
</div>
<el-select v-model="interfaceSettings.language" style="width: 120px;">
<el-option label="中文" value="zh-CN" />
<el-option label="English" value="en-US" />
</el-select>
</div>
<el-divider />
<div class="setting-item">
<div class="setting-info">
<div class="setting-title">页面大小</div>
<div class="setting-desc">每页显示的内容数量</div>
</div>
<el-select v-model="interfaceSettings.pageSize" style="width: 100px;">
<el-option label="10" :value="10" />
<el-option label="20" :value="20" />
<el-option label="50" :value="50" />
</el-select>
</div>
</el-card>
<!-- 账户安全 -->
<el-card class="settings-card" shadow="hover">
<template #header>
<h3>账户安全</h3>
</template>
<div class="setting-item">
<div class="setting-info">
<div class="setting-title">两步验证</div>
<div class="setting-desc">为你的账户添加额外的安全保护</div>
</div>
<el-button size="small" @click="setupTwoFactor">
{{ securitySettings.twoFactorEnabled ? '已启用' : '启用' }}
</el-button>
</div>
<el-divider />
<div class="setting-item">
<div class="setting-info">
<div class="setting-title">登录通知</div>
<div class="setting-desc">新设备登录时发送通知</div>
</div>
<el-switch v-model="securitySettings.loginNotification" />
</div>
<el-divider />
<div class="setting-item">
<div class="setting-info">
<div class="setting-title">会话管理</div>
<div class="setting-desc">查看和管理你的登录会话</div>
</div>
<el-button size="small" @click="manageSessions"></el-button>
</div>
</el-card>
<!-- 数据管理 -->
<el-card class="settings-card" shadow="hover">
<template #header>
<h3>数据管理</h3>
</template>
<div class="setting-item">
<div class="setting-info">
<div class="setting-title">数据导出</div>
<div class="setting-desc">下载你的所有数据</div>
</div>
<el-button size="small" @click="exportData"></el-button>
</div>
<el-divider />
<div class="setting-item">
<div class="setting-info">
<div class="setting-title">清除缓存</div>
<div class="setting-desc">清除本地缓存数据</div>
</div>
<el-button size="small" @click="clearCache"></el-button>
</div>
<el-divider />
<div class="setting-item danger">
<div class="setting-info">
<div class="setting-title">删除账户</div>
<div class="setting-desc">永久删除你的账户和所有数据</div>
</div>
<el-button size="small" type="danger" @click="deleteAccount"></el-button>
</div>
</el-card>
</div>
<!-- 保存按钮 -->
<div class="save-actions">
<el-button @click="resetSettings"></el-button>
<el-button type="primary" @click="saveSettings" :loading="saving">保存设置</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElMessage, ElMessageBox, ElCard, ElSwitch, ElSelect, ElOption, ElButton, ElDivider } from 'element-plus';
//
const notificationSettings = ref({
email: true,
browser: false,
message: true
});
const privacySettings = ref({
profileVisibility: 'public',
showOnlineStatus: true,
saveActivityHistory: true
});
const interfaceSettings = ref({
theme: 'auto',
language: 'zh-CN',
pageSize: 20
});
const securitySettings = ref({
twoFactorEnabled: false,
loginNotification: true
});
const saving = ref(false);
//
const saveSettings = async () => {
saving.value = true;
try {
// API
await new Promise(resolve => setTimeout(resolve, 1000));
// API
// await settingsApi.saveSettings({
// notification: notificationSettings.value,
// privacy: privacySettings.value,
// interface: interfaceSettings.value,
// security: securitySettings.value
// });
ElMessage.success('设置保存成功');
} catch (error) {
ElMessage.error('设置保存失败');
} finally {
saving.value = false;
}
};
//
const resetSettings = () => {
notificationSettings.value = {
email: true,
browser: false,
message: true
};
privacySettings.value = {
profileVisibility: 'public',
showOnlineStatus: true,
saveActivityHistory: true
};
interfaceSettings.value = {
theme: 'auto',
language: 'zh-CN',
pageSize: 20
};
securitySettings.value = {
twoFactorEnabled: false,
loginNotification: true
};
ElMessage.success('设置已重置');
};
//
const setupTwoFactor = () => {
ElMessage.info('两步验证功能开发中');
};
//
const manageSessions = () => {
ElMessage.info('会话管理功能开发中');
};
//
const exportData = () => {
ElMessage.info('数据导出功能开发中');
};
//
const clearCache = () => {
localStorage.clear();
sessionStorage.clear();
ElMessage.success('缓存已清除');
};
//
const deleteAccount = async () => {
try {
await ElMessageBox.confirm(
'确定要删除账户吗?此操作不可逆,所有数据将永久丢失。',
'删除账户',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'error'
}
);
ElMessage.info('账户删除功能开发中');
} catch {
//
}
};
onMounted(() => {
//
// loadUserSettings();
});
</script>
<style scoped>
.settings-view {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.page-header {
margin-bottom: 30px;
}
.page-header h1 {
font-size: 28px;
color: var(--el-text-color-primary);
margin-bottom: 8px;
}
.page-header p {
color: var(--el-text-color-secondary);
}
.settings-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.settings-card {
overflow: visible;
}
.settings-card :deep(.el-card__header) {
padding: 18px 20px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.settings-card h3 {
margin: 0;
font-size: 18px;
color: var(--el-text-color-primary);
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
}
.setting-item.danger {
border-left: 3px solid var(--el-color-danger);
padding-left: 16px;
}
.setting-info {
flex: 1;
}
.setting-title {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
margin-bottom: 4px;
}
.setting-desc {
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.4;
}
.save-actions {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid var(--el-border-color-lighter);
}
/* 响应式设计 */
@media (max-width: 768px) {
.settings-view {
padding: 10px;
}
.setting-item {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.save-actions {
flex-direction: column;
align-items: center;
}
}
</style>

@ -139,4 +139,26 @@ public class UserController {
} }
return userService.updateEmail(userId, emailDTO); return userService.updateEmail(userId, emailDTO);
} }
@Operation(summary = "获取用户统计数据")
@GetMapping("stats")
public Result<?> getUserStats() {
// 从当前上下文获取用户ID
Long userId = BaseContext.getId();
if (userId == null) {
return Result.error(401, "未登录");
}
return userService.getUserStats(userId);
}
@Operation(summary = "获取用户最近帖子")
@GetMapping("recent-posts")
public Result<?> getUserRecentPosts(@RequestParam(value = "limit", defaultValue = "5") Integer limit) {
// 从当前上下文获取用户ID
Long userId = BaseContext.getId();
if (userId == null) {
return Result.error(401, "未登录");
}
return userService.getUserRecentPosts(userId, limit);
}
} }

@ -30,4 +30,10 @@ public interface UserService {
Result updateAvatar(Long userId, MultipartFile file); Result updateAvatar(Long userId, MultipartFile file);
Result updateEmail(Long userId, UpdateEmailDTO emailDTO); Result updateEmail(Long userId, UpdateEmailDTO emailDTO);
// 用户统计数据
Result getUserStats(Long userId);
// 用户最近帖子
Result getUserRecentPosts(Long userId, Integer limit);
} }

@ -5,6 +5,9 @@ import cn.hutool.core.util.RandomUtil;
import com.unilife.common.constant.RedisConstant; import com.unilife.common.constant.RedisConstant;
import com.unilife.common.result.Result; import com.unilife.common.result.Result;
import com.unilife.mapper.UserMapper; import com.unilife.mapper.UserMapper;
import com.unilife.mapper.PostMapper;
import com.unilife.mapper.CommentMapper;
import com.unilife.mapper.PostLikeMapper;
import com.unilife.model.dto.LoginDTO; import com.unilife.model.dto.LoginDTO;
import com.unilife.model.dto.LoginEmailDTO; import com.unilife.model.dto.LoginEmailDTO;
import com.unilife.model.dto.RegisterDTO; import com.unilife.model.dto.RegisterDTO;
@ -12,6 +15,7 @@ import com.unilife.model.dto.UpdateEmailDTO;
import com.unilife.model.dto.UpdatePasswordDTO; import com.unilife.model.dto.UpdatePasswordDTO;
import com.unilife.model.dto.UpdateProfileDTO; import com.unilife.model.dto.UpdateProfileDTO;
import com.unilife.model.entity.User; import com.unilife.model.entity.User;
import com.unilife.model.entity.Post;
import com.unilife.model.vo.LoginVO; import com.unilife.model.vo.LoginVO;
import com.unilife.model.vo.RegisterVO; import com.unilife.model.vo.RegisterVO;
import com.unilife.service.IPLocationService; import com.unilife.service.IPLocationService;
@ -37,6 +41,7 @@ import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import static com.unilife.common.constant.RedisConstant.LOGIN_EMAIL_KEY; import static com.unilife.common.constant.RedisConstant.LOGIN_EMAIL_KEY;
@ -52,6 +57,15 @@ public class UserServiceImpl implements UserService {
@Autowired @Autowired
private UserMapper userMapper; private UserMapper userMapper;
@Autowired
private PostMapper postMapper;
@Autowired
private CommentMapper commentMapper;
@Autowired
private PostLikeMapper postLikeMapper;
@Autowired @Autowired
private JavaMailSender mailSender; private JavaMailSender mailSender;
@ -489,4 +503,72 @@ public class UserServiceImpl implements UserService {
return Result.success(null, "邮箱更新成功"); return Result.success(null, "邮箱更新成功");
} }
@Override
public Result getUserStats(Long userId) {
// 检查用户是否存在
User user = userMapper.getUserById(userId);
if (user == null) {
return Result.error(404, "用户不存在");
}
// 获取用户统计数据
Integer totalPosts = postMapper.getCountByUserId(userId);
// 获取用户所有帖子的总点赞数
List<Post> userPosts = postMapper.getListByUserId(userId, "latest");
Integer totalLikes = userPosts.stream()
.mapToInt(post -> post.getLikeCount() != null ? post.getLikeCount() : 0)
.sum();
// 获取用户所有帖子的总评论数
Integer totalComments = userPosts.stream()
.mapToInt(post -> post.getCommentCount() != null ? post.getCommentCount() : 0)
.sum();
// 获取用户所有帖子的总浏览数
Integer totalViews = userPosts.stream()
.mapToInt(post -> post.getViewCount() != null ? post.getViewCount() : 0)
.sum();
// 构建统计数据
Map<String, Object> stats = new HashMap<>();
stats.put("totalPosts", totalPosts != null ? totalPosts : 0);
stats.put("totalLikes", totalLikes);
stats.put("totalComments", totalComments);
stats.put("totalViews", totalViews);
return Result.success(stats);
}
@Override
public Result getUserRecentPosts(Long userId, Integer limit) {
// 检查用户是否存在
User user = userMapper.getUserById(userId);
if (user == null) {
return Result.error(404, "用户不存在");
}
// 参数校验
if (limit == null || limit <= 0) {
limit = 5;
}
if (limit > 20) {
limit = 20; // 限制最大数量
}
// 获取用户最近的帖子
List<Post> recentPosts = postMapper.getListByUserId(userId, "latest");
// 限制数量
if (recentPosts.size() > limit) {
recentPosts = recentPosts.subList(0, limit);
}
// 返回结果
Map<String, Object> data = new HashMap<>();
data.put("list", recentPosts);
return Result.success(data);
}
} }

Loading…
Cancel
Save