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 --from=builder /app/node_modules ./node_modules
|
||||||
COPY server.js .
|
COPY server.js .
|
||||||
COPY --from=client-builder /app/dist ./client/dist
|
COPY --from=client-builder /app/dist ./client/dist
|
||||||
COPY data ./data
|
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<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 Login from './pages/Login';
|
||||||
import { checkAuth } from './api';
|
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() {
|
export default function App() {
|
||||||
const [authed, setAuthed] = useState(null);
|
const [authed, setAuthed] = useState(null);
|
||||||
|
|
||||||
@@ -13,11 +60,17 @@ export default function App() {
|
|||||||
|
|
||||||
if (authed === null) {
|
if (authed === null) {
|
||||||
return (
|
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" />
|
<Spin size="large" />
|
||||||
</div>
|
</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,10 +1,24 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { ConfigProvider } from 'antd';
|
import { ConfigProvider, theme } from 'antd';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
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
|
<ConfigProvider
|
||||||
theme={{
|
theme={{
|
||||||
|
algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
||||||
token: {
|
token: {
|
||||||
colorPrimary: '#4a90e2',
|
colorPrimary: '#4a90e2',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
@@ -12,6 +26,11 @@ ReactDOM.createRoot(document.getElementById('root')).render(
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div data-theme={isDark ? 'dark' : 'light'}>
|
||||||
<App />
|
<App />
|
||||||
|
</div>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(<Root />);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { Button, Spin, message } from 'antd';
|
import { Button, Spin, message } from 'antd';
|
||||||
import { LogoutOutlined, ReloadOutlined } from '@ant-design/icons';
|
import { LogoutOutlined, ReloadOutlined, BugOutlined } from '@ant-design/icons';
|
||||||
import { BugOutlined } from '@ant-design/icons';
|
|
||||||
import { Lightning, ChartLine, ChartHistogram, Timer } from '@icon-park/react';
|
import { Lightning, ChartLine, ChartHistogram, Timer } from '@icon-park/react';
|
||||||
import { Chart, registerables } from 'chart.js';
|
import { Chart, registerables } from 'chart.js';
|
||||||
import { fetchCurrent, fetchHistory, triggerCollect, logout } from '../api';
|
import { fetchCurrent, fetchHistory, triggerCollect, logout } from '../api';
|
||||||
@@ -278,7 +277,7 @@ export default function Dashboard({ onLogout }) {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
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" />
|
<Spin size="large" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -288,21 +287,21 @@ export default function Dashboard({ onLogout }) {
|
|||||||
const stats = historyData?.stats || {};
|
const stats = historyData?.stats || {};
|
||||||
const statKey = days === 7 ? '7' : days === 15 ? '15' : '30';
|
const statKey = days === 7 ? '7' : days === 15 ? '15' : '30';
|
||||||
const avgDaily = stats[`avgDaily${statKey}`];
|
const avgDaily = stats[`avgDaily${statKey}`];
|
||||||
|
const avgPower = stats[`avgPower${statKey}`];
|
||||||
const estimatedDays = stats[`estimatedDays${statKey}`];
|
const estimatedDays = stats[`estimatedDays${statKey}`];
|
||||||
|
|
||||||
const todayUsage = currentData?.todayUsage ?? 0;
|
const todayUsage = currentData?.todayUsage ?? 0;
|
||||||
const todayCost = currentData?.todayCost ?? 0;
|
const todayCost = currentData?.todayCost ?? 0;
|
||||||
const todayAvgPower = currentData?.todayAvgPower ?? null;
|
|
||||||
|
|
||||||
return (
|
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}
|
<style>{`${styles}
|
||||||
body { margin: 0; }
|
body { margin: 0; }
|
||||||
.d-container { border-radius: 12px !important; box-shadow: none !important; }
|
.d-container { border-radius: 12px !important; box-shadow: none !important; }
|
||||||
`}</style>
|
`}</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 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' }} />
|
<Lightning theme="filled" size="28" fill="#4a90e2" style={{ display: 'flex' }} />
|
||||||
智能电量监控
|
智能电量监控
|
||||||
</div>
|
</div>
|
||||||
@@ -313,9 +312,9 @@ body { margin: 0; }
|
|||||||
loading={collecting}
|
loading={collecting}
|
||||||
size="small"
|
size="small"
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid #d9d9d9',
|
border: '1px solid var(--border-light)',
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
color: '#666',
|
color: 'var(--text-secondary)',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -326,9 +325,9 @@ body { margin: 0; }
|
|||||||
onClick={handleTestNotify}
|
onClick={handleTestNotify}
|
||||||
size="small"
|
size="small"
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid #d9d9d9',
|
border: '1px solid var(--border-light)',
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
color: '#666',
|
color: 'var(--text-secondary)',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -339,9 +338,9 @@ body { margin: 0; }
|
|||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
size="small"
|
size="small"
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid #d9d9d9',
|
border: '1px solid var(--border-light)',
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
color: '#666',
|
color: 'var(--text-secondary)',
|
||||||
fontSize: 13,
|
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" style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 24, marginBottom: 32 }}>
|
||||||
<div className="d-stats-card" style={{ padding: 20 }}>
|
<div className="d-stats-card" style={{ padding: 20 }}>
|
||||||
<div style={{ fontSize: 14, color: '#999', marginBottom: 8 }}>剩余电量</div>
|
<div style={{ fontSize: 14, color: 'var(--text-tertiary)', marginBottom: 8 }}>剩余电量</div>
|
||||||
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: '#1a1a1a', display: 'flex', alignItems: 'baseline', gap: 6 }}>
|
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: 'var(--text-primary)', display: 'flex', alignItems: 'baseline', gap: 6 }}>
|
||||||
{current?.surplus?.toFixed(1) ?? '--'}
|
{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>
|
</div>
|
||||||
<div className="d-stats-card" style={{ padding: 20 }}>
|
<div className="d-stats-card" style={{ padding: 20 }}>
|
||||||
<div style={{ fontSize: 14, color: '#999', marginBottom: 8 }}>剩余余额</div>
|
<div style={{ fontSize: 14, color: 'var(--text-tertiary)', marginBottom: 8 }}>剩余余额</div>
|
||||||
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: '#1a1a1a' }}>
|
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: 'var(--text-primary)' }}>
|
||||||
¥{current?.amount?.toFixed(2) ?? '--'}
|
¥{current?.amount?.toFixed(2) ?? '--'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="d-stats-card" style={{ padding: 20 }}>
|
<div className="d-stats-card" style={{ padding: 20 }}>
|
||||||
<div style={{ fontSize: 14, color: '#999', marginBottom: 8 }}>今日用电</div>
|
<div style={{ fontSize: 14, color: 'var(--text-tertiary)', marginBottom: 8 }}>今日用电</div>
|
||||||
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: '#1a1a1a', display: 'flex', alignItems: 'baseline', gap: 6 }}>
|
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: 'var(--text-primary)', display: 'flex', alignItems: 'baseline', gap: 6 }}>
|
||||||
{todayUsage.toFixed(1)}
|
{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>
|
</div>
|
||||||
<div className="d-stats-card" style={{ padding: 20 }}>
|
<div className="d-stats-card" style={{ padding: 20 }}>
|
||||||
<div style={{ fontSize: 14, color: '#999', marginBottom: 8 }}>今日电费</div>
|
<div style={{ fontSize: 14, color: 'var(--text-tertiary)', marginBottom: 8 }}>今日电费</div>
|
||||||
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: '#1a1a1a' }}>
|
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: 'var(--text-primary)' }}>
|
||||||
¥{todayCost.toFixed(2)}
|
¥{todayCost.toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -381,15 +380,15 @@ body { margin: 0; }
|
|||||||
|
|
||||||
<div className="d-section-gap" style={{ marginBottom: 32 }}>
|
<div className="d-section-gap" style={{ marginBottom: 32 }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
<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' }} />
|
<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>
|
</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) ? (
|
{(!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}
|
) : null}
|
||||||
<canvas ref={hourlyChartRef} />
|
<canvas ref={hourlyChartRef} />
|
||||||
</div>
|
</div>
|
||||||
@@ -397,7 +396,7 @@ body { margin: 0; }
|
|||||||
|
|
||||||
<div className="d-section-gap" style={{ marginBottom: 32 }}>
|
<div className="d-section-gap" style={{ marginBottom: 32 }}>
|
||||||
<div className="d-trend-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
<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' }} />
|
<ChartHistogram theme="filled" size="20" fill="#4a90e2" style={{ display: 'flex' }} />
|
||||||
用电 & 剩余电量趋势
|
用电 & 剩余电量趋势
|
||||||
</div>
|
</div>
|
||||||
@@ -409,8 +408,8 @@ body { margin: 0; }
|
|||||||
style={{
|
style={{
|
||||||
padding: '8px 20px',
|
padding: '8px 20px',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
background: days === d ? '#333' : '#f0f2f5',
|
background: days === d ? 'var(--tab-bg-active)' : 'var(--tab-bg)',
|
||||||
color: days === d ? 'white' : '#666',
|
color: days === d ? 'var(--tab-text-active)' : 'var(--tab-text)',
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@@ -422,44 +421,44 @@ body { margin: 0; }
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</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) ? (
|
{(!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}
|
) : null}
|
||||||
<canvas ref={trendChartRef} />
|
<canvas ref={trendChartRef} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="d-bottom" style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 24 }}>
|
<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={{ 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: '#e8f4fd' }}>
|
<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' }} />
|
<ChartHistogram theme="filled" size="22" fill="#4a90e2" style={{ display: 'flex' }} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<div style={{ fontSize: 13, color: '#666', marginBottom: 2 }}>日均耗电</div>
|
<div style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 2 }}>日均耗电</div>
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: '#1a1a1a' }}>
|
<div style={{ fontSize: 18, fontWeight: 600, color: 'var(--text-primary)' }}>
|
||||||
{avgDaily != null ? `${avgDaily.toFixed(1)} kWh` : '--'}
|
{avgDaily != null ? `${avgDaily.toFixed(1)} kWh` : '--'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 16, background: '#f8f9fa', borderRadius: 8 }}>
|
<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: '#f0e8fd' }}>
|
<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' }} />
|
<Lightning theme="filled" size="22" fill="#9b59b6" style={{ display: 'flex' }} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<div style={{ fontSize: 13, color: '#666', marginBottom: 2 }}>平均功率</div>
|
<div style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 2 }}>平均功率</div>
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: '#1a1a1a' }}>
|
<div style={{ fontSize: 18, fontWeight: 600, color: 'var(--text-primary)' }}>
|
||||||
{todayAvgPower != null ? `${(todayAvgPower * 1000).toFixed(0)} W` : '--'}
|
{avgPower != null ? `${(avgPower * 1000).toFixed(0)} W` : '--'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 16, background: '#f8f9fa', borderRadius: 8 }}>
|
<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: '#e8fdf4' }}>
|
<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' }} />
|
<Timer theme="filled" size="22" fill="#27ae60" style={{ display: 'flex' }} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<div style={{ fontSize: 13, color: '#666', marginBottom: 2 }}>预计可用</div>
|
<div style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 2 }}>预计可用</div>
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: '#1a1a1a' }}>
|
<div style={{ fontSize: 18, fontWeight: 600, color: 'var(--text-primary)' }}>
|
||||||
{estimatedDays != null ? `${estimatedDays.toFixed(0)} 天` : '--'}
|
{estimatedDays != null ? `${estimatedDays.toFixed(0)} 天` : '--'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function Login({ onLogin }) {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
background: '#f5f7fa',
|
background: 'var(--bg-body)',
|
||||||
padding: 0,
|
padding: 0,
|
||||||
}}>
|
}}>
|
||||||
<Card
|
<Card
|
||||||
@@ -37,7 +37,6 @@ export default function Login({ onLogin }) {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: 400,
|
maxWidth: 400,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
|
||||||
}}
|
}}
|
||||||
styles={{ body: { padding: '48px 40px', textAlign: 'center' } }}
|
styles={{ body: { padding: '48px 40px', textAlign: 'center' } }}
|
||||||
>
|
>
|
||||||
@@ -47,11 +46,11 @@ export default function Login({ onLogin }) {
|
|||||||
margin: 0,
|
margin: 0,
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: '#1a1a1a',
|
color: 'var(--text-primary)',
|
||||||
}}>
|
}}>
|
||||||
智能电量监控
|
智能电量监控
|
||||||
</h2>
|
</h2>
|
||||||
<p style={{ color: '#999', fontSize: 14, marginTop: 8, marginBottom: 0 }}>
|
<p style={{ color: 'var(--text-tertiary)', fontSize: 14, marginTop: 8, marginBottom: 0 }}>
|
||||||
请登录后查看用电数据
|
请登录后查看用电数据
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,7 +65,7 @@ export default function Login({ onLogin }) {
|
|||||||
rules={[{ required: true, message: '请输入用户名' }]}
|
rules={[{ required: true, message: '请输入用户名' }]}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
prefix={<UserOutlined style={{ color: '#999' }} />}
|
prefix={<UserOutlined style={{ color: 'var(--text-tertiary)' }} />}
|
||||||
placeholder="用户名"
|
placeholder="用户名"
|
||||||
size="large"
|
size="large"
|
||||||
/>
|
/>
|
||||||
@@ -77,7 +76,7 @@ export default function Login({ onLogin }) {
|
|||||||
rules={[{ required: true, message: '请输入密码' }]}
|
rules={[{ required: true, message: '请输入密码' }]}
|
||||||
>
|
>
|
||||||
<Input.Password
|
<Input.Password
|
||||||
prefix={<LockOutlined style={{ color: '#999' }} />}
|
prefix={<LockOutlined style={{ color: 'var(--text-tertiary)' }} />}
|
||||||
placeholder="密码"
|
placeholder="密码"
|
||||||
size="large"
|
size="large"
|
||||||
/>
|
/>
|
||||||
|
|||||||
79
server.js
79
server.js
@@ -314,63 +314,52 @@ app.get('/api/history', requireAuth, (req, res) => {
|
|||||||
const { dailyRecords } = calculateDailyUsage(allRecords);
|
const { dailyRecords } = calculateDailyUsage(allRecords);
|
||||||
const latest = allRecords.length > 0 ? allRecords[allRecords.length - 1] : null;
|
const latest = allRecords.length > 0 ? allRecords[allRecords.length - 1] : null;
|
||||||
|
|
||||||
|
const calcStats = (dailyRecords, n) => {
|
||||||
|
const records = dailyRecords.slice(-n);
|
||||||
|
if (records.length < 1) return { avgDaily: null, avgPower: null, estimatedDays: null };
|
||||||
|
|
||||||
const todayStr = getLocalDateStr(new Date());
|
const todayStr = getLocalDateStr(new Date());
|
||||||
const historyDailyRecords = dailyRecords.filter(d => d.date !== todayStr);
|
let totalUsage = 0;
|
||||||
|
let dayCount = 0;
|
||||||
|
|
||||||
const totalUsageLast7Days = historyDailyRecords.slice(-7).reduce((sum, d) => sum + d.usage, 0);
|
records.forEach(r => {
|
||||||
const totalUsageLast15Days = historyDailyRecords.slice(-15).reduce((sum, d) => sum + d.usage, 0);
|
if (r.date === todayStr) {
|
||||||
const totalUsageLast30Days = historyDailyRecords.slice(-30).reduce((sum, d) => sum + d.usage, 0);
|
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 totalHours7 = historyDailyRecords.slice(-7).reduce((sum, d) => sum + d.hoursSpan, 0);
|
const avgDaily = Math.round((totalUsage / dayCount) * 100) / 100;
|
||||||
const totalHours15 = historyDailyRecords.slice(-15).reduce((sum, d) => sum + d.hoursSpan, 0);
|
const avgPower = Math.round((avgDaily / 24) * 1000) / 1000;
|
||||||
const totalHours30 = historyDailyRecords.slice(-30).reduce((sum, d) => sum + d.hoursSpan, 0);
|
const estimatedDays = avgDaily > 0 && latest
|
||||||
|
? Math.round(latest.surplus / avgDaily * 10) / 10
|
||||||
const days7 = Math.min(7, historyDailyRecords.length);
|
|
||||||
const days15 = Math.min(15, historyDailyRecords.length);
|
|
||||||
const days30 = Math.min(30, historyDailyRecords.length);
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
const avgPower7 = totalHours7 >= 1
|
|
||||||
? Math.round((totalUsageLast7Days / totalHours7) * 1000) / 1000
|
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const avgPower15 = totalHours15 >= 1
|
return { avgDaily, avgPower, estimatedDays };
|
||||||
? Math.round((totalUsageLast15Days / totalHours15) * 1000) / 1000
|
};
|
||||||
: null;
|
|
||||||
|
|
||||||
const avgPower30 = totalHours30 >= 1
|
const s7 = calcStats(dailyRecords, 7);
|
||||||
? Math.round((totalUsageLast30Days / totalHours30) * 1000) / 1000
|
const s15 = calcStats(dailyRecords, 15);
|
||||||
: null;
|
const s30 = calcStats(dailyRecords, 30);
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
current: latest,
|
current: latest,
|
||||||
dailyRecords: dailyRecords.slice(-30),
|
dailyRecords: dailyRecords.slice(-30),
|
||||||
stats: {
|
stats: {
|
||||||
avgDaily7,
|
avgDaily7: s7.avgDaily,
|
||||||
avgDaily15,
|
avgDaily15: s15.avgDaily,
|
||||||
avgDaily30,
|
avgDaily30: s30.avgDaily,
|
||||||
avgPower7,
|
avgPower7: s7.avgPower,
|
||||||
avgPower15,
|
avgPower15: s15.avgPower,
|
||||||
avgPower30,
|
avgPower30: s30.avgPower,
|
||||||
estimatedDays7,
|
estimatedDays7: s7.estimatedDays,
|
||||||
estimatedDays15,
|
estimatedDays15: s15.estimatedDays,
|
||||||
estimatedDays30
|
estimatedDays30: s30.estimatedDays,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user