Files
dukang/apps/admin-web/src/pages/FulfillmentProvidersPage.tsx
T
jacy bde85a7b82
CI / verify (pull_request) Has been cancelled
仓库管理配置 小飞侠配置
2026-07-16 21:40:25 +08:00

275 lines
8.1 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from 'react';
import {
Alert,
Button,
Form,
Input,
Modal,
Select,
Space,
Table,
Tag,
Typography,
message,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import {
FULFILLMENT_PROVIDER_STATUS_LABELS,
FULFILLMENT_PROVIDER_TYPE_LABELS,
FulfillmentProviderStatus,
FulfillmentProviderType,
isXfxProviderCode,
type FulfillmentProviderDto,
} from '@dukang/shared-types';
import { request } from '../lib/api';
import { fmtTime } from '../lib/constants';
const TYPE_OPTIONS = Object.entries(FULFILLMENT_PROVIDER_TYPE_LABELS).map(([value, label]) => ({
value,
label,
}));
const STATUS_OPTIONS = Object.entries(FULFILLMENT_PROVIDER_STATUS_LABELS).map(([value, label]) => ({
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[]>([]);
const [loading, setLoading] = useState(false);
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);
try {
const res = await request<FulfillmentProviderDto[]>('/admin/fulfillment-providers');
setRows(res);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void load();
}, [load]);
function openCreate() {
setEditRow(null);
form.resetFields();
form.setFieldsValue({
code: 'XFX',
name: '小飞侠',
type: FulfillmentProviderType.API,
status: FulfillmentProviderStatus.ACTIVE,
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,
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(payload),
});
message.success('已更新');
} else {
await request('/admin/fulfillment-providers', {
method: 'POST',
body: JSON.stringify({
code: v.code,
...payload,
}),
});
message.success('已创建');
}
setOpen(false);
void load();
}
const columns: ColumnsType<FulfillmentProviderDto> = [
{ title: '编码', dataIndex: 'code', width: 100 },
{ title: '名称', dataIndex: 'name' },
{
title: '类型',
dataIndex: 'type',
render: (v) => FULFILLMENT_PROVIDER_TYPE_LABELS[v as FulfillmentProviderType] || v,
},
{
title: '状态',
dataIndex: 'status',
render: (v) => (
<Tag color={v === 'ACTIVE' ? 'green' : 'default'}>
{FULFILLMENT_PROVIDER_STATUS_LABELS[v as FulfillmentProviderStatus] || v}
</Tag>
),
},
{
title: '接口配置',
render: (_, row) => {
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 },
{
title: '操作',
width: 80,
render: (_, row) => (
<Button type="link" size="small" onClick={() => openEdit(row)}>
编辑
</Button>
),
},
];
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">
注册第三方履约接口并填写凭证;仓库绑定后,推单将使用此处配置的 API 地址与密钥
</Typography.Text>
</div>
<Button type="primary" onClick={openCreate}>
注册承运商
</Button>
</Space>
<Alert
type="info"
showIcon
style={{ marginBottom: 16 }}
message="小飞侠配置已从「系统设置 → 同城配送」迁至本页。寄件地址以仓库联系人/地址为准。"
/>
<Table rowKey="id" loading={loading} columns={columns} dataSource={rows} pagination={false} />
<Modal
title={editRow ? '编辑承运商' : '注册承运商'}
open={open}
onCancel={() => setOpen(false)}
onOk={() => void submit()}
width={560}
destroyOnClose
>
<Form form={form} layout="vertical">
<Form.Item name="code" label="编码" rules={[{ required: true }]}>
<Input disabled={Boolean(editRow)} placeholder="如 XFX、JD、SF" />
</Form.Item>
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
<Input placeholder="如 小飞侠、京东物流" />
</Form.Item>
<Form.Item name="type" label="类型" rules={[{ required: true }]}>
<Select options={TYPE_OPTIONS} />
</Form.Item>
<Form.Item name="status" label="状态" rules={[{ required: true }]}>
<Select options={STATUS_OPTIONS} />
</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>
);
}