refactor(mileage): use fixed-column table on mobile

This commit is contained in:
lingniu
2026-07-16 13:07:43 +08:00
parent 4877c3fe8f
commit 6d6c9ce534
3 changed files with 20 additions and 49 deletions

View File

@@ -115,30 +115,17 @@ test('removes the previous mileage scope while a new date range is loading', asy
expect(screen.getAllByText('2026-07-10 至 2026-07-16').length).toBeGreaterThan(0);
});
test('mounts only the mobile mileage cards and removes the viewport listener', async () => {
test('uses one responsive mileage matrix without viewport listeners', async () => {
prepareData();
const addEventListener = vi.fn();
const removeEventListener = vi.fn();
vi.spyOn(window, 'matchMedia').mockReturnValue({
matches: true,
media: '(max-width: 680px)',
onchange: null,
addEventListener,
removeEventListener,
addListener: vi.fn(),
removeListener: vi.fn(),
dispatchEvent: vi.fn(() => false)
} as unknown as MediaQueryList);
const view = renderPage('/statistics?vins=LTEST000000000001&dateFrom=2026-07-13&dateTo=2026-07-14');
expect(await screen.findByText('车辆每日里程')).toBeInTheDocument();
await waitFor(() => expect(view.container.querySelectorAll('.v2-mileage-mobile-list article')).toHaveLength(1));
expect(view.container.querySelector('.v2-mileage-table-wrap')).not.toBeInTheDocument();
expect(view.container.querySelectorAll('.v2-mileage-mobile-days > div')).toHaveLength(2);
expect(addEventListener).toHaveBeenCalledWith('change', expect.any(Function));
view.unmount();
expect(removeEventListener).toHaveBeenCalledWith('change', expect.any(Function));
await waitFor(() => expect(view.container.querySelectorAll('.v2-mileage-table tbody tr')).toHaveLength(1));
expect(view.container.querySelector('.v2-mileage-table-wrap')).toBeInTheDocument();
expect(view.container.querySelectorAll('.v2-mileage-table th.is-date')).toHaveLength(2);
expect(view.container.querySelector('.v2-mileage-table th.is-plate')).toHaveTextContent('车牌');
expect(view.container.querySelector('.v2-mileage-table th.is-total')).toHaveTextContent('区间总里程');
expect(view.container.querySelector('.v2-mileage-mobile-list')).not.toBeInTheDocument();
});
test('supports searching and selecting license plates before querying exact VINs', async () => {

View File

@@ -15,7 +15,6 @@ const MAX_SELECTED_VEHICLES = 20;
const PAGE_SIZE = 20;
const EXPORT_VEHICLE_PAGE_SIZE = 2_000;
const EXPORT_VIN_BATCH_SIZE = 50;
const MOBILE_MILEAGE_QUERY = '(max-width: 680px)';
const MAX_MILEAGE_RANGE_DAYS = 366;
type VehicleOption = Pick<VehicleRow, 'vin' | 'plate'>;
@@ -223,18 +222,6 @@ function SummaryRail({ data, criteria, fleetTotal, loading }: { data?: MileageSt
type VehicleMileageMatrix = VehicleOption & { days: Map<string, number>; sources: Map<string, string>; totalMileageKm: number };
function useMobileMileageLayout() {
const [mobile, setMobile] = useState(() => window.matchMedia(MOBILE_MILEAGE_QUERY).matches);
useEffect(() => {
const media = window.matchMedia(MOBILE_MILEAGE_QUERY);
const update = () => setMobile(media.matches);
media.addEventListener('change', update);
update();
return () => media.removeEventListener('change', update);
}, []);
return mobile;
}
function rangeDates(dateFrom: string, dateTo: string) {
const dates: string[] = [];
const cursor = new Date(`${dateFrom}T00:00:00Z`);
@@ -252,8 +239,6 @@ function dateLabel(date: string) {
}
function MileageTable({ rows, dates }: { rows: VehicleMileageMatrix[]; dates: string[] }) {
const mobile = useMobileMileageLayout();
if (mobile) return <div className="v2-mileage-mobile-list">{rows.map((row) => <article key={row.vin}><header><div><strong>{row.plate || '未绑定'}</strong><span>{row.vin}</span></div><b>{formatKm(row.totalMileageKm)} km<small></small></b></header><div className="v2-mileage-mobile-days" aria-label={`${row.plate || row.vin}每日里程,左右滑动查看 ${dates.length}`}>{dates.map((date) => { const source = row.sources.get(date); return <div key={date} aria-label={`${dateLabel(date)}${row.days.has(date) ? `${formatKm(row.days.get(date))}公里` : '无数据'}${source ? `,来源${source}` : ''}`}><time>{dateLabel(date)}</time><strong>{row.days.has(date) ? `${formatKm(row.days.get(date))} km` : '—'}</strong>{source ? <small>{source === 'YUTONG_MQTT' ? 'YUTONG' : source}</small> : null}</div>; })}</div></article>)}</div>;
let maxDailyMileage = 1;
for (const row of rows) {
for (const mileage of row.days.values()) maxDailyMileage = Math.max(maxDailyMileage, mileage);

View File

@@ -1332,8 +1332,8 @@ button, a { -webkit-tap-highlight-color: transparent; }
.v2-mileage-result-actions > button { height: 34px; flex: 0 0 auto; }
.v2-mileage-export-progress { align-items: stretch; flex-direction: column; gap: 7px; padding: 9px 12px; }
.v2-mileage-export-progress > span { min-width: 0; }
.v2-mileage-table-wrap { display: none; }
.v2-mileage-mobile-list { display: flex; flex-direction: column; }
.v2-mileage-table-wrap { display: block; }
.v2-mileage-mobile-list { display: none; }
.v2-mileage-mobile-list article { border-bottom: 1px solid #e9eef5; padding: 14px 12px; }
.v2-mileage-mobile-list article > header { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.v2-mileage-mobile-list article > header div { min-width: 0; }
@@ -1934,16 +1934,15 @@ button, a { -webkit-tap-highlight-color: transparent; }
.v2-mileage-results > header .v2-mileage-result-actions { gap: 6px; }
.v2-mileage-results > header .v2-mileage-result-actions em { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.v2-mileage-result-actions > button { height: 32px; padding: 0 9px; font-size: 10px; }
.v2-mileage-mobile-list { gap: 5px; background: #f4f7fb; padding: 5px; }
.v2-mileage-mobile-list article { overflow: hidden; border: 1px solid #dfe6ef; border-radius: 8px; background: #fff; padding: 9px 10px 8px; box-shadow: 0 1px 4px rgba(23,39,62,.03); }
.v2-mileage-mobile-list article > header { align-items: center; }
.v2-mileage-mobile-list article > header strong { font-size: 14px; }
.v2-mileage-mobile-list article > header span { margin-top: 1px; font-size: 8px; }
.v2-mileage-mobile-list article > header b { color: #135fcb; font-size: 17px; letter-spacing: -.025em; }
.v2-mileage-mobile-list article > header b small { margin-top: 1px; font-size: 8px; }
.v2-mileage-mobile-days { grid-auto-columns: 70px; gap: 4px; margin-top: 7px; border-top: 1px solid #edf1f5; padding-top: 6px; padding-bottom: 1px; scroll-snap-type: x proximity; }
.v2-mileage-mobile-days > div { min-height: 38px; border: 0; border-radius: 6px; background: #f6f8fb; padding: 5px 7px; scroll-snap-align: start; }
.v2-mileage-mobile-days time { font-size: 8px; }
.v2-mileage-mobile-days strong { margin-top: 3px; color: #26364b; font-size: 10px; }
.v2-mileage-mobile-days small { display: none; }
.v2-mileage-table-wrap { display: block; width: 100%; max-width: 100%; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; scrollbar-width: thin; }
.v2-mileage-table { width: max-content; min-width: 100%; font-size: 10px; }
.v2-mileage-table th { height: 38px; padding: 0 8px; font-size: 9px; }
.v2-mileage-table td { height: 44px; padding: 0 8px; font-size: 10px; }
.v2-mileage-table .is-vin { display: none; }
.v2-mileage-table .is-plate { left: 0; width: 92px; min-width: 92px; max-width: 92px; border-right: 1px solid #d8e0ea; box-shadow: 5px 0 10px rgba(15,23,42,.035); }
.v2-mileage-table td.is-plate strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.v2-mileage-table .is-date { width: 76px; min-width: 76px; }
.v2-mileage-table .is-total { width: 104px; min-width: 104px; padding-right: 10px; }
.v2-mileage-table .is-daily { font-size: 10px; }
.v2-mileage-table .is-period { font-size: 11px; }
}