初始化项目
This commit is contained in:
9
.env.example
Normal file
9
.env.example
Normal file
@@ -0,0 +1,9 @@
|
||||
# GEMINI_API_KEY: Required for Gemini AI API calls.
|
||||
# AI Studio automatically injects this at runtime from user secrets.
|
||||
# Users configure this via the Secrets panel in the AI Studio UI.
|
||||
GEMINI_API_KEY="MY_GEMINI_API_KEY"
|
||||
|
||||
# APP_URL: The URL where this applet is hosted.
|
||||
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
|
||||
# Used for self-referential links, OAuth callbacks, and API endpoints.
|
||||
APP_URL="MY_APP_URL"
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
coverage/
|
||||
.DS_Store
|
||||
*.log
|
||||
.env*
|
||||
!.env.example
|
||||
20
README.md
Normal file
20
README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
<div align="center">
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||
</div>
|
||||
|
||||
# Run and deploy your AI Studio app
|
||||
|
||||
This contains everything you need to run your app locally.
|
||||
|
||||
View your app in AI Studio: https://ai.studio/apps/3f26c725-3837-42f2-964d-3838c11594a0
|
||||
|
||||
## Run Locally
|
||||
|
||||
**Prerequisites:** Node.js
|
||||
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||
3. Run the app:
|
||||
`npm run dev`
|
||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>My Google AI Studio App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
6
metadata.json
Normal file
6
metadata.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "机器人智慧服务系统",
|
||||
"description": "提供设备管理、视频监控、状态预警、警报处理及视频下载功能的一站式机器人智能管理平台。",
|
||||
"requestFramePermissions": [],
|
||||
"majorCapabilities": []
|
||||
}
|
||||
5999
package-lock.json
generated
Normal file
5999
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
package.json
Normal file
41
package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "react-example",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port=3000 --host=0.0.0.0",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"clean": "rm -rf dist",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.1",
|
||||
"@google/genai": "^1.29.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"antd": "^5.24.1",
|
||||
"axios": "^1.15.0",
|
||||
"clsx": "^2.1.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.21.2",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.14.1",
|
||||
"recharts": "^3.8.1",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"vite": "^6.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^22.14.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
7
src/App.tsx
Normal file
7
src/App.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { router } from './router';
|
||||
|
||||
export default function App() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
150
src/components/Layout.tsx
Normal file
150
src/components/Layout.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
Layout,
|
||||
Menu,
|
||||
Button,
|
||||
Avatar,
|
||||
Space,
|
||||
ConfigProvider,
|
||||
theme,
|
||||
Breadcrumb,
|
||||
Badge
|
||||
} from 'antd';
|
||||
import {
|
||||
DashboardOutlined,
|
||||
ExperimentOutlined,
|
||||
VideoCameraOutlined,
|
||||
BellOutlined,
|
||||
DownloadOutlined,
|
||||
UserOutlined,
|
||||
LogoutOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
MenuFoldOutlined,
|
||||
GlobalOutlined,
|
||||
TeamOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { MOCK_USER } from '../constants';
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
|
||||
export default function AppLayout() {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [time, setTime] = useState(new Date().toLocaleString());
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// 从 localStorage 获取真实用户信息
|
||||
const userJson = localStorage.getItem('user');
|
||||
const user = userJson ? JSON.parse(userJson) : MOCK_USER;
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setTime(new Date().toLocaleString()), 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('isAuthenticated');
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('token');
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{ key: '/', icon: <DashboardOutlined />, label: '概览控制台', roles: ['admin', 'operator', 'viewer'] },
|
||||
{ key: '/devices', icon: <ExperimentOutlined />, label: '设备管理', roles: ['admin', 'operator'] },
|
||||
{ key: '/video', icon: <VideoCameraOutlined />, label: '在线视频', roles: ['admin', 'operator', 'viewer'] },
|
||||
{ key: '/alerts', icon: <BellOutlined />, label: '警报中心', roles: ['admin', 'operator'] },
|
||||
{ key: '/downloads', icon: <DownloadOutlined />, label: '视频下载', roles: ['admin', 'operator'] },
|
||||
{ key: '/users', icon: <TeamOutlined />, label: '用户管理', roles: ['admin'] },
|
||||
{ key: '/roles', icon: <UserOutlined />, label: '角色权限', roles: ['admin'] },
|
||||
];
|
||||
|
||||
const filteredMenu = menuItems.filter(item => item.roles.includes(user.role));
|
||||
|
||||
const currentPathLabel = menuItems.find(i => i.key === location.pathname)?.label || '概览控制台';
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
algorithm: theme.defaultAlgorithm,
|
||||
token: {
|
||||
colorPrimary: '#1890ff',
|
||||
borderRadius: 2,
|
||||
},
|
||||
components: {
|
||||
Layout: {
|
||||
headerBg: '#fff',
|
||||
siderBg: '#001529',
|
||||
},
|
||||
Menu: {
|
||||
darkItemBg: '#001529',
|
||||
darkItemSelectedBg: '#1890ff',
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Sider trigger={null} collapsible collapsed={collapsed} width={200} theme="dark">
|
||||
<div className="h-16 flex items-center justify-center border-b border-white/10 overflow-hidden">
|
||||
<Space size="small" className="px-2">
|
||||
<GlobalOutlined style={{ fontSize: 24, color: '#1890ff' }} />
|
||||
{!collapsed && <span className="text-white font-bold text-lg whitespace-nowrap">RobotMaster AI</span>}
|
||||
</Space>
|
||||
</div>
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
selectedKeys={[location.pathname]}
|
||||
items={filteredMenu}
|
||||
onClick={({ key }) => navigate(key)}
|
||||
/>
|
||||
{!collapsed && (
|
||||
<div className="absolute bottom-4 left-4 right-4 p-2 bg-white/5 rounded text-[12px] text-gray-400">
|
||||
当前权限: <span className="text-white font-bold">{user.role === 'admin' ? '系统管理员' : user.role === 'operator' ? '操作员' : '观察员'}</span>
|
||||
</div>
|
||||
)}
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Header className="px-6 flex items-center justify-between shadow-sm border-b border-gray-100 h-16 bg-white shrink-0 z-10">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
type="text"
|
||||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
style={{ fontSize: '16px', width: 40, height: 40 }}
|
||||
/>
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{ title: '系统首页' },
|
||||
{ title: currentPathLabel }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-3 pr-6 border-r border-gray-100">
|
||||
<span className="text-gray-400 font-mono text-[13px]">{time}</span>
|
||||
<Badge status="success" />
|
||||
<span className="text-gray-800 font-medium">{user.name}</span>
|
||||
<Avatar src={user.avatar} size="small" icon={<UserOutlined />} />
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<LogoutOutlined />}
|
||||
onClick={handleLogout}
|
||||
className="font-bold text-[12px] uppercase"
|
||||
>
|
||||
退出登录
|
||||
</Button>
|
||||
</div>
|
||||
</Header>
|
||||
<Content className="p-4 overflow-y-auto bg-[#f4f7f9] min-h-0">
|
||||
<Outlet />
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
12
src/components/ProtectedRoute.tsx
Normal file
12
src/components/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Navigate, Outlet } from 'react-router-dom';
|
||||
|
||||
export default function ProtectedRoute() {
|
||||
const isAuthenticated = localStorage.getItem('isAuthenticated') === 'true';
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
}
|
||||
34
src/constants.ts
Normal file
34
src/constants.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Device, Alert, VideoRecord, User } from './types';
|
||||
|
||||
export const MOCK_USER: User = {
|
||||
id: 'user-1',
|
||||
name: 'mabaoyu',
|
||||
role: 'admin',
|
||||
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix',
|
||||
};
|
||||
|
||||
export const DEVICES: Device[] = [
|
||||
{ id: 'R001', name: '巡检机器人 A1', type: '巡检型', status: 'online', battery: 85, cpu: 12, temp: 32.08, lastUpdate: '2024-03-20 10:00:00' },
|
||||
{ id: 'R002', name: '安防辅助 B2', type: '安防型', status: 'alert', battery: 45, cpu: 45, temp: 42.5, lastUpdate: '2024-03-20 09:55:00' },
|
||||
{ id: 'R003', name: '清洁终端 C1', type: '清洁型', status: 'offline', battery: 0, cpu: 0, temp: 20.0, lastUpdate: '2024-03-19 18:00:00' },
|
||||
{ id: 'R004', name: '运输搬运 D5', type: '运输型', status: 'online', battery: 92, cpu: 8, temp: 28.5, lastUpdate: '2024-03-20 10:05:00' },
|
||||
];
|
||||
|
||||
export const ALERTS: Alert[] = [
|
||||
{ id: 'A001', deviceId: 'R002', deviceName: '安防辅助 B2', type: 'critical', message: '芯片温度过高', time: '2024-03-20 10:02:15', handled: false },
|
||||
{ id: 'A002', deviceId: 'R001', deviceName: '巡检机器人 A1', type: 'warning', message: '电池电量低于20%', time: '2024-03-20 09:30:00', handled: true },
|
||||
{ id: 'A003', deviceId: 'R004', deviceName: '运输搬运 D5', type: 'info', message: '任务顺利完成', time: '2024-03-20 08:00:00', handled: true },
|
||||
];
|
||||
|
||||
export const VIDEOS: VideoRecord[] = [
|
||||
{ id: 'V001', deviceId: 'R001', deviceName: '巡检机器人 A1', startTime: '2024-03-20 08:00', endTime: '2024-03-20 09:00', size: '450MB', url: '#' },
|
||||
{ id: 'V002', deviceId: 'R001', deviceName: '巡检机器人 A1', startTime: '2024-03-20 09:00', endTime: '2024-03-20 10:00', size: '520MB', url: '#' },
|
||||
{ id: 'V003', deviceId: 'R002', deviceName: '安防辅助 B2', startTime: '2024-03-20 07:30', endTime: '2024-03-20 08:30', size: '380MB', url: '#' },
|
||||
];
|
||||
|
||||
export const MANAGED_USERS: User[] = [
|
||||
{ id: 'user-1', name: 'James Wilson', role: 'admin', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=James' },
|
||||
{ id: 'user-2', name: 'Sarah Chen', role: 'operator', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Sarah' },
|
||||
{ id: 'user-3', name: 'Mike Ross', role: 'viewer', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Mike' },
|
||||
{ id: 'user-4', name: 'Anna Lee', role: 'operator', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Anna' },
|
||||
];
|
||||
25
src/index.css
Normal file
25
src/index.css
Normal file
@@ -0,0 +1,25 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-bg: #f4f7f9;
|
||||
--color-sidebar-bg: #001529;
|
||||
--color-sidebar-text: #a6adb4;
|
||||
--color-sidebar-active: #ffffff;
|
||||
--color-sidebar-active-bg: #1890ff;
|
||||
--color-surface: #ffffff;
|
||||
--color-border: #e8e8e8;
|
||||
--color-primary: #1890ff;
|
||||
--color-danger: #f5222d;
|
||||
--color-warning: #faad14;
|
||||
--color-success: #52c41a;
|
||||
--color-text-main: #262626;
|
||||
--color-text-sec: #8c8c8c;
|
||||
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-bg text-text-main font-sans;
|
||||
}
|
||||
}
|
||||
72
src/lib/http.ts
Normal file
72
src/lib/http.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import axios, { AxiosInstance, AxiosResponse, AxiosError, InternalAxiosRequestConfig } from 'axios';
|
||||
import { message } from 'antd';
|
||||
|
||||
// 创建 axios 实例
|
||||
const http: AxiosInstance = axios.create({
|
||||
baseURL: '/api', // 根据实际后端 API 路径调整
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
http.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
// 从 localStorage 获取 token
|
||||
const token = localStorage.getItem('token');
|
||||
if (token && config.headers) {
|
||||
// 将 token 添加到请求头
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
http.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
// 处理成功响应
|
||||
const { data } = response;
|
||||
// 如果后端规范定义了 code,可以在这里统一处理
|
||||
// if (data.code !== 200) {
|
||||
// message.error(data.message || '请求失败');
|
||||
// return Promise.reject(new Error(data.message || 'Error'));
|
||||
// }
|
||||
return data;
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
// 处理错误响应
|
||||
if (error.response) {
|
||||
switch (error.response.status) {
|
||||
case 401:
|
||||
// 未授权,清除本地信息并跳转到登录页
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('isAuthenticated');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/login';
|
||||
message.error('会话已过期,请重新登录');
|
||||
break;
|
||||
case 403:
|
||||
message.error('拒绝访问:权限不足');
|
||||
break;
|
||||
case 404:
|
||||
message.error('资源不存在');
|
||||
break;
|
||||
case 500:
|
||||
message.error('服务器内部错误');
|
||||
break;
|
||||
default:
|
||||
message.error(error.message || '网络错误');
|
||||
}
|
||||
} else {
|
||||
message.error('连接服务器失败');
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default http;
|
||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import {StrictMode} from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
150
src/pages/Alerts.tsx
Normal file
150
src/pages/Alerts.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Table,
|
||||
Tag,
|
||||
Input,
|
||||
Button,
|
||||
Row,
|
||||
Col,
|
||||
Card,
|
||||
Typography,
|
||||
Space,
|
||||
Statistic
|
||||
} from 'antd';
|
||||
import {
|
||||
BellOutlined,
|
||||
AlertOutlined,
|
||||
InfoCircleOutlined,
|
||||
SearchOutlined,
|
||||
FilterOutlined,
|
||||
CheckCircleOutlined,
|
||||
SyncOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { ALERTS } from '../constants';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
export default function Alerts() {
|
||||
const columns = [
|
||||
{
|
||||
title: '级别',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
render: (type: string) => {
|
||||
const config = {
|
||||
critical: { color: 'error', text: '紧急', icon: <AlertOutlined /> },
|
||||
warning: { color: 'warning', text: '警告', icon: <BellOutlined /> },
|
||||
info: { color: 'processing', text: '提示', icon: <InfoCircleOutlined /> },
|
||||
}[type] || { color: 'default', text: '其他', icon: <InfoCircleOutlined /> };
|
||||
|
||||
return (
|
||||
<Tag color={config.color} icon={config.icon} bordered={false} style={{ fontWeight: 'bold' }}>
|
||||
{config.text}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '设备来源',
|
||||
key: 'device',
|
||||
render: (record: any) => (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text strong style={{ fontSize: 13 }}>{record.deviceName}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 10, fontFamily: 'monospace' }}>{record.deviceId}</Text>
|
||||
</Space>
|
||||
),
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '内容描述',
|
||||
dataIndex: 'message',
|
||||
key: 'message',
|
||||
render: (text: string) => <Text ellipsis style={{ maxWidth: 300 }}>{text}</Text>,
|
||||
},
|
||||
{
|
||||
title: '发生时间',
|
||||
dataIndex: 'time',
|
||||
key: 'time',
|
||||
render: (time: string) => <Text type="secondary" style={{ fontFamily: 'monospace', fontSize: 11 }}>{time}</Text>,
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
render: (record: any) => (
|
||||
record.handled ?
|
||||
<Text type="success" italic style={{ fontSize: 11 }}><CheckCircleOutlined /> 已归档</Text> :
|
||||
<Text type="warning" strong style={{ fontSize: 11 }}><SyncOutlined spin /> 待处理</Text>
|
||||
),
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
align: 'right' as const,
|
||||
render: () => (
|
||||
<Space>
|
||||
<Button type="primary" size="small" style={{ borderRadius: 2 }}>处理</Button>
|
||||
<Button type="default" size="small" style={{ borderRadius: 2 }}>忽略</Button>
|
||||
</Space>
|
||||
),
|
||||
width: 140,
|
||||
},
|
||||
];
|
||||
|
||||
const summary = [
|
||||
{ label: '严重故障', count: ALERTS.filter(a => a.type === 'critical').length, color: '#f5222d', icon: <AlertOutlined />, bgColor: '#fff1f0', borderColor: '#ffa39e' },
|
||||
{ label: '异常警告', count: ALERTS.filter(a => a.type === 'warning').length, color: '#faad14', icon: <BellOutlined />, bgColor: '#fffbe6', borderColor: '#ffe58f' },
|
||||
{ label: '状态提示', count: ALERTS.filter(a => a.type === 'info').length, color: '#1890ff', icon: <InfoCircleOutlined />, bgColor: '#e6f7ff', borderColor: '#91d5ff' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Title level={5} style={{ margin: 0 }}>警报中心: 实时检索</Title>
|
||||
<Space>
|
||||
<Input
|
||||
prefix={<SearchOutlined style={{ color: 'rgba(0,0,0,0.25)' }} />}
|
||||
placeholder="搜索设备ID或警报内容..."
|
||||
style={{ width: 250, borderRadius: 2 }}
|
||||
/>
|
||||
<Button icon={<FilterOutlined />} style={{ borderRadius: 2 }}>
|
||||
高级过滤
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Row gutter={16}>
|
||||
{summary.map((item) => (
|
||||
<Col span={8} key={item.label}>
|
||||
<Card
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: item.bgColor,
|
||||
borderColor: item.borderColor,
|
||||
borderRadius: 4
|
||||
}}
|
||||
>
|
||||
<Statistic
|
||||
title={<Text strong type="secondary" style={{ fontSize: 10, textTransform: 'uppercase', opacity: 0.7 }}>{item.label}</Text>}
|
||||
value={item.count}
|
||||
valueStyle={{ color: item.color, fontWeight: 900, fontSize: 24 }}
|
||||
prefix={React.cloneElement(item.icon, { style: { opacity: 0.4 } })}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
<Card styles={{ body: { padding: 0 } }} className="overflow-hidden">
|
||||
<Table
|
||||
size="small"
|
||||
columns={columns}
|
||||
dataSource={ALERTS.map(a => ({ ...a, key: a.id }))}
|
||||
pagination={{ pageSize: 12, size: 'small' }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
272
src/pages/Devices.tsx
Normal file
272
src/pages/Devices.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Input,
|
||||
Button,
|
||||
Tag,
|
||||
Progress,
|
||||
Space,
|
||||
Typography,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Descriptions,
|
||||
Tabs,
|
||||
Card
|
||||
} from 'antd';
|
||||
import {
|
||||
SearchOutlined,
|
||||
PlusOutlined,
|
||||
SettingOutlined,
|
||||
PoweroffOutlined,
|
||||
ClusterOutlined,
|
||||
ArrowRightOutlined,
|
||||
CloseOutlined,
|
||||
DashboardOutlined,
|
||||
HeatMapOutlined,
|
||||
InfoCircleOutlined,
|
||||
LineChartOutlined,
|
||||
ThunderboltOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { DEVICES } from '../constants';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
export default function Devices() {
|
||||
const [selectedDeviceId, setSelectedDeviceId] = useState<string | null>(null);
|
||||
const selectedDevice = DEVICES.find(d => d.id === selectedDeviceId);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '节点身份',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (text: string, record: any) => (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text strong style={{ fontSize: 13 }}>{text}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 10, fontFamily: 'monospace' }}>{record.id} | {record.type}</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '负载/状态',
|
||||
key: 'status',
|
||||
render: (record: any) => (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text
|
||||
strong
|
||||
italic
|
||||
style={{
|
||||
fontSize: 10,
|
||||
color: record.status === 'online' ? '#52c41a' : record.status === 'alert' ? '#f5222d' : '#bfbfbf'
|
||||
}}
|
||||
>
|
||||
{record.status.toUpperCase()}
|
||||
</Text>
|
||||
<Space size="small">
|
||||
<ClusterOutlined style={{ fontSize: 10, color: 'rgba(0,0,0,0.25)' }} />
|
||||
<Text type="secondary" style={{ fontSize: 11, fontFamily: 'monospace' }}>{record.cpu}%</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '能量效率',
|
||||
dataIndex: 'battery',
|
||||
key: 'battery',
|
||||
render: (percent: number) => (
|
||||
<Space size="middle">
|
||||
<Progress
|
||||
percent={percent}
|
||||
size={[60, 4]}
|
||||
showInfo={false}
|
||||
status={percent < 20 ? 'exception' : 'active'}
|
||||
strokeColor={percent < 20 ? '#f5222d' : percent < 50 ? '#faad14' : '#52c41a'}
|
||||
/>
|
||||
<Text style={{ fontSize: 11, fontFamily: 'monospace' }}>{percent}%</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '管理',
|
||||
key: 'action',
|
||||
align: 'right' as const,
|
||||
render: () => (
|
||||
<Space>
|
||||
<Button type="text" size="small" icon={<SettingOutlined />} />
|
||||
<Button type="text" size="small" danger icon={<PoweroffOutlined />} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Title level={5} style={{ margin: 0 }}>核心资产管理系统</Title>
|
||||
<Space>
|
||||
<Input
|
||||
prefix={<SearchOutlined style={{ color: 'rgba(0,0,0,0.25)' }} />}
|
||||
placeholder="搜索节点、ID或类型..."
|
||||
style={{ width: 250, borderRadius: 2 }}
|
||||
/>
|
||||
<Button type="primary" icon={<PlusOutlined />} style={{ borderRadius: 2 }}>
|
||||
接入新节点
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Row gutter={16} className="flex-1 overflow-hidden" style={{ minHeight: 0 }}>
|
||||
<Col flex="auto" className="h-full overflow-hidden">
|
||||
<Card styles={{ body: { padding: 0 } }} className="h-full overflow-hidden flex flex-col">
|
||||
<Table
|
||||
size="small"
|
||||
columns={columns}
|
||||
dataSource={DEVICES.map(d => ({ ...d, key: d.id }))}
|
||||
pagination={false}
|
||||
sticky
|
||||
scroll={{ y: 'calc(100vh - 250px)' }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => setSelectedDeviceId(record.id),
|
||||
className: selectedDeviceId === record.id ? 'bg-blue-50' : '',
|
||||
style: { cursor: 'pointer' }
|
||||
})}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<AnimatePresence>
|
||||
{selectedDevice && (
|
||||
<Col flex="0 0 340px" className="h-full">
|
||||
<motion.div
|
||||
initial={{ x: 340, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 340, opacity: 0 }}
|
||||
className="h-full"
|
||||
>
|
||||
<Card
|
||||
className="h-full overflow-hidden"
|
||||
styles={{ body: { padding: 0, height: '100%', display: 'flex', flexDirection: 'column' } }}
|
||||
>
|
||||
<div className="p-4 border-b border-gray-100 flex justify-between items-start bg-gray-50">
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: 10, letterSpacing: 2, textTransform: 'uppercase', display: 'block' }}>
|
||||
Asset Intelligence / v4.0
|
||||
</Text>
|
||||
<Title level={4} style={{ margin: '4px 0' }}>{selectedDevice.name}</Title>
|
||||
<Space>
|
||||
<Tag color="blue" bordered={false} style={{ fontSize: 10, fontWeight: 'bold' }}>{selectedDevice.type}</Tag>
|
||||
<Text type="secondary" style={{ fontSize: 11, fontFamily: 'monospace' }}>ID: {selectedDevice.id}</Text>
|
||||
</Space>
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CloseOutlined />}
|
||||
onClick={() => setSelectedDeviceId(null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<Tabs
|
||||
defaultActiveKey="1"
|
||||
className="px-4"
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: '基本信息',
|
||||
children: (
|
||||
<div className="space-y-6 pt-4 pb-4">
|
||||
<Descriptions column={1} size="small" bordered>
|
||||
<Descriptions.Item label="设备名称">{selectedDevice.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="设备 ID">{selectedDevice.id}</Descriptions.Item>
|
||||
<Descriptions.Item label="设备类型">{selectedDevice.type}</Descriptions.Item>
|
||||
<Descriptions.Item label="最后心跳">{selectedDevice.lastUpdate}</Descriptions.Item>
|
||||
<Descriptions.Item label="固件版本">FW_2.5.4_REL</Descriptions.Item>
|
||||
<Descriptions.Item label="接入协议">MQTT_SSL</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<div className="p-3 bg-blue-50 border border-blue-100 rounded">
|
||||
<Space size="small" align="start">
|
||||
<InfoCircleOutlined style={{ color: '#1890ff', marginTop: 3 }} />
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
该设备目前运行正常,已通过节点身份验证,正在进行全量数据透传。
|
||||
</Text>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: '运行状态',
|
||||
children: (
|
||||
<div className="space-y-6 pt-4 pb-4">
|
||||
<Row gutter={[12, 12]}>
|
||||
<Col span={12}>
|
||||
<Card size="small" bordered styles={{ body: { padding: 12 } }} style={{ backgroundColor: '#fafafa' }}>
|
||||
<Statistic title={<Space><ClusterOutlined />负载率</Space>} value={selectedDevice.cpu} suffix="%" valueStyle={{ fontSize: 16, fontFamily: 'monospace' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card size="small" bordered styles={{ body: { padding: 12 } }} style={{ backgroundColor: '#fafafa' }}>
|
||||
<Statistic title={<Space><HeatMapOutlined />核心温度</Space>} value={selectedDevice.temp} suffix="°C" valueStyle={{ fontSize: 16, fontFamily: 'monospace', color: '#f5222d' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card size="small" bordered styles={{ body: { padding: 12 } }} style={{ backgroundColor: '#fafafa' }}>
|
||||
<Statistic title={<Space><ThunderboltOutlined />剩余能量</Space>} value={selectedDevice.battery} suffix="%" valueStyle={{ fontSize: 16, fontFamily: 'monospace', color: '#52c41a' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card size="small" bordered styles={{ body: { padding: 12 } }} style={{ backgroundColor: '#fafafa' }}>
|
||||
<Statistic title={<Space><DashboardOutlined />系统健康</Space>} value={98.2} suffix="%" valueStyle={{ fontSize: 16, fontFamily: 'monospace' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div className="pt-2 border-t border-gray-100">
|
||||
<Text strong type="secondary" style={{ fontSize: 11, textTransform: 'uppercase', display: 'block', marginBottom: 12 }}>
|
||||
实时动态 (SIGNAL)
|
||||
</Text>
|
||||
<Space direction="vertical" className="w-full" size="small">
|
||||
<div className="flex justify-between items-center bg-gray-50 p-2 rounded">
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>上行链路速率</Text>
|
||||
<Text style={{ fontFamily: 'monospace', fontSize: 11 }}>1.2 Mbps</Text>
|
||||
</div>
|
||||
<div className="flex justify-between items-center bg-gray-50 p-2 rounded">
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>下行链路速率</Text>
|
||||
<Text style={{ fontFamily: 'monospace', fontSize: 11 }}>0.4 Mbps</Text>
|
||||
</div>
|
||||
<div className="flex justify-between items-center bg-gray-50 p-2 rounded">
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>当前信号强度</Text>
|
||||
<Tag color="success" style={{ margin: 0, fontSize: 10 }}>EXCELLENT</Tag>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-gray-100 bg-gray-50">
|
||||
<Space direction="vertical" className="w-full" size="small">
|
||||
<Button type="primary" block icon={<ArrowRightOutlined />} style={{ borderRadius: 2 }}>
|
||||
发起远程诊断
|
||||
</Button>
|
||||
<Button block icon={<LineChartOutlined />} style={{ borderRadius: 2 }}>
|
||||
同步设备策略
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Col>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
120
src/pages/Downloads.tsx
Normal file
120
src/pages/Downloads.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
DatePicker,
|
||||
Space,
|
||||
Typography,
|
||||
Card,
|
||||
Segmented,
|
||||
Row,
|
||||
Col,
|
||||
} from 'antd';
|
||||
import {
|
||||
DownloadOutlined,
|
||||
VideoCameraOutlined,
|
||||
CalendarOutlined,
|
||||
SearchOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { VIDEOS } from '../constants';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
export default function Downloads() {
|
||||
const columns = [
|
||||
{
|
||||
title: '文件详情',
|
||||
key: 'file',
|
||||
render: (record: any) => (
|
||||
<Space size="middle">
|
||||
<div className="w-8 h-8 bg-blue-50 text-blue-500 rounded flex items-center justify-center border border-blue-100">
|
||||
<VideoCameraOutlined style={{ fontSize: 16 }} />
|
||||
</div>
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text strong style={{ fontSize: 13 }}>ROB_{record.id}_RAW.mp4</Text>
|
||||
<Text type="secondary" style={{ fontSize: 10, fontFamily: 'monospace', fontStyle: 'italic' }}>Cloud-Storage-01</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '源设备',
|
||||
dataIndex: 'deviceName',
|
||||
key: 'deviceName',
|
||||
render: (text: string) => <Text style={{ fontSize: 13 }}>{text}</Text>,
|
||||
},
|
||||
{
|
||||
title: '起止时间',
|
||||
key: 'duration',
|
||||
render: (record: any) => (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text type="secondary" style={{ fontSize: 11, fontFamily: 'monospace' }}>{record.startTime.split(' ')[1]}</Text>
|
||||
<div className="h-[1px] w-4 bg-gray-200" />
|
||||
<Text type="secondary" style={{ fontSize: 11, fontFamily: 'monospace' }}>{record.endTime.split(' ')[1]}</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '容量',
|
||||
dataIndex: 'size',
|
||||
key: 'size',
|
||||
render: (text: string) => <Text type="secondary" style={{ fontSize: 12 }}>{text}</Text>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
align: 'right' as const,
|
||||
render: () => (
|
||||
<Button
|
||||
type="link"
|
||||
icon={<DownloadOutlined />}
|
||||
style={{ fontSize: 11, fontWeight: 'bold' }}
|
||||
>
|
||||
下载
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Title level={5} style={{ margin: 0 }}>视频回放中心</Title>
|
||||
<Segmented
|
||||
options={['全部录像', '今日录像']}
|
||||
defaultValue="全部录像"
|
||||
style={{ borderRadius: 2 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card size="small" style={{ backgroundColor: '#fafafa', borderRadius: 2 }}>
|
||||
<Row gutter={16} align="middle">
|
||||
<Col>
|
||||
<Space>
|
||||
<CalendarOutlined style={{ color: 'rgba(0,0,0,0.45)' }} />
|
||||
<Text strong type="secondary" style={{ fontSize: 11, textTransform: 'uppercase' }}>时间跨度</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col>
|
||||
<RangePicker size="small" style={{ borderRadius: 2 }} />
|
||||
</Col>
|
||||
<Col flex="auto" className="text-right">
|
||||
<Button type="primary" size="small" icon={<SearchOutlined />} style={{ borderRadius: 2 }}>
|
||||
联机检索
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Card styles={{ body: { padding: 0 } }} className="overflow-hidden">
|
||||
<Table
|
||||
size="small"
|
||||
columns={columns}
|
||||
dataSource={VIDEOS.map(v => ({ ...v, key: v.id }))}
|
||||
pagination={{ pageSize: 12, size: 'small' }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
218
src/pages/Home.tsx
Normal file
218
src/pages/Home.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Table,
|
||||
Tag,
|
||||
Progress,
|
||||
List,
|
||||
Button,
|
||||
Typography,
|
||||
Space,
|
||||
} from 'antd';
|
||||
import {
|
||||
ThunderboltOutlined,
|
||||
DashboardOutlined,
|
||||
DotChartOutlined,
|
||||
RocketOutlined,
|
||||
BellOutlined,
|
||||
CloudUploadOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { DEVICES, ALERTS } from '../constants';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
const AntdCard = Card as any;
|
||||
|
||||
export default function Home() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const stats = [
|
||||
{ title: '芯片温度', value: 32.08, icon: <DashboardOutlined />, color: '#ff4d4f', suffix: '℃' },
|
||||
{ title: '设备电流', value: 0, icon: <ThunderboltOutlined />, color: '#1677ff', suffix: 'A' },
|
||||
{ title: '设备电压', value: 0, icon: <DotChartOutlined />, color: '#fa8c16', suffix: 'V' },
|
||||
{ title: '设备数量', value: DEVICES.length, icon: <RocketOutlined />, color: '#52c41a', suffix: '' },
|
||||
{ title: '告警数量', value: ALERTS.filter(a => !a.handled).length, icon: <BellOutlined />, color: '#ff4d4f', suffix: '' },
|
||||
{ title: '上报事件', value: 1, icon: <CloudUploadOutlined />, color: '#52c41a', suffix: '' },
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Node / 标识',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (text: string, record: any) => (
|
||||
<Space direction="vertical" size={2}>
|
||||
<Text strong style={{ fontSize: 13 }}>{text}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 11, fontFamily: 'monospace' }}>{record.id}</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Status / 状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 110,
|
||||
render: (status: string) => (
|
||||
<Tag
|
||||
color={status === 'online' ? 'success' : status === 'alert' ? 'error' : 'default'}
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
padding: '2px 8px'
|
||||
}}
|
||||
>
|
||||
{status.toUpperCase()}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Batt / 电量',
|
||||
dataIndex: 'battery',
|
||||
key: 'battery',
|
||||
width: 140,
|
||||
render: (percent: number) => (
|
||||
<Progress
|
||||
percent={percent}
|
||||
size="small"
|
||||
status={percent < 20 ? 'exception' : 'active'}
|
||||
strokeColor={percent < 20 ? '#ff4d4f' : '#52c41a'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Load / 负载',
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 100,
|
||||
render: (cpu: number) => (
|
||||
<Text style={{ fontFamily: 'monospace', fontSize: 12 }}>{cpu}%</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Ping / 延迟',
|
||||
key: 'ping',
|
||||
width: 100,
|
||||
align: 'right',
|
||||
render: () => <Text style={{ fontFamily: 'monospace', fontSize: 12 }}>12ms</Text>,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '16px' }}>
|
||||
{/* 统计卡片 */}
|
||||
<Row gutter={[16, 16]}>
|
||||
{stats.map((stat, idx) => (
|
||||
<Col xs={12} sm={12} md={8} lg={4} xl={4} key={idx}>
|
||||
<AntdCard
|
||||
size="small"
|
||||
bordered={false}
|
||||
hoverable
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
boxShadow: '0 2px 12px rgba(0,0,0,0.06)',
|
||||
transition: 'all 0.3s',
|
||||
}}
|
||||
bodyStyle={{ padding: '16px 14px' }}
|
||||
>
|
||||
<Statistic
|
||||
title={<span style={{ fontSize: 12, color: '#666', fontWeight: 500 }}>{stat.title}</span>}
|
||||
value={stat.value}
|
||||
precision={idx === 0 ? 2 : 0}
|
||||
valueStyle={{
|
||||
color: '#1f2329',
|
||||
fontWeight: 600,
|
||||
fontSize: 22,
|
||||
marginTop: 4,
|
||||
}}
|
||||
prefix={React.cloneElement(stat.icon as React.ReactElement, {
|
||||
style: { color: stat.color, fontSize: 16, marginRight: 6 },
|
||||
})}
|
||||
suffix={<span style={{ fontSize: 13, color: '#888' }}>{stat.suffix}</span>}
|
||||
/>
|
||||
</AntdCard>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginTop: 4 }}>
|
||||
{/* 设备表格 */}
|
||||
<Col xs={24} lg={16}>
|
||||
<Card
|
||||
title={<span style={{ fontSize: 14, fontWeight: 600 }}>核心节点实时清单</span>}
|
||||
bordered={false}
|
||||
style={{ borderRadius: 10, boxShadow: '0 2px 12px rgba(0,0,0,0.06)' }}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<Table
|
||||
size="middle"
|
||||
columns={columns}
|
||||
dataSource={DEVICES.map(d => ({ ...d, key: d.id }))}
|
||||
pagination={false}
|
||||
onRow={(record) => ({
|
||||
onClick: () => navigate('/devices'),
|
||||
style: { cursor: 'pointer' },
|
||||
})}
|
||||
rowHoverable
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 告警列表 */}
|
||||
<Col xs={24} lg={8}>
|
||||
<Card
|
||||
title={<span style={{ fontSize: 14, fontWeight: 600 }}>当日重点警报</span>}
|
||||
extra={
|
||||
<Button type="text" danger size="small" style={{ fontWeight: 500 }}>
|
||||
一键清除
|
||||
</Button>
|
||||
}
|
||||
bordered={false}
|
||||
style={{ borderRadius: 10, boxShadow: '0 2px 12px rgba(0,0,0,0.06)' }}
|
||||
bodyStyle={{ padding: 0, height: 460, overflowY: 'auto' }}
|
||||
>
|
||||
<List
|
||||
dataSource={ALERTS.slice(0, 10)}
|
||||
renderItem={(alert) => (
|
||||
<List.Item
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
borderBottom: '1px solid #f5f5f5',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
hoverable
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||
<Tag
|
||||
color={alert.type === 'critical' ? 'error' : alert.type === 'warning' ? 'warning' : 'processing'}
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
padding: '0 6px',
|
||||
}}
|
||||
>
|
||||
{alert.type}
|
||||
</Tag>
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
{alert.time.split(' ')[1]}
|
||||
</Text>
|
||||
</div>
|
||||
<Text style={{ fontSize: 13, fontWeight: 500, lineHeight: 1.4 }}>
|
||||
{alert.message}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 11, marginTop: 4, display: 'block' }}>
|
||||
设备:{alert.deviceName}
|
||||
</Text>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
150
src/pages/Login.tsx
Normal file
150
src/pages/Login.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
Button,
|
||||
Checkbox,
|
||||
Typography,
|
||||
Space,
|
||||
Card,
|
||||
message
|
||||
} from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
LockOutlined,
|
||||
RocketOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
ArrowRightOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { motion } from 'motion/react';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
export default function Login() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFinish = (values: any) => {
|
||||
// 模拟登录逻辑
|
||||
console.log('Login attempt:', values);
|
||||
|
||||
// 根据用户名模拟不同的角色用于演示
|
||||
let role = 'viewer';
|
||||
let name = '观察员';
|
||||
|
||||
if (values.username === 'admin') {
|
||||
role = 'admin';
|
||||
name = '系统管理员';
|
||||
} else if (values.username === 'operator') {
|
||||
role = 'operator';
|
||||
name = '操作员';
|
||||
}
|
||||
|
||||
// 存储用户信息
|
||||
const userInfo = {
|
||||
username: values.username,
|
||||
name: name,
|
||||
role: role,
|
||||
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${values.username}`
|
||||
};
|
||||
|
||||
localStorage.setItem('isAuthenticated', 'true');
|
||||
localStorage.setItem('token', 'mock_token_' + Date.now()); // 模拟 Token
|
||||
localStorage.setItem('user', JSON.stringify(userInfo));
|
||||
|
||||
message.success(`欢迎回来, ${name}`);
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#001529] flex items-center justify-center p-4 relative overflow-hidden">
|
||||
{/* Background Decor */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none opacity-20">
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_50%_50%,#1890ff_0%,transparent_50%)] opacity-20" />
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
style={{ width: '100%', maxWidth: 400 }}
|
||||
>
|
||||
<Card
|
||||
bordered={false}
|
||||
className="shadow-2xl overflow-hidden"
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="bg-[#001529] p-8 text-center border-b border-white/10">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 rounded bg-blue-500/10 text-[#1890ff] mb-4 border border-blue-500/20">
|
||||
<RocketOutlined style={{ fontSize: 24 }} />
|
||||
</div>
|
||||
<Title level={4} style={{ color: '#fff', margin: 0, letterSpacing: 1 }}>RobotMaster AI</Title>
|
||||
<Text style={{ color: 'rgba(255,255,255,0.45)', fontSize: 10, marginTop: 4, display: 'block', letterSpacing: 2 }}>机器人智慧服务系统</Text>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="p-8">
|
||||
<Form
|
||||
name="login"
|
||||
layout="vertical"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
size="large"
|
||||
>
|
||||
<Form.Item
|
||||
label={<Text strong type="secondary" style={{ fontSize: 11, textTransform: 'uppercase' }}><UserOutlined style={{ color: '#1890ff', marginRight: 8 }} />账号名称</Text>}
|
||||
name="username"
|
||||
rules={[{ required: true, message: '请输入管理员账号' }]}
|
||||
>
|
||||
<Input placeholder="请输入管理员账号" style={{ borderRadius: 2 }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={<Text strong type="secondary" style={{ fontSize: 11, textTransform: 'uppercase' }}><LockOutlined style={{ color: '#1890ff', marginRight: 8 }} />访问令牌 / 密码</Text>}
|
||||
name="password"
|
||||
rules={[{ required: true, message: '请输入密码' }]}
|
||||
>
|
||||
<Input.Password placeholder="••••••••" style={{ borderRadius: 2 }} />
|
||||
</Form.Item>
|
||||
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<Form.Item name="remember" valuePropName="checked" noStyle>
|
||||
<Checkbox><Text type="secondary" style={{ fontSize: 12 }}>记住会话</Text></Checkbox>
|
||||
</Form.Item>
|
||||
<Button type="link" size="small" style={{ fontSize: 12 }}>找回凭据?</Button>
|
||||
</div>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
block
|
||||
icon={<SafetyCertificateOutlined />}
|
||||
style={{ borderRadius: 2, height: 44, fontWeight: 'bold', fontSize: 14 }}
|
||||
>
|
||||
核验并进入系统 <ArrowRightOutlined />
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-8 py-4 bg-gray-50 border-t border-gray-100 flex justify-between items-center">
|
||||
<Text type="secondary" style={{ fontSize: 10, fontFamily: 'monospace' }}>System Version: v4.2.1-stable</Text>
|
||||
<Space size={4}>
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
|
||||
<Text strong type="secondary" style={{ fontSize: 10, textTransform: 'uppercase' }}>Server Online</Text>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* Security Notice */}
|
||||
<div className="absolute bottom-8 text-center w-full px-4">
|
||||
<Text strong style={{ color: 'rgba(255,255,255,0.2)', fontSize: 10, letterSpacing: 4 }}>
|
||||
AUTHORIZED PERSONNEL ONLY | ENCRYPTED LINK | TRACE ENABLED
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
103
src/pages/Roles.tsx
Normal file
103
src/pages/Roles.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Button,
|
||||
Typography,
|
||||
Tag,
|
||||
Space,
|
||||
Divider,
|
||||
List
|
||||
} from 'antd';
|
||||
import {
|
||||
SafetyCertificateOutlined,
|
||||
LockOutlined,
|
||||
CheckCircleOutlined,
|
||||
PlusOutlined,
|
||||
TeamOutlined
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
export default function Roles() {
|
||||
const roles = [
|
||||
{ name: '超级管理员', key: 'admin', desc: '系统全权限,支持资源分配与权限审计', level: 'Level 1' },
|
||||
{ name: '系统操作员', key: 'operator', desc: '设备策略下发,实时指令干预及警报处理', level: 'Level 2' },
|
||||
{ name: '访客观察员', key: 'viewer', desc: '只读权限,支持实时视频预览与报表查阅', level: 'Level 3' },
|
||||
];
|
||||
|
||||
const permissions = [
|
||||
{ name: '设备控制权限', sectors: ['远程重启', '策略部署', '紧急停机'] },
|
||||
{ name: '视频管理权限', sectors: ['实时监控', '云台控制', '录像下载'] },
|
||||
{ name: '核心准入权限', sectors: ['审计日志', '成本报表', '用户配置'] },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Title level={5} style={{ margin: 0 }}>系统角色权限矩阵</Title>
|
||||
<Button type="primary" icon={<PlusOutlined />} style={{ borderRadius: 2 }}>
|
||||
新增角色组
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Row gutter={16}>
|
||||
{roles.map((role) => (
|
||||
<Col span={8} key={role.key}>
|
||||
<Card
|
||||
hoverable
|
||||
style={{ borderRadius: 2 }}
|
||||
actions={[
|
||||
<Button type="link" size="small" icon={<TeamOutlined />}>管理成员</Button>
|
||||
]}
|
||||
>
|
||||
<Card.Meta
|
||||
avatar={
|
||||
<div className="w-10 h-10 bg-blue-50 text-blue-500 rounded flex items-center justify-center border border-blue-100">
|
||||
<SafetyCertificateOutlined style={{ fontSize: 20 }} />
|
||||
</div>
|
||||
}
|
||||
title={<span style={{ fontSize: 14 }}>{role.name}</span>}
|
||||
description={
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text type="secondary" style={{ fontSize: 10, fontFamily: 'monospace', textTransform: 'uppercase', fontStyle: 'italic' }}>{role.level}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12, lineHeight: '1.5', height: 40, display: 'block' }}>{role.desc}</Text>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
<Card
|
||||
size="small"
|
||||
title={<Space><LockOutlined style={{ color: 'rgba(0,0,0,0.45)' }} /><Text strong style={{ fontSize: 14 }}>功能模块权限分配 (细分原子操作)</Text></Space>}
|
||||
style={{ borderRadius: 2 }}
|
||||
>
|
||||
<Row gutter={24} style={{ padding: '8px 0' }}>
|
||||
{permissions.map((p) => (
|
||||
<Col span={8} key={p.name}>
|
||||
<div className="space-y-3">
|
||||
<Text strong style={{ fontSize: 11, borderLeft: '2px solid #1890ff', paddingLeft: 8, textTransform: 'uppercase', letterSpacing: 1 }}>{p.name}</Text>
|
||||
<List
|
||||
size="small"
|
||||
dataSource={p.sectors}
|
||||
renderItem={(item) => (
|
||||
<List.Item style={{ padding: '8px 4px', border: 'none' }}>
|
||||
<div className="flex items-center justify-between w-full p-2 bg-gray-50/50 rounded-sm hover:bg-gray-50 border border-transparent hover:border-gray-200 transition-all">
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>{item}</Text>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a', fontSize: 12 }} />
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
97
src/pages/Users.tsx
Normal file
97
src/pages/Users.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Table,
|
||||
Tag,
|
||||
Space,
|
||||
Button,
|
||||
Input,
|
||||
Typography,
|
||||
Card,
|
||||
Avatar
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
UserOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { MANAGED_USERS } from '../constants';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
export default function Users() {
|
||||
const columns = [
|
||||
{
|
||||
title: '用户信息',
|
||||
key: 'userinfo',
|
||||
render: (record: any) => (
|
||||
<Space size="middle">
|
||||
<Avatar src={record.avatar} icon={<UserOutlined />} />
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text strong>{record.name}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>{record.id}</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '系统权限',
|
||||
dataIndex: 'role',
|
||||
key: 'role',
|
||||
render: (role: string) => (
|
||||
<Tag color={role === 'admin' ? 'blue' : 'gray'} style={{ borderRadius: 2, fontWeight: 'bold', textTransform: 'uppercase' }}>
|
||||
{role}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '账户状态',
|
||||
key: 'status',
|
||||
render: () => <Tag color="success">ACTIVE</Tag>,
|
||||
},
|
||||
{
|
||||
title: '最近登录',
|
||||
key: 'lastLogin',
|
||||
render: () => <Text type="secondary" style={{ fontSize: 12, fontFamily: 'monospace' }}>2024-03-24 10:22:45</Text>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
align: 'right' as const,
|
||||
render: () => (
|
||||
<Space>
|
||||
<Button icon={<EditOutlined />} size="small" type="link">编辑</Button>
|
||||
<Button icon={<DeleteOutlined />} size="small" type="link" danger>删除</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Title level={5} style={{ margin: 0 }}>用户管理权限控制</Title>
|
||||
<Space>
|
||||
<Input
|
||||
prefix={<SearchOutlined style={{ color: 'rgba(0,0,0,0.25)' }} />}
|
||||
placeholder="搜索用户名或工号..."
|
||||
style={{ width: 250, borderRadius: 2 }}
|
||||
/>
|
||||
<Button type="primary" icon={<PlusOutlined />} style={{ borderRadius: 2 }}>
|
||||
新增人员
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Card styles={{ body: { padding: 0 } }} className="overflow-hidden">
|
||||
<Table
|
||||
size="small"
|
||||
columns={columns}
|
||||
dataSource={MANAGED_USERS.map(u => ({ ...u, key: u.id }))}
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
208
src/pages/Video.tsx
Normal file
208
src/pages/Video.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Card,
|
||||
Button,
|
||||
Tag,
|
||||
Typography,
|
||||
Space,
|
||||
Tooltip,
|
||||
List,
|
||||
Avatar,
|
||||
Select,
|
||||
Divider,
|
||||
} from 'antd';
|
||||
import {
|
||||
CaretUpOutlined,
|
||||
CaretDownOutlined,
|
||||
CaretLeftOutlined,
|
||||
CaretRightOutlined,
|
||||
FullscreenOutlined,
|
||||
ReloadOutlined,
|
||||
MonitorOutlined,
|
||||
VideoCameraOutlined,
|
||||
ThunderboltOutlined,
|
||||
CustomerServiceOutlined,
|
||||
AlertOutlined,
|
||||
EyeOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { DEVICES } from '../constants';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
function cn(...classes: string[]) {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
export default function VideoControl() {
|
||||
const [selectedDevice, setSelectedDevice] = useState(DEVICES[0]);
|
||||
const onlineDevices = DEVICES.filter(d => d.status === 'online' || d.status === 'alert');
|
||||
|
||||
const getVideoConfig = (type: string) => {
|
||||
switch (type) {
|
||||
case '安防型':
|
||||
return {
|
||||
count: 4,
|
||||
labels: ['Front/主前', 'Back/后视', 'Left/左侧', 'Right/右侧'],
|
||||
gridClass: 'grid-cols-2 grid-rows-2'
|
||||
};
|
||||
case '运输型':
|
||||
return {
|
||||
count: 5,
|
||||
labels: ['Front/前驱', 'Back/后置', 'Left/左翼', 'Right/右翼', 'Cargo/货舱'],
|
||||
gridClass: 'grid-cols-4 grid-rows-2'
|
||||
};
|
||||
case '巡检型':
|
||||
default:
|
||||
return {
|
||||
count: 3,
|
||||
labels: ['Main/主探', 'Pan/全景', 'Detail/细节'],
|
||||
gridClass: 'grid-cols-2 grid-rows-2'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const config = getVideoConfig(selectedDevice.type);
|
||||
|
||||
return (
|
||||
<Row gutter={16} className="h-full overflow-hidden" style={{ minHeight: 0 }}>
|
||||
{/* Video Content */}
|
||||
<Col flex="auto" className="h-full flex flex-col gap-4 overflow-hidden">
|
||||
{/* Top Control Bar */}
|
||||
<div className="flex items-center justify-between bg-white p-3 rounded shadow-sm border border-gray-100">
|
||||
<Space size="large">
|
||||
<div className="flex flex-col">
|
||||
<Text type="secondary" style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: 1 }}>当前执勤设备</Text>
|
||||
<Select
|
||||
value={selectedDevice.id}
|
||||
onChange={(id) => setSelectedDevice(onlineDevices.find(d => d.id === id)!)}
|
||||
style={{ width: 220, fontWeight: 'bold' }}
|
||||
bordered={false}
|
||||
options={onlineDevices.map(d => ({ label: d.name, value: d.id }))}
|
||||
suffixIcon={<VideoCameraOutlined style={{ color: '#1890ff' }} />}
|
||||
/>
|
||||
</div>
|
||||
<Divider type="vertical" style={{ height: 32 }} />
|
||||
<div className="flex flex-col">
|
||||
<Text type="secondary" style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: 1 }}>视频流路数</Text>
|
||||
<Tag color="processing" style={{ marginTop: 2, fontWeight: 'bold', fontFamily: 'monospace' }}>
|
||||
{config.count} CHANNELS ACTIVE
|
||||
</Tag>
|
||||
</div>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button size="small" icon={<ReloadOutlined />}>刷新信号</Button>
|
||||
<Button size="small" type="primary" ghost>全屏墙</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* Multi-Video Grid */}
|
||||
<div className={cn(
|
||||
"bg-black rounded border border-gray-800 p-1 gap-1 flex-1 overflow-hidden grid",
|
||||
config.gridClass
|
||||
)}>
|
||||
{Array.from({ length: config.count }).map((_, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={cn(
|
||||
"relative bg-gray-900 overflow-hidden flex items-center justify-center group border border-white/5",
|
||||
config.count === 3 && idx === 0 && "col-span-2",
|
||||
config.count === 5 && idx === 0 && "col-span-2 row-span-2"
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={`https://picsum.photos/seed/${selectedDevice.id}_cam${idx}/640/360`}
|
||||
alt={`Camera ${idx}`}
|
||||
className="w-full h-full object-cover opacity-60 group-hover:opacity-100 transition-opacity"
|
||||
/>
|
||||
<div className="absolute top-2 left-2 bg-black/60 text-white px-2 py-0.5 text-[10px] rounded-sm backdrop-blur-sm flex items-center gap-2 border border-white/10 uppercase font-mono">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-red-500 animate-pulse shadow-[0_0_8px_rgba(239,68,68,0.8)]" />
|
||||
CAM-0{idx + 1} | {config.labels[idx]}
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-0 bg-blue-500/10 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center pointer-events-none">
|
||||
<Button shape="circle" icon={<FullscreenOutlined />} className="pointer-events-auto shadow-2xl" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Console Controls */}
|
||||
<Card size="small" styles={{ body: { padding: 12 } }}>
|
||||
<Row gutter={16} align="middle">
|
||||
<Col flex="0 0 160px" style={{ borderRight: '1px solid #f0f0f0', paddingRight: 24 }}>
|
||||
<Text strong type="secondary" style={{ fontSize: 10, textTransform: 'uppercase', display: 'block', textAlign: 'center', marginBottom: 8 }}>PTZ 舵机控制</Text>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<Button type="text" size="small" icon={<CaretUpOutlined />} />
|
||||
<Space>
|
||||
<Button type="text" size="small" icon={<CaretLeftOutlined />} />
|
||||
<div className="w-4 h-4 rounded-full bg-blue-500 shadow-[0_0_8px_rgba(24,144,255,0.8)]" />
|
||||
<Button type="text" size="small" icon={<CaretRightOutlined />} />
|
||||
</Space>
|
||||
<Button type="text" size="small" icon={<CaretDownOutlined />} />
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<Col flex="auto">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Text strong type="secondary" style={{ fontSize: 10, textTransform: 'uppercase' }}>系统集成作业指令 ({selectedDevice.type})</Text>
|
||||
<Tag color="success" bordered={false} style={{ fontSize: 10, fontWeight: 'bold' }}>SIGNAL: 100% EXCELLENT</Tag>
|
||||
</div>
|
||||
<Space className="w-full" size="middle">
|
||||
<Button icon={<CustomerServiceOutlined />} style={{ borderRadius: 2 }} block>双轴双向对讲</Button>
|
||||
<Button danger icon={<AlertOutlined />} style={{ borderRadius: 2 }} block>触发远程驱逐</Button>
|
||||
<Button type="primary" icon={<EyeOutlined />} style={{ borderRadius: 2 }} block>AI 视觉追踪锁定</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* Sidebar Channels */}
|
||||
<Col flex="0 0 260px" className="h-full">
|
||||
<Card
|
||||
title={<Space><MonitorOutlined /><Text strong style={{ fontSize: 13 }}>活跃视频流 ({onlineDevices.length})</Text></Space>}
|
||||
extra={<Button type="text" size="small" icon={<ReloadOutlined />} />}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
className="h-full flex flex-col overflow-hidden"
|
||||
>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<List
|
||||
dataSource={onlineDevices}
|
||||
renderItem={(device) => (
|
||||
<List.Item
|
||||
onClick={() => setSelectedDevice(device)}
|
||||
className={cn(
|
||||
"px-4 py-3 cursor-pointer transition-colors border-b last:border-0",
|
||||
selectedDevice.id === device.id ? "bg-blue-50 border-l-4 border-l-blue-500" : "hover:bg-gray-50 border-l-4 border-l-transparent"
|
||||
)}
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
<div className={cn("w-12 h-12 rounded overflow-hidden border", selectedDevice.id === device.id ? "border-blue-300" : "border-gray-200")}>
|
||||
<img src={`https://picsum.photos/seed/${device.id}/80/80`} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
}
|
||||
title={
|
||||
<Text strong style={{ fontSize: 13, color: selectedDevice.id === device.id ? '#1890ff' : 'inherit' }}>
|
||||
{device.name}
|
||||
</Text>
|
||||
}
|
||||
description={
|
||||
<Space size={4} style={{ fontSize: 10 }}>
|
||||
<Text type="secondary" style={{ fontFamily: 'monospace' }}>{device.id}</Text>
|
||||
<div className={cn("w-2 h-2 rounded-full", device.status === 'alert' ? "bg-red-500" : "bg-green-500")} />
|
||||
<Text type="secondary" italic>{device.type}</Text>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
62
src/router/index.tsx
Normal file
62
src/router/index.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
import Layout from '../components/Layout';
|
||||
import ProtectedRoute from '../components/ProtectedRoute';
|
||||
import Login from '../pages/Login';
|
||||
import Home from '../pages/Home';
|
||||
import Devices from '../pages/Devices';
|
||||
import VideoControl from '../pages/Video';
|
||||
import Alerts from '../pages/Alerts';
|
||||
import Downloads from '../pages/Downloads';
|
||||
import Roles from '../pages/Roles';
|
||||
import Users from '../pages/Users';
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: '/login',
|
||||
element: <Login />,
|
||||
},
|
||||
{
|
||||
element: <ProtectedRoute />,
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
element: <Layout />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Home />,
|
||||
},
|
||||
{
|
||||
path: 'devices',
|
||||
element: <Devices />,
|
||||
},
|
||||
{
|
||||
path: 'video',
|
||||
element: <VideoControl />,
|
||||
},
|
||||
{
|
||||
path: 'alerts',
|
||||
element: <Alerts />,
|
||||
},
|
||||
{
|
||||
path: 'downloads',
|
||||
element: <Downloads />,
|
||||
},
|
||||
{
|
||||
path: 'roles',
|
||||
element: <Roles />,
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
element: <Users />,
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <div className="p-10 text-center font-bold text-gray-400">模块开发中...</div>,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
39
src/types.ts
Normal file
39
src/types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export type Role = 'admin' | 'operator' | 'viewer';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
role: Role;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
export interface Device {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
status: 'online' | 'offline' | 'alert';
|
||||
battery: number;
|
||||
cpu: number;
|
||||
temp: number;
|
||||
lastUpdate: string;
|
||||
}
|
||||
|
||||
export interface Alert {
|
||||
id: string;
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
type: 'critical' | 'warning' | 'info';
|
||||
message: string;
|
||||
time: string;
|
||||
handled: boolean;
|
||||
}
|
||||
|
||||
export interface VideoRecord {
|
||||
id: string;
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
size: string;
|
||||
url: string;
|
||||
}
|
||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
24
vite.config.ts
Normal file
24
vite.config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import {defineConfig, loadEnv} from 'vite';
|
||||
|
||||
export default defineConfig(({mode}) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
return {
|
||||
plugins: [react(), tailwindcss()],
|
||||
define: {
|
||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
||||
// Do not modifyâfile watching is disabled to prevent flickering during agent edits.
|
||||
hmr: process.env.DISABLE_HMR !== 'true',
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user