@@ -14,6 +14,8 @@ 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 { downloadAnalysisWorkbook , statisticsExportRows , vehicleExportRows } from '../../analysis-export' ;
import './asset-analysis.css' ;
type Props = { selection : VehicleModalSelection | null ; subject : string | null ; close : ( ) = > void } ;
@@ -111,6 +113,23 @@ function AnalysisDialog({ selection, subject, close }: Props & { selection: Vehi
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 < VehicleListItem [ ] > ( 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 ) , ` ${ title } - ${ scopeLabel } ${ detailGroup ? ` - ${ detailGroup . label } ` : '' } ` ) ;
} ;
return < div className = "asset-analysis-overlay" >
< div ref = { dialog } role = "dialog" aria-modal = "true" aria-label = { title } tabIndex = { - 1 } className = { ` asset-analysis-dialog ${ view === 'abnormal' ? 'analysis-amber' : '' } ` } >
< header className = "analysis-header" > < div > < h2 > < BarChart3 size = { 16 } / > { title } < / h2 > < p > { scopeLabel } < / p > < / div > < button type = "button" aria-label = "关闭统计分析" onClick = { close } > < X size = { 18 } / > < / button > < / header >
@@ -122,7 +141,7 @@ function AnalysisDialog({ selection, subject, close }: Props & { selection: Vehi
< div className = "analysis-result-bar" role = "status" aria-live = "polite" > < span > 当 前 结 果 < strong > { number ( analysis . total ) } < / strong > 台 < / span > < span > { analysis . rows . length } 个 分 组 < / span > < span className = "analysis-result-dimensions" > { analysis . dimensions . length ? analysis . dimensions . map ( dimension = > DIMENSION_LABELS [ dimension ] ) . join ( ' × ' ) : '仅汇总,不分组' } < / span > < / div >
{ view === 'inventory' && < div className = "analysis-inventory-subsets" aria-label = "库存子集统计" > < span className = "analysis-pending-count" > 待 交 车 < strong > { number ( analysis . summary . pending ) } < / strong > 台 < / span > < span className = "analysis-abnormal-count" > 异 动 < strong > { number ( analysis . summary . abnormal ) } < / strong > 台 < / span > < small > 已 包 含 在 当 前 库 存 数 量 中 < / small > < / div > }
{ view === 'abnormal' && < p className = "analysis-definition" > 已 异 动 时 长 = 统 计 时 间 - 审 批 通 过 时 间 ; 区 间 左 含 右 不 含 , 缺 失 时 间 单 列 。 { data && ! data . approvalTimeAvailable && ' 当前未取得可用审批时间。' } < / p > }
< div className = "analysis-table-tools" > < p > { merged ? '按层级汇总数量排序,相同上级合并展示。' : '筛选分组可继续分析,查看明细可核对车辆。' } < / p > < div className = "analysis-table-options" > { analysis . dimensions . length > 1 && < label className = "analysis-merge-toggle" > < input type = "checkbox" checked = { mergeParents } onChange = { event = > { setMergeParents ( event . target . checked ) ; } } / > 合 并 上 级 维 度 < / label > } < label > 排 序 < select aria-label = "统计分组排序" value = { sort } onChange = { event = > { setSort ( event . target . value as AnalysisSort ) ; } } > < option value = "count-desc" > 数 量 从 多 到 少 < / option > < option value = "count-asc" > 数 量 从 少 到 多 < / option > < option value = "label" > 维 度 顺 序 < / option > < / select > < / label > < / div > < / div >
< div className = "analysis-table-tools" > < p > { merged ? '按层级汇总数量排序,相同上级合并展示。' : '筛选分组可继续分析,查看明细可核对车辆。' } < / p > < div className = "analysis-table-options" > < AnalysisExportButton disabled = { ! analysis . total } onExport = { exportWorkbook } / > { analysis . dimensions . length > 1 && < label className = "analysis-merge-toggle" > < input type = "checkbox" checked = { mergeParents } onChange = { event = > { setMergeParents ( event . target . checked ) ; } } / > 合 并 上 级 维 度 < / label > } < label > 排 序 < select aria-label = "统计分组排序" value = { sort } onChange = { event = > { setSort ( event . target . value as AnalysisSort ) ; } } > < option value = "count-desc" > 数 量 从 多 到 少 < / option > < option value = "count-asc" > 数 量 从 少 到 多 < / option > < option value = "label" > 维 度 顺 序 < / option > < / select > < / label > < / div > < / div >
{ analysis . total === 0 ? < div className = "analysis-message" > < BarChart3 size = { 22 } / > < h3 > 没 有 符 合 条 件 的 数 据 < / h3 > < p > 移 除 部 分 筛 选 条 件 , 或 恢 复 当 前 入 口 的 全 部 数 据 。 < / p > { Object . keys ( filterValues ) . length > 0 && < button className = "analysis-button" onClick = { ( ) = > updateFilters ( { } ) } > 清 空 筛 选 < / button > } < / div > : < >
< div className = { ` analysis-group-table ${ merged ? ' is-merged' : '' } ` } role = "region" aria-label = "组合统计表" tabIndex = { 0 } > < table >
< thead > < tr > { analysis . dimensions . length ? analysis . dimensions . map ( dimension = > < th key = { dimension } > { DIMENSION_LABELS [ dimension ] } < / th > ) : < th > 统 计 范 围 < / th > } < th className = "analysis-numeric" > 数 量 ( 台 ) < / th > { view === 'inventory' && < > < th className = "analysis-numeric" > 待 交 车 < / th > < th className = "analysis-numeric" > 异 动 < / th > < / > } < th className = "analysis-numeric" > 占 比 < / th > < th > 分 析 操 作 < / th > < / tr > < / thead >
@@ -142,7 +161,7 @@ function AnalysisDialog({ selection, subject, close }: Props & { selection: Vehi
< / > }
< / > }
{ showDetails && < div className = "analysis-details" >
< div className = "analysis-table-tools" > < h3 > 车 辆 明 细 < small > { number ( visibleVehicles . length ) } 台 < / small > < / h3 > < label className = "analysis-search" > < Search size = { 15 } / > < input aria-label = "搜索车辆明细" placeholder = "搜索车牌、VIN、型号、城市" value = { search } onChange = { event = > { setSearch ( event . target . value ) ; } } / > < / label > < / div >
< div className = "analysis-table-tools" > < h3 > 车 辆 明 细 < small > { number ( visibleVehicles . length ) } 台 < / small > < / h3 > < AnalysisExportButton disabled = { detailBusy || ! ! detailError || ! visibleVehicles . length } onExport = { exportWorkbook } / > < label className = "analysis-search" > < Search size = { 15 } / > < input aria-label = "搜索车辆明细" placeholder = "搜索车牌、VIN、型号、城市" value = { search } onChange = { event = > { setSearch ( event . target . value ) ; } } / > < / label > < / div >
{ detailBusy ? < div className = "analysis-message" role = "status" > < Loader2 className = "animate-spin" / > 正 在 加 载 车 辆 明 细 … < / div > : detailError ? < div className = "analysis-message" role = "alert" > < p > { detailError } < / p > < button className = "analysis-button" onClick = { ( ) = > setDetailRetry ( detailRetry + 1 ) } > 重 新 加 载 明 细 < / button > < / div > : < >
{ vehicles . length !== detailGroup . count && < p className = "analysis-definition" > 车 辆 状 态 可 能 已 更 新 : 统 计 快 照 { detailGroup . count } 台 , 当 前 明 细 { vehicles . length } 台 。 < / p > }
{ ( view === 'abnormal' || detailGroup . values . inventoryState === 'abnormal' || ( filterValues . inventoryState ? . length === 1 && filterValues . inventoryState [ 0 ] === 'abnormal' ) ) ? < AbnormalVehicleDetails vehicles = { shownVehicles } / > : ( view === 'pending' || detailGroup . values . inventoryState === 'pending' || ( filterValues . inventoryState ? . length === 1 && filterValues . inventoryState [ 0 ] === 'pending' ) ) ? < PendingVehicleDetails vehicles = { shownVehicles } / > : < VehicleRows vehicles = { shownVehicles } / > }
@@ -230,6 +249,13 @@ function WeeklyAnalysis({ rows }: { rows: WeeklyDetailItem[] }) {
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 ) : '当前筛选结果' } ` ) ;
} ;
return < div ref = { scrollRoot } className = "analysis-scroll" >
{ ! showDetails ? < >
< div className = "analysis-builder" >
@@ -244,7 +270,7 @@ function WeeklyAnalysis({ rows }: { rows: WeeklyDetailItem[] }) {
< / div >
< / div >
< div className = "analysis-result-bar" role = "status" aria-live = "polite" > < span > 当 前 结 果 < strong > { number ( filtered . length ) } < / strong > 车 次 < / span > < span > { groups . length } 个 分 组 < / span > < span className = "analysis-result-dimensions" > { dimensions . length ? dimensions . map ( dimension = > labels [ dimension ] ) . join ( ' × ' ) : '仅汇总,不分组' } < / span > < / div >
< div className = "analysis-table-tools" > < p > 筛 选 分 组 可 继 续 分 析 , 查 看 明 细 可 核 对 业 务 记 录 。 < / p > { dimensions . length > 1 && < label className = "analysis-merge-toggle" > < input type = "checkbox" checked = { mergeParents } onChange = { event = > { setMergeParents ( event . target . checked ) ; } } / > 合 并 上 级 维 度 < / label > } < / div >
< div className = "analysis-table-tools" > < p > 筛 选 分 组 可 继 续 分 析 , 查 看 明 细 可 核 对 业 务 记 录 。 < / p > < div className = "analysis-export-actions" > < AnalysisExportButton disabled = { ! filtered . length } onExport = { exportWorkbook } / > < / div > { dimensions . length > 1 && < label className = "analysis-merge-toggle" > < input type = "checkbox" checked = { mergeParents } onChange = { event = > { setMergeParents ( event . target . checked ) ; } } / > 合 并 上 级 维 度 < / label > } < / div >
{ groups . length ? < >
< div className = { ` analysis-group-table ${ mergeActive ? ' is-merged' : '' } ` } role = "region" aria-label = "交还车组合统计表" tabIndex = { 0 } > < table > < thead > < tr > { dimensions . length ? dimensions . map ( dimension = > < th key = { dimension } > { labels [ dimension ] } < / th > ) : < th > 统 计 范 围 < / th > } < th className = "analysis-numeric" > 数 量 ( 车 次 ) < / th > < th className = "analysis-numeric" > 占 比 < / th > < th > 分 析 操 作 < / th > < / tr > < / thead > < tbody >
{ shownGroups . map ( ( group , rowIndex ) = > < tr key = { group . key } className = { mergeActive && groupSpans [ rowIndex ] [ 0 ] > 0 ? 'analysis-group-start' : undefined } >
@@ -261,7 +287,7 @@ function WeeklyAnalysis({ rows }: { rows: WeeklyDetailItem[] }) {
< / > : < div className = "analysis-message" > < p > 没 有 符 合 条 件 的 数 据 , 请 调 整 筛 选 。 < / p > { activeFilters . length > 0 && < button type = "button" className = "analysis-button" onClick = { ( ) = > updateFilters ( { } ) } > 清 空 筛 选 < / button > } < / div > }
< / > : < >
< div className = "analysis-navigation" > < button type = "button" className = "analysis-back" onClick = { ( ) = > { setDetailKey ( null ) ; } } > < ArrowLeft size = { 14 } / > 返 回 统 计 分 析 < / button > < span className = "analysis-detail-scope" > { detailGroup ? groupLabel ( detailGroup ) : '当前分组' } < / span > < / div >
< div className = "analysis-table-tools" > < h3 > 交 还 车 业 务 明 细 < small > { number ( detailRows . length ) } 车 次 < / small > < / h3 > < / div >
< div className = "analysis-table-tools" > < h3 > 交 还 车 业 务 明 细 < small > { number ( detailRows . length ) } 车 次 < / small > < / h3 > < AnalysisExportButton disabled = { ! detailRows . length } onExport = { exportWorkbook } / > < / div >
{ detailRows . length ? < > < div className = "analysis-detail-table" role = "region" aria-label = "交还车业务明细表" tabIndex = { 0 } > < table > < thead > < tr > < th > 车 牌 < / th > < th > 日 期 < / th > < th > 合 同 类 型 < / th > < th > 客 户 < / th > < / tr > < / thead > < tbody > { shownDetails . map ( ( row , index ) = > < tr key = { ` ${ row . truck_id } - ${ index } ` } > < td > < Blur > { row . plate_number || '—' } < / Blur > < / td > < td > { row . handover_date || '—' } < / td > < td > { row . contract_type || '—' } < / td > < td > < Blur > { row . customer_name || '—' } < / Blur > < / td > < / tr > ) } < / tbody > < / table > < / div >
< ProgressiveRowsStatus { ...detailProgress } unit = "条" / >
< / > : < p className = "analysis-message" > 当 前 分 组 暂 无 数 据 , 请 返 回 统 计 分 析 。 < / p > }