feat(distribution): 新增分销看板

This commit is contained in:
yml2213
2026-10-30 15:51:56 +08:00
parent 090ab02a67
commit d37cbad323
8 changed files with 427 additions and 2 deletions
+9
View File
@@ -185,6 +185,11 @@ const AdminDistributionWalletPage = lazy(() =>
default: module.AdminDistributionWalletPage,
}))
)
const AdminDistributionDashboardPage = lazy(() =>
import('@/features/admin/AdminDistributionDashboardPage').then(module => ({
default: module.AdminDistributionDashboardPage,
}))
)
const AdminDistributionSMSPage = lazy(() =>
import('@/features/admin/AdminDistributionSMSPage').then(module => ({
default: module.AdminDistributionSMSPage,
@@ -299,6 +304,10 @@ export function AppRouter() {
<Route path="audit-logs" element={<AdminAuditLogsPage />} />
<Route path="system-configs" element={<AdminSystemConfigsPage />} />
<Route path="distribution/sites" element={<AdminDistributionSitesPage />} />
<Route
path="distribution/dashboard"
element={<AdminDistributionDashboardPage />}
/>
<Route path="distribution/orders" element={<AdminDistributionOrdersPage />} />
<Route
path="distribution/settlements"
@@ -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>
)
}
+8
View File
@@ -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
+24
View File
@@ -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[]
}
+2
View File
@@ -98,6 +98,7 @@ const allNavGroups: NavGroup[] = [
label: '分销',
icon: <ShareAltOutlined />,
children: [
{ key: '/distribution/dashboard', label: '分销看板', permission: 'distribution:site_view' },
{ key: '/distribution/sites', label: '站点管理', permission: 'distribution:site_view' },
{ key: '/distribution/orders', label: '分销订单', permission: 'distribution:order_view' },
{
@@ -153,6 +154,7 @@ const sectionMeta: Record<string, { title: string; description: string }> = {
'/finance/operating-expenses': { title: '运营开支', description: '登记与核对日常运营支出。' },
'/withdrawals': { title: '提现审核', description: '审核提现申请与打款确认。' },
'/distribution/sites': { title: '分销站点', description: '管理分站、规则、结算与站点钱包。' },
'/distribution/dashboard': { title: '分销看板', description: '查看站点收号、订单、利润与近 7 天趋势。' },
'/distribution/orders': { title: '分销订单', description: '核对分销订单快照与冷却期释放状态。' },
'/distribution/settlements': { title: '结算批次', description: '生成人工批次并核对、释放站内份额。' },
'/distribution/users': { title: '分销用户', description: '查看站点注册客户、下单客户与消费贡献。' },
+50 -1
View File
@@ -167,7 +167,56 @@
margin-bottom: 14px;
}
/* 二维码池 */
/* 分销看板 */
.admin-dashboard-grid {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.admin-trend-card {
margin-top: 16px;
}
.admin-trend-scroll {
overflow-x: auto;
}
.admin-trend-row {
display: grid;
grid-template-columns: 80px 70px 70px 130px 130px minmax(160px, 1fr);
align-items: center;
gap: 12px;
min-width: 760px;
padding: 10px 0;
border-bottom: 1px dashed var(--line);
font-size: 13px;
}
.admin-trend-row:last-child {
border-bottom: none;
}
.admin-trend-row--head {
color: var(--ink-muted);
font-size: 11px;
letter-spacing: 0.04em;
}
.admin-trend-bar {
display: block;
height: 8px;
overflow: hidden;
border-radius: 999px;
background: #ece7dc;
}
.admin-trend-bar i {
display: block;
height: 100%;
border-radius: inherit;
background: var(--brand-seal);
}
/* 分销用户 */
.admin-qr-thumb {
border: 1px solid var(--line);
border-radius: 3px;