feat(distribution): 新增分销看板
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Alert, Button, Card, Space, Tag, Typography } from 'antd'
|
||||
import {
|
||||
BarChartOutlined,
|
||||
ReloadOutlined,
|
||||
ShoppingCartOutlined,
|
||||
TeamOutlined,
|
||||
WalletOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { fetchAdminDistributionAnalytics } from '@/features/admin/api'
|
||||
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
|
||||
import {
|
||||
DistributionSiteSelect,
|
||||
useDistributionSiteSelection,
|
||||
} from '@/features/admin/components/DistributionSiteSelect'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import { getAdminBasePath } from '@/shared/api/adminClient'
|
||||
import { formatMoney } from '@/shared/utils/format'
|
||||
|
||||
function errorText(error: unknown) {
|
||||
const response = (error as { response?: { data?: { message?: string } } })?.response
|
||||
if (response?.data?.message) return response.data.message
|
||||
return error instanceof Error ? error.message : '请求失败,请稍后重试'
|
||||
}
|
||||
|
||||
function count(value?: number | null) {
|
||||
return (value ?? 0).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
export function AdminDistributionDashboardPage() {
|
||||
const navigate = useNavigate()
|
||||
const permissions = useAdminSessionStore(state => state.admin?.permissions)
|
||||
const canView = Boolean(
|
||||
permissions?.includes('*') || permissions?.includes('distribution:site_view')
|
||||
)
|
||||
const selection = useDistributionSiteSelection()
|
||||
const siteId = selection.siteId
|
||||
|
||||
const analyticsQuery = useQuery({
|
||||
queryKey: ['admin-distribution-analytics', siteId],
|
||||
queryFn: () => fetchAdminDistributionAnalytics(siteId as number),
|
||||
enabled: canView && Boolean(siteId),
|
||||
})
|
||||
|
||||
const data = analyticsQuery.data
|
||||
const trendDays = data?.trend_days ?? []
|
||||
const maxProfit = useMemo(
|
||||
() => Math.max(1, ...trendDays.map(point => point.profit_cent)),
|
||||
[trendDays]
|
||||
)
|
||||
|
||||
const metrics = [
|
||||
{
|
||||
key: 'today_received',
|
||||
label: '今日收号',
|
||||
value: count(data?.today_received_count),
|
||||
hint: `累计收货 ${count(data?.total_received_count)} 件`,
|
||||
},
|
||||
{
|
||||
key: 'active',
|
||||
label: '在售商品',
|
||||
value: count(data?.active_listing_count),
|
||||
hint: '已上架且审核通过',
|
||||
},
|
||||
{
|
||||
key: 'today_orders',
|
||||
label: '今日订单',
|
||||
value: count(data?.today_order_count),
|
||||
hint: '按订单创建时间统计',
|
||||
},
|
||||
{
|
||||
key: 'today_gmv',
|
||||
label: '今日 GMV',
|
||||
value: formatMoney(data?.today_gmv_cent),
|
||||
hint: '按支付快照实付金额',
|
||||
},
|
||||
{
|
||||
key: 'today_profit',
|
||||
label: '今日利润',
|
||||
value: formatMoney(data?.today_profit_cent),
|
||||
hint: '站点参与方利润',
|
||||
accent: true,
|
||||
},
|
||||
{
|
||||
key: 'month_profit',
|
||||
label: '本月利润',
|
||||
value: formatMoney(data?.month_profit_cent),
|
||||
hint: '自然月累计',
|
||||
},
|
||||
{
|
||||
key: 'balance',
|
||||
label: '可用余额',
|
||||
value: formatMoney(data?.available_balance_cent),
|
||||
hint: '可申请提现',
|
||||
},
|
||||
{
|
||||
key: 'today_users',
|
||||
label: '今日新增',
|
||||
value: count(data?.today_user_count),
|
||||
hint: `累计注册 ${count(data?.total_user_count)} 人`,
|
||||
},
|
||||
{
|
||||
key: 'order_users',
|
||||
label: '成交客户',
|
||||
value: count(data?.total_order_user_count),
|
||||
hint: '已产生有效订单',
|
||||
},
|
||||
{
|
||||
key: 'total_users',
|
||||
label: '累计用户',
|
||||
value: count(data?.total_user_count),
|
||||
hint: '本站注册用户',
|
||||
},
|
||||
]
|
||||
|
||||
const adminBase = getAdminBasePath()
|
||||
const go = (path: string) => navigate(`${adminBase}${path}`)
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader
|
||||
eyebrow="JIEYOU OPS · 分销"
|
||||
title="分销看板"
|
||||
description="按站点查看收号、订单、GMV、利润与近 7 天趋势。"
|
||||
meta={
|
||||
<Space size={8} wrap>
|
||||
<Tag color="geekblue">近 7 天趋势</Tag>
|
||||
{selection.site ? <Tag>{selection.site.site_name}</Tag> : null}
|
||||
{analyticsQuery.isFetching ? <Tag color="processing">正在刷新</Tag> : null}
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Button icon={<ShoppingCartOutlined />} onClick={() => go('/distribution/orders')}>
|
||||
分销订单
|
||||
</Button>
|
||||
<Button icon={<TeamOutlined />} onClick={() => go('/distribution/users')}>
|
||||
分销用户
|
||||
</Button>
|
||||
<Button icon={<WalletOutlined />} onClick={() => go('/distribution/wallet')}>
|
||||
站点钱包
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={analyticsQuery.isFetching}
|
||||
disabled={!canView}
|
||||
onClick={() => {
|
||||
void analyticsQuery.refetch()
|
||||
void selection.refetch()
|
||||
}}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
{!canView ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
className="admin-table-alert"
|
||||
message="当前账号没有分销站点查看权限"
|
||||
description="需要 distribution:site_view 权限才能读取分销看板。"
|
||||
/>
|
||||
) : (
|
||||
<DistributionSiteSelect selection={selection} />
|
||||
)}
|
||||
|
||||
{analyticsQuery.isError ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
className="admin-table-alert"
|
||||
message="分销看板没有加载出来"
|
||||
description={errorText(analyticsQuery.error)}
|
||||
action={
|
||||
<Button size="small" onClick={() => void analyticsQuery.refetch()}>
|
||||
重试
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="admin-wallet-grid admin-dashboard-grid">
|
||||
{metrics.map(metric => (
|
||||
<Card
|
||||
key={metric.key}
|
||||
className={`admin-wallet-card${metric.accent ? ' admin-wallet-card--accent' : ''}`}
|
||||
bordered={false}
|
||||
loading={analyticsQuery.isPending}
|
||||
>
|
||||
<span className="admin-wallet-card__label">{metric.label}</span>
|
||||
<strong className="admin-wallet-card__value">{metric.value}</strong>
|
||||
<span className="admin-wallet-card__hint">{metric.hint}</span>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Card
|
||||
className="admin-panel-card admin-trend-card"
|
||||
title={
|
||||
<Space>
|
||||
<BarChartOutlined />
|
||||
<span>近 7 天经营趋势</span>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Typography.Text type="secondary">
|
||||
收号 / 订单 / GMV / 站点利润
|
||||
</Typography.Text>
|
||||
}
|
||||
>
|
||||
{trendDays.length === 0 ? (
|
||||
<Typography.Text type="secondary">
|
||||
{analyticsQuery.isPending ? '加载中…' : '暂无趋势数据'}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<div className="admin-trend-scroll">
|
||||
<div className="admin-trend-row admin-trend-row--head">
|
||||
<span>日期</span>
|
||||
<span>收号</span>
|
||||
<span>订单</span>
|
||||
<span>GMV</span>
|
||||
<span>利润</span>
|
||||
<span>利润趋势</span>
|
||||
</div>
|
||||
{trendDays.map(point => (
|
||||
<div className="admin-trend-row" key={point.date}>
|
||||
<span className="admin-mono">{point.date.slice(5)}</span>
|
||||
<span>{count(point.received_count)}</span>
|
||||
<span>{count(point.order_count)}</span>
|
||||
<span>{formatMoney(point.gmv_cent)}</span>
|
||||
<strong>{formatMoney(point.profit_cent)}</strong>
|
||||
<span className="admin-trend-bar">
|
||||
<i
|
||||
style={{
|
||||
width: `${Math.max(2, Math.round((point.profit_cent / maxProfit) * 100))}%`,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
AdminDispute,
|
||||
AdminDisputePage,
|
||||
AdminDisputeQuery,
|
||||
AdminDistributionAnalytics,
|
||||
AdminDistributionInvitationConfig,
|
||||
AdminDistributionOrder,
|
||||
AdminDistributionOrderPage,
|
||||
@@ -511,6 +512,13 @@ export async function fetchAdminDistributionWallet(id: number | string) {
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchAdminDistributionAnalytics(id: number | string) {
|
||||
const { data } = await adminClient.get<ApiResponse<AdminDistributionAnalytics>>(
|
||||
`/admin/distribution/sites/${id}/analytics`
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function applyAdminDistributionWithdrawal(
|
||||
id: number | string,
|
||||
payload: ApplyAdminDistributionWithdrawalPayload
|
||||
|
||||
@@ -1256,3 +1256,27 @@ export interface AdminPaymentConfigOption {
|
||||
environment: string
|
||||
is_default: boolean
|
||||
}
|
||||
|
||||
/** 分销站点看板:金额单位全部为分,趋势固定为最近 7 天。 */
|
||||
export interface AdminDistributionTrendPoint {
|
||||
date: string
|
||||
received_count: number
|
||||
order_count: number
|
||||
gmv_cent: number
|
||||
profit_cent: number
|
||||
}
|
||||
|
||||
export interface AdminDistributionAnalytics {
|
||||
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
|
||||
today_user_count: number
|
||||
total_user_count: number
|
||||
total_order_user_count: number
|
||||
trend_days: AdminDistributionTrendPoint[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user