UI系统优化
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ae50b43961101f54f9b0c8c42f833c52
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -40,8 +40,8 @@ namespace BaseGames.Editor.Dialogue
|
||||
if (string.IsNullOrEmpty(resolved))
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
$"nameKey「{npc.nameKey}」在本地化表「{npc.localizationTable}」中未找到对应文本(或 LocalizationManager 未初始化)。\n" +
|
||||
"请检查本地化表中是否存在此 Key。",
|
||||
$"nameKey「{npc.nameKey}」在本地化表「{npc.localizationTable}」中未找到对应文本。\n" +
|
||||
"请检查本地化 JSON 文件中是否存在此 Key。",
|
||||
MessageType.Warning);
|
||||
}
|
||||
else
|
||||
@@ -51,10 +51,15 @@ namespace BaseGames.Editor.Dialogue
|
||||
s_previewStyle);
|
||||
}
|
||||
|
||||
// ── 跳转到本地化文件 ────────────────────────────────────────────
|
||||
// ── 操作按钮行 ─────────────────────────────────────────────────
|
||||
EditorGUILayout.Space(4);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
if (GUILayout.Button("刷新预览", GUILayout.Width(80)))
|
||||
{
|
||||
BaseGames.Localization.LocalizationManager.ClearEditorPreviewCache();
|
||||
Repaint();
|
||||
}
|
||||
if (GUILayout.Button($"跳转到本地化文件({npc.localizationTable} 表)", GUILayout.Width(220)))
|
||||
{
|
||||
PingLocalizationFile(npc.localizationTable);
|
||||
@@ -63,21 +68,13 @@ namespace BaseGames.Editor.Dialogue
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试通过 LocalizationManager(若已加载)解析 nameKey;
|
||||
/// 如未初始化或找不到 Key,返回 null。
|
||||
/// 编辑器预览:直接从 Resources 读取本地化文本,无需运行时服务实例。
|
||||
/// 找不到时返回 null(让上层显示 warning 而非 key 本身)。
|
||||
/// </summary>
|
||||
private static string TryResolveNameKey(string key, string table = "UI")
|
||||
private static string TryResolveNameKey(string key, string table = BaseGames.Localization.LocalizationTable.UI)
|
||||
{
|
||||
try
|
||||
{
|
||||
// LocalizationManager.Get 在编辑器下可能返回空字符串(未初始化),视为未找到
|
||||
var resolved = BaseGames.Localization.LocalizationManager.Get(key, table);
|
||||
return string.IsNullOrEmpty(resolved) ? null : resolved;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// GetEditorPreview 直接读 JSON,编辑器下不依赖 ServiceLocator 实例
|
||||
return BaseGames.Localization.LocalizationManager.GetEditorPreview(key, table);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
11
Assets/_Game/Scripts/Editor/Dialogue/NpcSOEditor.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Dialogue/NpcSOEditor.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9c09dcd9907ab854da42443d37ff52f9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/_Game/Scripts/Editor/Localization.meta
Normal file
8
Assets/_Game/Scripts/Editor/Localization.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5512b3a2ed772ad45a18a6340924f961
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
388
Assets/_Game/Scripts/Editor/Localization/LocalizationCsvTool.cs
Normal file
388
Assets/_Game/Scripts/Editor/Localization/LocalizationCsvTool.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b7198a6db4ff314b9fadc980f266744
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f25bdb0dfede0f24f863e08c78db8a87
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
143
Assets/_Game/Scripts/Editor/Localization/LocalizedTextEditor.cs
Normal file
143
Assets/_Game/Scripts/Editor/Localization/LocalizedTextEditor.cs
Normal 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");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2de9be8c918457447ad647c1af2b3c14
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -4,6 +4,7 @@ using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using BaseGames.Dialogue;
|
||||
using BaseGames.Localization;
|
||||
|
||||
namespace BaseGames.Editor.Modules
|
||||
{
|
||||
@@ -81,7 +82,7 @@ namespace BaseGames.Editor.Modules
|
||||
// 名称:优先显示本地化实际文本,回退到 key 本身
|
||||
string nameDisplay = string.IsNullOrEmpty(a.nameKey)
|
||||
? "(未设置)"
|
||||
: (BaseGames.Localization.LocalizationManager.GetEditorPreview(a.nameKey, "Dialogue") ?? a.nameKey);
|
||||
: (BaseGames.Localization.LocalizationManager.GetEditorPreview(a.nameKey, LocalizationTable.Dialogue) ?? a.nameKey);
|
||||
SkillModule.AddChip(card, "名称", nameDisplay);
|
||||
if (!string.IsNullOrEmpty(a.nameKey))
|
||||
SkillModule.AddChip(card, "名称 Key", a.nameKey);
|
||||
|
||||
@@ -6,6 +6,7 @@ using UnityEngine.UIElements;
|
||||
using BaseGames.Dialogue;
|
||||
using BaseGames.Editor.Dialogue;
|
||||
using BaseGames.Editor.Shared;
|
||||
using BaseGames.Localization;
|
||||
|
||||
namespace BaseGames.Editor.Modules
|
||||
{
|
||||
@@ -97,9 +98,9 @@ namespace BaseGames.Editor.Modules
|
||||
};
|
||||
}
|
||||
|
||||
filterRow.Add(QuestModule.MakeFilterChip("有变体", v => { filterVariants = v; RebuildFilter(); }));
|
||||
filterRow.Add(QuestModule.MakeFilterChip("有分支", v => { filterBranches = v; RebuildFilter(); }));
|
||||
filterRow.Add(QuestModule.MakeFilterChip("无语音", v => { filterNoVoice = v; RebuildFilter(); }));
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("有变体", v => { filterVariants = v; RebuildFilter(); }));
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("有分支", v => { filterBranches = v; RebuildFilter(); }));
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("无语音", v => { filterNoVoice = v; RebuildFilter(); }));
|
||||
|
||||
container.Add(_listPane);
|
||||
_listPane.Refresh();
|
||||
@@ -219,7 +220,7 @@ namespace BaseGames.Editor.Modules
|
||||
string speakerKey = line.ResolvedNameKey;
|
||||
if (!string.IsNullOrEmpty(speakerKey))
|
||||
{
|
||||
var speakerResolved = BaseGames.Localization.LocalizationManager.GetEditorPreview(speakerKey, "Dialogue");
|
||||
var speakerResolved = BaseGames.Localization.LocalizationManager.GetEditorPreview(speakerKey, LocalizationTable.Dialogue);
|
||||
bool speakerMissing = speakerResolved == null;
|
||||
string speakerText = speakerMissing ? speakerKey : speakerResolved;
|
||||
var spk = new Label(speakerText + ":");
|
||||
@@ -255,7 +256,7 @@ namespace BaseGames.Editor.Modules
|
||||
}
|
||||
else
|
||||
{
|
||||
var resolved = BaseGames.Localization.LocalizationManager.GetEditorPreview(line.textKey, "Dialogue");
|
||||
var resolved = BaseGames.Localization.LocalizationManager.GetEditorPreview(line.textKey, LocalizationTable.Dialogue);
|
||||
if (resolved != null)
|
||||
{
|
||||
textPreview = resolved;
|
||||
@@ -454,7 +455,7 @@ namespace BaseGames.Editor.Modules
|
||||
string GetLoc(string key)
|
||||
{
|
||||
if (locCache.TryGetValue(key, out var v)) return v;
|
||||
v = BaseGames.Localization.LocalizationManager.GetEditorPreview(key, "Dialogue");
|
||||
v = BaseGames.Localization.LocalizationManager.GetEditorPreview(key, LocalizationTable.Dialogue);
|
||||
locCache[key] = v;
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -74,9 +74,9 @@ namespace BaseGames.Editor.Modules
|
||||
};
|
||||
}
|
||||
|
||||
filterRow.Add(QuestModule.MakeFilterChip("可重复", v => { filterRepeatable = v; RebuildFilter(); }));
|
||||
filterRow.Add(QuestModule.MakeFilterChip("无条件", v => { filterNoCondition = v; RebuildFilter(); }));
|
||||
filterRow.Add(QuestModule.MakeFilterChip("无动作", v => { filterNoAction = v; RebuildFilter(); }));
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("可重复", v => { filterRepeatable = v; RebuildFilter(); }));
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("无条件", v => { filterNoCondition = v; RebuildFilter(); }));
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("无动作", v => { filterNoAction = v; RebuildFilter(); }));
|
||||
|
||||
container.Add(_listPane);
|
||||
_listPane.Refresh();
|
||||
|
||||
11
Assets/_Game/Scripts/Editor/Modules/EventChainModule.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Modules/EventChainModule.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c65f27c4b0792904087283cc4e901118
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -81,8 +81,8 @@ namespace BaseGames.Editor.Modules
|
||||
filterRow.style.paddingBottom = 3;
|
||||
container.Add(filterRow);
|
||||
|
||||
filterRow.Add(QuestModule.MakeFilterChip("仅孤立", v => { _filterOrphan = v; RebuildList(); }));
|
||||
filterRow.Add(QuestModule.MakeFilterChip("仅未注册", v => { _filterUnregistered = v; RebuildList(); }));
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("仅孤立", v => { _filterOrphan = v; RebuildList(); }));
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("仅未注册", v => { _filterUnregistered = v; RebuildList(); }));
|
||||
|
||||
// 列表 ScrollView
|
||||
var scroll = new ScrollView();
|
||||
|
||||
11
Assets/_Game/Scripts/Editor/Modules/FlagAuditModule.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Modules/FlagAuditModule.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de4bd4ec2a39e3c4e89649069cb0f6e2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
11
Assets/_Game/Scripts/Editor/Modules/IdCodegenModule.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Modules/IdCodegenModule.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4cbbfd509dce2f0489febf07574b652a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
572
Assets/_Game/Scripts/Editor/Modules/LocalizationAuditModule.cs
Normal file
572
Assets/_Game/Scripts/Editor/Modules/LocalizationAuditModule.cs
Normal file
@@ -0,0 +1,572 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using BaseGames.Localization;
|
||||
|
||||
namespace BaseGames.Editor.Modules
|
||||
{
|
||||
/// <summary>
|
||||
/// DataHub 本地化审计模块。
|
||||
/// 通过 <see cref="ILocalizableAsset"/> 接口扫描项目中所有 ScriptableObject 的本地化 Key,
|
||||
/// 与 Resources/Localization/ JSON 表比对,列出缺失条目和命名不规范条目。
|
||||
///
|
||||
/// 菜单入口:DataHub → "本地化审计"
|
||||
/// </summary>
|
||||
public class LocalizationAuditModule : IDataModule, IDataModuleOrdered
|
||||
{
|
||||
public string ModuleId => "localization-audit";
|
||||
public string DisplayName => "本地化审计";
|
||||
public string IconName => "d_UnityEditor.InspectorWindow";
|
||||
public int DisplayOrder => 135;
|
||||
|
||||
// Key 命名规范:UPPER_SNAKE_CASE(大写字母、数字、下划线,首字符必须是大写字母)
|
||||
private static readonly Regex s_keyPattern = new(@"^[A-Z][A-Z0-9_]*$", RegexOptions.Compiled);
|
||||
|
||||
// ── 数据 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private readonly List<AuditIssue> _issues = new();
|
||||
private readonly List<NamingIssue> _namingIssues = new();
|
||||
private readonly List<Language> _availableLanguages = new();
|
||||
private int _totalLanguageCount;
|
||||
private bool _hasScanned;
|
||||
|
||||
private class AuditIssue
|
||||
{
|
||||
public string key;
|
||||
public string table;
|
||||
public string soPath;
|
||||
public string fieldName;
|
||||
public UnityEngine.Object asset;
|
||||
public readonly List<string> missingLanguages = new();
|
||||
}
|
||||
|
||||
private class NamingIssue
|
||||
{
|
||||
public string key;
|
||||
public string table;
|
||||
public string soPath;
|
||||
public string fieldName;
|
||||
public UnityEngine.Object asset;
|
||||
}
|
||||
|
||||
// ── UI 引用 ───────────────────────────────────────────────────────────
|
||||
|
||||
private VisualElement _listItems;
|
||||
private Label _summaryLabel;
|
||||
private VisualElement _detailRoot;
|
||||
private VisualElement _namingSection;
|
||||
private bool _filterMissingAll, _filterMissingPartial, _filterNamingIssue;
|
||||
private string _filterTableName = "";
|
||||
|
||||
// ── IDataModule ───────────────────────────────────────────────────────
|
||||
|
||||
public void Initialize() { }
|
||||
|
||||
public void BuildListPane(VisualElement container, Action<UnityEngine.Object> onSelected)
|
||||
{
|
||||
// 扫描 + 导出 按钮行
|
||||
var btnRow = new VisualElement();
|
||||
btnRow.style.flexDirection = FlexDirection.Row;
|
||||
btnRow.style.marginTop = 8;
|
||||
btnRow.style.marginLeft = 8;
|
||||
btnRow.style.marginRight = 8;
|
||||
btnRow.style.marginBottom = 4;
|
||||
container.Add(btnRow);
|
||||
|
||||
var scanBtn = new Button(RunScan) { text = "🔍 扫描本地化缺失" };
|
||||
scanBtn.style.flexGrow = 1;
|
||||
scanBtn.style.marginRight = 4;
|
||||
btnRow.Add(scanBtn);
|
||||
|
||||
var exportBtn = new Button(ExportReport) { text = "📄 导出报告" };
|
||||
exportBtn.style.width = 90;
|
||||
btnRow.Add(exportBtn);
|
||||
|
||||
_summaryLabel = new Label("尚未扫描,点击左侧按钮开始。");
|
||||
_summaryLabel.style.fontSize = 10;
|
||||
_summaryLabel.style.opacity = 0.6f;
|
||||
_summaryLabel.style.paddingLeft = 10;
|
||||
_summaryLabel.style.marginBottom = 4;
|
||||
container.Add(_summaryLabel);
|
||||
|
||||
// 过滤行
|
||||
var filterRow = new VisualElement();
|
||||
filterRow.style.flexDirection = FlexDirection.Row;
|
||||
filterRow.style.flexWrap = Wrap.Wrap;
|
||||
filterRow.style.paddingLeft = 6;
|
||||
filterRow.style.paddingRight = 6;
|
||||
filterRow.style.paddingBottom = 3;
|
||||
container.Add(filterRow);
|
||||
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("全部语言缺失", v => { _filterMissingAll = v; RebuildList(); }));
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("部分语言缺失", v => { _filterMissingPartial = v; RebuildList(); }));
|
||||
// "命名不规范"Chip 现在只控制命名折叠区展开/折叠,不再隐藏缺失列表
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("展开命名问题", v => { _filterNamingIssue = v; RebuildList(); }));
|
||||
|
||||
// 表名过滤输入框
|
||||
var tableField = new TextField("表名过滤") { value = "" };
|
||||
tableField.style.paddingLeft = 6;
|
||||
tableField.style.paddingRight = 6;
|
||||
tableField.style.marginBottom = 3;
|
||||
tableField.RegisterValueChangedCallback(e =>
|
||||
{
|
||||
_filterTableName = e.newValue?.Trim() ?? "";
|
||||
RebuildList();
|
||||
});
|
||||
container.Add(tableField);
|
||||
|
||||
var scroll = new ScrollView { style = { flexGrow = 1 } };
|
||||
container.Add(scroll);
|
||||
|
||||
_listItems = new VisualElement();
|
||||
scroll.Add(_listItems);
|
||||
|
||||
_namingSection = new VisualElement();
|
||||
scroll.Add(_namingSection);
|
||||
}
|
||||
|
||||
public void BuildDetailPane(VisualElement container, UnityEngine.Object selected)
|
||||
{
|
||||
_detailRoot = container;
|
||||
RebuildDetail(null);
|
||||
}
|
||||
|
||||
public void OnActivated() { }
|
||||
|
||||
// ── 扫描 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void RunScan()
|
||||
{
|
||||
_issues.Clear();
|
||||
_namingIssues.Clear();
|
||||
_availableLanguages.Clear();
|
||||
LocalizationManager.ClearEditorPreviewCache();
|
||||
_hasScanned = true;
|
||||
|
||||
DiscoverLanguages();
|
||||
_totalLanguageCount = _availableLanguages.Count;
|
||||
|
||||
ScanAllLocalizableAssets();
|
||||
|
||||
int total = _issues.Count;
|
||||
int misAll = _issues.Count(i => i.missingLanguages.Count == _totalLanguageCount);
|
||||
int naming = _namingIssues.Count;
|
||||
|
||||
_summaryLabel.text = total == 0 && naming == 0
|
||||
? $"✅ 全部通过!已检查 {_totalLanguageCount} 个语言。"
|
||||
: $"⚠ {total} 个缺失问题(全语言缺失 {misAll} 个),{naming} 个命名不规范。";
|
||||
|
||||
RebuildList();
|
||||
}
|
||||
|
||||
// ── 语言发现 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void DiscoverLanguages()
|
||||
{
|
||||
string root = "Assets/Resources/Localization";
|
||||
if (!AssetDatabase.IsValidFolder(root)) return;
|
||||
|
||||
foreach (var langFolder in AssetDatabase.GetSubFolders(root))
|
||||
{
|
||||
string langName = Path.GetFileName(langFolder);
|
||||
if (Enum.TryParse<Language>(langName, out var lang))
|
||||
_availableLanguages.Add(lang);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 通用 ILocalizableAsset 扫描 ───────────────────────────────────────
|
||||
|
||||
private void ScanAllLocalizableAssets()
|
||||
{
|
||||
string[] guids = AssetDatabase.FindAssets("t:ScriptableObject");
|
||||
int total = guids.Length;
|
||||
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
if (EditorUtility.DisplayCancelableProgressBar(
|
||||
"本地化审计", $"扫描中… ({i + 1}/{total})", (float)(i + 1) / total))
|
||||
break;
|
||||
|
||||
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
|
||||
var so = AssetDatabase.LoadAssetAtPath<ScriptableObject>(path);
|
||||
if (so == null || so is not ILocalizableAsset loc) continue;
|
||||
|
||||
foreach (var keyRef in loc.GetLocalizationKeys())
|
||||
{
|
||||
CheckKey(so, keyRef.Key, keyRef.Table, keyRef.FieldName);
|
||||
CheckKeyNamingConvention(so, keyRef.Key, keyRef.Table, keyRef.FieldName, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
}
|
||||
}
|
||||
|
||||
// ── 导出审计报告 ──────────────────────────────────────────────────────
|
||||
|
||||
private void ExportReport()
|
||||
{
|
||||
if (!_hasScanned)
|
||||
{
|
||||
EditorUtility.DisplayDialog("导出报告", "请先执行扫描,再导出报告。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||
string dir = Path.Combine(Application.dataPath, "_Game", "Localization");
|
||||
Directory.CreateDirectory(dir);
|
||||
string filePath = Path.Combine(dir, $"AuditReport_{timestamp}.txt");
|
||||
|
||||
using var sw = new StreamWriter(filePath, false, System.Text.Encoding.UTF8);
|
||||
|
||||
sw.WriteLine("====================================================");
|
||||
sw.WriteLine(" 本地化审计报告");
|
||||
sw.WriteLine($" 生成时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss}");
|
||||
sw.WriteLine("====================================================");
|
||||
sw.WriteLine();
|
||||
sw.WriteLine($"缺失条目:{_issues.Count} 个");
|
||||
sw.WriteLine($"命名不规范:{_namingIssues.Count} 个");
|
||||
sw.WriteLine();
|
||||
|
||||
if (_issues.Count > 0)
|
||||
{
|
||||
sw.WriteLine("── 缺失翻译 ────────────────────────────────────────");
|
||||
foreach (var issue in _issues)
|
||||
{
|
||||
sw.WriteLine($" [{issue.table}] {issue.key}");
|
||||
sw.WriteLine($" 字段:{issue.fieldName}");
|
||||
sw.WriteLine($" 路径:{issue.soPath}");
|
||||
sw.WriteLine($" 缺失语言:{string.Join(", ", issue.missingLanguages)}");
|
||||
sw.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
if (_namingIssues.Count > 0)
|
||||
{
|
||||
sw.WriteLine("── 命名不规范(应为 UPPER_SNAKE_CASE)────────────────");
|
||||
foreach (var ni in _namingIssues)
|
||||
{
|
||||
sw.WriteLine($" [{ni.table}] {ni.key}");
|
||||
sw.WriteLine($" 字段:{ni.fieldName}");
|
||||
sw.WriteLine($" 路径:{ni.soPath}");
|
||||
sw.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
sw.WriteLine("====================================================");
|
||||
sw.Flush();
|
||||
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
string relPath = $"Assets/_Game/Localization/AuditReport_{timestamp}.txt";
|
||||
var asset = AssetDatabase.LoadAssetAtPath<TextAsset>(relPath);
|
||||
if (asset != null) EditorGUIUtility.PingObject(asset);
|
||||
|
||||
EditorUtility.DisplayDialog("导出成功",
|
||||
$"报告已保存至:\n{filePath}", "打开文件夹");
|
||||
EditorUtility.RevealInFinder(filePath);
|
||||
}
|
||||
|
||||
// ── Key 检查 ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
private void CheckKey(UnityEngine.Object asset, string key, string table, string fieldName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key)) return;
|
||||
|
||||
var issue = new AuditIssue
|
||||
{
|
||||
key = key,
|
||||
table = table,
|
||||
fieldName = fieldName,
|
||||
soPath = AssetDatabase.GetAssetPath(asset),
|
||||
asset = asset,
|
||||
};
|
||||
|
||||
foreach (var lang in _availableLanguages)
|
||||
{
|
||||
var dict = LocalizationManager.GetEditorTable(lang, table);
|
||||
if (dict == null || !dict.ContainsKey(key))
|
||||
issue.missingLanguages.Add(lang.ToString());
|
||||
}
|
||||
|
||||
if (issue.missingLanguages.Count > 0)
|
||||
_issues.Add(issue);
|
||||
}
|
||||
|
||||
private void CheckKeyNamingConvention(UnityEngine.Object asset, string key, string table,
|
||||
string fieldName, string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key)) return;
|
||||
if (s_keyPattern.IsMatch(key)) return;
|
||||
|
||||
_namingIssues.Add(new NamingIssue
|
||||
{
|
||||
key = key,
|
||||
table = table,
|
||||
fieldName = fieldName,
|
||||
soPath = path,
|
||||
asset = asset,
|
||||
});
|
||||
}
|
||||
|
||||
// ── 列表 / 详情 UI ────────────────────────────────────────────────────
|
||||
|
||||
private AuditIssue _selectedIssue;
|
||||
|
||||
private void RebuildList()
|
||||
{
|
||||
_listItems.Clear();
|
||||
_namingSection.Clear();
|
||||
if (!_hasScanned) return;
|
||||
|
||||
// ── 缺失问题列表(始终显示,不受命名 Chip 影响)─────────────────
|
||||
var filtered = _issues.AsEnumerable();
|
||||
|
||||
if (_filterMissingAll)
|
||||
filtered = filtered.Where(i => i.missingLanguages.Count == _totalLanguageCount);
|
||||
else if (_filterMissingPartial)
|
||||
filtered = filtered.Where(i => i.missingLanguages.Count > 0 && i.missingLanguages.Count < _totalLanguageCount);
|
||||
|
||||
if (!string.IsNullOrEmpty(_filterTableName))
|
||||
filtered = filtered.Where(i => i.table.IndexOf(_filterTableName, StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
|
||||
var list = filtered.ToList();
|
||||
|
||||
if (list.Count == 0)
|
||||
{
|
||||
_listItems.Add(new Label("无缺失问题或无匹配结果。") { style = { paddingLeft = 10, opacity = 0.5f } });
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var issue in list)
|
||||
{
|
||||
var row = BuildMissingRow(issue);
|
||||
_listItems.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 命名不规范列表(独立折叠区)────────────────────────────────
|
||||
if (_namingIssues.Count > 0)
|
||||
{
|
||||
var foldout = new Foldout
|
||||
{
|
||||
text = $"命名不规范 Key({_namingIssues.Count} 个)",
|
||||
value = _filterNamingIssue,
|
||||
};
|
||||
foldout.style.paddingLeft = 4;
|
||||
_namingSection.Add(foldout);
|
||||
|
||||
var namingFiltered = _namingIssues.AsEnumerable();
|
||||
if (!string.IsNullOrEmpty(_filterTableName))
|
||||
namingFiltered = namingFiltered.Where(i => i.table.IndexOf(_filterTableName, StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
|
||||
foreach (var ni in namingFiltered)
|
||||
{
|
||||
var row = new VisualElement();
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.paddingLeft = 8;
|
||||
row.style.paddingRight = 8;
|
||||
row.style.paddingTop = 3;
|
||||
row.style.paddingBottom = 3;
|
||||
row.style.borderBottomWidth = 1;
|
||||
row.style.borderBottomColor = new StyleColor(new Color(0.3f, 0.3f, 0.3f, 0.4f));
|
||||
row.style.backgroundColor = new StyleColor(new Color(0.2f, 0.2f, 0.45f, 0.25f));
|
||||
|
||||
var capturedNi = ni;
|
||||
row.RegisterCallback<ClickEvent>(_ =>
|
||||
{
|
||||
if (capturedNi.asset != null)
|
||||
{
|
||||
EditorGUIUtility.PingObject(capturedNi.asset);
|
||||
Selection.activeObject = capturedNi.asset;
|
||||
}
|
||||
RebuildNamingDetail(capturedNi);
|
||||
});
|
||||
|
||||
var lbl = new Label($"[{ni.table}] {ni.key} ({ni.fieldName})");
|
||||
lbl.style.flexGrow = 1;
|
||||
lbl.style.fontSize = 11;
|
||||
row.Add(lbl);
|
||||
|
||||
var hint = new Label("应为 UPPER_SNAKE_CASE");
|
||||
hint.style.fontSize = 10;
|
||||
hint.style.opacity = 0.6f;
|
||||
row.Add(hint);
|
||||
|
||||
foldout.Add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private VisualElement BuildMissingRow(AuditIssue issue)
|
||||
{
|
||||
var row = new VisualElement();
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.paddingLeft = 8;
|
||||
row.style.paddingRight = 8;
|
||||
row.style.paddingTop = 4;
|
||||
row.style.paddingBottom = 4;
|
||||
row.style.borderBottomWidth = 1;
|
||||
row.style.borderBottomColor = new StyleColor(new Color(0.3f, 0.3f, 0.3f, 0.4f));
|
||||
|
||||
var captured = issue;
|
||||
row.RegisterCallback<ClickEvent>(_ =>
|
||||
{
|
||||
_selectedIssue = captured;
|
||||
RebuildDetail(captured);
|
||||
if (captured.asset != null)
|
||||
EditorGUIUtility.PingObject(captured.asset);
|
||||
});
|
||||
|
||||
bool allMissing = issue.missingLanguages.Count == _totalLanguageCount;
|
||||
row.style.backgroundColor = new StyleColor(allMissing
|
||||
? new Color(0.45f, 0.15f, 0.05f, 0.35f)
|
||||
: new Color(0.40f, 0.35f, 0.00f, 0.25f));
|
||||
|
||||
var left = new Label($"[{issue.table}] {issue.key}");
|
||||
left.style.flexGrow = 1;
|
||||
left.style.fontSize = 11;
|
||||
left.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
row.Add(left);
|
||||
|
||||
var right = new Label(string.Join(", ", issue.missingLanguages));
|
||||
right.style.fontSize = 10;
|
||||
right.style.opacity = 0.7f;
|
||||
row.Add(right);
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
private void RebuildDetail(AuditIssue issue)
|
||||
{
|
||||
if (_detailRoot == null) return;
|
||||
_detailRoot.Clear();
|
||||
|
||||
if (issue == null)
|
||||
{
|
||||
_detailRoot.Add(new Label("← 选择左侧条目查看详情。") { style = { paddingLeft = 16, paddingTop = 16, opacity = 0.5f } });
|
||||
return;
|
||||
}
|
||||
|
||||
var title = new Label($"Key:{issue.key}");
|
||||
title.style.fontSize = 14;
|
||||
title.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
title.style.paddingLeft = 12;
|
||||
title.style.paddingTop = 10;
|
||||
title.style.paddingBottom = 6;
|
||||
_detailRoot.Add(title);
|
||||
|
||||
AddDetailRow(_detailRoot, "表名", issue.table);
|
||||
AddDetailRow(_detailRoot, "字段", issue.fieldName);
|
||||
AddDetailRow(_detailRoot, "资产路径", issue.soPath);
|
||||
|
||||
_detailRoot.Add(new Label("缺失语言:") { style = { paddingLeft = 12, paddingTop = 8, fontSize = 11, unityFontStyleAndWeight = FontStyle.Bold } });
|
||||
foreach (var lang in issue.missingLanguages)
|
||||
_detailRoot.Add(new Label($" • {lang}") { style = { paddingLeft = 20, fontSize = 11 } });
|
||||
|
||||
_detailRoot.Add(new VisualElement { style = { height = 8 } });
|
||||
var btnRow = new VisualElement { style = { flexDirection = FlexDirection.Row, paddingLeft = 10, paddingRight = 10 } };
|
||||
_detailRoot.Add(btnRow);
|
||||
|
||||
var pingBtn = new Button(() =>
|
||||
{
|
||||
if (issue.asset != null)
|
||||
{
|
||||
EditorGUIUtility.PingObject(issue.asset);
|
||||
Selection.activeObject = issue.asset;
|
||||
}
|
||||
}) { text = "定位 SO 资产", style = { flexGrow = 1, marginRight = 4 } };
|
||||
btnRow.Add(pingBtn);
|
||||
|
||||
var copyBtn = new Button(() =>
|
||||
{
|
||||
EditorGUIUtility.systemCopyBuffer = issue.key;
|
||||
Debug.Log($"[LocalizationAudit] 已复制 Key:{issue.key}");
|
||||
}) { text = "复制 Key", style = { flexGrow = 1 } };
|
||||
btnRow.Add(copyBtn);
|
||||
|
||||
foreach (var lang in issue.missingLanguages)
|
||||
{
|
||||
var capturedLang = lang;
|
||||
var openBtn = new Button(() => PingTableFile(capturedLang, issue.table))
|
||||
{
|
||||
text = $"打开 {lang}/{issue.table}.json",
|
||||
style = { marginTop = 4, marginLeft = 10, marginRight = 10 },
|
||||
};
|
||||
_detailRoot.Add(openBtn);
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildNamingDetail(NamingIssue ni)
|
||||
{
|
||||
if (_detailRoot == null) return;
|
||||
_detailRoot.Clear();
|
||||
|
||||
var title = new Label($"命名不规范:{ni.key}");
|
||||
title.style.fontSize = 14;
|
||||
title.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
title.style.paddingLeft = 12;
|
||||
title.style.paddingTop = 10;
|
||||
title.style.paddingBottom = 6;
|
||||
_detailRoot.Add(title);
|
||||
|
||||
AddDetailRow(_detailRoot, "表名", ni.table);
|
||||
AddDetailRow(_detailRoot, "字段", ni.fieldName);
|
||||
AddDetailRow(_detailRoot, "资产路径", ni.soPath);
|
||||
|
||||
_detailRoot.Add(new Label("规范要求:UPPER_SNAKE_CASE(如 QUEST_FIND_HERB_NAME)")
|
||||
{ style = { paddingLeft = 12, paddingTop = 8, fontSize = 11, opacity = 0.7f } });
|
||||
|
||||
_detailRoot.Add(new VisualElement { style = { height = 8 } });
|
||||
var pingBtn = new Button(() =>
|
||||
{
|
||||
if (ni.asset != null)
|
||||
{
|
||||
EditorGUIUtility.PingObject(ni.asset);
|
||||
Selection.activeObject = ni.asset;
|
||||
}
|
||||
}) { text = "定位 SO 资产", style = { marginLeft = 10, marginRight = 10 } };
|
||||
_detailRoot.Add(pingBtn);
|
||||
|
||||
var copyBtn = new Button(() =>
|
||||
{
|
||||
EditorGUIUtility.systemCopyBuffer = ni.key;
|
||||
Debug.Log($"[LocalizationAudit] 已复制 Key:{ni.key}");
|
||||
}) { text = "复制 Key", style = { marginLeft = 10, marginRight = 10, marginTop = 4 } };
|
||||
_detailRoot.Add(copyBtn);
|
||||
}
|
||||
|
||||
private static void AddDetailRow(VisualElement parent, string label, string value)
|
||||
{
|
||||
var row = new VisualElement { style = { flexDirection = FlexDirection.Row, paddingLeft = 12, paddingTop = 2, paddingBottom = 2 } };
|
||||
row.Add(new Label(label + ":") { style = { width = 80, opacity = 0.6f, fontSize = 11 } });
|
||||
row.Add(new Label(value) { style = { flexGrow = 1, fontSize = 11 } });
|
||||
parent.Add(row);
|
||||
}
|
||||
|
||||
private static void PingTableFile(string language, string tableName)
|
||||
{
|
||||
string path = $"Assets/Resources/Localization/{language}/{tableName}.json";
|
||||
var asset = AssetDatabase.LoadAssetAtPath<TextAsset>(path);
|
||||
if (asset != null)
|
||||
{
|
||||
EditorGUIUtility.PingObject(asset);
|
||||
Selection.activeObject = asset;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[LocalizationAudit] 未找到表文件:{path}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b311cc7d8950e35458e44e0523e23ef0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -6,6 +6,7 @@ using UnityEngine.UIElements;
|
||||
using BaseGames.Dialogue;
|
||||
using BaseGames.Quest;
|
||||
using BaseGames.Editor.Shared;
|
||||
using BaseGames.Localization;
|
||||
|
||||
namespace BaseGames.Editor.Modules
|
||||
{
|
||||
@@ -70,8 +71,8 @@ namespace BaseGames.Editor.Modules
|
||||
};
|
||||
}
|
||||
|
||||
filterRow.Add(QuestModule.MakeFilterChip("有好感度", v => { filterAffinity = v; RebuildFilter(); }));
|
||||
filterRow.Add(QuestModule.MakeFilterChip("有头像", v => { filterPortrait = v; RebuildFilter(); }));
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("有好感度", v => { filterAffinity = v; RebuildFilter(); }));
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("有头像", v => { filterPortrait = v; RebuildFilter(); }));
|
||||
|
||||
container.Add(_listPane);
|
||||
_listPane.Refresh();
|
||||
@@ -114,7 +115,7 @@ namespace BaseGames.Editor.Modules
|
||||
|
||||
string nameDisplay = string.IsNullOrEmpty(n.nameKey)
|
||||
? "(未设置)"
|
||||
: (BaseGames.Localization.LocalizationManager.GetEditorPreview(n.nameKey, "Dialogue") ?? n.nameKey);
|
||||
: (BaseGames.Localization.LocalizationManager.GetEditorPreview(n.nameKey, LocalizationTable.Dialogue) ?? n.nameKey);
|
||||
SkillModule.AddChip(card, "名称", nameDisplay);
|
||||
if (!string.IsNullOrEmpty(n.nameKey))
|
||||
SkillModule.AddChip(card, "名称 Key", n.nameKey);
|
||||
@@ -263,7 +264,7 @@ namespace BaseGames.Editor.Modules
|
||||
string GetLoc(string key)
|
||||
{
|
||||
if (locCache.TryGetValue(key, out var v)) return v;
|
||||
v = BaseGames.Localization.LocalizationManager.GetEditorPreview(key, "Dialogue");
|
||||
v = BaseGames.Localization.LocalizationManager.GetEditorPreview(key, LocalizationTable.Dialogue);
|
||||
locCache[key] = v;
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using BaseGames.Quest;
|
||||
using BaseGames.Editor.Shared;
|
||||
using BaseGames.Localization;
|
||||
|
||||
namespace BaseGames.Editor.Modules
|
||||
{
|
||||
@@ -32,7 +33,7 @@ namespace BaseGames.Editor.Modules
|
||||
private System.Action<UnityEditor.PlayModeStateChange> _playModeHandler;
|
||||
|
||||
// 依赖关系图中 FindAll<QuestSO>() 的静态缓存,同一编辑器会话内复用,避免重复扫描磁盘
|
||||
private static QuestSO[] s_allQuestCache;
|
||||
private static List<QuestSO> s_allQuestCache;
|
||||
private static double s_allQuestCacheTime;
|
||||
private const double k_AllQuestCacheTtl = 5.0; // 秒;超时后下次打开 foldout 时刷新
|
||||
|
||||
@@ -96,62 +97,26 @@ namespace BaseGames.Editor.Modules
|
||||
};
|
||||
}
|
||||
|
||||
filterRow.Add(MakeFilterChip("主线", v => { filterCategory = v ? QuestCategory.Main : (QuestCategory?)null; RebuildFilter(); }));
|
||||
filterRow.Add(MakeFilterChip("支线", v => { filterCategory = v ? QuestCategory.Side : (QuestCategory?)null; RebuildFilter(); }));
|
||||
filterRow.Add(MakeFilterChip("日常", v => { filterCategory = v ? QuestCategory.Daily : (QuestCategory?)null; RebuildFilter(); }));
|
||||
filterRow.Add(MakeFilterChip("隐藏", v => { filterCategory = v ? QuestCategory.Hidden : (QuestCategory?)null; RebuildFilter(); }));
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("主线", v => { filterCategory = v ? QuestCategory.Main : (QuestCategory?)null; RebuildFilter(); }));
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("支线", v => { filterCategory = v ? QuestCategory.Side : (QuestCategory?)null; RebuildFilter(); }));
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("日常", v => { filterCategory = v ? QuestCategory.Daily : (QuestCategory?)null; RebuildFilter(); }));
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("隐藏", v => { filterCategory = v ? QuestCategory.Hidden : (QuestCategory?)null; RebuildFilter(); }));
|
||||
// 分隔
|
||||
var sep = new Label("|");
|
||||
sep.style.opacity = 0.3f;
|
||||
sep.style.marginLeft = 2;
|
||||
sep.style.marginRight = 2;
|
||||
filterRow.Add(sep);
|
||||
filterRow.Add(MakeFilterChip("有前置", v => { filterPrereq = v; RebuildFilter(); }));
|
||||
filterRow.Add(MakeFilterChip("无目标", v => { filterNoObj = v; RebuildFilter(); }));
|
||||
filterRow.Add(MakeFilterChip("可失败", v => { filterCanFail = v; RebuildFilter(); }));
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("有前置", v => { filterPrereq = v; RebuildFilter(); }));
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("无目标", v => { filterNoObj = v; RebuildFilter(); }));
|
||||
filterRow.Add(DataHubEditorKit.MakeFilterChip("可失败", v => { filterCanFail = v; RebuildFilter(); }));
|
||||
|
||||
container.Add(_listPane);
|
||||
_listPane.Refresh();
|
||||
}
|
||||
|
||||
internal static VisualElement MakeFilterChip(string label, System.Action<bool> onToggle)
|
||||
{
|
||||
bool active = false;
|
||||
var chip = new Label(label);
|
||||
chip.style.fontSize = 10;
|
||||
chip.style.paddingLeft = 6;
|
||||
chip.style.paddingRight = 6;
|
||||
chip.style.paddingTop = 2;
|
||||
chip.style.paddingBottom = 2;
|
||||
chip.style.marginRight = 4;
|
||||
chip.style.marginBottom = 2;
|
||||
chip.style.borderTopLeftRadius = 8;
|
||||
chip.style.borderTopRightRadius = 8;
|
||||
chip.style.borderBottomLeftRadius = 8;
|
||||
chip.style.borderBottomRightRadius = 8;
|
||||
chip.style.borderTopWidth = 1;
|
||||
chip.style.borderRightWidth = 1;
|
||||
chip.style.borderBottomWidth = 1;
|
||||
chip.style.borderLeftWidth = 1;
|
||||
chip.style.borderTopColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.4f));
|
||||
chip.style.borderRightColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.4f));
|
||||
chip.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.4f));
|
||||
chip.style.borderLeftColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.4f));
|
||||
chip.style.opacity = 0.6f;
|
||||
|
||||
void SetActive(bool on)
|
||||
{
|
||||
active = on;
|
||||
chip.style.opacity = on ? 1f : 0.6f;
|
||||
chip.style.backgroundColor = on
|
||||
? new StyleColor(new Color(0.3f, 0.6f, 1f, 0.25f))
|
||||
: StyleKeyword.None;
|
||||
onToggle(on);
|
||||
}
|
||||
|
||||
chip.RegisterCallback<ClickEvent>(_ => SetActive(!active));
|
||||
return chip;
|
||||
}
|
||||
=> DataHubEditorKit.MakeFilterChip(label, onToggle);
|
||||
|
||||
public void BuildDetailPane(VisualElement container, UnityEngine.Object selected)
|
||||
{
|
||||
@@ -201,7 +166,7 @@ namespace BaseGames.Editor.Modules
|
||||
}
|
||||
else
|
||||
{
|
||||
var resolved = BaseGames.Localization.LocalizationManager.GetEditorPreview(s.displayNameKey, "Quest");
|
||||
var resolved = BaseGames.Localization.LocalizationManager.GetEditorPreview(s.displayNameKey, LocalizationTable.Quest);
|
||||
nameDisplay = resolved != null ? resolved : s.displayNameKey + " ⚠ [缺少本地化]";
|
||||
}
|
||||
SkillModule.AddChip(card, "名称", nameDisplay);
|
||||
@@ -343,7 +308,7 @@ namespace BaseGames.Editor.Modules
|
||||
// 目标描述(本地化预览,灰色小字,显示策划填写的实际内容)
|
||||
if (!string.IsNullOrEmpty(obj.displayTextKey))
|
||||
{
|
||||
var resolved = BaseGames.Localization.LocalizationManager.GetEditorPreview(obj.displayTextKey, "Quest");
|
||||
var resolved = BaseGames.Localization.LocalizationManager.GetEditorPreview(obj.displayTextKey, LocalizationTable.Quest);
|
||||
bool l10nMissing = resolved == null;
|
||||
string descText = l10nMissing ? obj.displayTextKey + " ⚠ [缺少本地化]" : resolved;
|
||||
var desc = new Label(descText);
|
||||
|
||||
@@ -55,16 +55,19 @@ namespace BaseGames.Editor.Modules
|
||||
|
||||
if (_selected == null) return;
|
||||
|
||||
// Stats Card
|
||||
// Stats Card(复用 SkillModule 共享构建方法)
|
||||
var statsCard = BuildStatsCard(_selected);
|
||||
container.Add(statsCard);
|
||||
|
||||
// 操作按钮行
|
||||
var toolbar = BuildActionBar(_selected);
|
||||
container.Add(toolbar);
|
||||
// 操作按钮行(复用 BuildStandardActionBar,统一按钮样式)
|
||||
container.Add(SkillModule.BuildStandardActionBar<WeaponSO>(
|
||||
_selected, Folder, Prefix,
|
||||
created => _listPane.Refresh(created),
|
||||
cloned => _listPane.Refresh(cloned),
|
||||
() => _listPane.Refresh(null)));
|
||||
|
||||
// 分隔线
|
||||
container.Add(MakeDivider());
|
||||
container.Add(SkillModule.MakeDivider());
|
||||
|
||||
// Inspector
|
||||
var insp = new InspectorElement(_selected); container.Add(insp);
|
||||
@@ -92,92 +95,12 @@ namespace BaseGames.Editor.Modules
|
||||
|
||||
private static VisualElement BuildStatsCard(WeaponSO w)
|
||||
{
|
||||
var card = new VisualElement();
|
||||
card.style.flexDirection = FlexDirection.Row;
|
||||
card.style.flexWrap = Wrap.Wrap;
|
||||
card.style.paddingLeft = 12;
|
||||
card.style.paddingRight = 12;
|
||||
card.style.paddingTop = 8;
|
||||
card.style.paddingBottom = 8;
|
||||
card.style.marginBottom = 4;
|
||||
card.style.backgroundColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.08f));
|
||||
card.style.borderBottomWidth = 1;
|
||||
card.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.2f));
|
||||
|
||||
AddStatChip(card, "类型", w.weaponType.ToString());
|
||||
AddStatChip(card, "地面段数", (w.groundComboSteps?.Length ?? 0).ToString());
|
||||
AddStatChip(card, "空中段数", (w.airComboSteps?.Length ?? 0).ToString());
|
||||
AddStatChip(card, "ID", string.IsNullOrEmpty(w.weaponId) ? "-" : w.weaponId);
|
||||
var card = SkillModule.MakeCard();
|
||||
SkillModule.AddChip(card, "类型", w.weaponType.ToString());
|
||||
SkillModule.AddChip(card, "地面段数", (w.groundComboSteps?.Length ?? 0).ToString());
|
||||
SkillModule.AddChip(card, "空中段数", (w.airComboSteps?.Length ?? 0).ToString());
|
||||
SkillModule.AddChip(card, "ID", string.IsNullOrEmpty(w.weaponId) ? "-" : w.weaponId);
|
||||
return card;
|
||||
}
|
||||
|
||||
private static void AddStatChip(VisualElement parent, string label, string value)
|
||||
{
|
||||
var chip = new VisualElement();
|
||||
chip.style.flexDirection = FlexDirection.Row;
|
||||
chip.style.alignItems = Align.Center;
|
||||
chip.style.marginRight = 14;
|
||||
chip.style.marginBottom = 2;
|
||||
|
||||
var lbl = new Label(label + ":");
|
||||
lbl.style.opacity = 0.6f;
|
||||
lbl.style.fontSize = 11;
|
||||
lbl.style.marginRight = 3;
|
||||
chip.Add(lbl);
|
||||
|
||||
var val = new Label(value);
|
||||
val.style.fontSize = 11;
|
||||
val.style.unityFontStyleAndWeight = UnityEngine.FontStyle.Bold;
|
||||
chip.Add(val);
|
||||
|
||||
parent.Add(chip);
|
||||
}
|
||||
|
||||
private VisualElement BuildActionBar(WeaponSO w)
|
||||
{
|
||||
var bar = new VisualElement();
|
||||
bar.style.flexDirection = FlexDirection.Row;
|
||||
bar.style.paddingLeft = 12;
|
||||
bar.style.paddingRight = 12;
|
||||
bar.style.paddingTop = 6;
|
||||
bar.style.paddingBottom = 6;
|
||||
bar.style.flexWrap = Wrap.Wrap;
|
||||
|
||||
var btnPing = new Button(() => { EditorGUIUtility.PingObject(w); Selection.activeObject = w; })
|
||||
{ text = "在 Project 中定位", tooltip = "在 Project 窗口高亮此资产" };
|
||||
bar.Add(btnPing);
|
||||
|
||||
var btnClone = new Button(() =>
|
||||
{
|
||||
var clone = AssetOperations.Clone(w, Folder);
|
||||
if (clone != null) _listPane.Refresh(clone);
|
||||
}) { text = "克隆..." };
|
||||
bar.Add(btnClone);
|
||||
|
||||
var btnDel = new Button(() =>
|
||||
{
|
||||
if (AssetOperations.Delete(w)) _listPane.Refresh(null);
|
||||
}) { text = "删除" };
|
||||
btnDel.style.borderLeftColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
|
||||
btnDel.style.borderRightColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
|
||||
btnDel.style.borderTopColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
|
||||
btnDel.style.borderBottomColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
|
||||
btnDel.style.borderLeftWidth = 1;
|
||||
btnDel.style.borderRightWidth = 1;
|
||||
btnDel.style.borderTopWidth = 1;
|
||||
btnDel.style.borderBottomWidth = 1;
|
||||
btnDel.style.marginLeft = 8;
|
||||
bar.Add(btnDel);
|
||||
|
||||
return bar;
|
||||
}
|
||||
|
||||
private static VisualElement MakeDivider()
|
||||
{
|
||||
var d = new VisualElement();
|
||||
d.style.height = 1;
|
||||
d.style.backgroundColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.2f));
|
||||
return d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 463effce1dd1fc648aea0ed635a9e7a6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
11
Assets/_Game/Scripts/Editor/Quest/QuestSOEditor.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Quest/QuestSOEditor.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 363d113e10945e240bf0260221ae2aff
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 472cbbc6ac2cacd46947af5235b8e47b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
56
Assets/_Game/Scripts/Editor/Shared/DataHubEditorKit.cs
Normal file
56
Assets/_Game/Scripts/Editor/Shared/DataHubEditorKit.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// DataHub 模块通用 UI 工具集。提供跨模块复用的 UIElements 控件工厂方法。
|
||||
/// </summary>
|
||||
public static class DataHubEditorKit
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建可切换的过滤标签按钮(圆角 chip 样式)。
|
||||
/// 点击后切换激活/非激活状态,并触发 <paramref name="onToggle"/> 回调。
|
||||
/// </summary>
|
||||
public static VisualElement MakeFilterChip(string label, Action<bool> onToggle)
|
||||
{
|
||||
bool active = false;
|
||||
var chip = new Label(label);
|
||||
chip.style.fontSize = 10;
|
||||
chip.style.paddingLeft = 6;
|
||||
chip.style.paddingRight = 6;
|
||||
chip.style.paddingTop = 2;
|
||||
chip.style.paddingBottom = 2;
|
||||
chip.style.marginRight = 4;
|
||||
chip.style.marginBottom = 2;
|
||||
chip.style.borderTopLeftRadius = 8;
|
||||
chip.style.borderTopRightRadius = 8;
|
||||
chip.style.borderBottomLeftRadius = 8;
|
||||
chip.style.borderBottomRightRadius = 8;
|
||||
chip.style.borderTopWidth = 1;
|
||||
chip.style.borderRightWidth = 1;
|
||||
chip.style.borderBottomWidth = 1;
|
||||
chip.style.borderLeftWidth = 1;
|
||||
var borderColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.4f));
|
||||
chip.style.borderTopColor = borderColor;
|
||||
chip.style.borderRightColor = borderColor;
|
||||
chip.style.borderBottomColor = borderColor;
|
||||
chip.style.borderLeftColor = borderColor;
|
||||
chip.style.opacity = 0.6f;
|
||||
|
||||
void SetActive(bool on)
|
||||
{
|
||||
active = on;
|
||||
chip.style.opacity = on ? 1f : 0.6f;
|
||||
chip.style.backgroundColor = on
|
||||
? new StyleColor(new Color(0.3f, 0.6f, 1f, 0.25f))
|
||||
: StyleKeyword.None;
|
||||
onToggle(on);
|
||||
}
|
||||
|
||||
chip.RegisterCallback<ClickEvent>(_ => SetActive(!active));
|
||||
return chip;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/Shared/DataHubEditorKit.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Shared/DataHubEditorKit.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 666eee8c08676294db8404d0eb3409f2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -247,7 +247,6 @@ namespace BaseGames.Editor
|
||||
if (_extraFilter != null && !_extraFilter(item)) continue;
|
||||
_filtered.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
_listView.RefreshItems();
|
||||
_countLabel.text = _all.Count == _filtered.Count
|
||||
|
||||
8
Assets/_Game/Scripts/Editor/UI.meta
Normal file
8
Assets/_Game/Scripts/Editor/UI.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f33c1c7220eea6d45a73820db7b94e35
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
121
Assets/_Game/Scripts/Editor/UI/UIManagerEditor.cs
Normal file
121
Assets/_Game/Scripts/Editor/UI/UIManagerEditor.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using BaseGames.UI;
|
||||
|
||||
namespace BaseGames.Editor.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// UIManager 自定义 Inspector。
|
||||
///
|
||||
/// 功能:
|
||||
/// 1. 绘制默认 Inspector(原有字段布局不变)
|
||||
/// 2. 在面板注册表下方实时显示验证状态:
|
||||
/// · 红色错误:PanelId 重复注册
|
||||
/// · 黄色警告:GameObject 引用为 null
|
||||
/// · 绿色信息:全部验证通过
|
||||
/// 3. 提供一键测试按钮(仅 Play Mode 有效)
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(UIManager))]
|
||||
public class UIManagerEditor : UnityEditor.Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
DrawDefaultInspector();
|
||||
|
||||
EditorGUILayout.Space(6);
|
||||
EditorGUILayout.LabelField("── 注册表验证 ──────────────────────", EditorStyles.boldLabel);
|
||||
|
||||
var panelsProp = serializedObject.FindProperty("_panels");
|
||||
if (panelsProp == null || !panelsProp.isArray || panelsProp.arraySize == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox("面板注册表为空,请在 Inspector 中添加面板绑定。", MessageType.Warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
var seenIds = new HashSet<int>();
|
||||
bool hasError = false;
|
||||
bool hasWarning = false;
|
||||
|
||||
for (int i = 0; i < panelsProp.arraySize; i++)
|
||||
{
|
||||
var elem = panelsProp.GetArrayElementAtIndex(i);
|
||||
var idProp = elem.FindPropertyRelative("id");
|
||||
var rootProp = elem.FindPropertyRelative("root");
|
||||
|
||||
string idName = idProp != null
|
||||
? idProp.enumDisplayNames[idProp.enumValueIndex]
|
||||
: $"[{i}]";
|
||||
|
||||
if (rootProp != null && rootProp.objectReferenceValue == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox($"[{i}] PanelId.{idName} 的 GameObject 引用为 null!", MessageType.Warning);
|
||||
hasWarning = true;
|
||||
}
|
||||
|
||||
if (idProp != null && !seenIds.Add(idProp.enumValueIndex))
|
||||
{
|
||||
EditorGUILayout.HelpBox($"[{i}] PanelId.{idName} 重复注册!同一 ID 只允许绑定一个 GameObject。", MessageType.Error);
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasError && !hasWarning)
|
||||
EditorGUILayout.HelpBox($"注册表验证通过 ✓ 共 {panelsProp.arraySize} 个面板", MessageType.Info);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
EditorGUILayout.LabelField("── 运行期快速测试(需 Play Mode)─────", EditorStyles.boldLabel);
|
||||
|
||||
using (new EditorGUI.DisabledScope(!Application.isPlaying))
|
||||
{
|
||||
var manager = (UIManager)target;
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("打开 Pause")) manager.OpenPanel(PanelId.Pause);
|
||||
if (GUILayout.Button("打开 Map")) manager.OpenPanel(PanelId.Map);
|
||||
if (GUILayout.Button("打开 Shop")) manager.OpenPanel(PanelId.Shop);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("打开 CharmPanel")) manager.OpenPanel(PanelId.CharmPanel);
|
||||
if (GUILayout.Button("打开 Settings")) manager.OpenPanel(PanelId.Settings);
|
||||
if (GUILayout.Button("关闭栈顶面板")) manager.CloseTopPanel();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
// ── 面板栈实时可视化 ───────────────────────────────────────────
|
||||
EditorGUILayout.Space(4);
|
||||
EditorGUILayout.LabelField("── 面板栈(栊顶 = 第一行)───────────────────", EditorStyles.boldLabel);
|
||||
|
||||
var snapshot = manager.EditorGetPanelSnapshot();
|
||||
if (snapshot == null || snapshot.Length == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox("面板栈为空", MessageType.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < snapshot.Length; i++)
|
||||
{
|
||||
var go = snapshot[i];
|
||||
string label = go != null ? go.name : "(null)";
|
||||
if (i == 0)
|
||||
{
|
||||
// 栈顶面板加粗显示
|
||||
var style = new GUIStyle(EditorStyles.label)
|
||||
{
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
EditorGUILayout.LabelField($"▶ [{i}] {label} (栈顶)", style);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.LabelField($" [{i}] {label}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool RequiresConstantRepaint() => Application.isPlaying;
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/UI/UIManagerEditor.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/UI/UIManagerEditor.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95f2af7feffeab44799a4127da2c7f8f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user