refactor: unify semi history date filters

This commit is contained in:
lingniu
2026-07-19 14:40:57 +08:00
parent 2d987fd185
commit 476069adc6
3 changed files with 233 additions and 15 deletions

View File

@@ -1,6 +1,6 @@
import { useMutation, useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query';
import { IconCalendar, IconChevronRight, IconClose, IconDownload, IconMapPin, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons';
import { Button, Card, CardGroup, Checkbox, Descriptions, Dropdown, Empty, Input, Progress, Select, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui';
import { Button, ButtonGroup, Card, CardGroup, Checkbox, DatePicker, Descriptions, Dropdown, Empty, Input, Progress, Select, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui';
import { FormEvent, KeyboardEvent, useCallback, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { api } from '../../api/client';
@@ -112,9 +112,45 @@ export function formatHistoryDateTime(value: string) {
}
function currentHistoryWindow() {
const now = new Date(); const pad = (value: number) => String(value).padStart(2, '0');
const day = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
return { dateFrom: `${day}T00:00`, dateTo: `${day}T${pad(now.getHours())}:${pad(now.getMinutes())}` };
return historyPresetWindow('today');
}
type HistoryRangePreset = 'today' | 'yesterday' | 'last6Hours' | 'last24Hours';
function localHistoryMinute(value: Date) {
const pad = (part: number) => String(part).padStart(2, '0');
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}T${pad(value.getHours())}:${pad(value.getMinutes())}`;
}
export function historyPresetWindow(preset: HistoryRangePreset, now = new Date()) {
if (preset === 'yesterday') {
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 0, 0);
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 23, 59);
return { dateFrom: localHistoryMinute(start), dateTo: localHistoryMinute(end) };
}
if (preset === 'last6Hours' || preset === 'last24Hours') {
const hours = preset === 'last6Hours' ? 6 : 24;
return { dateFrom: localHistoryMinute(new Date(now.getTime() - hours * 60 * 60_000)), dateTo: localHistoryMinute(now) };
}
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0);
return { dateFrom: localHistoryMinute(start), dateTo: localHistoryMinute(now) };
}
export function historyDateTimePickerRange(
dateValue?: Date | Date[] | string | string[],
formattedValue?: string | string[] | Date | Date[]
) {
const values = Array.isArray(formattedValue) && formattedValue.length === 2
? formattedValue
: Array.isArray(dateValue) && dateValue.length === 2 ? dateValue : [];
if (values.length !== 2) return;
const normalized = values.map((value) => {
if (value instanceof Date) return localHistoryMinute(value);
if (typeof value === 'number') return localHistoryMinute(new Date(value));
return String(value).trim().replace(' ', 'T').slice(0, 16);
});
if (normalized.some((value) => !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(value))) return;
return { dateFrom: normalized[0], dateTo: normalized[1] };
}
function CreateExportButton({ request, disabled }: { request: HistoryExportRequest; disabled: boolean }) {
@@ -411,6 +447,12 @@ export default function HistoryPage() {
setSelectedRowRef({ scope: dataScope, id: '' });
}, [dataScope]);
const mobileFiltersOpen = mobileLayout && !filtersCollapsed;
const historyRangePresets = useMemo(() => [
{ key: 'today' as const, label: '今天', range: historyPresetWindow('today') },
{ key: 'yesterday' as const, label: '昨天', range: historyPresetWindow('yesterday') },
{ key: 'last6Hours' as const, label: '近 6 小时', range: historyPresetWindow('last6Hours') },
{ key: 'last24Hours' as const, label: '近 24 小时', range: historyPresetWindow('last24Hours') }
], []);
const applyDraft = () => {
const parsed = parseHistoryKeywords(draft.keywords);
@@ -476,7 +518,32 @@ export default function HistoryPage() {
];
const historyFilterFields = <>
<label className="v2-history-vehicles"><span> 5 </span><Input prefix={<IconSearch />} value={draft.keywords} onChange={(value) => setDraft((current) => ({ ...current, keywords: value }))} placeholder="车牌 / VIN多台用逗号分隔" /></label>
<fieldset className="v2-history-range"><legend></legend><div><Input aria-label="开始时间" type="datetime-local" value={draft.dateFrom} onChange={(value) => setDraft((current) => ({ ...current, dateFrom: value }))} /><span></span><Input aria-label="结束时间" type="datetime-local" value={draft.dateTo} onChange={(value) => setDraft((current) => ({ ...current, dateTo: value }))} /></div></fieldset>
<fieldset className="v2-history-range">
<legend id="v2-history-date-time-label"></legend>
<DatePicker
className="v2-history-date-time-range"
type="dateTimeRange"
density="compact"
format="yyyy-MM-dd HH:mm"
value={[new Date(draft.dateFrom), new Date(draft.dateTo)]}
placeholder={['开始时间', '结束时间']}
showClear={false}
onChangeWithDateFirst
aria-labelledby="v2-history-date-time-label"
onChange={(value, formattedValue) => {
const range = historyDateTimePickerRange(value, formattedValue);
if (range) setDraft((current) => ({ ...current, ...range }));
}}
/>
<ButtonGroup className="v2-history-range-presets" aria-label="快捷时间范围">
{historyRangePresets.map((preset) => <Button
key={preset.key}
htmlType="button"
theme={draft.dateFrom === preset.range.dateFrom && draft.dateTo === preset.range.dateTo ? 'solid' : 'light'}
onClick={() => setDraft((current) => ({ ...current, ...preset.range }))}
>{preset.label}</Button>)}
</ButtonGroup>
</fieldset>
<label><span id="history-category-label"></span><Select aria-labelledby="history-category-label" value={draft.category} onChange={(value) => setDraft((current) => ({ ...current, category: String(value) }))} optionList={(catalogQuery.data?.categories ?? [{ key: 'location', label: '位置数据' }, { key: 'raw', label: '原始报文' }, { key: 'mileage', label: '日里程' }]).map((item) => ({ value: item.key, label: item.label }))} /></label>
<label><span id="history-protocol-label"></span><Select aria-labelledby="history-protocol-label" value={draft.protocol} onChange={(value) => setDraft((current) => ({ ...current, protocol: String(value) }))} optionList={[{ value: '', label: '全部来源' }, { value: 'GB32960', label: 'GB32960' }, { value: 'JT808', label: 'JT808' }, { value: 'YUTONG_MQTT', label: 'YUTONG_MQTT' }]} /></label>
</>;
@@ -536,7 +603,7 @@ export default function HistoryPage() {
>
<form className={`v2-history-toolbar${filtersCollapsed ? ' is-mobile-collapsed' : ''}`} onSubmit={submit}>
{historyFilterFields}
<div className="v2-history-toolbar-actions"><Button className="v2-primary-button" theme="solid" htmlType="submit" disabled={!parseHistoryKeywords(draft.keywords).length}></Button><Button className="v2-secondary-button" theme="light" onClick={reset}></Button>{exportAllowed ? <CreateExportButton disabled={!resultRows.length} request={exportRequest} /> : <Tag className="v2-history-permission-tag" color="grey" type="light" size="large"></Tag>}</div>
<div className="v2-history-toolbar-actions"><Button className="v2-primary-button" theme="solid" htmlType="submit" disabled={!parseHistoryKeywords(draft.keywords).length}></Button><Button className="v2-secondary-button" theme="light" onClick={reset}></Button>{!exportAllowed ? <Tag className="v2-history-permission-tag" color="grey" type="light" size="large"></Tag> : null}</div>
</form>
</WorkspaceFilterPanel>}
</div>
@@ -575,6 +642,7 @@ export default function HistoryPage() {
</> : null : <>
<Button className={`v2-history-table-trend${trendExpanded ? ' is-expanded' : ''}`} theme="borderless" aria-label={trendExpanded ? '收起趋势' : '展开趋势'} aria-expanded={trendExpanded} icon={<IconChevronRight />} onClick={() => setTrendExpanded((value) => !value)}>{trendExpanded ? '收起趋势' : '聚合趋势'}</Button>
<Button theme="borderless" aria-label="列设置" aria-haspopup="dialog" aria-expanded={columnSettingsOpen} aria-controls="v2-history-column-settings" icon={<IconSetting />} onClick={() => setColumnSettingsOpen((value) => !value)}></Button>
{exportAllowed ? <CreateExportButton disabled={!resultRows.length} request={exportRequest} /> : null}
{exportAllowed ? <Button theme="borderless" aria-label="查看导出任务" aria-haspopup="dialog" aria-expanded={exportJobsOpen} aria-controls="v2-history-export-jobs" icon={<IconDownload />} onClick={() => setExportJobsOpen(true)}></Button> : null}
<Select aria-label="表格密度" value={density} onChange={(value) => setDensity(String(value) as typeof density)} optionList={[{ value: 'compact', label: '紧凑' }, { value: 'comfortable', label: '舒适' }]} />
<Button theme="borderless" onClick={() => dataQuery.refetch()} aria-label="刷新历史数据" icon={<IconRefresh />} />