271 lines
10 KiB
TypeScript
271 lines
10 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { Card, Descriptions, Drawer, Image, Space, Spin, Table, Tag, Typography } from 'antd'
|
|
import { toast } from 'sonner'
|
|
import { merchantOrderDetail } from '@/apis/mallMerchant'
|
|
import type { OrderDetailInfo } from '@/types/mall'
|
|
import { formatUnixTime, getStatusColor, getStatusLabel } from '@/utils/mall'
|
|
|
|
interface OrderDetailDrawerProps {
|
|
open: boolean
|
|
merchantId: string
|
|
orderId?: number
|
|
refreshToken?: number
|
|
onClose: () => void
|
|
}
|
|
|
|
const normalizeSpecText = (value: unknown) => {
|
|
if (typeof value === 'string') {
|
|
return value.trim()
|
|
}
|
|
|
|
if (value === undefined || value === null) {
|
|
return ''
|
|
}
|
|
|
|
return String(value).trim()
|
|
}
|
|
|
|
const parseSkuSpecs = (value?: string) => {
|
|
if (!value?.trim()) {
|
|
return []
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(value) as unknown
|
|
if (!Array.isArray(parsed)) {
|
|
return []
|
|
}
|
|
|
|
return parsed
|
|
.map(item => {
|
|
const record = item as Record<string, unknown>
|
|
const attributeName =
|
|
normalizeSpecText(record.attributeName) || normalizeSpecText(record.name)
|
|
const valueName =
|
|
normalizeSpecText(record.valueName) || normalizeSpecText(record.value)
|
|
|
|
if (!attributeName && !valueName) {
|
|
return null
|
|
}
|
|
|
|
return { attributeName, valueName }
|
|
})
|
|
.filter((item): item is { attributeName: string; valueName: string } => Boolean(item))
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
export default function OrderDetailDrawer({
|
|
open,
|
|
merchantId,
|
|
orderId,
|
|
refreshToken,
|
|
onClose,
|
|
}: OrderDetailDrawerProps) {
|
|
const [loading, setLoading] = useState(false)
|
|
const [data, setData] = useState<OrderDetailInfo | null>(null)
|
|
|
|
useEffect(() => {
|
|
if (!open || !orderId) {
|
|
return
|
|
}
|
|
|
|
let cancelled = false
|
|
const run = async () => {
|
|
try {
|
|
setLoading(true)
|
|
const result = await merchantOrderDetail({
|
|
merchantId,
|
|
orderId,
|
|
})
|
|
|
|
if (!cancelled) {
|
|
setData(result.data)
|
|
}
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : '加载订单详情失败'
|
|
toast.error(message)
|
|
} finally {
|
|
if (!cancelled) {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
void run()
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [merchantId, open, orderId, refreshToken])
|
|
|
|
const drawerTitle = data?.orderNo ? `订单详情 ${data.orderNo}` : '订单详情'
|
|
|
|
return (
|
|
<Drawer title={drawerTitle} width={1080} open={open} onClose={onClose}>
|
|
<Spin spinning={loading}>
|
|
{!data ? null : (
|
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
|
<Card className="page-card" title="基础信息">
|
|
<Descriptions column={2} size="small">
|
|
<Descriptions.Item label="订单号">{data.orderNo}</Descriptions.Item>
|
|
<Descriptions.Item label="交易号">{data.tradeNo}</Descriptions.Item>
|
|
<Descriptions.Item label="商家名称">{data.merchantName || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label="订单状态">
|
|
<Tag color={getStatusColor(data.orderStatus)}>{getStatusLabel(data.orderStatus)}</Tag>
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="售后状态">
|
|
<Tag color={getStatusColor(data.afterSaleStatus)}>
|
|
{getStatusLabel(data.afterSaleStatus)}
|
|
</Tag>
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="商品金额">{data.goodsAmount}</Descriptions.Item>
|
|
<Descriptions.Item label="支付金额">{data.payAmount}</Descriptions.Item>
|
|
<Descriptions.Item label="运费">{data.freightAmount}</Descriptions.Item>
|
|
<Descriptions.Item label="优惠金额">{data.discountAmount}</Descriptions.Item>
|
|
<Descriptions.Item label="币种">{data.currency || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label="支付时间">{formatUnixTime(data.payTime)}</Descriptions.Item>
|
|
<Descriptions.Item label="发货时间">{formatUnixTime(data.shipTime)}</Descriptions.Item>
|
|
<Descriptions.Item label="完成时间">{formatUnixTime(data.finishTime)}</Descriptions.Item>
|
|
<Descriptions.Item label="取消时间">{formatUnixTime(data.cancelTime)}</Descriptions.Item>
|
|
<Descriptions.Item label="自动确认">{formatUnixTime(data.autoConfirmAt)}</Descriptions.Item>
|
|
<Descriptions.Item label="支付渠道">{data.payChannel || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label="创建时间">{formatUnixTime(data.createdAt)}</Descriptions.Item>
|
|
<Descriptions.Item label="更新时间">{formatUnixTime(data.updatedAt)}</Descriptions.Item>
|
|
</Descriptions>
|
|
</Card>
|
|
|
|
<Card className="page-card" title="收货与物流">
|
|
<Descriptions column={2} size="small">
|
|
<Descriptions.Item label="收货人">{data.receiverName}</Descriptions.Item>
|
|
<Descriptions.Item label="手机号">{data.receiverMobile}</Descriptions.Item>
|
|
<Descriptions.Item label="国家区号">{data.receiverCountryCode || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label="省份">{data.receiverProvince || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label="城市">{data.receiverCity || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label="区县">{data.receiverDistrict || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label="完整地址" span={2}>
|
|
{[data.receiverProvince, data.receiverCity, data.receiverDistrict, data.receiverAddress]
|
|
.filter(Boolean)
|
|
.join(' ')}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="邮编">{data.receiverZipCode || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label="物流公司">{data.deliveryCompany || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label="运单号">{data.deliveryNo || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label="商家备注" span={2}>
|
|
{data.sellerRemark || '-'}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="买家备注" span={2}>
|
|
{data.buyerRemark || '-'}
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
</Card>
|
|
|
|
<Card className="page-card" title="商品明细">
|
|
<Table
|
|
rowKey={record => String(record.id)}
|
|
pagination={false}
|
|
scroll={{ x: 960 }}
|
|
dataSource={data.items}
|
|
columns={[
|
|
{
|
|
title: '商品',
|
|
dataIndex: 'spuName',
|
|
width: 280,
|
|
render: (_, record) => (
|
|
<div className="order-detail-product">
|
|
{record.coverUrl ? (
|
|
<Image
|
|
src={record.coverUrl}
|
|
alt={record.spuName}
|
|
width={52}
|
|
height={52}
|
|
className="order-detail-product__image"
|
|
preview={{ src: record.coverUrl }}
|
|
/>
|
|
) : (
|
|
<div className="order-detail-product__placeholder">无图</div>
|
|
)}
|
|
<Space direction="vertical" size={2} className="order-detail-product__content">
|
|
<Typography.Text
|
|
strong
|
|
className="order-detail-product__title"
|
|
title={record.spuName}
|
|
>
|
|
{record.spuName}
|
|
</Typography.Text>
|
|
<Typography.Text type="secondary" ellipsis={{ tooltip: record.skuName }}>
|
|
{record.skuName || '-'}
|
|
</Typography.Text>
|
|
</Space>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
title: '规格',
|
|
dataIndex: 'skuSpecJson',
|
|
width: 220,
|
|
render: (_, record) => {
|
|
const specs = parseSkuSpecs(record.skuSpecJson)
|
|
|
|
if (!specs.length) {
|
|
return record.skuName || '-'
|
|
}
|
|
|
|
return (
|
|
<Space size={[4, 4]} wrap>
|
|
{specs.map(spec => (
|
|
<Tag key={`${spec.attributeName}-${spec.valueName}`}>
|
|
{spec.attributeName ? `${spec.attributeName}: ` : ''}
|
|
{spec.valueName}
|
|
</Tag>
|
|
))}
|
|
</Space>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
title: '数量',
|
|
dataIndex: 'buyNum',
|
|
width: 90,
|
|
},
|
|
{
|
|
title: '单价',
|
|
dataIndex: 'salePrice',
|
|
width: 100,
|
|
},
|
|
{
|
|
title: '原价',
|
|
dataIndex: 'originPrice',
|
|
width: 100,
|
|
},
|
|
{
|
|
title: '金额',
|
|
dataIndex: 'itemAmount',
|
|
width: 120,
|
|
},
|
|
{
|
|
title: '优惠',
|
|
dataIndex: 'discountAmount',
|
|
width: 100,
|
|
},
|
|
{
|
|
title: '退款',
|
|
dataIndex: 'refundAmount',
|
|
width: 100,
|
|
},
|
|
{
|
|
title: '售后状态',
|
|
dataIndex: 'afterSaleStatus',
|
|
width: 120,
|
|
render: value => <Tag color={getStatusColor(value)}>{getStatusLabel(value)}</Tag>,
|
|
},
|
|
]}
|
|
/>
|
|
</Card>
|
|
</Space>
|
|
)}
|
|
</Spin>
|
|
</Drawer>
|
|
)
|
|
}
|