import { useEffect, useState, useMemo } from 'react' import { Card, Table, Typography, Space, Select, Tag, message, Button, Input, Modal, InputNumber, Empty, Alert, Tooltip, Divider, Radio, Row, Col, } from 'antd' import { SyncOutlined, SearchOutlined, CheckCircleOutlined, StopOutlined, EditOutlined, DollarCircleOutlined, ReloadOutlined, FilterOutlined, ShopOutlined, ThunderboltOutlined, } from '@ant-design/icons' import { listAccountDisplays, listSites, updateAccountDisplay, syncSiteAccounts, updateAccountMarkup, batchUpdateAccountMarkup, formatMoney, getMe, type AccountDisplay, type Site, } from '../api' import { useMember } from '../MemberContext' const SOURCE_OPTIONS = [ { label: '全部货源', value: 'all' }, { label: '主站自营 (平台共享池)', value: 'main' }, { label: '本站收号 (独家货源)', value: 'self' }, { label: '合作分站 (跨站共享)', value: 'collab' }, ] const MARKUP_OPTIONS = [ { label: '全部提价状态', value: 'all' }, { label: '已自主提价 (赚差价)', value: 'marked' }, { label: '未加价 (基准销售价)', value: 'unmarked' }, ] const DISPLAY_OPTIONS = [ { label: '全部展示状态', value: 'all' }, { label: '展示中 (在售货架)', value: 'enabled' }, { label: '已下架 (隐藏货架)', value: 'disabled' }, ] const TX_OPTIONS = [ { label: '全部租借状态', value: 'all' }, { label: '待租中 (空闲可租)', value: 'idle' }, { label: '出租中 (租赁履约)', value: 'rented' }, ] const MAIN_LISTING_STATUS_MAP: Record = { published: { label: '主站在售', color: 'green' }, rented: { label: '在租中', color: 'cyan' }, completed: { label: '已售出/结单', color: 'blue' }, offline: { label: '主站下架', color: 'default' }, closed: { label: '已关闭', color: 'error' }, abnormal: { label: '异常冻结', color: 'error' }, sealed: { label: '已封存', color: 'magenta' }, draft: { label: '草稿', color: 'warning' }, } function formatGameName(name?: string) { if (!name) return '默认游戏' const lower = name.toLowerCase().trim() if ( lower === 'delta_force' || lower === 'deltaforce' || lower === 'delta' || lower.includes('delta_force') || lower.includes('delta') || name.includes('三角洲') ) { return '三角洲' } if (lower.includes('honor') || name.includes('王者')) { return '王者荣耀' } if (lower.includes('genshin') || name.includes('原神')) { return '原神' } if (lower.includes('cs') || lower.includes('csgo')) { return 'CS2' } return name } function formatLoginPlatform(platform?: string): string { if (!platform) return '' const lower = platform.toLowerCase().trim() if (lower === 'qq') return 'QQ' if (lower === 'wechat' || lower === 'wx') return '微信' if (lower === 'steam') return 'Steam' if (lower === 'epic') return 'Epic' if (lower === 'wegame') return 'WeGame' if (lower === 'mobile') return '手游端' if (lower === 'pc') return 'PC端' return platform } export default function Accounts() { const member = useMember() const [sites, setSites] = useState([]) const [siteID, setSiteID] = useState(member?.site_id) const [items, setItems] = useState([]) const [loading, setLoading] = useState(false) const [syncing, setSyncing] = useState(false) const [selectedRowKeys, setSelectedRowKeys] = useState([]) // Shelf product filter states const [sourceFilter, setSourceFilter] = useState('all') const [markupFilter, setMarkupFilter] = useState('all') const [displayFilter, setDisplayFilter] = useState('all') const [txFilter, setTxFilter] = useState('all') const [keyword, setKeyword] = useState('') // Multi-mode Pricing modal state const [markupModalVisible, setMarkupModalVisible] = useState(false) const [targetAccount, setTargetAccount] = useState(null) const [pricingMode, setPricingMode] = useState<'target_price' | 'add_amount' | 'percent' | 'ratio_delta'>('target_price') const [targetPriceYuan, setTargetPriceYuan] = useState(0) const [addAmountYuan, setAddAmountYuan] = useState(0) const [percentMarkup, setPercentMarkup] = useState(0) const [markupRatio, setMarkupRatio] = useState(0) const [markupSaving, setMarkupSaving] = useState(false) // Batch pricing state const [batchMode, setBatchMode] = useState<'percent' | 'add_amount' | 'ratio_delta'>('percent') const [batchValue, setBatchValue] = useState(10) async function load(site?: number) { if (!site) return setLoading(true) try { // Fetch full shelf goods to allow fast client-side multidimensional filtering const rows = await listAccountDisplays(site, '') setItems(rows) } catch { message.error('账号展示货架加载失败') } finally { setLoading(false) } } // Initialize site context: // Normal distribution merchant is strictly bound to member.site_id // Only superadmin can fetch sites and switch between tenants useEffect(() => { async function init() { let currentMember = member if (!currentMember) { try { currentMember = await getMe() } catch { // ignore } } const activeSiteID = currentMember?.site_id if (activeSiteID) { setSiteID(activeSiteID) void load(activeSiteID) } if (currentMember?.is_superadmin) { try { const rows = await listSites() setSites(rows) if (!activeSiteID && rows.length > 0) { setSiteID(rows[0].id) void load(rows[0].id) } } catch { // ignore } } } void init() }, [member]) 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) } 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) } 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) }, }) } function roundRatio(value: number): number { if (value <= 0 || isNaN(value) || !isFinite(value)) return 0 return Math.round(value * 10) / 10 } function calcDeltaFromTargetPrice(hafCoinAmount: number, basePriceCent: number, targetPriceYuan: number): number { if (basePriceCent <= 0 || targetPriceYuan <= 0) return 0 const coinWan = hafCoinAmount / 10000 const basePriceYuan = basePriceCent / 100 if (targetPriceYuan <= basePriceYuan) return 0 const baseRatio = roundRatio(coinWan / basePriceYuan) if (baseRatio <= 0) return 0 const siteRatio = roundRatio(coinWan / targetPriceYuan) const delta = roundRatio(baseRatio - siteRatio) return Math.max(0, delta) } function calcPreviewPrice(hafCoinAmount: number, basePriceCent: number, delta: number): { priceCent: number; siteRatio: number; markupCent: number } { const coinWan = hafCoinAmount / 10000 const basePriceYuan = basePriceCent / 100 const baseRatio = roundRatio(coinWan / basePriceYuan) if (delta <= 0 || baseRatio <= 0) { return { priceCent: basePriceCent, siteRatio: baseRatio, markupCent: 0 } } let siteRatio = roundRatio(baseRatio - delta) if (siteRatio <= 0.01) siteRatio = 0.01 const priceYuan = coinWan / siteRatio const priceCent = Math.max(basePriceCent, Math.round(priceYuan * 100)) return { priceCent, siteRatio, markupCent: priceCent - basePriceCent, } } function handleOpenMarkup(item?: AccountDisplay) { if (item) { setTargetAccount(item) const currentDelta = item.shelf_ratio_delta ?? 0 setMarkupRatio(currentDelta) const basePriceYuan = (item.price_cent || 0) / 100 const currentPriceYuan = (item.site_price_cent || item.price_cent || 0) / 100 setTargetPriceYuan(currentPriceYuan) setAddAmountYuan(Math.max(0, Number((currentPriceYuan - basePriceYuan).toFixed(2)))) setPercentMarkup( basePriceYuan > 0 ? Number((((currentPriceYuan - basePriceYuan) / basePriceYuan) * 100).toFixed(1)) : 0 ) setPricingMode('target_price') } else { if (selectedRowKeys.length === 0) { message.warning('请先勾选需要批量加价的账号商品') return } setTargetAccount(null) setBatchMode('percent') setBatchValue(10) } setMarkupModalVisible(true) } function onTargetPriceChange(val: number | null) { const nextTarget = Math.max(0, val ?? 0) setTargetPriceYuan(nextTarget) if (!targetAccount) return const basePriceYuan = (targetAccount.price_cent || 0) / 100 const addYuan = Math.max(0, Number((nextTarget - basePriceYuan).toFixed(2))) setAddAmountYuan(addYuan) setPercentMarkup(basePriceYuan > 0 ? Number(((addYuan / basePriceYuan) * 100).toFixed(1)) : 0) const delta = calcDeltaFromTargetPrice(targetAccount.haf_coin_amount || 0, targetAccount.price_cent || 0, nextTarget) setMarkupRatio(delta) } function onAddAmountChange(val: number | null) { const nextAdd = Math.max(0, val ?? 0) setAddAmountYuan(nextAdd) if (!targetAccount) return const basePriceYuan = (targetAccount.price_cent || 0) / 100 const nextTarget = Number((basePriceYuan + nextAdd).toFixed(2)) setTargetPriceYuan(nextTarget) setPercentMarkup(basePriceYuan > 0 ? Number(((nextAdd / basePriceYuan) * 100).toFixed(1)) : 0) const delta = calcDeltaFromTargetPrice(targetAccount.haf_coin_amount || 0, targetAccount.price_cent || 0, nextTarget) setMarkupRatio(delta) } function onPercentMarkupChange(val: number | null) { const nextPct = Math.max(0, val ?? 0) setPercentMarkup(nextPct) if (!targetAccount) return const basePriceYuan = (targetAccount.price_cent || 0) / 100 const nextTarget = Number((basePriceYuan * (1 + nextPct / 100)).toFixed(2)) setTargetPriceYuan(nextTarget) setAddAmountYuan(Number((nextTarget - basePriceYuan).toFixed(2))) const delta = calcDeltaFromTargetPrice(targetAccount.haf_coin_amount || 0, targetAccount.price_cent || 0, nextTarget) setMarkupRatio(delta) } function onMarkupRatioChange(val: number | null) { const nextDelta = Math.max(0, val ?? 0) setMarkupRatio(nextDelta) if (!targetAccount) return const preview = calcPreviewPrice(targetAccount.haf_coin_amount || 0, targetAccount.price_cent || 0, nextDelta) const previewYuan = Number((preview.priceCent / 100).toFixed(2)) const basePriceYuan = (targetAccount.price_cent || 0) / 100 setTargetPriceYuan(previewYuan) const addYuan = Number((preview.markupCent / 100).toFixed(2)) setAddAmountYuan(addYuan) setPercentMarkup(basePriceYuan > 0 ? Number(((addYuan / basePriceYuan) * 100).toFixed(1)) : 0) } async function handleSaveMarkup() { if (!siteID) return setMarkupSaving(true) try { if (targetAccount) { await updateAccountMarkup(targetAccount.id, markupRatio) message.success(`商品《${targetAccount.title || targetAccount.listing_no}》加价成功!`) } else { const selectedItems = items.filter((it) => selectedRowKeys.includes(it.id)) if (selectedItems.length === 0) { message.warning('请先勾选要加价的商品') return } if (batchMode === 'ratio_delta') { const listingIDs = selectedItems.map((it) => it.listing_id) await batchUpdateAccountMarkup(siteID, listingIDs, batchValue) } else if (batchMode === 'percent') { await Promise.all( selectedItems.map((it) => { const basePriceYuan = (it.price_cent || 0) / 100 const targetYuan = basePriceYuan * (1 + batchValue / 100) const delta = calcDeltaFromTargetPrice(it.haf_coin_amount || 0, it.price_cent || 0, targetYuan) return updateAccountMarkup(it.id, delta) }) ) } else if (batchMode === 'add_amount') { await Promise.all( selectedItems.map((it) => { const basePriceYuan = (it.price_cent || 0) / 100 const targetYuan = basePriceYuan + batchValue const delta = calcDeltaFromTargetPrice(it.haf_coin_amount || 0, it.price_cent || 0, targetYuan) return updateAccountMarkup(it.id, delta) }) ) } message.success(`已成功为选中的 ${selectedItems.length} 个商品批量设置提价!`) setSelectedRowKeys([]) } setMarkupModalVisible(false) void load(siteID) } catch (error: unknown) { const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message message.error(respMessage || '提价设置保存失败') } finally { setMarkupSaving(false) } } function handleResetFilters() { setSourceFilter('all') setMarkupFilter('all') setDisplayFilter('all') setTxFilter('all') setKeyword('') } // Multidimensional client-side filter const filteredItems = useMemo(() => { return items.filter((item) => { // 1. Source filter if (sourceFilter === 'main') { if (item.listing_site_id !== null && item.listing_site_id !== undefined) return false } else if (sourceFilter === 'self') { if (item.listing_site_id !== siteID) return false } else if (sourceFilter === 'collab') { if (item.listing_site_id === null || item.listing_site_id === undefined || item.listing_site_id === siteID) { return false } } // 2. Markup filter if (markupFilter === 'marked') { if (!item.shelf_ratio_delta || item.shelf_ratio_delta <= 0) return false } else if (markupFilter === 'unmarked') { if (item.shelf_ratio_delta && item.shelf_ratio_delta > 0) return false } // 3. Display status filter if (displayFilter !== 'all') { if (item.display_status !== displayFilter) return false } // 4. Transaction filter if (txFilter === 'idle') { if (item.in_transaction) return false } else if (txFilter === 'rented') { if (!item.in_transaction) return false } // 5. Keyword search if (keyword.trim()) { const kw = keyword.trim().toLowerCase() const matchTitle = item.title && item.title.toLowerCase().includes(kw) const matchNo = item.listing_no && item.listing_no.toLowerCase().includes(kw) const matchGame = (item.game_name && item.game_name.toLowerCase().includes(kw)) || formatGameName(item.game_name).includes(kw) const matchRegion = item.server_region && item.server_region.toLowerCase().includes(kw) const matchPlatform = item.login_platform && item.login_platform.toLowerCase().includes(kw) const matchId = String(item.account_id).includes(kw) || String(item.listing_id).includes(kw) if (!matchTitle && !matchNo && !matchGame && !matchRegion && !matchPlatform && !matchId) { return false } } return true }) }, [items, sourceFilter, markupFilter, displayFilter, txFilter, keyword, siteID]) // Statistics counters const counts = useMemo(() => { let mainCount = 0 let selfCount = 0 let collabCount = 0 let markedCount = 0 let enabledCount = 0 for (const item of items) { if (item.listing_site_id === null || item.listing_site_id === undefined) { mainCount++ } else if (item.listing_site_id === siteID) { selfCount++ } else { collabCount++ } if (item.shelf_ratio_delta && item.shelf_ratio_delta > 0) { markedCount++ } if (item.display_status === 'enabled') { enabledCount++ } } const hasActiveFilters = sourceFilter !== 'all' || markupFilter !== 'all' || displayFilter !== 'all' || txFilter !== 'all' || keyword.trim() !== '' return { total: items.length, main: mainCount, self: selfCount, collab: collabCount, marked: markedCount, enabled: enabledCount, filtered: filteredItems.length, hasActiveFilters, } }, [items, siteID, sourceFilter, markupFilter, displayFilter, txFilter, keyword, filteredItems.length]) const currentSiteName = member?.site_name || sites.find((s) => s.id === siteID)?.site_name || '当前分销商' return ( {/* Superadmin Cross-tenant Inspection Banner */} {member?.is_superadmin && sites.length > 0 && ( 平台超管巡检模式 切换巡查分销站点: {/* 提价状态 */} {/* 交易租借状态 */} } style={{ width: 250 }} allowClear value={keyword} onChange={(e) => setKeyword(e.target.value)} /> {counts.hasActiveFilters && ( )} {/* Batch actions */} {selectedRowKeys.length > 0 && ( 已选 {selectedRowKeys.length} 项 )} {/* Quick Statistics and Data Scope Badges */}
货架概览: 货架总计 {counts.total} 件 主站自营 {counts.main} 件 本站收号 {counts.self} 件 {counts.collab > 0 && 合作分站 {counts.collab} 件} 已自主加价 {counts.marked} 件 正常展示中 {counts.enabled} 件 {counts.hasActiveFilters && ( 已按条件筛选:展示 {counts.filtered} / {counts.total} 件 )}
{/* Products Table */} {counts.hasActiveFilters ? ( ) : ( )} ), }} rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys, }} pagination={{ pageSize: 20, showTotal: (total) => `共 ${total} 个账号商品`, showQuickJumper: true, }} columns={[ { title: '商品编号', dataIndex: 'listing_no', width: 170, render: (v: string | undefined, row: AccountDisplay) => (
{v || `#${row.listing_id}`}
{row.in_transaction ? ( 出租中 ) : ( 在售空闲 )}
), }, { title: '货源归属', width: 110, render: (_: unknown, row: AccountDisplay) => { if (row.listing_site_id === null || row.listing_site_id === undefined) { return ( 主站自营 ) } if (row.listing_site_id === siteID) { return ( 本站收号 ) } return ( 合作分站 ) }, }, { title: '账号信息', dataIndex: 'title', width: 260, render: (_: unknown, row: AccountDisplay) => (
{row.title || `账号 #${row.account_id}`} {formatGameName(row.game_name)} {row.server_region && {row.server_region}} {row.login_platform && {formatLoginPlatform(row.login_platform)}}
), }, { title: '段位 / 资产', width: 130, 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: 150, render: (_: unknown, row: AccountDisplay) => { const finalPrice = row.site_price_cent || row.price_cent const hasMarkup = row.shelf_ratio_delta && row.shelf_ratio_delta > 0 return (
{formatMoney(finalPrice)}
押金: {formatMoney(row.deposit_amount_cent)}
{hasMarkup && (
含加价: +{formatMoney(row.markup_amount_cent)}
)}
) }, }, { title: '加价比例', dataIndex: 'shelf_ratio_delta', width: 110, render: (v: number | undefined) => { if (v && v > 0) { return ( +{v} 比例点 ) } return 无加价 (基准) }, }, { title: '主站状态', dataIndex: 'listing_status', width: 110, render: (v: string | undefined) => { const conf = MAIN_LISTING_STATUS_MAP[v || ''] || { label: v || '未知', color: 'default' } return {conf.label} }, }, { title: '分销展示', dataIndex: 'display_status', width: 100, render: (v: string) => ( {v === 'enabled' ? '展示中' : '已下架'} ), }, { title: '操作', width: 110, fixed: 'right' as const, render: (_: unknown, row: AccountDisplay) => ( ), }, ]} /> {/* Rich Multi-mode Markup Modal */} {targetAccount ? `商品加价设置 - ${targetAccount.title || targetAccount.listing_no}` : `批量调价 (${selectedRowKeys.length} 个账号商品)`} } open={markupModalVisible} onCancel={() => setMarkupModalVisible(false)} onOk={handleSaveMarkup} confirmLoading={markupSaving} okText="保存并同步货架" cancelText="取消" width={580} destroyOnClose > {targetAccount ? ( <>
{targetAccount.title || targetAccount.listing_no} 游戏区服:{formatGameName(targetAccount.game_name) || '-'} / {targetAccount.server_region || '-'}哈夫币资产: {(targetAccount.haf_coin_amount || 0).toLocaleString()} 币 ({((targetAccount.haf_coin_amount || 0) / 10000).toFixed(1)} 万) 主站基准价:¥{((targetAccount.price_cent || 0) / 100).toFixed(2)}主站基准比例:{targetAccount.base_ratio || '-'} 点
加价模式选择: setPricingMode(e.target.value)} buttonStyle="solid" style={{ width: '100%' }} > 期望售价 固定加价 百分比上浮 比例调减
{pricingMode === 'target_price' && (
最终售价(元):
输入最终期望标价,系统自动换算对应比例点
)} {pricingMode === 'add_amount' && (
加价金额(元):
在主站基准价上直接增加的纯利润金额
)} {pricingMode === 'percent' && (
上浮百分比(%):
按基准价百分比上浮,例如 10% 或 20%
)} {pricingMode === 'ratio_delta' && (
调减比例点(点):
高级模式:直接调减 1 元兑换哈夫币比例,比例越小售价越高
)}
{/* Real-time Earnings Comparison Card */}
预计本站标价: ¥{targetPriceYuan.toFixed(2)}
本站 100% 独享溢价: +¥{addAmountYuan.toFixed(2)}
兑换比例换算: 基准 {targetAccount.base_ratio || '-'} → 标价{' '} {Math.max((targetAccount.base_ratio || 0) - markupRatio, 0.01).toFixed(1)}{' '} (调减 {markupRatio.toFixed(1)} 点)
) : ( <>
批量加价模式(已勾选 {selectedRowKeys.length} 件商品): setBatchMode(e.target.value)} buttonStyle="solid" style={{ width: '100%' }} > 按百分比批量上浮 (推荐) 固定加价金额 统一调减比例点
{batchMode === 'percent' && (
统一上浮百分比(%): setBatchValue(v ?? 0)} placeholder="例如 10 或 15" /> 所有勾选商品将按各自底价自动上浮 {batchValue}%,并自动为每件商品精确匹配比例点
)} {batchMode === 'add_amount' && (
统一加价金额(元): setBatchValue(v ?? 0)} placeholder="例如 5.00" /> 所有勾选商品统一定价加价 ¥{batchValue.toFixed(2)},溢价收益全额入账本站
)} {batchMode === 'ratio_delta' && (
统一调减比例点数: setBatchValue(v ?? 0)} placeholder="例如 0.5" /> 统一在各商品原比例基础上调减 {batchValue} 点
)}
💡 已勾选 {selectedRowKeys.length} 件商品,批量调价后差价收益将 100% 归本分销商账户独享!
)} ) }