171 lines
6.9 KiB
TypeScript
171 lines
6.9 KiB
TypeScript
import React, { useEffect, useMemo, useState } from 'react'
|
||
import { Card, Col, Progress, Row, Statistic, Table, Tag, Typography } from 'antd'
|
||
import api from '../utils/axios'
|
||
import { formatDateTime } from '../utils/datetime'
|
||
import PageHeader from '../components/PageHeader'
|
||
|
||
interface UserItem { id: number; username: string; nickname?: string; status?: number; created_at?: string }
|
||
|
||
export default function Dashboard() {
|
||
const [loading, setLoading] = useState(false)
|
||
const [userTotal, setUserTotal] = useState(0)
|
||
const [roleTotal, setRoleTotal] = useState(0)
|
||
const [deptTotal, setDeptTotal] = useState(0)
|
||
const [menuTotal, setMenuTotal] = useState(0)
|
||
|
||
const [userSample, setUserSample] = useState([] as UserItem[])
|
||
|
||
useEffect(() => {
|
||
const fetchAll = async () => {
|
||
setLoading(true)
|
||
try {
|
||
const [usersRes, rolesRes, deptsRes, menusRes, usersSampleRes] = await Promise.all([
|
||
api.get('/users', { params: { page: 1, page_size: 1 } }),
|
||
api.get('/roles', { params: { page: 1, page_size: 1 } }),
|
||
api.get('/departments', { params: { keyword: '' } }),
|
||
api.get('/menus'),
|
||
api.get('/users', { params: { page: 1, page_size: 200 } }),
|
||
])
|
||
if (usersRes.data?.code === 0) setUserTotal(Number(usersRes.data?.data?.total || 0))
|
||
if (rolesRes.data?.code === 0) setRoleTotal(Number(rolesRes.data?.data?.total || 0))
|
||
if (deptsRes.data?.code === 0) setDeptTotal(Array.isArray(deptsRes.data?.data) ? deptsRes.data.data.length : 0)
|
||
if (menusRes.data?.code === 0) setMenuTotal(Array.isArray(menusRes.data?.data) ? menusRes.data.data.length : 0)
|
||
if (usersSampleRes.data?.code === 0) setUserSample(Array.isArray(usersSampleRes.data?.data?.items) ? usersSampleRes.data.data.items : [])
|
||
} catch (e) {
|
||
// ignore on dashboard
|
||
} finally { setLoading(false) }
|
||
}
|
||
fetchAll()
|
||
}, [])
|
||
|
||
// 用户状态分布(基于 sample 数据近似统计)
|
||
const statusDist = useMemo(() => {
|
||
const enabled = userSample.reduce((acc, u) => acc + (u.status === 1 ? 1 : 0), 0)
|
||
const total = userSample.length || 1
|
||
const percentEnabled = Math.round((enabled / total) * 100)
|
||
return { enabled, disabled: total - enabled, percentEnabled }
|
||
}, [userSample])
|
||
|
||
// 近7天新增用户(基于 sample 的 created_at 统计)
|
||
const last7Days = useMemo(() => {
|
||
const today = new Date()
|
||
const days: string[] = []
|
||
for (let i = 6; i >= 0; i--) {
|
||
const d = new Date(today)
|
||
d.setDate(today.getDate() - i)
|
||
const y = d.getFullYear()
|
||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||
const da = String(d.getDate()).padStart(2, '0')
|
||
days.push(`${y}-${m}-${da}`)
|
||
}
|
||
const counter: Record<string, number> = Object.fromEntries(days.map(d => [d, 0]))
|
||
userSample.forEach(u => {
|
||
if (!u.created_at) return
|
||
const dt = new Date(u.created_at)
|
||
if (Number.isNaN(dt.getTime())) return
|
||
const y = dt.getFullYear()
|
||
const m = String(dt.getMonth() + 1).padStart(2, '0')
|
||
const da = String(dt.getDate()).padStart(2, '0')
|
||
const key = `${y}-${m}-${da}`
|
||
if (key in counter) counter[key] += 1
|
||
})
|
||
return days.map(d => ({ date: d, value: counter[d] || 0 }))
|
||
}, [userSample])
|
||
|
||
const userColumns = useMemo(() => [
|
||
{ title: 'ID', dataIndex: 'id', width: 80 },
|
||
{ title: '用户名', dataIndex: 'username' },
|
||
{ title: '昵称', dataIndex: 'nickname' },
|
||
{ title: '状态', dataIndex: 'status', render: (v: number) => v === 1 ? <Tag color="green">启用</Tag> : <Tag>禁用</Tag> },
|
||
{ title: '创建时间', dataIndex: 'created_at', render: (v: any) => formatDateTime(v) },
|
||
], [])
|
||
|
||
const recentUsers = useMemo(() => {
|
||
const withTime = userSample.filter(u => !!u.created_at)
|
||
withTime.sort((a, b) => (new Date(b.created_at!).getTime() - new Date(a.created_at!).getTime()))
|
||
return withTime.slice(0, 8)
|
||
}, [userSample])
|
||
|
||
const maxDaily = Math.max(...last7Days.map(d => d.value), 1)
|
||
|
||
return (
|
||
<div>
|
||
<PageHeader items={["首页"]} title="首页" />
|
||
<Row gutter={[16, 16]}>
|
||
<Col xs={12} sm={12} md={6}>
|
||
<Card loading={loading}>
|
||
<Statistic title="用户总数" value={userTotal} />
|
||
</Card>
|
||
</Col>
|
||
<Col xs={12} sm={12} md={6}>
|
||
<Card loading={loading}>
|
||
<Statistic title="角色总数" value={roleTotal} />
|
||
</Card>
|
||
</Col>
|
||
<Col xs={12} sm={12} md={6}>
|
||
<Card loading={loading}>
|
||
<Statistic title="部门总数" value={deptTotal} />
|
||
</Card>
|
||
</Col>
|
||
<Col xs={12} sm={12} md={6}>
|
||
<Card loading={loading}>
|
||
<Statistic title="菜单总数" value={menuTotal} />
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
|
||
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
||
<Col xs={24} md={12}>
|
||
<Card title="用户状态分布(样本)" loading={loading}>
|
||
<Row gutter={24}>
|
||
<Col span={12} style={{ textAlign: 'center' }}>
|
||
<Progress type="dashboard" percent={statusDist.percentEnabled} />
|
||
<div style={{ marginTop: 8 }}>启用占比</div>
|
||
</Col>
|
||
<Col span={12}>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||
<div>启用:<Tag color="green">{statusDist.enabled}</Tag></div>
|
||
<div>禁用:<Tag>{statusDist.disabled}</Tag></div>
|
||
<div>样本:{userSample.length}</div>
|
||
</div>
|
||
</Col>
|
||
</Row>
|
||
</Card>
|
||
</Col>
|
||
<Col xs={24} md={12}>
|
||
<Card title="近7天新增用户(样本)" loading={loading}>
|
||
<div style={{ height: 180, display: 'flex', alignItems: 'flex-end', gap: 8 }}>
|
||
{last7Days.map(d => (
|
||
<div key={d.date} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||
<div style={{ width: 20, background: '#1677ff', height: Math.max(4, (d.value / maxDaily) * 140) }} />
|
||
<div style={{ marginTop: 6, fontSize: 12 }}>{d.value}</div>
|
||
<div style={{ marginTop: 2, fontSize: 12, color: '#999' }}>{d.date.slice(5)}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
|
||
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
||
<Col span={24}>
|
||
<Card title="最近注册用户(样本)">
|
||
<Table<UserItem>
|
||
rowKey="id"
|
||
size="small"
|
||
pagination={false}
|
||
dataSource={recentUsers}
|
||
columns={userColumns as any}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
|
||
<Card style={{ marginTop: 16 }}>
|
||
<Typography.Paragraph>
|
||
欢迎使用 Udmin。当前首页展示了若干概览与示例报表,真实数据以接口返回为准。
|
||
</Typography.Paragraph>
|
||
</Card>
|
||
</div>
|
||
)
|
||
} |