支持 .env 配置,并完善源头对接文档与测试订单
- 增加 .env.example 与 godotenv 加载,start.sh 自动读环境变量 - 重写发给源头的开放接口对接文档 - 订单页支持创建测试订单(可直接已支付并复制店铺订单号)
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# 前端开发配置示例
|
||||
# 复制为 frontend/.env 后生效(Vite 仅暴露 VITE_ 前缀变量)
|
||||
|
||||
# 开发服务器端口(也可由根目录 FRONTEND_PORT 通过 start.sh 传入)
|
||||
# PORT=5173
|
||||
|
||||
# 后端 API 代理目标(见 vite.config.ts)
|
||||
VITE_API_PROXY_TARGET=http://localhost:8080
|
||||
@@ -45,8 +45,13 @@ export const skinApi = {
|
||||
export const orderApi = {
|
||||
list: (params?: Record<string, unknown>) =>
|
||||
request.get('/orders', { params }).then((r) => r.data.data as PageResult<Order>),
|
||||
create: (data: { skin_id: number; buyer_name?: string; remark?: string }) =>
|
||||
request.post('/orders', data).then((r) => r.data.data as Order),
|
||||
create: (data: {
|
||||
skin_id: number
|
||||
buyer_name?: string
|
||||
remark?: string
|
||||
status?: string
|
||||
distributor_id?: number
|
||||
}) => request.post('/orders', data).then((r) => r.data.data as Order),
|
||||
updateStatus: (id: number, status: string) =>
|
||||
request.patch(`/orders/${id}/status`, { status }).then((r) => r.data.data),
|
||||
}
|
||||
|
||||
+221
-13
@@ -1,6 +1,10 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
@@ -8,12 +12,12 @@ import {
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd'
|
||||
import { ReloadOutlined } from '@ant-design/icons'
|
||||
import { PlusOutlined, ReloadOutlined, CopyOutlined } from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import dayjs from 'dayjs'
|
||||
import { orderApi } from '../api'
|
||||
import { orderApi, skinApi } from '../api'
|
||||
import { useAuth } from '../store/auth'
|
||||
import type { Order } from '../types'
|
||||
import type { Order, Skin } from '../types'
|
||||
|
||||
const statusMap: Record<string, { color: string; text: string }> = {
|
||||
pending: { color: 'orange', text: '待支付' },
|
||||
@@ -32,6 +36,11 @@ export default function Orders() {
|
||||
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)
|
||||
@@ -50,6 +59,52 @@ export default function Orders() {
|
||||
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)
|
||||
@@ -61,7 +116,19 @@ export default function Orders() {
|
||||
}
|
||||
|
||||
const columns: ColumnsType<Order> = [
|
||||
{ title: '订单号', dataIndex: 'order_no', width: 190, ellipsis: true },
|
||||
{
|
||||
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'],
|
||||
@@ -71,9 +138,16 @@ export default function Orders() {
|
||||
},
|
||||
{
|
||||
title: 'SKU',
|
||||
width: 140,
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (_, r) => r.skin?.sku || '-',
|
||||
render: (_, r) =>
|
||||
r.skin?.sku ? (
|
||||
<Typography.Text code copyable={{ text: r.skin.sku }}>
|
||||
{r.skin.sku}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '分销商',
|
||||
@@ -117,9 +191,10 @@ export default function Orders() {
|
||||
columns.push({
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 220,
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Space size={0}>
|
||||
{record.status === 'pending' && (
|
||||
<>
|
||||
<Button type="link" size="small" onClick={() => changeStatus(record.id, 'paid')}>
|
||||
@@ -130,7 +205,9 @@ export default function Orders() {
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{(record.status === 'paid' || record.status === 'ship_failed' || record.status === 'delivering') && (
|
||||
{(record.status === 'paid' ||
|
||||
record.status === 'ship_failed' ||
|
||||
record.status === 'delivering') && (
|
||||
<Button type="link" size="small" onClick={() => changeStatus(record.id, 'delivered')}>
|
||||
标记交付
|
||||
</Button>
|
||||
@@ -143,9 +220,14 @@ export default function Orders() {
|
||||
return (
|
||||
<div>
|
||||
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
订单管理
|
||||
</Typography.Title>
|
||||
<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
|
||||
@@ -168,6 +250,9 @@ export default function Orders() {
|
||||
<Button icon={<ReloadOutlined />} onClick={load}>
|
||||
刷新
|
||||
</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
创建测试订单
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
|
||||
@@ -177,18 +262,141 @@ export default function Orders() {
|
||||
columns={columns}
|
||||
dataSource={list}
|
||||
tableLayout="fixed"
|
||||
scroll={{ x: 1100 }}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
+21
-14
@@ -1,19 +1,26 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/health': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
export default defineConfig(({ mode }) => {
|
||||
// 读取 frontend/.env* 与进程环境(start.sh 会注入根目录 .env)
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
const apiTarget = env.VITE_API_PROXY_TARGET || 'http://localhost:8080'
|
||||
const port = Number(env.PORT || env.FRONTEND_PORT || 5173)
|
||||
|
||||
return {
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: apiTarget,
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/health': {
|
||||
target: apiTarget,
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user