refactor: unify semi operational time ranges
This commit is contained in:
@@ -295,9 +295,13 @@ test('keeps the primary alert query compact and applies advanced filters from a
|
||||
await screen.findByText('当前筛选条件没有告警事件');
|
||||
expect(view.container.querySelector('.v2-alert-filter-primary')).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('协议')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: /更多筛选/ }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /更多筛选/ }));
|
||||
|
||||
const dialog = await screen.findByRole('dialog', { name: '告警高级筛选' });
|
||||
expect(within(dialog).getByRole('group', { name: '告警快捷时间范围' })).toHaveClass('semi-button-group');
|
||||
expect(within(dialog).getByPlaceholderText('开始时间')).toBeInTheDocument();
|
||||
expect(within(dialog).getByPlaceholderText('结束时间')).toBeInTheDocument();
|
||||
expect(within(dialog).queryByRole('textbox', { name: '告警起始时间' })).not.toBeInTheDocument();
|
||||
fireEvent.click(within(dialog).getByRole('combobox', { name: '协议' }));
|
||||
fireEvent.click(await screen.findByText('JT808'));
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '应用筛选' }));
|
||||
@@ -325,8 +329,10 @@ test('uses one focused Semi bottom SideSheet for the complete mobile event filte
|
||||
expect(document.querySelector('.v2-alert-mobile-filter-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(82dvh, 690px)' });
|
||||
expect(within(dialog).getByRole('textbox', { name: /关键词/ })).toBeInTheDocument();
|
||||
expect(within(dialog).getByRole('combobox', { name: '协议' })).toBeInTheDocument();
|
||||
expect(within(dialog).getByLabelText('告警起始时间')).toBeInTheDocument();
|
||||
expect(within(dialog).getByLabelText('告警结束时间')).toBeInTheDocument();
|
||||
expect(within(dialog).getByPlaceholderText('开始时间')).toBeInTheDocument();
|
||||
expect(within(dialog).getByPlaceholderText('结束时间')).toBeInTheDocument();
|
||||
expect(within(dialog).getByRole('group', { name: '告警快捷时间范围' })).toHaveClass('semi-button-group');
|
||||
expect(within(dialog).queryByRole('textbox', { name: '告警起始时间' })).not.toBeInTheDocument();
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '重置条件' }));
|
||||
expect(screen.getByRole('dialog', { name: '告警事件筛选' })).toBeInTheDocument();
|
||||
fireEvent.click(within(dialog).getByRole('combobox', { name: '协议' }));
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ProtocolTag } from '../shared/ProtocolTag';
|
||||
import { SegmentedTabs } from '../shared/SegmentedTabs';
|
||||
import { TablePagination } from '../shared/TablePagination';
|
||||
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
||||
import { WorkspaceDateTimeRange, workspaceDateTimePresetWindow } from '../shared/WorkspaceDateTimeRange';
|
||||
import { WorkspaceDetailSideSheet } from '../shared/WorkspaceDetailSideSheet';
|
||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
|
||||
@@ -195,6 +196,11 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e
|
||||
const selectEvent = useCallback((id: string) => setSelection({ scope: eventScope, id }), [eventScope]);
|
||||
const selectedEvent = detail.data ?? rows.find((row) => row.id === selectedID);
|
||||
const mobileFiltersOpen = mobileLayout && !filtersCollapsed;
|
||||
const alertTimePresets = useMemo(() => [
|
||||
{ key: 'today', label: '今天', range: workspaceDateTimePresetWindow('today') },
|
||||
{ key: 'yesterday', label: '昨天', range: workspaceDateTimePresetWindow('yesterday') },
|
||||
{ key: 'last24Hours', label: '近 24 小时', range: workspaceDateTimePresetWindow('last24Hours') }
|
||||
], []);
|
||||
const applyDraft = () => {
|
||||
setFilters(draft);
|
||||
setOffset(0);
|
||||
@@ -226,8 +232,18 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e
|
||||
const advancedFilterFields = <>
|
||||
<label><span id="alert-rule-label">规则</span><Select aria-labelledby="alert-rule-label" value={draft.ruleId} onChange={(value) => setDraft({ ...draft, ruleId: String(value) })} optionList={[{ value: '', label: '全部规则' }, ...rules.map((rule) => ({ value: rule.id, label: rule.name }))]} /></label>
|
||||
<label><span id="alert-protocol-label">协议</span><Select aria-labelledby="alert-protocol-label" value={draft.protocol} onChange={(value) => setDraft({ ...draft, protocol: String(value) })} optionList={[{ value: '', label: '全部协议' }, ...PROTOCOLS.map((item) => ({ value: item, label: item }))]} /></label>
|
||||
<label><span>起始时间</span><Input aria-label="告警起始时间" type="datetime-local" value={draft.dateFrom} onChange={(value) => setDraft({ ...draft, dateFrom: value })} /></label>
|
||||
<label><span>结束时间</span><Input aria-label="告警结束时间" type="datetime-local" value={draft.dateTo} onChange={(value) => setDraft({ ...draft, dateTo: value })} /></label>
|
||||
<WorkspaceDateTimeRange
|
||||
id="v2-alert-date-time"
|
||||
label="触发时间"
|
||||
className="v2-alert-date-time-range"
|
||||
pickerClassName="v2-alert-date-time-picker"
|
||||
presetsClassName="v2-alert-date-time-presets"
|
||||
presetsAriaLabel="告警快捷时间范围"
|
||||
value={draft}
|
||||
onChange={(range) => setDraft({ ...draft, ...range })}
|
||||
presets={alertTimePresets}
|
||||
showClear
|
||||
/>
|
||||
</>;
|
||||
const metricItem = (
|
||||
label: string,
|
||||
|
||||
@@ -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, ButtonGroup, Card, CardGroup, Checkbox, DatePicker, Descriptions, Dropdown, Empty, Input, Progress, Select, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import { Button, Card, CardGroup, Checkbox, 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';
|
||||
@@ -16,6 +16,11 @@ import { PlatformTime } from '../shared/PlatformTime';
|
||||
import { ProtocolTag } from '../shared/ProtocolTag';
|
||||
import { TablePagination } from '../shared/TablePagination';
|
||||
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
||||
import {
|
||||
normalizeWorkspaceDateTimeRange,
|
||||
WorkspaceDateTimeRange,
|
||||
workspaceDateTimePresetWindow
|
||||
} from '../shared/WorkspaceDateTimeRange';
|
||||
import { WorkspaceDetailSideSheet } from '../shared/WorkspaceDetailSideSheet';
|
||||
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
|
||||
import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail';
|
||||
@@ -117,40 +122,15 @@ function currentHistoryWindow() {
|
||||
|
||||
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) };
|
||||
return workspaceDateTimePresetWindow(preset, 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] };
|
||||
return normalizeWorkspaceDateTimeRange(dateValue, formattedValue);
|
||||
}
|
||||
|
||||
function CreateExportButton({ request, disabled }: { request: HistoryExportRequest; disabled: boolean }) {
|
||||
@@ -518,32 +498,16 @@ 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 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>
|
||||
<WorkspaceDateTimeRange
|
||||
id="v2-history-date-time"
|
||||
className="v2-history-range"
|
||||
pickerClassName="v2-history-date-time-range"
|
||||
presetsClassName="v2-history-range-presets"
|
||||
presetsAriaLabel="快捷时间范围"
|
||||
value={draft}
|
||||
onChange={(range) => setDraft((current) => ({ ...current, ...range }))}
|
||||
presets={historyRangePresets}
|
||||
/>
|
||||
<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>
|
||||
</>;
|
||||
|
||||
@@ -154,6 +154,9 @@ test('merges duplicate vehicle sources and keeps the active date shortcut visibl
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/tracks']}><TrackPage /></MemoryRouter></QueryClientProvider>);
|
||||
|
||||
expect(document.querySelector('.v2-track-date-time-picker.semi-datepicker')).toBeInTheDocument();
|
||||
expect(screen.getByRole('group', { name: '轨迹快捷时间范围' })).toHaveClass('semi-button-group');
|
||||
expect(document.querySelector('input[type="datetime-local"]')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '今天' })).toHaveAttribute('aria-pressed', 'true');
|
||||
fireEvent.click(screen.getByRole('button', { name: '昨天' }));
|
||||
expect(screen.getByRole('button', { name: '昨天' })).toHaveAttribute('aria-pressed', 'true');
|
||||
|
||||
@@ -18,6 +18,7 @@ import { SegmentedTabs } from '../shared/SegmentedTabs';
|
||||
import { VehicleCandidateList } from '../shared/VehicleCandidateList';
|
||||
import { mergeVehicleCandidates } from '../shared/vehicleCandidates';
|
||||
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
||||
import { WorkspaceDateTimeRange, workspaceDateTimePresetWindow } from '../shared/WorkspaceDateTimeRange';
|
||||
import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail';
|
||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet';
|
||||
@@ -249,7 +250,11 @@ const TrackRail = memo(function TrackRail({ draft, track, summaryItems, loading,
|
||||
onCollapse: () => void;
|
||||
}) {
|
||||
const activePreset = trackPreset(draft);
|
||||
const choosePreset = (daysBack: number) => onDraft({ ...draft, ...trackWindow(daysBack, daysBack > 0) });
|
||||
const trackRangePresets = useMemo(() => [
|
||||
{ key: 'today', label: '今天', range: workspaceDateTimePresetWindow('today') },
|
||||
{ key: 'yesterday', label: '昨天', range: workspaceDateTimePresetWindow('yesterday') },
|
||||
{ key: 'three-days', label: '近 3 天', range: workspaceDateTimePresetWindow('last3Days') }
|
||||
], []).map((preset) => ({ ...preset, active: activePreset === preset.key }));
|
||||
const appliedWindow = `${draft.dateFrom.replace('T', ' ').slice(5)} – ${draft.dateTo.replace('T', ' ').slice(5)}`;
|
||||
const appliedSource = draft.protocol ? protocolDisplayLabel(draft.protocol) : '自动来源';
|
||||
const contextTitle = queryCollapsed ? track?.plate || draft.keyword || '轨迹查询' : '轨迹查询';
|
||||
@@ -279,12 +284,16 @@ const TrackRail = memo(function TrackRail({ draft, track, summaryItems, loading,
|
||||
/> : null}
|
||||
{!queryCollapsed ? <form className="v2-track-query" onSubmit={onSubmit}>
|
||||
<label><span>车辆</span><VehiclePicker value={draft.keyword} onChange={(keyword) => onDraft({ ...draft, keyword })} onSelect={(vehicle) => onDraft({ ...draft, keyword: vehicle.plate || vehicle.vin })} /></label>
|
||||
<div className="v2-track-presets">
|
||||
<Button theme={activePreset === 'today' ? 'solid' : 'light'} aria-pressed={activePreset === 'today'} onClick={() => choosePreset(0)}>今天</Button>
|
||||
<Button theme={activePreset === 'yesterday' ? 'solid' : 'light'} aria-pressed={activePreset === 'yesterday'} onClick={() => choosePreset(1)}>昨天</Button>
|
||||
<Button theme={activePreset === 'three-days' ? 'solid' : 'light'} aria-pressed={activePreset === 'three-days'} onClick={() => { const end = new Date(); const start = new Date(end); start.setDate(start.getDate() - 2); start.setHours(0, 0, 0, 0); onDraft({ ...draft, dateFrom: localDateTime(start), dateTo: localDateTime(end) }); }}>近 3 天</Button>
|
||||
</div>
|
||||
<div className="v2-track-date-grid"><label><span>开始时间</span><Input aria-label="开始时间" type="datetime-local" value={draft.dateFrom} onChange={(value) => onDraft({ ...draft, dateFrom: value })} /></label><label><span>结束时间</span><Input aria-label="结束时间" type="datetime-local" value={draft.dateTo} onChange={(value) => onDraft({ ...draft, dateTo: value })} /></label></div>
|
||||
<WorkspaceDateTimeRange
|
||||
id="v2-track-date-time"
|
||||
className="v2-track-date-time-range"
|
||||
pickerClassName="v2-track-date-time-picker"
|
||||
presetsClassName="v2-track-date-time-presets"
|
||||
presetsAriaLabel="轨迹快捷时间范围"
|
||||
value={draft}
|
||||
onChange={(range) => onDraft({ ...draft, ...range })}
|
||||
presets={trackRangePresets}
|
||||
/>
|
||||
<label><span id="track-source-label">数据来源</span><Select aria-labelledby="track-source-label" value={draft.protocol} onChange={(value) => onDraft({ ...draft, protocol: String(value) })} optionList={TRACK_SOURCE_OPTIONS} /></label>
|
||||
<Button className="v2-track-query-button" theme="solid" htmlType="submit" icon={<IconSearch />} loading={loading} disabled={!draft.keyword.trim()}>查询轨迹</Button>
|
||||
</form> : null}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
import {
|
||||
normalizeWorkspaceDateTimeRange,
|
||||
WorkspaceDateTimeRange,
|
||||
workspaceDateTimePresetWindow
|
||||
} from './WorkspaceDateTimeRange';
|
||||
|
||||
test('normalizes Semi values and creates stable operational presets', () => {
|
||||
const now = new Date(2026, 6, 19, 14, 30);
|
||||
expect(normalizeWorkspaceDateTimeRange([], ['2026-07-19 08:15', '2026-07-19 14:30'])).toEqual({
|
||||
dateFrom: '2026-07-19T08:15',
|
||||
dateTo: '2026-07-19T14:30'
|
||||
});
|
||||
expect(workspaceDateTimePresetWindow('today', now)).toEqual({ dateFrom: '2026-07-19T00:00', dateTo: '2026-07-19T14:30' });
|
||||
expect(workspaceDateTimePresetWindow('yesterday', now)).toEqual({ dateFrom: '2026-07-18T00:00', dateTo: '2026-07-18T23:59' });
|
||||
expect(workspaceDateTimePresetWindow('last6Hours', now)).toEqual({ dateFrom: '2026-07-19T08:30', dateTo: '2026-07-19T14:30' });
|
||||
expect(workspaceDateTimePresetWindow('last24Hours', now)).toEqual({ dateFrom: '2026-07-18T14:30', dateTo: '2026-07-19T14:30' });
|
||||
expect(workspaceDateTimePresetWindow('last3Days', now)).toEqual({ dateFrom: '2026-07-17T00:00', dateTo: '2026-07-19T14:30' });
|
||||
});
|
||||
|
||||
test('renders one Semi range with accessible quick actions', () => {
|
||||
const onChange = vi.fn();
|
||||
const today = { dateFrom: '2026-07-19T00:00', dateTo: '2026-07-19T14:30' };
|
||||
const yesterday = { dateFrom: '2026-07-18T00:00', dateTo: '2026-07-18T23:59' };
|
||||
const view = render(<WorkspaceDateTimeRange
|
||||
id="qa-range"
|
||||
value={today}
|
||||
onChange={onChange}
|
||||
presets={[
|
||||
{ key: 'today', label: '今天', range: today },
|
||||
{ key: 'yesterday', label: '昨天', range: yesterday }
|
||||
]}
|
||||
/>);
|
||||
|
||||
expect(view.container.querySelector('.v2-workspace-date-time-picker.semi-datepicker')).toBeInTheDocument();
|
||||
expect(screen.getByRole('group', { name: '时间范围快捷范围' })).toHaveClass('semi-button-group');
|
||||
expect(screen.getByRole('button', { name: '今天' })).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(view.container.querySelector('input[type="datetime-local"]')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '昨天' }));
|
||||
expect(onChange).toHaveBeenCalledWith(yesterday);
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import { Button, ButtonGroup, DatePicker } from '@douyinfe/semi-ui';
|
||||
|
||||
export type WorkspaceDateTimeRangeValue = {
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
};
|
||||
|
||||
export type WorkspaceDateTimePresetKey =
|
||||
| 'today'
|
||||
| 'yesterday'
|
||||
| 'last6Hours'
|
||||
| 'last24Hours'
|
||||
| 'last3Days';
|
||||
|
||||
export type WorkspaceDateTimePreset = {
|
||||
key: string;
|
||||
label: string;
|
||||
range: WorkspaceDateTimeRangeValue;
|
||||
active?: boolean;
|
||||
};
|
||||
|
||||
function localMinute(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 workspaceDateTimePresetWindow(
|
||||
preset: WorkspaceDateTimePresetKey,
|
||||
now = new Date()
|
||||
): WorkspaceDateTimeRangeValue {
|
||||
if (preset === 'yesterday') {
|
||||
return {
|
||||
dateFrom: localMinute(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 0, 0)),
|
||||
dateTo: localMinute(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 23, 59))
|
||||
};
|
||||
}
|
||||
if (preset === 'last6Hours' || preset === 'last24Hours') {
|
||||
const hours = preset === 'last6Hours' ? 6 : 24;
|
||||
return {
|
||||
dateFrom: localMinute(new Date(now.getTime() - hours * 60 * 60_000)),
|
||||
dateTo: localMinute(now)
|
||||
};
|
||||
}
|
||||
if (preset === 'last3Days') {
|
||||
return {
|
||||
dateFrom: localMinute(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 2, 0, 0)),
|
||||
dateTo: localMinute(now)
|
||||
};
|
||||
}
|
||||
return {
|
||||
dateFrom: localMinute(new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0)),
|
||||
dateTo: localMinute(now)
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeWorkspaceDateTimeRange(
|
||||
dateValue?: Date | Date[] | string | string[],
|
||||
formattedValue?: string | string[] | Date | Date[]
|
||||
): WorkspaceDateTimeRangeValue | undefined {
|
||||
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 localMinute(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] };
|
||||
}
|
||||
|
||||
export function WorkspaceDateTimeRange({
|
||||
id,
|
||||
label = '时间范围',
|
||||
value,
|
||||
onChange,
|
||||
presets = [],
|
||||
showClear = false,
|
||||
className = '',
|
||||
pickerClassName = '',
|
||||
presetsClassName = '',
|
||||
presetsAriaLabel
|
||||
}: {
|
||||
id: string;
|
||||
label?: string;
|
||||
value: WorkspaceDateTimeRangeValue;
|
||||
onChange: (value: WorkspaceDateTimeRangeValue) => void;
|
||||
presets?: readonly WorkspaceDateTimePreset[];
|
||||
showClear?: boolean;
|
||||
className?: string;
|
||||
pickerClassName?: string;
|
||||
presetsClassName?: string;
|
||||
presetsAriaLabel?: string;
|
||||
}) {
|
||||
const labelId = `${id}-label`;
|
||||
const classes = ['v2-workspace-date-time-range', className].filter(Boolean).join(' ');
|
||||
const pickerClasses = ['v2-workspace-date-time-picker', pickerClassName].filter(Boolean).join(' ');
|
||||
const presetClasses = ['v2-workspace-date-time-presets', presetsClassName].filter(Boolean).join(' ');
|
||||
const pickerValue = value.dateFrom && value.dateTo
|
||||
? [new Date(value.dateFrom), new Date(value.dateTo)]
|
||||
: [];
|
||||
|
||||
return <fieldset className={classes}>
|
||||
<legend id={labelId}>{label}</legend>
|
||||
<DatePicker
|
||||
className={pickerClasses}
|
||||
type="dateTimeRange"
|
||||
density="compact"
|
||||
format="yyyy-MM-dd HH:mm"
|
||||
value={pickerValue}
|
||||
placeholder={['开始时间', '结束时间']}
|
||||
showClear={showClear}
|
||||
onChangeWithDateFirst
|
||||
aria-labelledby={labelId}
|
||||
onChange={(dateValue, formattedValue) => {
|
||||
const next = normalizeWorkspaceDateTimeRange(dateValue, formattedValue);
|
||||
if (next) onChange(next);
|
||||
else if (showClear && (!dateValue || (Array.isArray(dateValue) && !dateValue.length))) {
|
||||
onChange({ dateFrom: '', dateTo: '' });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{presets.length ? <ButtonGroup className={presetClasses} aria-label={presetsAriaLabel || `${label}快捷范围`}>
|
||||
{presets.map((preset) => {
|
||||
const active = preset.active ?? (
|
||||
value.dateFrom === preset.range.dateFrom
|
||||
&& value.dateTo === preset.range.dateTo
|
||||
);
|
||||
return <Button
|
||||
key={preset.key}
|
||||
htmlType="button"
|
||||
theme={active ? 'solid' : 'light'}
|
||||
aria-pressed={active}
|
||||
onClick={() => onChange(preset.range)}
|
||||
>{preset.label}</Button>;
|
||||
})}
|
||||
</ButtonGroup> : null}
|
||||
</fieldset>;
|
||||
}
|
||||
@@ -23880,3 +23880,164 @@
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Shared Semi UI date-time range.
|
||||
* History evidence, trajectory replay and alert handling now use one control
|
||||
* family instead of browser-native date inputs with platform-specific chrome.
|
||||
*/
|
||||
.v2-workspace-date-time-range {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-content: end;
|
||||
gap: 5px 8px;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.v2-workspace-date-time-range > legend {
|
||||
grid-column: 1;
|
||||
align-self: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #66778e;
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.v2-workspace-date-time-picker.semi-datepicker {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 40px;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.v2-workspace-date-time-picker .semi-datepicker-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.v2-workspace-date-time-picker > .semi-datepicker-input {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.v2-workspace-date-time-picker .semi-input-wrapper {
|
||||
min-width: 0;
|
||||
min-height: 40px;
|
||||
border-color: #d8e2ed;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.v2-workspace-date-time-picker .semi-input-wrapper:hover {
|
||||
border-color: #b9c9dc;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.v2-workspace-date-time-picker .semi-input-wrapper-focus {
|
||||
border-color: #75a5ed;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, .09);
|
||||
}
|
||||
|
||||
.v2-workspace-date-time-picker .semi-input {
|
||||
min-width: 0;
|
||||
color: #2d4058;
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.v2-workspace-date-time-presets.semi-button-group {
|
||||
display: flex;
|
||||
height: 24px;
|
||||
min-width: 0;
|
||||
min-height: 24px;
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
justify-self: end;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.v2-workspace-date-time-presets > .semi-button {
|
||||
height: 24px;
|
||||
min-width: 0;
|
||||
min-height: 24px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 6px;
|
||||
padding-inline: 7px;
|
||||
font-size: 10px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.v2-workspace-date-time-presets > .semi-button.semi-button-solid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.v2-track-query > .v2-track-date-time-range {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.v2-track-date-time-range > legend {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.v2-track-date-time-picker .semi-input-wrapper {
|
||||
min-height: 38px;
|
||||
background: #fbfcfe;
|
||||
}
|
||||
|
||||
.v2-track-date-time-picker .semi-input {
|
||||
padding-inline: 3px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.v2-track-date-time-presets > .semi-button {
|
||||
padding-inline: 6px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.v2-alert-advanced-filter > .v2-alert-date-time-range,
|
||||
.v2-alert-mobile-filter-grid > .v2-alert-date-time-range {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.v2-alert-advanced-filter > .v2-alert-date-time-range {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.v2-alert-mobile-filter-grid > .v2-alert-date-time-range {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.v2-workspace-date-time-picker.semi-datepicker,
|
||||
.v2-workspace-date-time-picker .semi-datepicker-input,
|
||||
.v2-workspace-date-time-picker .semi-input-wrapper {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.v2-workspace-date-time-presets.semi-button-group {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
justify-self: end;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.v2-workspace-date-time-presets.semi-button-group::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.v2-workspace-date-time-presets > .semi-button {
|
||||
height: 26px;
|
||||
min-height: 26px;
|
||||
padding-inline: 8px;
|
||||
}
|
||||
|
||||
.v2-alert-mobile-filter-grid .v2-alert-date-time-picker .semi-input {
|
||||
padding-inline: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user