661 lines
15 KiB
Vue
661 lines
15 KiB
Vue
<template>
|
||
<view class="page">
|
||
<!-- 评分统计 -->
|
||
<view class="rating-stats">
|
||
<view class="rating-content">
|
||
<view class="rating-distribution">
|
||
<view class="rating-bar" v-for="(item, index) in ratingDistribution" :key="index">
|
||
<text class="rating-label">{{5-index}}分</text>
|
||
<view class="bar-wrapper">
|
||
<view class="bar-fill" :style="{ width: item.percentage + '%' }"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="rating-summary">
|
||
<view class="total-score">
|
||
<text class="score">{{averageScore}}</text>
|
||
<text class="max-score">/5.0</text>
|
||
</view>
|
||
<text class="total-reviews">{{totalReviews}}条评价</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 排序选项 -->
|
||
<view class="sort-options">
|
||
<view
|
||
class="sort-item"
|
||
:class="{ active: sortType === 'time' }"
|
||
@tap="changeSortType('time')"
|
||
>
|
||
<text>最新酒评</text>
|
||
</view>
|
||
<view
|
||
class="sort-item"
|
||
:class="{ active: sortType === 'score' }"
|
||
@tap="changeSortType('score')"
|
||
>
|
||
<text>评分排序</text>
|
||
</view>
|
||
<view
|
||
class="sort-item"
|
||
:class="{ active: sortType === 'hot' }"
|
||
@tap="changeSortType('hot')"
|
||
>
|
||
<text>热门酒评</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 评价列表 -->
|
||
<view class="review-list">
|
||
<view class="review-item" v-for="(item, index) in reviewList" :key="index">
|
||
<!-- 用户信息头部 -->
|
||
<view class="review-header">
|
||
<view class="user-info">
|
||
<image class="avatar" :src="item.userAvatar || '/static/default-avatar.png'" mode="aspectFill"></image>
|
||
<view class="user-meta">
|
||
<text class="user-name">{{item.userName || '匿名用户'}}</text>
|
||
<view class="rating-row">
|
||
<uni-rate
|
||
:value="item.compositeScore"
|
||
:readonly="true"
|
||
:size="16"
|
||
:touchable="false"
|
||
color="#ECECEC"
|
||
active-color="#FEE034"
|
||
/>
|
||
<text class="score-text">{{item.compositeScore}}分</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<text class="review-time">{{item.createTime.slice(0,10)}}</text>
|
||
</view>
|
||
|
||
<!-- 评论内容区域 -->
|
||
<view class="review-content">
|
||
<!-- 评论文本 -->
|
||
<view class="review-text">{{item.reviewContent}}</view>
|
||
<!-- 评论图片区域 -->
|
||
<scroll-view v-if="item.reviewImg" class="image-scroll" scroll-x="true">
|
||
<view class="image-container">
|
||
<image
|
||
v-for="(img, imgIndex) in item.reviewImg.split(',')"
|
||
:key="imgIndex"
|
||
:src="img"
|
||
class="review-image"
|
||
mode="aspectFill"
|
||
@tap="previewImage(item.reviewImg.split(','), imgIndex)"
|
||
></image>
|
||
</view>
|
||
</scroll-view>
|
||
<!-- 评分详情 -->
|
||
<view class="rating-details">
|
||
<view class="rating-item">
|
||
<text class="label">外观</text>
|
||
<uni-rate :value="item.colorRating" :readonly="true" :size="14"></uni-rate>
|
||
<text class="rating-value">{{item.colorRating}}分</text>
|
||
</view>
|
||
<view class="rating-item">
|
||
<text class="label">香气</text>
|
||
<uni-rate :value="item.aromaRating" :readonly="true" :size="14"></uni-rate>
|
||
<text class="rating-value">{{item.aromaRating}}分</text>
|
||
</view>
|
||
<view class="rating-item">
|
||
<text class="label">口感</text>
|
||
<uni-rate :value="item.tasteRating" :readonly="true" :size="14"></uni-rate>
|
||
<text class="rating-value">{{item.tasteRating}}分</text>
|
||
</view>
|
||
</view>
|
||
<!-- 点赞按钮 -->
|
||
<view class="review-footer">
|
||
<view class="like-btn" :class="{ active: item.reviewLike }" @tap.stop="handleLike(item)">
|
||
<image class="like-icon" src="/static/like.png" mode="aspectFit"></image>
|
||
<text class="like-count">{{item.likeCount || 0}}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 加载更多 -->
|
||
<view class="load-more" v-if="reviewList.length > 0">
|
||
<text class="load-text">{{ hasMore ? '加载更多...' : '没有更多了' }}</text>
|
||
</view>
|
||
<!-- 无评论展示 -->
|
||
<view v-if="reviewList.length === 0" class="no-review">
|
||
暂无评论,快来发表第一条评论吧
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import {
|
||
getReviewList,
|
||
getReviewScoreList,
|
||
likeReview
|
||
} from '@/api/bar.js'
|
||
import loginPopup from '@/components/loginPopup.vue';
|
||
|
||
export default {
|
||
components: {
|
||
loginPopup
|
||
},
|
||
data() {
|
||
return {
|
||
beerId: '',
|
||
reviewList: [], // 酒评列表
|
||
reviewScoreList: {}, // 酒评评分
|
||
reviewTotal: 0, // 酒评总数
|
||
queryForm: {
|
||
beerId: '',
|
||
pageNum: 1,
|
||
pageSize: 10,
|
||
sortType: 'time' // 添加排序类型参数
|
||
},
|
||
sortType: 'time',
|
||
hasMore: true,
|
||
averageScore: 0,
|
||
totalReviews: 124,
|
||
ratingDistribution: [],
|
||
ratingCounts: {
|
||
five: 0,
|
||
four: 0,
|
||
three: 0,
|
||
two: 0,
|
||
one: 0
|
||
}
|
||
};
|
||
},
|
||
onLoad({beerId}) {
|
||
if (!beerId) {
|
||
uni.showToast({
|
||
title: '参数错误',
|
||
icon: 'none'
|
||
});
|
||
setTimeout(() => {
|
||
uni.navigateBack();
|
||
}, 1500);
|
||
return;
|
||
}
|
||
this.beerId = beerId;
|
||
this.queryForm.beerId = beerId;
|
||
this.initPageData();
|
||
},
|
||
onReachBottom() {
|
||
if (this.hasMore) {
|
||
this.queryForm.pageNum++;
|
||
this.getReviewListFun();
|
||
}
|
||
},
|
||
methods: {
|
||
// 初始化页面数据
|
||
async initPageData() {
|
||
await this.getReviewScoreListFun(); // 先获取评分统计
|
||
await this.getReviewListFun(); // 再获取评论列表
|
||
},
|
||
// 获取酒评列表
|
||
async getReviewListFun() {
|
||
uni.showLoading({
|
||
title: '加载中'
|
||
});
|
||
try {
|
||
const res = await getReviewList(this.queryForm);
|
||
if (res.rows) {
|
||
// 重置评分计数
|
||
this.ratingCounts = {
|
||
five: 0,
|
||
four: 0,
|
||
three: 0,
|
||
two: 0,
|
||
one: 0
|
||
};
|
||
|
||
// 统计各个评分的数量
|
||
res.rows.forEach(review => {
|
||
const rating = Math.round(review.overallRating); // 四舍五入到整数
|
||
switch(rating) {
|
||
case 5:
|
||
this.ratingCounts.five++;
|
||
break;
|
||
case 4:
|
||
this.ratingCounts.four++;
|
||
break;
|
||
case 3:
|
||
this.ratingCounts.three++;
|
||
break;
|
||
case 2:
|
||
this.ratingCounts.two++;
|
||
break;
|
||
case 1:
|
||
this.ratingCounts.one++;
|
||
break;
|
||
}
|
||
});
|
||
|
||
// 计算平均分
|
||
let totalScore = 0;
|
||
let totalCount = 0;
|
||
Object.entries(this.ratingCounts).forEach(([key, count]) => {
|
||
const score = key === 'five' ? 5 :
|
||
key === 'four' ? 4 :
|
||
key === 'three' ? 3 :
|
||
key === 'two' ? 2 : 1;
|
||
totalScore += score * count;
|
||
totalCount += count;
|
||
});
|
||
this.averageScore = totalCount > 0 ?
|
||
Math.round((totalScore / totalCount) * 10) / 10 :
|
||
'0.0';
|
||
this.totalReviews = res.total || 0;
|
||
|
||
// 更新评分分布数据
|
||
this.ratingDistribution = [
|
||
{
|
||
percentage: this.calculatePercentage(this.ratingCounts.five, res.total)
|
||
},
|
||
{
|
||
percentage: this.calculatePercentage(this.ratingCounts.four, res.total)
|
||
},
|
||
{
|
||
percentage: this.calculatePercentage(this.ratingCounts.three, res.total)
|
||
},
|
||
{
|
||
percentage: this.calculatePercentage(this.ratingCounts.two, res.total)
|
||
},
|
||
{
|
||
percentage: this.calculatePercentage(this.ratingCounts.one, res.total)
|
||
}
|
||
];
|
||
|
||
// 处理评论列表数据
|
||
const arr = res.rows.map(it => ({
|
||
...it,
|
||
compositeScore: this.getScore(it.aromaRating, it.tasteRating, it.colorRating)
|
||
}));
|
||
|
||
if (this.queryForm.pageNum === 1) {
|
||
this.reviewList = arr;
|
||
} else {
|
||
this.reviewList = [...this.reviewList, ...arr];
|
||
}
|
||
this.reviewTotal = res.total;
|
||
this.hasMore = this.reviewList.length < this.reviewTotal;
|
||
}
|
||
} catch (err) {
|
||
console.error('获取评论列表失败:', err);
|
||
uni.showToast({
|
||
title: '获取评论失败',
|
||
icon: 'none'
|
||
});
|
||
} finally {
|
||
uni.hideLoading();
|
||
}
|
||
},
|
||
// 获取酒评评分列表
|
||
async getReviewScoreListFun() {
|
||
try {
|
||
const res = await getReviewScoreList(this.beerId);
|
||
if (res.data) {
|
||
this.reviewScoreList = res.data;
|
||
this.averageScore = res.data.avgOverallRating?.toFixed(2) || '0.00';
|
||
|
||
// 构建评分分布数据
|
||
this.ratingDistribution = [
|
||
{ percentage: this.calculatePercentage(res.data.fiveStarCount) },
|
||
{ percentage: this.calculatePercentage(res.data.fourStarCount) },
|
||
{ percentage: this.calculatePercentage(res.data.threeStarCount) },
|
||
{ percentage: this.calculatePercentage(res.data.twoStarCount) },
|
||
{ percentage: this.calculatePercentage(res.data.oneStarCount) }
|
||
];
|
||
}
|
||
} catch (err) {
|
||
console.error('获取评分统计失败:', err);
|
||
uni.showToast({
|
||
title: '获取评分统计失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
// 计算评分百分比
|
||
calculatePercentage(count, total) {
|
||
if (!total) return 0;
|
||
return Math.round((count / total) * 100);
|
||
},
|
||
// 计算综合评分
|
||
getScore(a, b, c) {
|
||
if (!a && !b && !c) return '0.0';
|
||
const score = ((a || 0) + (b || 0) + (c || 0)) / 3;
|
||
return score.toFixed(1);
|
||
},
|
||
// 点赞
|
||
handleLike(item) {
|
||
const token = uni.getStorageSync('token')
|
||
if (!token) {
|
||
this.$refs.loginRef.open()
|
||
return
|
||
}
|
||
let data = {
|
||
reviewId: item.id,
|
||
status: item.reviewLike ? 2 : 1
|
||
}
|
||
likeReview(data).then(res => {
|
||
if (data.status == 1) {
|
||
uni.showToast({
|
||
title: '点赞成功',
|
||
icon: 'none'
|
||
})
|
||
item.reviewLike = true
|
||
item.likeCount = (item.likeCount || 0) + 1
|
||
} else {
|
||
uni.showToast({
|
||
title: '取消点赞',
|
||
icon: 'none'
|
||
})
|
||
item.reviewLike = false
|
||
item.likeCount = Math.max(0, (item.likeCount || 1) - 1)
|
||
}
|
||
})
|
||
},
|
||
// 预览图片
|
||
previewImage(imgs, index) {
|
||
uni.previewImage({
|
||
urls: imgs,
|
||
current: index
|
||
})
|
||
},
|
||
// 切换排序方式
|
||
changeSortType(type) {
|
||
if (this.sortType === type) return;
|
||
this.sortType = type;
|
||
this.queryForm.pageNum = 1;
|
||
this.reviewList = [];
|
||
this.getReviewListFun();
|
||
},
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* 页面容器 */
|
||
.page {
|
||
min-height: 100vh;
|
||
background-color: #F8F9FA;
|
||
padding-bottom: 40rpx;
|
||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Roboto, "Segoe UI", "Microsoft YaHei", sans-serif;
|
||
}
|
||
|
||
/* 评分统计 */
|
||
.rating-stats {
|
||
margin: 24rpx;
|
||
padding: 32rpx;
|
||
background: #fff;
|
||
border-radius: 16rpx;
|
||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||
|
||
.rating-content {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 40rpx;
|
||
|
||
.rating-distribution {
|
||
flex: 1;
|
||
min-width: 0;
|
||
|
||
.rating-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 16rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.rating-label {
|
||
width: 60rpx;
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.bar-wrapper {
|
||
flex: 1;
|
||
height: 12rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 6rpx;
|
||
margin-left: 16rpx;
|
||
overflow: hidden;
|
||
|
||
.bar-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #FFE034 0%, #FFD234 100%);
|
||
border-radius: 6rpx;
|
||
transition: width 0.3s ease;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.rating-summary {
|
||
text-align: center;
|
||
|
||
.total-score {
|
||
display: flex;
|
||
align-items: baseline;
|
||
white-space: nowrap;
|
||
|
||
.score {
|
||
font-size: 64rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
line-height: 1;
|
||
}
|
||
|
||
.max-score {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
margin-left: 4rpx;
|
||
}
|
||
}
|
||
|
||
.total-reviews {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
margin-top: 8rpx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 排序选项 */
|
||
.sort-options {
|
||
display: flex;
|
||
padding: 0 24rpx;
|
||
margin-bottom: 16rpx;
|
||
|
||
.sort-item {
|
||
padding: 12rpx 32rpx;
|
||
margin-right: 16rpx;
|
||
border-radius: 28rpx;
|
||
background: #F5F5F5;
|
||
transition: all 0.3s ease;
|
||
|
||
text {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
&.active {
|
||
background: #4E63E0;
|
||
|
||
text {
|
||
color: #FFFFFF;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 评价列表 */
|
||
.review-list {
|
||
padding: 0 24rpx;
|
||
|
||
.review-item {
|
||
margin-bottom: 24rpx;
|
||
padding: 24rpx;
|
||
background: #FFFFFF;
|
||
border-radius: 16rpx;
|
||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||
|
||
.review-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 20rpx;
|
||
|
||
.user-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
|
||
.avatar {
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
border-radius: 50%;
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.user-meta {
|
||
.user-name {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.rating-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
|
||
.score-text {
|
||
font-size: 24rpx;
|
||
color: #FFB800;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.review-time {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.review-content {
|
||
.review-text {
|
||
font-size: 28rpx;
|
||
line-height: 1.6;
|
||
color: #333;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.image-scroll {
|
||
margin-bottom: 16rpx;
|
||
|
||
.image-container {
|
||
display: flex;
|
||
gap: 12rpx;
|
||
|
||
.review-image {
|
||
width: 180rpx;
|
||
height: 180rpx;
|
||
border-radius: 8rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.rating-details {
|
||
padding: 16rpx;
|
||
background: #F8F9FA;
|
||
border-radius: 12rpx;
|
||
margin-bottom: 16rpx;
|
||
|
||
.rating-item {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 12rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.label {
|
||
width: 64rpx;
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.rating-value {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
margin-left: 12rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.review-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
|
||
.like-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 8rpx 16rpx;
|
||
gap: 8rpx;
|
||
border-radius: 24rpx;
|
||
background: #F5F5F5;
|
||
|
||
.like-icon {
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
}
|
||
|
||
.like-count {
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
}
|
||
|
||
&.active {
|
||
background: rgba(255, 184, 0, 0.1);
|
||
|
||
.like-count {
|
||
color: #FFB800;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.no-review {
|
||
text-align: center;
|
||
padding: 48rpx 0;
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
background: #FFFFFF;
|
||
border-radius: 16rpx;
|
||
margin: 24rpx;
|
||
}
|
||
|
||
.load-more {
|
||
text-align: center;
|
||
padding: 24rpx 0;
|
||
|
||
.load-text {
|
||
font-size: 26rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
</style> |