UI系统优化

This commit is contained in:
2026-05-25 11:54:37 +08:00
parent c7057db27d
commit 3c812cfb41
130 changed files with 4738 additions and 477 deletions

View File

@@ -0,0 +1,388 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;
using BaseGames.Localization;
namespace BaseGames.Editor.Localization
{
/// <summary>
/// 本地化 CSV 导入/导出工具。
///
/// 每个表导出为独立 CSV列顺序key, ChineseSimplified, English, Japanese, Korean。
/// 文件存放路径Assets/_Game/Localization/Export/{TableName}.csv
///
/// 导入:读取 CSV → 回写 Resources/Localization/{Language}/{TableName}.json
///
/// 菜单BaseGames / Localization / CSV 导入导出工具
/// </summary>
public class LocalizationCsvTool : EditorWindow
{
[MenuItem("BaseGames/Localization/CSV 导入导出工具")]
private static void Open()
{
var win = GetWindow<LocalizationCsvTool>("本地化 CSV 工具");
win.minSize = new Vector2(480, 360);
}
// ── 状态 ─────────────────────────────────────────────────────────────
private static readonly Language[] s_allLanguages =
(Language[])Enum.GetValues(typeof(Language));
private static readonly string[] s_allTables =
{
LocalizationTable.UI,
LocalizationTable.Dialogue,
LocalizationTable.Quest,
LocalizationTable.Spells,
LocalizationTable.Skills,
LocalizationTable.Items,
LocalizationTable.Character,
LocalizationTable.Tutorial,
};
private const string ExportDir = "Assets/_Game/Localization/Export";
private readonly bool[] _exportSelected = new bool[s_allTables.Length];
private readonly bool[] _importSelected = new bool[s_allTables.Length];
private string _statusMessage = "";
private MessageType _statusType = MessageType.None;
private Vector2 _scroll;
// ── GUI ───────────────────────────────────────────────────────────────
private void OnGUI()
{
_scroll = EditorGUILayout.BeginScrollView(_scroll);
EditorGUILayout.Space(6);
GUILayout.Label("📤 导出 → CSV", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
$"将 Resources/Localization/ 中的 JSON 表导出为 CSV 文件。\n" +
$"目标目录:{ExportDir}/",
MessageType.Info);
EditorGUI.indentLevel++;
for (int i = 0; i < s_allTables.Length; i++)
_exportSelected[i] = EditorGUILayout.Toggle(s_allTables[i], _exportSelected[i]);
EditorGUI.indentLevel--;
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("全选")) SetAll(_exportSelected, true);
if (GUILayout.Button("全不选")) SetAll(_exportSelected, false);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(4);
if (GUILayout.Button("▶ 导出选中表", GUILayout.Height(30)))
RunExport();
EditorGUILayout.Space(16);
EditorGUILayout.LabelField("", GUI.skin.horizontalSlider);
GUILayout.Label("📥 导入 ← CSV", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
$"读取 {ExportDir}/ 中的 CSV回写至 Resources/Localization/ JSON 文件。\n" +
"已有 Key 覆盖,新增 Key 追加,不删除多余 Key。",
MessageType.Info);
EditorGUI.indentLevel++;
for (int i = 0; i < s_allTables.Length; i++)
_importSelected[i] = EditorGUILayout.Toggle(s_allTables[i], _importSelected[i]);
EditorGUI.indentLevel--;
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("全选")) SetAll(_importSelected, true);
if (GUILayout.Button("全不选")) SetAll(_importSelected, false);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(4);
if (GUILayout.Button("▶ 导入选中表", GUILayout.Height(30)))
RunImport();
EditorGUILayout.Space(8);
if (!string.IsNullOrEmpty(_statusMessage))
EditorGUILayout.HelpBox(_statusMessage, _statusType);
EditorGUILayout.Space(6);
EditorGUILayout.EndScrollView();
}
// ── 导出 ─────────────────────────────────────────────────────────────
private void RunExport()
{
EnsureDirectory(ExportDir);
int exported = 0;
try
{
for (int ti = 0; ti < s_allTables.Length; ti++)
{
if (!_exportSelected[ti]) continue;
string tableName = s_allTables[ti];
EditorUtility.DisplayProgressBar("导出 CSV", $"导出 {tableName}…", (float)ti / s_allTables.Length);
// 收集所有语言的字典(以第一个语言的 Key 集合为主键集)
var langDicts = new Dictionary<Language, Dictionary<string, string>>();
var allKeys = new SortedSet<string>(StringComparer.Ordinal);
foreach (var lang in s_allLanguages)
{
LocalizationManager.ClearEditorPreviewCache();
var dict = LocalizationManager.GetEditorTable(lang, tableName);
if (dict != null)
{
langDicts[lang] = dict;
foreach (var k in dict.Keys) allKeys.Add(k);
}
}
if (allKeys.Count == 0)
{
Debug.LogWarning($"[CsvTool] 表「{tableName}」无数据,跳过导出。");
continue;
}
var sb = new StringBuilder();
// 表头
sb.Append("key");
foreach (var lang in s_allLanguages)
sb.Append(',').Append(lang);
sb.AppendLine();
// 数据行
foreach (var key in allKeys)
{
sb.Append(CsvEscape(key));
foreach (var lang in s_allLanguages)
{
string val = "";
if (langDicts.TryGetValue(lang, out var d))
d.TryGetValue(key, out val);
sb.Append(',').Append(CsvEscape(val ?? ""));
}
sb.AppendLine();
}
string csvPath = $"{ExportDir}/{tableName}.csv";
File.WriteAllText(csvPath, sb.ToString(), Encoding.UTF8);
exported++;
}
}
finally
{
EditorUtility.ClearProgressBar();
}
AssetDatabase.Refresh();
SetStatus($"✅ 成功导出 {exported} 个表到 {ExportDir}/", MessageType.Info);
}
// ── 导入 ─────────────────────────────────────────────────────────────
private void RunImport()
{
int imported = 0;
int errors = 0;
try
{
for (int ti = 0; ti < s_allTables.Length; ti++)
{
if (!_importSelected[ti]) continue;
string tableName = s_allTables[ti];
string csvPath = Path.Combine(Path.GetDirectoryName(Application.dataPath)!, $"{ExportDir}/{tableName}.csv");
EditorUtility.DisplayProgressBar("导入 CSV", $"导入 {tableName}…", (float)ti / s_allTables.Length);
if (!File.Exists(csvPath))
{
Debug.LogWarning($"[CsvTool] CSV 文件不存在,跳过:{ExportDir}/{tableName}.csv");
continue;
}
try
{
var rows = ParseCsv(File.ReadAllText(csvPath, Encoding.UTF8));
if (rows.Count < 2) continue;
// 表头行解析出列对应的语言
var header = rows[0];
var langCols = new List<(int col, Language lang)>();
for (int col = 1; col < header.Count; col++)
{
if (Enum.TryParse<Language>(header[col].Trim(), out var lang))
langCols.Add((col, lang));
}
// 为每个语言准备合并后的字典
var mergedDicts = new Dictionary<Language, Dictionary<string, string>>();
foreach (var (_, lang) in langCols)
{
LocalizationManager.ClearEditorPreviewCache();
var existing = LocalizationManager.GetEditorTable(lang, tableName)
?? new Dictionary<string, string>(StringComparer.Ordinal);
mergedDicts[lang] = new Dictionary<string, string>(existing, StringComparer.Ordinal);
}
// 用 CSV 数据覆盖/追加
for (int row = 1; row < rows.Count; row++)
{
var cells = rows[row];
if (cells.Count == 0 || string.IsNullOrWhiteSpace(cells[0])) continue;
string key = cells[0];
foreach (var (col, lang) in langCols)
{
string val = col < cells.Count ? cells[col] : "";
mergedDicts[lang][key] = val;
}
}
// 写回 JSON
foreach (var (lang, dict) in mergedDicts)
{
string jsonPath = GetJsonPath(lang, tableName);
EnsureDirectory(Path.GetDirectoryName(jsonPath));
File.WriteAllText(jsonPath, DictToJson(dict), Encoding.UTF8);
}
imported++;
}
catch (Exception ex)
{
Debug.LogError($"[CsvTool] 导入「{tableName}」失败:{ex.Message}");
errors++;
}
}
}
finally
{
EditorUtility.ClearProgressBar();
}
AssetDatabase.Refresh();
LocalizationManager.ClearEditorPreviewCache();
string msg = errors == 0
? $"✅ 成功导入 {imported} 个表。"
: $"⚠ 导入 {imported} 个表,{errors} 个失败(见控制台)。";
SetStatus(msg, errors == 0 ? MessageType.Info : MessageType.Warning);
}
// ── 工具函数 ─────────────────────────────────────────────────────────
/// <summary>将 value 按 RFC 4180 规范包裹(含逗号/双引号/换行时用双引号包裹,内部双引号转义为 "")。</summary>
private static string CsvEscape(string value)
{
if (value == null) return "";
bool needsQuote = value.Contains(',') || value.Contains('"') || value.Contains('\n') || value.Contains('\r');
if (!needsQuote) return value;
return "\"" + value.Replace("\"", "\"\"") + "\"";
}
/// <summary>简单 CSV 解析器,支持 RFC 4180 带引号字段(含换行)。</summary>
private static List<List<string>> ParseCsv(string text)
{
var rows = new List<List<string>>();
var row = new List<string>();
var cell = new StringBuilder();
bool inQuote = false;
int i = 0;
while (i < text.Length)
{
char c = text[i];
if (inQuote)
{
if (c == '"')
{
if (i + 1 < text.Length && text[i + 1] == '"')
{ cell.Append('"'); i += 2; }
else
{ inQuote = false; i++; }
}
else
{ cell.Append(c); i++; }
}
else
{
if (c == '"')
{ inQuote = true; i++; }
else if (c == ',')
{ row.Add(cell.ToString()); cell.Clear(); i++; }
else if (c == '\r')
{
row.Add(cell.ToString()); cell.Clear();
rows.Add(row); row = new List<string>();
if (i + 1 < text.Length && text[i + 1] == '\n') i++;
i++;
}
else if (c == '\n')
{ row.Add(cell.ToString()); cell.Clear(); rows.Add(row); row = new List<string>(); i++; }
else
{ cell.Append(c); i++; }
}
}
if (cell.Length > 0 || row.Count > 0)
{
row.Add(cell.ToString());
rows.Add(row);
}
return rows;
}
/// <summary>将字符串字典序列化为最小 JSON 对象UTF-8适合直接写入 Resources JSON。</summary>
private static string DictToJson(Dictionary<string, string> dict)
{
var sb = new StringBuilder();
sb.AppendLine("{");
int written = 0;
foreach (var kv in dict)
{
sb.Append(" ").Append(JsonString(kv.Key)).Append(": ").Append(JsonString(kv.Value));
if (++written < dict.Count) sb.Append(',');
sb.AppendLine();
}
sb.Append('}');
return sb.ToString();
}
private static string JsonString(string s)
{
if (s == null) return "\"\"";
s = s.Replace("\\", "\\\\")
.Replace("\"", "\\\"")
.Replace("\n", "\\n")
.Replace("\r", "\\r")
.Replace("\t", "\\t");
return $"\"{s}\"";
}
private static string GetJsonPath(Language lang, string tableName)
=> $"Assets/Resources/Localization/{lang}/{tableName}.json";
private static void EnsureDirectory(string dir)
{
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
}
private static void SetAll(bool[] arr, bool v)
{
for (int i = 0; i < arr.Length; i++) arr[i] = v;
}
private void SetStatus(string msg, MessageType type)
{
_statusMessage = msg;
_statusType = type;
Repaint();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8b7198a6db4ff314b9fadc980f266744
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,298 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using BaseGames.Localization;
namespace BaseGames.Editor.Localization
{
/// <summary>
/// 带搜索框的本地化 Key 选择器窗口。
/// 替代 <c>GenericMenu</c>,支持 1000+ 条目的快速模糊搜索key 和译文均可搜索)。
///
/// 键盘操作:
/// ↑ / ↓ — 上下移动高亮行
/// Enter — 确认选择当前高亮行
/// Esc — 关闭窗口(不选中任何 key
///
/// 用法:
/// <code>
/// LocalizationKeyPickerWindow.Show(table, currentKey, selectedKey => {
/// _keyProp.stringValue = selectedKey;
/// serializedObject.ApplyModifiedProperties();
/// });
/// </code>
/// </summary>
public class LocalizationKeyPickerWindow : EditorWindow
{
// ── 状态 ──────────────────────────────────────────────────────────────
private string _searchText = "";
private Vector2 _scroll;
private List<(string key, string preview)> _allEntries = new();
private List<(string key, string preview)> _filtered = new();
private Action<string> _onSelected;
private string _currentKey;
private int _hoveredIndex = -1;
private int _keyboardIndex = -1; // 键盘当前高亮行
// 预缓存样式,避免每帧 new GUIStyle
private GUIStyle _keyStyle;
private GUIStyle _previewStyle;
private const float RowHeight = 40f;
private const float SearchHeight = 22f;
// ── 公开入口 ──────────────────────────────────────────────────────────
/// <summary>
/// 打开 Key 选择器窗口。
/// </summary>
/// <param name="table">要从哪张表加载 Key先查简体中文找不到查英文。</param>
/// <param name="currentKey">当前已选 Key高亮显示。</param>
/// <param name="onSelected">选中 Key 后的回调。</param>
public static void Show(string table, string currentKey, Action<string> onSelected)
{
var dict = LocalizationManager.GetEditorTable(Language.ChineseSimplified, table)
?? LocalizationManager.GetEditorTable(Language.English, table);
if (dict == null || dict.Count == 0)
{
EditorUtility.DisplayDialog("Key 选择器",
$"表「{table}」尚无可用 Key。\n" +
$"请先在 Resources/Localization/{{语言}}/{table}.json 中添加条目。",
"确定");
return;
}
var win = CreateInstance<LocalizationKeyPickerWindow>();
win.titleContent = new GUIContent($"Key 选择器 — {table}");
win.minSize = new Vector2(440, 520);
win._currentKey = currentKey;
win._onSelected = onSelected;
win._allEntries = dict
.Select(kvp => (kvp.Key, kvp.Value))
.OrderBy(t => t.Item1, StringComparer.Ordinal)
.ToList();
win.ApplyFilter();
// 把键盘索引初始化到当前 key 所在行
int idx = win._filtered.FindIndex(t => t.key == currentKey);
if (idx >= 0)
{
win._keyboardIndex = idx;
win._scroll = new Vector2(0, idx * RowHeight);
}
win.ShowUtility();
}
// ── GUI ───────────────────────────────────────────────────────────────
private void OnGUI()
{
EnsureStyles();
HandleKeyboardInput(); // 在绘制之前处理按键,当帧即生效
DrawSearchBar();
DrawEntryCount();
DrawList();
}
// ── 键盘导航 ──────────────────────────────────────────────────────────
private void HandleKeyboardInput()
{
var e = Event.current;
if (e.type != EventType.KeyDown) return;
switch (e.keyCode)
{
case KeyCode.UpArrow:
MoveKeyboardSelection(-1);
e.Use();
break;
case KeyCode.DownArrow:
MoveKeyboardSelection(+1);
e.Use();
break;
case KeyCode.Return:
case KeyCode.KeypadEnter:
if (_keyboardIndex >= 0 && _keyboardIndex < _filtered.Count)
{
_onSelected?.Invoke(_filtered[_keyboardIndex].key);
e.Use();
Close();
}
break;
case KeyCode.Escape:
e.Use();
Close();
break;
}
}
private void MoveKeyboardSelection(int delta)
{
if (_filtered.Count == 0) return;
_keyboardIndex = _keyboardIndex < 0
? (delta > 0 ? 0 : _filtered.Count - 1)
: Mathf.Clamp(_keyboardIndex + delta, 0, _filtered.Count - 1);
ScrollToKeyboardIndex();
Repaint();
}
/// <summary>调整滚动位置,保证 _keyboardIndex 行始终在可见视口内。</summary>
private void ScrollToKeyboardIndex()
{
float viewportH = position.height - SearchHeight - 30f;
float rowTop = _keyboardIndex * RowHeight;
float rowBot = rowTop + RowHeight;
if (rowTop < _scroll.y)
_scroll.y = rowTop;
else if (rowBot > _scroll.y + viewportH)
_scroll.y = rowBot - viewportH;
}
// ── 搜索栏 ────────────────────────────────────────────────────────────
private void DrawSearchBar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
GUI.SetNextControlName("SearchField");
var newSearch = EditorGUILayout.TextField(_searchText, EditorStyles.toolbarSearchField);
if (GUILayout.Button("✕", EditorStyles.toolbarButton, GUILayout.Width(22)))
newSearch = "";
EditorGUILayout.EndHorizontal();
if (newSearch != _searchText)
{
_searchText = newSearch;
_hoveredIndex = -1;
_keyboardIndex = -1;
ApplyFilter();
}
}
private void DrawEntryCount()
{
EditorGUILayout.LabelField(
$"共 {_filtered.Count} 个结果(总 {_allEntries.Count} 条) | ↑↓ 导航 Enter 选中 Esc 关闭",
EditorStyles.centeredGreyMiniLabel);
EditorGUILayout.Space(2);
}
// ── 列表(虚拟渲染)──────────────────────────────────────────────────
private void DrawList()
{
float viewportH = position.height - SearchHeight - 30f;
_scroll = EditorGUILayout.BeginScrollView(_scroll,
GUILayout.Height(viewportH));
int firstVisible = Mathf.Max(0, (int)(_scroll.y / RowHeight) - 1);
int lastVisible = Mathf.Min(_filtered.Count - 1,
(int)((_scroll.y + viewportH) / RowHeight) + 1);
// 顶部占位(未渲染行)
if (firstVisible > 0)
GUILayout.Space(firstVisible * RowHeight);
for (int i = firstVisible; i <= lastVisible && i < _filtered.Count; i++)
{
var (key, preview) = _filtered[i];
bool isCurrent = string.Equals(key, _currentKey, StringComparison.Ordinal);
bool isKeyboard = i == _keyboardIndex;
bool isHovered = i == _hoveredIndex;
var rowRect = GUILayoutUtility.GetRect(0, RowHeight, GUILayout.ExpandWidth(true));
// 背景优先级:当前选中 > 键盘高亮 > 鼠标悬停
Color bg = isCurrent ? new Color(0.25f, 0.55f, 1f, 0.25f)
: isKeyboard ? new Color(0.40f, 0.75f, 0.4f, 0.20f)
: isHovered ? new Color(1f, 1f, 1f, 0.05f)
: Color.clear;
if (bg.a > 0)
EditorGUI.DrawRect(rowRect, bg);
// 当前 key 左边蓝色竖条
if (isCurrent)
EditorGUI.DrawRect(new Rect(rowRect.x, rowRect.y, 3, rowRect.height),
new Color(0.3f, 0.7f, 1f, 1f));
// 键盘选中绿色竖条
else if (isKeyboard)
EditorGUI.DrawRect(new Rect(rowRect.x, rowRect.y, 3, rowRect.height),
new Color(0.4f, 0.9f, 0.4f, 1f));
// Key 文本
EditorGUI.LabelField(
new Rect(rowRect.x + 8, rowRect.y + 4, rowRect.width - 12, 18),
key, _keyStyle);
// 预览文本(绿色)
string previewText = preview.Length > 60 ? preview[..60] + "…" : preview;
EditorGUI.LabelField(
new Rect(rowRect.x + 8, rowRect.y + 22, rowRect.width - 12, 14),
previewText, _previewStyle);
// 鼠标交互
var ev = Event.current;
if (rowRect.Contains(ev.mousePosition))
{
if (_hoveredIndex != i) { _hoveredIndex = i; Repaint(); }
if (ev.type == EventType.MouseDown && ev.button == 0)
{
_onSelected?.Invoke(key);
ev.Use();
Close();
return;
}
}
}
// 底部占位(未渲染行)
int remaining = _filtered.Count - lastVisible - 1;
if (remaining > 0)
GUILayout.Space(remaining * RowHeight);
EditorGUILayout.EndScrollView();
}
// ── 辅助 ──────────────────────────────────────────────────────────────
private void ApplyFilter()
{
if (string.IsNullOrEmpty(_searchText))
{
_filtered = new List<(string, string)>(_allEntries);
return;
}
_filtered = _allEntries
.Where(t => t.key.IndexOf(_searchText, StringComparison.OrdinalIgnoreCase) >= 0
|| t.preview.IndexOf(_searchText, StringComparison.OrdinalIgnoreCase) >= 0)
.ToList();
}
private void EnsureStyles()
{
_keyStyle ??= new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 11,
};
_previewStyle ??= new GUIStyle(EditorStyles.miniLabel)
{
normal = { textColor = new Color(0.5f, 0.88f, 0.5f) },
fontSize = 10,
};
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f25bdb0dfede0f24f863e08c78db8a87
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,143 @@
using UnityEditor;
using UnityEngine;
using BaseGames.Localization;
namespace BaseGames.Editor.Localization
{
/// <summary>
/// <see cref="LocalizedText"/> 自定义 Inspector。
/// 在 key / table 字段下方实时预览解析后的本地化文本,无需进入 Play Mode。
/// 支持模拟格式化参数预览(逗号分隔)。
/// </summary>
[CustomEditor(typeof(LocalizedText))]
public class LocalizedTextEditor : UnityEditor.Editor
{
private static GUIStyle s_foundStyle;
private SerializedProperty _keyProp;
private SerializedProperty _tableProp;
// 编辑器内模拟格式化参数(逗号分隔字符串,仅用于预览,不持久化)
private string _previewFormatArgs = "";
private void OnEnable()
{
_keyProp = serializedObject.FindProperty("_key");
_tableProp = serializedObject.FindProperty("_table");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
DrawDefaultInspector();
serializedObject.ApplyModifiedProperties();
if (s_foundStyle == null)
{
s_foundStyle = new GUIStyle(EditorStyles.helpBox)
{
fontSize = 11,
alignment = TextAnchor.MiddleLeft,
padding = new RectOffset(8, 8, 4, 4),
};
s_foundStyle.normal.textColor = new Color(0.55f, 0.90f, 0.55f);
}
string key = _keyProp?.stringValue;
string table = _tableProp?.stringValue ?? LocalizationTable.UI;
if (string.IsNullOrEmpty(key)) return;
EditorGUILayout.Space(4);
// ── 格式化参数模拟输入 ──────────────────────────────────────────
EditorGUI.BeginChangeCheck();
_previewFormatArgs = EditorGUILayout.TextField(
new GUIContent("预览参数", "模拟格式化参数,逗号分隔。例:\"100, 玩家名\" → 代入 {0} {1}"),
_previewFormatArgs);
bool argsChanged = EditorGUI.EndChangeCheck();
if (argsChanged) Repaint();
// ── 预览文本 ────────────────────────────────────────────────────
string rawPreview = LocalizationManager.GetEditorPreview(key, table);
if (string.IsNullOrEmpty(rawPreview))
{
EditorGUILayout.HelpBox(
$"Key「{key}」在表「{table}」中未找到(简体中文表与英文表均未命中)。\n" +
$"请检查 Resources/Localization/{{Language}}/{table}.json 文件。",
MessageType.Warning);
}
else
{
string displayText = ApplyPreviewArgs(rawPreview, _previewFormatArgs);
bool hasArgs = !string.IsNullOrWhiteSpace(_previewFormatArgs);
string label = hasArgs
? $"▸ 预览(参数展开):{displayText}"
: $"▸ 预览(简体中文):{displayText}";
EditorGUILayout.LabelField(label, s_foundStyle);
if (hasArgs && displayText == rawPreview)
EditorGUILayout.HelpBox(
"格式化参数未能代入(模板中可能没有 {0} 占位符,或参数数量不足)。",
MessageType.Info);
}
EditorGUILayout.Space(2);
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("选择 Key ▾", GUILayout.Width(90)))
LocalizationKeyPickerWindow.Show(table, key, selectedKey =>
{
_keyProp.stringValue = selectedKey;
serializedObject.ApplyModifiedProperties();
((LocalizedText)target).UpdateEditorPreview();
Repaint();
});
if (GUILayout.Button("刷新预览", GUILayout.Width(80)))
{
LocalizationManager.ClearEditorPreviewCache();
((LocalizedText)target).UpdateEditorPreview();
Repaint();
}
if (GUILayout.Button($"跳转到表文件({table}", GUILayout.Width(160)))
PingLocalizationFile(table);
EditorGUILayout.EndHorizontal();
}
/// <summary>将逗号分隔的参数字符串解析为 object[] 并代入模板。</summary>
private static string ApplyPreviewArgs(string template, string argsInput)
{
if (string.IsNullOrWhiteSpace(argsInput)) return template;
// 按逗号分割,保留空白(策划可能输入 "100, "
string[] parts = argsInput.Split(',');
var args = new object[parts.Length];
for (int i = 0; i < parts.Length; i++)
args[i] = parts[i].Trim();
try { return string.Format(template, args); }
catch { return template; }
}
private static void PingLocalizationFile(string tableName)
{
string[] guids = AssetDatabase.FindAssets(
$"t:TextAsset {tableName}",
new[] { "Assets/Resources/Localization" });
foreach (var guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
if (!path.EndsWith($"/{tableName}.json", System.StringComparison.OrdinalIgnoreCase)) continue;
var asset = AssetDatabase.LoadAssetAtPath<TextAsset>(path);
if (asset == null) continue;
EditorGUIUtility.PingObject(asset);
Selection.activeObject = asset;
return;
}
Debug.LogWarning($"[LocalizedTextEditor] 未找到本地化表文件Resources/Localization/…/{tableName}.json");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2de9be8c918457447ad647c1af2b3c14
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: