Files
FlashSaleSystem/flash-sale-frontend/src/components/business/ReviewDialog.vue
YoVinchen 28f41754d0 feat(groupbuying): 完善拼团订单全链路及错误处理
- 拼团订单发货校验:必须成团后才能发货
- 限购校验:跨团组统计用户参与次数,超限拒绝
- 成团/失败自动通知所有成员(Redis Pub/Sub)
- 拼团详情页区分"进行中"和"已成团"团组展示
- 订单类型支持三态(普通/秒杀/拼团)前后端联调
- 400错误只显示业务消息,不再重复弹出状态码
- 响应式导航栏适配及UI优化
- 新增历史数据修复SQL脚本
2026-03-17 00:08:21 +08:00

194 lines
5.8 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>
<el-dialog
:model-value="visible"
title="商品评价"
width="640px"
@update:model-value="$emit('update:visible', $event)"
>
<div v-if="checkLoading" class="text-center py-8">
<el-icon :size="32" class="animate-spin"><Loading /></el-icon>
<p class="mt-2 text-gray-500">加载评价状态...</p>
</div>
<div v-else class="space-y-6">
<div v-if="reviewableItems.length === 0 && reviewedItems.length === 0" class="text-center py-8">
<el-empty description="暂无可评价商品" />
</div>
<!-- 待评价商品 -->
<div v-for="item in reviewableItems" :key="item.productId" class="border rounded-lg p-4">
<div class="flex gap-4 mb-4">
<SafeImage :src="item.productImage" :alt="item.productName" wrapper-class="w-16 h-16 rounded" img-class="w-16 h-16 object-cover rounded" />
<div class="flex-1">
<h4 class="font-semibold">{{ item.productName }}</h4>
<div class="text-sm text-gray-500">¥{{ item.price }} × {{ item.quantity }}</div>
</div>
</div>
<div class="mb-3">
<label class="block text-sm text-gray-600 mb-1">评分</label>
<el-rate v-model="item.rating" show-text :texts="['很差', '较差', '一般', '满意', '非常满意']" />
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">评价内容</label>
<el-input
v-model="item.content"
type="textarea"
:rows="3"
placeholder="分享一下你的使用感受吧"
maxlength="500"
show-word-limit
/>
</div>
</div>
<!-- 已评价商品 -->
<div v-for="item in reviewedItems" :key="'reviewed-' + item.productId" class="border rounded-lg p-4 bg-gray-50">
<div class="flex gap-4">
<SafeImage :src="item.productImage" :alt="item.productName" wrapper-class="w-16 h-16 rounded" img-class="w-16 h-16 object-cover rounded" />
<div class="flex-1">
<div class="flex items-center justify-between mb-1">
<h4 class="font-semibold">{{ item.productName }}</h4>
<el-tag type="success" size="small">已评价</el-tag>
</div>
<el-rate :model-value="item.existingReview!.rating" disabled />
<p class="text-sm text-gray-600 mt-1">{{ item.existingReview!.content }}</p>
</div>
</div>
</div>
</div>
<template #footer>
<el-button @click="$emit('update:visible', false)">关闭</el-button>
<el-button
v-if="reviewableItems.length > 0"
type="primary"
:loading="submitting"
:disabled="!canSubmit"
@click="handleSubmit"
>
提交评价
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { reviewApi } from '@/api/modules/review'
import type { ReviewItem } from '@/api/modules/review'
import type { OrderItem } from '@/types/api'
import SafeImage from '@/components/common/SafeImage.vue'
interface ReviewableItem extends OrderItem {
rating: number
content: string
reviewed: boolean
existingReview?: ReviewItem
}
const props = defineProps<{
visible: boolean
orderId: number
orderItems: OrderItem[]
}>()
const emit = defineEmits<{
'update:visible': [value: boolean]
success: []
}>()
const checkLoading = ref(false)
const submitting = ref(false)
const items = ref<ReviewableItem[]>([])
const reviewableItems = computed(() => items.value.filter(i => !i.reviewed))
const reviewedItems = computed(() => items.value.filter(i => i.reviewed))
const canSubmit = computed(() => reviewableItems.value.some(i => i.content.trim()))
const loadReviewStatus = async () => {
if (!props.orderId || !props.orderItems.length) {
items.value = []
return
}
checkLoading.value = true
try {
const list: ReviewableItem[] = props.orderItems.map(item => ({
...item,
rating: 5,
content: '',
reviewed: false,
existingReview: undefined,
}))
const checks = await Promise.all(
list.map(item => reviewApi.checkReview(props.orderId, item.productId).catch(() => null))
)
checks.forEach((res, index) => {
if (res?.success && res.data.reviewed) {
list[index].reviewed = true
list[index].existingReview = res.data.review
}
})
items.value = list
} finally {
checkLoading.value = false
}
}
const handleSubmit = async () => {
const toSubmit = reviewableItems.value.filter(i => i.content.trim())
if (toSubmit.length === 0) {
ElMessage.warning('请至少填写一条评价内容')
return
}
submitting.value = true
let successCount = 0
try {
for (const item of toSubmit) {
try {
await reviewApi.create({
orderId: props.orderId,
productId: item.productId,
rating: item.rating,
content: item.content.trim(),
})
item.reviewed = true
item.existingReview = { rating: item.rating, content: item.content } as ReviewItem
successCount++
} catch (error: any) {
const respData = error?.response?.data
const msg = respData?.message || error?.message || '提交失败'
ElMessage.error(`${item.productName}: ${msg}`)
}
}
if (successCount > 0) {
ElMessage.success(`成功提交 ${successCount} 条评价`)
emit('success')
if (reviewableItems.value.length === 0) {
emit('update:visible', false)
}
}
} finally {
submitting.value = false
}
}
watch(() => props.visible, (val) => {
if (val) loadReviewStatus()
if (!val) items.value = []
})
watch(
() => [props.orderId, props.orderItems],
() => {
if (props.visible) loadReviewStatus()
},
{ immediate: true }
)
</script>