fix(mileage): 交投高里程车辆标红
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -12,6 +12,12 @@ import PlateMultiSelect from './PlateMultiSelect';
|
|||||||
import { exportMileageXlsx } from './xlsx-export';
|
import { exportMileageXlsx } from './xlsx-export';
|
||||||
import VehicleDetailModal from './VehicleDetailModal';
|
import VehicleDetailModal from './VehicleDetailModal';
|
||||||
|
|
||||||
|
const HIGH_MILEAGE_ALERT_TARGETS = new Set([
|
||||||
|
'交投40辆4.5T普货',
|
||||||
|
'交投190辆4.5T冷链车',
|
||||||
|
]);
|
||||||
|
const HIGH_MILEAGE_ALERT_KM = 800;
|
||||||
|
|
||||||
const SearchableSelect = ({
|
const SearchableSelect = ({
|
||||||
options,
|
options,
|
||||||
value,
|
value,
|
||||||
@@ -137,6 +143,12 @@ export default function MonitoringView() {
|
|||||||
const departments = filterOptions.departments;
|
const departments = filterOptions.departments;
|
||||||
const plateNumbers = filterOptions.plates;
|
const plateNumbers = filterOptions.plates;
|
||||||
|
|
||||||
|
const isHighMileageAlert = useCallback((v: MonitoringVehicle) => {
|
||||||
|
const inAlertTarget = v.targetNames?.some(name => HIGH_MILEAGE_ALERT_TARGETS.has(name))
|
||||||
|
|| HIGH_MILEAGE_ALERT_TARGETS.has(filterTargetName);
|
||||||
|
return inAlertTarget && Math.max(0, v.dailyKm || 0) >= HIGH_MILEAGE_ALERT_KM;
|
||||||
|
}, [filterTargetName]);
|
||||||
|
|
||||||
// 加载首页数据
|
// 加载首页数据
|
||||||
const loadFirstPage = useCallback(() => {
|
const loadFirstPage = useCallback(() => {
|
||||||
setPageLoading(true);
|
setPageLoading(true);
|
||||||
@@ -525,7 +537,9 @@ export default function MonitoringView() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-800/30">
|
<tbody className="divide-y divide-slate-800/30">
|
||||||
{fullscreenVehicles.map((v) => (
|
{fullscreenVehicles.map((v) => {
|
||||||
|
const highMileageAlert = isHighMileageAlert(v);
|
||||||
|
return (
|
||||||
<tr key={v.plate} className="hover:bg-slate-800/20 transition-colors">
|
<tr key={v.plate} className="hover:bg-slate-800/20 transition-colors">
|
||||||
<td className="px-3 py-2 text-center">
|
<td className="px-3 py-2 text-center">
|
||||||
<div className={`w-2 h-2 rounded-full mx-auto ${v.isOnline ? 'bg-green-500 shadow-[0_0_6px_rgba(34,197,94,0.4)]' : (v.isDataSynced || v.totalKm != null) ? 'bg-slate-600' : 'bg-amber-400 animate-pulse'}`}></div>
|
<div className={`w-2 h-2 rounded-full mx-auto ${v.isOnline ? 'bg-green-500 shadow-[0_0_6px_rgba(34,197,94,0.4)]' : (v.isDataSynced || v.totalKm != null) ? 'bg-slate-600' : 'bg-amber-400 animate-pulse'}`}></div>
|
||||||
@@ -535,8 +549,8 @@ export default function MonitoringView() {
|
|||||||
<td className="px-3 py-2 text-[11px] text-slate-400">{v.rentStatus || '-'}</td>
|
<td className="px-3 py-2 text-[11px] text-slate-400">{v.rentStatus || '-'}</td>
|
||||||
<td className="px-3 py-2 text-[11px] text-slate-400">{v.department || '-'}</td>
|
<td className="px-3 py-2 text-[11px] text-slate-400">{v.department || '-'}</td>
|
||||||
<td className="px-3 py-2 text-right">
|
<td className="px-3 py-2 text-right">
|
||||||
<span className={`text-xs font-mono font-bold ${(v.isDataSynced || v.totalKm != null) ? 'text-blue-400' : 'text-amber-400'}`}>
|
<span className={`text-xs font-mono font-bold ${(v.isDataSynced || v.totalKm != null) ? (highMileageAlert ? 'text-red-400' : 'text-blue-400') : 'text-amber-400'}`}>
|
||||||
{(v.isDataSynced || v.totalKm != null) ? <>{Math.max(0, v.dailyKm || 0).toLocaleString()} <span className="text-[8px] text-slate-500">km</span></> : <span className="text-[8px] text-amber-500/50">未对接</span>}
|
{(v.isDataSynced || v.totalKm != null) ? <>{Math.max(0, v.dailyKm || 0).toLocaleString()} <span className={`text-[8px] ${highMileageAlert ? 'text-red-400/70' : 'text-slate-500'}`}>km</span></> : <span className="text-[8px] text-amber-500/50">未对接</span>}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-right">
|
<td className="px-3 py-2 text-right">
|
||||||
@@ -545,7 +559,8 @@ export default function MonitoringView() {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -896,7 +911,9 @@ export default function MonitoringView() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-1.5">
|
<div className="grid grid-cols-1 gap-1.5">
|
||||||
{filteredVehicles.map((v) => (
|
{filteredVehicles.map((v) => {
|
||||||
|
const highMileageAlert = isHighMileageAlert(v);
|
||||||
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
@@ -930,8 +947,8 @@ export default function MonitoringView() {
|
|||||||
<div className="w-2 h-2 rounded-full bg-amber-400 animate-pulse" title="未对接车机数据"></div>
|
<div className="w-2 h-2 rounded-full bg-amber-400 animate-pulse" title="未对接车机数据"></div>
|
||||||
)}
|
)}
|
||||||
<span className="text-[7px] font-black text-blue-600/40 bg-blue-50 w-3 h-3 rounded flex items-center justify-center leading-none">今</span>
|
<span className="text-[7px] font-black text-blue-600/40 bg-blue-50 w-3 h-3 rounded flex items-center justify-center leading-none">今</span>
|
||||||
<div className={`text-sm font-black leading-none ${(v.isDataSynced || v.totalKm != null) ? 'text-blue-600' : 'text-amber-600'}`}>
|
<div className={`text-sm font-black leading-none ${(v.isDataSynced || v.totalKm != null) ? (highMileageAlert ? 'text-red-600' : 'text-blue-600') : 'text-amber-600'}`}>
|
||||||
{(v.isDataSynced || v.totalKm != null) ? <>{Math.max(0, v.dailyKm || 0).toLocaleString()} <span className="text-[8px] text-slate-400">km</span></> : <span className="text-[7px] text-amber-500/70">未对接</span>}
|
{(v.isDataSynced || v.totalKm != null) ? <>{Math.max(0, v.dailyKm || 0).toLocaleString()} <span className={`text-[8px] ${highMileageAlert ? 'text-red-400' : 'text-slate-400'}`}>km</span></> : <span className="text-[7px] text-amber-500/70">未对接</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@@ -942,7 +959,8 @@ export default function MonitoringView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filteredVehicles.length === 0 && !loadingMore && (
|
{filteredVehicles.length === 0 && !loadingMore && (
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface MonitoringVehicle {
|
|||||||
entity: string | null;
|
entity: string | null;
|
||||||
project: string | null;
|
project: string | null;
|
||||||
region: string | null;
|
region: string | null;
|
||||||
|
targetNames: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MonitoringStats {
|
export interface MonitoringStats {
|
||||||
|
|||||||
@@ -65,6 +65,41 @@ interface MileageRow {
|
|||||||
source: string;
|
source: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TargetRow {
|
||||||
|
id: number;
|
||||||
|
target_name: string;
|
||||||
|
plate_number: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchTargetRows(): Promise<TargetRow[]> {
|
||||||
|
return pool.execute(
|
||||||
|
`SELECT t.id, t.target_name, v.plate_number
|
||||||
|
FROM tab_mileage_assessment_target t
|
||||||
|
JOIN tab_mileage_assessment_vehicle v ON v.target_id = t.id AND v.is_deleted = 0
|
||||||
|
WHERE t.is_deleted = 0`
|
||||||
|
).then(([rows]) => rows as TargetRow[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTargetPlatesMap(targetRows: TargetRow[]): Map<string, Set<string>> {
|
||||||
|
const targetPlatesMap = new Map<string, Set<string>>();
|
||||||
|
for (const r of targetRows) {
|
||||||
|
const set = targetPlatesMap.get(r.target_name) || new Set();
|
||||||
|
set.add(r.plate_number);
|
||||||
|
targetPlatesMap.set(r.target_name, set);
|
||||||
|
}
|
||||||
|
return targetPlatesMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPlateTargetNamesMap(targetRows: TargetRow[]): Map<string, string[]> {
|
||||||
|
const map = new Map<string, string[]>();
|
||||||
|
for (const r of targetRows) {
|
||||||
|
const list = map.get(r.plate_number) || [];
|
||||||
|
list.push(r.target_name);
|
||||||
|
map.set(r.plate_number, list);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchBizTotalMileageMap(): Promise<Map<string, number>> {
|
async function fetchBizTotalMileageMap(): Promise<Map<string, number>> {
|
||||||
// v_vehicle_daily_stats.total_km 对 G7S 数据源常为 NULL(G7 只回传日增量),
|
// v_vehicle_daily_stats.total_km 对 G7S 数据源常为 NULL(G7 只回传日增量),
|
||||||
// 业务库 tab_mileage_assessment_vehicle.vehicle_total_mileage 是累加后的权威累计值,
|
// 业务库 tab_mileage_assessment_vehicle.vehicle_total_mileage 是累加后的权威累计值,
|
||||||
@@ -115,6 +150,7 @@ function mergeVehicles(
|
|||||||
yesterdayMap: Map<string, number>,
|
yesterdayMap: Map<string, number>,
|
||||||
bizTotalMap: Map<string, number>,
|
bizTotalMap: Map<string, number>,
|
||||||
latestPgTotalMap: Map<string, number>,
|
latestPgTotalMap: Map<string, number>,
|
||||||
|
targetNamesByPlate: Map<string, string[]>,
|
||||||
): CachedVehicle[] {
|
): CachedVehicle[] {
|
||||||
const mileageMap = new Map<string, MileageRow>();
|
const mileageMap = new Map<string, MileageRow>();
|
||||||
for (const row of mileageRows) {
|
for (const row of mileageRows) {
|
||||||
@@ -147,6 +183,7 @@ function mergeVehicles(
|
|||||||
entity: info?.entity || null,
|
entity: info?.entity || null,
|
||||||
project: info?.project || null,
|
project: info?.project || null,
|
||||||
region: regionMap[m.plate] || null,
|
region: regionMap[m.plate] || null,
|
||||||
|
targetNames: targetNamesByPlate.get(m.plate) || [],
|
||||||
yesterdayKm: yesterdayMap.get(m.plate) || 0,
|
yesterdayKm: yesterdayMap.get(m.plate) || 0,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -184,25 +221,16 @@ export async function refreshMonitoringCache(): Promise<void> {
|
|||||||
return map;
|
return map;
|
||||||
})(),
|
})(),
|
||||||
fetchVehicleInfoMap(),
|
fetchVehicleInfoMap(),
|
||||||
pool.execute(
|
fetchTargetRows(),
|
||||||
`SELECT t.id, t.target_name, v.plate_number
|
|
||||||
FROM tab_mileage_assessment_target t
|
|
||||||
JOIN tab_mileage_assessment_vehicle v ON v.target_id = t.id AND v.is_deleted = 0
|
|
||||||
WHERE t.is_deleted = 0`
|
|
||||||
).then(([rows]) => rows as { id: number; target_name: string; plate_number: string }[]),
|
|
||||||
fetchBizTotalMileageMap(),
|
fetchBizTotalMileageMap(),
|
||||||
fetchLatestPgTotalMileageMap(),
|
fetchLatestPgTotalMileageMap(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const targetPlatesMap = new Map<string, Set<string>>();
|
const targetPlatesMap = buildTargetPlatesMap(targetRows);
|
||||||
for (const r of targetRows) {
|
const targetNamesByPlate = buildPlateTargetNamesMap(targetRows);
|
||||||
const set = targetPlatesMap.get(r.target_name) || new Set();
|
|
||||||
set.add(r.plate_number);
|
|
||||||
targetPlatesMap.set(r.target_name, set);
|
|
||||||
}
|
|
||||||
const targetNames = Array.from(targetPlatesMap.keys());
|
const targetNames = Array.from(targetPlatesMap.keys());
|
||||||
|
|
||||||
const vehicles = mergeVehicles(mileageRows, infoMap, yesterdayMap, bizTotalMap, latestPgTotalMap);
|
const vehicles = mergeVehicles(mileageRows, infoMap, yesterdayMap, bizTotalMap, latestPgTotalMap, targetNamesByPlate);
|
||||||
const totalToday = vehicles.reduce((sum, v) => sum + v.dailyKm, 0);
|
const totalToday = vehicles.reduce((sum, v) => sum + v.dailyKm, 0);
|
||||||
const totalAll = vehicles.reduce((sum, v) => sum + (v.totalKm || 0), 0);
|
const totalAll = vehicles.reduce((sum, v) => sum + (v.totalKm || 0), 0);
|
||||||
|
|
||||||
@@ -221,7 +249,7 @@ export async function refreshMonitoringCache(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function queryDateMileage(dateStr: string): Promise<CachedVehicle[]> {
|
export async function queryDateMileage(dateStr: string): Promise<CachedVehicle[]> {
|
||||||
const [mileageRows, yesterdayRows, infoMap, bizTotalMap, latestPgTotalMap] = await Promise.all([
|
const [mileageRows, yesterdayRows, infoMap, targetRows, bizTotalMap, latestPgTotalMap] = await Promise.all([
|
||||||
mileagePool.execute(
|
mileagePool.execute(
|
||||||
'SELECT plate, vin, daily_km, total_km, source FROM v_vehicle_daily_stats WHERE stat_date = ?',
|
'SELECT plate, vin, daily_km, total_km, source FROM v_vehicle_daily_stats WHERE stat_date = ?',
|
||||||
[dateStr]
|
[dateStr]
|
||||||
@@ -231,6 +259,7 @@ export async function queryDateMileage(dateStr: string): Promise<CachedVehicle[]
|
|||||||
[dateStr]
|
[dateStr]
|
||||||
).then(([r]) => r as { plate: string; daily_km: string }[]),
|
).then(([r]) => r as { plate: string; daily_km: string }[]),
|
||||||
fetchVehicleInfoMap(),
|
fetchVehicleInfoMap(),
|
||||||
|
fetchTargetRows(),
|
||||||
fetchBizTotalMileageMap(),
|
fetchBizTotalMileageMap(),
|
||||||
fetchLatestPgTotalMileageMap(dateStr),
|
fetchLatestPgTotalMileageMap(dateStr),
|
||||||
]);
|
]);
|
||||||
@@ -242,7 +271,14 @@ export async function queryDateMileage(dateStr: string): Promise<CachedVehicle[]
|
|||||||
if (km > existing) yesterdayMap.set(r.plate, km);
|
if (km > existing) yesterdayMap.set(r.plate, km);
|
||||||
}
|
}
|
||||||
|
|
||||||
return mergeVehicles(mileageRows, infoMap, yesterdayMap, bizTotalMap, latestPgTotalMap);
|
return mergeVehicles(
|
||||||
|
mileageRows,
|
||||||
|
infoMap,
|
||||||
|
yesterdayMap,
|
||||||
|
bizTotalMap,
|
||||||
|
latestPgTotalMap,
|
||||||
|
buildPlateTargetNamesMap(targetRows),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildDateFilters(vehicles: CachedVehicle[]): MonitoringFilters {
|
export function buildDateFilters(vehicles: CachedVehicle[]): MonitoringFilters {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface CachedVehicle {
|
|||||||
entity: string | null;
|
entity: string | null;
|
||||||
project: string | null;
|
project: string | null;
|
||||||
region: string | null;
|
region: string | null;
|
||||||
|
targetNames: string[];
|
||||||
yesterdayKm: number;
|
yesterdayKm: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user