refactor(ui): polish semi mileage discovery

This commit is contained in:
lingniu
2026-07-19 16:25:48 +08:00
parent fa60bbb2c8
commit 40c210a192
3 changed files with 116 additions and 6 deletions

View File

@@ -122,6 +122,8 @@ test('defaults to seven days and supports today and yesterday shortcuts', async
await waitFor(() => expect(mocks.mileageStatistics).toHaveBeenCalled()); await waitFor(() => expect(mocks.mileageStatistics).toHaveBeenCalled());
expect(mocks.mileageStatistics.mock.calls[0][0].get('dateFrom')).toBe(sevenDaysAgo); expect(mocks.mileageStatistics.mock.calls[0][0].get('dateFrom')).toBe(sevenDaysAgo);
expect(mocks.mileageStatistics.mock.calls[0][0].get('dateTo')).toBe(today); expect(mocks.mileageStatistics.mock.calls[0][0].get('dateTo')).toBe(today);
expect(screen.getByRole('button', { name: '近 7 天' })).toHaveAttribute('aria-pressed', 'true');
expect(screen.getByRole('button', { name: '今天' })).toHaveAttribute('aria-pressed', 'false');
fireEvent.click(screen.getByRole('button', { name: '昨天' })); fireEvent.click(screen.getByRole('button', { name: '昨天' }));
await waitFor(() => { await waitFor(() => {
@@ -129,6 +131,7 @@ test('defaults to seven days and supports today and yesterday shortcuts', async
expect(params.get('dateFrom')).toBe(yesterday); expect(params.get('dateFrom')).toBe(yesterday);
expect(params.get('dateTo')).toBe(yesterday); expect(params.get('dateTo')).toBe(yesterday);
}); });
expect(screen.getByRole('button', { name: '昨天' })).toHaveAttribute('aria-pressed', 'true');
fireEvent.click(screen.getByRole('button', { name: '今天' })); fireEvent.click(screen.getByRole('button', { name: '今天' }));
await waitFor(() => { await waitFor(() => {
@@ -136,6 +139,7 @@ test('defaults to seven days and supports today and yesterday shortcuts', async
expect(params.get('dateFrom')).toBe(today); expect(params.get('dateFrom')).toBe(today);
expect(params.get('dateTo')).toBe(today); expect(params.get('dateTo')).toBe(today);
}); });
expect(screen.getByRole('button', { name: '今天' })).toHaveAttribute('aria-pressed', 'true');
}); });
test('preserves the exact monitor return after a mileage date shortcut', async () => { test('preserves the exact monitor return after a mileage date shortcut', async () => {
@@ -280,7 +284,7 @@ test('lets users disable mileage sources and persists the source priority', asyn
expect(screen.getByText('GPS 里程')).toBeInTheDocument(); expect(screen.getByText('GPS 里程')).toBeInTheDocument();
expect(screen.getByText('GPS 里程').closest('.semi-tag')).toBeInTheDocument(); expect(screen.getByText('GPS 里程').closest('.semi-tag')).toBeInTheDocument();
expect(screen.getAllByText('仪表盘里程')).toHaveLength(3); expect(screen.getAllByText('仪表盘里程')).toHaveLength(3);
expect(screen.getByRole('button', { name: /数据源.*GB32960 优先.*3\/3/ })).toHaveAttribute('title', '当前优先:国标 GB32960'); expect(screen.getByRole('button', { name: /数据源策略.*GB32960 优先.*3\/3/ })).toHaveAttribute('title', '当前优先:国标 GB32960');
fireEvent.click(screen.getByRole('switch', { name: '禁用 国标 GB32960' })); fireEvent.click(screen.getByRole('switch', { name: '禁用 国标 GB32960' }));
fireEvent.click(screen.getByRole('button', { name: '上移 宇通 MQTT' })); fireEvent.click(screen.getByRole('button', { name: '上移 宇通 MQTT' }));
fireEvent.click(screen.getByRole('button', { name: '查询' })); fireEvent.click(screen.getByRole('button', { name: '查询' }));

View File

@@ -59,6 +59,34 @@ function isSameRange(criteria: Pick<Criteria, 'dateFrom' | 'dateTo'>, range: { d
return criteria.dateFrom === range.dateFrom && criteria.dateTo === range.dateTo; return criteria.dateFrom === range.dateFrom && criteria.dateTo === range.dateTo;
} }
function MileageRangeShortcuts({
value,
ranges,
onChange
}: {
value: Pick<Criteria, 'dateFrom' | 'dateTo'>;
ranges: Array<{ label: string; range: { dateFrom: string; dateTo: string } }>;
onChange: (range: { dateFrom: string; dateTo: string }) => void;
}) {
return <div className="v2-mileage-ranges">
<span></span>
<ButtonGroup aria-label="快捷日期范围">
{ranges.map(({ label, range }) => {
const active = isSameRange(value, range);
return <Button
key={label}
theme={active ? 'solid' : 'light'}
htmlType="button"
aria-pressed={active}
onClick={() => onChange(range)}
>
{label}
</Button>;
})}
</ButtonGroup>
</div>;
}
export function mileageRangeContainsDate( export function mileageRangeContainsDate(
range: { dateFrom: string; dateTo: string }, range: { dateFrom: string; dateTo: string },
date = localDate() date = localDate()
@@ -160,8 +188,18 @@ function SourceStrategy({ value, onChange }: { value: MileageSourceOption[]; onC
}; };
return <div className="v2-mileage-source-strategy"> return <div className="v2-mileage-source-strategy">
<Button className="v2-mileage-source-trigger" theme="light" icon={<IconSetting />} aria-haspopup="dialog" aria-expanded={open} aria-controls="v2-mileage-source-strategy" title={`当前优先:${primarySource?.label ?? '未配置'}`} onClick={() => setOpen((current) => !current)}> <Button
<span></span><em>{primarySource?.protocol ?? '未配置'} </em><b>{enabled.length}/3</b> className="v2-mileage-source-trigger"
theme="light"
icon={<IconSetting />}
aria-label={`数据源策略,${primarySource?.protocol ?? '未配置'} 优先,${enabled.length}/3 已启用`}
aria-haspopup="dialog"
aria-expanded={open}
aria-controls="v2-mileage-source-strategy"
title={`当前优先:${primarySource?.label ?? '未配置'}`}
onClick={() => setOpen((current) => !current)}
>
<span></span><em>{primarySource?.protocol ?? '未配置'} </em><b>{enabled.length}/3</b>
</Button> </Button>
<WorkspaceSideSheet <WorkspaceSideSheet
className="v2-mileage-source-sidesheet" className="v2-mileage-source-sidesheet"
@@ -545,6 +583,13 @@ export default function StatisticsPage() {
const sevenDayRange = defaultWindow(7); const sevenDayRange = defaultWindow(7);
const thirtyDayRange = defaultWindow(30); const thirtyDayRange = defaultWindow(30);
const ninetyDayRange = defaultWindow(90); const ninetyDayRange = defaultWindow(90);
const quickRanges = [
{ label: '今天', range: todayRange },
{ label: '昨天', range: yesterdayRange },
{ label: '近 7 天', range: sevenDayRange },
{ label: '近 30 天', range: thirtyDayRange },
{ label: '近 90 天', range: ninetyDayRange }
];
const refreshing = statistics.isFetching || mileage.isFetching || fleetVehicles.isFetching; const refreshing = statistics.isFetching || mileage.isFetching || fleetVehicles.isFetching;
const resultsLoading = fleetVehicles.isLoading || mileage.isLoading || mileage.isPlaceholderData; const resultsLoading = fleetVehicles.isLoading || mileage.isLoading || mileage.isPlaceholderData;
const exportPercent = exportProgress?.total const exportPercent = exportProgress?.total
@@ -653,7 +698,7 @@ export default function StatisticsPage() {
</label> </label>
<Button className="v2-primary-button v2-mileage-inline-submit" theme="solid" htmlType="submit"></Button> <Button className="v2-primary-button v2-mileage-inline-submit" theme="solid" htmlType="submit"></Button>
<SourceStrategy value={draft.sources} onChange={(sources) => setDraft((current) => ({ ...current, sources }))} /> <SourceStrategy value={draft.sources} onChange={(sources) => setDraft((current) => ({ ...current, sources }))} />
<div className="v2-mileage-ranges"><span></span><ButtonGroup aria-label="快捷日期范围"><Button theme={isSameRange(draft, todayRange) ? 'solid' : 'light'} onClick={() => setRange(todayRange)}></Button><Button theme={isSameRange(draft, yesterdayRange) ? 'solid' : 'light'} onClick={() => setRange(yesterdayRange)}></Button><Button theme={isSameRange(draft, sevenDayRange) ? 'solid' : 'light'} onClick={() => setRange(sevenDayRange)}> 7 </Button><Button theme={isSameRange(draft, thirtyDayRange) ? 'solid' : 'light'} onClick={() => setRange(thirtyDayRange)}> 30 </Button><Button theme={isSameRange(draft, ninetyDayRange) ? 'solid' : 'light'} onClick={() => setRange(ninetyDayRange)}> 90 </Button></ButtonGroup></div> <MileageRangeShortcuts value={draft} ranges={quickRanges} onChange={setRange} />
</form>; </form>;
const filterValidation = validationError || criteriaError ? <p className="v2-mileage-validation" role="alert">{validationError || criteriaError}</p> : null; const filterValidation = validationError || criteriaError ? <p className="v2-mileage-validation" role="alert">{validationError || criteriaError}</p> : null;

View File

@@ -14551,6 +14551,8 @@
flex: 0 0 auto; flex: 0 0 auto;
gap: 0; gap: 0;
overflow: visible; overflow: visible;
container-name: mileage-discovery;
container-type: inline-size;
border: 1px solid #d8e3ef; border: 1px solid #d8e3ef;
border-radius: 12px; border-radius: 12px;
background: #fff; background: #fff;
@@ -14702,6 +14704,27 @@
font-size: 12px; font-size: 12px;
} }
.v2-mileage-source-trigger.semi-button .semi-button-content {
display: flex;
width: 100%;
min-width: 0;
flex: 1 1 auto;
align-items: center;
gap: 6px;
}
.v2-mileage-source-trigger.semi-button .semi-button-content > span,
.v2-mileage-source-trigger.semi-button .semi-button-content > em {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.v2-mileage-source-trigger.semi-button .semi-button-content > b {
flex: 0 0 auto;
margin-left: auto;
}
.v2-mileage-source-trigger.semi-button:hover, .v2-mileage-source-trigger.semi-button:hover,
.v2-mileage-source-trigger.semi-button[aria-expanded="true"] { .v2-mileage-source-trigger.semi-button[aria-expanded="true"] {
border-color: #aec6e4; border-color: #aec6e4;
@@ -14715,7 +14738,10 @@
} }
.v2-mileage-ranges { .v2-mileage-ranges {
display: flex;
min-height: 38px; min-height: 38px;
min-width: 0;
align-items: center;
gap: 2px; gap: 2px;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-radius: 9px; border-radius: 9px;
@@ -14723,14 +14749,22 @@
padding: 3px; padding: 3px;
} }
.v2-mileage-ranges > span {
flex: 0 0 auto;
}
.v2-mileage-ranges > .semi-button-group { .v2-mileage-ranges > .semi-button-group {
display: flex; display: flex;
width: 100%;
min-width: 0; min-width: 0;
flex: 1 1 auto;
gap: 2px; gap: 2px;
} }
.v2-mileage-ranges .semi-button { .v2-mileage-ranges .semi-button {
min-height: 30px; min-height: 30px;
min-width: 52px;
flex: 1 1 0;
border-radius: 6px; border-radius: 6px;
font-size: 11px; font-size: 11px;
font-weight: 650; font-weight: 650;
@@ -15003,9 +15037,9 @@
transform: translateY(-50%); transform: translateY(-50%);
} }
@media (min-width: 681px) and (max-width: 1050px) { @container mileage-discovery (min-width: 681px) and (max-width: 1100px) {
.v2-mileage-query-panel .v2-mileage-filter { .v2-mileage-query-panel .v2-mileage-filter {
grid-template-columns: minmax(200px, 1.25fr) minmax(220px, 1fr) auto; grid-template-columns: minmax(260px, 1.25fr) minmax(242px, .95fr) auto;
grid-template-areas: grid-template-areas:
"vehicle date submit" "vehicle date submit"
"source ranges ranges"; "source ranges ranges";
@@ -15028,8 +15062,35 @@
grid-area: source; grid-area: source;
} }
.v2-mileage-query-panel .v2-mileage-source-trigger.semi-button {
width: 100%;
min-width: 190px;
justify-content: flex-start;
}
.v2-mileage-query-panel .v2-mileage-ranges { .v2-mileage-query-panel .v2-mileage-ranges {
display: flex;
grid-area: ranges; grid-area: ranges;
width: 100%;
min-width: 0;
align-items: center;
justify-self: stretch;
}
.v2-mileage-query-panel .v2-mileage-ranges > span {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
padding-inline: 7px 5px;
color: #718196;
font-size: 10px;
font-weight: 650;
white-space: nowrap;
}
.v2-mileage-query-panel .v2-mileage-ranges .semi-button {
min-width: 52px;
flex: 1 1 0;
} }
} }