diff --git a/Front/vue-unilife/FORUM_FEATURES.md b/Front/vue-unilife/FORUM_FEATURES.md new file mode 100644 index 0000000..9fe3303 --- /dev/null +++ b/Front/vue-unilife/FORUM_FEATURES.md @@ -0,0 +1,82 @@ +# 论坛功能完善记录 + +## 已完成的功能 + +### 1. 帖子点赞功能 +- ✅ 在帖子详情页面添加了点赞/取消点赞按钮 +- ✅ 在帖子列表页面为每个帖子添加了点赞按钮 +- ✅ 实时更新点赞数量和状态 +- ✅ 未登录用户点击点赞会提示登录 +- ✅ 在postStore中实现了likePost方法 + +### 2. 评论系统 +- ✅ 创建了评论API接口 (`/src/api/comment.ts`) +- ✅ 创建了完整的评论组件 (`/src/components/CommentSection.vue`) +- ✅ 支持发表评论和回复评论 +- ✅ 支持评论的点赞和取消点赞 +- ✅ 支持删除自己的评论 +- ✅ 评论时间的友好显示(几分钟前、几小时前等) +- ✅ 未登录用户会提示登录 + +### 3. 用户体验优化 +- ✅ 添加了加载状态和错误处理 +- ✅ 优化了UI布局和样式 +- ✅ 支持未登录用户浏览但限制互动功能 +- ✅ 完善了错误提示和成功提示 + +## 技术实现 + +### API接口 +- `POST /posts/{id}/like` - 点赞/取消点赞帖子 +- `GET /comments/post/{postId}` - 获取帖子评论列表 +- `POST /comments` - 发表评论 +- `DELETE /comments/{id}` - 删除评论 +- `POST /comments/{id}/like` - 点赞/取消点赞评论 + +### 组件结构 +``` +PostListView.vue - 帖子列表页面 +├── 点赞按钮 +└── 帖子卡片交互 + +PostDetailView.vue - 帖子详情页面 +├── 点赞按钮 +└── CommentSection.vue - 评论区组件 + ├── 评论表单 + ├── 评论列表 + ├── 回复功能 + └── 评论点赞功能 +``` + +### 状态管理 +- 在PostStore中添加了`likePost`方法 +- 实现了帖子点赞状态的同步更新 +- 在评论组件中管理评论数据和状态 + +## 功能特点 + +### 点赞系统 +- 实时更新点赞数量 +- 视觉反馈(已点赞的按钮变为主色调) +- 防重复点击(loading状态) +- 同步更新列表和详情页状态 + +### 评论系统 +- 支持多层嵌套回复 +- 评论和回复的独立点赞 +- 用户权限控制(只能删除自己的评论) +- 富文本内容支持 +- 实时评论数量更新 + +### 安全和权限 +- 所有交互功能都需要登录 +- 后端API有用户身份验证 +- 前端有相应的权限检查和提示 + +## 待优化项目 +- [ ] 添加评论分页功能 +- [ ] 实现评论的编辑功能 +- [ ] 添加评论的举报功能 +- [ ] 优化评论的实时更新 +- [ ] 添加富文本编辑器支持图片等 +- [ ] 实现评论的@功能 \ No newline at end of file diff --git a/Front/vue-unilife/src/api/comment.ts b/Front/vue-unilife/src/api/comment.ts new file mode 100644 index 0000000..20f61f9 --- /dev/null +++ b/Front/vue-unilife/src/api/comment.ts @@ -0,0 +1,61 @@ +import { get, post as httpPost, del } from './request'; + +// 评论类型定义 +export interface CommentItem { + id: number; + postId: number; + userId: number; + nickname: string; + avatar: string; + content: string; + parentId?: number; + likeCount: number; + isLiked: boolean; + createdAt: string; + replies: CommentItem[]; +} + +export interface CreateCommentParams { + postId: number; + content: string; + parentId?: number; +} + +// 评论API方法 +export default { + // 获取帖子评论列表 + getCommentsByPostId(postId: number) { + return get<{ + code: number; + data: { + total: number; + list: CommentItem[] + } + }>(`/comments/post/${postId}`); + }, + + // 创建评论 + createComment(data: CreateCommentParams) { + return httpPost<{ + code: number; + message: string; + data: { commentId: number } + }>('/comments', data); + }, + + // 删除评论 + deleteComment(id: number) { + return del<{ + code: number; + message: string + }>(`/comments/${id}`); + }, + + // 点赞/取消点赞评论 + likeComment(id: number) { + return httpPost<{ + code: number; + message: string + }>(`/comments/${id}/like`); + } +}; \ No newline at end of file diff --git a/Front/vue-unilife/src/api/index.ts b/Front/vue-unilife/src/api/index.ts index 74f74be..9b9b67f 100644 --- a/Front/vue-unilife/src/api/index.ts +++ b/Front/vue-unilife/src/api/index.ts @@ -2,10 +2,19 @@ import userApi from './user'; import postApi from './post'; import resourceApi from './resource'; import scheduleApi from './schedule'; +import commentApi from './comment'; export { userApi, postApi, resourceApi, - scheduleApi + scheduleApi, + commentApi }; + +export * from './user'; +export * from './post'; +export * from './comment'; +export * from './resource'; +export * from './schedule'; +export * from './search'; diff --git a/Front/vue-unilife/src/api/search.ts b/Front/vue-unilife/src/api/search.ts new file mode 100644 index 0000000..78a5c39 --- /dev/null +++ b/Front/vue-unilife/src/api/search.ts @@ -0,0 +1,64 @@ +import { get } from './request' + +// 搜索相关接口类型定义 +export interface SearchParams { + keyword: string + type?: 'all' | 'post' | 'resource' | 'user' + categoryId?: number + sortBy?: 'time' | 'relevance' | 'popularity' + page?: number + size?: number +} + +export interface SearchItem { + id: number + title: string + summary: string + type: 'post' | 'resource' | 'user' + author: string + avatar: string + categoryName: string + createdAt: string + likeCount: number + viewCount: number + highlights?: string[] +} + +export interface SearchResult { + items: SearchItem[] + total: number + page: number + size: number + keyword: string + searchTime: number +} + +// 综合搜索 +export const search = (params: SearchParams) => { + return get('/search', params) +} + +// 搜索帖子 +export const searchPosts = (params: SearchParams) => { + return get('/search/posts', params) +} + +// 搜索资源 +export const searchResources = (params: SearchParams) => { + return get('/search/resources', params) +} + +// 搜索用户 +export const searchUsers = (params: SearchParams) => { + return get('/search/users', params) +} + +// 获取搜索建议 +export const getSuggestions = (keyword: string) => { + return get('/search/suggestions', { keyword }) +} + +// 获取热门搜索词 +export const getHotKeywords = () => { + return get('/search/hot-keywords') +} \ No newline at end of file diff --git a/Front/vue-unilife/src/components/CommentSection.vue b/Front/vue-unilife/src/components/CommentSection.vue new file mode 100644 index 0000000..2662d3a --- /dev/null +++ b/Front/vue-unilife/src/components/CommentSection.vue @@ -0,0 +1,521 @@ + + + + + \ No newline at end of file diff --git a/Front/vue-unilife/src/router/index.ts b/Front/vue-unilife/src/router/index.ts index 91a9c1a..7eb3348 100644 --- a/Front/vue-unilife/src/router/index.ts +++ b/Front/vue-unilife/src/router/index.ts @@ -78,6 +78,13 @@ const routes: Array = [ name: 'Schedule', component: () => import('../views/schedule/ScheduleView.vue'), meta: { title: '日程管理 - UniLife', requiresAuth: true } + }, + // 搜索页面 - 无需登录 + { + path: 'search', // URL: /search + name: 'Search', + component: () => import('../views/SearchView.vue'), + meta: { title: '搜索 - UniLife', requiresAuth: false } } ] }, diff --git a/Front/vue-unilife/src/stores/postStore.ts b/Front/vue-unilife/src/stores/postStore.ts index 9277bb6..2fde20c 100644 --- a/Front/vue-unilife/src/stores/postStore.ts +++ b/Front/vue-unilife/src/stores/postStore.ts @@ -88,7 +88,7 @@ export const usePostStore = defineStore('post', { this.currentPost = response.data; } else { // Construct a more informative error or use a default - const errorMessage = response?.message || (response?.data?.toString() ? `错误: ${response.data.toString()}` : '获取帖子详情失败'); + const errorMessage = response?.data?.toString() ? `错误: ${response.data.toString()}` : '获取帖子详情失败'; console.error('Failed to fetch post detail:', response); throw new Error(errorMessage); } @@ -96,15 +96,16 @@ export const usePostStore = defineStore('post', { // 检查是否是未登录错误 if (error.response && error.response.status === 401) { this.error = '您需要登录后才能查看帖子详情'; - ElMessage.warning(this.error); + ElMessage.warning('您需要登录后才能查看帖子详情'); // 将用户重定向到登录页面,并记录需要返回的帖子详情页面 const currentPath = `/post/${id}`; window.location.href = `/login?redirect=${encodeURIComponent(currentPath)}`; return; } else { // 处理其他错误 - this.error = error.message || '加载帖子详情时发生未知错误'; - ElMessage.error(this.error); + const errorMsg = error.message || '加载帖子详情时发生未知错误'; + this.error = errorMsg; + ElMessage.error(errorMsg); } } finally { this.loading = false; @@ -140,6 +141,47 @@ export const usePostStore = defineStore('post', { this.currentPage = 1; await this.fetchPosts(); } + }, + + async likePost(postId: number) { + try { + const response = await postApi.likePost(postId); + if (response && response.code === 200) { + // 更新当前帖子的点赞状态 + if (this.currentPost && this.currentPost.id === postId) { + if (this.currentPost.isLiked) { + this.currentPost.likeCount -= 1; + this.currentPost.isLiked = false; + ElMessage.success('取消点赞成功'); + } else { + this.currentPost.likeCount += 1; + this.currentPost.isLiked = true; + ElMessage.success('点赞成功'); + } + } + + // 更新帖子列表中的对应帖子 + const postInList = this.posts.find(post => post.id === postId); + if (postInList) { + if (postInList.isLiked) { + postInList.likeCount -= 1; + postInList.isLiked = false; + } else { + postInList.likeCount += 1; + postInList.isLiked = true; + } + } + } else { + throw new Error('操作失败'); + } + } catch (error: any) { + if (error.response && error.response.status === 401) { + ElMessage.warning('请先登录'); + // 可以在这里处理登录跳转 + } else { + ElMessage.error(error.message || '操作失败,请稍后重试'); + } + } } } }); diff --git a/Front/vue-unilife/src/stores/user.ts b/Front/vue-unilife/src/stores/user.ts index a215cb8..45a22e3 100644 --- a/Front/vue-unilife/src/stores/user.ts +++ b/Front/vue-unilife/src/stores/user.ts @@ -170,11 +170,11 @@ export const useUserStore = defineStore('user', () => { }) => { try { loading.value = true; - const params: UpdatePasswordParams = {}; + const params: Partial = {}; if (data.newPassword) params.newPassword = data.newPassword; if (data.code) params.code = data.code; - const res = await userApi.updatePassword(params); + const res = await userApi.updatePassword(params as UpdatePasswordParams); if (res.code === 200) { ElMessage.success('密码修改成功'); diff --git a/Front/vue-unilife/src/views/SearchView.vue b/Front/vue-unilife/src/views/SearchView.vue new file mode 100644 index 0000000..25093f8 --- /dev/null +++ b/Front/vue-unilife/src/views/SearchView.vue @@ -0,0 +1,391 @@ + + + + + \ No newline at end of file diff --git a/Front/vue-unilife/src/views/forum/PostDetailView.vue b/Front/vue-unilife/src/views/forum/PostDetailView.vue index 7d14c72..a7cba32 100644 --- a/Front/vue-unilife/src/views/forum/PostDetailView.vue +++ b/Front/vue-unilife/src/views/forum/PostDetailView.vue @@ -28,36 +28,54 @@
-
- {{ postStore.currentPost.viewCount }} 次浏览 - {{ postStore.currentPost.likeCount }} 个点赞 - {{ postStore.currentPost.commentCount }} 条评论 - 已点赞 +
+
+ {{ postStore.currentPost.viewCount }} 次浏览 + {{ postStore.currentPost.likeCount }} 个点赞 + {{ postStore.currentPost.commentCount }} 条评论 +
+
+ + + {{ postStore.currentPost.isLiked ? '已点赞' : '点赞' }} + + + + 点赞 + +
- + - - - +