三国卡牌客户端基础资源仓库
yyl
2026-05-11 195fe0ce2ef62facaa5bf836ebdf8866984e84a1
Merge branch 'master' into h5version
8个文件已修改
6个文件已添加
1443 ■■■■■ 已修改文件
Assets/Editor/AssetBundleBrowser/AssetBundleBuildTab.cs 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/Logo/ryzj/SplashImage.png 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/Logo/test/SplashImage.png 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/Logo/wgyx/SplashImage.png 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/Logo/xssg/SplashImage.png 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/Tool/ClientPackage.cs 282 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/UIComponent/ImageLanguageAdapterEditor.cs 217 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/UIComponent/ImageLanguageAdapterEditor.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/UIComponent/ImageLanguageAdapterHelper.cs 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/UIComponent/ImageLanguageAdapterHelper.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/UIComponent/ImageLanguageAdapterScanTool.cs 594 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/UIComponent/ImageLanguageAdapterScanTool.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/UIComponent/TextLanguageAdapterEditor.cs 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/UIComponent/TextLanguageAdapterScanTool.cs 150 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/AssetBundleBrowser/AssetBundleBuildTab.cs
@@ -90,6 +90,12 @@
            set { LocalSave.SetString("APKOutPutPath", value); }
        }
        public string AndroidExportPath
        {
            get { return LocalSave.GetString("AndroidExportPath"); }
            set { LocalSave.SetString("AndroidExportPath", value); }
        }
        string m_Publishers;
        public string publishers
        {
@@ -230,6 +236,15 @@
            var fromVersionConfig = ClientPackage.GetVersionConfig(_publisher, out versionName);
            var versionConfig = await VersionConfig.GetAsync();
            if (fromVersionConfig == null || versionConfig == null)
            {
                Debug.LogWarningFormat("SetVersionConfig failed: fromVersionConfig={0}, versionConfig={1}, publisher={2}",
                    fromVersionConfig != null ? "ok" : "null",
                    versionConfig != null ? "ok" : "null",
                    _publisher);
                return;
            }
            VersionConfig.Copy(fromVersionConfig, versionConfig);
@@ -516,6 +531,32 @@
#endif
            GUILayout.EndHorizontal();
#if UNITY_ANDROID
            EditorGUILayout.Space();
            GUILayout.Label("Export Android Project", EditorStyles.boldLabel);
            GUILayout.BeginHorizontal();
            var newExportPath = EditorGUILayout.TextField("Export Path", AndroidExportPath);
            if (!string.IsNullOrEmpty(newExportPath) && newExportPath != AndroidExportPath)
            {
                AndroidExportPath = newExportPath;
            }
            if (GUILayout.Button("Browse", GUILayout.MaxWidth(75f)))
            {
                BrowseForAndroidExport();
            }
            GUILayout.EndHorizontal();
            GUILayout.BeginHorizontal();
            if (GUILayout.Button("Export Android Project"))
            {
                EditorApplication.delayCall += ExecuteExportAndroidProject;
            }
            if (GUILayout.Button("Export Android Dev"))
            {
                EditorApplication.delayCall += ExecuteExportAndroidProjectDev;
            }
            GUILayout.EndHorizontal();
#endif
            // GUILayout.Space(20);
            // if (GUILayout.Button("SwitchVersionConfig"))
@@ -966,6 +1007,30 @@
            ClientPackage.BuildPublishers(ClientPackage.SDK_PLUGIN_PROJECT, outputPath, ApkOutputPath, publishers, packageIndex, true, false);
        }
        private void ExecuteExportAndroidProject()
        {
            if (string.IsNullOrEmpty(AndroidExportPath))
            {
                EditorUtility.DisplayDialog("提示", "请先设置 Android Export Path", "确定");
                return;
            }
            packageIndex++;
            var assetBundlePath = Application.dataPath.Replace("Assets", m_UserData.m_OutputPath);
            ClientPackage.ExportAndroidProject(publishers, packageIndex, false, AndroidExportPath, assetBundlePath);
        }
        private void ExecuteExportAndroidProjectDev()
        {
            if (string.IsNullOrEmpty(AndroidExportPath))
            {
                EditorUtility.DisplayDialog("提示", "请先设置 Android Export Path", "确定");
                return;
            }
            packageIndex++;
            var assetBundlePath = Application.dataPath.Replace("Assets", m_UserData.m_OutputPath);
            ClientPackage.ExportAndroidProject(publishers, packageIndex, true, AndroidExportPath, assetBundlePath);
        }
        private void ExecuteBuildClientPackageStandalone()
        {
            packageIndex++;
@@ -1084,6 +1149,15 @@
            }
        }
        private void BrowseForAndroidExport()
        {
            var newPath = EditorUtility.OpenFolderPanel("Android Export Folder", AndroidExportPath, string.Empty);
            if (!string.IsNullOrEmpty(newPath))
            {
                AndroidExportPath = newPath;
            }
        }
        private void ResetPathToDefault()
        {
            m_UserData.m_UseDefaultPath = true;
Assets/Editor/Logo/ryzj/SplashImage.png

Assets/Editor/Logo/test/SplashImage.png

Assets/Editor/Logo/wgyx/SplashImage.png

Assets/Editor/Logo/xssg/SplashImage.png

Assets/Editor/Tool/ClientPackage.cs
@@ -1,4 +1,4 @@
using System.Collections;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
@@ -386,6 +386,286 @@
    }
    public static VersionConfig versionConfig1 = null;
    /// <summary>
    /// 导出Android工程给AS打包
    /// Unity只负责导出unityLibrary,SDK/Gradle/签名等均由AS工程处理
    /// </summary>
    public static void ExportAndroidProject(string _publisher, int _buildIndex, bool _development, string _outputPath, string _assetBundlePath)
    {
        if (string.IsNullOrEmpty(_outputPath) || !Directory.Exists(_outputPath))
        {
            Debug.LogError("导出路径无效,请设置有效的 Android Export Path: " + _outputPath);
            return;
        }
        PreBuild(_publisher, _buildIndex);
        var versionName = string.Empty;
        var versionConfig = GetVersionConfig(_publisher, out versionName);
        PlayerSettings.Android.bundleVersionCode = VersionConfig.GetVersionNumber(versionConfig.version);
        PlayerSettings.enableInternalProfiler = _development;
        // 先清理StreamingAssets,再根据assetAccess配置拷贝资源到StreamingAssets
        var streamingPath = ResourcesPath.Instance.StreamingAssetPath;
        if (Directory.Exists(streamingPath))
            Directory.Delete(streamingPath, true);
        if (!string.IsNullOrEmpty(_assetBundlePath) && Directory.Exists(_assetBundlePath))
        {
            switch (versionConfig.assetAccess)
            {
                case InstalledAsset.NullAsset:
                    CopyNullAssetResources(_assetBundlePath, streamingPath);
                    break;
                case InstalledAsset.HalfAsset:
                    CopyHalfAssetResources(_assetBundlePath, streamingPath);
                    break;
                case InstalledAsset.FullAsset:
                case InstalledAsset.IngoreDownLoad:
                    CopyFullAssetResources(_assetBundlePath, streamingPath);
                    break;
            }
            Debug.LogFormat("资源拷贝完成({0}),从 {1} 到 {2}", versionConfig.assetAccess, _assetBundlePath, streamingPath);
        }
        else
        {
            Debug.LogWarningFormat("AssetBundle路径为空或不存在: {0},导出工程将不包含游戏资源", _assetBundlePath);
        }
        // 关键设置:导出为Gradle工程而非直接打APK
        EditorUserBuildSettings.exportAsGoogleAndroidProject = true;
        EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Gradle;
        EditorUserBuildSettings.development = _development;
        // 导出路径
        _outputPath = Path.GetFullPath(_outputPath);
        string outputDir = Path.Combine(_outputPath, versionName + "_" + versionConfig.clientPackageFlag);
        if (Directory.Exists(outputDir))
        {
            Directory.Delete(outputDir, true);
        }
        Debug.LogFormat("开始导出Android工程,输出路径: {0}", outputDir);
        BuildPipeline.BuildPlayer(baseLevels, outputDir, BuildTarget.Android,
            _development ? BuildOptions.AcceptExternalModificationsToPlayer | BuildOptions.Development | BuildOptions.AllowDebugging
                         : BuildOptions.AcceptExternalModificationsToPlayer);
        Debug.LogFormat("导出Android工程完成!输出路径: {0}", outputDir);
        // 导出完成后恢复设置
        EditorUserBuildSettings.exportAsGoogleAndroidProject = false;
        EditorUserBuildSettings.development = false;
    }
    /// <summary>
    /// 小包拷贝:仅拷贝builtin资源
    /// </summary>
    static void CopyNullAssetResources(string _assetBundlePath, string _streamingPath)
    {
        var files = new List<FileInfo>();
        var builtInFiles = new List<FileInfo>();
        FileExtersion.GetAllDirectoryFileInfos(_assetBundlePath, files);
        foreach (var file in files)
        {
            if (file.FullName.Contains("builtin"))
            {
                builtInFiles.Add(file);
            }
        }
        foreach (var item in builtInFiles)
        {
            var extension = Path.GetExtension(item.FullName);
            if (extension == ".meta")
                continue;
            var relativePath = FileExtersion.GetFileRelativePath(_assetBundlePath, item.FullName);
            var to = StringUtility.Concat(_streamingPath, relativePath);
            var directory = Path.GetDirectoryName(to);
            if (!Directory.Exists(directory))
                Directory.CreateDirectory(directory);
            File.Copy(item.FullName, to, true);
        }
    }
    /// <summary>
    /// 半包拷贝:根据PriorBundleConfig剔除优先级低的hero/maps/audio/uieffect/video资源,
    /// 以及config和ui资源(取决于includeConfig/includeUI开关)
    /// </summary>
    static void CopyHalfAssetResources(string _assetBundlePath, string _streamingPath)
    {
        PriorBundleConfig.LazyInit();
        var fromFiles = new List<FileInfo>();
        FileExtersion.GetAllDirectoryFileInfos(_assetBundlePath, fromFiles);
        var excludeFileFullNames = new List<string>();
        // 剔除优先级低的hero资源
        var tempFiles = new List<FileInfo>();
        FileExtersion.GetAllDirectoryFileInfos(StringUtility.Concat(_assetBundlePath, "/hero"), tempFiles);
        foreach (var file in tempFiles)
        {
            var fileName = Path.GetFileNameWithoutExtension(file.FullName);
            var prior = PriorBundleConfig.GetAssetPrior(AssetVersion.AssetCategory.Mob, AssetVersionUtility.DecodeFileName(fileName));
            if (prior > AssetPrior)
            {
                excludeFileFullNames.Add(file.FullName);
            }
        }
        // 剔除优先级低的maps资源
        tempFiles.Clear();
        FileExtersion.GetAllDirectoryFileInfos(StringUtility.Concat(_assetBundlePath, "/maps"), tempFiles);
        foreach (var file in tempFiles)
        {
            var fileName = Path.GetFileNameWithoutExtension(file.FullName);
            var prior = PriorBundleConfig.GetAssetPrior(AssetVersion.AssetCategory.Scene, AssetVersionUtility.DecodeFileName(fileName));
            if (prior > AssetPrior)
            {
                excludeFileFullNames.Add(file.FullName);
            }
        }
        // 剔除优先级低的audio资源
        tempFiles.Clear();
        FileExtersion.GetAllDirectoryFileInfos(StringUtility.Concat(_assetBundlePath, "/audio"), tempFiles);
        foreach (var file in tempFiles)
        {
            var fileName = Path.GetFileNameWithoutExtension(file.FullName);
            var prior = PriorBundleConfig.GetAssetPrior(AssetVersion.AssetCategory.Audio, AssetVersionUtility.DecodeFileName(fileName));
            if (prior > AssetPrior)
            {
                excludeFileFullNames.Add(file.FullName);
            }
        }
        // 剔除优先级低的uieffect资源
        tempFiles.Clear();
        FileExtersion.GetAllDirectoryFileInfos(StringUtility.Concat(_assetBundlePath, "/uieffect"), tempFiles);
        foreach (var file in tempFiles)
        {
            var fileName = Path.GetFileNameWithoutExtension(file.FullName);
            var prior = PriorBundleConfig.GetAssetPrior(AssetVersion.AssetCategory.Effect, AssetVersionUtility.DecodeFileName(fileName));
            if (prior > AssetPrior)
            {
                excludeFileFullNames.Add(file.FullName);
            }
        }
        // 剔除视频资源
        tempFiles.Clear();
        var videoPath = StringUtility.Concat(_assetBundlePath, "/video");
        if (Directory.Exists(videoPath))
        {
            FileExtersion.GetAllDirectoryFileInfos(videoPath, tempFiles);
            foreach (var file in tempFiles)
            {
                var fileName = Path.GetFileNameWithoutExtension(file.FullName);
                var prior = PriorBundleConfig.GetAssetPrior(AssetVersion.AssetCategory.Video, AssetVersionUtility.DecodeFileName(fileName));
                if (prior > AssetPrior)
                {
                    excludeFileFullNames.Add(file.FullName);
                }
            }
        }
        // 剔除表资源
        if (!includeConfig)
        {
            tempFiles.Clear();
            FileExtersion.GetAllDirectoryFileInfos(StringUtility.Concat(_assetBundlePath, "/config"), tempFiles);
            foreach (var file in tempFiles)
            {
                excludeFileFullNames.Add(file.FullName);
            }
        }
        // 剔除UI资源
        if (!includeUI)
        {
            tempFiles.Clear();
            FileExtersion.GetAllDirectoryFileInfos(StringUtility.Concat(_assetBundlePath, "/ui"), tempFiles);
            foreach (var file in tempFiles)
            {
                excludeFileFullNames.Add(file.FullName);
            }
        }
        else
        {
            tempFiles.Clear();
            FileExtersion.GetAllDirectoryFileInfos(StringUtility.Concat(_assetBundlePath, "/ui"), tempFiles);
            foreach (var file in tempFiles)
            {
                var fileName = Path.GetFileNameWithoutExtension(file.FullName);
                var prior = PriorBundleConfig.GetAssetPrior(AssetVersion.AssetCategory.UI, fileName);
                if (prior > AssetPrior)
                {
                    excludeFileFullNames.Add(file.FullName);
                }
            }
        }
        // 从文件列表中移除需要排除的文件
        for (int i = fromFiles.Count - 1; i >= 0; i--)
        {
            if (excludeFileFullNames.Contains(fromFiles[i].FullName))
            {
                fromFiles.RemoveAt(i);
            }
        }
        foreach (var item in fromFiles)
        {
            var extension = Path.GetExtension(item.FullName);
            if (extension == ".meta")
                continue;
            var relativePath = FileExtersion.GetFileRelativePath(_assetBundlePath, item.FullName);
            if (relativePath.StartsWith("patch"))
                continue;
            var to = StringUtility.Concat(_streamingPath, relativePath);
            var directory = Path.GetDirectoryName(to);
            if (!Directory.Exists(directory))
                Directory.CreateDirectory(directory);
            File.Copy(item.FullName, to, true);
        }
    }
    /// <summary>
    /// 全包拷贝:拷贝所有资源(排除patch和.meta)
    /// </summary>
    static void CopyFullAssetResources(string _assetBundlePath, string _streamingPath)
    {
        var files = new List<FileInfo>();
        FileExtersion.GetAllDirectoryFileInfos(_assetBundlePath, files);
        foreach (var file in files)
        {
            var extension = Path.GetExtension(file.FullName);
            if (extension == ".meta")
                continue;
            var relativePath = FileExtersion.GetFileRelativePath(_assetBundlePath, file.FullName);
            if (relativePath.StartsWith("patch"))
                continue;
            var to = StringUtility.Concat(_streamingPath, relativePath);
            var directory = Path.GetDirectoryName(to);
            if (!Directory.Exists(directory))
                Directory.CreateDirectory(directory);
            File.Copy(file.FullName, to, true);
        }
    }
    public static void BuildIpa(string _sdkPath, string _publisher, int _buildIndex, bool _replace)
    {
        PreBuild(_publisher, _buildIndex);
Assets/Editor/UIComponent/ImageLanguageAdapterEditor.cs
New file
@@ -0,0 +1,217 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
[CustomEditor(typeof(ImageLanguageAdapter), true)]
[CanEditMultipleObjects]
public class ImageLanguageAdapterEditor : Editor
{
    private Dictionary<string, bool> m_FoldoutStates = new Dictionary<string, bool>();
    private void OnEnable()
    {
        ImageLanguageAdapterHelper.Initialize();
        if (target is ImageLanguageAdapter adapter)
        {
            if (!adapter.HasConfig(ImageLanguageAdapter.DefaultLangId))
            {
                adapter.ReadCurrentToConfig(ImageLanguageAdapter.DefaultLangId);
                EditorUtility.SetDirty(adapter);
            }
            foreach (var langId in adapter.GetConfiguredLanguages()) m_FoldoutStates.TryAdd(langId, false);
        }
    }
    public override void OnInspectorGUI()
    {
        var adapter = target as ImageLanguageAdapter;
        DrawBasicInfo(adapter);
        EditorGUILayout.Space();
        DrawLanguageConfigs(adapter);
        if (GUI.changed) EditorUtility.SetDirty(target);
    }
    private void DrawBasicInfo(ImageLanguageAdapter adapter)
    {
        using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
        {
            EditorGUILayout.LabelField("基本信息", EditorStyles.boldLabel);
            EditorGUI.BeginChangeCheck();
            Component newTarget = (Component)EditorGUILayout.ObjectField("目标组件", adapter.TargetImageComponent, typeof(Component), true);
            ImageComponentType newType = (ImageComponentType)EditorGUILayout.EnumPopup("组件类型", adapter.TargetImageType);
            if (EditorGUI.EndChangeCheck())
            {
                Undo.RecordObject(adapter, "Update Basic Info");
                if (newTarget != null && newTarget != adapter.TargetImageComponent)
                {
                    string typeName = newTarget.GetType().Name;
                    if (typeName == "ImageEx") newType = ImageComponentType.ImageEx;
                    else if (newTarget is Image) newType = ImageComponentType.Image;
                }
                adapter.TargetImageComponent = newTarget;
                adapter.TargetImageType = newType;
            }
            EditorGUILayout.HelpBox("请手动拖拽要适配的图片组件,并确认组件类型是否正确。", MessageType.Info);
        }
    }
    private void DrawLanguageConfigs(ImageLanguageAdapter adapter)
    {
        using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
        {
            EditorGUILayout.LabelField("语言排版配置", EditorStyles.boldLabel);
            EditorGUILayout.HelpBox("default仅用于编辑时恢复初始状态,无对应语言配置时用组件当前状态", MessageType.Info);
            foreach (var langId in adapter.GetConfiguredLanguages()) DrawLanguageConfigItem(adapter, langId);
            EditorGUILayout.Space();
            DrawAddLanguageConfig(adapter);
        }
    }
    private void DrawLanguageConfigItem(ImageLanguageAdapter adapter, string langId)
    {
        m_FoldoutStates.TryAdd(langId, false);
        int presetIndex = System.Array.IndexOf(ImageLanguageAdapterHelper.PresetLanguageIds, langId);
        string name = presetIndex >= 0 ? ImageLanguageAdapterHelper.PresetLanguageNames[presetIndex] : ImageLanguageAdapter.GetLanguageShowName(langId);
        string label = string.IsNullOrEmpty(name) ? langId : $"{langId} ({name})";
        EditorGUI.indentLevel++;
        if (langId == ImageLanguageAdapter.DefaultLangId) GUI.backgroundColor = new Color(1f, 0.9f, 0.7f);
        m_FoldoutStates[langId] = EditorGUILayout.Foldout(m_FoldoutStates[langId], label, true);
        GUI.backgroundColor = Color.white;
        if (m_FoldoutStates[langId])
        {
            EditorGUI.indentLevel++;
            var config = adapter.LanguageConfigs.Get(langId);
            if (config != null)
            {
                DrawConfigItem(config);
                EditorGUILayout.Space();
                DrawConfigItemActions(adapter, langId);
            }
            EditorGUI.indentLevel--;
        }
        EditorGUI.indentLevel--;
    }
    private void DrawConfigItem(ImageLanguageConfigItem cfg)
    {
        EditorGUILayout.LabelField("RectTransform 配置", EditorStyles.miniBoldLabel);
        EditorGUI.BeginChangeCheck();
        cfg.anchoredPosition = EditorGUILayout.Vector2Field("Anchored Position", cfg.anchoredPosition);
        cfg.sizeDelta = EditorGUILayout.Vector2Field("Size Delta", cfg.sizeDelta);
        cfg.anchorMin = EditorGUILayout.Vector2Field("Anchor Min", cfg.anchorMin);
        cfg.anchorMax = EditorGUILayout.Vector2Field("Anchor Max", cfg.anchorMax);
        cfg.pivot = EditorGUILayout.Vector2Field("Pivot", cfg.pivot);
        cfg.localScale = EditorGUILayout.Vector3Field("Local Scale", cfg.localScale);
        cfg.localRotation = EditorGUILayout.Vector3Field("Local Rotation", cfg.localRotation);
        EditorGUILayout.Space();
        EditorGUILayout.LabelField("Image 配置", EditorStyles.miniBoldLabel);
        cfg.enabled = EditorGUILayout.Toggle("显示", cfg.enabled);
        cfg.color = EditorGUILayout.ColorField("Color", cfg.color);
        cfg.type = (Image.Type)EditorGUILayout.EnumPopup("Type", cfg.type);
        if (cfg.type == Image.Type.Sliced || cfg.type == Image.Type.Tiled)
        {
            cfg.fillCenter = EditorGUILayout.Toggle("Fill Center", cfg.fillCenter);
        }
        if (cfg.type == Image.Type.Filled)
        {
            cfg.fillMethod = (Image.FillMethod)EditorGUILayout.EnumPopup("Fill Method", cfg.fillMethod);
            cfg.fillAmount = EditorGUILayout.Slider("Fill Amount", cfg.fillAmount, 0f, 1f);
            cfg.fillOrigin = EditorGUILayout.IntField("Fill Origin", cfg.fillOrigin);
        }
        cfg.preserveAspect = EditorGUILayout.Toggle("Preserve Aspect", cfg.preserveAspect);
        cfg.useSpriteMesh = EditorGUILayout.Toggle("Use Sprite Mesh", cfg.useSpriteMesh);
        cfg.pixelsPerUnitMultiplier = EditorGUILayout.FloatField("Pixels Per Unit Multiplier", cfg.pixelsPerUnitMultiplier);
        if (EditorGUI.EndChangeCheck()) EditorUtility.SetDirty(target);
    }
    private void DrawConfigItemActions(ImageLanguageAdapter adapter, string langId)
    {
        using (new EditorGUILayout.HorizontalScope())
        {
            GUILayout.Space(EditorGUI.indentLevel * 15f);
            if (GUILayout.Button("从当前读取", GUILayout.Width(80)))
            {
                Undo.RecordObject(adapter, "Read Current Config");
                adapter.ReadCurrentToConfig(langId);
            }
            if (GUILayout.Button("应用", GUILayout.Width(60)))
            {
                Undo.RecordObject(adapter, "Apply Config");
                adapter.ApplyConfig(langId);
            }
            using (new EditorGUI.DisabledScope(langId == ImageLanguageAdapter.DefaultLangId))
            {
                if (GUILayout.Button("删除", GUILayout.Width(50)))
                {
                    Undo.RecordObject(adapter, "Remove Config");
                    adapter.RemoveConfig(langId);
                    m_FoldoutStates.Remove(langId);
                }
            }
        }
    }
    private void DrawAddLanguageConfig(ImageLanguageAdapter adapter)
    {
        EditorGUILayout.BeginHorizontal();
        GUILayout.Space(15f);
        EditorGUILayout.LabelField("添加预设语言:", GUILayout.Width(100));
        float viewWidth = EditorGUIUtility.currentViewWidth - 30f;
        float currentWidth = 115f;
        float buttonWidth = 64f;
        for (int i = 0; i < ImageLanguageAdapterHelper.PresetLanguageIds.Length; i++)
        {
            string langId = ImageLanguageAdapterHelper.PresetLanguageIds[i];
            if (!adapter.HasConfig(langId))
            {
                if (currentWidth + buttonWidth > viewWidth)
                {
                    EditorGUILayout.EndHorizontal();
                    EditorGUILayout.BeginHorizontal();
                    GUILayout.Space(115f);
                    currentWidth = 115f;
                }
                if (GUILayout.Button(ImageLanguageAdapterHelper.PresetLanguageNames[i], GUILayout.Width(60)))
                {
                    Undo.RecordObject(adapter, "Add Language Config");
                    var newConfig = adapter.HasConfig(ImageLanguageAdapter.DefaultLangId)
                        ? adapter.LanguageConfigs.Get(ImageLanguageAdapter.DefaultLangId).Clone()
                        : new ImageLanguageConfigItem();
                    adapter.SetConfig(langId, newConfig);
                    m_FoldoutStates[langId] = true;
                }
                currentWidth += buttonWidth;
            }
        }
        EditorGUILayout.EndHorizontal();
    }
}
Assets/Editor/UIComponent/ImageLanguageAdapterEditor.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6f2905447a660d641abe91b18a29680a
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Assets/Editor/UIComponent/ImageLanguageAdapterHelper.cs
New file
@@ -0,0 +1,67 @@
using System.Collections.Generic;
using System.IO;
using LitJson;
using UnityEngine;
/// <summary>
/// 编辑器图片语言配置读取助手
/// 负责统一解析 InitialFunction.txt
/// </summary>
public static class ImageLanguageAdapterHelper
{
    public static string[] PresetLanguageIds { get; private set; }
    public static string[] PresetLanguageNames { get; private set; }
    private static bool s_Initialized;
    public static void Initialize()
    {
        if (s_Initialized) return;
        var idList = new List<string> { ImageLanguageAdapter.DefaultLangId };
        var nameList = new List<string> { "默认" };
        string configPath = Path.Combine(Application.dataPath, "ResourcesOut/Config/InitialFunction.txt");
        if (File.Exists(configPath))
        {
            try
            {
                string[] lines = File.ReadAllLines(configPath);
                for (int i = 3; i < lines.Length; i++)
                {
                    string line = lines[i];
                    if (string.IsNullOrWhiteSpace(line)) continue;
                    int index = line.IndexOf('\t');
                    if (index == -1) continue;
                    string key = line.Substring(0, index);
                    if (key == "LanguageEx")
                    {
                        string[] fields = line.Split('\t');
                        if (fields.Length > 1 && !string.IsNullOrEmpty(fields[1]))
                        {
                            var dict = JsonMapper.ToObject<Dictionary<string, string>>(fields[1]);
                            if (dict != null)
                            {
                                foreach (var kvp in dict)
                                {
                                    idList.Add(kvp.Key);
                                    nameList.Add(kvp.Value);
                                }
                            }
                        }
                        break;
                    }
                }
            }
            catch (System.Exception ex)
            {
                Debug.LogWarning($"[ImageLanguageAdapterHelper] 读取配置失败: {ex.Message}");
            }
        }
        PresetLanguageIds = idList.ToArray();
        PresetLanguageNames = nameList.ToArray();
        s_Initialized = true;
    }
}
Assets/Editor/UIComponent/ImageLanguageAdapterHelper.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dd266766e23062e4eb668f7102620a06
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Assets/Editor/UIComponent/ImageLanguageAdapterScanTool.cs
New file
@@ -0,0 +1,594 @@
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
public class ImageScanResultItem
{
    public string PrefabPath { get; }
    public string GameObjectPath { get; }
    public List<string> MissingLanguages { get; set; } = new List<string>();
    public ImageComponentType ComponentType { get; }
    public string PrefabGUID { get; }
    public ImageScanResultItem(string path, string goPath, ImageComponentType type, string guid)
    {
        PrefabPath = path;
        GameObjectPath = goPath;
        ComponentType = type;
        PrefabGUID = guid;
    }
    public string GetDisplayName() => $"{GameObjectPath} (缺少: {string.Join(", ", MissingLanguages)}) [{ComponentType}]";
}
public class ImagePrefabScanResult
{
    public string PrefabPath { get; }
    public string PrefabGUID { get; }
    public List<ImageScanResultItem> Items { get; } = new List<ImageScanResultItem>();
    public ImagePrefabScanResult(string path, string guid)
    {
        PrefabPath = path;
        PrefabGUID = guid;
    }
}
public class ImageScanResultSummary
{
    public string ScanDirectory { get; }
    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<ImagePrefabScanResult> PrefabResults { get; } = new List<ImagePrefabScanResult>();
    public List<ImagePrefabScanResult> PrefabResultsWithoutIssue { get; } = new List<ImagePrefabScanResult>();
    public ImageScanResultSummary(string dir) => ScanDirectory = dir;
    public void AddResult(ImageScanResultItem item)
    {
        var prefabResult = PrefabResults.Find(p => p.PrefabPath == item.PrefabPath);
        if (prefabResult == null)
        {
            prefabResult = new ImagePrefabScanResult(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<ImageScanResultItem> items)
    {
        var prefabResult = new ImagePrefabScanResult(path, guid);
        prefabResult.Items.AddRange(items);
        PrefabResultsWithoutIssue.Add(prefabResult);
        PrefabsWithoutIssueCount++;
    }
}
public enum ImageScanResultFilterMode
{
    全部,
    仅显示有问题,
    仅显示无问题
}
public class ImageMetadataTreeViewItem : TreeViewItem
{
    public object Metadata { get; }
    public ImageMetadataTreeViewItem(int id, string name, object meta) : base(id, 0, name) => Metadata = meta;
}
public class ImageScanResultTreeView : TreeView
{
    private ImageScanResultSummary m_Summary;
    private ImageScanResultFilterMode m_FilterMode;
    public ImageScanResultTreeView(TreeViewState state, ImageScanResultSummary summary, ImageScanResultFilterMode filterMode) : base(state)
    {
        m_Summary = summary;
        m_FilterMode = filterMode;
        Reload();
        ExpandAll();
    }
    protected override TreeViewItem BuildRoot()
    {
        var root = new TreeViewItem { id = 0, depth = -1, displayName = "Root" };
        root.children = new List<TreeViewItem>();
        if (m_Summary == null)
            return root;
        int itemId = 1;
        bool showWithIssue = m_FilterMode == ImageScanResultFilterMode.全部 ||
                            m_FilterMode == ImageScanResultFilterMode.仅显示有问题;
        bool showWithoutIssue = m_FilterMode == ImageScanResultFilterMode.全部 ||
                               m_FilterMode == ImageScanResultFilterMode.仅显示无问题;
        if (showWithIssue)
        {
            foreach (var prefabResult in m_Summary.PrefabResults)
            {
                string prefabName = Path.GetFileNameWithoutExtension(prefabResult.PrefabPath);
                int issueCount = 0;
                foreach (var item in prefabResult.Items)
                {
                    if (item.MissingLanguages != null && item.MissingLanguages.Count > 0)
                        issueCount++;
                }
                var prefabItem = new ImageMetadataTreeViewItem(itemId++, $"{prefabName} ({issueCount}个问题)", prefabResult);
                foreach (var adapterItem in prefabResult.Items)
                {
                    string displayName;
                    if (adapterItem.MissingLanguages.Count > 0)
                        displayName = adapterItem.GetDisplayName();
                    else
                        displayName = $"{adapterItem.GameObjectPath} (配置完整)";
                    var adapterTreeItem = new ImageMetadataTreeViewItem(itemId++, displayName, adapterItem);
                    prefabItem.AddChild(adapterTreeItem);
                }
                root.AddChild(prefabItem);
            }
        }
        if (showWithoutIssue)
        {
            foreach (var prefabResult in m_Summary.PrefabResultsWithoutIssue)
            {
                string prefabName = Path.GetFileNameWithoutExtension(prefabResult.PrefabPath);
                var prefabItem = new ImageMetadataTreeViewItem(itemId++, $"{prefabName} (无问题)", prefabResult);
                foreach (var adapterItem in prefabResult.Items)
                {
                    var displayName = $"{adapterItem.GameObjectPath} (配置完整)";
                    var adapterTreeItem = new ImageMetadataTreeViewItem(itemId++, displayName, adapterItem);
                    prefabItem.AddChild(adapterTreeItem);
                }
                root.AddChild(prefabItem);
            }
        }
        SetupDepthsFromParentsAndChildren(root);
        return root;
    }
    protected override void RowGUI(RowGUIArgs args)
    {
        var item = args.item as ImageMetadataTreeViewItem;
        if (item != null && item.Metadata is ImagePrefabScanResult)
            GUI.Label(args.rowRect, item.displayName, EditorStyles.boldLabel);
        else if (item != null && item.Metadata is ImageScanResultItem 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);
    }
    protected override void DoubleClickedItem(int id)
    {
        var item = FindItem(id, rootItem) as ImageMetadataTreeViewItem;
        if (item == null) return;
        if (item.Metadata is ImageScanResultItem adapterItem)
            PingGameObject(adapterItem.PrefabPath, adapterItem.GameObjectPath);
        else if (item.Metadata is ImagePrefabScanResult prefabResult)
        {
            var obj = AssetDatabase.LoadAssetAtPath<GameObject>(prefabResult.PrefabPath);
            if (obj != null)
            {
                Selection.activeObject = obj;
                EditorGUIUtility.PingObject(obj);
            }
        }
    }
    private void PingGameObject(string prefabPath, string gameObjectPath)
    {
        var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
        if (prefab == null) return;
        Transform target = prefab.transform.Find(gameObjectPath);
        if (target != null)
        {
            Selection.activeObject = target.gameObject;
            EditorGUIUtility.PingObject(target.gameObject);
        }
        else
        {
            Selection.activeObject = prefab;
            EditorGUIUtility.PingObject(prefab);
            Debug.LogWarning($"[ImageScanTool] 找不到路径 '{gameObjectPath}',已选中整个预制体");
        }
    }
}
public class ImageLanguageAdapterScanTool : EditorWindow
{
    private string m_ScanDirectory = "Assets";
    private Vector2 m_ScrollPosition;
    private ImageScanResultSummary m_ScanResult;
    private bool m_IsScanning;
    private float m_ScanProgress;
    private string m_ScanStatus;
    private ImageScanResultTreeView m_TreeView;
    private TreeViewState m_TreeViewState;
    private ImageScanResultFilterMode m_ResultFilterMode = ImageScanResultFilterMode.全部;
    private int m_SourceLangIndex = 0;
    private int m_TargetLangIndex = 0;
    private bool m_OverwriteExisting = false;
    [MenuItem("程序/ImageLanguageAdapter扫描与管理工具")]
    public static void ShowWindow()
    {
        var window = GetWindow<ImageLanguageAdapterScanTool>("图片语言适配器扫描");
        window.minSize = new Vector2(600f, 500f);
    }
    private void OnEnable() => ImageLanguageAdapterHelper.Initialize();
    private void OnGUI()
    {
        DrawHeader();
        EditorGUILayout.Space(5f);
        DrawScanSettings();
        EditorGUILayout.Space(5f);
        DrawScanButton();
        EditorGUILayout.Space(5f);
        DrawResults();
        EditorGUILayout.Space(5f);
        DrawBatchOperations();
    }
    private void DrawHeader()
    {
        using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
        {
            EditorGUILayout.LabelField("ImageLanguageAdapter 配置缺失扫描与批量操作工具", EditorStyles.boldLabel);
            EditorGUILayout.HelpBox("扫描指定目录下所有预制体,检测组件的语言配置是否完整,或执行批量语言配置复制。", MessageType.Info);
        }
    }
    private void DrawScanSettings()
    {
        using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
        {
            EditorGUILayout.LabelField("扫描设置", EditorStyles.boldLabel);
            using (new EditorGUILayout.HorizontalScope())
            {
                EditorGUILayout.LabelField("目标目录:", GUILayout.Width(80f));
                m_ScanDirectory = EditorGUILayout.TextField(m_ScanDirectory);
                if (GUILayout.Button("选择...", GUILayout.Width(70f)))
                {
                    string path = EditorUtility.OpenFolderPanel("选择扫描目录", Application.dataPath, "");
                    if (!string.IsNullOrEmpty(path))
                        m_ScanDirectory = path.StartsWith(Application.dataPath) ? "Assets" + path.Substring(Application.dataPath.Length) : path;
                }
            }
            EditorGUILayout.Space(5f);
            EditorGUILayout.LabelField($"预设语言:");
            EditorGUILayout.BeginHorizontal();
            GUILayout.Space(15f);
            int displayCount = 0;
            foreach (var langId in ImageLanguageAdapterHelper.PresetLanguageIds)
            {
                if (langId == ImageLanguageAdapter.DefaultLangId) continue;
                EditorGUILayout.LabelField(langId, GUILayout.Width(50f));
                if (++displayCount % 8 == 0)
                {
                    EditorGUILayout.EndHorizontal();
                    EditorGUILayout.BeginHorizontal();
                    GUILayout.Space(15f);
                }
            }
            EditorGUILayout.EndHorizontal();
        }
    }
    private void DrawScanButton()
    {
        using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
        {
            using (new EditorGUILayout.HorizontalScope())
            {
                using (new EditorGUI.DisabledScope(m_IsScanning || string.IsNullOrEmpty(m_ScanDirectory)))
                {
                    if (GUILayout.Button("开始扫描", GUILayout.Height(30f))) StartScan();
                }
                if (m_IsScanning)
                {
                    EditorGUILayout.LabelField("扫描中...", GUILayout.Width(100f));
                    m_ScanProgress = EditorGUILayout.Slider(m_ScanProgress, 0f, 1f);
                    Repaint();
                }
            }
            if (!string.IsNullOrEmpty(m_ScanStatus)) EditorGUILayout.LabelField(m_ScanStatus);
        }
    }
    private void DrawResults()
    {
        using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
        {
            EditorGUILayout.LabelField("扫描结果", EditorStyles.boldLabel);
            if (m_ScanResult != null)
            {
                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 = (ImageScanResultFilterMode)EditorGUILayout.EnumPopup(m_ResultFilterMode);
                    if (EditorGUI.EndChangeCheck() && m_TreeView != null)
                    {
                        m_TreeViewState ??= new TreeViewState();
                        m_TreeView = new ImageScanResultTreeView(m_TreeViewState, m_ScanResult, m_ResultFilterMode);
                    }
                }
                EditorGUILayout.Space(5f);
                if (m_TreeView != null)
                {
                    m_ScrollPosition = EditorGUILayout.BeginScrollView(m_ScrollPosition, GUILayout.MinHeight(150f));
                    var rect = EditorGUILayout.GetControlRect(false, m_TreeView.totalHeight);
                    m_TreeView.OnGUI(rect);
                    EditorGUILayout.EndScrollView();
                }
                EditorGUILayout.Space(5f);
                using (new EditorGUILayout.HorizontalScope())
                {
                    if (GUILayout.Button("展开全部")) m_TreeView?.ExpandAll();
                    if (GUILayout.Button("折叠全部")) m_TreeView?.CollapseAll();
                }
            }
            else
            {
                EditorGUILayout.HelpBox("点击「开始扫描」按钮进行扫描", MessageType.None);
            }
        }
    }
    private void DrawBatchOperations()
    {
        using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
        {
            EditorGUILayout.LabelField("批量操作 (针对目标目录下的所有预制体上的ImageLanguageAdapter组件)", EditorStyles.boldLabel);
            if (ImageLanguageAdapterHelper.PresetLanguageIds == null || ImageLanguageAdapterHelper.PresetLanguageIds.Length == 0)
                return;
            using (new EditorGUILayout.HorizontalScope())
            {
                EditorGUILayout.LabelField("旧语言:", GUILayout.Width(60f));
                m_SourceLangIndex = EditorGUILayout.Popup(m_SourceLangIndex, ImageLanguageAdapterHelper.PresetLanguageNames);
                GUILayout.Space(20f);
                EditorGUILayout.LabelField("新语言:", GUILayout.Width(60f));
                m_TargetLangIndex = EditorGUILayout.Popup(m_TargetLangIndex, ImageLanguageAdapterHelper.PresetLanguageNames);
            }
            m_OverwriteExisting = EditorGUILayout.Toggle("覆盖已存在的目标配置", m_OverwriteExisting);
            EditorGUILayout.Space(5f);
            bool isSameLanguage = m_SourceLangIndex == m_TargetLangIndex;
            using (new EditorGUI.DisabledScope(isSameLanguage || m_IsScanning || string.IsNullOrEmpty(m_ScanDirectory)))
            {
                if (GUILayout.Button("批量复制配置", GUILayout.Height(30f)))
                {
                    string sourceLang = ImageLanguageAdapterHelper.PresetLanguageIds[m_SourceLangIndex];
                    string targetLang = ImageLanguageAdapterHelper.PresetLanguageIds[m_TargetLangIndex];
                    if (EditorUtility.DisplayDialog("高危操作确认",
                        $"此操作将遍历【{m_ScanDirectory}】下所有预制体。\n\n" +
                        $"把它们的 [{sourceLang}] 配置复制并应用到 [{targetLang}] 配置上。\n\n" +
                        $"此操作不可撤销!建议提前使用 Git/SVN 提交代码。\n确定要继续吗?",
                        "确定执行", "取消"))
                    {
                        ExecuteBatchCopy(sourceLang, targetLang);
                    }
                }
            }
            if (isSameLanguage)
            {
                EditorGUILayout.HelpBox("旧语言与新语言不能相同", MessageType.Warning);
            }
        }
    }
    private void ExecuteBatchCopy(string sourceLang, string targetLang)
    {
        string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { m_ScanDirectory });
        if (prefabGuids.Length == 0) return;
        int modifiedPrefabCount = 0;
        int modifiedAdapterCount = 0;
        try
        {
            for (int i = 0; i < prefabGuids.Length; i++)
            {
                string path = AssetDatabase.GUIDToAssetPath(prefabGuids[i]);
                EditorUtility.DisplayProgressBar("批量复制配置", $"处理中 ({i + 1}/{prefabGuids.Length}): {path}", (float)i / prefabGuids.Length);
                GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(path);
                bool isModified = false;
                foreach (var adapter in prefabAsset.GetComponentsInChildren<ImageLanguageAdapter>(true))
                {
                    if (adapter.HasConfig(sourceLang))
                    {
                        if (m_OverwriteExisting || !adapter.HasConfig(targetLang))
                        {
                            Undo.RecordObject(adapter, "Batch Copy Language Config");
                            var clonedConfig = adapter.GetConfig(sourceLang).Clone();
                            adapter.SetConfig(targetLang, clonedConfig);
                            EditorUtility.SetDirty(adapter);
                            isModified = true;
                            modifiedAdapterCount++;
                        }
                    }
                }
                if (isModified)
                {
                    modifiedPrefabCount++;
                }
            }
        }
        finally
        {
            EditorUtility.ClearProgressBar();
            AssetDatabase.SaveAssets();
            PerformScan();
            EditorUtility.DisplayDialog("批量操作完成",
                $"批量复制结束!\n\n修改的预制体数量: {modifiedPrefabCount} 个\n更新的适配器配置数量: {modifiedAdapterCount} 个",
                "确认");
        }
    }
    private void StartScan()
    {
        m_IsScanning = true;
        m_ScanProgress = 0f;
        m_ScanStatus = "准备扫描...";
        EditorApplication.CallbackFunction updateCallback = null;
        updateCallback = () =>
        {
            if (!m_IsScanning)
            {
                EditorApplication.update -= updateCallback;
                return;
            }
            PerformScan();
            m_IsScanning = false;
            EditorApplication.update -= updateCallback;
        };
        EditorApplication.update += updateCallback;
    }
    private void PerformScan()
    {
        m_ScanResult = new ImageScanResultSummary(m_ScanDirectory);
        string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { m_ScanDirectory });
        m_ScanResult.TotalPrefabsScanned = prefabGuids.Length;
        if (prefabGuids.Length == 0)
        {
            m_ScanStatus = "未找到任何预制体";
            return;
        }
        for (int i = 0; i < prefabGuids.Length; i++)
        {
            string path = AssetDatabase.GUIDToAssetPath(prefabGuids[i]);
            ScanPrefab(path, prefabGuids[i]);
            m_ScanProgress = (float)(i + 1) / prefabGuids.Length;
            m_ScanStatus = $"正在扫描: {Path.GetFileName(path)} ({i + 1}/{prefabGuids.Length})";
            if (i % 10 == 0) Repaint();
        }
        m_TreeViewState ??= new TreeViewState();
        m_TreeView = new ImageScanResultTreeView(m_TreeViewState, m_ScanResult, m_ResultFilterMode);
        m_ScanStatus = $"扫描完成! 发现 {m_ScanResult.AdaptersWithMissingConfig} 个缺失配置";
        Repaint();
    }
    private void ScanPrefab(string path, string guid)
    {
        GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
        if (prefab == null) return;
        var adapters = prefab.GetComponentsInChildren<ImageLanguageAdapter>(true);
        if (adapters.Length == 0) return;
        m_ScanResult.TotalAdaptersFound += adapters.Length;
        bool hasIssue = false;
        List<ImageScanResultItem> allItems = new List<ImageScanResultItem>();
        foreach (var adapter in adapters)
        {
            List<string> missing = new List<string>();
            foreach (var langId in ImageLanguageAdapterHelper.PresetLanguageIds)
            {
                if (langId == ImageLanguageAdapter.DefaultLangId) continue;
                if (!adapter.HasConfig(langId)) missing.Add(langId);
            }
            var item = new ImageScanResultItem(path, GetGameObjectPath(adapter.gameObject, prefab), adapter.TargetImageType, 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)
    {
        var parts = new List<string>();
        Transform curr = go.transform;
        while (curr != null && curr != root.transform)
        {
            parts.Insert(0, curr.name);
            curr = curr.parent;
        }
        return string.Join("/", parts);
    }
}
Assets/Editor/UIComponent/ImageLanguageAdapterScanTool.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7d753da3934048046828299f1ee02998
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Assets/Editor/UIComponent/TextLanguageAdapterEditor.cs
@@ -40,14 +40,30 @@
        {
            EditorGUILayout.LabelField("基本信息", EditorStyles.boldLabel);
            
            using (new EditorGUI.DisabledScope(true))
            EditorGUI.BeginChangeCheck();
            // 允许手动拖拽目标组件和设置类型
            Component newTarget = (Component)EditorGUILayout.ObjectField("目标组件", adapter.TargetTextComponent, typeof(Component), true);
            TextComponentType newType = (TextComponentType)EditorGUILayout.EnumPopup("组件类型", adapter.TargetTextType);
            if (EditorGUI.EndChangeCheck())
            {
                EditorGUILayout.ObjectField("目标组件", adapter.LanguageConfigs.keys.Count > 0 ? adapter.TargetTextComponent : null, typeof(Component), true);
                EditorGUILayout.EnumPopup("组件类型", adapter.TargetTextType);
                Undo.RecordObject(adapter, "Update Basic Info");
                // 智能辅助:当用户拖拽新组件时,尝试自动匹配一下类型,但也允许手动覆盖
                if (newTarget != null && newTarget != adapter.TargetTextComponent)
                {
                    string typeName = newTarget.GetType().Name;
                    if (typeName == "GradientText") newType = TextComponentType.GradientText;
                    else if (typeName == "TextEx") newType = TextComponentType.TextEx;
                    else if (newTarget is UnityEngine.UI.Text) newType = TextComponentType.Text;
                }
                adapter.TargetTextComponent = newTarget;
                adapter.TargetTextType = newType;
            }
            if (GUILayout.Button("刷新组件检测", GUILayout.Width(120))) adapter.Editor_ForceRefreshDetection();
            EditorGUILayout.HelpBox("组件或类型变更后需执行刷新组件检测", MessageType.Info);
            EditorGUILayout.HelpBox("请手动拖拽要适配的文本组件,并确认组件类型是否正确。", MessageType.Info);
        }
    }
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);
        AdaptersWithMissingConfig++;
        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;
        foreach (var prefabResult in m_Summary.PrefabResults)
        {
            string prefabName = Path.GetFileNameWithoutExtension(prefabResult.PrefabPath);
            var prefabItem = new MetadataTreeViewItem(itemId++, $"{prefabName} ({prefabResult.Items.Count}个问题)", prefabResult);
            foreach (var adapterItem in prefabResult.Items)
        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)
            {
                var adapterTreeItem = new MetadataTreeViewItem(itemId++, adapterItem.GetDisplayName(), adapterItem);
                prefabItem.AddChild(adapterTreeItem);
                string prefabName = Path.GetFileNameWithoutExtension(prefabResult.PrefabPath);
                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)
                {
                    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);
            }
            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)
            GUI.Label(args.rowRect, $"{adapterItem.GameObjectPath} (缺少: {string.Join(", ", adapterItem.MissingLanguages)})");
        {
            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);
            }
            var item = new ScanResultItem(path, GetGameObjectPath(adapter.gameObject, prefab), adapter.TargetTextType, guid)
            {
                MissingLanguages = missing
            };
            allItems.Add(item);
            if (missing.Count > 0)
            {
                var item = new ScanResultItem(path, GetGameObjectPath(adapter.gameObject, prefab), adapter.TargetTextType, guid)
                {
                    MissingLanguages = missing
                };
                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)