feat(energy): electric view — daily bar chart + anomaly tint + mobile 环比
- New 本月每日充电 bar chart (蓝青 gradient) sits between KPI row and table, fixing the previous "wall of numbers" feel - Day rows now tint emerald/red when |chainPct| >= 30% (matches hydrogen) - 环比 pill column now also shows on mobile (was desktop-only) - Today KPI: pill moves to second line alongside kwh via justify-between so it no longer gets clipped on narrow viewports - Day labels in table trimmed to MM-DD (parent month row already shows year) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { ChevronRight, Wallet, BatteryCharging, CalendarClock } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, Tooltip } from 'recharts';
|
||||
import TrendBadge from './TrendBadge';
|
||||
import { ELECTRIC_KPI, ELECTRIC_MONTHLY } from './mock';
|
||||
import type { CustomerType } from './types';
|
||||
@@ -22,6 +23,13 @@ export default function ElectricView() {
|
||||
return ELECTRIC_MONTHLY;
|
||||
}, [customer]);
|
||||
|
||||
// 本月每日数据(按日期升序,便于柱图按时间从左到右展示)
|
||||
const trendData = useMemo(() => {
|
||||
const first = ELECTRIC_MONTHLY[0];
|
||||
if (!first) return [];
|
||||
return [...first.rows].sort((a, b) => a.date.localeCompare(b.date));
|
||||
}, []);
|
||||
|
||||
const k = ELECTRIC_KPI;
|
||||
|
||||
const toggleMonth = (m: string) => setOpenMonths(prev => {
|
||||
@@ -56,14 +64,53 @@ export default function ElectricView() {
|
||||
<div className="flex items-center gap-1 text-[11px] text-slate-500 font-bold mb-1">
|
||||
<BatteryCharging size={11} className="text-blue-600" />今日
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="text-base md:text-2xl font-bold text-slate-800">{fmtYuan(k.todayFee)}</div>
|
||||
<div className="flex items-center justify-between gap-1 mt-0.5">
|
||||
<span className="text-[11px] text-slate-500 font-bold truncate">{fmtKwh(k.todayKwh)}</span>
|
||||
<TrendBadge value={k.todayChainPct} />
|
||||
</div>
|
||||
<div className="text-[11px] text-slate-500 font-bold mt-0.5">{fmtKwh(k.todayKwh)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 本月每日充电柱图 */}
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-bold text-slate-700">本月每日充电</span>
|
||||
<span className="text-[11px] text-slate-400 font-bold">单位 元</span>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={160}>
|
||||
<BarChart data={trendData} margin={{ top: 8, right: 4, bottom: 0, left: 0 }}>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickFormatter={(v: string) => v.slice(5)}
|
||||
tick={{ fontSize: 10, fill: '#94a3b8' }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
interval="preserveStartEnd"
|
||||
minTickGap={8}
|
||||
/>
|
||||
<YAxis hide />
|
||||
<Tooltip
|
||||
formatter={(v) => [`¥${Number(v ?? 0).toLocaleString('zh-CN', { maximumFractionDigits: 2 })}`, '充电费用']}
|
||||
labelFormatter={(d) => `日期 ${d}`}
|
||||
contentStyle={{ borderRadius: 12, fontSize: 12 }}
|
||||
cursor={{ fill: 'rgba(59, 130, 246, 0.06)' }}
|
||||
/>
|
||||
<Bar dataKey="fee" radius={[4, 4, 0, 0]}>
|
||||
{trendData.map((_, i) => (
|
||||
<Cell key={i} fill="url(#electricBarGrad)" />
|
||||
))}
|
||||
</Bar>
|
||||
<defs>
|
||||
<linearGradient id="electricBarGrad" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stopColor="#3b82f6" />
|
||||
<stop offset="100%" stopColor="#22d3ee" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* 客户类型 */}
|
||||
<div className="bg-slate-100 rounded-xl p-1 grid grid-cols-2 gap-1">
|
||||
{(['external', 'lingniu'] as const).map(c => (
|
||||
@@ -81,12 +128,12 @@ export default function ElectricView() {
|
||||
|
||||
{/* 月份分组表 */}
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
|
||||
<div className="grid grid-cols-[1fr_auto_auto] md:grid-cols-[1fr_120px_140px_120px] gap-2 px-3 py-2 bg-slate-50 text-[11px] font-bold text-slate-500">
|
||||
<div className="grid grid-cols-[1fr_auto_auto_auto] md:grid-cols-[1fr_120px_140px_120px] gap-2 px-3 py-2 bg-slate-50 text-[11px] font-bold text-slate-500">
|
||||
<span>月份 / 日期</span>
|
||||
<span className="hidden md:block text-right">充电电量</span>
|
||||
<span className="text-right md:hidden">度</span>
|
||||
<span className="text-right">充电费用(元)</span>
|
||||
<span className="text-right hidden md:block">环比</span>
|
||||
<span className="text-right">环比</span>
|
||||
</div>
|
||||
{months.map(m => {
|
||||
const open = openMonths.has(m.month);
|
||||
@@ -94,7 +141,7 @@ export default function ElectricView() {
|
||||
<div key={m.month} className="border-t border-slate-100 first:border-t-0">
|
||||
<button
|
||||
onClick={() => toggleMonth(m.month)}
|
||||
className={`w-full grid grid-cols-[1fr_auto_auto] md:grid-cols-[1fr_120px_140px_120px] gap-2 px-3 py-2.5 text-left transition-colors ${
|
||||
className={`w-full grid grid-cols-[1fr_auto_auto_auto] md:grid-cols-[1fr_120px_140px_120px] gap-2 px-3 py-2.5 text-left transition-colors ${
|
||||
open ? 'bg-blue-50/30' : 'hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
@@ -108,7 +155,7 @@ export default function ElectricView() {
|
||||
<span className="text-right text-[12px] text-slate-700 font-bold tabular-nums">
|
||||
{m.fee.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}
|
||||
</span>
|
||||
<span className="hidden md:block" />
|
||||
<span />
|
||||
</button>
|
||||
<AnimatePresence initial={false}>
|
||||
{open && (
|
||||
@@ -117,23 +164,29 @@ export default function ElectricView() {
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="overflow-hidden bg-slate-50/50"
|
||||
className="overflow-hidden"
|
||||
>
|
||||
{m.rows.map(d => (
|
||||
{m.rows.map(d => {
|
||||
const isAbnormal = Math.abs(d.chainPct) >= 0.3;
|
||||
const abnormalBg = isAbnormal
|
||||
? d.chainPct > 0 ? 'bg-emerald-50/40' : 'bg-red-50/40'
|
||||
: 'bg-slate-50/50';
|
||||
return (
|
||||
<div
|
||||
key={d.date}
|
||||
className="grid grid-cols-[1fr_auto_auto] md:grid-cols-[1fr_120px_140px_120px] gap-2 px-3 py-2 pl-9 border-t border-slate-100"
|
||||
className={`grid grid-cols-[1fr_auto_auto_auto] md:grid-cols-[1fr_120px_140px_120px] gap-2 px-3 py-2 pl-9 border-t border-slate-100 ${abnormalBg}`}
|
||||
>
|
||||
<span className="text-[12px] text-slate-600">{d.date}</span>
|
||||
<span className="text-[12px] text-slate-600">{d.date.slice(5)}</span>
|
||||
<span className="text-right text-[12px] text-slate-700 font-bold tabular-nums">
|
||||
{d.kwh.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}
|
||||
</span>
|
||||
<span className="text-right text-[12px] text-slate-700 font-bold tabular-nums">
|
||||
{d.fee.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}
|
||||
</span>
|
||||
<span className="text-right hidden md:block"><TrendBadge value={d.chainPct} /></span>
|
||||
<span className="text-right"><TrendBadge value={d.chainPct} /></span>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
Reference in New Issue
Block a user