new branch

This commit is contained in:
rayd1o
2026-03-07 13:06:37 +08:00
parent 3145ff083b
commit 4ada75ca14
64 changed files with 4324 additions and 35 deletions

View File

@@ -5,17 +5,21 @@ import Dashboard from './pages/Dashboard/Dashboard'
import Users from './pages/Users/Users'
import DataSources from './pages/DataSources/DataSources'
import DataList from './pages/DataList/DataList'
import Earth from './pages/Earth/Earth'
function App() {
const { token } = useAuthStore()
const isEarthPage = window.location.pathname === '/earth'
if (!token) {
if (!token && !isEarthPage) {
return <Login />
}
return (
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/admin" element={<Dashboard />} />
<Route path="/earth" element={<Earth />} />
<Route path="/users" element={<Users />} />
<Route path="/datasources" element={<DataSources />} />
<Route path="/data" element={<DataList />} />

View File

@@ -0,0 +1,99 @@
import { ReactNode } from 'react'
import { Layout, Menu, Typography, Button } from 'antd'
import {
DashboardOutlined,
DatabaseOutlined,
UserOutlined,
SettingOutlined,
BarChartOutlined,
MenuUnfoldOutlined,
} from '@ant-design/icons'
import { Link, useLocation } from 'react-router-dom'
import { useAuthStore } from '../../stores/auth'
const { Header, Sider, Content } = Layout
const { Text } = Typography
interface AppLayoutProps {
children: ReactNode
}
function AppLayout({ children }: AppLayoutProps) {
const location = useLocation()
const { user, logout } = useAuthStore()
const menuItems = [
{ key: '/', icon: <DashboardOutlined />, label: <Link to="/"></Link> },
{ key: '/datasources', icon: <DatabaseOutlined />, label: <Link to="/datasources"></Link> },
{ key: '/data', icon: <BarChartOutlined />, label: <Link to="/data"></Link> },
{ key: '/users', icon: <UserOutlined />, label: <Link to="/users"></Link> },
{ key: '/settings', icon: <SettingOutlined />, label: <Link to="/settings"></Link> },
]
return (
<Layout className="dashboard-layout">
<Sider
width={240}
collapsible
collapsed={false}
onCollapse={(collapsed) => {
const sider = document.querySelector('.dashboard-sider') as HTMLElement
if (sider) {
sider.style.width = collapsed ? '80px' : '240px'
sider.style.minWidth = collapsed ? '80px' : '240px'
sider.style.maxWidth = collapsed ? '80px' : '240px'
}
}}
className="dashboard-sider"
trigger={null}
breakpoint="lg"
onBreakpoint={(broken) => {
const sider = document.querySelector('.dashboard-sider') as HTMLElement
if (sider) {
sider.style.width = broken ? '80px' : '240px'
sider.style.minWidth = broken ? '80px' : '240px'
sider.style.maxWidth = broken ? '80px' : '240px'
}
}}
>
<div style={{ height: 64, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Text strong style={{ color: 'white', fontSize: 18 }}></Text>
</div>
<Menu
theme="dark"
mode="inline"
selectedKeys={[location.pathname]}
items={menuItems}
/>
</Sider>
<Layout>
<Header className="dashboard-header" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 16px' }}>
<Button
type="text"
icon={<MenuUnfoldOutlined />}
onClick={() => {
const sider = document.querySelector('.ant-layout-sider') as HTMLElement
if (sider) {
const currentWidth = sider.style.width || '240px'
const isCollapsed = currentWidth === '80px'
sider.style.width = isCollapsed ? '240px' : '80px'
sider.style.minWidth = isCollapsed ? '240px' : '80px'
sider.style.maxWidth = isCollapsed ? '240px' : '80px'
}
}}
style={{ fontSize: 16 }}
/>
<Text strong>, {user?.username}</Text>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<Button type="link" danger onClick={logout}>退</Button>
</div>
</Header>
<Content className="dashboard-content" style={{ padding: 24, minHeight: '100%', overflow: 'auto' }}>
{children}
</Content>
</Layout>
</Layout>
)
}
export default AppLayout

View File

@@ -77,3 +77,42 @@ body {
.stat-card .trend.down {
color: #ff4d4f;
}
/* Table column resize */
.ant-table-wrapper .ant-table-thead > tr > th {
position: relative;
}
.resize-handle {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 6px;
cursor: col-resize;
background: transparent;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
}
.resize-handle::before {
content: '';
width: 2px;
height: 20px;
background: #d9d9d9;
border-radius: 1px;
}
.resize-handle:hover::before {
background: #1890ff;
}
/* Table cell fixed width */
.ant-table-wrapper .ant-table-tbody > tr > td {
max-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { Table, Tag, Card, Row, Col, Statistic, Button, Modal, Space, Descriptions } from 'antd'
import { AlertOutlined, InfoCircleOutlined, ReloadOutlined } from '@ant-design/icons'
import { useAuthStore } from '../../stores/auth'
import AppLayout from '../../components/AppLayout/AppLayout'
interface Alert {
id: number
@@ -140,7 +141,7 @@ function Alerts() {
)
return (
<div>
<AppLayout>
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col span={8}>
<Card>
@@ -173,7 +174,7 @@ function Alerts() {
title="告警列表"
extra={<Button icon={<ReloadOutlined />} onClick={fetchAlerts}></Button>}
>
<Table columns={columns} dataSource={alerts} rowKey="id" loading={loading} pagination={{ pageSize: 10 }} />
<Table columns={columns} dataSource={alerts} rowKey="id" loading={loading} pagination={{ pageSize: 10 }} scroll={{ x: 'max-content' }} tableLayout="fixed" />
</Card>
<Modal
@@ -212,7 +213,7 @@ function Alerts() {
</Descriptions>
)}
</Modal>
</div>
</AppLayout>
)
}

View File

@@ -1,13 +1,15 @@
import { useEffect, useState } from 'react'
import { useEffect, useState, useRef } from 'react'
import {
Table, Tag, Space, Card, Row, Col, Select, Input, Button,
Statistic, Modal, Descriptions, Spin, Empty, Tooltip
} from 'antd'
import type { ColumnsType } from 'antd/es/table'
import {
DatabaseOutlined, GlobalOutlined, CloudServerOutlined,
AppstoreOutlined, EyeOutlined, SearchOutlined
} from '@ant-design/icons'
import axios from 'axios'
import AppLayout from '../../components/AppLayout/AppLayout'
interface CollectedData {
id: number
@@ -153,17 +155,77 @@ function DataList() {
return colors[type] || 'default'
}
const columns = [
const [columnsWidth, setColumnsWidth] = useState<Record<string, number>>({
id: 60,
name: 300,
source: 150,
data_type: 100,
country: 100,
value: 100,
collected_at: 160,
action: 80,
})
const resizeRef = useRef<{ startX: number; startWidth: number; key: string } | null>(null)
const handleResizeStart = (key: string) => (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
resizeRef.current = {
startX: e.clientX,
startWidth: columnsWidth[key],
key,
}
document.addEventListener('mousemove', handleResizeMove)
document.addEventListener('mouseup', handleResizeEnd)
}
const handleResizeMove = (e: MouseEvent) => {
if (!resizeRef.current) return
const diff = e.clientX - resizeRef.current.startX
const newWidth = Math.max(50, resizeRef.current.startWidth + diff)
setColumnsWidth((prev) => ({
...prev,
[resizeRef.current!.key]: newWidth,
}))
}
const handleResizeEnd = () => {
resizeRef.current = null
document.removeEventListener('mousemove', handleResizeMove)
document.removeEventListener('mouseup', handleResizeEnd)
}
const columns: ColumnsType<CollectedData> = [
{
title: 'ID',
title: () => (
<div style={{ display: 'flex', alignItems: 'center', position: 'relative' }}>
<span>ID</span>
<div
className="resize-handle"
onMouseDown={handleResizeStart('id')}
onClick={(e) => e.stopPropagation()}
/>
</div>
),
dataIndex: 'id',
key: 'id',
width: 80,
width: columnsWidth.id,
},
{
title: '名称',
title: () => (
<div style={{ display: 'flex', alignItems: 'center', position: 'relative' }}>
<span></span>
<div
className="resize-handle"
onMouseDown={handleResizeStart('name')}
onClick={(e) => e.stopPropagation()}
/>
</div>
),
dataIndex: 'name',
key: 'name',
width: columnsWidth.name,
ellipsis: true,
render: (name: string, record: CollectedData) => (
<Tooltip title={name}>
@@ -174,49 +236,95 @@ function DataList() {
),
},
{
title: '数据源',
title: () => (
<div style={{ display: 'flex', alignItems: 'center', position: 'relative' }}>
<span></span>
<div
className="resize-handle"
onMouseDown={handleResizeStart('source')}
onClick={(e) => e.stopPropagation()}
/>
</div>
),
dataIndex: 'source',
key: 'source',
width: 150,
width: columnsWidth.source,
render: (source: string) => (
<Tag icon={getSourceIcon(source)}>{source}</Tag>
),
},
{
title: '类型',
title: () => (
<div style={{ display: 'flex', alignItems: 'center', position: 'relative' }}>
<span></span>
<div
className="resize-handle"
onMouseDown={handleResizeStart('data_type')}
onClick={(e) => e.stopPropagation()}
/>
</div>
),
dataIndex: 'data_type',
key: 'data_type',
width: 120,
width: columnsWidth.data_type,
render: (type: string) => (
<Tag color={getTypeColor(type)}>{type}</Tag>
),
},
{
title: '国家/地区',
title: () => (
<div style={{ display: 'flex', alignItems: 'center', position: 'relative' }}>
<span>/</span>
<div
className="resize-handle"
onMouseDown={handleResizeStart('country')}
onClick={(e) => e.stopPropagation()}
/>
</div>
),
dataIndex: 'country',
key: 'country',
width: 120,
width: columnsWidth.country,
ellipsis: true,
},
{
title: '数值',
title: () => (
<div style={{ display: 'flex', alignItems: 'center', position: 'relative' }}>
<span></span>
<div
className="resize-handle"
onMouseDown={handleResizeStart('value')}
onClick={(e) => e.stopPropagation()}
/>
</div>
),
dataIndex: 'value',
key: 'value',
width: 120,
width: columnsWidth.value,
render: (value: string | null, record: CollectedData) => (
value ? `${value} ${record.unit || ''}` : '-'
),
},
{
title: '采集时间',
title: () => (
<div style={{ display: 'flex', alignItems: 'center', position: 'relative' }}>
<span></span>
<div
className="resize-handle"
onMouseDown={handleResizeStart('collected_at')}
onClick={(e) => e.stopPropagation()}
/>
</div>
),
dataIndex: 'collected_at',
key: 'collected_at',
width: 180,
width: columnsWidth.collected_at,
render: (time: string) => new Date(time).toLocaleString('zh-CN'),
},
{
title: '操作',
key: 'action',
width: 80,
width: columnsWidth.action,
render: (_: unknown, record: CollectedData) => (
<Button
type="link"
@@ -230,7 +338,7 @@ function DataList() {
]
return (
<div>
<AppLayout>
<h2></h2>
{/* Summary Cards */}
@@ -305,6 +413,8 @@ function DataList() {
dataSource={data}
rowKey="id"
loading={loading}
scroll={{ x: 'max-content' }}
tableLayout="fixed"
pagination={{
current: page,
pageSize,
@@ -361,7 +471,7 @@ function DataList() {
<Empty description="暂无数据" />
)}
</Modal>
</div>
</AppLayout>
)
}

View File

@@ -10,6 +10,7 @@ import {
SyncOutlined
} from '@ant-design/icons'
import axios from 'axios'
import AppLayout from '../../components/AppLayout/AppLayout'
interface BuiltInDataSource {
id: number
@@ -326,6 +327,8 @@ function DataSources() {
rowKey="id"
loading={loading}
pagination={false}
scroll={{ x: 'max-content' }}
tableLayout="fixed"
/>
),
},
@@ -352,6 +355,8 @@ function DataSources() {
rowKey="id"
loading={loading}
pagination={false}
scroll={{ x: 'max-content' }}
tableLayout="fixed"
/>
)}
</>
@@ -360,7 +365,7 @@ function DataSources() {
]
return (
<div>
<AppLayout>
<h2></h2>
<Tabs activeKey={activeTab} onChange={setActiveTab} items={tabItems} />
@@ -665,7 +670,7 @@ function DataSources() {
</Form>
)}
</Drawer>
</div>
</AppLayout>
)
}

View File

@@ -0,0 +1,15 @@
function Earth() {
return (
<iframe
src="/earth/3dearthmult.html"
style={{
width: '100vw',
height: '100vh',
border: 'none',
}}
title="3D Earth"
/>
)
}
export default Earth

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { Table, Tag, Card, Row, Col, Statistic, Button } from 'antd'
import { ReloadOutlined, CheckCircleOutlined, CloseCircleOutlined, SyncOutlined } from '@ant-design/icons'
import { useAuthStore } from '../../stores/auth'
import AppLayout from '../../components/AppLayout/AppLayout'
interface Task {
id: number
@@ -107,7 +108,7 @@ function Tasks() {
const successRate = tasks.length > 0 ? (statusCounts.success / tasks.length) * 100 : 0
return (
<div>
<AppLayout>
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card>
@@ -144,9 +145,9 @@ function Tasks() {
</Button>
}
>
<Table columns={columns} dataSource={tasks} rowKey="id" loading={loading} pagination={{ pageSize: 10 }} />
<Table columns={columns} dataSource={tasks} rowKey="id" loading={loading} pagination={{ pageSize: 10 }} scroll={{ x: 'max-content' }} tableLayout="fixed" />
</Card>
</div>
</AppLayout>
)
}

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { Table, Button, Tag, Space, message, Modal, Form, Input, Select } from 'antd'
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'
import axios from 'axios'
import AppLayout from '../../components/AppLayout/AppLayout'
interface User {
id: number
@@ -113,12 +114,12 @@ function Users() {
]
return (
<div>
<AppLayout>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<h2></h2>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}></Button>
</div>
<Table columns={columns} dataSource={users} rowKey="id" loading={loading} />
<Table columns={columns} dataSource={users} rowKey="id" loading={loading} scroll={{ x: 'max-content' }} tableLayout="fixed" />
<Modal
title={editingUser ? '编辑用户' : '添加用户'}
open={modalVisible}
@@ -150,7 +151,7 @@ function Users() {
</Form.Item>
</Form>
</Modal>
</div>
</AppLayout>
)
}