import { useCallback, useEffect, useState } from 'react' import { Button, Checkbox, Form, Input, InputNumber, Modal, Select, Space, Spin, Table, Tag, Typography, message, } from 'antd' import { GiftOutlined, PlusOutlined, ReloadOutlined, WalletOutlined } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' import { platformApi } from '../api' import type { Merchant, PageResult, ProductCatalogItem, WalletAccount } from '../types' import { formatDateTime } from '../utils/time' import { PageHeader } from '../components/PageHeader' const featureOptions = [ { value: 'products', label: '商品' }, { value: 'orders', label: '订单' }, { value: 'wallet', label: '钱包' }, { value: 'api', label: 'API' }, { value: 'callbacks', label: '回调' }, ] export default function PlatformMerchants() { const [data, setData] = useState>({ list: [], total: 0, page: 1, size: 10 }) const [loading, setLoading] = useState(false) const [createOpen, setCreateOpen] = useState(false) const [settingsOpen, setSettingsOpen] = useState(false) const [assignOpen, setAssignOpen] = useState(false) const [assignLoading, setAssignLoading] = useState(false) const [assignSubmitting, setAssignSubmitting] = useState(false) const [catalog, setCatalog] = useState([]) const [merchantProducts, setMerchantProducts] = useState([]) const [selectedCatalogIds, setSelectedCatalogIds] = useState([]) const [selectedMerchant, setSelectedMerchant] = useState(null) const [adjustOpen, setAdjustOpen] = useState(false) const [adjustLoading, setAdjustLoading] = useState(false) const [adjustSubmitting, setAdjustSubmitting] = useState(false) const [adjustWallet, setAdjustWallet] = useState(null) const [createForm] = Form.useForm() const [settingsForm] = Form.useForm() const [adjustForm] = Form.useForm() const load = useCallback(async (page = data.page, size = data.size) => { setLoading(true) try { const result = await platformApi.merchants({ page, size }) setData(result) } catch (e) { message.error(e instanceof Error ? e.message : '加载失败') } finally { setLoading(false) } }, [data.page, data.size]) useEffect(() => { load() }, [load]) const submitCreate = async () => { const values = await createForm.validateFields() try { await platformApi.createMerchant({ ...values, features: featureListToText(values.features), fee_type: values.fee_type, fee_rate_bp: Number(values.fee_rate_bp || 0), fee_fixed_amount: Number(values.fee_fixed_amount || 0), }) message.success('商户已创建') setCreateOpen(false) createForm.resetFields() load() } catch (e) { message.error(e instanceof Error ? e.message : '创建失败') } } const submitSettings = async () => { if (!selectedMerchant) return const values = await settingsForm.validateFields() try { await platformApi.updateMerchant(selectedMerchant.id, { name: values.name, status: values.status, contact_name: values.contact_name, contact_info: values.contact_info, features: featureListToText(values.features), fee_type: values.fee_type, fee_rate_bp: Number(values.fee_rate_bp || 0), fee_fixed_amount: Number(values.fee_fixed_amount || 0), }) message.success('商户设置已更新') setSettingsOpen(false) load() } catch (e) { message.error(e instanceof Error ? e.message : '更新失败') } } const openAdjust = async (record: Merchant) => { setSelectedMerchant(record) setAdjustOpen(true) setAdjustLoading(true) setAdjustWallet(null) adjustForm.resetFields() try { const walletData = await platformApi.wallet(record.id) setAdjustWallet(walletData) adjustForm.setFieldsValue({ idempotency_key: `manual-${Date.now()}` }) } catch (e) { message.error(e instanceof Error ? e.message : '加载钱包失败') } finally { setAdjustLoading(false) } } const submitAdjust = async () => { if (!selectedMerchant) return const values = await adjustForm.validateFields() setAdjustSubmitting(true) try { const walletData = await platformApi.adjustWallet(selectedMerchant.id, { amount: Number(values.amount), idempotency_key: values.idempotency_key, note: values.note, }) setAdjustWallet(walletData) message.success('积分已调整') setAdjustOpen(false) load() } catch (e) { message.error(e instanceof Error ? e.message : '调整失败') } finally { setAdjustSubmitting(false) } } const openAssign = async (record: Merchant) => { setSelectedMerchant(record) setAssignOpen(true) setAssignLoading(true) try { const [catalogData, productsData] = await Promise.all([ platformApi.productCatalog(), platformApi.merchantProducts(record.id), ]) setCatalog(catalogData || []) setMerchantProducts(productsData || []) // 用目录 ID 预选:商户已有商品中 SKU 与目录匹配的,视为已分配 const assignedSKUs = new Set((productsData || []).map((p) => p.sku)) const presetIds = (catalogData || []).filter((c) => assignedSKUs.has(c.sku)).map((c) => c.id) setSelectedCatalogIds(presetIds) } catch (e) { message.error(e instanceof Error ? e.message : '加载商品失败') setCatalog([]) setMerchantProducts([]) setSelectedCatalogIds([]) } finally { setAssignLoading(false) } } const submitAssign = async () => { if (!selectedMerchant) return setAssignSubmitting(true) try { const result = await platformApi.assignProducts(selectedMerchant.id, selectedCatalogIds) message.success(`已分配 ${result.assigned} 个商品`) setAssignOpen(false) load() } catch (e) { message.error(e instanceof Error ? e.message : '分配失败') } finally { setAssignSubmitting(false) } } const columns: ColumnsType = [ { title: 'ID', dataIndex: 'id', width: 70, align: 'center' }, { title: '编码', dataIndex: 'code', width: 160, render: (v) => {v} }, { title: '商户名称', dataIndex: 'name', width: 200, ellipsis: true, render: (v) => {v} }, { title: '联系人', dataIndex: 'contact_name', width: 120, render: (v) => v || '-' }, { title: '联系方式', dataIndex: 'contact_info', width: 150, render: (v) => v || '-' }, { title: '手续费率', key: 'fee', width: 140, render: (_, r) => r.fee_type === 'fixed' ? `固定 ${r.fee_fixed_amount} 积分/单` : `${(r.fee_rate_bp / 100).toFixed(2)}%`, }, { title: '开通功能', dataIndex: 'features', width: 220, render: (v) => ( {featuresToList(v).map((feature) => ( {featureText(feature)} ))} ), }, { title: '状态', dataIndex: 'status', width: 100, render: (v) => v === 'active' ? ( 已启用 ) : ( 已禁用 ), }, { title: '创建时间', dataIndex: 'created_at', width: 170, render: formatDateTime }, { title: '操作', key: 'action', width: 180, fixed: 'right', render: (_, record) => ( ), }, ] return (
} /> `共 ${total} 条`, onChange: load, }} /> setCreateOpen(false)} destroyOnClose width={680}>
prev.fee_type !== cur.fee_type}> {({ getFieldValue }) => getFieldValue('fee_type') === 'fixed' ? ( ) : ( ) }
setSettingsOpen(false)} destroyOnClose width={680}>
prev.fee_type !== cur.fee_type}> {({ getFieldValue }) => getFieldValue('fee_type') === 'fixed' ? ( ) : ( ) }
setAdjustOpen(false)} destroyOnClose width={520} confirmLoading={adjustSubmitting} okText="确认调整" >
当前可用积分
{(adjustWallet?.available_balance ?? 0).toLocaleString('zh-CN')} 积分
仅平台管理员可手动调整商户积分,商户侧无调账入口。调整记录会写入该商户的积分流水(类型:调整)。
setAssignOpen(false)} destroyOnClose width={760} confirmLoading={assignSubmitting} okText="保存分配" > 从平台商品目录勾选要分配给该商户的商品,保存后将以勾选结果同步(未勾选的已有商品会被移除)。 setSelectedCatalogIds(values as number[])} style={{ width: '100%' }} >
, }, { title: 'SKU', dataIndex: 'sku', width: 200, ellipsis: true, render: (v) => {v} }, { title: '名称', dataIndex: 'display_name', ellipsis: true }, { title: '品类', dataIndex: 'category', width: 100, render: (v) => v || '-' }, { title: '售价', dataIndex: 'price_amount', width: 90, render: (v) => `${v} 积分` }, { title: '状态', dataIndex: 'status', width: 80, render: (v) => (v === 'active' ? 上架 : 下架), }, ]} /> 已选 {selectedCatalogIds.length} / 目录共 {catalog.length} 个商品 · 该商户当前已分配 {merchantProducts.length} 个 ) } function featuresToList(features?: string) { return (features || '') .split(/[,\s]+/) .map((item) => item.trim()) .filter(Boolean) } function featureListToText(features?: string[]) { return (features || []).join(',') } function featureText(feature: string) { return featureOptions.find((item) => item.value === feature)?.label || feature }