237 lines
8.1 KiB
TypeScript
237 lines
8.1 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import {
|
|
Button, Card, Col, Descriptions, Modal, Row, Space, Statistic, Table, Typography, message,
|
|
} from 'antd';
|
|
import { CloudUploadOutlined, ReloadOutlined } from '@ant-design/icons';
|
|
import {
|
|
request,
|
|
type DashboardStats,
|
|
type DeployTriggerResult,
|
|
type HqProfile,
|
|
type SystemVersion,
|
|
} from '../lib/api';
|
|
import { 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 发布',
|
|
};
|
|
|
|
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 loadVersion = useCallback(() => {
|
|
setVersionLoading(true);
|
|
return request<SystemVersion | null>('/admin/dashboard/version')
|
|
.then(setVersion)
|
|
.catch(() => setVersion(null))
|
|
.finally(() => setVersionLoading(false));
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
request<DashboardStats>('/admin/dashboard/stats')
|
|
.then(setStats)
|
|
.finally(() => setLoading(false));
|
|
void loadVersion();
|
|
request<HqProfile>('/admin/auth/me').then(setProfile).catch(() => {});
|
|
}, [loadVersion]);
|
|
|
|
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) : '—';
|
|
|
|
return (
|
|
<div>
|
|
<Typography.Title level={4}>数据概览</Typography.Title>
|
|
|
|
<Card
|
|
title="系统版本"
|
|
loading={versionLoading}
|
|
style={{ marginBottom: 24 }}
|
|
extra={
|
|
<Space>
|
|
<Button icon={<ReloadOutlined />} onClick={() => void loadVersion()}>
|
|
刷新版本
|
|
</Button>
|
|
{isSuperAdmin && (
|
|
<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>
|
|
|
|
<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>
|
|
|
|
<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="待处理工单">
|
|
<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>
|
|
);
|
|
}
|