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());
expect(mocks.mileageStatistics.mock.calls[0][0].get('dateFrom')).toBe(sevenDaysAgo);
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: '昨天' }));
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('dateTo')).toBe(yesterday);
});
expect(screen.getByRole('button', { name: '昨天' })).toHaveAttribute('aria-pressed', 'true');
fireEvent.click(screen.getByRole('button', { name: '今天' }));
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('dateTo')).toBe(today);
});
expect(screen.getByRole('button', { name: '今天' })).toHaveAttribute('aria-pressed', 'true');
});
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 里程').closest('.semi-tag')).toBeInTheDocument();
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('button', { name: '上移 宇通 MQTT' }));
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;
}
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(
range: { dateFrom: string; dateTo: string },
date = localDate()
@@ -160,8 +188,18 @@ function SourceStrategy({ value, onChange }: { value: MileageSourceOption[]; onC
};
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)}>
<span></span><em>{primarySource?.protocol ?? '未配置'} </em><b>{enabled.length}/3</b>
<Button
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>
<WorkspaceSideSheet
className="v2-mileage-source-sidesheet"
@@ -545,6 +583,13 @@ export default function StatisticsPage() {
const sevenDayRange = defaultWindow(7);
const thirtyDayRange = defaultWindow(30);
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 resultsLoading = fleetVehicles.isLoading || mileage.isLoading || mileage.isPlaceholderData;
const exportPercent = exportProgress?.total
@@ -653,7 +698,7 @@ export default function StatisticsPage() {
</label>
<Button className="v2-primary-button v2-mileage-inline-submit" theme="solid" htmlType="submit"></Button>
<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>;
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;
gap: 0;
overflow: visible;
container-name: mileage-discovery;
container-type: inline-size;
border: 1px solid #d8e3ef;
border-radius: 12px;
background: #fff;
@@ -14702,6 +14704,27 @@
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[aria-expanded="true"] {
border-color: #aec6e4;
@@ -14715,7 +14738,10 @@
}
.v2-mileage-ranges {
display: flex;
min-height: 38px;
min-width: 0;
align-items: center;
gap: 2px;
border: 1px solid #e2e8f0;
border-radius: 9px;
@@ -14723,14 +14749,22 @@
padding: 3px;
}
.v2-mileage-ranges > span {
flex: 0 0 auto;
}
.v2-mileage-ranges > .semi-button-group {
display: flex;
width: 100%;
min-width: 0;
flex: 1 1 auto;
gap: 2px;
}
.v2-mileage-ranges .semi-button {
min-height: 30px;
min-width: 52px;
flex: 1 1 0;
border-radius: 6px;
font-size: 11px;
font-weight: 650;
@@ -15003,9 +15037,9 @@
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 {
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:
"vehicle date submit"
"source ranges ranges";
@@ -15028,8 +15062,35 @@
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 {
display: flex;
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;
}
}