三国卡牌客户端基础资源仓库
yyl
2026-05-11 195fe0ce2ef62facaa5bf836ebdf8866984e84a1
Assets/Editor/UIComponent/TextLanguageAdapterScanTool.cs
@@ -44,7 +44,10 @@
    public int TotalPrefabsScanned { get; set; }
    public int TotalAdaptersFound { get; set; } 
    public int AdaptersWithMissingConfig { get; private set; }
    public int PrefabsWithIssueCount { get; private set; }
    public int PrefabsWithoutIssueCount { get; private set; }
    public List<PrefabScanResult> PrefabResults { get; } = new List<PrefabScanResult>();
    public List<PrefabScanResult> PrefabResultsWithoutIssue { get; } = new List<PrefabScanResult>();
    public ScanResultSummary(string dir) => ScanDirectory = dir;
@@ -55,13 +58,33 @@
        {
            prefabResult = new PrefabScanResult(item.PrefabPath, item.PrefabGUID);
            PrefabResults.Add(prefabResult);
            PrefabsWithIssueCount++;
        }
        prefabResult.Items.Add(item);
        if (item.MissingLanguages.Count > 0)
        {
        AdaptersWithMissingConfig++;
    }
}
    public void AddPrefabWithoutIssue(string path, string guid, List<ScanResultItem> items)
    {
        var prefabResult = new PrefabScanResult(path, guid);
        prefabResult.Items.AddRange(items);
        PrefabResultsWithoutIssue.Add(prefabResult);
        PrefabsWithoutIssueCount++;
    }
}
// ======================== TreeView 实现 ========================
public enum ScanResultFilterMode
{
    全部,
    仅显示有问题,
    仅显示无问题
}
public class MetadataTreeViewItem : TreeViewItem
{
@@ -72,10 +95,12 @@
public class ScanResultTreeView : TreeView
{
    private ScanResultSummary m_Summary;
    private ScanResultFilterMode m_FilterMode;
    public ScanResultTreeView(TreeViewState state, ScanResultSummary summary) : base(state)
    public ScanResultTreeView(TreeViewState state, ScanResultSummary summary, ScanResultFilterMode filterMode) : base(state)
    {
        m_Summary = summary;
        m_FilterMode = filterMode;
        Reload();
        ExpandAll();
    }
@@ -84,28 +109,64 @@
    {
        var root = new TreeViewItem { id = 0, depth = -1, displayName = "Root" };
        
        // 【关键修复 1】:必须初始化 children 列表。
        // 否则当扫描结果完美(0个错误)时,root.children 为 null 会导致 Unity 报错
        root.children = new List<TreeViewItem>();
        if (m_Summary == null || m_Summary.PrefabResults.Count == 0)
        if (m_Summary == null)
            return root;
        int itemId = 1;
        bool showWithIssue = m_FilterMode == ScanResultFilterMode.全部 ||
                            m_FilterMode == ScanResultFilterMode.仅显示有问题;
        bool showWithoutIssue = m_FilterMode == ScanResultFilterMode.全部 ||
                               m_FilterMode == ScanResultFilterMode.仅显示无问题;
        if (showWithIssue)
        {
        foreach (var prefabResult in m_Summary.PrefabResults)
        {
            string prefabName = Path.GetFileNameWithoutExtension(prefabResult.PrefabPath);
            var prefabItem = new MetadataTreeViewItem(itemId++, $"{prefabName} ({prefabResult.Items.Count}个问题)", prefabResult);
                int issueCount = 0;
                foreach (var item in prefabResult.Items)
                {
                    if (item.MissingLanguages != null && item.MissingLanguages.Count > 0)
                        issueCount++;
                }
                var prefabItem = new MetadataTreeViewItem(itemId++, $"{prefabName} ({issueCount}个问题)", prefabResult);
            foreach (var adapterItem in prefabResult.Items)
            {
                var adapterTreeItem = new MetadataTreeViewItem(itemId++, adapterItem.GetDisplayName(), adapterItem);
                    string displayName;
                    if (adapterItem.MissingLanguages.Count > 0)
                        displayName = adapterItem.GetDisplayName();
                    else
                        displayName = $"{adapterItem.GameObjectPath} (配置完整)";
                    var adapterTreeItem = new MetadataTreeViewItem(itemId++, displayName, adapterItem);
                prefabItem.AddChild(adapterTreeItem);
            }
            root.AddChild(prefabItem);
        }
        }
        // 【关键修复 2】:Unity 官方规范要求,手动使用 AddChild 构建树之后,必须调用此方法刷新深度和层级关系
        if (showWithoutIssue)
        {
            foreach (var prefabResult in m_Summary.PrefabResultsWithoutIssue)
            {
                string prefabName = Path.GetFileNameWithoutExtension(prefabResult.PrefabPath);
                var prefabItem = new MetadataTreeViewItem(itemId++, $"{prefabName} (无问题)", prefabResult);
                foreach (var adapterItem in prefabResult.Items)
                {
                    var displayName = $"{adapterItem.GameObjectPath} (配置完整)";
                    var adapterTreeItem = new MetadataTreeViewItem(itemId++, displayName, adapterItem);
                    prefabItem.AddChild(adapterTreeItem);
                }
                root.AddChild(prefabItem);
            }
        }
        SetupDepthsFromParentsAndChildren(root);
        return root;
@@ -117,7 +178,12 @@
        if (item != null && item.Metadata is PrefabScanResult)
            GUI.Label(args.rowRect, item.displayName, EditorStyles.boldLabel);
        else if (item != null && item.Metadata is ScanResultItem adapterItem)
        {
            if (adapterItem.MissingLanguages.Count > 0)
            GUI.Label(args.rowRect, $"{adapterItem.GameObjectPath} (缺少: {string.Join(", ", adapterItem.MissingLanguages)})");
            else
                GUI.Label(args.rowRect, item.displayName);
        }
        else
            base.RowGUI(args);
    }
@@ -173,8 +239,8 @@
    
    private ScanResultTreeView m_TreeView;
    private TreeViewState m_TreeViewState;
    private ScanResultFilterMode m_ResultFilterMode = ScanResultFilterMode.全部;
    // 批量操作的UI状态
    private int m_SourceLangIndex = 0;
    private int m_TargetLangIndex = 0;
    private bool m_OverwriteExisting = false;
@@ -279,7 +345,20 @@
            if (m_ScanResult != null)
            {
                EditorGUILayout.LabelField($"预制体总数: {m_ScanResult.TotalPrefabsScanned} | Adapter总数: {m_ScanResult.TotalAdaptersFound} | 缺失配置: {m_ScanResult.AdaptersWithMissingConfig}");
                EditorGUILayout.LabelField($"预制体总数: {m_ScanResult.TotalPrefabsScanned} | 有Adapter的预制体: {m_ScanResult.PrefabsWithIssueCount + m_ScanResult.PrefabsWithoutIssueCount} | Adapter总数: {m_ScanResult.TotalAdaptersFound} | 缺失配置: {m_ScanResult.AdaptersWithMissingConfig} | 有问题预制体: {m_ScanResult.PrefabsWithIssueCount} | 无问题预制体: {m_ScanResult.PrefabsWithoutIssueCount}");
                EditorGUILayout.Space(5f);
                using (new EditorGUILayout.HorizontalScope())
                {
                    EditorGUILayout.LabelField("显示模式:", GUILayout.Width(60f));
                    EditorGUI.BeginChangeCheck();
                    m_ResultFilterMode = (ScanResultFilterMode)EditorGUILayout.EnumPopup(m_ResultFilterMode);
                    if (EditorGUI.EndChangeCheck() && m_TreeView != null)
                    {
                        m_TreeViewState ??= new TreeViewState();
                        m_TreeView = new ScanResultTreeView(m_TreeViewState, m_ScanResult, m_ResultFilterMode);
                    }
                }
                EditorGUILayout.Space(5f);
                if (m_TreeView != null)
@@ -304,7 +383,6 @@
        }
    }
    // ======================== 新增:批量操作功能 ========================
    private void DrawBatchOperations()
    {
        using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
@@ -370,7 +448,6 @@
                string path = AssetDatabase.GUIDToAssetPath(prefabGuids[i]);
                EditorUtility.DisplayProgressBar("批量复制配置", $"处理中 ({i + 1}/{prefabGuids.Length}): {path}", (float)i / prefabGuids.Length);
                // 【核心修改】:直接加载资产内存,不使用 LoadPrefabContents 进行实例化
                GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(path);
                bool isModified = false;
@@ -380,7 +457,6 @@
                    {
                        if (m_OverwriteExisting || !adapter.HasConfig(targetLang))
                        {
                            // 必须标记对象为脏,否则 Unity 不会把资产的修改存盘
                            Undo.RecordObject(adapter, "Batch Copy Language Config");
                            
                            var clonedConfig = adapter.GetConfig(sourceLang).Clone();
@@ -402,7 +478,6 @@
        finally
        {
            EditorUtility.ClearProgressBar();
            // 统一保存所有标记为 Dirty 的资产修改
            AssetDatabase.SaveAssets();
            
            PerformScan();
@@ -412,8 +487,6 @@
                "确认");
        }
    }
    // ======================== 核心扫描逻辑 ========================
    private void StartScan()
    {
@@ -460,7 +533,7 @@
        }
        m_TreeViewState ??= new TreeViewState();
        m_TreeView = new ScanResultTreeView(m_TreeViewState, m_ScanResult);
        m_TreeView = new ScanResultTreeView(m_TreeViewState, m_ScanResult, m_ResultFilterMode);
        m_ScanStatus = $"扫描完成! 发现 {m_ScanResult.AdaptersWithMissingConfig} 个缺失配置";
        Repaint();
    }
@@ -471,8 +544,12 @@
        if (prefab == null) return;
        var adapters = prefab.GetComponentsInChildren<TextLanguageAdapter>(true);
        if (adapters.Length == 0) return;
        
        m_ScanResult.TotalAdaptersFound += adapters.Length;
        bool hasIssue = false;
        List<ScanResultItem> allItems = new List<ScanResultItem>();
        foreach (var adapter in adapters)
        {
@@ -483,15 +560,30 @@
                if (!adapter.HasConfig(langId)) missing.Add(langId);
            }
            if (missing.Count > 0)
            {
                var item = new ScanResultItem(path, GetGameObjectPath(adapter.gameObject, prefab), adapter.TargetTextType, guid)
                {
                    MissingLanguages = missing
                };
            allItems.Add(item);
            if (missing.Count > 0)
            {
                hasIssue = true;
            }
        }
        if (hasIssue)
        {
            foreach (var item in allItems)
            {
                m_ScanResult.AddResult(item);
            }
        }
        else
        {
            m_ScanResult.AddPrefabWithoutIssue(path, guid, allItems);
        }
    }
    private string GetGameObjectPath(GameObject go, GameObject root)