//
// ZKBaseTreeView.swift
// TigeriotAppProject
//
// Created by iOSDeveloper on 2025/5/22.
//
import UIKit
struct ZKBaseTreeNode: Hashable, Codable {
var id: String?
var treeNodeId: String?
var parentId: String?
var name: String?
var entityType: String?
var children: [ZKBaseTreeNode]?
var isExpanded: Bool = false
var isHalfSelect: Bool = false
var canSelect: Bool = true
var isSelected: Bool = false
var depth: Int = 0
var uuid: UUID = UUID()
init(id: String?,
treeNodeId: String?,
parentId: String?,
name: String?,
children: [ZKBaseTreeNode]?,
isExpanded: Bool = false,
canSelect: Bool = true,
isSelected: Bool = false,
depth: Int = 0,
uuid: UUID = UUID()) {
self.id = id
self.treeNodeId = treeNodeId
self.parentId = parentId
self.name = name
self.children = children
self.isExpanded = isExpanded
self.canSelect = canSelect
self.isSelected = isSelected
self.depth = depth
self.uuid = uuid
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
isExpanded = try container.decodeIfPresent(Bool.self, forKey: .isExpanded) ?? false
isHalfSelect = try container.decodeIfPresent(Bool.self, forKey: .isHalfSelect) ?? false
canSelect = try container.decodeIfPresent(Bool.self, forKey: .canSelect) ?? true
isSelected = try container.decodeIfPresent(Bool.self, forKey: .isSelected) ?? false
depth = try container.decodeIfPresent(Int.self, forKey: .depth) ?? 0
uuid = try container.decodeIfPresent(UUID.self, forKey: .uuid) ?? UUID()
//-------------------------------------------------------------------//
id = try container.decodeIfPresent(String.self, forKey: .id)
name = try container.decodeIfPresent(String.self, forKey: .name)
parentId = try container.decodeIfPresent(String.self, forKey: .parentId)
treeNodeId = try container.decodeIfPresent(String.self, forKey: .treeNodeId)
children = try container.decodeIfPresent([ZKBaseTreeNode].self, forKey: .children)
entityType = try container.decodeIfPresent(String.self, forKey: .entityType)
}
func hash(into hasher: inout Hasher) {
hasher.combine(treeNodeId)
hasher.combine(parentId)
hasher.combine(uuid)
}
static func == (lhs: ZKBaseTreeNode, rhs: ZKBaseTreeNode) -> Bool {
lhs.treeNodeId == rhs.treeNodeId
}
}
extension ZKBaseTreeNode {
/// 深度优先遍历(DFS)方案
/// - Returns: 返回一维平铺的结构
func flattenedDFS() -> [ZKBaseTreeNode] {
var result = [self]
if let children = children, isExpanded {
for child in children {
result += child.flattenedDFS()
}
}
return result
}
/// 深度优先遍历(DFS)方案,并计算深度
/// - Returns: 返回一维平铺的结构
func flattenedDFSWithDepth() -> [ZKBaseTreeNode] {
var result = [self]
if let children = children, isExpanded {
for var child in children {
child.depth = self.depth + 1
result += child.flattenedDFSWithDepth()
}
}
return result
}
/// 广度优先遍历(BFS)方案
/// - Returns: 返回一维平铺的结构
func flattenedBFS() -> [ZKBaseTreeNode] {
var result = [self]
var queue = children ?? []
while !queue.isEmpty {
let node = queue.removeFirst()
result.append(node)
if let children = node.children, node.isExpanded {
queue.append(contentsOf: children)
}
}
return result
}
/// 广度优先遍历(BFS)方案,并计算深度
/// - Returns: 返回一维平铺的结构
func flattenedBFSWithDepth() -> [ZKBaseTreeNode] {
var result = [self]
var queue: [(node: ZKBaseTreeNode, depth: Int)] = (children ?? []).map { ($0, 1) }
while !queue.isEmpty {
let (currentNode, currentDepth) = queue.removeFirst()
// 更新当前节点的深度
var nodeWithDepth = currentNode
nodeWithDepth.depth = currentDepth
result.append(nodeWithDepth)
// 将子节点加入队列,深度+1
if let children = currentNode.children, currentNode.isExpanded {
queue.append(contentsOf: children.map { ($0, currentDepth + 1) })
}
}
return result
}
}
// 选择模式枚举
enum SelectionMode {
case single
case multiple
}
extension Array where Element == ZKBaseTreeNode {
/// 更新节点选中状态(支持单选/多选和级联控制)
/// - Parameters:
/// - targetNodes: 被点击的目标节点
/// - selectionMode: 选择模式(.single 或 .multiple)
/// - shouldCascade: 是否级联更新子节点和父节点状态
/// - Returns: 更新后的树结构
func updateSelection(for targetNodes: [ZKBaseTreeNode],selectionMode: SelectionMode = .single,shouldCascade: Bool = true) -> [ZKBaseTreeNode] {
var newTree = self
switch selectionMode {
case .single:
// 单选模式:先取消所有选中,再选中当前节点
newTree.deselectAllNodes()
guard let targetNode = targetNodes.first else { return newTree }
newTree.toggleSingleSelection(for: targetNode)
case .multiple:
// 多选模式:切换所有目标节点的选中状态
let targetIds = targetNodes.compactMap { $0.treeNodeId }
newTree.toggleMultipleSelections(targetIds: targetIds)
}
// 级联更新子节点和父节点状态
if shouldCascade {
// 级联更新子节点
for targetNode in targetNodes {
newTree.setChildrenSelection(for: targetNode)
}
// 级联更新父节点状态
newTree.refreshParentStatesWithPostOrder()
}
return newTree
}
/// 取消所有节点的选中状态
mutating func deselectAllNodes() {
for i in indices {
self[i].isSelected = false
self[i].isHalfSelect = false
if var children = self[i].children {
children.deselectAllNodes()
self[i].children = children
}
}
}
/// 切换单个节点的选中状态(用于单选模式)
mutating func toggleSingleSelection(for targetNode: ZKBaseTreeNode) {
for i in indices {
if self[i] == targetNode {
self[i].isSelected = true
self[i].isHalfSelect = false
return
}
if var children = self[i].children {
children.toggleSingleSelection(for: targetNode)
self[i].children = children
}
}
}
// MARK: - 级联控制辅助方法
/// 设置指定节点的所有子节点的选中状态(使用该节点当前的选中状态)
mutating func setChildrenSelection(for targetNode: ZKBaseTreeNode) {
for i in indices {
if self[i] == targetNode {
if var children = self[i].children {
children.setAllChildrenSelection(to: self[i].isSelected)
self[i].children = children
}
return
}
if var children = self[i].children {
children.setChildrenSelection(for: targetNode)
self[i].children = children
}
}
}
}
extension Array where Element == ZKBaseTreeNode {
///平铺
func flattened() -> [ZKBaseTreeNode] {
var result = [ZKBaseTreeNode]()
for node in self {
result += node.flattenedDFSWithDepth()
}
return result
}
/// 更新节点展开状态并返回新树结构
/// - Parameter targetNode: 被点击的目标节点
/// - Returns: 更新后的完整树结构
func updatingParentExpandedStates(for targetNode: ZKBaseTreeNode) -> [ZKBaseTreeNode] {
var newTree = self
let (found, shouldExpand) = newTree.toggleExpandedState(for: targetNode)
if found && shouldExpand {
newTree.expandPathToNode(targetNode)
}
return newTree
}
private mutating func toggleExpandedState(for targetNode: ZKBaseTreeNode) -> (Bool, Bool) {
for i in 0..<self.count {
if self[i] == targetNode {
self[i].isExpanded.toggle()
return (true, self[i].isExpanded)
}
if var children = self[i].children {
let (found, shouldExpand) = children.toggleExpandedState(for: targetNode)
if found {
self[i].children = children
if shouldExpand {
self[i].isExpanded = true
}
return (true, false)
}
}
}
return (false, false)
}
private mutating func expandPathToNode(_ targetNode: ZKBaseTreeNode) {
for i in 0..<self.count {
if self[i] == targetNode {
return
}
if var children = self[i].children {
// let containsTargetBefore = children.contains { $0 == targetNode }
// 递归展开子路径
children.expandPathToNode(targetNode)
// 检查递归后是否包含目标节点
let containsTargetAfter = children.contains { $0 == targetNode }
// 如果子节点包含目标节点,确保当前节点展开
if containsTargetAfter {
self[i].isExpanded = true
}
self[i].children = children
}
}
}
/// 恢复节点展开状态(支持展开整棵子树)
func recoveryExpandedStates(for targetNodes: [ZKBaseTreeNode], selectionMode: SelectionMode = .single, shouldCascade: Bool = true) -> [ZKBaseTreeNode] {
var newTree = self
for node in targetNodes {
newTree.expandNodeAndChildren(for: node, selectionMode: selectionMode, shouldCascade: shouldCascade)
}
return newTree
}
/// 递归展开目标节点及其所有子节点
@discardableResult private mutating func expandNodeAndChildren(for targetNode: ZKBaseTreeNode, selectionMode: SelectionMode = .single, shouldCascade: Bool = true) -> Bool {
var found = false
for i in 0..<self.count {
// 匹配目标节点
if self[i] == targetNode {
found = true
if selectionMode == .single && !shouldCascade {
} else {
// 展开当前节点
self[i].isExpanded = true
// 递归展开所有子节点
if var children = self[i].children {
children.expandAllChildren()
self[i].children = children
}
}
}
// 递归查找目标节点
if var children = self[i].children {
if children.expandNodeAndChildren(for: targetNode, selectionMode: selectionMode, shouldCascade: shouldCascade) {
// 确保父节点展开以显示路径
self[i].isExpanded = true
found = true
}
self[i].children = children
}
}
return found
}
/// 展开所有子节点(深度优先)
private mutating func expandAllChildren() {
for i in 0..<self.count {
self[i].isExpanded = true
if var children = self[i].children {
children.expandAllChildren()
self[i].children = children
}
}
}
/// 更新节点选中状态(级联模式,不依赖 parentId)
/// 更新节点选中状态(支持同链路多节点操作)
/// - Parameter targetNode: 被点击的目标节点
/// - Returns: 更新后的树结构
/*
func updateSelection(for targetNodes: [ZKCustomTreeNode]) -> [ZKCustomTreeNode] {
var newTree = self
// 1. 批量切换目标节点状态(不立即返回)
let targetIds = targetNodes.compactMap { $0.treeNodeId }
newTree.toggleMultipleSelections(targetIds: targetIds)
// 2. 统一更新所有父节点状态
newTree.refreshParentStatesWithPostOrder()
return newTree
}
*/
// MARK: - 改进的递归切换逻辑
private mutating func toggleMultipleSelections(targetIds: [String]) {
guard !targetIds.isEmpty else { return }
for i in 0..<self.count {
var needUpdateChildren = false
// 检查当前节点是否需要切换状态
if targetIds.contains(self[i].treeNodeId ?? "") {
let newState = !self[i].isSelected
self[i].isSelected = newState
self[i].isHalfSelect = false
needUpdateChildren = true
}
// 递归处理子节点(无论当前节点是否匹配都继续)
if var children = self[i].children {
children.toggleMultipleSelections(targetIds: targetIds)
self[i].children = children
}
// 级联更新子节点状态
if needUpdateChildren, var children = self[i].children {
children.setAllChildrenSelection(to: self[i].isSelected)
self[i].children = children
}
}
}
// MARK: - 改进的状态更新逻辑
private mutating func refreshParentStatesWithPostOrder() {
// 先处理子节点
for i in 0..<self.count {
if var children = self[i].children {
children.refreshParentStatesWithPostOrder()
self[i].children = children
}
}
// 再处理当前节点
self.updateNodeState()
}
private mutating func updateNodeState() {
for i in 0..<self.count {
guard let children = self[i].children, !children.isEmpty else {
self[i].isHalfSelect = false
continue
}
// 计算子节点状态
var selectedCount = 0
var hasPartial = false
var hasUnselected = false
for child in children {
if child.isSelected { selectedCount += 1 }
if child.isHalfSelect { hasPartial = true }
if !child.isSelected { hasUnselected = true }
}
// 更新当前节点状态
self[i].isSelected = (selectedCount == children.count) && !hasPartial
self[i].isHalfSelect = hasPartial || (selectedCount > 0 && hasUnselected)
}
}
// MARK: - 优化的子节点设置方法
private mutating func setAllChildrenSelection(to isSelected: Bool) {
for i in 0..<self.count {
// 只修改子节点的选中状态,不修改展开状态
self[i].isSelected = isSelected
self[i].isHalfSelect = false
if var children = self[i].children {
children.setAllChildrenSelection(to: isSelected)
self[i].children = children
}
}
}
/// 获取所有选中节点(包含子节点中的选中项)
func selectedNodes() -> [ZKBaseTreeNode] {
var result = [ZKBaseTreeNode]()
self.traverse { node in
if node.isSelected {
result.append(node)
}
}
return result
}
/// 递归遍历所有节点(深度优先遍历)
private func traverse(_ action: (ZKBaseTreeNode) -> Void) {
for node in self {
action(node)
if let children = node.children {
children.traverse(action)
}
}
}
}
class ZKBaseTreeCell: UITableViewCell {
//左侧stackView
private lazy var leftStackView: UIStackView = {
let leftStackView = UIStackView()
leftStackView.axis = .horizontal
leftStackView.alignment = .center
leftStackView.spacing = 8
self.contentView.addSubview(leftStackView)
return leftStackView
}()
//选择按钮
private lazy var check: UIButton = {
let check = UIButton()
check.setImage(UIImage(named: "tree_check_unselect")?.withTintColor(UIColor(hex: AppThemeUtil.getcolor(themeEnum: .color_text_secondary)) ?? UIColor.text_color_tertiary, renderingMode: .alwaysOriginal), for: .normal)
check.setImage(UIImage(named: "tree_check_half")?.withTintColor(UIColor(hex: AppThemeUtil.getcolor(themeEnum: .color_active)) ?? UIColor.normal_r, renderingMode: .alwaysOriginal), for: .highlighted)
check.setImage(UIImage(named: "tree_check_select")?.withTintColor(UIColor(hex: AppThemeUtil.getcolor(themeEnum: .color_active)) ?? UIColor.normal_r, renderingMode: .alwaysOriginal), for: .selected)
check.addTarget(self, action: #selector(checkClick), for: .touchUpInside)
return check
}()
//树节点图标
private lazy var icon: UIButton = {
let icon = UIButton()
icon.isUserInteractionEnabled = false
icon.setImage(UIImage(named: "c_expand")?.withRenderingMode(.alwaysTemplate), for: .normal)
icon.setImage(UIImage(named: "c_select_expand")?.withRenderingMode(.alwaysTemplate), for: .selected)
icon.tintColor = UIColor(hex: AppThemeUtil.getcolor(themeEnum: .color_text_secondary))
self.contentView.addSubview(icon)
return icon
}()
//节点标题头图片
private lazy var headImageView: UIImageView = {
let headImageView = UIImageView()
containerView.addSubview(headImageView)
return headImageView
}()
//树标题
private lazy var titleLabel: UILabel = {
let titleLabel = UILabel()
titleLabel.font = UIFont.small
titleLabel.textColor = UIColor(hex: AppThemeUtil.getcolor(themeEnum: .color_text_primary))
containerView.addSubview(titleLabel)
return titleLabel
}()
private lazy var containerView: UIView = {
let containerView = UIView()
return containerView
}()
private lazy var containerButton: UIButton = {
let containerButton = UIButton()
containerButton.addTarget(self, action: #selector(buttonClick), for: .touchUpInside)
containerView.addSubview(containerButton)
return containerButton
}()
var buttonBlock: (() -> Void)?
var checkClickBlock: (() -> Void)?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
drawCellThemeColor()
setupViews()
setupConstraints()
}
func setTitleLabel(title: String?) {
titleLabel.text = title
}
//更新树节点中图片文字颜色
func updateColor(color: UIColor?) {
if let color = color {
headImageView.tintColor = color
titleLabel.textColor = color
} else {
headImageView.tintColor = UIColor(hex: AppThemeUtil.getcolor(themeEnum: .color_text_primary))
titleLabel.textColor = UIColor(hex: AppThemeUtil.getcolor(themeEnum: .color_text_primary))
}
}
func updateDepth(depth: Int) {
let offsetValue: CGFloat = CGFloat(depth) * 20
headImageView.snp.remakeConstraints { make in
make.left.equalTo(offsetValue)
make.width.height.equalTo(20)
make.centerY.equalToSuperview()
}
}
func updateCheck(hidden: Bool) {
check.isHidden = hidden
}
func updateHeadImage(image: UIImage?) {
headImageView.image = image?.withRenderingMode(.alwaysTemplate)
headImageView.tintColor = UIColor(hex: AppThemeUtil.getcolor(themeEnum: .color_text_primary))
}
func updateIcon(hidden: Bool) {
icon.isHidden = hidden
}
func updateExpanded(expand: Bool) {
if expand {
icon.isSelected = true
} else {
icon.isSelected = false
}
}
func updateSelected(selected: Bool, halfSelect: Bool) {
if selected {
check.isHighlighted = false
check.isSelected = true
} else if halfSelect {
check.isHighlighted = true
check.isSelected = false
} else {
check.isHighlighted = false
check.isSelected = false
}
}
private func setupViews() {
self.selectionStyle = .none
leftStackView.addArrangedSubview(check)
leftStackView.addArrangedSubview(containerView)
}
private func setupConstraints() {
leftStackView.snp.remakeConstraints { make in
make.left.equalTo(8)
make.height.equalTo(44)
make.centerY.equalToSuperview()
}
check.snp.remakeConstraints { make in
make.width.equalTo(30)
make.height.equalTo(30)
}
containerView.snp.remakeConstraints { make in
make.height.equalTo(30)
}
headImageView.snp.remakeConstraints { make in
make.left.equalTo(0)
make.width.height.equalTo(16)
make.centerY.equalToSuperview()
}
titleLabel.snp.remakeConstraints { make in
make.left.equalTo(headImageView.snp.right).offset(5)
make.centerY.equalToSuperview()
make.right.equalTo(0)
}
containerButton.snp.remakeConstraints { make in
make.edges.equalToSuperview()
}
icon.snp.remakeConstraints { make in
make.right.equalTo(-5)
make.width.height.equalTo(15)
make.centerY.equalToSuperview()
}
}
@objc func buttonClick() {
buttonBlock?()
}
@objc func checkClick() {
checkClickBlock?()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
class ZKBaseTreeView: UIView {
var trees = [ZKBaseTreeNode]()
enum Section: Hashable {
case main
}
var dataSource: UITableViewDiffableDataSource<Section, ZKBaseTreeNode>!
lazy var treeTableView: UITableView = {
let treeTableView = UITableView(frame: .zero, style: .insetGrouped)
treeTableView.backgroundColor = UIColor.clear
treeTableView.sectionHeaderHeight = 0
treeTableView.sectionFooterHeight = 0
treeTableView.estimatedSectionHeaderHeight = 0
treeTableView.estimatedSectionFooterHeight = 0
treeTableView.delegate = self
treeTableView.register(ZKBaseTreeCell.self, forCellReuseIdentifier: "ZKBaseTreeCell")
self.addSubview(treeTableView)
return treeTableView
}()
var selectNodes = [ZKBaseTreeNode]()
var updateSelectBlock: (([ZKBaseTreeNode]) -> Void)?
var selectionMode: SelectionMode = .single
var shouldCascade: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
setupUI()
configureDataSource()
updateExpandedStates()
applyInitialSnapshot()
}
func setTreeData(datas: [ZKBaseTreeNode]) {
trees = datas
if selectNodes.count > 0 {
updateExpandedStates()
selectNodes = trees.selectedNodes()
updateSelectBlock?(selectNodes)
}
applyInitialSnapshot()
}
private func setupUI() {
treeTableView.snp.remakeConstraints { make in
make.edges.equalToSuperview()
}
}
func configureDataSource() {
dataSource = UITableViewDiffableDataSource(tableView: treeTableView, cellProvider: { [weak self] tableView, indexPath, itemIdentifier in
let cell: ZKBaseTreeCell = tableView.dequeueReusableCell(withIdentifier: "ZKBaseTreeCell", for: indexPath) as! ZKBaseTreeCell
guard let `self` = self else { return cell }
cell.setTitleLabel(title: itemIdentifier.name)
cell.updateDepth(depth: itemIdentifier.depth)
cell.updateHeadImage(image: CustomTreeTool.getTreeNodeImage(type: itemIdentifier.entityType))
cell.updateSelected(selected: itemIdentifier.isSelected, halfSelect: itemIdentifier.isHalfSelect)
cell.updateIcon(hidden: itemIdentifier.children?.count == 0)
cell.updateExpanded(expand: itemIdentifier.isExpanded)
cell.checkClickBlock = { [weak self] in
guard let `self` = self else { return }
trees = trees.updateSelection(for: [itemIdentifier], selectionMode: selectionMode, shouldCascade: shouldCascade)
updateSnapshot()
selectNodes = trees.selectedNodes()
updateSelectBlock?(selectNodes)
}
return cell
})
}
private func updateExpandedStates() {
trees = trees.recoveryExpandedStates(for: selectNodes, selectionMode: selectionMode, shouldCascade: shouldCascade)
trees = trees.updateSelection(for: selectNodes, selectionMode: selectionMode, shouldCascade: shouldCascade)
}
func applyInitialSnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Section, ZKBaseTreeNode>()
snapshot.appendSections([.main])
let items = trees.flattened()
snapshot.appendItems(items, toSection: .main)
dataSource.apply(snapshot, animatingDifferences: false)
}
func updateSnapshot(animated: Bool = false) {
applyInitialSnapshot()
treeTableView.visibleCells.forEach { cell in
let cell: ZKBaseTreeCell = cell as! ZKBaseTreeCell
if let indexPath = treeTableView.indexPath(for: cell) {
if let item = dataSource.itemIdentifier(for: indexPath) {
cell.setTitleLabel(title: item.name)
cell.updateDepth(depth: item.depth)
cell.updateHeadImage(image: CustomTreeTool.getTreeNodeImage(type: item.entityType))
cell.updateSelected(selected: item.isSelected, halfSelect: item.isHalfSelect)
cell.updateIcon(hidden: item.children?.count == 0)
cell.updateExpanded(expand: item.isExpanded)
}
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension ZKBaseTreeView: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
trees = trees.updatingParentExpandedStates(for: item)
updateSnapshot()
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return CGFloat.leastNormalMagnitude
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 44
}
}
UITableView实现树结构
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
相关阅读更多精彩内容
- ### Meta Description 探索JavaScript树结构的高效实现方法。本文详细讲解树结构基础概念...
- 今天在做栏目树的处理的时候,对于树结构的操作有点晕,看了一篇文章,感觉讲解的挺好的,测试了一下文中的代码,感觉挺好...
- 1.堆排序基本介绍 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间...
- 我们会经常用到树形,那么树形结构的数据是在前端做还是在后台做呢? 我自己用过前端的ztree,selectTree...