feat(distribution): 完善分销收号资产双视图、商户提现、自主加价、品牌装修与数据大盘
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user