import { useCallback, useEffect, useMemo, useState } from 'react'; import { Alert, Button, Divider, Form, Input, InputNumber, Modal, Select, Space, Table, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { DEFAULT_XFX_LOGISTICS_PRICING, FULFILLMENT_PROVIDER_STATUS_LABELS, FULFILLMENT_PROVIDER_TYPE_LABELS, FulfillmentProviderStatus, FulfillmentProviderType, LOGISTICS_SETTLEMENT_METHOD_LABELS, LogisticsSettlementMethod, 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 SETTLEMENT_OPTIONS = Object.entries(LOGISTICS_SETTLEMENT_METHOD_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([]); const [loading, setLoading] = useState(false); const [open, setOpen] = useState(false); const [editRow, setEditRow] = useState(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('/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', settlementMethod: LogisticsSettlementMethod.PREPAID, baseBottles: DEFAULT_XFX_LOGISTICS_PRICING.baseBottles, baseFee: DEFAULT_XFX_LOGISTICS_PRICING.baseFee, extraBottleFee: DEFAULT_XFX_LOGISTICS_PRICING.extraBottleFee, boxBottles: DEFAULT_XFX_LOGISTICS_PRICING.boxBottles, boxFee: DEFAULT_XFX_LOGISTICS_PRICING.boxFee, }); setOpen(true); } function openEdit(row: FulfillmentProviderDto) { setEditRow(row); const xfx = row.xiaofeixiaConfig; const pricing = row.pricingRules; 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 || '', bankAccountName: row.bankAccountName || '', bankName: row.bankName || '', bankBranch: row.bankBranch || '', bankAccountNo: row.bankAccountNo || '', settlementMethod: row.settlementMethod || LogisticsSettlementMethod.PREPAID, baseBottles: pricing?.baseBottles ?? DEFAULT_XFX_LOGISTICS_PRICING.baseBottles, baseFee: pricing?.baseFee ?? DEFAULT_XFX_LOGISTICS_PRICING.baseFee, extraBottleFee: pricing?.extraBottleFee ?? DEFAULT_XFX_LOGISTICS_PRICING.extraBottleFee, boxBottles: pricing?.boxBottles ?? DEFAULT_XFX_LOGISTICS_PRICING.boxBottles, boxFee: pricing?.boxFee ?? DEFAULT_XFX_LOGISTICS_PRICING.boxFee, }); setOpen(true); } async function submit() { const v = await form.validateFields(); const payload: Record = { name: v.name, type: v.type, status: v.status, bankAccountName: v.bankAccountName || null, bankName: v.bankName || null, bankBranch: v.bankBranch || null, bankAccountNo: v.bankAccountNo || null, settlementMethod: v.settlementMethod, pricingRules: { baseBottles: Number(v.baseBottles), baseFee: Number(v.baseFee), extraBottleFee: Number(v.extraBottleFee), boxBottles: v.boxBottles != null ? Number(v.boxBottles) : undefined, boxFee: v.boxFee != null ? Number(v.boxFee) : undefined, }, }; 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 = [ { title: '编码', dataIndex: 'code', width: 100 }, { title: '名称', dataIndex: 'name' }, { title: '类型', dataIndex: 'type', render: (v) => FULFILLMENT_PROVIDER_TYPE_LABELS[v as FulfillmentProviderType] || v, }, { title: '结算', dataIndex: 'settlementMethod', width: 100, render: (v) => LOGISTICS_SETTLEMENT_METHOD_LABELS[v as LogisticsSettlementMethod] || v || '—', }, { title: '充值余额', dataIndex: 'prepaidBalance', width: 100, render: (v) => `¥${Number(v || 0).toFixed(2)}`, }, { title: '状态', dataIndex: 'status', render: (v) => ( {FULFILLMENT_PROVIDER_STATUS_LABELS[v as FulfillmentProviderStatus] || v} ), }, { title: '接口配置', render: (_, row) => { if (isXfxProviderCode(row.code)) { const cfg = row.xiaofeixiaConfig; if (!cfg?.apiUrl) return 未配置; return ( {cfg.hasApiKey ? '已配置' : '缺 Key'} · {cfg.mchId || '无商户号'} ); } return row.hasConfig ? '已配置' : '—'; }, }, { title: '更新时间', dataIndex: 'updatedAt', width: 170, render: fmtTime }, { title: '操作', width: 80, render: (_, row) => ( ), }, ]; return (
仓配管理 注册承运商接口凭证,并配置物流对账用的银行账户、结算方式与计价标准
setOpen(false)} onOk={() => void submit()} width={640} destroyOnClose >
物流对账 {showXfxFields && ( <> 小飞侠接口参数 )}
); }