1193 lines
28 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 || 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 || '-') : (myReviewInfo ? myReviewInfo.overallRating : '-')}}</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 ? 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.compositeScore"
:readonly="true"
:size="16"
:touchable="false"
color="#ECECEC"
active-color="#FEE034"
/>
<text class="score-text">{{item.compositeScore}}</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"></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',
};
},
onLoad({beerId}) {
this.beerId = beerId
this.queryForm.beerId = beerId
this.getBeerInfoFun()
this.getReviewListFun()
this.getReviewScoreListFun()
// 只在登录状态下获取收藏状态和我的评价信息
const token = uni.getStorageSync('token')
if (token) {
this.getBeerFavorStatusFun()
this.getMyReviewInfoFun()
}
},
onShow() {
console.log('show')
this.reviewList = []
this.queryForm.pageNum = 1
this.getReviewListFun()
this.getReviewScoreListFun()
// 只在登录状态下获取收藏状态和我的评价信息
const token = uni.getStorageSync('token')
if (token) {
this.getBeerFavorStatusFun()
this.getMyReviewInfoFun()
}
// 滚动到最顶
uni.pageScrollTo({
scrollTop: 0,
duration: 0,
})
},
methods: {
toPageBottom() {
uni.pageScrollTo({
scrollTop: 9999,
})
},
// 查询酒款收藏状态
getBeerFavorStatusFun() {
const token = uni.getStorageSync('token')
if (!token) {
this.$refs.loginRef.open()
return
}
getBeerFavorStatus(this.beerId).then(res => {
if(res.data) {
this.isFavor = true
}else {
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,
compositeScore: this.getScore(it.aromaRating, it.tasteRating, it.colorRating),
isExpanded: false,
needExpand: it.reviewContent && it.reviewContent.length > 50
}))
if (arr.length > 0) {
console.log('更新评论列表')
this.reviewList = [...this.reviewList, ...arr]
}
}
this.reviewTotal = res.total
})
},
// 酒评列表分页
reviewPageChange() {
this.queryForm.pageNum++
this.getReviewListFun()
},
// 获取酒评评分列表
getReviewScoreListFun() {
getReviewScoreList(this.beerId).then(res => {
this.reviewScoreList = res.data
})
},
// 获取我的酒评信息
getMyReviewInfoFun() {
const token = uni.getStorageSync('token')
if (!token) {
this.$refs.loginRef.open()
return
}
getMyReviewInfo(this.beerId).then(res => {
this.myReviewInfo = res.data
})
},
// 写酒评
toWrite() {
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 === 0) {
uni.showModal({
title: '提示',
content: '请先认证门店',
showCancel: true,
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/index/registration'
})
}
}
})
return
} else if (barInfo.authState === 1) {
uni.showToast({
title: '您的门店正在认证中,请耐心等待',
icon: 'none'
})
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
})
},
// 收藏酒款
favorBeerFun(status) {
const token = uni.getStorageSync('token')
if (!token) {
this.$refs.loginRef.open()
return
}
// 检查认证状态
const barInfo = uni.getStorageSync('barInfo')
if (!barInfo || barInfo.authState === 0) {
uni.showModal({
title: '提示',
content: '请先认证门店',
showCancel: true,
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/index/registration'
})
}
}
})
return
}
if (barInfo.authState === 1) {
uni.showToast({
title: '您的门店正在认证中,请耐心等待',
icon: 'none'
})
return
}
let data = {
beerId: this.beerId,
status
}
favorBeer(data).then(res => {
if (status == 1) {
this.isFavor = true
uni.showToast({
title: '收藏成功',
icon: 'success'
})
} else {
this.isFavor = false
uni.showToast({
title: '取消收藏',
icon: 'none'
})
}
}).catch(err => {
console.error('收藏操作失败:', err)
if (err.code === 500 && err.msg.includes('门店未认证')) {
uni.showModal({
title: '提示',
content: '请先认证门店',
showCancel: true,
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/index/registration'
})
}
}
})
} else {
uni.showToast({
title: err.msg || '操作失败',
icon: 'none'
})
}
})
},
// 点赞
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)
}
})
},
// 生成酒单
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 === 0) {
uni.showModal({
title: '提示',
content: '请先认证门店',
showCancel: true,
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/index/registration'
})
}
}
})
return
} else if (barInfo.authState === 1) {
uni.showToast({
title: '您的门店正在认证中,请耐心等待',
icon: 'none'
})
return
}
uni.navigateTo({
url: "/pagesActivity/winelist?beerId=" + this.beerId
})
},
// 分享
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
})
},
}
}
</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;
}
/* 商品信息卡片 */
.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);
}
}
}
</style>