54fca99208
总部财务以门店账单为唯一入口;新增 store-settlements API,去掉独立提现审菜单并更新企微引导文案。 Co-authored-by: Cursor <cursoragent@cursor.com>
775 lines
27 KiB
TypeScript
775 lines
27 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||
import { Link } from 'react-router-dom';
|
||
import {
|
||
Button, Card, Col, DatePicker, Descriptions, Form, Modal, Row, Select, Space, Statistic, Table, Typography, message,
|
||
} from 'antd';
|
||
import { CloudUploadOutlined, ReloadOutlined } from '@ant-design/icons';
|
||
import dayjs, { type Dayjs } from 'dayjs';
|
||
import ReactECharts from 'echarts-for-react';
|
||
import type { EChartsOption } from 'echarts';
|
||
import {
|
||
request,
|
||
type DashboardAnalytics,
|
||
type DashboardStats,
|
||
type DeployTriggerResult,
|
||
type HqProfile,
|
||
type Paginated,
|
||
type SystemVersion,
|
||
} from '../lib/api';
|
||
import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants';
|
||
|
||
const STATUS_LABELS: Record<string, string> = {
|
||
PENDING_PAY: '待付款',
|
||
PENDING_SHIP: '待发货',
|
||
OUT_WAREHOUSE: '已出库',
|
||
SHIPPING: '配送中',
|
||
PENDING_RECEIVE: '待收货',
|
||
COMPLETED: '已完成',
|
||
CANCELLED: '已取消',
|
||
REFUNDING: '退款中',
|
||
REFUNDED: '已退款',
|
||
};
|
||
|
||
const DEPLOYED_BY_LABELS: Record<string, string> = {
|
||
webhook: 'Webhook',
|
||
manual: '手动脚本',
|
||
admin: 'Admin 发布',
|
||
};
|
||
|
||
type CityOption = { id: string; name: string; code: string };
|
||
type PromoOption = { id: string; code: string; name: string };
|
||
|
||
type AnalyticsFilters = {
|
||
range: [Dayjs, Dayjs];
|
||
cityId?: string;
|
||
promoCodeId?: string;
|
||
partnerAccountId?: string;
|
||
};
|
||
|
||
function buildAnalyticsQs(f: AnalyticsFilters) {
|
||
const qs = new URLSearchParams();
|
||
qs.set('dateFrom', f.range[0].format('YYYY-MM-DD'));
|
||
qs.set('dateTo', f.range[1].format('YYYY-MM-DD'));
|
||
if (f.cityId) qs.set('cityId', f.cityId);
|
||
if (f.promoCodeId) qs.set('promoCodeId', f.promoCodeId);
|
||
if (f.partnerAccountId) qs.set('partnerAccountId', f.partnerAccountId);
|
||
return qs.toString();
|
||
}
|
||
|
||
export default function DashboardPage() {
|
||
const [stats, setStats] = useState<DashboardStats | null>(null);
|
||
const [version, setVersion] = useState<SystemVersion | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [versionLoading, setVersionLoading] = useState(true);
|
||
const [profile, setProfile] = useState<HqProfile | null>(null);
|
||
const [deploying, setDeploying] = useState(false);
|
||
const isSuperAdmin = profile?.adminRole === 'SUPER_ADMIN';
|
||
|
||
const [filterForm] = Form.useForm<{
|
||
range: [Dayjs, Dayjs];
|
||
cityId?: string;
|
||
promoCodeId?: string;
|
||
partnerAccountId?: string;
|
||
}>();
|
||
const [filters, setFilters] = useState<AnalyticsFilters>({
|
||
range: [dayjs().subtract(29, 'day'), dayjs()],
|
||
});
|
||
const [analytics, setAnalytics] = useState<DashboardAnalytics | null>(null);
|
||
const [analyticsLoading, setAnalyticsLoading] = useState(true);
|
||
const [cities, setCities] = useState<CityOption[]>([]);
|
||
const [promos, setPromos] = useState<PromoOption[]>([]);
|
||
const [partners, setPartners] = useState<Array<{ id: string; companyName?: string | null; name: string }>>([]);
|
||
|
||
const loadVersion = useCallback(() => {
|
||
setVersionLoading(true);
|
||
return request<SystemVersion | null>('/admin/dashboard/version')
|
||
.then(setVersion)
|
||
.catch(() => setVersion(null))
|
||
.finally(() => setVersionLoading(false));
|
||
}, []);
|
||
|
||
const loadAnalytics = useCallback((f: AnalyticsFilters) => {
|
||
setAnalyticsLoading(true);
|
||
return request<DashboardAnalytics>(`/admin/dashboard/analytics?${buildAnalyticsQs(f)}`)
|
||
.then(setAnalytics)
|
||
.catch((e) => {
|
||
message.error(e instanceof Error ? e.message : '加载统计失败');
|
||
setAnalytics(null);
|
||
})
|
||
.finally(() => setAnalyticsLoading(false));
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
request<DashboardStats>('/admin/dashboard/stats')
|
||
.then(setStats)
|
||
.finally(() => setLoading(false));
|
||
request<HqProfile>('/admin/auth/me')
|
||
.then((p) => {
|
||
setProfile(p);
|
||
if (p.adminRole === 'SUPER_ADMIN') {
|
||
void loadVersion();
|
||
} else {
|
||
setVersionLoading(false);
|
||
}
|
||
})
|
||
.catch(() => {
|
||
setVersionLoading(false);
|
||
});
|
||
void request<Paginated<CityOption>>(`/admin/cities?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`)
|
||
.then((res) => setCities(res.items ?? []))
|
||
.catch(() => setCities([]));
|
||
void request<Paginated<PromoOption>>(`/admin/promo-codes?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`)
|
||
.then((res) => setPromos(res.items ?? []))
|
||
.catch(() => setPromos([]));
|
||
void request<Paginated<{ id: string; companyName?: string | null; name: string }>>(
|
||
`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`,
|
||
)
|
||
.then((res) => setPartners(res.items ?? []))
|
||
.catch(() => setPartners([]));
|
||
}, [loadVersion]);
|
||
|
||
useEffect(() => {
|
||
void loadAnalytics(filters);
|
||
}, [filters, loadAnalytics]);
|
||
|
||
function applyFilters(values: {
|
||
range?: [Dayjs, Dayjs];
|
||
cityId?: string;
|
||
promoCodeId?: string;
|
||
partnerAccountId?: string;
|
||
}) {
|
||
const next: AnalyticsFilters = {
|
||
range: values.range ?? filters.range,
|
||
cityId: values.cityId || undefined,
|
||
promoCodeId: values.promoCodeId || undefined,
|
||
partnerAccountId: values.partnerAccountId || undefined,
|
||
};
|
||
setFilters(next);
|
||
}
|
||
|
||
function setCityFilter(cityId: string) {
|
||
const next = { ...filters, cityId: cityId === filters.cityId ? undefined : cityId };
|
||
filterForm.setFieldsValue({ cityId: next.cityId });
|
||
setFilters(next);
|
||
}
|
||
|
||
function setPromoFilter(promoCodeId: string) {
|
||
const key = promoCodeId === 'null' || promoCodeId === '' ? 'none' : promoCodeId;
|
||
const next = {
|
||
...filters,
|
||
promoCodeId: key === filters.promoCodeId ? undefined : key,
|
||
};
|
||
filterForm.setFieldsValue({ promoCodeId: next.promoCodeId });
|
||
setFilters(next);
|
||
}
|
||
|
||
function setPartnerFilter(partnerAccountId: string) {
|
||
const next = {
|
||
...filters,
|
||
partnerAccountId:
|
||
partnerAccountId === filters.partnerAccountId ? undefined : partnerAccountId,
|
||
};
|
||
filterForm.setFieldsValue({ partnerAccountId: next.partnerAccountId });
|
||
setFilters(next);
|
||
}
|
||
|
||
function handleDeploy() {
|
||
Modal.confirm({
|
||
title: '确认发布更新?',
|
||
content: '将触发服务器 webhook,拉取 auto-release.env 中配置的 GIT_BRANCH 并执行发版。发版通常需要数分钟,完成后可刷新版本信息。',
|
||
okText: '开始发布',
|
||
cancelText: '取消',
|
||
onOk: async () => {
|
||
setDeploying(true);
|
||
try {
|
||
const result = await request<DeployTriggerResult>('/admin/deploy/trigger', { method: 'POST' });
|
||
message.success(result.message || '已触发发布');
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '触发发布失败');
|
||
throw e;
|
||
} finally {
|
||
setDeploying(false);
|
||
}
|
||
},
|
||
});
|
||
}
|
||
|
||
const shortSha = version?.commitId ? version.commitId.slice(0, 7) : '—';
|
||
|
||
const ordersDrillQs = useMemo(() => {
|
||
const qs = new URLSearchParams();
|
||
qs.set('createdFrom', filters.range[0].format('YYYY-MM-DD'));
|
||
qs.set('createdTo', filters.range[1].format('YYYY-MM-DD'));
|
||
if (filters.cityId && filters.cityId !== 'none') qs.set('cityId', filters.cityId);
|
||
return qs.toString();
|
||
}, [filters]);
|
||
|
||
const byDateOption = useMemo<EChartsOption>(() => {
|
||
const rows = analytics?.byDate ?? [];
|
||
return {
|
||
tooltip: { trigger: 'axis' },
|
||
legend: { data: ['新增用户', '订单数'] },
|
||
grid: { left: 40, right: 20, top: 40, bottom: 40 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: rows.map((r) => r.date.slice(5)),
|
||
axisLabel: { rotate: rows.length > 14 ? 45 : 0 },
|
||
},
|
||
yAxis: { type: 'value', minInterval: 1 },
|
||
series: [
|
||
{ name: '新增用户', type: 'line', smooth: true, data: rows.map((r) => r.users) },
|
||
{ name: '订单数', type: 'line', smooth: true, data: rows.map((r) => r.orders) },
|
||
],
|
||
};
|
||
}, [analytics]);
|
||
|
||
const byCityOption = useMemo<EChartsOption>(() => {
|
||
const rows = analytics?.byCity ?? [];
|
||
return {
|
||
tooltip: { trigger: 'axis' },
|
||
legend: { data: ['用户', '订单'] },
|
||
grid: { left: 48, right: 20, top: 40, bottom: 48 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: rows.map((r) => r.cityName),
|
||
axisLabel: { interval: 0, rotate: rows.length > 4 ? 30 : 0 },
|
||
},
|
||
yAxis: { type: 'value', minInterval: 1 },
|
||
series: [
|
||
{ name: '用户', type: 'bar', data: rows.map((r) => r.users), barMaxWidth: 36 },
|
||
{ name: '订单', type: 'bar', data: rows.map((r) => r.orders), barMaxWidth: 36 },
|
||
],
|
||
};
|
||
}, [analytics]);
|
||
|
||
const byPromoOption = useMemo<EChartsOption>(() => {
|
||
const rows = analytics?.byPromo ?? [];
|
||
return {
|
||
tooltip: { trigger: 'axis' },
|
||
legend: { data: ['用户', '订单'] },
|
||
grid: { left: 48, right: 20, top: 40, bottom: 64 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: rows.map((r) => r.name || r.code),
|
||
axisLabel: { interval: 0, rotate: rows.length > 3 ? 30 : 0 },
|
||
},
|
||
yAxis: { type: 'value', minInterval: 1 },
|
||
series: [
|
||
{ name: '用户', type: 'bar', data: rows.map((r) => r.users), barMaxWidth: 36 },
|
||
{ name: '订单', type: 'bar', data: rows.map((r) => r.orders), barMaxWidth: 36 },
|
||
],
|
||
};
|
||
}, [analytics]);
|
||
|
||
const opsByDateOption = useMemo<EChartsOption>(() => {
|
||
const rows = analytics?.byDate ?? [];
|
||
return {
|
||
tooltip: { trigger: 'axis' },
|
||
legend: { data: ['新增合伙人', '新签门店', '核销笔数', '核销金额'] },
|
||
grid: { left: 48, right: 48, top: 48, bottom: 40 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: rows.map((r) => r.date.slice(5)),
|
||
axisLabel: { rotate: rows.length > 14 ? 45 : 0 },
|
||
},
|
||
yAxis: [
|
||
{ type: 'value', name: '数量', minInterval: 1 },
|
||
{ type: 'value', name: '金额', minInterval: 1 },
|
||
],
|
||
series: [
|
||
{ name: '新增合伙人', type: 'line', smooth: true, data: rows.map((r) => r.partners) },
|
||
{ name: '新签门店', type: 'line', smooth: true, data: rows.map((r) => r.stores) },
|
||
{ name: '核销笔数', type: 'line', smooth: true, data: rows.map((r) => r.redeems) },
|
||
{
|
||
name: '核销金额',
|
||
type: 'line',
|
||
smooth: true,
|
||
yAxisIndex: 1,
|
||
data: rows.map((r) => r.redeemAmount),
|
||
},
|
||
],
|
||
};
|
||
}, [analytics]);
|
||
|
||
const opsByCityOption = useMemo<EChartsOption>(() => {
|
||
const rows = analytics?.byCity ?? [];
|
||
return {
|
||
tooltip: { trigger: 'axis' },
|
||
legend: { data: ['合伙人', '门店', '核销笔数'] },
|
||
grid: { left: 48, right: 20, top: 40, bottom: 48 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: rows.map((r) => r.cityName),
|
||
axisLabel: { interval: 0, rotate: rows.length > 4 ? 30 : 0 },
|
||
},
|
||
yAxis: { type: 'value', minInterval: 1 },
|
||
series: [
|
||
{ name: '合伙人', type: 'bar', data: rows.map((r) => r.partners), barMaxWidth: 28 },
|
||
{ name: '门店', type: 'bar', data: rows.map((r) => r.stores), barMaxWidth: 28 },
|
||
{ name: '核销笔数', type: 'bar', data: rows.map((r) => r.redeems), barMaxWidth: 28 },
|
||
],
|
||
};
|
||
}, [analytics]);
|
||
|
||
const opsByPartnerOption = useMemo<EChartsOption>(() => {
|
||
const rows = analytics?.byPartner ?? [];
|
||
return {
|
||
tooltip: { trigger: 'axis' },
|
||
legend: { data: ['门店', '核销笔数', '核销金额'] },
|
||
grid: { left: 48, right: 48, top: 40, bottom: 64 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: rows.map((r) => r.companyName),
|
||
axisLabel: { interval: 0, rotate: rows.length > 3 ? 30 : 0 },
|
||
},
|
||
yAxis: [
|
||
{ type: 'value', name: '数量', minInterval: 1 },
|
||
{ type: 'value', name: '金额' },
|
||
],
|
||
series: [
|
||
{ name: '门店', type: 'bar', data: rows.map((r) => r.stores), barMaxWidth: 28 },
|
||
{ name: '核销笔数', type: 'bar', data: rows.map((r) => r.redeems), barMaxWidth: 28 },
|
||
{
|
||
name: '核销金额',
|
||
type: 'line',
|
||
yAxisIndex: 1,
|
||
data: rows.map((r) => r.redeemAmount),
|
||
},
|
||
],
|
||
};
|
||
}, [analytics]);
|
||
|
||
const redeemByCityOption = useMemo<EChartsOption>(() => {
|
||
const rows = analytics?.byCity ?? [];
|
||
return {
|
||
tooltip: { trigger: 'axis' },
|
||
legend: { data: ['核销笔数', '核销金额'] },
|
||
grid: { left: 48, right: 48, top: 40, bottom: 48 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: rows.map((r) => r.cityName),
|
||
axisLabel: { interval: 0, rotate: rows.length > 4 ? 30 : 0 },
|
||
},
|
||
yAxis: [
|
||
{ type: 'value', name: '笔数', minInterval: 1 },
|
||
{ type: 'value', name: '金额' },
|
||
],
|
||
series: [
|
||
{ name: '核销笔数', type: 'bar', data: rows.map((r) => r.redeems), barMaxWidth: 36 },
|
||
{
|
||
name: '核销金额',
|
||
type: 'line',
|
||
yAxisIndex: 1,
|
||
data: rows.map((r) => r.redeemAmount),
|
||
},
|
||
],
|
||
};
|
||
}, [analytics]);
|
||
|
||
return (
|
||
<div>
|
||
<Typography.Title level={4}>数据概览</Typography.Title>
|
||
|
||
{isSuperAdmin ? (
|
||
<Card
|
||
title="系统版本"
|
||
loading={versionLoading}
|
||
style={{ marginBottom: 24 }}
|
||
extra={
|
||
<Space>
|
||
<Button icon={<ReloadOutlined />} onClick={() => void loadVersion()}>
|
||
刷新版本
|
||
</Button>
|
||
<Button
|
||
type="primary"
|
||
icon={<CloudUploadOutlined />}
|
||
loading={deploying}
|
||
onClick={handleDeploy}
|
||
>
|
||
发布更新
|
||
</Button>
|
||
</Space>
|
||
}
|
||
>
|
||
{version ? (
|
||
<Descriptions column={{ xs: 1, sm: 2, lg: 3 }} size="small">
|
||
<Descriptions.Item label="分支">{version.branch || '—'}</Descriptions.Item>
|
||
<Descriptions.Item label="Tag">{version.gitTag || '—'}</Descriptions.Item>
|
||
<Descriptions.Item label="Commit">
|
||
<Typography.Text copyable={{ text: version.commitId }}>{shortSha}</Typography.Text>
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="提交说明" span={3}>{version.commitMessage}</Descriptions.Item>
|
||
<Descriptions.Item label="发布时间">{fmtTime(version.deployedAt)}</Descriptions.Item>
|
||
<Descriptions.Item label="触发来源">
|
||
{DEPLOYED_BY_LABELS[version.deployedBy || ''] || version.deployedBy || '—'}
|
||
</Descriptions.Item>
|
||
</Descriptions>
|
||
) : (
|
||
<Typography.Text type="secondary">尚未记录发版信息</Typography.Text>
|
||
)}
|
||
</Card>
|
||
) : null}
|
||
|
||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||
<Col xs={24} sm={12} lg={6}>
|
||
<Card loading={loading}>
|
||
<Statistic title="有效用户" value={stats?.usersTotal ?? 0} />
|
||
</Card>
|
||
</Col>
|
||
<Col xs={24} sm={12} lg={6}>
|
||
<Card loading={loading}>
|
||
<Statistic title="访客(未验手机)" value={stats?.guestUsers ?? 0} valueStyle={{ color: '#faad14' }} />
|
||
</Card>
|
||
</Col>
|
||
<Col xs={24} sm={12} lg={6}>
|
||
<Card loading={loading}>
|
||
<Statistic title="已验手机" value={stats?.verifiedUsers ?? 0} valueStyle={{ color: '#52c41a' }} />
|
||
</Card>
|
||
</Col>
|
||
<Col xs={24} sm={12} lg={6}>
|
||
<Card loading={loading}>
|
||
<Statistic title="今日下单" value={stats?.ordersToday ?? 0} />
|
||
</Card>
|
||
</Col>
|
||
<Col xs={24} sm={12} lg={6}>
|
||
<Card loading={loading}>
|
||
<Statistic title="门店" value={stats?.storesTotal ?? 0} />
|
||
</Card>
|
||
</Col>
|
||
<Col xs={24} sm={12} lg={6}>
|
||
<Card loading={loading}>
|
||
<Statistic title="合伙人" value={stats?.partnersTotal ?? 0} />
|
||
</Card>
|
||
</Col>
|
||
<Col xs={24} sm={12} lg={6}>
|
||
<Card loading={loading}>
|
||
<Statistic title="今日核销" value={stats?.redeemToday ?? 0} />
|
||
</Card>
|
||
</Col>
|
||
<Col xs={24} sm={12} lg={6}>
|
||
<Card loading={loading}>
|
||
<Statistic title="配送单" value={stats?.deliveriesTotal ?? 0} />
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
|
||
<Card
|
||
title="数据统计筛选"
|
||
style={{ marginBottom: 24 }}
|
||
extra={
|
||
<Button
|
||
icon={<ReloadOutlined />}
|
||
size="small"
|
||
onClick={() => void loadAnalytics(filters)}
|
||
>
|
||
刷新
|
||
</Button>
|
||
}
|
||
>
|
||
<Form
|
||
form={filterForm}
|
||
layout="inline"
|
||
initialValues={{ range: filters.range }}
|
||
onFinish={applyFilters}
|
||
>
|
||
<Form.Item name="range" label="日期" rules={[{ required: true, message: '请选择日期' }]}>
|
||
<DatePicker.RangePicker allowClear={false} />
|
||
</Form.Item>
|
||
<Form.Item name="cityId" label="城市">
|
||
<Select
|
||
allowClear
|
||
placeholder="全部开城"
|
||
style={{ width: 160 }}
|
||
options={[
|
||
{ value: 'none', label: '未选城' },
|
||
...cities.map((c) => ({ value: c.id, label: c.name })),
|
||
]}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item name="promoCodeId" label="推广码">
|
||
<Select
|
||
allowClear
|
||
placeholder="全部来源"
|
||
style={{ width: 200 }}
|
||
options={[
|
||
{ value: 'none', label: '自然量 / 无推广码' },
|
||
...promos.map((p) => ({ value: p.id, label: `${p.name}(${p.code})` })),
|
||
]}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item name="partnerAccountId" label="合伙人">
|
||
<Select
|
||
allowClear
|
||
placeholder="全部合伙人"
|
||
style={{ width: 200 }}
|
||
options={partners.map((p) => ({
|
||
value: p.id,
|
||
label: p.companyName || p.name,
|
||
}))}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item>
|
||
<Button type="primary" htmlType="submit">查询</Button>
|
||
</Form.Item>
|
||
</Form>
|
||
</Card>
|
||
|
||
<Card
|
||
title="用户 / 订单统计"
|
||
style={{ marginBottom: 24 }}
|
||
extra={
|
||
<Space>
|
||
<Link to={`/orders?${ordersDrillQs}`}>查看订单</Link>
|
||
<Link to="/users">查看用户</Link>
|
||
</Space>
|
||
}
|
||
>
|
||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||
<Col xs={24} sm={8}>
|
||
<Statistic title="区间新增用户" value={analytics?.summary.users ?? 0} />
|
||
</Col>
|
||
<Col xs={24} sm={8}>
|
||
<Statistic title="区间订单数" value={analytics?.summary.orders ?? 0} />
|
||
</Col>
|
||
<Col xs={24} sm={8}>
|
||
<Statistic title="区间付费用户" value={analytics?.summary.payingUsers ?? 0} />
|
||
</Col>
|
||
</Row>
|
||
|
||
<Row gutter={[16, 16]}>
|
||
<Col xs={24}>
|
||
<Card type="inner" title="按日趋势" loading={analyticsLoading} size="small">
|
||
<ReactECharts option={byDateOption} style={{ height: 320 }} notMerge />
|
||
</Card>
|
||
</Col>
|
||
<Col xs={24} lg={12}>
|
||
<Card
|
||
type="inner"
|
||
title="按城市(点击柱联动筛选)"
|
||
loading={analyticsLoading}
|
||
size="small"
|
||
>
|
||
<ReactECharts
|
||
option={byCityOption}
|
||
style={{ height: 300 }}
|
||
notMerge
|
||
onEvents={{
|
||
click: (params: { name?: string }) => {
|
||
const row = (analytics?.byCity ?? []).find((r) => r.cityName === params.name);
|
||
if (row) setCityFilter(row.cityId);
|
||
},
|
||
}}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
<Col xs={24} lg={12}>
|
||
<Card
|
||
type="inner"
|
||
title="按推广码(点击柱联动筛选)"
|
||
loading={analyticsLoading}
|
||
size="small"
|
||
>
|
||
<ReactECharts
|
||
option={byPromoOption}
|
||
style={{ height: 300 }}
|
||
notMerge
|
||
onEvents={{
|
||
click: (params: { name?: string }) => {
|
||
const row = (analytics?.byPromo ?? []).find(
|
||
(r) => (r.name || r.code) === params.name,
|
||
);
|
||
if (row) setPromoFilter(row.promoCodeId ?? 'none');
|
||
},
|
||
}}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
</Card>
|
||
|
||
<Card
|
||
title="合伙人 / 门店 / 核销统计"
|
||
style={{ marginBottom: 24 }}
|
||
extra={
|
||
<Space>
|
||
<Link to="/city-partners">查看合伙人</Link>
|
||
<Link to="/stores">查看门店</Link>
|
||
<Link to="/redeem-records">查看核销</Link>
|
||
</Space>
|
||
}
|
||
>
|
||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||
<Col xs={24} sm={6}>
|
||
<Statistic title="区间新增合伙人" value={analytics?.summary.partners ?? 0} />
|
||
</Col>
|
||
<Col xs={24} sm={6}>
|
||
<Statistic title="区间新签门店" value={analytics?.summary.stores ?? 0} />
|
||
</Col>
|
||
<Col xs={24} sm={6}>
|
||
<Statistic title="区间核销笔数" value={analytics?.summary.redeems ?? 0} />
|
||
</Col>
|
||
<Col xs={24} sm={6}>
|
||
<Statistic title="区间核销金额" value={analytics?.summary.redeemAmount ?? 0} precision={2} />
|
||
</Col>
|
||
</Row>
|
||
|
||
<Row gutter={[16, 16]}>
|
||
<Col xs={24}>
|
||
<Card type="inner" title="按日趋势(合伙人 / 门店 / 核销)" loading={analyticsLoading} size="small">
|
||
<ReactECharts option={opsByDateOption} style={{ height: 320 }} notMerge />
|
||
</Card>
|
||
</Col>
|
||
<Col xs={24} lg={12}>
|
||
<Card
|
||
type="inner"
|
||
title="按城市(点击柱联动筛选)"
|
||
loading={analyticsLoading}
|
||
size="small"
|
||
>
|
||
<ReactECharts
|
||
option={opsByCityOption}
|
||
style={{ height: 300 }}
|
||
notMerge
|
||
onEvents={{
|
||
click: (params: { name?: string }) => {
|
||
const row = (analytics?.byCity ?? []).find((r) => r.cityName === params.name);
|
||
if (row) setCityFilter(row.cityId);
|
||
},
|
||
}}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
<Col xs={24} lg={12}>
|
||
<Card
|
||
type="inner"
|
||
title="按城市核销金额"
|
||
loading={analyticsLoading}
|
||
size="small"
|
||
>
|
||
<ReactECharts
|
||
option={redeemByCityOption}
|
||
style={{ height: 300 }}
|
||
notMerge
|
||
onEvents={{
|
||
click: (params: { name?: string }) => {
|
||
const row = (analytics?.byCity ?? []).find((r) => r.cityName === params.name);
|
||
if (row) setCityFilter(row.cityId);
|
||
},
|
||
}}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
<Col xs={24}>
|
||
<Card
|
||
type="inner"
|
||
title="按合伙人(门店 / 核销,点击联动筛选)"
|
||
loading={analyticsLoading}
|
||
size="small"
|
||
>
|
||
<ReactECharts
|
||
option={opsByPartnerOption}
|
||
style={{ height: 320 }}
|
||
notMerge
|
||
onEvents={{
|
||
click: (params: { name?: string }) => {
|
||
const row = (analytics?.byPartner ?? []).find(
|
||
(r) => r.companyName === params.name,
|
||
);
|
||
if (row) setPartnerFilter(row.partnerAccountId);
|
||
},
|
||
}}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
</Card>
|
||
|
||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||
<Col xs={24} sm={12} lg={8}>
|
||
<Card
|
||
loading={loading}
|
||
title="待审核合伙人打款"
|
||
extra={<Link to="/finance/partner-bills">去处理</Link>}
|
||
>
|
||
<Statistic
|
||
value={stats?.pendingBills ?? 0}
|
||
suffix="笔"
|
||
valueStyle={{ color: (stats?.pendingBills ?? 0) > 0 ? '#fa8c16' : undefined }}
|
||
/>
|
||
<Typography.Text type="secondary">
|
||
合伙人已确认申请,待总部通过或驳回
|
||
{(stats?.pendingPartnerDraftBills ?? 0) > 0
|
||
? `(另有 ${stats?.pendingPartnerDraftBills} 笔待合伙人确认)`
|
||
: ''}
|
||
</Typography.Text>
|
||
</Card>
|
||
</Col>
|
||
<Col xs={24} sm={12} lg={8}>
|
||
<Card
|
||
loading={loading}
|
||
title="待打款门店结算"
|
||
extra={<Link to="/finance/store-bills">去处理</Link>}
|
||
>
|
||
<Statistic
|
||
value={stats?.pendingPayouts ?? 0}
|
||
suffix="笔"
|
||
valueStyle={{ color: (stats?.pendingPayouts ?? 0) > 0 ? '#fa8c16' : undefined }}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
<Col xs={24} sm={12} lg={8}>
|
||
<Card
|
||
loading={loading}
|
||
title="待审门店提现"
|
||
extra={<Link to="/finance/store-bills?kind=WITHDRAW">去处理</Link>}
|
||
>
|
||
<Statistic
|
||
value={stats?.pendingStoreWithdrawals ?? 0}
|
||
suffix="笔"
|
||
valueStyle={{
|
||
color: (stats?.overdueStoreWithdrawals ?? 0) > 0 ? '#cf1322' : (stats?.pendingStoreWithdrawals ?? 0) > 0 ? '#fa8c16' : undefined,
|
||
}}
|
||
/>
|
||
<Typography.Text type="secondary">
|
||
{(stats?.overdueStoreWithdrawals ?? 0) > 0
|
||
? `超时未审 ${stats?.overdueStoreWithdrawals} 笔(FIN-003)`
|
||
: '工作日 T+0 审完'}
|
||
</Typography.Text>
|
||
</Card>
|
||
</Col>
|
||
<Col xs={24} sm={12} lg={8}>
|
||
<Card loading={loading} title="待处理工单">
|
||
<Statistic value={stats?.openTickets ?? 0} suffix="个" />
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
|
||
<Row gutter={[16, 16]}>
|
||
<Col xs={24} lg={12}>
|
||
<Card title="已合并访客账号" loading={loading}>
|
||
<Statistic value={stats?.mergedUsers ?? 0} suffix="个" />
|
||
</Card>
|
||
</Col>
|
||
<Col xs={24} lg={12}>
|
||
<Card title="订单状态分布" loading={loading}>
|
||
<Table
|
||
size="small"
|
||
pagination={false}
|
||
rowKey="status"
|
||
dataSource={stats?.ordersByStatus ?? []}
|
||
columns={[
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'status',
|
||
render: (s: string) => STATUS_LABELS[s] || s,
|
||
},
|
||
{ title: '数量', dataIndex: 'count' },
|
||
]}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
</div>
|
||
);
|
||
}
|