661 lines
15 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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