import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { ArrowLeft, BarChart3, Filter, Loader2, Search, X } from 'lucide-react'; import { fetchJson } from '../../../../auth/api-client'; import Blur from '../../../../components/Blur'; import type { AssetAnalysisResponse, AssetAnalysisDimension } from '../../../../shared/assets/analytics'; import { ANALYSIS_META, DIMENSION_LABELS, compareCustomLabels, getCustomAnalysis, getAvailableDimensions, getDefaultDimensions, getDimensionValueLabel, type AnalysisFilterValues, type CustomAnalysisRow, type AnalysisSort, type AnalysisView } from '../../analytics-model'; import { dimensionChildCounts, dimensionCountUnit, dimensionParentTotals, dimensionRowSpans, orderDimensionGroups, visibleDimensionRowSpans } from '../../dimension-layout'; import { AnalysisControls } from './AnalysisControls'; import { DimensionPicker } from './DimensionPicker'; import { ProgressiveRowsStatus } from './ProgressiveRowsStatus'; import { useProgressiveRows } from './useProgressiveRows'; import { buildVehicleModalRequest, formatLocalDateTime, type VehicleModalSelection } from '../../model'; import { fetchWeeklyDetail, type WeeklyDetailItem } from '../../api'; import type { VehicleListItem } from '../../types'; import { AbnormalVehicleDetails, PendingVehicleDetails } from '../AbnormalVehicleDetails'; import { useDialogFocus } from '../useDialogFocus'; import { AnalysisExportButton } from './AnalysisExportButton'; import { MobileAnalysisControls } from './MobileAnalysisControls'; import { MobileGroupedTable } from './MobileGroupedTable'; import { downloadAnalysisWorkbook, statisticsExportRows, vehicleExportRows } from '../../analysis-export'; import { useMobileLayout } from '../../useMobileLayout'; import './asset-analysis.css'; import './mobile-analysis.css'; type Props = { selection: VehicleModalSelection | null; subject: string | null; close: () => void }; const number = (value: number) => value.toLocaleString('zh-CN'); function initialView(selection: VehicleModalSelection): AnalysisView { if (selection.category === 'Abnormal') return 'abnormal'; if (selection.category === 'Inventory') return 'inventory'; if (selection.category === 'Pending') return 'pending'; if (selection.source === 'department') return 'department'; if (selection.source === 'customer') return 'customer'; if (selection.category === 'Operating') return 'operating'; return 'assets'; } export function AssetAnalysisModal({ selection, subject, close }: Props) { if (!selection) return null; return ; } function AnalysisDialog({ selection, subject, close }: Props & { selection: VehicleModalSelection }) { const mobile = useMobileLayout(); const dialog = useRef(null); const scrollRoot = useRef(null); useDialogFocus(true, dialog, close); const [data, setData] = useState(null); const [weekly, setWeekly] = useState([]); const [busy, setBusy] = useState(true); const [error, setError] = useState(''); const [retry, setRetry] = useState(0); const view = initialView(selection); const [dimensions, setDimensions] = useState(() => getDefaultDimensions(view)); const [filterValues, setFilterValues] = useState({}); const [sort, setSort] = useState('count-desc'); const [mergeParents, setMergeParents] = useState(true); const [detailGroup, setDetailGroup] = useState(null); const showDetails = detailGroup !== null; const overviewScroll = useRef(0); const openDetails = (row: CustomAnalysisRow) => { overviewScroll.current = scrollRoot.current?.scrollTop || 0; setDetailGroup(row); }; useLayoutEffect(() => { if (scrollRoot.current) scrollRoot.current.scrollTop = showDetails ? 0 : overviewScroll.current; }, [showDetails]); const [vehicles, setVehicles] = useState([]); const [detailBusy, setDetailBusy] = useState(false); const [detailError, setDetailError] = useState(''); const [detailRetry, setDetailRetry] = useState(0); const [search, setSearch] = useState(''); const request = useMemo(() => buildVehicleModalRequest(selection, subject), [selection, subject]); const endpoint = `/api/vehicles/analysis${subject ? `?subject=${encodeURIComponent(subject)}` : ''}`; const available = getAvailableDimensions(view); const analysis = useMemo(() => getCustomAnalysis(data?.cells ?? [], { view, dimensions, filterValues, sort }), [data, view, dimensions, filterValues, sort]); const filterValuesKey = JSON.stringify(filterValues); const detailFiltersKey = JSON.stringify(detailGroup?.values ?? {}); useEffect(() => { let cancelled = false; const controller = new AbortController(); setBusy(true); setError(''); const load = request.kind === 'weekly' ? fetchWeeklyDetail(request.type, { ...request.filters, subject }).then(value => { if (!cancelled) setWeekly(value); }) : fetchJson(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ scope: request.params }), signal: controller.signal }).then(value => { if (!cancelled) setData(value); }); load.catch(e => { if (!cancelled) setError(e instanceof Error ? e.message : '统计加载失败'); }) .finally(() => { if (!cancelled) setBusy(false); }); return () => { cancelled = true; controller.abort(); }; }, [request, subject, endpoint, retry]); useEffect(() => { if (!showDetails || request.kind !== 'vehicles' || !data) return; const controller = new AbortController(); let cancelled = false; setDetailBusy(true); setDetailError(''); setVehicles([]); setSearch(''); fetchJson(endpoint.replace('/analysis', '/analysis/details'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: controller.signal, body: JSON.stringify({ metric: analysis.metric, filters: JSON.parse(detailFiltersKey), filterValues: JSON.parse(filterValuesKey), generatedAt: data.generatedAt, scope: request.params }), }).then(value => { if (!cancelled) setVehicles(value); }) .catch(e => { if (!cancelled) setDetailError(e instanceof Error ? e.message : '车辆明细加载失败'); }) .finally(() => { if (!cancelled) setDetailBusy(false); }); return () => { cancelled = true; controller.abort(); }; }, [showDetails, analysis.metric, detailFiltersKey, filterValuesKey, request, data, endpoint, detailRetry]); const visibleVehicles = useMemo(() => { const query = search.toLocaleLowerCase(); return vehicles.filter(vehicle => !query || [vehicle.plateNumber, vehicle.vin, vehicle.model, vehicle.city].some(value => value?.toLocaleLowerCase().includes(query))); }, [vehicles, search]); const detailProgress = useProgressiveRows({ total: visibleVehicles.length, rootRef: scrollRoot, resetKey: visibleVehicles, enabled: showDetails && !detailBusy && !busy, batchSize: 40 }); const shownVehicles = useMemo(() => visibleVehicles.slice(0, detailProgress.visibleCount), [visibleVehicles, detailProgress.visibleCount]); const updateDimensions = (next: AssetAnalysisDimension[]) => { setDimensions(next); }; const updateFilters = (next: AnalysisFilterValues) => { setFilterValues(next); }; const reset = () => { updateDimensions(getDefaultDimensions(view)); updateFilters({}); setMergeParents(true); }; const narrowToGroup = (row: CustomAnalysisRow) => { updateFilters({ ...filterValues, ...Object.fromEntries(Object.entries(row.values).map(([dimension, value]) => [dimension, [value]])) }); }; const merged = mergeParents && analysis.dimensions.length > 1; const orderedGroups = useMemo(() => merged ? orderDimensionGroups(analysis.rows, analysis.dimensions.length, row => analysis.dimensions.map(dimension => row.values[dimension] || ''), row => row.count, sort, (a, b, index) => compareCustomLabels(a, b, [analysis.dimensions[index]])) : analysis.rows, [analysis, merged, sort]); const parentTotals = useMemo(() => merged ? dimensionParentTotals(orderedGroups, analysis.dimensions.length, row => analysis.dimensions.map(dimension => row.values[dimension] || ''), row => row.count) : [], [orderedGroups, analysis.dimensions, merged]); const childCounts = useMemo(() => merged ? dimensionChildCounts(orderedGroups, analysis.dimensions.length, row => analysis.dimensions.map(dimension => row.values[dimension] || '')) : [], [orderedGroups, analysis.dimensions, merged]); const groupProgress = useProgressiveRows({ total: orderedGroups.length, rootRef: scrollRoot, resetKey: orderedGroups, enabled: !showDetails && !busy, batchSize: 40, preserveOnDisable: true }); const shownGroups = useMemo(() => orderedGroups.slice(0, groupProgress.visibleCount), [orderedGroups, groupProgress.visibleCount]); const completeSpans = useMemo(() => merged ? dimensionRowSpans(orderedGroups, analysis.dimensions.length, row => analysis.dimensions.map(dimension => row.values[dimension] || '')) : [], [merged, orderedGroups, analysis.dimensions]); const spans = useMemo(() => visibleDimensionRowSpans(completeSpans, groupProgress.visibleCount), [completeSpans, groupProgress.visibleCount]); const title = request.kind === 'weekly' ? '交还车统计分析' : ANALYSIS_META[view].title; const scopeLabel = selection.scopeLabel || [subject || '全部公司', selection.title].filter(Boolean).join(' · '); const exportWorkbook = async () => { const rows = showDetails ? visibleVehicles : await fetchJson(endpoint.replace('/analysis', '/analysis/details'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ metric: analysis.metric, filters: {}, filterValues, generatedAt: data?.generatedAt, scope: request.kind === 'vehicles' ? request.params : {} }), }); const exportGroups = detailGroup ? [{ ...detailGroup, count: rows.length, totals: { pending: rows.filter(v => v.status === 'Pending').length, abnormal: rows.filter(v => v.status === 'Abnormal').length, } }] : orderedGroups; const statistics = statisticsExportRows( analysis.dimensions.map(d => DIMENSION_LABELS[d]), exportGroups.map(row => ({ values: analysis.dimensions.map(d => getDimensionValueLabel(d, row.values[d] || '')), count: row.count, extra: view === 'inventory' ? { 待交车: row.totals.pending, 异动: row.totals.abnormal } : undefined })), showDetails ? rows.length : analysis.total, ); await downloadAnalysisWorkbook(statistics, vehicleExportRows(rows, view === 'inventory'), `${title}-${scopeLabel}${detailGroup ? `-${detailGroup.label}` : ''}`); }; return
{(!mobile || request.kind !== 'weekly' || busy || error) &&
{mobile && }

{title}

{scopeLabel}

} {busy ?
正在汇总统计数据…
: error ?

统计数据加载失败:{error}

: request.kind === 'weekly' ? : <> {showDetails && !mobile &&
{detailGroup.label}
}
{!showDetails && <> {!mobile && }
当前结果 {number(analysis.total)} 台{analysis.rows.length} 个分组{analysis.dimensions.length ? analysis.dimensions.map(dimension => DIMENSION_LABELS[dimension]).join(' × ') : '仅汇总,不分组'}
{view === 'inventory' &&
待交车 {number(analysis.summary.pending)} 台异动 {number(analysis.summary.abnormal)} 台已包含在当前库存数量中
} {mobile && (analysis.total > 0 || Object.keys(filterValues).length > 0) && } {!mobile && view === 'abnormal' &&

已异动时长=统计时间-审批通过时间;区间左含右不含,缺失时间单列。{data && !data.approvalTimeAvailable && ' 当前未取得可用审批时间。'}

}

{merged ? '按层级汇总数量排序,相同上级合并展示。' : '筛选分组可继续分析,查看明细可核对车辆。'}

{analysis.dimensions.length > 1 && }
{analysis.total === 0 ?

{Object.keys(filterValues).length ? '没有符合条件的数据' : view === 'abnormal' ? '当前无异动车辆' : view === 'pending' ? '当前无待交车辆' : '当前范围暂无车辆'}

{Object.keys(filterValues).length ? '可移除筛选条件后重新查看。' : '当前可查看范围内没有相关记录。'}

{Object.keys(filterValues).length > 0 && }
: <> {mobile ? DIMENSION_LABELS[d])} unit="台" merged={merged} setMerged={setMergeParents} spans={spans} parentTotals={parentTotals} rows={shownGroups.map(row => ({ key: row.key, label: row.label, values: analysis.dimensions.map(d => {getDimensionValueLabel(d, row.values[d] || '')}), count: row.count, share: row.share, note: view === 'inventory' && (row.totals.pending || row.totals.abnormal) ? [row.totals.pending ? `待交 ${row.totals.pending}` : '', row.totals.abnormal ? `异动 ${row.totals.abnormal}` : ''].filter(Boolean).join(' · ') : undefined }))} onDetails={index => openDetails(shownGroups[index])} onFilter={index => narrowToGroup(shownGroups[index])} /> : <>
{analysis.dimensions.length ? analysis.dimensions.map(dimension => ) : }{view === 'inventory' && <>}{shownGroups.map((row, rowIndex) => 0 ? 'analysis-group-start' : undefined}> {analysis.dimensions.length ? analysis.dimensions.map((dimension, columnIndex) => { const span = merged ? spans[rowIndex][columnIndex] : 1; return span === 0 ? null : ; }) : } {view === 'inventory' && <>} )}
{DIMENSION_LABELS[dimension]}统计范围数量(台)待交车异动占比分析操作
1 ? 'analysis-parent-cell' : undefined} data-label={DIMENSION_LABELS[dimension]}>{getDimensionValueLabel(dimension, row.values[dimension] || '')}{merged && columnIndex < analysis.dimensions.length - 1 && childCounts[rowIndex][columnIndex] > 1 && <>合计 {number(parentTotals[rowIndex][columnIndex])} 台{DIMENSION_LABELS[analysis.dimensions[columnIndex + 1]]}:{number(childCounts[rowIndex][columnIndex])} {dimensionCountUnit(analysis.dimensions[columnIndex + 1])}}当前筛选结果{number(row.count)}{number(row.totals.pending)}{number(row.totals.abnormal)}{row.share.toFixed(1)}% {analysis.dimensions.length > 0 && }
}
合计 {number(analysis.total)} 台{view === 'inventory' && ` · 待交车 ${number(analysis.summary.pending)} 台 · 异动 ${number(analysis.summary.abnormal)} 台`} · 占比基于当前筛选结果100%
} } {showDetails &&
{mobile &&

{detailGroup.label} · 当前分组

}

车辆明细 {number(visibleVehicles.length)} 台

{detailBusy ?
正在加载车辆明细…
: detailError ?

{detailError}

: <> {vehicles.length !== detailGroup.count &&

车辆状态可能已更新:统计快照 {detailGroup.count} 台,当前明细 {vehicles.length} 台。

} {(view === 'abnormal' || detailGroup.values.inventoryState === 'abnormal' || (filterValues.inventoryState?.length === 1 && filterValues.inventoryState[0] === 'abnormal')) ? : (view === 'pending' || detailGroup.values.inventoryState === 'pending' || (filterValues.inventoryState?.length === 1 && filterValues.inventoryState[0] === 'pending')) ? : } }
}
{mobile ?
统计口径与数据时间

统计时间 {data ? formatLocalDateTime(new Date(data.generatedAt)) : '—'} · 当前可查看范围

同维度筛选为“或”,不同维度为“且”。仅展示当前入口与筛选条件内的车辆。

{view === 'abnormal' &&

已异动时长=统计时间-审批通过时间;区间左含右不含,缺失时间单列。{data && !data.approvalTimeAvailable && ' 当前未取得可用审批时间。'}

}
: <>统计时间 {data ? formatLocalDateTime(new Date(data.generatedAt)) : '—'} · 当前可查看范围{showDetails ? '仅展示当前分组与筛选条件内的车辆' : '同维度筛选为“或”,不同维度筛选为“且”'}}
}
; } function VehicleRows({ vehicles, inventory = false }: { vehicles: VehicleListItem[]; inventory?: boolean }) { const mobile = useMobileLayout(); if (!vehicles.length) return
暂无匹配车辆
; const stateLabel = (vehicle: VehicleListItem) => { const operation = ({ '1': '租赁', '2': '自营', '3': '可运营', '4': '待运营' } as Record)[vehicle.operationStatus || ''] || vehicle.operationStatus; const business = ({ Abnormal: '异动', Pending: '待交车', Inventory: '在库', Operating: '运营中' } as Record)[vehicle.status] || vehicle.status; return [business, operation].filter(Boolean).join(' · '); }; if (mobile) return
{vehicles.map(vehicle =>

{vehicle.plateNumber || vehicle.vin || '未提供'}

{stateLabel(vehicle)}

{vehicle.model || `${vehicle.brandLabel || '—'} · ${vehicle.type}`} · {vehicle.city || vehicle.location || '未标注城市'}

{!inventory && vehicle.customerName &&

{vehicle.customerName}

}
车辆资料
{[['VIN', vehicle.vin], ['品牌 / 车型', `${vehicle.brandLabel || '—'} · ${vehicle.type}`], ['省份 / 城市', `${vehicle.province || '未标注'} / ${vehicle.city || vehicle.location || '未标注'}`], ...(inventory ? [['运维负责人', vehicle.opsManagers?.trim() || '-']] : [['签约公司', vehicle.orgName], ['部门', vehicle.departmentName], ['业务负责人', vehicle.customerManager]]), ['所属公司', vehicle.subjectOrg]].map(([label, value]) =>
{label}
{value || '未提供'}
)}
)}
; return
{['车牌 / VIN', '品牌 / 车型 / 型号', '省份 / 城市', '状态', ...(inventory ? ['运维负责人'] : ['客户 / 签约公司', '部门 / 业务负责人']), '所属公司'].map(label => )}{vehicles.map(vehicle => {inventory ? : <>})}
{label}
{vehicle.plateNumber || vehicle.vin || '未提供'}{vehicle.brandLabel || '—'} · {vehicle.type}{vehicle.model}{vehicle.province || '未标注'} / {vehicle.city || vehicle.location || '未标注'}{stateLabel(vehicle)}{vehicle.opsManagers?.trim() || '-'}{vehicle.customerName || '—'}{vehicle.orgName || '—'}{vehicle.departmentName || '—'}{vehicle.customerManager || '—'}{vehicle.subjectOrg || '—'}
; } type WeeklyDimension = 'date' | 'contract_type'; const WEEKLY_DIMENSIONS: WeeklyDimension[] = ['date', 'contract_type']; const EMPTY_WEEKLY_ROWS: WeeklyDetailItem[] = []; const weeklyDimensionValue = (row: WeeklyDetailItem, dimension: WeeklyDimension) => dimension === 'date' ? row.handover_date?.slice(0, 10) || '' : row.contract_type?.trim() || ''; function WeeklyAnalysis({ rows, title, scopeLabel, close }: { rows: WeeklyDetailItem[]; title: string; scopeLabel: string; close: () => void }) { const mobile = useMobileLayout(); type Dimension = WeeklyDimension; const dimensionsAvailable = WEEKLY_DIMENSIONS; const scrollRoot = useRef(null); const labels: Record = { date: '交还日期', contract_type: '合同类型' }; const [dimensions, setDimensions] = useState(['date']); const [mergeParents, setMergeParents] = useState(true); const mergeActive = mergeParents && dimensions.length > 1; const [filters, setFilters] = useState>>({}); const [detailKey, setDetailKey] = useState(null); const value = weeklyDimensionValue; const label = (dimension: Dimension, entry: string) => entry || (dimension === 'date' ? '日期缺失' : '未标注'); const filtered = useMemo(() => rows.filter(row => dimensionsAvailable.every(dimension => !filters[dimension]?.length || filters[dimension]!.includes(value(row, dimension)))), [rows, filters]); const grouped = useMemo(() => { const result = new Map(); for (const row of filtered) { const values = dimensions.map(dimension => value(row, dimension)); const key = JSON.stringify(values); const group = result.get(key) ?? { key, values, rows: [] }; group.rows.push(row); result.set(key, group); } return result; }, [filtered, dimensions]); const groups = useMemo(() => { const flatGroups = [...grouped.values()].sort((a, b) => b.rows.length - a.rows.length || a.key.localeCompare(b.key, 'zh-CN')); return mergeActive ? orderDimensionGroups(flatGroups, dimensions.length, group => group.values, group => group.rows.length) : flatGroups; }, [grouped, mergeActive, dimensions.length]); const detailGroup = detailKey === null ? null : grouped.get(detailKey); const detailRows = detailGroup?.rows ?? EMPTY_WEEKLY_ROWS; const showDetails = detailKey !== null; const overviewScroll = useRef(0); const openDetails = (key: string) => { overviewScroll.current = scrollRoot.current?.scrollTop || 0; setDetailKey(key); }; useLayoutEffect(() => { if (scrollRoot.current) scrollRoot.current.scrollTop = showDetails ? 0 : overviewScroll.current; }, [showDetails]); const groupLabel = (group: { values: string[] }) => dimensions.length ? dimensions.map((dimension, index) => `${labels[dimension]}:${label(dimension, group.values[index])}`).join(' · ') : '当前筛选结果'; const parentTotals = useMemo(() => mergeActive ? dimensionParentTotals(groups, dimensions.length, group => group.values, group => group.rows.length) : [], [groups, dimensions.length, mergeActive]); const childCounts = useMemo(() => mergeActive ? dimensionChildCounts(groups, dimensions.length, group => group.values) : [], [groups, dimensions.length, mergeActive]); const groupProgress = useProgressiveRows({ total: groups.length, rootRef: scrollRoot, resetKey: groups, enabled: !showDetails, preserveOnDisable: true }); const detailProgress = useProgressiveRows({ total: detailRows.length, rootRef: scrollRoot, resetKey: detailRows, enabled: showDetails }); const shownGroups = useMemo(() => groups.slice(0, groupProgress.visibleCount), [groups, groupProgress.visibleCount]); const completeSpans = useMemo(() => mergeActive ? dimensionRowSpans(groups, dimensions.length, group => group.values) : [], [groups, dimensions.length, mergeActive]); const groupSpans = useMemo(() => visibleDimensionRowSpans(completeSpans, groupProgress.visibleCount), [completeSpans, groupProgress.visibleCount]); const shownDetails = useMemo(() => detailRows.slice(0, detailProgress.visibleCount), [detailRows, detailProgress.visibleCount]); const updateFilters = (next: Partial>) => { setFilters(next); setDetailKey(null); }; const setFilter = (dimension: Dimension, selected: string[] | undefined) => { const next = { ...filters }; if (selected === undefined) delete next[dimension]; else next[dimension] = selected; updateFilters(next); }; const updateDimensions = (next: Dimension[]) => { setDimensions(next); setDetailKey(null); }; const activeFilters = dimensionsAvailable.filter(dimension => filters[dimension]?.length); const filterOptions = useMemo(() => Object.fromEntries(dimensionsAvailable.map(dimension => { const eligible = rows.filter(row => dimensionsAvailable.every(other => other === dimension || !filters[other]?.length || filters[other]!.includes(value(row, other)))); const counts = new Map(); for (const row of eligible) { const entry = value(row, dimension); counts.set(entry, (counts.get(entry) || 0) + 1); } for (const entry of filters[dimension] || []) if (!counts.has(entry)) counts.set(entry, 0); return [dimension, [...counts].map(([entry, count]) => ({ value: entry, label: label(dimension, entry), count })).sort((a, b) => b.count - a.count || a.label.localeCompare(b.label, 'zh-CN'))]; })), [rows, filters]); const exportWorkbook = () => { const selectedRows = showDetails ? detailRows : filtered; const selectedGroups = detailGroup ? [detailGroup] : groups; const statistics = statisticsExportRows(dimensions.map(d => labels[d]), selectedGroups.map(group => ({ values: group.values.map((v, i) => label(dimensions[i], v)), count: group.rows.length })), selectedRows.length, '车次'); const details = selectedRows.map(row => ({ 车牌: row.plate_number, 日期: row.handover_date, 合同类型: row.contract_type, 客户: row.customer_name })); return downloadAnalysisWorkbook(statistics, details, `交还车分析-${detailGroup ? groupLabel(detailGroup) : '当前筛选结果'}`); }; if (mobile) return <>

{title}

{scopeLabel}

当前结果 {number(showDetails ? detailRows.length : filtered.length)} 车次{!showDetails && {groups.length} 个分组}
{!showDetails ? <> {(filtered.length > 0 || activeFilters.length > 0) && ({ key, label: labels[key], options: filterOptions[key] }))} dimensions={dimensions} filters={filters} defaults={['date']} apply={(nextDimensions, nextFilters) => { updateDimensions(nextDimensions as Dimension[]); updateFilters(nextFilters); }} />} {filtered.length > 0 &&
} {groups.length ? <> labels[d])} unit="车次" merged={mergeActive} setMerged={setMergeParents} spans={groupSpans} parentTotals={parentTotals} rows={shownGroups.map(group => ({ key: group.key, label: groupLabel(group), values: group.values.map((entry, i) => label(dimensions[i], entry)), count: group.rows.length, share: group.rows.length / filtered.length * 100 }))} onDetails={index => openDetails(shownGroups[index].key)} onFilter={index => updateFilters({ ...filters, ...Object.fromEntries(dimensions.map((dimension, i) => [dimension, [shownGroups[index].values[i]]])) })} /> :

{activeFilters.length ? '没有符合条件的数据,可调整筛选。' : '当前范围暂无交还车记录。'}

{activeFilters.length > 0 && }
} : <>

{detailGroup ? groupLabel(detailGroup) : '当前分组'}

交还车业务明细

{shownDetails.map((row, index) =>

{row.plate_number || '未登记车牌'}

{row.contract_type || '未标注合同类型'}

{row.handover_date || '日期缺失'}

{row.customer_name || '未提供客户'}

)}
}
统计口径

按当前入口的周业务记录统计车次,同一车辆多笔记录分别计次。仅展示当前可查看范围和筛选条件内的数据。

; return
{!showDetails ? <>

分析维度

点击名称分组,点击箭头筛选
{dimensionsAvailable.map(dimension => { const options = filterOptions[dimension]; return updateDimensions(dimensions.includes(dimension) ? dimensions.filter(item => item !== dimension) : [...dimensions, dimension])} onApplyFilter={selected => setFilter(dimension, selected)} />; })} {activeFilters.length > 0 && }
当前结果 {number(filtered.length)} 车次{groups.length} 个分组{dimensions.length ? dimensions.map(dimension => labels[dimension]).join(' × ') : '仅汇总,不分组'}

筛选分组可继续分析,查看明细可核对业务记录。

{dimensions.length > 1 && }
{groups.length ? <>
{dimensions.length ? dimensions.map(dimension => ) : } {shownGroups.map((group, rowIndex) => 0 ? 'analysis-group-start' : undefined}> {dimensions.length ? dimensions.map((dimension, index) => { const span = mergeActive ? groupSpans[rowIndex][index] : 1; return span === 0 ? null : ; }) : } )}
{labels[dimension]}统计范围数量(车次)占比分析操作
1 ? span : undefined} className={span > 1 ? 'analysis-parent-cell' : undefined}>{label(dimension, group.values[index])}{mergeActive && index < dimensions.length - 1 && childCounts[rowIndex][index] > 1 && <>合计 {number(parentTotals[rowIndex][index])} 车次{labels[dimensions[index + 1]]}:{number(childCounts[rowIndex][index])} {dimensionCountUnit(dimensions[index + 1])}}当前筛选结果{number(group.rows.length)}{(group.rows.length / filtered.length * 100).toFixed(1)}% {dimensions.length > 0 && }
合计 {number(filtered.length)} 车次 · 占比基于当前筛选结果100%
:

没有符合条件的数据,请调整筛选。

{activeFilters.length > 0 && }
} : <>
{detailGroup ? groupLabel(detailGroup) : '当前分组'}

交还车业务明细 {number(detailRows.length)} 车次

{detailRows.length ? <>
{shownDetails.map((row, index) => )}
车牌日期合同类型客户
{row.plate_number || '—'}{row.handover_date || '—'}{row.contract_type || '—'}{row.customer_name || '—'}
:

当前分组暂无数据,请返回统计分析。

} }
; }