import { useEffect, useState, useMemo } from 'react' import { Card, Table, Typography, Space, Select, Tag, message, Button, Input, Modal, InputNumber } from 'antd' import { SyncOutlined, SearchOutlined, CheckCircleOutlined, StopOutlined, EditOutlined, DollarCircleOutlined } from '@ant-design/icons' import { listAccountDisplays, listSites, updateAccountDisplay, syncSiteAccounts, updateAccountMarkup, batchUpdateAccountMarkup, formatMoney, type AccountDisplay, type Site, } from '../api' const STATUS_LABELS: Record = { enabled: { label: '展示中', color: 'success' }, disabled: { label: '已下架', color: 'default' }, sold: { label: '已售出', color: 'blue' }, offline: { label: '已离线', color: 'error' }, } export default function Accounts() { const [sites, setSites] = useState([]) const [siteID, setSiteID] = useState() const [status, setStatus] = useState('') const [keyword, setKeyword] = useState('') const [items, setItems] = useState([]) const [loading, setLoading] = useState(false) const [syncing, setSyncing] = useState(false) const [selectedRowKeys, setSelectedRowKeys] = useState([]) // Markup modal state const [markupModalVisible, setMarkupModalVisible] = useState(false) const [targetAccount, setTargetAccount] = useState(null) const [markupRatio, setMarkupRatio] = useState(0) const [markupSaving, setMarkupSaving] = useState(false) async function load(site?: number, statusFilter?: string) { if (!site) return setLoading(true) try { setItems(await listAccountDisplays(site, statusFilter)) } catch { message.error('账号展示加载失败') } finally { setLoading(false) } } useEffect(() => { listSites() .then((rows) => { setSites(rows) if (rows.length > 0) { setSiteID(rows[0].id) void load(rows[0].id) } }) .catch(() => message.error('站点加载失败')) }, []) async function handleSync() { if (!siteID) { message.warning('请先选择分销站点') return } setSyncing(true) try { const res = await syncSiteAccounts(siteID) message.success( `同步完成!新增本站上传货 ${res.synced_count} 个,已存在 ${res.skipped_count} 个(本次候选 ${res.total_active} 个)`, 5, ) await load(siteID, status) } catch (error: unknown) { const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message message.error(respMessage ?? '同步本站上传商品失败') } finally { setSyncing(false) } } async function toggle(item: AccountDisplay) { const next = item.display_status === 'enabled' ? 'disabled' : 'enabled' try { await updateAccountDisplay(item.id, next) message.success(next === 'enabled' ? '已恢复展示' : '已下架') if (siteID) void load(siteID, status) } catch (error: unknown) { const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message message.error(respMessage ?? '状态更新失败') } } async function handleBatchUpdate(targetStatus: 'enabled' | 'disabled') { if (selectedRowKeys.length === 0) { message.warning('请先勾选需要批量操作的账号') return } const actionLabel = targetStatus === 'enabled' ? '批量恢复展示' : '批量下架' Modal.confirm({ title: `确认${actionLabel}?`, content: `即将对选中的 ${selectedRowKeys.length} 个账号执行${actionLabel}操作。`, onOk: async () => { let successCount = 0 for (const id of selectedRowKeys) { try { await updateAccountDisplay(Number(id), targetStatus) successCount++ } catch { // continue next } } message.success(`已完成 ${successCount}/${selectedRowKeys.length} 个账号的${actionLabel}`) setSelectedRowKeys([]) if (siteID) void load(siteID, status) }, }) } function handleOpenMarkup(item?: AccountDisplay) { if (item) { setTargetAccount(item) setMarkupRatio(item.shelf_ratio_delta ?? 0) } else { if (selectedRowKeys.length === 0) { message.warning('请先勾选需要批量加价的账号商品') return } setTargetAccount(null) setMarkupRatio(0) } setMarkupModalVisible(true) } async function handleSaveMarkup() { if (!siteID) return if (markupRatio < 0) { message.error('加价比例不能小于0') return } setMarkupSaving(true) try { if (targetAccount) { await updateAccountMarkup(targetAccount.id, markupRatio) message.success('商品加价比例设置成功') } else { const listingIDs = items .filter((it) => selectedRowKeys.includes(it.id)) .map((it) => it.listing_id) if (listingIDs.length === 0) { message.warning('请先勾选要加价的商品') return } await batchUpdateAccountMarkup(siteID, listingIDs, markupRatio) message.success(`已成功为选中的 ${listingIDs.length} 个商品批量设置加价!`) setSelectedRowKeys([]) } setMarkupModalVisible(false) void load(siteID, status) } catch (error: unknown) { const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message message.error(respMessage || '加价设置保存失败') } finally { setMarkupSaving(false) } } const filteredItems = useMemo(() => { const kw = keyword.trim().toLowerCase() if (!kw) return items return items.filter( (item) => (item.title && item.title.toLowerCase().includes(kw)) || (item.listing_no && item.listing_no.toLowerCase().includes(kw)) || (item.game_name && item.game_name.toLowerCase().includes(kw)) || (item.server_region && item.server_region.toLowerCase().includes(kw)), ) }, [items, keyword]) const currentSite = sites.find((s) => s.id === siteID) return (
商品与账号展示 管理当前分销网站对外展示的商品。默认同步本站上传货,主站货需显式勾选,不会灌入全站目录。
({ label: conf.label, value, }))} onChange={(value) => { setStatus(value ?? '') void load(siteID, value ?? '') }} /> } style={{ width: 240 }} allowClear value={keyword} onChange={(e) => setKeyword(e.target.value)} /> {selectedRowKeys.length > 0 && ( 已勾选 {selectedRowKeys.length} 项 )} `共 ${total} 个账号商品`, showQuickJumper: true, }} columns={[ { title: '商品编号', dataIndex: 'listing_no', width: 140, render: (v: string | undefined, row: AccountDisplay) => ( {v || `#${row.listing_id}`} {row.in_transaction && 出租中} ), }, { title: '账号信息', dataIndex: 'title', minWidth: 240, render: (_: unknown, row: AccountDisplay) => (
{row.title || `账号 #${row.account_id}`} {row.game_name || '默认游戏'} {row.server_region && {row.server_region}} {row.login_platform && {row.login_platform}}
), }, { title: '段位 / 资产', width: 140, render: (_: unknown, row: AccountDisplay) => (
{row.rank_level ? {row.rank_level} : '-'}
{row.haf_coin_amount !== undefined && row.haf_coin_amount > 0 && ( 哈夫币: {row.haf_coin_amount.toLocaleString()} )}
), }, { title: '租金 / 押金', width: 160, render: (_: unknown, row: AccountDisplay) => (
{formatMoney(row.site_price_cent || row.price_cent)}
押金: {formatMoney(row.deposit_amount_cent)}
), }, { title: '加价比例', dataIndex: 'shelf_ratio_delta', width: 120, render: (v: number | undefined) => { if (v && v > 0) { return ( +{v} ) } return 无加价 }, }, { title: '主站状态', dataIndex: 'listing_status', width: 110, render: (v: string | undefined) => ( {v === 'published' ? '主站在售' : v || '未知'} ), }, { title: '分销展示', dataIndex: 'display_status', width: 110, render: (v: string) => { const conf = STATUS_LABELS[v] ?? { label: v, color: 'default' } return {conf.label} }, }, { title: '操作', width: 160, render: (_: unknown, row: AccountDisplay) => ( ), }, ]} /> setMarkupModalVisible(false)} onOk={handleSaveMarkup} confirmLoading={markupSaving} okText="保存提价" destroyOnClose > 分站提价是通过调小「1 元兑换哈夫币」的兑换比例来提高销售价。比例越小售价越高;提价多挣的差价收益 100% 归本分站所有。
提价调减比例: setMarkupRatio(val ?? 0)} addonAfter="比例点" placeholder="0" />
{targetAccount && (
主站基准比例 {targetAccount.base_ratio || '-'} → 分站提价后比例{' '} {Math.max((targetAccount.base_ratio || 0) - markupRatio, 0.1).toFixed(1)}, 预计售价{' '} {markupRatio > 0 && targetAccount.base_ratio ? `¥${(((targetAccount.haf_coin_amount || 0) / 10000) / Math.max(targetAccount.base_ratio - markupRatio, 0.1)).toFixed(2)}` : formatMoney(targetAccount.site_price_cent || targetAccount.price_cent)} (加价溢价全额归本站独享)
)}
) }