2025-07-21 18:58:03 +08:00

746 lines
17 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)">
{{ foldedComments.includes(comment.id) ? '展开' : '折叠' }}
{{ getChildCount(comment) }}条回复
</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: {
// 图片预览
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() {
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 || 0
};
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) {
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 1px 3px rgba(0, 0, 0, 0.05); // 小阴影
$shadow-md: 0 4px 6px rgba(0, 0, 0, 0.08); // 中等阴影
$radius-lg: 12px; // 大圆角
$radius-md: 8px; // 中圆角
$radius-sm: 4px; // 小圆角
$transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); // 平滑过渡
.content {
padding: 16px;
background-color: $bg-color;
min-height: 100vh;
padding-bottom: 120px;
}
/* 主帖样式优化 */
.post-container {
background-color: white;
border-radius: $radius-lg;
padding: 24px;
margin-bottom: 20px;
box-shadow: $shadow-sm;
transition: $transition;
border: 1px solid $border-color;
&:hover {
box-shadow: $shadow-md;
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
}
.post-header {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
margin-right: 16px;
border: 2px 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: 16px;
font-weight: 600;
color: $text-color;
line-height: 1.5;
}
.time {
font-size: 13px;
color: $light-text;
line-height: 1.5;
}
.post-content {
.title {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 16px;
display: block;
line-height: 1.6;
letter-spacing: -0.01em;
}
.content-text {
font-size: 15px;
color: $text-color;
line-height: 1.7;
margin-bottom: 20px;
display: block;
word-break: break-word;
}
}
.images {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 16px;
}
.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: 300px;
}
&.double-img {
width: calc(50% - 6px);
height: 180px;
}
&.multi-img {
width: calc(33.33% - 8px);
height: 120px;
}
}
/* 评论区样式优化 */
.comments-container {
background-color: white;
border-radius: $radius-lg;
padding: 0 20px;
margin-bottom: 16px;
box-shadow: $shadow-sm;
border: 1px solid $border-color;
}
.comments-title {
font-size: 16px;
font-weight: 600;
color: $text-color;
padding: 20px 0;
border-bottom: 1px solid $border-color;
position: relative;
&::after {
content: "";
position: absolute;
left: 0;
bottom: -1px;
width: 40px;
height: 2px;
background-color: $primary-color;
}
}
.comments-list {
display: flex;
flex-direction: column;
}
.comment-item {
position: relative;
padding: 20px 0;
border-bottom: 1px solid $border-color;
transition: $transition;
&:hover {
background-color: rgba($primary-light, 0.3);
}
&:last-child {
border-bottom: none;
}
}
.comment-level-indicator {
position: absolute;
top: 50px;
width: 2px;
background-color: #e5e5e5;
left: calc(30px + var(--indent) - 20px);
height: calc(100% - 50px);
}
.comment-content-wrapper {
display: flex;
align-items: flex-start;
padding-left: var(--indent);
}
.comment-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
margin-right: 12px;
flex-shrink: 0;
background-color: #f5f5f5;
border: 1px solid $border-color;
transition: $transition;
&:hover {
transform: scale(1.1);
}
}
.comment-main {
flex: 1;
}
.comment-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.comment-username {
font-size: 14px;
color: $text-color;
font-weight: 500;
}
.comment-time {
font-size: 12px;
color: $light-text;
}
.comment-text {
font-size: 14px;
color: $text-color;
line-height: 1.6;
margin-bottom: 12px;
padding: 4px 0;
word-break: break-word;
}
.comment-actions {
display: flex;
align-items: center;
gap: 16px;
padding: 4px 0;
}
.reply-btn,
.fold-btn {
font-size: 13px;
color: $primary-color;
padding: 4px 8px;
border-radius: $radius-sm;
transition: $transition;
cursor: pointer;
&:hover {
background-color: rgba($primary-color, 0.1);
}
&:active {
transform: scale(0.95);
}
}
.fold-btn {
color: $light-text;
}
/* 评论输入框样式优化 */
.comment-input-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: white;
padding: 16px;
border-top: 1px solid $border-color;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.08);
z-index: 100;
}
.replying-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background-color: $primary-light;
border-radius: $radius-md;
margin-bottom: 12px;
font-size: 13px;
}
.replying-text {
color: $primary-color;
font-weight: 500;
}
.cancel-reply {
color: $primary-color;
padding: 4px 8px;
border-radius: $radius-sm;
cursor: pointer;
transition: $transition;
&:hover {
background-color: rgba($primary-color, 0.1);
}
}
.input-wrapper {
display: flex;
align-items: center;
gap: 12px;
}
.comment-input {
flex: 1;
height: 48px;
padding: 0 16px;
font-size: 14px;
border: 1px solid $border-color;
border-radius: 24px;
background-color: #f5f5f5;
transition: $transition;
outline: none;
&:focus {
border-color: $primary-color;
background-color: #fff;
box-shadow: 0 0 0 2px rgba($primary-color, 0.1);
}
&::placeholder {
color: #a0aec0;
}
}
.send-btn {
min-width: 80px;
height: 48px;
background-color: $primary-color;
color: white;
border: none;
border-radius: 24px;
font-size: 14px;
font-weight: 500;
transition: $transition;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0 16px;
&: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: 16px 0;
color: $light-text;
font-size: 14px;
cursor: pointer;
transition: $transition;
&:hover {
color: $primary-color;
}
text {
margin-right: 8px;
}
}
/* 添加一些动画效果 */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.comment-item {
animation: fadeIn 0.3s ease-out forwards;
}
/* 响应式调整 */
@media (max-width: 480px) {
.content {
padding: 12px;
}
.post-container {
padding: 16px;
}
.comment-input-container {
padding: 12px;
}
}
</style>