775 lines
16 KiB
Plaintext
775 lines
16 KiB
Plaintext
<!-- 评价页面 -->
|
||
<template>
|
||
<view class="review-page">
|
||
<!-- 顶部栏 -->
|
||
<view class="review-header">
|
||
<text class="back-btn" @click="goBack">‹</text>
|
||
<text class="header-title">评价商品</text>
|
||
</view>
|
||
|
||
<scroll-view class="review-content" scroll-y>
|
||
<!-- 订单信息 -->
|
||
<view class="order-section">
|
||
<text class="order-no">订单号: {{ order?.order_no }}</text>
|
||
<text class="order-time">下单时间: {{ formatTime(order?.created_at) }}</text>
|
||
</view>
|
||
|
||
<!-- 商品评价 -->
|
||
<view class="products-section">
|
||
<view v-for="(item, index) in orderItems" :key="item.id" class="product-review">
|
||
<view class="product-header">
|
||
<image class="product-image" :src="item.product_image || '/static/default-product.png'" />
|
||
<view class="product-info">
|
||
<text class="product-name">{{ item.product_name }}</text>
|
||
<text v-if="item.sku_specifications" class="product-spec">{{ getSpecText(item.sku_specifications) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 评分 -->
|
||
<view class="rating-section">
|
||
<text class="rating-label">评分</text>
|
||
<view class="rating-stars">
|
||
<text v-for="star in 5"
|
||
:key="star"
|
||
class="star-icon"
|
||
:class="{ active: star <= ratings[index] }"
|
||
@click="setRating(index, star)">
|
||
⭐
|
||
</text>
|
||
</view>
|
||
<text class="rating-text">{{ getRatingText(ratings[index]) }}</text>
|
||
</view>
|
||
|
||
<!-- 评价内容 -->
|
||
<view class="content-section">
|
||
<textarea class="review-textarea"
|
||
v-model="contents[index]"
|
||
placeholder="请写下您的使用感受,分享给其他小伙伴吧"
|
||
maxlength="500" />
|
||
<text class="word-count">{{ contents[index]?.length || 0 }}/500</text>
|
||
</view>
|
||
|
||
<!-- 图片上传 -->
|
||
<view class="images-section">
|
||
<text class="images-label">上传图片(可选)</text>
|
||
<view class="images-grid">
|
||
<view v-for="(image, imgIndex) in images[index]"
|
||
:key="imgIndex"
|
||
class="image-item">
|
||
<image class="uploaded-image" :src="image" />
|
||
<text class="delete-image" @click="deleteImage(index, imgIndex)">×</text>
|
||
</view>
|
||
<view v-if="images[index].length < 9"
|
||
class="upload-btn"
|
||
@click="uploadImage(index)">
|
||
<text class="upload-icon">+</text>
|
||
<text class="upload-text">添加图片</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 匿名评价 -->
|
||
<view class="anonymous-section">
|
||
<view class="anonymous-switch">
|
||
<text class="switch-label">匿名评价</text>
|
||
<switch :checked="anonymous" @change="toggleAnonymous" />
|
||
</view>
|
||
<text class="anonymous-tip">评价内容对其他用户不可见</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 店铺评价 -->
|
||
<view v-if="merchant" class="merchant-section">
|
||
<text class="section-title">店铺评价</text>
|
||
<view class="merchant-rating">
|
||
<text class="rating-item">商品描述相符</text>
|
||
<view class="rating-stars small">
|
||
<text v-for="star in 5"
|
||
:key="star"
|
||
class="star-icon"
|
||
:class="{ active: star <= merchantRating.description }"
|
||
@click="setMerchantRating('description', star)">
|
||
⭐
|
||
</text>
|
||
</view>
|
||
</view>
|
||
<view class="merchant-rating">
|
||
<text class="rating-item">物流服务</text>
|
||
<view class="rating-stars small">
|
||
<text v-for="star in 5"
|
||
:key="star"
|
||
class="star-icon"
|
||
:class="{ active: star <= merchantRating.logistics }"
|
||
@click="setMerchantRating('logistics', star)">
|
||
⭐
|
||
</text>
|
||
</view>
|
||
</view>
|
||
<view class="merchant-rating">
|
||
<text class="rating-item">服务态度</text>
|
||
<view class="rating-stars small">
|
||
<text v-for="star in 5"
|
||
:key="star"
|
||
class="star-icon"
|
||
:class="{ active: star <= merchantRating.service }"
|
||
@click="setMerchantRating('service', star)">
|
||
⭐
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 评价提示 -->
|
||
<view class="tips-section">
|
||
<text class="tip-title">评价须知</text>
|
||
<text class="tip-item">1. 评价后不可修改,请谨慎评价</text>
|
||
<text class="tip-item">2. 上传图片需为真实商品照片</text>
|
||
<text class="tip-item">3. 恶意评价将被删除并限制评价功能</text>
|
||
<text class="tip-item">4. 优质评价可获得积分奖励</text>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 提交按钮 -->
|
||
<view class="submit-section">
|
||
<button class="submit-btn"
|
||
:class="{ disabled: !canSubmit || isSubmitting }"
|
||
@click="submitReview">
|
||
<text v-if="!isSubmitting" class="submit-text">提交评价</text>
|
||
<text v-else class="submit-text">提交中...</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, onMounted, computed } from 'vue'
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
|
||
type OrderItemType = {
|
||
id: string
|
||
product_id: string
|
||
product_name: string
|
||
product_image: string
|
||
sku_specifications: any
|
||
price: number
|
||
quantity: number
|
||
}
|
||
|
||
type MerchantType = {
|
||
id: string
|
||
shop_name: string
|
||
rating: number
|
||
}
|
||
|
||
const orderId = ref<string>('')
|
||
const order = ref<any>(null)
|
||
const orderItems = ref<Array<OrderItemType>>([])
|
||
const merchant = ref<MerchantType | null>(null)
|
||
const ratings = ref<Array<number>>([])
|
||
const contents = ref<Array<string>>([])
|
||
const images = ref<Array<Array<string>>>([])
|
||
const anonymous = ref<boolean>(false)
|
||
const merchantRating = ref({
|
||
description: 5,
|
||
logistics: 5,
|
||
service: 5
|
||
})
|
||
const isSubmitting = ref<boolean>(false)
|
||
|
||
// 计算属性
|
||
const canSubmit = computed(() => {
|
||
// 检查是否所有商品都已评分
|
||
if (ratings.value.length === 0) return false
|
||
return ratings.value.every(rating => rating > 0)
|
||
})
|
||
|
||
// 生命周期
|
||
onMounted(() => {
|
||
const pages = getCurrentPages()
|
||
const currentPage = pages[pages.length - 1]
|
||
const options = currentPage.options as any
|
||
|
||
if (options.orderId) {
|
||
orderId.value = options.orderId
|
||
loadOrderData()
|
||
}
|
||
})
|
||
|
||
// 加载订单数据
|
||
const loadOrderData = async () => {
|
||
try {
|
||
const { data: orderData, error: orderError } = await supa
|
||
.from('orders')
|
||
.select('*')
|
||
.eq('id', orderId.value)
|
||
.single()
|
||
|
||
if (orderError !== null) {
|
||
console.error('加载订单失败:', orderError)
|
||
return
|
||
}
|
||
|
||
order.value = orderData
|
||
|
||
// 加载订单商品
|
||
const { data: itemsData, error: itemsError } = await supa
|
||
.from('order_items')
|
||
.select(`
|
||
*,
|
||
product:product_id(images)
|
||
`)
|
||
.eq('order_id', orderId.value)
|
||
|
||
if (itemsError !== null) {
|
||
console.error('加载订单商品失败:', itemsError)
|
||
return
|
||
}
|
||
|
||
orderItems.value = (itemsData || []).map((item: any) => ({
|
||
...item,
|
||
product_image: item.product?.images?.[0] || '/static/default-product.png'
|
||
}))
|
||
|
||
// 初始化评分和内容数组
|
||
const count = orderItems.value.length
|
||
ratings.value = new Array(count).fill(5)
|
||
contents.value = new Array(count).fill('')
|
||
images.value = new Array(count).fill([])
|
||
|
||
// 加载商家信息
|
||
if (order.value.merchant_id) {
|
||
const { data: merchantData, error: merchantError } = await supa
|
||
.from('merchants')
|
||
.select('id, shop_name, rating')
|
||
.eq('id', order.value.merchant_id)
|
||
.single()
|
||
|
||
if (!merchantError) {
|
||
merchant.value = merchantData
|
||
}
|
||
}
|
||
|
||
} catch (err) {
|
||
console.error('加载订单数据异常:', err)
|
||
}
|
||
}
|
||
|
||
// 格式化时间
|
||
const formatTime = (timeStr?: string): string => {
|
||
if (!timeStr) return ''
|
||
const date = new Date(timeStr)
|
||
const year = date.getFullYear()
|
||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||
const day = date.getDate().toString().padStart(2, '0')
|
||
return `${year}-${month}-${day}`
|
||
}
|
||
|
||
// 获取规格文本
|
||
const getSpecText = (specs: any): string => {
|
||
if (!specs) return ''
|
||
if (typeof specs === 'object') {
|
||
return Object.keys(specs)
|
||
.map(key => `${key}: ${specs[key]}`)
|
||
.join('; ')
|
||
}
|
||
return String(specs)
|
||
}
|
||
|
||
// 获取评分文本
|
||
const getRatingText = (rating: number): string => {
|
||
const texts = ['非常差', '差', '一般', '好', '非常好']
|
||
return texts[rating - 1] || '未评价'
|
||
}
|
||
|
||
// 设置商品评分
|
||
const setRating = (index: number, rating: number) => {
|
||
ratings.value[index] = rating
|
||
ratings.value = [...ratings.value]
|
||
}
|
||
|
||
// 设置商家评分
|
||
const setMerchantRating = (type: keyof typeof merchantRating.value, rating: number) => {
|
||
merchantRating.value[type] = rating
|
||
merchantRating.value = { ...merchantRating.value }
|
||
}
|
||
|
||
// 切换匿名
|
||
const toggleAnonymous = (event: any) => {
|
||
anonymous.value = event.detail.value
|
||
}
|
||
|
||
// 上传图片
|
||
const uploadImage = async (index: number) => {
|
||
// 检查图片数量限制
|
||
if (images.value[index].length >= 9) {
|
||
uni.showToast({
|
||
title: '最多上传9张图片',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
// 使用uni.chooseImage选择图片
|
||
uni.chooseImage({
|
||
count: 9 - images.value[index].length,
|
||
sizeType: ['compressed'],
|
||
sourceType: ['album', 'camera'],
|
||
success: (res) => {
|
||
const tempFiles = res.tempFilePaths
|
||
|
||
// 模拟上传过程
|
||
uni.showLoading({
|
||
title: '上传中...'
|
||
})
|
||
|
||
setTimeout(() => {
|
||
// 这里应该调用真实的上传接口
|
||
images.value[index].push(...tempFiles)
|
||
images.value = [...images.value]
|
||
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: '上传成功',
|
||
icon: 'success'
|
||
})
|
||
}, 1000)
|
||
}
|
||
})
|
||
}
|
||
|
||
// 删除图片
|
||
const deleteImage = (index: number, imgIndex: number) => {
|
||
images.value[index].splice(imgIndex, 1)
|
||
images.value = [...images.value]
|
||
}
|
||
|
||
// 提交评价
|
||
const submitReview = async () => {
|
||
if (!canSubmit.value || isSubmitting.value) return
|
||
|
||
isSubmitting.value = true
|
||
|
||
try {
|
||
const userId = getCurrentUserId()
|
||
if (!userId) {
|
||
uni.showToast({
|
||
title: '用户信息错误',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
// 提交商品评价
|
||
/* const productReviews = orderItems.value.map((item, index) => ({
|
||
user_id: userId,
|
||
product_id: item.product_id,
|
||
order_id: orderId.value,
|
||
rating: ratings.value[index],
|
||
content: contents.value[index] || '',
|
||
images: images.value[index],
|
||
is_anonymous: anonymous.value,
|
||
is_valid: true,
|
||
created_at: new Date().toISOString()
|
||
}))
|
||
|
||
const { error: reviewsError } = await supa
|
||
.from('product_reviews')
|
||
.insert(productReviews)
|
||
|
||
if (reviewsError !== null) {
|
||
throw reviewsError
|
||
}
|
||
|
||
// 提交店铺评价
|
||
if (merchant.value) {
|
||
const merchantReview = {
|
||
user_id: userId,
|
||
merchant_id: merchant.value.id,
|
||
order_id: orderId.value,
|
||
description_rating: merchantRating.value.description,
|
||
logistics_rating: merchantRating.value.logistics,
|
||
service_rating: merchantRating.value.service,
|
||
average_rating: (
|
||
merchantRating.value.description +
|
||
merchantRating.value.logistics +
|
||
merchantRating.value.service
|
||
) / 3,
|
||
is_anonymous: anonymous.value,
|
||
created_at: new Date().toISOString()
|
||
}
|
||
|
||
const { error: merchantError } = await supa
|
||
.from('merchant_reviews')
|
||
.insert(merchantReview)
|
||
|
||
if (merchantError !== null) {
|
||
console.error('提交店铺评价失败:', merchantError)
|
||
}
|
||
}
|
||
|
||
// 更新订单状态为已评价
|
||
const { error: orderError } = await supa
|
||
.from('orders')
|
||
.update({ status: 4 }) // 已完成
|
||
.eq('id', orderId.value)
|
||
|
||
if (orderError !== null) {
|
||
console.error('更新订单状态失败:', orderError)
|
||
} */
|
||
|
||
// MOCK SUBMIT
|
||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||
|
||
// 显示成功提示
|
||
uni.showToast({
|
||
title: '评价成功',
|
||
icon: 'success',
|
||
duration: 2000
|
||
})
|
||
|
||
// 跳转到评价成功页面
|
||
setTimeout(() => {
|
||
uni.navigateBack()
|
||
}, 1500)
|
||
|
||
} catch (err) {
|
||
console.error('提交评价失败:', err)
|
||
uni.showToast({
|
||
title: '提交失败',
|
||
icon: 'none'
|
||
})
|
||
} finally {
|
||
isSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
// 获取当前用户ID
|
||
const getCurrentUserId = (): string => {
|
||
const userStore = uni.getStorageSync('userInfo')
|
||
return userStore?.id || ''
|
||
}
|
||
|
||
// 返回
|
||
const goBack = () => {
|
||
uni.navigateBack()
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.review-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100vh;
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.review-header {
|
||
background-color: #ffffff;
|
||
padding: 15px;
|
||
display: flex;
|
||
align-items: center;
|
||
border-bottom: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.back-btn {
|
||
font-size: 24px;
|
||
color: #333333;
|
||
padding: 5px;
|
||
margin-right: 15px;
|
||
}
|
||
|
||
.header-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
}
|
||
|
||
.review-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.order-section {
|
||
background-color: #ffffff;
|
||
padding: 15px;
|
||
margin-bottom: 10px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.order-no {
|
||
font-size: 14px;
|
||
color: #333333;
|
||
}
|
||
|
||
.order-time {
|
||
font-size: 12px;
|
||
color: #999999;
|
||
}
|
||
|
||
.products-section {
|
||
background-color: #ffffff;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.product-review {
|
||
padding: 15px;
|
||
border-bottom: 1px solid #f5f5f5;
|
||
}
|
||
|
||
.product-review:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.product-header {
|
||
display: flex;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.product-image {
|
||
width: 60px;
|
||
height: 60px;
|
||
border-radius: 5px;
|
||
margin-right: 15px;
|
||
}
|
||
|
||
.product-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.product-name {
|
||
font-size: 14px;
|
||
color: #333333;
|
||
line-height: 1.4;
|
||
margin-bottom: 5px;
|
||
display: block;
|
||
}
|
||
|
||
.product-spec {
|
||
font-size: 12px;
|
||
color: #999999;
|
||
display: block;
|
||
}
|
||
|
||
.rating-section {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.rating-label {
|
||
font-size: 14px;
|
||
color: #333333;
|
||
margin-right: 15px;
|
||
}
|
||
|
||
.rating-stars {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
.rating-stars.small {
|
||
gap: 5px;
|
||
}
|
||
|
||
.star-icon {
|
||
font-size: 24px;
|
||
color: #cccccc;
|
||
}
|
||
|
||
.star-icon.active {
|
||
color: #ffa726;
|
||
}
|
||
|
||
.rating-text {
|
||
margin-left: 15px;
|
||
font-size: 14px;
|
||
color: #666666;
|
||
}
|
||
|
||
.content-section {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.review-textarea {
|
||
width: 100%;
|
||
min-height: 80px;
|
||
padding: 10px;
|
||
border: 1px solid #e5e5e5;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
color: #333333;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.word-count {
|
||
display: block;
|
||
text-align: right;
|
||
font-size: 12px;
|
||
color: #999999;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
.images-section {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.images-label {
|
||
display: block;
|
||
font-size: 14px;
|
||
color: #333333;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.images-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
|
||
.image-item {
|
||
width: 70px;
|
||
height: 70px;
|
||
border-radius: 5px;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
.uploaded-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.delete-image {
|
||
position: absolute;
|
||
top: 2px;
|
||
right: 2px;
|
||
width: 16px;
|
||
height: 16px;
|
||
border-radius: 8px;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
color: #ffffff;
|
||
font-size: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.upload-btn {
|
||
width: 70px;
|
||
height: 70px;
|
||
border: 1px dashed #cccccc;
|
||
border-radius: 5px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.upload-icon {
|
||
font-size: 24px;
|
||
color: #999999;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.upload-text {
|
||
font-size: 10px;
|
||
color: #999999;
|
||
}
|
||
|
||
.anonymous-section {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.anonymous-switch {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.switch-label {
|
||
font-size: 14px;
|
||
color: #333333;
|
||
}
|
||
|
||
.anonymous-tip {
|
||
display: block;
|
||
font-size: 12px;
|
||
color: #999999;
|
||
}
|
||
|
||
.merchant-section {
|
||
background-color: #ffffff;
|
||
padding: 15px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.section-title {
|
||
display: block;
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.merchant-rating {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.rating-item {
|
||
font-size: 14px;
|
||
color: #333333;
|
||
}
|
||
|
||
.tips-section {
|
||
background-color: #ffffff;
|
||
padding: 15px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.tip-title {
|
||
display: block;
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.tip-item {
|
||
display: block;
|
||
font-size: 12px;
|
||
color: #666666;
|
||
line-height: 1.6;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.tip-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.submit-section {
|
||
background-color: #ffffff;
|
||
padding: 15px;
|
||
border-top: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.submit-btn {
|
||
background-color: #007aff;
|
||
color: #ffffff;
|
||
height: 50px;
|
||
border-radius: 25px;
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
border: none;
|
||
}
|
||
|
||
.submit-btn.disabled {
|
||
background-color: #cccccc;
|
||
opacity: 0.6;
|
||
}
|
||
</style> |