Files
medical-mall/pages/mall/consumer/my-reviews.uvue

698 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="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>