Merge remote-tracking branch 'origin/main'

# Conflicts:
#	珞珈岛-项目相关文件/luojia-island/service/src/main/resources/application-local.yaml
main
forely 2 months ago
commit 5a0117edc0

@ -23,11 +23,11 @@ function getRandomInt(min, max) {
onMounted(() => { onMounted(() => {
new Snow('#snow', new Snow('#snow',
{ {
num: getRandomInt(1,4), num: getRandomInt(1,2),
color: '#fff', color: '#fff',
maxR: 3, maxR: 3,
minR: 12, minR: 12,
maxSpeed: 0.4, maxSpeed: 0.3,
minSpeed: 0.1, minSpeed: 0.1,
swing: true, swing: true,
swingProbability: 0.1, swingProbability: 0.1,
@ -46,6 +46,21 @@ onMounted(() => {
text-align: center; text-align: center;
color: #2c3e50; color: #2c3e50;
margin-top: 60px; margin-top: 60px;
position: relative;
z-index: 1;
}
/* 背景图层 */
#app::before {
content: "";
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
width: 100vw;
height: 100vh;
z-index: -1;
background: url('./assets/background.jpg') no-repeat center center / cover;
opacity: 0.3; /* 不透明度,可调整 */
pointer-events: none;
} }
.snow { .snow {
position: fixed; position: fixed;

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 KiB

File diff suppressed because one or more lines are too long

@ -173,12 +173,13 @@ watch(() => userStore.userInfo, (newInfo) => {
align-items: center; align-items: center;
padding: 0 20px; /* 增加左右内边距,避免内容过于靠边 */ padding: 0 20px; /* 增加左右内边距,避免内容过于靠边 */
border-bottom: 2px solid #e0e0e0; border-bottom: 2px solid #e0e0e0;
background: white; background: rgba(255, 255, 255, 0.5);
position: fixed; position: fixed;
top: 0; top: 0;
width: 100%; width: 100%;
z-index: 1000; z-index: 1000;
box-sizing: border-box; /* 确保内边距不会影响宽度 */ box-sizing: border-box; /* 确保内边距不会影响宽度 */
} }
/* Logo 区域样式 */ /* Logo 区域样式 */

@ -206,7 +206,7 @@ async function login() {
userData.userid = userData.id; userData.userid = userData.id;
userStore.login(userData); userStore.login(userData);
window.location.reload(); //
ElMessage({ ElMessage({
message: '登录成功', message: '登录成功',
type: 'success', type: 'success',

@ -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.viewCount }}</span> <span>👁 {{ post.viewCount }}</span>
<span>评论 {{ post.comments }}</span> <span>🗨 {{ post.comments }}</span>
<span> {{ post.likes }}</span> <span> {{ post.likes }}</span>
</div> </div>
</div> </div>
</div> </div>

@ -2,7 +2,7 @@
<div class="welcome-container"> <div class="welcome-container">
<!-- 上部分欢迎文字 --> <!-- 上部分欢迎文字 -->
<div class="welcome-header"> <div class="welcome-header">
欢迎来到珞珈岛珈人 欢迎来到珞珈岛{{ userStore.userInfo?.username ? userStore.userInfo.username : '珈人' }}
</div> </div>
<!-- 中间部分月份日期星期 --> <!-- 中间部分月份日期星期 -->
@ -33,6 +33,7 @@
<script setup lang="js" name="WelcomeCalendar"> <script setup lang="js" name="WelcomeCalendar">
import { ref } from 'vue'; import { ref } from 'vue';
import { useUserStore } from '@/stores/user';
const today = new Date(); const today = new Date();
@ -40,10 +41,31 @@ const today = new Date();
const currentMonthText = ref(['一', '月']); // const currentMonthText = ref(['一', '月']); //
const currentDay = ref(today.getDate()); // const currentDay = ref(today.getDate()); //
const currentWeekday = ref(['星', '期', '日']); // const currentWeekday = ref(['星', '期', '日']); //
const userStore = useUserStore();
// //
const isCheckedIn = ref(false); const isCheckedIn = ref(false);
//
const getUserKey = () => {
return userStore.userInfo?.userid || userStore.userInfo?.username || 'guest';
};
//
const checkToday = () => {
const todayStr = new Date().toISOString().slice(0, 10);
const userKey = getUserKey();
return localStorage.getItem(`checkin-${userKey}-${todayStr}`) === '1';
};
//
const checkIn = () => {
const todayStr = new Date().toISOString().slice(0, 10);
const userKey = getUserKey();
isCheckedIn.value = true;
localStorage.setItem(`checkin-${userKey}-${todayStr}`, '1');
};
// //
const initializeDate = () => { const initializeDate = () => {
const months = [ const months = [
@ -55,11 +77,7 @@ const initializeDate = () => {
currentMonthText.value = months[today.getMonth()].split(''); currentMonthText.value = months[today.getMonth()].split('');
currentDay.value = today.getDate(); currentDay.value = today.getDate();
currentWeekday.value = weekdays[today.getDay()].split(''); currentWeekday.value = weekdays[today.getDay()].split('');
}; isCheckedIn.value = checkToday();
//
const checkIn = () => {
isCheckedIn.value = true;
}; };
// //
@ -81,6 +99,8 @@ initializeDate();
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
padding: 20px; padding: 20px;
box-sizing: border-box; box-sizing: border-box;
background: rgba(255, 255, 255, 0.7); /* 半透明白色背景 */
backdrop-filter: blur(4px); /* 可选,增加毛玻璃效果 */
} }
.welcome-header { .welcome-header {

@ -2,20 +2,6 @@ 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, // 帖子主要信息
@ -145,7 +131,7 @@ export const usePostDetailStore = defineStore("postDetail", {
userAvatar, userAvatar,
createTime createTime
} = postRes.data; } = postRes.data;
console.log("获取帖子详情返回:", postRes.data);
// 主要帖子信息 // 主要帖子信息
this.post = { this.post = {
postId: id, postId: id,
@ -244,16 +230,34 @@ export const usePostDetailStore = defineStore("postDetail", {
} }
}, },
// 删除评论或回复 // 删除评论或回复
async deleteComment(commentId, parentCommentId = null) { async deleteComment(commentObj) {
try { try {
const commentId = commentObj.id;
const topId = commentObj.topId;
const res = await request.delete('/comment', { params: { id: commentId} }); const res = await request.delete('/comment', { params: { id: commentId} });
if (res.code === 200) { if (res.code === 200) {
// 一级评论 // 一级评论
if (!parentCommentId) { if (commentObj.id === commentObj.topId) {
this.comments = this.comments.filter(c => c.id !== commentId); // 删除一级评论及其所有回复
const idx = this.comments.findIndex(c => c.id === commentId);
if (idx !== -1) {
const comment = this.comments[idx];
// 总评论数 -= replyCount
if (this.post && this.post.commentCount > 0) {
this.post.commentCount -= (comment.replyCount || 0);
if (this.post.commentCount < 0) this.post.commentCount = 0;
}
this.comments.splice(idx, 1);
}
} else { } else {
// 回复评论,递归删除 const topComment = this.comments.find(c => c.id === topId);
removeReply(this.comments,commentId); if (topComment) {
const replyIdx = topComment.replies.findIndex(r => r.id === commentId);
if (replyIdx !== -1) {
topComment.replies.splice(replyIdx, 1);
if (topComment.replyCount > 0) topComment.replyCount--;
}
}
} }
// 更新评论数 // 更新评论数
if (this.post && this.post.commentCount > 0) { if (this.post && this.post.commentCount > 0) {
@ -267,5 +271,65 @@ export const usePostDetailStore = defineStore("postDetail", {
ElMessage.error(e.response?.message || '删除失败,请稍后重试'); ElMessage.error(e.response?.message || '删除失败,请稍后重试');
} }
}, },
//帖子点赞和取消
async PostLike() {
if (!this.post || !this.post.postId) return;
try {
const url = `/post/like/${this.post.postId}`;
if (!this.isLike) {
// 点赞
const res = await request.put(url);
if (res.code === 200) {
this.isLike = true;
this.post.likeCount = (this.post.likeCount || 0) + 1;
ElMessage.success('点赞成功');
} else {
ElMessage.error(res.message || '点赞失败');
}
} else {
// 取消点赞
const res = await request.put(url);
if (res.code === 200) {
this.isLike = false;
this.post.likeCount = Math.max((this.post.likeCount || 1) - 1, 0);
ElMessage.success('已取消点赞');
} else {
ElMessage.error(res.message || '取消点赞失败');
}
}
} catch (e) {
ElMessage.error(e.response?.message || '操作失败,请稍后重试');
}
},
// 评论点赞和取消
async CommentLike(commentObj) {
if (!commentObj || !commentObj.id) return;
try {
const url = `/comment/like/${commentObj.id}`;
if (!commentObj.isLike) {
// 点赞
const res = await request.put(url);
if (res.code === 200) {
commentObj.isLike = true;
commentObj.likeCount = (commentObj.likeCount || 0) + 1;
ElMessage.success('点赞成功');
} else {
ElMessage.error(res.message || '点赞失败');
}
} else {
// 取消点赞
const res = await request.put(url);
if (res.code === 200) {
commentObj.isLike = false;
commentObj.likeCount = Math.max((commentObj.likeCount || 1) - 1, 0);
ElMessage.success('已取消点赞');
} else {
ElMessage.error(res.message || '取消点赞失败');
}
}
} catch (e) {
ElMessage.error(e.response?.message || '操作失败,请稍后重试');
}
}
}, },
}); });

@ -65,6 +65,7 @@ export const usePostListStore = defineStore('postList', {
this.offset = newOffset; this.offset = newOffset;
this.pageSize = newSize; this.pageSize = newSize;
} }
console.log('获取帖子列表成功', this.posts);
if (!records || records.length < this.pageSize) { if (!records || records.length < this.pageSize) {
this.finished = true; // 没有更多数据 this.finished = true; // 没有更多数据
} }

@ -150,7 +150,7 @@ const userInfo = computed(() => userStore.userInfo);
const router = useRouter(); const router = useRouter();
// //
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 + "'";

@ -2,12 +2,12 @@
<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 <img
:src="author.userAvatar || require('@/assets/default-avatar/boy_1.png')" :src="author.userAvatar || require('@/assets/default-avatar/boy_1.png')"
alt="头像" alt="头像"
class="author-avatar" class="author-avatar"
@click="goUserHome(author.userId)" :style="{ cursor: postDetailStore.post?.status === 1 ? 'not-allowed' : 'pointer' }"
style="cursor: pointer;" @click="handleAuthorAvatarClick"
/> />
<div class="author-details"> <div class="author-details">
<h3 class="author-name">{{ author.userName || '匿名用户' }}</h3> <h3 class="author-name">{{ author.userName || '匿名用户' }}</h3>
@ -24,9 +24,19 @@
<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?.viewCount ?? 0 }}</span> <span> 👁 {{ postDetailStore.post?.viewCount ?? 0 }}</span>
<span> 点赞 {{ postDetailStore.post?.likeCount ?? 0 }}</span> <span> 🗨 {{ postDetailStore.post?.commentCount ?? 0 }}</span>
<span> 评论 {{ postDetailStore.post?.commentCount ?? 0 }}</span> <span>
<button
@click="postDetailStore.PostLike"
class="like-btn"
:class="{ liked: postDetailStore.isLike }"
>
<span v-if="!postDetailStore.isLike"></span>
<span v-else></span>
{{ postDetailStore.post?.likeCount ?? 0 }}
</button>
</span>
</div> </div>
<div class="post-time"> <div class="post-time">
发布时间{{ postDetailStore.post?.createTime ? formatTime(postDetailStore.post.createTime) : '' }} 发布时间{{ postDetailStore.post?.createTime ? formatTime(postDetailStore.post.createTime) : '' }}
@ -50,7 +60,14 @@
<p class="comment-text">{{ comment.content || '' }}</p> <p class="comment-text">{{ comment.content || '' }}</p>
<div class="comment-meta"> <div class="comment-meta">
<span class="comment-time">{{ comment.createTime ? formatTime(comment.createTime) : '' }}</span> <span class="comment-time">{{ comment.createTime ? formatTime(comment.createTime) : '' }}</span>
<span class="comment-likes"> {{ comment.likeCount ?? 0 }}</span> <button
@click="postDetailStore.CommentLike(comment)"
:class="['like-btn', { liked: comment.isLike }]"
>
<span v-if="!comment.isLike"></span>
<span v-else></span>
{{ comment.likeCount ?? 0 }}
</button>
<span v-if="comment.replyCount > 0"> <span v-if="comment.replyCount > 0">
<button @click="loadReplies(comment)"> <button @click="loadReplies(comment)">
{{ comment.showReplies> 0 ? '收起回复' : '展开回复' }} ({{ comment.replyCount }}) {{ comment.showReplies> 0 ? '收起回复' : '展开回复' }} ({{ comment.replyCount }})
@ -81,12 +98,19 @@
<p class="comment-text">{{ reply.content || '' }}</p> <p class="comment-text">{{ reply.content || '' }}</p>
<div class="comment-meta"> <div class="comment-meta">
<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> <button
@click="postDetailStore.CommentLike(reply)"
:class="['like-btn', { liked: reply.isLike }]"
>
<span v-if="!reply.isLike"></span>
<span v-else></span>
{{ reply.likeCount ?? 0 }}
</button>
<button class="reply-btn" @click="startReply(reply)"></button> <button class="reply-btn" @click="startReply(reply)"></button>
<button <button
class="delete-btn" class="delete-btn"
v-if="String(reply.userId) === String(userStore.userInfo?.userid)" v-if="String(reply.userId) === String(userStore.userInfo?.userid)"
@click="handleDelete(reply, comment.id)" @click="handleDelete(reply)"
>删除</button> >删除</button>
</div> </div>
</div> </div>
@ -118,7 +142,7 @@
<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,useRouter } from 'vue-router'; import { useRoute,useRouter } from 'vue-router';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox, ElMessage } 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';
@ -201,7 +225,7 @@ const sendComment = () => {
} }
}; };
function handleDelete(comment, parentCommentId = null) { function handleDelete(comment) {
ElMessageBox.confirm( ElMessageBox.confirm(
'确定要删除这条评论吗?', '确定要删除这条评论吗?',
'提示', '提示',
@ -211,9 +235,22 @@ function handleDelete(comment, parentCommentId = null) {
type: 'warning', type: 'warning',
} }
).then(() => { ).then(() => {
postDetailStore.deleteComment(comment.id, parentCommentId); postDetailStore.deleteComment(comment);
}).catch(() => {}); }).catch(() => {});
} }
function handleAuthorAvatarClick() {
//
if (
(author.value.userName === '匿名用户' || postDetailStore.post?.status === 1) &&
(!author.value.userAvatar || author.value.userAvatar === '')
) {
ElMessage.info('该用户为匿名用户');
return;
}
goUserHome(author.value.userId);
}
// postDetailStore.post // postDetailStore.post
watch( watch(
() => postDetailStore.post, () => postDetailStore.post,
@ -540,4 +577,15 @@ onUnmounted(() => {
.delete-btn:hover { .delete-btn:hover {
text-decoration: underline; text-decoration: underline;
} }
.like-btn {
margin-left: 8px;
background: none;
border: none;
color: #409eff;
cursor: pointer;
font-size: 14px;
}
.like-btn.liked {
color: #f56c6c;
}
</style> </style>

@ -34,6 +34,14 @@
</div> </div>
</div> </div>
<!-- 匿名发布选项放在内容编辑器下方或合适位置 -->
<div class="form-row">
<label>
<input type="checkbox" v-model="form.status" true-value="1" false-value="0" />
匿名发布
</label>
</div>
<!-- 内容编辑器 --> <!-- 内容编辑器 -->
<div class="form-row"> <div class="form-row">
<label>内容</label> <label>内容</label>

Loading…
Cancel
Save