- 增加 .env.example 与 godotenv 加载,start.sh 自动读环境变量 - 重写发给源头的开放接口对接文档 - 订单页支持创建测试订单(可直接已支付并复制店铺订单号)
403 lines
12 KiB
TypeScript
403 lines
12 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react'
|
||
import {
|
||
Button,
|
||
Checkbox,
|
||
Form,
|
||
Input,
|
||
Modal,
|
||
Select,
|
||
Space,
|
||
Table,
|
||
Tag,
|
||
Typography,
|
||
message,
|
||
} from 'antd'
|
||
import { PlusOutlined, ReloadOutlined, CopyOutlined } from '@ant-design/icons'
|
||
import type { ColumnsType } from 'antd/es/table'
|
||
import dayjs from 'dayjs'
|
||
import { orderApi, skinApi } from '../api'
|
||
import { useAuth } from '../store/auth'
|
||
import type { Order, Skin } from '../types'
|
||
|
||
const statusMap: Record<string, { color: string; text: string }> = {
|
||
pending: { color: 'orange', text: '待支付' },
|
||
paid: { color: 'blue', text: '已支付' },
|
||
delivering: { color: 'cyan', text: '发货中' },
|
||
delivered: { color: 'green', text: '已交付' },
|
||
ship_failed: { color: 'red', text: '发货失败' },
|
||
cancelled: { color: 'default', text: '已取消' },
|
||
}
|
||
|
||
export default function Orders() {
|
||
const { isAdmin } = useAuth()
|
||
const [list, setList] = useState<Order[]>([])
|
||
const [total, setTotal] = useState(0)
|
||
const [page, setPage] = useState(1)
|
||
const [size, setSize] = useState(10)
|
||
const [status, setStatus] = useState<string | undefined>()
|
||
const [loading, setLoading] = useState(false)
|
||
const [createOpen, setCreateOpen] = useState(false)
|
||
const [skins, setSkins] = useState<Skin[]>([])
|
||
const [creating, setCreating] = useState(false)
|
||
const [createdOrder, setCreatedOrder] = useState<Order | null>(null)
|
||
const [form] = Form.useForm()
|
||
|
||
const load = useCallback(async () => {
|
||
setLoading(true)
|
||
try {
|
||
const data = await orderApi.list({ page, size, status })
|
||
setList(data.list || [])
|
||
setTotal(data.total)
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '加载失败')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [page, size, status])
|
||
|
||
useEffect(() => {
|
||
load()
|
||
}, [load])
|
||
|
||
const openCreate = async () => {
|
||
form.resetFields()
|
||
form.setFieldsValue({
|
||
buyer_name: '测试买家',
|
||
mark_paid: true,
|
||
remark: '联调测试订单',
|
||
})
|
||
setCreatedOrder(null)
|
||
setCreateOpen(true)
|
||
try {
|
||
const data = await skinApi.list({ page: 1, size: 100, status: 1 })
|
||
setSkins(data.list || [])
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '加载商品失败')
|
||
}
|
||
}
|
||
|
||
const onCreate = async () => {
|
||
const values = await form.validateFields()
|
||
setCreating(true)
|
||
try {
|
||
const order = await orderApi.create({
|
||
skin_id: values.skin_id,
|
||
buyer_name: values.buyer_name,
|
||
remark: values.remark,
|
||
status: isAdmin && values.mark_paid ? 'paid' : undefined,
|
||
})
|
||
setCreatedOrder(order)
|
||
message.success(`测试订单已创建:${order.order_no}`)
|
||
load()
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '创建失败')
|
||
} finally {
|
||
setCreating(false)
|
||
}
|
||
}
|
||
|
||
const copyText = async (text: string, tip = '已复制') => {
|
||
try {
|
||
await navigator.clipboard.writeText(text)
|
||
message.success(tip)
|
||
} catch {
|
||
message.error('复制失败,请手动选择')
|
||
}
|
||
}
|
||
|
||
const changeStatus = async (id: number, next: string) => {
|
||
try {
|
||
await orderApi.updateStatus(id, next)
|
||
message.success('状态已更新')
|
||
load()
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '更新失败')
|
||
}
|
||
}
|
||
|
||
const columns: ColumnsType<Order> = [
|
||
{
|
||
title: '店铺订单号',
|
||
dataIndex: 'order_no',
|
||
width: 210,
|
||
ellipsis: true,
|
||
render: (v: string) => (
|
||
<Space size={4}>
|
||
<Typography.Text copyable={{ text: v }} style={{ maxWidth: 160 }} ellipsis>
|
||
{v}
|
||
</Typography.Text>
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '皮肤',
|
||
dataIndex: ['skin', 'name'],
|
||
width: 140,
|
||
ellipsis: true,
|
||
render: (_, r) => r.skin?.name || `#${r.skin_id}`,
|
||
},
|
||
{
|
||
title: 'SKU',
|
||
width: 150,
|
||
ellipsis: true,
|
||
render: (_, r) =>
|
||
r.skin?.sku ? (
|
||
<Typography.Text code copyable={{ text: r.skin.sku }}>
|
||
{r.skin.sku}
|
||
</Typography.Text>
|
||
) : (
|
||
'-'
|
||
),
|
||
},
|
||
{
|
||
title: '分销商',
|
||
dataIndex: ['distributor', 'nickname'],
|
||
width: 100,
|
||
ellipsis: true,
|
||
render: (_, r) => r.distributor?.nickname || r.distributor?.username || `#${r.distributor_id}`,
|
||
},
|
||
{ title: '买家', dataIndex: 'buyer_name', width: 90, ellipsis: true },
|
||
{
|
||
title: '金额',
|
||
dataIndex: 'amount',
|
||
width: 90,
|
||
render: (v: number) => `¥${Number(v ?? 0).toFixed(2)}`,
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'status',
|
||
width: 100,
|
||
render: (v: string) => {
|
||
const s = statusMap[v] || { color: 'default', text: v }
|
||
return <Tag color={s.color}>{s.text}</Tag>
|
||
},
|
||
},
|
||
{
|
||
title: '上游单号',
|
||
dataIndex: 'provider_order_no',
|
||
width: 120,
|
||
ellipsis: true,
|
||
render: (v?: string) => v || '-',
|
||
},
|
||
{
|
||
title: '时间',
|
||
dataIndex: 'created_at',
|
||
width: 160,
|
||
render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm'),
|
||
},
|
||
]
|
||
|
||
if (isAdmin) {
|
||
columns.push({
|
||
title: '操作',
|
||
key: 'action',
|
||
width: 200,
|
||
fixed: 'right',
|
||
render: (_, record) => (
|
||
<Space size={0}>
|
||
{record.status === 'pending' && (
|
||
<>
|
||
<Button type="link" size="small" onClick={() => changeStatus(record.id, 'paid')}>
|
||
标记已付
|
||
</Button>
|
||
<Button type="link" size="small" danger onClick={() => changeStatus(record.id, 'cancelled')}>
|
||
取消
|
||
</Button>
|
||
</>
|
||
)}
|
||
{(record.status === 'paid' ||
|
||
record.status === 'ship_failed' ||
|
||
record.status === 'delivering') && (
|
||
<Button type="link" size="small" onClick={() => changeStatus(record.id, 'delivered')}>
|
||
标记交付
|
||
</Button>
|
||
)}
|
||
</Space>
|
||
),
|
||
})
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
|
||
<div>
|
||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||
订单管理
|
||
</Typography.Title>
|
||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||
店铺订单号 order_no 给源头查询发货;已支付订单 can_ship=true
|
||
</Typography.Text>
|
||
</div>
|
||
<Space>
|
||
<Select
|
||
allowClear
|
||
placeholder="订单状态"
|
||
style={{ width: 140 }}
|
||
value={status}
|
||
onChange={(v) => {
|
||
setPage(1)
|
||
setStatus(v)
|
||
}}
|
||
options={[
|
||
{ value: 'pending', label: '待支付' },
|
||
{ value: 'paid', label: '已支付' },
|
||
{ value: 'delivering', label: '发货中' },
|
||
{ value: 'delivered', label: '已交付' },
|
||
{ value: 'ship_failed', label: '发货失败' },
|
||
{ value: 'cancelled', label: '已取消' },
|
||
]}
|
||
/>
|
||
<Button icon={<ReloadOutlined />} onClick={load}>
|
||
刷新
|
||
</Button>
|
||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||
创建测试订单
|
||
</Button>
|
||
</Space>
|
||
</Space>
|
||
|
||
<Table
|
||
rowKey="id"
|
||
loading={loading}
|
||
columns={columns}
|
||
dataSource={list}
|
||
tableLayout="fixed"
|
||
scroll={{ x: 1200 }}
|
||
pagination={{
|
||
current: page,
|
||
pageSize: size,
|
||
total,
|
||
showSizeChanger: true,
|
||
showTotal: (t) => `共 ${t} 条`,
|
||
onChange: (p, s) => {
|
||
setPage(p)
|
||
setSize(s)
|
||
},
|
||
}}
|
||
/>
|
||
|
||
<Modal
|
||
title="创建测试订单(联调用)"
|
||
open={createOpen}
|
||
onCancel={() => setCreateOpen(false)}
|
||
footer={
|
||
createdOrder
|
||
? [
|
||
<Button key="close" onClick={() => setCreateOpen(false)}>
|
||
关闭
|
||
</Button>,
|
||
<Button
|
||
key="again"
|
||
type="primary"
|
||
onClick={() => {
|
||
setCreatedOrder(null)
|
||
form.setFieldsValue({ mark_paid: true })
|
||
}}
|
||
>
|
||
再下一单
|
||
</Button>,
|
||
]
|
||
: [
|
||
<Button key="cancel" onClick={() => setCreateOpen(false)}>
|
||
取消
|
||
</Button>,
|
||
<Button key="ok" type="primary" loading={creating} onClick={onCreate}>
|
||
创建
|
||
</Button>,
|
||
]
|
||
}
|
||
destroyOnClose
|
||
width={560}
|
||
>
|
||
{createdOrder ? (
|
||
<div>
|
||
<Typography.Paragraph>
|
||
订单已创建。把下面的 <Typography.Text strong>店铺订单号</Typography.Text>{' '}
|
||
发给源头,或用开放接口查询:
|
||
</Typography.Paragraph>
|
||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||
<div
|
||
style={{
|
||
padding: 16,
|
||
background: '#f6ffed',
|
||
border: '1px solid #b7eb8f',
|
||
borderRadius: 8,
|
||
}}
|
||
>
|
||
<div style={{ marginBottom: 8, color: '#666' }}>店铺订单号 order_no</div>
|
||
<Space>
|
||
<Typography.Title level={4} style={{ margin: 0 }} copyable>
|
||
{createdOrder.order_no}
|
||
</Typography.Title>
|
||
<Button
|
||
icon={<CopyOutlined />}
|
||
onClick={() => copyText(createdOrder.order_no, '订单号已复制')}
|
||
>
|
||
复制
|
||
</Button>
|
||
</Space>
|
||
</div>
|
||
<div>
|
||
<div>商品:{createdOrder.skin?.name || `#${createdOrder.skin_id}`}</div>
|
||
<div>
|
||
SKU:
|
||
<Typography.Text code copyable>
|
||
{createdOrder.skin?.sku || '-'}
|
||
</Typography.Text>
|
||
</div>
|
||
<div>
|
||
状态:
|
||
<Tag color={statusMap[createdOrder.status]?.color}>
|
||
{statusMap[createdOrder.status]?.text || createdOrder.status}
|
||
</Tag>
|
||
{createdOrder.status === 'paid' && (
|
||
<Typography.Text type="success">(可发货 can_ship=true)</Typography.Text>
|
||
)}
|
||
</div>
|
||
<div>买家:{createdOrder.buyer_name}</div>
|
||
</div>
|
||
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, fontSize: 12 }}>
|
||
源头查询:GET /api/open/v1/orders/{createdOrder.order_no}
|
||
<br />
|
||
需带 X-Api-Key / X-Timestamp / X-Nonce / X-Sign
|
||
</Typography.Paragraph>
|
||
</Space>
|
||
</div>
|
||
) : (
|
||
<Form form={form} layout="vertical" style={{ marginTop: 8 }}>
|
||
<Form.Item
|
||
name="skin_id"
|
||
label="商品皮肤"
|
||
rules={[{ required: true, message: '请选择商品' }]}
|
||
>
|
||
<Select
|
||
showSearch
|
||
optionFilterProp="label"
|
||
placeholder="选择要发货的皮肤"
|
||
options={skins.map((s) => ({
|
||
value: s.id,
|
||
label: `${s.name}(${s.sku})`,
|
||
}))}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item name="buyer_name" label="买家名称">
|
||
<Input placeholder="测试买家" />
|
||
</Form.Item>
|
||
<Form.Item name="remark" label="备注">
|
||
<Input.TextArea rows={2} placeholder="联调说明" />
|
||
</Form.Item>
|
||
{isAdmin && (
|
||
<Form.Item name="mark_paid" valuePropName="checked">
|
||
<Checkbox>直接标记为已支付(源头可立即查询并发货)</Checkbox>
|
||
</Form.Item>
|
||
)}
|
||
<Typography.Paragraph type="secondary" style={{ fontSize: 12, marginBottom: 0 }}>
|
||
创建后系统会生成店铺订单号(如 O20260720…),这就是源头「输入订单号」要用的号。
|
||
</Typography.Paragraph>
|
||
</Form>
|
||
)}
|
||
</Modal>
|
||
</div>
|
||
)
|
||
}
|