From 224233421d99b8f6024440a606c96bf14073705d Mon Sep 17 00:00:00 2001 From: EchoZenith <60392923+EchoZenith@users.noreply.github.com> Date: Sat, 23 May 2026 01:38:49 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E7=94=B5?= =?UTF-8?q?=E9=87=8F=E7=9B=91=E6=8E=A7=E7=B3=BB=E7=BB=9F=E4=B8=BB=E9=A2=98?= =?UTF-8?q?=E4=B8=8E=E7=BB=9F=E8=AE=A1=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加深色模式适配,跟随系统主题自动切换 - 替换硬编码样式为CSS变量实现主题统一管理 - 重构后端统计计算逻辑,优化当日用电估算方式 - 更新页面标题与站点图标,调整登录页样式 - 移除冗余的data目录拷贝配置 --- Dockerfile | 1 - client/index.html | 3 +- client/public/lightning.svg | 1 + client/src/App.jsx | 57 +++++++++++++++++++++- client/src/main.jsx | 47 ++++++++++++------ client/src/pages/Dashboard.jsx | 89 +++++++++++++++++----------------- client/src/pages/Login.jsx | 11 ++--- server.js | 81 +++++++++++++------------------ 8 files changed, 175 insertions(+), 115 deletions(-) create mode 100644 client/public/lightning.svg diff --git a/Dockerfile b/Dockerfile index 4756fb4..f395ad9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/client/index.html b/client/index.html index 904bce5..82ed006 100644 --- a/client/index.html +++ b/client/index.html @@ -3,7 +3,8 @@ - 电费监控系统 + + 智能电量监控
diff --git a/client/public/lightning.svg b/client/public/lightning.svg new file mode 100644 index 0000000..cab721e --- /dev/null +++ b/client/public/lightning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/App.jsx b/client/src/App.jsx index 1062e92..68e64d7 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -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 ( -
+
+
); } - return authed ? setAuthed(false)} /> : setAuthed(true)} />; + return ( + <> + + {authed ? setAuthed(false)} /> : setAuthed(true)} />} + + ); } diff --git a/client/src/main.jsx b/client/src/main.jsx index f2d3707..0bd9ba9 100644 --- a/client/src/main.jsx +++ b/client/src/main.jsx @@ -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( - - - -); +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 ( + +
+ +
+
+ ); +} + +ReactDOM.createRoot(document.getElementById('root')).render(); diff --git a/client/src/pages/Dashboard.jsx b/client/src/pages/Dashboard.jsx index 23305cc..f2bfce1 100644 --- a/client/src/pages/Dashboard.jsx +++ b/client/src/pages/Dashboard.jsx @@ -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 ( -
+
); @@ -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 ( -
+
-
+
-
+
智能电量监控
@@ -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; }
-
剩余电量
-
+
剩余电量
+
{current?.surplus?.toFixed(1) ?? '--'} - kWh + kWh
-
剩余余额
-
+
剩余余额
+
¥{current?.amount?.toFixed(2) ?? '--'}
-
今日用电
-
+
今日用电
+
{todayUsage.toFixed(1)} - kWh + kWh
-
今日电费
-
+
今日电费
+
¥{todayCost.toFixed(2)}
@@ -381,15 +380,15 @@ body { margin: 0; }
-
+
今日用电趋势 - (每时段) + (每时段)
-
+
{(!currentData?.todayRecords || currentData.todayRecords.length === 0) ? ( -
暂无今日数据
+
暂无今日数据
) : null}
@@ -397,7 +396,7 @@ body { margin: 0; }
-
+
用电 & 剩余电量趋势
@@ -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; } ))}
-
+
{(!historyData?.dailyRecords || historyData.dailyRecords.length === 0) ? ( -
暂无历史数据
+
暂无历史数据
) : null}
-
-
+
+
-
日均耗电
-
+
日均耗电
+
{avgDaily != null ? `${avgDaily.toFixed(1)} kWh` : '--'}
-
-
+
+
-
平均功率
-
- {todayAvgPower != null ? `${(todayAvgPower * 1000).toFixed(0)} W` : '--'} +
平均功率
+
+ {avgPower != null ? `${(avgPower * 1000).toFixed(0)} W` : '--'}
-
-
+
+
-
预计可用
-
+
预计可用
+
{estimatedDays != null ? `${estimatedDays.toFixed(0)} 天` : '--'}
diff --git a/client/src/pages/Login.jsx b/client/src/pages/Login.jsx index 667531d..37a7c6a 100644 --- a/client/src/pages/Login.jsx +++ b/client/src/pages/Login.jsx @@ -29,7 +29,7 @@ export default function Login({ onLogin }) { justifyContent: 'center', alignItems: 'center', minHeight: '100vh', - background: '#f5f7fa', + background: 'var(--bg-body)', padding: 0, }}> @@ -47,11 +46,11 @@ export default function Login({ onLogin }) { margin: 0, fontSize: 24, fontWeight: 600, - color: '#1a1a1a', + color: 'var(--text-primary)', }}> 智能电量监控 -

+

请登录后查看用电数据

@@ -66,7 +65,7 @@ export default function Login({ onLogin }) { rules={[{ required: true, message: '请输入用户名' }]} > } + prefix={} placeholder="用户名" size="large" /> @@ -77,7 +76,7 @@ export default function Login({ onLogin }) { rules={[{ required: true, message: '请输入密码' }]} > } + prefix={} placeholder="密码" size="large" /> diff --git a/server.js b/server.js index 28e5806..81fada3 100644 --- a/server.js +++ b/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, } }); });