Files
zpark-ems/frontend/src/pages/Login/index.tsx
Du Wenbo 93af4bc16b feat: solar KPIs, version display, feature flags (v1.4.0)
New dashboard KPI cards:
- Performance Ratio (PR) with color thresholds
- Equivalent Utilization Hours
- Daily Revenue (¥)
- Self-Consumption Rate

Version display for field engineers:
- Login page footer: "v1.4.0 | Core: v1.4.0"
- Sidebar footer: version when expanded
- System Settings: full version breakdown

Backend (core sync):
- GET /api/v1/version (no auth) — reads VERSIONS.json
- GET /api/v1/kpi/solar — PR, revenue, equiv hours calculations
- Dashboard energy_today fallback from raw energy_data
- PV device filter includes sungrow_inverter type

Feature flags:
- Sidebar hides disabled features (charging, bigscreen_3d)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 22:37:09 +08:00

99 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react';
import { Form, Input, Button, Card, message, Typography } from 'antd';
import { UserOutlined, LockOutlined, ThunderboltOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { login, getVersion } from '../../services/api';
import { setToken, setUser } from '../../utils/auth';
const { Title, Text } = Typography;
export default function LoginPage() {
const [loading, setLoading] = useState(false);
const [guestLoading, setGuestLoading] = useState(false);
const [versionInfo, setVersionInfo] = useState<any>(null);
const navigate = useNavigate();
useEffect(() => {
getVersion().then(setVersionInfo).catch(() => {});
}, []);
const doLogin = async (username: string, password: string) => {
const res: any = await login(username, password);
setToken(res.access_token);
setUser(res.user);
return res;
};
const onFinish = async (values: { username: string; password: string }) => {
setLoading(true);
try {
await doLogin(values.username, values.password);
message.success('登录成功');
navigate('/');
} catch {
message.error('用户名或密码错误');
} finally {
setLoading(false);
}
};
const onGuestLogin = async () => {
setGuestLoading(true);
try {
await doLogin('visitor', 'visitor123');
message.success('访客登录成功');
navigate('/');
} catch {
message.error('访客登录失败,请联系管理员');
} finally {
setGuestLoading(false);
}
};
return (
<div style={{
minHeight: '100vh', display: 'flex', justifyContent: 'center', alignItems: 'center',
background: 'linear-gradient(135deg, #0a1628 0%, #1a3a5c 50%, #0d2137 100%)',
}}>
<Card style={{ width: 400, borderRadius: 12, boxShadow: '0 8px 32px rgba(0,0,0,0.3)' }}>
<div style={{ textAlign: 'center', marginBottom: 32 }}>
<ThunderboltOutlined style={{ fontSize: 48, color: '#52c41a' }} />
<Title level={3} style={{ marginTop: 12, marginBottom: 4 }}>
</Title>
<Text type="secondary"> · </Text>
</div>
<Form onFinish={onFinish} size="large">
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}>
<Input prefix={<UserOutlined />} placeholder="用户名" />
</Form.Item>
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}>
<Input.Password prefix={<LockOutlined />} placeholder="密码" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={loading} block>
</Button>
</Form.Item>
<Form.Item style={{ marginBottom: 8 }}>
<Button block loading={guestLoading} onClick={onGuestLogin}
style={{ borderColor: '#52c41a', color: '#52c41a' }}>
访
</Button>
</Form.Item>
<div style={{ textAlign: 'center' }}>
<Text type="secondary" style={{ fontSize: 12 }}>
访
</Text>
</div>
</Form>
{versionInfo && (
<div style={{ textAlign: 'center', marginTop: 16, opacity: 0.4, fontSize: 11 }}>
v{versionInfo.project_version} | Core: v{versionInfo.core_version || '—'}
</div>
)}
</Card>
</div>
);
}