feat(mileage): compact mobile results and add date presets
This commit is contained in:
@@ -36,11 +36,15 @@ function localDate(value = new Date()) {
|
||||
return new Date(value.getTime() - offset).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function defaultWindow(days = 30) {
|
||||
const end = new Date();
|
||||
function defaultWindow(days = 7, endOffsetDays = 0) {
|
||||
const end = new Date(Date.now() - endOffsetDays * DAY);
|
||||
return { dateFrom: localDate(new Date(end.getTime() - (days - 1) * DAY)), dateTo: localDate(end) };
|
||||
}
|
||||
|
||||
function isSameRange(criteria: Pick<Criteria, 'dateFrom' | 'dateTo'>, range: { dateFrom: string; dateTo: string }) {
|
||||
return criteria.dateFrom === range.dateFrom && criteria.dateTo === range.dateTo;
|
||||
}
|
||||
|
||||
function formatKm(value?: number) {
|
||||
if (value == null || !Number.isFinite(value)) return '—';
|
||||
return formatZhNumber(value, 1);
|
||||
@@ -74,7 +78,7 @@ function mileageParams(criteria: Criteria, offset = 0) {
|
||||
}
|
||||
|
||||
function initialCriteria(searchParams: URLSearchParams): Criteria {
|
||||
const defaults = defaultWindow(30);
|
||||
const defaults = defaultWindow();
|
||||
const vins = (searchParams.get('vins') ?? '').split(',').map((vin) => vin.trim()).filter(Boolean).slice(0, MAX_SELECTED_VEHICLES);
|
||||
const requestedProtocols = (searchParams.get('protocols') ?? '').split(',').filter((protocol): protocol is MileageProtocol => DEFAULT_SOURCES.some((source) => source.protocol === protocol));
|
||||
let sources = DEFAULT_SOURCES.map((source) => ({ ...source }));
|
||||
@@ -249,7 +253,7 @@ 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-day-heading"><span>每日里程</span><em>左右滑动查看 {dates.length} 天</em></div><div className="v2-mileage-mobile-days">{dates.map((date) => { const source = row.sources.get(date); return <div key={date}><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>;
|
||||
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);
|
||||
@@ -337,7 +341,12 @@ export default function StatisticsPage() {
|
||||
if (error) return;
|
||||
setPage(1); setExportFeedback(''); setCriteria(draft); setSearchParams(mileageParams(draft, -1), { replace: true }); setFiltersCollapsed(true);
|
||||
};
|
||||
const setDays = (days: number) => { const range = defaultWindow(days); const next = { ...draft, ...range }; setPage(1); setExportFeedback(''); setValidationError(''); setDraft(next); setCriteria(next); setSearchParams(mileageParams(next, -1), { replace: true }); };
|
||||
const setRange = (range: { dateFrom: string; dateTo: string }) => { const next = { ...draft, ...range }; setPage(1); setExportFeedback(''); setValidationError(''); setDraft(next); setCriteria(next); setSearchParams(mileageParams(next, -1), { replace: true }); };
|
||||
const todayRange = defaultWindow(1);
|
||||
const yesterdayRange = defaultWindow(1, 1);
|
||||
const sevenDayRange = defaultWindow(7);
|
||||
const thirtyDayRange = defaultWindow(30);
|
||||
const ninetyDayRange = defaultWindow(90);
|
||||
const refreshing = statistics.isFetching || mileage.isFetching || fleetVehicles.isFetching;
|
||||
const resultsLoading = fleetVehicles.isLoading || mileage.isLoading || mileage.isPlaceholderData;
|
||||
const exportPercent = exportProgress?.total
|
||||
@@ -432,7 +441,7 @@ export default function StatisticsPage() {
|
||||
<label><span>结束日期</span><input type="date" value={draft.dateTo} min={draft.dateFrom} onChange={(event) => setDraft((current) => ({ ...current, dateTo: event.target.value }))} /></label>
|
||||
<button className="v2-primary-button" type="submit">查询</button>
|
||||
<SourceStrategy value={draft.sources} onChange={(sources) => setDraft((current) => ({ ...current, sources }))} />
|
||||
<div className="v2-mileage-ranges"><span>快捷范围</span><button className={inclusiveDays(draft.dateFrom, draft.dateTo) === 7 ? 'is-active' : ''} type="button" onClick={() => setDays(7)}>近 7 天</button><button className={inclusiveDays(draft.dateFrom, draft.dateTo) === 30 ? 'is-active' : ''} type="button" onClick={() => setDays(30)}>近 30 天</button><button className={inclusiveDays(draft.dateFrom, draft.dateTo) === 90 ? 'is-active' : ''} type="button" onClick={() => setDays(90)}>近 90 天</button></div>
|
||||
<div className="v2-mileage-ranges"><span>快捷范围</span><button className={isSameRange(draft, todayRange) ? 'is-active' : ''} type="button" onClick={() => setRange(todayRange)}>今天</button><button className={isSameRange(draft, yesterdayRange) ? 'is-active' : ''} type="button" onClick={() => setRange(yesterdayRange)}>昨天</button><button className={isSameRange(draft, sevenDayRange) ? 'is-active' : ''} type="button" onClick={() => setRange(sevenDayRange)}>近 7 天</button><button className={isSameRange(draft, thirtyDayRange) ? 'is-active' : ''} type="button" onClick={() => setRange(thirtyDayRange)}>近 30 天</button><button className={isSameRange(draft, ninetyDayRange) ? 'is-active' : ''} type="button" onClick={() => setRange(ninetyDayRange)}>近 90 天</button></div>
|
||||
</form>
|
||||
{validationError || criteriaError ? <p className="v2-mileage-validation" role="alert">{validationError || criteriaError}</p> : null}
|
||||
<SummaryRail data={statistics.data} criteria={criteria} fleetTotal={fleetVehicles.data?.total} loading={statistics.isLoading} />
|
||||
|
||||
Reference in New Issue
Block a user