Files
hfb_jysh/frontend/src/features/admin/AdminDistributionDashboardPage.tsx
T

253 lines
7.9 KiB
TypeScript

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>
)
}