Files
medical-mall/pages/mall/consumer/orders.uvue
2026-05-14 15:28:09 +08:00

1950 lines
56 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" :style="ordersPageStyle">
<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
class="orders-content"
:style="ordersContentStyle"
scroll-y="true"
:scroll-top="scrollTop"
refresher-enabled
:refresher-triggered="refreshing"
:lower-threshold="120"
@refresherrefresh="onRefresh"
@scroll="onOrdersScroll"
@scrolltolower="loadMore"
>
<view v-if="!loading && loadedOrders.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="virtual-list">
<view class="virtual-top-spacer" :style="{ height: topSpacerHeight + 'px' }"></view>
<view
v-for="order in visibleOrders"
:key="order.id"
class="virtual-order-item"
:style="{ height: getOrderCardHeight(order) + 'px' }"
>
<view class="order-card" :style="{ height: getOrderCardBodyHeight(order) + 'px' }" @click="viewOrderDetail(order.id)">
<view class="order-card-header">
<view class="shop-info">
<view class="shop-title-row">
<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>
<text class="order-meta-line">{{ getOrderMetaLine(order) }}</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 getOrderPreviewProducts(order)"
:key="product.id"
class="order-product"
>
<image
class="product-image"
:src="product.image"
mode="aspectFill"
lazy-load="true"
@click.stop="navigateToProduct(product)"
/>
<view class="product-info">
<view class="product-top-info">
<text class="product-name">{{ product.name }}</text>
<view class="product-spec-row">
<text v-if="shouldShowProductSpecTag(product)" class="product-spec-tag">{{ getProductSpecTag(product) }}</text>
<text class="product-spec">{{ getProductSpecDisplay(product) }}</text>
</view>
</view>
<view class="product-footer">
<text class="product-price">¥{{ product.price }}</text>
<text class="product-quantity">x{{ product.quantity }}</text>
</view>
</view>
</view>
<view class="order-preview-bottom" @click.stop="viewOrderDetail(order.id)">
<text class="order-preview-hint">{{ getOrderPreviewHint(order) }}</text>
<text class="order-preview-link">查看详情 ></text>
</view>
</view>
<view class="order-summary">
<text class="order-time">{{ formatDate(order.create_time) }}</text>
<view class="summary-right">
<text class="summary-shipping">含运费¥{{ formatAmount(order.shipping_fee) }}</text>
<text class="summary-label">共{{ order.products.length }}件商品 实付:</text>
<text class="summary-price">¥{{ order.total_amount }}</text>
</view>
</view>
<view class="share-free-slot">
<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 v-else class="share-free-placeholder"></view>
</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.stop="viewOrderDetail(order.id)">查看详情</button>
<button class="action-btn repurchase" @click.stop="repurchase(order)">再次购买</button>
</view>
</view>
</view>
</view>
<view class="virtual-bottom-spacer" :style="{ height: bottomSpacerHeight + 'px' }"></view>
</view>
<view v-if="loadingMore" class="loading-more">
<view class="loading-spinner"></view>
<text>加载中...</text>
</view>
<view v-if="!hasMore && loadedOrders.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 { computed, ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts'
import { goToLogin } from '@/utils/utils.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 ORDER_CARD_GAP = 10
const ORDER_HEADER_HEIGHT = 56
const ORDER_PRODUCTS_HEIGHT_SINGLE = 104
const ORDER_PRODUCTS_HEIGHT_DOUBLE = 166
const ORDER_SUMMARY_HEIGHT = 38
const ORDER_SHARE_SLOT_HEIGHT = 40
const ORDER_ACTIONS_HEIGHT = 54
const ORDER_CARD_AVERAGE_HEIGHT = 332
const ORDER_VISIBLE_PRODUCT_LIMIT = 2
const VIRTUAL_BUFFER = 5
const PAGE_SIZE = 10
const PREFETCH_THRESHOLD = 5
const loadedOrders = ref<OrderItem[]>([])
const visibleOrders = ref<OrderItem[]>([])
const loading = ref<boolean>(false)
const loadingMore = ref<boolean>(false)
const refreshing = ref<boolean>(false)
const hasMore = ref<boolean>(true)
const initialized = ref<boolean>(false)
const pageSize = ref<number>(PAGE_SIZE)
const nextCursor = ref<string>('')
const totalLoaded = ref<number>(0)
const scrollTop = ref<number>(0)
const viewportHeight = ref<number>(0)
const pageWindowHeight = ref<number>(0)
const isAndroidApp = ref<boolean>(false)
const startIndex = ref<number>(0)
const endIndex = ref<number>(0)
const topSpacerHeight = ref<number>(0)
const bottomSpacerHeight = ref<number>(0)
const activeTab = ref<string>('all')
const searchKeyword = ref<string>('')
const statusBarHeight = ref<number>(0)
const ordersPageStyle = computed((): string => {
if (!isAndroidApp.value) {
return ''
}
if (pageWindowHeight.value <= 0) {
return ''
}
return 'height:' + pageWindowHeight.value + 'px;min-height:' + pageWindowHeight.value + 'px;'
})
const ordersContentStyle = computed((): string => {
if (!isAndroidApp.value) {
return ''
}
if (viewportHeight.value <= 0) {
return ''
}
return 'height:' + viewportHeight.value + 'px;min-height:' + viewportHeight.value + 'px;'
})
let searchTimer: number = 0
const merchantShareFreeEnabled = ref<UTSJSONObject>(new UTSJSONObject())
const merchantRequiredCount = ref<UTSJSONObject>(new UTSJSONObject())
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
}
const getOptionString = (options: UTSJSONObject, key: string): string => {
const value = options.getString(key)
if (value != null) {
return value
}
return ''
}
function unwrapJsonScalar(value: UTSJSONObject | string | number | null): string {
if (value == null) return ''
const raw = JSON.stringify(value)
if (raw == 'null') return ''
if (raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) {
return raw.substring(1, raw.length - 1)
}
return raw
}
function formatSpecObj(obj: UTSJSONObject | string | number | null): string {
if (obj == null) return ''
const scalar = unwrapJsonScalar(obj)
if (scalar == '') return ''
if (!scalar.startsWith('{') && !scalar.startsWith('[')) {
return scalar
}
try {
const objParsed = JSON.parse(scalar)
if (objParsed == null) return ''
const specObj = objParsed as UTSJSONObject
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)
}
if (parts.length === 0) {
return specObjStr.replace(/[{}\"]/g, '').replace(/:/g, ': ').replace(/,/g, ' | ')
}
return parts.join(' | ')
} catch (e) {
return ''
}
}
function parseSpecText(specs: UTSJSONObject | string | number | null): string {
if (specs == null) return ''
const scalar = unwrapJsonScalar(specs)
if (scalar == '') return ''
if (scalar.startsWith('{') || scalar.startsWith('[')) {
try {
const parsed = JSON.parse(scalar)
if (parsed == null) return scalar
return formatSpecObj(parsed)
} catch (e) {
return scalar
}
}
return scalar
}
const mapRawOrdersToViewModels = (rawOrders: UTSJSONObject[]): OrderItem[] => {
const mappedOrders: OrderItem[] = []
for (let i = 0; i < rawOrders.length; i++) {
const order = rawOrders[i]
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[] = []
if (itemsRaw != null && Array.isArray(itemsRaw)) {
const items = itemsRaw as UTSJSONObject[]
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')
productsList.push({
id: productId ?? '',
name: productName ?? '未知商品',
price: price ?? 0,
image: imageUrl ?? '/static/images/default.png',
spec: specText,
quantity: quantity ?? 1
})
}
}
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')
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 = '商家店铺'
}
if (productsList.length === 0) {
productsList.push({
id: 'placeholder',
name: '订单商品',
price: totalAmount ?? paidAmount ?? 0,
image: '/static/images/default.png',
spec: '',
quantity: 1
})
}
mappedOrders.push({
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
})
}
return mappedOrders
}
const mergeOrders = (currentOrders: OrderItem[], appendOrders: OrderItem[]): OrderItem[] => {
const merged: OrderItem[] = []
for (let i = 0; i < currentOrders.length; i++) {
merged.push(currentOrders[i])
}
for (let i = 0; i < appendOrders.length; i++) {
const nextOrder = appendOrders[i]
let exists = false
for (let j = 0; j < merged.length; j++) {
if (merged[j].id === nextOrder.id) {
merged[j] = nextOrder
exists = true
break
}
}
if (!exists) {
merged.push(nextOrder)
}
}
return merged
}
const resetListState = () => {
loadedOrders.value = []
visibleOrders.value = []
totalLoaded.value = 0
scrollTop.value = 0
startIndex.value = 0
endIndex.value = 0
topSpacerHeight.value = 0
bottomSpacerHeight.value = 0
nextCursor.value = ''
hasMore.value = true
}
const getShortOrderNo = (orderNo: string): string => {
if (orderNo == '') {
return '--'
}
if (orderNo.length <= 10) {
return orderNo
}
return orderNo.substring(orderNo.length - 10)
}
const formatAmount = (value: number): string => {
return value.toFixed(2)
}
function formatOrderHeaderTime(isoString: string): string {
if (isoString == '') return '--'
const date = new Date(isoString)
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hour = date.getHours().toString().padStart(2, '0')
const minute = date.getMinutes().toString().padStart(2, '0')
return `${month}-${day} ${hour}:${minute}`
}
function getOrderMetaLine(order: OrderItem): string {
return `订单号 ${getShortOrderNo(order.order_no)} · 下单 ${formatOrderHeaderTime(order.create_time)}`
}
const getOrderPreviewHint = (order: OrderItem): string => {
const hiddenCount = order.products.length - ORDER_VISIBLE_PRODUCT_LIMIT
if (hiddenCount > 0) {
return `其余${hiddenCount}件商品、完整规格和物流请查看详情`
}
return '完整规格、物流进度和售后入口请查看详情'
}
const getOrderPreviewProducts = (order: OrderItem): OrderProduct[] => {
const previewList: OrderProduct[] = []
const limit = Math.min(order.products.length, ORDER_VISIBLE_PRODUCT_LIMIT)
for (let i = 0; i < limit; i++) {
previewList.push(order.products[i])
}
return previewList
}
const getOrderProductsHeight = (order: OrderItem): number => {
const previewCount = Math.min(order.products.length, ORDER_VISIBLE_PRODUCT_LIMIT)
if (previewCount <= 1) {
return ORDER_PRODUCTS_HEIGHT_SINGLE
}
return ORDER_PRODUCTS_HEIGHT_DOUBLE
}
function getOrderCardBodyHeight(order: OrderItem): number {
return ORDER_HEADER_HEIGHT + getOrderProductsHeight(order) + ORDER_SUMMARY_HEIGHT + ORDER_SHARE_SLOT_HEIGHT + ORDER_ACTIONS_HEIGHT
}
function getOrderCardHeight(order: OrderItem): number {
return getOrderCardBodyHeight(order) + ORDER_CARD_GAP
}
function getAccumulatedHeight(endIndexExclusive: number): number {
let totalHeight = 0
for (let i = 0; i < endIndexExclusive; i++) {
totalHeight += getOrderCardHeight(loadedOrders.value[i])
}
return totalHeight
}
function getIndexByScrollTop(currentScrollTop: number): number {
let accumulatedHeight = 0
for (let i = 0; i < loadedOrders.value.length; i++) {
accumulatedHeight += getOrderCardHeight(loadedOrders.value[i])
if (accumulatedHeight > currentScrollTop) {
return i
}
}
return Math.max(0, loadedOrders.value.length - 1)
}
const updateVisibleWindow = () => {
const total = loadedOrders.value.length
totalLoaded.value = total
if (total === 0) {
visibleOrders.value = []
topSpacerHeight.value = 0
bottomSpacerHeight.value = 0
startIndex.value = 0
endIndex.value = 0
return
}
const rawStart = getIndexByScrollTop(scrollTop.value)
const start = Math.max(0, rawStart - VIRTUAL_BUFFER)
const targetHeight = viewportHeight.value + (VIRTUAL_BUFFER * ORDER_CARD_AVERAGE_HEIGHT)
let end = start
let renderedHeight = 0
while (end < total && renderedHeight < targetHeight) {
renderedHeight += getOrderCardHeight(loadedOrders.value[end])
end++
}
if (end < total) {
end = Math.min(total, end + VIRTUAL_BUFFER)
}
startIndex.value = start
endIndex.value = end
visibleOrders.value = loadedOrders.value.slice(start, end)
topSpacerHeight.value = getAccumulatedHeight(start)
const totalHeight = getAccumulatedHeight(total)
const visibleHeight = getAccumulatedHeight(end) - topSpacerHeight.value
bottomSpacerHeight.value = Math.max(0, totalHeight - topSpacerHeight.value - visibleHeight)
}
const shouldKeepInCurrentTab = (status: number): boolean => {
if (activeTab.value === 'all') return true
if (activeTab.value === 'aftersale') {
return status === 6 || status === 7
}
return status === getStatusByTab(activeTab.value)
}
const getLoadedOrderById = (orderId: string): OrderItem | null => {
for (let i = 0; i < loadedOrders.value.length; i++) {
const order = loadedOrders.value[i]
if (order.id === orderId) {
return order
}
}
return null
}
const removeLoadedOrder = (orderId: string) => {
loadedOrders.value = loadedOrders.value.filter((order: OrderItem) => order.id !== orderId)
updateVisibleWindow()
}
const updateLoadedOrderStatus = (orderId: string, nextStatus: number): OrderItem | null => {
let targetOrder: OrderItem | null = null
const nextList: OrderItem[] = []
for (let i = 0; i < loadedOrders.value.length; i++) {
const currentOrder = loadedOrders.value[i]
if (currentOrder.id === orderId) {
currentOrder.status = nextStatus
targetOrder = currentOrder
if (shouldKeepInCurrentTab(nextStatus)) {
nextList.push(currentOrder)
}
} else {
nextList.push(currentOrder)
}
}
loadedOrders.value = nextList
updateVisibleWindow()
return targetOrder
}
const getProductSpecDisplay = (product: OrderProduct): string => {
if (product.spec != '') {
return product.spec
}
return '默认规格'
}
const shouldShowProductSpecTag = (product: OrderProduct): boolean => {
return product.spec != ''
}
const getProductSpecTag = (product: OrderProduct): string => {
if (product.spec != '') {
return '已选'
}
return ''
}
const loadOrderCounts = async () => {
try {
const counts = await supabaseService.getOrderCounts()
orderTabs.value[0].count = counts.all
orderTabs.value[1].count = counts.pending
orderTabs.value[2].count = counts.shipping
orderTabs.value[3].count = counts.delivering
orderTabs.value[4].count = counts.completed
orderTabs.value[5].count = counts.aftersale
orderTabs.value[6].count = counts.cancelled
} catch (e) {
console.error('[orders] 加载订单数量失败:', e)
}
}
const isShareFreeEnabled = (merchantId: string): boolean => {
const val = merchantShareFreeEnabled.value.get(merchantId)
return val === true
}
const getRequiredCount = (merchantId: string): number => {
const val = merchantRequiredCount.value.getNumber(merchantId)
if (val != null) {
return val
}
return 4
}
const loadMerchantPromotionConfigs = async (orderList: OrderItem[]) => {
const merchantIdArray: 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) {
let alreadyIn = false
for (let j = 0; j < merchantIdArray.length; j++) {
if (merchantIdArray[j] === merchantId) {
alreadyIn = true
break
}
}
if (!alreadyIn) {
merchantIdArray.push(merchantId)
}
}
}
for (let i = 0; i < merchantIdArray.length; i++) {
const merchantId = merchantIdArray[i]
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 = (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)
}
} catch (e) {
console.error('[orders] 加载商家推销配置失败:', merchantId, e)
merchantShareFreeEnabled.value.set(merchantId, false)
merchantRequiredCount.value.set(merchantId, 4)
}
}
}
async function loadOrders(reset: boolean): Promise<void> {
if (loading.value || loadingMore.value) {
return
}
if (!reset && !hasMore.value) {
return
}
if (reset) {
loading.value = true
hasMore.value = true
nextCursor.value = ''
} else {
loadingMore.value = true
}
try {
const status = getStatusByTab(activeTab.value)
const keyword = searchKeyword.value.trim()
console.log('[orders] 按需加载 reset=', reset, 'cursor=', nextCursor.value, 'status=', status, 'keyword=', keyword)
const result = await supabaseService.getOrdersByCursor({
cursor: nextCursor.value,
limit: pageSize.value,
status: status,
keyword: keyword
})
const mappedOrders = mapRawOrdersToViewModels(result.list)
if (reset) {
loadedOrders.value = mappedOrders
scrollTop.value = 0
} else {
loadedOrders.value = mergeOrders(loadedOrders.value, mappedOrders)
}
nextCursor.value = result.nextCursor
hasMore.value = result.hasMore
await loadMerchantPromotionConfigs(mappedOrders)
updateVisibleWindow()
initialized.value = true
console.log('[orders] loaded=', loadedOrders.value.length, 'visible=', visibleOrders.value.length, 'hasMore=', hasMore.value)
} catch (err) {
console.error('[orders] 加载订单失败:', err)
uni.showToast({ title: '加载订单失败', icon: 'none' })
} finally {
loading.value = false
loadingMore.value = false
refreshing.value = false
}
}
function loadMore(): void {
console.log('[orders] loadMore 触发')
loadOrders(false)
}
const refreshCurrentSlice = async () => {
resetListState()
await loadOrders(true)
await loadOrderCounts()
}
onLoad((options) => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight ?? 0
pageWindowHeight.value = systemInfo.windowHeight ?? systemInfo.screenHeight ?? 0
const windowHeight = pageWindowHeight.value
const calculatedViewport = windowHeight - statusBarHeight.value - 44 - 48
viewportHeight.value = calculatedViewport > 0 ? calculatedViewport : windowHeight
// #ifdef APP-ANDROID
isAndroidApp.value = true
// #endif
if (options == null) return
const optionsObj = options as UTSJSONObject
const status = getOptionString(optionsObj, 'status')
if (status != '') {
if (['all', 'pending', 'shipping', 'delivering', 'completed', 'aftersale', 'cancelled'].includes(status)) {
activeTab.value = status
}
}
const type = getOptionString(optionsObj, 'type')
if (type != '') {
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(() => {
if (!initialized.value) {
loadOrders(true)
loadOrderCounts()
}
})
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')}`
}
const performSearch = () => {
resetListState()
loadOrders(true)
}
const onSearchInput = (e: UniInputEvent) => {
searchKeyword.value = e.detail.value as string
if (searchTimer > 0) {
clearTimeout(searchTimer)
}
searchTimer = setTimeout(() => {
performSearch()
}, 500)
}
const onSearchConfirm = () => {
if (searchTimer > 0) {
clearTimeout(searchTimer)
searchTimer = 0
}
performSearch()
}
const clearSearch = () => {
if (searchTimer > 0) {
clearTimeout(searchTimer)
searchTimer = 0
}
searchKeyword.value = ''
performSearch()
}
const switchTab = (tabId: string) => {
if (activeTab.value === tabId) return
activeTab.value = tabId
resetListState()
loadOrders(true)
loadOrderCounts()
}
const onOrdersScroll = (e: UniScrollEvent) => {
try {
const detail = e.detail
if (detail == null) return
let top = 0
if (typeof detail.scrollTop == 'number') {
top = detail.scrollTop
}
scrollTop.value = top
updateVisibleWindow()
} catch (err) {
console.error('[orders虚拟列表] 处理滚动失败:', err)
}
}
const onRefresh = async () => {
refreshing.value = true
resetListState()
await loadOrders(true)
await loadOrderCounts()
}
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 doDeleteOrder = async (orderId: string): Promise<void> => {
uni.showLoading({ title: '删除中...' })
try {
const success = await supabaseService.deleteOrder(orderId)
uni.hideLoading()
if (success) {
uni.showToast({ title: '订单已删除', icon: 'success' })
removeLoadedOrder(orderId)
loadOrderCounts()
} else {
uni.showToast({ title: '删除失败', icon: 'none' })
}
} catch (e) {
uni.hideLoading()
uni.showToast({ title: '删除失败', icon: 'none' })
}
}
const deleteOrder = (orderId: string) => {
uni.showModal({
title: '删除订单',
content: '确定要删除此订单吗?',
success: (res) => {
if (res.confirm) {
doDeleteOrder(orderId)
}
}
})
}
const doCancelOrder = async (orderId: string): Promise<void> => {
uni.showLoading({ title: '取消中...' })
try {
const success = await supabaseService.cancelOrder(orderId)
uni.hideLoading()
if (success) {
uni.showToast({ title: '订单已取消', icon: 'success' })
updateLoadedOrderStatus(orderId, 5)
loadOrderCounts()
} else {
uni.showToast({ title: '取消失败', icon: 'none' })
}
} catch (e) {
uni.hideLoading()
uni.showToast({ title: '取消失败', icon: 'none' })
}
}
const cancelOrder = (orderId: string) => {
uni.showModal({
title: '确认取消',
content: '确定要取消此订单吗?',
success: (res) => {
if (res.confirm) {
doCancelOrder(orderId)
}
}
})
}
const payOrder = (orderId: string) => {
const userId = supabaseService.getCurrentUserId()
if (userId == null || userId === '') {
goToLogin(`/pages/mall/consumer/payment?orderId=${orderId}`)
return
}
uni.navigateTo({ url: `/pages/mall/consumer/payment?orderId=${orderId}` })
}
const remindShipping = async (orderId: string) => {
uni.showLoading({ title: '正在提醒...' })
try {
const order = getLoadedOrderById(orderId)
if (order != null && order.merchant_id != '') {
const message = `你好,我的订单[${order.order_no}]还没有发货,请尽快安排,谢谢。`
await supabaseService.sendChatMessage(message, order.merchant_id)
}
} 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}` })
}
const goReview = (order: OrderItem) => {
const productIds = order.products.map((p: OrderProduct): string => {
return p.id
}).join(',')
uni.navigateTo({ url: `/pages/mall/consumer/review?orderId=${order.id}&productIds=${productIds}` })
}
const doConfirmReceipt = async (orderId: string): Promise<void> => {
uni.showLoading({ title: '处理中...' })
try {
const result = await supabaseService.confirmReceipt(orderId)
uni.hideLoading()
if (result.success) {
uni.showToast({ title: '收货成功', icon: 'success' })
const updatedOrder = updateLoadedOrderStatus(orderId, 4)
loadOrderCounts()
setTimeout(() => {
if (updatedOrder != null) {
goReview(updatedOrder)
}
}, 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 userId = supabaseService.getCurrentUserId()
if (userId == null || userId === '') {
goToLogin(`/pages/mall/consumer/apply-refund?orderId=${order.id}`)
return
}
uni.navigateTo({ url: `/pages/mall/consumer/apply-refund?orderId=${order.id}` })
}
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' })
refreshCurrentSlice()
} 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 = ['再次购买', '联系卖家']
}
if (actions.length === 0) {
uni.showToast({ title: '暂无可执行操作', icon: 'none' })
return
}
uni.showActionSheet({
itemList: actions,
success: (res) => {
const action = actions[res.tapIndex]
handleOrderAction(order, action)
}
})
}
const navigateToProduct = (product: OrderProduct) => {
uni.navigateTo({ url: `/pages/mall/consumer/product-detail?id=${product.id}` })
}
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 {
width: 100%;
height: 100vh;
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
overflow: hidden;
}
.orders-header,
.order-tabs-fixed-container {
flex-shrink: 0;
}
.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: 10;
box-sizing: border-box;
}
.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: #999999;
font-size: 12px;
}
.search-input:focus {
border-color: #ff5000;
background-color: #ffffff;
}
.search-icon,
.search-clear {
position: absolute;
right: 12px;
color: #999999;
}
.search-icon {
font-size: 18px;
}
.search-clear {
font-size: 20px;
width: 20px;
height: 20px;
line-height: 18px;
text-align: center;
border-radius: 10px;
background-color: #dddddd;
}
.order-tabs-fixed-container {
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
display: flex;
flex-direction: row;
align-items: center;
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: 48px;
}
.tab-item-fixed.active,
.tab-item-mobile.active {
color: #ff5000;
font-weight: bold;
}
.tab-scroll-mobile {
flex: 1;
white-space: nowrap;
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;
}
.active-indicator {
position: absolute;
bottom: 0;
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: #ffffff;
font-size: 10px;
padding: 1px 4px;
border-radius: 8px;
min-width: 12px;
text-align: center;
}
.orders-content {
flex: 1;
height: 0;
width: 100%;
background-color: #f5f5f5;
}
.empty-orders {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
}
.empty-icon {
font-size: 80px;
color: #dddddd;
margin-bottom: 20px;
}
.empty-title {
font-size: 18px;
color: #666666;
margin-bottom: 10px;
}
.empty-desc {
font-size: 14px;
color: #999999;
margin-bottom: 30px;
}
.go-shopping-btn {
background-color: #ff5000;
color: #ffffff;
border: none;
border-radius: 25px;
padding: 10px 40px;
font-size: 16px;
}
.virtual-list,
.virtual-top-spacer,
.virtual-bottom-spacer {
width: 100%;
}
.virtual-order-item {
width: 100%;
box-sizing: border-box;
padding: 10px 12px 0 12px;
}
.order-card {
width: 100%;
border-radius: 12px;
background-color: #ffffff;
overflow: hidden;
box-sizing: border-box;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
}
.order-card-header {
height: 56px;
padding: 8px 15px 6px 15px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
border-bottom: 1px solid #f9f9f9;
box-sizing: border-box;
flex-shrink: 0;
}
.shop-info,
.summary-right,
.action-buttons {
display: flex;
flex-direction: row;
align-items: center;
}
.shop-info {
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
overflow: hidden;
}
.shop-title-row {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
}
.status-row {
display: flex;
flex-direction: row;
align-items: center;
flex-shrink: 0;
margin-top: 2px;
}
.shop-icon {
font-size: 16px;
margin-right: 6px;
}
.shop-name {
font-size: 14px;
font-weight: bold;
color: #333333;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
lines: 1;
}
.order-meta-line {
width: 100%;
margin-top: 2px;
font-size: 11px;
color: #999999;
overflow: hidden;
text-overflow: ellipsis;
lines: 1;
}
.arrow-right {
font-size: 14px;
color: #cccccc;
margin-left: 4px;
}
.more-btn {
font-size: 18px;
color: #999999;
margin-left: 8px;
padding: 0 5px;
}
.order-status {
font-size: 14px;
font-weight: bold;
}
.status-pending,
.status-refunding {
color: #ff5000;
}
.status-shipping {
color: #ff9500;
}
.status-delivering {
color: #007aff;
}
.status-completed {
color: #34c759;
}
.status-cancelled,
.status-refunded {
color: #999999;
}
.order-products {
padding: 10px 15px 0 15px;
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.order-product {
display: flex;
flex-direction: row;
margin-bottom: 8px;
width: 100%;
}
.order-product:last-child {
margin-bottom: 0;
}
.product-image {
width: 60px;
height: 60px;
border-radius: 8px;
margin-right: 12px;
flex-shrink: 0;
background-color: #f5f5f5;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 60px;
overflow: hidden;
}
.product-top-info {
display: flex;
flex-direction: column;
width: 100%;
}
.product-name {
font-size: 14px;
color: #333333;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
lines: 2;
}
.product-spec-row {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 2px;
}
.product-spec-tag {
font-size: 10px;
line-height: 1;
color: #ff6b35;
background-color: #fff2eb;
border-radius: 8px;
padding: 3px 6px;
margin-right: 6px;
flex-shrink: 0;
}
.product-spec {
font-size: 12px;
color: #999999;
overflow: hidden;
text-overflow: ellipsis;
lines: 1;
flex: 1;
}
.product-footer {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.product-price {
font-size: 15px;
color: #ff5000;
font-weight: bold;
}
.product-quantity {
font-size: 13px;
color: #999999;
}
.order-preview-bottom {
height: 26px;
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 6px;
border-top: 1px solid #f8f8f8;
}
.order-preview-hint {
font-size: 12px;
color: #999999;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
lines: 1;
}
.order-preview-link {
font-size: 12px;
color: #ff6b35;
margin-left: 10px;
flex-shrink: 0;
}
.order-summary {
height: 38px;
padding: 0 15px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
border-top: 1px solid #f9f9f9;
box-sizing: border-box;
flex-shrink: 0;
}
.order-time {
font-size: 12px;
color: #999999;
}
.summary-label {
font-size: 12px;
color: #666666;
margin-right: 5px;
}
.summary-shipping {
font-size: 12px;
color: #999999;
margin-right: 8px;
}
.summary-price {
font-size: 16px;
font-weight: bold;
color: #333333;
}
.share-free-slot {
height: 40px;
padding: 0 15px;
box-sizing: border-box;
flex-shrink: 0;
}
.share-free-row,
.share-free-placeholder {
width: 100%;
height: 100%;
border-radius: 8px;
}
.share-free-row {
background: linear-gradient(135deg, #fff5f0 0%, #ffecd2 100%);
display: flex;
flex-direction: row;
align-items: center;
padding: 0 12px;
box-sizing: border-box;
}
.share-free-placeholder {
background-color: transparent;
}
.share-free-icon {
font-size: 18px;
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;
overflow: hidden;
text-overflow: ellipsis;
lines: 1;
}
.share-free-arrow {
font-size: 16px;
color: #ff8c42;
}
.order-actions {
height: 54px;
padding: 0 15px 12px 15px;
box-sizing: border-box;
display: flex;
align-items: flex-end;
flex-shrink: 0;
}
.action-buttons {
width: 100%;
justify-content: flex-end;
}
.action-btn {
padding: 6px 15px;
border-radius: 15px;
font-size: 13px;
border: 1px solid;
background-color: transparent;
margin-left: 10px;
}
.action-btn.pay,
.action-btn.repurchase {
color: #ff5000;
border-color: #ff5000;
}
.action-btn.confirm {
color: #34c759;
border-color: #34c759;
}
.action-btn.review {
color: #ff9500;
border-color: #ff9500;
}
.action-btn.cancel {
color: #ff6b6b;
border-color: #ff6b6b;
}
.action-btn.view,
.action-btn.refund,
.action-btn.remind {
color: #666666;
border-color: #cccccc;
}
.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: #999999;
font-size: 13px;
padding: 20px 0;
}
.safe-area {
height: 80px;
}
.tabbar-placeholder {
height: 50px;
background-color: #f5f5f5;
}
@media screen and (max-width: 320px) {
.action-btn {
padding: 6px 10px;
font-size: 12px;
}
.shop-name {
max-width: 110px;
}
}
</style>