Browse Source

feat(评论): 实现多级评论功能及UI优化

feat(产品): 添加支付状态控制显示逻辑

fix(配置): 修改默认环境为local

refactor(组件): 重构评论组件结构,支持子评论加载和回复

style(评论): 优化评论列表和子评论的UI样式
master
主管理员 3 days ago
parent
commit
0a0e99678b
8 changed files with 438 additions and 50 deletions
  1. +2
    -2
      components/user/productList.vue
  2. +1
    -1
      config.js
  3. +247
    -41
      pages_order/components/list/comment/commentItem.vue
  4. +98
    -3
      pages_order/components/list/comment/commentList.vue
  5. +74
    -3
      pages_order/components/list/comment/commentPublish.vue
  6. +5
    -0
      pages_order/components/product/submit.vue
  7. +8
    -0
      pages_order/gourmet/gourmetDetail.vue
  8. +3
    -0
      pages_order/product/productDetail.vue

+ 2
- 2
components/user/productList.vue View File

@ -2,7 +2,7 @@
<view class="list">
<view class="item"
v-for="(item, index) in productList"
@click="$utils.navigateTo('/pages_order/product/productDetail?id=' + item.id)"
@click="$utils.navigateTo(`/pages_order/product/productDetail?id=${item.id}&pay=${pay}`)"
:key="index">
<image
class="image"
@ -71,7 +71,7 @@
export default {
mixins : [productMixins],
name:"productList",
props : ['productList'],
props : ['productList', 'pay'],
data() {
return {


+ 1
- 1
config.js View File

@ -7,7 +7,7 @@ import uvUI from '@/uni_modules/uv-ui-tools'
Vue.use(uvUI);
// 当前环境
const type = 'prod'
const type = 'local'
// 环境配置


+ 247
- 41
pages_order/components/list/comment/commentItem.vue View File

@ -1,45 +1,78 @@
<template>
<view class="comment" @click="navigateToSubComment(item)">
<view class="box">
<view class="headPortraitimg">
<view class="comment">
<view class="comment-header">
<view class="avatar">
<image :src="item.userHead"
@click.stop="previewImage([item.userHead])"
mode="aspectFill"></image>
</view>
<view class="YaoduUniversalWall">
<view class="heide">
<view class="username text-ellipsis">
{{ item.userName }}
</view>
</view>
<view class="Times">
<view class="TimeMonth">
{{ item.createTime }}发布
</view>
<view class="user-info">
<view class="username">{{ item.userName }}</view>
<view class="comment-content" v-html="$utils.stringFormatHtml(item.userValue)"></view>
<view class="comment-meta">
<text class="time">{{ item.createTime }}</text>
<text class="location">贵州</text>
<text class="reply-btn" @click.stop="handleReply">回复</text>
</view>
</view>
</view>
<view class="dynamics" v-html="$utils.stringFormatHtml(item.userValue)">
</view>
<view class="images">
<!-- 主评论图片 -->
<view class="images" v-if="images && images.length > 0">
<view class="image"
@click.stop="previewImage(images, i)"
:key="i" v-for="(img, i) in images">
<image :src="img" mode="aspectFill"></image>
</view>
</view>
<!-- 子评论列表 -->
<view class="sub-comments-list" v-if="subComments && subComments.length > 0">
<view class="sub-comment-item" v-for="(subComment, index) in subComments" :key="subComment.id">
<view class="sub-comment-header">
<view class="sub-avatar">
<image :src="subComment.userHead" mode="aspectFill" @click.stop="previewImage([subComment.userHead])"></image>
</view>
<view class="sub-user-info">
<view class="sub-username">
<text>{{ subComment.userName }}</text>
<!-- 显示回复信息 -->
<text class="reply-to" v-if="subComment.replyToName">回复 @{{ subComment.replyToName }}:</text>
</view>
<view class="sub-comment-content" v-html="$utils.stringFormatHtml(subComment.userValue)"></view>
<view class="sub-comment-meta">
<text class="sub-time">{{ subComment.createTime }}</text>
<text class="sub-reply-btn" @click.stop="handleSubReply(subComment)">回复</text>
</view>
</view>
</view>
<!-- 子评论图片 -->
<view class="sub-comment-images" v-if="subComment.userImage">
<view class="sub-image"
@click.stop="previewImage(subComment.userImage.split(','), i)"
:key="i" v-for="(img, i) in subComment.userImage.split(',')">
<image :src="img" mode="aspectFill"></image>
</view>
</view>
</view>
</view>
<!-- 加载更多子评论按钮 -->
<view class="load-more-section" v-if="item.replyNum && item.replyNum > 0 && (!subComments || subComments.length === 0)">
<view class="load-more-btn" @click.stop="loadSubComments">
<text class="load-more-text">查看{{item.replyNum}}条回复</text>
</view>
</view>
</view>
</template>
<script>
export default {
props : ['item', 'parentId', 'sourceType', 'sourceId'],
props : ['item', 'parentId', 'sourceType', 'sourceId', 'subComments'],
data() {
return {
isLoadingSubComments: false //
}
},
computed : {
@ -48,7 +81,7 @@
return []
}
return this.item.userImage.split(',')
},
}
},
methods: {
//
@ -57,6 +90,34 @@
url: `/pages_order/comment/commentDetail?id=${comment.id}&parentId=${this.parentId}&sourceType=${this.sourceType}&sourceId=${this.sourceId}`
});
},
//
loadSubComments() {
if (this.isLoadingSubComments) return; //
this.isLoadingSubComments = true;
uni.showLoading({
title: '加载中...'
});
this.$emit('loadSubComments', this.item);
//
setTimeout(() => {
this.isLoadingSubComments = false;
uni.hideLoading();
}, 1000);
},
//
handleReply() {
this.$emit('reply', this.item);
},
//
handleSubReply(subComment) {
this.$emit('reply', subComment);
}
}
}
</script>
@ -65,17 +126,20 @@
.comment {
background-color: #fff;
padding: 30rpx 40rpx;
margin-top: 10rpx;
margin-bottom: 2rpx;
border-bottom: 2rpx solid #f5f5f5;
.box {
.comment-header {
display: flex;
align-items: center;
align-items: flex-start;
.headPortraitimg {
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 15rpx;
border-radius: 50%;
overflow: hidden;
margin-right: 20rpx;
flex-shrink: 0;
image {
width: 100%;
@ -83,23 +147,47 @@
}
}
.YaoduUniversalWall {
padding: 0rpx 10rpx;
font-size: 26rpx;
line-height: 40rpx;
.user-info {
flex: 1;
.Times {
font-size: 22rpx;
.username {
font-size: 28rpx;
font-weight: 600;
color: $uni-text-color;
margin-bottom: 8rpx;
}
}
}
.dynamics {
margin-top: 20rpx;
margin-left: 100rpx;
font-size: 28rpx;
letter-spacing: 3rpx;
word-break: break-all;
.comment-content {
font-size: 30rpx;
line-height: 1.5;
color: $uni-text-color;
margin-bottom: 12rpx;
word-break: break-all;
}
.comment-meta {
display: flex;
align-items: center;
font-size: 24rpx;
color: $uni-text-color-grey;
.time {
margin-right: 20rpx;
}
.location {
margin-right: 20rpx;
}
.reply-btn {
color: $uni-text-color-grey;
padding: 8rpx 16rpx;
background-color: #f5f5f5;
border-radius: 20rpx;
font-size: 22rpx;
}
}
}
}
.images {
@ -107,14 +195,132 @@
flex-wrap: wrap;
margin-top: 20rpx;
margin-left: 100rpx;
.image {
margin: 10rpx;
image {
height: 120rpx;
width: 120rpx;
border-radius: 20rpx;
border-radius: 12rpx;
}
}
}
//
.sub-comments-list {
margin-top: 20rpx;
margin-left: 100rpx;
.sub-comment-item {
margin-bottom: 30rpx;
&:last-child {
margin-bottom: 0;
}
.sub-comment-header {
display: flex;
align-items: flex-start;
.sub-avatar {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
overflow: hidden;
margin-right: 16rpx;
flex-shrink: 0;
image {
width: 100%;
height: 100%;
}
}
.sub-user-info {
flex: 1;
.sub-username {
font-size: 26rpx;
font-weight: 600;
color: $uni-text-color;
margin-bottom: 6rpx;
.reply-to {
font-size: 24rpx;
color: $uni-color-primary;
font-weight: 500;
margin-left: 10rpx;
}
}
.sub-comment-content {
font-size: 28rpx;
line-height: 1.5;
color: $uni-text-color;
margin-bottom: 10rpx;
word-break: break-all;
}
.sub-comment-meta {
display: flex;
align-items: center;
font-size: 22rpx;
color: $uni-text-color-grey;
.sub-time {
margin-right: 16rpx;
}
.sub-location {
margin-right: 16rpx;
}
.sub-reply-btn {
color: $uni-text-color-grey;
padding: 6rpx 12rpx;
background-color: #f5f5f5;
border-radius: 16rpx;
font-size: 20rpx;
margin-left: auto;
}
}
}
}
.sub-comment-images {
display: flex;
flex-wrap: wrap;
margin-top: 12rpx;
margin-left: 76rpx;
.sub-image {
margin-right: 8rpx;
margin-bottom: 8rpx;
image {
width: 96rpx;
height: 96rpx;
border-radius: 8rpx;
}
}
}
}
}
.load-more-section {
margin-top: 20rpx;
margin-left: 100rpx;
.load-more-btn {
display: inline-flex;
align-items: center;
padding: 8rpx 16rpx;
background-color: #f5f5f5;
border-radius: 20rpx;
font-size: 24rpx;
color: $uni-text-color-grey;
}
}
}
</style>

+ 98
- 3
pages_order/components/list/comment/commentList.vue View File

@ -2,7 +2,8 @@
<view class="commemt">
<view class="comment-list">
<commentItem v-for="(item, index) in list" :key="index" :parentId="item.id" :sourceType="params.type"
:sourceId="params.orderId" :item="item" />
:sourceId="params.orderId" :item="item" :subComments="getSubComments(item.id)"
@loadSubComments="handleLoadSubComments" @reply="handleReply" />
</view>
<view class="submit-box">
@ -32,17 +33,111 @@ export default {
},
props: ['list', 'params'],
data() {
return {}
return {
currentReplyComment: null, //
subCommentsMap: {} // keyIDvalue
}
},
methods: {
//
getSubComments(parentId) {
return this.subCommentsMap[parentId] || [];
},
//
openCommentPublish() {
this.currentReplyComment = null
this.$refs.commentPublish.open()
},
//
handleCommentSuccess() {
handleCommentSuccess(successData) {
console.log('handleCommentSuccess 接收到的数据:', successData);
//
if (successData && successData.isReply && successData.parentId) {
console.log('准备重新加载子评论,parentId:', successData.parentId);
//
this.loadSubCommentsData(successData.parentId);
} else {
console.log('不是回复评论或缺少parentId,successData:', successData);
}
//
this.$emit('getData')
},
//
handleLoadSubComments(comment) {
// API
this.loadSubCommentsData(comment.id);
},
//
async loadSubCommentsData(parentId) {
try {
//
uni.showLoading({
title: '加载中...'
});
const apiParams = {
pid: parentId,
type: this.params.type,
orderId: this.params.orderId,
page: 1,
pageSize: 99999999
};
// 使API
this.$api('getCommentPage', apiParams, res => {
uni.hideLoading();
if (res.code === 200) {
//
const subComments = res.result.records || [];
this.$set(this.subCommentsMap, parentId, subComments);
}
});
} catch (error) {
uni.hideLoading();
}
},
//
handleReply(comment) {
this.currentReplyComment = comment
//
let replyParams
let parentCommentId // IDID
if (comment.pid && comment.pid !== 0) {
// 使IDpid
parentCommentId = comment.pid // pidID
replyParams = {
...this.params,
pid: comment.pid, // 使ID
replyToId: comment.id,
replyToName: comment.userName,
replyToAvatar: comment.userHead,
parentCommentId: parentCommentId // ID
}
} else {
// 使IDpid
parentCommentId = comment.id // IDID
replyParams = {
...this.params,
pid: comment.id, // 使IDpid
replyToId: comment.id,
replyToName: comment.userName,
replyToAvatar: comment.userHead,
parentCommentId: parentCommentId // ID
}
}
this.$refs.commentPublish.setReplyParams(replyParams)
this.$refs.commentPublish.open()
}
}
}


+ 74
- 3
pages_order/components/list/comment/commentPublish.vue View File

@ -2,9 +2,17 @@
<view>
<uv-popup ref="popup" :round="30">
<view class="comment-publish">
<!-- 回复目标信息显示 -->
<view class="reply-target" v-if="replyParams && replyParams.replyToName">
<view class="reply-info">
<text class="reply-text">回复 @{{ replyParams.replyToName }}:</text>
<text class="cancel-reply" @click="cancelReply">×</text>
</view>
</view>
<view class="content-input">
<uv-textarea v-model="form.userValue" :maxlength="200" autoHeight count focus
:placeholder="placeholder"></uv-textarea>
:placeholder="replyParams && replyParams.replyToName ? `回复 @${replyParams.replyToName}` : placeholder"></uv-textarea>
</view>
<view class="images box">
@ -54,7 +62,8 @@ export default {
userValue: ''
},
imageMax: 9,
fileList: []
fileList: [],
replyParams: null //
}
},
methods: {
@ -97,6 +106,25 @@ export default {
...this.form,
...this.params,
}
//
if (this.replyParams) {
data = {
...data,
pid: this.replyParams.pid, // pid
replyToId: this.replyParams.replyToId,
replyToName: this.replyParams.replyToName, // replyToName
replyToAvatar: this.replyParams.replyToAvatar
}
// replyToName
console.log('回复参数:', {
pid: data.pid,
replyToId: data.replyToId,
replyToName: data.replyToName,
replyToAvatar: data.replyToAvatar
});
}
if (this.$utils.verificationAll(data, {
userValue: '说点什么吧',
@ -109,6 +137,7 @@ export default {
data.userImage = this.fileList.map((item) => item.url).join(",")
this.$api('addComment', data, res => {
console.log('addComment API 响应:', res);
if (res.code == 200) {
this.close()
this.resetForm()
@ -116,7 +145,14 @@ export default {
title: '发布成功!',
icon: 'none'
})
this.$emit('success')
//
const successData = {
isReply: !!data.pid,
parentId: data.pid,
replyData: data
};
console.log('准备触发success事件,数据:', successData);
this.$emit('success', successData)
}
})
},
@ -125,6 +161,17 @@ export default {
resetForm() {
this.form.userValue = ''
this.fileList = []
this.replyParams = null
},
//
setReplyParams(params) {
this.replyParams = params
},
//
cancelReply() {
this.replyParams = null
},
deleteImage(e) {
@ -147,6 +194,30 @@ export default {
<style scoped lang="scss">
.comment-publish {
.reply-target {
padding: 20rpx;
background-color: #f5f5f5;
border-bottom: 1px solid #e5e5e5;
.reply-info {
display: flex;
justify-content: space-between;
align-items: center;
.reply-text {
color: #666;
font-size: 28rpx;
}
.cancel-reply {
color: #999;
font-size: 36rpx;
font-weight: bold;
padding: 0 10rpx;
}
}
}
.content-input {
min-height: 400rpx;
}


+ 5
- 0
pages_order/components/product/submit.vue View File

@ -28,6 +28,7 @@
</view> -->
<view class="submit-btn"
v-if="pay == 'Y'"
@click="submit">
<view class="r"
v-if="isProductPoint(detail)">
@ -52,6 +53,10 @@
default : '立即兑换',
type : String,
},
pay : {
default : 'N',
type : String,
},
detail : {
default : {}
},


+ 8
- 0
pages_order/gourmet/gourmetDetail.vue View File

@ -56,11 +56,17 @@
<view class="goodList"
v-if="tagIndex == 0">
<productSelectList
v-if="detail.pay == 'Y'"
:shopId="detail.id"
:edit="detail.shopUser == userInfo.id"
:detail="detail"
@getData="getData"
:list="list"/>
<view v-else style="padding: 0 20rpx;">
<productList :productList="list" :pay="detail.pay"/>
</view>
</view>
<!-- 店铺介绍 -->
@ -126,11 +132,13 @@
import commentList from '../components/list/comment/commentList.vue'
// import goodList from '../components/list/gourmet/goodList.vue'
import productSelectList from '../components/list/gourmet/productSelectList.vue'
import productList from '@/components/user/productList.vue'
export default {
mixins: [mixinsSex, mixinsList],
components: {
commentList,
productSelectList,
productList,
},
data() {
return {


+ 3
- 0
pages_order/product/productDetail.vue View File

@ -86,6 +86,7 @@
<!-- 分享和租赁按钮 -->
<submit
:detail="detail"
:pay="pay"
@submit="handleSubmit"
@share="share"/>
@ -111,10 +112,12 @@
return {
detail : {},
id : 0,
pay : 'N',
}
},
onLoad(args) {
this.id = args.id
this.pay = args.pay
this.getData()
//


Loading…
Cancel
Save