refactor: 优化电量监控系统主题与统计逻辑

- 添加深色模式适配,跟随系统主题自动切换
- 替换硬编码样式为CSS变量实现主题统一管理
- 重构后端统计计算逻辑,优化当日用电估算方式
- 更新页面标题与站点图标,调整登录页样式
- 移除冗余的data目录拷贝配置
This commit is contained in:
EchoZenith
2026-05-23 01:38:49 +08:00
parent e1b4a0ec5d
commit 224233421d
8 changed files with 175 additions and 115 deletions

View File

@@ -3,7 +3,8 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>电费监控系统</title>
<link rel="icon" type="image/svg+xml" href="/lightning.svg" />
<title>智能电量监控</title>
</head>
<body>
<div id="root"></div>

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg width="40" height="40" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19 4H37L26 18H41L17 44L22 25H8L19 4Z" fill="#4a90e2" stroke="#4a90e2" stroke-width="4" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 261 B

View File

@@ -4,6 +4,53 @@ import Dashboard from './pages/Dashboard';
import Login from './pages/Login';
import { checkAuth } from './api';
const cssVars = `
[data-theme="light"], [data-theme="dark"] {
--bg-body: #f5f7fa;
--bg-card: #ffffff;
--bg-chart: #fafbfc;
--bg-item: #f8f9fa;
--bg-item-blue: #e8f4fd;
--bg-item-purple: #f0e8fd;
--bg-item-green: #e8fdf4;
--text-primary: #1a1a1a;
--text-secondary: #666;
--text-tertiary: #999;
--text-placeholder: #999;
--border-light: #e8e8e8;
--shadow-card: 0 2px 8px rgba(0,0,0,0.08);
--color-blue: #4a90e2;
--color-orange: #f39c12;
--color-purple: #9b59b6;
--color-green: #27ae60;
--tab-bg: #f0f2f5;
--tab-bg-active: #333;
--tab-text: #666;
--tab-text-active: white;
}
[data-theme="dark"] {
--bg-body: #141414;
--bg-card: #1f1f1f;
--bg-chart: #2a2a2a;
--bg-item: #2a2a2a;
--bg-item-blue: rgba(74, 144, 226, 0.15);
--bg-item-purple: rgba(155, 89, 182, 0.15);
--bg-item-green: rgba(39, 174, 96, 0.15);
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--text-tertiary: #808080;
--border-light: #333;
--shadow-card: none;
--tab-bg: #2a2a2a;
--tab-bg-active: #4a90e2;
--tab-text: #808080;
--tab-text-active: white;
}
body { margin: 0; background: var(--bg-body); }
`;
export default function App() {
const [authed, setAuthed] = useState(null);
@@ -13,11 +60,17 @@ export default function App() {
if (authed === null) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh', background: 'var(--bg-body)' }}>
<style>{cssVars}</style>
<Spin size="large" />
</div>
);
}
return authed ? <Dashboard onLogout={() => setAuthed(false)} /> : <Login onLogin={() => setAuthed(true)} />;
return (
<>
<style>{cssVars}</style>
{authed ? <Dashboard onLogout={() => setAuthed(false)} /> : <Login onLogin={() => setAuthed(true)} />}
</>
);
}

View File

@@ -1,17 +1,36 @@
import { useState, useEffect } from 'react';
import ReactDOM from 'react-dom/client';
import { ConfigProvider } from 'antd';
import { ConfigProvider, theme } from 'antd';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<ConfigProvider
theme={{
token: {
colorPrimary: '#4a90e2',
borderRadius: 8,
fontSize: 14,
},
}}
>
<App />
</ConfigProvider>
);
function Root() {
const [isDark, setIsDark] = useState(() =>
window.matchMedia('(prefers-color-scheme: dark)').matches
);
useEffect(() => {
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e) => setIsDark(e.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);
return (
<ConfigProvider
theme={{
algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
token: {
colorPrimary: '#4a90e2',
borderRadius: 8,
fontSize: 14,
},
}}
>
<div data-theme={isDark ? 'dark' : 'light'}>
<App />
</div>
</ConfigProvider>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<Root />);

View File

@@ -1,7 +1,6 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { Button, Spin, message } from 'antd';
import { LogoutOutlined, ReloadOutlined } from '@ant-design/icons';
import { BugOutlined } from '@ant-design/icons';
import { LogoutOutlined, ReloadOutlined, BugOutlined } from '@ant-design/icons';
import { Lightning, ChartLine, ChartHistogram, Timer } from '@icon-park/react';
import { Chart, registerables } from 'chart.js';
import { fetchCurrent, fetchHistory, triggerCollect, logout } from '../api';
@@ -278,7 +277,7 @@ export default function Dashboard({ onLogout }) {
if (loading) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh', background: '#f5f7fa' }}>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh', background: 'var(--bg-body)' }}>
<Spin size="large" />
</div>
);
@@ -288,21 +287,21 @@ export default function Dashboard({ onLogout }) {
const stats = historyData?.stats || {};
const statKey = days === 7 ? '7' : days === 15 ? '15' : '30';
const avgDaily = stats[`avgDaily${statKey}`];
const avgPower = stats[`avgPower${statKey}`];
const estimatedDays = stats[`estimatedDays${statKey}`];
const todayUsage = currentData?.todayUsage ?? 0;
const todayCost = currentData?.todayCost ?? 0;
const todayAvgPower = currentData?.todayAvgPower ?? null;
return (
<div className="d-body" style={{ background: '#f5f7fa', minHeight: '100vh', padding: 20 }}>
<div className="d-body" style={{ background: 'var(--bg-body)', minHeight: '100vh', padding: 20 }}>
<style>{`${styles}
body { margin: 0; }
.d-container { border-radius: 12px !important; box-shadow: none !important; }
`}</style>
<div className="d-container" style={{ maxWidth: 1200, margin: '0 auto', background: 'white', padding: 32, borderRadius: 12 }}>
<div className="d-container" style={{ maxWidth: 1200, margin: '0 auto', background: 'var(--bg-card)', padding: 32, borderRadius: 12 }}>
<div className="d-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 32 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, fontSize: 24, fontWeight: 600, color: '#1a1a1a' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, fontSize: 24, fontWeight: 600, color: 'var(--text-primary)' }}>
<Lightning theme="filled" size="28" fill="#4a90e2" style={{ display: 'flex' }} />
智能电量监控
</div>
@@ -313,9 +312,9 @@ body { margin: 0; }
loading={collecting}
size="small"
style={{
border: '1px solid #d9d9d9',
border: '1px solid var(--border-light)',
borderRadius: 20,
color: '#666',
color: 'var(--text-secondary)',
fontSize: 13,
}}
>
@@ -326,9 +325,9 @@ body { margin: 0; }
onClick={handleTestNotify}
size="small"
style={{
border: '1px solid #d9d9d9',
border: '1px solid var(--border-light)',
borderRadius: 20,
color: '#666',
color: 'var(--text-secondary)',
fontSize: 13,
}}
>
@@ -339,9 +338,9 @@ body { margin: 0; }
onClick={handleLogout}
size="small"
style={{
border: '1px solid #d9d9d9',
border: '1px solid var(--border-light)',
borderRadius: 20,
color: '#666',
color: 'var(--text-secondary)',
fontSize: 13,
}}
>
@@ -352,28 +351,28 @@ body { margin: 0; }
<div className="d-stats" style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 24, marginBottom: 32 }}>
<div className="d-stats-card" style={{ padding: 20 }}>
<div style={{ fontSize: 14, color: '#999', marginBottom: 8 }}>剩余电量</div>
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: '#1a1a1a', display: 'flex', alignItems: 'baseline', gap: 6 }}>
<div style={{ fontSize: 14, color: 'var(--text-tertiary)', marginBottom: 8 }}>剩余电量</div>
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: 'var(--text-primary)', display: 'flex', alignItems: 'baseline', gap: 6 }}>
{current?.surplus?.toFixed(1) ?? '--'}
<span style={{ fontSize: 16, fontWeight: 400, color: '#999' }}>kWh</span>
<span style={{ fontSize: 16, fontWeight: 400, color: 'var(--text-tertiary)' }}>kWh</span>
</div>
</div>
<div className="d-stats-card" style={{ padding: 20 }}>
<div style={{ fontSize: 14, color: '#999', marginBottom: 8 }}>剩余余额</div>
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: '#1a1a1a' }}>
<div style={{ fontSize: 14, color: 'var(--text-tertiary)', marginBottom: 8 }}>剩余余额</div>
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: 'var(--text-primary)' }}>
¥{current?.amount?.toFixed(2) ?? '--'}
</div>
</div>
<div className="d-stats-card" style={{ padding: 20 }}>
<div style={{ fontSize: 14, color: '#999', marginBottom: 8 }}>今日用电</div>
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: '#1a1a1a', display: 'flex', alignItems: 'baseline', gap: 6 }}>
<div style={{ fontSize: 14, color: 'var(--text-tertiary)', marginBottom: 8 }}>今日用电</div>
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: 'var(--text-primary)', display: 'flex', alignItems: 'baseline', gap: 6 }}>
{todayUsage.toFixed(1)}
<span style={{ fontSize: 16, fontWeight: 400, color: '#999' }}>kWh</span>
<span style={{ fontSize: 16, fontWeight: 400, color: 'var(--text-tertiary)' }}>kWh</span>
</div>
</div>
<div className="d-stats-card" style={{ padding: 20 }}>
<div style={{ fontSize: 14, color: '#999', marginBottom: 8 }}>今日电费</div>
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: '#1a1a1a' }}>
<div style={{ fontSize: 14, color: 'var(--text-tertiary)', marginBottom: 8 }}>今日电费</div>
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: 'var(--text-primary)' }}>
¥{todayCost.toFixed(2)}
</div>
</div>
@@ -381,15 +380,15 @@ body { margin: 0; }
<div className="d-section-gap" style={{ marginBottom: 32 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 16, fontWeight: 600, color: '#1a1a1a' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 16, fontWeight: 600, color: 'var(--text-primary)' }}>
<ChartLine theme="filled" size="20" fill="#4a90e2" style={{ display: 'flex' }} />
今日用电趋势
<span style={{ fontSize: 13, color: '#999', fontWeight: 400 }}>(每时段)</span>
<span style={{ fontSize: 13, color: 'var(--text-tertiary)', fontWeight: 400 }}>(每时段)</span>
</div>
</div>
<div className="d-chart" style={{ position: 'relative', height: 280, background: '#fafbfc', borderRadius: 8, padding: 20 }}>
<div className="d-chart" style={{ position: 'relative', height: 280, background: 'var(--bg-chart)', borderRadius: 8, padding: 20 }}>
{(!currentData?.todayRecords || currentData.todayRecords.length === 0) ? (
<div className="d-chart-empty" style={{ textAlign: 'center', paddingTop: 110, color: '#999' }}>暂无今日数据</div>
<div className="d-chart-empty" style={{ textAlign: 'center', paddingTop: 110, color: 'var(--text-tertiary)' }}>暂无今日数据</div>
) : null}
<canvas ref={hourlyChartRef} />
</div>
@@ -397,7 +396,7 @@ body { margin: 0; }
<div className="d-section-gap" style={{ marginBottom: 32 }}>
<div className="d-trend-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 16, fontWeight: 600, color: '#1a1a1a' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 16, fontWeight: 600, color: 'var(--text-primary)' }}>
<ChartHistogram theme="filled" size="20" fill="#4a90e2" style={{ display: 'flex' }} />
用电 & 剩余电量趋势
</div>
@@ -409,8 +408,8 @@ body { margin: 0; }
style={{
padding: '8px 20px',
border: 'none',
background: days === d ? '#333' : '#f0f2f5',
color: days === d ? 'white' : '#666',
background: days === d ? 'var(--tab-bg-active)' : 'var(--tab-bg)',
color: days === d ? 'var(--tab-text-active)' : 'var(--tab-text)',
borderRadius: 20,
cursor: 'pointer',
fontSize: 14,
@@ -422,44 +421,44 @@ body { margin: 0; }
))}
</div>
</div>
<div className="d-chart" style={{ position: 'relative', height: 280, background: '#fafbfc', borderRadius: 8, padding: 20 }}>
<div className="d-chart" style={{ position: 'relative', height: 280, background: 'var(--bg-chart)', borderRadius: 8, padding: 20 }}>
{(!historyData?.dailyRecords || historyData.dailyRecords.length === 0) ? (
<div className="d-chart-empty" style={{ textAlign: 'center', paddingTop: 110, color: '#999' }}>暂无历史数据</div>
<div className="d-chart-empty" style={{ textAlign: 'center', paddingTop: 110, color: 'var(--text-tertiary)' }}>暂无历史数据</div>
) : null}
<canvas ref={trendChartRef} />
</div>
</div>
<div className="d-bottom" style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 24 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 16, background: '#f8f9fa', borderRadius: 8 }}>
<div style={{ width: 40, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, background: '#e8f4fd' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 16, background: 'var(--bg-item)', borderRadius: 8 }}>
<div style={{ width: 40, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, background: 'var(--bg-item-blue)' }}>
<ChartHistogram theme="filled" size="22" fill="#4a90e2" style={{ display: 'flex' }} />
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13, color: '#666', marginBottom: 2 }}>日均耗电</div>
<div style={{ fontSize: 18, fontWeight: 600, color: '#1a1a1a' }}>
<div style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 2 }}>日均耗电</div>
<div style={{ fontSize: 18, fontWeight: 600, color: 'var(--text-primary)' }}>
{avgDaily != null ? `${avgDaily.toFixed(1)} kWh` : '--'}
</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 16, background: '#f8f9fa', borderRadius: 8 }}>
<div style={{ width: 40, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, background: '#f0e8fd' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 16, background: 'var(--bg-item)', borderRadius: 8 }}>
<div style={{ width: 40, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, background: 'var(--bg-item-purple)' }}>
<Lightning theme="filled" size="22" fill="#9b59b6" style={{ display: 'flex' }} />
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13, color: '#666', marginBottom: 2 }}>平均功率</div>
<div style={{ fontSize: 18, fontWeight: 600, color: '#1a1a1a' }}>
{todayAvgPower != null ? `${(todayAvgPower * 1000).toFixed(0)} W` : '--'}
<div style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 2 }}>平均功率</div>
<div style={{ fontSize: 18, fontWeight: 600, color: 'var(--text-primary)' }}>
{avgPower != null ? `${(avgPower * 1000).toFixed(0)} W` : '--'}
</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 16, background: '#f8f9fa', borderRadius: 8 }}>
<div style={{ width: 40, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, background: '#e8fdf4' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 16, background: 'var(--bg-item)', borderRadius: 8 }}>
<div style={{ width: 40, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, background: 'var(--bg-item-green)' }}>
<Timer theme="filled" size="22" fill="#27ae60" style={{ display: 'flex' }} />
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13, color: '#666', marginBottom: 2 }}>预计可用</div>
<div style={{ fontSize: 18, fontWeight: 600, color: '#1a1a1a' }}>
<div style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 2 }}>预计可用</div>
<div style={{ fontSize: 18, fontWeight: 600, color: 'var(--text-primary)' }}>
{estimatedDays != null ? `${estimatedDays.toFixed(0)}` : '--'}
</div>
</div>

View File

@@ -29,7 +29,7 @@ export default function Login({ onLogin }) {
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
background: '#f5f7fa',
background: 'var(--bg-body)',
padding: 0,
}}>
<Card
@@ -37,7 +37,6 @@ export default function Login({ onLogin }) {
width: '100%',
maxWidth: 400,
borderRadius: 12,
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
}}
styles={{ body: { padding: '48px 40px', textAlign: 'center' } }}
>
@@ -47,11 +46,11 @@ export default function Login({ onLogin }) {
margin: 0,
fontSize: 24,
fontWeight: 600,
color: '#1a1a1a',
color: 'var(--text-primary)',
}}>
智能电量监控
</h2>
<p style={{ color: '#999', fontSize: 14, marginTop: 8, marginBottom: 0 }}>
<p style={{ color: 'var(--text-tertiary)', fontSize: 14, marginTop: 8, marginBottom: 0 }}>
请登录后查看用电数据
</p>
</div>
@@ -66,7 +65,7 @@ export default function Login({ onLogin }) {
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input
prefix={<UserOutlined style={{ color: '#999' }} />}
prefix={<UserOutlined style={{ color: 'var(--text-tertiary)' }} />}
placeholder="用户名"
size="large"
/>
@@ -77,7 +76,7 @@ export default function Login({ onLogin }) {
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password
prefix={<LockOutlined style={{ color: '#999' }} />}
prefix={<LockOutlined style={{ color: 'var(--text-tertiary)' }} />}
placeholder="密码"
size="large"
/>