feat(energy): connect to real DB (lingniu_prod)
Replace front-end mock data with live API backed by: - tab_energy_hydrogen_bill (66.5K rows) joined with tab_hydrogen_site (internal stations) and tab_outside_hydrogen_site (external stations, joined via inner_site_id) - tab_energy_electricity_bill (4.4K rows, all 龙王路充电站) New server routes (src/server/routes/energy/): - GET /api/energy/hydrogen/overview → KPI + Top5 站点 + 区域占比 - GET /api/energy/hydrogen/daily?range=&customer= → 日级 + 站点级下钻 - GET /api/energy/electric/overview → KPI + 本月柱图 (fallback to last available month if current month has no data) - GET /api/energy/electric/monthly?customer= → 6 个月分组日级表 Business rules encoded server-side: - 客户类型: customer_id IS NULL = 羚牛承担, NOT NULL = 外部 - 时区: DATETIME 列字面值是 UTC,分组前 +8h 转成 CST - 数据清理: hydrogen_time >= 2024-01-01 (排除 1900 年脏数据) - 站点名 fallback: short_name → name → fixed_station_name → station_name → '未知站点' - 区域归一化: SUBSTRING_INDEX(city, '-', -1) 取最后一段,去掉 '省'/'市' 让 '四川省-成都市' 和 '成都市' 合并为 '成都' Component changes: - All 4 components (HydrogenOverview, HydrogenDaily, ElectricOverview, ElectricDaily) now use useEffect + fetch with loading/error states - HydrogenDaily filtering moved to server (range + customer params) → drops client-side TODAY constant + isInPick switch - ElectricOverview chart title is dynamic: shows 'YYYY-MM 每日充电' when fallback kicks in (current month has no data) - mock.ts deleted Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, Tooltip } from 'recharts';
|
||||
import TrendBadge from './TrendBadge';
|
||||
import { HYDROGEN_DAILY } from './mock';
|
||||
import { fetchHydrogenDaily } from './api';
|
||||
import type { CustomerType, DateQuickPick, HydrogenDailyRow } from './types';
|
||||
|
||||
const TODAY = new Date('2026-04-28');
|
||||
|
||||
const QUICK_PICK_OPTIONS: Array<{ id: DateQuickPick; label: string }> = [
|
||||
{ id: 'today', label: '当天' },
|
||||
{ id: 'thisWeek', label: '本周' },
|
||||
@@ -17,51 +15,25 @@ const QUICK_PICK_OPTIONS: Array<{ id: DateQuickPick; label: string }> = [
|
||||
{ id: 'last30', label: '最近30天' },
|
||||
];
|
||||
|
||||
function isInPick(date: string, pick: DateQuickPick): boolean {
|
||||
const d = new Date(date);
|
||||
switch (pick) {
|
||||
case 'today': {
|
||||
return d.toISOString().slice(0, 10) === TODAY.toISOString().slice(0, 10);
|
||||
}
|
||||
case 'thisWeek': {
|
||||
const day = TODAY.getDay() || 7;
|
||||
const start = new Date(TODAY); start.setDate(TODAY.getDate() - day + 1);
|
||||
return d >= start && d <= TODAY;
|
||||
}
|
||||
case 'thisMonth':
|
||||
return d.getFullYear() === TODAY.getFullYear() && d.getMonth() === TODAY.getMonth();
|
||||
case 'thisQuarter': {
|
||||
const q = Math.floor(TODAY.getMonth() / 3);
|
||||
const dq = Math.floor(d.getMonth() / 3);
|
||||
return d.getFullYear() === TODAY.getFullYear() && dq === q;
|
||||
}
|
||||
case 'last7': {
|
||||
const c = new Date(TODAY); c.setDate(TODAY.getDate() - 6);
|
||||
return d >= c && d <= TODAY;
|
||||
}
|
||||
case 'last30': {
|
||||
const c = new Date(TODAY); c.setDate(TODAY.getDate() - 29);
|
||||
return d >= c && d <= TODAY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function HydrogenDaily() {
|
||||
const [pick, setPick] = useState<DateQuickPick>('last30');
|
||||
const [customer, setCustomer] = useState<CustomerType>('external');
|
||||
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
||||
const [rows, setRows] = useState<HydrogenDailyRow[] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const rows = useMemo<HydrogenDailyRow[]>(() => {
|
||||
return HYDROGEN_DAILY
|
||||
.filter(r => r.customerType === customer)
|
||||
.filter(r => isInPick(r.date, pick))
|
||||
.sort((a, b) => b.date.localeCompare(a.date));
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setError(null);
|
||||
fetchHydrogenDaily(pick, customer)
|
||||
.then(r => { if (!cancelled) setRows(r); })
|
||||
.catch(e => { if (!cancelled) setError(e instanceof Error ? e.message : String(e)); });
|
||||
return () => { cancelled = true; };
|
||||
}, [pick, customer]);
|
||||
|
||||
// 柱图:按日期升序,用于"从左到右时间流"
|
||||
const trendData = useMemo(() => [...rows].sort((a, b) => a.date.localeCompare(b.date)), [rows]);
|
||||
|
||||
const totalKg = rows.reduce((a, r) => a + r.totalKg, 0);
|
||||
const trendData = useMemo(() => (rows ? [...rows].sort((a, b) => a.date.localeCompare(b.date)) : []), [rows]);
|
||||
const totalKg = (rows ?? []).reduce((a, r) => a + r.totalKg, 0);
|
||||
|
||||
const toggle = (date: string) => setExpanded(prev => {
|
||||
const next = new Set(prev);
|
||||
@@ -161,7 +133,11 @@ export default function HydrogenDaily() {
|
||||
<span />
|
||||
</div>
|
||||
{/* 主行 + 子行 */}
|
||||
{rows.length === 0 ? (
|
||||
{error ? (
|
||||
<div className="px-3 py-10 text-center text-red-500 text-[12px] font-bold">加载失败:{error}</div>
|
||||
) : rows === null ? (
|
||||
<div className="px-3 py-10 text-center text-slate-400 text-[12px] font-bold">加载中…</div>
|
||||
) : rows.length === 0 ? (
|
||||
<div className="px-3 py-10 text-center text-slate-400 text-[12px] font-bold">暂无数据</div>
|
||||
) : rows.map(r => {
|
||||
const open = expanded.has(r.date);
|
||||
|
||||
Reference in New Issue
Block a user