完成consumer端同步
This commit is contained in:
@@ -0,0 +1,697 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user