fix(energy): 顶部双 sticky 间隙泄露 + 加氢站名单价完整显示
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

问题 1:原本「氢能/电能/ETC」与「每日/总览」是两个独立的 sticky
元素,分别 top:0 / top:[58px],中间 gap-3 +位置不精确导致滚动时
图表内容从 14px 缝隙里穿过。

修复:
- 把 HydrogenView 内部的 sub-tab 状态提到 EnergyModule
- top tab + 子 tab 合并到「同一张白色 rounded-2xl 卡片」里,无内部间隙
- 外层 sticky 容器 frosted glass:bg-[#F8F9FB]/85 + backdrop-blur-md
  -mx -mt 扩到页面边,消除左右上的微缝
- HydrogenView 改为受控组件(接收 sub prop)

问题 2:站点行 mobile 上 name + ' · 价格' 共用一格还 truncate,
导致长名称(广州新锋交通联新…/上海浦江加氢站…)截断、单价不可见。

修复:
- 行改为「站名换行 + 下方单价 chip」纵向排列
- 单价用 amber 小徽章「单价 X 元/Kg」,不再 inline 跟着名字
- name 用 break-words 允许折行,items-start 顶部对齐
- 单价为 0(免费/赠送)时不显示徽章,desktop 列里显示「—」

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-30 15:08:22 +08:00
parent e0183986ee
commit d24ce55a59
3 changed files with 74 additions and 59 deletions

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import { Fuel, BatteryCharging, Receipt } from 'lucide-react';
import { Fuel, BatteryCharging, Receipt, LayoutDashboard, CalendarDays } from 'lucide-react';
import { motion } from 'motion/react';
import HydrogenView from './HydrogenView';
import HydrogenView, { type HydrogenSubTab } from './HydrogenView';
import ElectricView from './ElectricView';
import ETCView from './ETCView';
@@ -13,31 +13,65 @@ const TABS: { key: TopTab; label: string; icon: typeof Fuel }[] = [
{ key: 'etc', label: 'ETC', icon: Receipt },
];
const HYDROGEN_SUB_TABS: { id: HydrogenSubTab; label: string; icon: typeof LayoutDashboard }[] = [
{ id: 'daily', label: '每日', icon: CalendarDays },
{ id: 'overview', label: '总览', icon: LayoutDashboard },
];
export default function EnergyModule() {
const [activeTab, setActiveTab] = useState<TopTab>('hydrogen');
const [hydroSub, setHydroSub] = useState<HydrogenSubTab>('daily');
return (
<div className="min-h-screen bg-[#F8F9FB] text-gray-800 font-sans p-3 md:p-6 relative" style={{ overflowX: 'clip' }}>
<div className="max-w-6xl mx-auto flex flex-col gap-3 pb-16 landscape:pb-0 landscape:h-full landscape:flex-1 landscape:overflow-hidden">
<div className="bg-white px-4 py-2 rounded-2xl border border-slate-100 shadow-sm flex items-center gap-6 sticky top-0 z-30">
{TABS.map(tab => {
const Icon = tab.icon;
const active = activeTab === tab.key;
return (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`flex items-center gap-2 py-1 transition-all relative ${active ? 'text-blue-600' : 'text-slate-400'}`}
>
<Icon size={14} />
<span className="text-[11px] font-bold">{tab.label}</span>
{active && (
<motion.div layoutId="activeEnergyTopTab" className="absolute -bottom-2 left-0 right-0 h-0.5 bg-blue-600 rounded-full" />
)}
</button>
);
})}
{/* 统一 sticky 头部top tab + (氢能时) 子 tab同一张卡片无间隙 */}
<div className="sticky top-0 z-30 -mx-3 md:-mx-6 px-3 md:px-6 -mt-3 md:-mt-6 pt-3 md:pt-6 pb-2 bg-[#F8F9FB]/85 backdrop-blur-md">
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
{/* 顶部 tab氢能 / 电能 / ETC */}
<div className={`px-4 py-2 flex items-center gap-6 ${activeTab === 'hydrogen' ? 'border-b border-slate-50' : ''}`}>
{TABS.map(tab => {
const Icon = tab.icon;
const active = activeTab === tab.key;
return (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`flex items-center gap-2 py-1 transition-colors relative ${active ? 'text-blue-600' : 'text-slate-400 hover:text-slate-600'}`}
>
<Icon size={14} />
<span className="text-[11px] font-bold">{tab.label}</span>
{active && (
<motion.div layoutId="activeEnergyTopTab" className="absolute -bottom-2 left-0 right-0 h-0.5 bg-blue-600 rounded-full" />
)}
</button>
);
})}
</div>
{/* 子 tab仅氢能时显示 */}
{activeTab === 'hydrogen' && (
<div className="p-1 flex gap-1">
{HYDROGEN_SUB_TABS.map(({ id, label, icon: Icon }) => {
const active = hydroSub === id;
return (
<button
key={id}
onClick={() => setHydroSub(id)}
className={`flex-1 flex items-center justify-center gap-1.5 rounded-xl py-1.5 text-[12px] font-bold transition-all ${
active ? 'bg-blue-50 text-blue-600' : 'text-slate-400 hover:bg-slate-50'
}`}
>
<Icon size={14} />
<span>{label}</span>
</button>
);
})}
</div>
)}
</div>
</div>
{activeTab === 'hydrogen' && <HydrogenView />}
{activeTab === 'hydrogen' && <HydrogenView sub={hydroSub} />}
{activeTab === 'electric' && <ElectricView />}
{activeTab === 'etc' && <ETCView />}
</div>

View File

@@ -191,13 +191,19 @@ export default function HydrogenDaily() {
{r.stations.map(s => (
<div
key={s.name}
className="grid grid-cols-[minmax(0,1fr)_120px_88px] md:grid-cols-[minmax(0,1fr)_140px_140px_120px] gap-3 px-3 py-2 pl-9 border-t border-slate-100 first:border-t-0"
className="grid grid-cols-[minmax(0,1fr)_120px_88px] md:grid-cols-[minmax(0,1fr)_140px_140px_120px] gap-3 px-3 py-2.5 pl-9 border-t border-slate-100 first:border-t-0 items-start"
>
<span className="text-[12px] text-slate-600 truncate">
{s.name}
<span className="md:hidden text-slate-400 text-[10px]"> · {s.pricePerKg}</span>
</span>
<span className="hidden md:block text-right text-[12px] text-slate-500 font-bold tabular-nums">{s.pricePerKg}</span>
<div className="min-w-0">
<div className="text-[12px] text-slate-700 font-medium leading-snug break-words">
{s.name}
</div>
{s.pricePerKg > 0 && (
<div className="md:hidden mt-1 inline-flex items-center gap-1 text-[10px] text-amber-600 bg-amber-50 px-1.5 py-0.5 rounded font-bold">
{s.pricePerKg} /Kg
</div>
)}
</div>
<span className="hidden md:block text-right text-[12px] text-slate-500 font-bold tabular-nums">{s.pricePerKg > 0 ? s.pricePerKg : '—'}</span>
<span className="text-right text-[12px] text-slate-700 font-bold tabular-nums">
{s.kg.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}
</span>

View File

@@ -1,37 +1,12 @@
import { useState } from 'react';
import { LayoutDashboard, CalendarDays } from 'lucide-react';
import HydrogenOverview from './HydrogenOverview';
import HydrogenDaily from './HydrogenDaily';
type SubTab = 'daily' | 'overview';
export type HydrogenSubTab = 'daily' | 'overview';
const SUB_TABS: Array<{ id: SubTab; label: string; icon: typeof LayoutDashboard }> = [
{ id: 'daily', label: '每日', icon: CalendarDays },
{ id: 'overview', label: '总览', icon: LayoutDashboard },
];
export default function HydrogenView() {
const [sub, setSub] = useState<SubTab>('daily');
return (
<>
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-1 sticky top-[58px] z-20 flex gap-1">
{SUB_TABS.map(({ id, label, icon: Icon }) => {
const active = sub === id;
return (
<button
key={id}
onClick={() => setSub(id)}
className={`flex-1 flex items-center justify-center gap-1.5 rounded-xl py-1.5 text-[12px] font-bold transition-all ${
active ? 'bg-blue-50 text-blue-600' : 'text-slate-400 hover:bg-slate-50'
}`}
>
<Icon size={14} />
<span>{label}</span>
</button>
);
})}
</div>
{sub === 'overview' ? <HydrogenOverview /> : <HydrogenDaily />}
</>
);
interface Props {
sub: HydrogenSubTab;
}
export default function HydrogenView({ sub }: Props) {
return sub === 'overview' ? <HydrogenOverview /> : <HydrogenDaily />;
}