- 在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> |