feat: add version display on login page and sidebar (v1.4.0)

- Login page: shows "vX.Y.Z | Core: vA.B.C" at bottom of card
- Sidebar footer: shows version when expanded
- Calls GET /api/v1/version (no auth required)
- For field engineers to instantly identify platform version

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Du Wenbo
2026-04-06 22:36:12 +08:00
parent ba190c33ca
commit 3c909a51af
5 changed files with 33 additions and 7 deletions

View File

@@ -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"
} }

View File

@@ -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",

View File

@@ -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={{

View File

@@ -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>
); );

View File

@@ -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 });