feat: customer frontend, Sungrow collector fixes, real data (v1.2.0)

- Add frontend/ at root (no Three.js, no Charging, green #52c41a theme)
- Fix Sungrow collector: add curPage/size params, unit conversion
- Fix station-level dedup to prevent double-counting
- Add shared token cache for API rate limit protection
- Add .githooks/pre-commit, CLAUDE.md, .gitignore
- Update docker-compose.override.yml frontend -> ./frontend
- Pin bcrypt in requirements.txt
- Add BUYOFF_RESULTS_2026-04-05.md (39/43 pass)
- Data accuracy: 0.0% diff vs iSolarCloud

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Du Wenbo
2026-04-05 23:43:24 +08:00
parent ed30ac31e4
commit d3f47d664c
121 changed files with 21784 additions and 23 deletions

View File

@@ -0,0 +1,88 @@
import { useState } 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 } 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 navigate = useNavigate();
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: '#1890ff' }} />
<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: '#1890ff', color: '#1890ff' }}>
访
</Button>
</Form.Item>
<div style={{ textAlign: 'center' }}>
<Text type="secondary" style={{ fontSize: 12 }}>
访
</Text>
</div>
</Form>
</Card>
</div>
);
}