| | |
| | | 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; |
| | | |
| | |
| | | { |
| | | 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 |
| | | { |
| | |
| | | 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(); |
| | | } |
| | |
| | | { |
| | | 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; |
| | |
| | | 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); |
| | | } |
| | |
| | | |
| | | 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; |
| | |
| | | |
| | | 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) |
| | |
| | | } |
| | | } |
| | | |
| | | // ======================== 新增:批量操作功能 ======================== |
| | | private void DrawBatchOperations() |
| | | { |
| | | using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) |
| | |
| | | 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; |
| | | |
| | |
| | | { |
| | | if (m_OverwriteExisting || !adapter.HasConfig(targetLang)) |
| | | { |
| | | // 必须标记对象为脏,否则 Unity 不会把资产的修改存盘 |
| | | Undo.RecordObject(adapter, "Batch Copy Language Config"); |
| | | |
| | | var clonedConfig = adapter.GetConfig(sourceLang).Clone(); |
| | |
| | | finally |
| | | { |
| | | EditorUtility.ClearProgressBar(); |
| | | // 统一保存所有标记为 Dirty 的资产修改 |
| | | AssetDatabase.SaveAssets(); |
| | | |
| | | PerformScan(); |
| | |
| | | "确认"); |
| | | } |
| | | } |
| | | |
| | | // ======================== 核心扫描逻辑 ======================== |
| | | |
| | | private void StartScan() |
| | | { |
| | |
| | | } |
| | | |
| | | 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(); |
| | | } |
| | |
| | | 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) |
| | | { |
| | |
| | | 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) |