优化酒款详细页面和酒评页面

This commit is contained in:
davy 2025-04-07 00:15:19 +08:00
parent 9a40cea7fa
commit a069a9e7cc
8 changed files with 1395 additions and 481 deletions

View File

@ -1,15 +0,0 @@
a58ff14 (HEAD -> devs) HEAD@{0}: reset: moving to a58ff14
6d203c0 HEAD@{1}: reset: moving to HEAD
6d203c0 HEAD@{2}: commit: 更新啤酒评分写酒评页面UI交互方式
eb39ad8 (origin/devs) HEAD@{3}: commit: feat: 1. 优化啤酒币页面登录验证 2. 添加商家入驻协议页面 3. 添加联系二维码
94415fb HEAD@{4}: commit: feat: 登陆弹窗优化,添加商家入驻协议页面,登录页面优化,添加添加企业微信二维码,啤酒币页面逻辑判定。
30c802c HEAD@{5}: commit: 更新1. 优化注册页面逻辑 2. 更新API请求配置
9c4f90a HEAD@{6}: commit: feat: 优化新酒页面布局,配置啤啤猩球对接
e5441ad HEAD@{7}: reset: moving to origin/devs
e5441ad HEAD@{8}: commit: feat: optimize pages style and functionality
a58ff14 (HEAD -> devs) HEAD@{9}: commit: feat: optimize activity list page
d53c847 HEAD@{10}: commit: fix: 优化搜索栏和筛选栏的布局修复ui样式问题
3081fcd HEAD@{11}: commit: feat: 优化首页搜索功能交互,添加默认头像
05d27af HEAD@{12}: reset: moving to origin/devs
5a3853f (master) HEAD@{13}: checkout: moving from master to devs
5a3853f (master) HEAD@{14}: commit (initial): 初始化提交:完成活动列表页面功能

View File

@ -177,6 +177,14 @@
"navigationBarBackgroundColor": "#19367A", "navigationBarBackgroundColor": "#19367A",
"navigationBarTextStyle": "white" "navigationBarTextStyle": "white"
} }
},
{
"path" : "pages/index/allReviews",
"style" : {
"navigationBarTitleText" : "全部酒评",
"navigationBarBackgroundColor": "#19367A",
"navigationBarTextStyle": "white"
}
} }

661
pages/index/allReviews.vue Normal file
View File

@ -0,0 +1,661 @@
<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>

File diff suppressed because it is too large Load Diff

BIN
static/heart-add.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
static/heart-tick.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
static/like.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

35
tatus
View File

@ -1,35 +0,0 @@
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
Commands marked with * may be preceded by a number, _N.
Notes in parentheses indicate the behavior if _N is given.
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
h H Display this help.
q :q Q :Q ZZ Exit.
---------------------------------------------------------------------------
MMOOVVIINNGG
e ^E j ^N CR * Forward one line (or _N lines).
y ^Y k ^K ^P * Backward one line (or _N lines).
f ^F ^V SPACE * Forward one window (or _N lines).
b ^B ESC-v * Backward one window (or _N lines).
z * Forward one window (and set window to _N).
w * Backward one window (and set window to _N).
ESC-SPACE * Forward one window, but don't stop at end-of-file.
d ^D * Forward one half-window (and set half-window to _N).
u ^U * Backward one half-window (and set half-window to _N).
ESC-) RightArrow * Right one half screen width (or _N positions).
ESC-( LeftArrow * Left one half screen width (or _N positions).
ESC-} ^RightArrow Right to last column displayed.
ESC-{ ^LeftArrow Left to first column.
F Forward forever; like "tail -f".
ESC-F Like F but stop when search pattern is found.
r ^R ^L Repaint screen.
R Repaint screen, discarding buffered input.
---------------------------------------------------
Default "window" is the screen height.
Default "half-window" is half of the screen height.
---------------------------------------------------------------------------