1288 lines
29 KiB
Vue
1288 lines
29 KiB
Vue
<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',
|
||
isLoggedIn: false,
|
||
isBarAuthenticated: false,
|
||
ratingAnimation: null,
|
||
isAnimating: false
|
||
};
|
||
},
|
||
onLoad({beerId}) {
|
||
this.beerId = beerId
|
||
this.queryForm.beerId = beerId
|
||
this.initPageData()
|
||
},
|
||
onShow() {
|
||
this.checkLoginStatus()
|
||
this.initPageData()
|
||
},
|
||
methods: {
|
||
// 检查登录状态
|
||
checkLoginStatus() {
|
||
const token = uni.getStorageSync('token')
|
||
this.isLoggedIn = !!token
|
||
|
||
if (this.isLoggedIn) {
|
||
const barInfo = uni.getStorageSync('barInfo')
|
||
this.isBarAuthenticated = barInfo && barInfo.authState === 2
|
||
}
|
||
},
|
||
|
||
// 初始化页面数据
|
||
async initPageData() {
|
||
// 重置数据
|
||
this.reviewList = []
|
||
this.queryForm.pageNum = 1
|
||
|
||
// 获取基础数据
|
||
await Promise.all([
|
||
this.getBeerInfoFun(),
|
||
this.getReviewListFun(),
|
||
this.getReviewScoreListFun()
|
||
])
|
||
|
||
// 获取需要登录的数据
|
||
if (this.isLoggedIn) {
|
||
await Promise.all([
|
||
this.getBeerFavorStatusFun(),
|
||
this.getMyReviewInfoFun()
|
||
])
|
||
}
|
||
|
||
// 滚动到顶部
|
||
uni.pageScrollTo({
|
||
scrollTop: 0,
|
||
duration: 0
|
||
})
|
||
},
|
||
|
||
// 获取酒款收藏状态
|
||
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,
|
||
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
|
||
})
|
||
},
|
||
// 获取我的酒评信息
|
||
async getMyReviewInfoFun() {
|
||
try {
|
||
if (!this.isLoggedIn) {
|
||
this.myReviewInfo = null
|
||
return
|
||
}
|
||
const res = await getMyReviewInfo(this.beerId)
|
||
this.myReviewInfo = res.data
|
||
} catch (error) {
|
||
console.error('获取我的酒评信息失败:', error)
|
||
this.myReviewInfo = null
|
||
}
|
||
},
|
||
// 写酒评
|
||
toWrite() {
|
||
if (!this.isLoggedIn) {
|
||
this.$refs.loginRef.open()
|
||
return
|
||
}
|
||
|
||
if (!this.isBarAuthenticated) {
|
||
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
|
||
}
|
||
}
|
||
|
||
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 === 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
|
||
}
|
||
|
||
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'
|
||
})
|
||
}
|
||
},
|
||
// 点赞
|
||
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
|
||
})
|
||
},
|
||
// 处理评分变化时的动画效果
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
</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> |