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