diff --git a/src/components/MultiSearchSelect.tsx b/src/components/MultiSearchSelect.tsx new file mode 100644 index 0000000..f829f73 --- /dev/null +++ b/src/components/MultiSearchSelect.tsx @@ -0,0 +1,88 @@ +import { useState, useEffect, useMemo, useRef } from 'react'; +import { ChevronDown } from 'lucide-react'; + +export function MultiSearchSelect({ value, onChange, options, placeholder, className }: { + value: string[]; + onChange: (v: string[]) => void; + options: string[]; + placeholder: string; + className?: string; +}) { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(''); + const ref = useRef(null); + + useEffect(() => { + const handler = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); + }; + document.addEventListener('mousedown', handler); + return () => document.removeEventListener('mousedown', handler); + }, []); + + const filtered = useMemo(() => { + if (!query) return options; + const q = query.toLowerCase(); + return options.filter((o) => o.toLowerCase().includes(q)); + }, [options, query]); + + const toggle = (name: string) => { + onChange(value.includes(name) ? value.filter(c => c !== name) : [...value, name]); + }; + + return ( +
+ {value.length > 0 && ( +
+ {value.map(c => ( + + {c} + + + ))} +
+ )} +
setOpen(!open)} + > + 0 ? `已选 ${value.length} 个客户` : placeholder} + value={open ? query : ''} + onChange={(e) => { setQuery(e.target.value); if (!open) setOpen(true); }} + onFocus={() => { setOpen(true); setQuery(''); }} + /> + +
+ {open && ( +
+ {value.length > 0 && ( +
onChange([])} + > + 清除全部已选 +
+ )} + {filtered.map(o => ( +
toggle(o)} + > + + {value.includes(o) && } + + {o} +
+ ))} + {filtered.length === 0 && ( +
无匹配项
+ )} +
+ )} +
+ ); +} diff --git a/src/modules/assets/AssetsModule.tsx b/src/modules/assets/AssetsModule.tsx index e74d988..e10d214 100644 --- a/src/modules/assets/AssetsModule.tsx +++ b/src/modules/assets/AssetsModule.tsx @@ -33,6 +33,7 @@ import type { SummaryData, TypeSummary, VehicleListItem, DeptGroup, RegionGroup, import { fetchSummary, fetchByType, fetchVehicleList, fetchWeeklyDetail, fetchDeptStats, fetchRegionStats, fetchCustomerStats, fetchInventoryStats, fetchRegionChart } from './api'; import type { WeeklyDetailItem } from './api'; import { SearchSelect } from '../../components/SearchSelect'; +import { MultiSearchSelect } from '../../components/MultiSearchSelect'; // --- Constants --- @@ -105,9 +106,9 @@ export default function AssetsModule() { // Customer section state const [expandedCustomers, setExpandedCustomers] = useState>(new Set()); - const [customerFilters, setCustomerFilters] = useState({ customer: '', brand: '', department: '', manager: '', region: '' }); + const [customerFilters, setCustomerFilters] = useState({ customer: [] as string[], brand: '', department: '', manager: '', region: '' }); const [isCustomerFilterOpen, setIsCustomerFilterOpen] = useState(false); - const [draftCustomerFilters, setDraftCustomerFilters] = useState({ customer: '', brand: '', department: '', manager: '', region: '' }); + const [draftCustomerFilters, setDraftCustomerFilters] = useState({ customer: [] as string[], brand: '', department: '', manager: '', region: '' }); // Inventory statistics section state const [inventoryData, setInventoryData] = useState([]); @@ -371,7 +372,7 @@ export default function AssetsModule() { // Derived data for customer section const filteredCustomerStats = useMemo(() => customerData.filter((s) => { - const mc = !customerFilters.customer || s.customer === customerFilters.customer; + const mc = customerFilters.customer.length === 0 || customerFilters.customer.includes(s.customer); const mb = !customerFilters.brand || s.brand === customerFilters.brand; const md = !customerFilters.department || s.department === customerFilters.department; const mm = !customerFilters.manager || s.manager === customerFilters.manager; @@ -2148,14 +2149,14 @@ export default function AssetsModule() { @@ -2176,14 +2177,14 @@ export default function AssetsModule() {

数据筛选

- +
- setDraftCustomerFilters(prev => ({ ...prev, customer: v }))} options={uniqueCustomerNames} placeholder="所有客户" className="text-xs py-2 px-2" /> + setDraftCustomerFilters(prev => ({ ...prev, customer: v }))} options={uniqueCustomerNames} placeholder="所有客户" className="text-xs py-2 px-2" />
@@ -2233,12 +2234,12 @@ export default function AssetsModule() {
- {Object.values(customerFilters).some(v => v !== '') && ( + {Object.values(customerFilters).some(v => Array.isArray(v) ? v.length > 0 : v !== '') && (
- {customerFilters.customer && ( + {customerFilters.customer.length > 0 && ( - 客户: {customerFilters.customer} - + 客户: {customerFilters.customer.join(', ')} + )} {customerFilters.manager && ( @@ -2265,7 +2266,7 @@ export default function AssetsModule() { )} - +
)} diff --git a/src/server/routes/mileage.ts b/src/server/routes/mileage.ts index 663f9e3..741daba 100644 --- a/src/server/routes/mileage.ts +++ b/src/server/routes/mileage.ts @@ -380,6 +380,23 @@ app.get('/targets', async (c) => { periodsMap.set(p.target_id, list); } + // 使用监控缓存的里程数据(与里程看板一致) + const cacheVehicleMap = new Map(); + if (monitoringCache) { + for (const v of monitoringCache.vehicles) { + cacheVehicleMap.set(v.plate, Math.max(0, v.dailyKm || 0)); + } + } + const [targetVehicleRows] = await pool.execute( + `SELECT target_id, plate_number FROM tab_mileage_assessment_vehicle WHERE is_deleted = 0` + ) as any; + const targetIdPlatesMap = new Map(); + for (const r of targetVehicleRows) { + const list = targetIdPlatesMap.get(r.target_id) || []; + list.push(r.plate_number); + targetIdPlatesMap.set(r.target_id, list); + } + const now = new Date(); const result = targets.map((t: any) => { const s = statsMap.get(t.id) || {}; @@ -409,7 +426,7 @@ app.get('/targets', async (c) => { annualMileagePerVehicle: Number(t.annual_mileage_per_vehicle), assessmentYears: t.assessment_years, periods, - todayTotal: Number(s.today_total) || 0, + todayTotal: (targetIdPlatesMap.get(t.id) || []).reduce((sum: number, plate: string) => sum + (cacheVehicleMap.get(plate) || 0), 0), cumulativeTotal: Number(s.cumulative_total) || 0, avgCompletion: (Number(s.avg_completion) || 0) * 100, qualifiedCount: Number(s.qualified_count) || 0,