refactor: 优化电量监控系统主题与统计逻辑
- 添加深色模式适配,跟随系统主题自动切换 - 替换硬编码样式为CSS变量实现主题统一管理 - 重构后端统计计算逻辑,优化当日用电估算方式 - 更新页面标题与站点图标,调整登录页样式 - 移除冗余的data目录拷贝配置
This commit is contained in:
@@ -30,7 +30,6 @@ ENV TZ=Asia/Shanghai
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY server.js .
|
||||
COPY --from=client-builder /app/dist ./client/dist
|
||||
COPY data ./data
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
1
client/public/lightning.svg
Normal file
1
client/public/lightning.svg
Normal 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 |
@@ -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)} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 />);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
81
server.js
81
server.js
@@ -314,63 +314,52 @@ app.get('/api/history', requireAuth, (req, res) => {
|
||||
const { dailyRecords } = calculateDailyUsage(allRecords);
|
||||
const latest = allRecords.length > 0 ? allRecords[allRecords.length - 1] : null;
|
||||
|
||||
const todayStr = getLocalDateStr(new Date());
|
||||
const historyDailyRecords = dailyRecords.filter(d => d.date !== todayStr);
|
||||
const calcStats = (dailyRecords, n) => {
|
||||
const records = dailyRecords.slice(-n);
|
||||
if (records.length < 1) return { avgDaily: null, avgPower: null, estimatedDays: null };
|
||||
|
||||
const totalUsageLast7Days = historyDailyRecords.slice(-7).reduce((sum, d) => sum + d.usage, 0);
|
||||
const totalUsageLast15Days = historyDailyRecords.slice(-15).reduce((sum, d) => sum + d.usage, 0);
|
||||
const totalUsageLast30Days = historyDailyRecords.slice(-30).reduce((sum, d) => sum + d.usage, 0);
|
||||
const todayStr = getLocalDateStr(new Date());
|
||||
let totalUsage = 0;
|
||||
let dayCount = 0;
|
||||
|
||||
const totalHours7 = historyDailyRecords.slice(-7).reduce((sum, d) => sum + d.hoursSpan, 0);
|
||||
const totalHours15 = historyDailyRecords.slice(-15).reduce((sum, d) => sum + d.hoursSpan, 0);
|
||||
const totalHours30 = historyDailyRecords.slice(-30).reduce((sum, d) => sum + d.hoursSpan, 0);
|
||||
records.forEach(r => {
|
||||
if (r.date === todayStr) {
|
||||
const elapsed = Math.max(1, r.hoursSpan + 1);
|
||||
const normalized = Math.round((r.usage / elapsed) * 24 * 100) / 100;
|
||||
totalUsage += normalized;
|
||||
} else {
|
||||
totalUsage += r.usage;
|
||||
}
|
||||
dayCount++;
|
||||
});
|
||||
|
||||
const days7 = Math.min(7, historyDailyRecords.length);
|
||||
const days15 = Math.min(15, historyDailyRecords.length);
|
||||
const days30 = Math.min(30, historyDailyRecords.length);
|
||||
const avgDaily = Math.round((totalUsage / dayCount) * 100) / 100;
|
||||
const avgPower = Math.round((avgDaily / 24) * 1000) / 1000;
|
||||
const estimatedDays = avgDaily > 0 && latest
|
||||
? Math.round(latest.surplus / avgDaily * 10) / 10
|
||||
: null;
|
||||
|
||||
const avgDaily7 = days7 >= 1 ? Math.round((totalUsageLast7Days / days7) * 100) / 100 : null;
|
||||
const avgDaily15 = days15 >= 1 ? Math.round((totalUsageLast15Days / days15) * 100) / 100 : null;
|
||||
const avgDaily30 = days30 >= 1 ? Math.round((totalUsageLast30Days / days30) * 100) / 100 : null;
|
||||
return { avgDaily, avgPower, estimatedDays };
|
||||
};
|
||||
|
||||
const avgPower7 = totalHours7 >= 1
|
||||
? Math.round((totalUsageLast7Days / totalHours7) * 1000) / 1000
|
||||
: null;
|
||||
|
||||
const avgPower15 = totalHours15 >= 1
|
||||
? Math.round((totalUsageLast15Days / totalHours15) * 1000) / 1000
|
||||
: null;
|
||||
|
||||
const avgPower30 = totalHours30 >= 1
|
||||
? Math.round((totalUsageLast30Days / totalHours30) * 1000) / 1000
|
||||
: null;
|
||||
|
||||
const estimatedDays7 = avgDaily7 && avgDaily7 > 0 && latest
|
||||
? Math.round(latest.surplus / avgDaily7 * 10) / 10
|
||||
: null;
|
||||
|
||||
const estimatedDays15 = avgDaily15 && avgDaily15 > 0 && latest
|
||||
? Math.round(latest.surplus / avgDaily15 * 10) / 10
|
||||
: null;
|
||||
|
||||
const estimatedDays30 = avgDaily30 && avgDaily30 > 0 && latest
|
||||
? Math.round(latest.surplus / avgDaily30 * 10) / 10
|
||||
: null;
|
||||
const s7 = calcStats(dailyRecords, 7);
|
||||
const s15 = calcStats(dailyRecords, 15);
|
||||
const s30 = calcStats(dailyRecords, 30);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
current: latest,
|
||||
dailyRecords: dailyRecords.slice(-30),
|
||||
stats: {
|
||||
avgDaily7,
|
||||
avgDaily15,
|
||||
avgDaily30,
|
||||
avgPower7,
|
||||
avgPower15,
|
||||
avgPower30,
|
||||
estimatedDays7,
|
||||
estimatedDays15,
|
||||
estimatedDays30
|
||||
avgDaily7: s7.avgDaily,
|
||||
avgDaily15: s15.avgDaily,
|
||||
avgDaily30: s30.avgDaily,
|
||||
avgPower7: s7.avgPower,
|
||||
avgPower15: s15.avgPower,
|
||||
avgPower30: s30.avgPower,
|
||||
estimatedDays7: s7.estimatedDays,
|
||||
estimatedDays15: s15.estimatedDays,
|
||||
estimatedDays30: s30.estimatedDays,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user