900 lines
28 KiB
Plaintext
900 lines
28 KiB
Plaintext
<template>
|
||
<div :class="[prefixCls]">
|
||
<section :class="`${prefixCls}__header`">
|
||
<div :class="`${prefixCls}__search`">
|
||
<van-search v-model="searchValue" show-action shape="round" placeholder="鎧乞땐데 / <20>틔츰냔" @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><3E>틔쏜띨</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: '<27>소攣瞳구새', 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: '<27>헝簡빈', plain: true },
|
||
delete: { label: '<27>뇜땐데', 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: '<27>뇜땐데', 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: '<27>소구새櫓' },
|
||
{ 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 || '넓頓<EB8493>뇹잿櫓',
|
||
desc: latest?.context || '관범頓渴櫓,헝켐懃된덤',
|
||
sub: latest?.areaName || pkg?.shippingTo,
|
||
trackNumber,
|
||
icon: 'logistics',
|
||
}
|
||
}
|
||
|
||
const firstLine: TradeOrderLineEntity | undefined = order.vvTradeOrderLineEntityList?.[0]
|
||
if (firstLine) {
|
||
return {
|
||
title: '<27>소구새櫓',
|
||
desc: firstLine.logisticsDesc || '<27>소攣瞳槨퀭硫구<E7A1AB>틔',
|
||
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(`綠瓊今<E7938A>소쐴우槨땐데 ${order.orderNo || order.id} 랙새`)
|
||
}
|
||
|
||
const handleExtendReceive = (order: EnhancedOrderItem) => {
|
||
showToast(`綠槨땐데 ${order.orderNo || order.id} <20>헝儺낀澗새`)
|
||
}
|
||
|
||
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} 돨혤句<ED98A4>헝綠瓊슥`),
|
||
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>
|