Files
medical-mall/pages/mall/consumer/orders.uvue

1858 lines
54 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.
<!-- pages/mall/consumer/orders.uvue -->
<template>
<view class="orders-page">
<!-- 顶部标题栏 -->
<view class="orders-header" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="header-search full-width">
<input
class="search-input"
type="text"
placeholder="搜索订单号或商品名称"
:value="searchKeyword"
@input="onSearchInput"
@confirm="onSearchConfirm"
/>
<text v-if="searchKeyword" class="search-clear" @click="clearSearch">×</text>
<text v-else class="search-icon">🔍</text>
</view>
</view>
<!-- 订单状态筛选 -->
<view class="order-tabs-fixed-container">
<view
:class="['tab-item-fixed', { active: activeTab === 'all' }]"
@click="switchTab('all')"
>
<text class="tab-name">全部</text>
<view v-if="activeTab === 'all'" class="active-indicator"></view>
</view>
<scroll-view scroll-x="true" class="tab-scroll-mobile" :show-scrollbar="false" :scroll-with-animation="true">
<view class="tab-container-mobile">
<view
v-for="tab in orderTabsMobile"
:key="tab.id"
:class="['tab-item-mobile', { active: activeTab === tab.id }]"
@click="switchTab(tab.id)"
>
<text class="tab-name">{{ tab.name }}</text>
<text v-if="tab.count > 0" class="tab-count">{{ tab.count }}</text>
<view v-if="activeTab === tab.id" class="active-indicator"></view>
</view>
</view>
</scroll-view>
</view>
<!-- 订单列表 -->
<scroll-view
direction="vertical"
class="orders-content"
refresher-enabled
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="loadMore"
>
<!-- 空状态 -->
<view v-if="!loading && orders.length === 0" class="empty-orders">
<text class="empty-icon">📦</text>
<text class="empty-title">暂无订单</text>
<text class="empty-desc">去逛逛,发现心仪的商品</text>
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
</view>
<!-- 订单列表 -->
<view v-else class="order-list">
<view
v-for="order in orders"
:key="order.id"
class="order-card"
@click="viewOrderDetail(order.id)"
>
<!-- 订单头部:显示店铺名称 -->
<view class="order-card-header">
<view class="shop-info">
<text class="shop-icon">🏪</text>
<text class="shop-name">{{ order.shop_name != null && order.shop_name != '' ? order.shop_name : '自营店铺' }}</text>
<text class="arrow-right"></text>
</view>
<view class="status-row">
<text :class="['order-status', getStatusClass(order.status)]">
{{ getStatusText(order.status) }}
</text>
<text class="more-btn" @click.stop="showOrderMenu(order)">⋯</text>
</view>
</view>
<!-- 订单商品 -->
<view class="order-products">
<view
v-for="product in order.products"
:key="product.id"
class="order-product"
>
<image
class="product-image"
:src="product.image"
mode="aspectFill"
@click.stop="navigateToProduct(product)"
/>
<view class="product-info">
<view class="product-top-info">
<text class="product-name">{{ product.name }}</text>
<text class="product-spec">{{ product.spec }}</text>
</view>
<view class="product-footer">
<text class="product-price">¥{{ product.price }}</text>
<text class="product-quantity">x{{ product.quantity }}</text>
</view>
</view>
</view>
</view>
<!-- 订单汇总信息 -->
<view class="order-summary">
<text class="order-time">{{ formatDate(order.create_time) }}</text>
<view class="summary-right">
<text class="summary-label">共{{ order.products.length }}件商品 实付:</text>
<text class="summary-price">¥{{ order.total_amount }}</text>
</view>
</view>
<!-- 分享免单入口 (已付款订单显示: 待发货、待收货、已完成,且商家开启了分享免单) -->
<view v-if="order.status >= 2 && order.status <= 4 && isShareFreeEnabled(order.merchant_id)" class="share-free-row" @click.stop="shareForFree(order)">
<text class="share-free-icon">🎁</text>
<text class="share-free-text">分享免单</text>
<text class="share-free-tip">分享给好友,{{ getRequiredCount(order.merchant_id) }}人购买即可免单</text>
<text class="share-free-arrow"></text>
</view>
<!-- 订单操作 -->
<view class="order-actions" @click.stop="">
<view v-if="order.status === 1" class="action-buttons">
<button class="action-btn cancel" @click="cancelOrder(order.id)">取消订单</button>
<button class="action-btn pay" @click="payOrder(order.id)">立即支付</button>
</view>
<view v-if="order.status === 2" class="action-buttons">
<button class="action-btn remind" @click.stop="remindShipping(order.id)">提醒发货</button>
<button class="action-btn refund" @click.stop="onApplyRefund(order)">申请售后</button>
</view>
<view v-if="order.status === 3" class="action-buttons">
<button class="action-btn view" @click.stop="viewLogistics(order.id)">查看物流</button>
<button class="action-btn confirm" @click.stop="confirmReceipt(order.id)">确认收货</button>
<button class="action-btn refund" @click.stop="onApplyRefund(order)">申请售后</button>
</view>
<view v-if="order.status === 4" class="action-buttons">
<button class="action-btn review" @click.stop="goReview(order)">评价</button>
<button class="action-btn refund" @click.stop="onApplyRefund(order)">申请售后</button>
<button class="action-btn repurchase" @click.stop="repurchase(order)">再次购买</button>
</view>
<view v-if="order.status === 5" class="action-buttons">
<button class="action-btn view" @click.stop="viewOrderDetail(order.id)">查看详情</button>
</view>
<view v-if="order.status === 6" class="action-buttons">
<button class="action-btn view" @click.stop="viewOrderDetail(order.id)">查看详情</button>
<button class="action-btn cancel" @click.stop="cancelRefund(order.id)">取消退款</button>
<button class="action-btn refund" @click.stop="viewRefundProgress(order.id)">退款进度</button>
</view>
<view v-if="order.status === 7" class="action-buttons">
<button class="action-btn view" @click="viewOrderDetail(order.id)">查看详情</button>
<button class="action-btn repurchase" @click="repurchase(order)">再次购买</button>
</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="loadingMore" class="loading-more">
<view class="loading-spinner"></view>
<text>加载中...</text>
</view>
<view v-if="!hasMore && orders.length > 0" class="no-more">
<text>没有更多订单了</text>
</view>
<!-- 安全区域 -->
<view class="safe-area"></view>
</scroll-view>
<!-- 底部导航 -->
<view class="tabbar-placeholder"></view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, onMounted, computed } from 'vue'
import { onShow, onLoad, onBackPress } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts'
// 定义标签页类型
type OrderTabItem = {
id: string,
name: string,
count: number
}
// 定义订单产品类型
type OrderProduct = {
id: string,
name: string,
price: number,
image: string,
spec: string,
quantity: number
}
// 定义订单类型
type OrderItem = {
id: string,
order_no: string,
status: number,
create_time: string,
product_amount: number,
shipping_fee: number,
total_amount: number,
merchant_id: string,
shop_name: string,
products: OrderProduct[]
}
// 响应式数据
const orders = ref<OrderItem[]>([])
const allOrdersList = ref<OrderItem[]>([]) // Store all fetched orders for client-side filtering
const loading = ref<boolean>(false)
const loadingMore = ref<boolean>(false)
const hasMore = ref<boolean>(true)
const refreshing = ref<boolean>(false)
const page = ref<number>(1)
const activeTab = ref<string>('all')
const statusBarHeight = ref<number>(0)
const searchKeyword = ref<string>('')
// 商家推销配置缓存
const merchantShareFreeEnabled = ref<UTSJSONObject>(new UTSJSONObject())
const merchantRequiredCount = ref<UTSJSONObject>(new UTSJSONObject())
// 订单标签页 - 使用 ref 以便整体替换
const orderTabs = ref<OrderTabItem[]>([
{ id: 'all', name: '全部', count: 0 },
{ id: 'pending', name: '待付款', count: 0 },
{ id: 'shipping', name: '待发货', count: 0 },
{ id: 'delivering', name: '待收货', count: 0 },
{ id: 'completed', name: '已完成', count: 0 },
{ id: 'aftersale', name: '售后', count: 0 },
{ id: 'cancelled', name: '已取消', count: 0 }
])
// 模拟状态筛选(除去"全部"后的其余标签)
const orderTabsMobile = computed((): OrderTabItem[] => {
return orderTabs.value.filter((tab: OrderTabItem) => tab.id !== 'all')
})
// 辅助函数:获取状态码
const getStatusByTab = (tabId: string): number => {
if (tabId == 'pending') return 1
if (tabId == 'shipping') return 2
if (tabId == 'delivering') return 3
if (tabId == 'completed') return 4
if (tabId == 'cancelled') return 5
if (tabId == 'aftersale') return 6
return 0
}
// 格式化规格对象为友好的文本 - 必须在 parseSpecText 之前定义
function formatSpecObj(obj: any): string {
if (obj == null) return ''
if (typeof obj !== 'object') {
// 非对象类型直接返回字符串形式
if (typeof obj === 'string') return obj
if (typeof obj === 'number') return obj.toString()
return ''
}
try {
const objStr = JSON.stringify(obj)
const objParsed = JSON.parse(objStr)
if (objParsed == null) return ''
const specObj = objParsed as UTSJSONObject
// 使用 JSON.stringify 获取所有键
const specObjStr = JSON.stringify(specObj)
const specObjForKeys = JSON.parse(specObjStr) as UTSJSONObject
// 手动提取键值对
const parts: string[] = []
// 尝试获取已知字段
const colorVal = specObjForKeys.getString('Color')
if (colorVal != null && colorVal != '') {
parts.push('Color: ' + colorVal)
}
const sizeVal = specObjForKeys.getString('Size')
if (sizeVal != null && sizeVal != '') {
parts.push('Size: ' + sizeVal)
}
const defaultVal = specObjForKeys.getString('默认')
if (defaultVal != null && defaultVal != '') {
parts.push('默认: ' + defaultVal)
}
// 如果没有匹配到已知字段,尝试直接显示 JSON
if (parts.length === 0) {
// 尝试遍历对象
const objAny = specObjForKeys as any
if (objAny != null) {
return specObjStr.replace(/[{}"]/g, '').replace(/:/g, ': ').replace(/,/g, ' | ')
}
}
return parts.join(' | ')
} catch (e) {
return ''
}
}
// 辅助函数:解析规格文本
function parseSpecText(specs: any): string {
if (specs == null) return ''
if (typeof specs === 'string') {
// 如果是 JSON 字符串,尝试解析
if (specs.startsWith('{') || specs.startsWith('[')) {
try {
const parsed = JSON.parse(specs)
if (parsed == null) return specs
return formatSpecObj(parsed)
} catch (e) {
return specs
}
}
return specs
}
// 对于对象类型,格式化显示
return formatSpecObj(specs)
}
// 辅助函数:更新标签计数
const updateTabsCounts = (allOrders: OrderItem[]) => {
const countAll = allOrders.length
const countPending = allOrders.filter((o: OrderItem) => o.status === 1).length
const countShipping = allOrders.filter((o: OrderItem) => o.status === 2).length
const countDelivering = allOrders.filter((o: OrderItem) => o.status === 3).length
const countCompleted = allOrders.filter((o: OrderItem) => o.status === 4).length
const countCancelled = allOrders.filter((o: OrderItem) => o.status === 5).length
const countAftersale = allOrders.filter((o: OrderItem) => o.status === 6 || o.status === 7).length
orderTabs.value[0].count = countAll
orderTabs.value[1].count = countPending
orderTabs.value[2].count = countShipping
orderTabs.value[3].count = countDelivering
orderTabs.value[4].count = countCompleted
orderTabs.value[5].count = countAftersale
orderTabs.value[6].count = countCancelled
}
// 辅助函数:按标签筛选订单
const filterOrdersByTab = () => {
if (activeTab.value === 'all') {
orders.value = allOrdersList.value
} else if (activeTab.value === 'aftersale') {
orders.value = allOrdersList.value.filter((o: OrderItem) => {
return o.status === 6 || o.status === 7
})
} else {
const targetStatus = getStatusByTab(activeTab.value)
orders.value = allOrdersList.value.filter((o: OrderItem) => {
return o.status === targetStatus
})
}
}
// 检查商家是否开启分享免单
const isShareFreeEnabled = (merchantId: string): boolean => {
const val = merchantShareFreeEnabled.value.get(merchantId)
return val === true
}
// 获取商家要求的购买人数
const getRequiredCount = (merchantId: string): number => {
const val = merchantRequiredCount.value.get(merchantId)
if (val != null && typeof val === 'number') {
return val as number
}
return 4
}
// 加载商家推销配置
const loadMerchantPromotionConfigs = async (orderList: OrderItem[]) => {
// 收集所有唯一的商家ID
const merchantIds = new Set<string>()
for (let i = 0; i < orderList.length; i++) {
const merchantId = orderList[i].merchant_id
const existingVal = merchantShareFreeEnabled.value.get(merchantId)
if (merchantId != null && merchantId !== '' && existingVal == null) {
merchantIds.add(merchantId)
}
}
// 批量加载商家配置
const merchantIdArray = Array.from(merchantIds)
for (let i = 0; i < merchantIdArray.length; i++) {
const merchantIdRaw = merchantIdArray[i]
const merchantId = merchantIdRaw as string
try {
const config = await supabaseService.getMerchantPromotionConfig(merchantId)
const promotionEnabled = config.get('promotion_enabled')
const shareFreeEnabled = config.get('share_free_enabled')
const requiredCount = config.get('required_count')
const isEnabled: any =
(promotionEnabled === true || promotionEnabled === 'true') &&
(shareFreeEnabled === true || shareFreeEnabled === 'true')
merchantShareFreeEnabled.value.set(merchantId, isEnabled)
if (requiredCount != null) {
merchantRequiredCount.value.set(merchantId, requiredCount)
} else {
merchantRequiredCount.value.set(merchantId, 4 as any)
}
} catch (e) {
console.error('加载商家推销配置失败:', merchantId, e)
merchantShareFreeEnabled.value.set(merchantId, false as any)
merchantRequiredCount.value.set(merchantId, 4 as any)
}
}
}
// 加载订单数据
const loadOrders = async () => {
loading.value = true
try {
// Fetch all orders from Supabase (status=0)
const fetchedOrders = await supabaseService.getOrders(0)
console.log('[loadOrders] 获取到订单数量:', fetchedOrders.length)
// Map to View Model
const mappedOrders: OrderItem[] = []
for (let i = 0; i < fetchedOrders.length; i++) {
const order = fetchedOrders[i]
// 使用 JSON 序列化转换
const orderStr = JSON.stringify(order)
const orderParsed = JSON.parse(orderStr)
if (orderParsed == null) continue
const orderObj = orderParsed as UTSJSONObject
const itemsRaw = orderObj.get('ml_order_items')
const productsList: OrderProduct[] = []
console.log('[loadOrders] 订单商品数据:', itemsRaw)
if (itemsRaw != null) {
// 先检查是否为数组
if (Array.isArray(itemsRaw)) {
const items = itemsRaw as any[]
console.log('[loadOrders] 商品数量:', items.length)
for (let j = 0; j < items.length; j++) {
const item = items[j]
const itemStr = JSON.stringify(item)
const itemParsed = JSON.parse(itemStr)
if (itemParsed == null) continue
const itemObj = itemParsed as UTSJSONObject
const specRaw = itemObj.get('specifications')
const specText = specRaw != null ? parseSpecText(specRaw) : ''
const productId = itemObj.getString('product_id')
const productName = itemObj.getString('product_name')
const price = itemObj.getNumber('price')
const imageUrl = itemObj.getString('image_url')
const quantity = itemObj.getNumber('quantity')
console.log('[loadOrders] 商品:', productName, '图片:', imageUrl, '规格:', specText)
const productItem: OrderProduct = {
id: productId ?? '',
name: productName ?? '未知商品',
price: price ?? 0,
image: imageUrl ?? '/static/default-product.png',
spec: specText,
quantity: quantity ?? 1
}
productsList.push(productItem)
}
}
}
const orderId = orderObj.getString('id')
const orderNo = orderObj.getString('order_no')
const orderStatus = orderObj.getNumber('order_status')
const createdAt = orderObj.getString('created_at')
const productAmount = orderObj.getNumber('product_amount')
const shippingFee = orderObj.getNumber('shipping_fee')
const totalAmount = orderObj.getNumber('total_amount')
const paidAmount = orderObj.getNumber('paid_amount')
const merchantId = orderObj.getString('merchant_id')
// 从关联查询的 ml_shops 表获取店铺名称
let shopName = '自营店铺'
const shopsRaw = orderObj.get('ml_shops')
if (shopsRaw != null) {
const shopStr = JSON.stringify(shopsRaw)
const shopParsed = JSON.parse(shopStr)
if (shopParsed != null) {
const shopObj = shopParsed as UTSJSONObject
const shopNameFromDb = shopObj.getString('shop_name')
if (shopNameFromDb != null && shopNameFromDb != '') {
shopName = shopNameFromDb
}
}
} else if (merchantId != null && merchantId != '') {
shopName = '商家店铺'
}
console.log('[loadOrders] 订单号:', orderNo, '店铺:', shopName, '商品数:', productsList.length)
// 如果没有商品数据,添加一个占位商品
if (productsList.length === 0) {
const placeholderProduct: OrderProduct = {
id: 'placeholder',
name: '订单商品',
price: totalAmount ?? paidAmount ?? 0,
image: '/static/default-product.png',
spec: '',
quantity: 1
}
productsList.push(placeholderProduct)
}
const mappedOrder: OrderItem = {
id: orderId ?? '',
order_no: orderNo ?? '',
status: orderStatus ?? 1,
create_time: createdAt ?? '',
product_amount: productAmount ?? 0,
shipping_fee: shippingFee ?? 0,
total_amount: totalAmount ?? paidAmount ?? 0,
merchant_id: merchantId ?? '',
shop_name: shopName,
products: productsList
}
mappedOrders.push(mappedOrder)
}
// Sort by created_at desc - 直接使用 OrderItem 类型访问属性
mappedOrders.sort((a: OrderItem, b: OrderItem) => {
const timeA = new Date(a.create_time).getTime()
const timeB = new Date(b.create_time).getTime()
return timeB - timeA
})
allOrdersList.value = mappedOrders
// Update tab counts
updateTabsCounts(mappedOrders)
// Apply current tab filter
filterOrdersByTab()
// 加载商家推销配置
loadMerchantPromotionConfigs(mappedOrders)
} catch (err) {
console.error('加载订单异常:', err)
uni.showToast({ title: '加载订单失败', icon: 'none' })
} finally {
loading.value = false
}
}
// 生命周期
onLoad((options) => {
// 初始化状态栏高度
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight ?? 0
if (options == null) return
const statusVal = options['status']
if (statusVal != null) {
const status = statusVal as string
if (['all', 'pending', 'shipping', 'delivering', 'completed', 'aftersale', 'cancelled'].includes(status)) {
activeTab.value = status
}
}
const typeVal = options['type']
if (typeVal != null) {
const type = typeVal as string
if (type === 'pending') activeTab.value = 'pending'
else if (type === 'shipped') activeTab.value = 'delivering' // 映射到待收货
else if (type === 'review') activeTab.value = 'completed' // 映射到已完成
else if (type === 'refund') activeTab.value = 'aftersale' // 退款/售后跳转到售后标签页
}
})
onShow(() => {
loadOrders()
})
const formatDate = (isoString: string): string => {
if (isoString == '') return ''
const date = new Date(isoString)
return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
}
// 辅助函数:获取当前订单数据(必须在 performSearch 之前定义)
function getCurrentOrderData(): OrderItem[] {
return allOrdersList.value
}
// 搜索执行函数(必须在 onSearchInput 等之前定义)
const performSearch = () => {
const keyword = searchKeyword.value.trim().toLowerCase()
if (keyword == '') {
loadOrders()
return
}
// 在当前订单数据中搜索
const allOrders = getCurrentOrderData()
const filtered = allOrders.filter((order: any) => {
const orderObj = order as Record<string, any>
// 搜索订单号
const orderNo = orderObj['order_no'] as string
if (orderNo != null && orderNo.toLowerCase().includes(keyword)) {
return true
}
// 搜索商品名称
const products = orderObj['products']
if (products != null && Array.isArray(products)) {
return products.some((product: any) => {
const productObj = product as Record<string, any>
const name = productObj['name'] as string
return name != null && name.toLowerCase().includes(keyword)
})
}
return false
})
orders.value = filtered
}
// 搜索相关函数
const onSearchInput = (e: any) => {
const eObj = e as Record<string, any>
const detail = eObj['detail'] as Record<string, any>
searchKeyword.value = detail['value'] as string
performSearch()
}
const onSearchConfirm = () => {
performSearch()
}
const clearSearch = () => {
searchKeyword.value = ''
performSearch()
}
const formatSpec = (specs: any): string => {
if (specs == null) return ''
if (typeof specs === 'string') return specs
if (typeof specs === 'object') {
return JSON.stringify(specs)
}
return ''
}
// 切换标签
const switchTab = (tabId: string) => {
activeTab.value = tabId
filterOrdersByTab()
}
// 获取状态文本
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 '已取消'
if (status == 6) return '退款中'
if (status == 7) return '已退款'
return '未知状态'
}
// 获取状态类名
const getStatusClass = (status: number): string => {
if (status == 1) return 'status-pending'
if (status == 2) return 'status-shipping'
if (status == 3) return 'status-delivering'
if (status == 4) return 'status-completed'
if (status == 5) return 'status-cancelled'
if (status == 6) return 'status-refunding'
if (status == 7) return 'status-refunded'
return 'status-unknown'
}
// 联系卖家
const contactSeller = (order: OrderItem) => {
if (order.merchant_id != '') {
uni.navigateTo({
url: `/pages/mall/consumer/chat?merchantId=${order.merchant_id}`
})
} else {
uni.showToast({
title: '暂无卖家联系方式',
icon: 'none'
})
}
}
// 删除订单
const deleteOrder = (orderId: string) => {
uni.showModal({
title: '删除订单',
content: '确定要删除此订单吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({ title: '删除中...' })
supabaseService.deleteOrder(orderId).then(() => {
uni.hideLoading()
uni.showToast({
title: '订单已删除',
icon: 'success'
})
loadOrders()
}).catch(() => {
uni.hideLoading()
uni.showToast({
title: '删除失败',
icon: 'none'
})
})
}
}
})
}
// 下拉刷新
const onRefresh = () => {
refreshing.value = true
setTimeout(() => {
loadOrders()
refreshing.value = false
uni.showToast({
title: '刷新成功',
icon: 'success'
})
}, 1000)
}
// 上拉加载更多
const loadMore = () => {
if (loadingMore.value || !hasMore.value) return
// 暂未实现分页,直接返回
hasMore.value = false
}
// 订单操作函数
const cancelOrder = (orderId: string) => {
uni.showModal({
title: '确认取消',
content: '确定要取消此订单吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({ title: '取消中...' })
supabaseService.cancelOrder(orderId).then((success) => {
uni.hideLoading()
if (success) {
uni.showToast({
title: '订单已取消',
icon: 'success'
})
loadOrders()
} else {
uni.showToast({
title: '取消失败',
icon: 'none'
})
}
}).catch(() => {
uni.hideLoading()
uni.showToast({
title: '取消失败',
icon: 'none'
})
})
}
}
})
}
const payOrder = (orderId: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${orderId}`
})
}
const remindShipping = async (orderId: string) => {
// 基础提醒
uni.showLoading({ title: '正在提醒...' })
try {
// 查找订单中的商家ID
const order = orders.value.find(o => o.id === orderId)
if (order != null) {
const merchantId = order.merchant_id
const orderNo = order.order_no
if (merchantId != '') {
// 向商家发送自动催单消息
const message = `你好,我的订单[${orderNo}]还没有发货,请尽快安排,谢谢。`
const success = await supabaseService.sendChatMessage(message, merchantId)
if (success) {
console.log('催单消息发送成功')
} else {
console.warn('催单消息发送失败,可能是由于网络原因')
}
}
}
} catch (e) {
console.error('提醒发货异常:', e)
} finally {
uni.hideLoading()
}
uni.showToast({
title: '已提醒卖家发货',
icon: 'success'
})
}
const viewLogistics = (orderId: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/logistics?orderId=${orderId}`
})
}
// goReview 必须在 doConfirmReceipt 之前定义,因为 doConfirmReceipt 会调用它
const goReview = (order: OrderItem) => {
const productIds = order.products.map((p: OrderProduct): string => {
return p.id
}).join(',')
const orderId = order.id
uni.navigateTo({
url: `/pages/mall/consumer/review?orderId=${orderId}&productIds=${productIds}`
})
}
const doConfirmReceipt = async (orderId: string) => {
uni.showLoading({ title: '处理中...' })
try {
const result = await supabaseService.confirmReceipt(orderId)
uni.hideLoading()
if (result.success) {
uni.showToast({
title: '收货成功',
icon: 'success'
})
// 更新 allOrdersList 中的订单状态
const allIndex = allOrdersList.value.findIndex((o: OrderItem): boolean => o.id === orderId)
if (allIndex !== -1) {
allOrdersList.value[allIndex].status = 4
allOrdersList.value = [...allOrdersList.value]
}
// 更新标签计数
updateTabsCounts(allOrdersList.value)
// 重新应用当前标签筛选
filterOrdersByTab()
// 跳转到评价页面
setTimeout(() => {
const order = allOrdersList.value.find((o: OrderItem): boolean => o.id === orderId)
if (order != null) {
goReview(order)
}
}, 1000)
} else {
uni.showToast({
title: result.error ?? '确认收货失败',
icon: 'none'
})
}
} catch (e) {
uni.hideLoading()
uni.showToast({
title: '系统异常',
icon: 'none'
})
}
}
const confirmReceipt = (orderId: string) => {
uni.showModal({
title: '确认收货',
content: '请确认您已收到商品,且商品无误',
success: (res) => {
if (res.confirm) {
doConfirmReceipt(orderId)
}
}
})
}
const repurchase = (order: OrderItem) => {
const products = order.products
if (products.length === 0) {
uni.showToast({
title: '订单无商品',
icon: 'none'
})
return
}
uni.showLoading({ title: '处理中...' })
let completed = 0
const total = products.length
let successCount = 0
for (let i = 0; i < products.length; i++) {
const product = products[i]
const productId = product.id
const merchantId = order.merchant_id
if (productId != null && productId !== '') {
supabaseService.addToCart(productId, 1, '', merchantId ?? '').then((success: boolean) => {
completed++
if (success) successCount++
if (completed === total) {
uni.hideLoading()
if (successCount > 0) {
uni.showToast({
title: `已添加${successCount}件商品`,
icon: 'success'
})
} else {
uni.showToast({
title: '添加失败',
icon: 'none'
})
}
}
}).catch(() => {
completed++
if (completed === total) {
uni.hideLoading()
if (successCount > 0) {
uni.showToast({
title: `已添加${successCount}件商品`,
icon: 'success'
})
} else {
uni.showToast({
title: '添加失败',
icon: 'none'
})
}
}
})
} else {
completed++
if (completed === total) {
uni.hideLoading()
if (successCount > 0) {
uni.showToast({
title: `已添加${successCount}件商品`,
icon: 'success'
})
} else {
uni.showToast({
title: '添加失败',
icon: 'none'
})
}
}
}
}
}
const viewOrderDetail = (orderId: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/order-detail?id=${orderId}`
})
}
const onApplyRefund = (order: OrderItem) => {
const orderId = order.id
uni.navigateTo({
url: `/pages/mall/consumer/apply-refund?orderId=${orderId}`
})
}
const viewRefundProgress = (orderId: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/refund?orderId=${orderId}`
})
}
const doCancelRefund = async (orderId: string) => {
uni.showLoading({ title: '处理中...' })
const result = await supabaseService.cancelRefund(orderId)
uni.hideLoading()
if (result.success) {
uni.showToast({ title: '已取消退款', icon: 'success' })
loadOrders()
} else {
uni.showToast({ title: result.message, icon: 'none' })
}
}
// 取消退款申请
const cancelRefund = (orderId: string) => {
uni.showModal({
title: '确认取消',
content: '确定要取消退款申请吗?',
success: (res) => {
if (res.confirm) {
doCancelRefund(orderId)
}
}
})
}
// 处理订单操作
const handleOrderAction = (order: OrderItem, action: string) => {
if (action === '取消订单') {
cancelOrder(order.id)
} else if (action === '联系卖家') {
contactSeller(order)
} else if (action === '提醒发货') {
remindShipping(order.id)
} else if (action === '申请退款' || action === '申请售后') {
onApplyRefund(order)
} else if (action === '查看物流') {
viewLogistics(order.id)
} else if (action === '确认收货') {
confirmReceipt(order.id)
} else if (action === '再次购买') {
repurchase(order)
} else if (action === '删除订单') {
deleteOrder(order.id)
} else if (action === '退款进度') {
viewRefundProgress(order.id)
} else if (action === '取消退款') {
cancelRefund(order.id)
}
}
// 显示订单操作菜单
const showOrderMenu = (order: OrderItem) => {
const status = order.status
let actions: string[] = []
if (status === 1) {
actions = ['取消订单', '联系卖家']
} else if (status === 2) {
actions = ['提醒发货', '申请退款', '联系卖家']
} else if (status === 3) {
actions = ['查看物流', '确认收货', '申请退款', '联系卖家']
} else if (status === 4) {
actions = ['申请售后', '再次购买', '联系卖家']
} else if (status === 5) {
actions = ['删除订单', '再次购买', '联系卖家']
} else if (status === 6) {
actions = ['取消退款', '退款进度', '联系卖家']
} else if (status === 7) {
actions = ['再次购买', '联系卖家']
}
uni.showActionSheet({
itemList: actions,
success: (res) => {
const action = actions[res.tapIndex]
handleOrderAction(order, action)
}
})
}
// 导航函数
const navigateToSearch = () => {
uni.navigateTo({ url: '/pages/mall/consumer/search' })
}
const navigateToProduct = (product: OrderProduct) => {
const productId = product.id
uni.navigateTo({ url: `/pages/mall/consumer/product-detail?id=${productId}` })
}
const goShopping = () => {
uni.switchTab({ url: '/pages/main/index' })
}
const shareForFree = async (order: OrderItem) => {
if (order.products.length === 0) {
uni.showToast({ title: '没有可分享的商品', icon: 'none' })
return
}
const firstProduct = order.products[0]
try {
uni.showLoading({ title: '创建分享...' })
const result = await supabaseService.createShareRecord(
firstProduct.id,
order.id,
'',
firstProduct.name,
firstProduct.image,
firstProduct.price
)
uni.hideLoading()
const shareIdRaw = result.get('id')
const shareCodeRaw = result.get('share_code')
if (shareIdRaw != null && shareCodeRaw != null) {
const shareId = shareIdRaw as string
const shareCode = shareCodeRaw as string
uni.showModal({
title: '分享成功',
content: `您的分享码: ${shareCode}\n分享给好友当有4人购买后即可免单`,
confirmText: '查看详情',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: `/pages/mall/consumer/share/detail?id=${shareId}` })
}
}
})
} else {
uni.showToast({ title: '分享创建失败', icon: 'none' })
}
} catch (e) {
uni.hideLoading()
console.error('[shareForFree] 创建分享失败:', e)
uni.showToast({ title: '分享失败', icon: 'none' })
}
}
</script>
<style>
.orders-page {
display: flex;
flex-direction: column;
/* 采用相对于窗口的 100% 方案 */
width: 100%;
height: 100%;
/* App/小程序通过 fixed 解决根容器撑满问题H5/电脑端建议使用 flex 1 */
/* #ifdef APP-PLUS || MP-WEIXIN */
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
/* #endif */
/* #ifdef H5 */
position: relative;
/* #endif */
background-color: #f5f5f5;
overflow: hidden;
}
/* 头部:确保显式可见且不被遮挡 */
.orders-header {
background-color: #ffffff;
padding: 10px 15px;
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
border-bottom: 1px solid #eeeeee;
z-index: 999;
position: relative;
box-sizing: border-box;
flex-shrink: 0;
}
.header-search.full-width {
display: flex;
flex-direction: row;
align-items: center;
position: relative;
width: 100%;
flex: 1;
}
.search-input {
flex: 1;
height: 36px;
line-height: 36px;
border: 1px solid #dddddd;
border-radius: 18px;
padding: 0 40px 0 16px;
font-size: 14px;
background-color: #f5f5f5;
color: #333333;
width: 100%;
display: flex;
align-items: center;
}
.search-input::placeholder {
color: #999;
font-size: 12px;
}
.search-input:focus {
border-color: #ff5000;
background-color: white;
}
.search-icon {
position: absolute;
right: 12px;
font-size: 18px;
color: #999;
}
.search-clear {
position: absolute;
right: 12px;
font-size: 20px;
color: #999;
width: 20px;
height: 20px;
line-height: 18px;
text-align: center;
border-radius: 10px; /* fixed 50% */
background-color: #ddd;
/* cursor: pointer; removed */
}
/* 标签页容器:确保在安卓下层级正确且不随内容滚动 */
.order-tabs-fixed-container {
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
z-index: 100 !important;
flex-shrink: 0;
position: relative;
}
/* 安卓端滚动修复:关键容器 */
.orders-content {
flex: 1;
width: 100%;
/* 极致方案:不再设置 height: 0改用 flex: 1 填满,并显式设置 scroll-y 的表现 */
min-height: 0;
flex-shrink: 1;
background-color: #f5f5f5;
}
/* 顶部内容区域必须显式不可伸缩,防止撑占滚动空间 */
.orders-header, .order-tabs-fixed-container {
flex-shrink: 0;
width: 100%;
}
.tab-item-fixed {
padding: 0 15px;
text-align: center;
position: relative;
display: flex;
justify-content: center;
align-items: center;
white-space: nowrap;
flex-shrink: 0;
min-width: 60px;
height: 100%;
}
.tab-item-fixed.active {
color: #ff5000;
font-weight: bold;
}
.tab-scroll-mobile {
flex: 1;
white-space: nowrap;
/* 移除 width: 0改用更稳健的安卓适配方式 */
display: flex;
flex-direction: row;
}
.tab-container-mobile {
display: flex;
flex-direction: row;
/* 加上这个确保容器能够横向撑开 */
flex-wrap: nowrap;
}
.tab-item-mobile {
padding: 15px 15px;
text-align: center;
position: relative;
display: flex;
flex-direction: row; /* 显式声明 */
justify-content: center;
align-items: center;
white-space: nowrap;
flex-shrink: 0; /* 绝对不能被压缩,否则无法滚动 */
}
.tab-item-mobile.active {
color: #ff5000;
font-weight: bold;
}
.active-indicator {
position: absolute;
bottom: 0px;
left: 10px;
right: 10px;
height: 3px;
background-color: #ff5000;
border-radius: 2px;
}
.tab-name {
font-size: 14px;
}
.tab-count {
margin-left: 4px;
background-color: #ff5000;
color: white;
font-size: 10px;
padding: 1px 4px;
border-radius: 8px;
min-width: 12px;
text-align: center;
}
/* 内容区 */
.orders-content {
flex: 1;
width: 100%;
height: 0; /* 关键:强制让 flex:1 在安卓端生效,防止内容撑开父容器 */
}
/* 空状态 */
.empty-orders {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
}
.empty-icon {
font-size: 80px;
color: #ddd;
margin-bottom: 20px;
}
.empty-title {
font-size: 18px;
color: #666;
margin-bottom: 10px;
}
.empty-desc {
font-size: 14px;
color: #999;
margin-bottom: 30px;
}
.go-shopping-btn {
background-color: #ff5000;
color: white;
border: none;
border-radius: 25px;
padding: 10px 40px;
font-size: 16px;
}
/* 订单列表 */
.order-list {
padding: 10px;
width: 100%;
box-sizing: border-box;
}
.order-card {
background-color: white;
border-radius: 10px;
margin-bottom: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
flex-shrink: 0;
box-sizing: border-box;
}
/* 订单头部 */
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f5f5f5;
}
.order-no {
font-size: 14px;
color: #666;
}
.order-status {
font-size: 14px;
font-weight: bold;
}
.status-pending {
color: #ff5000;
}
.status-shipping {
color: #ff9500;
}
.status-delivering {
color: #007aff;
}
.status-completed {
color: #34c759;
}
.status-cancelled {
color: #999;
}
.status-refunding {
color: #ff5000;
}
.status-refunded {
color: #999;
}
/* 订单商品 */
.order-products {
padding: 15px;
}
.order-product {
display: flex;
flex-direction: row; /* 显式声明横向排列 */
margin-bottom: 15px;
width: 100%;
}
.order-product:last-child {
margin-bottom: 0;
}
.product-image {
width: 80px;
height: 80px;
border-radius: 8px;
margin-right: 12px;
flex-shrink: 0; /* 防止图片被压缩 */
}
.product-info {
flex: 1; /* 占据右侧剩余所有空间 */
display: flex;
flex-direction: column;
justify-content: space-between;
height: 80px;
overflow: hidden; /* 防止文字溢出 */
}
.product-top-info {
display: flex;
flex-direction: column;
width: 100%;
}
.product-name {
font-size: 14px;
color: #333;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
lines: 2;
}
.product-spec {
font-size: 12px;
color: #999;
margin-top: 2px;
}
.product-footer {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.product-price {
font-size: 16px;
color: #ff5000;
font-weight: bold;
}
.product-quantity {
font-size: 13px;
color: #999;
}
/* 订单信息 */
.order-info {
padding: 15px;
border-top: 1px solid #f5f5f5;
border-bottom: 1px solid #f5f5f5;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-row.total {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #f5f5f5;
}
.info-label {
font-size: 14px;
color: #666;
}
.info-value {
font-size: 14px;
color: #333;
}
.total-price {
font-size: 18px;
color: #ff5000;
font-weight: bold;
}
/* 订单操作 */
.order-actions {
padding: 15px;
}
.action-buttons {
display: flex;
justify-content: flex-end;
/* gap: 10px; removed */
}
.action-btn {
padding: 6px 15px;
border-radius: 15px;
font-size: 13px;
border: 1px solid;
background-color: transparent; /* fixed background: none */
margin-left: 10px; /* alternative to gap */
}
.action-btn.cancel {
color: #666;
border-color: #ccc;
}
.action-btn.pay {
color: #ff5000;
border-color: #ff5000;
}
.action-btn.remind {
color: #666;
border-color: #ccc;
}
.action-btn.view {
color: #666;
border-color: #ccc;
}
.action-btn.confirm {
color: #34c759;
border-color: #34c759;
}
.action-btn.refund {
color: #666;
border-color: #ccc;
}
.action-btn.cancel {
color: #ff6b6b;
border-color: #ff6b6b;
}
.action-btn.review {
color: #ff9500;
border-color: #ff9500;
}
.action-btn.repurchase {
color: #ff5000;
border-color: #ff5000;
}
/* 加载更多 */
.loading-more {
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid #f0f5ff;
border-top-color: #ff5000;
border-radius: 12px;
margin-bottom: 10px;
}
.no-more {
text-align: center;
color: #999;
font-size: 13px;
padding: 20px 0;
}
/* 安全区域 */
.safe-area {
height: 20px;
}
/* 底部导航占位 */
.tabbar-placeholder {
height: 50px;
background-color: #f5f5f5;
}
/* 响应式适配 */
@media screen and (min-width: 768px) {
.order-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 100%;
max-width: 1200px;
margin: 0 auto;
justify-content: flex-start;
align-content: flex-start;
box-sizing: border-box;
}
.order-card {
width: 48%;
margin: 0 1% 20px 1%;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
flex: none;
box-sizing: border-box;
}
}
@media screen and (min-width: 1200px) {
.order-card {
width: 31%;
margin: 0 1% 20px 1%;
flex: none;
box-sizing: border-box;
}
}
/* 订单卡片新样式 */
.order-card-header {
padding: 12px 15px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #f9f9f9;
}
.status-row {
display: flex;
flex-direction: row;
align-items: center;
}
.more-btn {
font-size: 18px;
color: #999;
margin-left: 8px;
padding: 0 5px;
}
.shop-info {
display: flex;
flex-direction: row;
align-items: center;
}
.shop-icon {
font-size: 16px;
margin-right: 6px;
}
.shop-name {
font-size: 14px;
font-weight: bold;
color: #333;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
lines: 1;
}
.arrow-right {
font-size: 14px;
color: #ccc;
margin-left: 4px;
}
.product-title-row, .product-spec-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
}
.product-title-row {
margin-bottom: 4px;
}
.product-spec-row {
margin-top: 2px;
}
.order-summary {
padding: 10px 15px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
border-top: 1px solid #f9f9f9;
}
.order-time {
font-size: 12px;
color: #999;
}
.summary-right {
display: flex;
flex-direction: row;
align-items: center;
}
.summary-label {
font-size: 12px;
color: #666;
margin-right: 5px;
}
.summary-price {
font-size: 16px;
font-weight: bold;
color: #333;
}
/* 分享免单入口样式 */
.share-free-row {
margin: 0 15px;
padding: 12px 15px;
background: linear-gradient(135deg, #fff5f0 0%, #ffecd2 100%);
border-radius: 8px;
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 10px;
}
.share-free-icon {
font-size: 20px;
margin-right: 8px;
}
.share-free-text {
font-size: 14px;
font-weight: bold;
color: #ff6b35;
margin-right: 8px;
}
.share-free-tip {
flex: 1;
font-size: 12px;
color: #ff8c42;
}
.share-free-arrow {
font-size: 16px;
color: #ff8c42;
}
@media screen and (max-width: 320px) {
.tab-item {
padding: 0 10px;
margin-right: 5px;
}
.action-btn {
padding: 6px 10px;
font-size: 12px;
}
}
@media screen and (min-width: 415px) {
.order-card {
margin-bottom: 15px;
}
}
</style>