UI系统组件

This commit is contained in:
2026-06-06 09:00:11 +08:00
parent fe4fd60083
commit d794b83ebe
107 changed files with 25690 additions and 476 deletions

View File

@@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;
using BaseGames.Localization;
namespace BaseGames.Editor.Localization
{
/// <summary>
/// 本地化 JSON 表的编辑器侧磁盘读写门面(唯一入口)。
///
/// 所有编辑器工具表格编辑器、CSV 工具、审计模块、各 Inspector读写本地化文件
/// 都必须经此类,统一:路径(<see cref="LocalizationPaths"/>)、格式
/// <see cref="LocalizationSerializer"/>、Addressable 注册、编辑器缓存刷新。
///
/// 这样运行时加载与编辑器写盘永不脱节——杜绝"编辑器看着正常、Play 加载不到"的隐藏 bug。
/// </summary>
public static class LocalizationFileIO
{
/// <summary>所有受支持语言(枚举顺序)。</summary>
public static readonly Language[] AllLanguages =
(Language[])Enum.GetValues(typeof(Language));
// ── 读 ───────────────────────────────────────────────────────────────
/// <summary>
/// 读取指定语言 + 表的 key→value 字典(返回可安全修改的副本,不存在时返回空字典)。
/// 内部走 <see cref="LocalizationManager.GetEditorTable"/> 复用其静态缓存。
/// </summary>
public static Dictionary<string, string> Read(Language language, string table)
{
var src = LocalizationManager.GetEditorTable(language, table);
return src == null
? new Dictionary<string, string>(StringComparer.Ordinal)
: new Dictionary<string, string>(src, StringComparer.Ordinal);
}
/// <summary>该表的 JSON 文件是否已存在于磁盘。</summary>
public static bool TableExists(Language language, string table)
=> AssetDatabase.LoadAssetAtPath<TextAsset>(LocalizationPaths.AssetPath(language, table)) != null;
/// <summary>扫描数据根目录,返回磁盘上实际存在子目录的语言。</summary>
public static List<Language> DiscoverLanguages()
{
var result = new List<Language>();
if (!AssetDatabase.IsValidFolder(LocalizationPaths.DataRoot)) return result;
foreach (var langFolder in AssetDatabase.GetSubFolders(LocalizationPaths.DataRoot))
{
string langName = Path.GetFileName(langFolder);
if (Enum.TryParse<Language>(langName, out var lang))
result.Add(lang);
}
return result;
}
// ── 写 ───────────────────────────────────────────────────────────────
/// <summary>
/// 将字典写回指定语言 + 表的 JSON 文件(正确的 <c>{entries:[…]}</c> 格式 + 正确目录)。
/// 新文件会自动注册到 Addressables地址 <see cref="LocalizationPaths.Address"/>
/// 写盘后清除编辑器预览缓存。
/// </summary>
/// <param name="language">目标语言。</param>
/// <param name="table">目标表名(<see cref="LocalizationTable"/> 常量)。</param>
/// <param name="dict">key→value 字典。</param>
/// <param name="registerAddressable">是否确保 Addressable 注册(默认 true。</param>
public static void Write(Language language, string table,
IReadOnlyDictionary<string, string> dict, bool registerAddressable = true)
{
string assetPath = LocalizationPaths.AssetPath(language, table);
EditorScaffoldUtils.EnsureFolder(LocalizationPaths.LanguageFolder(language));
// JSON 文件不写 BOMAddressables/TextAsset 解析期望纯 UTF-8
string json = LocalizationSerializer.Serialize(dict, sortKeys: true);
string fullPath = ToFullPath(assetPath);
File.WriteAllText(fullPath, json, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);
if (registerAddressable)
EnsureAddressable(language, table, assetPath);
LocalizationManager.ClearEditorPreviewCache();
}
// ── Ping ───────────────────────────────────────────────────────────────
/// <summary>在 Project 窗口中定位指定语言 + 表的 JSON 文件。</summary>
public static TextAsset Ping(Language language, string table)
{
var asset = AssetDatabase.LoadAssetAtPath<TextAsset>(LocalizationPaths.AssetPath(language, table));
if (asset != null) EditorScaffoldUtils.PingAndSelect(asset);
return asset;
}
/// <summary>在 Project 窗口中定位指定表的任一存在语言文件(优先简体中文 → 英文 → 其余)。</summary>
public static TextAsset PingAny(string table)
{
foreach (var lang in OrderedForPing())
{
var asset = AssetDatabase.LoadAssetAtPath<TextAsset>(LocalizationPaths.AssetPath(lang, table));
if (asset != null) { EditorScaffoldUtils.PingAndSelect(asset); return asset; }
}
Debug.LogWarning($"[LocalizationFileIO] 未找到本地化表文件:{LocalizationPaths.DataRoot}/…/{table}.json");
return null;
}
// ── 内部 ───────────────────────────────────────────────────────────────
private static IEnumerable<Language> OrderedForPing()
{
yield return Language.ChineseSimplified;
yield return Language.English;
foreach (var lang in AllLanguages)
if (lang != Language.ChineseSimplified && lang != Language.English)
yield return lang;
}
/// <summary>
/// 确保该 JSON 文件已注册为对应 Addressable 地址。
/// 复用 <see cref="BaseGames.Editor.Addressables.CoreSceneRegistrar"/> 的 FindEntry-or-Create 幂等范式。
/// </summary>
private static void EnsureAddressable(Language language, string table, string assetPath)
{
var settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null)
{
Debug.LogWarning(
$"[LocalizationFileIO] Addressable Settings 未初始化,未能注册 " +
$"{LocalizationPaths.Address(language, table)}。运行时可能加载不到该表。");
return;
}
string guid = AssetDatabase.AssetPathToGUID(assetPath);
if (string.IsNullOrEmpty(guid)) return;
string address = LocalizationPaths.Address(language, table);
var entry = settings.FindAssetEntry(guid)
?? settings.CreateOrMoveEntry(guid, settings.DefaultGroup, false, false);
// 已注册且地址正确则无需改动(避免无意义的 SetDirty
if (entry.address == address) return;
entry.address = address;
EditorUtility.SetDirty(settings);
AssetDatabase.SaveAssets();
}
private static string ToFullPath(string assetPath)
{
string projectRoot = Path.GetDirectoryName(Application.dataPath)!;
return Path.Combine(projectRoot, assetPath.Replace('/', Path.DirectorySeparatorChar));
}
}
}