refactor(ui): polish semi mileage discovery
This commit is contained in:
@@ -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: '查询' }));
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user