feat: focus ETC metric drilldowns
This commit is contained in:
@@ -89,7 +89,11 @@ export default function ETCDetails({
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="overflow-hidden rounded-lg border border-slate-100 bg-white shadow-sm">
|
||||
<section
|
||||
id="etc-details"
|
||||
tabIndex={-1}
|
||||
className="scroll-mt-3 overflow-hidden rounded-lg border border-slate-100 bg-white shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
>
|
||||
<header className="flex flex-col gap-3 border-b border-slate-100 px-3 py-3 md:px-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { CircleCheck, CircleX, Clock3, Database, ReceiptText, RefreshCw, Route, Truck } from 'lucide-react';
|
||||
import { fetchEtcOverview } from './api';
|
||||
import type { EtcOverviewResponse } from './types';
|
||||
@@ -23,6 +24,31 @@ function syncStatusLabel(status: EtcOverviewResponse['integration']['lastSyncSta
|
||||
return '状态未知';
|
||||
}
|
||||
|
||||
function DrilldownMetricButton({
|
||||
label,
|
||||
title,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
title: string;
|
||||
onClick: () => void;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-controls="etc-details"
|
||||
aria-label={label}
|
||||
className="min-w-0 rounded-2xl text-left outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ETCView() {
|
||||
const [data, setData] = useState<EtcOverviewResponse | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -47,6 +73,18 @@ export default function ETCView() {
|
||||
);
|
||||
}, [commitDetailContext, detailContext]);
|
||||
|
||||
const openDetailView = useCallback((next: EtcDetailView) => {
|
||||
selectDetailView(next);
|
||||
window.requestAnimationFrame(() => {
|
||||
const details = document.getElementById('etc-details');
|
||||
details?.focus({ preventScroll: true });
|
||||
details?.scrollIntoView({
|
||||
behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
});
|
||||
}, [selectDetailView]);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePopState = () => setDetailContext(parseEtcDrillContext(window.location.search));
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
@@ -104,10 +142,18 @@ export default function ETCView() {
|
||||
</SurfaceCard>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
<button type="button" onClick={() => selectDetailView('records')} className="text-left" title="查看通行记录"><MetricTile icon={ReceiptText} label="ETC 总费用" value={fmtMoney(kpi.totalAmount)} helper={`通行费 ${fmtMoney(kpi.tollAmount)} · 服务费 ${fmtMoney(kpi.serviceFee)}`} /></button>
|
||||
<button type="button" onClick={() => selectDetailView('records')} className="text-left" title="查看通行记录"><MetricTile icon={Route} label="通行次数" value={kpi.passageCount} unit="次" helper={kpi.latestTransactionTime ? `最新 ${kpi.latestTransactionTime}` : '暂无通行记录'} tone="emerald" /></button>
|
||||
<button type="button" onClick={() => selectDetailView('records')} className="text-left" title="查看通行记录"><MetricTile icon={Truck} label="通行车辆" value={kpi.vehicleCount} unit="辆" helper="按车牌去重" tone="amber" /></button>
|
||||
<button type="button" onClick={() => selectDetailView('bills')} className="text-left" title="查看结算账单"><MetricTile icon={Database} label="账单应收" value={fmtMoney(kpi.receivableAmount)} helper={`${kpi.billCount} 账单 · 已收 ${fmtMoney(kpi.paidAmount)} · ${collectionRate.toFixed(1)}%`} tone="slate" /></button>
|
||||
<DrilldownMetricButton label={`查看 ETC 通行记录,总费用 ${fmtMoney(kpi.totalAmount)}`} title="查看通行记录" onClick={() => openDetailView('records')}>
|
||||
<MetricTile icon={ReceiptText} label="ETC 总费用" value={fmtMoney(kpi.totalAmount)} helper={`通行费 ${fmtMoney(kpi.tollAmount)} · 服务费 ${fmtMoney(kpi.serviceFee)}`} />
|
||||
</DrilldownMetricButton>
|
||||
<DrilldownMetricButton label={`查看 ETC 通行记录,共 ${kpi.passageCount} 次`} title="查看通行记录" onClick={() => openDetailView('records')}>
|
||||
<MetricTile icon={Route} label="通行次数" value={kpi.passageCount} unit="次" helper={kpi.latestTransactionTime ? `最新 ${kpi.latestTransactionTime}` : '暂无通行记录'} tone="emerald" />
|
||||
</DrilldownMetricButton>
|
||||
<DrilldownMetricButton label={`查看 ETC 通行记录,共 ${kpi.vehicleCount} 辆车`} title="查看通行记录" onClick={() => openDetailView('records')}>
|
||||
<MetricTile icon={Truck} label="通行车辆" value={kpi.vehicleCount} unit="辆" helper="按车牌去重" tone="amber" />
|
||||
</DrilldownMetricButton>
|
||||
<DrilldownMetricButton label={`查看 ETC 结算账单,应收 ${fmtMoney(kpi.receivableAmount)}`} title="查看结算账单" onClick={() => openDetailView('bills')}>
|
||||
<MetricTile icon={Database} label="账单应收" value={fmtMoney(kpi.receivableAmount)} helper={`${kpi.billCount} 账单 · 已收 ${fmtMoney(kpi.paidAmount)} · ${collectionRate.toFixed(1)}%`} tone="slate" />
|
||||
</DrilldownMetricButton>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
|
||||
Reference in New Issue
Block a user