using SuperTreeView; using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using ZXKFramework; using static UnityEditor.Progress; namespace YiLiao.XinFeiTingZhen { public class TreeViewPanel : UIBase { public override string GroupName => "TreeViewPanel"; public override string Name => "TreeViewPanel"; public TreeView mTreeView; int mCurSelectedItemId = 0; public ScrollRect scrollRect; // ================= 新增:缓存映射变量 ================= // Key: 数据ID (如 "DiYiXinYin"), Value: 对应的 TreeViewItem private Dictionary m_dataNodeMap = new Dictionary(); // Key: 子节点, Value: 父节点 (用于向上回溯) private Dictionary m_parentMap = new Dictionary(); // 虚拟根节点引用,作为回溯的终点 private TreeViewItem m_virtualRootInstance; // ===================================================== public override void Init(IUIManager uictrl) { base.Init(uictrl); scrollRect = transform.FindFirst("TreeScrollView"); mTreeView = transform.FindFirst("TreeView"); mTreeView.OnTreeListAddOneItem = OnTreeListAddOneItem; mTreeView.OnItemExpandBegin = OnItemExpandBegin; mTreeView.OnItemCollapseBegin = OnItemCollapseBegin; mTreeView.OnItemCustomEvent = OnItemCustomEvent; mTreeView.OnTreeListRepositionFinish = OnTreeListRepositionFinish; mTreeView.InitView(); BuildTreeAutomatically(); mTreeView.CollapseAllItem(); } void BuildTreeAutomatically() { var datas1 = new List { // --- 胸部体表标志 --- new[] { "胸部体表标志", "胸骨柄", "XiongGuBing" }, new[] { "胸部体表标志", "胸骨上切迹", "XiongGuShangQieJi" }, new[] { "胸部体表标志", "胸骨角", "XiongGuJiao" }, new[] { "胸部体表标志", "剑突", "JianTu" }, new[] { "胸部体表标志", "1-7肋骨", "LeiGu1_7" }, new[] { "胸部体表标志", "11-12肋骨", "LeiGu11_12" }, new[] { "胸部体表标志", "肋间隙", "LeiJianXi" }, new[] { "胸部体表标志", "肩胛骨", "JianJiaGu" }, new[] { "胸部体表标志", "肩胛下角", "JianJiaXiaJiao" }, // --- 垂直线标志 --- new[] { "垂直线标志", "前正中线", "QianZhengZhongXian" }, new[] { "垂直线标志", "(左、右)锁骨中线", "SuoGuZhongXian" }, new[] { "垂直线标志", "(左、右)胸骨线", "XiongGuXian" }, new[] { "垂直线标志", "(左、右)胸骨旁线", "XiongGuPangXian" }, new[] { "垂直线标志", "(左、右)腋前线", "YeQianXian" }, new[] { "垂直线标志", "(左、右)腋后线", "YeHouXian" }, new[] { "垂直线标志", "(左、右)腋中线", "YeZhongXian" }, new[] { "垂直线标志", "(左、右)肩胛线", "JianJiaXian" }, new[] { "垂直线标志", "后正中线", "HouZhengZhongXian" }, // --- 解剖区域 --- new[] { "解剖区域", "(左、右)肩胛上区", "JianJiaShangQu" }, new[] { "解剖区域", "(左、右)肩胛下区", "JianJiaXiaQu" }, new[] { "解剖区域", "(左、右)肩胛间区", "JianJiaQuJian" }, // --- 动画 --- new[] { "肺功能动画", "FeiGongNengDongHua" }, new[] { "心功能动画", "XinGongNengDongHua" }, // --- 肺部听诊 --- new[] { "肺部听诊", "正常呼吸音", "气管呼吸音", "QiGuanHuXiYin" }, new[] { "肺部听诊", "正常呼吸音", "支气管呼吸音", "ZhiQiGuanHuXiYin" }, new[] { "肺部听诊", "正常呼吸音", "支气管肺泡呼吸音", "ZhiQiGuanFeiPaoHuXiYin" }, new[] { "肺部听诊", "正常呼吸音", "肺泡呼吸音", "FeiPaoHuXiYin" }, new[] { "肺部听诊", "异常呼吸音", "异常肺泡呼吸音", "肺泡呼吸音减弱或消失", "FeiPaoHuXiYinJianRuoHuoXiaoShi" }, new[] { "肺部听诊", "异常呼吸音", "异常肺泡呼吸音", "肺泡呼吸音增强", "FeiPaoHuXiYinZengQiang" }, new[] { "肺部听诊", "异常呼吸音", "异常肺泡呼吸音", "呼吸音延长", "HuXiYinYanChang" }, new[] { "肺部听诊", "异常呼吸音", "异常肺泡呼吸音", "断续性呼吸音", "DuanXuXingHuXiYin" }, new[] { "肺部听诊", "异常呼吸音", "异常肺泡呼吸音", "粗糙性呼吸音", "CuCaoXingHuXiYin" }, new[] { "肺部听诊", "异常呼吸音", "异常支气管呼吸音", "YiChangZhiQiGuanHuXiYin" }, new[] { "肺部听诊", "异常呼吸音", "异常支气管肺泡呼吸音", "YiChangZhiQiGuanFeiPaoHuXiYin" }, new[] { "肺部听诊", "异常呼吸音", "啰音", "粗湿啰音(大水泡音)", "CuShiLuoYin" }, new[] { "肺部听诊", "异常呼吸音", "啰音", "中湿啰音(中水泡音)", "ZhongShiLuoYin" }, new[] { "肺部听诊", "异常呼吸音", "啰音", "细湿啰音(小水泡音)", "XiShiLuoYin" }, new[] { "肺部听诊", "异常呼吸音", "啰音", "Velcro啰音", "VelcroLuoYin" }, new[] { "肺部听诊", "异常呼吸音", "啰音", "捻发音", "NianFaYin" }, new[] { "肺部听诊", "异常呼吸音", "啰音", "高调干啰音(哨笛音)", "GaoDiaoGanLuoYin" }, new[] { "肺部听诊", "异常呼吸音", "啰音", "低调干啰音(鼾音)", "DiDiaoGanLuoYin" }, new[] { "肺部听诊", "异常呼吸音", "胸膜摩擦音", "XiongMoMoCaYin" }, // --- 心脏听诊 --- new[] { "心脏听诊", "心脏瓣膜听诊区", "二尖瓣区(心尖区)", "ErJianBanQu" }, new[] { "心脏听诊", "心脏瓣膜听诊区", "肺动脉瓣区", "FeiDongMaiBanQu" }, new[] { "心脏听诊", "心脏瓣膜听诊区", "主动脉瓣区", "ZhuDongMaiBanQu" }, new[] { "心脏听诊", "心脏瓣膜听诊区", "主动脉瓣第二听诊区", "ZhuDongMaiBanDiErTingZhenQu" }, new[] { "心脏听诊", "心脏瓣膜听诊区", "三尖瓣区", "SanJianBanQu" }, new[] { "心脏听诊", "心率", "XinLv" }, new[] { "心脏听诊", "心音", "第一心音", "DiYiXinYin" }, new[] { "心脏听诊", "心音", "第二心音", "DiErXinYin" }, new[] { "心脏听诊", "心音", "第三心音", "DiSanXinYin" }, new[] { "心脏听诊", "心音", "第四心音", "DiSiXinYin" }, new[] { "心脏听诊", "心音", "第一心音增强", "DiYiXinYinZengQiang" }, new[] { "心脏听诊", "心音", "第一心音减弱", "DiYiXinYinJianRuo" }, new[] { "心脏听诊", "心音", "第一心音强弱不等", "DiYiXinYinQiangRuoBuDeng" }, //new[] { "心脏听诊", "心音", "第二心音增强", "DiErXinYinZengQiang" }, new[] { "心脏听诊", "心音", "第二心音减弱", "DiErXinYinJianRuo" }, new[] { "心脏听诊", "心音", "第一心音分裂", "DiYiXinYinFenLie" }, new[] { "心脏听诊", "心音", "第二心音通常分裂", "DiErXinYinTongChangFenLie" }, new[] { "心脏听诊", "心音", "第二心音固定分裂", "DiErXinYinGuDingFenLie" }, new[] { "心脏听诊", "心音", "第二心音反常分裂", "DiErXinYinFanChangFenLie" }, new[] { "心脏听诊", "额外心音", "舒张期额外心音", "舒张早期奔马律", "ShuZhangZaoQiBenMaLv" }, new[] { "心脏听诊", "额外心音", "舒张期额外心音", "舒张晚期奔马律", "ShuZhangWanQiBenMaLv" }, new[] { "心脏听诊", "额外心音", "舒张期额外心音", "开瓣音", "KaiBanYin" }, new[] { "心脏听诊", "额外心音", "收缩期额外心音", "收缩中、晚期喀喇音", "ShouSuoZhongWanQiKaLaYin" }, new[] { "心脏听诊", "心脏杂音", "收缩期杂音", "二尖瓣区", "ErJianBanQu2" }, new[] { "心脏听诊", "心脏杂音", "收缩期杂音", "主动脉瓣区", "ZhuDongMaiBanQu2" }, new[] { "心脏听诊", "心脏杂音", "收缩期杂音", "肺动脉瓣区", "FeiDongMaiBanQu2" }, new[] { "心脏听诊", "心脏杂音", "收缩期杂音", "三尖瓣区", "SanJianBanQu2" }, new[] { "心脏听诊", "心脏杂音", "舒张期杂音", "二尖瓣区", "ErJianBanQu3" }, new[] { "心脏听诊", "心脏杂音", "舒张期杂音", "主动脉瓣区", "ZhuDongMaiBanQu3" }, new[] { "心脏听诊", "心脏杂音", "舒张期杂音", "肺动脉瓣区", "FeiDongMaiBanQu3" }, new[] { "心脏听诊", "心脏杂音", "舒张期杂音", "三尖瓣区", "SanJianBanQu3" }, new[] { "心脏听诊", "心脏杂音", "连续性杂音", "LianXuXingZaYin" }, new[] { "心脏听诊", "心脏杂音", "心包摩擦音", "XinBaoMoCaYin" } }; var datas2 = new List { // --- 胸部体表标志 --- new[] { "胸部体表标志", "胸骨柄", "XiongGuBing" }, new[] { "胸部体表标志", "胸骨上切迹", "XiongGuShangQieJi" }, new[] { "胸部体表标志", "胸骨角", "XiongGuJiao" }, new[] { "胸部体表标志", "剑突", "JianTu" }, new[] { "胸部体表标志", "浮肋", "FuLei" }, new[] { "胸部体表标志", "肩胛骨、肩胛下角", "JianJiaGuJianJiaXiaJiao" }, // --- 垂直线标志 --- new[] { "垂直线标志", "前正中线", "QianZhengZhongXian" }, new[] { "垂直线标志", "(左、右)锁骨中线", "SuoGuZhongXian" }, new[] { "垂直线标志", "(左、右)胸骨线", "XiongGuXian" }, new[] { "垂直线标志", "(左、右)胸骨旁线", "XiongGuPangXian" }, new[] { "垂直线标志", "(左、右)腋中线", "YeZhongXian" }, new[] { "垂直线标志", "(左、右)肩胛线", "JianJiaXian" }, // --- 解剖区域 --- new[] { "解剖区域", "(左、右)肩胛上区", "JianJiaShangQu" }, new[] { "解剖区域", "(左、右)肩胛下区", "JianJiaXiaQu" }, new[] { "解剖区域", "(左、右)肩胛间区", "JianJiaQuJian" }, // --- 肺部听诊 --- new[] { "肺部听诊", "正常呼吸音", "支气管呼吸音", "ZhiQiGuanHuXiYin" }, new[] { "肺部听诊", "正常呼吸音", "支气管肺泡呼吸音", "ZhiQiGuanFeiPaoHuXiYin" }, new[] { "肺部听诊", "正常呼吸音", "肺泡呼吸音", "FeiPaoHuXiYin" }, new[] { "肺部听诊", "异常呼吸音", "异常肺泡呼吸音", "肺泡呼吸音减弱或消失", "FeiPaoHuXiYinJianRuoHuoXiaoShi" }, new[] { "肺部听诊", "异常呼吸音", "啰音", "干啰音", "GanLuoYin" }, new[] { "肺部听诊", "异常呼吸音", "啰音", "低调干啰音(鼾音)", "DiDiaoGanLuoYin" }, new[] { "肺部听诊", "异常呼吸音", "啰音", "湿啰音", "ShiLuoYin" }, new[] { "肺部听诊", "异常呼吸音", "啰音", "干啰音&湿啰音", "GanLuoYin_ShiLuoYin" }, new[] { "肺部听诊", "异常呼吸音", "胸膜摩擦音", "XiongMoMoCaYin" }, // --- 心脏听诊 --- new[] { "心脏听诊", "心脏瓣膜听诊区", "二尖瓣区(心尖区)", "ErJianBanQu" }, new[] { "心脏听诊", "额外心音", "舒张期额外心音", "舒张早期奔马律", "ShuZhangZaoQiBenMaLv" }, new[] { "心脏听诊", "心脏杂音", "连续性杂音", "LianXuXingZaYin" }, new[] { "心脏听诊", "心脏杂音", "心包摩擦音", "XinBaoMoCaYin" } }; var datas = new List(); if(MVC.GetModel().modeType == ModeType.ShiXun) { datas = datas1; } if (MVC.GetModel().modeType == ModeType.KaoHe) { datas = datas2; } // 【重要】初始化虚拟根节点并保存引用 m_virtualRootInstance = new TreeViewItem(); // 缓存结构:Key=父节点对象, Value=该父节点下的子节点字典 (名称->节点对象) var nodeCache = new Dictionary>(); nodeCache[m_virtualRootInstance] = new Dictionary(); // 【重要】清空旧的映射数据 m_dataNodeMap.Clear(); m_parentMap.Clear(); foreach (var row in datas) { if (row.Length == 0) continue; TreeViewItem currentParent = m_virtualRootInstance; int nodeCount = row.Length - 1; for (int i = 0; i < nodeCount; i++) { string nodeName = row[i]; bool isLastNodeInRow = (i == nodeCount - 1); string payload = isLastNodeInRow ? row[row.Length - 1] : null; if (!nodeCache.ContainsKey(currentParent)) { nodeCache[currentParent] = new Dictionary(); } var childrenMap = nodeCache[currentParent]; TreeViewItem newNode; if (childrenMap.TryGetValue(nodeName, out newNode)) { // 节点已存在,复用 // 注意:如果数据结构可能变化,这里可能需要更新 payload,但通常静态数据无需处理 } else { // 创建新节点 if (currentParent == m_virtualRootInstance) { newNode = mTreeView.AppendItem("ItemPrefab1"); } else { newNode = currentParent.ChildTree.AppendItem("ItemPrefab1"); } // 【核心修改 1】记录父子关系 (用于后续回溯展开) // 即使父节点是虚拟根,也记录下来,方便判断边界 if (!m_parentMap.ContainsKey(newNode)) { m_parentMap[newNode] = currentParent; } var script = newNode.GetComponent(); if (script != null) { if (!string.IsNullOrEmpty(payload)) { script.SetItemInfo("", nodeName, payload); // 【核心修改 2】记录数据ID到节点的映射 if (!m_dataNodeMap.ContainsKey(payload)) { m_dataNodeMap.Add(payload, newNode); } else { Debug.LogWarning($"[TreeViewPanel] 发现重复的数据ID: {payload},将覆盖旧节点引用。"); m_dataNodeMap[payload] = newNode; } } else { script.SetItemInfo("", nodeName); } } childrenMap[nodeName] = newNode; nodeCache[newNode] = new Dictionary(); } currentParent = newNode; } } //Debug.Log($"TreeView 构建完成!共处理 {datas.Count} 条数据,索引了 {m_dataNodeMap.Count} 个可检索节点。"); } // ================= 新增:自动展开方法 ================= /// /// 根据数据ID自动查找并展开树节点 /// /// 数据ID,例如 "DiYiXinYin" public void Expand(string targetDataId) { mTreeView.CollapseAllItem(); if (string.IsNullOrEmpty(targetDataId)) { Debug.LogWarning("[TreeViewPanel] Expand failed: targetDataId is null or empty."); return; } if (!m_dataNodeMap.TryGetValue(targetDataId, out TreeViewItem targetItem)) { Debug.LogWarning($"[TreeViewPanel] 未找到数据ID为 '{targetDataId}' 的节点。请检查 ID 是否正确或树是否已构建。"); return; } //Debug.Log($"[TreeViewPanel] 找到目标节点: {targetDataId},开始自动展开路径..."); List nodesToExpand = new List(); TreeViewItem curr = targetItem; // 1. 向上回溯收集所有需要展开的父节点 while (m_parentMap.ContainsKey(curr)) { TreeViewItem parent = m_parentMap[curr]; // 如果遇到虚拟根节点,停止回溯(虚拟根不需要也不应该被调用 Expand) if (parent == m_virtualRootInstance) { break; } nodesToExpand.Add(parent); curr = parent; } // 2. 反转列表,确保从最外层父节点开始展开 (Root -> ... -> Parent) nodesToExpand.Reverse(); // 3. 依次展开父节点 foreach (var node in nodesToExpand) { if (!node.IsExpand) { node.Expand(); } } // 4. 展开目标节点本身(如果它有子节点) if (!targetItem.IsExpand && targetItem.ChildItemCount > 0) { targetItem.Expand(); } ItemScript2 st = targetItem.GetComponent(); if (mCurSelectedItemId > 0) { if (targetItem.ItemId != mCurSelectedItemId) { TreeViewItem curSelectedItem = mTreeView.GetTreeItemById(mCurSelectedItemId); if (curSelectedItem != null) { curSelectedItem.GetComponent().IsSelected = false; } mCurSelectedItemId = 0; } } st.IsSelected = true; mCurSelectedItemId = targetItem.ItemId; //Debug.Log($"[TreeViewPanel] 成功展开路径至节点: {targetDataId}"); // 5. 滚动到目标节点视野 // 建议延迟一帧执行,确保 UI 布局已经根据 Expand 操作刷新完毕 // 如果 ZXKFramework 没有 CoroutineUtils,可以使用 StartCoroutine(IENextFrame()) if (targetItem.CachedRectTransform != null) { // 尝试直接调用,如果位置不准,请改为协程下一帧调用 Nevigate(targetItem.CachedRectTransform); } } // ===================================================== void OnTreeListAddOneItem(TreeList treeList) { int count = treeList.ItemCount; TreeViewItem parentTreeItem = treeList.ParentTreeItem; if (count > 0 && parentTreeItem != null) { ItemScript2 st = parentTreeItem.GetComponent(); st.SetExpandBtnVisible(true); st.SetExpandStatus(parentTreeItem.IsExpand); } } void OnItemExpandBegin(TreeViewItem item) { ItemScript2 st = item.GetComponent(); st.SetExpandStatus(true); } void OnItemCollapseBegin(TreeViewItem item) { ItemScript2 st = item.GetComponent(); st.SetExpandStatus(false); } void OnItemCustomEvent(TreeViewItem item, CustomEvent customEvent, System.Object param) { if (MVC.GetModel().modeType == ModeType.KaoHe) return; if (customEvent == CustomEvent.ItemClicked) { ItemScript2 st = item.GetComponent(); if (mCurSelectedItemId > 0) { if (item.ItemId == mCurSelectedItemId) { return; } TreeViewItem curSelectedItem = mTreeView.GetTreeItemById(mCurSelectedItemId); if (curSelectedItem != null) { curSelectedItem.GetComponent().IsSelected = false; } mCurSelectedItemId = 0; } st.IsSelected = true; mCurSelectedItemId = item.ItemId; // 确保 param 是 string 类型 string sceneName = param as string; if (!string.IsNullOrEmpty(sceneName)) { GameManager.Instance.fsm.PlayClip("跳步"); GameManager.Instance.sceneDataHandler.LoadSceneDataFromJson( GetModel().mainData.folder + "/SaveData/" + sceneName + ".json", () => { GameManager.Instance.fsm.ChangeState(sceneName); }); } } } void OnTreeListRepositionFinish(TreeList treeList) { int count = treeList.ItemCount; if (count == 0) { return; } //draw the line linked from item to item TreeViewItem item0 = treeList.GetItemByIndex(0); ItemScript2 itemScript0 = item0.GetComponent(); float topY = item0.transform.localPosition.y - item0.CachedRectTransform.rect.height / 2; RectTransform rt0 = itemScript0.mLineVertical.GetComponent(); RectTransform lineRf = rt0; for (int i = 1; i < count; ++i) { TreeViewItem item = treeList.GetItemByIndex(i); ItemScript2 itemScript = item.GetComponent(); float centerY = item.transform.localPosition.y - item.CachedRectTransform.rect.height / 2; float dist = topY - centerY; topY = centerY; RectTransform rt = itemScript.mLineVertical.GetComponent(); //set the line length SetRectTransformHeight(lineRf, dist); lineRf = rt; } SetRectTransformHeight(lineRf, 0); //draw the line linked from item to item' childTree for (int i = 0; i < count; ++i) { TreeViewItem item = treeList.GetItemByIndex(i); ItemScript2 itemScript = item.GetComponent(); RectTransform rt2 = itemScript.mLineVertical_2.GetComponent(); if (item.ChildItemCount > 0 && item.IsCollapseEnd == false) { //get the childTree's first item TreeViewItem childItem0 = item.ChildTree.GetItemByIndex(0); ItemScript2 childItemScript0 = childItem0.GetComponent(); Vector3 pos0 = item.transform.InverseTransformPoint(childItemScript0.mExpandBtn.transform.position); float dist2 = rt2.localPosition.y - pos0.y; //set the line length SetRectTransformHeight(rt2, dist2); } else { SetRectTransformHeight(rt2, 0); } } } void SetRectTransformHeight(RectTransform rt, float height) { Vector2 size = rt.sizeDelta; size.y = height; rt.sizeDelta = size; } public void Nevigate(RectTransform item) { if (scrollRect == null || item == null) return; Vector3 itemCurrentLocalPostion = scrollRect.GetComponent().InverseTransformPoint(ConvertLocalPosToWorldPos(item)); Vector3 itemTargetLocalPos = scrollRect.GetComponent().InverseTransformPoint(ConvertLocalPosToWorldPos(scrollRect.viewport)); Vector3 diff = itemTargetLocalPos - itemCurrentLocalPostion; var contentRect = scrollRect.content.GetComponent(); var viewportRect = scrollRect.viewport.rect; float contentHeight = contentRect.rect.height; float viewportHeight = viewportRect.height; if (contentHeight <= viewportHeight) return; // 内容不够长,无需滚动 var newNormalizedPosition = new Vector2( diff.x / (contentRect.rect.width - viewportRect.width), diff.y / (contentHeight - viewportHeight) ); newNormalizedPosition = scrollRect.GetComponent().normalizedPosition - newNormalizedPosition; newNormalizedPosition.x = Mathf.Clamp01(newNormalizedPosition.x); newNormalizedPosition.y = Mathf.Clamp01(newNormalizedPosition.y); scrollRect.GetComponent().normalizedPosition = newNormalizedPosition; } private Vector3 ConvertLocalPosToWorldPos(RectTransform target) { var pivotOffset = new Vector3( (0.5f - target.pivot.x) * target.rect.size.x, (0.5f - target.pivot.y) * target.rect.size.y, 0f); var localPosition = target.localPosition + pivotOffset; return target.parent.TransformPoint(localPosition); } public void LockAllBtn() { if (mTreeView == null) return; ItemPrefab2[] itemPrefab2s = mTreeView.GetComponentsInChildren(true); for (int i = 0; i < itemPrefab2s.Length; i++) { itemPrefab2s[i].LockClick(); } } public void UnLockAllBtn() { if (mTreeView == null) return; ItemPrefab2[] itemPrefab2s = mTreeView.GetComponentsInChildren(true); for (int i = 0; i < itemPrefab2s.Length; i++) { itemPrefab2s[i].UnlockClick(); } } } }