- 文档:升级至 v0.7,固化号主保底、主站提价利润分账(35/40/25)与分站加价溢价独享逻辑 - 定价:修正货架兑换比例调价方向为调减比例提价(site_ratio = base_ratio - delta) - 分账:分成基数剔除号主成本与分站加价,提价利润按规则切分,分站自主加价溢价 100% 归卖出方 - 前端:更新分销后台提价说明文案,提示调减比例提高售价且溢价归本站
456 lines
16 KiB
TypeScript
456 lines
16 KiB
TypeScript
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<string, { label: string; color: string }> = {
|
||
enabled: { label: '展示中', color: 'success' },
|
||
disabled: { label: '已下架', color: 'default' },
|
||
sold: { label: '已售出', color: 'blue' },
|
||
offline: { label: '已离线', color: 'error' },
|
||
}
|
||
|
||
export default function Accounts() {
|
||
const [sites, setSites] = useState<Site[]>([])
|
||
const [siteID, setSiteID] = useState<number>()
|
||
const [status, setStatus] = useState<string>('')
|
||
const [keyword, setKeyword] = useState<string>('')
|
||
const [items, setItems] = useState<AccountDisplay[]>([])
|
||
const [loading, setLoading] = useState(false)
|
||
const [syncing, setSyncing] = useState(false)
|
||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([])
|
||
|
||
// Markup modal state
|
||
const [markupModalVisible, setMarkupModalVisible] = useState(false)
|
||
const [targetAccount, setTargetAccount] = useState<AccountDisplay | null>(null)
|
||
const [markupRatio, setMarkupRatio] = useState<number>(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 (
|
||
<Space direction="vertical" className="page" style={{ width: '100%' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<div>
|
||
<Typography.Title level={3} style={{ marginBottom: 4 }}>
|
||
商品与账号展示
|
||
</Typography.Title>
|
||
<Typography.Text type="secondary">
|
||
管理当前分销网站对外展示的商品。默认同步本站上传货,主站货需显式勾选,不会灌入全站目录。
|
||
</Typography.Text>
|
||
</div>
|
||
<Button
|
||
type="primary"
|
||
icon={<SyncOutlined spin={syncing} />}
|
||
loading={syncing}
|
||
onClick={handleSync}
|
||
style={{ background: '#1677ff' }}
|
||
>
|
||
同步本站上传商品
|
||
</Button>
|
||
</div>
|
||
|
||
<Card size="small">
|
||
<Space wrap style={{ width: '100%', justifyContent: 'space-between' }}>
|
||
<Space wrap>
|
||
<Select
|
||
placeholder="分销网站"
|
||
style={{ width: 220 }}
|
||
value={siteID}
|
||
options={sites.map((site) => ({
|
||
label: `${site.site_name} (${site.site_code})`,
|
||
value: site.id,
|
||
}))}
|
||
onChange={(value) => {
|
||
setSiteID(value)
|
||
setSelectedRowKeys([])
|
||
void load(value, status)
|
||
}}
|
||
/>
|
||
<Select
|
||
allowClear
|
||
placeholder="展示状态"
|
||
style={{ width: 140 }}
|
||
value={status || undefined}
|
||
options={Object.entries(STATUS_LABELS).map(([value, conf]) => ({
|
||
label: conf.label,
|
||
value,
|
||
}))}
|
||
onChange={(value) => {
|
||
setStatus(value ?? '')
|
||
void load(siteID, value ?? '')
|
||
}}
|
||
/>
|
||
<Input
|
||
placeholder="搜索商品编号/标题/区服"
|
||
prefix={<SearchOutlined />}
|
||
style={{ width: 240 }}
|
||
allowClear
|
||
value={keyword}
|
||
onChange={(e) => setKeyword(e.target.value)}
|
||
/>
|
||
</Space>
|
||
|
||
{selectedRowKeys.length > 0 && (
|
||
<Space>
|
||
<Typography.Text type="secondary">已勾选 {selectedRowKeys.length} 项</Typography.Text>
|
||
<Button
|
||
size="small"
|
||
icon={<DollarCircleOutlined />}
|
||
onClick={() => handleOpenMarkup()}
|
||
>
|
||
批量加价
|
||
</Button>
|
||
<Button
|
||
size="small"
|
||
icon={<CheckCircleOutlined />}
|
||
onClick={() => handleBatchUpdate('enabled')}
|
||
>
|
||
批量展示
|
||
</Button>
|
||
<Button
|
||
size="small"
|
||
danger
|
||
icon={<StopOutlined />}
|
||
onClick={() => handleBatchUpdate('disabled')}
|
||
>
|
||
批量下架
|
||
</Button>
|
||
</Space>
|
||
)}
|
||
</Space>
|
||
</Card>
|
||
|
||
<Card>
|
||
<Table
|
||
rowKey="id"
|
||
loading={loading}
|
||
dataSource={filteredItems}
|
||
rowSelection={{
|
||
selectedRowKeys,
|
||
onChange: setSelectedRowKeys,
|
||
}}
|
||
pagination={{
|
||
pageSize: 20,
|
||
showTotal: (total) => `共 ${total} 个账号商品`,
|
||
showQuickJumper: true,
|
||
}}
|
||
columns={[
|
||
{
|
||
title: '商品编号',
|
||
dataIndex: 'listing_no',
|
||
width: 140,
|
||
render: (v: string | undefined, row: AccountDisplay) => (
|
||
<Space direction="vertical" size={2}>
|
||
<Typography.Text copyable strong>
|
||
{v || `#${row.listing_id}`}
|
||
</Typography.Text>
|
||
{row.in_transaction && <Tag color="warning">出租中</Tag>}
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '账号信息',
|
||
dataIndex: 'title',
|
||
minWidth: 240,
|
||
render: (_: unknown, row: AccountDisplay) => (
|
||
<div>
|
||
<Typography.Text strong style={{ display: 'block', fontSize: 14 }}>
|
||
{row.title || `账号 #${row.account_id}`}
|
||
</Typography.Text>
|
||
<Space size={4} style={{ marginTop: 4 }}>
|
||
<Tag color="blue">{row.game_name || '默认游戏'}</Tag>
|
||
{row.server_region && <Tag>{row.server_region}</Tag>}
|
||
{row.login_platform && <Tag>{row.login_platform}</Tag>}
|
||
</Space>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
title: '段位 / 资产',
|
||
width: 140,
|
||
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 }}>
|
||
哈夫币: {row.haf_coin_amount.toLocaleString()}
|
||
</Typography.Text>
|
||
)}
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
title: '租金 / 押金',
|
||
width: 160,
|
||
render: (_: unknown, row: AccountDisplay) => (
|
||
<div>
|
||
<Typography.Text strong style={{ color: '#cf1322' }}>
|
||
{formatMoney(row.site_price_cent || row.price_cent)}
|
||
</Typography.Text>
|
||
<div style={{ fontSize: 12, color: '#666' }}>
|
||
押金: {formatMoney(row.deposit_amount_cent)}
|
||
</div>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
title: '加价比例',
|
||
dataIndex: 'shelf_ratio_delta',
|
||
width: 120,
|
||
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) => (
|
||
<Tag color={v === 'published' ? 'green' : 'default'}>
|
||
{v === 'published' ? '主站在售' : v || '未知'}
|
||
</Tag>
|
||
),
|
||
},
|
||
{
|
||
title: '分销展示',
|
||
dataIndex: 'display_status',
|
||
width: 110,
|
||
render: (v: string) => {
|
||
const conf = STATUS_LABELS[v] ?? { label: v, color: 'default' }
|
||
return <Tag color={conf.color}>{conf.label}</Tag>
|
||
},
|
||
},
|
||
{
|
||
title: '操作',
|
||
width: 160,
|
||
render: (_: unknown, row: AccountDisplay) => (
|
||
<Space size={2}>
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
icon={<EditOutlined />}
|
||
onClick={() => handleOpenMarkup(row)}
|
||
>
|
||
加价
|
||
</Button>
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
danger={row.display_status === 'enabled'}
|
||
onClick={() => toggle(row)}
|
||
>
|
||
{row.display_status === 'enabled' ? '下架' : '展示'}
|
||
</Button>
|
||
</Space>
|
||
),
|
||
},
|
||
]}
|
||
/>
|
||
</Card>
|
||
|
||
<Modal
|
||
title={targetAccount ? `设置提价 - ${targetAccount.title || targetAccount.listing_no}` : `批量提价 (${selectedRowKeys.length} 个账号商品)`}
|
||
open={markupModalVisible}
|
||
onCancel={() => setMarkupModalVisible(false)}
|
||
onOk={handleSaveMarkup}
|
||
confirmLoading={markupSaving}
|
||
okText="保存提价"
|
||
destroyOnClose
|
||
>
|
||
<Space direction="vertical" style={{ width: '100%', padding: '12px 0' }}>
|
||
<Typography.Paragraph type="secondary" style={{ marginBottom: 8 }}>
|
||
分站提价是通过调小「1 元兑换哈夫币」的兑换比例来提高销售价。比例越小售价越高;<strong>提价多挣的差价收益 100% 归本分站所有</strong>。
|
||
</Typography.Paragraph>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||
<Typography.Text strong>提价调减比例:</Typography.Text>
|
||
<InputNumber
|
||
min={0}
|
||
max={100}
|
||
precision={1}
|
||
style={{ width: 180 }}
|
||
value={markupRatio}
|
||
onChange={(val) => setMarkupRatio(val ?? 0)}
|
||
addonAfter="比例点"
|
||
placeholder="0"
|
||
/>
|
||
</div>
|
||
{targetAccount && (
|
||
<div style={{ marginTop: 8, padding: '8px 12px', background: '#f6ffed', borderRadius: 4, border: '1px solid #b7eb8f', fontSize: 12 }}>
|
||
主站基准比例 {targetAccount.base_ratio || '-'} → 分站提价后比例{' '}
|
||
<strong>{Math.max((targetAccount.base_ratio || 0) - markupRatio, 0.1).toFixed(1)}</strong>,
|
||
预计售价{' '}
|
||
<strong style={{ color: '#cf1322' }}>
|
||
{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)}
|
||
</strong>
|
||
(加价溢价全额归本站独享)
|
||
</div>
|
||
)}
|
||
</Space>
|
||
</Modal>
|
||
</Space>
|
||
)
|
||
}
|