完成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,876 @@
<!-- 退款页面 -->
<template>
<view class="refund-page">
<!-- 顶部栏 -->
<view class="refund-header">
<text class="back-btn" @click="goBack"></text>
<text class="header-title">退款/售后</text>
</view>
<!-- 标签页 -->
<view class="refund-tabs">
<view :class="['refund-tab', { active: activeTab === 'all' }]" @click="changeTab('all')">
<text class="tab-text">全部</text>
</view>
<view :class="['refund-tab', { active: activeTab === 'processing' }]" @click="changeTab('processing')">
<text class="tab-text">处理中</text>
<text v-if="tabCounts.processing > 0" class="tab-badge">{{ tabCounts.processing }}</text>
</view>
<view :class="['refund-tab', { active: activeTab === 'completed' }]" @click="changeTab('completed')">
<text class="tab-text">已完成</text>
</view>
</view>
<!-- 售后列表 -->
<scroll-view class="refund-content" direction="vertical" @scrolltolower="loadMore">
<!-- 空状态 -->
<view v-if="refunds.length === 0 && !isLoading" class="empty-refunds">
<text class="empty-icon">🔄</text>
<text class="empty-text">暂无售后记录</text>
<text class="empty-subtext">您可以在订单详情中申请售后</text>
<button class="go-orders-btn" @click="goToOrders">查看订单</button>
</view>
<!-- 售后项 -->
<view v-for="refund in refunds" :key="refund.id" class="refund-item">
<view class="refund-header">
<text class="refund-no">售后单号: {{ refund.refund_no }}</text>
<text :class="['refund-status', getStatusClass(refund.status)]">
{{ getStatusText(refund.status) }}
</text>
</view>
<view class="order-info">
<text class="order-no">订单号: {{ refund.order?.order_no }}</text>
<text class="order-time">{{ formatTime(refund.order?.created_at) }}</text>
</view>
<view class="product-info" @click="viewOrder(refund.order_id)">
<image class="product-image" :src="getProductImage(refund)" />
<view class="product-details">
<text class="product-name">{{ getProductName(refund) }}</text>
<text v-if="refund.refund_reason" class="refund-reason">原因: {{ refund.refund_reason }}</text>
<view class="refund-amount">
<text class="amount-label">退款金额:</text>
<text class="amount-value">¥{{ refund.refund_amount }}</text>
</view>
</view>
</view>
<!-- 进度时间线 -->
<view v-if="refund.status_history != null && refund.status_history.length > 0" class="timeline">
<view v-for="(step, index) in getTimelineSteps(refund)"
:key="index"
class="timeline-step">
<view class="step-dot" :class="{ active: step.active, completed: step.completed }"></view>
<view class="step-info">
<text class="step-title">{{ step.title }}</text>
<text class="step-time">{{ step.time }}</text>
<text v-if="step.desc" class="step-desc">{{ step.desc }}</text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view v-if="refund.status === 1" class="refund-actions">
<button class="action-btn cancel" @click="cancelRefund(refund)">取消申请</button>
<button class="action-btn contact" @click="contactService(refund)">联系客服</button>
</view>
<view v-if="refund.status === 3" class="refund-actions">
<button class="action-btn review" @click="reviewRefund(refund)">评价服务</button>
<button class="action-btn delete" @click="deleteRefund(refund)">删除记录</button>
</view>
</view>
<!-- 加载更多 -->
<view v-if="isLoading" class="loading-more">
<text class="loading-text">加载中...</text>
</view>
<view v-if="!hasMore && refunds.length > 0" class="no-more">
<text class="no-more-text">没有更多了</text>
</view>
</scroll-view>
<!-- 申请售后按钮 -->
<view class="apply-btn-container">
<button class="apply-btn" @click="applyRefund">申请售后</button>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, watch } from 'vue'
import { supabaseService } from '@/utils/supabaseService.uts'
type RefundStatusHistoryItem = {
status: number
remark: string
created_at: string
}
type RefundProductInfo = {
images: string[]
}
type RefundOrderItem = {
id: string
product_name: string
sku_specifications: any | null
price: number
quantity: number
product?: RefundProductInfo
}
type RefundOrderInfo = {
id: string
order_no: string
created_at: string
order_items: RefundOrderItem[]
}
type RefundType = {
id: string
user_id: string
order_id: string
refund_no: string
refund_type: number // 1:仅退款 2:退货退款
refund_reason: string
refund_amount: number
status: number // 1:待处理 2:处理中 3:已完成 4:已取消 5:已拒绝
status_history: RefundStatusHistoryItem[] | null
created_at: string
order?: RefundOrderInfo
}
type TabCountsType = {
processing: number
}
const activeTab = ref<string>('all')
const refunds = ref<Array<RefundType>>([])
const tabCounts = ref<TabCountsType>({
processing: 0
})
const isLoading = ref<boolean>(false)
const currentPage = ref<number>(1)
const pageSize = ref<number>(15)
const hasMore = ref<boolean>(true)
const getCurrentUserId = (): string => {
return supabaseService.getCurrentUserId() ?? ''
}
const resetData = () => {
refunds.value = []
currentPage.value = 1
hasMore.value = true
}
const loadRefunds = async (loadMore: boolean): Promise<void> => {
if (isLoading.value || (!hasMore.value && loadMore)) {
return
}
isLoading.value = true
try {
const userId = getCurrentUserId()
if (userId == '') {
uni.navigateTo({
url: '/pages/user/login'
})
return
}
const page = loadMore ? currentPage.value + 1 : 1
let statusList: number[] = []
if (activeTab.value === 'processing') {
statusList = [1, 2]
} else if (activeTab.value === 'completed') {
statusList = [3, 4, 5]
}
const rawData = await supabaseService.getRefunds(statusList, page, pageSize.value)
const newRefunds: Array<RefundType> = []
for (let i: number = 0; i < rawData.length; i++) {
const item = rawData[i] as UTSJSONObject
const orderObjRaw = item.get('order')
const orderObj = (orderObjRaw != null) ? (orderObjRaw as UTSJSONObject) : (new UTSJSONObject())
const dbItemsRaw = orderObj.get('ml_order_items')
const dbItems = (dbItemsRaw != null) ? (dbItemsRaw as any[]) : []
const uiItems: Array<RefundOrderItem> = []
for (let j: number = 0; j < dbItems.length; j++) {
const di = dbItems[j] as UTSJSONObject
const imgRaw = di.get('image_url')
const imgUrl = (imgRaw != null) ? (imgRaw as string) : '/static/default-product.png'
const productInfo: RefundProductInfo = {
images: [imgUrl]
} as RefundProductInfo
const specRaw = di.get('specifications')
const specifications = (specRaw != null) ? (specRaw as any) : null
const orderItem: RefundOrderItem = {
id: di.getString('id') ?? '',
product_name: di.getString('product_name') ?? '',
sku_specifications: specifications,
price: 0,
quantity: di.getNumber('quantity') ?? 1,
product: productInfo
} as RefundOrderItem
uiItems.push(orderItem)
}
const statusHistoryRaw = item.get('status_history')
const statusHistory = (statusHistoryRaw != null) ? (statusHistoryRaw as RefundStatusHistoryItem[]) : []
const refundItem: RefundType = {
id: item.getString('id') ?? '',
user_id: item.getString('user_id') ?? '',
order_id: item.getString('order_id') ?? '',
refund_no: item.getString('refund_no') ?? '',
refund_type: item.getNumber('refund_type') ?? 1,
refund_reason: item.getString('refund_reason') ?? '',
refund_amount: item.getNumber('refund_amount') ?? 0,
status: item.getNumber('status') ?? 1,
status_history: statusHistory,
created_at: item.getString('created_at') ?? '',
order: {
id: item.getString('order_id') ?? '',
order_no: orderObj.getString('order_no') ?? '',
created_at: orderObj.getString('created_at') ?? '',
order_items: uiItems
} as RefundOrderInfo
} as RefundType
newRefunds.push(refundItem)
}
if (loadMore) {
refunds.value.push(...newRefunds)
currentPage.value = page
} else {
refunds.value = newRefunds
currentPage.value = 1
}
hasMore.value = newRefunds.length === pageSize.value
} catch (err) {
console.error('加载售后记录异常:', err)
} finally {
isLoading.value = false
}
}
const loadTabCounts = async () => {
const userId = getCurrentUserId()
if (userId == '') return
try {
const processingRefunds = await supabaseService.getRefunds([1, 2], 1, 100)
tabCounts.value.processing = processingRefunds.length
} catch (err) {
console.error('加载计数异常:', err)
}
}
watch(activeTab, () => {
resetData()
loadRefunds(false)
})
onMounted(() => {
loadRefunds(false)
loadTabCounts()
})
const getStatusText = (status: number): string => {
if (status === 1) return '待处理'
if (status === 2) return '处理中'
if (status === 3) return '已完成'
if (status === 4) return '已取消'
if (status === 5) return '已拒绝'
return '未知状态'
}
const getStatusClass = (status: number): string => {
if (status === 1) return 'status-pending'
if (status === 2) return 'status-processing'
if (status === 3) return 'status-completed'
if (status === 4) return 'status-cancelled'
if (status === 5) return 'status-rejected'
return 'status-unknown'
}
// 获取商品图片
const getProductImage = (refund: RefundType): string => {
const firstItem = refund.order?.order_items?.[0]
if (firstItem?.product?.images == null || firstItem?.product?.images.length == 0) {
return '/static/default-product.png'
}
return firstItem.product!.images[0]
}
// 获取商品名称
const getProductName = (refund: RefundType): string => {
const items = refund.order?.order_items ?? []
if (items.length === 0) return '未知商品'
if (items.length === 1) {
return items[0].product_name
} else {
return `${items[0].product_name}等${items.length}件商品`
}
}
// 格式化时间
const formatTime = (timeStr?: string): string => {
if (timeStr == null || timeStr == '') return ''
const date = new Date(timeStr)
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
return `${month}-${day}`
}
const getCurrentStepIndex = (status: number): number => {
if (status === 1) return 0
if (status === 2) return 1
if (status === 3) return 2
if (status === 4) return 0
if (status === 5) return 1
return 0
}
type TimelineStepType = {
status: number,
title: string,
time: string,
active: boolean,
completed: boolean,
desc: string
}
const getTimelineSteps = (refund: RefundType): Array<TimelineStepType> => {
const steps: Array<TimelineStepType> = [
{ status: 0, title: '提交申请', time: refund.created_at, active: false, completed: false, desc: '' },
{ status: 1, title: '商家处理', time: '', active: false, completed: false, desc: '' },
{ status: 3, title: '退款完成', time: '', active: false, completed: false, desc: '' }
]
if (refund.status_history != null) {
for (let i: number = 0; i < refund.status_history.length; i++) {
const history = refund.status_history[i]
if (history.status === 1 || history.status === 2) {
steps[1].time = history.created_at ?? ''
steps[1].desc = history.remark ?? ''
} else if (history.status === 3) {
steps[2].time = history.created_at ?? ''
steps[2].desc = history.remark ?? ''
}
}
}
const currentStepIndex = getCurrentStepIndex(refund.status)
const result: Array<TimelineStepType> = []
for (let i: number = 0; i < steps.length; i++) {
const step = steps[i]
result.push({
status: step.status,
title: step.title,
time: step.time,
desc: step.desc,
active: i === currentStepIndex,
completed: i < currentStepIndex
})
}
return result
}
// 切换标签页
const changeTab = (tab: string) => {
activeTab.value = tab
}
// 加载更多
const loadMore = () => {
if (hasMore.value && !isLoading.value) {
loadRefunds(true)
}
}
// 查看订单
const viewOrder = (orderId: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/order-detail?id=${orderId}`
})
}
const doCancelRefund = async (refund: RefundType) => {
try {
const result = await supabaseService.createRefund({
id: refund.id,
status: 4
} as any)
if (result.success) {
refund.status = 4
loadTabCounts()
uni.showToast({
title: '已取消',
icon: 'success'
})
} else {
uni.showToast({
title: '取消失败',
icon: 'none'
})
}
} catch (err) {
console.error('取消退款失败:', err)
uni.showToast({
title: '取消失败',
icon: 'none'
})
}
}
const cancelRefund = (refund: RefundType) => {
uni.showModal({
title: '取消申请',
content: '确定要取消这个退款申请吗?',
success: (res) => {
if (res.confirm) {
doCancelRefund(refund)
}
}
})
}
// 联系客服
const contactService = (refund: RefundType) => {
// #ifndef MP-WEIXIN
uni.navigateTo({
url: `/pages/mall/service/chat?refundId=${refund.id}`
})
// #endif
}
// 评价服务
const reviewRefund = (refund: RefundType) => {
uni.navigateTo({
url: `/pages/mall/consumer/refund-review?id=${refund.id}`
})
}
const doDeleteRefund = async (refund: RefundType) => {
try {
const result = await supabaseService.deleteRefund(refund.id)
if (result) {
const newRefunds: Array<RefundType> = []
for (let i: number = 0; i < refunds.value.length; i++) {
if (refunds.value[i].id !== refund.id) {
newRefunds.push(refunds.value[i])
}
}
refunds.value = newRefunds
uni.showToast({
title: '删除成功',
icon: 'success'
})
} else {
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
} catch (err) {
console.error('删除记录失败:', err)
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
}
const deleteRefund = (refund: RefundType) => {
uni.showModal({
title: '删除记录',
content: '确定要删除这个售后记录吗?',
success: (res) => {
if (res.confirm) {
doDeleteRefund(refund)
}
}
})
}
// 申请售后
const applyRefund = () => {
uni.navigateTo({
url: '/pages/mall/consumer/apply-refund'
})
}
// 查看订单
const goToOrders = () => {
uni.switchTab({
url: '/pages/mall/consumer/orders'
})
}
// 返回
const goBack = () => {
uni.navigateBack()
}
</script>
<style scoped>
.refund-page {
display: flex;
flex-direction: column;
flex: 1;
background-color: #f5f5f5;
}
.refund-header {
background-color: #ffffff;
padding: 15px;
display: flex;
align-items: center;
border-bottom: 1px solid #e5e5e5;
}
.back-btn {
font-size: 20px;
color: #333333;
padding: 5px;
margin-right: 15px;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.refund-tabs {
background-color: #ffffff;
display: flex;
border-bottom: 1px solid #e5e5e5;
}
.refund-tab {
flex: 1;
padding: 15px;
text-align: center;
position: relative;
}
.refund-tab.active {
color: #007aff;
border-bottom: 2px solid #007aff;
}
.tab-text {
font-size: 16px;
color: #666666;
}
.refund-tab.active .tab-text {
color: #007aff;
font-weight: bold;
}
.tab-badge {
position: absolute;
top: 10px;
right: 20px;
background-color: #ff4757;
color: #ffffff;
font-size: 10px;
padding: 2px 5px;
border-radius: 8px;
min-width: 16px;
text-align: center;
}
.refund-content {
flex: 1;
}
.empty-refunds {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
background-color: #ffffff;
}
.empty-icon {
font-size: 80px;
margin-bottom: 20px;
}
.empty-text {
font-size: 16px;
color: #666666;
margin-bottom: 10px;
}
.empty-subtext {
font-size: 14px;
color: #999999;
margin-bottom: 30px;
}
.go-orders-btn {
background-color: #007aff;
color: #ffffff;
padding: 10px 40px;
border-radius: 25px;
font-size: 14px;
border: none;
}
.refund-item {
background-color: #ffffff;
margin-bottom: 10px;
padding: 15px;
}
.refund-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #f5f5f5;
}
.refund-no {
font-size: 14px;
color: #333333;
}
.refund-status {
font-size: 14px;
padding: 4px 10px;
border-radius: 12px;
color: #ffffff;
}
.status-pending {
background-color: #ff5000;
}
.status-processing {
background-color: #2196f3;
}
.status-completed {
background-color: #4caf50;
}
.status-cancelled {
background-color: #9e9e9e;
}
.status-rejected {
background-color: #f44336;
}
.order-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #f5f5f5;
}
.order-no {
font-size: 13px;
color: #666666;
}
.order-time {
font-size: 12px;
color: #999999;
}
.product-info {
display: flex;
margin-bottom: 15px;
}
.product-image {
width: 60px;
height: 60px;
border-radius: 5px;
margin-right: 15px;
}
.product-details {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.product-name {
font-size: 14px;
color: #333333;
line-height: 1.4;
margin-bottom: 5px;
}
.refund-reason {
font-size: 12px;
color: #666666;
margin-bottom: 8px;
}
.refund-amount {
display: flex;
align-items: flex-end;
}
.amount-label {
font-size: 13px;
color: #666666;
margin-right: 5px;
}
.amount-value {
font-size: 16px;
color: #ff4757;
font-weight: bold;
}
.timeline {
padding: 15px 0;
border-top: 1px solid #f5f5f5;
}
.timeline-step {
display: flex;
margin-bottom: 15px;
}
.timeline-step:last-child {
margin-bottom: 0;
}
.step-dot {
width: 12px;
height: 12px;
border-radius: 6px;
border: 2px solid #e5e5e5;
margin-right: 15px;
position: relative;
top: 3px;
}
.step-dot.active {
border-color: #007aff;
background-color: #007aff;
}
.step-dot.completed {
border-color: #4caf50;
background-color: #4caf50;
}
.step-info {
flex: 1;
}
.step-title {
font-size: 14px;
color: #333333;
font-weight: bold;
margin-bottom: 3px;
}
.step-time {
font-size: 12px;
color: #999999;
margin-bottom: 3px;
}
.step-desc {
font-size: 12px;
color: #666666;
}
.refund-actions {
display: flex;
justify-content: flex-end;
/* gap: 10px; removed for uni-app-x */
padding-top: 15px;
border-top: 1px solid #f5f5f5;
}
.action-btn {
margin-left: 10px;
padding: 6px 15px;
border-radius: 15px;
font-size: 12px;
border: 1px solid;
background-color: #ffffff;
}
.action-btn.cancel {
border-color: #666666;
color: #666666;
}
.action-btn.contact {
border-color: #007aff;
color: #007aff;
}
.action-btn.review {
border-color: #ff5000;
color: #ff5000;
}
.action-btn.delete {
border-color: #f44336;
color: #f44336;
}
.loading-more,
.no-more {
padding: 20px;
text-align: center;
background-color: #ffffff;
}
.loading-text,
.no-more-text {
color: #999999;
font-size: 14px;
}
.apply-btn-container {
background-color: #ffffff;
padding: 15px;
border-top: 1px solid #e5e5e5;
}
.apply-btn {
background-color: #007aff;
color: #ffffff;
height: 50px;
border-radius: 25px;
font-size: 16px;
font-weight: bold;
border: none;
}
</style>