Browse Source

feat(动态列表): 添加评论列表组件和头像堆叠组件

- 在dynamicItem.vue中新增评论列表组件,支持显示评论及查看更多功能
- 创建avatarStack.vue组件实现头像堆叠显示效果
- 在postDetail.vue中集成头像堆叠组件展示浏览用户
- 优化评论列表样式和时间格式化功能
master
主管理员 3 weeks ago
parent
commit
fe78ce9a3c
4 changed files with 578 additions and 19 deletions
  1. +201
    -0
      components/list/avatarStack.vue
  2. +269
    -0
      components/list/dynamic/commentList.vue
  3. +16
    -1
      components/list/dynamic/dynamicItem.vue
  4. +92
    -18
      pages_order/post/postDetail.vue

+ 201
- 0
components/list/avatarStack.vue View File

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

+ 269
- 0
components/list/dynamic/commentList.vue View File

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

+ 16
- 1
components/list/dynamic/dynamicItem.vue View File

@ -13,6 +13,14 @@
:item="item"
type="0"
/>
<!-- 评论列表组件 -->
<commentList
:comments="item.comments"
:hasMoreComments="item.isComment > item.comments.length"
@goToDetail="handleGoToDetail"
v-if="item.comments && item.comments.length > 0"
/>
</view>
</template>
@ -21,12 +29,14 @@
import daynamicInfo from '@/components/list/dynamic/daynamicInfo.vue'
import dynamicToShop from '@/components/list/dynamic/dynamicToShop.vue'
import statisticalDataInfo from '@/components/list/statisticalDataInfo.vue'
import commentList from '@/components/list/dynamic/commentList.vue'
export default {
components: {
userHeadItem,
daynamicInfo,
statisticalDataInfo,
dynamicToShop,
commentList,
},
props: {
item: {},
@ -34,7 +44,12 @@
data() {
return {}
},
methods: {},
methods: {
handleGoToDetail() {
// item
this.$emit('goToDetail', this.item)
}
},
}
</script>


+ 92
- 18
pages_order/post/postDetail.vue View File

@ -14,8 +14,22 @@
<statisticalDataInfo :item="detail"/>
<!-- 头像堆叠组件 - 显示查看过的用户 -->
<avatarStack
:avatars="viewedUsers || []"
:maxDisplay="5"
:avatarSize="50"
:overlapOffset="12"
descriptionType="viewed"
@avatarClick="handleAvatarClick"
@moreClick="handleMoreViewers"
v-if="viewedUsers && viewedUsers.length > 0"
/>
</view>
<view style="background-color: #fff;margin-top: 20rpx;">
<!-- <view style="background-color: #fff;margin-top: 20rpx;">
<uv-tabs :list="tags"
:activeStyle="{color : '#000', fontWeight : 900}"
lineColor="#5baaff"
@ -23,17 +37,11 @@
lineWidth="50rpx"
:scrollable="false"
@click="tabsClick"></uv-tabs>
</view>
<view class="content" v-if="tagIndex == 1">
</view>
</view> -->
<commentList v-if="tagIndex == 0" @getData="getData" :list="list" :params="params" />
<!-- <commentList
@getData="getData"
:list="list"
@ -51,6 +59,7 @@
import daynamicInfo from '@/components/list/dynamic/daynamicInfo.vue'
import statisticalDataInfo from '@/components/list/statisticalDataInfo.vue'
import dynamicToShop from '@/components/list/dynamic/dynamicToShop.vue'
import avatarStack from '@/components/list/avatarStack.vue'
export default {
mixins: [mixinsSex, mixinsList],
components: {
@ -59,10 +68,51 @@
daynamicInfo,
statisticalDataInfo,
dynamicToShop,
avatarStack,
},
data() {
return {
detail: {},
detail: {
},
// -
viewedUsers: [
{
id: '1',
userHead: '/static/image/logo.jpg',
name: '用户A'
},
{
id: '2',
userHead: 'https://image.hhlm1688.com/2025-06-08/67fbe844-da4a-4272-8d51-364453d0f3aa.jpeg',
name: '用户B'
},
{
id: '3',
userHead: '/static/image/logo.jpg',
name: '用户C'
},
{
id: '4',
userHead: 'https://image.hhlm1688.com/2025-06-08/67fbe844-da4a-4272-8d51-364453d0f3aa.jpeg',
name: '用户D'
},
{
id: '5',
userHead: '/static/image/logo.jpg',
name: '用户E'
},
{
id: '6',
userHead: 'https://image.hhlm1688.com/2025-06-08/67fbe844-da4a-4272-8d51-364453d0f3aa.jpeg',
name: '用户F'
},
{
id: '7',
userHead: '/static/image/logo.jpg',
name: '用户G'
}
],
mixinsListApi : 'getCommentPage',
params : {
type : '0',
@ -73,15 +123,15 @@
{
name : '评论'
},
{
name : '浏览'
},
{
name : '点赞'
},
{
name : '分享'
},
// {
// name : ''
// },
// {
// name : ''
// },
// {
// name : ''
// },
],
tagIndex : 0,
id : 0,
@ -133,6 +183,30 @@
}
})
},
//
handleAvatarClick(avatar, index) {
console.log('点击头像:', avatar, index);
//
this.$u.route({
url: '/pages_order/user/userDetail',
params: {
userId: avatar.id
}
});
},
//
handleMoreViewers() {
console.log('查看更多查看者');
//
this.$u.route({
url: '/pages_order/post/viewersList',
params: {
postId: this.detail.id
}
});
},
}
}
</script>


Loading…
Cancel
Save