qiuyuan 42b6d2db33 1
2025-08-07 12:22:19 +08:00

604 lines
14 KiB
Vue

<template>
<view class="content">
<!-- 主帖内容 -->
<view class="post-container">
<view class="post-header">
<image class="avatar" :src="`${IMAGE_BASE_URL}`+postData.customerPortrait" mode="aspectFill"></image>
<view class="post-info">
<text class="username">{{ postData.customerName }}</text>
<text class="time">{{ formatTime(postData.pushAt) }}</text>
</view>
</view>
<view class="post-content">
<text class="title">{{ postData.title }}</text>
<text class="content-text">{{ postData.content }}</text>
<view class="images" v-if="postData.images && postData.images.length">
<image v-for="(img, index) in postData.images" :key="index" :src="`${IMAGE_BASE_URL}`+img"
class="post-image" :class="{
'single-img': postData.images.length === 1,
'double-img': postData.images.length === 2,
'multi-img': postData.images.length >= 3
}" mode="aspectFill" @click="previewImage(index)"></image>
</view>
</view>
</view>
<!-- 评论列表 -->
<view class="comments-container">
<view class="section-header">
<text class="section-title">评论 ({{ getTotalComments() }})</text>
</view>
<view class="comments-list">
<view v-for="comment in flatComments" :key="comment.id" class="comment-item"
:class="{'is-reply': comment.toUserName}">
<view class="comment-header">
<image class="avatar"
:src="comment.pusherPortrait === '/static/imgs/index/nav.png' ? comment.pusherPortrait : `${IMAGE_BASE_URL}${comment.pusherPortrait}`" mode="aspectFill"></image>
<view class="comment-user-info">
<text class="username">{{ comment.pusherName }}</text>
<view class="meta-info">
<text v-if="comment.toUserName" class="reply-to">
回复 {{ comment.toUserName }}
</text>
<text class="time">{{ formatTime(comment.createdAt) }}</text>
</view>
</view>
</view>
<view class="comment-content">{{ comment.content }}</view>
<view class="comment-actions">
<!-- <view class="action-item" @click="toggleLike(comment.id)">
<text class="action-icon">{{ comment.isLiked ? '👍' : '👎' }}</text>
<text class="action-count">{{ comment.likeCount || 0 }}</text>
</view> -->
<view class="action-item" @click="handleReply(comment.id)">
<text class="action-text">回复</text>
</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<view class="load-more" v-if="showLoadMore && !loading" @click="loadMoreComments">
<text>加载更多</text>
</view>
</view>
<!-- 新评论输入框 -->
<view class="comment-input-container">
<view class="replying-bar" v-if="replyingTo">
<text class="replying-text">回复 @{{ getReplyUsername(replyingTo) }}</text>
<text class="cancel-reply" @click="cancelReply">取消</text>
</view>
<view class="input-wrapper">
<input class="comment-input" v-model="newComment" :placeholder="replyingTo ? '输入回复内容...' : '写下你的评论...'"
@confirm="submitComment" />
<button class="send-btn" @click="submitComment" :disabled="!newComment.trim()">
发送
</button>
</view>
</view>
</view>
</template>
<script>
import { get, post } from '@/utils/request';
import { formatTime } from '@/utils/timeFormat';
import { IMAGE_BASE_URL,BASE_URL } from '@/utils/config';
export default {
data() {
return {
IMAGE_BASE_URL,
Id: '',
flatComments: [],
newComment: "",
replyingTo: null,
postData: {
histories: []
},
loading: false,
error: '',
submitting: false,
foldedComments: [],
showLoadMore: false,
currentPage: 1,
pageSize: 10,
};
},
onLoad(options) {
if (options && options.Id) {
this.Id = options.Id;
this.getDetail();
} else {
this.error = '缺少内容ID';
this.loading = false;
}
},
methods: {
// 扁平化评论结构(核心修改)
updateFlatComments() {
const flattenComments = (comments, parentUser = '') => {
return comments.reduce((arr, comment) => {
// 添加回复关系和点赞状态
const newComment = {
...comment,
toUserName: parentUser,
isLiked: false,
likeCount: comment.likeCount || 0
};
arr.push(newComment);
if (comment.children && comment.children.length) {
arr.push(...flattenComments(comment.children, comment.pusherName));
}
return arr;
}, []);
};
this.flatComments = flattenComments(this.postData.histories || []);
},
// 检查用户是否已登录
checkLogin() {
const token = uni.getStorageSync('token');
return !!token;
},
// 跳转到登录页面
goToLogin() {
uni.showModal({
title: '提示',
content: '请先登录再进行评论',
confirmText: '前往登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/mine/index'
});
}
}
});
},
// 图片预览
previewImage(index) {
if (!this.postData.images || !this.postData.images.length) return;
const fullImageUrls = this.postData.images.map(imagePath => {
return IMAGE_BASE_URL + imagePath;
});
uni.previewImage({
current: index,
urls: fullImageUrls // 传递完整的 URL 数组
});
},
// 切换点赞状态
toggleLike(commentId) {
const comment = this.flatComments.find(item => item.id === commentId);
if (comment) {
comment.isLiked = !comment.isLiked;
comment.likeCount += comment.isLiked ? 1 : -1;
}
},
// 加载更多评论
loadMoreComments() {
this.currentPage++;
this.getDetail();
},
// 获取帖子详情
async getDetail() {
try {
this.loading = true;
const res = await get(`/api/v1/apps/home/reciprocities/${this.Id}`, {
page: this.currentPage,
pageSize: this.pageSize
});
if (!res?.success) throw new Error(res?.message || '获取详情失败');
if (this.currentPage === 1) {
this.postData = res.data;
} else {
// 合并评论数据
this.postData.histories = [...this.postData.histories, ...res.data.histories];
}
this.updateFlatComments();
this.showLoadMore = (res.data.histories.length >= this.pageSize);
} catch (err) {
console.error('获取详情失败:', err);
this.error = '加载失败,请重试';
} finally {
this.loading = false;
}
},
formatTime(isoTime) {
if (!isoTime) return '';
return formatTime(isoTime);
},
// 计算总评论数
getTotalComments() {
return this.flatComments.length;
},
// 提交评论/回复
async submitComment() {
if (!this.checkLogin()) {
this.goToLogin();
this.newComment = "";
return;
}
const content = this.newComment.trim();
if (!content || this.submitting) return;
try {
this.submitting = true;
const requestData = {
content: content,
reciprocityId: this.postData.id,
rootHistoryId: this.replyingTo || ""
};
const response = await post(
'/api/v1/app_auth/reciprocities/send',
requestData
);
if (response.success) {
this.newComment = '';
this.replyingTo = null;
uni.showToast({
title: '评论成功',
icon: 'success',
duration: 1500
});
setTimeout(() => {
this.currentPage = 1;
this.getDetail();
}, 500);
} else {
throw new Error('评论提交失败,请重试');
}
} catch (err) {
console.error('评论失败:', err);
uni.showToast({
title: err.message || '评论失败,请重试',
icon: 'none',
duration: 2000
});
} finally {
this.submitting = false;
}
},
// 处理回复按钮点击
handleReply(commentId) {
if (!this.checkLogin()) {
this.goToLogin();
return;
}
this.replyingTo = commentId;
this.$nextTick(() => {
uni.createSelectorQuery().select('.comment-input')
.fields({
focus: true
}, () => {}).exec();
});
},
// 取消回复
cancelReply() {
this.replyingTo = null;
this.newComment = '';
},
// 获取被回复人的用户名
getReplyUsername(commentId) {
const comment = this.flatComments.find(item => item.id === commentId);
return comment ? comment.pusherName : '';
}
}
};
</script>
<style lang="scss">
/* 基础变量定义 */
$primary-color: #3881FF;
$text-color: #333;
$light-text: #999;
$border-color: #eee;
$bg-color: #f8f8f8;
$radius-md: 16rpx;
$radius-sm: 8rpx;
$spacing-lg: 32rpx;
$spacing-md: 24rpx;
$spacing-sm: 16rpx;
.content {
padding: $spacing-lg;
background-color: white;
padding-bottom: 240rpx;
min-height: 100vh;
box-sizing: border-box;
}
/* 主帖样式 */
.post-container {
background-color: white;
padding: 0 0 $spacing-lg 0;
margin-bottom: $spacing-lg;
border-bottom: 1rpx solid $border-color;
.post-header {
display: flex;
align-items: center;
margin-bottom: $spacing-md;
.avatar {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
margin-right: $spacing-md;
}
.post-info {
flex: 1;
.username {
font-size: 32rpx;
font-weight: 600;
color: $text-color;
display: block;
margin-bottom: 8rpx;
}
.time {
font-size: 24rpx;
color: $light-text;
}
}
}
.post-content {
.title {
font-size: 36rpx;
font-weight: 600;
color: $text-color;
margin-bottom: $spacing-md;
display: block;
line-height: 1.4;
}
.content-text {
font-size: 30rpx;
color: $text-color;
line-height: 1.6;
margin-bottom: $spacing-lg;
}
.images {
display: flex;
flex-wrap: wrap;
gap: $spacing-sm;
.post-image {
border-radius: $radius-md;
background-color: $bg-color;
&.single-img {
width: 100%;
height: 600rpx;
}
&.double-img {
width: calc(50% - 8rpx);
height: 360rpx;
}
&.multi-img {
width: calc(33.33% - 10.67rpx);
height: 240rpx;
}
}
}
}
}
/* 评论区样式(核心修改) */
.comments-container {
background-color: white;
padding: 0;
.section-header {
padding: $spacing-md 0;
border-bottom: 1rpx solid $border-color;
margin-bottom: $spacing-md;
.section-title {
font-size: 32rpx;
font-weight: 600;
color: $text-color;
}
}
.comments-list {
.comment-item {
padding: $spacing-md 0;
border-bottom: 1rpx solid $border-color;
&.is-reply {
margin-left: 80rpx;
/* 回复评论缩进 */
padding-left: $spacing-md;
background-color: rgba(248, 248, 248, 0.5);
border-radius: $radius-md;
margin-bottom: $spacing-sm;
}
.comment-header {
display: flex;
align-items: center;
margin-bottom: $spacing-sm;
.avatar {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
margin-right: $spacing-sm;
}
.comment-user-info {
flex: 1;
.username {
font-size: 28rpx;
font-weight: 600;
color: $text-color;
display: block;
margin-bottom: 4rpx;
}
.meta-info {
display: flex;
align-items: center;
.reply-to {
font-size: 24rpx;
color: $primary-color;
margin-right: $spacing-sm;
}
.time {
font-size: 24rpx;
color: $light-text;
}
}
}
}
.comment-content {
font-size: 28rpx;
color: $text-color;
line-height: 1.5;
margin-bottom: $spacing-sm;
padding-left: 88rpx;
/* 与头像对齐 */
}
.comment-actions {
display: flex;
gap: $spacing-lg;
padding-left: 88rpx;
/* 与头像对齐 */
.action-item {
display: flex;
align-items: center;
color: $light-text;
font-size: 24rpx;
.action-icon {
margin-right: 8rpx;
}
&:active {
opacity: 0.7;
}
}
}
}
}
/* 加载更多 */
.load-more {
display: flex;
align-items: center;
justify-content: center;
padding: $spacing-lg 0;
color: $light-text;
font-size: 28rpx;
cursor: pointer;
&:active {
color: $primary-color;
}
}
}
/* 底部输入区 */
.comment-input-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: white;
padding: $spacing-md;
border-top: 1rpx solid $border-color;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.08);
z-index: 100;
.replying-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: $spacing-sm $spacing-md;
background-color: $bg-color;
border-radius: $radius-md;
margin-bottom: $spacing-sm;
font-size: 26rpx;
.replying-text {
color: $primary-color;
font-weight: 500;
}
.cancel-reply {
color: $primary-color;
padding: 8rpx 16rpx;
border-radius: $radius-sm;
cursor: pointer;
&:active {
opacity: 0.7;
}
}
}
.input-wrapper {
display: flex;
align-items: center;
gap: $spacing-md;
.comment-input {
flex: 1;
height: 80rpx;
background-color: $bg-color;
border-radius: 40rpx;
padding: 0 $spacing-md;
font-size: 28rpx;
}
.send-btn {
background-color: $primary-color;
color: white;
font-size: 28rpx;
padding: 16rpx 32rpx;
border-radius: 40rpx;
min-width: 120rpx;
text-align: center;
&:disabled {
opacity: 0.5;
}
&:active {
opacity: 0.8;
}
}
}
}
</style>