Files
medical-mall/pages/mall/consumer/review.uvue

775 lines
16 KiB
Plaintext
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="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>