feat(distribution): 完善分销收号资产双视图、商户提现、自主加价、品牌装修与数据大盘
This commit is contained in:
@@ -5,12 +5,15 @@ import {
|
||||
AccountBookOutlined,
|
||||
ApiOutlined,
|
||||
DashboardOutlined,
|
||||
DollarCircleOutlined,
|
||||
EyeOutlined,
|
||||
FileSearchOutlined,
|
||||
LogoutOutlined,
|
||||
SettingOutlined,
|
||||
ShopOutlined,
|
||||
SkinOutlined,
|
||||
UserOutlined,
|
||||
WalletOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { getMe, logout, type CurrentMember, getStoredToken } from './api'
|
||||
import Dashboard from './pages/Dashboard'
|
||||
@@ -20,6 +23,8 @@ import Payments from './pages/Payments'
|
||||
import Accounts from './pages/Accounts'
|
||||
import Settlements from './pages/Settlements'
|
||||
import Orders from './pages/Orders'
|
||||
import Wallet from './pages/Wallet'
|
||||
import SiteSettings from './pages/SiteSettings'
|
||||
import Login from './pages/Login'
|
||||
import Storefront from './pages/Storefront'
|
||||
import './style.css'
|
||||
@@ -29,11 +34,13 @@ type MenuItem = Required<MenuProps>['items'][number] & { superadminOnly?: boolea
|
||||
const ALL_MENU_ITEMS: MenuItem[] = [
|
||||
{ key: '/', icon: <DashboardOutlined />, label: <Link to="/">仪表盘</Link> },
|
||||
{ key: '/sites', icon: <ShopOutlined />, label: <Link to="/sites">分销网站</Link>, superadminOnly: true },
|
||||
{ key: '/sold', icon: <FileSearchOutlined />, label: <Link to="/sold">售出账号</Link> },
|
||||
{ key: '/receive', icon: <AccountBookOutlined />, label: <Link to="/receive">分销收号</Link> },
|
||||
{ key: '/settlements', icon: <AccountBookOutlined />, label: <Link to="/settlements">结算管理</Link> },
|
||||
{ key: '/sold', icon: <FileSearchOutlined />, label: <Link to="/sold">售出账号</Link> },
|
||||
{ key: '/accounts', icon: <ShopOutlined />, label: <Link to="/accounts">商品展示与加价</Link> },
|
||||
{ key: '/wallet', icon: <WalletOutlined />, label: <Link to="/wallet">资金与提现</Link> },
|
||||
{ key: '/settlements', icon: <DollarCircleOutlined />, label: <Link to="/settlements">结算管理</Link> },
|
||||
{ key: '/settings', icon: <SkinOutlined />, label: <Link to="/settings">品牌装修</Link> },
|
||||
{ key: '/payments', icon: <ApiOutlined />, label: <Link to="/payments">支付通道</Link> },
|
||||
{ key: '/accounts', icon: <SettingOutlined />, label: <Link to="/accounts">账号展示</Link> },
|
||||
{ key: '/rules', icon: <SettingOutlined />, label: <Link to="/rules">比例与税费</Link> },
|
||||
]
|
||||
|
||||
@@ -131,6 +138,8 @@ function MainLayout({ member, onLogout }: { member: CurrentMember; onLogout: ()
|
||||
<Route path="/rules" element={<Rules />} />
|
||||
<Route path="/payments" element={<Payments />} />
|
||||
<Route path="/accounts" element={<Accounts />} />
|
||||
<Route path="/wallet" element={<Wallet />} />
|
||||
<Route path="/settings" element={<SiteSettings />} />
|
||||
<Route path="/settlements" element={<Settlements />} />
|
||||
<Route path="/sold" element={<Orders kind="sold" />} />
|
||||
<Route path="/receive" element={<Orders kind="receive" />} />
|
||||
|
||||
@@ -115,6 +115,11 @@ export interface Site {
|
||||
display_account_enabled: boolean
|
||||
tax_enabled: boolean
|
||||
tax_rate_bps: number
|
||||
logo_url?: string
|
||||
favicon_url?: string
|
||||
banners_json?: string
|
||||
announcements_json?: string
|
||||
footer_info_json?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
@@ -170,6 +175,7 @@ export interface AccountDisplay {
|
||||
account_id: number
|
||||
listing_id: number
|
||||
display_status: string
|
||||
markup_amount_cent?: number
|
||||
listing_no?: string
|
||||
title?: string
|
||||
game_name?: string
|
||||
@@ -402,3 +408,174 @@ export async function listSettlementItems(batchID: number): Promise<SettlementIt
|
||||
const resp = await api.get<ApiResponse<{ items: SettlementItem[] }>>(`/admin/distribution/settlement-batches/${batchID}/items`)
|
||||
return extract(resp).items ?? []
|
||||
}
|
||||
|
||||
export interface ReceivedListing {
|
||||
listing_id: number
|
||||
listing_no: string
|
||||
site_id: number
|
||||
account_id: number
|
||||
owner_id: number
|
||||
owner_phone?: string
|
||||
owner_nickname?: string
|
||||
title: string
|
||||
game_name: string
|
||||
server_region: string
|
||||
login_platform: string
|
||||
rank_level: string
|
||||
haf_coin_amount: number
|
||||
price_cent: number
|
||||
deposit_amount_cent: number
|
||||
status: string
|
||||
review_status: string
|
||||
review_reason: string
|
||||
published_at?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface ReceivedListingPage {
|
||||
items: ReceivedListing[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export async function fetchReceivedListings(params: {
|
||||
siteID?: number
|
||||
page?: number
|
||||
pageSize?: number
|
||||
status?: string
|
||||
reviewStatus?: string
|
||||
gameName?: string
|
||||
keyword?: string
|
||||
}): Promise<ReceivedListingPage> {
|
||||
const resp = await api.get<ApiResponse<ReceivedListingPage>>('/admin/distribution/received-listings', {
|
||||
params: {
|
||||
site_id: params.siteID,
|
||||
page: params.page || 1,
|
||||
page_size: params.pageSize || 20,
|
||||
status: params.status || undefined,
|
||||
review_status: params.reviewStatus || undefined,
|
||||
game_name: params.gameName || undefined,
|
||||
keyword: params.keyword || undefined,
|
||||
},
|
||||
})
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export async function updateReceivedListingStatus(id: number, status: string): Promise<void> {
|
||||
await api.put(`/admin/distribution/received-listings/${id}/status`, { status })
|
||||
}
|
||||
|
||||
export interface SiteWallet {
|
||||
site_id: number
|
||||
available_balance_cent: number
|
||||
frozen_balance_cent: number
|
||||
total_settled_cent: number
|
||||
total_withdrawn_cent: number
|
||||
}
|
||||
|
||||
export async function fetchSiteWallet(siteID: number): Promise<SiteWallet> {
|
||||
const resp = await api.get<ApiResponse<SiteWallet>>(`/admin/distribution/sites/${siteID}/wallet`)
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export interface SiteWithdrawal {
|
||||
id: number
|
||||
withdraw_no: string
|
||||
site_id: number
|
||||
amount_cent: number
|
||||
fee_cent: number
|
||||
actual_amount_cent: number
|
||||
account_type: string
|
||||
account_name: string
|
||||
account_no: string
|
||||
bank_name?: string
|
||||
bank_branch?: string
|
||||
status: string
|
||||
review_remark?: string
|
||||
payment_proof_url?: string
|
||||
payment_remark?: string
|
||||
paid_at?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface SiteWithdrawalPage {
|
||||
items: SiteWithdrawal[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export async function fetchSiteWithdrawals(siteID: number, page = 1, pageSize = 20): Promise<SiteWithdrawalPage> {
|
||||
const resp = await api.get<ApiResponse<SiteWithdrawalPage>>(`/admin/distribution/sites/${siteID}/withdrawals`, {
|
||||
params: { page, page_size: pageSize },
|
||||
})
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export interface ApplySiteWithdrawalPayload {
|
||||
site_id: number
|
||||
amount_cent: number
|
||||
account_type: 'alipay' | 'bank'
|
||||
account_name: string
|
||||
account_no: string
|
||||
bank_name?: string
|
||||
bank_branch?: string
|
||||
}
|
||||
|
||||
export async function applySiteWithdrawal(payload: ApplySiteWithdrawalPayload): Promise<SiteWithdrawal> {
|
||||
const resp = await api.post<ApiResponse<SiteWithdrawal>>(`/admin/distribution/sites/${payload.site_id}/withdrawals`, payload)
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export interface SiteBrandingPayload {
|
||||
logo_url?: string
|
||||
favicon_url?: string
|
||||
banners_json?: string
|
||||
announcements_json?: string
|
||||
footer_info_json?: string
|
||||
}
|
||||
|
||||
export async function updateSiteBranding(siteID: number, payload: SiteBrandingPayload): Promise<Site> {
|
||||
const resp = await api.put<ApiResponse<Site>>(`/admin/distribution/sites/${siteID}/branding`, payload)
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export async function updateAccountMarkup(id: number, markupCent: number): Promise<AccountDisplay> {
|
||||
const resp = await api.put<ApiResponse<AccountDisplay>>(`/admin/distribution/accounts/${id}/markup`, {
|
||||
markup_amount_cent: markupCent,
|
||||
})
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export async function batchUpdateAccountMarkup(siteID: number, listingIDs: number[], markupCent: number): Promise<void> {
|
||||
await api.post(`/admin/distribution/accounts/batch-markup`, {
|
||||
listing_ids: listingIDs,
|
||||
markup_amount_cent: markupCent,
|
||||
}, {
|
||||
params: { site_id: siteID },
|
||||
})
|
||||
}
|
||||
|
||||
export interface DistributionAnalytics {
|
||||
today_received_count: number
|
||||
total_received_count: number
|
||||
active_listing_count: number
|
||||
today_order_count: number
|
||||
today_gmv_cent: number
|
||||
today_profit_cent: number
|
||||
month_profit_cent: number
|
||||
available_balance_cent: number
|
||||
trend_days: {
|
||||
date: string
|
||||
received_count: number
|
||||
order_count: number
|
||||
gmv_cent: number
|
||||
profit_cent: number
|
||||
}[]
|
||||
}
|
||||
|
||||
export async function fetchDashboardAnalytics(siteID: number): Promise<DistributionAnalytics> {
|
||||
const resp = await api.get<ApiResponse<DistributionAnalytics>>(`/admin/distribution/sites/${siteID}/analytics`)
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useEffect, useState, useMemo } from 'react'
|
||||
import { Card, Table, Typography, Space, Select, Tag, message, Button, Input, Modal } from 'antd'
|
||||
import { SyncOutlined, SearchOutlined, CheckCircleOutlined, StopOutlined } from '@ant-design/icons'
|
||||
import { Card, Table, Typography, Space, Select, Tag, message, Button, Input, Modal, InputNumber } from 'antd'
|
||||
import { SyncOutlined, SearchOutlined, CheckCircleOutlined, StopOutlined, EditOutlined, DollarCircleOutlined } from '@ant-design/icons'
|
||||
import {
|
||||
listAccountDisplays,
|
||||
listSites,
|
||||
updateAccountDisplay,
|
||||
syncSiteAccounts,
|
||||
updateAccountMarkup,
|
||||
batchUpdateAccountMarkup,
|
||||
formatMoney,
|
||||
type AccountDisplay,
|
||||
type Site,
|
||||
@@ -28,6 +30,12 @@ export default function Accounts() {
|
||||
const [syncing, setSyncing] = useState(false)
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([])
|
||||
|
||||
// Markup modal state
|
||||
const [markupModalVisible, setMarkupModalVisible] = useState(false)
|
||||
const [targetAccount, setTargetAccount] = useState<AccountDisplay | null>(null)
|
||||
const [markupYuan, setMarkupYuan] = useState<number>(0)
|
||||
const [markupSaving, setMarkupSaving] = useState(false)
|
||||
|
||||
async function load(site?: number, statusFilter?: string) {
|
||||
if (!site) return
|
||||
setLoading(true)
|
||||
@@ -111,6 +119,51 @@ export default function Accounts() {
|
||||
})
|
||||
}
|
||||
|
||||
function handleOpenMarkup(item?: AccountDisplay) {
|
||||
if (item) {
|
||||
setTargetAccount(item)
|
||||
setMarkupYuan((item.markup_amount_cent ?? 0) / 100)
|
||||
} else {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
message.warning('请先勾选需要批量加价的账号商品')
|
||||
return
|
||||
}
|
||||
setTargetAccount(null)
|
||||
setMarkupYuan(0)
|
||||
}
|
||||
setMarkupModalVisible(true)
|
||||
}
|
||||
|
||||
async function handleSaveMarkup() {
|
||||
if (!siteID) return
|
||||
const markupCent = Math.round(markupYuan * 100)
|
||||
if (markupCent < 0) {
|
||||
message.error('加价金额不能小于0')
|
||||
return
|
||||
}
|
||||
setMarkupSaving(true)
|
||||
try {
|
||||
if (targetAccount) {
|
||||
await updateAccountMarkup(targetAccount.id, markupCent)
|
||||
message.success('商品加价设置成功!')
|
||||
} else {
|
||||
const listingIDs = items
|
||||
.filter((it) => selectedRowKeys.includes(it.id))
|
||||
.map((it) => it.listing_id)
|
||||
await batchUpdateAccountMarkup(siteID, listingIDs, markupCent)
|
||||
message.success(`已成功为选中的 ${listingIDs.length} 个商品批量设置加价!`)
|
||||
setSelectedRowKeys([])
|
||||
}
|
||||
setMarkupModalVisible(false)
|
||||
void load(siteID, status)
|
||||
} catch (error: unknown) {
|
||||
const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message
|
||||
message.error(respMessage || '加价设置保存失败')
|
||||
} finally {
|
||||
setMarkupSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
const kw = keyword.trim().toLowerCase()
|
||||
if (!kw) return items
|
||||
@@ -191,6 +244,13 @@ export default function Accounts() {
|
||||
{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 />}
|
||||
@@ -285,6 +345,21 @@ export default function Accounts() {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '自主加价',
|
||||
dataIndex: 'markup_amount_cent',
|
||||
width: 120,
|
||||
render: (v: number | undefined) => {
|
||||
if (v && v > 0) {
|
||||
return (
|
||||
<Tag color="gold" style={{ fontWeight: 'bold' }}>
|
||||
+{formatMoney(v)}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
return <span style={{ color: '#aaa', fontSize: 12 }}>无加价</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '主站状态',
|
||||
dataIndex: 'listing_status',
|
||||
@@ -306,21 +381,69 @@ export default function Accounts() {
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 120,
|
||||
width: 160,
|
||||
render: (_: unknown, row: AccountDisplay) => (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger={row.display_status === 'enabled'}
|
||||
onClick={() => toggle(row)}
|
||||
>
|
||||
{row.display_status === 'enabled' ? '下架' : '恢复展示'}
|
||||
</Button>
|
||||
<Space size={2}>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleOpenMarkup(row)}
|
||||
>
|
||||
加价
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger={row.display_status === 'enabled'}
|
||||
onClick={() => toggle(row)}
|
||||
>
|
||||
{row.display_status === 'enabled' ? '下架' : '展示'}
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={targetAccount ? `设置加价 - ${targetAccount.title || targetAccount.listing_no}` : `批量加价 (${selectedRowKeys.length} 个账号商品)`}
|
||||
open={markupModalVisible}
|
||||
onCancel={() => setMarkupModalVisible(false)}
|
||||
onOk={handleSaveMarkup}
|
||||
confirmLoading={markupSaving}
|
||||
okText="保存加价"
|
||||
destroyOnClose
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%', padding: '12px 0' }}>
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 8 }}>
|
||||
自主加价金额将直接叠加在分销前台展示的每小时租金上,该部分溢价产生的收益将在成单结算时归当前分销商所有。
|
||||
</Typography.Paragraph>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<Typography.Text strong>每小时加价金额:</Typography.Text>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={1000}
|
||||
precision={2}
|
||||
style={{ width: 180 }}
|
||||
value={markupYuan}
|
||||
onChange={(val) => setMarkupYuan(val ?? 0)}
|
||||
addonAfter="元/小时"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
{markupYuan > 0 && targetAccount && (
|
||||
<div style={{ marginTop: 8, padding: '8px 12px', background: '#f6ffed', borderRadius: 4, border: '1px solid #b7eb8f', fontSize: 12 }}>
|
||||
原租金 {formatMoney(targetAccount.price_cent)}/时 ➔ 分销前台标价为{' '}
|
||||
<strong style={{ color: '#cf1322' }}>
|
||||
¥{(((targetAccount.price_cent ?? 0) + Math.round(markupYuan * 100)) / 100).toFixed(2)}
|
||||
</strong>
|
||||
/时
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Modal>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,43 +1,252 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Table, Typography, Statistic, Space, message } from 'antd'
|
||||
import { listOrders, listSites, formatMoney, type Site } from '../api'
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Typography,
|
||||
Statistic,
|
||||
Space,
|
||||
Row,
|
||||
Col,
|
||||
Select,
|
||||
Tag,
|
||||
Button,
|
||||
message,
|
||||
} from 'antd'
|
||||
import {
|
||||
ShoppingCartOutlined,
|
||||
DollarCircleOutlined,
|
||||
WalletOutlined,
|
||||
AppstoreOutlined,
|
||||
LineChartOutlined,
|
||||
ArrowRightOutlined,
|
||||
SkinOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
listOrders,
|
||||
listSites,
|
||||
fetchDashboardAnalytics,
|
||||
formatMoney,
|
||||
type Site,
|
||||
type DistributionAnalytics,
|
||||
type DistributionOrder,
|
||||
} from '../api'
|
||||
|
||||
export default function Dashboard() {
|
||||
const [sites, setSites] = useState<Site[]>([])
|
||||
const [orders, setOrders] = useState<Awaited<ReturnType<typeof listOrders>>['items']>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [siteID, setSiteID] = useState<number>()
|
||||
const [analytics, setAnalytics] = useState<DistributionAnalytics | null>(null)
|
||||
const [recentOrders, setRecentOrders] = useState<DistributionOrder[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([listSites(), listOrders({ pageSize: 100 })])
|
||||
.then(([siteItems, page]) => {
|
||||
setSites(siteItems)
|
||||
setOrders(page.items)
|
||||
setTotal(page.total)
|
||||
})
|
||||
.catch(() => message.error('数据加载失败'))
|
||||
const loadData = useCallback(async (sid: number) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [anaRes, ordRes] = await Promise.all([
|
||||
fetchDashboardAnalytics(sid),
|
||||
listOrders({ siteID: sid, pageSize: 8 }),
|
||||
])
|
||||
setAnalytics(anaRes)
|
||||
setRecentOrders(ordRes.items)
|
||||
} catch {
|
||||
message.error('加载大盘数据失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const netPaid = orders.reduce((sum, item) => sum + item.net_paid_amount_cent, 0)
|
||||
useEffect(() => {
|
||||
listSites()
|
||||
.then((rows) => {
|
||||
setSites(rows)
|
||||
if (rows.length > 0) {
|
||||
setSiteID(rows[0].id)
|
||||
void loadData(rows[0].id)
|
||||
}
|
||||
})
|
||||
.catch(() => message.error('站点加载失败'))
|
||||
}, [loadData])
|
||||
|
||||
const selectedSite = sites.find((s) => s.id === siteID)
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large" className="page">
|
||||
<Typography.Title level={3}>分销运营概览</Typography.Title>
|
||||
<div className="stats">
|
||||
<Card><Statistic title="分销网站" value={sites.length} /></Card>
|
||||
<Card><Statistic title="分销订单" value={total} /></Card>
|
||||
<Card><Statistic title="最近净支付" value={(netPaid / 100).toFixed(2)} suffix="元" /></Card>
|
||||
<Space direction="vertical" size="large" className="page" style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<Typography.Title level={3} style={{ marginBottom: 4 }}>
|
||||
运营数据大盘
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
当前站点:{selectedSite?.site_name || '默认分销站'} ({selectedSite?.site_code}) · 实时聚合收号、货架、订单与结算收益
|
||||
</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)
|
||||
}}
|
||||
/>
|
||||
<Link to="/wallet">
|
||||
<Button type="primary" icon={<WalletOutlined />}>
|
||||
去提现
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/settings">
|
||||
<Button icon={<SkinOutlined />}>
|
||||
站点装修
|
||||
</Button>
|
||||
</Link>
|
||||
</Space>
|
||||
</div>
|
||||
<Card title="最近订单">
|
||||
|
||||
{/* 核心业务指标看板 */}
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} md={8} lg={4}>
|
||||
<Card bordered={false} style={{ background: '#fafafa' }} loading={loading}>
|
||||
<Statistic
|
||||
title="今日收号 / 累计收号"
|
||||
value={analytics?.today_received_count ?? 0}
|
||||
suffix={`/ ${analytics?.total_received_count ?? 0} 个`}
|
||||
valueStyle={{ color: '#1677ff', fontWeight: 'bold' }}
|
||||
/>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Link to="/receive" style={{ fontSize: 12 }}>查看收号资产 <ArrowRightOutlined /></Link>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8} lg={4}>
|
||||
<Card bordered={false} style={{ background: '#fafafa' }} loading={loading}>
|
||||
<Statistic
|
||||
title="在售可租货源"
|
||||
value={analytics?.active_listing_count ?? 0}
|
||||
prefix={<AppstoreOutlined style={{ color: '#52c41a' }} />}
|
||||
suffix="个"
|
||||
valueStyle={{ color: '#52c41a', fontWeight: 'bold' }}
|
||||
/>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Link to="/accounts" style={{ fontSize: 12 }}>管理商品货架 <ArrowRightOutlined /></Link>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8} lg={4}>
|
||||
<Card bordered={false} style={{ background: '#fafafa' }} loading={loading}>
|
||||
<Statistic
|
||||
title="今日订单量"
|
||||
value={analytics?.today_order_count ?? 0}
|
||||
prefix={<ShoppingCartOutlined style={{ color: '#722ed1' }} />}
|
||||
suffix="单"
|
||||
valueStyle={{ color: '#722ed1', fontWeight: 'bold' }}
|
||||
/>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Link to="/sold" style={{ fontSize: 12 }}>查看交易订单 <ArrowRightOutlined /></Link>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8} lg={4}>
|
||||
<Card bordered={false} style={{ background: '#fafafa' }} loading={loading}>
|
||||
<Statistic
|
||||
title="今日成交 GMV"
|
||||
value={((analytics?.today_gmv_cent ?? 0) / 100).toFixed(2)}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#fa8c16', fontWeight: 'bold' }}
|
||||
/>
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: '#888' }}>
|
||||
今日收益: {formatMoney(analytics?.today_profit_cent)}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8} lg={4}>
|
||||
<Card bordered={false} style={{ background: '#fafafa' }} loading={loading}>
|
||||
<Statistic
|
||||
title="本月预估分润收益"
|
||||
value={((analytics?.month_profit_cent ?? 0) / 100).toFixed(2)}
|
||||
prefix={<DollarCircleOutlined style={{ color: '#eb2f96' }} />}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#eb2f96', fontWeight: 'bold' }}
|
||||
/>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Link to="/settlements" style={{ fontSize: 12 }}>结算对账 <ArrowRightOutlined /></Link>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8} lg={4}>
|
||||
<Card bordered={false} style={{ background: '#f6ffed', border: '1px solid #b7eb8f' }} loading={loading}>
|
||||
<Statistic
|
||||
title="钱包可提现余额"
|
||||
value={((analytics?.available_balance_cent ?? 0) / 100).toFixed(2)}
|
||||
prefix={<WalletOutlined style={{ color: '#52c41a' }} />}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#52c41a', fontWeight: 'bold' }}
|
||||
/>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Link to="/wallet" style={{ fontSize: 12 }}>立即提现 <ArrowRightOutlined /></Link>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 近7天运营走势 */}
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<LineChartOutlined />
|
||||
<span>近 7 天交易与收号走势</span>
|
||||
</Space>
|
||||
}
|
||||
loading={loading}
|
||||
>
|
||||
<Table
|
||||
rowKey="order_id"
|
||||
dataSource={orders.slice(0, 10)}
|
||||
rowKey="date"
|
||||
dataSource={analytics?.trend_days ?? []}
|
||||
pagination={false}
|
||||
columns={[
|
||||
{ title: '订单号', dataIndex: 'order_no' },
|
||||
{ title: '状态', dataIndex: 'status' },
|
||||
{ title: '日期', dataIndex: 'date', width: 140, render: (v: string) => <strong>{v}</strong> },
|
||||
{
|
||||
title: '收号数量',
|
||||
dataIndex: 'received_count',
|
||||
render: (v: number) => <Tag color={v > 0 ? 'blue' : 'default'}>{v} 个</Tag>,
|
||||
},
|
||||
{
|
||||
title: '成交订单',
|
||||
dataIndex: 'order_count',
|
||||
render: (v: number) => <Tag color={v > 0 ? 'purple' : 'default'}>{v} 单</Tag>,
|
||||
},
|
||||
{
|
||||
title: '成交 GMV',
|
||||
dataIndex: 'gmv_cent',
|
||||
render: (v: number) => <span style={{ color: '#cf1322', fontWeight: 'bold' }}>{formatMoney(v)}</span>,
|
||||
},
|
||||
{
|
||||
title: '预计分成收益',
|
||||
dataIndex: 'profit_cent',
|
||||
render: (v: number) => <span style={{ color: '#52c41a', fontWeight: 'bold' }}>{formatMoney(v)}</span>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 最近交易订单 */}
|
||||
<Card
|
||||
title="最近分销订单"
|
||||
extra={<Link to="/sold">查看全部订单 <ArrowRightOutlined /></Link>}
|
||||
loading={loading}
|
||||
>
|
||||
<Table
|
||||
rowKey="order_id"
|
||||
dataSource={recentOrders}
|
||||
pagination={false}
|
||||
columns={[
|
||||
{ title: '订单号', dataIndex: 'order_no', render: (v: string) => <Typography.Text copyable>{v}</Typography.Text> },
|
||||
{ title: '状态', dataIndex: 'status', render: (v: string) => <Tag>{v}</Tag> },
|
||||
{ title: '净支付', dataIndex: 'net_paid_amount_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '手续费', dataIndex: 'payment_fee_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '分成基数', dataIndex: 'split_base_amount_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '结算', dataIndex: 'settlement_status' },
|
||||
{ title: '结算状态', dataIndex: 'settlement_status', render: (v: string) => <Tag color={v === 'settled' ? 'success' : 'default'}>{v}</Tag> },
|
||||
{ title: '创建时间', dataIndex: 'created_at' },
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Card, Table, Typography, Space, Select, Tag, message } from 'antd'
|
||||
import { formatMoney, listOrders, listSites, type DistributionOrder, type Site } from '../api'
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Typography,
|
||||
Space,
|
||||
Select,
|
||||
Tag,
|
||||
message,
|
||||
Tabs,
|
||||
Input,
|
||||
Button,
|
||||
Modal,
|
||||
} from 'antd'
|
||||
import { SearchOutlined, CheckCircleOutlined, StopOutlined } from '@ant-design/icons'
|
||||
import {
|
||||
formatMoney,
|
||||
listOrders,
|
||||
listSites,
|
||||
fetchReceivedListings,
|
||||
updateReceivedListingStatus,
|
||||
type DistributionOrder,
|
||||
type ReceivedListing,
|
||||
type Site,
|
||||
} from '../api'
|
||||
|
||||
const ORDER_STATUSES = [
|
||||
'pending_payment',
|
||||
@@ -17,30 +39,79 @@ const ORDER_STATUSES = [
|
||||
'cancelled',
|
||||
]
|
||||
|
||||
const REVIEW_STATUS_LABELS: Record<string, { label: string; color: string }> = {
|
||||
pending: { label: '待审核', color: 'orange' },
|
||||
approved: { label: '审核通过', color: 'success' },
|
||||
rejected: { label: '审核未通过', color: 'error' },
|
||||
none: { label: '免审', color: 'default' },
|
||||
}
|
||||
|
||||
const LISTING_STATUS_LABELS: Record<string, { label: string; color: string }> = {
|
||||
published: { label: '上架在售', color: 'green' },
|
||||
inactive: { label: '已下架', color: 'default' },
|
||||
closed: { label: '已关闭', color: 'error' },
|
||||
draft: { label: '草稿', color: 'warning' },
|
||||
}
|
||||
|
||||
export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
||||
const [sites, setSites] = useState<Site[]>([])
|
||||
const [siteID, setSiteID] = useState<number>()
|
||||
const [status, setStatus] = useState<string>('')
|
||||
const [items, setItems] = useState<DistributionOrder[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const pageSize = 20
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState<'received_listings' | 'orders'>('received_listings')
|
||||
|
||||
const load = useCallback(async (targetPage: number) => {
|
||||
setLoading(true)
|
||||
// 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)
|
||||
|
||||
// Received Listings Tab State
|
||||
const [receivedListings, setReceivedListings] = useState<ReceivedListing[]>([])
|
||||
const [recvTotal, setRecvTotal] = useState(0)
|
||||
const [recvPage, setRecvPage] = useState(1)
|
||||
const [recvLoading, setRecvLoading] = useState(false)
|
||||
const [recvStatus, setRecvStatus] = useState<string>('')
|
||||
const [recvReviewStatus, setRecvReviewStatus] = useState<string>('')
|
||||
const [recvKeyword, setRecvKeyword] = useState<string>('')
|
||||
|
||||
const pageSize = 20
|
||||
|
||||
const loadOrders = useCallback(async (targetPage: number) => {
|
||||
setOrderLoading(true)
|
||||
try {
|
||||
const result = await listOrders({ kind, status, siteID, page: targetPage, pageSize })
|
||||
setItems(result.items)
|
||||
setTotal(result.total)
|
||||
setPage(result.page)
|
||||
setOrders(result.items)
|
||||
setOrderTotal(result.total)
|
||||
setOrderPage(result.page)
|
||||
} catch {
|
||||
message.error('订单加载失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setOrderLoading(false)
|
||||
}
|
||||
}, [kind, status, siteID])
|
||||
|
||||
const loadReceivedListings = useCallback(async (targetPage: number) => {
|
||||
if (!siteID) return
|
||||
setRecvLoading(true)
|
||||
try {
|
||||
const result = await fetchReceivedListings({
|
||||
siteID,
|
||||
page: targetPage,
|
||||
pageSize,
|
||||
status: recvStatus,
|
||||
reviewStatus: recvReviewStatus,
|
||||
keyword: recvKeyword,
|
||||
})
|
||||
setReceivedListings(result.items)
|
||||
setRecvTotal(result.total)
|
||||
setRecvPage(result.page)
|
||||
} catch {
|
||||
message.error('分销收号资产加载失败')
|
||||
} finally {
|
||||
setRecvLoading(false)
|
||||
}
|
||||
}, [siteID, recvStatus, recvReviewStatus, recvKeyword])
|
||||
|
||||
useEffect(() => {
|
||||
listSites()
|
||||
.then((rows) => {
|
||||
@@ -51,51 +122,294 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
void load(1)
|
||||
}, [load])
|
||||
if (kind === 'receive') {
|
||||
if (activeTab === 'received_listings') {
|
||||
void loadReceivedListings(1)
|
||||
} else {
|
||||
void loadOrders(1)
|
||||
}
|
||||
} else {
|
||||
void loadOrders(1)
|
||||
}
|
||||
}, [kind, activeTab, loadOrders, loadReceivedListings])
|
||||
|
||||
const title = kind === 'sold' ? '售出账号订单' : kind === 'receive' ? '分销收号订单' : '全部分销订单'
|
||||
async function handleToggleListingStatus(item: ReceivedListing) {
|
||||
const nextStatus = item.status === 'published' ? 'inactive' : 'published'
|
||||
const actionName = nextStatus === 'published' ? '重新上架' : '下架'
|
||||
Modal.confirm({
|
||||
title: `确认${actionName}该账号?`,
|
||||
content: `账号编号:${item.listing_no} (${item.title})`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await updateReceivedListingStatus(item.listing_id, nextStatus)
|
||||
message.success(`已${actionName}`)
|
||||
void loadReceivedListings(recvPage)
|
||||
} catch (error: unknown) {
|
||||
const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message
|
||||
message.error(respMessage || `${actionName}操作失败`)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const title = kind === 'sold' ? '售出账号订单' : kind === 'receive' ? '分销收号管理' : '全部分销订单'
|
||||
|
||||
return (
|
||||
<Space direction="vertical" className="page">
|
||||
<Typography.Title level={3}>{title}</Typography.Title>
|
||||
<Space>
|
||||
<Space direction="vertical" className="page" style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<Typography.Title level={3} style={{ marginBottom: 4 }}>{title}</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
{kind === 'receive'
|
||||
? '查看由当前分销网站上传的号源资产及审核流转状态,并跟踪对应的收号分成订单'
|
||||
: '查看分销订单交易及财务明细'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="分销网站"
|
||||
style={{ width: 200 }}
|
||||
placeholder="切换分销网站"
|
||||
style={{ width: 220 }}
|
||||
value={siteID}
|
||||
options={sites.map((site) => ({ label: site.site_name, value: site.id }))}
|
||||
onChange={(value) => setSiteID(value ?? undefined)}
|
||||
options={sites.map((site) => ({ label: `${site.site_name} (${site.site_code})`, value: site.id }))}
|
||||
onChange={(value) => setSiteID(value)}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="订单状态"
|
||||
style={{ width: 200 }}
|
||||
options={ORDER_STATUSES.map((value) => ({ label: value, value }))}
|
||||
onChange={(value) => setStatus(value ?? '')}
|
||||
/>
|
||||
</Space>
|
||||
<Card>
|
||||
<Table
|
||||
rowKey="order_id"
|
||||
loading={loading}
|
||||
dataSource={items}
|
||||
pagination={{ current: page, pageSize, total, onChange: (p) => void load(p) }}
|
||||
columns={[
|
||||
{ title: '订单号', dataIndex: 'order_no' },
|
||||
{ title: '订单状态', dataIndex: 'status', render: (v: string) => <Tag>{v}</Tag> },
|
||||
{ title: '退款状态', dataIndex: 'refund_status' },
|
||||
{ title: '净支付', dataIndex: 'net_paid_amount_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '手续费', dataIndex: 'payment_fee_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '服务费', dataIndex: 'service_fee_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '分成基数', dataIndex: 'split_base_amount_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '尾差', dataIndex: 'rounding_adjustment_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '结算', dataIndex: 'settlement_status' },
|
||||
{ title: '创建时间', dataIndex: 'created_at' },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{kind === 'receive' ? (
|
||||
<Card>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={(k) => setActiveTab(k as 'received_listings' | 'orders')}
|
||||
items={[
|
||||
{
|
||||
key: 'received_listings',
|
||||
label: `收到的账号资产 (${recvTotal})`,
|
||||
children: (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Space wrap style={{ marginBottom: 16 }}>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="审核状态"
|
||||
style={{ width: 140 }}
|
||||
value={recvReviewStatus || undefined}
|
||||
options={Object.entries(REVIEW_STATUS_LABELS).map(([k, v]) => ({ label: v.label, value: k }))}
|
||||
onChange={(val) => {
|
||||
setRecvReviewStatus(val ?? '')
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="货架状态"
|
||||
style={{ width: 140 }}
|
||||
value={recvStatus || undefined}
|
||||
options={Object.entries(LISTING_STATUS_LABELS).map(([k, v]) => ({ label: v.label, value: k }))}
|
||||
onChange={(val) => {
|
||||
setRecvStatus(val ?? '')
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
placeholder="搜索编号/标题/联系电话/昵称"
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: 260 }}
|
||||
allowClear
|
||||
value={recvKeyword}
|
||||
onChange={(e) => setRecvKeyword(e.target.value)}
|
||||
onPressEnter={() => void loadReceivedListings(1)}
|
||||
/>
|
||||
<Button type="primary" onClick={() => void loadReceivedListings(1)}>
|
||||
筛选
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
rowKey="listing_id"
|
||||
loading={recvLoading}
|
||||
dataSource={receivedListings}
|
||||
pagination={{
|
||||
current: recvPage,
|
||||
pageSize,
|
||||
total: recvTotal,
|
||||
showTotal: (tot) => `共 ${tot} 个分销收号商品`,
|
||||
onChange: (p) => void loadReceivedListings(p),
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
title: '商品编号',
|
||||
dataIndex: 'listing_no',
|
||||
width: 140,
|
||||
render: (v: string) => <Typography.Text strong copyable>{v}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '账号标题与游戏',
|
||||
dataIndex: 'title',
|
||||
minWidth: 220,
|
||||
render: (_: unknown, row: ReceivedListing) => (
|
||||
<div>
|
||||
<Typography.Text strong style={{ display: 'block' }}>
|
||||
{row.title || '无标题'}
|
||||
</Typography.Text>
|
||||
<Space size={4} style={{ marginTop: 4 }}>
|
||||
<Tag color="blue">{row.game_name}</Tag>
|
||||
{row.server_region && <Tag>{row.server_region}</Tag>}
|
||||
{row.login_platform && <Tag>{row.login_platform}</Tag>}
|
||||
{row.rank_level && <Tag color="purple">{row.rank_level}</Tag>}
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '上传卖家',
|
||||
dataIndex: 'owner_phone',
|
||||
width: 150,
|
||||
render: (_: unknown, row: ReceivedListing) => (
|
||||
<div>
|
||||
<div>{row.owner_nickname || '匿名卖家'}</div>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{row.owner_phone || '-'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '价格 / 押金',
|
||||
width: 140,
|
||||
render: (_: unknown, row: ReceivedListing) => (
|
||||
<div>
|
||||
<Typography.Text strong style={{ color: '#cf1322' }}>
|
||||
{formatMoney(row.price_cent)}/时
|
||||
</Typography.Text>
|
||||
<div style={{ fontSize: 12, color: '#888' }}>
|
||||
押金: {formatMoney(row.deposit_amount_cent)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '主站审核',
|
||||
dataIndex: 'review_status',
|
||||
width: 120,
|
||||
render: (v: string, row: ReceivedListing) => {
|
||||
const conf = REVIEW_STATUS_LABELS[v] || { label: v, color: 'default' }
|
||||
return (
|
||||
<div>
|
||||
<Tag color={conf.color}>{conf.label}</Tag>
|
||||
{row.review_reason && (
|
||||
<Typography.Text type="danger" style={{ display: 'block', fontSize: 11 }}>
|
||||
{row.review_reason}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '货架状态',
|
||||
dataIndex: 'status',
|
||||
width: 110,
|
||||
render: (v: string) => {
|
||||
const conf = LISTING_STATUS_LABELS[v] || { label: v, color: 'default' }
|
||||
return <Tag color={conf.color}>{conf.label}</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '上传时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 160,
|
||||
render: (v: string) => <span style={{ fontSize: 12, color: '#666' }}>{v}</span>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 110,
|
||||
render: (_: unknown, row: ReceivedListing) => (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger={row.status === 'published'}
|
||||
icon={row.status === 'published' ? <StopOutlined /> : <CheckCircleOutlined />}
|
||||
onClick={() => void handleToggleListingStatus(row)}
|
||||
>
|
||||
{row.status === 'published' ? '下架' : '上架'}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'orders',
|
||||
label: `收号分成订单 (${orderTotal})`,
|
||||
children: (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="订单状态"
|
||||
style={{ width: 180 }}
|
||||
options={ORDER_STATUSES.map((value) => ({ label: value, value }))}
|
||||
onChange={(value) => setStatus(value ?? '')}
|
||||
/>
|
||||
</Space>
|
||||
<Table
|
||||
rowKey="order_id"
|
||||
loading={orderLoading}
|
||||
dataSource={orders}
|
||||
pagination={{
|
||||
current: orderPage,
|
||||
pageSize,
|
||||
total: orderTotal,
|
||||
onChange: (p) => void loadOrders(p),
|
||||
}}
|
||||
columns={[
|
||||
{ title: '订单号', dataIndex: 'order_no' },
|
||||
{ title: '订单状态', dataIndex: 'status', render: (v: string) => <Tag>{v}</Tag> },
|
||||
{ title: '退款状态', dataIndex: 'refund_status' },
|
||||
{ title: '净支付', dataIndex: 'net_paid_amount_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '手续费', dataIndex: 'payment_fee_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '服务费', dataIndex: 'service_fee_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '分成基数', dataIndex: 'split_base_amount_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '尾差', dataIndex: 'rounding_adjustment_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '结算', dataIndex: 'settlement_status' },
|
||||
{ title: '创建时间', dataIndex: 'created_at' },
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="订单状态"
|
||||
style={{ width: 200 }}
|
||||
options={ORDER_STATUSES.map((value) => ({ label: value, value }))}
|
||||
onChange={(value) => setStatus(value ?? '')}
|
||||
/>
|
||||
</Space>
|
||||
<Table
|
||||
rowKey="order_id"
|
||||
loading={orderLoading}
|
||||
dataSource={orders}
|
||||
pagination={{ current: orderPage, pageSize, total: orderTotal, onChange: (p) => void loadOrders(p) }}
|
||||
columns={[
|
||||
{ title: '订单号', dataIndex: 'order_no' },
|
||||
{ title: '订单状态', dataIndex: 'status', render: (v: string) => <Tag>{v}</Tag> },
|
||||
{ title: '退款状态', dataIndex: 'refund_status' },
|
||||
{ title: '净支付', dataIndex: 'net_paid_amount_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '手续费', dataIndex: 'payment_fee_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '服务费', dataIndex: 'service_fee_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '分成基数', dataIndex: 'split_base_amount_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '尾差', dataIndex: 'rounding_adjustment_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '结算', dataIndex: 'settlement_status' },
|
||||
{ title: '创建时间', dataIndex: 'created_at' },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import {
|
||||
Card,
|
||||
Typography,
|
||||
Space,
|
||||
Select,
|
||||
message,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Row,
|
||||
Col,
|
||||
Divider,
|
||||
Image,
|
||||
Alert,
|
||||
Tag,
|
||||
} from 'antd'
|
||||
import {
|
||||
SaveOutlined,
|
||||
PictureOutlined,
|
||||
NotificationOutlined,
|
||||
EyeOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
listSites,
|
||||
updateSiteBranding,
|
||||
type Site,
|
||||
type SiteBrandingPayload,
|
||||
} from '../api'
|
||||
|
||||
export default function SiteSettings() {
|
||||
const [sites, setSites] = useState<Site[]>([])
|
||||
const [siteID, setSiteID] = useState<number>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [currentSite, setCurrentSite] = useState<Site | null>(null)
|
||||
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const loadSite = useCallback(async (id: number) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const rows = await listSites()
|
||||
setSites(rows)
|
||||
const found = rows.find((s) => s.id === id)
|
||||
if (found) {
|
||||
setCurrentSite(found)
|
||||
form.setFieldsValue({
|
||||
logo_url: found.logo_url || '',
|
||||
favicon_url: found.favicon_url || '',
|
||||
banners_json: found.banners_json || '[]',
|
||||
announcements_json: found.announcements_json || '[]',
|
||||
footer_info_json: found.footer_info_json || '{}',
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
message.error('加载站点设置失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [form])
|
||||
|
||||
useEffect(() => {
|
||||
listSites()
|
||||
.then((rows) => {
|
||||
setSites(rows)
|
||||
if (rows.length > 0) {
|
||||
setSiteID(rows[0].id)
|
||||
void loadSite(rows[0].id)
|
||||
}
|
||||
})
|
||||
.catch(() => message.error('站点加载失败'))
|
||||
}, [loadSite])
|
||||
|
||||
async function handleSave(values: SiteBrandingPayload) {
|
||||
if (!siteID) return
|
||||
// Validate JSON formatting
|
||||
try {
|
||||
if (values.banners_json) JSON.parse(values.banners_json)
|
||||
if (values.announcements_json) JSON.parse(values.announcements_json)
|
||||
if (values.footer_info_json) JSON.parse(values.footer_info_json)
|
||||
} catch {
|
||||
message.error('轮播图、公告或页脚配置的 JSON 格式不正确,请检查!')
|
||||
return
|
||||
}
|
||||
|
||||
setSaving(true)
|
||||
try {
|
||||
const updated = await updateSiteBranding(siteID, values)
|
||||
setCurrentSite(updated)
|
||||
message.success('分销站点品牌与装修配置保存成功!')
|
||||
} catch (error: unknown) {
|
||||
const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message
|
||||
message.error(respMessage || '保存装修配置失败')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const logoUrl = Form.useWatch('logo_url', form)
|
||||
const bannersJsonStr = Form.useWatch('banners_json', form)
|
||||
const announcementsJsonStr = Form.useWatch('announcements_json', form)
|
||||
|
||||
let banners: { image_url: string; title?: string }[] = []
|
||||
try {
|
||||
if (bannersJsonStr) banners = JSON.parse(bannersJsonStr)
|
||||
} catch {
|
||||
// ignore json error in preview
|
||||
}
|
||||
|
||||
let announcements: { title: string; content?: string }[] = []
|
||||
try {
|
||||
if (announcementsJsonStr) announcements = JSON.parse(announcementsJsonStr)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" className="page" style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<Typography.Title level={3} style={{ marginBottom: 4 }}>
|
||||
品牌与站点装修
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
自定义当前分销站前台 Logo、浏览器图标、轮播横幅、站点公告及页脚备案信息。
|
||||
</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)
|
||||
void loadSite(value)
|
||||
}}
|
||||
/>
|
||||
{currentSite?.site_code && (
|
||||
<Button
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => {
|
||||
window.open(`http://${window.location.hostname}:5189/?site=${currentSite.site_code}`, '_blank')
|
||||
}}
|
||||
>
|
||||
预览分销前台
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col span={14}>
|
||||
<Card title="装修配置表单" loading={loading}>
|
||||
<Form form={form} layout="vertical" onFinish={handleSave}>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="站点 Logo 图片地址"
|
||||
name="logo_url"
|
||||
extra="支持 PNG / SVG / JPG 格式的公开图片链接"
|
||||
>
|
||||
<Input placeholder="https://.../logo.png" allowClear />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="浏览器 Favicon 图标地址"
|
||||
name="favicon_url"
|
||||
extra="浏览器标签页小图标 (ICO / PNG)"
|
||||
>
|
||||
<Input placeholder="https://.../favicon.ico" allowClear />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Space>
|
||||
<PictureOutlined />
|
||||
<span>首页轮播图配置 (Banners JSON)</span>
|
||||
</Space>
|
||||
}
|
||||
name="banners_json"
|
||||
extra='格式:[{"image_url": "图片URL", "title": "标题", "link": "点击跳转链接"}]'
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder='[{"image_url":"https://.../banner1.jpg","title":"暑期特惠"}]' />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Space>
|
||||
<NotificationOutlined />
|
||||
<span>站点跑马灯公告 (Announcements JSON)</span>
|
||||
</Space>
|
||||
}
|
||||
name="announcements_json"
|
||||
extra='格式:[{"title": "公告标题", "content": "详细内容"}]'
|
||||
>
|
||||
<Input.TextArea rows={3} placeholder='[{"title":"本站三角洲账号货源充足,上号快!"}]' />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="页脚信息配置 (Footer Info JSON)"
|
||||
name="footer_info_json"
|
||||
extra='格式:{"copyright": "© 2026 分销站点", "icp": "京ICP备XXXXX号", "contact": "客服QQ: 123456"}'
|
||||
>
|
||||
<Input.TextArea rows={3} placeholder='{"copyright":"© 2026 演示分销网","icp":"浙ICP备000000号"}' />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<Button type="primary" htmlType="submit" icon={<SaveOutlined />} loading={saving} size="large">
|
||||
保存装修配置
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={10}>
|
||||
<Card title="效果实时预览" style={{ background: '#f5f7fa' }}>
|
||||
<div style={{ background: '#fff', borderRadius: 8, padding: 16, border: '1px solid #e8e8e8' }}>
|
||||
{/* Header Preview */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', borderBottom: '1px solid #eee', paddingBottom: 12 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
{logoUrl ? (
|
||||
<Image src={logoUrl} height={28} preview={false} style={{ objectFit: 'contain' }} fallback="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='28'%3E%3Ctext x='10' y='20' fill='%23999'%3ELogo 加载中%3C/text%3E%3C/svg%3E" />
|
||||
) : (
|
||||
<Typography.Text strong style={{ fontSize: 16, color: '#1677ff' }}>
|
||||
{currentSite?.site_name || '分销站点名称'}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<Tag color="cyan" style={{ margin: 0 }}>分销前台顶栏</Tag>
|
||||
</div>
|
||||
|
||||
{/* Announcement Preview */}
|
||||
{announcements.length > 0 && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Alert
|
||||
message={
|
||||
<Space>
|
||||
<NotificationOutlined style={{ color: '#fa8c16' }} />
|
||||
<span>{announcements[0].title}</span>
|
||||
</Space>
|
||||
}
|
||||
type="warning"
|
||||
banner
|
||||
style={{ borderRadius: 4, padding: '4px 12px' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Banner Preview */}
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>轮播图预览:</div>
|
||||
{banners.length > 0 && banners[0].image_url ? (
|
||||
<div style={{ width: '100%', height: 120, borderRadius: 6, overflow: 'hidden', background: '#000' }}>
|
||||
<img
|
||||
src={banners[0].image_url}
|
||||
alt="Banner"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = 'https://images.unsplash.com/photo-1542751371-adc38448a05e?w=600&auto=format&fit=crop&q=60'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ width: '100%', height: 90, background: '#f0f2f5', borderRadius: 6, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#999', fontSize: 12 }}>
|
||||
暂无轮播图配置
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content Placeholder */}
|
||||
<div style={{ marginTop: 12, padding: 12, background: '#fafafa', borderRadius: 4, textAlign: 'center', color: '#999', fontSize: 12 }}>
|
||||
[ 游戏商品列表展示区域 ]
|
||||
</div>
|
||||
|
||||
{/* Footer Preview */}
|
||||
<div style={{ marginTop: 16, borderTop: '1px solid #eee', paddingTop: 12, textAlign: 'center', fontSize: 11, color: '#999' }}>
|
||||
<div>{currentSite?.site_name || '分销站点'} · 技术支持由 HFB 平台提供</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Typography,
|
||||
Space,
|
||||
Select,
|
||||
Tag,
|
||||
message,
|
||||
Button,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Radio,
|
||||
Statistic,
|
||||
Row,
|
||||
Col,
|
||||
Image,
|
||||
} from 'antd'
|
||||
import {
|
||||
WalletOutlined,
|
||||
PlusOutlined,
|
||||
DollarCircleOutlined,
|
||||
LockOutlined,
|
||||
CheckCircleOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
fetchSiteWallet,
|
||||
fetchSiteWithdrawals,
|
||||
applySiteWithdrawal,
|
||||
listSites,
|
||||
formatMoney,
|
||||
type SiteWallet,
|
||||
type SiteWithdrawal,
|
||||
type Site,
|
||||
} from '../api'
|
||||
|
||||
const STATUS_CONFIG: Record<string, { label: string; color: string }> = {
|
||||
pending: { label: '待审核', color: 'orange' },
|
||||
processing: { label: '处理中/审核通过', color: 'blue' },
|
||||
completed: { label: '已打款', color: 'success' },
|
||||
rejected: { label: '已拒绝', color: 'error' },
|
||||
}
|
||||
|
||||
export default function Wallet() {
|
||||
const [sites, setSites] = useState<Site[]>([])
|
||||
const [siteID, setSiteID] = useState<number>()
|
||||
const [wallet, setWallet] = useState<SiteWallet | null>(null)
|
||||
const [withdrawals, setWithdrawals] = useState<SiteWithdrawal[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const pageSize = 20
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const loadData = useCallback(async (targetPage: number) => {
|
||||
if (!siteID) return
|
||||
setLoading(true)
|
||||
try {
|
||||
const [walletRes, wdRes] = await Promise.all([
|
||||
fetchSiteWallet(siteID),
|
||||
fetchSiteWithdrawals(siteID, targetPage, pageSize),
|
||||
])
|
||||
setWallet(walletRes)
|
||||
setWithdrawals(wdRes.items)
|
||||
setTotal(wdRes.total)
|
||||
setPage(wdRes.page)
|
||||
} catch {
|
||||
message.error('加载钱包与提现数据失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [siteID])
|
||||
|
||||
useEffect(() => {
|
||||
listSites()
|
||||
.then((rows) => {
|
||||
setSites(rows)
|
||||
if (rows.length > 0) setSiteID(rows[0].id)
|
||||
})
|
||||
.catch(() => message.error('站点加载失败'))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
void loadData(1)
|
||||
}, [loadData])
|
||||
|
||||
async function handleApplyWithdrawal(values: {
|
||||
amount: number
|
||||
account_type: 'alipay' | 'bank'
|
||||
account_name: string
|
||||
account_no: string
|
||||
bank_name?: string
|
||||
bank_branch?: string
|
||||
}) {
|
||||
if (!siteID) return
|
||||
const amountCent = Math.round(values.amount * 100)
|
||||
if (amountCent <= 0) {
|
||||
message.error('提现金额必须大于0')
|
||||
return
|
||||
}
|
||||
if (wallet && amountCent > wallet.available_balance_cent) {
|
||||
message.error('提现金额不能超过当前可提现余额')
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await applySiteWithdrawal({
|
||||
site_id: siteID,
|
||||
amount_cent: amountCent,
|
||||
account_type: values.account_type,
|
||||
account_name: values.account_name,
|
||||
account_no: values.account_no,
|
||||
bank_name: values.bank_name,
|
||||
bank_branch: values.bank_branch,
|
||||
})
|
||||
message.success('提现申请提交成功,请等待主站财务审核打款')
|
||||
setModalVisible(false)
|
||||
form.resetFields()
|
||||
void loadData(1)
|
||||
} catch (error: unknown) {
|
||||
const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message
|
||||
message.error(respMessage || '提现申请提交失败')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const selectedSite = sites.find((s) => s.id === siteID)
|
||||
|
||||
return (
|
||||
<Space direction="vertical" className="page" style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<Typography.Title level={3} style={{ marginBottom: 4 }}>
|
||||
资金管理与商户提现
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
查看分销结算收益到账情况,申请提取收益并实时追踪财务打款流水。
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<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)}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
form.setFieldsValue({ account_type: 'alipay' })
|
||||
setModalVisible(true)
|
||||
}}
|
||||
disabled={!wallet || wallet.available_balance_cent <= 0}
|
||||
>
|
||||
申请提现
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Card bordered={false} style={{ background: 'linear-gradient(135deg, #1677ff 0%, #0958d9 100%)', color: '#fff' }}>
|
||||
<Statistic
|
||||
title={<span style={{ color: 'rgba(255,255,255,0.85)' }}>可提现余额</span>}
|
||||
value={((wallet?.available_balance_cent ?? 0) / 100).toFixed(2)}
|
||||
prefix={<WalletOutlined style={{ color: '#fff' }} />}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#fff', fontSize: 28, fontWeight: 'bold' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card bordered={false} style={{ background: '#fafafa' }}>
|
||||
<Statistic
|
||||
title="冻结中余额 (提现审核中)"
|
||||
value={((wallet?.frozen_balance_cent ?? 0) / 100).toFixed(2)}
|
||||
prefix={<LockOutlined style={{ color: '#faad14' }} />}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#faad14' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card bordered={false} style={{ background: '#fafafa' }}>
|
||||
<Statistic
|
||||
title="累计已结算收益"
|
||||
value={((wallet?.total_settled_cent ?? 0) / 100).toFixed(2)}
|
||||
prefix={<DollarCircleOutlined style={{ color: '#52c41a' }} />}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card bordered={false} style={{ background: '#fafafa' }}>
|
||||
<Statistic
|
||||
title="累计已成功提现"
|
||||
value={((wallet?.total_withdrawn_cent ?? 0) / 100).toFixed(2)}
|
||||
prefix={<CheckCircleOutlined style={{ color: '#13c2c2' }} />}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#13c2c2' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card title="提现记录明细">
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
dataSource={withdrawals}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showTotal: (tot) => `共 ${tot} 条提现记录`,
|
||||
onChange: (p) => void loadData(p),
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
title: '提现单号',
|
||||
dataIndex: 'withdraw_no',
|
||||
width: 170,
|
||||
render: (v: string) => <Typography.Text copyable strong>{v}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '提现金额',
|
||||
dataIndex: 'amount_cent',
|
||||
width: 140,
|
||||
render: (v: number) => (
|
||||
<Typography.Text strong style={{ color: '#cf1322', fontSize: 15 }}>
|
||||
{formatMoney(v)}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '收款账户',
|
||||
width: 220,
|
||||
render: (_: unknown, row: SiteWithdrawal) => (
|
||||
<div>
|
||||
<Space size={4}>
|
||||
<Tag color={row.account_type === 'alipay' ? 'blue' : 'gold'}>
|
||||
{row.account_type === 'alipay' ? '支付宝' : '银行卡'}
|
||||
</Tag>
|
||||
<Typography.Text strong>{row.account_name}</Typography.Text>
|
||||
</Space>
|
||||
<div style={{ fontSize: 12, color: '#666', marginTop: 2 }}>{row.account_no}</div>
|
||||
{row.bank_name && (
|
||||
<div style={{ fontSize: 11, color: '#999' }}>
|
||||
{row.bank_name} {row.bank_branch}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 130,
|
||||
render: (v: string) => {
|
||||
const conf = STATUS_CONFIG[v] || { label: v, color: 'default' }
|
||||
return <Tag color={conf.color}>{conf.label}</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '审核 / 付款备注',
|
||||
minWidth: 160,
|
||||
render: (_: unknown, row: SiteWithdrawal) => (
|
||||
<div>
|
||||
{row.review_remark && (
|
||||
<div style={{ fontSize: 12 }}>
|
||||
<span style={{ color: '#888' }}>审核: </span>{row.review_remark}
|
||||
</div>
|
||||
)}
|
||||
{row.payment_remark && (
|
||||
<div style={{ fontSize: 12 }}>
|
||||
<span style={{ color: '#888' }}>打款: </span>{row.payment_remark}
|
||||
</div>
|
||||
)}
|
||||
{row.payment_proof_url && (
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Image src={row.payment_proof_url} width={40} height={40} style={{ objectFit: 'cover', borderRadius: 4 }} />
|
||||
</div>
|
||||
)}
|
||||
{!row.review_remark && !row.payment_remark && !row.payment_proof_url && '-'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '申请时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 160,
|
||||
render: (v: string) => <span style={{ fontSize: 12, color: '#666' }}>{v}</span>,
|
||||
},
|
||||
{
|
||||
title: '打款时间',
|
||||
dataIndex: 'paid_at',
|
||||
width: 160,
|
||||
render: (v: string | undefined) => (
|
||||
<span style={{ fontSize: 12, color: v ? '#52c41a' : '#999' }}>{v || '待打款'}</span>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="申请提现"
|
||||
open={modalVisible}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleApplyWithdrawal}>
|
||||
<Form.Item label="提现站点">
|
||||
<Input disabled value={`${selectedSite?.site_name ?? ''} (${selectedSite?.site_code ?? ''})`} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={`提现金额 (当前可提现: ${formatMoney(wallet?.available_balance_cent)})`}
|
||||
name="amount"
|
||||
rules={[{ required: true, message: '请输入提现金额' }]}
|
||||
>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
min={0.01}
|
||||
max={((wallet?.available_balance_cent ?? 0) / 100)}
|
||||
precision={2}
|
||||
placeholder="请输入提现金额(元)"
|
||||
addonAfter={
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
form.setFieldsValue({
|
||||
amount: (wallet?.available_balance_cent ?? 0) / 100,
|
||||
})
|
||||
}}
|
||||
>
|
||||
全部提现
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="收款方式" name="account_type" rules={[{ required: true }]}>
|
||||
<Radio.Group>
|
||||
<Radio value="alipay">支付宝</Radio>
|
||||
<Radio value="bank">银行卡</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="收款人姓名" name="account_name" rules={[{ required: true, message: '请输入收款人姓名' }]}>
|
||||
<Input placeholder="真实姓名" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="收款账号" name="account_no" rules={[{ required: true, message: '请输入收款账号' }]}>
|
||||
<Input placeholder="支付宝账号 / 银行卡号" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prev, cur) => prev.account_type !== cur.account_type}
|
||||
>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue('account_type') === 'bank' ? (
|
||||
<>
|
||||
<Form.Item label="开户银行" name="bank_name" rules={[{ required: true, message: '请输入开户行' }]}>
|
||||
<Input placeholder="例如:招商银行" />
|
||||
</Form.Item>
|
||||
<Form.Item label="支行名称" name="bank_branch">
|
||||
<Input placeholder="例如:上海徐家汇支行(可选)" />
|
||||
</Form.Item>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
|
||||
<Space>
|
||||
<Button onClick={() => setModalVisible(false)}>取消</Button>
|
||||
<Button type="primary" htmlType="submit" loading={submitting}>
|
||||
提交提现申请
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user