仓库管理配置 小飞侠配置
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-07-16 21:40:25 +08:00
parent f6d97b4ee8
commit bde85a7b82
15 changed files with 580 additions and 197 deletions
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
Alert,
Button,
Form,
Input,
@@ -17,6 +18,7 @@ import {
FULFILLMENT_PROVIDER_TYPE_LABELS,
FulfillmentProviderStatus,
FulfillmentProviderType,
isXfxProviderCode,
type FulfillmentProviderDto,
} from '@dukang/shared-types';
import { request } from '../lib/api';
@@ -30,6 +32,12 @@ const STATUS_OPTIONS = Object.entries(FULFILLMENT_PROVIDER_STATUS_LABELS).map(([
value,
label,
}));
const SIGN_OPTIONS = [
{ value: 'MD5', label: 'MD5' },
{ value: 'HMAC-SHA256', label: 'HMAC-SHA256' },
];
const DEFAULT_XFX_API_URL = 'https://beta.51xiaoju.cn/app/api/interface.do';
export default function FulfillmentProvidersPage() {
const [rows, setRows] = useState<FulfillmentProviderDto[]>([]);
@@ -37,6 +45,13 @@ export default function FulfillmentProvidersPage() {
const [open, setOpen] = useState(false);
const [editRow, setEditRow] = useState<FulfillmentProviderDto | null>(null);
const [form] = Form.useForm();
const watchedCode = Form.useWatch('code', form);
const watchedType = Form.useWatch('type', form);
const showXfxFields = useMemo(
() => isXfxProviderCode(String(watchedCode || '')) && watchedType === FulfillmentProviderType.API,
[watchedCode, watchedType],
);
const load = useCallback(async () => {
setLoading(true);
@@ -56,48 +71,64 @@ export default function FulfillmentProvidersPage() {
setEditRow(null);
form.resetFields();
form.setFieldsValue({
code: 'XFX',
name: '小飞侠',
type: FulfillmentProviderType.API,
status: FulfillmentProviderStatus.ACTIVE,
capabilitiesJson: JSON.stringify(
{ createShipment: true, getTrack: true, callback: true },
null,
2,
),
apiUrl: DEFAULT_XFX_API_URL,
signType: 'MD5',
});
setOpen(true);
}
function openEdit(row: FulfillmentProviderDto) {
setEditRow(row);
const xfx = row.xiaofeixiaConfig;
form.setFieldsValue({
code: row.code,
name: row.name,
type: row.type,
status: row.status,
capabilitiesJson: row.capabilities ? JSON.stringify(row.capabilities, null, 2) : '',
configJson: '',
apiUrl: xfx?.apiUrl || DEFAULT_XFX_API_URL,
mchId: xfx?.mchId || '',
apiKey: '',
signType: xfx?.signType || 'MD5',
appId: xfx?.appId || '',
});
setOpen(true);
}
async function submit() {
const v = await form.validateFields();
const payload: Record<string, unknown> = {
name: v.name,
type: v.type,
status: v.status,
};
if (isXfxProviderCode(String(v.code)) && v.type === FulfillmentProviderType.API) {
payload.xiaofeixiaConfig = {
apiUrl: v.apiUrl,
mchId: v.mchId,
apiKey: v.apiKey || undefined,
signType: v.signType,
appId: v.appId || undefined,
};
}
if (editRow) {
await request(`/admin/fulfillment-providers/${editRow.id}`, {
method: 'PUT',
body: JSON.stringify({
name: v.name,
type: v.type,
status: v.status,
configJson: v.configJson || undefined,
capabilitiesJson: v.capabilitiesJson || undefined,
}),
body: JSON.stringify(payload),
});
message.success('已更新');
} else {
await request('/admin/fulfillment-providers', {
method: 'POST',
body: JSON.stringify(v),
body: JSON.stringify({
code: v.code,
...payload,
}),
});
message.success('已创建');
}
@@ -123,14 +154,18 @@ export default function FulfillmentProvidersPage() {
),
},
{
title: '能力',
title: '接口配置',
render: (_, row) => {
const caps = row.capabilities;
if (!caps) return '—';
return Object.entries(caps)
.filter(([, on]) => on)
.map(([k]) => k)
.join('、') || '';
if (isXfxProviderCode(row.code)) {
const cfg = row.xiaofeixiaConfig;
if (!cfg?.apiUrl) return <Tag></Tag>;
return (
<span title={cfg.apiUrl}>
{cfg.hasApiKey ? '已配置' : '缺 Key'} · {cfg.mchId || '无商户号'}
</span>
);
}
return row.hasConfig ? '已配置' : '—';
},
},
{ title: '更新时间', dataIndex: 'updatedAt', width: 170, render: fmtTime },
@@ -153,7 +188,7 @@ export default function FulfillmentProvidersPage() {
</Typography.Title>
<Typography.Text type="secondary">
使 API
</Typography.Text>
</div>
<Button type="primary" onClick={openCreate}>
@@ -161,6 +196,13 @@ export default function FulfillmentProvidersPage() {
</Button>
</Space>
<Alert
type="info"
showIcon
style={{ marginBottom: 16 }}
message="小飞侠配置已从「系统设置 → 同城配送」迁至本页。寄件地址以仓库联系人/地址为准。"
/>
<Table rowKey="id" loading={loading} columns={columns} dataSource={rows} pagination={false} />
<Modal
@@ -169,6 +211,7 @@ export default function FulfillmentProvidersPage() {
onCancel={() => setOpen(false)}
onOk={() => void submit()}
width={560}
destroyOnClose
>
<Form form={form} layout="vertical">
<Form.Item name="code" label="编码" rules={[{ required: true }]}>
@@ -183,12 +226,47 @@ export default function FulfillmentProvidersPage() {
<Form.Item name="status" label="状态" rules={[{ required: true }]}>
<Select options={STATUS_OPTIONS} />
</Form.Item>
<Form.Item name="configJson" label="凭证配置 JSON(可选)">
<Input.TextArea rows={3} placeholder="API 密钥等,仅存服务端" />
</Form.Item>
<Form.Item name="capabilitiesJson" label="能力配置 JSON">
<Input.TextArea rows={4} />
</Form.Item>
{showXfxFields && (
<>
<Typography.Title level={5} style={{ marginTop: 8 }}>
</Typography.Title>
<Form.Item
name="apiUrl"
label="API 地址"
rules={[{ required: true, message: '请填写小飞侠 API 地址' }]}
extra="推单请求将发往此地址"
>
<Input placeholder={DEFAULT_XFX_API_URL} />
</Form.Item>
<Form.Item
name="mchId"
label="商户号"
rules={[{ required: true, message: '请填写商户号' }]}
>
<Input />
</Form.Item>
<Form.Item
name="apiKey"
label="API Key"
rules={editRow ? [] : [{ required: true, message: '请填写 API Key' }]}
extra={
editRow?.xiaofeixiaConfig?.hasApiKey
? '已配置密钥;留空则保持不变'
: undefined
}
>
<Input.Password placeholder={editRow ? '留空则不修改' : '请输入'} />
</Form.Item>
<Form.Item name="signType" label="签名类型" initialValue="MD5">
<Select options={SIGN_OPTIONS} />
</Form.Item>
<Form.Item name="appId" label="AppID(可选)">
<Input />
</Form.Item>
</>
)}
</Form>
</Modal>
</div>
@@ -1,4 +1,5 @@
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,
@@ -15,6 +16,8 @@ type XfxConfig = {
hasApiKey: boolean;
signType: string;
ready: boolean;
source?: string;
hint?: string;
};
type ApiResult = {
@@ -115,8 +118,9 @@ export default function XiaofeixiaTestPage() {
<div>
<Typography.Title level={4}></Typography.Title>
<Typography.Paragraph type="secondary">
HQ APIcmd 100101~100301
{' '}<code>XIAOFEIXIA_MCH_ID</code><code>XIAOFEIXIA_API_KEY</code>
HQ APIcmd 100101~100301
<Link to="/fulfillment-providers"></Link>
使
</Typography.Paragraph>
{config && (
@@ -132,13 +136,17 @@ export default function XiaofeixiaTestPage() {
<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="请在 server/dukang-api/.env 中配置 XIAOFEIXIA_MCH_ID 与 XIAOFEIXIA_API_KEY 后重启 API"
message="请先在仓配管理中注册小飞侠并填写 API 地址、商户号与 API Key"
/>
)}
</Card>