2513 lines
78 KiB
Plaintext
2513 lines
78 KiB
Plaintext
<!-- pages/mall/consumer/orders.uvue -->
|
||
<template>
|
||
<view class="orders-page" :style="ordersPageStyle">
|
||
<view class="orders-header" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||
<view class="custom-nav-row" :style="{ paddingRight: headerRightReserve + 'px', height: navHeight + 'px' }">
|
||
<view class="nav-back-btn" @click="goBack">
|
||
<text class="nav-back-icon">‹</text>
|
||
</view>
|
||
|
||
<view class="search-shell">
|
||
<text class="search-icon-left">🔍</text>
|
||
<input
|
||
class="search-input"
|
||
type="text"
|
||
placeholder="搜索订单号、药品、器械或服务"
|
||
:value="searchKeyword"
|
||
@input="onSearchInput"
|
||
@confirm="onSearchConfirm"
|
||
/>
|
||
<text v-if="searchKeyword != ''" class="search-clear" @click="clearSearch">×</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="biz-tabs-wrap">
|
||
<view
|
||
v-for="tab in businessTabs"
|
||
:key="tab.id"
|
||
:class="['biz-tab-item', { active: activeBizType === tab.id }]"
|
||
@click="switchBizTab(tab.id)"
|
||
>
|
||
<text class="biz-tab-text">{{ tab.name }}</text>
|
||
<text v-if="tab.count > 0" class="biz-tab-badge">{{ tab.count }}</text>
|
||
<view v-if="activeBizType === tab.id" class="biz-tab-indicator"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="status-tabs-wrap">
|
||
<scroll-view
|
||
class="status-tabs-scroll"
|
||
direction="horizontal"
|
||
:show-scrollbar="false"
|
||
:scroll-with-animation="true"
|
||
>
|
||
<view class="status-tabs-inner">
|
||
<view
|
||
v-for="tab in currentStatusTabs"
|
||
:key="tab.id"
|
||
:class="['status-tab-item', { active: activeStatusTab === tab.id }]"
|
||
@click="switchStatusTab(tab.id)"
|
||
>
|
||
<text class="status-tab-text">{{ tab.name }}</text>
|
||
<text v-if="tab.count > 0" class="status-tab-badge">{{ tab.count }}</text>
|
||
</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">{{ getEmptyStateTitle() }}</text>
|
||
<text class="empty-desc">{{ getEmptyStateDescription() }}</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)">
|
||
<view class="order-card-header">
|
||
<view class="shop-info">
|
||
<view class="shop-title-row">
|
||
<text class="shop-icon">🏥</text>
|
||
<text class="shop-name">{{ getOrderShopName(order) }}</text>
|
||
<text class="arrow-right">></text>
|
||
</view>
|
||
<text class="order-meta-line">{{ getOrderMetaLine(order) }}</text>
|
||
</view>
|
||
<view class="status-row">
|
||
<view class="status-main">
|
||
<text :class="['order-status', getStatusClass(order)]">{{ getOrderStatusText(order) }}</text>
|
||
<text v-if="getOrderStatusCountdownText(order) != ''" :class="['order-status-countdown', getStatusClass(order)]">{{ getOrderStatusCountdownText(order) }}</text>
|
||
</view>
|
||
<text v-if="shouldShowDeleteIcon(order)" class="delete-order-icon" @click.stop="deleteOrder(order.id)">🗑</text>
|
||
<text v-else class="more-btn" @click.stop="showOrderMenu(order)">⋯</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="order-main" @click.stop="onCardContentTap(order)">
|
||
<image class="order-cover" :src="getOrderCover(order)" mode="aspectFill" lazy-load="true" />
|
||
<view class="order-main-info">
|
||
<text class="order-main-title">{{ getOrderTitle(order) }}</text>
|
||
|
||
<view v-if="order.biz_type === 'service'" class="service-meta-wrap">
|
||
<text class="service-meta-text">预约:{{ getServiceAppointmentText(order) }}</text>
|
||
<text class="service-meta-text">地址:{{ getServiceAddressText(order) }}</text>
|
||
</view>
|
||
|
||
<view v-else class="goods-meta-wrap">
|
||
<text class="goods-spec-chip">{{ getPrimaryProductSpec(order) }}</text>
|
||
<text v-if="getExtraGoodsCount(order) > 0" class="goods-extra-text">共{{ order.products.length }}件商品</text>
|
||
</view>
|
||
|
||
<view class="order-main-footer">
|
||
<text class="order-main-price">¥{{ formatAmount(getOrderPrimaryPrice(order)) }}</text>
|
||
<text class="order-main-count">{{ getOrderPrimaryCountText(order) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="order-summary">
|
||
<text class="order-time">{{ formatDate(order.create_time) }}</text>
|
||
<view class="summary-right">
|
||
<text class="summary-text">{{ getOrderSummaryPrefix(order) }}</text>
|
||
<text class="summary-price-label">实付:</text>
|
||
<text class="summary-price">¥{{ formatAmount(order.total_amount) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="order-actions" @click.stop="">
|
||
<view class="action-buttons">
|
||
<button
|
||
v-for="action in getOrderActions(order)"
|
||
:key="action.id"
|
||
:class="['action-btn', action.kind === 'primary' ? 'primary' : 'secondary']"
|
||
@click.stop="onOrderActionTap(order, action.id)"
|
||
>
|
||
{{ action.name }}
|
||
</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 { onHide, onLoad, onShow, onUnload } from '@dcloudio/uni-app'
|
||
import { supabaseService } from '@/utils/supabaseService.uts'
|
||
import { goToLogin } from '@/utils/utils.uts'
|
||
import { formatCountdownHM, getRemainingSeconds, getUnifiedDisplayState, isOrderPayExpired, isPendingPayOrder, ORDER_STATUS_CANCELLED, ORDER_STATUS_PENDING, ORDER_STATUS_TIMEOUT_LEGACY, ORDER_TIMEOUT_CANCEL_REASON, PAYMENT_STATUS_TIMEOUT, PAYMENT_STATUS_UNPAID, type OrderStatusSource } from '@/utils/orderStatus.uts'
|
||
import { dispatchPaidHomecareOrder, showHomecareDispatchFailureModal } from '@/services/serviceOrderService.uts'
|
||
|
||
type BusinessTabItem = {
|
||
id: string,
|
||
name: string,
|
||
count: number
|
||
}
|
||
|
||
type StatusTabItem = {
|
||
id: string,
|
||
name: string,
|
||
count: number
|
||
}
|
||
|
||
type OrderProduct = {
|
||
id: string,
|
||
name: string,
|
||
price: number,
|
||
image: string,
|
||
spec: string,
|
||
quantity: number
|
||
}
|
||
|
||
type UnifiedServiceInfo = {
|
||
service_name: string,
|
||
service_image: string,
|
||
appointment_time: string,
|
||
address: string,
|
||
contact_name: string,
|
||
contact_phone: string,
|
||
provider_name: string
|
||
}
|
||
|
||
type OrderItem = {
|
||
id: string,
|
||
order_no: string,
|
||
biz_type: string,
|
||
source: string,
|
||
status: number,
|
||
payment_status: number,
|
||
cancel_reason: string,
|
||
pay_expire_at: string,
|
||
create_time: string,
|
||
product_amount: number,
|
||
shipping_fee: number,
|
||
total_amount: number,
|
||
merchant_id: string,
|
||
shop_name: string,
|
||
products: OrderProduct[],
|
||
service_info: UnifiedServiceInfo | null,
|
||
dispatch_status: string,
|
||
dispatch_error_message: string
|
||
}
|
||
|
||
type UnifiedOrdersCursorResult = {
|
||
list: OrderItem[],
|
||
nextCursor: string,
|
||
hasMore: boolean
|
||
}
|
||
|
||
type OrderActionItem = {
|
||
id: string,
|
||
name: string,
|
||
kind: string
|
||
}
|
||
|
||
const SEARCH_HEADER_HEIGHT = 44
|
||
const BIZ_TABS_HEIGHT = 44
|
||
const STATUS_TABS_HEIGHT = 42
|
||
const TABBAR_HEIGHT = 50
|
||
|
||
const ORDER_CARD_GAP = 10
|
||
const ORDER_HEADER_HEIGHT = 44
|
||
const ORDER_PRODUCT_HEIGHT = 88
|
||
const ORDER_SERVICE_PRODUCT_HEIGHT = 104
|
||
const ORDER_SUMMARY_HEIGHT = 36
|
||
const ORDER_ACTIONS_HEIGHT = 54
|
||
const ORDER_CARD_AVERAGE_HEIGHT = 232
|
||
const ORDER_VISIBLE_PRODUCT_LIMIT = 1
|
||
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 activeBizType = ref<string>('all')
|
||
const activeStatusTab = ref<string>('all')
|
||
const searchKeyword = ref<string>('')
|
||
const statusBarHeight = ref<number>(0)
|
||
const headerRightReserve = ref<number>(12)
|
||
const navHeight = ref<number>(44)
|
||
|
||
const goodsStatusCounts = ref<UTSJSONObject>(createCountBucket())
|
||
const serviceStatusCounts = ref<UTSJSONObject>(createCountBucket())
|
||
|
||
let searchTimer: number = 0
|
||
const nowTick = ref<number>(Date.now())
|
||
let orderTicker = 0
|
||
const expiringOrderIds: string[] = []
|
||
|
||
function toOrderStatusSource(order: OrderItem): OrderStatusSource {
|
||
let inferredPaymentStatus = order.payment_status
|
||
if (inferredPaymentStatus <= 0) {
|
||
if (order.status == ORDER_STATUS_PENDING) {
|
||
inferredPaymentStatus = PAYMENT_STATUS_UNPAID
|
||
} else if (order.status == ORDER_STATUS_CANCELLED || order.status == ORDER_STATUS_TIMEOUT_LEGACY) {
|
||
inferredPaymentStatus = PAYMENT_STATUS_TIMEOUT
|
||
} else {
|
||
inferredPaymentStatus = 2
|
||
}
|
||
}
|
||
|
||
return {
|
||
source: order.source,
|
||
order_status: order.status,
|
||
payment_status: inferredPaymentStatus,
|
||
pay_expire_at: order.pay_expire_at,
|
||
created_at: order.create_time,
|
||
cancel_reason: order.cancel_reason
|
||
}
|
||
}
|
||
|
||
function isTimeoutOrder(order: OrderItem): boolean {
|
||
return isOrderPayExpired(toOrderStatusSource(order))
|
||
}
|
||
|
||
function isPendingPayDisplayOrder(order: OrderItem): boolean {
|
||
return isPendingPayOrder(toOrderStatusSource(order))
|
||
}
|
||
|
||
function getOrderStatusCountdownText(order: OrderItem): string {
|
||
const currentTick = nowTick.value
|
||
if (currentTick < 0) {
|
||
return ''
|
||
}
|
||
if (!isPendingPayDisplayOrder(order)) {
|
||
return ''
|
||
}
|
||
const seconds = getRemainingSeconds(toOrderStatusSource(order))
|
||
if (seconds <= 0) {
|
||
return ''
|
||
}
|
||
return formatCountdownHM(seconds)
|
||
}
|
||
|
||
function shouldShowDeleteIcon(order: OrderItem): boolean {
|
||
const displayState = getUnifiedDisplayState(toOrderStatusSource(order))
|
||
return displayState == 'cancelled' || displayState == 'expired'
|
||
}
|
||
|
||
function isExpiring(orderId: string): boolean {
|
||
return expiringOrderIds.indexOf(orderId) >= 0
|
||
}
|
||
|
||
function pushExpiringOrder(orderId: string): void {
|
||
if (!isExpiring(orderId)) {
|
||
expiringOrderIds.push(orderId)
|
||
}
|
||
}
|
||
|
||
function removeExpiringOrder(orderId: string): void {
|
||
const index = expiringOrderIds.indexOf(orderId)
|
||
if (index >= 0) {
|
||
expiringOrderIds.splice(index, 1)
|
||
}
|
||
}
|
||
|
||
function syncExpiredOrders(): void {
|
||
for (let i = 0; i < loadedOrders.value.length; i++) {
|
||
const order = loadedOrders.value[i]
|
||
const source = toOrderStatusSource(order)
|
||
if (isPendingPayDisplayOrder(order) && isOrderPayExpired(source) && !isExpiring(order.id)) {
|
||
pushExpiringOrder(order.id)
|
||
const expiredStatus = order.biz_type === 'service' ? ORDER_STATUS_TIMEOUT_LEGACY : ORDER_STATUS_CANCELLED
|
||
updateLoadedOrderStatus(order.id, expiredStatus, PAYMENT_STATUS_TIMEOUT, ORDER_TIMEOUT_CANCEL_REASON, order.pay_expire_at)
|
||
supabaseService.expireUnifiedOrder(order.id, order.source).finally(() => {
|
||
removeExpiringOrder(order.id)
|
||
loadOrderCounts()
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
function startOrderTicker(): void {
|
||
if (orderTicker > 0) {
|
||
return
|
||
}
|
||
nowTick.value = Date.now()
|
||
syncExpiredOrders()
|
||
orderTicker = setInterval(() => {
|
||
nowTick.value = Date.now()
|
||
syncExpiredOrders()
|
||
}, 1000)
|
||
}
|
||
|
||
function stopOrderTicker(): void {
|
||
if (orderTicker > 0) {
|
||
clearInterval(orderTicker)
|
||
orderTicker = 0
|
||
}
|
||
}
|
||
|
||
function createCountBucket(): UTSJSONObject {
|
||
const bucket = new UTSJSONObject()
|
||
bucket.set('all', 0)
|
||
bucket.set('pending', 0)
|
||
bucket.set('inprogress', 0)
|
||
bucket.set('shipping', 0)
|
||
bucket.set('delivering', 0)
|
||
bucket.set('completed', 0)
|
||
bucket.set('aftersale', 0)
|
||
bucket.set('accepted', 0)
|
||
bucket.set('scheduled', 0)
|
||
bucket.set('inservice', 0)
|
||
return bucket
|
||
}
|
||
|
||
function getBucketCount(bucket: UTSJSONObject, key: string): number {
|
||
const value = bucket.getNumber(key)
|
||
if (value != null) {
|
||
return value
|
||
}
|
||
return 0
|
||
}
|
||
|
||
function setBucketCount(bucket: UTSJSONObject, key: string, value: number): void {
|
||
bucket.set(key, value)
|
||
}
|
||
|
||
function goBack(): void {
|
||
const pages = getCurrentPages()
|
||
if (pages.length > 1) {
|
||
uni.navigateBack()
|
||
} else {
|
||
uni.switchTab({ url: '/pages/mall/consumer/profile' })
|
||
}
|
||
}
|
||
|
||
const businessTabs = computed((): BusinessTabItem[] => {
|
||
const tabs: BusinessTabItem[] = []
|
||
tabs.push({ id: 'all', name: '全部', count: getBucketCount(goodsStatusCounts.value, 'all') + getBucketCount(serviceStatusCounts.value, 'all') })
|
||
tabs.push({ id: 'goods', name: '购物', count: getBucketCount(goodsStatusCounts.value, 'all') })
|
||
tabs.push({ id: 'service', name: '服务', count: getBucketCount(serviceStatusCounts.value, 'all') })
|
||
return tabs
|
||
})
|
||
|
||
const currentStatusTabs = computed((): StatusTabItem[] => {
|
||
const tabs: StatusTabItem[] = []
|
||
if (activeBizType.value === 'service') {
|
||
tabs.push({ id: 'all', name: '全部', count: getBucketCount(serviceStatusCounts.value, 'all') })
|
||
tabs.push({ id: 'pending', name: '待付款', count: getBucketCount(serviceStatusCounts.value, 'pending') })
|
||
tabs.push({ id: 'accepted', name: '待接单', count: getBucketCount(serviceStatusCounts.value, 'accepted') })
|
||
tabs.push({ id: 'scheduled', name: '待服务', count: getBucketCount(serviceStatusCounts.value, 'scheduled') })
|
||
tabs.push({ id: 'inservice', name: '服务中', count: getBucketCount(serviceStatusCounts.value, 'inservice') })
|
||
tabs.push({ id: 'completed', name: '已完成', count: getBucketCount(serviceStatusCounts.value, 'completed') })
|
||
tabs.push({ id: 'aftersale', name: '售后', count: getBucketCount(serviceStatusCounts.value, 'aftersale') })
|
||
return tabs
|
||
}
|
||
|
||
if (activeBizType.value === 'goods') {
|
||
tabs.push({ id: 'all', name: '全部', count: getBucketCount(goodsStatusCounts.value, 'all') })
|
||
tabs.push({ id: 'pending', name: '待付款', count: getBucketCount(goodsStatusCounts.value, 'pending') })
|
||
tabs.push({ id: 'shipping', name: '待发货', count: getBucketCount(goodsStatusCounts.value, 'shipping') })
|
||
tabs.push({ id: 'delivering', name: '待收货', count: getBucketCount(goodsStatusCounts.value, 'delivering') })
|
||
tabs.push({ id: 'completed', name: '已完成', count: getBucketCount(goodsStatusCounts.value, 'completed') })
|
||
tabs.push({ id: 'aftersale', name: '售后', count: getBucketCount(goodsStatusCounts.value, 'aftersale') })
|
||
return tabs
|
||
}
|
||
|
||
tabs.push({ id: 'all', name: '全部', count: getBucketCount(goodsStatusCounts.value, 'all') + getBucketCount(serviceStatusCounts.value, 'all') })
|
||
tabs.push({ id: 'pending', name: '待付款', count: getBucketCount(goodsStatusCounts.value, 'pending') + getBucketCount(serviceStatusCounts.value, 'pending') })
|
||
tabs.push({ id: 'inprogress', name: '进行中', count: getBucketCount(goodsStatusCounts.value, 'inprogress') + getBucketCount(serviceStatusCounts.value, 'accepted') + getBucketCount(serviceStatusCounts.value, 'scheduled') + getBucketCount(serviceStatusCounts.value, 'inservice') })
|
||
tabs.push({ id: 'completed', name: '已完成', count: getBucketCount(goodsStatusCounts.value, 'completed') + getBucketCount(serviceStatusCounts.value, 'completed') })
|
||
tabs.push({ id: 'aftersale', name: '售后', count: getBucketCount(goodsStatusCounts.value, 'aftersale') + getBucketCount(serviceStatusCounts.value, 'aftersale') })
|
||
return tabs
|
||
})
|
||
|
||
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;'
|
||
})
|
||
|
||
const getOptionString = (options: UTSJSONObject, key: string): string => {
|
||
try {
|
||
const value = options.getString(key)
|
||
if (value != null) {
|
||
return value
|
||
}
|
||
} catch (e) {
|
||
}
|
||
|
||
try {
|
||
const normalized = JSON.parse(JSON.stringify(options)) as UTSJSONObject
|
||
const value = normalized.getString(key)
|
||
if (value != null) {
|
||
return value
|
||
}
|
||
} catch (e) {
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
function mapServiceInfo(rawValue: UTSJSONObject | string | number | null): UnifiedServiceInfo | null {
|
||
if (rawValue == null) {
|
||
return null
|
||
}
|
||
|
||
try {
|
||
const rawText = unwrapJsonScalar(rawValue)
|
||
if (rawText == '') {
|
||
return null
|
||
}
|
||
|
||
let infoObj: UTSJSONObject | null = null
|
||
if (rawText.startsWith('{')) {
|
||
const parsed = JSON.parse(rawText)
|
||
if (parsed != null) {
|
||
infoObj = parsed as UTSJSONObject
|
||
}
|
||
} else if (rawValue instanceof UTSJSONObject) {
|
||
infoObj = rawValue
|
||
}
|
||
|
||
if (infoObj == null) {
|
||
return null
|
||
}
|
||
|
||
return {
|
||
service_name: infoObj.getString('service_name') ?? '',
|
||
service_image: infoObj.getString('service_image') ?? '/static/images/default.png',
|
||
appointment_time: infoObj.getString('appointment_time') ?? '',
|
||
address: infoObj.getString('address') ?? '',
|
||
contact_name: infoObj.getString('contact_name') ?? '',
|
||
contact_phone: infoObj.getString('contact_phone') ?? '',
|
||
provider_name: infoObj.getString('provider_name') ?? ''
|
||
}
|
||
} catch (e) {
|
||
return null
|
||
}
|
||
}
|
||
|
||
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') ?? itemObj.getString('product_image')
|
||
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')
|
||
const bizTypeRaw = orderObj.getString('biz_type')
|
||
const bizType = bizTypeRaw != null && bizTypeRaw != '' ? bizTypeRaw : 'goods'
|
||
|
||
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 = '商家店铺'
|
||
}
|
||
|
||
const serviceInfo = mapServiceInfo(orderObj.get('service_info'))
|
||
|
||
if (productsList.length === 0 && bizType !== 'service') {
|
||
productsList.push({
|
||
id: 'placeholder',
|
||
name: '订单商品',
|
||
price: totalAmount ?? paidAmount ?? 0,
|
||
image: '/static/images/default.png',
|
||
spec: '',
|
||
quantity: 1
|
||
})
|
||
}
|
||
|
||
mappedOrders.push({
|
||
id: orderId ?? '',
|
||
order_no: orderNo ?? '',
|
||
biz_type: bizType,
|
||
source: orderObj.getString('source') ?? (bizType == 'service' ? 'service' : 'goods'),
|
||
status: orderStatus ?? 1,
|
||
payment_status: orderObj.getNumber('payment_status') ?? 1,
|
||
cancel_reason: orderObj.getString('cancel_reason') ?? '',
|
||
pay_expire_at: orderObj.getString('pay_expire_at') ?? '',
|
||
create_time: createdAt ?? '',
|
||
product_amount: productAmount ?? 0,
|
||
shipping_fee: shippingFee ?? 0,
|
||
total_amount: totalAmount ?? paidAmount ?? 0,
|
||
merchant_id: merchantId ?? '',
|
||
shop_name: shopName,
|
||
products: productsList,
|
||
service_info: serviceInfo,
|
||
dispatch_status: orderObj.getString('dispatch_status') ?? '',
|
||
dispatch_error_message: orderObj.getString('dispatch_error_message') ?? ''
|
||
})
|
||
}
|
||
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 <= 12) {
|
||
return orderNo
|
||
}
|
||
return orderNo.substring(orderNo.length - 12)
|
||
}
|
||
|
||
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 getPrimaryOrderProduct = (order: OrderItem): OrderProduct | null => {
|
||
if (order.products.length > 0) {
|
||
return order.products[0]
|
||
}
|
||
return null
|
||
}
|
||
|
||
const getExtraGoodsCount = (order: OrderItem): number => {
|
||
const hiddenCount = order.products.length - ORDER_VISIBLE_PRODUCT_LIMIT
|
||
if (hiddenCount > 0) {
|
||
return hiddenCount
|
||
}
|
||
return 0
|
||
}
|
||
|
||
const getOrderCover = (order: OrderItem): string => {
|
||
if (order.biz_type === 'service' && order.service_info != null && order.service_info.service_image != '') {
|
||
return order.service_info.service_image
|
||
}
|
||
const product = getPrimaryOrderProduct(order)
|
||
if (product != null && product.image != '') {
|
||
return product.image
|
||
}
|
||
return '/static/images/default.png'
|
||
}
|
||
|
||
const getOrderTitle = (order: OrderItem): string => {
|
||
if (order.biz_type === 'service' && order.service_info != null && order.service_info.service_name != '') {
|
||
return order.service_info.service_name
|
||
}
|
||
const product = getPrimaryOrderProduct(order)
|
||
if (product != null && product.name != '') {
|
||
return product.name
|
||
}
|
||
return '订单商品'
|
||
}
|
||
|
||
const getPrimaryProductSpec = (order: OrderItem): string => {
|
||
const product = getPrimaryOrderProduct(order)
|
||
if (product != null && product.spec != '') {
|
||
return product.spec
|
||
}
|
||
return '默认规格'
|
||
}
|
||
|
||
const getServiceAppointmentText = (order: OrderItem): string => {
|
||
if (order.service_info != null && order.service_info.appointment_time != '') {
|
||
return order.service_info.appointment_time
|
||
}
|
||
return '待确认'
|
||
}
|
||
|
||
const getServiceAddressText = (order: OrderItem): string => {
|
||
if (order.service_info != null && order.service_info.address != '') {
|
||
return order.service_info.address
|
||
}
|
||
return '服务地址待确认'
|
||
}
|
||
|
||
const getOrderPrimaryPrice = (order: OrderItem): number => {
|
||
const product = getPrimaryOrderProduct(order)
|
||
if (product != null) {
|
||
return product.price
|
||
}
|
||
return order.total_amount
|
||
}
|
||
|
||
const getOrderPrimaryCountText = (order: OrderItem): string => {
|
||
if (order.biz_type === 'service') {
|
||
return '服务预约'
|
||
}
|
||
const product = getPrimaryOrderProduct(order)
|
||
if (product != null) {
|
||
return 'x' + product.quantity
|
||
}
|
||
return 'x1'
|
||
}
|
||
|
||
const getOrderItemCount = (order: OrderItem): number => {
|
||
if (order.biz_type === 'service') {
|
||
return 1
|
||
}
|
||
if (order.products.length > 0) {
|
||
return order.products.length
|
||
}
|
||
return 1
|
||
}
|
||
|
||
const getOrderSummaryPrefix = (order: OrderItem): string => {
|
||
if (order.biz_type === 'service') {
|
||
return '共1项服务 '
|
||
}
|
||
return '含运费¥' + formatAmount(order.shipping_fee) + ' 共' + getOrderItemCount(order) + '件商品 '
|
||
}
|
||
|
||
const getOrderProductBlockHeight = (order: OrderItem): number => {
|
||
if (order.biz_type === 'service') {
|
||
return ORDER_SERVICE_PRODUCT_HEIGHT
|
||
}
|
||
return ORDER_PRODUCT_HEIGHT
|
||
}
|
||
|
||
function getOrderCardBodyHeight(order: OrderItem): number {
|
||
return ORDER_HEADER_HEIGHT + getOrderProductBlockHeight(order) + ORDER_SUMMARY_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)
|
||
}
|
||
|
||
function matchesGoodsStatusTab(status: number): boolean {
|
||
if (activeStatusTab.value === 'all') return true
|
||
if (activeStatusTab.value === 'pending') return status === 1
|
||
if (activeStatusTab.value === 'shipping') return status === 2
|
||
if (activeStatusTab.value === 'delivering') return status === 3
|
||
if (activeStatusTab.value === 'completed') return status === 4
|
||
if (activeStatusTab.value === 'aftersale') return status === 6 || status === 7
|
||
if (activeStatusTab.value === 'inprogress') return status === 2 || status === 3
|
||
return true
|
||
}
|
||
|
||
function matchesServiceStatusTab(status: number): boolean {
|
||
if (activeStatusTab.value === 'all') return true
|
||
if (activeStatusTab.value === 'pending') return status === 1
|
||
if (activeStatusTab.value === 'accepted') return status === 2
|
||
if (activeStatusTab.value === 'scheduled') return status === 3
|
||
if (activeStatusTab.value === 'inservice') return status === 4
|
||
if (activeStatusTab.value === 'completed') return status === 5
|
||
if (activeStatusTab.value === 'aftersale') return status === 6 || status === 7
|
||
return true
|
||
}
|
||
|
||
function matchesCurrentFilters(order: OrderItem): boolean {
|
||
if (activeBizType.value === 'goods' && order.biz_type !== 'goods') {
|
||
return false
|
||
}
|
||
if (activeBizType.value === 'service' && order.biz_type !== 'service') {
|
||
return false
|
||
}
|
||
if (order.biz_type === 'service') {
|
||
return matchesServiceStatusTab(order.status)
|
||
}
|
||
if (isTimeoutOrder(order)) {
|
||
return activeStatusTab.value === 'all'
|
||
}
|
||
return matchesGoodsStatusTab(order.status)
|
||
}
|
||
|
||
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, nextPaymentStatus: number = 0, nextCancelReason: string = '', nextPayExpireAt: string = ''): 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
|
||
if (nextPaymentStatus > 0) {
|
||
currentOrder.payment_status = nextPaymentStatus
|
||
}
|
||
if (nextCancelReason != '') {
|
||
currentOrder.cancel_reason = nextCancelReason
|
||
}
|
||
if (nextPayExpireAt != '') {
|
||
currentOrder.pay_expire_at = nextPayExpireAt
|
||
}
|
||
targetOrder = currentOrder
|
||
if (matchesCurrentFilters(currentOrder)) {
|
||
nextList.push(currentOrder)
|
||
}
|
||
} else {
|
||
nextList.push(currentOrder)
|
||
}
|
||
}
|
||
loadedOrders.value = nextList
|
||
updateVisibleWindow()
|
||
return targetOrder
|
||
}
|
||
|
||
function getGoodsStatusCodeByTab(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 == 'aftersale') return 6
|
||
return 0
|
||
}
|
||
|
||
async function getUnifiedOrdersCompatByCursor(): Promise<UnifiedOrdersCursorResult> {
|
||
const result = await supabaseService.getUnifiedOrdersByCursor({
|
||
cursor: nextCursor.value,
|
||
limit: pageSize.value,
|
||
bizType: activeBizType.value,
|
||
statusTab: activeStatusTab.value,
|
||
keyword: searchKeyword.value.trim()
|
||
})
|
||
|
||
return {
|
||
list: mapRawOrdersToViewModels(result.list),
|
||
nextCursor: result.nextCursor,
|
||
hasMore: result.hasMore
|
||
}
|
||
}
|
||
|
||
const loadOrderCounts = async () => {
|
||
const nextGoodsCounts = createCountBucket()
|
||
const nextServiceCounts = createCountBucket()
|
||
|
||
try {
|
||
const counts = await supabaseService.getOrderCounts()
|
||
setBucketCount(nextGoodsCounts, 'all', counts.all)
|
||
setBucketCount(nextGoodsCounts, 'pending', counts.pending)
|
||
setBucketCount(nextGoodsCounts, 'shipping', counts.shipping)
|
||
setBucketCount(nextGoodsCounts, 'delivering', counts.delivering)
|
||
setBucketCount(nextGoodsCounts, 'inprogress', counts.shipping + counts.delivering)
|
||
setBucketCount(nextGoodsCounts, 'completed', counts.completed)
|
||
setBucketCount(nextGoodsCounts, 'aftersale', counts.aftersale)
|
||
|
||
const serviceCounts = await supabaseService.getServiceOrderStatusCounts()
|
||
setBucketCount(nextServiceCounts, 'all', serviceCounts.getNumber('all') ?? 0)
|
||
setBucketCount(nextServiceCounts, 'pending', serviceCounts.getNumber('pending') ?? 0)
|
||
setBucketCount(nextServiceCounts, 'accepted', serviceCounts.getNumber('accepted') ?? 0)
|
||
setBucketCount(nextServiceCounts, 'scheduled', serviceCounts.getNumber('scheduled') ?? 0)
|
||
setBucketCount(nextServiceCounts, 'inservice', serviceCounts.getNumber('inservice') ?? 0)
|
||
setBucketCount(nextServiceCounts, 'completed', serviceCounts.getNumber('completed') ?? 0)
|
||
setBucketCount(nextServiceCounts, 'aftersale', serviceCounts.getNumber('aftersale') ?? 0)
|
||
} catch (e) {
|
||
console.error('[orders] 加载订单数量失败:', e)
|
||
}
|
||
|
||
goodsStatusCounts.value = nextGoodsCounts
|
||
serviceStatusCounts.value = nextServiceCounts
|
||
}
|
||
|
||
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 result = await getUnifiedOrdersCompatByCursor()
|
||
if (reset) {
|
||
loadedOrders.value = result.list
|
||
scrollTop.value = 0
|
||
} else {
|
||
loadedOrders.value = mergeOrders(loadedOrders.value, result.list)
|
||
}
|
||
|
||
nextCursor.value = result.nextCursor
|
||
hasMore.value = result.hasMore
|
||
updateVisibleWindow()
|
||
initialized.value = true
|
||
} catch (err) {
|
||
console.error('[orders] 加载订单失败:', err)
|
||
uni.showToast({ title: '加载订单失败', icon: 'none' })
|
||
} finally {
|
||
loading.value = false
|
||
loadingMore.value = false
|
||
refreshing.value = false
|
||
}
|
||
}
|
||
|
||
function loadMore(): void {
|
||
if (loadedOrders.value.length > 0 && loadedOrders.value.length % PREFETCH_THRESHOLD == 0) {
|
||
loadOrders(false)
|
||
return
|
||
}
|
||
loadOrders(false)
|
||
}
|
||
|
||
const refreshCurrentSlice = async () => {
|
||
resetListState()
|
||
await loadOrders(true)
|
||
await loadOrderCounts()
|
||
}
|
||
|
||
function applyLegacyOptions(status: string, type: string): void {
|
||
if (type == 'service') {
|
||
activeBizType.value = 'service'
|
||
activeStatusTab.value = 'all'
|
||
} else if (type == 'goods') {
|
||
activeBizType.value = 'goods'
|
||
activeStatusTab.value = 'all'
|
||
} else if (type == 'pending') {
|
||
activeBizType.value = 'all'
|
||
activeStatusTab.value = 'pending'
|
||
} else if (type == 'shipped') {
|
||
activeBizType.value = 'goods'
|
||
activeStatusTab.value = 'delivering'
|
||
} else if (type == 'review') {
|
||
activeBizType.value = 'all'
|
||
activeStatusTab.value = 'completed'
|
||
} else if (type == 'refund') {
|
||
activeBizType.value = 'all'
|
||
activeStatusTab.value = 'aftersale'
|
||
}
|
||
|
||
if (status == 'all') {
|
||
activeBizType.value = 'all'
|
||
activeStatusTab.value = 'all'
|
||
} else if (status == 'pending') {
|
||
activeStatusTab.value = 'pending'
|
||
} else if (status == 'shipping') {
|
||
activeBizType.value = 'goods'
|
||
activeStatusTab.value = 'shipping'
|
||
} else if (status == 'delivering') {
|
||
activeBizType.value = 'goods'
|
||
activeStatusTab.value = 'delivering'
|
||
} else if (status == 'completed') {
|
||
activeStatusTab.value = 'completed'
|
||
} else if (status == 'aftersale') {
|
||
activeStatusTab.value = 'aftersale'
|
||
}
|
||
}
|
||
|
||
onLoad((options) => {
|
||
uni.$on('orderUpdated', handleOrderUpdated)
|
||
|
||
const systemInfo = uni.getSystemInfoSync()
|
||
statusBarHeight.value = systemInfo.statusBarHeight ?? 0
|
||
let windowWidth = systemInfo.windowWidth ?? 375
|
||
headerRightReserve.value = 12
|
||
navHeight.value = 44
|
||
|
||
// #ifdef MP-WEIXIN
|
||
try {
|
||
const menuButton = uni.getMenuButtonBoundingClientRect()
|
||
headerRightReserve.value = windowWidth - menuButton.left + 8
|
||
navHeight.value = menuButton.height + 8
|
||
} catch (e) {
|
||
headerRightReserve.value = 96
|
||
navHeight.value = 44
|
||
}
|
||
// #endif
|
||
|
||
// #ifndef MP-WEIXIN
|
||
headerRightReserve.value = 12
|
||
navHeight.value = 44
|
||
// #endif
|
||
|
||
pageWindowHeight.value = systemInfo.windowHeight ?? systemInfo.screenHeight ?? 0
|
||
const windowHeight = pageWindowHeight.value
|
||
const calculatedViewport = windowHeight - statusBarHeight.value - navHeight.value - BIZ_TABS_HEIGHT - STATUS_TABS_HEIGHT - TABBAR_HEIGHT
|
||
viewportHeight.value = calculatedViewport > 0 ? calculatedViewport : windowHeight
|
||
// #ifdef APP-ANDROID
|
||
isAndroidApp.value = true
|
||
// #endif
|
||
|
||
if (options == null) return
|
||
const optionsObj = JSON.parse(JSON.stringify(options)) as UTSJSONObject
|
||
const status = getOptionString(optionsObj, 'status')
|
||
const type = getOptionString(optionsObj, 'type')
|
||
applyLegacyOptions(status, type)
|
||
})
|
||
|
||
onUnload(() => {
|
||
stopOrderTicker()
|
||
uni.$off('orderUpdated', handleOrderUpdated)
|
||
})
|
||
|
||
onHide(() => {
|
||
stopOrderTicker()
|
||
})
|
||
|
||
onShow(() => {
|
||
startOrderTicker()
|
||
if (!initialized.value) {
|
||
loadOrders(true)
|
||
loadOrderCounts()
|
||
}
|
||
})
|
||
|
||
const handleOrderUpdated = (payload: any) => {
|
||
try {
|
||
const payloadObj = JSON.parse(JSON.stringify(payload)) as UTSJSONObject
|
||
const targetOrderId = payloadObj.getString('orderId') ?? ''
|
||
if (targetOrderId == '') {
|
||
return
|
||
}
|
||
const nextStatus = payloadObj.getNumber('status') ?? 0
|
||
const nextPaymentStatus = payloadObj.getNumber('paymentStatus') ?? 0
|
||
const nextCancelReason = payloadObj.getString('cancelReason') ?? ''
|
||
updateLoadedOrderStatus(targetOrderId, nextStatus, nextPaymentStatus, nextCancelReason)
|
||
loadOrderCounts()
|
||
} catch (e) {
|
||
refreshCurrentSlice()
|
||
}
|
||
}
|
||
|
||
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 switchBizTab = (tabId: string) => {
|
||
if (activeBizType.value === tabId) return
|
||
activeBizType.value = tabId
|
||
activeStatusTab.value = 'all'
|
||
resetListState()
|
||
loadOrders(true)
|
||
loadOrderCounts()
|
||
}
|
||
|
||
const switchStatusTab = (tabId: string) => {
|
||
if (activeStatusTab.value === tabId) return
|
||
activeStatusTab.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 getOrderStatusText = (order: OrderItem): string => {
|
||
const displayState = getUnifiedDisplayState(toOrderStatusSource(order))
|
||
if (displayState == 'pending_pay') return '待付款'
|
||
if (displayState == 'cancelled' || displayState == 'expired') return '已取消'
|
||
|
||
if (order.biz_type === 'service') {
|
||
if (order.payment_status == 1 && order.status == 1) return '待付款'
|
||
if (order.payment_status == 2 && (order.dispatch_status == 'pending' || order.dispatch_status == '')) return '待派单'
|
||
if (order.payment_status == 2 && order.dispatch_status == 'dispatching') return '正在派单'
|
||
if (order.payment_status == 2 && order.dispatch_status == 'failed') return '派单未成功'
|
||
if (order.payment_status == 2 && order.dispatch_status == 'assigned') return '已派单'
|
||
if (order.status == 2) return '待接单'
|
||
if (order.status == 3) return '待服务'
|
||
if (order.status == 4) return '服务中'
|
||
if (order.status == 5) return '已完成'
|
||
if (order.status == 6) return '退款中'
|
||
if (order.status == 7) return '已退款'
|
||
if (order.status == 8) return '已取消'
|
||
return '服务处理中'
|
||
}
|
||
|
||
if (order.status == 1) return '待付款'
|
||
if (order.status == 2) return '待发货'
|
||
if (order.status == 3) return '待收货'
|
||
if (order.status == 4) return '已完成'
|
||
if (order.status == 5) return '已取消'
|
||
if (order.status == 6) return '退款中'
|
||
if (order.status == 7) return '已退款'
|
||
return '未知状态'
|
||
}
|
||
|
||
const getStatusClass = (order: OrderItem): string => {
|
||
const displayState = getUnifiedDisplayState(toOrderStatusSource(order))
|
||
if (displayState == 'pending_pay') return 'status-pending'
|
||
if (displayState == 'cancelled' || displayState == 'expired') return 'status-cancelled'
|
||
if (order.status == 2 || order.status == 3 || order.status == 4) return 'status-processing'
|
||
if (order.status == 5 || order.status == ORDER_STATUS_TIMEOUT_LEGACY) return 'status-cancelled'
|
||
if (order.status == 6 || order.status == 7) return 'status-aftersale'
|
||
return 'status-default'
|
||
}
|
||
|
||
const getOrderShopName = (order: OrderItem): string => {
|
||
if (order.biz_type === 'service') {
|
||
if (order.service_info != null && order.service_info.provider_name != '') {
|
||
return order.service_info.provider_name
|
||
}
|
||
if (order.shop_name != '') {
|
||
return order.shop_name
|
||
}
|
||
return '康养上门服务'
|
||
}
|
||
if (order.shop_name != '') {
|
||
return order.shop_name
|
||
}
|
||
return '康养商城'
|
||
}
|
||
|
||
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 currentOrder = getLoadedOrderById(orderId)
|
||
const orderSource = currentOrder != null ? currentOrder.source : 'goods'
|
||
const success = await supabaseService.softDeleteUnifiedOrderForConsumer(orderId, orderSource)
|
||
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 currentOrder = getLoadedOrderById(orderId)
|
||
const oldPayExpireAt = currentOrder != null ? currentOrder.pay_expire_at : ''
|
||
const orderSource = currentOrder != null ? currentOrder.source : 'goods'
|
||
const success = await supabaseService.cancelUnifiedOrder(orderId, orderSource, '用户取消订单')
|
||
uni.hideLoading()
|
||
if (success) {
|
||
const nextStatus = orderSource == 'service' ? ORDER_STATUS_TIMEOUT_LEGACY : ORDER_STATUS_CANCELLED
|
||
uni.showToast({ title: '订单已取消', icon: 'success' })
|
||
updateLoadedOrderStatus(orderId, nextStatus, PAYMENT_STATUS_TIMEOUT, '用户取消订单', oldPayExpireAt)
|
||
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 = async (order: OrderItem) => {
|
||
nowTick.value = Date.now()
|
||
|
||
const totalAmount = order.total_amount
|
||
const userId = supabaseService.getCurrentUserId()
|
||
const paymentUrl = `/pages/mall/consumer/payment?orderId=${encodeURIComponent(order.id)}&bizType=${encodeURIComponent(order.biz_type)}&source=${encodeURIComponent(order.source)}&amount=${totalAmount}`
|
||
if (userId == null || userId === '') {
|
||
goToLogin(paymentUrl)
|
||
return
|
||
}
|
||
|
||
const latestOrder = await supabaseService.getUnifiedOrderDetail(order.id, order.source)
|
||
if (latestOrder == null) {
|
||
uni.showToast({ title: '订单不存在或已删除', icon: 'none' })
|
||
return
|
||
}
|
||
if (latestOrder != null) {
|
||
const latestObj = JSON.parse(JSON.stringify(latestOrder)) as UTSJSONObject
|
||
const latestStatus = latestObj.getNumber('order_status') ?? 1
|
||
const latestPaymentStatus = latestObj.getNumber('payment_status') ?? 1
|
||
const latestCancelReason = latestObj.getString('cancel_reason') ?? ''
|
||
const latestPayExpireAt = latestObj.getString('pay_expire_at') ?? ''
|
||
const latestViewOrder: OrderItem = {
|
||
...order,
|
||
status: latestStatus,
|
||
payment_status: latestPaymentStatus,
|
||
cancel_reason: latestCancelReason,
|
||
pay_expire_at: latestPayExpireAt
|
||
}
|
||
if (isTimeoutOrder(latestViewOrder)) {
|
||
await supabaseService.expireUnifiedOrder(order.id, order.source)
|
||
const nextStatus = order.source == 'service' ? ORDER_STATUS_TIMEOUT_LEGACY : ORDER_STATUS_CANCELLED
|
||
updateLoadedOrderStatus(order.id, nextStatus, PAYMENT_STATUS_TIMEOUT, ORDER_TIMEOUT_CANCEL_REASON, latestPayExpireAt)
|
||
uni.showToast({ title: '订单已取消,不能继续支付', icon: 'none' })
|
||
loadOrderCounts()
|
||
return
|
||
}
|
||
if (latestStatus != ORDER_STATUS_PENDING || latestPaymentStatus != PAYMENT_STATUS_UNPAID) {
|
||
updateLoadedOrderStatus(order.id, latestStatus, latestPaymentStatus, latestCancelReason, latestPayExpireAt)
|
||
uni.showToast({ title: '订单状态已变更,不能继续支付', icon: 'none' })
|
||
loadOrderCounts()
|
||
return
|
||
}
|
||
}
|
||
|
||
uni.navigateTo({ url: paymentUrl })
|
||
}
|
||
|
||
const viewSimilar = (order: OrderItem) => {
|
||
const firstProduct = order.products.length > 0 ? order.products[0] : null
|
||
if (firstProduct == null) {
|
||
uni.showToast({ title: '暂无相似商品', icon: 'none' })
|
||
return
|
||
}
|
||
const keyword = firstProduct.name != '' ? firstProduct.name : '商品'
|
||
uni.navigateTo({ url: `/pages/mall/consumer/search?keyword=${encodeURIComponent(keyword)}` })
|
||
}
|
||
|
||
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 confirmServiceFinished = (order: OrderItem) => {
|
||
uni.showModal({
|
||
title: '确认完成',
|
||
content: '确认本次康养服务已完成吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
// TODO: 服务订单接入统一接口后,替换为真实完成服务 API。
|
||
uni.showToast({ title: '服务完成接口待接入', icon: 'none' })
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
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 rebookService = (order: OrderItem) => {
|
||
if (order.service_info != null && order.service_info.service_name != '') {
|
||
uni.showToast({ title: '再次预约功能待接入', icon: 'none' })
|
||
return
|
||
}
|
||
uni.showToast({ title: '暂无可预约服务', icon: 'none' })
|
||
}
|
||
|
||
let isRedispatching = false
|
||
|
||
const redispatchServiceOrder = async (order: OrderItem) => {
|
||
if (isRedispatching) {
|
||
return
|
||
}
|
||
isRedispatching = true
|
||
uni.showLoading({ title: '正在重新派单', mask: true })
|
||
try {
|
||
const result = await dispatchPaidHomecareOrder(order.id)
|
||
uni.hideLoading()
|
||
if (result.success) {
|
||
uni.showToast({ title: '派单成功', icon: 'success' })
|
||
setTimeout(() => {
|
||
uni.navigateTo({ url: `/pages/mall/consumer/home-service/order-detail?id=${order.id}` })
|
||
}, 800)
|
||
return
|
||
}
|
||
showHomecareDispatchFailureModal(order.id, result, (id: string) => {
|
||
const targetOrder = getLoadedOrderById(id)
|
||
if (targetOrder != null) {
|
||
redispatchServiceOrder(targetOrder)
|
||
}
|
||
})
|
||
} catch (e) {
|
||
uni.hideLoading()
|
||
console.error('[redispatchServiceOrder] 重新派单异常:', e)
|
||
uni.showModal({
|
||
title: '派单服务异常',
|
||
content: '派单服务暂时异常,请稍后重试',
|
||
showCancel: true,
|
||
cancelText: '稍后再试',
|
||
confirmText: '重新派单',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
redispatchServiceOrder(order)
|
||
}
|
||
}
|
||
})
|
||
} finally {
|
||
isRedispatching = false
|
||
}
|
||
}
|
||
|
||
const viewSimilarService = (order: OrderItem) => {
|
||
const serviceName = order.service_info != null ? order.service_info.service_name : ''
|
||
const url = serviceName != ''
|
||
? `/pages/mall/consumer/home-service/index?keyword=${encodeURIComponent(serviceName)}`
|
||
: '/pages/mall/consumer/home-service/index'
|
||
uni.navigateTo({ url })
|
||
}
|
||
|
||
const viewOrderDetail = (order: OrderItem) => {
|
||
if (order.biz_type === 'service') {
|
||
uni.navigateTo({ url: `/pages/mall/consumer/home-service/order-detail?id=${order.id}&source=${order.source}` })
|
||
return
|
||
}
|
||
uni.navigateTo({ url: `/pages/mall/consumer/order-detail?id=${order.id}&source=${order.source}` })
|
||
}
|
||
|
||
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 contactServiceProvider = (order: OrderItem) => {
|
||
contactSeller(order)
|
||
}
|
||
|
||
const getOrderActions = (order: OrderItem): OrderActionItem[] => {
|
||
const actions: OrderActionItem[] = []
|
||
const displayState = getUnifiedDisplayState(toOrderStatusSource(order))
|
||
|
||
if (displayState == 'pending_pay') {
|
||
actions.push({ id: 'cancel', name: order.biz_type === 'service' ? '取消预约' : '取消订单', kind: 'secondary' })
|
||
actions.push({ id: 'pay', name: '立即支付', kind: 'primary' })
|
||
return actions
|
||
}
|
||
|
||
if (displayState == 'cancelled' || displayState == 'expired') {
|
||
if (order.biz_type === 'service') {
|
||
actions.push({ id: 'view-similar-service', name: '看相似服务', kind: 'secondary' })
|
||
actions.push({ id: 'rebook', name: '再次预约', kind: 'primary' })
|
||
return actions
|
||
}
|
||
actions.push({ id: 'view-similar', name: '看相似', kind: 'secondary' })
|
||
actions.push({ id: 'repurchase', name: '再次购买', kind: 'primary' })
|
||
return actions
|
||
}
|
||
|
||
if (order.biz_type === 'service') {
|
||
if (order.payment_status == 2 && order.dispatch_status == 'failed') {
|
||
actions.push({ id: 'view-detail', name: '查看详情', kind: 'secondary' })
|
||
actions.push({ id: 'redispatch', name: '重新派单', kind: 'primary' })
|
||
return actions
|
||
}
|
||
if (order.status === 2) {
|
||
actions.push({ id: 'cancel', name: '取消预约', kind: 'secondary' })
|
||
actions.push({ id: 'contact-service', name: '联系客服', kind: 'secondary' })
|
||
} else if (order.status === 3) {
|
||
actions.push({ id: 'view-detail', name: '查看预约', kind: 'secondary' })
|
||
actions.push({ id: 'contact-provider', name: '联系服务人员', kind: 'secondary' })
|
||
} else if (order.status === 4) {
|
||
actions.push({ id: 'contact-provider', name: '联系服务人员', kind: 'secondary' })
|
||
actions.push({ id: 'confirm-service', name: '确认完成', kind: 'primary' })
|
||
} else if (displayState == 'completed') {
|
||
actions.push({ id: 'service-review', name: '评价服务', kind: 'secondary' })
|
||
actions.push({ id: 'rebook', name: '再次预约', kind: 'primary' })
|
||
} else if (order.status === 6 || order.status === 7) {
|
||
actions.push({ id: 'refund-progress', name: '退款进度', kind: 'secondary' })
|
||
actions.push({ id: 'view-detail', name: '查看详情', kind: 'secondary' })
|
||
}
|
||
return actions
|
||
}
|
||
|
||
if (order.status === 2) {
|
||
actions.push({ id: 'remind', name: '提醒发货', kind: 'secondary' })
|
||
actions.push({ id: 'refund', name: '申请售后', kind: 'secondary' })
|
||
} else if (order.status === 3) {
|
||
actions.push({ id: 'logistics', name: '查看物流', kind: 'secondary' })
|
||
actions.push({ id: 'confirm', name: '确认收货', kind: 'primary' })
|
||
actions.push({ id: 'refund', name: '申请售后', kind: 'secondary' })
|
||
} else if (displayState == 'completed') {
|
||
actions.push({ id: 'refund', name: '申请售后', kind: 'secondary' })
|
||
actions.push({ id: 'repurchase', name: '再次购买', kind: 'primary' })
|
||
actions.push({ id: 'review', name: '评价晒单', kind: 'secondary' })
|
||
} else if (order.status === 6 || order.status === 7) {
|
||
actions.push({ id: 'refund-progress', name: '退款进度', kind: 'secondary' })
|
||
actions.push({ id: 'view-detail', name: '查看详情', kind: 'secondary' })
|
||
}
|
||
return actions
|
||
}
|
||
|
||
const onOrderActionTap = (order: OrderItem, actionId: string) => {
|
||
if (actionId === 'cancel') {
|
||
cancelOrder(order.id)
|
||
} else if (actionId === 'pay') {
|
||
payOrder(order)
|
||
} else if (actionId === 'remind') {
|
||
remindShipping(order.id)
|
||
} else if (actionId === 'refund') {
|
||
onApplyRefund(order)
|
||
} else if (actionId === 'logistics') {
|
||
viewLogistics(order.id)
|
||
} else if (actionId === 'confirm') {
|
||
confirmReceipt(order.id)
|
||
} else if (actionId === 'review') {
|
||
goReview(order)
|
||
} else if (actionId === 'repurchase') {
|
||
repurchase(order)
|
||
} else if (actionId === 'view-similar') {
|
||
viewSimilar(order)
|
||
} else if (actionId === 'view-similar-service') {
|
||
viewSimilarService(order)
|
||
} else if (actionId === 'view-detail') {
|
||
viewOrderDetail(order)
|
||
} else if (actionId === 'refund-progress') {
|
||
viewRefundProgress(order.id)
|
||
} else if (actionId === 'contact-service' || actionId === 'contact-provider') {
|
||
contactServiceProvider(order)
|
||
} else if (actionId === 'confirm-service') {
|
||
confirmServiceFinished(order)
|
||
} else if (actionId === 'service-review') {
|
||
uni.showToast({ title: '服务评价功能待接入', icon: 'none' })
|
||
} else if (actionId === 'rebook') {
|
||
rebookService(order)
|
||
} else if (actionId === 'redispatch') {
|
||
redispatchServiceOrder(order)
|
||
}
|
||
}
|
||
|
||
const showOrderMenu = (order: OrderItem) => {
|
||
const actions = getOrderActions(order)
|
||
if (actions.length === 0) {
|
||
uni.showToast({ title: '暂无可执行操作', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
const actionNames: string[] = []
|
||
for (let i = 0; i < actions.length; i++) {
|
||
actionNames.push(actions[i].name)
|
||
}
|
||
|
||
uni.showActionSheet({
|
||
itemList: actionNames,
|
||
success: (res) => {
|
||
const action = actions[res.tapIndex]
|
||
onOrderActionTap(order, action.id)
|
||
}
|
||
})
|
||
}
|
||
|
||
const navigateToProduct = (product: OrderProduct) => {
|
||
if (product.id == '' || product.id == 'placeholder') {
|
||
return
|
||
}
|
||
uni.navigateTo({ url: `/pages/mall/consumer/product-detail?id=${product.id}` })
|
||
}
|
||
|
||
const onCardContentTap = (order: OrderItem) => {
|
||
if (order.biz_type === 'service') {
|
||
viewOrderDetail(order)
|
||
return
|
||
}
|
||
const product = getPrimaryOrderProduct(order)
|
||
if (product != null) {
|
||
navigateToProduct(product)
|
||
}
|
||
}
|
||
|
||
const goShopping = () => {
|
||
uni.switchTab({ url: '/pages/main/index' })
|
||
}
|
||
|
||
const getEmptyStateTitle = (): string => {
|
||
if (activeBizType.value === 'service') {
|
||
return '暂无服务订单'
|
||
}
|
||
if (activeBizType.value === 'goods') {
|
||
return '暂无购物订单'
|
||
}
|
||
return '暂无订单'
|
||
}
|
||
|
||
const getEmptyStateDescription = (): string => {
|
||
if (activeBizType.value === 'service') {
|
||
return '康养服务预约记录会展示在这里'
|
||
}
|
||
return '去逛逛,发现心仪的商品与服务'
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.orders-page {
|
||
width: 100%;
|
||
height: 100vh;
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background-color: #f5f5f5;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.orders-header,
|
||
.biz-tabs-wrap,
|
||
.status-tabs-wrap {
|
||
width: 100%;
|
||
flex-shrink: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.orders-header {
|
||
background-color: #ffffff;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
flex-shrink: 0;
|
||
z-index: 20;
|
||
}
|
||
|
||
.custom-nav-row {
|
||
height: 44px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
padding-left: 10px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.nav-back-btn {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 18px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
margin-right: 6px;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.nav-back-icon {
|
||
font-size: 30px;
|
||
line-height: 32px;
|
||
color: #222222;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.search-shell {
|
||
flex: 1;
|
||
height: 36px;
|
||
border-radius: 18px;
|
||
background-color: #f5f5f5;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
padding: 0 12px;
|
||
box-sizing: border-box;
|
||
min-width: 0;
|
||
}
|
||
|
||
.search-icon-left {
|
||
font-size: 15px;
|
||
color: #999999;
|
||
margin-right: 6px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.search-clear {
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 10px;
|
||
line-height: 20px;
|
||
text-align: center;
|
||
color: #999999;
|
||
background-color: #dddddd;
|
||
font-size: 16px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.search-input {
|
||
flex: 1;
|
||
height: 36px;
|
||
line-height: 36px;
|
||
font-size: 14px;
|
||
color: #333333;
|
||
background-color: transparent;
|
||
border: none;
|
||
padding: 0;
|
||
min-width: 0;
|
||
}
|
||
|
||
.search-input::placeholder {
|
||
color: #a0a0a0;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.biz-tabs-wrap {
|
||
height: 44px;
|
||
background-color: #ffffff;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
border-bottom: 1px solid #f2f2f2;
|
||
}
|
||
|
||
.biz-tab-item {
|
||
flex: 1;
|
||
height: 44px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
}
|
||
|
||
.biz-tab-item.active .biz-tab-text {
|
||
color: #ff3b1f;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.biz-tab-text {
|
||
font-size: 16px;
|
||
color: #333333;
|
||
}
|
||
|
||
.biz-tab-badge {
|
||
margin-left: 6px;
|
||
min-width: 16px;
|
||
padding: 0 5px;
|
||
height: 16px;
|
||
line-height: 16px;
|
||
border-radius: 8px;
|
||
background-color: #ffebe7;
|
||
color: #ff3b1f;
|
||
font-size: 10px;
|
||
text-align: center;
|
||
}
|
||
|
||
.biz-tab-indicator {
|
||
position: absolute;
|
||
left: 28px;
|
||
right: 28px;
|
||
bottom: 4px;
|
||
height: 3px;
|
||
border-radius: 2px;
|
||
background-color: #ff3b1f;
|
||
}
|
||
|
||
.status-tabs-wrap {
|
||
height: 42px;
|
||
background-color: #ffffff;
|
||
flex-shrink: 0;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.status-tabs-scroll {
|
||
width: 100%;
|
||
height: 42px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.status-tabs-inner {
|
||
height: 42px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: nowrap;
|
||
align-items: center;
|
||
padding: 0 10px;
|
||
box-sizing: border-box;
|
||
width: max-content;
|
||
}
|
||
|
||
.status-tab-item {
|
||
height: 30px;
|
||
min-width: 72px;
|
||
padding: 0 14px;
|
||
margin-right: 8px;
|
||
border-radius: 15px;
|
||
background-color: #f7f7f7;
|
||
border-radius: 15px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.status-tab-item.active {
|
||
background-color: #fff1ef;
|
||
border: 1px solid #ff3b1f;
|
||
}
|
||
|
||
.status-tab-text {
|
||
font-size: 13px;
|
||
color: #333333;
|
||
}
|
||
|
||
.status-tab-item.active .status-tab-text {
|
||
color: #ff3b1f;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.status-tab-badge {
|
||
margin-left: 4px;
|
||
min-width: 16px;
|
||
height: 16px;
|
||
border-radius: 8px;
|
||
padding: 0 4px;
|
||
background-color: #ff3b1f;
|
||
color: #ffffff;
|
||
font-size: 10px;
|
||
line-height: 16px;
|
||
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: 88px 24px;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 72px;
|
||
color: #d8d8d8;
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.empty-title {
|
||
font-size: 18px;
|
||
color: #444444;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.empty-desc {
|
||
font-size: 13px;
|
||
color: #999999;
|
||
margin-bottom: 26px;
|
||
}
|
||
|
||
.go-shopping-btn {
|
||
height: 36px;
|
||
line-height: 36px;
|
||
border-radius: 18px;
|
||
background-color: #ff3b1f;
|
||
color: #ffffff;
|
||
font-size: 14px;
|
||
padding: 0 28px;
|
||
border: none;
|
||
}
|
||
|
||
.virtual-list,
|
||
.virtual-top-spacer,
|
||
.virtual-bottom-spacer {
|
||
width: 100%;
|
||
}
|
||
|
||
.virtual-order-item {
|
||
width: 100%;
|
||
padding: 10px 12px 0 12px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.order-card {
|
||
width: 100%;
|
||
background-color: #ffffff;
|
||
border-radius: 12px;
|
||
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.04);
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.order-card-header {
|
||
height: 44px;
|
||
padding: 8px 12px 4px 12px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.shop-icon {
|
||
font-size: 14px;
|
||
margin-right: 6px;
|
||
}
|
||
|
||
.shop-name {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #222222;
|
||
max-width: 180px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
lines: 1;
|
||
}
|
||
|
||
.arrow-right {
|
||
font-size: 12px;
|
||
color: #bbbbbb;
|
||
margin-left: 4px;
|
||
}
|
||
|
||
.order-meta-line {
|
||
margin-top: 2px;
|
||
font-size: 11px;
|
||
color: #9a9a9a;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
lines: 1;
|
||
}
|
||
|
||
.status-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.status-main {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.order-status {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.order-status-countdown {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
margin-left: 4px;
|
||
}
|
||
|
||
.status-pending,
|
||
.status-processing,
|
||
.status-aftersale {
|
||
color: #ff3b1f;
|
||
}
|
||
|
||
.status-cancelled,
|
||
.status-expired,
|
||
.status-default {
|
||
color: #999999;
|
||
}
|
||
|
||
.delete-order-icon {
|
||
font-size: 16px;
|
||
color: #999999;
|
||
margin-left: 8px;
|
||
padding: 0 2px;
|
||
}
|
||
|
||
.more-btn {
|
||
font-size: 18px;
|
||
color: #999999;
|
||
margin-left: 6px;
|
||
padding: 0 4px;
|
||
}
|
||
|
||
.order-main {
|
||
height: 88px;
|
||
padding: 8px 12px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: stretch;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.order-cover {
|
||
width: 72px;
|
||
height: 72px;
|
||
border-radius: 8px;
|
||
background-color: #f2f2f2;
|
||
margin-right: 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.order-main-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.order-main-title {
|
||
font-size: 14px;
|
||
color: #262626;
|
||
line-height: 1.35;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
lines: 2;
|
||
}
|
||
|
||
.goods-meta-wrap,
|
||
.service-meta-wrap {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.goods-spec-chip {
|
||
max-width: 190px;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
background-color: #f3f3f3;
|
||
color: #888888;
|
||
font-size: 11px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
lines: 1;
|
||
}
|
||
|
||
.goods-extra-text {
|
||
margin-top: 4px;
|
||
font-size: 11px;
|
||
color: #999999;
|
||
}
|
||
|
||
.service-meta-text {
|
||
font-size: 11px;
|
||
color: #8a8a8a;
|
||
line-height: 1.4;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
lines: 1;
|
||
}
|
||
|
||
.order-main-footer {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-top: auto;
|
||
}
|
||
|
||
.order-main-price {
|
||
font-size: 15px;
|
||
color: #222222;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.order-main-count {
|
||
font-size: 12px;
|
||
color: #999999;
|
||
}
|
||
|
||
.order-summary {
|
||
height: 36px;
|
||
padding: 0 12px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.order-time {
|
||
font-size: 11px;
|
||
color: #9a9a9a;
|
||
}
|
||
|
||
.summary-right {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.summary-text,
|
||
.summary-price-label {
|
||
font-size: 12px;
|
||
color: #666666;
|
||
}
|
||
|
||
.summary-price-label {
|
||
margin-left: 4px;
|
||
margin-right: 4px;
|
||
}
|
||
|
||
.summary-price {
|
||
font-size: 15px;
|
||
color: #222222;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.order-actions {
|
||
height: 54px;
|
||
padding: 8px 12px 12px 12px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.action-buttons {
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
}
|
||
|
||
.action-btn {
|
||
min-width: 74px;
|
||
height: 32px;
|
||
line-height: 32px;
|
||
border-radius: 16px;
|
||
font-size: 12px;
|
||
padding: 0 14px;
|
||
margin-left: 8px;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.action-btn.secondary {
|
||
color: #666666;
|
||
border: 1px solid #d8d8d8;
|
||
}
|
||
|
||
.action-btn.primary {
|
||
color: #ff3b1f;
|
||
border: 1px solid #ff3b1f;
|
||
background-color: #fff6f4;
|
||
}
|
||
|
||
.loading-more,
|
||
.no-more {
|
||
width: 100%;
|
||
padding: 18px 0 8px 0;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #999999;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.loading-spinner {
|
||
width: 14px;
|
||
height: 14px;
|
||
border-radius: 7px;
|
||
border: 2px solid #ffd4cc;
|
||
border-top-color: #ff3b1f;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.safe-area,
|
||
.tabbar-placeholder {
|
||
width: 100%;
|
||
height: 50px;
|
||
flex-shrink: 0;
|
||
}
|
||
</style>
|