diff --git a/src/modules/mileage/MonitoringView.tsx b/src/modules/mileage/MonitoringView.tsx index e5bb6a2..43cc730 100644 --- a/src/modules/mileage/MonitoringView.tsx +++ b/src/modules/mileage/MonitoringView.tsx @@ -33,7 +33,7 @@ const SearchableSelect = ({ setIsOpen(true)} onChange={(e) => setSearch(e.target.value)} @@ -73,7 +73,7 @@ const SearchableSelect = ({ setIsOpen(false); }} > - {opt} + {opt === '__EMPTY__' ? '无值' : opt} ))} {filtered.length === 0 && ( @@ -95,6 +95,10 @@ export default function MonitoringView() { const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); const [isFilterOpen, setIsFilterOpen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); + const [fullscreenVehicles, setFullscreenVehicles] = useState([]); + const [fullscreenStats, setFullscreenStats] = useState({ totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 }); + const [fullscreenRefresh, setFullscreenRefresh] = useState(0); + const [fullscreenLoading, setFullscreenLoading] = useState(false); // New filters from image const [filterPlate, setFilterPlate] = useState('All'); @@ -183,6 +187,12 @@ export default function MonitoringView() { loadFirstPage(); }, [loadFirstPage]); + // 每分钟自动刷新 + useEffect(() => { + const timer = setInterval(loadFirstPage, 60 * 1000); + return () => clearInterval(timer); + }, [loadFirstPage]); + // 触底检测:用 IntersectionObserver 监听哨兵元素 const loadMoreRef = useRef(loadMore); loadMoreRef.current = loadMore; @@ -226,6 +236,28 @@ export default function MonitoringView() { setIsFullscreen(!isFullscreen); }; + // 全屏时加载全部数据(无分页),筛选变化时重新加载 + useEffect(() => { + if (!isFullscreen) return; + setFullscreenLoading(true); + fetchMonitoring({ + sortBy, + sortOrder, + limit: 9999, + page: 1, + search: searchTerm || undefined, + dept: filterDept !== 'All' ? filterDept : undefined, + customer: filterCustomer !== 'All' ? filterCustomer : undefined, + rentStatus: filterRentStatus !== 'All' ? filterRentStatus : undefined, + plate: filterPlate !== 'All' ? filterPlate : undefined, + date: filterDate || undefined, + }).then(d => { + setFullscreenVehicles(d.vehicles); + setFullscreenStats(d.stats); + setFilterOptions(d.filters); + }).catch(() => {}).finally(() => setFullscreenLoading(false)); + }, [isFullscreen, sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterRentStatus, filterPlate, filterDate, fullscreenRefresh]); + // 全屏时禁止背景滚动 useEffect(() => { if (isFullscreen) { @@ -258,19 +290,19 @@ export default function MonitoringView() {

全屏监控

- 今日 {Math.round(stats.totalToday).toLocaleString()} km + 今日 {Math.round(fullscreenStats.totalToday).toLocaleString()} km | - 累计 {Math.round(stats.totalAll).toLocaleString()} km + 累计 {Math.round(fullscreenStats.totalAll).toLocaleString()} km | - 车辆 {stats.vehicleCount} + 车辆 {fullscreenStats.vehicleCount} | - {(stats.vehicleCount > 0 ? (sortBy === 'today' ? stats.totalToday : stats.totalAll) / stats.vehicleCount : 0).toFixed(0)} km + {(fullscreenStats.vehicleCount > 0 ? (sortBy === 'today' ? fullscreenStats.totalToday : fullscreenStats.totalAll) / fullscreenStats.vehicleCount : 0).toFixed(0)} km
@@ -300,7 +332,15 @@ export default function MonitoringView() {
-
+
+ {fullscreenLoading && ( +
+
+ + 加载中... +
+
+ )} @@ -327,6 +367,7 @@ export default function MonitoringView() { onChange={(e) => setFilterCustomer(e.target.value)} > + {filterOptions.customers.map(p => )} @@ -353,6 +394,7 @@ export default function MonitoringView() { onChange={(e) => setFilterDept(e.target.value)} > + {departments.map(d => )} @@ -396,7 +438,7 @@ export default function MonitoringView() { - {filteredVehicles.map((v) => ( + {fullscreenVehicles.map((v) => ( @@ -478,13 +520,13 @@ export default function MonitoringView() {
setFilterDept(e.target.value)} > + {departments.map(d => )}
@@ -658,8 +701,8 @@ export default function MonitoringView() { {(() => { const tags: { label: string; onClear: () => void }[] = []; if (filterRentStatus !== 'All') tags.push({ label: `状态: ${filterRentStatus}`, onClear: () => setFilterRentStatus('All') }); - if (filterDept !== 'All') tags.push({ label: `部门: ${filterDept}`, onClear: () => setFilterDept('All') }); - if (filterCustomer !== 'All') tags.push({ label: `客户: ${filterCustomer}`, onClear: () => setFilterCustomer('All') }); + if (filterDept !== 'All') tags.push({ label: `部门: ${filterDept === '__EMPTY__' ? '无值' : filterDept}`, onClear: () => setFilterDept('All') }); + if (filterCustomer !== 'All') tags.push({ label: `客户: ${filterCustomer === '__EMPTY__' ? '无值' : filterCustomer}`, onClear: () => setFilterCustomer('All') }); if (filterProject !== 'All') tags.push({ label: `项目: ${filterProject}`, onClear: () => setFilterProject('All') }); if (filterEntity !== 'All') tags.push({ label: `主体: ${filterEntity}`, onClear: () => setFilterEntity('All') }); if (filterPlate !== 'All') tags.push({ label: `车牌: ${filterPlate}`, onClear: () => setFilterPlate('All') }); @@ -670,7 +713,7 @@ export default function MonitoringView() { if (filterDate) tags.push({ label: `日期: ${filterDate}`, onClear: () => setFilterDate('') }); if (tags.length === 0) return null; const clearAll = () => { - setFilterDept('All'); setFilterCustomer('All'); setFilterProject('All'); setFilterEntity('All'); + setFilterDept('All'); setFilterCustomer('All'); setFilterRentStatus('All'); setFilterProject('All'); setFilterEntity('All'); setFilterPlate('All'); setSearchTerm(''); setFilterRegionCode('All'); setFilterMileageRange({ min: '', max: '' }); setAppliedMileageRange({ min: '', max: '' }); setFilterDate(''); @@ -763,17 +806,14 @@ export default function MonitoringView() { )}
- {v.isDataSynced ? <>{v.dailyKm?.toLocaleString()} km : '-'} + {v.isDataSynced ? <>{Math.max(0, v.dailyKm || 0).toLocaleString()} km : 未对接}
- {v.isDataSynced && v.totalKm != null ? `${v.totalKm.toLocaleString()} km` : '-'} + {v.isDataSynced && v.totalKm != null ? `${v.totalKm.toLocaleString()} km` : '未对接'} - {!v.isDataSynced && ( - 未同步 - )}
diff --git a/src/modules/mileage/StatisticsView.tsx b/src/modules/mileage/StatisticsView.tsx index bb437cb..77a95a5 100644 --- a/src/modules/mileage/StatisticsView.tsx +++ b/src/modules/mileage/StatisticsView.tsx @@ -7,11 +7,17 @@ import { } from 'recharts'; import { Truck, ChevronDown, Maximize2, Minimize2, - Search, ArrowUpDown, X, + Search, ArrowUpDown, X, RotateCcw, Calendar, } from 'lucide-react'; import type { TargetSummary, TargetVehicle, TrendPoint } from './types'; import { fetchTargets, fetchTargetVehicles, fetchTrend } from './api'; +function getDefaultDate(): string { + const now = new Date(); + if (now.getHours() < 5) now.setDate(now.getDate() - 1); + return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; +} + function fmtKm(value: number): string { if (value >= 10000) return (value / 10000).toFixed(2) + '万'; return value.toLocaleString(); @@ -44,6 +50,8 @@ export default function StatisticsView() { const [viewAllTargetName, setViewAllTargetName] = useState(''); const [viewAllSearch, setViewAllSearch] = useState(''); const [viewAllSort, setViewAllSort] = useState<'asc' | 'desc'>('desc'); + const [viewAllDate, setViewAllDate] = useState(getDefaultDate); + const [viewAllLoading, setViewAllLoading] = useState(false); // Load targets on mount useEffect(() => { @@ -61,10 +69,19 @@ export default function StatisticsView() { fetchTrend(selectedTargetId).then(setTrendData).catch(() => setTrendData([])); }, [selectedTargetId]); + // Re-fetch target vehicles when viewAllDate changes + useEffect(() => { + if (viewAllTargetId === null) return; + setViewAllLoading(true); + fetchTargetVehicles(viewAllTargetId, viewAllDate).then(data => { + setTargetVehiclesMap(prev => ({ ...prev, [viewAllTargetId]: data })); + }).catch(() => {}).finally(() => setViewAllLoading(false)); + }, [viewAllTargetId, viewAllDate]); + return (
- {/* Project Selector - Full width even in landscape */} -
+ {/* Project Selector */} +
{targets.map(target => (
@@ -203,7 +225,7 @@ export default function StatisticsView() { {targets.map((target, idx) => (
{ const name = target.targetName; setExpandedModel(expandedModel === name ? null : name); @@ -216,13 +238,13 @@ export default function StatisticsView() { >
-
+
- {target.targetName} - {target.vehicleCount}台 + {target.targetName} + {target.vehicleCount}台
@@ -231,14 +253,14 @@ export default function StatisticsView() {
达标: - {target.yearQualifiedCount}台 + {target.yearQualifiedCount}台
-
+
{fmtKm(target.todayTotal)} KM
@@ -262,28 +284,28 @@ export default function StatisticsView() { exit={{ height: 0, opacity: 0 }} className="overflow-hidden" > -
+

考核区间

{target.periods.map((p, i) => ( -

{p}

+

{p}

))}

总考核里程

-

{fmtKm(target.totalMileagePerVehicle * target.vehicleCount)} km

+

{fmtKm(target.totalMileagePerVehicle * target.vehicleCount)} km

年考核任务/辆

-

{fmtKm(target.annualMileagePerVehicle)} km

+

{fmtKm(target.annualMileagePerVehicle)} km

50%达标数

-

{target.halfQualifiedCount} 台

+

{target.halfQualifiedCount} 台

本年需完成

-

{fmtKm(target.currentYearTarget)} km

+

{fmtKm(target.currentYearTarget)} km

已完成(截止3.31)

@@ -297,9 +319,9 @@ export default function StatisticsView() {

日均需完成

{fmtKm(target.dailyTarget)} km

-
+
剩余考核天数 - {target.daysLeft} 天 + {target.daysLeft} 天
{/* Vehicle List Detail */} @@ -311,11 +333,7 @@ export default function StatisticsView() { e.stopPropagation(); setViewAllTargetId(target.id); setViewAllTargetName(target.targetName); - if (!targetVehiclesMap[target.id]) { - fetchTargetVehicles(target.id).then(data => { - setTargetVehiclesMap(prev => ({ ...prev, [target.id]: data })); - }).catch(() => {}); - } + setViewAllDate(getDefaultDate()); }} className="text-[8px] text-blue-500 font-bold hover:underline" > @@ -324,15 +342,15 @@ export default function StatisticsView() {
{(targetVehiclesMap[target.id] || []).slice(0, 5).map(tv => ( -
+
- {tv.plateNumber} - + {tv.plateNumber} + 在线
- {tv.todayMileage} + {tv.todayMileage} KM
@@ -356,115 +374,96 @@ export default function StatisticsView() { initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} - className="fixed inset-0 z-[100] bg-slate-950 flex flex-col p-4 landscape:flex-row gap-4 overflow-hidden" + className="fixed inset-0 z-[100] bg-slate-950 flex flex-col overflow-hidden" > - {/* Sidebar with KPI Cards */} -
-
-
-
-

车型考核汇总

+ {/* Top bar: compact inline KPI */} +
+
+
+
+

车型考核汇总

+
+ 今日 {fmtKm(targets.reduce((sum, t) => sum + t.todayTotal, 0))} km + | + 累计 {fmtKm(targets.reduce((sum, t) => sum + t.cumulativeTotal, 0))} km + | + 车辆 {targets.reduce((sum, t) => sum + t.vehicleCount, 0)} + | + 完成率 {targets.length > 0 ? (targets.reduce((sum, t) => sum + t.avgCompletion, 0) / targets.length).toFixed(1) : '0.0'} % +
+
+
+
- -
-
-
今日总里程
-
- {fmtKm(targets.reduce((sum, t) => sum + t.todayTotal, 0))} - KM -
-
-
-
累计总里程
-
- {fmtKm(targets.reduce((sum, t) => sum + t.cumulativeTotal, 0))} - KM -
-
-
-
总考核车辆
-
- {targets.reduce((sum, t) => sum + t.vehicleCount, 0)} - -
-
-
-
平均完成率
-
- {targets.length > 0 ? (targets.reduce((sum, t) => sum + t.avgCompletion, 0) / targets.length).toFixed(1) : '0.0'} - % -
-
-
-
-
- 车型考核明细数据 - 最后更新: {new Date().toLocaleTimeString()} -
- -
-
@@ -407,12 +449,12 @@ export default function MonitoringView() {
{v.department || '-'} - {v.isDataSynced ? <>{v.dailyKm?.toLocaleString()} km : '-'} + {v.isDataSynced ? <>{Math.max(0, v.dailyKm || 0).toLocaleString()} km : 未对接} - {v.isDataSynced && v.totalKm != null ? <>{v.totalKm.toLocaleString()} km : '-'} + {v.isDataSynced && v.totalKm != null ? <>{v.totalKm.toLocaleString()} km : 未对接}
- - - - - - - - - - - - - - - - - - - - - {targets.map((target, idx) => ( - - - - - - + + + + + + + + + + + ))} + +
车型车辆数总考核里程已行驶总里程总完成率考核区间年考核任务/辆达标车辆数50%达标数今日总里程本年需完成已完成(截止3.31)未完成总数剩余天数日均需完成
{target.targetName}{target.vehicleCount}{fmtKm(target.totalMileagePerVehicle * target.vehicleCount)} km{fmtKm(target.cumulativeTotal)} km -
-
-
= 90 ? 'bg-emerald-500' : target.avgCompletion >= 80 ? 'bg-blue-500' : 'bg-amber-500'}`} - style={{ width: `${target.avgCompletion}%` }} - /> -
- {target.avgCompletion.toFixed(1)}% + {/* Table Area */} +
+ + + + + + + + + + + + + + + + + + + {targets.map((target, idx) => ( + + + + - - - - - - - - - - - - ))} - -
车型台数完成进度年任务/辆达标50%达标今日里程本年目标已完成未完成余天日均需完成
+
{target.targetName}
+
{target.periods.map((p, i) => {p})}
+
{target.vehicleCount} +
+
+
= 90 ? 'bg-emerald-500' : target.avgCompletion >= 50 ? 'bg-amber-500' : 'bg-amber-500/60'}`} + style={{ width: `${Math.min(target.avgCompletion, 100)}%` }} + />
-
{target.periods.join('\n')}{fmtKm(target.annualMileagePerVehicle)} km{target.yearQualifiedCount}{target.halfQualifiedCount}{fmtKm(target.todayTotal)} km{fmtKm(target.currentYearTarget)} km{fmtKm(target.currentYearCompleted)} km{fmtKm(target.remaining)} km{target.daysLeft}{target.dailyTarget} km
-
+ {target.avgCompletion.toFixed(1)}% +
+
+ {fmtKm(target.cumulativeTotal)} + / {fmtKm(target.totalMileagePerVehicle * target.vehicleCount)} km +
+
{fmtKm(target.annualMileagePerVehicle)} km{target.yearQualifiedCount}{target.halfQualifiedCount}{fmtKm(target.todayTotal)} km{fmtKm(target.currentYearTarget)} km{fmtKm(target.currentYearCompleted)} km{fmtKm(target.remaining)} km{target.daysLeft}{fmtKm(target.dailyTarget)} km
)} @@ -504,19 +503,36 @@ export default function StatisticsView() {
-
-
- - setViewAllSearch(e.target.value)} - className="w-full pl-9 pr-4 py-2 bg-slate-50 border border-slate-100 rounded-xl text-xs font-bold focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all" - /> +
+
+
+ + setViewAllSearch(e.target.value)} + className="w-full pl-9 pr-3 py-2 bg-slate-50 border border-slate-100 rounded-xl text-xs font-bold focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all" + /> +
+
+ + setViewAllDate(e.target.value)} + className="pl-8 pr-2 py-2 bg-slate-50 border border-slate-100 rounded-xl text-xs font-bold text-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all w-[130px]" + /> +
- 排序方式: 今日里程 + + {viewAllLoading ? '加载中...' : (() => { + const filtered = (viewAllTargetId !== null ? (targetVehiclesMap[viewAllTargetId] || []) : []).filter(tv => tv.plateNumber.toLowerCase().includes(viewAllSearch.toLowerCase())); + const totalKm = filtered.reduce((sum, tv) => sum + (tv.todayMileage || 0), 0); + return `${filtered.length} 辆 · 合计 ${fmtKm(totalKm)} km`; + })()} +