1405 lines
33 KiB
Vue
Raw 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="product-card">
<!-- 商品头部 -->
<view class="product-header">
<image class="product-image" :src="beerInfo.cover" mode="aspectFill"></image>
<view class="product-info">
<view class="title-row">
<text class="product-title">{{beerInfo.beerName || '--'}}</text>
<image v-if="!isFavor" class="like-btn" src="/static/heart-add.png" mode="aspectFit" @click="favorBeerFun(1)"></image>
<image v-else class="like-btn" src="/static/heart-tick.png" mode="aspectFit" @click="favorBeerFun(2)"></image>
</view>
<text class="product-subtitle">{{beerInfo.beerStyles || '--'}}</text>
<view class="rating">
<text class="rating-score">{{reviewScoreList.avgOverallRating ? Number(reviewScoreList.avgOverallRating).toFixed(1) : '0.0'}}/5.0</text>
<uni-rate :value="reviewScoreList.avgOverallRating || 0" :readonly="true" :touchable="false" size="18" />
</view>
<view v-if="beerInfo.breweryId" class="brand" @click="toBrand">
<image class="brand-logo" :src="beerInfo.brandLogo" mode="aspectFill"></image>
<text class="brand-name">{{beerInfo.brandName || ''}}</text>
<image class="arrow-icon" src="/static/arrow-right.png" mode="aspectFit"></image>
</view>
</view>
</view>
<!-- 商品参数 -->
<view class="product-params">
<view class="param-item">
<text class="param-label">ABV</text>
<text class="param-value">≈{{beerInfo.beerAbv || '--'}}</text>
</view>
<view class="param-item">
<text class="param-label">IBU</text>
<text class="param-value">{{beerInfo.beerIbus || '--'}}</text>
</view>
<view class="param-item">
<text class="param-label">原麦汁</text>
<text class="param-value">{{beerInfo.onSale == 1 ? '在售款' : '停售'}}</text>
</view>
</view>
<!-- 商品描述 -->
<view class="product-description">
<rich-text :nodes="beerInfo.desc"></rich-text>
</view>
</view>
<!-- 评分和评价 -->
<view class="rating-section">
<view class="section-title">评分和评价</view>
<!-- 评分总览 -->
<view class="rating-overview">
<text class="rating-score-large">{{currentTab === 2 ? (reviewScoreList.avgOverallRating ? Number(reviewScoreList.avgOverallRating).toFixed(1) : '-') : (myReviewInfo ? Number(myReviewInfo.overallRating).toFixed(1) : '-')}}</text>
<view class="rating-right">
<uni-rate
:value="currentTab === 2 ? (reviewScoreList.avgOverallRating || 0) : (myReviewInfo ? myReviewInfo.overallRating : 0)"
:readonly="true"
:size="24"
:touchable="false"
color="#ECECEC"
active-color="#FEE034"
/>
<view class="rating-count" :class="{ highlight: currentTab === 1 && !myReviewInfo }">
<template v-if="currentTab === 2">
({{reviewTotal}}条)
</template>
<template v-else>
<template v-if="myReviewInfo">
{{myReviewInfo.createTime ? '评分于 ' + myReviewInfo.createTime.slice(0,10) : '我的评分'}}
</template>
<template v-else>
首次评分还可以领取品牌啤酒币哦
</template>
</template>
</view>
</view>
</view>
<!-- 添加评分切换按钮 -->
<view class="rating-tabs">
<view class="tab-item" :class="{ active: currentTab === 2 }" @click="currentTab = 2">
<image class="tab-icon" src="/static/users.png" mode="aspectFit"></image>
</view>
<view class="tab-item" :class="{ active: currentTab === 1 }" @click="currentTab = 1">
<image class="tab-icon" src="/static/user.png" mode="aspectFit"></image>
</view>
</view>
<!-- 评分条区域 -->
<view class="rating-bars">
<view class="rating-bar" v-for="(item, index) in [
{label: '外观', key: 'avgColorRating', weight: '10%',
value: currentTab === 2 ? reviewScoreList.avgColorRating : (myReviewInfo ? myReviewInfo.colorRating : 0)},
{label: '香气', key: 'avgAromaRating', weight: '30%',
value: currentTab === 2 ? reviewScoreList.avgAromaRating : (myReviewInfo ? myReviewInfo.aromaRating : 0)},
{label: '口感', key: 'avgTasteRating', weight: '60%',
value: currentTab === 2 ? reviewScoreList.avgTasteRating : (myReviewInfo ? myReviewInfo.tasteRating : 0)}
]" :key="index">
<view class="bar-header">
<text class="bar-label">{{item.label}} ({{item.weight}})</text>
<text class="bar-value">{{item.value ? Number(item.value).toFixed(1) : '0.0'}}</text>
</view>
<view class="bar">
<view class="bar-filled" :style="{width: item.value ? (item.value/5*100 + '%') : '0%'}"></view>
</view>
</view>
</view>
<!-- 评价排序按钮 -->
<view class="review-sort">
<view class="sort-item" :class="{ active: sortType === 'latest' }" @click="sortType = 'latest'">
<text>最新</text>
</view>
<view class="sort-item" :class="{ active: sortType === 'hot' }" @click="sortType = 'hot'">
<text>最热</text>
</view>
</view>
<!-- 评论列表 -->
<view class="reviews" v-if="reviewList.length > 0">
<view class="review-item" v-for="(item, index) in reviewList.slice(0, 3)" :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>
<text class="review-time">{{item.createTime.slice(0,10)}}</text>
</view>
</view>
</view>
<!-- 评论内容区域 -->
<view class="review-content">
<view class="content-left">
<!-- 评论文本 -->
<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="review-footer">
<uni-rate
:value="item.overallRating"
:readonly="true"
:size="16"
:touchable="false"
color="#ECECEC"
active-color="#FEE034"
/>
<text class="score-text">{{item.overallRating ? Number(item.overallRating).toFixed(1) : '0.0'}}</text>
<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>
</view>
<view v-else class="no-review">巧了还没有酒评呢快拿下首评</view>
<!-- 查看全部评价按钮 -->
<view v-if="reviewList.length > 0"
class="view-all-reviews"
:class="{ 'disabled': reviewList.length === 0 }"
@tap="toAllReviews">
查看全部评价
</view>
</view>
<!-- 底部操作栏 -->
<view class="bottom-bar">
<view class="action-box">
<view class="action-item" @click="createPic">
<image class="action-icon" src="@/static/share.png" mode="aspectFit"></image>
<text class="action-text">酒款分享</text>
</view>
<view class="action-item" @click="toWinelist">
<image class="action-icon" src="@/static/map.png" mode="aspectFit"></image>
<text class="action-text">生成酒单</text>
</view>
<view class="write-review" @click="toWrite">
{{ myReviewInfo ? '修改评分' : '为这款酒评分' }}
</view>
</view>
</view>
<!-- 组件 -->
<loginPopup ref="loginRef" @loginSuccess="loginSuccess"></loginPopup>
<createPoster v-if="showShare" ref="createPosterRef" :url="tempUrl" @close="showShare=false"></createPoster>
<canvas type="2d" id="myCanvas" style="width: 360px;height: 570px;position: fixed;left:8888px"></canvas>
</view>
</template>
<script>
import {
getReviewInfo,
getReviewList,
getReviewScoreList,
getMyReviewInfo,
getBeerInfo,
favorBeer,
likeReview,
getBeerFavorStatus
} from '@/api/bar.js'
import loginPopup from '@/components/loginPopup.vue';
import createPoster from '@/components/createPoster.vue'
export default {
components: {
loginPopup,
createPoster
},
data() {
return {
tempUrl: '',
showShare: false,
beerId: '',
beerInfo: {}, // 酒款信息
reviewInfo: {},
reviewList: [], // 酒评列表
reviewScoreList: [], // 酒评评分
myReviewInfo: null,
currentTab: 2,
reviewTotal: 0, // 酒评总数
queryForm: {
beerId:'',
pageNum: 1,
pageSize: 5,
},
isFavor: false, // 是否收藏
sortType: 'latest',
isLoggedIn: false,
isBarAuthenticated: false,
ratingAnimation: null,
isAnimating: false,
lastRefreshTime: 0,
refreshDebounceTime: 1000,
loading: false,
needRefresh: false // 添加标记,用于标识是否需要刷新
};
},
onLoad({beerId}) {
this.beerId = beerId
this.queryForm.beerId = beerId
this.initPageData()
},
onShow() {
this.checkLoginStatus()
this.initPageData()
},
methods: {
// 检查登录状态
checkLoginStatus() {
const token = uni.getStorageSync('token')
const wasLoggedIn = this.isLoggedIn
this.isLoggedIn = !!token
if (this.isLoggedIn) {
const barInfo = uni.getStorageSync('barInfo')
this.isBarAuthenticated = barInfo && barInfo.authState === 1 // 1表示认证通过
// 如果之前未登录,现在登录了,标记需要刷新
if (!wasLoggedIn) {
this.needRefresh = true
}
} else {
this.isBarAuthenticated = false
}
},
// 初始化页面数据
async initPageData() {
const now = Date.now()
if (now - this.lastRefreshTime < this.refreshDebounceTime && !this.needRefresh) {
return
}
this.lastRefreshTime = now
this.loading = true
this.needRefresh = false // 重置刷新标记
try {
// 重置数据
this.reviewList = []
this.queryForm.pageNum = 1
// 获取基础数据
const [beerInfoRes, reviewListRes, reviewScoreListRes] = await Promise.all([
getBeerInfo(this.beerId),
getReviewList(this.queryForm),
getReviewScoreList(this.beerId)
])
// 更新数据
this.beerInfo = beerInfoRes.data
if (reviewListRes.rows) {
this.reviewList = reviewListRes.rows.map(it => ({
...it,
isExpanded: false,
needExpand: it.reviewContent && it.reviewContent.length > 50
}))
this.reviewTotal = reviewListRes.total
}
this.reviewScoreList = reviewScoreListRes.data
// 获取需要登录的数据
if (this.isLoggedIn) {
const [favorStatusRes, myReviewInfoRes] = await Promise.all([
getBeerFavorStatus(this.beerId),
getMyReviewInfo(this.beerId)
])
this.isFavor = !!favorStatusRes.data
this.myReviewInfo = myReviewInfoRes.data
}
// 滚动到顶部
uni.pageScrollTo({
scrollTop: 0,
duration: 0
})
} catch (error) {
console.error('初始化数据失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
// 获取酒款收藏状态
async getBeerFavorStatusFun() {
try {
if (!this.isLoggedIn) {
this.isFavor = false
return
}
const res = await getBeerFavorStatus(this.beerId)
this.isFavor = !!res.data
} catch (error) {
console.error('获取收藏状态失败:', error)
this.isFavor = false
}
},
// 获取酒款信息
getBeerInfoFun() {
getBeerInfo(this.beerId).then(res => {
this.beerInfo = res.data
})
},
// 获取酒评信息
getReviewInfoFun() {
getReviewInfo(this.beerId).then(res => {
this.reviewInfo = res.data
})
},
// 获取酒评列表
getReviewListFun() {
getReviewList(this.queryForm).then(res => {
if (res.rows) {
console.log('获取评论列表数据:', res.rows.length)
let arr = res.rows.map(it => ({
...it,
isExpanded: false,
needExpand: it.reviewContent && it.reviewContent.length > 50
}))
if (arr.length > 0) {
console.log('更新评论列表')
// 如果是第一页,直接替换列表;否则追加到列表末尾
if (this.queryForm.pageNum === 1) {
this.reviewList = arr
} else {
this.reviewList = [...this.reviewList, ...arr]
}
}
}
this.reviewTotal = res.total
})
},
// 酒评列表分页
async reviewPageChange() {
if (this.loading) return
this.loading = true
try {
this.queryForm.pageNum++
const res = await getReviewList(this.queryForm)
if (res.rows) {
const newReviews = res.rows.map(it => ({
...it,
isExpanded: false,
needExpand: it.reviewContent && it.reviewContent.length > 50
}))
this.reviewList = [...this.reviewList, ...newReviews]
}
} catch (error) {
console.error('加载更多评论失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
// 获取酒评评分列表
getReviewScoreListFun() {
getReviewScoreList(this.beerId).then(res => {
this.reviewScoreList = res.data
})
},
// 获取我的酒评信息
getMyReviewInfoFun() {
getMyReviewInfo(this.beerId).then(res => {
this.myReviewInfo = res.data
})
},
// 写酒评
toWrite() {
if (!this.isLoggedIn) {
this.$refs.loginRef.open()
return
}
if (!this.isBarAuthenticated) {
const barInfo = uni.getStorageSync('barInfo')
if (!barInfo || barInfo.authState === 2) {
uni.showModal({
title: '提示',
content: '请先完成门店认证',
showCancel: true,
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/index/registration'
})
}
}
})
return
}
if (barInfo.authState === 1) {
// 已认证,允许写酒评
uni.navigateTo({
url: '/pages/index/writeReview?beerId=' + this.beerId
})
return
}
}
uni.navigateTo({
url: '/pages/index/writeReview?beerId=' + this.beerId
})
},
// 计算综合评分
getScore(a, b, c) {
let score = 0
score = (a + b + c) / 3
return score.toFixed(0)
},
// 跳转 品牌方
toBrand() {
if (!this.beerInfo.breweryId) return
uni.navigateTo({
url: '/pages/index/brandHome?breweryId=' + this.beerInfo.breweryId
})
},
// 收藏酒款
async favorBeerFun(status) {
const token = uni.getStorageSync('token')
if (!token) {
this.$refs.loginRef.open()
return
}
// 检查认证状态
const barInfo = uni.getStorageSync('barInfo')
if (!barInfo) {
uni.showModal({
title: '提示',
content: '请先认证门店',
showCancel: true,
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/index/registration'
})
}
}
})
return
}
// 处理不同的认证状态
if (barInfo.authState === 2) {
uni.showModal({
title: '提示',
content: '请先完成门店认证',
showCancel: true,
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/index/registration'
})
}
}
})
return
}
try {
await favorBeer({
beerId: this.beerId,
status
})
this.isFavor = status === 1
uni.showToast({
title: status === 1 ? '收藏成功' : '取消收藏',
icon: status === 1 ? 'success' : 'none'
})
} catch (error) {
console.error('收藏操作失败:', error)
uni.showToast({
title: error.msg || '操作失败',
icon: 'none'
})
}
},
// 处理点赞
async handleLike(item) {
if (!this.isLoggedIn) {
this.$refs.loginRef.open()
return
}
try {
// 发送点赞请求前,先确定当前的点赞状态
const nextStatus = item.reviewLike ? 2 : 1 // 如果当前是已点赞,则发送取消点赞(2);否则发送点赞(1)
const res = await likeReview({
reviewId: item.id,
status: nextStatus
})
if (res.code === 200) {
// 根据发送的状态更新UI
if (nextStatus === 1) { // 点赞操作
item.reviewLike = true
item.likeCount = (item.likeCount || 0) + 1
} else { // 取消点赞操作
item.reviewLike = false
item.likeCount = Math.max(0, (item.likeCount || 1) - 1)
}
uni.showToast({
title: nextStatus === 1 ? '点赞成功' : '取消点赞',
icon: 'none'
})
} else {
uni.showToast({
title: '操作失败,请重试',
icon: 'none'
})
}
} catch (error) {
console.error('点赞操作失败:', error)
uni.showToast({
title: '操作失败,请重试',
icon: 'none'
})
}
},
// 生成酒单
toWinelist() {
const token = uni.getStorageSync('token')
if (!token) {
this.$refs.loginRef.open()
return
}
// 检查认证状态
const barInfo = uni.getStorageSync('barInfo')
if (!barInfo) {
uni.showModal({
title: '提示',
content: '请先认证门店',
showCancel: true,
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/index/registration'
})
}
}
})
return
}
// 处理不同的认证状态
if (barInfo.authState === 2) {
uni.showModal({
title: '提示',
content: '您的门店正在认证中,请耐心等待',
showCancel: true,
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/index/registration'
})
}
}
})
return
} else if (barInfo.authState === 1) {
// 已认证,允许生成酒单
uni.navigateTo({
url: "/pagesActivity/winelist?beerId=" + this.beerId
})
return
}
// 未认证状态
uni.showModal({
title: '提示',
content: '请先认证门店',
showCancel: true,
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/index/registration'
})
}
}
})
},
// 分享
createPic() {
uni.showLoading({
title: '加载中'
})
let _this = this
// 获取的画布
uni.createSelectorQuery().select('#myCanvas').fields({
node: true,
size: true
}).exec((res) => {
const ctx = res[0].node.getContext('2d')
const canvas = res[0].node
const dpr = uni.getSystemInfoSync().pixelRatio
// const dpr = uni.getDeviceInfo().devicePixelRatio
// const dpr = uni.getSystemSetting().canvasToTempFilePath()
// 设置画布的宽高
canvas.width = res[0].width * dpr
canvas.height = res[0].height * dpr
ctx.scale(dpr, dpr)
let imageCover = canvas.createImage()
imageCover.src = this.beerInfo.cover
imageCover.onload = () => {
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(imageCover, 0, 0, 360, 460)
// 绘制文字
ctx.font = `12px Arial`
ctx.fillStyle = '#0B0E26'
ctx.fillText(this.beerInfo.brandName, 20, 470)
ctx.font = `12px Arial`
ctx.fillStyle = '#0B0E26'
ctx.fillText(this.beerInfo.beerName, 20, 490)
ctx.font = `12px Arial`
ctx.fillStyle = '#5E5F60'
ctx.fillText(this.beerInfo.beerStyles, 20, 510)
// ctx.font = `${this.els.text3.fontSize}px Arial`
// ctx.fillStyle = this.els.text3.color
// ctx.fillText(this.els.text3.value, this.els.text3.x, this.els.text3.y)
// ctx.font = `${this.els.text4.fontSize}px Arial`
// ctx.fillStyle = this.els.text4.color
// ctx.fillText(this.els.text4.value, this.els.text4.x, this.els.text4.y)
// 保存图片
uni.canvasToTempFilePath({
canvas: canvas,
success: (res) => {
_this.tempUrl = res.tempFilePath
this.showShare = true
setTimeout(() => {
uni.hideLoading()
this.$refs.createPosterRef.open()
}, 1000)
},
fail: (err) => {
console.log(err)
uni.hideLoading()
},
})
}
})
},
// 预览图片
previewImage(imgs, index) {
// 实现图片预览逻辑
console.log('Previewing image:', imgs[index])
},
// 跳转到全部评价页面
toAllReviews() {
uni.navigateTo({
url: '/pages/index/allReviews?beerId=' + this.beerId
})
},
// 处理评分变化时的动画效果
handleRatingChange(newValue, type) {
if (this.isAnimating) return;
this.isAnimating = true;
// 创建动画实例
const animation = uni.createAnimation({
duration: 300,
timingFunction: 'ease'
});
// 为评分值添加缩放动画
animation.scale(1.2).step();
animation.scale(1.0).step();
this.ratingAnimation = animation.export();
// 添加触感反馈
if (uni.vibrateShort) {
uni.vibrateShort();
}
// 300ms后重置动画状态
setTimeout(() => {
this.isAnimating = false;
}, 300);
},
// 切换标签页时的动画效果
handleTabChange(tab) {
if (this.currentTab === tab) return;
// 添加触感反馈
if (uni.vibrateShort) {
uni.vibrateShort();
}
this.currentTab = tab;
},
// 登录成功回调
async loginSuccess() {
console.log('登录成功,开始刷新页面数据')
// 重置状态
this.needRefresh = true
this.loading = true
try {
// 检查登录状态
await this.checkLoginStatus()
// 如果已登录,刷新页面数据
if (this.isLoggedIn) {
// 重置数据
this.reviewList = []
this.queryForm.pageNum = 1
// 获取新数据
await this.initPageData()
// 显示成功提示
uni.showToast({
title: '登录成功',
icon: 'success'
})
}
} catch (error) {
console.error('登录后刷新数据失败:', error)
uni.showToast({
title: '刷新数据失败',
icon: 'none'
})
} finally {
this.loading = false
}
}
}
}
</script>
<style lang="scss" scoped>
/* 页面容器 */
.page {
min-height: 100vh;
background-color: #F8F9FA;
padding-bottom: 220rpx;
font-family:-apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Roboto, "Segoe UI", "Microsoft YaHei", sans-serif;
margin-top: -24rpx;
}
/* 商品信息卡片 */
.product-card {
background: #fff;
margin: 24rpx;
padding: 32rpx;
border-radius: 24rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06);
.product-header {
display: flex;
padding: 0;
.product-image {
width: 242rpx;
height: 342rpx;
border-radius: 16rpx;
margin-right: 32rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.12);
}
.product-info {
flex: 1;
padding-top: 10rpx;
.title-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16rpx;
.product-title {
flex: 1;
font-size: 40rpx;
font-weight: bold;
color: #0B0E26;
margin-right: 20rpx;
}
.like-btn {
margin-top: 4rpx;
width: 48rpx;
height: 48rpx;
padding: 4rpx;
}
}
.product-subtitle {
font-size: 28rpx;
color: #606060;
margin: 10rpx 0;
}
.rating {
display: flex;
align-items: center;
gap: 16rpx;
margin: 20rpx 0;
.rating-score {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
}
.brand {
display: flex;
align-items: center;
padding: 20rpx;
background: linear-gradient(to right, #f9f9f9, #ffffff);
border-radius: 16rpx;
margin-top: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
.brand-logo {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
margin-right: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.brand-name {
flex: 1;
font-size: 28rpx;
color: #0B0E26;
}
.arrow-icon {
width: 32rpx;
height: 32rpx;
opacity: 0.6;
}
}
}
}
.product-params {
display: flex;
justify-content: space-between;
padding: 0;
margin: 32rpx 0;
.param-item {
width: 200rpx;
height: 180rpx;
background: linear-gradient(135deg, rgba(245, 197, 24, 0.1), rgba(245, 197, 24, 0.05));
border-radius: 16rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12rpx;
box-shadow: 0 4rpx 16rpx rgba(245, 197, 24, 0.1);
.param-label {
font-size: 32rpx;
font-weight: 600;
color: #5F5F63;
}
.param-value {
font-size: 32rpx;
font-weight: bold;
color: #FDCA40;
}
}
}
.product-description {
padding: 0;
font-size: 28rpx;
font-weight: normal;
line-height: 1.6;
color: #A1A1A1;
margin-bottom: 32rpx;
}
}
/* 评分区域 */
.rating-section {
background-color: #fff;
margin: 24rpx;
padding: 32rpx;
border-radius: 24rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06);
.section-title {
font-size: 40rpx;
font-weight: bold;
line-height: 130%;
color: #030303;
margin-bottom: 36rpx;
}
.rating-tabs {
display: flex;
align-items: center;
justify-content: flex-end;
margin: 56rpx 0;
width: 100%;
.tab-item {
width: 120rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
background: #F9F9F9;
position: relative;
&:first-child {
border-radius: 12rpx 0 0 12rpx;
}
&:last-child {
border-radius: 0 12rpx 12rpx 0;
}
.tab-icon {
width: 60rpx;
height: 40rpx;
opacity: 0.6;
}
&.active {
background: #19367A;
box-shadow: 0 4rpx 12rpx rgba(25, 54, 122, 0.2);
.tab-icon {
opacity: 1;
filter: brightness(0) invert(1);
}
}
}
}
.rating-overview {
display: flex;
align-items: center;
padding-left: 80rpx;
gap: 30rpx;
margin-bottom: 40rpx;
.rating-score-large {
font-size: 104rpx;
font-weight: bold;
color: #030303;
line-height: 1;
min-width: 120rpx;
text-align: center;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.rating-right {
display: flex;
flex-direction: column;
margin-left: 12rpx;
gap: 10rpx;
min-height: 80rpx;
}
.rating-count {
font-size: 24rpx;
margin-left: 12rpx;
color: #606060;
white-space: nowrap;
min-height: 24rpx;
&.highlight {
color: #FDCA40;
font-weight: 500;
}
}
}
.rating-bars {
margin: 32rpx 0;
.rating-bar {
margin-bottom: 16rpx;
.bar-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8rpx;
.bar-label {
font-size: 24rpx;
color: #606060;
}
.bar-value {
font-size: 32rpx;
color: #030303;
font-weight: 600;
}
}
.bar {
width: 100%;
height: 46rpx;
background: #F9F9F9;
border-radius: 23rpx;
overflow: hidden;
box-shadow: inset 0 2rpx 4rpx rgba(0, 0, 0, 0.05);
.bar-filled {
height: 100%;
background: linear-gradient(to right, #FDCA40, #FEE034);
border-radius: 23rpx;
box-shadow: 0 2rpx 8rpx rgba(253, 202, 64, 0.3);
}
}
}
}
.review-sort {
display: flex;
align-items: center;
margin: 32rpx 0;
gap: 16rpx;
padding-top: 24rpx;
.sort-item {
height: 64rpx;
padding: 0 24rpx;
background: #F9F9F9;
border-radius: 8rpx;
font-size: 24rpx;
color: #606060;
display: flex;
align-items: center;
justify-content: center;
&.active {
background: #19367A;
color: #FFFFFF;
box-shadow: 0 4rpx 12rpx rgba(25, 54, 122, 0.2);
}
}
}
.reviews {
.review-item {
padding: 40rpx 32rpx;
margin: 24rpx 0;
background: #FFFFFF;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
border: none;
&:last-child {
border-bottom: none;
}
.review-header {
margin-bottom: 32rpx;
.user-info {
display: flex;
align-items: center;
gap: 24rpx;
.avatar {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: #f5f5f5;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.user-meta {
flex: 1;
display: flex;
flex-direction: column;
gap: 12rpx;
.user-name {
font-size: 32rpx;
color: #333333;
font-weight: 500;
}
.review-time {
font-size: 24rpx;
color: #999999;
}
}
}
}
.review-content {
.content-left {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 24rpx;
.review-text {
font-size: 28rpx;
line-height: 44rpx;
color: #333333;
word-break: break-all;
}
.image-scroll {
margin-top: 32rpx;
width: 100%;
white-space: nowrap;
.image-container {
display: inline-flex;
gap: 16rpx;
padding-right: 32rpx;
.review-image {
width: 220rpx;
height: 220rpx;
border-radius: 12rpx;
flex-shrink: 0;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
}
}
.review-footer {
display: flex;
align-items: center;
gap: 24rpx;
margin-top: 24rpx;
padding-top: 24rpx;
border-top: 2rpx solid #f5f5f5;
.score-text {
font-size: 28rpx;
color: #FDCA40;
font-weight: 500;
}
.like-btn {
display: flex;
align-items: center;
gap: 8rpx;
padding: 4rpx 12rpx;
border-radius: 8rpx;
background: rgba(25, 54, 122, 0.05);
.like-icon {
width: 32rpx;
height: 32rpx;
opacity: 0.6;
}
.like-count {
font-size: 24rpx;
color: #666666;
}
&.active {
background: rgba(253, 202, 64, 0.1);
box-shadow: 0 2rpx 8rpx rgba(253, 202, 64, 0.2);
.like-icon {
opacity: 1;
}
.like-count {
color: #FDCA40;
}
}
}
}
}
}
}
}
.view-all-reviews {
width: 100%;
height: 88rpx;
margin: 40rpx 0;
border: 2rpx solid #19367A;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: #19367A;
background: #FFFFFF;
box-shadow: 0 4rpx 12rpx rgba(25, 54, 122, 0.08);
&.disabled {
border-color: #CCCCCC;
color: #CCCCCC;
pointer-events: none;
box-shadow: none;
}
}
.no-review {
text-align: center;
padding: 80rpx 0;
font-size: 32rpx;
color: #606060;
background: #FFFFFF;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
}
}
/* 底部操作栏 */
.bottom-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
padding: 24rpx 32rpx env(safe-area-inset-bottom);
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
.action-box {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12rpx 0;
.action-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
padding: 8rpx 16rpx;
.action-icon {
width: 48rpx;
height: 48rpx;
}
.action-text {
font-size: 24rpx;
color: #333;
margin-top: 4rpx;
}
}
.write-review {
width: 358rpx;
height: 96rpx;
background: linear-gradient(135deg, #19367A, #1E4B9E);
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #fff;
font-weight: 500;
margin: 0 16rpx;
box-shadow: 0 4rpx 16rpx rgba(25, 54, 122, 0.2);
}
}
}
.rating-bars {
.rating-bar {
.bar {
.bar-filled {
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
}
.bar-value {
transition: all 0.3s ease;
}
}
}
.rating-overview {
.rating-score-large {
transition: all 0.3s ease;
}
}
.rating-tabs {
.tab-item {
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
}
}
}
.review-sort {
.sort-item {
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
}
}
}
.write-review {
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
opacity: 0.9;
}
}
.like-btn {
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
}
}
</style>