698 lines
16 KiB
Plaintext
698 lines
16 KiB
Plaintext
<template>
|
||
<view class="my-reviews-page">
|
||
<view class="tabs">
|
||
<view
|
||
class="tab-item"
|
||
:class="{ active: activeTab === 'published' }"
|
||
@click="switchTab('published')"
|
||
>
|
||
<text class="tab-text">已评价</text>
|
||
</view>
|
||
<view
|
||
class="tab-item"
|
||
:class="{ active: activeTab === 'pending' }"
|
||
@click="switchTab('pending')"
|
||
>
|
||
<text class="tab-text">待评价</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="review-list" v-if="activeTab === 'published'">
|
||
<view class="review-item" v-for="review in reviews" :key="review.id">
|
||
<view class="product-info" @click="goToProduct(review.product_id)">
|
||
<image
|
||
class="product-image"
|
||
:src="review.product_image.length > 0 ? review.product_image : defaultImage"
|
||
mode="aspectFill"
|
||
/>
|
||
<view class="product-detail">
|
||
<text class="product-name">{{ review.product_name }}</text>
|
||
<view class="rating-row">
|
||
<view class="rating-stars">
|
||
<text
|
||
v-for="star in 5"
|
||
:key="star"
|
||
class="star"
|
||
:class="{ filled: star <= review.rating }"
|
||
>★</text>
|
||
</view>
|
||
<text class="review-time">{{ formatTime(review.created_at) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="review-content">
|
||
<text class="review-text">{{ review.content }}</text>
|
||
</view>
|
||
|
||
<view class="review-images" v-if="review.images.length > 0">
|
||
<image
|
||
v-for="(img, idx) in review.images.slice(0, 4)"
|
||
:key="idx"
|
||
class="review-image"
|
||
:src="img"
|
||
mode="aspectFill"
|
||
@click="previewImage(review.images, idx)"
|
||
/>
|
||
</view>
|
||
|
||
<view class="review-append" v-if="review.append_content">
|
||
<text class="append-label">追评:</text>
|
||
<text class="append-text">{{ review.append_content }}</text>
|
||
</view>
|
||
|
||
<view class="review-actions">
|
||
<view
|
||
class="action-btn append"
|
||
v-if="review.can_append"
|
||
@click="showAppendPopup(review)"
|
||
>
|
||
<text class="action-text">追加评价</text>
|
||
</view>
|
||
<view
|
||
class="action-btn delete"
|
||
@click="confirmDelete(review)"
|
||
>
|
||
<text class="action-text">删除</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="pending-list" v-if="activeTab === 'pending'">
|
||
<view class="pending-item" v-for="item in pendingItems" :key="item.order_id">
|
||
<view class="product-info">
|
||
<image
|
||
class="product-image"
|
||
:src="item.product_image.length > 0 ? item.product_image : defaultImage"
|
||
mode="aspectFill"
|
||
/>
|
||
<view class="product-detail">
|
||
<text class="product-name">{{ item.product_name }}</text>
|
||
<text class="order-time">下单时间:{{ formatTime(item.order_time) }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="pending-actions">
|
||
<button class="review-btn" @click="goToReview(item)">去评价</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="empty-state" v-if="!loading && ((activeTab === 'published' && reviews.length === 0) || (activeTab === 'pending' && pendingItems.length === 0))">
|
||
<text class="empty-text">{{ activeTab === 'published' ? '暂无评价记录' : '暂无待评价商品' }}</text>
|
||
</view>
|
||
|
||
<view class="loading-state" v-if="loading">
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
|
||
<view class="append-popup" v-if="showAppendModal" @click="closeAppendPopup">
|
||
<view class="popup-content" @click.stop>
|
||
<view class="popup-header">
|
||
<text class="popup-title">追加评价</text>
|
||
<text class="popup-close" @click="closeAppendPopup">×</text>
|
||
</view>
|
||
|
||
<textarea
|
||
class="append-input"
|
||
v-model="appendContent"
|
||
placeholder="请输入追加评价内容"
|
||
:maxlength="500"
|
||
/>
|
||
|
||
<view class="popup-footer">
|
||
<button class="cancel-btn" @click="closeAppendPopup">取消</button>
|
||
<button class="submit-btn" @click="submitAppend">提交</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, onMounted } from 'vue'
|
||
import { supabaseService } from '@/utils/supabaseService.uts'
|
||
|
||
type MyReviewItem = {
|
||
id: string
|
||
product_id: string
|
||
product_name: string
|
||
product_image: string
|
||
rating: number
|
||
content: string
|
||
images: string[]
|
||
append_content: string | null
|
||
can_append: boolean
|
||
can_edit: boolean
|
||
created_at: string
|
||
}
|
||
|
||
type PendingItem = {
|
||
order_id: string
|
||
product_id: string
|
||
product_name: string
|
||
product_image: string
|
||
order_time: string
|
||
}
|
||
|
||
const activeTab = ref<string>('published')
|
||
const reviews = ref<MyReviewItem[]>([])
|
||
const pendingItems = ref<PendingItem[]>([])
|
||
const loading = ref<boolean>(true)
|
||
const showAppendModal = ref<boolean>(false)
|
||
const appendContent = ref<string>('')
|
||
const selectedReview = ref<MyReviewItem | null>(null)
|
||
|
||
const defaultImage: string = '/static/images/default-product.png'
|
||
|
||
const loadReviews = async (): Promise<void> => {
|
||
loading.value = true
|
||
try {
|
||
const result = await supabaseService.getMyReviews()
|
||
const parsed: MyReviewItem[] = []
|
||
|
||
for (let i = 0; i < result.length; i++) {
|
||
const item = result[i]
|
||
let reviewObj: UTSJSONObject
|
||
if (item instanceof UTSJSONObject) {
|
||
reviewObj = item
|
||
} else {
|
||
reviewObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
}
|
||
|
||
let images: string[] = []
|
||
const imagesRaw = reviewObj.get('images')
|
||
if (imagesRaw != null && typeof imagesRaw === 'string') {
|
||
try {
|
||
const parsedImages = JSON.parse(imagesRaw as string)
|
||
if (Array.isArray(parsedImages)) {
|
||
images = parsedImages as string[]
|
||
}
|
||
} catch (e) {
|
||
console.error('解析图片失败:', e)
|
||
}
|
||
}
|
||
|
||
const review: MyReviewItem = {
|
||
id: reviewObj.getString('id') ?? '',
|
||
product_id: reviewObj.getString('product_id') ?? '',
|
||
product_name: reviewObj.getString('product_name') ?? '',
|
||
product_image: reviewObj.getString('product_image') ?? '',
|
||
rating: reviewObj.getNumber('rating') ?? 5,
|
||
content: reviewObj.getString('content') ?? '',
|
||
images: images,
|
||
append_content: reviewObj.getString('append_content'),
|
||
can_append: reviewObj.getBoolean('can_append') ?? false,
|
||
can_edit: reviewObj.getBoolean('can_edit') ?? false,
|
||
created_at: reviewObj.getString('created_at') ?? ''
|
||
}
|
||
parsed.push(review)
|
||
}
|
||
|
||
reviews.value = parsed
|
||
} catch (e) {
|
||
console.error('加载评价失败:', e)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const loadPendingItems = async (): Promise<void> => {
|
||
loading.value = true
|
||
try {
|
||
const orders = await supabaseService.getOrders(4)
|
||
const pending: PendingItem[] = []
|
||
|
||
for (let i = 0; i < orders.length; i++) {
|
||
const order = orders[i]
|
||
let orderObj: UTSJSONObject
|
||
if (order instanceof UTSJSONObject) {
|
||
orderObj = order
|
||
} else {
|
||
orderObj = JSON.parse(JSON.stringify(order)) as UTSJSONObject
|
||
}
|
||
|
||
const orderId = orderObj.getString('id') ?? ''
|
||
const itemsRaw = orderObj.get('items')
|
||
|
||
if (itemsRaw != null && Array.isArray(itemsRaw)) {
|
||
const items = itemsRaw as any[]
|
||
for (let j = 0; j < items.length; j++) {
|
||
const orderItem = items[j]
|
||
let itemObj: UTSJSONObject
|
||
if (orderItem instanceof UTSJSONObject) {
|
||
itemObj = orderItem
|
||
} else {
|
||
itemObj = JSON.parse(JSON.stringify(orderItem)) as UTSJSONObject
|
||
}
|
||
|
||
pending.push({
|
||
order_id: orderId,
|
||
product_id: itemObj.getString('product_id') ?? '',
|
||
product_name: itemObj.getString('product_name') ?? '',
|
||
product_image: itemObj.getString('product_image') ?? '',
|
||
order_time: orderObj.getString('created_at') ?? ''
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
pendingItems.value = pending
|
||
} catch (e) {
|
||
console.error('加载待评价商品失败:', e)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const switchTab = (tab: string): void => {
|
||
activeTab.value = tab
|
||
if (tab === 'published' && reviews.value.length === 0) {
|
||
loadReviews()
|
||
} else if (tab === 'pending' && pendingItems.value.length === 0) {
|
||
loadPendingItems()
|
||
}
|
||
}
|
||
|
||
const goToProduct = (productId: string): void => {
|
||
uni.navigateTo({
|
||
url: `/pages/mall/consumer/product-detail?id=${productId}`
|
||
})
|
||
}
|
||
|
||
const goToReview = (item: PendingItem): void => {
|
||
uni.navigateTo({
|
||
url: `/pages/mall/consumer/review?order_id=${item.order_id}`
|
||
})
|
||
}
|
||
|
||
const showAppendPopup = (review: MyReviewItem): void => {
|
||
selectedReview.value = review
|
||
appendContent.value = ''
|
||
showAppendModal.value = true
|
||
}
|
||
|
||
const closeAppendPopup = (): void => {
|
||
showAppendModal.value = false
|
||
selectedReview.value = null
|
||
appendContent.value = ''
|
||
}
|
||
|
||
const submitAppend = async (): Promise<void> => {
|
||
if (selectedReview.value == null || appendContent.value.trim() === '') {
|
||
uni.showToast({ title: '请输入评价内容', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
uni.showLoading({ title: '提交中...' })
|
||
|
||
try {
|
||
const success = await supabaseService.appendReview(
|
||
selectedReview.value.id,
|
||
appendContent.value.trim(),
|
||
[]
|
||
)
|
||
|
||
if (success) {
|
||
selectedReview.value.append_content = appendContent.value.trim()
|
||
selectedReview.value.can_append = false
|
||
closeAppendPopup()
|
||
uni.showToast({ title: '追加成功', icon: 'success' })
|
||
} else {
|
||
uni.showToast({ title: '追加失败', icon: 'none' })
|
||
}
|
||
} catch (e) {
|
||
console.error('追加评价失败:', e)
|
||
uni.showToast({ title: '追加失败', icon: 'none' })
|
||
} finally {
|
||
uni.hideLoading()
|
||
}
|
||
}
|
||
|
||
const doDelete = async (review: MyReviewItem): Promise<void> => {
|
||
uni.showLoading({ title: '删除中...' })
|
||
|
||
try {
|
||
const success = await supabaseService.deleteReview(review.id)
|
||
if (success) {
|
||
const index = reviews.value.indexOf(review)
|
||
if (index > -1) {
|
||
reviews.value.splice(index, 1)
|
||
}
|
||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||
} else {
|
||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||
}
|
||
} catch (e) {
|
||
console.error('删除评价失败:', e)
|
||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||
} finally {
|
||
uni.hideLoading()
|
||
}
|
||
}
|
||
|
||
const confirmDelete = (review: MyReviewItem): void => {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '确定要删除这条评价吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
doDelete(review)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
const previewImage = (images: string[], index: number): void => {
|
||
uni.previewImage({
|
||
urls: images,
|
||
current: index
|
||
})
|
||
}
|
||
|
||
const formatTime = (timeStr: string | null): string => {
|
||
if (timeStr == null || timeStr === '') return ''
|
||
const date = new Date(timeStr)
|
||
const y = date.getFullYear()
|
||
const m = (date.getMonth() + 1).toString().padStart(2, '0')
|
||
const d = date.getDate().toString().padStart(2, '0')
|
||
return `${y}-${m}-${d}`
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadReviews()
|
||
})
|
||
</script>
|
||
|
||
<style>
|
||
.my-reviews-page {
|
||
flex: 1;
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
flex-direction: row;
|
||
background-color: white;
|
||
}
|
||
|
||
.tab-item {
|
||
flex: 1;
|
||
padding: 14px 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-bottom: 2px solid transparent;
|
||
}
|
||
|
||
.tab-item.active {
|
||
border-bottom-color: #ff6b35;
|
||
}
|
||
|
||
.tab-text {
|
||
font-size: 15px;
|
||
color: #666;
|
||
}
|
||
|
||
.tab-item.active .tab-text {
|
||
color: #ff6b35;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.review-list {
|
||
padding: 8px;
|
||
}
|
||
|
||
.review-item {
|
||
background-color: white;
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.product-info {
|
||
display: flex;
|
||
flex-direction: row;
|
||
}
|
||
|
||
.product-image {
|
||
width: 60px;
|
||
height: 60px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.product-detail {
|
||
flex: 1;
|
||
margin-left: 10px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
}
|
||
|
||
.product-name {
|
||
font-size: 14px;
|
||
color: #333;
|
||
lines: 2;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.rating-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.rating-stars {
|
||
display: flex;
|
||
flex-direction: row;
|
||
}
|
||
|
||
.star {
|
||
font-size: 12px;
|
||
color: #ddd;
|
||
}
|
||
|
||
.star.filled {
|
||
color: #ff6b35;
|
||
}
|
||
|
||
.review-time {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
.review-content {
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.review-text {
|
||
font-size: 14px;
|
||
color: #333;
|
||
line-height: 20px;
|
||
}
|
||
|
||
.review-images {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.review-image {
|
||
width: 70px;
|
||
height: 70px;
|
||
border-radius: 4px;
|
||
margin-right: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.review-append {
|
||
background-color: #f9f9f9;
|
||
padding: 10px;
|
||
border-radius: 4px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.append-label {
|
||
font-size: 12px;
|
||
color: #ff6b35;
|
||
}
|
||
|
||
.append-text {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.review-actions {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: flex-end;
|
||
margin-top: 10px;
|
||
padding-top: 10px;
|
||
border-top: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.action-btn {
|
||
padding: 6px 16px;
|
||
border-radius: 16px;
|
||
margin-left: 10px;
|
||
}
|
||
|
||
.action-btn.append {
|
||
background-color: #fff5f0;
|
||
}
|
||
|
||
.action-btn.append .action-text {
|
||
color: #ff6b35;
|
||
}
|
||
|
||
.action-btn.delete {
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.action-btn.delete .action-text {
|
||
color: #999;
|
||
}
|
||
|
||
.action-text {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.pending-list {
|
||
padding: 8px;
|
||
}
|
||
|
||
.pending-item {
|
||
background-color: white;
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.pending-actions {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: flex-end;
|
||
margin-top: 10px;
|
||
padding-top: 10px;
|
||
border-top: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.review-btn {
|
||
background-color: #ff6b35;
|
||
color: white;
|
||
font-size: 14px;
|
||
border-radius: 16px;
|
||
padding: 0 20px;
|
||
height: 32px;
|
||
line-height: 32px;
|
||
}
|
||
|
||
.order-time {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.empty-state {
|
||
padding: 60px 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 14px;
|
||
color: #999;
|
||
}
|
||
|
||
.loading-state {
|
||
padding: 30px 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 14px;
|
||
color: #999;
|
||
}
|
||
|
||
.append-popup {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.popup-content {
|
||
background-color: white;
|
||
border-radius: 16px 16px 0 0;
|
||
width: 100%;
|
||
padding: 16px;
|
||
}
|
||
|
||
.popup-header {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.popup-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.popup-close {
|
||
font-size: 24px;
|
||
color: #999;
|
||
}
|
||
|
||
.append-input {
|
||
width: 100%;
|
||
height: 120px;
|
||
background-color: #f9f9f9;
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.popup-footer {
|
||
display: flex;
|
||
flex-direction: row;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.cancel-btn {
|
||
flex: 1;
|
||
background-color: #f5f5f5;
|
||
color: #666;
|
||
font-size: 16px;
|
||
border-radius: 24px;
|
||
height: 44px;
|
||
line-height: 44px;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.submit-btn {
|
||
flex: 1;
|
||
background-color: #ff6b35;
|
||
color: white;
|
||
font-size: 16px;
|
||
border-radius: 24px;
|
||
height: 44px;
|
||
line-height: 44px;
|
||
}
|
||
</style>
|