Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99add8050b | ||
|
|
ba190c33ca | ||
|
|
240975b2c0 | ||
|
|
21c0cf8b15 | ||
|
|
6a62272ba4 |
149
README.md
149
README.md
@@ -1,24 +1,87 @@
|
|||||||
# EMS Frontend Template
|
# EMS Frontend Template
|
||||||
|
|
||||||
Base React frontend template for EMS customer projects.
|
> Base React/TypeScript UI template for EMS customer projects
|
||||||
|
|
||||||
## Usage
|
**Current Version: 1.2.0** | See `VERSIONS.json`
|
||||||
|
|
||||||
When creating a new customer project, copy this template:
|
This is the shared frontend template used to bootstrap new EMS customer frontends. Copy it into a new customer repo and customize.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage — New Customer Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Copy template into customer repo
|
||||||
cp -r ems-frontend-template/ <customer-repo>/frontend/
|
cp -r ems-frontend-template/ <customer-repo>/frontend/
|
||||||
cd <customer-repo>/frontend
|
cd <customer-repo>/frontend
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
|
# Start dev server
|
||||||
|
npm run dev
|
||||||
|
# Opens at http://localhost:3000, proxies /api to localhost:8000
|
||||||
```
|
```
|
||||||
|
|
||||||
Then customize:
|
### Customize for your customer:
|
||||||
1. Edit `src/App.tsx` — add/remove routes per customer needs
|
|
||||||
2. Edit `src/layouts/MainLayout.tsx` — customer branding (logo, colors, sidebar)
|
1. `src/contexts/ThemeContext.tsx` -- Branding colors, customer name
|
||||||
3. Edit `package.json` — remove unused deps (e.g., Three.js if no 3D)
|
2. `src/layouts/MainLayout.tsx` -- Logo, sidebar menu items
|
||||||
4. Edit `vite.config.ts` — update proxy target if backend port differs
|
3. `src/App.tsx` -- Add/remove routes per customer needs
|
||||||
|
4. `vite.config.ts` -- Update proxy target if backend port differs
|
||||||
|
5. `package.json` -- Remove unused deps (e.g., Three.js if no 3D)
|
||||||
|
6. Update `VERSIONS.json` -- Set new project name and version
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
ems-frontend-template/
|
||||||
|
+-- src/
|
||||||
|
| +-- pages/ # Page components
|
||||||
|
| | +-- Dashboard/ # Main dashboard
|
||||||
|
| | +-- Monitoring/ # Real-time monitoring
|
||||||
|
| | +-- Devices/ # Device management
|
||||||
|
| | +-- DeviceDetail/ # Device detail view
|
||||||
|
| | +-- Analysis/ # Energy analysis (cost, loss, YoY, MoM)
|
||||||
|
| | +-- Alarms/ # Alarm center
|
||||||
|
| | +-- Carbon/ # Carbon emissions
|
||||||
|
| | +-- Reports/ # Report center
|
||||||
|
| | +-- BigScreen/ # 2D visualization screen
|
||||||
|
| | +-- BigScreen3D/ # 3D visualization screen
|
||||||
|
| | +-- Charging/ # EV charging management
|
||||||
|
| | +-- Prediction/ # Energy prediction
|
||||||
|
| | +-- EnergyStrategy/ # Energy strategy
|
||||||
|
| | +-- AIOperations/ # AI-assisted operations
|
||||||
|
| | +-- Maintenance/ # Maintenance management
|
||||||
|
| | +-- DataQuery/ # Data query
|
||||||
|
| | +-- Management/ # General management
|
||||||
|
| | +-- Quota/ # Energy quota
|
||||||
|
| | +-- System/ # System settings & audit log
|
||||||
|
| | +-- Login/ # Login page
|
||||||
|
| +-- layouts/ # Layout components (MainLayout)
|
||||||
|
| +-- contexts/ # React contexts (ThemeContext)
|
||||||
|
| +-- hooks/ # Custom hooks (useRealtimeWebSocket)
|
||||||
|
| +-- services/ # API service layer
|
||||||
|
| +-- i18n/ # Internationalization (zh + en)
|
||||||
|
| +-- utils/ # Utility functions
|
||||||
|
| +-- assets/ # Static assets
|
||||||
|
| +-- App.tsx # Root component with routes
|
||||||
|
| +-- main.tsx # Entry point
|
||||||
|
+-- public/
|
||||||
|
| +-- devices/ # SVG device icons
|
||||||
|
+-- Dockerfile # Dev container
|
||||||
|
+-- Dockerfile.prod # Production container
|
||||||
|
+-- vite.config.ts # Vite config (proxy, port)
|
||||||
|
+-- package.json
|
||||||
|
+-- VERSIONS.json # Version tracking
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- React 19 + TypeScript
|
- React 19 + TypeScript
|
||||||
- Ant Design 5 + ProComponents
|
- Ant Design 5 + ProComponents
|
||||||
- ECharts 6
|
- ECharts 6
|
||||||
@@ -26,18 +89,62 @@ Then customize:
|
|||||||
- i18next (zh + en)
|
- i18next (zh + en)
|
||||||
- Vite 8
|
- Vite 8
|
||||||
|
|
||||||
## Available Pages
|
---
|
||||||
- Dashboard, Monitoring, Devices, DeviceDetail
|
|
||||||
- Analysis (cost, loss, YoY, MoM, subitem)
|
## Available Commands
|
||||||
- Alarms, Carbon, Reports
|
|
||||||
- BigScreen (2D), BigScreen3D (3D)
|
```bash
|
||||||
- Charging (stations, piles, orders, pricing)
|
npm run dev # Start dev server (port 3000)
|
||||||
- Prediction, EnergyStrategy, AIOperations
|
npm run build # Production build
|
||||||
- Maintenance, DataQuery, Management, Quota
|
npm run preview # Preview production build
|
||||||
- System (settings, audit log)
|
```
|
||||||
- Login
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
docker build -t ems-frontend .
|
||||||
|
docker run -p 3000:3000 ems-frontend
|
||||||
|
|
||||||
|
# Production
|
||||||
|
docker build -f Dockerfile.prod -t ems-frontend-prod .
|
||||||
|
docker run -p 80:80 ems-frontend-prod
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all versions
|
||||||
|
git tag -l
|
||||||
|
|
||||||
|
# Download a specific version
|
||||||
|
git checkout v1.1.0
|
||||||
|
|
||||||
|
# Download as zip
|
||||||
|
curl -o template-v1.1.0.zip \
|
||||||
|
"http://100.69.143.96:3300/tianpu/ems-frontend-template/archive/v1.1.0.zip"
|
||||||
|
|
||||||
|
# Go back to latest
|
||||||
|
git checkout master
|
||||||
|
```
|
||||||
|
|
||||||
|
When releasing: update `VERSIONS.json` + `package.json` version, then tag and push.
|
||||||
|
See [ems-core README](http://100.69.143.96:3300/tianpu/ems-core) for the full version management guide.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- Not all pages are needed for every customer
|
|
||||||
- Remove unused page imports from App.tsx
|
- Not all pages are needed for every customer -- remove unused page imports from `App.tsx`
|
||||||
- Remove corresponding dependencies from package.json
|
- Remove corresponding dependencies from `package.json` (e.g., Three.js if no 3D)
|
||||||
|
- The template is designed to work with ems-core backend at `/api`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright 2026. All rights reserved.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"project": "ems-frontend-template",
|
"project": "ems-frontend-template",
|
||||||
"project_version": "1.1.0",
|
"project_version": "1.4.0",
|
||||||
"core_version": "1.1.0",
|
"core_version": "1.4.0",
|
||||||
"last_updated": "2026-04-05",
|
"last_updated": "2026-04-06",
|
||||||
"notes": "Initial frontend template from ems-core v1.1.0"
|
"notes": "Version display on login + sidebar for field engineers"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>天普智慧能源管理平台</title>
|
<title>智慧能源管理平台</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
"version": "1.4.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ const ThemeContext = createContext<ThemeContextType>({
|
|||||||
|
|
||||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||||
const [darkMode, setDarkMode] = useState(() => {
|
const [darkMode, setDarkMode] = useState(() => {
|
||||||
const saved = localStorage.getItem('tianpu-dark-mode');
|
const saved = localStorage.getItem('ems-dark-mode');
|
||||||
return saved === 'true';
|
return saved === 'true';
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('tianpu-dark-mode', String(darkMode));
|
localStorage.setItem('ems-dark-mode', String(darkMode));
|
||||||
document.documentElement.setAttribute('data-theme', darkMode ? 'dark' : 'light');
|
document.documentElement.setAttribute('data-theme', darkMode ? 'dark' : 'light');
|
||||||
}, [darkMode]);
|
}, [darkMode]);
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ i18n.use(initReactI18next).init({
|
|||||||
zh: { translation: zh },
|
zh: { translation: zh },
|
||||||
en: { translation: en },
|
en: { translation: en },
|
||||||
},
|
},
|
||||||
lng: localStorage.getItem('tianpu-lang') || 'zh',
|
lng: localStorage.getItem('ems-lang') || 'zh',
|
||||||
fallbackLng: 'zh',
|
fallbackLng: 'zh',
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false,
|
escapeValue: false,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"viewAllAlarms": "View all alarms",
|
"viewAllAlarms": "View all alarms",
|
||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
"logout": "Sign Out",
|
"logout": "Sign Out",
|
||||||
"brandName": "Tianpu EMS"
|
"brandName": "EMS Platform"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"viewAllAlarms": "查看全部告警",
|
"viewAllAlarms": "查看全部告警",
|
||||||
"profile": "个人信息",
|
"profile": "个人信息",
|
||||||
"logout": "退出登录",
|
"logout": "退出登录",
|
||||||
"brandName": "天普EMS"
|
"brandName": "EMS Platform"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { getUser, removeToken } from '../utils/auth';
|
import { getUser, removeToken } from '../utils/auth';
|
||||||
import { getAlarmStats, getAlarmEvents } from '../services/api';
|
import { getAlarmStats, getAlarmEvents, getVersion } from '../services/api';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
|
||||||
const { Header, Sider, Content } = Layout;
|
const { Header, Sider, Content } = Layout;
|
||||||
@@ -28,6 +28,7 @@ export default function MainLayout() {
|
|||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const [alarmCount, setAlarmCount] = useState(0);
|
const [alarmCount, setAlarmCount] = useState(0);
|
||||||
const [recentAlarms, setRecentAlarms] = useState<any[]>([]);
|
const [recentAlarms, setRecentAlarms] = useState<any[]>([]);
|
||||||
|
const [versionInfo, setVersionInfo] = useState<any>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const user = getUser();
|
const user = getUser();
|
||||||
@@ -93,6 +94,10 @@ export default function MainLayout() {
|
|||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, [fetchAlarms]);
|
}, [fetchAlarms]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getVersion().then(setVersionInfo).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
removeToken();
|
removeToken();
|
||||||
localStorage.removeItem('user');
|
localStorage.removeItem('user');
|
||||||
@@ -101,7 +106,7 @@ export default function MainLayout() {
|
|||||||
|
|
||||||
const handleLanguageChange = (lang: string) => {
|
const handleLanguageChange = (lang: string) => {
|
||||||
i18n.changeLanguage(lang);
|
i18n.changeLanguage(lang);
|
||||||
localStorage.setItem('tianpu-lang', lang);
|
localStorage.setItem('ems-lang', lang);
|
||||||
};
|
};
|
||||||
|
|
||||||
const userMenu = {
|
const userMenu = {
|
||||||
@@ -136,6 +141,14 @@ export default function MainLayout() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{!collapsed && versionInfo && (
|
||||||
|
<div style={{
|
||||||
|
padding: '8px 16px', fontSize: 11, color: 'rgba(255,255,255,0.3)',
|
||||||
|
borderTop: '1px solid rgba(255,255,255,0.06)', textAlign: 'center',
|
||||||
|
}}>
|
||||||
|
v{versionInfo.project_version}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Sider>
|
</Sider>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Header style={{
|
<Header style={{
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export default function BigScreen() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<span className={styles.headerDate}>{formatDate(clock)}</span>
|
<span className={styles.headerDate}>{formatDate(clock)}</span>
|
||||||
<h1 className={styles.headerTitle}>天普零碳园区智慧能源管理平台</h1>
|
<h1 className={styles.headerTitle}>智慧能源管理平台</h1>
|
||||||
<span className={styles.headerTime}>{formatTime(clock)}</span>
|
<span className={styles.headerTime}>{formatTime(clock)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default function HUDOverlay({ overview, realtimeData }: HUDOverlayProps)
|
|||||||
<div className={styles.hudOverlay}>
|
<div className={styles.hudOverlay}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<span className={styles.headerDate}>{formatDate(now)}</span>
|
<span className={styles.headerDate}>{formatDate(now)}</span>
|
||||||
<span className={styles.headerTitle}>天普零碳园区 3D智慧能源管理平台</span>
|
<span className={styles.headerTitle}>3D智慧能源管理平台</span>
|
||||||
<span className={styles.headerClock}>{formatTime(now)}</span>
|
<span className={styles.headerClock}>{formatTime(now)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export default function BigScreen3D() {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.placeholder}>
|
<div className={styles.placeholder}>
|
||||||
<h2 className={styles.placeholderTitle}>天普零碳园区 3D智慧能源管理平台</h2>
|
<h2 className={styles.placeholderTitle}>3D智慧能源管理平台</h2>
|
||||||
<p style={{ color: '#8899aa' }}>正在加载设备数据...</p>
|
<p style={{ color: '#8899aa' }}>正在加载设备数据...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Form, Input, Button, Card, message, Typography } from 'antd';
|
import { Form, Input, Button, Card, message, Typography } from 'antd';
|
||||||
import { UserOutlined, LockOutlined, ThunderboltOutlined } from '@ant-design/icons';
|
import { UserOutlined, LockOutlined, ThunderboltOutlined } from '@ant-design/icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { login } from '../../services/api';
|
import { login, getVersion } from '../../services/api';
|
||||||
import { setToken, setUser } from '../../utils/auth';
|
import { setToken, setUser } from '../../utils/auth';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
@@ -10,8 +10,13 @@ const { Title, Text } = Typography;
|
|||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [guestLoading, setGuestLoading] = useState(false);
|
const [guestLoading, setGuestLoading] = useState(false);
|
||||||
|
const [versionInfo, setVersionInfo] = useState<any>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getVersion().then(setVersionInfo).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const doLogin = async (username: string, password: string) => {
|
const doLogin = async (username: string, password: string) => {
|
||||||
const res: any = await login(username, password);
|
const res: any = await login(username, password);
|
||||||
setToken(res.access_token);
|
setToken(res.access_token);
|
||||||
@@ -54,9 +59,9 @@ export default function LoginPage() {
|
|||||||
<div style={{ textAlign: 'center', marginBottom: 32 }}>
|
<div style={{ textAlign: 'center', marginBottom: 32 }}>
|
||||||
<ThunderboltOutlined style={{ fontSize: 48, color: '#1890ff' }} />
|
<ThunderboltOutlined style={{ fontSize: 48, color: '#1890ff' }} />
|
||||||
<Title level={3} style={{ marginTop: 12, marginBottom: 4 }}>
|
<Title level={3} style={{ marginTop: 12, marginBottom: 4 }}>
|
||||||
天普智慧能源管理平台
|
智慧能源管理平台
|
||||||
</Title>
|
</Title>
|
||||||
<Text type="secondary">零碳园区 · 智慧运维</Text>
|
<Text type="secondary">智慧能源 · 高效运维</Text>
|
||||||
</div>
|
</div>
|
||||||
<Form onFinish={onFinish} size="large">
|
<Form onFinish={onFinish} size="large">
|
||||||
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}>
|
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}>
|
||||||
@@ -82,6 +87,11 @@ export default function LoginPage() {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
{versionInfo && (
|
||||||
|
<div style={{ textAlign: 'center', marginTop: 16, opacity: 0.4, fontSize: 11 }}>
|
||||||
|
v{versionInfo.project_version} | Core: v{versionInfo.core_version || '—'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -342,6 +342,9 @@ export const getStrategySavingsReport = (params: Record<string, any>) => api.get
|
|||||||
export const getStrategyRecommendations = () => api.get('/strategy/recommendations');
|
export const getStrategyRecommendations = () => api.get('/strategy/recommendations');
|
||||||
export const simulateStrategy = (data: any) => api.post('/strategy/simulate', data);
|
export const simulateStrategy = (data: any) => api.post('/strategy/simulate', data);
|
||||||
|
|
||||||
|
// Version info (no auth required)
|
||||||
|
export const getVersion = () => api.get('/version').then(r => r.data);
|
||||||
|
|
||||||
// Weather (气象数据)
|
// Weather (气象数据)
|
||||||
export const getWeatherCurrent = () => api.get('/weather/current');
|
export const getWeatherCurrent = () => api.get('/weather/current');
|
||||||
export const getWeatherForecast = (params?: Record<string, any>) => api.get('/weather/forecast', { params });
|
export const getWeatherForecast = (params?: Record<string, any>) => api.get('/weather/forecast', { params });
|
||||||
|
|||||||
Reference in New Issue
Block a user