style:优化 订单列表样式
This commit is contained in:
parent
891a87927c
commit
cfb0838170
3
components.d.ts
vendored
3
components.d.ts
vendored
@ -17,6 +17,7 @@ declare module 'vue' {
|
||||
VanButton: typeof import('vant/es')['Button']
|
||||
VanCell: typeof import('vant/es')['Cell']
|
||||
VanCellGroup: typeof import('vant/es')['CellGroup']
|
||||
VanEmpty: typeof import('vant/es')['Empty']
|
||||
VanField: typeof import('vant/es')['Field']
|
||||
VanForm: typeof import('vant/es')['Form']
|
||||
VanIcon: typeof import('vant/es')['Icon']
|
||||
@ -25,12 +26,14 @@ declare module 'vue' {
|
||||
VanPopup: typeof import('vant/es')['Popup']
|
||||
VanRate: typeof import('vant/es')['Rate']
|
||||
VanSearch: typeof import('vant/es')['Search']
|
||||
VanSkeleton: typeof import('vant/es')['Skeleton']
|
||||
VanStepper: typeof import('vant/es')['Stepper']
|
||||
VanSwipe: typeof import('vant/es')['Swipe']
|
||||
VanSwipeItem: typeof import('vant/es')['SwipeItem']
|
||||
VanSwitch: typeof import('vant/es')['Switch']
|
||||
VanTab: typeof import('vant/es')['Tab']
|
||||
VanTabs: typeof import('vant/es')['Tabs']
|
||||
VanTag: typeof import('vant/es')['Tag']
|
||||
VanUploader: typeof import('vant/es')['Uploader']
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,6 +67,178 @@ export type ProudictProductPropertyType = {
|
||||
modifyTime: number
|
||||
productId: number
|
||||
productPropertyName: string
|
||||
vvProductPropertyList?: ProudictProductPropertyType[] // 如需树状结构可用此字段
|
||||
vvProductPropertyValueList: ProudictProductPropertyType[] // 按你原写法保持不变
|
||||
vvProductPropertyList?: ProudictProductPropertyType[]
|
||||
vvProductPropertyValueList?: ProudictProductPropertyType[]
|
||||
}
|
||||
|
||||
/** ----------------------------- Orders ------------------------------ */
|
||||
|
||||
export type OrderStatus =
|
||||
| 'wait_pay'
|
||||
| 'wait_shipping'
|
||||
| 'shipping'
|
||||
| 'delivered'
|
||||
| 'all_refund'
|
||||
| 'part_refund'
|
||||
| 'close'
|
||||
| 'unknown'
|
||||
|
||||
export type ReverseStatus = 'all_refund' | 'part_refund' | 'close' | 'cancel' | string
|
||||
|
||||
export type OrderSkuInfoItem = {
|
||||
propertyName: string
|
||||
propertyValue: string
|
||||
}
|
||||
|
||||
export type PackageLogisticsData = {
|
||||
areaCode?: string
|
||||
areaName?: string
|
||||
context?: string
|
||||
status?: string
|
||||
time?: string
|
||||
ftime?: string
|
||||
}
|
||||
|
||||
export type PackageLogisticsInfo = {
|
||||
com?: string
|
||||
nu?: string
|
||||
state?: string
|
||||
status?: string
|
||||
condition?: string
|
||||
ischeck?: string
|
||||
data?: PackageLogisticsData[]
|
||||
routeInfo?: Record<string, any>
|
||||
message?: string
|
||||
}
|
||||
|
||||
export type OrderPackageEntity = {
|
||||
id: number
|
||||
logisticsCompany?: string
|
||||
trackNumber?: string
|
||||
packageImageUrl?: string
|
||||
packageLogisticsInfo?: string | PackageLogisticsInfo
|
||||
shippingAmount?: number
|
||||
shippingFrom?: string
|
||||
shippingTo?: string
|
||||
com?: string
|
||||
status?: string
|
||||
state?: string
|
||||
shippingType?: string
|
||||
}
|
||||
|
||||
export type TradeOrderLineEntity = {
|
||||
id: number
|
||||
isDelete?: number
|
||||
tradeOrderId?: number
|
||||
orderNo?: string
|
||||
productId?: number
|
||||
productName?: string
|
||||
productMainImageUrl?: string
|
||||
skuId?: number
|
||||
skuImageUrl?: string
|
||||
skuInfo?: string | OrderSkuInfoItem[]
|
||||
singlePrice?: number
|
||||
num?: number
|
||||
allPrice?: number
|
||||
trackNumber?: string
|
||||
logisticsCompany?: string
|
||||
logisticsDesc?: string
|
||||
status?: string
|
||||
payAmount?: number
|
||||
shippingFrom?: string
|
||||
shippingTo?: string
|
||||
createTime?: number
|
||||
modifyTime?: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export type TradeOrderEntity = {
|
||||
id: number
|
||||
buyerId?: number
|
||||
buyerDetailAddress?: string
|
||||
province?: string
|
||||
city?: string
|
||||
district?: string
|
||||
contry?: string
|
||||
gmtDownOrder?: number
|
||||
transactionId?: string
|
||||
tradeInfo?: string
|
||||
allPrice?: number
|
||||
num?: number
|
||||
createTime?: number
|
||||
modifyTime?: number
|
||||
}
|
||||
|
||||
export type OrderListItem = {
|
||||
id: number
|
||||
tradeOrderId?: number
|
||||
orderNo?: string
|
||||
tradeOrderEntity?: TradeOrderEntity
|
||||
vvPackageEntity?: OrderPackageEntity
|
||||
vvTradeOrderLineEntityList?: TradeOrderLineEntity[]
|
||||
productId: number
|
||||
productName: string
|
||||
productMainImageUrl?: string
|
||||
skuId?: number
|
||||
skuInfo?: string | OrderSkuInfoItem[]
|
||||
num: number
|
||||
status: OrderStatus | string
|
||||
reverseStatus?: ReverseStatus
|
||||
promotionPrice?: number
|
||||
originPrice?: number
|
||||
discountAmount?: number
|
||||
shippingAmount?: number
|
||||
freightFee?: number
|
||||
payAmount?: number
|
||||
profitAmount?: number
|
||||
activityAwardCount?: number
|
||||
batchNum?: number
|
||||
buyerId?: number
|
||||
buyerDetailAddress?: string
|
||||
city?: string
|
||||
province?: string
|
||||
district?: string
|
||||
createTime?: number
|
||||
createTimestamp?: number
|
||||
modifyTime?: number
|
||||
modifyTimestamp?: number
|
||||
gmtPay?: number
|
||||
gmtPrePay?: number
|
||||
gmtShipped?: number
|
||||
gmtDelivered?: number
|
||||
gmtCancel?: number
|
||||
gmtClose?: number
|
||||
gmtToShipping?: number
|
||||
payType?: string
|
||||
prepayId?: string
|
||||
transactionId?: string
|
||||
trackNumber?: string
|
||||
shippingFrom?: string
|
||||
shippingTo?: string
|
||||
skuDesc?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export type OrderListQuery = {
|
||||
keyword?: string
|
||||
status?: string | number
|
||||
pageNum?: number
|
||||
pageSize?: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export type OrderListResponseMeta = {
|
||||
code?: string | number
|
||||
msg?: string
|
||||
traceId?: string
|
||||
}
|
||||
|
||||
export type OrderListResponsePayload = {
|
||||
rows?: OrderListItem[]
|
||||
list?: OrderListItem[]
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export type OrderListResponseData = OrderListItem[] | OrderListResponsePayload | null | undefined
|
||||
|
||||
export type OrderListResponse = OrderListResponseMeta & { data?: OrderListResponseData }
|
||||
|
||||
@ -52,7 +52,8 @@ watch(
|
||||
detail: data.detail,
|
||||
province: data.province,
|
||||
city: data.city,
|
||||
status: data.status,
|
||||
// 需要默认值,不能undefin
|
||||
status: data.status || 'common',
|
||||
}
|
||||
areaData.areaCode = data.areaCode
|
||||
}
|
||||
@ -107,12 +108,12 @@ const onSubmit = () => {
|
||||
.van-checkbox__label {
|
||||
margin-left: 0.04rem !important;
|
||||
}
|
||||
.van-cell__title {
|
||||
// width: 0;
|
||||
}
|
||||
.van-cell__value {
|
||||
// flex: none;
|
||||
}
|
||||
// .van-cell__title {
|
||||
// // width: 0;
|
||||
// }
|
||||
// .van-cell__value {
|
||||
// // flex: none;
|
||||
// }
|
||||
.van-checkbox__label {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
@ -57,6 +57,7 @@
|
||||
import api from '@/api'
|
||||
import { requestPayment } from '@/utils/wx-minprogram'
|
||||
import type { SkuType } from '@/api/types/shop'
|
||||
import { showToast } from 'vant'
|
||||
const model = defineModel<boolean>()
|
||||
const showAddressList = defineModel<boolean>('showAddressList')
|
||||
const props = defineProps<{ list: Array<any>; curAddressData: Recordable<any>; scene: 'order' | 'cart' }>()
|
||||
@ -88,8 +89,11 @@ watch(model, (val) => {
|
||||
// 更新
|
||||
const updateOptionDisabled = () => {
|
||||
Object.entries(skuData.value).forEach(([categoryName, options]) => {
|
||||
console.log(categoryName, options)
|
||||
options.forEach((opt) => {
|
||||
const trialSelection = { ...curSelection.value, [categoryName]: opt.label }
|
||||
|
||||
console.log('trialSelection', trialSelection)
|
||||
const availableStock = skuSelectionList.value
|
||||
.filter(({ selection }) =>
|
||||
categoryOrder.value.every((name) => !trialSelection[name] || selection[name] === trialSelection[name]),
|
||||
@ -175,6 +179,15 @@ const onSubmit = async () => {
|
||||
await api.shop.addCart.post(formData.value)
|
||||
showToast('加入购物车成功')
|
||||
} else {
|
||||
|
||||
// const curAddressData = props.curAddressData
|
||||
|
||||
// if(!curAddressData.buyerAddressId ) {
|
||||
|
||||
// showToast("请填写收获地址")
|
||||
// return false
|
||||
// }
|
||||
// buyerAddressId 收获地址 测试写死
|
||||
const res = await api.shop.order.post<{ timeStamp: any; package: string; prepayId: string }>({ buyerAddressId: 2, vvTradeOrderLineDTOList: [formData.value] })
|
||||
res.data.package = res.data.prepayId
|
||||
res.data.timeStamp = String(res.data.timeStamp)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
899
temp.txt
Normal file
899
temp.txt
Normal file
@ -0,0 +1,899 @@
|
||||
<template>
|
||||
<div :class="[prefixCls]">
|
||||
<section :class="`${prefixCls}__header`">
|
||||
<div :class="`${prefixCls}__search`">
|
||||
<van-search v-model="searchValue" show-action shape="round" placeholder="搜索订单 / 商品名称" @search="onSearch" @cancel="onCancel">
|
||||
<template #action>
|
||||
<span @click="onSearch(searchValue)">搜索</span>
|
||||
</template>
|
||||
</van-search>
|
||||
</div>
|
||||
<div :class="`${prefixCls}__status-bar`">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
type="button"
|
||||
:class="[
|
||||
`${prefixCls}__status-item`,
|
||||
{ [`${prefixCls}__status-item--active`]: activeTab === tab.value },
|
||||
]"
|
||||
@click="handleTabChange(tab.value)"
|
||||
>
|
||||
{{ tab.name }}
|
||||
</button>
|
||||
<van-icon name="filter-o" :class="`${prefixCls}__status-filter`" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div :class="`${prefixCls}__scroll-box`">
|
||||
<section v-if="summaryMetrics.length" :class="`${prefixCls}__summary`">
|
||||
<div v-for="metric in summaryMetrics" :key="metric.label" :class="`${prefixCls}__summary-card`">
|
||||
<p class="summary-value">{{ metric.value }}</p>
|
||||
<p class="summary-label">{{ metric.label }}</p>
|
||||
<p class="summary-desc">{{ metric.desc }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div :class="`${prefixCls}__list`">
|
||||
<van-skeleton v-if="isListLoading && !orders.length" :row="4" title />
|
||||
<template v-else>
|
||||
<div v-for="order in orders" :key="order.id" :class="`${prefixCls}__card`">
|
||||
<div :class="`${prefixCls}__card-header`">
|
||||
<p class="store">
|
||||
<van-icon name="shop-o" size=".16rem" />
|
||||
<span>{{ order.storeName || '未知' }}</span>
|
||||
</p>
|
||||
<div class="status" :class="getStatusMeta(order.displayStatus).className">
|
||||
<span class="status-label">{{ getStatusMeta(order.displayStatus).label }}</span>
|
||||
<p class="status-desc">{{ getStatusDescription(order) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="`${prefixCls}__card-meta`">
|
||||
<span>订单编号:{{ order.orderNo || order.id }}</span>
|
||||
<span>下单时间:{{ order.formattedCreateTime }}</span>
|
||||
</div>
|
||||
|
||||
<div :class="`${prefixCls}__goods`">
|
||||
<div v-for="goods in order.goods" :key="goods.id || goods.productId" class="goods-item" @click="goDetail(order.id)">
|
||||
<img :src="goods.productMainImageUrl || defaultCover" :alt="goods.productName" />
|
||||
<div class="info">
|
||||
<p class="name">{{ goods.productName || order.productName }}</p>
|
||||
<p class="spec" v-if="goods.skuDesc">{{ goods.skuDesc }}</p>
|
||||
</div>
|
||||
<div class="price">
|
||||
<p>¥{{ formatAmount(goods.payAmount ?? order.payAmount ?? order.promotionPrice) }}</p>
|
||||
<span>x{{ goods.num || order.num || 1 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="order.logisticsInfo" :class="`${prefixCls}__card-logistics`">
|
||||
<van-icon :name="order.logisticsInfo.icon" size=".14rem" />
|
||||
<div class="logistics-text">
|
||||
<p class="title">{{ order.logisticsInfo.title }}</p>
|
||||
<p>{{ order.logisticsInfo.desc }}</p>
|
||||
<p v-if="order.logisticsInfo.sub" class="sub">{{ order.logisticsInfo.sub }}</p>
|
||||
</div>
|
||||
<van-button v-if="order.logisticsInfo.trackNumber" size="mini" type="primary" plain @click.stop="handleViewLogistics(order)">查物流</van-button>
|
||||
</div>
|
||||
|
||||
<ul :class="`${prefixCls}__amount`">
|
||||
<li>
|
||||
<span>商品金额</span><b>¥{{ formatAmount(order.originPrice ?? order.payAmount) }}</b>
|
||||
</li>
|
||||
<li>
|
||||
<span>优惠</span><b>-¥{{ formatAmount(order.discountAmount) }}</b>
|
||||
</li>
|
||||
<li>
|
||||
<span>运费</span><b>¥{{ formatAmount(order.freightFee ?? order.shippingAmount) }}</b>
|
||||
</li>
|
||||
<li class="total">
|
||||
<span>实付</span><b>¥{{ formatAmount(order.payAmount ?? order.promotionPrice) }}</b>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-if="getTextActionKeys(order).length" :class="`${prefixCls}__tool-links`">
|
||||
<button v-for="actionKey in getTextActionKeys(order)" :key="actionKey" @click.stop="triggerAction(actionKey, order)">
|
||||
{{ ORDER_ACTION_CONFIG[actionKey]?.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div :class="`${prefixCls}__card-footer`">
|
||||
<van-popover
|
||||
:class="`${prefixCls}__popover`"
|
||||
:show="activePopoverId === order.id"
|
||||
placement="top-start"
|
||||
:actions="moreActions"
|
||||
@select="(action) => handleMoreAction(action, order)"
|
||||
@update:show="(val) => onPopoverVisibleChange(order, val)"
|
||||
>
|
||||
<template #reference>
|
||||
<span class="more-action">更多</span>
|
||||
</template>
|
||||
</van-popover>
|
||||
<div :class="`${prefixCls}__action-buttons`">
|
||||
<template v-for="buttonKeys in [getButtonActionKeys(order)]" :key="`buttons-${order.id}`">
|
||||
<van-button
|
||||
v-for="(actionKey, index) in buttonKeys"
|
||||
:key="actionKey"
|
||||
size="small"
|
||||
:plain="index !== buttonKeys.length - 1"
|
||||
:type="index === buttonKeys.length - 1 ? ORDER_ACTION_CONFIG[actionKey]?.type : undefined"
|
||||
:class="[index === buttonKeys.length - 1 ? ORDER_ACTION_CONFIG[actionKey]?.className : '', index === buttonKeys.length - 1 ? 'is-main-button' : 'is-sub-button']"
|
||||
@click.stop="triggerAction(actionKey, order)"
|
||||
>
|
||||
{{ ORDER_ACTION_CONFIG[actionKey]?.label }}
|
||||
</van-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<van-empty v-if="!orders.length" image="search" description="暂无订单" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import api from '@/api'
|
||||
import { showToast } from 'vant'
|
||||
import { useDesign } from '@/hooks/web/useDesign.ts'
|
||||
import type { OrderListItem, OrderStatus, PackageLogisticsInfo, TradeOrderLineEntity } from '@/api/types/shop'
|
||||
|
||||
type OrderTabValue = 'all' | 'wait_pay' | 'wait_shipping' | 'shipping' | 'after_sale'
|
||||
|
||||
interface OrderTab {
|
||||
name: string
|
||||
value: OrderTabValue
|
||||
apiStatus?: string
|
||||
}
|
||||
|
||||
type OrderActionKey = 'pay' | 'cancel' | 'remind' | 'viewLogistics' | 'extend' | 'confirm' | 'applyRefund' | 'delete' | 'contact' | 'buyAgain' | 'addToCart' | 'review'
|
||||
|
||||
interface GoodsItem {
|
||||
id?: number | string
|
||||
productId?: number
|
||||
productName?: string
|
||||
productMainImageUrl?: string
|
||||
skuDesc?: string
|
||||
num?: number
|
||||
payAmount?: number
|
||||
}
|
||||
|
||||
interface LogisticsInfo {
|
||||
title: string
|
||||
desc: string
|
||||
sub?: string
|
||||
trackNumber?: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
interface EnhancedOrderItem extends OrderListItem {
|
||||
displayStatus: OrderStatus
|
||||
formattedCreateTime: string
|
||||
goods: GoodsItem[]
|
||||
logisticsInfo: LogisticsInfo | null
|
||||
}
|
||||
|
||||
interface StatusActionConfig {
|
||||
text?: OrderActionKey[]
|
||||
buttons?: OrderActionKey[]
|
||||
primary?: OrderActionKey
|
||||
}
|
||||
|
||||
const tabs: OrderTab[] = [
|
||||
{ name: '全部', value: 'all' },
|
||||
{ name: '待付款', value: 'wait_pay', apiStatus: 'wait_pay' },
|
||||
{ name: '待发货', value: 'wait_shipping', apiStatus: 'wait_shipping' },
|
||||
{ name: '待收货', value: 'shipping', apiStatus: 'shipping' },
|
||||
{ name: '退款/售后', value: 'after_sale', apiStatus: 'after_sale' },
|
||||
]
|
||||
|
||||
const ORDER_STATUS_META: Record<OrderStatus, { label: string; desc: string; className: string }> = {
|
||||
wait_pay: { label: '待付款', desc: '请尽快完成支付', className: 'is-wait-pay' },
|
||||
wait_shipping: { label: '待发货', desc: '商家正在备货', className: 'is-wait-ship' },
|
||||
shipping: { label: '运输中', desc: '包裹已发出', className: 'is-shipping' },
|
||||
delivered: { label: '已收货', desc: '欢迎评价本次购物', className: 'is-delivered' },
|
||||
all_refund: { label: '退款完成', desc: '已原路退回', className: 'is-refund' },
|
||||
part_refund: { label: '部分退款', desc: '退款处理中', className: 'is-refund' },
|
||||
close: { label: '交易关闭', desc: '订单已关闭', className: 'is-close' },
|
||||
unknown: { label: '处理中', desc: '系统处理中', className: 'is-unknown' },
|
||||
}
|
||||
|
||||
const ORDER_STATUS_ACTIONS: Record<OrderStatus, StatusActionConfig> = {
|
||||
wait_pay: { text: ['cancel'], buttons: ['pay'], primary: 'pay' },
|
||||
wait_shipping: { text: ['remind', 'contact'], buttons: ['buyAgain', 'applyRefund'], primary: 'applyRefund' },
|
||||
shipping: { text: ['extend'], buttons: ['viewLogistics', 'buyAgain', 'confirm'], primary: 'confirm' },
|
||||
delivered: { text: [], buttons: ['viewLogistics', 'buyAgain', 'review'], primary: 'review' },
|
||||
all_refund: { text: ['contact'], buttons: ['buyAgain'], primary: 'buyAgain' },
|
||||
part_refund: { text: ['contact'], buttons: ['buyAgain'], primary: 'buyAgain' },
|
||||
close: { text: ['delete'], buttons: ['buyAgain'], primary: 'buyAgain' },
|
||||
unknown: { text: ['contact'], buttons: ['buyAgain'], primary: 'buyAgain' },
|
||||
}
|
||||
|
||||
const ORDER_ACTION_CONFIG: Record<OrderActionKey, { label: string; type?: 'primary' | 'success' | 'warning' | 'danger'; plain?: boolean; className?: string }> = {
|
||||
pay: { label: '去支付', type: 'warning' },
|
||||
cancel: { label: '取消订单', plain: true },
|
||||
remind: { label: '提醒发货', plain: true },
|
||||
viewLogistics: { label: '查看物流', plain: true },
|
||||
extend: { label: '延长收货', plain: true },
|
||||
confirm: { label: '确认收货', type: 'warning' },
|
||||
applyRefund: { label: '申请售后', plain: true },
|
||||
delete: { label: '删除订单', plain: true },
|
||||
contact: { label: '联系客服', plain: true },
|
||||
buyAgain: { label: '再买一单', type: 'warning', className: 'theme-bg-color theme-border-color color-white' },
|
||||
addToCart: { label: '加入购物车', plain: true },
|
||||
review: { label: '去评价', type: 'primary' },
|
||||
}
|
||||
|
||||
const moreActions = [
|
||||
{ text: '复制订单号', value: 'copy' },
|
||||
{ text: '联系客服', value: 'service' },
|
||||
{ text: '删除订单', value: 'delete' },
|
||||
]
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
const prefixCls = getPrefixCls('order-page')
|
||||
const router = useRouter()
|
||||
const defaultCover = 'https://img.yzcdn.cn/vant/cat.jpeg'
|
||||
|
||||
const orders = ref<EnhancedOrderItem[]>([])
|
||||
const searchValue = ref('')
|
||||
const activeTab = ref<OrderTabValue>(tabs[0].value)
|
||||
const isListLoading = ref(false)
|
||||
const activePopoverId = ref<number | string | null>(null)
|
||||
|
||||
const queryParams = reactive({
|
||||
status: tabs[0].apiStatus ?? '',
|
||||
keyword: '',
|
||||
})
|
||||
|
||||
const summaryMetrics = computed(() => {
|
||||
if (!orders.value.length) return []
|
||||
const stats = orders.value.reduce(
|
||||
(acc, order) => {
|
||||
acc.total += 1
|
||||
if (order.displayStatus === 'wait_pay') acc.waitPay += 1
|
||||
if (order.displayStatus === 'wait_shipping') acc.waitShip += 1
|
||||
if (order.displayStatus === 'shipping') acc.waitReceive += 1
|
||||
if (order.displayStatus === 'delivered') acc.received += 1
|
||||
if (order.displayStatus === 'all_refund' || order.displayStatus === 'part_refund') acc.afterSale += 1
|
||||
return acc
|
||||
},
|
||||
{ total: 0, waitPay: 0, waitShip: 0, waitReceive: 0, received: 0, afterSale: 0 },
|
||||
)
|
||||
|
||||
return [
|
||||
{ label: '总订单', value: stats.total, desc: '含全部交易记录' },
|
||||
{ label: '待付款', value: stats.waitPay, desc: '记得及时支付' },
|
||||
{ label: '待发货', value: stats.waitShip, desc: '商家备货中' },
|
||||
{ label: '待收货', value: stats.waitReceive, desc: '关注物流动态' },
|
||||
{ label: '已收货', value: stats.received, desc: '欢迎前往评价' },
|
||||
{ label: '售后/退款', value: stats.afterSale, desc: '售后处理中' },
|
||||
]
|
||||
})
|
||||
|
||||
const parseJSON = <T,>(value?: string | T): T | null => {
|
||||
if (!value) return null
|
||||
if (typeof value === 'object') return value as T
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (error) {
|
||||
console.warn('[order] parseJSON failed', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const parseSkuInfo = (skuInfo?: string | any[]) => {
|
||||
const list = parseJSON<any[]>(skuInfo) || []
|
||||
if (!Array.isArray(list)) return ''
|
||||
return list
|
||||
.map((item) => {
|
||||
if (!item?.propertyName && !item?.propertyValue) return ''
|
||||
return `${item.propertyName || ''}${item.propertyName ? ':' : ''}${item.propertyValue || ''}`
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(' · ')
|
||||
}
|
||||
|
||||
const parseLogisticsInfo = (payload?: string | PackageLogisticsInfo) => parseJSON<PackageLogisticsInfo>(payload)
|
||||
|
||||
const formatTimestamp = (timestamp?: number | string) => {
|
||||
if (!timestamp) return '--'
|
||||
const value = typeof timestamp === 'string' ? Number(timestamp) : timestamp
|
||||
if (!Number.isFinite(value)) return '--'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return '--'
|
||||
const pad = (num: number) => num.toString().padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
}
|
||||
|
||||
const formatAmount = (value?: number | string) => {
|
||||
const num = Number(value ?? 0)
|
||||
return Number.isFinite(num) ? num.toFixed(2) : '0.00'
|
||||
}
|
||||
|
||||
const hasTrackNumber = (order: OrderListItem) => Boolean(order.trackNumber || order.vvPackageEntity?.trackNumber || order.vvTradeOrderLineEntityList?.some((line) => Boolean(line.trackNumber)))
|
||||
|
||||
const deriveStatus = (order: OrderListItem): OrderStatus => {
|
||||
const reversed = order.reverseStatus?.toLowerCase?.()
|
||||
if (reversed === 'all_refund') return 'all_refund'
|
||||
if (reversed === 'part_refund') return 'part_refund'
|
||||
|
||||
const raw = String(order.status || '').toLowerCase()
|
||||
if (raw.includes('wait_pay')) return 'wait_pay'
|
||||
if (raw.includes('wait_shipping')) return 'wait_shipping'
|
||||
if (raw.includes('shipping')) return 'shipping'
|
||||
if (raw.includes('delivered')) return 'delivered'
|
||||
if (raw.includes('all_refund')) return 'all_refund'
|
||||
if (raw.includes('part_refund')) return 'part_refund'
|
||||
if (raw.includes('close')) return 'close'
|
||||
if (hasTrackNumber(order)) return 'shipping'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
const formatGoods = (order: OrderListItem): GoodsItem[] => {
|
||||
const lines = order.vvTradeOrderLineEntityList
|
||||
if (Array.isArray(lines) && lines.length) {
|
||||
return lines.map((line) => ({
|
||||
id: line.id,
|
||||
productId: line.productId ?? order.productId,
|
||||
productName: line.productName ?? order.productName,
|
||||
productMainImageUrl: line.productMainImageUrl ?? order.productMainImageUrl,
|
||||
skuDesc: parseSkuInfo(line.skuInfo),
|
||||
num: line.num ?? order.num,
|
||||
payAmount: line.singlePrice ?? line.payAmount ?? order.payAmount,
|
||||
}))
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: order.id,
|
||||
productId: order.productId,
|
||||
productName: order.productName,
|
||||
productMainImageUrl: order.productMainImageUrl,
|
||||
skuDesc: parseSkuInfo(order.skuInfo),
|
||||
num: order.num,
|
||||
payAmount: order.payAmount ?? order.promotionPrice,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const buildLogisticsInfo = (order: OrderListItem): LogisticsInfo | null => {
|
||||
const pkg = order.vvPackageEntity
|
||||
const trackNumber = order.trackNumber || pkg?.trackNumber
|
||||
if (trackNumber) {
|
||||
const info = parseLogisticsInfo(pkg?.packageLogisticsInfo)
|
||||
const latest = info?.data?.[0]
|
||||
return {
|
||||
title: pkg?.logisticsCompany || info?.com || '承运商处理中',
|
||||
desc: latest?.context || '包裹运输中,请耐心等待',
|
||||
sub: latest?.areaName || pkg?.shippingTo,
|
||||
trackNumber,
|
||||
icon: 'logistics',
|
||||
}
|
||||
}
|
||||
|
||||
const firstLine: TradeOrderLineEntity | undefined = order.vvTradeOrderLineEntityList?.[0]
|
||||
if (firstLine) {
|
||||
return {
|
||||
title: '商家备货中',
|
||||
desc: firstLine.logisticsDesc || '商家正在为您准备商品',
|
||||
sub: order.tradeOrderEntity?.buyerDetailAddress || [order.province, order.city, order.district].filter(Boolean).join(' '),
|
||||
trackNumber: firstLine.trackNumber,
|
||||
icon: 'shop-o',
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const normalizeOrder = (item: OrderListItem): EnhancedOrderItem => {
|
||||
const displayStatus = deriveStatus(item)
|
||||
return {
|
||||
...item,
|
||||
displayStatus,
|
||||
formattedCreateTime: formatTimestamp(item.createTime ?? item.tradeOrderEntity?.gmtDownOrder),
|
||||
goods: formatGoods(item),
|
||||
logisticsInfo: buildLogisticsInfo(item),
|
||||
}
|
||||
}
|
||||
|
||||
const extractOrderRows = (payload: any): OrderListItem[] => {
|
||||
if (Array.isArray(payload?.rows)) return payload.rows
|
||||
if (Array.isArray(payload?.data)) return payload.data
|
||||
if (Array.isArray(payload)) return payload
|
||||
return []
|
||||
}
|
||||
|
||||
const getStatusMeta = (status: OrderStatus) => ORDER_STATUS_META[status] ?? ORDER_STATUS_META.unknown
|
||||
const getStatusDescription = (order: EnhancedOrderItem) => {
|
||||
if (order.displayStatus === 'shipping') {
|
||||
return order.logisticsInfo?.desc || ORDER_STATUS_META.shipping.desc
|
||||
}
|
||||
if (order.displayStatus === 'wait_shipping') return ORDER_STATUS_META.wait_shipping.desc
|
||||
return getStatusMeta(order.displayStatus).desc
|
||||
}
|
||||
const getTextActionKeys = (order: EnhancedOrderItem) => ORDER_STATUS_ACTIONS[order.displayStatus]?.text ?? []
|
||||
const getButtonActionKeys = (order: EnhancedOrderItem) => {
|
||||
const config = ORDER_STATUS_ACTIONS[order.displayStatus]
|
||||
const keys = config?.buttons ? [...config.buttons] : ['buyAgain']
|
||||
if (config?.primary && keys.includes(config.primary)) {
|
||||
return [...keys.filter((key) => key !== config.primary), config.primary]
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
const handleTabChange = (value: OrderTabValue) => {
|
||||
const target = tabs.find((tab) => tab.value === value)
|
||||
if (!target) return
|
||||
activeTab.value = target.value
|
||||
queryParams.status = target.apiStatus ?? ''
|
||||
handleGetList()
|
||||
}
|
||||
|
||||
const onSearch = (value: string) => {
|
||||
queryParams.keyword = value?.trim?.() || ''
|
||||
handleGetList()
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
searchValue.value = ''
|
||||
queryParams.keyword = ''
|
||||
handleGetList()
|
||||
}
|
||||
|
||||
const handleAddToCart = (order: EnhancedOrderItem) => {
|
||||
showToast(`已将「${order.productName || '该商品'}」加入购物车`)
|
||||
}
|
||||
|
||||
const handleBuyAgain = (order: EnhancedOrderItem | number | string) => {
|
||||
const id = typeof order === 'object' ? order.productId || order.id : order
|
||||
router.push({ name: 'commodity-detail', query: { id } })
|
||||
}
|
||||
|
||||
const handleViewLogistics = (order: EnhancedOrderItem) => {
|
||||
router.push({ path: '/order/detail', query: { id: order.id, scene: 'logistics' } })
|
||||
}
|
||||
|
||||
const handleRemindDelivery = (order: EnhancedOrderItem) => {
|
||||
showToast(`已提醒商家尽快为订单 ${order.orderNo || order.id} 发货`)
|
||||
}
|
||||
|
||||
const handleExtendReceive = (order: EnhancedOrderItem) => {
|
||||
showToast(`已为订单 ${order.orderNo || order.id} 申请延长收货`)
|
||||
}
|
||||
|
||||
const handleConfirmReceive = () => {
|
||||
showToast('确认收货成功,感谢您的支持')
|
||||
}
|
||||
|
||||
const handleApplyRefund = (order: EnhancedOrderItem) => {
|
||||
router.push({ path: '/order/detail', query: { id: order.id, scene: 'afterSale' } })
|
||||
}
|
||||
|
||||
const handleReview = (order: EnhancedOrderItem) => {
|
||||
router.push({ name: 'review/write', query: { id: order.productId } })
|
||||
}
|
||||
|
||||
const handleDeleteOrder = (order: EnhancedOrderItem) => {
|
||||
showToast(`订单 ${order.orderNo || order.id} 已移入回收站`)
|
||||
}
|
||||
|
||||
const handleContactService = () => {
|
||||
showToast('客服将尽快与您联系')
|
||||
}
|
||||
|
||||
const actionHandlers: Record<OrderActionKey, (order: EnhancedOrderItem) => void> = {
|
||||
pay: (order) => showToast(`跳转订单 ${order.orderNo || order.id} 支付`),
|
||||
cancel: (order) => showToast(`订单 ${order.orderNo || order.id} 的取消申请已提交`),
|
||||
remind: handleRemindDelivery,
|
||||
viewLogistics: handleViewLogistics,
|
||||
extend: handleExtendReceive,
|
||||
confirm: handleConfirmReceive,
|
||||
applyRefund: handleApplyRefund,
|
||||
delete: handleDeleteOrder,
|
||||
contact: () => handleContactService(),
|
||||
buyAgain: (order) => handleBuyAgain(order),
|
||||
addToCart: handleAddToCart,
|
||||
review: handleReview,
|
||||
}
|
||||
|
||||
const triggerAction = (actionKey: OrderActionKey, order: EnhancedOrderItem) => {
|
||||
actionHandlers[actionKey]?.(order)
|
||||
}
|
||||
|
||||
const handleCopyOrderNo = async (order: EnhancedOrderItem) => {
|
||||
const text = String(order.orderNo || order.id || '')
|
||||
if (!text) return
|
||||
try {
|
||||
if (typeof navigator !== 'undefined' && navigator?.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} else {
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = text
|
||||
textarea.style.position = 'fixed'
|
||||
textarea.style.opacity = '0'
|
||||
document.body.appendChild(textarea)
|
||||
textarea.focus()
|
||||
textarea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textarea)
|
||||
}
|
||||
showToast('订单编号已复制')
|
||||
} catch (error) {
|
||||
console.warn('[order] copy failed', error)
|
||||
showToast('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
const handleMoreAction = (action: { value: string }, order: EnhancedOrderItem) => {
|
||||
switch (action.value) {
|
||||
case 'copy':
|
||||
handleCopyOrderNo(order)
|
||||
break
|
||||
case 'service':
|
||||
triggerAction('contact', order)
|
||||
break
|
||||
case 'delete':
|
||||
triggerAction('delete', order)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
activePopoverId.value = null
|
||||
}
|
||||
|
||||
const onPopoverVisibleChange = (order: EnhancedOrderItem, visible: boolean) => {
|
||||
if (visible) {
|
||||
activePopoverId.value = order.id
|
||||
} else if (activePopoverId.value === order.id) {
|
||||
activePopoverId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const goDetail = (id: number | string) => {
|
||||
router.push({ path: '/order/detail', query: { id } })
|
||||
}
|
||||
|
||||
const handleGetList = () => {
|
||||
isListLoading.value = true
|
||||
api.shop.orderList
|
||||
.post({
|
||||
keyword: queryParams.keyword,
|
||||
status: queryParams.status || undefined,
|
||||
})
|
||||
.then((res) => {
|
||||
const list = extractOrderRows(res?.data)
|
||||
const normalized = list.map(normalizeOrder)
|
||||
if (activeTab.value === 'after_sale') {
|
||||
orders.value = normalized.filter((item) => item.displayStatus === 'all_refund' || item.displayStatus === 'part_refund')
|
||||
return
|
||||
}
|
||||
orders.value = normalized
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('[order] handleGetList error', error)
|
||||
orders.value = []
|
||||
})
|
||||
.finally(() => {
|
||||
isListLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const init = () => {
|
||||
handleGetList()
|
||||
}
|
||||
|
||||
init()
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
$prefix-cls: #{$namespace}-order-page;
|
||||
|
||||
.#{$prefix-cls} {
|
||||
height: 100vh;
|
||||
background: #f6f6f6;
|
||||
overflow: hidden;
|
||||
|
||||
&__search {
|
||||
padding: 0.12rem 0.12rem 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
&_scroll-box {
|
||||
height: calc(100vh - 0.1rem);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
&__tabs {
|
||||
background: #fff;
|
||||
padding-bottom: 0.08rem;
|
||||
:deep(.van-tabs__wrap) {
|
||||
height: 0.32rem;
|
||||
}
|
||||
:deep(.van-tabs__line) {
|
||||
background: var(--van-primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
&__summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.12rem;
|
||||
padding: 0.12rem;
|
||||
}
|
||||
|
||||
&__summary-card {
|
||||
padding: 0.12rem;
|
||||
border-radius: 0.12rem;
|
||||
background: linear-gradient(135deg, #fff, #fdf4ff);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
.summary-value {
|
||||
font-size: 0.2rem;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
.summary-label {
|
||||
margin-top: 0.04rem;
|
||||
font-size: 0.13rem;
|
||||
color: #666;
|
||||
}
|
||||
.summary-desc {
|
||||
margin-top: 0.02rem;
|
||||
font-size: 0.11rem;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
padding: 0.12rem;
|
||||
}
|
||||
|
||||
&__card {
|
||||
padding: 0.16rem;
|
||||
border-radius: 0.16rem;
|
||||
background: #fff;
|
||||
box-shadow: 0 12px 24px rgba(17, 24, 39, 0.08);
|
||||
& + & {
|
||||
margin-top: 0.16rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
.store {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
color: #111;
|
||||
span {
|
||||
margin-left: 0.06rem;
|
||||
}
|
||||
}
|
||||
.status {
|
||||
text-align: right;
|
||||
.status-label {
|
||||
font-size: 0.13rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.status-desc {
|
||||
margin-top: 0.02rem;
|
||||
font-size: 0.11rem;
|
||||
color: #999;
|
||||
}
|
||||
&.is-wait-pay .status-label,
|
||||
&.is-wait-ship .status-label,
|
||||
&.is-shipping .status-label {
|
||||
color: #f97316;
|
||||
}
|
||||
&.is-delivered .status-label {
|
||||
color: #10b981;
|
||||
}
|
||||
&.is-refund .status-label {
|
||||
color: #6366f1;
|
||||
}
|
||||
&.is-close .status-label {
|
||||
color: #94a3b8;
|
||||
}
|
||||
&.is-unknown .status-label {
|
||||
color: #2563eb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__card-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 0.08rem;
|
||||
font-size: 0.11rem;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
&__goods {
|
||||
margin-top: 0.12rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.12rem;
|
||||
.goods-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
img {
|
||||
width: 0.92rem;
|
||||
height: 0.92rem;
|
||||
border-radius: 0.12rem;
|
||||
object-fit: cover;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
.info {
|
||||
flex: 1;
|
||||
margin: 0 0.12rem;
|
||||
.name {
|
||||
font-size: 0.14rem;
|
||||
color: #333;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.spec {
|
||||
margin-top: 0.04rem;
|
||||
font-size: 0.12rem;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
.price {
|
||||
text-align: right;
|
||||
p {
|
||||
font-size: 0.16rem;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
span {
|
||||
display: block;
|
||||
margin-top: 0.04rem;
|
||||
font-size: 0.12rem;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__card-logistics {
|
||||
margin-top: 0.12rem;
|
||||
padding: 0.12rem;
|
||||
border-radius: 0.12rem;
|
||||
background: #f9fafb;
|
||||
display: flex;
|
||||
gap: 0.08rem;
|
||||
align-items: flex-start;
|
||||
font-size: 0.12rem;
|
||||
color: #444;
|
||||
:deep(.van-icon) {
|
||||
color: #2563eb;
|
||||
}
|
||||
.logistics-text {
|
||||
flex: 1;
|
||||
}
|
||||
.title {
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
.sub {
|
||||
color: #999;
|
||||
}
|
||||
:deep(.van-button) {
|
||||
margin-left: auto;
|
||||
border-radius: 999px;
|
||||
}
|
||||
}
|
||||
|
||||
&__amount {
|
||||
margin-top: 0.12rem;
|
||||
font-size: 0.12rem;
|
||||
color: #666;
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
& + li {
|
||||
margin-top: 0.04rem;
|
||||
}
|
||||
b {
|
||||
font-weight: 500;
|
||||
}
|
||||
&.total {
|
||||
margin-top: 0.08rem;
|
||||
padding-top: 0.08rem;
|
||||
border-top: 1px dashed #eee;
|
||||
span {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
b {
|
||||
font-size: 0.16rem;
|
||||
color: #111;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__tool-links {
|
||||
margin-top: 0.12rem;
|
||||
padding-top: 0.12rem;
|
||||
border-top: 1px dashed #f0f0f0;
|
||||
display: flex;
|
||||
gap: 0.12rem;
|
||||
flex-wrap: wrap;
|
||||
button {
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 0.12rem;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
&:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -0.06rem;
|
||||
top: 50%;
|
||||
width: 1px;
|
||||
height: 0.12rem;
|
||||
background: #e5e7eb;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__card-footer {
|
||||
margin-top: 0.16rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.more-action {
|
||||
font-size: 0.12rem;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
&__action-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.08rem;
|
||||
justify-content: flex-end;
|
||||
:deep(.van-button) {
|
||||
min-width: 0.86rem;
|
||||
border-radius: 999px;
|
||||
height: 0.3rem;
|
||||
line-height: 0.3rem;
|
||||
}
|
||||
:deep(.van-button.is-sub-button) {
|
||||
border-color: #e5e7eb;
|
||||
color: #111;
|
||||
}
|
||||
:deep(.van-button.is-main-button) {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&__popover {
|
||||
:deep(.van-popover__content) {
|
||||
.van-popover__action {
|
||||
width: 1.3rem;
|
||||
height: 0.34rem;
|
||||
line-height: 0.34rem;
|
||||
}
|
||||
.van-popover__action-text {
|
||||
font-size: 0.12rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user