Files
medical-mall/pages/mall/merchant/orders.uvue
2026-04-13 11:32:31 +08:00

1605 lines
44 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 机构端 - 服务订单管理页面 -->
<template>
<view class="orders-page">
<!-- #ifdef MP-WEIXIN -->
<view class="mp-tab-navbar">
<text class="mp-tab-title">服务订单</text>
</view>
<!-- #endif -->
<!-- 一级切换:服务订单 / 取消售后 -->
<view class="lvl1-tabs">
<view
class="lvl1-tab-item"
:class="{ active: mainTab === 'order' }"
@click="switchMainTab('order')"
>
<text class="lvl1-tab-text">服务订单</text>
</view>
<view
class="lvl1-tab-item"
:class="{ active: mainTab === 'aftersale' }"
@click="switchMainTab('aftersale')"
>
<text class="lvl1-tab-text">取消售后</text>
</view>
</view>
<!-- 服务订单内容 -->
<view v-if="mainTab === 'order'" class="tab-page-content">
<!-- 状态 Tabs -->
<view class="tabs-container">
<view class="tabs-scroll">
<view
v-for="(tab, index) in orderTabs"
:key="index"
class="tab-item"
:class="{ active: currentTab === tab.status }"
@click="switchTab(tab.status)"
>
<text class="tab-text">{{ tab.name }}</text>
<view v-if="tab.count > 0" class="tab-badge">{{ tab.count }}</view>
</view>
</view>
</view>
<!-- 待接单筛选胶囊 -->
<view v-if="currentTab === 2" class="filter-capsules">
<view class="filter-capsule active">
<text class="filter-capsule-text">全部待接单</text>
</view>
<view class="filter-capsule" @click="toastNotSupported">
<text class="filter-capsule-text">即将超时</text>
</view>
<view class="filter-capsule" @click="toastNotSupported">
<text class="filter-capsule-text">今日上门</text>
</view>
</view>
<!-- 搜索栏 -->
<view class="search-bar">
<input
class="search-input"
type="text"
v-model="searchKeyword"
placeholder="搜索订单号/服务对象"
@confirm="handleSearch"
/>
<view class="search-btn" @click="handleSearch">搜索</view>
</view>
<!-- 待接单工具条 -->
<view v-if="currentTab === 2" class="toolbar-strip">
<text class="toolbar-item">最新预约在上</text>
<view class="toolbar-divider"></view>
<text class="toolbar-item" @click="toastNotSupported">标记</text>
<text class="toolbar-item" @click="toastNotSupported">备注</text>
<text class="toolbar-item" @click="toastNotSupported">催服务</text>
</view>
<!-- 服务订单列表 -->
<scroll-view
class="orders-list"
direction="vertical"
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="loadMore"
>
<view v-if="loading && orders.length === 0" class="ske-orders-wrap">
<view v-for="n in 4" :key="n" class="ske-order-card">
<view class="ske-order-hd">
<view class="ske-bar ske-w50 ske-h26"></view>
<view class="ske-bar ske-w22 ske-h26"></view>
</view>
<view class="ske-order-product">
<view class="ske-product-img"></view>
<view class="ske-product-info">
<view class="ske-bar ske-w70 ske-h28 ske-mb10"></view>
<view class="ske-bar ske-w40 ske-h22"></view>
</view>
</view>
<view class="ske-order-ft">
<view class="ske-bar ske-w30 ske-h22"></view>
<view class="ske-bar ske-w25 ske-h30"></view>
</view>
</view>
</view>
<view v-else-if="orders.length === 0" class="empty-container">
<text class="empty-icon">📋</text>
<text class="empty-text">暂无服务订单</text>
</view>
<view v-else>
<view
v-for="order in orders"
:key="order.id"
class="order-card"
@click="viewOrderDetail(order.id)"
>
<!-- 卡片顶部:服务对象 + 状态 -->
<view class="card-header">
<view class="card-header-left">
<text class="buyer-label">服务对象</text>
<text class="buyer-name">{{ maskBuyerName(order.user_id) }}</text>
<text class="order-time-small">{{ formatTime(order.created_at) }}</text>
</view>
<view class="card-header-right">
<text class="card-status-text" :class="'cstatus-' + order.order_status">{{ getStatusText(order.order_status) }}</text>
</view>
</view>
<!-- 服务项目区 -->
<view class="card-products">
<view
v-for="item in order.items"
:key="item.id"
class="pdd-product-item"
>
<image
:src="item.image_url || '/static/images/default-product.png'"
class="pdd-product-img"
mode="aspectFill"
/>
<view class="pdd-product-main">
<text class="pdd-product-name">{{ item.product_name }}</text>
<view v-if="item.sku_name" class="pdd-product-spec-wrap">
<text class="pdd-product-spec">{{ item.sku_name }}</text>
</view>
<text class="pdd-product-received">实收:¥{{ (item.price * item.quantity).toFixed(2) }}</text>
</view>
<view class="pdd-product-right">
<text class="pdd-product-price">¥{{ item.price }}</text>
<text class="pdd-product-qty">x{{ item.quantity }}</text>
</view>
</view>
</view>
<!-- 卡片底部操作区 -->
<view class="card-footer">
<view class="card-footer-left">
<text class="footer-amount-label">共{{ getTotalQuantity(order.items) }}项服务</text>
<text class="footer-amount-val">实付 ¥{{ order.total_amount }}</text>
</view>
<view class="card-footer-btns">
<template v-if="order.order_status === 2">
<view class="card-btn secondary" @click.stop="toastNotSupported">备注</view>
<view class="card-btn primary" @click.stop="shipOrder(order)">安排服务</view>
</template>
<template v-else-if="order.order_status === 3">
<view class="card-btn secondary" @click.stop="viewLogistics(order)">服务进度</view>
</template>
<template v-else-if="order.order_status === -1 || order.order_status === 5">
<view class="card-btn danger-outline" @click.stop="deleteOrder(order)">删除</view>
</template>
</view>
</view>
</view>
</view>
<view v-if="loadingMore" class="load-more">
<text class="load-more-text">加载中...</text>
</view>
<view v-if="!hasMore && orders.length > 0" class="no-more">
<text class="no-more-text">没有更多了</text>
</view>
<view class="safe-bottom" :style="{ height: safeBottomHeight }"></view>
</scroll-view>
</view>
<!-- 取消售后内容 -->
<view v-if="mainTab === 'aftersale'" class="tab-page-content">
<!-- 售后状态 Tabs -->
<view class="tabs-container">
<view class="tabs-scroll">
<view
v-for="(tab, index) in aftersaleTabs"
:key="index"
class="tab-item"
:class="{ active: currentAftersaleTab === tab.status }"
@click="switchAftersaleTab(tab.status)"
>
<text class="tab-text">{{ tab.name }}</text>
<view v-if="tab.count > 0" class="tab-badge">{{ tab.count }}</view>
</view>
</view>
</view>
<!-- 售后搜索栏 -->
<view class="search-bar">
<input
class="search-input"
type="text"
v-model="aftersaleKeyword"
placeholder="搜索订单号/服务对象"
@confirm="handleAftersaleSearch"
/>
<view class="search-btn" @click="handleAftersaleSearch">搜索</view>
</view>
<!-- 售后列表 -->
<scroll-view
class="orders-list"
direction="vertical"
:refresher-enabled="true"
:refresher-triggered="aftersaleRefreshing"
@refresherrefresh="onAftersaleRefresh"
@scrolltolower="loadMoreAftersale"
>
<view v-if="aftersaleLoading && aftersaleOrders.length === 0" class="ske-orders-wrap">
<view v-for="n in 3" :key="n" class="ske-order-card">
<view class="ske-order-hd">
<view class="ske-bar ske-w50 ske-h26"></view>
<view class="ske-bar ske-w22 ske-h26"></view>
</view>
<view class="ske-order-product">
<view class="ske-product-img"></view>
<view class="ske-product-info">
<view class="ske-bar ske-w70 ske-h28 ske-mb10"></view>
<view class="ske-bar ske-w40 ske-h22"></view>
</view>
</view>
</view>
</view>
<view v-else-if="aftersaleOrders.length === 0" class="empty-container">
<text class="empty-icon">↩️</text>
<text class="empty-text">暂无退款售后</text>
</view>
<view v-else>
<view
v-for="order in aftersaleOrders"
:key="order.id"
class="order-card"
@click="viewOrderDetail(order.id)"
>
<view class="card-header">
<view class="card-header-left">
<text class="buyer-label">服务对象</text>
<text class="buyer-name">{{ maskBuyerName(order.user_id) }}</text>
<text class="order-time-small">{{ formatTime(order.created_at) }}</text>
</view>
<view class="card-header-right">
<text class="card-status-text cstatus-refund">{{ getAftersaleStatusText(order.order_status) }}</text>
</view>
</view>
<view class="card-products">
<view v-for="item in order.items" :key="item.id" class="pdd-product-item">
<image
:src="item.image_url || '/static/images/default-product.png'"
class="pdd-product-img"
mode="aspectFill"
/>
<view class="pdd-product-main">
<text class="pdd-product-name">{{ item.product_name }}</text>
<text class="pdd-product-received">实付:¥{{ (item.price * item.quantity).toFixed(2) }}</text>
</view>
<view class="pdd-product-right">
<text class="pdd-product-price">¥{{ item.price }}</text>
<text class="pdd-product-qty">x{{ item.quantity }}</text>
</view>
</view>
</view>
<view class="card-footer">
<view class="card-footer-left">
<text class="footer-amount-val">退款金额 ¥{{ order.total_amount }}</text>
</view>
<view class="card-footer-btns">
<view class="card-btn secondary" @click.stop="toastNotSupported">联系服务对象</view>
<view class="card-btn primary-warn" @click.stop="toastNotSupported">处理退款</view>
</view>
</view>
</view>
</view>
<view v-if="aftersaleLoadingMore" class="load-more">
<text class="load-more-text">加载中...</text>
</view>
<view v-if="!aftersaleHasMore && aftersaleOrders.length > 0" class="no-more">
<text class="no-more-text">没有更多了</text>
</view>
<view class="safe-bottom" :style="{ height: safeBottomHeight }"></view>
</scroll-view>
</view>
<!-- 安排服务弹窗 -->
<view v-if="showShipModal" class="modal-mask" @click="closeShipModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">安排服务上门</text>
<text class="modal-close" @click="closeShipModal">×</text>
</view>
<view class="modal-body">
<view class="form-item">
<text class="form-label">服务人员</text>
<picker
class="form-picker"
:range="serviceStaff"
range-key="name"
@change="onStaffChange"
>
<view class="picker-value">
{{ selectedStaff?.name || '请选择服务人员' }}
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">服务编号</text>
<input
class="form-input"
v-model="serviceCode"
placeholder="请输入服务工单号"
/>
</view>
</view>
<view class="modal-footer">
<view class="modal-btn cancel" @click="closeShipModal">取消</view>
<view class="modal-btn confirm" @click="confirmShip">确认派单</view>
</view>
</view>
</view>
<!-- 机构端自定义 TabBar -->
<merchant-tab-bar :current="2"></merchant-tab-bar>
</view>
</template>
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
import MerchantTabBar from '@/components/merchant-tabbar/MerchantTabBar.uvue'
import { USE_MOCK, MOCK_MERCHANT_ID, getMockOrdersByStatus, getMockAftersaleByStatus, getMockOrderTabCounts, getMockAftersaleTabCounts } from '@/pages/mall/merchant/mock/merchant-mock-data.uts'
type OrderItemType = {
id: string
order_id: string
product_id: string
sku_id: string
product_name: string
sku_name: string
price: number
quantity: number
image_url: string
sku_snapshot: string
}
type OrderType = {
id: string
order_no: string
user_id: string
merchant_id: string
order_status: number
total_amount: number
product_amount: number
shipping_fee: number
paid_amount: number
shipping_address: string
remark: string
created_at: string
updated_at: string
items: OrderItemType[]
}
type TabType = {
name: string
status: number
count: number
}
type LogisticsType = {
name: string | null
code: string | null
}
export default {
components: { MerchantTabBar },
data() {
return {
// 一级 tab服务订单 / 取消售后
mainTab: 'order' as string,
// 服务订单状态 tabs
orderTabs: [
{ name: '待接单', status: 2, count: 0 },
{ name: '服务中', status: 3, count: 0 },
{ name: '已完成', status: 4, count: 0 },
{ name: '全部', status: -2, count: 0 }
] as TabType[],
currentTab: 2 as number,
// 取消售后 tabs
aftersaleTabs: [
{ name: '全部售后', status: -3, count: 0 },
{ name: '退款中', status: 0, count: 0 },
{ name: '退款完成', status: 6, count: 0 }
] as TabType[],
currentAftersaleTab: -3 as number,
// 服务订单列表
searchKeyword: '',
orders: [] as OrderType[],
loading: false,
loadingMore: false,
refreshing: false,
page: 1,
limit: 20,
hasMore: true,
// 售后列表
aftersaleKeyword: '',
aftersaleOrders: [] as OrderType[],
aftersaleLoading: false,
aftersaleLoadingMore: false,
aftersaleRefreshing: false,
aftersalePage: 1,
aftersaleHasMore: true,
merchantId: '',
showShipModal: false,
currentOrder: null as OrderType | null,
serviceStaff: [
{ name: '张医师', code: 'ZS001' },
{ name: '李护士', code: 'LH001' },
{ name: '王康复师', code: 'WK001' },
{ name: '陈营养师', code: 'CY001' },
{ name: '刘家政员', code: 'LJ001' }
] as LogisticsType[],
selectedStaff: null as LogisticsType | null,
serviceCode: '',
// 动态计算的底部安全高度tabbar高度 + safeAreaInsets.bottom
safeBottomHeight: '160rpx' as string
}
},
onLoad(options: any) {
const type = options.type as string
if (type) {
const statusMap: Record<string, number> = {
'pending': 2,
'inprogress': 3,
'completed': 4,
'refund': -3,
'all': -2
}
const mapped = statusMap[type]
if (mapped === -3) {
this.mainTab = 'aftersale'
this.currentAftersaleTab = -3
} else if (mapped != null) {
this.currentTab = mapped
}
}
this.initMerchantId()
this.initSafeArea()
},
onShow() {
if (this.merchantId) {
this.refreshCurrentTab()
} else {
setTimeout(() => {
this.refreshCurrentTab()
}, 500)
}
},
methods: {
// 计算底部安全区
initSafeArea() {
// #ifdef MP-WEIXIN
try {
const info = wx.getWindowInfo()
const safeObj = info.safeArea
if (safeObj != null) {
const bottomInset = info.screenHeight - safeObj.bottom
this.safeBottomHeight = (60 + bottomInset) + 'px'
}
} catch(_e : any) {
this.safeBottomHeight = '160rpx'
}
// #endif
},
refreshCurrentTab() {
if (this.mainTab === 'order') {
this.page = 1
this.loadOrders()
this.loadOrderCounts()
} else {
this.aftersalePage = 1
this.loadAftersaleOrders()
}
},
switchMainTab(tab: string) {
this.mainTab = tab
if (tab === 'order') {
this.page = 1
this.loadOrders()
this.loadOrderCounts()
} else {
this.aftersalePage = 1
this.loadAftersaleOrders()
}
},
switchAftersaleTab(status: number) {
this.currentAftersaleTab = status
this.aftersalePage = 1
this.aftersaleHasMore = true
this.loadAftersaleOrders()
},
async initMerchantId() {
if (USE_MOCK) {
this.merchantId = MOCK_MERCHANT_ID
return
}
try {
const session = supa.getSession()
if (session != null && session.user != null) {
this.merchantId = session.user.getString('id') || ''
}
if (!this.merchantId) {
this.merchantId = uni.getStorageSync('user_id') || ''
}
} catch (e) {
console.error('获取商户ID失败:', e)
}
},
async loadOrders() {
if (this.loading) return
this.loading = true
if (USE_MOCK) {
const filtered = getMockOrdersByStatus(this.currentTab)
this.orders = filtered as OrderType[]
this.hasMore = false
this.loading = false
this.refreshing = false
return
}
try {
let query = supa
.from('ml_orders')
.select(`
*,
order_items (
id,
order_id,
product_id,
sku_id,
product_name,
sku_name,
price,
quantity,
image_url,
sku_snapshot
)
`)
.eq('merchant_id', this.merchantId)
.order('created_at', { ascending: false })
.page(this.page)
.limit(this.limit)
if (this.currentTab !== -2) {
if (this.currentTab === 6) {
// 退款状态同时查询 0 和 6
query = query.in('order_status', [0, 6])
} else {
query = query.eq('order_status', this.currentTab)
}
}
if (this.searchKeyword) {
query = query.like('order_no', `%${this.searchKeyword}%`)
}
const response = await query.execute()
if (response.error != null || (response.status ?? 200) >= 400) {
console.error('获取订单失败:', response.error)
uni.showToast({ title: '加载失败', icon: 'none' })
return
}
const rawData = response.data as any[]
if (rawData == null || rawData.length === 0) {
this.orders = []
this.hasMore = false
return
}
const ordersData: OrderType[] = []
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i]
const str = JSON.stringify(item)
const orderObj = JSON.parse(str) as UTSJSONObject
const order: OrderType = {
id: orderObj.getString('id') || '',
order_no: orderObj.getString('order_no') || '',
user_id: orderObj.getString('user_id') || '',
merchant_id: orderObj.getString('merchant_id') || '',
order_status: orderObj.getNumber('order_status') ?? (orderObj.get('order_status') == null ? 1 : (orderObj.get('order_status') as number)),
total_amount: orderObj.getNumber('total_amount') || 0,
product_amount: orderObj.getNumber('product_amount') || 0,
shipping_fee: orderObj.getNumber('shipping_fee') || 0,
paid_amount: orderObj.getNumber('paid_amount') || 0,
shipping_address: orderObj.get('shipping_address') != null ? (typeof orderObj.get('shipping_address') === 'string' ? orderObj.getString('shipping_address')! : JSON.stringify(orderObj.get('shipping_address'))) : '',
remark: orderObj.getString('remark') || '',
created_at: orderObj.getString('created_at') || '',
updated_at: orderObj.getString('updated_at') || '',
items: []
}
const itemsObj = orderObj.get('order_items')
if (itemsObj != null && Array.isArray(itemsObj)) {
const itemsArray = itemsObj as any[]
for (let j = 0; j < itemsArray.length; j++) {
const rawItem = itemsArray[j]
const itemStr = JSON.stringify(rawItem)
const orderItem = JSON.parse(itemStr) as UTSJSONObject
order.items.push({
id: orderItem.getString('id') || '',
order_id: orderItem.getString('order_id') || '',
product_id: orderItem.getString('product_id') || '',
sku_id: orderItem.getString('sku_id') || '',
product_name: orderItem.getString('product_name') || '',
sku_name: orderItem.getString('sku_name') || '',
price: orderItem.getNumber('price') || 0,
quantity: orderItem.getNumber('quantity') || 0,
image_url: orderItem.getString('image_url') || '',
sku_snapshot: ''
} as OrderItemType)
}
}
ordersData.push(order)
}
if (this.page === 1) {
this.orders = ordersData
} else {
this.orders = [...this.orders, ...ordersData]
}
this.hasMore = rawData.length >= this.limit
} catch (e) {
console.error('获取订单异常:', e)
} finally {
this.loading = false
this.refreshing = false
}
},
async loadOrderCounts() {
if (USE_MOCK) {
const c = getMockOrderTabCounts()
this.orderTabs[0].count = c.pending
this.orderTabs[1].count = c.inprogress
this.orderTabs[2].count = c.completed
this.orderTabs[3].count = c.total
const ac = getMockAftersaleTabCounts()
this.aftersaleTabs[0].count = ac.total
this.aftersaleTabs[1].count = ac.refunding
this.aftersaleTabs[2].count = ac.refunded
return
}
try {
const response = await supa
.from('ml_orders')
.select('order_status', { count: 'exact' })
.eq('merchant_id', this.merchantId)
.execute()
if (response.error != null || response.total == null) return
const counts = {
1: 0, 2: 0, 3: 0, 4: 0, 0: 0
}
let total = 0
const rawData = response.data as any[]
if (rawData != null) {
for (let i = 0; i < rawData.length; i++) {
const row = rawData[i]
const istr = JSON.stringify(row)
const item = JSON.parse(istr) as UTSJSONObject
const status_val = item.get('order_status')
let status = 1
if (status_val != null) {
status = (typeof status_val === 'number') ? (status_val as number) : parseInt(status_val.toString())
}
if (status === 1) counts[1]++
else if (status === 2) counts[2]++
else if (status === 3) counts[3]++
else if (status === 4) counts[4]++
else if (status === 0 || status === 6) counts[0]++
total++
}
}
this.orderTabs[0].count = counts[2] || 0
this.orderTabs[1].count = counts[3] || 0
this.orderTabs[2].count = counts[4] || 0
this.orderTabs[3].count = total
} catch (e) {
console.error('获取订单数量异常:', e)
}
},
switchTab(status: number) {
this.currentTab = status
this.page = 1
this.hasMore = true
this.loadOrders()
},
handleAftersaleSearch() {
this.aftersalePage = 1
this.aftersaleHasMore = true
this.loadAftersaleOrders()
},
onAftersaleRefresh() {
this.aftersaleRefreshing = true
this.aftersalePage = 1
this.loadAftersaleOrders()
},
loadMoreAftersale() {
if (!this.aftersaleLoadingMore && this.aftersaleHasMore) {
this.aftersaleLoadingMore = true
this.aftersalePage++
this.loadAftersaleOrders().then(() => {
this.aftersaleLoadingMore = false
})
}
},
async loadAftersaleOrders() {
if (this.aftersaleLoading) return
this.aftersaleLoading = true
if (USE_MOCK) {
const filtered = getMockAftersaleByStatus(this.currentAftersaleTab)
this.aftersaleOrders = filtered as OrderType[]
this.aftersaleHasMore = false
this.aftersaleLoading = false
this.aftersaleRefreshing = false
return
}
try {
let query = supa
.from('ml_orders')
.select(`*, order_items (id, order_id, product_id, sku_id, product_name, sku_name, price, quantity, image_url, sku_snapshot)`)
.eq('merchant_id', this.merchantId)
.order('created_at', { ascending: false })
.page(this.aftersalePage)
.limit(this.limit)
if (this.currentAftersaleTab === 0) {
query = query.eq('order_status', 0)
} else if (this.currentAftersaleTab === 6) {
query = query.eq('order_status', 6)
} else {
query = query.in('order_status', [0, 6])
}
if (this.aftersaleKeyword) {
query = query.like('order_no', `%${this.aftersaleKeyword}%`)
}
const response = await query.execute()
if (response.error != null || (response.status ?? 200) >= 400) return
const rawData = response.data as any[]
if (rawData == null || rawData.length === 0) {
if (this.aftersalePage === 1) this.aftersaleOrders = []
this.aftersaleHasMore = false
return
}
const ordersData: OrderType[] = []
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i]
const orderObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
const order: OrderType = {
id: orderObj.getString('id') || '',
order_no: orderObj.getString('order_no') || '',
user_id: orderObj.getString('user_id') || '',
merchant_id: orderObj.getString('merchant_id') || '',
order_status: orderObj.getNumber('order_status') ?? 0,
total_amount: orderObj.getNumber('total_amount') || 0,
product_amount: orderObj.getNumber('product_amount') || 0,
shipping_fee: orderObj.getNumber('shipping_fee') || 0,
paid_amount: orderObj.getNumber('paid_amount') || 0,
shipping_address: orderObj.getString('shipping_address') || '',
remark: orderObj.getString('remark') || '',
created_at: orderObj.getString('created_at') || '',
updated_at: orderObj.getString('updated_at') || '',
items: []
}
const itemsObj = orderObj.get('order_items')
if (itemsObj != null && Array.isArray(itemsObj)) {
const itemsArray = itemsObj as any[]
for (let j = 0; j < itemsArray.length; j++) {
const orderItem = JSON.parse(JSON.stringify(itemsArray[j])) as UTSJSONObject
order.items.push({
id: orderItem.getString('id') || '',
order_id: orderItem.getString('order_id') || '',
product_id: orderItem.getString('product_id') || '',
sku_id: orderItem.getString('sku_id') || '',
product_name: orderItem.getString('product_name') || '',
sku_name: orderItem.getString('sku_name') || '',
price: orderItem.getNumber('price') || 0,
quantity: orderItem.getNumber('quantity') || 0,
image_url: orderItem.getString('image_url') || '',
sku_snapshot: ''
} as OrderItemType)
}
}
ordersData.push(order)
}
if (this.aftersalePage === 1) {
this.aftersaleOrders = ordersData
} else {
this.aftersaleOrders = [...this.aftersaleOrders, ...ordersData]
}
this.aftersaleHasMore = rawData.length >= this.limit
} catch (e) {
console.error('获取售后订单异常:', e)
} finally {
this.aftersaleLoading = false
this.aftersaleRefreshing = false
}
},
handleSearch() {
this.page = 1
this.hasMore = true
this.loadOrders()
},
onRefresh() {
this.refreshing = true
this.page = 1
this.loadOrders()
this.loadOrderCounts()
},
loadMore() {
if (!this.loadingMore && this.hasMore) {
this.loadingMore = true
this.page++
this.loadOrders().then(() => {
this.loadingMore = false
})
}
},
viewOrderDetail(orderId: string) {
uni.navigateTo({
url: `/pages/mall/merchant/order-detail?id=${orderId}`
})
},
shipOrder(order: OrderType) {
this.currentOrder = order
this.showShipModal = true
},
closeShipModal() {
this.showShipModal = false
this.currentOrder = null
this.selectedStaff = null
this.serviceCode = ''
},
onStaffChange(e: any) {
const index = e.detail.value as number
this.selectedStaff = this.serviceStaff[index]
},
async confirmShip() {
if (this.selectedStaff == null || !this.selectedStaff?.name) {
uni.showToast({ title: '请选择服务人员', icon: 'none' })
return
}
if (!this.serviceCode) {
uni.showToast({ title: '请输入服务工单号', icon: 'none' })
return
}
try {
const payloadStr = JSON.stringify({
order_status: 3,
shipping_status: 2,
carrier_name: this.selectedStaff?.name ?? '未知',
tracking_no: this.serviceCode,
shipped_at: new Date().toISOString(),
updated_at: new Date().toISOString()
});
const payload = JSON.parse(payloadStr) as UTSJSONObject;
console.log('--- PAYLOAD TO SEND ---', JSON.stringify(payload));
const response = await supa.from('ml_orders').update(payload
)
.eq('id', this.currentOrder!.id)
.execute()
if (response.error != null || (response.status ?? 200) >= 400) {
let msg = '';
if (response.error != null) msg = response.error!.message;
else if (response.data != null) {
const rData = response.data as UTSJSONObject;
msg = rData.getString('message') ?? rData.getString('code') ?? JSON.stringify(rData);
}
if (!msg) msg = '请检查网络或登录状态'; uni.showToast({ title: '派单被拦截: ' + msg, icon: 'none', duration: 4500 }); console.error('SUPABASE API ERR:', response)
return
}
uni.showToast({ title: '派单成功', icon: 'success' })
this.closeShipModal()
this.loadOrders()
this.loadOrderCounts()
} catch (e) { uni.showToast({ title: '派单发生异常', icon: 'none' }); console.error(e) }
},
viewLogistics(order: OrderType) {
uni.navigateTo({
url: `/pages/mall/consumer/logistics?orderId=${order.id}`
})
},
async deleteOrder(order: OrderType) {
uni.showModal({
title: '确认删除',
content: '确定要删除该订单吗?',
success: async (res) => {
if (res.confirm) {
try {
const response = await supa
.from('ml_orders')
.delete()
.eq('id', order.id)
.execute()
if (response.error != null || (response.status ?? 200) >= 400) {
uni.showToast({ title: '删除失败', icon: 'none' })
return
}
uni.showToast({ title: '删除成功', icon: 'success' })
this.loadOrders()
this.loadOrderCounts()
} catch (e) {
uni.showToast({ title: '删除失败', icon: 'none' })
}
}
}
})
},
getStatusText(status: number): string {
if (status === 2) return '待接单'
if (status === 3) return '服务中'
if (status === 4) return '已完成'
if (status === 0 || status === 6) return '退款售后'
if (status === 5 || status === -1) return '已取消'
return '未知'
},
getAftersaleStatusText(status: number): string {
if (status === 0) return '退款中'
if (status === 6) return '退款完成'
return '售后处理中'
},
maskBuyerName(userId: string): string {
if (!userId) return '服务对象'
return userId.substring(0, 4) + '**'
},
toastNotSupported() {
uni.showToast({ title: '暂未接入', icon: 'none' })
},
getTotalQuantity(items: OrderItemType[]): number {
let total = 0
for (let i = 0; i < items.length; i++) {
total += items[i].quantity
}
return total
},
formatTime(timeStr: string): string {
if (!timeStr) return ''
const date = new Date(timeStr)
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}`
}
}
}
</script>
<style>
/* ===== 页面容器 ===== */
.orders-page {
background-color: #f4f5f7;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* ===== 微信小程序顶部导航栏 ===== */
.mp-tab-navbar {
height: calc(88rpx + var(--status-bar-height));
padding-top: var(--status-bar-height);
background-color: #ffffff;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #f0f0f0;
}
.mp-tab-title {
font-size: 34rpx;
font-weight: bold;
color: #333333;
}
/* ===== 一级切换:服务订单 / 取消售后 ===== */
.lvl1-tabs {
display: flex;
flex-direction: row;
background-color: #ffffff;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #eeeeee;
flex-shrink: 0;
}
.lvl1-tab-item {
flex: 1;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.lvl1-tab-text {
font-size: 30rpx;
color: #666;
}
.lvl1-tab-item.active .lvl1-tab-text {
color: #09C39D;
font-weight: bold;
}
.lvl1-tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 48rpx;
height: 4rpx;
background-color: #09C39D;
border-radius: 2rpx;
}
/* ===== Tab 页内容容器 ===== */
.tab-page-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ===== 二级状态 Tabs ===== */
.tabs-container {
background-color: #fff;
flex-shrink: 0;
}
.tabs-scroll {
display: flex;
flex-direction: row;
overflow-x: auto;
white-space: nowrap;
padding: 0 20rpx;
}
.tab-item {
position: relative;
padding: 22rpx 28rpx;
text-align: center;
flex-shrink: 0;
}
.tab-text {
font-size: 27rpx;
color: #666;
}
.tab-item.active .tab-text {
color: #09C39D;
font-weight: bold;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 36rpx;
height: 4rpx;
background-color: #09C39D;
border-radius: 2rpx;
}
.tab-badge {
position: absolute;
top: 8rpx;
right: 8rpx;
min-width: 30rpx;
height: 30rpx;
padding: 0 6rpx;
background-color: #E1251B;
color: #fff;
font-size: 18rpx;
border-radius: 15rpx;
text-align: center;
line-height: 30rpx;
}
/* ===== 待接单筛选胶囊 ===== */
.filter-capsules {
display: flex;
flex-direction: row;
padding: 16rpx 24rpx;
background-color: #fff;
flex-shrink: 0;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #f0f0f0;
}
.filter-capsule {
height: 48rpx;
padding: 0 24rpx;
border-radius: 24rpx;
border-width: 1rpx;
border-style: solid;
border-color: #ddd;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
}
.filter-capsule.active {
border-color: #09C39D;
background-color: #E3F7ED;
}
.filter-capsule-text {
font-size: 24rpx;
color: #666;
}
.filter-capsule.active .filter-capsule-text {
color: #09C39D;
}
/* ===== 搜索栏 ===== */
.search-bar {
display: flex;
flex-direction: row;
align-items: center;
padding: 16rpx 24rpx;
background-color: #f7f7f7;
flex-shrink: 0;
}
.search-input {
flex: 1;
height: 60rpx;
background-color: #fff;
border-radius: 30rpx;
padding: 0 24rpx;
font-size: 26rpx;
border-width: 1rpx;
border-style: solid;
border-color: #e5e5e5;
}
.search-btn {
margin-left: 16rpx;
height: 60rpx;
padding: 0 28rpx;
background-color: #09C39D;
color: #fff;
font-size: 26rpx;
border-radius: 30rpx;
display: flex;
align-items: center;
justify-content: center;
}
/* ===== 待接单工具条 ===== */
.toolbar-strip {
display: flex;
flex-direction: row;
align-items: center;
padding: 0 24rpx;
height: 64rpx;
background-color: #fff;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #f0f0f0;
flex-shrink: 0;
}
.toolbar-item {
font-size: 24rpx;
color: #666;
margin-right: 32rpx;
}
.toolbar-item:first-child {
color: #999;
font-size: 22rpx;
flex: 1;
margin-right: 0;
}
.toolbar-divider {
width: 1rpx;
height: 28rpx;
background-color: #eee;
margin-right: 20rpx;
}
/* ===== 列表滚动区 ===== */
.orders-list {
flex: 1;
overflow: hidden;
padding: 0;
}
/* ===== 订单卡片PDD 风格) ===== */
.order-card {
background-color: #fff;
border-radius: 16rpx;
margin: 16rpx 20rpx 0;
overflow: hidden;
}
/* 卡片顶部 */
.card-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
padding: 20rpx 24rpx 16rpx;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #f5f5f5;
}
.card-header-left {
display: flex;
flex-direction: row;
align-items: center;
flex: 1;
}
.buyer-label {
font-size: 22rpx;
color: #fff;
background-color: #999;
padding: 2rpx 10rpx;
border-radius: 4rpx;
margin-right: 10rpx;
flex-shrink: 0;
}
.buyer-name {
font-size: 26rpx;
color: #333;
font-weight: 500;
flex-shrink: 0;
}
.order-time-small {
font-size: 21rpx;
color: #aaa;
margin-left: 14rpx;
}
.card-header-right {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-left: 16rpx;
}
.card-status-text {
font-size: 24rpx;
font-weight: bold;
}
.cstatus-2 { color: #09C39D; }
.cstatus-3 { color: #2E7D32; }
.cstatus-4 { color: #999; }
.cstatus-5, .cstatus--1 { color: #bbb; }
.cstatus-refund { color: #E1251B; }
/* 服务项目区 */
.card-products {
padding: 0 24rpx;
}
.pdd-product-item {
display: flex;
flex-direction: row;
align-items: flex-start;
padding: 18rpx 0;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #f8f8f8;
}
.pdd-product-item:last-child {
border-bottom: none;
}
.pdd-product-img {
width: 140rpx;
height: 140rpx;
border-radius: 8rpx;
margin-right: 18rpx;
background-color: #f5f5f5;
flex-shrink: 0;
}
.pdd-product-main {
flex: 1;
display: flex;
flex-direction: column;
padding-top: 4rpx;
}
.pdd-product-name {
font-size: 26rpx;
color: #333;
font-weight: 500;
line-height: 1.4;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.pdd-product-spec-wrap {
background-color: #f5f5f5;
border-radius: 4rpx;
padding: 4rpx 12rpx;
align-self: flex-start;
margin-bottom: 10rpx;
}
.pdd-product-spec {
font-size: 21rpx;
color: #888;
}
.pdd-product-received {
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
}
.pdd-product-right {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-left: 16rpx;
padding-top: 4rpx;
min-width: 100rpx;
}
.pdd-product-price {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.pdd-product-qty {
font-size: 22rpx;
color: #999;
margin-top: 6rpx;
}
/* 卡片底部 */
.card-footer {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 18rpx 24rpx;
background-color: #fafafa;
border-top-width: 1rpx;
border-top-style: solid;
border-top-color: #f0f0f0;
}
.card-footer-left {
display: flex;
flex-direction: column;
}
.footer-amount-label {
font-size: 21rpx;
color: #aaa;
}
.footer-amount-val {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
.card-footer-btns {
display: flex;
flex-direction: row;
align-items: center;
}
.card-btn {
min-width: 120rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
border-radius: 56rpx;
margin-left: 16rpx;
padding: 0 24rpx;
}
.card-btn.secondary {
border-width: 1rpx;
border-style: solid;
border-color: #ddd;
background-color: #fff;
color: #555;
}
.card-btn.primary {
background-color: #09C39D;
color: #fff;
border: none;
font-weight: bold;
}
.card-btn.primary-warn {
background-color: #FF7800;
color: #fff;
border: none;
}
.card-btn.danger-outline {
border-width: 1rpx;
border-style: solid;
border-color: #E1251B;
color: #E1251B;
background-color: #fff;
}
/* ===== 加载更多 / 无更多 ===== */
.load-more, .no-more {
padding: 30rpx 0;
text-align: center;
}
.load-more-text, .no-more-text {
font-size: 24rpx;
color: #bbb;
}
/* ===== 空状态 ===== */
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
}
.empty-icon {
font-size: 100rpx;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 28rpx;
color: #bbb;
}
/* ===== 底部安全区占位 ===== */
.safe-bottom {
background-color: transparent;
}
/* ===== 安排服务弹窗 ===== */
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 200;
}
.modal-content {
width: 100%;
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
padding-bottom: env(safe-area-inset-bottom);
position: relative;
z-index: 200;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #f5f5f5;
}
.modal-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.modal-close {
font-size: 44rpx;
color: #999;
line-height: 1;
}
.modal-body {
padding: 30rpx;
}
.form-item {
margin-bottom: 30rpx;
}
.form-label {
font-size: 28rpx;
color: #333;
display: block;
margin-bottom: 16rpx;
}
.form-picker, .form-input {
height: 72rpx;
border-width: 1rpx;
border-style: solid;
border-color: #e5e5e5;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
}
.picker-value {
height: 72rpx;
line-height: 72rpx;
color: #333;
}
.modal-footer {
display: flex;
border-top-width: 1rpx;
border-top-style: solid;
border-top-color: #f5f5f5;
}
.modal-btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
text-align: center;
font-size: 28rpx;
}
.modal-btn.cancel {
color: #666;
border-right-width: 1rpx;
border-right-style: solid;
border-right-color: #f5f5f5;
}
.modal-btn.confirm {
color: #09C39D;
font-weight: bold;
}
/* ===== 骨架屏 ===== */
@keyframes ske-pulse { 0% { opacity: 1; } 50% { opacity: 0.45; } 100% { opacity: 1; } }
.ske-orders-wrap { padding: 20rpx; }
.ske-order-card { background-color: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; }
.ske-order-hd { display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
.ske-order-product { display: flex; flex-direction: row; align-items: center; margin-bottom: 20rpx; }
.ske-product-img { width: 140rpx; height: 140rpx; border-radius: 8rpx; background-color: #e8e8e8; margin-right: 18rpx; flex-shrink: 0; animation: ske-pulse 1.4s ease-in-out infinite; }
.ske-product-info { flex: 1; }
.ske-order-ft { display: flex; flex-direction: row; justify-content: space-between; align-items: center; border-top-width: 1rpx; border-top-style: solid; border-top-color: #f5f5f5; padding-top: 16rpx; }
.ske-bar { border-radius: 8rpx; background-color: #e8e8e8; animation: ske-pulse 1.4s ease-in-out infinite; }
.ske-w22 { width: 22%; } .ske-w25 { width: 25%; } .ske-w30 { width: 30%; } .ske-w40 { width: 40%; } .ske-w50 { width: 50%; } .ske-w70 { width: 70%; }
.ske-h22 { height: 22rpx; } .ske-h26 { height: 26rpx; } .ske-h28 { height: 28rpx; } .ske-h30 { height: 30rpx; }
.ske-mb10 { margin-bottom: 10rpx; }
</style>