style:优化 订单列表样式

This commit is contained in:
jzp 2025-11-13 16:37:37 +08:00
parent 891a87927c
commit cfb0838170
6 changed files with 1983 additions and 152 deletions

3
components.d.ts vendored
View File

@ -17,6 +17,7 @@ declare module 'vue' {
VanButton: typeof import('vant/es')['Button'] VanButton: typeof import('vant/es')['Button']
VanCell: typeof import('vant/es')['Cell'] VanCell: typeof import('vant/es')['Cell']
VanCellGroup: typeof import('vant/es')['CellGroup'] VanCellGroup: typeof import('vant/es')['CellGroup']
VanEmpty: typeof import('vant/es')['Empty']
VanField: typeof import('vant/es')['Field'] VanField: typeof import('vant/es')['Field']
VanForm: typeof import('vant/es')['Form'] VanForm: typeof import('vant/es')['Form']
VanIcon: typeof import('vant/es')['Icon'] VanIcon: typeof import('vant/es')['Icon']
@ -25,12 +26,14 @@ declare module 'vue' {
VanPopup: typeof import('vant/es')['Popup'] VanPopup: typeof import('vant/es')['Popup']
VanRate: typeof import('vant/es')['Rate'] VanRate: typeof import('vant/es')['Rate']
VanSearch: typeof import('vant/es')['Search'] VanSearch: typeof import('vant/es')['Search']
VanSkeleton: typeof import('vant/es')['Skeleton']
VanStepper: typeof import('vant/es')['Stepper'] VanStepper: typeof import('vant/es')['Stepper']
VanSwipe: typeof import('vant/es')['Swipe'] VanSwipe: typeof import('vant/es')['Swipe']
VanSwipeItem: typeof import('vant/es')['SwipeItem'] VanSwipeItem: typeof import('vant/es')['SwipeItem']
VanSwitch: typeof import('vant/es')['Switch'] VanSwitch: typeof import('vant/es')['Switch']
VanTab: typeof import('vant/es')['Tab'] VanTab: typeof import('vant/es')['Tab']
VanTabs: typeof import('vant/es')['Tabs'] VanTabs: typeof import('vant/es')['Tabs']
VanTag: typeof import('vant/es')['Tag']
VanUploader: typeof import('vant/es')['Uploader'] VanUploader: typeof import('vant/es')['Uploader']
} }
} }

View File

@ -67,6 +67,178 @@ export type ProudictProductPropertyType = {
modifyTime: number modifyTime: number
productId: number productId: number
productPropertyName: string productPropertyName: string
vvProductPropertyList?: ProudictProductPropertyType[] // 如需树状结构可用此字段 vvProductPropertyList?: ProudictProductPropertyType[]
vvProductPropertyValueList: 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 }

View File

@ -1,5 +1,5 @@
<template> <template>
<van-popup id="address-form" class="flex flex-col" v-model:show="model" closeable round> <van-popup id="address-form" class="flex flex-col" v-model:show="model" closeable round>
<h3 class="text-center pt-5 text-base">{{ `${isEdit ? '修改' : '新增'}收货地址` }}</h3> <h3 class="text-center pt-5 text-base">{{ `${isEdit ? '修改' : '新增'}收货地址` }}</h3>
<div class="pt-3 flex-1 overflow-y-scroll"> <div class="pt-3 flex-1 overflow-y-scroll">
<van-form @submit="onSubmit"> <van-form @submit="onSubmit">
@ -52,7 +52,8 @@ watch(
detail: data.detail, detail: data.detail,
province: data.province, province: data.province,
city: data.city, city: data.city,
status: data.status, // ,undefin
status: data.status || 'common',
} }
areaData.areaCode = data.areaCode areaData.areaCode = data.areaCode
} }
@ -107,12 +108,12 @@ const onSubmit = () => {
.van-checkbox__label { .van-checkbox__label {
margin-left: 0.04rem !important; margin-left: 0.04rem !important;
} }
.van-cell__title { // .van-cell__title {
// width: 0; // // width: 0;
} // }
.van-cell__value { // .van-cell__value {
// flex: none; // // flex: none;
} // }
.van-checkbox__label { .van-checkbox__label {
color: #666; color: #666;
} }

View File

@ -57,6 +57,7 @@
import api from '@/api' import api from '@/api'
import { requestPayment } from '@/utils/wx-minprogram' import { requestPayment } from '@/utils/wx-minprogram'
import type { SkuType } from '@/api/types/shop' import type { SkuType } from '@/api/types/shop'
import { showToast } from 'vant'
const model = defineModel<boolean>() const model = defineModel<boolean>()
const showAddressList = defineModel<boolean>('showAddressList') const showAddressList = defineModel<boolean>('showAddressList')
const props = defineProps<{ list: Array<any>; curAddressData: Recordable<any>; scene: 'order' | 'cart' }>() const props = defineProps<{ list: Array<any>; curAddressData: Recordable<any>; scene: 'order' | 'cart' }>()
@ -88,8 +89,11 @@ watch(model, (val) => {
// //
const updateOptionDisabled = () => { const updateOptionDisabled = () => {
Object.entries(skuData.value).forEach(([categoryName, options]) => { Object.entries(skuData.value).forEach(([categoryName, options]) => {
console.log(categoryName, options)
options.forEach((opt) => { options.forEach((opt) => {
const trialSelection = { ...curSelection.value, [categoryName]: opt.label } const trialSelection = { ...curSelection.value, [categoryName]: opt.label }
console.log('trialSelection', trialSelection)
const availableStock = skuSelectionList.value const availableStock = skuSelectionList.value
.filter(({ selection }) => .filter(({ selection }) =>
categoryOrder.value.every((name) => !trialSelection[name] || selection[name] === trialSelection[name]), categoryOrder.value.every((name) => !trialSelection[name] || selection[name] === trialSelection[name]),
@ -175,6 +179,15 @@ const onSubmit = async () => {
await api.shop.addCart.post(formData.value) await api.shop.addCart.post(formData.value)
showToast('加入购物车成功') showToast('加入购物车成功')
} else { } 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] }) 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.package = res.data.prepayId
res.data.timeStamp = String(res.data.timeStamp) res.data.timeStamp = String(res.data.timeStamp)

File diff suppressed because it is too large Load Diff

899
temp.txt Normal file
View 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>