- 在dynamicItem.vue中新增评论列表组件,支持显示评论及查看更多功能 - 创建avatarStack.vue组件实现头像堆叠显示效果 - 在postDetail.vue中集成头像堆叠组件展示浏览用户 - 优化评论列表样式和时间格式化功能master
| @ -0,0 +1,201 @@ | |||||
| <template> | |||||
| <view class="avatar-stack" v-if="avatars && avatars.length > 0"> | |||||
| <view class="avatar-container"> | |||||
| <!-- 显示的头像列表 --> | |||||
| <view | |||||
| class="avatar-item" | |||||
| v-for="(avatar, index) in displayAvatars" | |||||
| :key="index" | |||||
| :style="{ | |||||
| width: avatarSize + 'rpx', | |||||
| height: avatarSize + 'rpx', | |||||
| marginLeft: index > 0 ? (-overlapOffset + 'rpx') : '0', | |||||
| zIndex: displayAvatars.length - index | |||||
| }" | |||||
| @click="handleAvatarClick(avatar, index)" | |||||
| > | |||||
| <image | |||||
| :src="avatar.userHead || '/static/image/center/default-avatar.png'" | |||||
| mode="aspectFill" | |||||
| class="avatar-image" | |||||
| /> | |||||
| </view> | |||||
| <!-- 更多数量显示 --> | |||||
| <view | |||||
| class="more-count" | |||||
| v-if="remainingCount > 0" | |||||
| :style="{ | |||||
| width: avatarSize + 'rpx', | |||||
| height: avatarSize + 'rpx', | |||||
| marginLeft: displayAvatars.length > 0 ? (-overlapOffset + 'rpx') : '0', | |||||
| zIndex: 1 | |||||
| }" | |||||
| @click="handleMoreClick" | |||||
| > | |||||
| <text class="count-text">+{{ remainingCount }}</text> | |||||
| </view> | |||||
| </view> | |||||
| <!-- 描述文本 --> | |||||
| <view class="description" v-if="showDescription"> | |||||
| <text class="desc-text">{{ getDescriptionText() }}</text> | |||||
| </view> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| export default { | |||||
| name: 'AvatarStack', | |||||
| props: { | |||||
| // 头像数据数组 | |||||
| avatars: { | |||||
| type: Array, | |||||
| default: () => [] | |||||
| }, | |||||
| // 最大显示数量 | |||||
| maxDisplay: { | |||||
| type: Number, | |||||
| default: 5 | |||||
| }, | |||||
| // 头像大小 | |||||
| avatarSize: { | |||||
| type: Number, | |||||
| default: 60 // rpx | |||||
| }, | |||||
| // 重叠偏移量 | |||||
| overlapOffset: { | |||||
| type: Number, | |||||
| default: 15 // rpx | |||||
| }, | |||||
| // 是否显示描述文本 | |||||
| showDescription: { | |||||
| type: Boolean, | |||||
| default: true | |||||
| }, | |||||
| // 描述文本类型 | |||||
| descriptionType: { | |||||
| type: String, | |||||
| default: 'viewed' // viewed, liked, shared | |||||
| }, | |||||
| // 自定义描述文本 | |||||
| customDescription: { | |||||
| type: String, | |||||
| default: '' | |||||
| } | |||||
| }, | |||||
| computed: { | |||||
| // 显示的头像列表 | |||||
| displayAvatars() { | |||||
| return this.avatars.slice(0, this.maxDisplay) | |||||
| }, | |||||
| // 剩余数量 | |||||
| remainingCount() { | |||||
| return Math.max(0, this.avatars.length - this.maxDisplay) | |||||
| } | |||||
| }, | |||||
| methods: { | |||||
| // 获取描述文本 | |||||
| getDescriptionText() { | |||||
| if (this.customDescription) { | |||||
| return this.customDescription | |||||
| } | |||||
| const total = this.avatars.length | |||||
| if (total === 0) return '' | |||||
| switch (this.descriptionType) { | |||||
| case 'viewed': | |||||
| return `${total}人看过` | |||||
| case 'liked': | |||||
| return `${total}人点赞` | |||||
| case 'shared': | |||||
| return `${total}人分享` | |||||
| default: | |||||
| return `${total}人参与` | |||||
| } | |||||
| }, | |||||
| // 头像点击事件 | |||||
| handleAvatarClick(avatar, index) { | |||||
| this.$emit('avatarClick', { avatar, index }) | |||||
| }, | |||||
| // 更多按钮点击事件 | |||||
| handleMoreClick() { | |||||
| this.$emit('moreClick', { | |||||
| total: this.avatars.length, | |||||
| remaining: this.remainingCount | |||||
| }) | |||||
| } | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <style scoped lang="scss"> | |||||
| .avatar-stack { | |||||
| display: flex; | |||||
| flex-direction: column; | |||||
| align-items: flex-start; | |||||
| .avatar-container { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| .avatar-item { | |||||
| position: relative; | |||||
| border-radius: 50%; | |||||
| overflow: hidden; | |||||
| background-color: #fff; | |||||
| border: 4rpx solid #fff; | |||||
| box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); | |||||
| transition: transform 0.2s ease; | |||||
| &:hover { | |||||
| transform: scale(1.1); | |||||
| } | |||||
| .avatar-image { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| border-radius: 50%; | |||||
| } | |||||
| } | |||||
| .more-count { | |||||
| position: relative; | |||||
| border-radius: 50%; | |||||
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |||||
| display: flex; | |||||
| align-items: center; | |||||
| justify-content: center; | |||||
| border: 4rpx solid #fff; | |||||
| box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); | |||||
| .count-text { | |||||
| color: #fff; | |||||
| font-size: 20rpx; | |||||
| font-weight: 600; | |||||
| } | |||||
| } | |||||
| } | |||||
| .description { | |||||
| margin-top: 15rpx; | |||||
| .desc-text { | |||||
| font-size: 24rpx; | |||||
| color: #666; | |||||
| line-height: 1.4; | |||||
| } | |||||
| } | |||||
| } | |||||
| // 不同尺寸适配 | |||||
| @media screen and (max-width: 750rpx) { | |||||
| .avatar-stack .description .desc-text { | |||||
| font-size: 22rpx; | |||||
| } | |||||
| } | |||||
| </style> | |||||
| @ -0,0 +1,269 @@ | |||||
| <template> | |||||
| <view class="comment-list" v-if="comments && comments.length > 0"> | |||||
| <view class="comment-header"> | |||||
| <view class="comment-icon">💬</view> | |||||
| <text class="comment-count">评论</text> | |||||
| </view> | |||||
| <view class="comment-container"> | |||||
| <view | |||||
| class="comment-item" | |||||
| v-for="(comment, index) in comments" | |||||
| :key="index" | |||||
| > | |||||
| <view class="comment-user"> | |||||
| <image | |||||
| class="user-avatar" | |||||
| :src="comment.userHead || '/static/image/center/default-avatar.png'" | |||||
| @click.stop="previewImage([comment.userHead])" | |||||
| mode="aspectFill" | |||||
| ></image> | |||||
| <view class="comment-content"> | |||||
| <view class="user-info"> | |||||
| <text class="username">{{ comment.userName }}</text> | |||||
| <text class="comment-time">{{ formatTime(comment.createTime) }}</text> | |||||
| </view> | |||||
| <view class="comment-text" v-html="$utils.stringFormatHtml(comment.userValue)"> | |||||
| </view> | |||||
| <!-- 评论图片 --> | |||||
| <view class="comment-images" v-if="getCommentImages(comment).length > 0"> | |||||
| <view | |||||
| class="comment-image" | |||||
| v-for="(img, imgIndex) in getCommentImages(comment)" | |||||
| :key="imgIndex" | |||||
| @click.stop="previewImage(getCommentImages(comment), imgIndex)" | |||||
| > | |||||
| <image :src="img" mode="aspectFill"></image> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| <!-- 查看更多评论按钮 --> | |||||
| <view | |||||
| class="more-btn" | |||||
| v-if="hasMoreComments" | |||||
| @click="goToDetail" | |||||
| > | |||||
| <text>查看更多评论</text> | |||||
| <text class="more-icon">→</text> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| export default { | |||||
| name: 'CommentList', | |||||
| props: { | |||||
| comments: { | |||||
| type: Array, | |||||
| default: () => [] | |||||
| }, | |||||
| hasMoreComments: { | |||||
| type: Boolean, | |||||
| default: false | |||||
| } | |||||
| }, | |||||
| data() { | |||||
| return { | |||||
| } | |||||
| }, | |||||
| computed: { | |||||
| }, | |||||
| methods: { | |||||
| goToDetail() { | |||||
| this.$emit('goToDetail') | |||||
| }, | |||||
| getCommentImages(comment) { | |||||
| if (!comment.userImage) { | |||||
| return [] | |||||
| } | |||||
| return comment.userImage.split(',').filter(img => img.trim()) | |||||
| }, | |||||
| formatTime(time) { | |||||
| if (!time) return '' | |||||
| // 如果时间已经包含"发布",直接返回 | |||||
| if (time.includes && time.includes('发布')) { | |||||
| return time | |||||
| } | |||||
| // 使用dayjs格式化时间戳 | |||||
| if (typeof time === 'number' || (typeof time === 'string' && /^\d+$/.test(time))) { | |||||
| const timestamp = Number(time) | |||||
| const now = this.$dayjs() | |||||
| const commentTime = this.$dayjs(timestamp) | |||||
| // 计算时间差 | |||||
| const diffMinutes = now.diff(commentTime, 'minute') | |||||
| const diffHours = now.diff(commentTime, 'hour') | |||||
| const diffDays = now.diff(commentTime, 'day') | |||||
| if (diffMinutes < 1) { | |||||
| return '刚刚' | |||||
| } else if (diffMinutes < 60) { | |||||
| return `${diffMinutes}分钟前` | |||||
| } else if (diffHours < 24) { | |||||
| return `${diffHours}小时前` | |||||
| } else if (diffDays < 7) { | |||||
| return `${diffDays}天前` | |||||
| } else { | |||||
| return commentTime.format('MM-DD HH:mm') | |||||
| } | |||||
| } | |||||
| // 否则添加"发布"后缀 | |||||
| return time + '发布' | |||||
| }, | |||||
| previewImage(images, current = 0) { | |||||
| if (!images || images.length === 0) return | |||||
| uni.previewImage({ | |||||
| urls: images, | |||||
| current: current | |||||
| }) | |||||
| } | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <style scoped lang="scss"> | |||||
| .comment-list { | |||||
| margin-top: 20rpx; | |||||
| background-color: #f8f8f8; | |||||
| border-radius: 12rpx; | |||||
| overflow: hidden; | |||||
| .comment-header { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| padding: 20rpx 30rpx 15rpx; | |||||
| background-color: #f0f0f0; | |||||
| border-bottom: 1rpx solid #e5e5e5; | |||||
| .comment-icon { | |||||
| font-size: 28rpx; | |||||
| margin-right: 10rpx; | |||||
| } | |||||
| .comment-count { | |||||
| font-size: 26rpx; | |||||
| color: #666; | |||||
| font-weight: 500; | |||||
| } | |||||
| } | |||||
| .comment-container { | |||||
| padding: 0 30rpx 20rpx; | |||||
| .comment-item { | |||||
| padding: 20rpx 0; | |||||
| border-bottom: 1rpx solid #f0f0f0; | |||||
| &:last-child { | |||||
| border-bottom: none; | |||||
| } | |||||
| .comment-user { | |||||
| display: flex; | |||||
| align-items: flex-start; | |||||
| .user-avatar { | |||||
| width: 60rpx; | |||||
| height: 60rpx; | |||||
| border-radius: 50%; | |||||
| margin-right: 20rpx; | |||||
| flex-shrink: 0; | |||||
| } | |||||
| .comment-content { | |||||
| flex: 1; | |||||
| .user-info { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| margin-bottom: 8rpx; | |||||
| .username { | |||||
| font-size: 26rpx; | |||||
| color: #576b95; | |||||
| font-weight: 500; | |||||
| margin-right: 20rpx; | |||||
| } | |||||
| .comment-time { | |||||
| font-size: 22rpx; | |||||
| color: #999; | |||||
| } | |||||
| } | |||||
| .comment-text { | |||||
| font-size: 28rpx; | |||||
| color: #333; | |||||
| line-height: 1.5; | |||||
| word-break: break-all; | |||||
| margin-bottom: 10rpx; | |||||
| } | |||||
| .comment-images { | |||||
| display: flex; | |||||
| flex-wrap: wrap; | |||||
| margin-top: 15rpx; | |||||
| .comment-image { | |||||
| margin-right: 10rpx; | |||||
| margin-bottom: 10rpx; | |||||
| image { | |||||
| width: 100rpx; | |||||
| height: 100rpx; | |||||
| border-radius: 8rpx; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| .expand-btn { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| justify-content: center; | |||||
| padding: 15rpx 0; | |||||
| margin-top: 10rpx; | |||||
| color: #576b95; | |||||
| font-size: 26rpx; | |||||
| .expand-icon { | |||||
| margin-left: 10rpx; | |||||
| font-size: 20rpx; | |||||
| } | |||||
| } | |||||
| .more-btn { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| justify-content: center; | |||||
| padding: 15rpx 0; | |||||
| margin-top: 10rpx; | |||||
| color: #576b95; | |||||
| font-size: 26rpx; | |||||
| background-color: #f5f5f5; | |||||
| border-radius: 8rpx; | |||||
| .more-icon { | |||||
| margin-left: 10rpx; | |||||
| font-size: 24rpx; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| </style> | |||||