Files
hfb_sys/distribution-admin/src/pages/Accounts.tsx
T

1249 lines
47 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, { label: string; color: string }> = {
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<Site[]>([])
const [siteID, setSiteID] = useState<number | undefined>(member?.site_id)
const [items, setItems] = useState<AccountDisplay[]>([])
const [loading, setLoading] = useState(false)
const [syncing, setSyncing] = useState(false)
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([])
// Shelf product filter states
const [sourceFilter, setSourceFilter] = useState<string>('all')
const [markupFilter, setMarkupFilter] = useState<string>('all')
const [displayFilter, setDisplayFilter] = useState<string>('all')
const [txFilter, setTxFilter] = useState<string>('all')
const [keyword, setKeyword] = useState<string>('')
// Multi-mode Pricing modal state
const [markupModalVisible, setMarkupModalVisible] = useState(false)
const [targetAccount, setTargetAccount] = useState<AccountDisplay | null>(null)
const [pricingMode, setPricingMode] = useState<'target_price' | 'add_amount' | 'percent' | 'ratio_delta'>('target_price')
const [targetPriceYuan, setTargetPriceYuan] = useState<number>(0)
const [addAmountYuan, setAddAmountYuan] = useState<number>(0)
const [percentMarkup, setPercentMarkup] = useState<number>(0)
const [markupRatio, setMarkupRatio] = useState<number>(0)
const [markupSaving, setMarkupSaving] = useState(false)
// Batch pricing state
const [batchMode, setBatchMode] = useState<'percent' | 'add_amount' | 'ratio_delta'>('percent')
const [batchValue, setBatchValue] = useState<number>(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 (
<Space direction="vertical" className="page" style={{ width: '100%' }}>
{/* Superadmin Cross-tenant Inspection Banner */}
{member?.is_superadmin && sites.length > 0 && (
<Alert
type="info"
showIcon
style={{ marginBottom: 4 }}
message={
<Space align="center" wrap>
<Tag color="purple">平台超管巡检模式</Tag>
<span>切换巡查分销站点:</span>
<Select
style={{ width: 260 }}
value={siteID}
options={sites.map((s) => ({
label: `${s.site_name} (${s.site_code})`,
value: s.id,
}))}
onChange={(val) => {
setSiteID(val)
setSelectedRowKeys([])
void load(val)
}}
/>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
(普通分销商登录时已完全自动锁定所属本站,无需选择分销网站)
</Typography.Text>
</Space>
}
/>
)}
{/* Page Header */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
<div>
<Space align="baseline">
<Typography.Title level={3} style={{ marginBottom: 4 }}>
商品与账号展示
</Typography.Title>
<Tag color="blue" style={{ fontSize: 13, padding: '2px 8px' }}>
<ShopOutlined /> {currentSiteName}
</Tag>
</Space>
<div>
<Typography.Text type="secondary">
管理本站对外展示与销售的货架商品。所有账号源均来自主站及平台共享池,支持一键铺货并自主提价(调小兑换比例)销售,<strong>加价溢价全额归本站所有</strong>。
</Typography.Text>
</div>
</div>
<Button
type="primary"
icon={<SyncOutlined spin={syncing} />}
loading={syncing}
onClick={handleSync}
style={{ background: '#1677ff' }}
>
一键同步主站商品
</Button>
</div>
{/* Shelf Filter Bar */}
<Card size="small">
<Space direction="vertical" style={{ width: '100%' }} size="middle">
{/* Controls row */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 8 }}>
<Space wrap size="small">
{/* 货源归属 */}
<Select
style={{ width: 170 }}
value={sourceFilter}
options={SOURCE_OPTIONS}
onChange={setSourceFilter}
/>
{/* 提价状态 */}
<Select
style={{ width: 170 }}
value={markupFilter}
options={MARKUP_OPTIONS}
onChange={setMarkupFilter}
/>
{/* 展示状态 */}
<Select
style={{ width: 160 }}
value={displayFilter}
options={DISPLAY_OPTIONS}
onChange={setDisplayFilter}
/>
{/* 交易租借状态 */}
<Select
style={{ width: 150 }}
value={txFilter}
options={TX_OPTIONS}
onChange={setTxFilter}
/>
{/* 搜索框 */}
<Input
placeholder="搜索商品编号/账号标题/游戏/区服"
prefix={<SearchOutlined />}
style={{ width: 250 }}
allowClear
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
{counts.hasActiveFilters && (
<Button icon={<ReloadOutlined />} onClick={handleResetFilters}>
重置筛选
</Button>
)}
</Space>
{/* Batch actions */}
{selectedRowKeys.length > 0 && (
<Space>
<Typography.Text strong style={{ color: '#1677ff' }}>
已选 {selectedRowKeys.length} 项
</Typography.Text>
<Button
size="small"
type="primary"
ghost
icon={<DollarCircleOutlined />}
onClick={() => handleOpenMarkup()}
>
批量加价
</Button>
<Button
size="small"
icon={<CheckCircleOutlined />}
onClick={() => handleBatchUpdate('enabled')}
>
批量展示
</Button>
<Button
size="small"
danger
icon={<StopOutlined />}
onClick={() => handleBatchUpdate('disabled')}
>
批量下架
</Button>
<Button size="small" type="link" onClick={() => setSelectedRowKeys([])}>
取消勾选
</Button>
</Space>
)}
</div>
<Divider style={{ margin: '4px 0' }} />
{/* Quick Statistics and Data Scope Badges */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 8 }}>
<Space wrap size={4}>
<Typography.Text type="secondary" style={{ fontSize: 12, marginRight: 4 }}>
<FilterOutlined /> 货架概览:
</Typography.Text>
<Tag color="default">货架总计 {counts.total} 件</Tag>
<Tag color="green">主站自营 {counts.main} 件</Tag>
<Tag color="purple">本站收号 {counts.self} 件</Tag>
{counts.collab > 0 && <Tag color="blue">合作分站 {counts.collab} 件</Tag>}
<Tag color="gold">已自主加价 {counts.marked} 件</Tag>
<Tag color="cyan">正常展示中 {counts.enabled} 件</Tag>
</Space>
{counts.hasActiveFilters && (
<Tag color="processing" style={{ fontWeight: 'bold' }}>
已按条件筛选:展示 {counts.filtered} / {counts.total} 件
</Tag>
)}
</div>
</Space>
</Card>
{/* Products Table */}
<Card>
<Table
rowKey="id"
loading={loading}
dataSource={filteredItems}
scroll={{ x: 1200 }}
locale={{
emptyText: (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
counts.hasActiveFilters
? '没有符合当前筛选条件的货架商品,请尝试重置筛选条件'
: '当前分站货架暂无商品。点击下方按钮一键同步主站共享池,即可在此设置提价并对外展示。'
}
>
{counts.hasActiveFilters ? (
<Button icon={<ReloadOutlined />} onClick={handleResetFilters}>
重置筛选条件
</Button>
) : (
<Button
type="primary"
icon={<SyncOutlined spin={syncing} />}
loading={syncing}
onClick={handleSync}
>
一键同步主站商品
</Button>
)}
</Empty>
),
}}
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) => (
<div style={{ whiteSpace: 'nowrap' }}>
<div style={{ display: 'inline-flex', alignItems: 'center', whiteSpace: 'nowrap', marginBottom: 4 }}>
<Typography.Text
copyable={{ text: v || String(row.listing_id), tooltips: ['复制商品编号', '已复制'] }}
strong
className="mono-code"
style={{
fontSize: 13,
color: '#1677ff',
whiteSpace: 'nowrap',
wordBreak: 'keep-all',
display: 'inline-flex',
alignItems: 'center',
}}
>
{v || `#${row.listing_id}`}
</Typography.Text>
</div>
<div>
{row.in_transaction ? (
<Tag color="warning" style={{ margin: 0, fontSize: 11 }}>
出租中
</Tag>
) : (
<Tag color="green" style={{ margin: 0, fontSize: 11 }}>
在售空闲
</Tag>
)}
</div>
</div>
),
},
{
title: '货源归属',
width: 110,
render: (_: unknown, row: AccountDisplay) => {
if (row.listing_site_id === null || row.listing_site_id === undefined) {
return (
<Tooltip title="来自主站官方统一共享池,分销商可提价转售">
<Tag color="green">主站自营</Tag>
</Tooltip>
)
}
if (row.listing_site_id === siteID) {
return (
<Tooltip title="本分销站专属收录上架的号源">
<Tag color="purple">本站收号</Tag>
</Tooltip>
)
}
return (
<Tooltip title="其他合作分销站铺过来的共享号源">
<Tag color="blue">合作分站</Tag>
</Tooltip>
)
},
},
{
title: '账号信息',
dataIndex: 'title',
width: 260,
render: (_: unknown, row: AccountDisplay) => (
<div>
<Typography.Text
strong
style={{ fontSize: 14, lineHeight: '20px', display: 'block' }}
ellipsis={{ tooltip: row.title }}
>
{row.title || `账号 #${row.account_id}`}
</Typography.Text>
<Space size={4} style={{ marginTop: 4 }} wrap>
<Tag color="blue">{formatGameName(row.game_name)}</Tag>
{row.server_region && <Tag>{row.server_region}</Tag>}
{row.login_platform && <Tag color="cyan">{formatLoginPlatform(row.login_platform)}</Tag>}
</Space>
</div>
),
},
{
title: '段位 / 资产',
width: 130,
render: (_: unknown, row: AccountDisplay) => (
<div>
<div>{row.rank_level ? <Tag color="purple">{row.rank_level}</Tag> : '-'}</div>
{row.haf_coin_amount !== undefined && row.haf_coin_amount > 0 && (
<Typography.Text type="secondary" style={{ fontSize: 12, whiteSpace: 'nowrap', display: 'block', marginTop: 2 }}>
哈夫币: {row.haf_coin_amount.toLocaleString()}
</Typography.Text>
)}
</div>
),
},
{
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 (
<div>
<Typography.Text strong style={{ color: '#cf1322', fontSize: 15, whiteSpace: 'nowrap', display: 'block' }}>
{formatMoney(finalPrice)}
</Typography.Text>
<div style={{ fontSize: 12, color: '#888', whiteSpace: 'nowrap', marginTop: 2 }}>
押金: {formatMoney(row.deposit_amount_cent)}
</div>
{hasMarkup && (
<div style={{ fontSize: 11, color: '#389e0d', marginTop: 2, whiteSpace: 'nowrap' }}>
含加价: +{formatMoney(row.markup_amount_cent)}
</div>
)}
</div>
)
},
},
{
title: '加价比例',
dataIndex: 'shelf_ratio_delta',
width: 110,
render: (v: number | undefined) => {
if (v && v > 0) {
return (
<Tag color="gold" style={{ fontWeight: 'bold' }}>
+{v} 比例点
</Tag>
)
}
return <span style={{ color: '#aaa', fontSize: 12 }}>无加价 (基准)</span>
},
},
{
title: '主站状态',
dataIndex: 'listing_status',
width: 110,
render: (v: string | undefined) => {
const conf = MAIN_LISTING_STATUS_MAP[v || ''] || { label: v || '未知', color: 'default' }
return <Tag color={conf.color}>{conf.label}</Tag>
},
},
{
title: '分销展示',
dataIndex: 'display_status',
width: 100,
render: (v: string) => (
<Tag color={v === 'enabled' ? 'success' : 'default'}>
{v === 'enabled' ? '展示中' : '已下架'}
</Tag>
),
},
{
title: '操作',
width: 110,
fixed: 'right' as const,
render: (_: unknown, row: AccountDisplay) => (
<Space size={2}>
<Button
type="link"
size="small"
icon={<EditOutlined />}
style={{ padding: 0 }}
onClick={() => handleOpenMarkup(row)}
>
加价
</Button>
<Button
type="link"
size="small"
danger={row.display_status === 'enabled'}
style={{ padding: 0 }}
onClick={() => toggle(row)}
>
{row.display_status === 'enabled' ? '下架' : '展示'}
</Button>
</Space>
),
},
]}
/>
</Card>
{/* Rich Multi-mode Markup Modal */}
<Modal
title={
<Space>
<DollarCircleOutlined style={{ color: '#52c41a' }} />
<span>{targetAccount ? `商品加价设置 - ${targetAccount.title || targetAccount.listing_no}` : `批量调价 (${selectedRowKeys.length} 个账号商品)`}</span>
</Space>
}
open={markupModalVisible}
onCancel={() => setMarkupModalVisible(false)}
onOk={handleSaveMarkup}
confirmLoading={markupSaving}
okText="保存并同步货架"
cancelText="取消"
width={580}
destroyOnClose
>
<Space direction="vertical" style={{ width: '100%', padding: '8px 0' }} size="middle">
<Alert
type="success"
showIcon
message="分销商 100% 独享溢价说明"
description="在主站基准租金之上增加的全部溢价金额(加价差价),100% 独立结算至本分销商账户,不与任何第三方分成!"
/>
{targetAccount ? (
<>
<div
style={{
background: '#f8fafc',
border: '1px solid #e2e8f0',
borderRadius: 6,
padding: '12px 16px',
}}
>
<Row gutter={[16, 8]}>
<Col span={24}>
<Typography.Text strong style={{ fontSize: 14 }}>
{targetAccount.title || targetAccount.listing_no}
</Typography.Text>
</Col>
<Col span={12}>
<Typography.Text type="secondary">游戏区服:</Typography.Text>
<span>{formatGameName(targetAccount.game_name) || '-'} / {targetAccount.server_region || '-'}</span>
</Col>
<Col span={12}>
<Typography.Text type="secondary">哈夫币资产:</Typography.Text>
<span>
{(targetAccount.haf_coin_amount || 0).toLocaleString()} 币
<span style={{ color: '#8c8c8c', marginLeft: 4 }}>
({((targetAccount.haf_coin_amount || 0) / 10000).toFixed(1)} 万)
</span>
</span>
</Col>
<Col span={12}>
<Typography.Text type="secondary">主站基准价:</Typography.Text>
<strong style={{ color: '#1677ff' }}>¥{((targetAccount.price_cent || 0) / 100).toFixed(2)}</strong>
</Col>
<Col span={12}>
<Typography.Text type="secondary">主站基准比例:</Typography.Text>
<span>{targetAccount.base_ratio || '-'} 点</span>
</Col>
</Row>
</div>
<div>
<Typography.Text strong style={{ display: 'block', marginBottom: 8 }}>
加价模式选择:
</Typography.Text>
<Radio.Group
value={pricingMode}
onChange={(e) => setPricingMode(e.target.value)}
buttonStyle="solid"
style={{ width: '100%' }}
>
<Radio.Button value="target_price" style={{ width: '25%', textAlign: 'center' }}>期望售价</Radio.Button>
<Radio.Button value="add_amount" style={{ width: '25%', textAlign: 'center' }}>固定加价</Radio.Button>
<Radio.Button value="percent" style={{ width: '25%', textAlign: 'center' }}>百分比上浮</Radio.Button>
<Radio.Button value="ratio_delta" style={{ width: '25%', textAlign: 'center' }}>比例调减</Radio.Button>
</Radio.Group>
</div>
<div style={{ background: '#fafafa', padding: 12, borderRadius: 6 }}>
{pricingMode === 'target_price' && (
<div>
<div style={{ marginBottom: 6 }}>
<Typography.Text strong>最终售价(元):</Typography.Text>
</div>
<InputNumber
prefix="¥"
min={Number(((targetAccount.price_cent || 0) / 100).toFixed(2))}
precision={2}
step={1}
style={{ width: '100%' }}
value={targetPriceYuan}
onChange={onTargetPriceChange}
placeholder="输入租客最终看到的标价"
/>
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 4 }}>
输入最终期望标价,系统自动换算对应比例点
</Typography.Text>
</div>
)}
{pricingMode === 'add_amount' && (
<div>
<div style={{ marginBottom: 6 }}>
<Typography.Text strong>加价金额(元):</Typography.Text>
</div>
<InputNumber
prefix="+¥"
min={0}
precision={2}
step={1}
style={{ width: '100%' }}
value={addAmountYuan}
onChange={onAddAmountChange}
placeholder="输入在主站价格基础上增加的金额"
/>
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 4 }}>
在主站基准价上直接增加的纯利润金额
</Typography.Text>
</div>
)}
{pricingMode === 'percent' && (
<div>
<div style={{ marginBottom: 6 }}>
<Typography.Text strong>上浮百分比(%):</Typography.Text>
</div>
<InputNumber
prefix="+"
addonAfter="%"
min={0}
max={500}
precision={1}
step={5}
style={{ width: '100%' }}
value={percentMarkup}
onChange={onPercentMarkupChange}
placeholder="例如 15"
/>
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 4 }}>
按基准价百分比上浮,例如 10% 或 20%
</Typography.Text>
</div>
)}
{pricingMode === 'ratio_delta' && (
<div>
<div style={{ marginBottom: 6 }}>
<Typography.Text strong>调减比例点(点):</Typography.Text>
</div>
<InputNumber
min={0}
max={100}
precision={1}
step={0.1}
addonAfter="点"
style={{ width: '100%' }}
value={markupRatio}
onChange={onMarkupRatioChange}
placeholder="0"
/>
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 4 }}>
高级模式:直接调减 1 元兑换哈夫币比例,比例越小售价越高
</Typography.Text>
</div>
)}
</div>
{/* Real-time Earnings Comparison Card */}
<div
style={{
padding: '14px 16px',
background: '#f6ffed',
borderRadius: 6,
border: '1px solid #b7eb8f',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
<Typography.Text type="secondary">预计本站标价:</Typography.Text>
<Typography.Text strong style={{ fontSize: 16, color: '#cf1322' }}>
¥{targetPriceYuan.toFixed(2)}
</Typography.Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
<Typography.Text type="secondary">本站 100% 独享溢价:</Typography.Text>
<Typography.Text strong style={{ fontSize: 18, color: '#389e0d' }}>
+¥{addAmountYuan.toFixed(2)}
</Typography.Text>
</div>
<Divider style={{ margin: '8px 0' }} />
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, color: '#595959' }}>
<span>兑换比例换算:</span>
<span>
基准 {targetAccount.base_ratio || '-'} → 标价{' '}
<strong>{Math.max((targetAccount.base_ratio || 0) - markupRatio, 0.01).toFixed(1)}</strong>{' '}
(调减 {markupRatio.toFixed(1)} 点)
</span>
</div>
</div>
</>
) : (
<>
<div>
<Typography.Text strong style={{ display: 'block', marginBottom: 8 }}>
批量加价模式(已勾选 {selectedRowKeys.length} 件商品):
</Typography.Text>
<Radio.Group
value={batchMode}
onChange={(e) => setBatchMode(e.target.value)}
buttonStyle="solid"
style={{ width: '100%' }}
>
<Radio.Button value="percent" style={{ width: '33.3%', textAlign: 'center' }}>
按百分比批量上浮 (推荐)
</Radio.Button>
<Radio.Button value="add_amount" style={{ width: '33.3%', textAlign: 'center' }}>
固定加价金额
</Radio.Button>
<Radio.Button value="ratio_delta" style={{ width: '33.3%', textAlign: 'center' }}>
统一调减比例点
</Radio.Button>
</Radio.Group>
</div>
<div style={{ background: '#fafafa', padding: 16, borderRadius: 6 }}>
{batchMode === 'percent' && (
<div>
<Typography.Text strong style={{ display: 'block', marginBottom: 6 }}>
统一上浮百分比(%):
</Typography.Text>
<InputNumber
prefix="+"
addonAfter="%"
min={0}
max={500}
precision={1}
step={5}
style={{ width: '100%' }}
value={batchValue}
onChange={(v) => setBatchValue(v ?? 0)}
placeholder="例如 10 或 15"
/>
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 6 }}>
所有勾选商品将按各自底价自动上浮 {batchValue}%,并自动为每件商品精确匹配比例点
</Typography.Text>
</div>
)}
{batchMode === 'add_amount' && (
<div>
<Typography.Text strong style={{ display: 'block', marginBottom: 6 }}>
统一加价金额(元):
</Typography.Text>
<InputNumber
prefix="+¥"
addonAfter="元"
min={0}
precision={2}
step={1}
style={{ width: '100%' }}
value={batchValue}
onChange={(v) => setBatchValue(v ?? 0)}
placeholder="例如 5.00"
/>
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 6 }}>
所有勾选商品统一定价加价 ¥{batchValue.toFixed(2)},溢价收益全额入账本站
</Typography.Text>
</div>
)}
{batchMode === 'ratio_delta' && (
<div>
<Typography.Text strong style={{ display: 'block', marginBottom: 6 }}>
统一调减比例点数:
</Typography.Text>
<InputNumber
min={0}
max={100}
precision={1}
step={0.1}
addonAfter="点"
style={{ width: '100%' }}
value={batchValue}
onChange={(v) => setBatchValue(v ?? 0)}
placeholder="例如 0.5"
/>
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 6 }}>
统一在各商品原比例基础上调减 {batchValue} 点
</Typography.Text>
</div>
)}
</div>
<div
style={{
padding: '12px 16px',
background: '#f6ffed',
borderRadius: 6,
border: '1px solid #b7eb8f',
fontSize: 13,
color: '#389e0d',
}}
>
💡 已勾选 <strong>{selectedRowKeys.length}</strong> 件商品,批量调价后差价收益将 100% 归本分销商账户独享!
</div>
</>
)}
</Space>
</Modal>
</Space>
)
}