Files
mall-merchant/src/pages/orders/OrderDetailDrawer.tsx
魔方熊成祥 bd0163af2d 1
2026-05-07 22:54:11 +08:00

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>
)
}