253 lines
7.9 KiB
TypeScript
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>
|
|
)
|
|
}
|