init
This commit is contained in:
171
frontend/src/pages/Dashboard.tsx
Normal file
171
frontend/src/pages/Dashboard.tsx
Normal file
@ -0,0 +1,171 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user