Files
udmin/frontend/src/pages/Dashboard.tsx
2025-08-28 00:55:35 +08:00

171 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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