完成consumer端同步

This commit is contained in:
2026-05-14 15:28:09 +08:00
parent 612fb3d360
commit 0ffbc53902
197 changed files with 92657 additions and 7564 deletions

View File

@@ -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>