Files
points-mall/supplied-h5/src/view/joyful-selection/profile/orders/index.vue
T
2026-07-31 19:08:15 +08:00

797 lines
20 KiB
Vue
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>
<div>
<joyful-selection-orders-skeleton v-if="loading" />
<main v-else class="orders-page">
<van-nav-bar title="我的订单" left-arrow @click-left="$router.replace('/joyful-selection/profile')" />
<van-tabs
v-model="active"
class="order-tabs"
line-width="18"
line-height="2"
color="#ff5b20"
title-active-color="#ff5b20"
>
<van-tab v-for="tab in tabs" :key="tab.label" :title="tab.label" :badge="tabBadge(tab)" />
</van-tabs>
<van-pull-refresh v-model="refreshing" class="order-list" @refresh="onRefresh">
<van-empty v-if="visibleOrders.length === 0" description="暂无订单" />
<template v-else>
<article v-for="order in visibleOrders" :key="order.viewId" class="order-card">
<header class="order-card__header">
<div class="order-card__no">
<van-icon name="orders-o" />
<span>订单号:{{ order.orderNumberId || order.orderId || '-' }}</span>
</div>
<b :class="order.statusClass">{{ order.status }}</b>
</header>
<div class="order-card__body" @click="goOrderDetail(order)">
<div class="order-goods-list">
<div
v-for="item in previewProducts(order.products)"
:key="item.id"
class="order-goods-row"
>
<van-image
class="order-thumb"
:src="item.image"
fit="cover"
/>
<div class="order-goods__info">
<h3>{{ item.name || '-' }}</h3>
<p>{{ skuText(item) }}</p>
<b>¥{{ item.price || order.amount }}</b>
</div>
<span class="order-goods-count">x{{ productCount(item) }}</span>
</div>
</div>
</div>
<div v-if="showLogistics(order)" class="logistics-row">
<van-icon name="logistics" />
<span>物流状态:包裹已到达目的地城市</span>
<van-icon name="arrow" />
</div>
<div class="order-card__summary">
<span>共{{ orderProductCount(order) }}件商品</span>
<span>合计:<b>¥{{ order.amount }}</b></span>
</div>
<p class="order-create-time">下单时间:{{ order.createTime || '-' }}</p>
<footer v-if="orderActions(order).length">
<div class="order-actions">
<van-button
v-for="action in orderActions(order)"
:key="action.key"
size="small"
round
:class="action.buttonClass"
:loading="isOperating(action.key, action.orderId)"
@click.stop="submitOrderAction(order, action)"
>
{{ action.text }}
</van-button>
</div>
</footer>
</article>
</template>
</van-pull-refresh>
</main>
</div>
</template>
<script>
import { mapActions, mapState } from 'pinia'
import { Dialog, Toast } from 'vant'
import { postFunction } from '@/api/supplied-service'
import { useUserStore } from '@/store/user'
import { useUiStore } from '@/store/ui'
import { savePaymentInfo } from '../../order/payment/payment-data'
import JoyfulSelectionOrdersSkeleton from '@/skeleton-view/joyful-selection/profile/orders/index.vue'
const ORDER_STATE = {
PENDING_PAY: 0,
FINISH_PAY: 1,
IN_PROGRESS: 2,
CANCELLED: 3,
COMPLETED: 5,
EXCHANGE: 6,
REFUNDED: 7
}
const ORDER_EVENT_TYPE = {
RETURN_ORDER: 4,
EXCHANGE_ORDER: 5,
CONFIRM_RECEIVE: 6,
INVOICE_INFO: 11
}
const ORDER_STATE_VIEW = {
[ORDER_STATE.PENDING_PAY]: { text: '待付款', statusClass: 'pending' },
[ORDER_STATE.FINISH_PAY]: { text: '待发货', statusClass: 'pending' },
[ORDER_STATE.IN_PROGRESS]: { text: '待收货', statusClass: 'pending' },
[ORDER_STATE.CANCELLED]: { text: '已取消', statusClass: 'muted' },
[ORDER_STATE.COMPLETED]: { text: '已完成', statusClass: 'done' },
[ORDER_STATE.EXCHANGE]: { text: '换货', statusClass: 'muted' },
[ORDER_STATE.REFUNDED]: { text: '退货', statusClass: 'muted' }
}
const SKU_COLOR_MAP = {
cream: '奶油白',
blue: '雾霾蓝',
orange: '暖橙色'
}
const SKU_SIZE_MAP = {
standard: '标准款',
plus: '升级款'
}
export default {
name: 'JoyfulSelectionOrders',
components: {
JoyfulSelectionOrdersSkeleton
},
data: () => ({
tabs: [
{ label: '全部', type: 'all' },
{ label: '待付款', type: 'pending-main' },
{ label: '待发货', type: 'sub-state', state: ORDER_STATE.FINISH_PAY },
{ label: '待收货', type: 'sub-state', state: ORDER_STATE.IN_PROGRESS },
{ label: '已取消', type: 'cancelled' },
{ label: '已完成', type: 'sub-state', state: ORDER_STATE.COMPLETED }
],
orderItems: [],
loading: false,
refreshing: false,
operationKey: ''
}),
computed: {
...mapState(useUserStore, ['supplierId', 'userId']),
...mapState(useUiStore, ['ordersTab']),
active: {
get() {
return this.ordersTab
},
set(value) {
this.setOrdersTab(value)
}
},
activeTab() {
return this.tabs[this.active] || this.tabs[0]
},
visibleOrders() {
const tab = this.activeTab
if (tab.type === 'all') {
return this.orderItems
}
if (tab.type === 'pending-main') {
return this.orderItems.filter(order => order.displayType === 'main' && Number(order.orderState) === ORDER_STATE.PENDING_PAY)
}
if (tab.type === 'sub-state') {
return this.orderItems.filter(order => order.displayType === 'sub' && Number(order.orderState) === tab.state)
}
if (tab.type === 'cancelled') {
return this.orderItems.filter(order => Number(order.orderState) === ORDER_STATE.CANCELLED)
}
if (tab.type === 'after-sale') {
return this.orderItems.filter(order => {
return order.displayType === 'sub' && [ORDER_STATE.EXCHANGE, ORDER_STATE.REFUNDED].includes(Number(order.orderState))
})
}
return []
}
},
created() {
this.applyRouteTab()
this.loadOrders()
},
methods: {
...mapActions(useUserStore, ['fetchBridgeToken']),
...mapActions(useUiStore, ['loadUi', 'setOrdersTab']),
applyRouteTab() {
this.loadUi()
const tabIndexMap = {
'pending-pay': 1,
'pending-ship': 2,
'pending-receive': 3,
completed: 5
}
const nextActive = tabIndexMap[this.$route.query.tab]
if (nextActive !== undefined) {
this.setOrdersTab(nextActive)
}
},
previewProducts(products = []) {
return products.slice(0, 3)
},
firstProduct(order) {
return order.products?.[0] || {}
},
productCount(product = {}) {
return Number(product.number || product.count || 1)
},
orderProductCount(order) {
const products = Array.isArray(order.products) ? order.products : []
const count = products.reduce((total, product) => total + this.productCount(product), 0)
return count || Number(order.count || 0)
},
skuText(product = {}) {
const sku = String(product.sku || product.skuText || '').trim()
const parts = sku.split('-').slice(-2).filter(Boolean)
const color = SKU_COLOR_MAP[parts[0]] || parts[0]
const size = SKU_SIZE_MAP[parts[1]] || parts[1]
return color || size ? [color, size].filter(Boolean).join(' | ') : '-'
},
showLogistics(order) {
return ![ORDER_STATE.PENDING_PAY, ORDER_STATE.CANCELLED, ORDER_STATE.COMPLETED].includes(Number(order.orderState))
},
actionKey(type, orderId) {
return `${type}-${orderId}`
},
isOperating(type, orderId) {
return this.operationKey === this.actionKey(type, orderId)
},
tabBadge(tab) {
if (tab.type !== 'pending-main') {
return undefined
}
const count = this.orderItems.filter(order => {
return order.displayType === 'main' && Number(order.orderState) === ORDER_STATE.PENDING_PAY
}).length
return count > 0 ? count : undefined
},
subActions(subOrder) {
const state = Number(subOrder.orderState)
const actions = []
if ([ORDER_STATE.FINISH_PAY, ORDER_STATE.IN_PROGRESS, ORDER_STATE.COMPLETED].includes(state)) {
actions.push({ orderType: ORDER_EVENT_TYPE.RETURN_ORDER, text: '申请售后', buttonClass: 'dark-action' })
}
if (state === ORDER_STATE.IN_PROGRESS) {
actions.push({ orderType: ORDER_EVENT_TYPE.CONFIRM_RECEIVE, text: '确认收货', buttonClass: 'primary-action' })
}
return actions
},
goOrderDetail(order) {
const subOrder = order.displayType === 'main' ? order.subOrders?.[0] : order
if (!subOrder?.orderId) {
return
}
this.$router.push({
path: '/joyful-selection/order-detail',
query: {
mainOrderId: order.mainOrderId,
orderId: subOrder.orderId
}
})
},
async loadOrders(showLoading = true) {
if (showLoading) {
this.loading = true
}
try {
await this.fetchBridgeToken()
if (!this.supplierId || !this.userId) {
Toast('用户信息不完整,请重新进入商城')
this.orderItems = []
return
}
const data = await postFunction('order-list', {
supplierId: this.supplierId,
userId: this.userId
})
this.orderItems = data?.orderItems || []
} catch (error) {
Toast(error.message || '查询订单失败,请稍后再试')
this.orderItems = []
} finally {
if (showLoading) {
this.loading = false
}
}
},
async onRefresh() {
try {
await this.loadOrders(false)
} finally {
this.refreshing = false
}
},
orderActions(order) {
if (order.displayType === 'main') {
return Number(order.orderState) === ORDER_STATE.PENDING_PAY
? [
{ key: 'cancel-main', orderId: order.mainOrderId, text: '取消订单', buttonClass: 'dark-action' },
{ key: 'pay-main', orderId: order.mainOrderId, text: '去支付', buttonClass: 'primary-action' }
]
: []
}
return this.subActions(order).map(action => ({
...action,
key: action.key || action.orderType,
orderId: order.orderId
}))
},
submitOrderAction(order, action) {
if (action.key === 'cancel-main') {
this.cancelMainOrder(order)
return
}
if (action.key === 'ship-order') {
this.shipOrder(order)
return
}
if (action.key === 'pay-main') {
this.goPay(order)
return
}
if ([ORDER_EVENT_TYPE.RETURN_ORDER, ORDER_EVENT_TYPE.EXCHANGE_ORDER].includes(action.orderType)) {
this.goAfterSaleSelect(order)
return
}
this.submitSubOrderEvent(
{ mainOrderId: order.mainOrderId },
order,
action
)
},
goPay(order) {
const firstProduct = this.firstProduct(order)
savePaymentInfo({
orderNo: order.mainOrderId || order.orderId,
orderNumberId: order.orderNumberId || '',
amount: Math.round(Number(order.amount || 0) * 100),
receiverName: order.receiverName || '',
receiverTel: order.receiverMobile || '',
goods: order.products || (firstProduct.id ? [firstProduct] : [])
})
this.$router.push('/joyful-selection/payment')
},
goAfterSaleSelect(order) {
this.$router.push({
path: '/joyful-selection/after-sale/select',
query: {
mainOrderId: order.mainOrderId,
orderId: order.orderId
}
})
},
async cancelMainOrder(order) {
try {
await Dialog.confirm({
title: '取消订单',
message: '确定取消该未支付订单吗?'
})
} catch (error) {
return
}
this.operationKey = this.actionKey('cancel-main', order.mainOrderId)
try {
await this.fetchBridgeToken()
if (!this.supplierId || !this.userId || !order.mainOrderId) {
Toast('订单信息不完整,请稍后再试')
return
}
await postFunction('order-cancel-main', {
supplierId: this.supplierId,
userId: this.userId,
orderId: order.mainOrderId,
mainOrderId: order.mainOrderId
})
Toast('订单已取消')
await this.loadOrders()
} catch (error) {
Toast(error.message || '取消订单失败,请稍后再试')
} finally {
this.operationKey = ''
}
},
async shipOrder(order) {
try {
await Dialog.confirm({
title: '发货',
message: '确定将该订单更新为已发货吗?'
})
} catch (error) {
return
}
this.operationKey = this.actionKey('ship-order', order.orderId)
try {
await this.fetchBridgeToken()
if (!this.supplierId || !this.userId || !order.orderId) {
Toast('订单信息不完整,请稍后再试')
return
}
await postFunction('order-ship', {
supplierId: this.supplierId,
userId: this.userId,
orderId: order.orderId,
mainOrderId: order.mainOrderId,
deliveryStatus: 1
})
Toast('发货成功')
await this.loadOrders()
} catch (error) {
Toast(error.message || '发货失败,请稍后再试')
} finally {
this.operationKey = ''
}
},
async submitAfterSale(order, action) {
try {
await Dialog.confirm({
title: action.text,
message: `确定申请${action.text}吗?提交后将进入商家待审核。`
})
} catch (error) {
return
}
this.operationKey = this.actionKey(action.orderType, order.orderId)
try {
await this.fetchBridgeToken()
if (!this.supplierId || !this.userId || !order.mainOrderId || !order.orderId) {
Toast('订单信息不完整,请稍后再试')
return
}
await postFunction('after-sale-apply', {
supplierId: this.supplierId,
userId: this.userId,
serviceType: action.orderType,
orderId: order.orderId,
subOrderId: order.orderId,
mainOrderId: order.mainOrderId,
amount: Number(order.amount || 0),
reason: action.text
})
Toast(`${action.text}申请已提交,等待商家审核`)
await this.loadOrders()
} catch (error) {
Toast(error.message || `${action.text}申请失败,请稍后再试`)
} finally {
this.operationKey = ''
}
},
async submitSubOrderEvent(order, subOrder, action) {
try {
await Dialog.confirm({
title: action.text,
message: `确定对该子单进行“${action.text}”操作吗?`
})
} catch (error) {
return
}
this.operationKey = this.actionKey(action.orderType, subOrder.orderId)
try {
await this.fetchBridgeToken()
if (!this.supplierId || !this.userId || !order.mainOrderId || !subOrder.orderId) {
Toast('订单信息不完整,请稍后再试')
return
}
const data = await postFunction('order-push-event', {
supplierId: this.supplierId,
userId: this.userId,
orderType: action.orderType,
orderId: subOrder.orderId,
mainOrderId: order.mainOrderId,
subOrderId: subOrder.orderId,
amount: Number(subOrder.amount || 0),
reason: action.text,
invoiceBizId: action.orderType === ORDER_EVENT_TYPE.INVOICE_INFO ? `INV_${subOrder.orderId}` : undefined,
invoiceIds: action.orderType === ORDER_EVENT_TYPE.INVOICE_INFO ? `INV_${subOrder.orderId}` : undefined,
orderIds: action.orderType === ORDER_EVENT_TYPE.INVOICE_INFO ? subOrder.orderId : undefined,
time: Date.now()
})
Toast(data?.pushSuccess === false ? '本地已保存,远程推送失败' : `${action.text}成功`)
await this.loadOrders()
} catch (error) {
Toast(error.message || `${action.text}失败,请稍后再试`)
} finally {
this.operationKey = ''
}
}
}
}
</script>
<style scoped lang="less">
.orders-page {
min-height: 100vh;
padding-top: 90px;
background: #f7f7f7;
}
.orders-page ::v-deep .van-nav-bar {
position: fixed;
z-index: 20;
top: 0;
right: 0;
left: 0;
box-shadow: 0 1px 0 rgba(0, 0, 0, .04);
}
.order-tabs {
position: fixed;
z-index: 19;
top: 46px;
right: 0;
left: 0;
}
.orders-page ::v-deep .van-tabs__wrap {
height: 44px;
box-shadow: 0 1px 0 rgba(0, 0, 0, .04);
}
.orders-page ::v-deep .van-tab {
color: #333;
font-size: 13px;
}
.order-list {
min-height: calc(100vh - 90px);
box-sizing: border-box;
padding: 12px 10px 18px;
}
.order-card {
overflow: hidden;
margin-bottom: 14px;
border-radius: 10px;
background: #fff;
}
.order-card__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 14px 8px;
}
.order-card__no {
display: flex;
min-width: 0;
align-items: center;
}
.order-card__no .van-icon {
flex: none;
margin-right: 8px;
color: #ff5b20;
font-size: 18px;
}
.order-card__no span {
min-width: 0;
overflow: hidden;
color: #222;
font-size: 13px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.order-card__header b {
flex: none;
margin-left: 10px;
font-size: 15px;
font-weight: 700;
}
.order-card__header .pending {
color: #ff5b20;
}
.order-card__header .done {
color: #21b36b;
}
.order-card__header .muted {
color: #999;
}
.order-card__body {
padding: 12px 14px 14px;
}
.order-goods-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.order-goods-row {
display: flex;
min-width: 0;
align-items: flex-start;
}
.order-thumb {
width: 82px;
height: 82px;
flex: none;
overflow: hidden;
border-radius: 8px;
background: #f5f5f5;
}
.order-goods__info {
min-width: 0;
flex: 1;
padding: 2px 10px 0 12px;
}
.order-goods__info h3 {
display: -webkit-box;
overflow: hidden;
margin: 0 0 8px;
color: #222;
font-size: 15px;
font-weight: 700;
line-height: 21px;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.order-goods__info p {
overflow: hidden;
margin: 4px 0 0;
color: #777;
font-size: 12px;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
.order-goods__info b {
display: block;
margin-top: 14px;
color: #ff5b20;
font-size: 16px;
font-weight: 700;
}
.order-goods-count {
flex: none;
align-self: flex-end;
padding-bottom: 4px;
color: #222;
font-size: 14px;
}
.logistics-row {
display: flex;
align-items: center;
margin: 0 14px;
padding: 13px 0;
border-top: 1px dashed #eee;
border-bottom: 1px solid #f2f2f2;
color: #777;
font-size: 13px;
line-height: 20px;
}
.logistics-row .van-icon {
flex: none;
color: #777;
font-size: 17px;
}
.logistics-row span {
min-width: 0;
flex: 1;
margin: 0 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.order-card__summary {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px 0;
color: #555;
font-size: 13px;
line-height: 22px;
}
.order-card__summary b {
color: #ff5b20;
font-size: 16px;
font-weight: 700;
}
.order-create-time {
margin: 6px 14px 14px;
color: #aaa;
font-size: 13px;
line-height: 20px;
}
.order-card footer {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 14px 16px;
}
.order-actions {
display: flex;
min-width: 0;
flex: 1;
justify-content: flex-end;
gap: 10px;
}
.order-actions .van-button {
min-width: 88px;
height: 34px;
padding: 0 16px;
border: 0;
border-radius: 6px;
font-size: 14px;
font-weight: 700;
}
.primary-action {
color: #ff5b20;
background: #fff1e8;
}
.dark-action {
color: #333;
background: #f6f7f9;
}
</style>