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, TeamOutlined } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' import dayjs from 'dayjs' import { useNavigate } from 'react-router-dom' import { platformApi } from '../api' import type { Merchant, MerchantMember, PageResult, ProductCatalogItem } from '../types' const memberRoleOptions = [ { value: 'owner', label: '负责人' }, { value: 'operator', label: '运营' }, { value: 'finance', label: '财务' }, { value: 'viewer', label: '只读' }, ] 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 [memberOpen, setMemberOpen] = 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 [createForm] = Form.useForm() const [settingsForm] = Form.useForm() const [memberForm] = 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 submitMember = async () => { if (!selectedMerchant) return const values = await memberForm.validateFields() try { await platformApi.addMember(selectedMerchant.id, { user_id: values.user_id, role: values.role as MerchantMember['role'], is_default: values.is_default, }) message.success('成员已添加') setMemberOpen(false) } catch (e) { message.error(e instanceof Error ? e.message : '添加失败') } } 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: 80 }, { title: '编码', dataIndex: 'code', width: 180, render: (v) => {v} }, { title: '名称', dataIndex: 'name', ellipsis: true }, { title: '联系人', dataIndex: 'contact_name', width: 120, render: (v) => v || '-' }, { title: '联系方式', dataIndex: 'contact_info', width: 160, render: (v) => v || '-' }, { title: '手续费', key: 'fee', width: 160, 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: 90, render: (v) => v === 'active' ? 启用 : 禁用 }, { title: '创建时间', dataIndex: 'created_at', width: 160, render: (v) => dayjs(v).format('YYYY-MM-DD HH:mm') }, { title: '操作', key: 'action', width: 260, 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' ? ( ) : ( ) }
setMemberOpen(false)} destroyOnClose >
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 }