492 lines
14 KiB
TypeScript
492 lines
14 KiB
TypeScript
|
|
import { MyBetaModalFormProps, MyButtons } from '@/common';
|
||
|
|
import { MyModal } from '@/components/MyModal';
|
||
|
|
import { Apis } from '@/gen/Apis';
|
||
|
|
import { ProCard } from '@ant-design/pro-components';
|
||
|
|
import {
|
||
|
|
Alert,
|
||
|
|
Button,
|
||
|
|
Checkbox,
|
||
|
|
message,
|
||
|
|
Space,
|
||
|
|
Tree,
|
||
|
|
Typography,
|
||
|
|
} from 'antd';
|
||
|
|
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||
|
|
import type { DataNode } from 'antd/es/tree';
|
||
|
|
import { useEffect, useRef, useState } from 'react';
|
||
|
|
|
||
|
|
const { Title } = Typography;
|
||
|
|
|
||
|
|
interface TreeNodeType extends DataNode {
|
||
|
|
id: number;
|
||
|
|
key: string;
|
||
|
|
title: string;
|
||
|
|
isLeaf?: boolean;
|
||
|
|
children?: TreeNodeType[];
|
||
|
|
asset_buildings_id?: number;
|
||
|
|
asset_units_id?: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 扩展 MyBetaModalFormProps 接口,添加 onCancel 属性
|
||
|
|
interface ChargeStandardHasHouseProps extends MyBetaModalFormProps {
|
||
|
|
onCancel?: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function ChargeStandardHasHouse(
|
||
|
|
props: ChargeStandardHasHouseProps,
|
||
|
|
) {
|
||
|
|
const [treeData, setTreeData] = useState<TreeNodeType[]>([]);
|
||
|
|
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
|
||
|
|
const [checkedKeys, setCheckedKeys] = useState<React.Key[]>([]);
|
||
|
|
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
||
|
|
const [autoExpandParent, setAutoExpandParent] = useState<boolean>(true);
|
||
|
|
const [loading, setLoading] = useState<boolean>(false);
|
||
|
|
const [selectAll, setSelectAll] = useState<boolean>(false);
|
||
|
|
const [selectedHouses, setSelectedHouses] = useState<
|
||
|
|
{ id: number; name: string }[]
|
||
|
|
>([]);
|
||
|
|
const modalRef: any = useRef(null);
|
||
|
|
|
||
|
|
// 加载楼栋数据
|
||
|
|
const loadBuildings = async () => {
|
||
|
|
setLoading(true);
|
||
|
|
|
||
|
|
try {
|
||
|
|
const res = await Apis.Asset.AssetBuildings.List({
|
||
|
|
asset_projects_id: props?.item?.asset_projects_id,
|
||
|
|
});
|
||
|
|
if (res?.data) {
|
||
|
|
const buildings = res.data.map((building: any) => ({
|
||
|
|
id: building.id,
|
||
|
|
key: `building-${building.id}`,
|
||
|
|
title: building.name,
|
||
|
|
children: [],
|
||
|
|
isLeaf: false,
|
||
|
|
}));
|
||
|
|
setTreeData(buildings);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('加载楼栋失败:', error);
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 加载单元数据
|
||
|
|
const loadUnits = async (buildingId: number, buildingKey: string) => {
|
||
|
|
setLoading(true);
|
||
|
|
try {
|
||
|
|
const res = await Apis.Asset.AssetUnits.List({
|
||
|
|
asset_projects_id: props?.item?.asset_projects_id,
|
||
|
|
asset_buildings_id: buildingId,
|
||
|
|
});
|
||
|
|
|
||
|
|
if (res?.data) {
|
||
|
|
const units = res.data.map((unit: any) => ({
|
||
|
|
id: unit.id,
|
||
|
|
key: `unit-${unit.id}`,
|
||
|
|
title: unit.name,
|
||
|
|
children: [],
|
||
|
|
isLeaf: false,
|
||
|
|
asset_buildings_id: buildingId,
|
||
|
|
}));
|
||
|
|
|
||
|
|
// 更新树形数据
|
||
|
|
const newTreeData = [...treeData];
|
||
|
|
const buildingNode = newTreeData.find(
|
||
|
|
(node) => node.key === buildingKey,
|
||
|
|
);
|
||
|
|
if (buildingNode) {
|
||
|
|
buildingNode.children = units;
|
||
|
|
}
|
||
|
|
|
||
|
|
setTreeData(newTreeData);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('加载单元失败:', error);
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 加载房屋数据
|
||
|
|
const loadHouses = async (
|
||
|
|
buildingId: number,
|
||
|
|
unitId: number,
|
||
|
|
unitKey: string,
|
||
|
|
) => {
|
||
|
|
setLoading(true);
|
||
|
|
try {
|
||
|
|
const res = await Apis.Asset.AssetHouses.List({
|
||
|
|
asset_projects_id: props?.item?.asset_projects_id,
|
||
|
|
asset_buildings_id: buildingId,
|
||
|
|
asset_units_id: unitId,
|
||
|
|
});
|
||
|
|
|
||
|
|
if (res?.data) {
|
||
|
|
const houses = res.data.map((house: any) => ({
|
||
|
|
id: house.id,
|
||
|
|
key: `house-${house.id}`,
|
||
|
|
title: `${house.name} (${house.floor}层)`,
|
||
|
|
isLeaf: true,
|
||
|
|
asset_buildings_id: buildingId,
|
||
|
|
asset_units_id: unitId,
|
||
|
|
}));
|
||
|
|
|
||
|
|
// 更新树形数据
|
||
|
|
const newTreeData = [...treeData];
|
||
|
|
const buildingNode = newTreeData.find(
|
||
|
|
(node) =>
|
||
|
|
node.asset_buildings_id === undefined && node.id === buildingId,
|
||
|
|
);
|
||
|
|
|
||
|
|
if (buildingNode && buildingNode.children) {
|
||
|
|
const unitNode = buildingNode.children.find(
|
||
|
|
(node) => node.key === unitKey,
|
||
|
|
);
|
||
|
|
if (unitNode) {
|
||
|
|
unitNode.children = houses;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
setTreeData(newTreeData);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('加载房屋失败:', error);
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 初始化加载数据
|
||
|
|
useEffect(() => {
|
||
|
|
if (props?.item?.asset_projects_id) {
|
||
|
|
loadBuildings();
|
||
|
|
} else {
|
||
|
|
console.warn('缺少 asset_projects_id 参数');
|
||
|
|
}
|
||
|
|
}, [props?.item?.id]);
|
||
|
|
|
||
|
|
// 处理节点展开
|
||
|
|
const onExpand = (expandedKeysValue: React.Key[]) => {
|
||
|
|
setExpandedKeys(expandedKeysValue);
|
||
|
|
setAutoExpandParent(false);
|
||
|
|
};
|
||
|
|
|
||
|
|
// 处理节点选中
|
||
|
|
const onCheck = (
|
||
|
|
checkedKeysValue:
|
||
|
|
| React.Key[]
|
||
|
|
| { checked: React.Key[]; halfChecked: React.Key[] },
|
||
|
|
) => {
|
||
|
|
// 处理不同格式的返回值
|
||
|
|
const keys = Array.isArray(checkedKeysValue)
|
||
|
|
? checkedKeysValue
|
||
|
|
: checkedKeysValue.checked;
|
||
|
|
|
||
|
|
// 获取之前的选中状态,用于比较变化
|
||
|
|
const prevKeys = new Set(checkedKeys);
|
||
|
|
const newKeys = new Set(keys);
|
||
|
|
|
||
|
|
// 找出新取消选中的节点
|
||
|
|
const uncheckedKeys = [...prevKeys].filter((key) => !newKeys.has(key));
|
||
|
|
|
||
|
|
// 如果有节点被取消选中,同步取消其所有子节点
|
||
|
|
if (uncheckedKeys.length > 0) {
|
||
|
|
const keysToRemove = new Set<React.Key>();
|
||
|
|
const findChildKeys = (nodes: TreeNodeType[], parentKey: React.Key) => {
|
||
|
|
nodes.forEach((node) => {
|
||
|
|
if (node.key === parentKey) {
|
||
|
|
// 将当前节点及其所有子节点的key加入待移除集合
|
||
|
|
const collectKeys = (n: TreeNodeType) => {
|
||
|
|
keysToRemove.add(n.key);
|
||
|
|
if (n.children) {
|
||
|
|
n.children.forEach(collectKeys);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
collectKeys(node);
|
||
|
|
} else if (node.children) {
|
||
|
|
findChildKeys(node.children, parentKey);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
uncheckedKeys.forEach((key) => {
|
||
|
|
findChildKeys(treeData, key);
|
||
|
|
});
|
||
|
|
|
||
|
|
// 从选中keys中移除所有需要取消的节点
|
||
|
|
const finalKeys = keys.filter((key) => !keysToRemove.has(key));
|
||
|
|
setCheckedKeys(finalKeys);
|
||
|
|
} else {
|
||
|
|
setCheckedKeys(keys);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 收集所有选中的房屋
|
||
|
|
const selectedHousesList: { id: number; name: string }[] = [];
|
||
|
|
|
||
|
|
// 遍历树形数据,收集选中节点下的所有房屋
|
||
|
|
const collectHouses = (nodes: TreeNodeType[], checkedKeys: React.Key[]) => {
|
||
|
|
nodes.forEach((node) => {
|
||
|
|
if (checkedKeys.includes(node.key)) {
|
||
|
|
if (node.isLeaf) {
|
||
|
|
// 如果是房屋节点,直接添加
|
||
|
|
selectedHousesList.push({
|
||
|
|
id: node.id,
|
||
|
|
name: node.title as string,
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
// 如果是楼栋或单元节点,递归收集其下的所有房屋
|
||
|
|
if (node.children) {
|
||
|
|
collectHouses(node.children, checkedKeys);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
collectHouses(treeData, keys);
|
||
|
|
|
||
|
|
setSelectedHouses(selectedHousesList);
|
||
|
|
setSelectAll(
|
||
|
|
selectedHousesList.length > 0 &&
|
||
|
|
selectedHousesList.length ===
|
||
|
|
treeData.reduce(
|
||
|
|
(total, building) =>
|
||
|
|
total +
|
||
|
|
(building.children?.reduce(
|
||
|
|
(unitTotal, unit) => unitTotal + (unit.children?.length || 0),
|
||
|
|
0,
|
||
|
|
) || 0),
|
||
|
|
0,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
// 处理节点选择
|
||
|
|
const onSelect = (selectedKeysValue: React.Key[]) => {
|
||
|
|
setSelectedKeys(selectedKeysValue);
|
||
|
|
};
|
||
|
|
|
||
|
|
// 处理动态加载数据
|
||
|
|
const onLoadData = async (node: TreeNodeType) => {
|
||
|
|
if (node.isLeaf) {
|
||
|
|
return Promise.resolve();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 加载楼栋下的单元
|
||
|
|
if (node.key.toString().startsWith('building-')) {
|
||
|
|
const buildingId = node.id;
|
||
|
|
await loadUnits(buildingId, node.key as string);
|
||
|
|
|
||
|
|
// 如果楼栋被选中,加载并选中其下所有单元和房屋
|
||
|
|
if (checkedKeys.includes(node.key)) {
|
||
|
|
const newTreeData = [...treeData];
|
||
|
|
const buildingNode = newTreeData.find((n) => n.key === node.key);
|
||
|
|
if (buildingNode?.children) {
|
||
|
|
// 加载每个单元下的房屋
|
||
|
|
for (const unit of buildingNode.children) {
|
||
|
|
await loadHouses(buildingId, unit.id, unit.key as string);
|
||
|
|
}
|
||
|
|
// 更新树形数据
|
||
|
|
setTreeData(newTreeData);
|
||
|
|
// 收集所有房屋的key
|
||
|
|
const allKeys: React.Key[] = [];
|
||
|
|
const collectKeys = (nodes: TreeNodeType[]) => {
|
||
|
|
nodes.forEach((node) => {
|
||
|
|
allKeys.push(node.key);
|
||
|
|
if (node.children) {
|
||
|
|
collectKeys(node.children);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
};
|
||
|
|
collectKeys([buildingNode]);
|
||
|
|
// 更新选中状态
|
||
|
|
setCheckedKeys(Array.from(new Set([...checkedKeys, ...allKeys])));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return Promise.resolve();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 加载单元下的房屋
|
||
|
|
if (node.key.toString().startsWith('unit-')) {
|
||
|
|
const unitId = node.id;
|
||
|
|
const buildingId = node.asset_buildings_id as number;
|
||
|
|
await loadHouses(buildingId, unitId, node.key as string);
|
||
|
|
|
||
|
|
// 如果单元被选中,选中其下所有房屋
|
||
|
|
if (checkedKeys.includes(node.key)) {
|
||
|
|
const newTreeData = [...treeData];
|
||
|
|
const buildingNode = newTreeData.find((n) =>
|
||
|
|
n.children?.some((unit) => unit.key === node.key),
|
||
|
|
);
|
||
|
|
const unitNode = buildingNode?.children?.find(
|
||
|
|
(n) => n.key === node.key,
|
||
|
|
);
|
||
|
|
if (unitNode?.children) {
|
||
|
|
const houseKeys = unitNode.children.map((house) => house.key);
|
||
|
|
setCheckedKeys(Array.from(new Set([...checkedKeys, ...houseKeys])));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return Promise.resolve();
|
||
|
|
}
|
||
|
|
|
||
|
|
return Promise.resolve();
|
||
|
|
};
|
||
|
|
|
||
|
|
// 处理全选
|
||
|
|
const handleSelectAll = (e: CheckboxChangeEvent) => {
|
||
|
|
setSelectAll(e.target.checked);
|
||
|
|
|
||
|
|
if (e.target.checked) {
|
||
|
|
// 收集所有房屋节点的key
|
||
|
|
const allHouseKeys: React.Key[] = [];
|
||
|
|
const allHouses: { id: number; name: string }[] = [];
|
||
|
|
|
||
|
|
treeData.forEach((building) => {
|
||
|
|
building.children?.forEach((unit) => {
|
||
|
|
unit.children?.forEach((house) => {
|
||
|
|
if (house.isLeaf) {
|
||
|
|
allHouseKeys.push(house.key);
|
||
|
|
allHouses.push({
|
||
|
|
id: house.id,
|
||
|
|
name: house.title as string,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
setCheckedKeys(allHouseKeys);
|
||
|
|
setSelectedHouses(allHouses);
|
||
|
|
} else {
|
||
|
|
// 取消全选
|
||
|
|
setCheckedKeys([]);
|
||
|
|
setSelectedHouses([]);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 提交选中的房屋
|
||
|
|
const handleSubmit = async () => {
|
||
|
|
if (selectedHouses.length === 0) {
|
||
|
|
message.warning('请至少选择一个房屋');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
setLoading(true);
|
||
|
|
// 将 number[] 转换为 string[]
|
||
|
|
const houses_ids = selectedHouses.map((house) => house.id.toString());
|
||
|
|
|
||
|
|
await Apis.HouseCharage.HouseChargeHasHouses.Store({
|
||
|
|
house_charge_standards_id: props?.item?.id,
|
||
|
|
houses_ids,
|
||
|
|
});
|
||
|
|
|
||
|
|
message.success('绑定房屋成功');
|
||
|
|
props?.reload?.();
|
||
|
|
props?.onCancel?.();
|
||
|
|
} catch (error) {
|
||
|
|
console.error('绑定房屋失败:', error);
|
||
|
|
message.error('绑定房屋失败');
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<MyModal
|
||
|
|
title={props.title || '查看'}
|
||
|
|
width="800px"
|
||
|
|
myRef={modalRef}
|
||
|
|
trigger={
|
||
|
|
<MyButtons.Default
|
||
|
|
type="primary"
|
||
|
|
size="middle"
|
||
|
|
title={`${props.title}`}
|
||
|
|
/>
|
||
|
|
}
|
||
|
|
node={
|
||
|
|
<ProCard
|
||
|
|
title={
|
||
|
|
<Alert
|
||
|
|
message="请选择需要绑定的房屋"
|
||
|
|
type="info"
|
||
|
|
showIcon
|
||
|
|
style={{ margin: 0 }}
|
||
|
|
/>
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<div style={{ display: 'flex', height: '500px' }}>
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
width: '50%',
|
||
|
|
borderRight: '1px solid #f0f0f0',
|
||
|
|
padding: '0 16px',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div style={{ marginBottom: 16 }}>
|
||
|
|
<Checkbox checked={selectAll} onChange={handleSelectAll}>
|
||
|
|
全选
|
||
|
|
</Checkbox>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{loading && <div>加载中...</div>}
|
||
|
|
|
||
|
|
<Tree
|
||
|
|
checkable
|
||
|
|
checkStrictly={false}
|
||
|
|
onExpand={onExpand}
|
||
|
|
expandedKeys={expandedKeys}
|
||
|
|
autoExpandParent={autoExpandParent}
|
||
|
|
onCheck={onCheck}
|
||
|
|
checkedKeys={checkedKeys}
|
||
|
|
onSelect={onSelect}
|
||
|
|
selectedKeys={selectedKeys}
|
||
|
|
loadData={onLoadData}
|
||
|
|
treeData={treeData}
|
||
|
|
height={400}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style={{ width: '50%', padding: '0 16px' }}>
|
||
|
|
<div style={{ marginBottom: 16 }}>
|
||
|
|
<Title level={5}>已选房屋 ({selectedHouses.length})</Title>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style={{ height: 400, overflow: 'auto' }}>
|
||
|
|
{selectedHouses.length > 0 ? (
|
||
|
|
<ul style={{ padding: '0 0 0 20px' }}>
|
||
|
|
{selectedHouses.map((house) => (
|
||
|
|
<li key={house.id}>{house.name}</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
) : (
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
color: '#999',
|
||
|
|
textAlign: 'center',
|
||
|
|
marginTop: 100,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
暂无选中房屋{' '}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style={{ marginTop: 16, textAlign: 'right' }}>
|
||
|
|
<Space>
|
||
|
|
<Button onClick={props?.onCancel}>取消</Button>
|
||
|
|
<Button type="primary" loading={loading} onClick={handleSubmit}>
|
||
|
|
确认绑定
|
||
|
|
</Button>
|
||
|
|
</Space>
|
||
|
|
</div>
|
||
|
|
</ProCard>
|
||
|
|
}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
}
|