new branch
This commit is contained in:
@@ -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 />} />
|
||||
|
||||
99
frontend/src/components/AppLayout/AppLayout.tsx
Normal file
99
frontend/src/components/AppLayout/AppLayout.tsx
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
15
frontend/src/pages/Earth/Earth.tsx
Normal file
15
frontend/src/pages/Earth/Earth.tsx
Normal 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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user