624 lines
14 KiB
Vue
624 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="getAvatarSrc(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;
|
|
}
|
|
},
|
|
computed: {
|
|
|
|
},
|
|
methods: {
|
|
getAvatarSrc(portrait) {
|
|
const defaultAvatar = '/static/imgs/index/nav.png';
|
|
|
|
if (!portrait) {
|
|
return defaultAvatar;
|
|
}
|
|
|
|
const defaultUrls = ['/static/imgs/index/nav.png', 'https://jinshan.nantong.info'];
|
|
|
|
if (defaultUrls.includes(portrait) || portrait.startsWith('http')) {
|
|
return portrait;
|
|
}
|
|
|
|
return `${this.IMAGE_BASE_URL}${portrait}`;
|
|
},
|
|
|
|
// 扁平化评论结构(核心修改)
|
|
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> |