import { useCallback, useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { Button, Checkbox, Divider, Form, Input, InputNumber, Modal, Select, Space, Spin, Switch, Table, Tag, Typography, message, } from 'antd' import { GiftOutlined, PlusOutlined, ReloadOutlined, UserOutlined, WalletOutlined } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' import { platformApi } from '../api' import type { Merchant, MerchantOwnerAccount, 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 navigate = useNavigate() 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 [editProduct, setEditProduct] = useState(null) const [editProductOpen, setEditProductOpen] = useState(false) const [editProductSubmitting, setEditProductSubmitting] = useState(false) const [accounts, setAccounts] = useState([]) const [accountsLoading, setAccountsLoading] = useState(false) const [accountModalOpen, setAccountModalOpen] = useState(false) const [editingAccount, setEditingAccount] = useState(null) const [accountSubmitting, setAccountSubmitting] = useState(false) const [createForm] = Form.useForm() const [settingsForm] = Form.useForm() const [adjustForm] = Form.useForm() const [editProductForm] = Form.useForm() const [accountForm] = 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 loadAccounts = useCallback(async (merchantId: number) => { setAccountsLoading(true) try { setAccounts(await platformApi.merchantAccount(merchantId)) } catch (e) { message.error(e instanceof Error ? e.message : '加载商户账号失败') setAccounts([]) } finally { setAccountsLoading(false) } }, []) const openAccountEdit = (account: MerchantOwnerAccount) => { setEditingAccount(account) accountForm.resetFields() accountForm.setFieldsValue({ username: account.username, nickname: account.nickname, password: '', status: account.status === 1, }) setAccountModalOpen(true) } const submitAccount = async () => { if (!selectedMerchant || !editingAccount) return const values = await accountForm.validateFields() setAccountSubmitting(true) try { await platformApi.updateMerchantAccount(selectedMerchant.id, { user_id: editingAccount.user_id, username: values.username, nickname: values.nickname || '', password: values.password || undefined, status: values.status ? 1 : 0, }) message.success(values.password ? '商户账号已更新,密码已重置' : '商户账号已更新') setAccountModalOpen(false) void loadAccounts(selectedMerchant.id) } catch (e) { message.error(e instanceof Error ? e.message : '保存失败') } finally { setAccountSubmitting(false) } } 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 openProductEdit = (record: ProductCatalogItem) => { setEditProduct(record) editProductForm.resetFields() editProductForm.setFieldsValue({ display_name: record.display_name, price_amount: record.price_amount, cost_amount: record.cost_amount, stock: record.stock, status: record.status, }) setEditProductOpen(true) } const submitProductEdit = async () => { if (!selectedMerchant || !editProduct) return const values = await editProductForm.validateFields() setEditProductSubmitting(true) try { await platformApi.updateMerchantProduct(selectedMerchant.id, editProduct.id, { display_name: values.display_name, price_amount: Number(values.price_amount || 0), cost_amount: Number(values.cost_amount || 0), stock: Number(values.stock ?? -1), status: values.status, }) message.success('商品已更新') setEditProductOpen(false) const productsData = await platformApi.merchantProducts(selectedMerchant.id) setMerchantProducts(productsData || []) } catch (e) { message.error(e instanceof Error ? e.message : '保存失败') } finally { setEditProductSubmitting(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: 240, 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' ? ( ) : ( ) } 商户账号 {accounts.length === 0 ? ( 该商户暂无负责人登录账号 ) : (
{v} }, { title: '昵称', dataIndex: 'nickname', render: (v) => v || '-' }, { title: '账号状态', dataIndex: 'status', width: 90, render: (v) => (v === 1 ? 启用 : 停用), }, { title: '操作', key: 'action', width: 70, render: (_, record: MerchantOwnerAccount) => ( ), }, ]} /> )} 商户账号是负责人登录商户后台的凭证:可修改登录用户名与昵称、重置密码、启用或停用账号(至少保留一个启用的负责人账号)。停用账号仅影响登录,不等于停用商户;重置密码后旧密码立即失效。 setAccountModalOpen(false)} destroyOnClose width={520} confirmLoading={accountSubmitting} okText="保存" >
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' ? 上架 : 下架), }, { title: '操作', key: 'action', width: 60, render: (_, record: ProductCatalogItem) => ( ), }, ]} /> 已选 {selectedCatalogIds.length} / 目录共 {catalog.length} 个商品 · 该商户当前已分配 {merchantProducts.length} 个 setEditProductOpen(false)} destroyOnClose width={560} confirmLoading={editProductSubmitting} okText="保存" >