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

@@ -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);

View File

@@ -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;
}

View File

@@ -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();

View File

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

View File

@@ -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();

View File

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

View File

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

View 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}");
}
}
}
}

View File

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

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;
}
}
}