Files
dukang/apps/admin-web/src/pages/XiaofeixiaTestPage.tsx
T
2026-08-04 21:38:49 +08:00

299 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import {
Alert, Button, Card, Col, Descriptions, Form, Input, InputNumber, Row, Select, Space,
Tabs, Tag, Typography, message,
} from 'antd';
import { request } from '../lib/api';
type XfxConfig = {
provider: string;
activeProvider: string;
apiUrl: string;
appId: string | null;
mchId: string | null;
mchIdMasked: string | null;
hasApiKey: boolean;
signType: string;
ready: boolean;
source?: string;
hint?: string;
};
type ApiResult = {
ok: boolean;
elapsedMs: number;
data?: unknown;
error?: string;
code?: string;
provider?: string;
raw?: unknown;
};
const CREATE_DEFAULTS = {
outNumber: `TEST-${Date.now()}`,
fromName: '杜康仓库',
fromMobile: '13800000000',
fromAddress: '河南省郑州市金水区',
fromAddressDetail: '杜康酒业仓',
fromLng: 113.665,
fromLat: 34.757,
toName: '测试收货人',
toMobile: '13800000001',
toAddress: '河南省郑州市二七区',
toAddressDetail: '测试路 1 号',
toLng: 113.640,
toLat: 34.720,
goodsName: '杜康酒',
goodsNum: 2,
weight: 2,
payMode: '1',
remark: 'Admin 联调测试',
};
function ResultPanel({ result }: { result: ApiResult | null }) {
if (!result) return <Typography.Text type="secondary">点击「调用接口」后在此显示响应</Typography.Text>;
return (
<div>
<Space style={{ marginBottom: 8 }}>
<Tag color={result.ok ? 'green' : 'red'}>{result.ok ? '成功' : '失败'}</Tag>
<span>{result.elapsedMs} ms</span>
{result.code && <span>code: {result.code}</span>}
</Space>
<pre style={{
margin: 0, padding: 12, background: '#f5f5f5', borderRadius: 4,
maxHeight: 360, overflow: 'auto', fontSize: 12,
}}>
{JSON.stringify(result, null, 2)}
</pre>
</div>
);
}
export default function XiaofeixiaTestPage() {
const [config, setConfig] = useState<XfxConfig | null>(null);
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<ApiResult | null>(null);
const [freightForm] = Form.useForm();
const [coverageForm] = Form.useForm();
const [createForm] = Form.useForm();
const [queryForm] = Form.useForm();
const [batchForm] = Form.useForm();
const [trackForm] = Form.useForm();
const [cancelForm] = Form.useForm();
useEffect(() => {
void request<XfxConfig>('/admin/courier/xiaofeixia/config').then(setConfig);
createForm.setFieldsValue(CREATE_DEFAULTS);
freightForm.setFieldsValue({ weight: 2 });
coverageForm.setFieldsValue({ toAddress: '河南省郑州市二七区测试路1号' });
}, [createForm, freightForm, coverageForm]);
async function invoke(path: string, body: unknown) {
setLoading(true);
setResult(null);
try {
const res = await request<ApiResult>(path, {
method: 'POST',
body: JSON.stringify(body),
});
setResult(res);
if (res.ok) message.success('调用成功');
else message.warning(res.error || '调用失败');
} catch (e) {
const err = e instanceof Error ? e.message : String(e);
message.error(err);
setResult({ ok: false, elapsedMs: 0, error: err });
} finally {
setLoading(false);
}
}
function parseList(text?: string) {
return (text ?? '').split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
}
return (
<div>
<Typography.Title level={4}>小飞侠接口联调</Typography.Title>
<Typography.Paragraph type="secondary">
通过 HQ 后台直接调用后端封装的小飞侠 APIcmd 100101~100301)。凭证请在
<Link to="/fulfillment-providers">仓配管理</Link>
中配置;联调会优先使用仓配管理里启用的小飞侠承运商。
</Typography.Paragraph>
{config && (
<Card size="small" style={{ marginBottom: 16 }}>
<Descriptions column={3} size="small">
<Descriptions.Item label="Provider">{config.activeProvider}</Descriptions.Item>
<Descriptions.Item label="API">{config.apiUrl}</Descriptions.Item>
<Descriptions.Item label="商户号">{config.mchIdMasked || '—'}</Descriptions.Item>
<Descriptions.Item label="签名">{config.signType}</Descriptions.Item>
<Descriptions.Item label="密钥">
{config.hasApiKey ? <Tag color="green">已配置</Tag> : <Tag color="red">未配置</Tag>}
</Descriptions.Item>
<Descriptions.Item label="状态">
{config.ready ? <Tag color="green">可调用</Tag> : <Tag color="orange">配置不完整</Tag>}
</Descriptions.Item>
<Descriptions.Item label="来源" span={3}>
{config.source === 'fulfillment_provider' ? '仓配管理' : '环境变量(兼容)'}
{config.hint ? ` · ${config.hint}` : ''}
</Descriptions.Item>
</Descriptions>
{!config.ready && (
<Alert
type="warning"
showIcon
style={{ marginTop: 12 }}
message="请先在仓配管理中注册小飞侠并填写 API 地址、商户号与 API Key"
/>
)}
</Card>
)}
<Row gutter={16}>
<Col xs={24} lg={14}>
<Tabs
items={[
{
key: 'freight',
label: '运费预估 (100105)',
children: (
<Form form={freightForm} layout="vertical" onFinish={(v) => void invoke('/admin/courier/xiaofeixia/estimate-freight', v)}>
<Form.Item name="weight" label="重量 (kg)" rules={[{ required: true }]}>
<InputNumber min={0.01} step={0.5} style={{ width: '100%' }} />
</Form.Item>
<Button type="primary" htmlType="submit" loading={loading}>调用接口</Button>
</Form>
),
},
{
key: 'coverage',
label: '配送范围 (100301)',
children: (
<Form form={coverageForm} layout="vertical" onFinish={(v) => void invoke('/admin/courier/xiaofeixia/check-coverage', v)}>
<Form.Item name="toAddress" label="收件地址" rules={[{ required: true }]}>
<Input.TextArea rows={2} placeholder="完整收件地址" />
</Form.Item>
<Button type="primary" htmlType="submit" loading={loading}>调用接口</Button>
</Form>
),
},
{
key: 'create',
label: '创建订单 (100101)',
children: (
<Form form={createForm} layout="vertical" onFinish={(v) => void invoke('/admin/courier/xiaofeixia/create-shipment', v)}>
<Form.Item name="outNumber" label="商家单号 outNumber" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Row gutter={12}>
<Col span={12}>
<Typography.Text strong>寄件人</Typography.Text>
<Form.Item name="fromName" label="姓名" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="fromMobile" label="手机" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="fromAddress" label="地址" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="fromAddressDetail" label="详细地址" rules={[{ required: true }]}><Input /></Form.Item>
<Space>
<Form.Item name="fromLng" label="经度"><InputNumber style={{ width: 120 }} /></Form.Item>
<Form.Item name="fromLat" label="纬度"><InputNumber style={{ width: 120 }} /></Form.Item>
</Space>
</Col>
<Col span={12}>
<Typography.Text strong>收件人</Typography.Text>
<Form.Item name="toName" label="姓名" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="toMobile" label="手机" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="toAddress" label="地址" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="toAddressDetail" label="详细地址" rules={[{ required: true }]}><Input /></Form.Item>
<Space>
<Form.Item name="toLng" label="经度"><InputNumber style={{ width: 120 }} /></Form.Item>
<Form.Item name="toLat" label="纬度"><InputNumber style={{ width: 120 }} /></Form.Item>
</Space>
</Col>
</Row>
<Row gutter={12}>
<Col span={8}><Form.Item name="goodsName" label="货品"><Input /></Form.Item></Col>
<Col span={8}><Form.Item name="goodsNum" label="件数"><InputNumber min={1} style={{ width: '100%' }} /></Form.Item></Col>
<Col span={8}><Form.Item name="weight" label="重量 kg"><InputNumber min={0.01} style={{ width: '100%' }} /></Form.Item></Col>
</Row>
<Form.Item name="payMode" label="付费方式" rules={[{ required: true }]}>
<Select options={[
{ value: '1', label: '寄付' },
{ value: '2', label: '到付' },
]} />
</Form.Item>
<Form.Item name="remark" label="备注"><Input /></Form.Item>
<Space>
<Button type="primary" htmlType="submit" loading={loading}>调用接口</Button>
<Button onClick={() => createForm.setFieldsValue({ ...CREATE_DEFAULTS, outNumber: `TEST-${Date.now()}` })}>
重置示例
</Button>
</Space>
</Form>
),
},
{
key: 'query',
label: '查询订单 (100104)',
children: (
<Form form={queryForm} layout="vertical" onFinish={(v) => void invoke('/admin/courier/xiaofeixia/get-shipment', v)}>
<Form.Item name="trackingNumber" label="运单号 number"><Input placeholder="小飞侠运单号" /></Form.Item>
<Form.Item name="outNumber" label="商家单号 outNumber"><Input /></Form.Item>
<Button type="primary" htmlType="submit" loading={loading}>调用接口</Button>
</Form>
),
},
{
key: 'batch',
label: '批量查询 (100106)',
children: (
<Form form={batchForm} layout="vertical" onFinish={(v) => void invoke('/admin/courier/xiaofeixia/batch-get-shipments', {
trackingNumbers: parseList(v.trackingNumbersText),
outNumbers: parseList(v.outNumbersText),
})}>
<Form.Item name="trackingNumbersText" label="运单号(逗号分隔)">
<Input.TextArea rows={2} placeholder="NO001,NO002" />
</Form.Item>
<Form.Item name="outNumbersText" label="商家单号(逗号分隔)">
<Input.TextArea rows={2} />
</Form.Item>
<Button type="primary" htmlType="submit" loading={loading}>调用接口</Button>
</Form>
),
},
{
key: 'track',
label: '路由查询 (100102)',
children: (
<Form form={trackForm} layout="vertical" onFinish={(v) => void invoke('/admin/courier/xiaofeixia/get-track', v)}>
<Form.Item name="trackingNumber" label="运单号"><Input /></Form.Item>
<Form.Item name="outNumber" label="商家单号"><Input /></Form.Item>
<Button type="primary" htmlType="submit" loading={loading}>调用接口</Button>
</Form>
),
},
{
key: 'cancel',
label: '取消订单 (100103)',
children: (
<Form form={cancelForm} layout="vertical" onFinish={(v) => void invoke('/admin/courier/xiaofeixia/cancel-shipment', v)}>
<Form.Item name="trackingNumber" label="运单号"><Input /></Form.Item>
<Form.Item name="outNumber" label="商家单号"><Input /></Form.Item>
<Button type="primary" danger htmlType="submit" loading={loading}>调用接口</Button>
</Form>
),
},
]}
/>
</Col>
<Col xs={24} lg={10}>
<Card title="响应结果" size="small">
<ResultPanel result={result} />
</Card>
</Col>
</Row>
</div>
);
}