feat: persist mileage monitoring context
This commit is contained in:
@@ -161,6 +161,7 @@ SUM(fee) / NULLIF(SUM(kwh), 0)
|
|||||||
- 已完成里程考核归因、车辆详情和每日汇报跳转。
|
- 已完成里程考核归因、车辆详情和每日汇报跳转。
|
||||||
- 里程考核年度已纳入 `mileageYear` URL 状态;目标、部门/客户归因和车辆清单下钻继承同一年度,无效年度会按目标真实年度归一。
|
- 里程考核年度已纳入 `mileageYear` URL 状态;目标、部门/客户归因和车辆清单下钻继承同一年度,无效年度会按目标真实年度归一。
|
||||||
- 车辆级里程详情继承 `mileageStart`、`mileageEnd` 和数据源优先级;显式下钻日期严格展示所选日,预设区间继续排除尚未完整的当天数据,请求故障显示为不可用并支持重试。
|
- 车辆级里程详情继承 `mileageStart`、`mileageEnd` 和数据源优先级;显式下钻日期严格展示所选日,预设区间继续排除尚未完整的当天数据,请求故障显示为不可用并支持重试。
|
||||||
|
- 里程实时监控的日期、来源、批次、区域、车牌、归属、高级筛选和排序已纳入独立 URL 状态;分享或刷新链接可恢复完整查询,车辆详情前进后退保留列表上下文且不把下钻车牌误写为筛选车牌。
|
||||||
- 已完成氢能站点、客户下钻代码。
|
- 已完成氢能站点、客户下钻代码。
|
||||||
- 已完成氢能每日趋势到加氢订单的 URL 下钻、分页和结构化故障状态;待数据源恢复后验收真实明细。
|
- 已完成氢能每日趋势到加氢订单的 URL 下钻、分页和结构化故障状态;待数据源恢复后验收真实明细。
|
||||||
- 已为电能总览与氢能每日趋势柱图统一 Enter/Space 键盘下钻;氢能有效日期柱提供可读名称、焦点和选中反馈。
|
- 已为电能总览与氢能每日趋势柱图统一 Enter/Space 键盘下钻;氢能有效日期柱提供可读名称、焦点和选中反馈。
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ import {
|
|||||||
type MileageDrillContext,
|
type MileageDrillContext,
|
||||||
} from './drill-context';
|
} from './drill-context';
|
||||||
import { formatMileageSnapshotTime } from './data-freshness';
|
import { formatMileageSnapshotTime } from './data-freshness';
|
||||||
|
import {
|
||||||
|
buildMileageMonitoringUrl,
|
||||||
|
parseMileageMonitoringContext,
|
||||||
|
type MileageMonitoringContext,
|
||||||
|
} from './monitoring-context';
|
||||||
|
|
||||||
const HIGH_MILEAGE_ALERT_TARGETS = new Set([
|
const HIGH_MILEAGE_ALERT_TARGETS = new Set([
|
||||||
'交投40辆4.5T普货',
|
'交投40辆4.5T普货',
|
||||||
@@ -454,10 +459,11 @@ export default function MonitoringView() {
|
|||||||
const [drillContext, setDrillContext] = useState<MileageDrillContext>(() => (
|
const [drillContext, setDrillContext] = useState<MileageDrillContext>(() => (
|
||||||
parseMileageDrillContext(window.location.search)
|
parseMileageDrillContext(window.location.search)
|
||||||
));
|
));
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [initialMonitoringContext] = useState(() => parseMileageMonitoringContext(window.location.search));
|
||||||
const [filterDept, setFilterDept] = useState('All');
|
const [searchTerm, setSearchTerm] = useState(initialMonitoringContext.search || '');
|
||||||
const [sortBy, setSortBy] = useState<'today' | 'total' | 'statisticTime'>('today');
|
const [filterDept, setFilterDept] = useState(initialMonitoringContext.department || 'All');
|
||||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
const [sortBy, setSortBy] = useState<'today' | 'total' | 'statisticTime'>(initialMonitoringContext.sortBy);
|
||||||
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>(initialMonitoringContext.sortOrder);
|
||||||
const [isFilterOpen, setIsFilterOpen] = useState(false);
|
const [isFilterOpen, setIsFilterOpen] = useState(false);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
const [fullscreenVehicles, setFullscreenVehicles] = useState<MonitoringVehicle[]>([]);
|
const [fullscreenVehicles, setFullscreenVehicles] = useState<MonitoringVehicle[]>([]);
|
||||||
@@ -469,24 +475,31 @@ export default function MonitoringView() {
|
|||||||
const [fullscreenUpdatedAt, setFullscreenUpdatedAt] = useState<string | null>(null);
|
const [fullscreenUpdatedAt, setFullscreenUpdatedAt] = useState<string | null>(null);
|
||||||
|
|
||||||
// New filters from image
|
// New filters from image
|
||||||
const [filterPlates, setFilterPlates] = useState<string[]>(() => (
|
const [filterPlates, setFilterPlates] = useState<string[]>(initialMonitoringContext.plates);
|
||||||
drillContext.level === 'vehicle' && drillContext.plate ? [drillContext.plate] : []
|
const [drillPlateFallback, setDrillPlateFallback] = useState<string | undefined>(() => (
|
||||||
|
drillContext.level === 'vehicle' && drillContext.plate ? drillContext.plate : undefined
|
||||||
));
|
));
|
||||||
const [filterCustomer, setFilterCustomer] = useState('All');
|
const [filterCustomer, setFilterCustomer] = useState(initialMonitoringContext.customer || 'All');
|
||||||
const [filterProject, setFilterProject] = useState('All');
|
const [filterProject, setFilterProject] = useState(initialMonitoringContext.project || 'All');
|
||||||
const [filterEntity, setFilterEntity] = useState('All');
|
const [filterEntity, setFilterEntity] = useState(initialMonitoringContext.entity || 'All');
|
||||||
const [filterRentStatus, setFilterRentStatus] = useState('All');
|
const [filterRentStatus, setFilterRentStatus] = useState(initialMonitoringContext.rentStatus || 'All');
|
||||||
const [filterPlatePrefix, setFilterPlatePrefix] = useState('All');
|
const [filterPlatePrefix, setFilterPlatePrefix] = useState(initialMonitoringContext.platePrefix || 'All');
|
||||||
const [filterTargetNames, setFilterTargetNames] = useState<string[]>([]);
|
const [filterTargetNames, setFilterTargetNames] = useState<string[]>(initialMonitoringContext.targetNames);
|
||||||
const [filterRegion, setFilterRegion] = useState('All');
|
const [filterRegion, setFilterRegion] = useState(initialMonitoringContext.region || 'All');
|
||||||
const [filterBrands, setFilterBrands] = useState<string[]>([]);
|
const [filterBrands, setFilterBrands] = useState<string[]>(initialMonitoringContext.brands);
|
||||||
const [filterMileageRange, setFilterMileageRange] = useState({ min: '', max: '' });
|
const [filterMileageRange, setFilterMileageRange] = useState({
|
||||||
const [appliedMileageRange, setAppliedMileageRange] = useState({ min: '', max: '' });
|
min: initialMonitoringContext.mileageMin || '',
|
||||||
|
max: initialMonitoringContext.mileageMax || '',
|
||||||
|
});
|
||||||
|
const [appliedMileageRange, setAppliedMileageRange] = useState({
|
||||||
|
min: initialMonitoringContext.mileageMin || '',
|
||||||
|
max: initialMonitoringContext.mileageMax || '',
|
||||||
|
});
|
||||||
const [exporting, setExporting] = useState(false);
|
const [exporting, setExporting] = useState(false);
|
||||||
const [exportStatus, setExportStatus] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
|
const [exportStatus, setExportStatus] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
|
||||||
const [rangeStart, setRangeStart] = useState(() => drillContext.startDate || defaultMileageDate());
|
const [rangeStart, setRangeStart] = useState(() => initialMonitoringContext.startDate || drillContext.startDate || defaultMileageDate());
|
||||||
const [rangeEnd, setRangeEnd] = useState(() => drillContext.endDate || defaultMileageDate());
|
const [rangeEnd, setRangeEnd] = useState(() => initialMonitoringContext.endDate || drillContext.endDate || defaultMileageDate());
|
||||||
const [sourcePriority, setSourcePriority] = useState<MileageSourceGroup[]>(drillContext.sourcePriority);
|
const [sourcePriority, setSourcePriority] = useState<MileageSourceGroup[]>(initialMonitoringContext.sourcePriority);
|
||||||
|
|
||||||
const [vehicles, setVehicles] = useState<MonitoringVehicle[]>([]);
|
const [vehicles, setVehicles] = useState<MonitoringVehicle[]>([]);
|
||||||
const [stats, setStats] = useState<MonitoringStats>({ totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 });
|
const [stats, setStats] = useState<MonitoringStats>({ totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 });
|
||||||
@@ -515,6 +528,7 @@ export default function MonitoringView() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const openVehicleDetail = useCallback((vehicle: MonitoringVehicle) => {
|
const openVehicleDetail = useCallback((vehicle: MonitoringVehicle) => {
|
||||||
|
setDrillPlateFallback(undefined);
|
||||||
commitDrillContext({
|
commitDrillContext({
|
||||||
level: 'vehicle',
|
level: 'vehicle',
|
||||||
plate: vehicle.plate,
|
plate: vehicle.plate,
|
||||||
@@ -525,6 +539,7 @@ export default function MonitoringView() {
|
|||||||
}, [commitDrillContext, effectiveRange.end, effectiveRange.start, sourcePriority]);
|
}, [commitDrillContext, effectiveRange.end, effectiveRange.start, sourcePriority]);
|
||||||
|
|
||||||
const closeVehicleDetail = useCallback(() => {
|
const closeVehicleDetail = useCallback(() => {
|
||||||
|
setDrillPlateFallback(undefined);
|
||||||
commitDrillContext({
|
commitDrillContext({
|
||||||
level: 'overview',
|
level: 'overview',
|
||||||
startDate: effectiveRange.start,
|
startDate: effectiveRange.start,
|
||||||
@@ -541,11 +556,31 @@ export default function MonitoringView() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handlePopState = () => {
|
const handlePopState = () => {
|
||||||
const next = parseMileageDrillContext(window.location.search);
|
const next = parseMileageDrillContext(window.location.search);
|
||||||
|
const nextMonitoring = parseMileageMonitoringContext(window.location.search);
|
||||||
setDrillContext(next);
|
setDrillContext(next);
|
||||||
if (next.startDate) setRangeStart(next.startDate);
|
setRangeStart(nextMonitoring.startDate || defaultMileageDate());
|
||||||
if (next.endDate) setRangeEnd(next.endDate);
|
setRangeEnd(nextMonitoring.endDate || defaultMileageDate());
|
||||||
setSourcePriority(next.sourcePriority);
|
setSourcePriority(nextMonitoring.sourcePriority);
|
||||||
if (next.level === 'vehicle' && next.plate) setFilterPlates([next.plate]);
|
setFilterTargetNames(nextMonitoring.targetNames);
|
||||||
|
setFilterRegion(nextMonitoring.region || 'All');
|
||||||
|
setFilterPlates(nextMonitoring.plates);
|
||||||
|
setDrillPlateFallback(next.level === 'vehicle' && next.plate ? next.plate : undefined);
|
||||||
|
setFilterDept(nextMonitoring.department || 'All');
|
||||||
|
setFilterCustomer(nextMonitoring.customer || 'All');
|
||||||
|
setFilterProject(nextMonitoring.project || 'All');
|
||||||
|
setFilterEntity(nextMonitoring.entity || 'All');
|
||||||
|
setFilterRentStatus(nextMonitoring.rentStatus || 'All');
|
||||||
|
setFilterBrands(nextMonitoring.brands);
|
||||||
|
setFilterPlatePrefix(nextMonitoring.platePrefix || 'All');
|
||||||
|
setSearchTerm(nextMonitoring.search || '');
|
||||||
|
const nextMileageRange = {
|
||||||
|
min: nextMonitoring.mileageMin || '',
|
||||||
|
max: nextMonitoring.mileageMax || '',
|
||||||
|
};
|
||||||
|
setFilterMileageRange(nextMileageRange);
|
||||||
|
setAppliedMileageRange(nextMileageRange);
|
||||||
|
setSortBy(nextMonitoring.sortBy);
|
||||||
|
setSortOrder(nextMonitoring.sortOrder);
|
||||||
};
|
};
|
||||||
window.addEventListener('popstate', handlePopState);
|
window.addEventListener('popstate', handlePopState);
|
||||||
return () => window.removeEventListener('popstate', handlePopState);
|
return () => window.removeEventListener('popstate', handlePopState);
|
||||||
@@ -565,6 +600,11 @@ export default function MonitoringView() {
|
|||||||
return inAlertTarget && Math.max(0, v.dailyKm || 0) >= HIGH_MILEAGE_ALERT_KM;
|
return inAlertTarget && Math.max(0, v.dailyKm || 0) >= HIGH_MILEAGE_ALERT_KM;
|
||||||
}, [filterTargetNames]);
|
}, [filterTargetNames]);
|
||||||
|
|
||||||
|
const requestPlates = useMemo(() => Array.from(new Set([
|
||||||
|
...filterPlates,
|
||||||
|
...(drillPlateFallback ? [drillPlateFallback] : []),
|
||||||
|
])), [drillPlateFallback, filterPlates]);
|
||||||
|
|
||||||
const monitoringFilters = useMemo<MonitoringRequestParams>(() => ({
|
const monitoringFilters = useMemo<MonitoringRequestParams>(() => ({
|
||||||
search: searchTerm || undefined,
|
search: searchTerm || undefined,
|
||||||
dept: filterDept !== 'All' ? filterDept : undefined,
|
dept: filterDept !== 'All' ? filterDept : undefined,
|
||||||
@@ -575,13 +615,50 @@ export default function MonitoringView() {
|
|||||||
platePrefix: filterPlatePrefix !== 'All' ? filterPlatePrefix : undefined,
|
platePrefix: filterPlatePrefix !== 'All' ? filterPlatePrefix : undefined,
|
||||||
targetNames: filterTargetNames.length > 0 ? filterTargetNames : undefined,
|
targetNames: filterTargetNames.length > 0 ? filterTargetNames : undefined,
|
||||||
region: filterRegion !== 'All' ? filterRegion : undefined,
|
region: filterRegion !== 'All' ? filterRegion : undefined,
|
||||||
plate: filterPlates.length > 0 ? filterPlates.join(',') : undefined,
|
plate: requestPlates.length > 0 ? requestPlates.join(',') : undefined,
|
||||||
mileageMin: appliedMileageRange.min || undefined,
|
mileageMin: appliedMileageRange.min || undefined,
|
||||||
mileageMax: appliedMileageRange.max || undefined,
|
mileageMax: appliedMileageRange.max || undefined,
|
||||||
startDate: rangeStart || undefined,
|
startDate: rangeStart || undefined,
|
||||||
endDate: rangeEnd || undefined,
|
endDate: rangeEnd || undefined,
|
||||||
brands: filterBrands.length > 0 ? filterBrands : undefined,
|
brands: filterBrands.length > 0 ? filterBrands : undefined,
|
||||||
sourcePriority,
|
sourcePriority,
|
||||||
|
}), [
|
||||||
|
appliedMileageRange.max,
|
||||||
|
appliedMileageRange.min,
|
||||||
|
filterBrands,
|
||||||
|
filterCustomer,
|
||||||
|
filterDept,
|
||||||
|
filterEntity,
|
||||||
|
filterPlatePrefix,
|
||||||
|
filterProject,
|
||||||
|
filterRegion,
|
||||||
|
filterRentStatus,
|
||||||
|
filterTargetNames,
|
||||||
|
rangeEnd,
|
||||||
|
rangeStart,
|
||||||
|
requestPlates,
|
||||||
|
searchTerm,
|
||||||
|
sourcePriority,
|
||||||
|
]);
|
||||||
|
const monitoringUrlContext = useMemo<MileageMonitoringContext>(() => ({
|
||||||
|
startDate: rangeStart || undefined,
|
||||||
|
endDate: rangeEnd || undefined,
|
||||||
|
sourcePriority,
|
||||||
|
targetNames: filterTargetNames,
|
||||||
|
region: filterRegion !== 'All' ? filterRegion : undefined,
|
||||||
|
plates: filterPlates,
|
||||||
|
department: filterDept !== 'All' ? filterDept : undefined,
|
||||||
|
customer: filterCustomer !== 'All' ? filterCustomer : undefined,
|
||||||
|
project: filterProject !== 'All' ? filterProject : undefined,
|
||||||
|
entity: filterEntity !== 'All' ? filterEntity : undefined,
|
||||||
|
rentStatus: filterRentStatus !== 'All' ? filterRentStatus : undefined,
|
||||||
|
brands: filterBrands,
|
||||||
|
platePrefix: filterPlatePrefix !== 'All' ? filterPlatePrefix : undefined,
|
||||||
|
search: searchTerm || undefined,
|
||||||
|
mileageMin: appliedMileageRange.min || undefined,
|
||||||
|
mileageMax: appliedMileageRange.max || undefined,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
}), [
|
}), [
|
||||||
appliedMileageRange.max,
|
appliedMileageRange.max,
|
||||||
appliedMileageRange.min,
|
appliedMileageRange.min,
|
||||||
@@ -598,8 +675,17 @@ export default function MonitoringView() {
|
|||||||
rangeEnd,
|
rangeEnd,
|
||||||
rangeStart,
|
rangeStart,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
sourcePriority,
|
sourcePriority,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const nextUrl = buildMileageMonitoringUrl(window.location, monitoringUrlContext);
|
||||||
|
const currentUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||||
|
if (nextUrl !== currentUrl) window.history.replaceState(null, '', nextUrl);
|
||||||
|
}, [monitoringUrlContext]);
|
||||||
|
|
||||||
const monitoringRequestKey = useMemo(
|
const monitoringRequestKey = useMemo(
|
||||||
() => JSON.stringify({ ...monitoringFilters, sortBy, sortOrder }),
|
() => JSON.stringify({ ...monitoringFilters, sortBy, sortOrder }),
|
||||||
[monitoringFilters, sortBy, sortOrder],
|
[monitoringFilters, sortBy, sortOrder],
|
||||||
@@ -705,6 +791,7 @@ export default function MonitoringView() {
|
|||||||
// 区域级联:plate 选项收窄后,剔除已选但已不属于该区域的车牌
|
// 区域级联:plate 选项收窄后,剔除已选但已不属于该区域的车牌
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filterPlates.length === 0) return;
|
if (filterPlates.length === 0) return;
|
||||||
|
if (filterOptions.plates.length === 0) return;
|
||||||
const valid = new Set(filterOptions.plates);
|
const valid = new Set(filterOptions.plates);
|
||||||
const next = filterPlates.filter(p => valid.has(p));
|
const next = filterPlates.filter(p => valid.has(p));
|
||||||
if (next.length !== filterPlates.length) setFilterPlates(next);
|
if (next.length !== filterPlates.length) setFilterPlates(next);
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
buildMileageMonitoringUrl,
|
||||||
|
parseMileageMonitoringContext,
|
||||||
|
} from './monitoring-context.js';
|
||||||
|
|
||||||
|
test('parses a complete mileage monitoring context', () => {
|
||||||
|
const context = parseMileageMonitoringContext(
|
||||||
|
'?mileageStart=2026-08-01&mileageEnd=2026-08-07&mileageSource=gps,instrument'
|
||||||
|
+ '&mileageBatch=%E4%BA%A4%E6%8A%95190%E8%BE%86&mileageBatch=%E7%BE%9A%E7%89%9B136%E8%BE%86'
|
||||||
|
+ '&mileageRegion=%E5%B9%BF%E5%B7%9E&mileageFilterPlate=%E7%B2%A4A12345&mileageDepartment=%E4%B8%9A%E5%8A%A1%E4%B8%80%E9%83%A8'
|
||||||
|
+ '&mileageCustomer=%E5%AE%A2%E6%88%B7A&mileageProject=%E9%A1%B9%E7%9B%AEA&mileageEntity=%E4%B8%BB%E4%BD%93A'
|
||||||
|
+ '&mileageRentStatus=%E7%A7%9F%E8%B5%81&mileageBrand=%E7%8E%B0%E4%BB%A3&mileagePlatePrefix=%E7%B2%A4A'
|
||||||
|
+ '&mileageSearch=123&mileageMin=10&mileageMax=500&mileageSort=total&mileageOrder=asc',
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(context, {
|
||||||
|
startDate: '2026-08-01',
|
||||||
|
endDate: '2026-08-07',
|
||||||
|
sourcePriority: ['gps', 'instrument'],
|
||||||
|
targetNames: ['交投190辆', '羚牛136辆'],
|
||||||
|
region: '广州',
|
||||||
|
plates: ['粤A12345'],
|
||||||
|
department: '业务一部',
|
||||||
|
customer: '客户A',
|
||||||
|
project: '项目A',
|
||||||
|
entity: '主体A',
|
||||||
|
rentStatus: '租赁',
|
||||||
|
brands: ['现代'],
|
||||||
|
platePrefix: '粤A',
|
||||||
|
search: '123',
|
||||||
|
mileageMin: '10',
|
||||||
|
mileageMax: '500',
|
||||||
|
sortBy: 'total',
|
||||||
|
sortOrder: 'asc',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizes invalid monitoring URL values', () => {
|
||||||
|
const context = parseMileageMonitoringContext(
|
||||||
|
'?mileageStart=invalid&mileageSource=unknown&mileageBatch=&mileageRegion=All&mileageSort=unknown&mileageOrder=unknown',
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(context, {
|
||||||
|
startDate: undefined,
|
||||||
|
endDate: undefined,
|
||||||
|
sourcePriority: ['instrument'],
|
||||||
|
targetNames: [],
|
||||||
|
region: undefined,
|
||||||
|
plates: [],
|
||||||
|
department: undefined,
|
||||||
|
customer: undefined,
|
||||||
|
project: undefined,
|
||||||
|
entity: undefined,
|
||||||
|
rentStatus: undefined,
|
||||||
|
brands: [],
|
||||||
|
platePrefix: undefined,
|
||||||
|
search: undefined,
|
||||||
|
mileageMin: undefined,
|
||||||
|
mileageMax: undefined,
|
||||||
|
sortBy: 'today',
|
||||||
|
sortOrder: 'desc',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('builds monitoring state without removing drill or unrelated context', () => {
|
||||||
|
const url = buildMileageMonitoringUrl(
|
||||||
|
{
|
||||||
|
pathname: '/asset',
|
||||||
|
search: '?company=5&mileageLevel=vehicle&mileagePlate=%E7%B2%A4A08190F&mileageBatch=old',
|
||||||
|
hash: '#mileage',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startDate: '2026-08-01',
|
||||||
|
endDate: '2026-08-07',
|
||||||
|
sourcePriority: ['instrument'],
|
||||||
|
targetNames: ['交投190辆', '羚牛136辆'],
|
||||||
|
region: '广州',
|
||||||
|
plates: ['粤A12345'],
|
||||||
|
department: undefined,
|
||||||
|
customer: undefined,
|
||||||
|
project: undefined,
|
||||||
|
entity: undefined,
|
||||||
|
rentStatus: undefined,
|
||||||
|
brands: [],
|
||||||
|
platePrefix: undefined,
|
||||||
|
search: undefined,
|
||||||
|
mileageMin: undefined,
|
||||||
|
mileageMax: undefined,
|
||||||
|
sortBy: 'statisticTime',
|
||||||
|
sortOrder: 'asc',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
url,
|
||||||
|
'/asset?company=5&mileageLevel=vehicle&mileagePlate=%E7%B2%A4A08190F&mileageStart=2026-08-01&mileageEnd=2026-08-07&mileageSource=instrument&mileageBatch=%E4%BA%A4%E6%8A%95190%E8%BE%86&mileageBatch=%E7%BE%9A%E7%89%9B136%E8%BE%86&mileageRegion=%E5%B9%BF%E5%B7%9E&mileageFilterPlate=%E7%B2%A4A12345&mileageSort=statisticTime&mileageOrder=asc#mileage',
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import type { MileageSourceGroup } from './types';
|
||||||
|
|
||||||
|
export type MonitoringSortBy = 'today' | 'total' | 'statisticTime';
|
||||||
|
export type MonitoringSortOrder = 'asc' | 'desc';
|
||||||
|
|
||||||
|
export interface MileageMonitoringContext {
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
sourcePriority: MileageSourceGroup[];
|
||||||
|
targetNames: string[];
|
||||||
|
region?: string;
|
||||||
|
plates: string[];
|
||||||
|
department?: string;
|
||||||
|
customer?: string;
|
||||||
|
project?: string;
|
||||||
|
entity?: string;
|
||||||
|
rentStatus?: string;
|
||||||
|
brands: string[];
|
||||||
|
platePrefix?: string;
|
||||||
|
search?: string;
|
||||||
|
mileageMin?: string;
|
||||||
|
mileageMax?: string;
|
||||||
|
sortBy: MonitoringSortBy;
|
||||||
|
sortOrder: MonitoringSortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LocationParts {
|
||||||
|
pathname: string;
|
||||||
|
search: string;
|
||||||
|
hash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
|
const CONTEXT_KEYS = [
|
||||||
|
'mileageStart',
|
||||||
|
'mileageEnd',
|
||||||
|
'mileageSource',
|
||||||
|
'mileageBatch',
|
||||||
|
'mileageRegion',
|
||||||
|
'mileageFilterPlate',
|
||||||
|
'mileageDepartment',
|
||||||
|
'mileageCustomer',
|
||||||
|
'mileageProject',
|
||||||
|
'mileageEntity',
|
||||||
|
'mileageRentStatus',
|
||||||
|
'mileageBrand',
|
||||||
|
'mileagePlatePrefix',
|
||||||
|
'mileageSearch',
|
||||||
|
'mileageMin',
|
||||||
|
'mileageMax',
|
||||||
|
'mileageSort',
|
||||||
|
'mileageOrder',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function validDate(value: string | null): string | undefined {
|
||||||
|
if (!value || !DATE_PATTERN.test(value)) return undefined;
|
||||||
|
const timestamp = Date.parse(`${value}T00:00:00+08:00`);
|
||||||
|
return Number.isFinite(timestamp) ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function boundedValue(value: string | null, maxLength = 200): string | undefined {
|
||||||
|
const normalized = value?.trim();
|
||||||
|
if (!normalized || normalized === 'All') return undefined;
|
||||||
|
return normalized.slice(0, maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueValues(values: string[], limit = 200): string[] {
|
||||||
|
return Array.from(new Set(values.map(value => boundedValue(value)).filter((value): value is string => !!value))).slice(0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSourcePriority(value: string | null): MileageSourceGroup[] {
|
||||||
|
const sources = value
|
||||||
|
?.split(',')
|
||||||
|
.filter((source): source is MileageSourceGroup => source === 'instrument' || source === 'gps');
|
||||||
|
return sources && sources.length > 0 ? Array.from(new Set(sources)).slice(0, 2) : ['instrument'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendValues(params: URLSearchParams, key: string, values: string[]): void {
|
||||||
|
uniqueValues(values).forEach(value => params.append(key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseMileageMonitoringContext(search: string): MileageMonitoringContext {
|
||||||
|
const params = new URLSearchParams(search);
|
||||||
|
const rawSortBy = params.get('mileageSort');
|
||||||
|
const rawSortOrder = params.get('mileageOrder');
|
||||||
|
return {
|
||||||
|
startDate: validDate(params.get('mileageStart')),
|
||||||
|
endDate: validDate(params.get('mileageEnd')),
|
||||||
|
sourcePriority: parseSourcePriority(params.get('mileageSource')),
|
||||||
|
targetNames: uniqueValues(params.getAll('mileageBatch')),
|
||||||
|
region: boundedValue(params.get('mileageRegion')),
|
||||||
|
plates: uniqueValues(params.getAll('mileageFilterPlate')),
|
||||||
|
department: boundedValue(params.get('mileageDepartment')),
|
||||||
|
customer: boundedValue(params.get('mileageCustomer')),
|
||||||
|
project: boundedValue(params.get('mileageProject')),
|
||||||
|
entity: boundedValue(params.get('mileageEntity')),
|
||||||
|
rentStatus: boundedValue(params.get('mileageRentStatus')),
|
||||||
|
brands: uniqueValues(params.getAll('mileageBrand')),
|
||||||
|
platePrefix: boundedValue(params.get('mileagePlatePrefix')),
|
||||||
|
search: boundedValue(params.get('mileageSearch')),
|
||||||
|
mileageMin: boundedValue(params.get('mileageMin'), 32),
|
||||||
|
mileageMax: boundedValue(params.get('mileageMax'), 32),
|
||||||
|
sortBy: rawSortBy === 'total' || rawSortBy === 'statisticTime' ? rawSortBy : 'today',
|
||||||
|
sortOrder: rawSortOrder === 'asc' ? 'asc' : 'desc',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMileageMonitoringUrl(
|
||||||
|
location: LocationParts,
|
||||||
|
context: MileageMonitoringContext,
|
||||||
|
): string {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
CONTEXT_KEYS.forEach(key => params.delete(key));
|
||||||
|
|
||||||
|
if (context.startDate) params.set('mileageStart', context.startDate);
|
||||||
|
if (context.endDate) params.set('mileageEnd', context.endDate);
|
||||||
|
if (context.sourcePriority.length > 0) params.set('mileageSource', context.sourcePriority.join(','));
|
||||||
|
appendValues(params, 'mileageBatch', context.targetNames);
|
||||||
|
if (context.region) params.set('mileageRegion', context.region);
|
||||||
|
appendValues(params, 'mileageFilterPlate', context.plates);
|
||||||
|
if (context.department) params.set('mileageDepartment', context.department);
|
||||||
|
if (context.customer) params.set('mileageCustomer', context.customer);
|
||||||
|
if (context.project) params.set('mileageProject', context.project);
|
||||||
|
if (context.entity) params.set('mileageEntity', context.entity);
|
||||||
|
if (context.rentStatus) params.set('mileageRentStatus', context.rentStatus);
|
||||||
|
appendValues(params, 'mileageBrand', context.brands);
|
||||||
|
if (context.platePrefix) params.set('mileagePlatePrefix', context.platePrefix);
|
||||||
|
if (context.search) params.set('mileageSearch', context.search);
|
||||||
|
if (context.mileageMin) params.set('mileageMin', context.mileageMin);
|
||||||
|
if (context.mileageMax) params.set('mileageMax', context.mileageMax);
|
||||||
|
if (context.sortBy !== 'today') params.set('mileageSort', context.sortBy);
|
||||||
|
if (context.sortOrder !== 'desc') params.set('mileageOrder', context.sortOrder);
|
||||||
|
|
||||||
|
const query = params.toString();
|
||||||
|
return `${location.pathname}${query ? `?${query}` : ''}${location.hash}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user