diff --git a/docs/bi-refactor-roadmap.md b/docs/bi-refactor-roadmap.md index 3ecd1f3..e89e106 100644 --- a/docs/bi-refactor-roadmap.md +++ b/docs/bi-refactor-roadmap.md @@ -161,6 +161,7 @@ SUM(fee) / NULLIF(SUM(kwh), 0) - 已完成里程考核归因、车辆详情和每日汇报跳转。 - 已完成氢能站点、客户下钻代码。 - 已完成氢能每日趋势到加氢订单的 URL 下钻、分页和结构化故障状态;待数据源恢复后验收真实明细。 +- 已为电能总览与氢能每日趋势柱图统一 Enter/Space 键盘下钻;氢能有效日期柱提供可读名称、焦点和选中反馈。 - 已完成电能全部车辆、日期和订单下钻。 - 已完成 ETC 通行记录与结算账单下钻代码、筛选、分页和未同步空状态;待首次真实同步后验收真实数据。 diff --git a/src/modules/energy/ElectricOverview.tsx b/src/modules/energy/ElectricOverview.tsx index 00e48fb..94fcd6c 100644 --- a/src/modules/energy/ElectricOverview.tsx +++ b/src/modules/energy/ElectricOverview.tsx @@ -5,6 +5,7 @@ import { fetchElectricOverview, type ElectricOverviewResponse } from './api'; import RotatingFooterHint from '../../components/RotatingFooterHint'; import { ErrorState, LoadingState, MetricTile, SurfaceCard } from '../../components/ui/surface'; import { buildElectricDrillUrl } from './electric-drill-context'; +import { activateDrillOnKeyDown } from './drill-interaction'; function fmtYuan(yuan: number) { return `¥${yuan.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}`; @@ -141,12 +142,7 @@ export default function ElectricOverview() { tabIndex={0} aria-label={`${item.date},充电费用 ${fmtYuan(item.fee)},查看全部车辆订单`} onClick={() => openDay(item.date)} - onKeyDown={event => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - openDay(item.date); - } - }} + onKeyDown={event => activateDrillOnKeyDown(event, () => openDay(item.date))} /> ))} diff --git a/src/modules/energy/HydrogenDaily.tsx b/src/modules/energy/HydrogenDaily.tsx index 41ffd78..1642789 100644 --- a/src/modules/energy/HydrogenDaily.tsx +++ b/src/modules/energy/HydrogenDaily.tsx @@ -15,6 +15,7 @@ import { import HydrogenOrders from './HydrogenOrders'; import AnalysisDateFilters from './AnalysisDateFilters'; import AnalysisScopeSwitch, { type AnalysisScopeOption } from './AnalysisScopeSwitch'; +import { activateDrillOnKeyDown } from './drill-interaction'; import { dateRangeModeLabel, getQuickDateRange, @@ -88,6 +89,11 @@ export default function HydrogenDaily() { const avgKg = activeDays > 0 ? totalKg / activeDays : 0; const scopeLabel = dateRangeModeLabel(pick); const rangeText = `${effectiveRange.start} 至 ${effectiveRange.end}`; + const orderScopeLabel = selectedCustomerName + ? `客户 ${selectedCustomerName}` + : selectedStationName + ? `站点 ${selectedStationName}` + : vehicleScope === 'external' ? '外部车辆' : '羚牛车辆'; const peakDay = trendData.reduce((best, item) => (!best || item.totalKg > best.totalKg ? item : best), null); const lowDay = trendData .filter(item => item.totalKg > 0) @@ -346,17 +352,23 @@ export default function HydrogenDaily() { label={{ value: '均值', position: 'right', fill: '#d97706', fontSize: 10, fontWeight: 700 }} /> )} - { - const date = String(entry.payload?.date ?? ''); - if (date && Number(entry.payload?.totalKg) > 0) openOrders(date); - }} - > - {trendData.map((_, i) => ( - + + {trendData.map(item => ( + 0 ? 'cursor-pointer outline-none focus:stroke-blue-700 focus:stroke-2' : undefined} + role={item.totalKg > 0 ? 'button' : undefined} + tabIndex={item.totalKg > 0 ? 0 : -1} + aria-pressed={item.totalKg > 0 ? selectedDate === item.date : undefined} + aria-label={`${item.date},加氢量 ${item.totalKg.toLocaleString('zh-CN', { maximumFractionDigits: 2 })} Kg${item.totalKg > 0 ? `,查看${orderScopeLabel}加氢订单` : ',无可下钻订单'}`} + onClick={item.totalKg > 0 ? () => openOrders(item.date) : undefined} + onKeyDown={item.totalKg > 0 + ? event => activateDrillOnKeyDown(event, () => openOrders(item.date)) + : undefined} + /> ))} diff --git a/src/modules/energy/drill-interaction.test.ts b/src/modules/energy/drill-interaction.test.ts new file mode 100644 index 0000000..2ffbba6 --- /dev/null +++ b/src/modules/energy/drill-interaction.test.ts @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { activateDrillOnKeyDown } from './drill-interaction.js'; + +test('activates drilldown with Enter and Space only', () => { + for (const key of ['Enter', ' ']) { + let prevented = false; + let activations = 0; + const handled = activateDrillOnKeyDown( + { key, preventDefault: () => { prevented = true; } }, + () => { activations += 1; }, + ); + assert.equal(handled, true); + assert.equal(prevented, true); + assert.equal(activations, 1); + } + + let activations = 0; + const handled = activateDrillOnKeyDown( + { key: 'ArrowRight', preventDefault: () => assert.fail('must not prevent navigation keys') }, + () => { activations += 1; }, + ); + assert.equal(handled, false); + assert.equal(activations, 0); +}); diff --git a/src/modules/energy/drill-interaction.ts b/src/modules/energy/drill-interaction.ts new file mode 100644 index 0000000..7c5730f --- /dev/null +++ b/src/modules/energy/drill-interaction.ts @@ -0,0 +1,11 @@ +export interface DrillKeyEvent { + key: string; + preventDefault: () => void; +} + +export function activateDrillOnKeyDown(event: DrillKeyEvent, activate: () => void): boolean { + if (event.key !== 'Enter' && event.key !== ' ') return false; + event.preventDefault(); + activate(); + return true; +}