Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f6fa3e310 | ||
|
|
99add8050b |
80
CLAUDE.md
Normal file
80
CLAUDE.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# EMS Frontend Template Development Guidelines
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
EMS Frontend Template is the shared base UI framework for all EMS customer projects. Customer repos (tianpu-ems, zpark-ems, etc.) fork or copy from this template and customize for their specific deployment.
|
||||||
|
|
||||||
|
## Golden Rules
|
||||||
|
- This is a TEMPLATE repo -- changes here must be propagated to customer repos manually
|
||||||
|
- Use i18n for ALL user-visible strings (`src/i18n/zh.json` + `en.json`), never hardcode Chinese or English text
|
||||||
|
- Theme customization goes through ThemeContext (`src/contexts/ThemeContext.tsx`)
|
||||||
|
- All API calls go through `src/services/api.ts` -- never use raw axios/fetch in page components
|
||||||
|
- Feature flags control page visibility via `/api/v1/branding` endpoint
|
||||||
|
- Follow Conventional Commits: `<type>(<scope>): <description>`
|
||||||
|
- Types: feat, fix, refactor, docs, test, chore, style, perf
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- React 19 + TypeScript
|
||||||
|
- Ant Design 5 + ProComponents
|
||||||
|
- ECharts 6 (charts and dashboards)
|
||||||
|
- Three.js + React Three Fiber (optional 3D visualization)
|
||||||
|
- i18next (zh + en localization)
|
||||||
|
- Vite 8 (build tool)
|
||||||
|
- Axios (HTTP client)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
pages/ # Page components (one folder per feature)
|
||||||
|
layouts/ # Layout components (MainLayout with sidebar)
|
||||||
|
contexts/ # React contexts (ThemeContext for branding)
|
||||||
|
hooks/ # Custom hooks (useRealtimeWebSocket, etc.)
|
||||||
|
services/ # API service layer (api.ts is the single entry point)
|
||||||
|
i18n/ # Internationalization files (zh.json, en.json)
|
||||||
|
utils/ # Utility functions
|
||||||
|
assets/ # Static assets (images, fonts)
|
||||||
|
App.tsx # Root component with route definitions
|
||||||
|
main.tsx # Entry point
|
||||||
|
```
|
||||||
|
|
||||||
|
## Page Structure
|
||||||
|
- `Dashboard/` -- Main overview dashboard
|
||||||
|
- `Monitoring/` -- Real-time device monitoring
|
||||||
|
- `Devices/` + `DeviceDetail/` -- Device management and detail views
|
||||||
|
- `Analysis/` -- Energy analysis (cost, loss, YoY, MoM)
|
||||||
|
- `Alarms/` -- Alarm center
|
||||||
|
- `Carbon/` -- Carbon emissions tracking
|
||||||
|
- `Reports/` -- Report center
|
||||||
|
- `BigScreen/` + `BigScreen3D/` -- 2D and 3D visualization screens
|
||||||
|
- `Charging/` -- EV charging management
|
||||||
|
- `Prediction/` -- Energy prediction
|
||||||
|
- `EnergyStrategy/` -- Energy strategy optimization
|
||||||
|
- `AIOperations/` -- AI-assisted operations
|
||||||
|
- `Maintenance/` -- Maintenance management
|
||||||
|
- `DataQuery/` -- Data query interface
|
||||||
|
- `Management/` -- General management
|
||||||
|
- `Quota/` -- Energy quota
|
||||||
|
- `System/` -- System settings and audit log
|
||||||
|
- `Login/` -- Login page
|
||||||
|
|
||||||
|
## How to Customize for a New Customer
|
||||||
|
1. Copy this template into the new customer repo's `frontend/` directory
|
||||||
|
2. Update `src/contexts/ThemeContext.tsx` -- branding colors, customer name
|
||||||
|
3. Update `src/layouts/MainLayout.tsx` -- logo, sidebar menu items
|
||||||
|
4. Edit `src/App.tsx` -- add/remove routes per customer needs
|
||||||
|
5. Update `vite.config.ts` -- adjust proxy target if backend port differs
|
||||||
|
6. Remove unused dependencies from `package.json` (e.g., Three.js if no 3D)
|
||||||
|
7. Update `VERSIONS.json` with new project name and version
|
||||||
|
|
||||||
|
## Version Management
|
||||||
|
- `VERSIONS.json` and `VERSION` file must stay in sync
|
||||||
|
- `package.json` version should also match
|
||||||
|
- Follow SemVer (MAJOR.MINOR.PATCH)
|
||||||
|
- When releasing: update all three version references, then tag and push
|
||||||
|
- Current: see `VERSION` file
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
```bash
|
||||||
|
npm run dev # Start dev server (port 3000, proxies /api to backend)
|
||||||
|
npm run build # Production build (tsc + vite build)
|
||||||
|
npm run preview # Preview production build
|
||||||
|
```
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> Base React/TypeScript UI template for EMS customer projects
|
> Base React/TypeScript UI template for EMS customer projects
|
||||||
|
|
||||||
**Current Version: 1.2.0** | See `VERSIONS.json`
|
**Current Version: 1.4.0** | See `VERSIONS.json`
|
||||||
|
|
||||||
This is the shared frontend template used to bootstrap new EMS customer frontends. Copy it into a new customer repo and customize.
|
This is the shared frontend template used to bootstrap new EMS customer frontends. Copy it into a new customer repo and customize.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"project": "ems-frontend-template",
|
"project": "ems-frontend-template",
|
||||||
"project_version": "1.3.0",
|
"project_version": "1.4.0",
|
||||||
"core_version": "1.3.0",
|
"core_version": "1.4.0",
|
||||||
"last_updated": "2026-04-06",
|
"last_updated": "2026-04-06",
|
||||||
"notes": "Generic branding, removed all Tianpu-specific hardcoding"
|
"notes": "Version display on login + sidebar for field engineers"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.3.0",
|
"version": "1.4.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -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');
|
||||||
@@ -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={{
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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