2025-07-28 12:03:59 +08:00

806 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="content">
<!-- 主帖内容 -->
<view class="post-container">
<view class="post-header">
<image class="avatar" :src="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="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 class="post-footer">
<view class="post-actions">
<view class="action-item">
<text class="action-text">{{ getTotalComments() }}</text>
</view>
</view>
</view> -->
</view>
<!-- 评论列表 -->
<view class="comments-container">
<view class="comments-title">评论 ({{ getTotalComments() }})</view>
<view class="comments-list">
<view v-for="(comment, index) in flatComments" :key="comment.id" class="comment-item" :style="{
'--level': comment.level,
'--indent': comment.level > 0 ? (comment.level * 60) + 'rpx' : '0'
}">
<!-- 层级视觉指示线条 -->
<view class="comment-level-indicator" v-if="comment.level > 0"></view>
<!-- 评论内容区 -->
<view class="comment-content-wrapper">
<image class="comment-avatar" :src="comment.pusherPortrait" mode="aspectFill"></image>
<view class="comment-main">
<view class="comment-meta">
<text class="comment-username">{{ comment.pusherName }}</text>
<text class="comment-time">{{ formatTime(comment.createdAt) }}</text>
</view>
<view class="comment-text">{{ comment.content }}</view>
<view class="comment-actions">
<text class="reply-btn" @click="handleReply(comment.id)">回复</text>
<!-- 只在一级评论显示折叠按钮 -->
<text class="fold-btn" v-if="comment.level === 0 && hasChildren(comment)"
@click="toggleFold(comment.id)">
{{ getChildCount(comment) }}条回复{{ foldedComments.includes(comment.id) ? '展开' : '折叠' }}
</text>
</view>
</view>
</view>
</view>
</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 uniIcons from '@/components/uni-icons/uni-icons.vue';
export default {
components: {
// uniIcons
},
data() {
return {
Id: '',
flatComments: [],
newComment: "",
replyingTo: null,
postData: {
histories: []
},
loading: false,
error: '',
submitting: false,
foldedComments: [], // 存储被折叠的评论ID
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: {
// 检查用户是否已登录
checkLogin() {
const token = uni.getStorageSync('token'); // 假设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;
uni.previewImage({
current: index,
urls: this.postData.images
});
},
// 检查评论是否有子评论
hasChildren(comment) {
return comment.children && comment.children.length > 0;
},
// 获取子评论数量
getChildCount(comment) {
if (!this.hasChildren(comment)) return 0;
return comment.children.length;
},
// 切换折叠状态
toggleFold(commentId) {
const index = this.foldedComments.indexOf(commentId);
if (index === -1) {
this.foldedComments.push(commentId);
} else {
this.foldedComments.splice(index, 1);
}
this.updateFlatComments();
},
// 更新平铺评论列表(处理折叠逻辑)
updateFlatComments() {
const flattenWithFold = (comments, level = 0, parentFolded = false) => {
return comments.reduce((arr, comment) => {
// 只有一级评论才检查折叠状态
const isFolded = level === 0 && this.foldedComments.includes(comment.id);
const currentFolded = parentFolded || isFolded;
if (!parentFolded) {
arr.push({
...comment,
level,
isFolded
});
}
// 如果是一级评论且被折叠,则不显示子评论
if (comment.children && comment.children.length && !(level === 0 && isFolded)) {
arr.push(...flattenWithFold(comment.children, level + 1, currentFolded));
}
return arr;
}, []);
};
this.flatComments = flattenWithFold(this.postData.histories || []);
},
// 加载更多评论
loadMoreComments() {
this.currentPage++;
// 这里应该调用API获取更多评论示例中简化处理
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() {
const count = (comments) => {
return comments.reduce((total, comment) => {
return total + 1 + (comment.children ? count(comment.children) : 0);
}, 0);
};
return count(this.postData.histories || []);
},
// 提交评论/回复
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 findUser = (comments) => {
for (const c of comments) {
if (c.id === commentId) return c.pusherName;
if (c.children && c.children.length) {
const user = findUser(c.children);
if (user) return user;
}
}
return "";
};
return findUser(this.postData.histories);
}
}
};
</script>
<style lang="scss">
/* 使用SCSS增强样式能力 */
$primary-color: #4361ee; // 更现代的蓝色
$primary-light: #e9f0ff; // 浅蓝色背景
$text-color: #2d3748; // 深灰色文字
$light-text: #718096; // 浅灰色文字
$border-color: #e2e8f0; // 更柔和的边框色
$bg-color: #f7fafc; // 浅灰色背景
$comment-bg: #ffffff; // 白色评论背景
$shadow-sm: 0 1rpx 3rpx rgba(0, 0, 0, 0.05); // 小阴影
$shadow-md: 0 4rpx 6rpx rgba(0, 0, 0, 0.08); // 中等阴影
$radius-lg: 24rpx; // 大圆角
$radius-md: 16rpx; // 中圆角
$radius-sm: 8rpx; // 小圆角
$transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); // 平滑过渡
.content {
padding: 32rpx;
background-color: $bg-color;
/* 增加底部内边距,确保内容不被输入框遮挡 */
padding-bottom: 240rpx;
min-height: 100vh;
box-sizing: border-box;
}
/* 主帖样式优化 */
.post-container {
background-color: white;
border-radius: $radius-lg;
padding: 48rpx;
margin-bottom: 40rpx;
box-shadow: $shadow-sm;
transition: $transition;
border: 1rpx solid $border-color;
&:hover {
box-shadow: $shadow-md;
transform: translateY(-1rpx);
}
&:active {
transform: translateY(0);
}
}
.post-header {
display: flex;
align-items: center;
margin-bottom: 40rpx;
}
.avatar {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
margin-right: 32rpx;
border: 2rpx solid white;
box-shadow: $shadow-sm;
background-color: #f5f5f5;
transition: $transition;
&:hover {
transform: scale(1.05);
}
}
.post-info {
flex: 1;
display: flex;
flex-direction: column;
}
.username {
font-size: 32rpx;
font-weight: 600;
color: $text-color;
line-height: 1.5;
}
.time {
font-size: 26rpx;
color: $light-text;
line-height: 1.5;
}
.post-content {
.title {
font-size: 36rpx;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 32rpx;
display: block;
line-height: 1.6;
letter-spacing: -0.01em;
}
.content-text {
font-size: 30rpx;
color: $text-color;
line-height: 1.7;
margin-bottom: 40rpx;
display: block;
word-break: break-word;
}
}
.images {
display: flex;
flex-wrap: wrap;
gap: 24rpx;
margin-top: 32rpx;
}
.post-image {
border-radius: $radius-md;
background-color: #f5f5f5;
object-fit: cover;
transition: $transition;
box-shadow: $shadow-sm;
cursor: zoom-in;
&:hover {
transform: scale(1.02);
box-shadow: $shadow-md;
}
&:active {
transform: scale(0.98);
}
&.single-img {
width: 100%;
height: 600rpx;
}
&.double-img {
width: calc(50% - 12rpx);
height: 360rpx;
}
&.multi-img {
width: calc(33.33% - 16rpx);
height: 240rpx;
}
}
/* 评论区样式优化 */
.comments-container {
background-color: white;
border-radius: $radius-lg;
padding: 0 40rpx;
margin-bottom: 32rpx;
box-shadow: $shadow-sm;
border: 1rpx solid $border-color;
}
.comments-title {
font-size: 32rpx;
font-weight: 600;
color: $text-color;
padding: 40rpx 0;
border-bottom: 1rpx solid $border-color;
position: relative;
&::after {
content: "";
position: absolute;
left: 0;
bottom: -1rpx;
width: 80rpx;
height: 4rpx;
background-color: $primary-color;
}
}
.comments-list {
display: flex;
flex-direction: column;
}
.comment-item {
position: relative;
padding: 20rpx 0;
transition: $transition;
margin-bottom: 8rpx;
&:not(:last-child) {
border-bottom: 1rpx solid rgba($border-color, 0.5);
}
&:hover {
background-color: rgba($primary-light, 0.2);
border-radius: $radius-md;
}
}
.comment-level-indicator {
position: absolute;
top: 70rpx;
width: 4rpx;
background-color: rgba($border-color, 0.5);
left: calc(60rpx + var(--indent) - 40rpx);
height: calc(100% - 70rpx);
}
.comment-content-wrapper {
display: flex;
align-items: flex-start;
padding-left: var(--indent);
}
.comment-avatar {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
margin-right: 24rpx;
flex-shrink: 0;
background-color: #f5f5f5;
border: 1rpx solid $border-color;
transition: $transition;
&:hover {
transform: scale(1.1);
}
}
.comment-main {
flex: 1;
padding-top: 0;
}
.comment-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8rpx;
}
.comment-username {
font-size: 28rpx;
color: $text-color;
font-weight: 500;
}
.comment-time {
font-size: 24rpx;
color: $light-text;
}
.comment-text {
font-size: 28rpx;
color: $text-color;
line-height: 1.6;
margin-bottom: 12rpx;
padding: 8rpx 0;
word-break: break-word;
}
.comment-actions {
display: flex;
align-items: center;
gap: 32rpx;
padding: 4rpx 0;
}
.reply-btn,
.fold-btn {
font-size: 24rpx;
color: $primary-color;
padding: 6rpx 12rpx;
border-radius: $radius-sm;
transition: $transition;
cursor: pointer;
background-color: rgba($primary-color, 0.05);
&:hover {
background-color: rgba($primary-color, 0.1);
}
&:active {
transform: scale(0.95);
}
}
.fold-btn {
color: $light-text;
background-color: rgba($light-text, 0.05);
&:hover {
background-color: rgba($light-text, 0.1);
}
}
/* 评论输入框样式优化 */
.comment-input-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: white;
padding: 24rpx 32rpx;
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: 16rpx 24rpx;
background-color: $primary-light;
border-radius: $radius-md;
margin-bottom: 16rpx;
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;
transition: $transition;
background-color: rgba($primary-color, 0.1);
&:hover {
background-color: rgba($primary-color, 0.2);
}
}
.input-wrapper {
display: flex;
align-items: center;
gap: 16rpx;
}
.comment-input {
flex: 1;
height: 88rpx;
padding: 0 32rpx;
font-size: 28rpx;
border: 1rpx solid $border-color;
border-radius: 48rpx;
background-color: #f5f5f5;
transition: $transition;
outline: none;
&:focus {
border-color: $primary-color;
background-color: #fff;
box-shadow: 0 0 0 4rpx rgba($primary-color, 0.1);
}
&::placeholder {
color: #a0aec0;
}
}
.send-btn {
min-width: 140rpx;
height: 88rpx;
background-color: $primary-color;
color: white;
border: none;
border-radius: 48rpx;
font-size: 28rpx;
font-weight: 500;
transition: $transition;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0 32rpx;
&:hover {
background-color: darken($primary-color, 5%);
box-shadow: $shadow-sm;
}
&:active {
background-color: darken($primary-color, 10%);
transform: scale(0.98);
}
&:disabled {
background-color: #edf2f7;
color: #a0aec0;
cursor: not-allowed;
box-shadow: none;
}
}
/* 加载更多按钮 */
.load-more {
display: flex;
align-items: center;
justify-content: center;
padding: 32rpx 0;
color: $light-text;
font-size: 28rpx;
cursor: pointer;
transition: $transition;
&:hover {
color: $primary-color;
}
text {
margin-right: 16rpx;
}
}
/* 添加一些动画效果 */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20rpx); }
to { opacity: 1; transform: translateY(0); }
}
.comment-item {
animation: fadeIn 0.3s ease-out forwards;
}
/* 响应式调整 */
@media (max-width: 480px) {
.content {
padding: 24rpx;
padding-bottom: 220rpx;
}
.post-container {
padding: 32rpx;
}
.comment-input-container {
padding: 24rpx;
}
.comment-item {
padding: 16rpx 0;
margin-bottom: 8rpx;
}
.comment-avatar {
width: 56rpx;
height: 56rpx;
}
.comment-text {
font-size: 26rpx;
}
}
</style>