461 lines
16 KiB
Vue
461 lines
16 KiB
Vue
|
|
<script setup lang="ts">
|
||
|
|
import dayjs from 'dayjs'
|
||
|
|
import { api } from '@/utils/request'
|
||
|
|
import {
|
||
|
|
useOrderStore,
|
||
|
|
type OrderRecord,
|
||
|
|
type OrderDetail,
|
||
|
|
type RawOrderDetail,
|
||
|
|
} from '@/stores/order'
|
||
|
|
import CascadeFilter from '@/components/CascadeFilter.vue'
|
||
|
|
import {
|
||
|
|
SearchOutlined,
|
||
|
|
ReloadOutlined,
|
||
|
|
EyeOutlined,
|
||
|
|
CopyOutlined,
|
||
|
|
} from '@ant-design/icons-vue'
|
||
|
|
|
||
|
|
const store = useOrderStore()
|
||
|
|
|
||
|
|
// Detail drawer
|
||
|
|
const drawerVisible = ref(false)
|
||
|
|
const drawerLoading = ref(false)
|
||
|
|
const orderDetail = ref<OrderDetail | null>(null)
|
||
|
|
const rawDetail = ref<RawOrderDetail | null>(null)
|
||
|
|
let detailRequestId = 0
|
||
|
|
|
||
|
|
const orderStatusMap: Record<number, { text: string; color: string }> = {
|
||
|
|
1: { text: '待付款', color: 'orange' },
|
||
|
|
2: { text: '支付失败', color: 'red' },
|
||
|
|
3: { text: '已支付', color: 'blue' },
|
||
|
|
4: { text: '待发货', color: 'cyan' },
|
||
|
|
5: { text: '已发货', color: 'geekblue' },
|
||
|
|
6: { text: '取消申请中', color: 'volcano' },
|
||
|
|
7: { text: '已取消', color: 'default' },
|
||
|
|
8: { text: '已完成', color: 'green' },
|
||
|
|
9: { text: '发货前取消', color: 'default' },
|
||
|
|
}
|
||
|
|
|
||
|
|
const columns = [
|
||
|
|
{ title: 'ID', dataIndex: 'id', width: 80, fixed: 'left' as const },
|
||
|
|
{ title: '公司', key: 'company', width: 120 },
|
||
|
|
{ title: '平台', key: 'platform', width: 100 },
|
||
|
|
{ title: '店铺', key: 'store', width: 140 },
|
||
|
|
{ title: '平台订单ID', key: 'platform_order_id', width: 180, ellipsis: true },
|
||
|
|
{ title: '订单金额', key: 'total_amount', width: 120 },
|
||
|
|
{ title: '已支付', key: 'total_paid', width: 120 },
|
||
|
|
{ title: '总折扣', key: 'total_discount', width: 120 },
|
||
|
|
{ title: '状态', key: 'status', width: 110 },
|
||
|
|
{ title: '创建时间', key: 'created_date', width: 160 },
|
||
|
|
{ title: '付款时间', key: 'paid_date', width: 160 },
|
||
|
|
{ title: '操作', key: 'action', width: 100, fixed: 'right' as const },
|
||
|
|
]
|
||
|
|
|
||
|
|
const orderItemColumns = [
|
||
|
|
{ title: 'SKU', dataIndex: 'product_sku', width: 140, ellipsis: true },
|
||
|
|
{ title: '平台商品ID', dataIndex: 'platform_product_id', width: 160, ellipsis: true },
|
||
|
|
{ title: '数量', dataIndex: 'quantity', width: 70 },
|
||
|
|
{ title: '单价', key: 'unit_price', width: 100 },
|
||
|
|
{ title: '优惠', key: 'item_discount', width: 100 },
|
||
|
|
{ title: '小计', key: 'item_total', width: 100 },
|
||
|
|
]
|
||
|
|
|
||
|
|
// RangePicker dayjs 桥接(undefined 替代 null,匹配 antdv 类型要求)
|
||
|
|
const createdDateRange = computed({
|
||
|
|
get: () => {
|
||
|
|
if (!store.filters.created_date_range) return undefined
|
||
|
|
return store.filters.created_date_range.map((d) => dayjs(d)) as [
|
||
|
|
dayjs.Dayjs,
|
||
|
|
dayjs.Dayjs,
|
||
|
|
]
|
||
|
|
},
|
||
|
|
set: (val) => {
|
||
|
|
store.filters.created_date_range = val
|
||
|
|
? [val[0].format('YYYY-MM-DD'), val[1].format('YYYY-MM-DD')]
|
||
|
|
: null
|
||
|
|
},
|
||
|
|
})
|
||
|
|
|
||
|
|
const paidDateRange = computed({
|
||
|
|
get: () => {
|
||
|
|
if (!store.filters.paid_date_range) return undefined
|
||
|
|
return store.filters.paid_date_range.map((d) => dayjs(d)) as [
|
||
|
|
dayjs.Dayjs,
|
||
|
|
dayjs.Dayjs,
|
||
|
|
]
|
||
|
|
},
|
||
|
|
set: (val) => {
|
||
|
|
store.filters.paid_date_range = val
|
||
|
|
? [val[0].format('YYYY-MM-DD'), val[1].format('YYYY-MM-DD')]
|
||
|
|
: null
|
||
|
|
},
|
||
|
|
})
|
||
|
|
|
||
|
|
onMounted(() => {
|
||
|
|
store.loadLookups()
|
||
|
|
store.fetchOrders()
|
||
|
|
})
|
||
|
|
|
||
|
|
function handleSearch() {
|
||
|
|
store.pagination.page = 1
|
||
|
|
store.fetchOrders()
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleReset() {
|
||
|
|
store.resetFilters()
|
||
|
|
store.fetchOrders()
|
||
|
|
}
|
||
|
|
|
||
|
|
function handlePageChange(page: number, pageSize: number) {
|
||
|
|
store.pagination.page = page
|
||
|
|
store.pagination.per_page = pageSize
|
||
|
|
store.fetchOrders()
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatAmount(val: string | null | undefined) {
|
||
|
|
if (!val || val === '0' || val === '0.00') return '-'
|
||
|
|
return val
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatTime(time: string | null) {
|
||
|
|
if (!time) return '-'
|
||
|
|
return time.replace('T', ' ').substring(0, 16)
|
||
|
|
}
|
||
|
|
|
||
|
|
async function handleCopyOrderId(platformOrderId: string) {
|
||
|
|
try {
|
||
|
|
await navigator.clipboard.writeText(platformOrderId)
|
||
|
|
message.success('已复制平台订单ID')
|
||
|
|
} catch {
|
||
|
|
message.error('复制失败')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function handleViewDetail(record: { id: number }) {
|
||
|
|
const currentRequestId = ++detailRequestId
|
||
|
|
drawerVisible.value = true
|
||
|
|
drawerLoading.value = true
|
||
|
|
orderDetail.value = null
|
||
|
|
rawDetail.value = null
|
||
|
|
|
||
|
|
const [normalResult, rawResult] = await Promise.allSettled([
|
||
|
|
api.get<OrderDetail>(`/api/v1/orders/${record.id}`),
|
||
|
|
api.get<RawOrderDetail>(`/api/v1/raw/orders/${record.id}`),
|
||
|
|
])
|
||
|
|
|
||
|
|
// 竞态保护:若用户已切换到另一订单详情,丢弃过期响应
|
||
|
|
if (currentRequestId !== detailRequestId) return
|
||
|
|
|
||
|
|
if (normalResult.status === 'fulfilled') {
|
||
|
|
orderDetail.value = normalResult.value
|
||
|
|
} else {
|
||
|
|
message.error('获取订单详情失败')
|
||
|
|
}
|
||
|
|
|
||
|
|
if (rawResult.status === 'fulfilled') {
|
||
|
|
rawDetail.value = rawResult.value
|
||
|
|
} else {
|
||
|
|
console.warn('加载原始数据(raw)失败', rawResult.reason)
|
||
|
|
}
|
||
|
|
|
||
|
|
drawerLoading.value = false
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<template>
|
||
|
|
<div>
|
||
|
|
<h2 class="text-xl font-semibold mb-4">订单管理</h2>
|
||
|
|
|
||
|
|
<!-- Filter area -->
|
||
|
|
<a-card class="mb-4">
|
||
|
|
<a-form layout="inline" @submit.prevent="handleSearch">
|
||
|
|
<a-form-item>
|
||
|
|
<CascadeFilter v-model="store.cascadeValue" />
|
||
|
|
</a-form-item>
|
||
|
|
<a-form-item label="订单状态">
|
||
|
|
<a-select
|
||
|
|
v-model:value="store.filters.order_status_id"
|
||
|
|
placeholder="全部"
|
||
|
|
allow-clear
|
||
|
|
style="width: 130px"
|
||
|
|
>
|
||
|
|
<a-select-option
|
||
|
|
v-for="(item, key) in orderStatusMap"
|
||
|
|
:key="key"
|
||
|
|
:value="Number(key)"
|
||
|
|
>
|
||
|
|
{{ item.text }}
|
||
|
|
</a-select-option>
|
||
|
|
</a-select>
|
||
|
|
</a-form-item>
|
||
|
|
<a-form-item label="平台订单ID">
|
||
|
|
<a-input
|
||
|
|
v-model:value="store.filters.platform_order_id"
|
||
|
|
placeholder="精确搜索"
|
||
|
|
allow-clear
|
||
|
|
@press-enter="handleSearch"
|
||
|
|
/>
|
||
|
|
</a-form-item>
|
||
|
|
<a-form-item label="创建时间">
|
||
|
|
<a-range-picker
|
||
|
|
v-model:value="createdDateRange"
|
||
|
|
format="YYYY-MM-DD"
|
||
|
|
/>
|
||
|
|
</a-form-item>
|
||
|
|
<a-form-item label="付款时间">
|
||
|
|
<a-range-picker
|
||
|
|
v-model:value="paidDateRange"
|
||
|
|
format="YYYY-MM-DD"
|
||
|
|
/>
|
||
|
|
</a-form-item>
|
||
|
|
<a-form-item>
|
||
|
|
<a-space>
|
||
|
|
<a-button type="primary" html-type="submit">
|
||
|
|
<template #icon><SearchOutlined /></template>
|
||
|
|
搜索
|
||
|
|
</a-button>
|
||
|
|
<a-button @click="handleReset">
|
||
|
|
<template #icon><ReloadOutlined /></template>
|
||
|
|
重置
|
||
|
|
</a-button>
|
||
|
|
</a-space>
|
||
|
|
</a-form-item>
|
||
|
|
</a-form>
|
||
|
|
</a-card>
|
||
|
|
|
||
|
|
<!-- Table -->
|
||
|
|
<a-card>
|
||
|
|
<a-table
|
||
|
|
:columns="columns"
|
||
|
|
:data-source="store.orders"
|
||
|
|
:loading="store.loading"
|
||
|
|
:pagination="false"
|
||
|
|
:scroll="{ x: 1360 }"
|
||
|
|
row-key="id"
|
||
|
|
>
|
||
|
|
<template #bodyCell="{ column, record }">
|
||
|
|
<template v-if="column.key === 'company'">
|
||
|
|
{{ store.companyMap.get(record.company_id) || record.company_id }}
|
||
|
|
</template>
|
||
|
|
<template v-else-if="column.key === 'platform'">
|
||
|
|
{{ store.platformMap.get(record.platform_id) || record.platform_id }}
|
||
|
|
</template>
|
||
|
|
<template v-else-if="column.key === 'store'">
|
||
|
|
{{ store.storeMap.get(record.store_id) || record.store_id }}
|
||
|
|
</template>
|
||
|
|
<template v-else-if="column.key === 'platform_order_id'">
|
||
|
|
{{ record.platform_order_id }}
|
||
|
|
</template>
|
||
|
|
<template v-else-if="column.key === 'total_amount'">
|
||
|
|
{{ formatAmount(record.total_amount) }}
|
||
|
|
</template>
|
||
|
|
<template v-else-if="column.key === 'total_paid'">
|
||
|
|
{{ formatAmount(record.total_paid) }}
|
||
|
|
</template>
|
||
|
|
<template v-else-if="column.key === 'total_discount'">
|
||
|
|
{{ formatAmount(record.total_discount) }}
|
||
|
|
</template>
|
||
|
|
<template v-else-if="column.key === 'status'">
|
||
|
|
<a-tag :color="orderStatusMap[record.order_status_id]?.color || 'default'">
|
||
|
|
{{ orderStatusMap[record.order_status_id]?.text || `状态 ${record.order_status_id}` }}
|
||
|
|
</a-tag>
|
||
|
|
</template>
|
||
|
|
<template v-else-if="column.key === 'created_date'">
|
||
|
|
{{ formatTime(record.created_date) }}
|
||
|
|
</template>
|
||
|
|
<template v-else-if="column.key === 'paid_date'">
|
||
|
|
{{ formatTime(record.paid_date) }}
|
||
|
|
</template>
|
||
|
|
<template v-else-if="column.key === 'action'">
|
||
|
|
<a-button
|
||
|
|
type="link"
|
||
|
|
size="small"
|
||
|
|
@click="handleViewDetail(record as OrderRecord)"
|
||
|
|
>
|
||
|
|
<template #icon><EyeOutlined /></template>
|
||
|
|
查看
|
||
|
|
</a-button>
|
||
|
|
</template>
|
||
|
|
</template>
|
||
|
|
</a-table>
|
||
|
|
|
||
|
|
<div class="mt-4 flex justify-end">
|
||
|
|
<a-pagination
|
||
|
|
:current="store.pagination.page"
|
||
|
|
:page-size="store.pagination.per_page"
|
||
|
|
:total="store.pagination.total"
|
||
|
|
show-size-changer
|
||
|
|
show-quick-jumper
|
||
|
|
:show-total="(total: number) => `共 ${total} 条`"
|
||
|
|
@change="handlePageChange"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</a-card>
|
||
|
|
|
||
|
|
<!-- Detail drawer -->
|
||
|
|
<a-drawer
|
||
|
|
title="订单详情"
|
||
|
|
:open="drawerVisible"
|
||
|
|
:width="720"
|
||
|
|
@close="drawerVisible = false"
|
||
|
|
>
|
||
|
|
<a-spin :spinning="drawerLoading">
|
||
|
|
<template v-if="orderDetail">
|
||
|
|
<!-- 基本信息 -->
|
||
|
|
<a-descriptions title="基本信息" :column="2" bordered class="mb-4">
|
||
|
|
<a-descriptions-item label="ID">{{ orderDetail.id }}</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="状态">
|
||
|
|
<a-tag
|
||
|
|
:color="orderStatusMap[orderDetail.order_status_id]?.color || 'default'"
|
||
|
|
>
|
||
|
|
{{
|
||
|
|
orderStatusMap[orderDetail.order_status_id]?.text
|
||
|
|
|| `状态 ${orderDetail.order_status_id}`
|
||
|
|
}}
|
||
|
|
</a-tag>
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="平台订单ID" :span="2">
|
||
|
|
{{ orderDetail.platform_order_id }}
|
||
|
|
<CopyOutlined
|
||
|
|
class="ml-2 cursor-pointer text-blue-500"
|
||
|
|
@click="handleCopyOrderId(orderDetail.platform_order_id)"
|
||
|
|
/>
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="公司">
|
||
|
|
{{
|
||
|
|
store.companyMap.get(orderDetail.company_id) || orderDetail.company_id
|
||
|
|
}}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="平台">
|
||
|
|
{{
|
||
|
|
store.platformMap.get(orderDetail.platform_id)
|
||
|
|
|| orderDetail.platform_id
|
||
|
|
}}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="店铺">
|
||
|
|
{{
|
||
|
|
store.storeMap.get(orderDetail.store_id) || orderDetail.store_id
|
||
|
|
}}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="买家ID">
|
||
|
|
{{ orderDetail.buyer_user_id || '-' }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="支付方式">
|
||
|
|
{{ orderDetail.payment_method_id }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="预售">
|
||
|
|
{{ orderDetail.presale ? '是' : '否' }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
</a-descriptions>
|
||
|
|
|
||
|
|
<!-- 金额信息 -->
|
||
|
|
<a-descriptions title="金额信息" :column="2" bordered class="mb-4">
|
||
|
|
<a-descriptions-item label="订单总金额">
|
||
|
|
{{ formatAmount(orderDetail.total_amount) }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="已支付金额">
|
||
|
|
{{ formatAmount(orderDetail.total_paid) }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="总折扣">
|
||
|
|
{{ formatAmount(orderDetail.total_discount) }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="实收金额">
|
||
|
|
{{ formatAmount(orderDetail.total_received) }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="运费">
|
||
|
|
{{ formatAmount(orderDetail.freight_fee) }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="税费">
|
||
|
|
{{ formatAmount(orderDetail.tax_fee) }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="优惠金额">
|
||
|
|
{{ formatAmount(orderDetail.discount_fee) }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="佣金">
|
||
|
|
{{ formatAmount(orderDetail.commission_fee) }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="优惠券金额">
|
||
|
|
{{ formatAmount(orderDetail.coupon_amount) }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="代金券金额">
|
||
|
|
{{ formatAmount(orderDetail.voucher_amount) }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
</a-descriptions>
|
||
|
|
|
||
|
|
<!-- 时间与地址 -->
|
||
|
|
<a-descriptions title="时间与地址" :column="2" bordered class="mb-4">
|
||
|
|
<a-descriptions-item label="创建时间">
|
||
|
|
{{ formatTime(orderDetail.created_date) }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="更新时间">
|
||
|
|
{{ formatTime(orderDetail.updated_date) }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="付款时间">
|
||
|
|
{{ formatTime(orderDetail.paid_date) }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="发货时间">
|
||
|
|
{{ formatTime(orderDetail.shipping_date) }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="国家">
|
||
|
|
{{ orderDetail.country || '-' }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="省份">
|
||
|
|
{{ orderDetail.province || '-' }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="城市">
|
||
|
|
{{ orderDetail.city || '-' }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="邮编">
|
||
|
|
{{ orderDetail.zipcode || '-' }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
</a-descriptions>
|
||
|
|
|
||
|
|
<!-- 订单子项 -->
|
||
|
|
<h4 class="text-base font-medium mb-2">订单子项</h4>
|
||
|
|
<a-table
|
||
|
|
:columns="orderItemColumns"
|
||
|
|
:data-source="orderDetail.order_items"
|
||
|
|
:pagination="false"
|
||
|
|
size="small"
|
||
|
|
row-key="id"
|
||
|
|
class="mb-4"
|
||
|
|
>
|
||
|
|
<template #bodyCell="{ column, record }">
|
||
|
|
<template v-if="column.key === 'unit_price'">
|
||
|
|
{{ formatAmount(record.unit_price) }}
|
||
|
|
</template>
|
||
|
|
<template v-else-if="column.key === 'item_discount'">
|
||
|
|
{{ formatAmount(record.discount) }}
|
||
|
|
</template>
|
||
|
|
<template v-else-if="column.key === 'item_total'">
|
||
|
|
{{ formatAmount(record.total) }}
|
||
|
|
</template>
|
||
|
|
</template>
|
||
|
|
</a-table>
|
||
|
|
|
||
|
|
<!-- ext JSON -->
|
||
|
|
<a-descriptions title="扩展数据 (ext)" :column="1" bordered class="mb-4">
|
||
|
|
<a-descriptions-item>
|
||
|
|
<pre v-if="orderDetail.ext" class="m-0 text-xs max-h-80 overflow-auto">{{
|
||
|
|
JSON.stringify(orderDetail.ext, null, 2)
|
||
|
|
}}</pre>
|
||
|
|
<span v-else class="text-gray-400">暂无数据</span>
|
||
|
|
</a-descriptions-item>
|
||
|
|
</a-descriptions>
|
||
|
|
|
||
|
|
<!-- raw JSON -->
|
||
|
|
<a-descriptions title="原始数据 (raw)" :column="1" bordered>
|
||
|
|
<a-descriptions-item>
|
||
|
|
<pre
|
||
|
|
v-if="rawDetail?.raw"
|
||
|
|
class="m-0 text-xs max-h-80 overflow-auto"
|
||
|
|
>{{ JSON.stringify(rawDetail.raw, null, 2) }}</pre>
|
||
|
|
<span v-else class="text-gray-400">暂无数据</span>
|
||
|
|
</a-descriptions-item>
|
||
|
|
</a-descriptions>
|
||
|
|
</template>
|
||
|
|
</a-spin>
|
||
|
|
</a-drawer>
|
||
|
|
</div>
|
||
|
|
</template>
|