661 lines
15 KiB
Vue
Raw Permalink Normal View History

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