fix(distribution-admin): bind site context to member and enhance shelf product filters
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
WalletOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { getMe, logout, type CurrentMember, getStoredToken } from './api'
|
||||
import { MemberContext } from './MemberContext'
|
||||
import Dashboard from './pages/Dashboard'
|
||||
import Sites from './pages/Sites'
|
||||
import Rules from './pages/Rules'
|
||||
@@ -61,7 +62,8 @@ function MainLayout({ member, onLogout }: { member: CurrentMember; onLogout: ()
|
||||
: '分销成员'
|
||||
|
||||
return (
|
||||
<Layout className="app">
|
||||
<MemberContext.Provider value={member}>
|
||||
<Layout className="app">
|
||||
<Layout.Sider breakpoint="lg" collapsedWidth="0">
|
||||
<div className="brand">
|
||||
<ShopOutlined style={{ fontSize: 20, color: '#1677ff' }} />
|
||||
@@ -149,6 +151,7 @@ function MainLayout({ member, onLogout }: { member: CurrentMember; onLogout: ()
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</MemberContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
import type { CurrentMember } from './api'
|
||||
|
||||
export const MemberContext = createContext<CurrentMember | null>(null)
|
||||
|
||||
export function useMember(): CurrentMember | null {
|
||||
return useContext(MemberContext)
|
||||
}
|
||||
@@ -1,6 +1,33 @@
|
||||
import { useEffect, useState, useMemo } from 'react'
|
||||
import { Card, Table, Typography, Space, Select, Tag, message, Button, Input, Modal, InputNumber, Empty } from 'antd'
|
||||
import { SyncOutlined, SearchOutlined, CheckCircleOutlined, StopOutlined, EditOutlined, DollarCircleOutlined } from '@ant-design/icons'
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Typography,
|
||||
Space,
|
||||
Select,
|
||||
Tag,
|
||||
message,
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
InputNumber,
|
||||
Empty,
|
||||
Alert,
|
||||
Tooltip,
|
||||
Divider,
|
||||
} from 'antd'
|
||||
import {
|
||||
SyncOutlined,
|
||||
SearchOutlined,
|
||||
CheckCircleOutlined,
|
||||
StopOutlined,
|
||||
EditOutlined,
|
||||
DollarCircleOutlined,
|
||||
ReloadOutlined,
|
||||
FilterOutlined,
|
||||
ShopOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
listAccountDisplays,
|
||||
listSites,
|
||||
@@ -9,70 +36,128 @@ import {
|
||||
updateAccountMarkup,
|
||||
batchUpdateAccountMarkup,
|
||||
formatMoney,
|
||||
getMe,
|
||||
type AccountDisplay,
|
||||
type Site,
|
||||
} from '../api'
|
||||
import { useMember } from '../MemberContext'
|
||||
|
||||
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' },
|
||||
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' },
|
||||
]
|
||||
|
||||
function formatGameName(name?: string) {
|
||||
if (!name) return '默认游戏'
|
||||
if (name === 'delta_force') return '三角洲行动'
|
||||
return name
|
||||
}
|
||||
|
||||
export default function Accounts() {
|
||||
const member = useMember()
|
||||
const [sites, setSites] = useState<Site[]>([])
|
||||
const [siteID, setSiteID] = useState<number>()
|
||||
const [status, setStatus] = useState<string>('')
|
||||
const [keyword, setKeyword] = useState<string>('')
|
||||
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>('')
|
||||
|
||||
// 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) {
|
||||
async function load(site?: number) {
|
||||
if (!site) return
|
||||
setLoading(true)
|
||||
try {
|
||||
setItems(await listAccountDisplays(site, statusFilter))
|
||||
// Fetch full shelf goods to allow fast client-side multidimensional filtering
|
||||
const rows = await listAccountDisplays(site, '')
|
||||
setItems(rows)
|
||||
} catch {
|
||||
message.error('账号展示加载失败')
|
||||
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(() => {
|
||||
listSites()
|
||||
.then((rows) => {
|
||||
setSites(rows)
|
||||
if (rows.length > 0) {
|
||||
setSiteID(rows[0].id)
|
||||
void load(rows[0].id)
|
||||
async function init() {
|
||||
let currentMember = member
|
||||
if (!currentMember) {
|
||||
try {
|
||||
currentMember = await getMe()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
})
|
||||
.catch(() => message.error('站点加载失败'))
|
||||
}, [])
|
||||
}
|
||||
|
||||
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('请先选择分销站点')
|
||||
message.warning('无法获取当前分销站点信息')
|
||||
return
|
||||
}
|
||||
setSyncing(true)
|
||||
try {
|
||||
const res = await syncSiteAccounts(siteID)
|
||||
message.success(
|
||||
`同步完成!成功同步新增商品 ${res.synced_count} 个,已在货架 ${res.skipped_count} 个(平台在售共 ${res.total_active} 个)`,
|
||||
`同步完成!成功同步新增商品 ${res.synced_count} 个,已在货架 ${res.skipped_count} 个(主站共享池在售共 ${res.total_active} 个)`,
|
||||
5,
|
||||
)
|
||||
await load(siteID, status)
|
||||
await load(siteID)
|
||||
} catch (error: unknown) {
|
||||
const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message
|
||||
message.error(respMessage ?? '同步主站商品失败')
|
||||
@@ -85,8 +170,8 @@ export default function Accounts() {
|
||||
const next = item.display_status === 'enabled' ? 'disabled' : 'enabled'
|
||||
try {
|
||||
await updateAccountDisplay(item.id, next)
|
||||
message.success(next === 'enabled' ? '已恢复展示' : '已下架')
|
||||
if (siteID) void load(siteID, status)
|
||||
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 ?? '状态更新失败')
|
||||
@@ -114,7 +199,7 @@ export default function Accounts() {
|
||||
}
|
||||
message.success(`已完成 ${successCount}/${selectedRowKeys.length} 个账号的${actionLabel}`)
|
||||
setSelectedRowKeys([])
|
||||
if (siteID) void load(siteID, status)
|
||||
if (siteID) void load(siteID)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -137,7 +222,7 @@ export default function Accounts() {
|
||||
async function handleSaveMarkup() {
|
||||
if (!siteID) return
|
||||
if (markupRatio < 0) {
|
||||
message.error('加价比例不能小于0')
|
||||
message.error('提价调减比例不能小于0')
|
||||
return
|
||||
}
|
||||
setMarkupSaving(true)
|
||||
@@ -154,43 +239,174 @@ export default function Accounts() {
|
||||
return
|
||||
}
|
||||
await batchUpdateAccountMarkup(siteID, listingIDs, markupRatio)
|
||||
message.success(`已成功为选中的 ${listingIDs.length} 个商品批量设置加价!`)
|
||||
message.success(`已成功为选中的 ${listingIDs.length} 个商品批量设置提价!`)
|
||||
setSelectedRowKeys([])
|
||||
}
|
||||
setMarkupModalVisible(false)
|
||||
void load(siteID, status)
|
||||
void load(siteID)
|
||||
} catch (error: unknown) {
|
||||
const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message
|
||||
message.error(respMessage || '加价设置保存失败')
|
||||
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])
|
||||
function handleResetFilters() {
|
||||
setSourceFilter('all')
|
||||
setMarkupFilter('all')
|
||||
setDisplayFilter('all')
|
||||
setTxFilter('all')
|
||||
setKeyword('')
|
||||
}
|
||||
|
||||
const currentSite = sites.find((s) => s.id === siteID)
|
||||
// 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)
|
||||
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%' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
{/* 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>
|
||||
<Typography.Title level={3} style={{ marginBottom: 4 }}>
|
||||
商品与账号展示
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
管理当前分销网站对外展示的商品货架。所有账号源均来自主站及平台共享池,支持一键同步主站商品,并进行自主提价(调小比例)销售。
|
||||
</Typography.Text>
|
||||
<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"
|
||||
@@ -203,77 +419,124 @@ export default function Accounts() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Shelf Filter Bar */}
|
||||
<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>
|
||||
<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}
|
||||
/>
|
||||
|
||||
{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>
|
||||
{/* 提价状态 */}
|
||||
<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"
|
||||
@@ -283,16 +546,26 @@ export default function Accounts() {
|
||||
emptyText: (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="当前分站货架暂无商品。点击下方按钮一键同步主站账号池,即可在此设置提价并开始销售。"
|
||||
description={
|
||||
counts.hasActiveFilters
|
||||
? '没有符合当前筛选条件的货架商品,请尝试重置筛选条件'
|
||||
: '当前分站货架暂无商品。点击下方按钮一键同步主站共享池,即可在此设置提价并对外展示。'
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SyncOutlined spin={syncing} />}
|
||||
loading={syncing}
|
||||
onClick={handleSync}
|
||||
>
|
||||
一键同步主站商品
|
||||
</Button>
|
||||
{counts.hasActiveFilters ? (
|
||||
<Button icon={<ReloadOutlined />} onClick={handleResetFilters}>
|
||||
重置筛选条件
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SyncOutlined spin={syncing} />}
|
||||
loading={syncing}
|
||||
onClick={handleSync}
|
||||
>
|
||||
一键同步主站商品
|
||||
</Button>
|
||||
)}
|
||||
</Empty>
|
||||
),
|
||||
}}
|
||||
@@ -315,7 +588,15 @@ export default function Accounts() {
|
||||
<Typography.Text copyable strong>
|
||||
{v || `#${row.listing_id}`}
|
||||
</Typography.Text>
|
||||
{row.in_transaction && <Tag color="warning">出租中</Tag>}
|
||||
{row.in_transaction ? (
|
||||
<Tag color="warning" style={{ margin: 0 }}>
|
||||
出租中
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag color="green" style={{ margin: 0 }}>
|
||||
在售空闲
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
@@ -324,12 +605,24 @@ export default function Accounts() {
|
||||
width: 110,
|
||||
render: (_: unknown, row: AccountDisplay) => {
|
||||
if (row.listing_site_id === null || row.listing_site_id === undefined) {
|
||||
return <Tag color="green">主站自营</Tag>
|
||||
return (
|
||||
<Tooltip title="来自主站官方统一共享池,分销商可提价转售">
|
||||
<Tag color="green">主站自营</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
if (row.listing_site_id === siteID) {
|
||||
return <Tag color="purple">本站收号</Tag>
|
||||
return (
|
||||
<Tooltip title="本分销站专属收录上架的号源">
|
||||
<Tag color="purple">本站收号</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
return <Tag color="blue">合作分站</Tag>
|
||||
return (
|
||||
<Tooltip title="其他合作分销站铺过来的共享号源">
|
||||
<Tag color="blue">合作分站</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -342,7 +635,7 @@ export default function Accounts() {
|
||||
{row.title || `账号 #${row.account_id}`}
|
||||
</Typography.Text>
|
||||
<Space size={4} style={{ marginTop: 4 }}>
|
||||
<Tag color="blue">{row.game_name || '默认游戏'}</Tag>
|
||||
<Tag color="blue">{formatGameName(row.game_name)}</Tag>
|
||||
{row.server_region && <Tag>{row.server_region}</Tag>}
|
||||
{row.login_platform && <Tag>{row.login_platform}</Tag>}
|
||||
</Space>
|
||||
@@ -365,17 +658,26 @@ export default function Accounts() {
|
||||
},
|
||||
{
|
||||
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)}
|
||||
width: 170,
|
||||
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 }}>
|
||||
{formatMoney(finalPrice)}
|
||||
</Typography.Text>
|
||||
<div style={{ fontSize: 12, color: '#666' }}>
|
||||
押金: {formatMoney(row.deposit_amount_cent)}
|
||||
</div>
|
||||
{hasMarkup && (
|
||||
<div style={{ fontSize: 11, color: '#389e0d', marginTop: 2 }}>
|
||||
含提价溢价: +{formatMoney(row.markup_amount_cent)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '加价比例',
|
||||
@@ -385,17 +687,17 @@ export default function Accounts() {
|
||||
if (v && v > 0) {
|
||||
return (
|
||||
<Tag color="gold" style={{ fontWeight: 'bold' }}>
|
||||
+{v}
|
||||
+{v} 比例点
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
return <span style={{ color: '#aaa', fontSize: 12 }}>无加价</span>
|
||||
return <span style={{ color: '#aaa', fontSize: 12 }}>无加价 (基准)</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '主站状态',
|
||||
dataIndex: 'listing_status',
|
||||
width: 110,
|
||||
width: 100,
|
||||
render: (v: string | undefined) => (
|
||||
<Tag color={v === 'published' ? 'green' : 'default'}>
|
||||
{v === 'published' ? '主站在售' : v || '未知'}
|
||||
@@ -405,15 +707,16 @@ export default function Accounts() {
|
||||
{
|
||||
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>
|
||||
},
|
||||
width: 100,
|
||||
render: (v: string) => (
|
||||
<Tag color={v === 'enabled' ? 'success' : 'default'}>
|
||||
{v === 'enabled' ? '展示中' : '已下架'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 160,
|
||||
width: 140,
|
||||
render: (_: unknown, row: AccountDisplay) => (
|
||||
<Space size={2}>
|
||||
<Button
|
||||
@@ -439,8 +742,13 @@ export default function Accounts() {
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Markup Modal */}
|
||||
<Modal
|
||||
title={targetAccount ? `设置提价 - ${targetAccount.title || targetAccount.listing_no}` : `批量提价 (${selectedRowKeys.length} 个账号商品)`}
|
||||
title={
|
||||
targetAccount
|
||||
? `设置提价 - ${targetAccount.title || targetAccount.listing_no}`
|
||||
: `批量提价 (${selectedRowKeys.length} 个账号商品)`
|
||||
}
|
||||
open={markupModalVisible}
|
||||
onCancel={() => setMarkupModalVisible(false)}
|
||||
onOk={handleSaveMarkup}
|
||||
@@ -450,7 +758,8 @@ export default function Accounts() {
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%', padding: '12px 0' }}>
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 8 }}>
|
||||
分站提价是通过调小「1 元兑换哈夫币」的兑换比例来提高销售价。比例越小售价越高;<strong>提价多挣的差价收益 100% 归本分站所有</strong>。
|
||||
分站提价是通过调小「1 元兑换哈夫币」的兑换比例来提高销售价。比例越小售价越高;
|
||||
<strong style={{ color: '#389e0d' }}>提价多挣的差价收益 100% 归本分站所有</strong>。
|
||||
</Typography.Paragraph>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<Typography.Text strong>提价调减比例:</Typography.Text>
|
||||
@@ -466,13 +775,26 @@ export default function Accounts() {
|
||||
/>
|
||||
</div>
|
||||
{targetAccount && (
|
||||
<div style={{ marginTop: 8, padding: '8px 12px', background: '#f6ffed', borderRadius: 4, border: '1px solid #b7eb8f', fontSize: 12 }}>
|
||||
<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>{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)}`
|
||||
? `¥${(
|
||||
(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>
|
||||
(加价溢价全额归本站独享)
|
||||
|
||||
@@ -27,14 +27,17 @@ import {
|
||||
listSites,
|
||||
fetchDashboardAnalytics,
|
||||
formatMoney,
|
||||
getMe,
|
||||
type Site,
|
||||
type DistributionAnalytics,
|
||||
type DistributionOrder,
|
||||
} from '../api'
|
||||
import { useMember } from '../MemberContext'
|
||||
|
||||
export default function Dashboard() {
|
||||
const member = useMember()
|
||||
const [sites, setSites] = useState<Site[]>([])
|
||||
const [siteID, setSiteID] = useState<number>()
|
||||
const [siteID, setSiteID] = useState<number | undefined>(member?.site_id)
|
||||
const [analytics, setAnalytics] = useState<DistributionAnalytics | null>(null)
|
||||
const [recentOrders, setRecentOrders] = useState<DistributionOrder[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -56,18 +59,39 @@ export default function Dashboard() {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
listSites()
|
||||
.then((rows) => {
|
||||
setSites(rows)
|
||||
if (rows.length > 0) {
|
||||
setSiteID(rows[0].id)
|
||||
void loadData(rows[0].id)
|
||||
async function init() {
|
||||
let currentMember = member
|
||||
if (!currentMember) {
|
||||
try {
|
||||
currentMember = await getMe()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
})
|
||||
.catch(() => message.error('站点加载失败'))
|
||||
}, [loadData])
|
||||
}
|
||||
const activeSiteID = currentMember?.site_id
|
||||
if (activeSiteID) {
|
||||
setSiteID(activeSiteID)
|
||||
void loadData(activeSiteID)
|
||||
}
|
||||
|
||||
const selectedSite = sites.find((s) => s.id === siteID)
|
||||
if (currentMember?.is_superadmin) {
|
||||
try {
|
||||
const rows = await listSites()
|
||||
setSites(rows)
|
||||
if (!activeSiteID && rows.length > 0) {
|
||||
setSiteID(rows[0].id)
|
||||
void loadData(rows[0].id)
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
void init()
|
||||
}, [member, loadData])
|
||||
|
||||
const selectedSiteName =
|
||||
member?.site_name || sites.find((s) => s.id === siteID)?.site_name || '当前分销商'
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large" className="page" style={{ width: '100%' }}>
|
||||
@@ -77,20 +101,22 @@ export default function Dashboard() {
|
||||
运营数据大盘
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
当前站点:{selectedSite?.site_name || '默认分销站'} ({selectedSite?.site_code}) · 实时聚合收号、货架、订单与结算收益
|
||||
当前站点:{selectedSiteName} · 实时聚合收号、货架、订单与结算收益
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Space>
|
||||
<Select
|
||||
placeholder="切换分销网站"
|
||||
style={{ width: 220 }}
|
||||
value={siteID}
|
||||
options={sites.map((site) => ({ label: `${site.site_name} (${site.site_code})`, value: site.id }))}
|
||||
onChange={(val) => {
|
||||
setSiteID(val)
|
||||
void loadData(val)
|
||||
}}
|
||||
/>
|
||||
{member?.is_superadmin && sites.length > 1 && (
|
||||
<Select
|
||||
placeholder="切换分销网站"
|
||||
style={{ width: 220 }}
|
||||
value={siteID}
|
||||
options={sites.map((site) => ({ label: `${site.site_name} (${site.site_code})`, value: site.id }))}
|
||||
onChange={(val) => {
|
||||
setSiteID(val)
|
||||
void loadData(val)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Link to="/wallet">
|
||||
<Button type="primary" icon={<WalletOutlined />}>
|
||||
去提现
|
||||
|
||||
@@ -54,18 +54,20 @@ const LISTING_STATUS_LABELS: Record<string, { label: string; color: string }> =
|
||||
}
|
||||
|
||||
export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
||||
const member = useMember()
|
||||
const [sites, setSites] = useState<Site[]>([])
|
||||
const [siteID, setSiteID] = useState<number>()
|
||||
const [activeTab, setActiveTab] = useState<'received_listings' | 'orders'>('received_listings')
|
||||
|
||||
const [siteID, setSiteID] = useState<number | undefined>(member?.site_id)
|
||||
|
||||
// Orders Tab State
|
||||
const [status, setStatus] = useState<string>('')
|
||||
const [orders, setOrders] = useState<DistributionOrder[]>([])
|
||||
const [orderTotal, setOrderTotal] = useState(0)
|
||||
const [orderPage, setOrderPage] = useState(1)
|
||||
const [orderLoading, setOrderLoading] = useState(false)
|
||||
const [status, setStatus] = useState<string>('')
|
||||
const [orderKeyword, setOrderKeyword] = useState<string>('')
|
||||
|
||||
// Received Listings Tab State
|
||||
const [activeTab, setActiveTab] = useState<'received_listings' | 'orders'>('received_listings')
|
||||
const [receivedListings, setReceivedListings] = useState<ReceivedListing[]>([])
|
||||
const [recvTotal, setRecvTotal] = useState(0)
|
||||
const [recvPage, setRecvPage] = useState(1)
|
||||
@@ -77,9 +79,17 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
||||
const pageSize = 20
|
||||
|
||||
const loadOrders = useCallback(async (targetPage: number) => {
|
||||
if (!siteID) return
|
||||
setOrderLoading(true)
|
||||
try {
|
||||
const result = await listOrders({ kind, status, siteID, page: targetPage, pageSize })
|
||||
const result = await listOrders({
|
||||
kind,
|
||||
status,
|
||||
siteID,
|
||||
page: targetPage,
|
||||
pageSize,
|
||||
keyword: orderKeyword || undefined
|
||||
})
|
||||
setOrders(result.items)
|
||||
setOrderTotal(result.total)
|
||||
setOrderPage(result.page)
|
||||
@@ -88,7 +98,7 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
||||
} finally {
|
||||
setOrderLoading(false)
|
||||
}
|
||||
}, [kind, status, siteID])
|
||||
}, [kind, status, siteID, orderKeyword])
|
||||
|
||||
const loadReceivedListings = useCallback(async (targetPage: number) => {
|
||||
if (!siteID) return
|
||||
@@ -98,9 +108,9 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
||||
siteID,
|
||||
page: targetPage,
|
||||
pageSize,
|
||||
status: recvStatus,
|
||||
reviewStatus: recvReviewStatus,
|
||||
keyword: recvKeyword,
|
||||
status: recvStatus || undefined,
|
||||
reviewStatus: recvReviewStatus || undefined,
|
||||
keyword: recvKeyword || undefined,
|
||||
})
|
||||
setReceivedListings(result.items)
|
||||
setRecvTotal(result.total)
|
||||
@@ -113,15 +123,37 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
||||
}, [siteID, recvStatus, recvReviewStatus, recvKeyword])
|
||||
|
||||
useEffect(() => {
|
||||
listSites()
|
||||
.then((rows) => {
|
||||
setSites(rows)
|
||||
if (rows.length > 0) setSiteID(rows[0].id)
|
||||
})
|
||||
.catch(() => message.error('站点加载失败'))
|
||||
}, [])
|
||||
async function init() {
|
||||
let currentMember = member
|
||||
if (!currentMember) {
|
||||
try {
|
||||
currentMember = await getMe()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
const activeSiteID = currentMember?.site_id
|
||||
if (activeSiteID) {
|
||||
setSiteID(activeSiteID)
|
||||
}
|
||||
|
||||
if (currentMember?.is_superadmin) {
|
||||
try {
|
||||
const rows = await listSites()
|
||||
setSites(rows)
|
||||
if (!activeSiteID && rows.length > 0) {
|
||||
setSiteID(rows[0].id)
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
void init()
|
||||
}, [member])
|
||||
|
||||
useEffect(() => {
|
||||
if (!siteID) return
|
||||
if (kind === 'receive') {
|
||||
if (activeTab === 'received_listings') {
|
||||
void loadReceivedListings(1)
|
||||
@@ -165,13 +197,15 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
||||
: '查看分销订单交易及财务明细'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<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)}
|
||||
/>
|
||||
{member?.is_superadmin && sites.length > 1 && (
|
||||
<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)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{kind === 'receive' ? (
|
||||
|
||||
@@ -7,15 +7,18 @@ import {
|
||||
listSettlementItems,
|
||||
listSites,
|
||||
settleBatch,
|
||||
getMe,
|
||||
type SettlementBatch,
|
||||
type SettlementItem,
|
||||
type Site,
|
||||
} from '../api'
|
||||
import { useMember } from '../MemberContext'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
|
||||
export default function Settlements() {
|
||||
const member = useMember()
|
||||
const [sites, setSites] = useState<Site[]>([])
|
||||
const [siteID, setSiteID] = useState<number>()
|
||||
const [siteID, setSiteID] = useState<number | undefined>(member?.site_id)
|
||||
const [batches, setBatches] = useState<SettlementBatch[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [range, setRange] = useState<[Dayjs | null, Dayjs | null] | null>(null)
|
||||
@@ -34,16 +37,36 @@ export default function Settlements() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
listSites()
|
||||
.then((rows) => {
|
||||
setSites(rows)
|
||||
if (rows.length > 0) {
|
||||
setSiteID(rows[0].id)
|
||||
void load(rows[0].id)
|
||||
async function init() {
|
||||
let currentMember = member
|
||||
if (!currentMember) {
|
||||
try {
|
||||
currentMember = await getMe()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
})
|
||||
.catch(() => message.error('站点加载失败'))
|
||||
}, [])
|
||||
}
|
||||
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 handleCreate() {
|
||||
if (!siteID || !range?.[0] || !range?.[1]) {
|
||||
@@ -84,16 +107,18 @@ export default function Settlements() {
|
||||
<Space direction="vertical" className="page">
|
||||
<Typography.Title level={3}>结算管理</Typography.Title>
|
||||
<Space>
|
||||
<Select
|
||||
placeholder="分销网站"
|
||||
style={{ width: 200 }}
|
||||
value={siteID}
|
||||
options={sites.map((site) => ({ label: site.site_name, value: site.id }))}
|
||||
onChange={(value) => {
|
||||
setSiteID(value)
|
||||
void load(value)
|
||||
}}
|
||||
/>
|
||||
{member?.is_superadmin && sites.length > 1 && (
|
||||
<Select
|
||||
placeholder="分销网站"
|
||||
style={{ width: 200 }}
|
||||
value={siteID}
|
||||
options={sites.map((site) => ({ label: site.site_name, value: site.id }))}
|
||||
onChange={(value) => {
|
||||
setSiteID(value)
|
||||
void load(value)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<DatePicker.RangePicker showTime onChange={(value) => setRange(value)} />
|
||||
<Button type="primary" onClick={handleCreate}>创建批次</Button>
|
||||
</Space>
|
||||
|
||||
@@ -31,10 +31,12 @@ import {
|
||||
applySiteWithdrawal,
|
||||
listSites,
|
||||
formatMoney,
|
||||
getMe,
|
||||
type SiteWallet,
|
||||
type SiteWithdrawal,
|
||||
type Site,
|
||||
} from '../api'
|
||||
import { useMember } from '../MemberContext'
|
||||
|
||||
const STATUS_CONFIG: Record<string, { label: string; color: string }> = {
|
||||
pending: { label: '待审核', color: 'orange' },
|
||||
@@ -44,8 +46,9 @@ const STATUS_CONFIG: Record<string, { label: string; color: string }> = {
|
||||
}
|
||||
|
||||
export default function Wallet() {
|
||||
const member = useMember()
|
||||
const [sites, setSites] = useState<Site[]>([])
|
||||
const [siteID, setSiteID] = useState<number>()
|
||||
const [siteID, setSiteID] = useState<number | undefined>(member?.site_id)
|
||||
const [wallet, setWallet] = useState<SiteWallet | null>(null)
|
||||
const [withdrawals, setWithdrawals] = useState<SiteWithdrawal[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
@@ -77,13 +80,34 @@ export default function Wallet() {
|
||||
}, [siteID])
|
||||
|
||||
useEffect(() => {
|
||||
listSites()
|
||||
.then((rows) => {
|
||||
setSites(rows)
|
||||
if (rows.length > 0) setSiteID(rows[0].id)
|
||||
})
|
||||
.catch(() => message.error('站点加载失败'))
|
||||
}, [])
|
||||
async function init() {
|
||||
let currentMember = member
|
||||
if (!currentMember) {
|
||||
try {
|
||||
currentMember = await getMe()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
const activeSiteID = currentMember?.site_id
|
||||
if (activeSiteID) {
|
||||
setSiteID(activeSiteID)
|
||||
}
|
||||
|
||||
if (currentMember?.is_superadmin) {
|
||||
try {
|
||||
const rows = await listSites()
|
||||
setSites(rows)
|
||||
if (!activeSiteID && rows.length > 0) {
|
||||
setSiteID(rows[0].id)
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
void init()
|
||||
}, [member])
|
||||
|
||||
useEffect(() => {
|
||||
void loadData(1)
|
||||
@@ -145,13 +169,15 @@ export default function Wallet() {
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Space>
|
||||
<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)}
|
||||
/>
|
||||
{member?.is_superadmin && sites.length > 1 && (
|
||||
<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)}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
|
||||
Reference in New Issue
Block a user