Merge remote-tracking branch 'origin/main'

# Conflicts:
#	珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/post/service/impl/CommentServiceImpl.java
main
forely 4 weeks ago
commit a467856a7c

@ -1,21 +1,21 @@
##本地开发环境 ##本地开发环境
lj: #lj:
db: # db:
host: 192.168.59.129 # host: 192.168.59.129
password: Forely123! # password: Forely123!
redis: # redis:
host: 192.168.59.129 # host: 192.168.59.129
port: 6379 # port: 6379
password: Forely123! # password: Forely123!
rabbitmq: # rabbitmq:
host: 192.168.59.129 # host: 192.168.59.129
port: 5672 # port: 5672
username: admin # username: admin
password: Forely123! # password: Forely123!
minio: # minio:
endpoint: http://192.168.59.129:9000 # endpoint: http://192.168.59.129:9000
accessKey: forely # accessKey: forely
secretKey: Forely123! # secretKey: Forely123!
#lj: #lj:
# db: # db:
@ -33,4 +33,22 @@ lj:
# minio: # minio:
# endpoint: http://192.168.125.128:9000 # endpoint: http://192.168.125.128:9000
# accessKey: minio_admin # accessKey: minio_admin
# secretKey: Minio@1234 # secretKey: Minio@1234
lj:
db:
host: localhost
password: 123456
redis:
host: localhost
port: 6379
password: 123456
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
minio:
endpoint: http://localhost:9005
accessKey: leezt
secretKey: lzt264610

@ -1,3 +1,4 @@
USE luojia_channel; -- 指定目标数据库
## 用户表 ## 用户表
DROP TABLE IF EXISTS `user`; DROP TABLE IF EXISTS `user`;

@ -92,7 +92,7 @@ const colleges = ref([
]); ]);
// //
const defaultAvatar = require("@/assets/default-avatar/boy_4.png"); const defaultAvatar = require("@/assets/default-avatar/boy_1.png");
// //
const isLoggedIn = computed(() => userStore.isLoggedIn); const isLoggedIn = computed(() => userStore.isLoggedIn);

@ -180,17 +180,6 @@ async function sendCode() {
} }
//
//function Login1() {
// userStore.login({
// avatar:require('@/assets/default-avatar/boy_1.png'),
// userName: '',
// userid:1
// });
// emit('LoginSuccess');
//}
// //
async function login() { async function login() {
if (!isvalidForm.value) return; if (!isvalidForm.value) return;

@ -27,9 +27,9 @@
</div> </div>
<p class="post-summary">{{ post.summary }}</p> <p class="post-summary">{{ post.summary }}</p>
<div class="post-stats"> <div class="post-stats">
<span>热度 {{ post.likes }}</span> <span>浏览量 {{ post.viewCount }}</span>
<span>评论 {{ post.comments }}</span> <span>评论 {{ post.comments }}</span>
<span> {{ post.favorites }}</span> <span> {{ post.likes }}</span>
</div> </div>
</div> </div>
</div> </div>

@ -34,7 +34,7 @@ const routes = [
component: PostDetail component: PostDetail
}, },
{ {
path: '/user', path: '/user/:userId',
name: 'UserPage', name: 'UserPage',
component: UserPage, component: UserPage,
meta: { requiresAuth: true } meta: { requiresAuth: true }

@ -2,6 +2,20 @@ import { defineStore } from "pinia";
import request from "@/utils/request"; import request from "@/utils/request";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
// 递归删除子评论的工具函数
function removeReply(list, commentId) {
for (let i = 0; i < list.length; i++) {
if (list[i].id === commentId) {
list.splice(i, 1);
return true;
}
if (list[i].replies && list[i].replies.length > 0) {
if (removeReply(list[i].replies, commentId)) return true;
}
}
return false;
}
export const usePostDetailStore = defineStore("postDetail", { export const usePostDetailStore = defineStore("postDetail", {
state: () => ({ state: () => ({
post: null, // 帖子主要信息 post: null, // 帖子主要信息
@ -16,67 +30,67 @@ export const usePostDetailStore = defineStore("postDetail", {
commentsFinished: false, // 是否加载完全部评论 commentsFinished: false, // 是否加载完全部评论
}), }),
actions: { actions: {
// ...existing code... async fetchComments() {
async fetchComments() { if (this.commentsLoading || this.commentsFinished) return;
if (this.commentsLoading || this.commentsFinished) return; this.commentsLoading = true;
this.commentsLoading = true; // 拼接参数到URL
// 拼接参数到URL const params = [
const params = [ `lastVal=${this.lastVal}`,
`lastVal=${this.lastVal}`, `offset=${this.offset}`,
`offset=${this.offset}`, `size=${this.size}`,
`size=${this.size}`, `postId=${this.post.postId}`
`postId=${this.post.postId}` ].join('&');
].join('&'); const url = `/comment/list?${params}`;
const url = `/comment/list?${params}`; try {
try { const response = await request.get(url);
const response = await request.get(url); if (response.code === 200) {
if (response.code === 200) { // 初始化每条评论的子评论分页状态
// 初始化每条评论的子评论分页状态 const comments = (response.data.records || []).map(item => ({
const comments = (response.data.records || []).map(item => ({ ...item,
...item, replies: [],
replies: [], showReplies: false, // 是否显示子评论
repliesLastVal: Date.now(), repliesLastVal: Date.now(),
repliesOffset: 0, repliesOffset: 0,
repliesSize: 5, repliesSize: 5,
repliesLoading: false, repliesLoading: false,
repliesFinished: false, repliesFinished: false,
})); }));
this.comments.push(...comments); this.comments.push(...comments);
this.lastVal = response.data.lastVal; this.lastVal = response.data.lastVal;
this.offset = response.data.offset; this.offset = response.data.offset;
if (comments.length < this.size) { if (comments.length < this.size) {
this.commentsFinished = true; // 如果评论数少于每页大小,标记为已加载完 this.commentsFinished = true; // 如果评论数少于每页大小,标记为已加载完
}
}
else {
ElMessage({
message: '获取评论失败,请稍后重试',
type: 'error',
duration: 500
});
}
} catch (error) {
console.error("获取评论失败:", error);
alert(error.response?.message || '获取评论失败,请稍后重试');
}finally {
this.commentsLoading = false;
} }
} },
else {
ElMessage({
message: '获取评论失败,请稍后重试',
type: 'error',
duration: 500
});
}
} catch (error) {
console.error("获取评论失败:", error);
alert(error.response?.message || '获取评论失败,请稍后重试');
}finally {
this.commentsLoading = false;
}
},
// ...existing code...
// 获取某条评论的子评论 // 获取某条评论的子评论
async fetchReplies(parentCommentId, commentObj) { async fetchReplies(parentCommentId, commentObj) {
if (commentObj.repliesLoading || commentObj.repliesFinished) return; if (commentObj.repliesLoading || commentObj.repliesFinished) return;
commentObj.repliesLoading = true; commentObj.repliesLoading = true;
// 请求子评论数据 /// 拼接参数到URL
const RequestReplyData = { const params = [
lastVal: commentObj.repliesLastVal, `lastVal=${commentObj.repliesLastVal}`,
offset: commentObj.repliesOffset, `offset=${commentObj.repliesOffset}`,
size: commentObj.repliesSize, `size=${commentObj.repliesSize}`,
postId: this.post?.postId, `postId=${this.post?.postId}`,
parentCommentId: parentCommentId, `parentCommentId=${parentCommentId}`
}; ].join('&');
const url = `/comment/list/reply?${params}`;
try { try {
const res = await request.post('/comment/list/reply', RequestReplyData); const res = await request.get(url);
if (res.code === 200) { if (res.code === 200) {
const records = (res.data.records || []).map(item => ({ const records = (res.data.records || []).map(item => ({
...item, ...item,
@ -106,6 +120,12 @@ async fetchComments() {
async fetchPostDetail(postId) { async fetchPostDetail(postId) {
this.detailLoading = true; this.detailLoading = true;
// 先重置评论相关状态,防止串数据
this.comments = [];
this.lastVal = Date.now();
this.offset = 0;
this.commentsFinished = false;
this.commentsLoading = false;
try { try {
// 获取帖子详情 // 获取帖子详情
const postRes = await request.get('/post/detail', { params: { id: postId } }); const postRes = await request.get('/post/detail', { params: { id: postId } });
@ -169,15 +189,17 @@ async fetchComments() {
async sendComment(newCommentData) { async sendComment(newCommentData) {
if (!newCommentData.content || !this.post?.postId) return; if (!newCommentData.content || !this.post?.postId) return;
const RequestData = { const RequestData = {
id:null,
postId: newCommentData.postId, // 帖子ID postId: newCommentData.postId, // 帖子ID
content: newCommentData.content, // 评论内容 content: newCommentData.content, // 评论内容
parentCommentId: newCommentData.parentCommentId, parentCommentId: newCommentData.parentCommentId,
}; };
try { try {
const res = await request.post('/comment', RequestData); const res = await request.post('/comment', RequestData);
console.log("发送评论返回:", res.data);
if (res.code === 200) { if (res.code === 200) {
const commentObj = { const commentObj = {
id: res.data.id, id: res.data,
content: newCommentData.content, content: newCommentData.content,
userId: newCommentData.userId, userId: newCommentData.userId,
userName: newCommentData.userName, userName: newCommentData.userName,
@ -191,6 +213,7 @@ async fetchComments() {
topId: newCommentData.topId, topId: newCommentData.topId,
isLike: 0, isLike: 0,
}; };
console.log("发送评论数据:", commentObj.id);
// 新增评论后刷新评论列表或插入到对应位置 // 新增评论后刷新评论列表或插入到对应位置
if (!newCommentData.parentCommentId) { if (!newCommentData.parentCommentId) {
// 一级评论,插入到最前面 // 一级评论,插入到最前面
@ -201,17 +224,17 @@ async fetchComments() {
commentObj.repliesSize = 5; commentObj.repliesSize = 5;
commentObj.repliesLoading = false; commentObj.repliesLoading = false;
commentObj.repliesFinished = false; commentObj.repliesFinished = false;
this.comments.unshift(commentObj); this.comments.unshift(commentObj);
this.post.commentCount = (this.post.commentCount || 0) + 1; // 更新帖子评论数
} else { } else {
// 回复,只插入到一级评论的 replies // 回复,只插入到一级评论的 replies
let parent = this.comments.find(c => c.id === newCommentData.parentCommentId); let parent = this.comments.find(c => c.id === newCommentData.topId);
if (parent) { if (parent) {
parent.replies.unshift(commentObj); parent.replies.push(commentObj);
parent.replyCount = (parent.replyCount || 0) + 1; parent.replyCount = (parent.replyCount || 0) + 1;
} }
} }
console.log("评论成功:", res); this.post.commentCount = (this.post.commentCount || 0) + 1; // 更新帖子评论数
console.log("评论成功:", commentObj);
}else { }else {
console.error("评论失败:", res); console.error("评论失败:", res);
ElMessage.error(res.message || '评论失败'); ElMessage.error(res.message || '评论失败');
@ -220,5 +243,29 @@ async fetchComments() {
alert(e.response?.message || '发送评论失败,请稍后重试'); alert(e.response?.message || '发送评论失败,请稍后重试');
} }
}, },
// 删除评论或回复
async deleteComment(commentId, parentCommentId = null) {
try {
const res = await request.delete('/comment', { params: { id: commentId} });
if (res.code === 200) {
// 一级评论
if (!parentCommentId) {
this.comments = this.comments.filter(c => c.id !== commentId);
} else {
// 回复评论,递归删除
removeReply(this.comments,commentId);
}
// 更新评论数
if (this.post && this.post.commentCount > 0) {
this.post.commentCount--;
}
ElMessage.success('删除成功');
} else {
ElMessage.error(res.message || '删除失败');
}
} catch (e) {
ElMessage.error(e.response?.message || '删除失败,请稍后重试');
}
},
}, },
}); });

@ -52,9 +52,9 @@ export const usePostListStore = defineStore('postList', {
avatar: post.userAvatar || require('@/assets/default-avatar/boy_1.png'), avatar: post.userAvatar || require('@/assets/default-avatar/boy_1.png'),
title: post.title, title: post.title,
summary: post.content ? post.content.slice(0, 40) + (post.content.length > 40 ? '...' : '') : '', summary: post.content ? post.content.slice(0, 40) + (post.content.length > 40 ? '...' : '') : '',
viewCount: post.viewCount || 0,
likes: post.likeCount || 0, likes: post.likeCount || 0,
comments: post.commentCount || 0, comments: post.commentCount || 0,
favorites: post.favoriteCount || 0,
category: post.category || '全部', category: post.category || '全部',
createTime: post.createTime, createTime: post.createTime,
userName: post.userName, userName: post.userName,
@ -70,6 +70,7 @@ export const usePostListStore = defineStore('postList', {
} }
} else { } else {
// 处理错误情况 // 处理错误情况
} }
} catch (error) { } catch (error) {
// 处理异常情况 // 处理异常情况

@ -2,7 +2,13 @@
<div class="post-detail-container"> <div class="post-detail-container">
<!-- 作者信息栏 --> <!-- 作者信息栏 -->
<div class="author-info" v-if="author && author.userName"> <div class="author-info" v-if="author && author.userName">
<img :src="author.userAvatar || require('@/assets/default-avatar/boy_1.png')" alt="头像" class="author-avatar" /> <img
:src="author.userAvatar || require('@/assets/default-avatar/boy_1.png')"
alt="头像"
class="author-avatar"
@click="goUserHome(author.userId)"
style="cursor: pointer;"
/>
<div class="author-details"> <div class="author-details">
<h3 class="author-name">{{ author.userName || '匿名用户' }}</h3> <h3 class="author-name">{{ author.userName || '匿名用户' }}</h3>
<p class="author-stats">粉丝数{{ author.followers ?? 0 }}</p> <p class="author-stats">粉丝数{{ author.followers ?? 0 }}</p>
@ -18,8 +24,8 @@
<h1 class="post-title">{{ postDetailStore.post?.title || '' }}</h1> <h1 class="post-title">{{ postDetailStore.post?.title || '' }}</h1>
<p class="post-body">{{ postDetailStore.post?.content || '' }}</p> <p class="post-body">{{ postDetailStore.post?.content || '' }}</p>
<div class="post-stats"> <div class="post-stats">
<span> 热度 {{ postDetailStore.post?.likeCount ?? 0 }}</span> <span> 浏览量 {{ postDetailStore.post?.viewCount ?? 0 }}</span>
<span> 点赞 {{ postDetailStore.post?.favoriteCount ?? 0 }}</span> <span> 点赞 {{ postDetailStore.post?.likeCount ?? 0 }}</span>
<span> 评论 {{ postDetailStore.post?.commentCount ?? 0 }}</span> <span> 评论 {{ postDetailStore.post?.commentCount ?? 0 }}</span>
</div> </div>
<div class="post-time"> <div class="post-time">
@ -32,7 +38,13 @@
<h2 class="comments-title">评论</h2> <h2 class="comments-title">评论</h2>
<ul class="comments-list"> <ul class="comments-list">
<li v-for="comment in postDetailStore.comments" :key="comment.id" class="comment-item"> <li v-for="comment in postDetailStore.comments" :key="comment.id" class="comment-item">
<img :src="comment.userAvatar || require('@/assets/default-avatar/boy_1.png')" alt="评论者头像" class="comment-avatar" /> <img
:src="comment.userAvatar || require('@/assets/default-avatar/boy_1.png')"
alt="评论者头像"
class="comment-avatar"
@click="goUserHome(comment.userId)"
style="cursor: pointer;"
/>
<div class="comment-content"> <div class="comment-content">
<p class="comment-name">{{ comment.userName || '匿名用户' }}</p> <p class="comment-name">{{ comment.userName || '匿名用户' }}</p>
<p class="comment-text">{{ comment.content || '' }}</p> <p class="comment-text">{{ comment.content || '' }}</p>
@ -41,15 +53,26 @@
<span class="comment-likes"> {{ comment.likeCount ?? 0 }}</span> <span class="comment-likes"> {{ comment.likeCount ?? 0 }}</span>
<span v-if="comment.replyCount > 0"> <span v-if="comment.replyCount > 0">
<button @click="loadReplies(comment)"> <button @click="loadReplies(comment)">
{{ comment.replies && comment.replies.length > 0 ? '收起回复' : '展开回复' }} ({{ comment.replyCount }}) {{ comment.showReplies> 0 ? '收起回复' : '展开回复' }} ({{ comment.replyCount }})
</button> </button>
</span> </span>
<button class="reply-btn" @click="startReply(comment)"></button> <button class="reply-btn" @click="startReply(comment)"></button>
<button
class="delete-btn"
v-if="String(comment.userId) === String(userStore.userInfo?.userid)"
@click="handleDelete(comment)"
>删除</button>
</div> </div>
<!-- 子评论列表 --> <!-- 子评论列表 -->
<ul v-if="comment.replies && comment.replies.length > 0" class="replies-list"> <ul v-if="comment.showReplies && comment.replies && comment.replies.length > 0" class="replies-list">
<li v-for="reply in comment.replies" :key="reply.id" class="comment-item reply-item"> <li v-for="reply in comment.replies" :key="reply.id" class="comment-item reply-item">
<img :src="reply.userAvatar || require('@/assets/default-avatar/boy_1.png')" alt="评论者头像" class="comment-avatar" /> <img
:src="reply.userAvatar || require('@/assets/default-avatar/boy_1.png')"
alt="评论者头像"
class="comment-avatar"
@click="goUserHome(reply.userId)"
style="cursor: pointer;"
/>
<div class="comment-content"> <div class="comment-content">
<p class="comment-name"> <p class="comment-name">
{{ reply.userName || '匿名用户' }} {{ reply.userName || '匿名用户' }}
@ -60,6 +83,11 @@
<span class="comment-time">{{ reply.createTime ? formatTime(reply.createTime) : '' }}</span> <span class="comment-time">{{ reply.createTime ? formatTime(reply.createTime) : '' }}</span>
<span class="comment-likes"> {{ reply.likeCount ?? 0 }}</span> <span class="comment-likes"> {{ reply.likeCount ?? 0 }}</span>
<button class="reply-btn" @click="startReply(reply)"></button> <button class="reply-btn" @click="startReply(reply)"></button>
<button
class="delete-btn"
v-if="String(reply.userId) === String(userStore.userInfo?.userid)"
@click="handleDelete(reply, comment.id)"
>删除</button>
</div> </div>
</div> </div>
</li> </li>
@ -89,10 +117,12 @@
<script setup lang="js" name="PostDetail"> <script setup lang="js" name="PostDetail">
import { ref, computed, onMounted , onUnmounted, watch } from 'vue'; import { ref, computed, onMounted , onUnmounted, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute,useRouter } from 'vue-router';
import { ElMessageBox } from 'element-plus';
import { usePostDetailStore } from '@/stores/postdetail.js'; import { usePostDetailStore } from '@/stores/postdetail.js';
import { useUserStore } from '@/stores/user.js'; import { useUserStore } from '@/stores/user.js';
const router = useRouter();
const route = useRoute(); const route = useRoute();
const postDetailStore = usePostDetailStore(); const postDetailStore = usePostDetailStore();
const userStore = useUserStore(); const userStore = useUserStore();
@ -106,7 +136,9 @@ const isFollowing = ref(false);
const toggleFollow = () => { const toggleFollow = () => {
isFollowing.value = !isFollowing.value; isFollowing.value = !isFollowing.value;
}; };
const goUserHome = (userId) => {
if (userId) router.push({name: 'UserPage',params: { userId: userId }})
}
function startReply(comment) { function startReply(comment) {
replyingComment.value = comment; replyingComment.value = comment;
} }
@ -132,7 +164,14 @@ function handleScroll() {
} }
} }
function loadReplies(comment) { function loadReplies(comment) {
if (!comment.repliesLoading && !comment.repliesFinished) { //
if (comment.showReplies) {
comment.showReplies = false;
return;
}
//
comment.showReplies = true;
if (!comment.repliesLoading && !comment.repliesFinished && comment.replies.length === 0) {
postDetailStore.fetchReplies(comment.id, comment); postDetailStore.fetchReplies(comment.id, comment);
} }
} }
@ -157,11 +196,24 @@ const sendComment = () => {
topId: replyingComment.value ? replyingComment.value.topId : null, topId: replyingComment.value ? replyingComment.value.topId : null,
isLike: false, isLike: false,
} }
postDetailStore.addComment(newCommentData); postDetailStore.sendComment(newCommentData);
newComment.value = ''; newComment.value = '';
} }
}; };
function handleDelete(comment, parentCommentId = null) {
ElMessageBox.confirm(
'确定要删除这条评论吗?',
'提示',
{
confirmButtonText: '删除',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
postDetailStore.deleteComment(comment.id, parentCommentId);
}).catch(() => {});
}
// postDetailStore.post // postDetailStore.post
watch( watch(
() => postDetailStore.post, () => postDetailStore.post,
@ -477,4 +529,15 @@ onUnmounted(() => {
.reply-btn:hover { .reply-btn:hover {
text-decoration: underline; text-decoration: underline;
} }
.delete-btn {
background: none;
border: none;
color: #f56c6c;
cursor: pointer;
font-size: 13px;
margin-left: 10px;
}
.delete-btn:hover {
text-decoration: underline;
}
</style> </style>

@ -103,9 +103,10 @@
</div> </div>
<p class="post-summary">{{ post.summary }}</p> <p class="post-summary">{{ post.summary }}</p>
<div class="post-stats"> <div class="post-stats">
<span><i class="icon-view"></i> {{ post.viewCount || 0 }}</span> <span><i class="icon-view"></i> 热度 {{ post.viewCount || 0 }}</span>
<span><i class="icon-like"></i> {{ post.likeCount || 0 }}</span> <span><i class="icon-comment"></i> 评论 {{ post.commentCount || 0 }}</span>
<span><i class="icon-comment"></i> {{ post.commentCount || 0 }}</span> <span><i class="icon-like"></i> {{ post.likeCount || 0 }}</span>
<span v-if="isCurrentUser" class="delete-btn" @click.stop="deletePost(post.id)"></span>
</div> </div>
</div> </div>
</div> </div>
@ -213,7 +214,7 @@ const privacySettings = ref({
const recommendations = ref([]); const recommendations = ref([]);
// //
const defaultAvatar = require('@/assets/default-avatar/boy_4.png'); const defaultAvatar = require('@/assets/default-avatar/boy_1.png');
// //
const handleAvatarError = "this.onerror=null;this.src='" + defaultAvatar + "'"; const handleAvatarError = "this.onerror=null;this.src='" + defaultAvatar + "'";
@ -306,6 +307,26 @@ const loadUserPosts = async () => {
} }
}; };
//
const deletePost = async (postId) => {
if (!postId) return;
if (!confirm('确定要删除这条帖子吗?')) return;
try {
const res = await request.delete('/post', { params: { id: postId } });
if (res && res.code === 200) {
//
userPosts.value = userPosts.value.filter(post => post.id !== postId);
ElMessage.success('删除成功');
} else {
ElMessage.error(res.message || '删除失败');
}
} catch (e) {
ElMessage.error(e.response?.message || '删除失败');
}
};
// //
const loadMorePosts = () => { const loadMorePosts = () => {
loadUserPosts(); loadUserPosts();
@ -752,4 +773,16 @@ onUnmounted(() => {
color: #999; color: #999;
margin-top: 4px; margin-top: 4px;
} }
.delete-btn {
right: 16px;
bottom: 12px;
color: #fff;
background: #f56c6c;
border: none;
border-radius: 4px;
padding: 4px 12px;
cursor: pointer;
font-size: 12px;
}
</style> </style>
Loading…
Cancel
Save