|
|
- <template>
- <view class="comment-detail-container">
- <navbar :title="pageTitle" leftClick @leftClick="navigateBack"/>
-
- <!-- 加载状态 -->
- <view class="loading-container" v-if="loading">
- <view class="loading-spinner"></view>
- <text class="loading-text">加载中...</text>
- </view>
-
- <!-- 错误状态 -->
- <view class="error-container" v-if="!loading && hasError">
- <text class="error-text">加载失败,请重试</text>
- <button class="retry-button" @click="loadCommentDetail">重新加载</button>
- </view>
-
- <!-- 主评论内容 -->
- <view class="main-comment" v-if="!loading && !hasError && mainComment">
- <view class="comment-header">
- <image class="avatar" :src="mainComment.userHead || '/static/image/center/default-avatar.png'" mode="aspectFill"></image>
- <view class="user-info">
- <text class="username">{{mainComment.userName}}</text>
- <text class="time">{{formatTime(mainComment.createTime)}}</text>
- </view>
- </view>
- <view class="comment-content">
- <text>{{mainComment.userValue}}</text>
- </view>
- <!-- 评论图片 -->
- <view class="comment-images" v-if="mainComment.userImage">
- <view class="image-grid">
- <image
- v-for="(img, index) in mainComment.userImage.split(',')"
- :key="index"
- :src="img"
- mode="aspectFill"
- @click="previewImage(mainComment.userImage.split(','), index)"
- class="comment-image"
- ></image>
- </view>
- </view>
- <view class="comment-footer">
- <text class="reply-count">{{totalReplies}}条回复</text>
- <view class="reply-btn" @click="showReplyInput(mainComment.id, mainComment.userName)">
- <uv-icon name="chat" color="#666" size="32rpx"></uv-icon>
- <text>回复</text>
- </view>
- </view>
- </view>
-
- <!-- 回复列表 -->
- <view class="replies-container" v-if="!loading && !hasError && commentList.length > 0">
- <view class="reply-item" v-for="(item, index) in commentList" :key="index">
- <view class="reply-header">
- <image class="avatar" :src="item.userHead || '/static/image/center/default-avatar.png'" mode="aspectFill"></image>
- <view class="user-info">
- <text class="username">{{item.userName}}</text>
- <text class="time">{{formatTime(item.createTime)}}</text>
- </view>
- </view>
- <view class="reply-content">
- <view v-if="item.replyToUserName" class="reply-to">
- <text>回复 </text>
- <text class="reply-username">@{{item.replyToUserName}}</text>
- <text>:</text>
- </view>
- <text>{{item.userValue}}</text>
- </view>
- <!-- 回复图片 -->
- <view class="reply-images" v-if="item.userImage">
- <view class="image-grid">
- <image
- v-for="(img, imgIndex) in item.userImage.split(',')"
- :key="imgIndex"
- :src="img"
- mode="aspectFill"
- @click="previewImage(item.userImage.split(','), imgIndex)"
- class="reply-image"
- ></image>
- </view>
- </view>
- <view class="reply-footer">
- <view class="reply-info">
- <text v-if="item.replyNum > 0" class="sub-reply-count" @click="navigateToSubComment(item)">
- {{item.replyNum}}条回复 >
- </text>
- </view>
- <view class="reply-btn" @click="showReplyInput(item.id, item.userName)">
- <uv-icon name="chat" color="#666" size="28rpx"></uv-icon>
- <text>回复</text>
- </view>
- </view>
- </view>
- </view>
-
- <!-- 加载更多 -->
- <view class="load-more" v-if="!loading && !hasError && hasMore">
- <text @click="loadMoreReplies">加载更多</text>
- </view>
-
- <!-- 回复输入框 -->
- <view class="reply-input-container" v-if="showInput">
- <view class="input-wrapper">
- <input
- class="reply-input"
- v-model="replyContent"
- :placeholder="replyPlaceholder"
- focus
- confirm-type="send"
- @confirm="submitReply"
- />
- <button class="send-btn" :disabled="!replyContent.trim()" @click="submitReply">发送</button>
- </view>
- </view>
- </view>
- </template>
-
- <script>
- export default {
- data() {
- return {
- commentId: '', // 当前评论ID
- parentId: '', // 父评论ID,用于无限层级
- sourceType: '', // 评论来源类型(文章、帖子等)
- sourceId: '', // 评论来源ID
- pageTitle: '评论详情',
- loading: true,
- hasError: false,
- mainComment: null, // 主评论
- commentList: [], // 回复列表
- page: 1,
- pageSize: 20,
- hasMore: false,
- totalReplies: 0,
-
- // 回复相关
- showInput: false,
- replyContent: '',
- replyToId: '', // 回复的评论ID
- replyToUserName: '', // 回复的用户名
- replyPlaceholder: '写回复...'
- }
- },
- onLoad(options) {
- // 获取参数
- if (options.id) {
- this.commentId = options.id;
- }
- if (options.parentId) {
- this.parentId = options.parentId;
- }
- if (options.sourceType) {
- this.sourceType = options.sourceType;
- }
- if (options.sourceId) {
- this.sourceId = options.sourceId;
- }
-
- // 加载评论详情
- this.loadCommentDetail();
- // 加载回复列表
- this.loadReplies();
- },
- methods: {
- // 加载评论详情
- loadCommentDetail() {
- this.loading = true;
- this.hasError = false;
-
- // 调用API获取评论详情
- this.$api('getCommentDetail', {
- id: this.commentId
- }, res => {
- this.loading = false;
-
- if (res.code === 200) {
- this.mainComment = res.result;
- // 获取回复总数
- this.totalReplies = res.result.replyNum || 0;
- } else {
- this.hasError = true;
- uni.showToast({
- title: res.message || '加载失败',
- icon: 'none'
- });
- }
- });
- },
-
- // 加载回复列表
- loadReplies() {
- // 使用现有的评论列表接口,传入pid参数
- this.$api('getCommentPage', {
- pid: this.commentId,
- page: this.page,
- pageSize: this.pageSize
- }, res => {
- if (res.code === 200) {
- if (this.page === 1) {
- this.commentList = res.result.records || [];
- } else {
- this.commentList = [...this.commentList, ...(res.result.records || [])];
- }
-
- // 判断是否有更多数据
- this.hasMore = this.commentList.length < res.result.total;
- } else {
- uni.showToast({
- title: res.message || '加载回复失败',
- icon: 'none'
- });
- }
- });
- },
-
- // 加载更多回复
- loadMoreReplies() {
- this.page++;
- this.loadReplies();
- },
-
- // 显示回复输入框
- showReplyInput(commentId, userName = '') {
- this.showInput = true;
- this.replyToId = commentId;
- this.replyToUserName = userName;
- this.replyPlaceholder = userName ? `回复 @${userName}` : '写回复...';
- this.replyContent = '';
- },
-
- // 提交回复
- submitReply() {
- if (!this.replyContent.trim()) return;
-
- // 调用API提交回复
- this.$api('addComment', {
- userValue: this.replyContent,
- pid: this.commentId,
- replyToId: this.replyToId,
- replyToUserName: this.replyToUserName,
- type: this.sourceType,
- orderId: this.sourceId
- }, res => {
- if (res.code === 200) {
- // 清空输入框
- this.replyContent = '';
- this.showInput = false;
-
- // 重新加载回复列表
- this.page = 1;
- this.loadReplies();
-
- // 更新回复总数
- this.totalReplies++;
-
- uni.showToast({
- title: '回复成功',
- icon: 'success'
- });
- } else {
- uni.showToast({
- title: res.message || '回复失败',
- icon: 'none'
- });
- }
- });
- },
-
- // 导航到子评论详情
- navigateToSubComment(comment) {
- if (comment.replyNum > 0) {
- uni.navigateTo({
- url: `/pages_order/comment/commentDetail?id=${comment.id}&parentId=${this.commentId}&sourceType=${this.sourceType}&sourceId=${this.sourceId}`
- });
- }
- },
-
- // 返回上一页
- navigateBack() {
- uni.navigateBack();
- },
-
- // 预览图片
- previewImage(images, index) {
- uni.previewImage({
- urls: images,
- current: index
- });
- },
-
- // 格式化时间
- formatTime(timestamp) {
- if (!timestamp) return '';
-
- const now = new Date().getTime();
- const diff = now - timestamp;
-
- // 小于1分钟
- if (diff < 60000) {
- return '刚刚';
- }
-
- // 小于1小时
- if (diff < 3600000) {
- return Math.floor(diff / 60000) + '分钟前';
- }
-
- // 小于24小时
- if (diff < 86400000) {
- return Math.floor(diff / 3600000) + '小时前';
- }
-
- // 小于30天
- if (diff < 2592000000) {
- return Math.floor(diff / 86400000) + '天前';
- }
-
- // 大于30天显示具体日期
- const date = new Date(timestamp);
- return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
- }
- }
- }
- </script>
-
- <style lang="scss" scoped>
- .comment-detail-container {
- display: flex;
- flex-direction: column;
- min-height: 100vh;
- background-color: #f5f5f5;
- padding-bottom: 100rpx;
- }
-
- .loading-container {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- margin-top: 200rpx;
-
- .loading-spinner {
- width: 80rpx;
- height: 80rpx;
- border: 6rpx solid #f3f3f3;
- border-top: 6rpx solid $uni-color-primary;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- margin-bottom: 20rpx;
- }
-
- .loading-text {
- font-size: 28rpx;
- color: #666;
- }
-
- @keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
- }
-
- .error-container {
- display: flex;
- flex-direction: column;
- align-items: center;
- margin-top: 200rpx;
-
- .error-text {
- font-size: 28rpx;
- color: #999;
- margin-bottom: 30rpx;
- }
-
- .retry-button {
- background-color: $uni-color-primary;
- color: #fff;
- font-size: 28rpx;
- padding: 16rpx 40rpx;
- border-radius: 40rpx;
- border: none;
- }
- }
-
- .main-comment {
- background-color: #fff;
- padding: 30rpx;
- margin-bottom: 20rpx;
-
- .comment-header {
- display: flex;
- align-items: center;
- margin-bottom: 20rpx;
-
- .avatar {
- width: 80rpx;
- height: 80rpx;
- border-radius: 50%;
- margin-right: 20rpx;
- background-color: #f0f0f0;
- }
-
- .user-info {
- flex: 1;
-
- .username {
- font-size: 28rpx;
- font-weight: bold;
- color: #333;
- margin-bottom: 6rpx;
- }
-
- .time {
- font-size: 24rpx;
- color: #999;
- }
- }
- }
-
- .comment-content {
- font-size: 30rpx;
- color: #333;
- line-height: 1.6;
- margin-bottom: 20rpx;
- }
-
- .comment-images {
- margin-bottom: 20rpx;
-
- .image-grid {
- display: flex;
- flex-wrap: wrap;
-
- .comment-image {
- width: 200rpx;
- height: 200rpx;
- margin-right: 10rpx;
- margin-bottom: 10rpx;
- border-radius: 8rpx;
- background-color: #f0f0f0;
- }
- }
- }
-
- .comment-footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-top: 20rpx;
- border-top: 1px solid #f0f0f0;
-
- .reply-count {
- font-size: 26rpx;
- color: #666;
- }
-
- .reply-btn {
- display: flex;
- align-items: center;
-
- text {
- font-size: 26rpx;
- color: #666;
- margin-left: 6rpx;
- }
- }
- }
- }
-
- .replies-container {
- background-color: #fff;
-
- .reply-item {
- padding: 30rpx;
- border-bottom: 1px solid #f0f0f0;
-
- &:active {
- background-color: #f9f9f9;
- }
-
- .reply-header {
- display: flex;
- align-items: center;
- margin-bottom: 16rpx;
-
- .avatar {
- width: 70rpx;
- height: 70rpx;
- border-radius: 50%;
- margin-right: 16rpx;
- background-color: #f0f0f0;
- }
-
- .user-info {
- flex: 1;
-
- .username {
- font-size: 26rpx;
- font-weight: bold;
- color: #333;
- margin-bottom: 4rpx;
- }
-
- .time {
- font-size: 22rpx;
- color: #999;
- }
- }
- }
-
- .reply-content {
- font-size: 28rpx;
- color: #333;
- line-height: 1.5;
- margin-bottom: 16rpx;
- padding-left: 86rpx;
-
- .reply-to {
- display: inline;
-
- .reply-username {
- color: $uni-color-primary;
- }
- }
- }
-
- .reply-images {
- padding-left: 86rpx;
- margin-bottom: 16rpx;
-
- .image-grid {
- display: flex;
- flex-wrap: wrap;
-
- .reply-image {
- width: 150rpx;
- height: 150rpx;
- margin-right: 10rpx;
- margin-bottom: 10rpx;
- border-radius: 8rpx;
- background-color: #f0f0f0;
- }
- }
- }
-
- .reply-footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-left: 86rpx;
-
- .reply-info {
- .sub-reply-count {
- font-size: 24rpx;
- color: $uni-color-primary;
- }
- }
-
- .reply-btn {
- display: flex;
- align-items: center;
-
- text {
- font-size: 24rpx;
- color: #666;
- margin-left: 4rpx;
- }
- }
- }
- }
- }
-
- .load-more {
- text-align: center;
- padding: 30rpx 0;
-
- text {
- font-size: 26rpx;
- color: #666;
- }
- }
-
- .reply-input-container {
- position: fixed;
- bottom: 0;
- left: 0;
- right: 0;
- background-color: #fff;
- padding: 20rpx 30rpx;
- box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
- z-index: 100;
- padding-bottom: env(safe-area-inset-bottom);
-
- .input-wrapper {
- display: flex;
- align-items: center;
-
- .reply-input {
- flex: 1;
- height: 70rpx;
- background-color: #f5f5f5;
- border-radius: 35rpx;
- padding: 0 30rpx;
- font-size: 28rpx;
- }
-
- .send-btn {
- margin-left: 20rpx;
- background-color: $uni-color-primary;
- color: #fff;
- font-size: 28rpx;
- height: 70rpx;
- line-height: 70rpx;
- padding: 0 30rpx;
- border-radius: 35rpx;
-
- &[disabled] {
- background-color: #cccccc;
- }
- }
- }
- }
- </style>
|