import { Card, Button, Spin } from 'antd'
import {
ProForm,
ProFormDateRangePicker,
ProFormTreeSelect,
} from '@ant-design/pro-components'
import ElectricityTotal from '@/assets/electricityTotal.png'
import ElectricityWaste from '@/assets/wasteTotal.png'
import ElectricityRate from '@/assets/wasteRate.png'
import WaterTotal from '@/assets/waterTotal.png'
import WaterWaste from '@/assets/waterWasteTotal.png'
import WaterRate from '@/assets/waterWasteRate.png'
import Electricity from '@/assets/electricity.png'
import Water from '@/assets/water.png'
import style from './index.module.scss'
import { useState, useRef, useEffect } from 'react'
import { useAreaListTree, useSearch } from './query/index'
// 1. 原始树形数据类型
interface MeterItem {
deviceId: string
deviceName: string
totalWastageNum: number
totalWastagePercent: string
wastagePercent?: string
usageNum: number
wastageNum?: number
childList?: MeterItem[]
}
// 2. 层级节点类型(新增 rowspan)
interface LevelNode extends Omit<MeterItem, 'childList'> {
level: number
parentId: string | null
hasChildren: boolean
rowspan: number
}
export function Component() {
const [index, setIndex] = useState(0)
const [loading, setLoading] = useState(false)
const [treeNodes, setTreeNodes] = useState<LevelNode[]>([])
const [maxLevel, setMaxLevel] = useState(1)
const [tableRows, setTableRows] = useState<LevelNode[][]>([])
const { data: areaListTree } = useAreaListTree({})
const [form, setForm] = useState({
dateStart: '',
dateEnd: '',
areaId: '',
})
// 接口数据解构
const {
data: {
totalUsageNumElectricity,
totalUsageNumWater,
totalWastageNumElectricity,
totalWastageNumWater,
totalWastagePercentElectricity,
totalWastagePercentWater,
childListWater = [],
childListElectricity = [],
} = {
childListWater: [],
childListElectricity: [],
},
refetch,
isSuccess,
} = useSearch(form)
const isInited = useRef(false)
const isUpdating = useRef(false)
const items = [
{ key: 1, label: '用电损耗' },
{ key: 2, label: '用水损耗' },
]
// ------------------------------ 计算垂直合并行数(rowspan) ------------------------------
const calculateNodeRowspan = (
node: MeterItem
): { node: LevelNode; children: LevelNode[] } => {
const { childList, ...rest } = node
const hasChildren = !!childList?.length
let totalRowspan = 1
const childNodes: LevelNode[] = []
if (hasChildren) {
childList.forEach(child => {
const { node: childNode, children: grandChildren } =
calculateNodeRowspan(child)
childNodes.push(childNode, ...grandChildren)
totalRowspan += childNode.rowspan
})
}
const levelNode: LevelNode = {
...rest,
level: 0,
parentId: null,
hasChildren,
rowspan: totalRowspan,
}
return { node: levelNode, children: childNodes }
}
// ------------------------------ 生成带层级的扁平节点列表 ------------------------------
const generateLevelNodes = (
rootItems: MeterItem[]
): { nodes: LevelNode[]; maxLevel: number } => {
if (!rootItems || rootItems.length === 0) {
return { nodes: [], maxLevel: 1 }
}
const allNodes: LevelNode[] = []
const traverse = (
items: MeterItem[],
parentId: string | null,
currentLevel: number
) => {
items.forEach(item => {
const { node: currentNode, children: childNodes } =
calculateNodeRowspan(item)
const levelNode = { ...currentNode, level: currentLevel, parentId }
allNodes.push(levelNode)
if (item.childList && item.childList.length > 0) {
traverse(item.childList, currentNode.deviceId, currentLevel + 1)
}
})
}
traverse(rootItems, null, 1)
const currentMaxLevel = Math.max(...allNodes.map(n => n.level), 1)
return { nodes: allNodes, maxLevel: currentMaxLevel }
}
// ------------------------------ 核心:生成表格行(每行仅含实际列,无空格)------------------------------
const generateTableRows = (
nodes: LevelNode[]
): { rows: LevelNode[][]; rowColMap: Record<number, number> } => {
if (!nodes || nodes.length === 0) {
return { rows: [], rowColMap: {} }
}
const rows: LevelNode[][] = []
const rowColMap: Record<number, number> = {}
// 构建父子映射
const childrenMap: Record<string, LevelNode[]> = {}
nodes.forEach(node => {
const key = node.parentId || 'root'
if (!childrenMap[key]) childrenMap[key] = []
childrenMap[key].push(node)
})
// DFS 深度优先构建每一行的实际内容
const dfs = (parentId: string | null, currentPath: LevelNode[]) => {
const key = parentId || 'root'
const siblings = childrenMap[key] || []
siblings.forEach(node => {
// 创建当前路径副本,并在对应层级插入节点
const path = [...currentPath]
const idx = node.level - 1
path[idx] = node
path.length = idx + 1 // 截断多余部分
rows.push(path)
const rowIndex = rows.length - 1
rowColMap[rowIndex] = path.length
// 继续子节点
if (node.hasChildren) {
dfs(node.deviceId, path)
}
})
}
dfs(null, [])
return { rows, rowColMap }
}
// ------------------------------ 空数据重置 ------------------------------
const resetEmptyTable = () => {
setTreeNodes([])
setTableRows([])
setMaxLevel(1)
}
// ------------------------------ 更新表格 ------------------------------
const updateTreeTable = (targetType: 0 | 1) => {
if (loading || isUpdating.current) return
isUpdating.current = true
try {
const targetData =
targetType === 0 ? childListElectricity : childListWater
if (!targetData || targetData.length === 0) {
resetEmptyTable()
return
}
const { nodes: levelNodes, maxLevel: newMaxLevel } =
generateLevelNodes(targetData)
const { rows: newTableRows } = generateTableRows(levelNodes)
setTreeNodes(levelNodes)
setTableRows(newTableRows)
setMaxLevel(newMaxLevel)
} finally {
isUpdating.current = false
}
}
// 初始加载
useEffect(() => {
if (!isInited.current && isSuccess) {
updateTreeTable(0)
isInited.current = true
}
}, [isSuccess])
// 查询处理
const handleSearch = async (value: {
date?: [string, string]
areaId?: string
}) => {
setLoading(true)
try {
resetEmptyTable()
setForm({
dateStart: value.date?.[0] || '',
dateEnd: value.date?.[1] || '',
areaId: value.areaId || '',
})
const result = await refetch()
const data = result.data
const targetData =
index === 0 ? data?.childListElectricity : data?.childListWater
if (!targetData || targetData.length === 0) {
resetEmptyTable()
return
}
const { nodes: levelNodes, maxLevel: newMaxLevel } =
generateLevelNodes(targetData)
const { rows: newTableRows } = generateTableRows(levelNodes)
setTreeNodes(levelNodes)
setTableRows(newTableRows)
setMaxLevel(newMaxLevel)
} finally {
setLoading(false)
}
}
// Tab切换
const handleTabChange = (i: number) => {
if (i === index || loading || isUpdating.current) return
setIndex(i)
resetEmptyTable()
const targetData = i === 0 ? childListElectricity : childListWater
if (!targetData || targetData.length === 0) return
const { nodes: levelNodes, maxLevel: newMaxLevel } =
generateLevelNodes(targetData)
const { rows: newTableRows } = generateTableRows(levelNodes)
setTreeNodes(levelNodes)
setTableRows(newTableRows)
setMaxLevel(newMaxLevel)
}
// 数字格式化
const formatNumber = (num: number | string): string => {
const value = Number(num)
return isNaN(value) || value === 0 ? '0' : (value / 10000).toFixed(2)
}
// ============================== 渲染 ==============================
return (
<Card>
{/* 搜索表单 */}
<ProForm
layout="inline"
style={{ marginTop: '30px' }}
onFinish={handleSearch}
submitter={{
render: ({ submit, reset }) => (
<>
<Button
type="primary"
onClick={submit}
style={{ marginRight: 8 }}
loading={loading}
disabled={loading}
>
查询
</Button>
<Button onClick={reset} disabled={loading}>
重置
</Button>
</>
),
}}
>
<ProFormDateRangePicker
name="date"
label="统计日期"
fieldProps={{ style: { width: 300 } }}
/>
<ProFormTreeSelect
name="areaId"
label="区域"
fieldProps={{
style: { width: 300 },
fieldNames: { label: 'name', value: 'id', children: 'children' },
treeData: areaListTree,
placeholder: '请选择区域',
allowClear: true,
disabled: loading,
showSearch: true,
treeNodeFilterProp: 'name',
}}
/>
</ProForm>
{/* 汇总信息栏 */}
<div className={style.infoBox}>
<div className={style.infoElectricity}>
<div className={style.infoElectricityTotal}>
<img src={ElectricityTotal} alt="用电总量图标" />
<div className={style.infoExtra}>
<div className={style.commonBold}>
{formatNumber(totalUsageNumElectricity as number) || 0}万kWh
</div>
<div>用电总量</div>
</div>
</div>
<div className={style.infoElectricityWaste}>
<img src={ElectricityWaste} alt="用电损耗总量图标" />
<div className={style.infoExtra}>
<div className={style.commonBold}>
{formatNumber(totalWastageNumElectricity as number) || 0}万KWH
</div>
<div>损耗总量</div>
</div>
</div>
<div className={style.infoElectricityRate}>
<img src={ElectricityRate} alt="用电损耗率图标" />
<div className={style.infoExtra}>
<div className={style.commonBold}>
{totalWastagePercentElectricity || '-'}
</div>
<div>损耗率</div>
</div>
</div>
</div>
<div className={style.infoWater}>
<div className={style.infoWaterTotal}>
<img src={WaterTotal} alt="用水总量图标" />
<div className={style.infoExtra}>
<div className={style.commonBold}>
{formatNumber(totalUsageNumWater as number) || 0}万m³
</div>
<div>用水总量</div>
</div>
</div>
<div className={style.infoWaterWaste}>
<img src={WaterWaste} alt="用水损耗总量图标" />
<div className={style.infoExtra}>
<div className={style.commonBold}>
{formatNumber(totalWastageNumWater as number) || 0}万m³
</div>
<div>损耗总量</div>
</div>
</div>
<div className={style.infoWaterRate}>
<img src={WaterRate} alt="用水损耗率图标" />
<div className={style.infoExtra}>
<div className={style.commonBold}>
{totalWastagePercentWater || '-'}
</div>
<div>损耗率</div>
</div>
</div>
</div>
</div>
{/* 标签切换 */}
<div className={style.navBox}>
{items.map((v, i) => (
<div
key={v.key}
className={index === i ? style.navActive : ''}
onClick={() => handleTabChange(i)}
style={{
marginRight: i === 0 ? '32px' : 0,
cursor: loading || isUpdating.current ? 'not-allowed' : 'pointer',
opacity: loading || isUpdating.current ? 0.6 : 1,
}}
>
{v.label}
</div>
))}
<div
style={{
flexGrow: 1,
textAlign: 'right',
fontSize: '16px',
color: '#333333',
paddingRight: '20px',
}}
>
单位:{index === 0 ? 'kwh' : 'm³'}
</div>
</div>
{/* 树形表格 */}
<div
className={style.treeTableContainer}
style={{ marginTop: 20, overflowX: 'auto' }}
>
<Spin spinning={loading || (!isInited.current && !isSuccess)}>
<table
className={style.horizontalTreeTable}
border={1}
cellPadding="0"
cellSpacing="0"
>
<thead>
<tr>
{Array.from({ length: maxLevel }).map((_, colIdx) => (
<th
key={colIdx}
className={style.tableTh}
style={{ width: '200px' }}
>
{colIdx + 1}级{index === 0 ? '电表' : '水表'}
</th>
))}
</tr>
</thead>
<tbody>
{tableRows.length > 0 ? (
tableRows.map((row, rowIndex) => {
const actualCols = row.length
return (
<tr key={rowIndex} className={style.tableTr}>
{row.map((node, colIndex) => {
if (!node) return null
return (
<td
key={`${node.deviceId}-${colIndex}`}
className={style.nodeTd}
rowSpan={node.rowspan}
>
<div className={style.nodeContent}>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<img
src={index === 0 ? Electricity : Water}
alt={`${node.level}级表`}
className={style.nodeIcon}
/>
<div className={style.deviceName}>
{node.deviceName}
</div>
</div>
<div className={style.nodeInfo}>
<div className={style.deviceData}>
<div>
用量:
<span>
{formatNumber(node.usageNum) || 0}
</span>
</div>
{node.wastageNum !== undefined && (
<div>
本级损耗:
<span>
{formatNumber(node.wastageNum) || 0}
</span>
</div>
)}
{node.wastagePercent !== undefined && (
<div>
本级损耗率:
<span>{node.wastagePercent || '0%'}</span>
</div>
)}
<div>
总损耗:
<span>
{formatNumber(node.totalWastageNum) || 0}
</span>
</div>
<div>
总损耗率:
<span>
{node.totalWastagePercent || '0%'}
</span>
</div>
</div>
</div>
</div>
</td>
)
})}
{/* 补齐剩余列,使与表头对齐 */}
{Array.from({ length: maxLevel - actualCols }).map(
(_, i) => (
<td key={`pad-${i}`} className={style.emptyTd}></td>
)
)}
</tr>
)
})
) : (
<tr>
<td colSpan={maxLevel} className={style.emptyRowTd}>
暂无设备数据
</td>
</tr>
)}
</tbody>
</table>
</Spin>
</div>
</Card>
)
}
最新发布