diff --git a/Scripts/Editor.meta b/Scripts/Editor.meta
new file mode 100644
index 0000000..d51de57
--- /dev/null
+++ b/Scripts/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5706044f5f8a1d9469a0d192ad6a6da8
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/Design_Tools.meta b/Scripts/Editor/Design_Tools.meta
new file mode 100644
index 0000000..60f6149
--- /dev/null
+++ b/Scripts/Editor/Design_Tools.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5700364c75144e9418afa8c01c54ec6d
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/Design_Tools/Collections.meta b/Scripts/Editor/Design_Tools/Collections.meta
new file mode 100644
index 0000000..af78fa1
--- /dev/null
+++ b/Scripts/Editor/Design_Tools/Collections.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 878d1e2621294df439f0cf7a06d20f32
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/Design_Tools/Collections/EmojiConfigEditor.cs b/Scripts/Editor/Design_Tools/Collections/EmojiConfigEditor.cs
new file mode 100644
index 0000000..a9282b3
--- /dev/null
+++ b/Scripts/Editor/Design_Tools/Collections/EmojiConfigEditor.cs
@@ -0,0 +1,937 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+using OfficeOpenXml;
+using ArtResource;
+using Debug = UnityEngine.Debug;
+
+namespace DesignTools.Collections
+{
+ ///
+ /// 表情配置Editor工具
+ /// 读取Docs/config/Emoji.xlsx,关联Art_SO/Collections/EmojiResource.asset
+ ///
+ public class EmojiConfigEditor : EditorWindow
+ {
+ private const string EMOJI_SO_PATH = "Assets/Art_SubModule/Art_SO/Collections";
+ private const string EMOJI_SO_NAME = "EmojiResource";
+ private const string EMOJI_EXCEL_NAME = "Emoji.xlsx";
+ private const string LANGUAGE_EXCEL_NAME = "AllLanguage.xlsx";
+ private const string EMOJI_SHEET_NAME = "Emoji";
+ private const string LANGUAGE_SHEET_NAME = "client";
+ private const string DOCS_PATH_PREF_KEY = "EmojiConfigEditor_DocsPath";
+ private const string DESIGN_SUBMODULE_PATH = "Assets/Design_SubModule";
+
+ private string docsRootPath = "";
+ private List emojiDataList = new List();
+ private ArtTableSO emojiTableSO;
+ private Dictionary> languageDict = new Dictionary>();
+ private Vector2 scrollPosition;
+ private bool isDataLoaded = false;
+ private string pendingTooltipText = "";
+
+ [MenuItem("策划工具/收藏品/表情")]
+ public static void ShowWindow()
+ {
+ var window = GetWindow("表情配置");
+ window.minSize = new Vector2(1000, 600);
+ window.Show();
+ }
+
+ private void OnEnable()
+ {
+ // 读取上次保存的路径
+ if (EditorPrefs.HasKey(DOCS_PATH_PREF_KEY))
+ {
+ docsRootPath = EditorPrefs.GetString(DOCS_PATH_PREF_KEY);
+ }
+ }
+
+ private void OnGUI()
+ {
+ EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+ EditorGUILayout.LabelField("表情配置工具", EditorStyles.boldLabel);
+ GUILayout.FlexibleSpace();
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.Space(5);
+
+ // Docs路径选择
+ EditorGUILayout.BeginVertical("box");
+ EditorGUILayout.LabelField("Docs项目根目录", EditorStyles.boldLabel);
+
+ EditorGUILayout.BeginHorizontal();
+ docsRootPath = EditorGUILayout.TextField("路径", docsRootPath);
+ if (GUILayout.Button("选择文件夹", GUILayout.Width(100)))
+ {
+ string selectedPath = EditorUtility.OpenFolderPanel("选择Docs项目根目录", "", "");
+ if (!string.IsNullOrEmpty(selectedPath))
+ {
+ docsRootPath = selectedPath;
+ EditorPrefs.SetString(DOCS_PATH_PREF_KEY, docsRootPath);
+ }
+ }
+ EditorGUILayout.EndHorizontal();
+
+ if (GUILayout.Button("加载配置数据", GUILayout.Height(30)))
+ {
+ LoadData();
+ }
+
+ EditorGUILayout.EndVertical();
+
+ EditorGUILayout.Space(5);
+
+ // 数据编辑区域
+ if (isDataLoaded)
+ {
+ DrawDataEditor();
+ }
+ else
+ {
+ EditorGUILayout.HelpBox("请先选择Docs根目录并加载配置数据", MessageType.Info);
+ }
+ }
+
+ ///
+ /// 加载配置数据
+ ///
+ private void LoadData()
+ {
+ try
+ {
+ // 校验路径
+ if (string.IsNullOrEmpty(docsRootPath) || !Directory.Exists(docsRootPath))
+ {
+ EditorUtility.DisplayDialog("错误", "请选择有效的Docs根目录", "确定");
+ return;
+ }
+
+ // 1. 检查Docs是否为Git仓库并更新
+ if (!CheckAndUpdateDocsRepository())
+ {
+ return;
+ }
+
+ // 2. 检查Design_SubModule分支
+ if (!CheckAndSwitchDesignSubModuleBranch())
+ {
+ return;
+ }
+
+ // 校验Emoji SO是否存在
+ string emojiSOPath = Path.Combine(EMOJI_SO_PATH, $"{EMOJI_SO_NAME}.asset");
+ emojiTableSO = AssetDatabase.LoadAssetAtPath(emojiSOPath);
+
+ if (emojiTableSO == null)
+ {
+ EditorUtility.DisplayDialog("错误",
+ $"未找到表情资源配置\n路径: {emojiSOPath}\n\n请先在美术资源配置工具中创建",
+ "确定");
+ return;
+ }
+
+ if (emojiTableSO.Items == null || emojiTableSO.Items.Count == 0)
+ {
+ EditorUtility.DisplayDialog("错误", "表情资源配置数据为空", "确定");
+ return;
+ }
+
+ // 加载语言表
+ LoadLanguageData();
+
+ // 加载Emoji.xlsx
+ LoadEmojiExcel();
+
+ isDataLoaded = true;
+ EditorUtility.DisplayDialog("成功", "配置数据加载成功!", "确定");
+ }
+ catch (Exception e)
+ {
+ EditorUtility.DisplayDialog("错误", $"加载数据失败: {e.Message}\n{e.StackTrace}", "确定");
+ isDataLoaded = false;
+ }
+ }
+
+ ///
+ /// 执行Git命令
+ ///
+ private string ExecuteGitCommand(string workingDirectory, string arguments, out bool success)
+ {
+ try
+ {
+ ProcessStartInfo startInfo = new ProcessStartInfo
+ {
+ FileName = "git",
+ Arguments = arguments,
+ WorkingDirectory = workingDirectory,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using (Process process = Process.Start(startInfo))
+ {
+ string output = process.StandardOutput.ReadToEnd();
+ string error = process.StandardError.ReadToEnd();
+ process.WaitForExit();
+
+ success = process.ExitCode == 0;
+ return success ? output : error;
+ }
+ }
+ catch (Exception e)
+ {
+ success = false;
+ return $"执行Git命令失败: {e.Message}";
+ }
+ }
+
+ ///
+ /// 检查并更新Docs仓库
+ ///
+ private bool CheckAndUpdateDocsRepository()
+ {
+ string gitPath = Path.Combine(docsRootPath, ".git");
+ if (!Directory.Exists(gitPath))
+ {
+ EditorUtility.DisplayDialog("错误",
+ $"Docs目录不是Git仓库\n路径: {docsRootPath}\n\n请确保Docs项目已正确克隆",
+ "确定");
+ return false;
+ }
+
+ EditorUtility.DisplayProgressBar("检查更新", "正在检查Docs仓库远程更新...", 0.3f);
+
+ string fetchResult = ExecuteGitCommand(docsRootPath, "fetch", out bool fetchSuccess);
+
+ if (!fetchSuccess)
+ {
+ EditorUtility.ClearProgressBar();
+ EditorUtility.DisplayDialog("Git Fetch失败",
+ $"无法检查远程更新:\n{fetchResult}\n\n请在SourceTree或GitHubDesktop中检查网络连接和仓库状态",
+ "确定");
+ return false;
+ }
+
+ EditorUtility.DisplayProgressBar("检查更新", "正在检查是否有新提交...", 0.6f);
+
+ string statusResult = ExecuteGitCommand(docsRootPath, "status -uno", out bool statusSuccess);
+
+ if (!statusSuccess)
+ {
+ EditorUtility.ClearProgressBar();
+ EditorUtility.DisplayDialog("Git Status失败",
+ $"无法获取仓库状态:\n{statusResult}\n\n请在SourceTree或GitHubDesktop中检查仓库状态",
+ "确定");
+ return false;
+ }
+
+ if (statusResult.Contains("Changes not staged") || statusResult.Contains("Changes to be committed"))
+ {
+ EditorUtility.ClearProgressBar();
+ bool proceed = EditorUtility.DisplayDialog("警告:有未提交的更改",
+ "Docs仓库中有未提交的更改,这可能导致拉取时产生冲突。\n\n建议先提交或暂存这些更改。\n\n是否继续加载配置?(不推荐)",
+ "继续(不推荐)", "取消,前往处理");
+
+ if (!proceed)
+ {
+ Debug.Log("请在SourceTree或GitHubDesktop中处理未提交的更改");
+ return false;
+ }
+ }
+
+ if (statusResult.Contains("Your branch is behind"))
+ {
+ EditorUtility.DisplayProgressBar("更新中", "正在从远程拉取最新代码...", 0.8f);
+
+ string pullResult = ExecuteGitCommand(docsRootPath, "pull", out bool pullSuccess);
+
+ EditorUtility.ClearProgressBar();
+
+ if (!pullSuccess)
+ {
+ if (pullResult.Contains("CONFLICT") || pullResult.Contains("conflict"))
+ {
+ EditorUtility.DisplayDialog("拉取失败:存在冲突",
+ $"拉取远程更新时发生冲突:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中解决冲突后再操作",
+ "确定");
+ }
+ else
+ {
+ EditorUtility.DisplayDialog("拉取失败",
+ $"无法拉取远程更新:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中检查并解决问题",
+ "确定");
+ }
+ return false;
+ }
+
+ Debug.Log($"Docs仓库已更新到最新版本:\n{pullResult}");
+ EditorUtility.DisplayDialog("更新成功", "Docs仓库已更新到最新版本", "确定");
+ }
+ else
+ {
+ EditorUtility.ClearProgressBar();
+ Debug.Log("Docs仓库已是最新版本");
+ }
+
+ return true;
+ }
+
+ ///
+ /// 检查并切换Design_SubModule到main分支
+ ///
+ private bool CheckAndSwitchDesignSubModuleBranch()
+ {
+ string designSubModulePath = Path.Combine(Application.dataPath, "..", DESIGN_SUBMODULE_PATH);
+ designSubModulePath = Path.GetFullPath(designSubModulePath);
+
+ if (!Directory.Exists(designSubModulePath))
+ {
+ EditorUtility.DisplayDialog("错误",
+ $"Design_SubModule目录不存在:\n{designSubModulePath}\n\n请确保子模块已正确初始化",
+ "确定");
+ return false;
+ }
+
+ EditorUtility.DisplayProgressBar("检查分支", "正在检查Design_SubModule分支...", 0.5f);
+
+ string branchResult = ExecuteGitCommand(designSubModulePath, "branch --show-current", out bool branchSuccess);
+
+ if (!branchSuccess)
+ {
+ EditorUtility.ClearProgressBar();
+ EditorUtility.DisplayDialog("Git Branch失败",
+ $"无法获取当前分支:\n{branchResult}\n\n请在SourceTree或GitHubDesktop中检查Design_SubModule状态",
+ "确定");
+ return false;
+ }
+
+ string currentBranch = branchResult.Trim();
+
+ if (currentBranch != "main")
+ {
+ EditorUtility.DisplayProgressBar("切换分支", "正在切换到main分支...", 0.8f);
+
+ string checkoutResult = ExecuteGitCommand(designSubModulePath, "checkout main", out bool checkoutSuccess);
+
+ EditorUtility.ClearProgressBar();
+
+ if (!checkoutSuccess)
+ {
+ EditorUtility.DisplayDialog("切换分支失败",
+ $"无法切换到main分支:\n{checkoutResult}\n\n当前分支: {currentBranch}\n\n请在SourceTree或GitHubDesktop中手动切换到main分支",
+ "确定");
+ return false;
+ }
+
+ Debug.Log($"Design_SubModule已从 {currentBranch} 切换到 main 分支");
+ EditorUtility.DisplayDialog("分支切换成功",
+ $"Design_SubModule已从 {currentBranch} 切换到 main 分支",
+ "确定");
+ }
+ else
+ {
+ EditorUtility.ClearProgressBar();
+ Debug.Log("Design_SubModule已在main分支");
+ }
+
+ return true;
+ }
+
+ ///
+ /// 加载语言数据
+ ///
+ private void LoadLanguageData()
+ {
+ languageDict.Clear();
+
+ string languageExcelPath = Path.Combine(docsRootPath, "config", LANGUAGE_EXCEL_NAME);
+ if (!File.Exists(languageExcelPath))
+ {
+ Debug.LogWarning($"未找到语言表: {languageExcelPath}");
+ return;
+ }
+
+ using (var package = new ExcelPackage(new FileInfo(languageExcelPath)))
+ {
+ var worksheet = package.Workbook.Worksheets[LANGUAGE_SHEET_NAME];
+ if (worksheet == null)
+ {
+ Debug.LogWarning($"语言表中未找到Sheet: {LANGUAGE_SHEET_NAME}");
+ return;
+ }
+
+ // 查找Key列和语言列
+ int keyColumnIndex = -1;
+ int zhCNColumnIndex = -1;
+ int enUSColumnIndex = -1;
+ int ptBRColumnIndex = -1;
+ int columnCount = worksheet.Dimension.Columns;
+
+ for (int col = 1; col <= columnCount; col++)
+ {
+ string header = worksheet.Cells[1, col].Text;
+ if (header == "key")
+ {
+ keyColumnIndex = col;
+ }
+ else if (header == "zh_CN")
+ {
+ zhCNColumnIndex = col;
+ }
+ else if (header == "en_US")
+ {
+ enUSColumnIndex = col;
+ }
+ else if (header == "pt_BR")
+ {
+ ptBRColumnIndex = col;
+ }
+ }
+
+ if (keyColumnIndex < 0)
+ {
+ Debug.LogWarning("语言表中未找到Key列");
+ return;
+ }
+
+ // 读取数据(从第3行开始)
+ int rowCount = worksheet.Dimension.Rows;
+ for (int row = 3; row <= rowCount; row++)
+ {
+ string key = worksheet.Cells[row, keyColumnIndex].Text;
+ if (string.IsNullOrEmpty(key)) continue;
+
+ if (!languageDict.ContainsKey(key))
+ {
+ languageDict[key] = new Dictionary();
+ }
+
+ if (zhCNColumnIndex > 0)
+ {
+ languageDict[key]["zh_CN"] = worksheet.Cells[row, zhCNColumnIndex].Text;
+ }
+ if (enUSColumnIndex > 0)
+ {
+ languageDict[key]["en_US"] = worksheet.Cells[row, enUSColumnIndex].Text;
+ }
+ if (ptBRColumnIndex > 0)
+ {
+ languageDict[key]["pt_BR"] = worksheet.Cells[row, ptBRColumnIndex].Text;
+ }
+ }
+ }
+ }
+
+ ///
+ /// 加载Emoji.xlsx
+ ///
+ private void LoadEmojiExcel()
+ {
+ emojiDataList.Clear();
+
+ string emojiExcelPath = Path.Combine(docsRootPath, "config", EMOJI_EXCEL_NAME);
+ if (!File.Exists(emojiExcelPath))
+ {
+ throw new Exception($"未找到Emoji配置文件: {emojiExcelPath}");
+ }
+
+ using (var package = new ExcelPackage(new FileInfo(emojiExcelPath)))
+ {
+ var worksheet = package.Workbook.Worksheets[EMOJI_SHEET_NAME];
+ if (worksheet == null)
+ {
+ throw new Exception($"Emoji.xlsx中未找到Sheet: {EMOJI_SHEET_NAME}");
+ }
+
+ // 读取表头(第1行)
+ int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1;
+ int columnCount = worksheet.Dimension.Columns;
+
+ for (int col = 1; col <= columnCount; col++)
+ {
+ string header = worksheet.Cells[1, col].Text;
+ switch (header)
+ {
+ case "Id":
+ idCol = col;
+ break;
+ case "NameKey":
+ nameKeyCol = col;
+ break;
+ case "Init":
+ initCol = col;
+ break;
+ case "Icon":
+ iconCol = col;
+ break;
+ }
+ }
+
+ if (idCol < 0 || nameKeyCol < 0 || initCol < 0 || iconCol < 0)
+ {
+ throw new Exception("Emoji.xlsx表结构不正确,缺少必要列(Id/NameKey/Init/Icon)");
+ }
+
+ // 读取数据(从第3行开始)
+ int rowCount = worksheet.Dimension.Rows;
+ for (int row = 3; row <= rowCount; row++)
+ {
+ string idText = worksheet.Cells[row, idCol].Text;
+ if (string.IsNullOrEmpty(idText)) continue;
+
+ if (!int.TryParse(idText, out int id)) continue;
+
+ string nameKey = worksheet.Cells[row, nameKeyCol].Text;
+ string initText = worksheet.Cells[row, initCol].Text;
+ string iconText = worksheet.Cells[row, iconCol].Text;
+
+ int init = 0;
+ if (!string.IsNullOrEmpty(initText))
+ {
+ int.TryParse(initText, out init);
+ }
+
+ int iconId = -1;
+ if (!string.IsNullOrEmpty(iconText))
+ {
+ if (!int.TryParse(iconText, out iconId))
+ {
+ iconId = -1;
+ }
+ }
+
+ var emojiData = new EmojiData
+ {
+ Id = id,
+ NameKey = nameKey,
+ Init = init,
+ IconId = iconId
+ };
+
+ emojiDataList.Add(emojiData);
+ }
+ }
+ }
+
+ ///
+ /// 绘制数据编辑器
+ ///
+ private void DrawDataEditor()
+ {
+ pendingTooltipText = "";
+
+ EditorGUILayout.BeginVertical("box");
+
+ EditorGUILayout.BeginHorizontal();
+ EditorGUILayout.LabelField($"表情数据列表(共 {emojiDataList.Count} 条)", EditorStyles.boldLabel);
+ GUILayout.FlexibleSpace();
+
+ GUI.backgroundColor = Color.cyan;
+ if (GUILayout.Button("+ 添加表情", GUILayout.Height(25), GUILayout.Width(100)))
+ {
+ AddNewEmoji();
+ }
+ GUI.backgroundColor = Color.white;
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.Space(3);
+
+ scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
+
+ // 表头
+ EditorGUILayout.BeginHorizontal("box");
+ EditorGUILayout.LabelField("Id", EditorStyles.boldLabel, GUILayout.Width(50));
+ EditorGUILayout.LabelField("NameKey", EditorStyles.boldLabel, GUILayout.Width(150));
+ EditorGUILayout.LabelField("中文名称", EditorStyles.boldLabel, GUILayout.Width(120));
+ EditorGUILayout.LabelField("Init", EditorStyles.boldLabel, GUILayout.Width(50));
+ EditorGUILayout.LabelField("Icon", EditorStyles.boldLabel, GUILayout.Width(200));
+ EditorGUILayout.LabelField("预览", EditorStyles.boldLabel, GUILayout.Width(100));
+ EditorGUILayout.LabelField("操作", EditorStyles.boldLabel, GUILayout.Width(60));
+ EditorGUILayout.EndHorizontal();
+
+ // 数据行
+ for (int i = 0; i < emojiDataList.Count; i++)
+ {
+ DrawEmojiDataRow(emojiDataList[i]);
+ }
+
+ EditorGUILayout.EndScrollView();
+ EditorGUILayout.EndVertical();
+
+ EditorGUILayout.Space(10);
+
+ // 保存按钮
+ EditorGUILayout.BeginHorizontal();
+ GUILayout.FlexibleSpace();
+
+ GUI.backgroundColor = Color.green;
+ if (GUILayout.Button("保存配置到Excel", GUILayout.Height(30), GUILayout.Width(200)))
+ {
+ SaveData();
+ }
+ GUI.backgroundColor = Color.white;
+
+ GUILayout.FlexibleSpace();
+ EditorGUILayout.EndHorizontal();
+
+ // 在所有内容绘制完后,绘制tooltip(确保在最上层)
+ if (!string.IsNullOrEmpty(pendingTooltipText))
+ {
+ Vector2 tooltipSize = GUI.skin.box.CalcSize(new GUIContent(pendingTooltipText));
+ tooltipSize.x += 10;
+ tooltipSize.y += 10;
+
+ Vector2 mousePos = Event.current.mousePosition;
+ Rect tooltipRect = new Rect(
+ mousePos.x + 1,
+ mousePos.y + 1,
+ tooltipSize.x,
+ tooltipSize.y
+ );
+
+ GUI.Box(tooltipRect, pendingTooltipText);
+ }
+ }
+
+ ///
+ /// 绘制单行数据
+ ///
+ private void DrawEmojiDataRow(EmojiData data)
+ {
+ EditorGUILayout.BeginHorizontal("box");
+
+ // Id(可编辑)
+ data.Id = EditorGUILayout.IntField(data.Id, GUILayout.Width(50));
+
+ // NameKey(可编辑)
+ data.NameKey = EditorGUILayout.TextField(data.NameKey, GUILayout.Width(150));
+
+ // 中文名称(只读预览,带即时多语言显示)
+ GUI.enabled = false;
+ string zhName = "未找到语言Key";
+
+ if (languageDict.ContainsKey(data.NameKey))
+ {
+ var langs = languageDict[data.NameKey];
+ if (langs.ContainsKey("zh_CN"))
+ {
+ zhName = langs["zh_CN"];
+ }
+ }
+
+ Rect nameRect = GUILayoutUtility.GetRect(new GUIContent(zhName), GUI.skin.textField, GUILayout.Width(120));
+ EditorGUI.TextField(nameRect, zhName);
+
+ // 检测鼠标悬停并准备tooltip内容
+ if (nameRect.Contains(Event.current.mousePosition) && languageDict.ContainsKey(data.NameKey))
+ {
+ var langs = languageDict[data.NameKey];
+ List tooltipLines = new List();
+
+ if (langs.ContainsKey("zh_CN") && !string.IsNullOrEmpty(langs["zh_CN"]))
+ {
+ tooltipLines.Add($"[中文] {langs["zh_CN"]}");
+ }
+ if (langs.ContainsKey("en_US") && !string.IsNullOrEmpty(langs["en_US"]))
+ {
+ tooltipLines.Add($"[English] {langs["en_US"]}");
+ }
+ if (langs.ContainsKey("pt_BR") && !string.IsNullOrEmpty(langs["pt_BR"]))
+ {
+ tooltipLines.Add($"[Português] {langs["pt_BR"]}");
+ }
+
+ if (tooltipLines.Count > 0)
+ {
+ pendingTooltipText = string.Join("\n", tooltipLines);
+ Repaint();
+ }
+ }
+
+ GUI.enabled = true;
+
+ // Init(Checkbox)
+ bool initChecked = data.Init == 1;
+ bool newInitChecked = EditorGUILayout.Toggle(initChecked, GUILayout.Width(50));
+ data.Init = newInitChecked ? 1 : 0;
+
+ // Icon(下拉列表)
+ var iconItems = emojiTableSO.Items;
+ var iconNamesList = new List { "未选择" };
+ iconNamesList.AddRange(iconItems.Select(x => x.Name));
+ var iconNames = iconNamesList.ToArray();
+
+ int currentIndex = 0;
+ var currentItem = iconItems.Find(x => x.Id == data.IconId);
+ if (currentItem != null)
+ {
+ int itemIndex = iconItems.IndexOf(currentItem);
+ if (itemIndex >= 0)
+ {
+ currentIndex = itemIndex + 1;
+ }
+ }
+
+ EditorGUI.BeginChangeCheck();
+ int newIndex = EditorGUILayout.Popup(currentIndex, iconNames, GUILayout.Width(200));
+ if (EditorGUI.EndChangeCheck())
+ {
+ if (newIndex == 0)
+ {
+ data.IconId = -1;
+ }
+ else if (newIndex > 0 && newIndex <= iconItems.Count)
+ {
+ data.IconId = iconItems[newIndex - 1].Id;
+ }
+ }
+
+ // 预览
+ if (data.IconId >= 0)
+ {
+ var item = iconItems.Find(x => x.Id == data.IconId);
+ if (item != null)
+ {
+ Sprite sprite = item.Sprite;
+
+ if (sprite != null && sprite.texture != null)
+ {
+ Rect previewRect = GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
+
+ EditorGUI.DrawRect(previewRect, new Color(0.5f, 0.5f, 0.5f, 1f));
+
+ Rect texCoords = sprite.textureRect;
+ Texture2D tex = sprite.texture;
+
+ Rect normalizedCoords = new Rect(
+ texCoords.x / tex.width,
+ texCoords.y / tex.height,
+ texCoords.width / tex.width,
+ texCoords.height / tex.height
+ );
+
+ float aspect = texCoords.width / texCoords.height;
+ Rect drawRect = previewRect;
+ if (aspect > 1f)
+ {
+ float height = drawRect.width / aspect;
+ drawRect.y += (drawRect.height - height) * 0.5f;
+ drawRect.height = height;
+ }
+ else
+ {
+ float width = drawRect.height * aspect;
+ drawRect.x += (drawRect.width - width) * 0.5f;
+ drawRect.width = width;
+ }
+
+ GUI.DrawTextureWithTexCoords(drawRect, tex, normalizedCoords, true);
+
+ if (!string.IsNullOrEmpty(item.Desc))
+ {
+ GUI.Label(previewRect, new GUIContent("", item.Desc));
+ }
+ }
+ else
+ {
+ GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
+ GUILayout.Space(-50);
+ EditorGUILayout.LabelField("无图片", GUILayout.Width(50));
+ }
+ }
+ else
+ {
+ GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
+ GUILayout.Space(-50);
+ EditorGUILayout.LabelField("未选择", GUILayout.Width(50));
+ }
+ }
+ else
+ {
+ GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
+ GUILayout.Space(-50);
+ EditorGUILayout.LabelField("未选择", GUILayout.Width(50));
+ }
+
+ // 删除按钮
+ GUI.backgroundColor = Color.red;
+ if (GUILayout.Button("删除", GUILayout.Width(60)))
+ {
+ if (EditorUtility.DisplayDialog("确认删除",
+ $"确定要删除表情 {data.Id} ({data.NameKey}) 吗?",
+ "删除", "取消"))
+ {
+ emojiDataList.Remove(data);
+ }
+ }
+ GUI.backgroundColor = Color.white;
+
+ EditorGUILayout.EndHorizontal();
+ }
+
+ ///
+ /// 添加新表情
+ ///
+ private void AddNewEmoji()
+ {
+ int newId = 1;
+ if (emojiDataList.Count > 0)
+ {
+ newId = emojiDataList.Max(x => x.Id) + 1;
+ }
+
+ var newEmoji = new EmojiData
+ {
+ Id = newId,
+ NameKey = "",
+ Init = 0,
+ IconId = -1
+ };
+
+ emojiDataList.Add(newEmoji);
+
+ scrollPosition = new Vector2(0, float.MaxValue);
+ }
+
+ ///
+ /// 保存数据到Excel
+ ///
+ private void SaveData()
+ {
+ try
+ {
+ string emojiExcelPath = Path.Combine(docsRootPath, "config", EMOJI_EXCEL_NAME);
+ if (!File.Exists(emojiExcelPath))
+ {
+ EditorUtility.DisplayDialog("错误", $"未找到Emoji配置文件: {emojiExcelPath}", "确定");
+ return;
+ }
+
+ using (var package = new ExcelPackage(new FileInfo(emojiExcelPath)))
+ {
+ var worksheet = package.Workbook.Worksheets[EMOJI_SHEET_NAME];
+ if (worksheet == null)
+ {
+ EditorUtility.DisplayDialog("错误", $"Emoji.xlsx中未找到Sheet: {EMOJI_SHEET_NAME}", "确定");
+ return;
+ }
+
+ // 查找列索引
+ int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1;
+ int columnCount = worksheet.Dimension.Columns;
+
+ for (int col = 1; col <= columnCount; col++)
+ {
+ string header = worksheet.Cells[1, col].Text;
+ switch (header)
+ {
+ case "Id":
+ idCol = col;
+ break;
+ case "NameKey":
+ nameKeyCol = col;
+ break;
+ case "Init":
+ initCol = col;
+ break;
+ case "Icon":
+ iconCol = col;
+ break;
+ }
+ }
+
+ // 更新和删除数据(从第3行开始)
+ int rowCount = worksheet.Dimension.Rows;
+ var processedIds = new HashSet();
+
+ // 第一遍:更新现有行或删除
+ for (int row = rowCount; row >= 3; row--)
+ {
+ string idText = worksheet.Cells[row, idCol].Text;
+ if (string.IsNullOrEmpty(idText)) continue;
+
+ if (!int.TryParse(idText, out int id)) continue;
+
+ var emojiData = emojiDataList.Find(x => x.Id == id);
+ if (emojiData != null)
+ {
+ worksheet.Cells[row, nameKeyCol].Value = emojiData.NameKey;
+ worksheet.Cells[row, initCol].Value = emojiData.Init;
+
+ if (emojiData.IconId < 0)
+ {
+ worksheet.Cells[row, iconCol].Value = "";
+ }
+ else
+ {
+ worksheet.Cells[row, iconCol].Value = emojiData.IconId;
+ }
+
+ processedIds.Add(id);
+ }
+ else
+ {
+ worksheet.DeleteRow(row);
+ }
+ }
+
+ // 第二遍:添加新行
+ int currentRow = worksheet.Dimension?.Rows ?? 2;
+ foreach (var emojiData in emojiDataList)
+ {
+ if (!processedIds.Contains(emojiData.Id))
+ {
+ currentRow++;
+ worksheet.Cells[currentRow, idCol].Value = emojiData.Id;
+ worksheet.Cells[currentRow, nameKeyCol].Value = emojiData.NameKey;
+ worksheet.Cells[currentRow, initCol].Value = emojiData.Init;
+
+ if (emojiData.IconId < 0)
+ {
+ worksheet.Cells[currentRow, iconCol].Value = "";
+ }
+ else
+ {
+ worksheet.Cells[currentRow, iconCol].Value = emojiData.IconId;
+ }
+ }
+ }
+
+ package.Save();
+ }
+
+ // 提示成功并提醒推送
+ bool understood = EditorUtility.DisplayDialog("保存成功",
+ "配置已保存到Excel文件!\n\n" +
+ "⚠️ 重要提醒:\n" +
+ "请及时在SourceTree或GitHubDesktop中:\n" +
+ "1. 提交(Commit)本次修改\n" +
+ "2. 推送(Push)到远程仓库\n\n" +
+ "避免与其他策划产生冲突!",
+ "我知道了");
+ }
+ catch (Exception e)
+ {
+ EditorUtility.DisplayDialog("错误", $"保存数据失败: {e.Message}\n{e.StackTrace}", "确定");
+ }
+ }
+
+ ///
+ /// 表情数据类
+ ///
+ private class EmojiData
+ {
+ public int Id;
+ public string NameKey;
+ public int Init;
+ public int IconId;
+ }
+ }
+}
diff --git a/Scripts/Editor/Design_Tools/Collections/EmojiConfigEditor.cs.meta b/Scripts/Editor/Design_Tools/Collections/EmojiConfigEditor.cs.meta
new file mode 100644
index 0000000..d536e12
--- /dev/null
+++ b/Scripts/Editor/Design_Tools/Collections/EmojiConfigEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a9ae11b153bc3454cab7be603efdb90b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/Design_Tools/Collections/HeadConfigEditor.cs b/Scripts/Editor/Design_Tools/Collections/HeadConfigEditor.cs
new file mode 100644
index 0000000..5720632
--- /dev/null
+++ b/Scripts/Editor/Design_Tools/Collections/HeadConfigEditor.cs
@@ -0,0 +1,987 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+using OfficeOpenXml;
+using ArtResource;
+using Debug = UnityEngine.Debug;
+
+namespace DesignTools.Collections
+{
+ ///
+ /// 头像配置Editor工具
+ /// 读取Docs/config/Face.xlsx,关联Art_SO/HeadResources.asset
+ ///
+ public class HeadConfigEditor : EditorWindow
+ {
+ private const string HEAD_SO_PATH = "Assets/Art_SubModule/Art_SO";
+ private const string FACE_EXCEL_NAME = "Face.xlsx";
+ private const string LANGUAGE_EXCEL_NAME = "AllLanguage.xlsx";
+ private const string FACE_SHEET_NAME = "Face";
+ private const string LANGUAGE_SHEET_NAME = "client";
+ private const string DOCS_PATH_PREF_KEY = "HeadConfigEditor_DocsPath";
+ private const string DESIGN_SUBMODULE_PATH = "Assets/Design_SubModule";
+
+ private string docsRootPath = "";
+ private List faceDataList = new List();
+ private ArtTableSO headTableSO;
+ private Dictionary> languageDict = new Dictionary>();
+ private Vector2 scrollPosition;
+ private bool isDataLoaded = false;
+ private string pendingTooltipText = "";
+
+ [MenuItem("策划工具/收藏品/头像")]
+ public static void ShowWindow()
+ {
+ var window = GetWindow("头像配置");
+ window.minSize = new Vector2(900, 600);
+ window.Show();
+ }
+
+ private void OnEnable()
+ {
+ // 设置EPPlus许可证
+ // ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
+
+ // 读取上次保存的路径
+ if (EditorPrefs.HasKey(DOCS_PATH_PREF_KEY))
+ {
+ docsRootPath = EditorPrefs.GetString(DOCS_PATH_PREF_KEY);
+ }
+ }
+
+ private void OnGUI()
+ {
+ EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+ EditorGUILayout.LabelField("头像配置工具", EditorStyles.boldLabel);
+ GUILayout.FlexibleSpace();
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.Space(5);
+
+ // Docs路径选择
+ EditorGUILayout.BeginVertical("box");
+ EditorGUILayout.LabelField("Docs项目根目录", EditorStyles.boldLabel);
+
+ EditorGUILayout.BeginHorizontal();
+ docsRootPath = EditorGUILayout.TextField("路径", docsRootPath);
+ if (GUILayout.Button("选择文件夹", GUILayout.Width(100)))
+ {
+ string selectedPath = EditorUtility.OpenFolderPanel("选择Docs项目根目录", "", "");
+ if (!string.IsNullOrEmpty(selectedPath))
+ {
+ docsRootPath = selectedPath;
+ EditorPrefs.SetString(DOCS_PATH_PREF_KEY, docsRootPath);
+ }
+ }
+ EditorGUILayout.EndHorizontal();
+
+ if (GUILayout.Button("加载配置数据", GUILayout.Height(30)))
+ {
+ LoadData();
+ }
+
+ EditorGUILayout.EndVertical();
+
+ EditorGUILayout.Space(5);
+
+ // 数据编辑区域
+ if (isDataLoaded)
+ {
+ DrawDataEditor();
+ }
+ else
+ {
+ EditorGUILayout.HelpBox("请先选择Docs根目录并加载配置数据", MessageType.Info);
+ }
+ }
+
+ ///
+ /// 加载配置数据
+ ///
+ private void LoadData()
+ {
+ try
+ {
+ // 校验路径
+ if (string.IsNullOrEmpty(docsRootPath) || !Directory.Exists(docsRootPath))
+ {
+ EditorUtility.DisplayDialog("错误", "请选择有效的Docs根目录", "确定");
+ return;
+ }
+
+ // 1. 检查Docs是否为Git仓库并更新
+ if (!CheckAndUpdateDocsRepository())
+ {
+ return; // 检查失败,提示已在方法内显示
+ }
+
+ // 2. 检查Design_SubModule分支
+ if (!CheckAndSwitchDesignSubModuleBranch())
+ {
+ return; // 检查失败,提示已在方法内显示
+ }
+
+ // 校验Head SO是否存在
+ string[] soGuids = AssetDatabase.FindAssets("t:ArtTableSO", new[] { HEAD_SO_PATH });
+ if (soGuids.Length == 0)
+ {
+ EditorUtility.DisplayDialog("错误", $"未在 {HEAD_SO_PATH} 目录下找到头像资源配置(ArtTableSO)\n请先在美术资源配置工具中创建", "确定");
+ return;
+ }
+
+ // 查找名称为"HeadResource"的SO
+ ArtTableSO foundSO = null;
+ foreach (var guid in soGuids)
+ {
+ string path = AssetDatabase.GUIDToAssetPath(guid);
+ var so = AssetDatabase.LoadAssetAtPath(path);
+ if (so != null && so.TableName == "HeadResource")
+ {
+ foundSO = so;
+ break;
+ }
+ }
+
+ if (foundSO == null)
+ {
+ EditorUtility.DisplayDialog("错误", "无法加载头像资源配置", "确定");
+ return;
+ }
+
+ headTableSO = foundSO;
+
+ if (headTableSO.Items == null || headTableSO.Items.Count == 0)
+ {
+ EditorUtility.DisplayDialog("错误", "头像资源配置数据为空", "确定");
+ return;
+ }
+
+ // 加载语言表
+ LoadLanguageData();
+
+ // 加载Face.xlsx
+ LoadFaceExcel();
+
+ isDataLoaded = true;
+ EditorUtility.DisplayDialog("成功", "配置数据加载成功!", "确定");
+ }
+ catch (Exception e)
+ {
+ EditorUtility.DisplayDialog("错误", $"加载数据失败: {e.Message}\n{e.StackTrace}", "确定");
+ isDataLoaded = false;
+ }
+ }
+
+ ///
+ /// 执行Git命令
+ ///
+ private string ExecuteGitCommand(string workingDirectory, string arguments, out bool success)
+ {
+ try
+ {
+ ProcessStartInfo startInfo = new ProcessStartInfo
+ {
+ FileName = "git",
+ Arguments = arguments,
+ WorkingDirectory = workingDirectory,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using (Process process = Process.Start(startInfo))
+ {
+ string output = process.StandardOutput.ReadToEnd();
+ string error = process.StandardError.ReadToEnd();
+ process.WaitForExit();
+
+ success = process.ExitCode == 0;
+ return success ? output : error;
+ }
+ }
+ catch (Exception e)
+ {
+ success = false;
+ return $"执行Git命令失败: {e.Message}";
+ }
+ }
+
+ ///
+ /// 检查并更新Docs仓库
+ ///
+ private bool CheckAndUpdateDocsRepository()
+ {
+ // 检查是否是Git仓库
+ string gitPath = Path.Combine(docsRootPath, ".git");
+ if (!Directory.Exists(gitPath))
+ {
+ EditorUtility.DisplayDialog("错误",
+ $"Docs目录不是Git仓库\n路径: {docsRootPath}\n\n请确保Docs项目已正确克隆",
+ "确定");
+ return false;
+ }
+
+ EditorUtility.DisplayProgressBar("检查更新", "正在检查Docs仓库远程更新...", 0.3f);
+
+ // 执行git fetch检查远程更新
+ string fetchResult = ExecuteGitCommand(docsRootPath, "fetch", out bool fetchSuccess);
+
+ if (!fetchSuccess)
+ {
+ EditorUtility.ClearProgressBar();
+ EditorUtility.DisplayDialog("Git Fetch失败",
+ $"无法检查远程更新:\n{fetchResult}\n\n请在SourceTree或GitHubDesktop中检查网络连接和仓库状态",
+ "确定");
+ return false;
+ }
+
+ EditorUtility.DisplayProgressBar("检查更新", "正在检查是否有新提交...", 0.6f);
+
+ // 检查本地和远程的差异
+ string statusResult = ExecuteGitCommand(docsRootPath, "status -uno", out bool statusSuccess);
+
+ if (!statusSuccess)
+ {
+ EditorUtility.ClearProgressBar();
+ EditorUtility.DisplayDialog("Git Status失败",
+ $"无法获取仓库状态:\n{statusResult}\n\n请在SourceTree或GitHubDesktop中检查仓库状态",
+ "确定");
+ return false;
+ }
+
+ // 检查是否有未提交的更改
+ if (statusResult.Contains("Changes not staged") || statusResult.Contains("Changes to be committed"))
+ {
+ EditorUtility.ClearProgressBar();
+ bool proceed = EditorUtility.DisplayDialog("警告:有未提交的更改",
+ "Docs仓库中有未提交的更改,这可能导致拉取时产生冲突。\n\n建议先提交或暂存这些更改。\n\n是否继续加载配置?(不推荐)",
+ "继续(不推荐)", "取消,前往处理");
+
+ if (!proceed)
+ {
+ Debug.Log("请在SourceTree或GitHubDesktop中处理未提交的更改");
+ return false;
+ }
+ }
+
+ // 检查是否behind远程
+ if (statusResult.Contains("Your branch is behind"))
+ {
+ EditorUtility.DisplayProgressBar("更新中", "正在从远程拉取最新代码...", 0.8f);
+
+ string pullResult = ExecuteGitCommand(docsRootPath, "pull", out bool pullSuccess);
+
+ EditorUtility.ClearProgressBar();
+
+ if (!pullSuccess)
+ {
+ // 检查是否是合并冲突
+ if (pullResult.Contains("CONFLICT") || pullResult.Contains("conflict"))
+ {
+ EditorUtility.DisplayDialog("拉取失败:存在冲突",
+ $"拉取远程更新时发生冲突:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中解决冲突后再操作",
+ "确定");
+ }
+ else
+ {
+ EditorUtility.DisplayDialog("拉取失败",
+ $"无法拉取远程更新:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中检查并解决问题",
+ "确定");
+ }
+ return false;
+ }
+
+ Debug.Log($"Docs仓库已更新到最新版本:\n{pullResult}");
+ EditorUtility.DisplayDialog("更新成功", "Docs仓库已更新到最新版本", "确定");
+ }
+ else
+ {
+ EditorUtility.ClearProgressBar();
+ Debug.Log("Docs仓库已是最新版本");
+ }
+
+ return true;
+ }
+
+ ///
+ /// 检查并切换Design_SubModule到main分支
+ ///
+ private bool CheckAndSwitchDesignSubModuleBranch()
+ {
+ string designSubModulePath = Path.Combine(Application.dataPath, "..", DESIGN_SUBMODULE_PATH);
+ designSubModulePath = Path.GetFullPath(designSubModulePath);
+
+ // 检查Design_SubModule是否存在
+ if (!Directory.Exists(designSubModulePath))
+ {
+ EditorUtility.DisplayDialog("错误",
+ $"Design_SubModule目录不存在:\n{designSubModulePath}\n\n请确保子模块已正确初始化",
+ "确定");
+ return false;
+ }
+
+ EditorUtility.DisplayProgressBar("检查分支", "正在检查Design_SubModule分支...", 0.5f);
+
+ // 检查当前分支
+ string branchResult = ExecuteGitCommand(designSubModulePath, "branch --show-current", out bool branchSuccess);
+
+ if (!branchSuccess)
+ {
+ EditorUtility.ClearProgressBar();
+ EditorUtility.DisplayDialog("Git Branch失败",
+ $"无法获取当前分支:\n{branchResult}\n\n请在SourceTree或GitHubDesktop中检查Design_SubModule状态",
+ "确定");
+ return false;
+ }
+
+ string currentBranch = branchResult.Trim();
+
+ if (currentBranch != "main")
+ {
+ EditorUtility.DisplayProgressBar("切换分支", "正在切换到main分支...", 0.8f);
+
+ string checkoutResult = ExecuteGitCommand(designSubModulePath, "checkout main", out bool checkoutSuccess);
+
+ EditorUtility.ClearProgressBar();
+
+ if (!checkoutSuccess)
+ {
+ EditorUtility.DisplayDialog("切换分支失败",
+ $"无法切换到main分支:\n{checkoutResult}\n\n当前分支: {currentBranch}\n\n请在SourceTree或GitHubDesktop中手动切换到main分支",
+ "确定");
+ return false;
+ }
+
+ Debug.Log($"Design_SubModule已从 {currentBranch} 切换到 main 分支");
+ EditorUtility.DisplayDialog("分支切换成功",
+ $"Design_SubModule已从 {currentBranch} 切换到 main 分支",
+ "确定");
+ }
+ else
+ {
+ EditorUtility.ClearProgressBar();
+ Debug.Log("Design_SubModule已在main分支");
+ }
+
+ return true;
+ }
+
+ ///
+ /// 加载语言数据
+ ///
+ private void LoadLanguageData()
+ {
+ languageDict.Clear();
+
+ string languageExcelPath = Path.Combine(docsRootPath, "config", LANGUAGE_EXCEL_NAME);
+ if (!File.Exists(languageExcelPath))
+ {
+ Debug.LogWarning($"未找到语言表: {languageExcelPath}");
+ return;
+ }
+
+ using (var package = new ExcelPackage(new FileInfo(languageExcelPath)))
+ {
+ var worksheet = package.Workbook.Worksheets[LANGUAGE_SHEET_NAME];
+ if (worksheet == null)
+ {
+ Debug.LogWarning($"语言表中未找到Sheet: {LANGUAGE_SHEET_NAME}");
+ return;
+ }
+
+ // 查找Key列和语言列
+ int keyColumnIndex = -1;
+ int zhCNColumnIndex = -1;
+ int enUSColumnIndex = -1;
+ int ptBRColumnIndex = -1;
+ int columnCount = worksheet.Dimension.Columns;
+
+ for (int col = 1; col <= columnCount; col++)
+ {
+ string header = worksheet.Cells[1, col].Text;
+ if (header == "key")
+ {
+ keyColumnIndex = col;
+ }
+ else if (header == "zh_CN")
+ {
+ zhCNColumnIndex = col;
+ }
+ else if (header == "en_US")
+ {
+ enUSColumnIndex = col;
+ }
+ else if (header == "pt_BR")
+ {
+ ptBRColumnIndex = col;
+ }
+ }
+
+ if (keyColumnIndex < 0)
+ {
+ Debug.LogWarning("语言表中未找到Key列");
+ return;
+ }
+
+ // 读取数据(从第3行开始)
+ int rowCount = worksheet.Dimension.Rows;
+ for (int row = 3; row <= rowCount; row++)
+ {
+ string key = worksheet.Cells[row, keyColumnIndex].Text;
+ if (string.IsNullOrEmpty(key)) continue;
+
+ if (!languageDict.ContainsKey(key))
+ {
+ languageDict[key] = new Dictionary();
+ }
+
+ if (zhCNColumnIndex > 0)
+ {
+ languageDict[key]["zh_CN"] = worksheet.Cells[row, zhCNColumnIndex].Text;
+ }
+ if (enUSColumnIndex > 0)
+ {
+ languageDict[key]["en_US"] = worksheet.Cells[row, enUSColumnIndex].Text;
+ }
+ if (ptBRColumnIndex > 0)
+ {
+ languageDict[key]["pt_BR"] = worksheet.Cells[row, ptBRColumnIndex].Text;
+ }
+ }
+ }
+ }
+
+ ///
+ /// 加载Face.xlsx
+ ///
+ private void LoadFaceExcel()
+ {
+ faceDataList.Clear();
+
+ string faceExcelPath = Path.Combine(docsRootPath, "config", FACE_EXCEL_NAME);
+ if (!File.Exists(faceExcelPath))
+ {
+ throw new Exception($"未找到Face配置文件: {faceExcelPath}");
+ }
+
+ using (var package = new ExcelPackage(new FileInfo(faceExcelPath)))
+ {
+ var worksheet = package.Workbook.Worksheets[FACE_SHEET_NAME];
+ if (worksheet == null)
+ {
+ throw new Exception($"Face.xlsx中未找到Sheet: {FACE_SHEET_NAME}");
+ }
+
+ // 读取表头(第1行)
+ int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1;
+ int columnCount = worksheet.Dimension.Columns;
+
+ for (int col = 1; col <= columnCount; col++)
+ {
+ string header = worksheet.Cells[1, col].Text;
+ switch (header)
+ {
+ case "Id":
+ idCol = col;
+ break;
+ case "NameKey":
+ nameKeyCol = col;
+ break;
+ case "Init":
+ initCol = col;
+ break;
+ case "Icon":
+ iconCol = col;
+ break;
+ }
+ }
+
+ if (idCol < 0 || nameKeyCol < 0 || initCol < 0 || iconCol < 0)
+ {
+ throw new Exception("Face.xlsx表结构不正确,缺少必要列(Id/NameKey/Init/Icon)");
+ }
+
+ // 读取数据(从第3行开始)
+ int rowCount = worksheet.Dimension.Rows;
+ for (int row = 3; row <= rowCount; row++)
+ {
+ string idText = worksheet.Cells[row, idCol].Text;
+ if (string.IsNullOrEmpty(idText)) continue;
+
+ if (!int.TryParse(idText, out int id)) continue;
+
+ string nameKey = worksheet.Cells[row, nameKeyCol].Text;
+ string initText = worksheet.Cells[row, initCol].Text;
+ string iconText = worksheet.Cells[row, iconCol].Text;
+
+ int init = 0;
+ if (!string.IsNullOrEmpty(initText))
+ {
+ int.TryParse(initText, out init);
+ }
+
+ int iconId = -1; // 默认为未选择
+ if (!string.IsNullOrEmpty(iconText))
+ {
+ if (!int.TryParse(iconText, out iconId))
+ {
+ iconId = -1; // 解析失败设为未选择
+ }
+ }
+
+ var faceData = new FaceData
+ {
+ Id = id,
+ NameKey = nameKey,
+ Init = init,
+ IconId = iconId
+ };
+
+ faceDataList.Add(faceData);
+ }
+ }
+ }
+
+ ///
+ /// 绘制数据编辑器
+ ///
+ private void DrawDataEditor()
+ {
+ pendingTooltipText = "";
+
+ EditorGUILayout.BeginVertical("box");
+
+ EditorGUILayout.BeginHorizontal();
+ EditorGUILayout.LabelField($"头像数据列表(共 {faceDataList.Count} 条)", EditorStyles.boldLabel);
+ GUILayout.FlexibleSpace();
+
+ GUI.backgroundColor = Color.cyan;
+ if (GUILayout.Button("+ 添加头像", GUILayout.Height(25), GUILayout.Width(100)))
+ {
+ AddNewFace();
+ }
+ GUI.backgroundColor = Color.white;
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.Space(3);
+
+ scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
+
+ // 表头
+ EditorGUILayout.BeginHorizontal("box");
+ EditorGUILayout.LabelField("Id", EditorStyles.boldLabel, GUILayout.Width(50));
+ EditorGUILayout.LabelField("NameKey", EditorStyles.boldLabel, GUILayout.Width(150));
+ EditorGUILayout.LabelField("中文名称", EditorStyles.boldLabel, GUILayout.Width(120));
+ EditorGUILayout.LabelField("Init", EditorStyles.boldLabel, GUILayout.Width(50));
+ EditorGUILayout.LabelField("Icon", EditorStyles.boldLabel, GUILayout.Width(200));
+ EditorGUILayout.LabelField("预览", EditorStyles.boldLabel, GUILayout.Width(100));
+ EditorGUILayout.LabelField("操作", EditorStyles.boldLabel, GUILayout.Width(60));
+ EditorGUILayout.EndHorizontal();
+
+ // 数据行
+ for (int i = 0; i < faceDataList.Count; i++)
+ {
+ DrawFaceDataRow(faceDataList[i]);
+ }
+
+ EditorGUILayout.EndScrollView();
+ EditorGUILayout.EndVertical();
+
+ EditorGUILayout.Space(10);
+
+ // 保存按钮
+ EditorGUILayout.BeginHorizontal();
+ GUILayout.FlexibleSpace();
+
+ GUI.backgroundColor = Color.green;
+ if (GUILayout.Button("保存配置到Excel", GUILayout.Height(30), GUILayout.Width(200)))
+ {
+ SaveData();
+ }
+ GUI.backgroundColor = Color.white;
+
+ GUILayout.FlexibleSpace();
+ EditorGUILayout.EndHorizontal();
+
+ // 在所有内容绘制完后,绘制tooltip(确保在最上层)
+ if (!string.IsNullOrEmpty(pendingTooltipText))
+ {
+ Vector2 tooltipSize = GUI.skin.box.CalcSize(new GUIContent(pendingTooltipText));
+ tooltipSize.x += 10;
+ tooltipSize.y += 10;
+
+ // 使用当前鼠标位置,而不是之前记录的位置
+ Vector2 mousePos = Event.current.mousePosition;
+ Rect tooltipRect = new Rect(
+ mousePos.x + 1,
+ mousePos.y + 1,
+ tooltipSize.x,
+ tooltipSize.y
+ );
+
+ GUI.Box(tooltipRect, pendingTooltipText);
+ }
+ }
+
+ ///
+ /// 绘制单行数据
+ ///
+ private void DrawFaceDataRow(FaceData data)
+ {
+ EditorGUILayout.BeginHorizontal("box");
+
+ // Id(只读)
+ GUI.enabled = false;
+ EditorGUILayout.IntField(data.Id, GUILayout.Width(50));
+ GUI.enabled = true;
+
+ // NameKey(可编辑)
+ data.NameKey = EditorGUILayout.TextField(data.NameKey, GUILayout.Width(150));
+
+ // 中文名称(只读预览,带即时多语言显示)
+ GUI.enabled = false;
+ string zhName = "未找到语言Key";
+
+ if (languageDict.ContainsKey(data.NameKey))
+ {
+ var langs = languageDict[data.NameKey];
+ if (langs.ContainsKey("zh_CN"))
+ {
+ zhName = langs["zh_CN"];
+ }
+ }
+
+ Rect nameRect = GUILayoutUtility.GetRect(new GUIContent(zhName), GUI.skin.textField, GUILayout.Width(120));
+ EditorGUI.TextField(nameRect, zhName);
+
+ // 检测鼠标悬停并准备tooltip内容(不立即绘制)
+ if (nameRect.Contains(Event.current.mousePosition) && languageDict.ContainsKey(data.NameKey))
+ {
+ var langs = languageDict[data.NameKey];
+ List tooltipLines = new List();
+
+ if (langs.ContainsKey("zh_CN") && !string.IsNullOrEmpty(langs["zh_CN"]))
+ {
+ tooltipLines.Add($"[中文] {langs["zh_CN"]}");
+ }
+ if (langs.ContainsKey("en_US") && !string.IsNullOrEmpty(langs["en_US"]))
+ {
+ tooltipLines.Add($"[English] {langs["en_US"]}");
+ }
+ if (langs.ContainsKey("pt_BR") && !string.IsNullOrEmpty(langs["pt_BR"]))
+ {
+ tooltipLines.Add($"[Português] {langs["pt_BR"]}");
+ }
+
+ if (tooltipLines.Count > 0)
+ {
+ pendingTooltipText = string.Join("\n", tooltipLines);
+ Repaint();
+ }
+ }
+
+ GUI.enabled = true;
+
+ // Init(Checkbox)
+ bool initChecked = data.Init == 1;
+ bool newInitChecked = EditorGUILayout.Toggle(initChecked, GUILayout.Width(50));
+ data.Init = newInitChecked ? 1 : 0;
+
+ // Icon(下拉列表,包含未选择选项)
+ var iconItems = headTableSO.Items;
+ var iconNamesList = new List { "未选择" };
+ iconNamesList.AddRange(iconItems.Select(x => x.Name));
+ var iconNames = iconNamesList.ToArray();
+
+ // 查找当前选中的索引
+ int currentIndex = 0; // 默认为"未选择"
+ var currentItem = iconItems.Find(x => x.Id == data.IconId);
+ if (currentItem != null)
+ {
+ // 找到了对应的Item,索引需要+1(因为第0项是"未选择")
+ int itemIndex = iconItems.IndexOf(currentItem);
+ if (itemIndex >= 0)
+ {
+ currentIndex = itemIndex + 1;
+ }
+ }
+
+ EditorGUI.BeginChangeCheck();
+ int newIndex = EditorGUILayout.Popup(currentIndex, iconNames, GUILayout.Width(200));
+ if (EditorGUI.EndChangeCheck())
+ {
+ if (newIndex == 0)
+ {
+ // 选择了"未选择"
+ data.IconId = -1;
+ }
+ else if (newIndex > 0 && newIndex <= iconItems.Count)
+ {
+ // 选择了具体的Icon(索引需要-1)
+ data.IconId = iconItems[newIndex - 1].Id;
+ }
+ }
+
+ // 预览和Desc提示
+ if (data.IconId >= 0)
+ {
+ var item = iconItems.Find(x => x.Id == data.IconId);
+ if (item != null)
+ {
+ // 直接使用Sprite引用
+ Sprite sprite = item.Sprite;
+
+ if (sprite != null && sprite.texture != null)
+ {
+ // 正确处理图集sprite的预览
+ Rect previewRect = GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
+
+ // 绘制背景
+ EditorGUI.DrawRect(previewRect, new Color(0.5f, 0.5f, 0.5f, 1f));
+
+ Rect texCoords = sprite.textureRect;
+ Texture2D tex = sprite.texture;
+
+ // 归一化UV坐标
+ Rect normalizedCoords = new Rect(
+ texCoords.x / tex.width,
+ texCoords.y / tex.height,
+ texCoords.width / tex.width,
+ texCoords.height / tex.height
+ );
+
+ // 计算保持宽高比的显示区域
+ float aspect = texCoords.width / texCoords.height;
+ Rect drawRect = previewRect;
+ if (aspect > 1f)
+ {
+ float height = drawRect.width / aspect;
+ drawRect.y += (drawRect.height - height) * 0.5f;
+ drawRect.height = height;
+ }
+ else
+ {
+ float width = drawRect.height * aspect;
+ drawRect.x += (drawRect.width - width) * 0.5f;
+ drawRect.width = width;
+ }
+
+ GUI.DrawTextureWithTexCoords(drawRect, tex, normalizedCoords, true);
+
+ // 添加Tooltip
+ if (!string.IsNullOrEmpty(item.Desc))
+ {
+ GUI.Label(previewRect, new GUIContent("", item.Desc));
+ }
+ }
+ else
+ {
+ GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
+ GUILayout.Space(-50);
+ EditorGUILayout.LabelField("无图片", GUILayout.Width(50));
+ }
+ }
+ else
+ {
+ // ID存在但找不到对应的Item
+ GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
+ GUILayout.Space(-50);
+ EditorGUILayout.LabelField("未选择", GUILayout.Width(50));
+ }
+ }
+ else
+ {
+ // IconId < 0,未选择状态
+ GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
+ GUILayout.Space(-50);
+ EditorGUILayout.LabelField("未选择", GUILayout.Width(50));
+ }
+
+ // 删除按钮
+ GUI.backgroundColor = Color.red;
+ if (GUILayout.Button("删除", GUILayout.Width(60)))
+ {
+ if (EditorUtility.DisplayDialog("确认删除",
+ $"确定要删除头像 {data.Id} ({data.NameKey}) 吗?",
+ "删除", "取消"))
+ {
+ faceDataList.Remove(data);
+ }
+ }
+ GUI.backgroundColor = Color.white;
+
+ EditorGUILayout.EndHorizontal();
+ }
+
+ ///
+ /// 添加新头像
+ ///
+ private void AddNewFace()
+ {
+ // 计算新ID(当前最大ID + 1)
+ int newId = 1;
+ if (faceDataList.Count > 0)
+ {
+ newId = faceDataList.Max(x => x.Id) + 1;
+ }
+
+ var newFace = new FaceData
+ {
+ Id = newId,
+ NameKey = "",
+ Init = 0,
+ IconId = -1
+ };
+
+ faceDataList.Add(newFace);
+
+ // 滚动到底部
+ scrollPosition = new Vector2(0, float.MaxValue);
+ }
+
+ ///
+ /// 保存数据到Excel
+ ///
+ private void SaveData()
+ {
+ try
+ {
+ string faceExcelPath = Path.Combine(docsRootPath, "config", FACE_EXCEL_NAME);
+ if (!File.Exists(faceExcelPath))
+ {
+ EditorUtility.DisplayDialog("错误", $"未找到Face配置文件: {faceExcelPath}", "确定");
+ return;
+ }
+
+ using (var package = new ExcelPackage(new FileInfo(faceExcelPath)))
+ {
+ var worksheet = package.Workbook.Worksheets[FACE_SHEET_NAME];
+ if (worksheet == null)
+ {
+ EditorUtility.DisplayDialog("错误", $"Face.xlsx中未找到Sheet: {FACE_SHEET_NAME}", "确定");
+ return;
+ }
+
+ // 查找列索引
+ int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1;
+ int columnCount = worksheet.Dimension.Columns;
+
+ for (int col = 1; col <= columnCount; col++)
+ {
+ string header = worksheet.Cells[1, col].Text;
+ switch (header)
+ {
+ case "Id":
+ idCol = col;
+ break;
+ case "NameKey":
+ nameKeyCol = col;
+ break;
+ case "Init":
+ initCol = col;
+ break;
+ case "Icon":
+ iconCol = col;
+ break;
+ }
+ }
+
+ // 更新和删除数据(从第3行开始)
+ int rowCount = worksheet.Dimension.Rows;
+ var processedIds = new HashSet();
+
+ // 第一遍:更新现有行或标记删除
+ for (int row = rowCount; row >= 3; row--)
+ {
+ string idText = worksheet.Cells[row, idCol].Text;
+ if (string.IsNullOrEmpty(idText)) continue;
+
+ if (!int.TryParse(idText, out int id)) continue;
+
+ // 查找对应的FaceData
+ var faceData = faceDataList.Find(x => x.Id == id);
+ if (faceData != null)
+ {
+ // 更新数据
+ worksheet.Cells[row, nameKeyCol].Value = faceData.NameKey;
+ worksheet.Cells[row, initCol].Value = faceData.Init;
+
+ // IconId为-1时写入空字符串,否则写入实际值
+ if (faceData.IconId < 0)
+ {
+ worksheet.Cells[row, iconCol].Value = "";
+ }
+ else
+ {
+ worksheet.Cells[row, iconCol].Value = faceData.IconId;
+ }
+
+ processedIds.Add(id);
+ }
+ else
+ {
+ // 删除行
+ worksheet.DeleteRow(row);
+ }
+ }
+
+ // 第二遍:添加新行
+ int currentRow = worksheet.Dimension?.Rows ?? 2;
+ foreach (var faceData in faceDataList)
+ {
+ if (!processedIds.Contains(faceData.Id))
+ {
+ // 这是新增的数据
+ currentRow++;
+ worksheet.Cells[currentRow, idCol].Value = faceData.Id;
+ worksheet.Cells[currentRow, nameKeyCol].Value = faceData.NameKey;
+ worksheet.Cells[currentRow, initCol].Value = faceData.Init;
+
+ if (faceData.IconId < 0)
+ {
+ worksheet.Cells[currentRow, iconCol].Value = "";
+ }
+ else
+ {
+ worksheet.Cells[currentRow, iconCol].Value = faceData.IconId;
+ }
+ }
+ }
+
+ // 保存文件
+ package.Save();
+ }
+
+ // 提示成功并提醒推送
+ bool understood = EditorUtility.DisplayDialog("保存成功",
+ "配置已保存到Excel文件!\n\n" +
+ "⚠️ 重要提醒:\n" +
+ "请及时在SourceTree或GitHubDesktop中:\n" +
+ "1. 提交(Commit)本次修改\n" +
+ "2. 推送(Push)到远程仓库\n\n" +
+ "避免与其他策划产生冲突!",
+ "我知道了");
+ }
+ catch (Exception e)
+ {
+ EditorUtility.DisplayDialog("错误", $"保存数据失败: {e.Message}\n{e.StackTrace}", "确定");
+ }
+ }
+
+ ///
+ /// 头像数据类
+ ///
+ private class FaceData
+ {
+ public int Id;
+ public string NameKey;
+ public int Init;
+ public int IconId;
+ }
+ }
+}
diff --git a/Scripts/Editor/Design_Tools/Collections/HeadConfigEditor.cs.meta b/Scripts/Editor/Design_Tools/Collections/HeadConfigEditor.cs.meta
new file mode 100644
index 0000000..e550a2c
--- /dev/null
+++ b/Scripts/Editor/Design_Tools/Collections/HeadConfigEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b3c6d1239cdbf0748b5fb75f163c3121
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/Design_Tools/Collections/HeadFrameConfigEditor.cs b/Scripts/Editor/Design_Tools/Collections/HeadFrameConfigEditor.cs
new file mode 100644
index 0000000..437ae57
--- /dev/null
+++ b/Scripts/Editor/Design_Tools/Collections/HeadFrameConfigEditor.cs
@@ -0,0 +1,965 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+using OfficeOpenXml;
+using ArtResource;
+using Debug = UnityEngine.Debug;
+
+namespace DesignTools.Collections
+{
+ ///
+ /// 头像框配置Editor工具
+ /// 读取Docs/config/Avatar.xlsx,关联Art_SO/Collections/HeadFrameResource.asset
+ ///
+ public class HeadFrameConfigEditor : EditorWindow
+ {
+ private const string HEADFRAME_SO_PATH = "Assets/Art_SubModule/Art_SO/Collections";
+ private const string HEADFRAME_SO_NAME = "HeadFrameResource";
+ private const string AVATAR_EXCEL_NAME = "Avatar.xlsx";
+ private const string LANGUAGE_EXCEL_NAME = "AllLanguage.xlsx";
+ private const string AVATAR_SHEET_NAME = "Avatar";
+ private const string LANGUAGE_SHEET_NAME = "client";
+ private const string DOCS_PATH_PREF_KEY = "HeadFrameConfigEditor_DocsPath";
+ private const string DESIGN_SUBMODULE_PATH = "Assets/Design_SubModule";
+
+ private string docsRootPath = "";
+ private List headFrameDataList = new List();
+ private ArtTableSO headFrameTableSO;
+ private Dictionary> languageDict = new Dictionary>();
+ private Vector2 scrollPosition;
+ private bool isDataLoaded = false;
+ private string pendingTooltipText = "";
+
+ [MenuItem("策划工具/收藏品/头像框")]
+ public static void ShowWindow()
+ {
+ var window = GetWindow("头像框配置");
+ window.minSize = new Vector2(1000, 600);
+ window.Show();
+ }
+
+ private void OnEnable()
+ {
+ // 读取上次保存的路径
+ if (EditorPrefs.HasKey(DOCS_PATH_PREF_KEY))
+ {
+ docsRootPath = EditorPrefs.GetString(DOCS_PATH_PREF_KEY);
+ }
+ }
+
+ private void OnGUI()
+ {
+ EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+ EditorGUILayout.LabelField("头像框配置工具", EditorStyles.boldLabel);
+ GUILayout.FlexibleSpace();
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.Space(5);
+
+ // Docs路径选择
+ EditorGUILayout.BeginVertical("box");
+ EditorGUILayout.LabelField("Docs项目根目录", EditorStyles.boldLabel);
+
+ EditorGUILayout.BeginHorizontal();
+ docsRootPath = EditorGUILayout.TextField("路径", docsRootPath);
+ if (GUILayout.Button("选择文件夹", GUILayout.Width(100)))
+ {
+ string selectedPath = EditorUtility.OpenFolderPanel("选择Docs项目根目录", "", "");
+ if (!string.IsNullOrEmpty(selectedPath))
+ {
+ docsRootPath = selectedPath;
+ EditorPrefs.SetString(DOCS_PATH_PREF_KEY, docsRootPath);
+ }
+ }
+ EditorGUILayout.EndHorizontal();
+
+ if (GUILayout.Button("加载配置数据", GUILayout.Height(30)))
+ {
+ LoadData();
+ }
+
+ EditorGUILayout.EndVertical();
+
+ EditorGUILayout.Space(5);
+
+ // 数据编辑区域
+ if (isDataLoaded)
+ {
+ DrawDataEditor();
+ }
+ else
+ {
+ EditorGUILayout.HelpBox("请先选择Docs根目录并加载配置数据", MessageType.Info);
+ }
+ }
+
+ ///
+ /// 加载配置数据
+ ///
+ private void LoadData()
+ {
+ try
+ {
+ // 校验路径
+ if (string.IsNullOrEmpty(docsRootPath) || !Directory.Exists(docsRootPath))
+ {
+ EditorUtility.DisplayDialog("错误", "请选择有效的Docs根目录", "确定");
+ return;
+ }
+
+ // 1. 检查Docs是否为Git仓库并更新
+ if (!CheckAndUpdateDocsRepository())
+ {
+ return;
+ }
+
+ // 2. 检查Design_SubModule分支
+ if (!CheckAndSwitchDesignSubModuleBranch())
+ {
+ return;
+ }
+
+ // 校验HeadFrame SO是否存在
+ string headFrameSOPath = Path.Combine(HEADFRAME_SO_PATH, $"{HEADFRAME_SO_NAME}.asset");
+ headFrameTableSO = AssetDatabase.LoadAssetAtPath(headFrameSOPath);
+
+ if (headFrameTableSO == null)
+ {
+ EditorUtility.DisplayDialog("错误",
+ $"未找到头像框资源配置\n路径: {headFrameSOPath}\n\n请先在美术资源配置工具中创建",
+ "确定");
+ return;
+ }
+
+ if (headFrameTableSO.Items == null || headFrameTableSO.Items.Count == 0)
+ {
+ EditorUtility.DisplayDialog("错误", "头像框资源配置数据为空", "确定");
+ return;
+ }
+
+ // 加载语言表
+ LoadLanguageData();
+
+ // 加载Avatar.xlsx
+ LoadAvatarExcel();
+
+ isDataLoaded = true;
+ EditorUtility.DisplayDialog("成功", "配置数据加载成功!", "确定");
+ }
+ catch (Exception e)
+ {
+ EditorUtility.DisplayDialog("错误", $"加载数据失败: {e.Message}\n{e.StackTrace}", "确定");
+ isDataLoaded = false;
+ }
+ }
+
+ ///
+ /// 执行Git命令
+ ///
+ private string ExecuteGitCommand(string workingDirectory, string arguments, out bool success)
+ {
+ try
+ {
+ ProcessStartInfo startInfo = new ProcessStartInfo
+ {
+ FileName = "git",
+ Arguments = arguments,
+ WorkingDirectory = workingDirectory,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using (Process process = Process.Start(startInfo))
+ {
+ string output = process.StandardOutput.ReadToEnd();
+ string error = process.StandardError.ReadToEnd();
+ process.WaitForExit();
+
+ success = process.ExitCode == 0;
+ return success ? output : error;
+ }
+ }
+ catch (Exception e)
+ {
+ success = false;
+ return $"执行Git命令失败: {e.Message}";
+ }
+ }
+
+ ///
+ /// 检查并更新Docs仓库
+ ///
+ private bool CheckAndUpdateDocsRepository()
+ {
+ string gitPath = Path.Combine(docsRootPath, ".git");
+ if (!Directory.Exists(gitPath))
+ {
+ EditorUtility.DisplayDialog("错误",
+ $"Docs目录不是Git仓库\n路径: {docsRootPath}\n\n请确保Docs项目已正确克隆",
+ "确定");
+ return false;
+ }
+
+ EditorUtility.DisplayProgressBar("检查更新", "正在检查Docs仓库远程更新...", 0.3f);
+
+ string fetchResult = ExecuteGitCommand(docsRootPath, "fetch", out bool fetchSuccess);
+
+ if (!fetchSuccess)
+ {
+ EditorUtility.ClearProgressBar();
+ EditorUtility.DisplayDialog("Git Fetch失败",
+ $"无法检查远程更新:\n{fetchResult}\n\n请在SourceTree或GitHubDesktop中检查网络连接和仓库状态",
+ "确定");
+ return false;
+ }
+
+ EditorUtility.DisplayProgressBar("检查更新", "正在检查是否有新提交...", 0.6f);
+
+ string statusResult = ExecuteGitCommand(docsRootPath, "status -uno", out bool statusSuccess);
+
+ if (!statusSuccess)
+ {
+ EditorUtility.ClearProgressBar();
+ EditorUtility.DisplayDialog("Git Status失败",
+ $"无法获取仓库状态:\n{statusResult}\n\n请在SourceTree或GitHubDesktop中检查仓库状态",
+ "确定");
+ return false;
+ }
+
+ if (statusResult.Contains("Changes not staged") || statusResult.Contains("Changes to be committed"))
+ {
+ EditorUtility.ClearProgressBar();
+ bool proceed = EditorUtility.DisplayDialog("警告:有未提交的更改",
+ "Docs仓库中有未提交的更改,这可能导致拉取时产生冲突。\n\n建议先提交或暂存这些更改。\n\n是否继续加载配置?(不推荐)",
+ "继续(不推荐)", "取消,前往处理");
+
+ if (!proceed)
+ {
+ Debug.Log("请在SourceTree或GitHubDesktop中处理未提交的更改");
+ return false;
+ }
+ }
+
+ if (statusResult.Contains("Your branch is behind"))
+ {
+ EditorUtility.DisplayProgressBar("更新中", "正在从远程拉取最新代码...", 0.8f);
+
+ string pullResult = ExecuteGitCommand(docsRootPath, "pull", out bool pullSuccess);
+
+ EditorUtility.ClearProgressBar();
+
+ if (!pullSuccess)
+ {
+ if (pullResult.Contains("CONFLICT") || pullResult.Contains("conflict"))
+ {
+ EditorUtility.DisplayDialog("拉取失败:存在冲突",
+ $"拉取远程更新时发生冲突:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中解决冲突后再操作",
+ "确定");
+ }
+ else
+ {
+ EditorUtility.DisplayDialog("拉取失败",
+ $"无法拉取远程更新:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中检查并解决问题",
+ "确定");
+ }
+ return false;
+ }
+
+ Debug.Log($"Docs仓库已更新到最新版本:\n{pullResult}");
+ EditorUtility.DisplayDialog("更新成功", "Docs仓库已更新到最新版本", "确定");
+ }
+ else
+ {
+ EditorUtility.ClearProgressBar();
+ Debug.Log("Docs仓库已是最新版本");
+ }
+
+ return true;
+ }
+
+ ///
+ /// 检查并切换Design_SubModule到main分支
+ ///
+ private bool CheckAndSwitchDesignSubModuleBranch()
+ {
+ string designSubModulePath = Path.Combine(Application.dataPath, "..", DESIGN_SUBMODULE_PATH);
+ designSubModulePath = Path.GetFullPath(designSubModulePath);
+
+ if (!Directory.Exists(designSubModulePath))
+ {
+ EditorUtility.DisplayDialog("错误",
+ $"Design_SubModule目录不存在:\n{designSubModulePath}\n\n请确保子模块已正确初始化",
+ "确定");
+ return false;
+ }
+
+ EditorUtility.DisplayProgressBar("检查分支", "正在检查Design_SubModule分支...", 0.5f);
+
+ string branchResult = ExecuteGitCommand(designSubModulePath, "branch --show-current", out bool branchSuccess);
+
+ if (!branchSuccess)
+ {
+ EditorUtility.ClearProgressBar();
+ EditorUtility.DisplayDialog("Git Branch失败",
+ $"无法获取当前分支:\n{branchResult}\n\n请在SourceTree或GitHubDesktop中检查Design_SubModule状态",
+ "确定");
+ return false;
+ }
+
+ string currentBranch = branchResult.Trim();
+
+ if (currentBranch != "main")
+ {
+ EditorUtility.DisplayProgressBar("切换分支", "正在切换到main分支...", 0.8f);
+
+ string checkoutResult = ExecuteGitCommand(designSubModulePath, "checkout main", out bool checkoutSuccess);
+
+ EditorUtility.ClearProgressBar();
+
+ if (!checkoutSuccess)
+ {
+ EditorUtility.DisplayDialog("切换分支失败",
+ $"无法切换到main分支:\n{checkoutResult}\n\n当前分支: {currentBranch}\n\n请在SourceTree或GitHubDesktop中手动切换到main分支",
+ "确定");
+ return false;
+ }
+
+ Debug.Log($"Design_SubModule已从 {currentBranch} 切换到 main 分支");
+ EditorUtility.DisplayDialog("分支切换成功",
+ $"Design_SubModule已从 {currentBranch} 切换到 main 分支",
+ "确定");
+ }
+ else
+ {
+ EditorUtility.ClearProgressBar();
+ Debug.Log("Design_SubModule已在main分支");
+ }
+
+ return true;
+ }
+
+ ///
+ /// 加载语言数据
+ ///
+ private void LoadLanguageData()
+ {
+ languageDict.Clear();
+
+ string languageExcelPath = Path.Combine(docsRootPath, "config", LANGUAGE_EXCEL_NAME);
+ if (!File.Exists(languageExcelPath))
+ {
+ Debug.LogWarning($"未找到语言表: {languageExcelPath}");
+ return;
+ }
+
+ using (var package = new ExcelPackage(new FileInfo(languageExcelPath)))
+ {
+ var worksheet = package.Workbook.Worksheets[LANGUAGE_SHEET_NAME];
+ if (worksheet == null)
+ {
+ Debug.LogWarning($"语言表中未找到Sheet: {LANGUAGE_SHEET_NAME}");
+ return;
+ }
+
+ // 查找Key列和语言列
+ int keyColumnIndex = -1;
+ int zhCNColumnIndex = -1;
+ int enUSColumnIndex = -1;
+ int ptBRColumnIndex = -1;
+ int columnCount = worksheet.Dimension.Columns;
+
+ for (int col = 1; col <= columnCount; col++)
+ {
+ string header = worksheet.Cells[1, col].Text;
+ if (header == "key")
+ {
+ keyColumnIndex = col;
+ }
+ else if (header == "zh_CN")
+ {
+ zhCNColumnIndex = col;
+ }
+ else if (header == "en_US")
+ {
+ enUSColumnIndex = col;
+ }
+ else if (header == "pt_BR")
+ {
+ ptBRColumnIndex = col;
+ }
+ }
+
+ if (keyColumnIndex < 0)
+ {
+ Debug.LogWarning("语言表中未找到Key列");
+ return;
+ }
+
+ // 读取数据(从第3行开始)
+ int rowCount = worksheet.Dimension.Rows;
+ for (int row = 3; row <= rowCount; row++)
+ {
+ string key = worksheet.Cells[row, keyColumnIndex].Text;
+ if (string.IsNullOrEmpty(key)) continue;
+
+ if (!languageDict.ContainsKey(key))
+ {
+ languageDict[key] = new Dictionary();
+ }
+
+ if (zhCNColumnIndex > 0)
+ {
+ languageDict[key]["zh_CN"] = worksheet.Cells[row, zhCNColumnIndex].Text;
+ }
+ if (enUSColumnIndex > 0)
+ {
+ languageDict[key]["en_US"] = worksheet.Cells[row, enUSColumnIndex].Text;
+ }
+ if (ptBRColumnIndex > 0)
+ {
+ languageDict[key]["pt_BR"] = worksheet.Cells[row, ptBRColumnIndex].Text;
+ }
+ }
+ }
+ }
+
+ ///
+ /// 加载Avatar.xlsx
+ ///
+ private void LoadAvatarExcel()
+ {
+ headFrameDataList.Clear();
+
+ string avatarExcelPath = Path.Combine(docsRootPath, "config", AVATAR_EXCEL_NAME);
+ if (!File.Exists(avatarExcelPath))
+ {
+ throw new Exception($"未找到Avatar配置文件: {avatarExcelPath}");
+ }
+
+ using (var package = new ExcelPackage(new FileInfo(avatarExcelPath)))
+ {
+ var worksheet = package.Workbook.Worksheets[AVATAR_SHEET_NAME];
+ if (worksheet == null)
+ {
+ throw new Exception($"Avatar.xlsx中未找到Sheet: {AVATAR_SHEET_NAME}");
+ }
+
+ // 读取表头(第1行)
+ int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1, frameImageScaleCol = -1;
+ int columnCount = worksheet.Dimension.Columns;
+
+ for (int col = 1; col <= columnCount; col++)
+ {
+ string header = worksheet.Cells[1, col].Text;
+ switch (header)
+ {
+ case "Id":
+ idCol = col;
+ break;
+ case "NameKey":
+ nameKeyCol = col;
+ break;
+ case "Init":
+ initCol = col;
+ break;
+ case "Icon":
+ iconCol = col;
+ break;
+ case "FrameImageScale":
+ frameImageScaleCol = col;
+ break;
+ }
+ }
+
+ if (idCol < 0 || nameKeyCol < 0 || initCol < 0 || iconCol < 0 || frameImageScaleCol < 0)
+ {
+ throw new Exception("Avatar.xlsx表结构不正确,缺少必要列(Id/NameKey/Init/Icon/FrameImageScale)");
+ }
+
+ // 读取数据(从第3行开始)
+ int rowCount = worksheet.Dimension.Rows;
+ for (int row = 3; row <= rowCount; row++)
+ {
+ string idText = worksheet.Cells[row, idCol].Text;
+ if (string.IsNullOrEmpty(idText)) continue;
+
+ if (!int.TryParse(idText, out int id)) continue;
+
+ string nameKey = worksheet.Cells[row, nameKeyCol].Text;
+ string initText = worksheet.Cells[row, initCol].Text;
+ string iconText = worksheet.Cells[row, iconCol].Text;
+ string frameImageScaleText = worksheet.Cells[row, frameImageScaleCol].Text;
+
+ int init = 0;
+ if (!string.IsNullOrEmpty(initText))
+ {
+ int.TryParse(initText, out init);
+ }
+
+ int iconId = -1;
+ if (!string.IsNullOrEmpty(iconText))
+ {
+ if (!int.TryParse(iconText, out iconId))
+ {
+ iconId = -1;
+ }
+ }
+
+ float frameImageScale = 1.0f;
+ if (!string.IsNullOrEmpty(frameImageScaleText))
+ {
+ if (!float.TryParse(frameImageScaleText, out frameImageScale))
+ {
+ frameImageScale = 1.0f;
+ }
+ }
+
+ var frameData = new HeadFrameData
+ {
+ Id = id,
+ NameKey = nameKey,
+ Init = init,
+ IconId = iconId,
+ FrameImageScale = frameImageScale
+ };
+
+ headFrameDataList.Add(frameData);
+ }
+ }
+ }
+
+ ///
+ /// 绘制数据编辑器
+ ///
+ private void DrawDataEditor()
+ {
+ pendingTooltipText = "";
+
+ EditorGUILayout.BeginVertical("box");
+
+ EditorGUILayout.BeginHorizontal();
+ EditorGUILayout.LabelField($"头像框数据列表(共 {headFrameDataList.Count} 条)", EditorStyles.boldLabel);
+ GUILayout.FlexibleSpace();
+
+ GUI.backgroundColor = Color.cyan;
+ if (GUILayout.Button("+ 添加头像框", GUILayout.Height(25), GUILayout.Width(100)))
+ {
+ AddNewHeadFrame();
+ }
+ GUI.backgroundColor = Color.white;
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.Space(3);
+
+ scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
+
+ // 表头
+ EditorGUILayout.BeginHorizontal("box");
+ EditorGUILayout.LabelField("Id", EditorStyles.boldLabel, GUILayout.Width(50));
+ EditorGUILayout.LabelField("NameKey", EditorStyles.boldLabel, GUILayout.Width(150));
+ EditorGUILayout.LabelField("中文名称", EditorStyles.boldLabel, GUILayout.Width(120));
+ EditorGUILayout.LabelField("Init", EditorStyles.boldLabel, GUILayout.Width(50));
+ EditorGUILayout.LabelField("Icon", EditorStyles.boldLabel, GUILayout.Width(200));
+ EditorGUILayout.LabelField("预览", EditorStyles.boldLabel, GUILayout.Width(100));
+ EditorGUILayout.LabelField("缩放", EditorStyles.boldLabel, GUILayout.Width(80));
+ EditorGUILayout.LabelField("操作", EditorStyles.boldLabel, GUILayout.Width(60));
+ EditorGUILayout.EndHorizontal();
+
+ // 数据行
+ for (int i = 0; i < headFrameDataList.Count; i++)
+ {
+ DrawHeadFrameDataRow(headFrameDataList[i]);
+ }
+
+ EditorGUILayout.EndScrollView();
+ EditorGUILayout.EndVertical();
+
+ EditorGUILayout.Space(10);
+
+ // 保存按钮
+ EditorGUILayout.BeginHorizontal();
+ GUILayout.FlexibleSpace();
+
+ GUI.backgroundColor = Color.green;
+ if (GUILayout.Button("保存配置到Excel", GUILayout.Height(30), GUILayout.Width(200)))
+ {
+ SaveData();
+ }
+ GUI.backgroundColor = Color.white;
+
+ GUILayout.FlexibleSpace();
+ EditorGUILayout.EndHorizontal();
+
+ // 在所有内容绘制完后,绘制tooltip(确保在最上层)
+ if (!string.IsNullOrEmpty(pendingTooltipText))
+ {
+ Vector2 tooltipSize = GUI.skin.box.CalcSize(new GUIContent(pendingTooltipText));
+ tooltipSize.x += 10;
+ tooltipSize.y += 10;
+
+ // 使用当前鼠标位置,而不是之前记录的位置
+ Vector2 mousePos = Event.current.mousePosition;
+ Rect tooltipRect = new Rect(
+ mousePos.x + 1,
+ mousePos.y + 1,
+ tooltipSize.x,
+ tooltipSize.y
+ );
+
+ GUI.Box(tooltipRect, pendingTooltipText);
+ }
+ }
+
+ ///
+ /// 绘制单行数据
+ ///
+ private void DrawHeadFrameDataRow(HeadFrameData data)
+ {
+ EditorGUILayout.BeginHorizontal("box");
+
+ // Id(可编辑)
+ data.Id = EditorGUILayout.IntField(data.Id, GUILayout.Width(50));
+
+ // NameKey(可编辑)
+ data.NameKey = EditorGUILayout.TextField(data.NameKey, GUILayout.Width(150));
+
+ // 中文名称(只读预览,带即时多语言显示)
+ GUI.enabled = false;
+ string zhName = "未找到语言Key";
+
+ if (languageDict.ContainsKey(data.NameKey))
+ {
+ var langs = languageDict[data.NameKey];
+ if (langs.ContainsKey("zh_CN"))
+ {
+ zhName = langs["zh_CN"];
+ }
+ }
+
+ Rect nameRect = GUILayoutUtility.GetRect(new GUIContent(zhName), GUI.skin.textField, GUILayout.Width(120));
+ EditorGUI.TextField(nameRect, zhName);
+
+ // 检测鼠标悬停并准备tooltip内容(不立即绘制)
+ if (nameRect.Contains(Event.current.mousePosition) && languageDict.ContainsKey(data.NameKey))
+ {
+ var langs = languageDict[data.NameKey];
+ List tooltipLines = new List();
+
+ if (langs.ContainsKey("zh_CN") && !string.IsNullOrEmpty(langs["zh_CN"]))
+ {
+ tooltipLines.Add($"[中文] {langs["zh_CN"]}");
+ }
+ if (langs.ContainsKey("en_US") && !string.IsNullOrEmpty(langs["en_US"]))
+ {
+ tooltipLines.Add($"[English] {langs["en_US"]}");
+ }
+ if (langs.ContainsKey("pt_BR") && !string.IsNullOrEmpty(langs["pt_BR"]))
+ {
+ tooltipLines.Add($"[Português] {langs["pt_BR"]}");
+ }
+
+ if (tooltipLines.Count > 0)
+ {
+ pendingTooltipText = string.Join("\n", tooltipLines);
+ Repaint();
+ }
+ }
+
+ GUI.enabled = true;
+
+ // Init(Checkbox)
+ bool initChecked = data.Init == 1;
+ bool newInitChecked = EditorGUILayout.Toggle(initChecked, GUILayout.Width(50));
+ data.Init = newInitChecked ? 1 : 0;
+
+ // Icon(下拉列表)
+ var iconItems = headFrameTableSO.Items;
+ var iconNamesList = new List { "未选择" };
+ iconNamesList.AddRange(iconItems.Select(x => x.Name));
+ var iconNames = iconNamesList.ToArray();
+
+ int currentIndex = 0;
+ var currentItem = iconItems.Find(x => x.Id == data.IconId);
+ if (currentItem != null)
+ {
+ int itemIndex = iconItems.IndexOf(currentItem);
+ if (itemIndex >= 0)
+ {
+ currentIndex = itemIndex + 1;
+ }
+ }
+
+ EditorGUI.BeginChangeCheck();
+ int newIndex = EditorGUILayout.Popup(currentIndex, iconNames, GUILayout.Width(200));
+ if (EditorGUI.EndChangeCheck())
+ {
+ if (newIndex == 0)
+ {
+ data.IconId = -1;
+ }
+ else if (newIndex > 0 && newIndex <= iconItems.Count)
+ {
+ data.IconId = iconItems[newIndex - 1].Id;
+ }
+ }
+
+ // 预览
+ if (data.IconId >= 0)
+ {
+ var item = iconItems.Find(x => x.Id == data.IconId);
+ if (item != null)
+ {
+ Sprite sprite = item.Sprite;
+
+ if (sprite != null && sprite.texture != null)
+ {
+ Rect previewRect = GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
+
+ EditorGUI.DrawRect(previewRect, new Color(0.5f, 0.5f, 0.5f, 1f));
+
+ Rect texCoords = sprite.textureRect;
+ Texture2D tex = sprite.texture;
+
+ Rect normalizedCoords = new Rect(
+ texCoords.x / tex.width,
+ texCoords.y / tex.height,
+ texCoords.width / tex.width,
+ texCoords.height / tex.height
+ );
+
+ float aspect = texCoords.width / texCoords.height;
+ Rect drawRect = previewRect;
+ if (aspect > 1f)
+ {
+ float height = drawRect.width / aspect;
+ drawRect.y += (drawRect.height - height) * 0.5f;
+ drawRect.height = height;
+ }
+ else
+ {
+ float width = drawRect.height * aspect;
+ drawRect.x += (drawRect.width - width) * 0.5f;
+ drawRect.width = width;
+ }
+
+ GUI.DrawTextureWithTexCoords(drawRect, tex, normalizedCoords, true);
+
+ if (!string.IsNullOrEmpty(item.Desc))
+ {
+ GUI.Label(previewRect, new GUIContent("", item.Desc));
+ }
+ }
+ else
+ {
+ GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
+ GUILayout.Space(-50);
+ EditorGUILayout.LabelField("无图片", GUILayout.Width(50));
+ }
+ }
+ else
+ {
+ GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
+ GUILayout.Space(-50);
+ EditorGUILayout.LabelField("未选择", GUILayout.Width(50));
+ }
+ }
+ else
+ {
+ GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
+ GUILayout.Space(-50);
+ EditorGUILayout.LabelField("未选择", GUILayout.Width(50));
+ }
+
+ // FrameImageScale(float输入框)
+ data.FrameImageScale = EditorGUILayout.FloatField(data.FrameImageScale, GUILayout.Width(80));
+
+ // 删除按钮
+ GUI.backgroundColor = Color.red;
+ if (GUILayout.Button("删除", GUILayout.Width(60)))
+ {
+ if (EditorUtility.DisplayDialog("确认删除",
+ $"确定要删除头像框 {data.Id} ({data.NameKey}) 吗?",
+ "删除", "取消"))
+ {
+ headFrameDataList.Remove(data);
+ }
+ }
+ GUI.backgroundColor = Color.white;
+
+ EditorGUILayout.EndHorizontal();
+ }
+
+ ///
+ /// 添加新头像框
+ ///
+ private void AddNewHeadFrame()
+ {
+ int newId = 1;
+ if (headFrameDataList.Count > 0)
+ {
+ newId = headFrameDataList.Max(x => x.Id) + 1;
+ }
+
+ var newFrame = new HeadFrameData
+ {
+ Id = newId,
+ NameKey = "",
+ Init = 0,
+ IconId = -1,
+ FrameImageScale = 1.0f
+ };
+
+ headFrameDataList.Add(newFrame);
+
+ scrollPosition = new Vector2(0, float.MaxValue);
+ }
+
+ ///
+ /// 保存数据到Excel
+ ///
+ private void SaveData()
+ {
+ try
+ {
+ string avatarExcelPath = Path.Combine(docsRootPath, "config", AVATAR_EXCEL_NAME);
+ if (!File.Exists(avatarExcelPath))
+ {
+ EditorUtility.DisplayDialog("错误", $"未找到Avatar配置文件: {avatarExcelPath}", "确定");
+ return;
+ }
+
+ using (var package = new ExcelPackage(new FileInfo(avatarExcelPath)))
+ {
+ var worksheet = package.Workbook.Worksheets[AVATAR_SHEET_NAME];
+ if (worksheet == null)
+ {
+ EditorUtility.DisplayDialog("错误", $"Avatar.xlsx中未找到Sheet: {AVATAR_SHEET_NAME}", "确定");
+ return;
+ }
+
+ // 查找列索引
+ int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1, frameImageScaleCol = -1;
+ int columnCount = worksheet.Dimension.Columns;
+
+ for (int col = 1; col <= columnCount; col++)
+ {
+ string header = worksheet.Cells[1, col].Text;
+ switch (header)
+ {
+ case "Id":
+ idCol = col;
+ break;
+ case "NameKey":
+ nameKeyCol = col;
+ break;
+ case "Init":
+ initCol = col;
+ break;
+ case "Icon":
+ iconCol = col;
+ break;
+ case "FrameImageScale":
+ frameImageScaleCol = col;
+ break;
+ }
+ }
+
+ // 更新和删除数据(从第3行开始)
+ int rowCount = worksheet.Dimension.Rows;
+ var processedIds = new HashSet();
+
+ // 第一遍:更新现有行或删除
+ for (int row = rowCount; row >= 3; row--)
+ {
+ string idText = worksheet.Cells[row, idCol].Text;
+ if (string.IsNullOrEmpty(idText)) continue;
+
+ if (!int.TryParse(idText, out int id)) continue;
+
+ var frameData = headFrameDataList.Find(x => x.Id == id);
+ if (frameData != null)
+ {
+ worksheet.Cells[row, nameKeyCol].Value = frameData.NameKey;
+ worksheet.Cells[row, initCol].Value = frameData.Init;
+
+ if (frameData.IconId < 0)
+ {
+ worksheet.Cells[row, iconCol].Value = "";
+ }
+ else
+ {
+ worksheet.Cells[row, iconCol].Value = frameData.IconId;
+ }
+
+ worksheet.Cells[row, frameImageScaleCol].Value = frameData.FrameImageScale;
+
+ processedIds.Add(id);
+ }
+ else
+ {
+ worksheet.DeleteRow(row);
+ }
+ }
+
+ // 第二遍:添加新行
+ int currentRow = worksheet.Dimension?.Rows ?? 2;
+ foreach (var frameData in headFrameDataList)
+ {
+ if (!processedIds.Contains(frameData.Id))
+ {
+ currentRow++;
+ worksheet.Cells[currentRow, idCol].Value = frameData.Id;
+ worksheet.Cells[currentRow, nameKeyCol].Value = frameData.NameKey;
+ worksheet.Cells[currentRow, initCol].Value = frameData.Init;
+
+ if (frameData.IconId < 0)
+ {
+ worksheet.Cells[currentRow, iconCol].Value = "";
+ }
+ else
+ {
+ worksheet.Cells[currentRow, iconCol].Value = frameData.IconId;
+ }
+
+ worksheet.Cells[currentRow, frameImageScaleCol].Value = frameData.FrameImageScale;
+ }
+ }
+
+ package.Save();
+ }
+
+ // 提示成功并提醒推送
+ bool understood = EditorUtility.DisplayDialog("保存成功",
+ "配置已保存到Excel文件!\n\n" +
+ "⚠️ 重要提醒:\n" +
+ "请及时在SourceTree或GitHubDesktop中:\n" +
+ "1. 提交(Commit)本次修改\n" +
+ "2. 推送(Push)到远程仓库\n\n" +
+ "避免与其他策划产生冲突!",
+ "我知道了");
+ }
+ catch (Exception e)
+ {
+ EditorUtility.DisplayDialog("错误", $"保存数据失败: {e.Message}\n{e.StackTrace}", "确定");
+ }
+ }
+
+ ///
+ /// 头像框数据类
+ ///
+ private class HeadFrameData
+ {
+ public int Id;
+ public string NameKey;
+ public int Init;
+ public int IconId;
+ public float FrameImageScale;
+ }
+ }
+}
diff --git a/Scripts/Editor/Design_Tools/Collections/HeadFrameConfigEditor.cs.meta b/Scripts/Editor/Design_Tools/Collections/HeadFrameConfigEditor.cs.meta
new file mode 100644
index 0000000..380181f
--- /dev/null
+++ b/Scripts/Editor/Design_Tools/Collections/HeadFrameConfigEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 370961b2539ed9d49ade3ec27aedd25e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/Design_Tools/ItemConfigEditor.cs b/Scripts/Editor/Design_Tools/ItemConfigEditor.cs
new file mode 100644
index 0000000..41bdd8a
--- /dev/null
+++ b/Scripts/Editor/Design_Tools/ItemConfigEditor.cs
@@ -0,0 +1,1806 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+using OfficeOpenXml;
+using ArtResource;
+using Debug = UnityEngine.Debug;
+
+namespace DesignTools
+{
+ ///
+ /// Item配置Editor工具
+ /// 读取Docs/config/Item.xlsx,关联所有Art_SO资源
+ ///
+ public class ItemConfigEditor : EditorWindow
+ {
+ private const string ART_SO_PATH = "Assets/Art_SubModule/Art_SO";
+ private const string ITEM_EXCEL_NAME = "Item.xlsx";
+ private const string LIMITED_TIME_EVENT_EXCEL_NAME = "LimitedTimeEvent.xlsx";
+ private const string AVATAR_EXCEL_NAME = "Avatar.xlsx";
+ private const string FACE_EXCEL_NAME = "Face.xlsx";
+ private const string EMOJI_EXCEL_NAME = "Emoji.xlsx";
+ private const string LANGUAGE_EXCEL_NAME = "AllLanguage.xlsx";
+ private const string ITEM_SHEET_NAME = "Item";
+ private const string EVENT_SHEET_NAME = "Event";
+ private const string AVATAR_SHEET_NAME = "Avatar";
+ private const string FACE_SHEET_NAME = "Face";
+ private const string EMOJI_SHEET_NAME = "Emoji";
+ private const string LANGUAGE_SHEET_NAME = "client";
+ private const string DOCS_PATH_PREF_KEY = "ItemConfigEditor_DocsPath";
+ private const string DESIGN_SUBMODULE_PATH = "Assets/Design_SubModule";
+
+ // IType类型定义
+ private static readonly Dictionary ITEM_TYPES = new Dictionary
+ {
+ { 1, "能量" },
+ { 2, "星星" },
+ { 3, "钻石" },
+ { 97, "Playroom宠物道具" },
+ { 98, "卡牌" },
+ { 99, "背包道具" },
+ { 100, "棋子" },
+ { 101, "卡包" },
+ { 102, "限时事件" },
+ { 103, "小猪存钱罐" },
+ { 104, "万能卡" },
+ { 105, "头像框" },
+ { 106, "活动代币" },
+ { 107, "竞赛游戏代币" },
+ { 108, "Pet Playroom拜访道具" },
+ { 109, "表情" },
+ { 110, "头像" },
+ { 111, "Playroom装饰" },
+ { 112, "Playroom服装" },
+ { 113, "Playroom装饰套装" },
+ { 114, "Playroom服装套装" },
+ { 115, "Playroom道具宝箱" },
+ { 116, "活动通行证代币道具" }
+ };
+
+ private string docsRootPath = "";
+ private List allItemDataList = new List();
+ private List filteredItemDataList = new List();
+ private Dictionary limitedTimeEventDict = new Dictionary(); // Id -> Name
+ private Dictionary avatarDict = new Dictionary(); // Id -> AvatarData
+ private Dictionary faceDict = new Dictionary(); // Id -> FaceData
+ private Dictionary emojiDict = new Dictionary(); // Id -> EmojiData
+ private Dictionary> languageDict = new Dictionary>();
+ private List allArtTables = new List();
+ private Dictionary> artTablesByFolder = new Dictionary>(); // 按文件夹分组的表格
+
+ private ArtTableSO headFrameResourceSO;
+ private ArtTableSO headResourceSO;
+ private ArtTableSO emojiResourceSO;
+
+ // Res选择状态缓存 (ItemData的Id -> 选择的组名)
+ private Dictionary resSelectedFolderCache = new Dictionary();
+
+ private Vector2 scrollPosition;
+ private bool isDataLoaded = false;
+
+ // 筛选和分页
+ private int selectedIType = -1;
+ private int currentPage = 0;
+ private int itemsPerPage = 20;
+ private int totalPages = 0;
+
+ [MenuItem("策划工具/Item配置")]
+ public static void ShowWindow()
+ {
+ var window = GetWindow("Item配置");
+ window.minSize = new Vector2(1200, 700);
+ window.Show();
+ }
+
+ private void OnEnable()
+ {
+ // 读取上次保存的路径
+ if (EditorPrefs.HasKey(DOCS_PATH_PREF_KEY))
+ {
+ docsRootPath = EditorPrefs.GetString(DOCS_PATH_PREF_KEY);
+ }
+ }
+
+ private void OnGUI()
+ {
+ EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+ EditorGUILayout.LabelField("Item配置工具", EditorStyles.boldLabel);
+ GUILayout.FlexibleSpace();
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.Space(5);
+
+ // Docs路径选择
+ EditorGUILayout.BeginVertical("box");
+ EditorGUILayout.LabelField("Docs项目根目录", EditorStyles.boldLabel);
+
+ EditorGUILayout.BeginHorizontal();
+ docsRootPath = EditorGUILayout.TextField("路径", docsRootPath);
+ if (GUILayout.Button("选择文件夹", GUILayout.Width(100)))
+ {
+ string selectedPath = EditorUtility.OpenFolderPanel("选择Docs项目根目录", "", "");
+ if (!string.IsNullOrEmpty(selectedPath))
+ {
+ docsRootPath = selectedPath;
+ EditorPrefs.SetString(DOCS_PATH_PREF_KEY, docsRootPath);
+ }
+ }
+ EditorGUILayout.EndHorizontal();
+
+ if (GUILayout.Button("加载配置数据", GUILayout.Height(30)))
+ {
+ LoadData();
+ }
+
+ EditorGUILayout.EndVertical();
+
+ EditorGUILayout.Space(5);
+
+ // 数据编辑区域
+ if (isDataLoaded)
+ {
+ DrawFilterAndDataEditor();
+ }
+ else
+ {
+ EditorGUILayout.HelpBox("请先选择Docs根目录并加载配置数据", MessageType.Info);
+ }
+ }
+
+ ///
+ /// 加载配置数据
+ ///
+ private void LoadData()
+ {
+ try
+ {
+ // 校验路径
+ if (string.IsNullOrEmpty(docsRootPath) || !Directory.Exists(docsRootPath))
+ {
+ EditorUtility.DisplayDialog("错误", "请选择有效的Docs根目录", "确定");
+ return;
+ }
+
+ // 1. 检查Docs是否为Git仓库并更新
+ if (!CheckAndUpdateDocsRepository())
+ {
+ return;
+ }
+
+ // 2. 检查Design_SubModule分支
+ if (!CheckAndSwitchDesignSubModuleBranch())
+ {
+ return;
+ }
+
+ // 3. 加载所有Art_SO资源
+ LoadAllArtTableSO();
+
+ // 4. 加载语言表
+ LoadLanguageData();
+
+ // 5. 加载LimitedTimeEvent表
+ LoadLimitedTimeEventData();
+
+ // 6. 加载Avatar表
+ LoadAvatarData();
+
+ // 7. 加载Face表
+ LoadFaceData();
+
+ // 8. 加载Emoji表
+ LoadEmojiData();
+
+ // 9. 加载Item表
+ LoadItemExcel();
+
+ isDataLoaded = true;
+ EditorUtility.DisplayDialog("成功", "配置数据加载成功!", "确定");
+ }
+ catch (Exception e)
+ {
+ EditorUtility.DisplayDialog("错误", $"加载数据失败: {e.Message}\n{e.StackTrace}", "确定");
+ isDataLoaded = false;
+ }
+ }
+
+ ///
+ /// 执行Git命令
+ ///
+ private string ExecuteGitCommand(string workingDirectory, string arguments, out bool success)
+ {
+ try
+ {
+ ProcessStartInfo startInfo = new ProcessStartInfo
+ {
+ FileName = "git",
+ Arguments = arguments,
+ WorkingDirectory = workingDirectory,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using (Process process = Process.Start(startInfo))
+ {
+ string output = process.StandardOutput.ReadToEnd();
+ string error = process.StandardError.ReadToEnd();
+ process.WaitForExit();
+
+ success = process.ExitCode == 0;
+ return success ? output : error;
+ }
+ }
+ catch (Exception e)
+ {
+ success = false;
+ return $"执行Git命令失败: {e.Message}";
+ }
+ }
+
+ ///
+ /// 检查并更新Docs仓库
+ ///
+ private bool CheckAndUpdateDocsRepository()
+ {
+ string gitPath = Path.Combine(docsRootPath, ".git");
+ if (!Directory.Exists(gitPath))
+ {
+ EditorUtility.DisplayDialog("错误",
+ $"Docs目录不是Git仓库\n路径: {docsRootPath}\n\n请确保Docs项目已正确克隆",
+ "确定");
+ return false;
+ }
+
+ EditorUtility.DisplayProgressBar("检查更新", "正在检查Docs仓库远程更新...", 0.3f);
+
+ string fetchResult = ExecuteGitCommand(docsRootPath, "fetch", out bool fetchSuccess);
+
+ if (!fetchSuccess)
+ {
+ EditorUtility.ClearProgressBar();
+ EditorUtility.DisplayDialog("Git Fetch失败",
+ $"无法检查远程更新:\n{fetchResult}\n\n请在SourceTree或GitHubDesktop中检查网络连接和仓库状态",
+ "确定");
+ return false;
+ }
+
+ EditorUtility.DisplayProgressBar("检查更新", "正在检查是否有新提交...", 0.6f);
+
+ string statusResult = ExecuteGitCommand(docsRootPath, "status -uno", out bool statusSuccess);
+
+ if (!statusSuccess)
+ {
+ EditorUtility.ClearProgressBar();
+ EditorUtility.DisplayDialog("Git Status失败",
+ $"无法获取仓库状态:\n{statusResult}\n\n请在SourceTree或GitHubDesktop中检查仓库状态",
+ "确定");
+ return false;
+ }
+
+ if (statusResult.Contains("Changes not staged") || statusResult.Contains("Changes to be committed"))
+ {
+ EditorUtility.ClearProgressBar();
+ bool proceed = EditorUtility.DisplayDialog("警告:有未提交的更改",
+ "Docs仓库中有未提交的更改,这可能导致拉取时产生冲突。\n\n建议先提交或暂存这些更改。\n\n是否继续加载配置?(不推荐)",
+ "继续(不推荐)", "取消,前往处理");
+
+ if (!proceed)
+ {
+ Debug.Log("请在SourceTree或GitHubDesktop中处理未提交的更改");
+ return false;
+ }
+ }
+
+ if (statusResult.Contains("Your branch is behind"))
+ {
+ EditorUtility.DisplayProgressBar("更新中", "正在从远程拉取最新代码...", 0.8f);
+
+ string pullResult = ExecuteGitCommand(docsRootPath, "pull", out bool pullSuccess);
+
+ EditorUtility.ClearProgressBar();
+
+ if (!pullSuccess)
+ {
+ if (pullResult.Contains("CONFLICT") || pullResult.Contains("conflict"))
+ {
+ EditorUtility.DisplayDialog("拉取失败:存在冲突",
+ $"拉取远程更新时发生冲突:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中解决冲突后再操作",
+ "确定");
+ }
+ else
+ {
+ EditorUtility.DisplayDialog("拉取失败",
+ $"无法拉取远程更新:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中检查并解决问题",
+ "确定");
+ }
+ return false;
+ }
+
+ Debug.Log($"Docs仓库已更新到最新版本:\n{pullResult}");
+ EditorUtility.DisplayDialog("更新成功", "Docs仓库已更新到最新版本", "确定");
+ }
+ else
+ {
+ EditorUtility.ClearProgressBar();
+ Debug.Log("Docs仓库已是最新版本");
+ }
+
+ return true;
+ }
+
+ ///
+ /// 检查并切换Design_SubModule到main分支
+ ///
+ private bool CheckAndSwitchDesignSubModuleBranch()
+ {
+ string designSubModulePath = Path.Combine(Application.dataPath, "..", DESIGN_SUBMODULE_PATH);
+ designSubModulePath = Path.GetFullPath(designSubModulePath);
+
+ if (!Directory.Exists(designSubModulePath))
+ {
+ EditorUtility.DisplayDialog("错误",
+ $"Design_SubModule目录不存在:\n{designSubModulePath}\n\n请确保子模块已正确初始化",
+ "确定");
+ return false;
+ }
+
+ EditorUtility.DisplayProgressBar("检查分支", "正在检查Design_SubModule分支...", 0.5f);
+
+ string branchResult = ExecuteGitCommand(designSubModulePath, "branch --show-current", out bool branchSuccess);
+
+ if (!branchSuccess)
+ {
+ EditorUtility.ClearProgressBar();
+ EditorUtility.DisplayDialog("Git Branch失败",
+ $"无法获取当前分支:\n{branchResult}\n\n请在SourceTree或GitHubDesktop中检查Design_SubModule状态",
+ "确定");
+ return false;
+ }
+
+ string currentBranch = branchResult.Trim();
+
+ if (currentBranch != "main")
+ {
+ EditorUtility.DisplayProgressBar("切换分支", "正在切换到main分支...", 0.8f);
+
+ string checkoutResult = ExecuteGitCommand(designSubModulePath, "checkout main", out bool checkoutSuccess);
+
+ EditorUtility.ClearProgressBar();
+
+ if (!checkoutSuccess)
+ {
+ EditorUtility.DisplayDialog("切换分支失败",
+ $"无法切换到main分支:\n{checkoutResult}\n\n当前分支: {currentBranch}\n\n请在SourceTree或GitHubDesktop中手动切换到main分支",
+ "确定");
+ return false;
+ }
+
+ Debug.Log($"Design_SubModule已从 {currentBranch} 切换到 main 分支");
+ }
+ else
+ {
+ EditorUtility.ClearProgressBar();
+ Debug.Log("Design_SubModule已在main分支");
+ }
+
+ return true;
+ }
+
+ ///
+ /// 加载所有Art_SO资源
+ ///
+ private void LoadAllArtTableSO()
+ {
+ allArtTables.Clear();
+ artTablesByFolder.Clear();
+
+ string[] guids = AssetDatabase.FindAssets("t:ArtTableSO", new[] { ART_SO_PATH });
+ foreach (string guid in guids)
+ {
+ string path = AssetDatabase.GUIDToAssetPath(guid);
+ ArtTableSO tableSO = AssetDatabase.LoadAssetAtPath(path);
+ if (tableSO != null)
+ {
+ allArtTables.Add(tableSO);
+
+ // 按文件夹分组
+ string folder = GetRelativeFolder(path);
+ if (!artTablesByFolder.ContainsKey(folder))
+ {
+ artTablesByFolder[folder] = new List();
+ }
+ artTablesByFolder[folder].Add(tableSO);
+ }
+ }
+
+ // 加载特定资源
+ headFrameResourceSO = allArtTables.FirstOrDefault(x => x.TableName == "HeadFrameResource");
+ headResourceSO = allArtTables.FirstOrDefault(x => x.TableName == "HeadResource");
+ emojiResourceSO = allArtTables.FirstOrDefault(x => x.TableName == "EmojiResource");
+
+ Debug.Log($"加载了 {allArtTables.Count} 个ArtTableSO资源,分为 {artTablesByFolder.Count} 个组");
+ }
+
+ ///
+ /// 获取相对文件夹路径
+ ///
+ private string GetRelativeFolder(string assetPath)
+ {
+ string relativePath = assetPath.Replace(ART_SO_PATH + "/", "");
+ int lastSlash = relativePath.LastIndexOf('/');
+ if (lastSlash >= 0)
+ {
+ return relativePath.Substring(0, lastSlash);
+ }
+ return "根目录";
+ }
+
+ ///
+ /// 加载语言数据
+ ///
+ private void LoadLanguageData()
+ {
+ languageDict.Clear();
+
+ string languageExcelPath = Path.Combine(docsRootPath, "config", LANGUAGE_EXCEL_NAME);
+ if (!File.Exists(languageExcelPath))
+ {
+ Debug.LogWarning($"未找到语言表: {languageExcelPath}");
+ return;
+ }
+
+ using (var package = new ExcelPackage(new FileInfo(languageExcelPath)))
+ {
+ var worksheet = package.Workbook.Worksheets[LANGUAGE_SHEET_NAME];
+ if (worksheet == null)
+ {
+ Debug.LogWarning($"语言表中未找到Sheet: {LANGUAGE_SHEET_NAME}");
+ return;
+ }
+
+ // 查找Key列和语言列
+ int keyColumnIndex = -1;
+ int zhCNColumnIndex = -1;
+ int columnCount = worksheet.Dimension.Columns;
+
+ for (int col = 1; col <= columnCount; col++)
+ {
+ string header = worksheet.Cells[1, col].Text;
+ if (header == "key")
+ {
+ keyColumnIndex = col;
+ }
+ else if (header == "zh_CN")
+ {
+ zhCNColumnIndex = col;
+ }
+ }
+
+ if (keyColumnIndex < 0)
+ {
+ Debug.LogWarning("语言表中未找到Key列");
+ return;
+ }
+
+ // 读取数据(从第3行开始)
+ int rowCount = worksheet.Dimension.Rows;
+ for (int row = 3; row <= rowCount; row++)
+ {
+ string key = worksheet.Cells[row, keyColumnIndex].Text;
+ if (string.IsNullOrEmpty(key)) continue;
+
+ if (!languageDict.ContainsKey(key))
+ {
+ languageDict[key] = new Dictionary();
+ }
+
+ if (zhCNColumnIndex > 0)
+ {
+ languageDict[key]["zh_CN"] = worksheet.Cells[row, zhCNColumnIndex].Text;
+ }
+ }
+ }
+ }
+
+ ///
+ /// 加载限时事件数据
+ ///
+ private void LoadLimitedTimeEventData()
+ {
+ limitedTimeEventDict.Clear();
+
+ string eventExcelPath = Path.Combine(docsRootPath, "config", LIMITED_TIME_EVENT_EXCEL_NAME);
+ if (!File.Exists(eventExcelPath))
+ {
+ Debug.LogWarning($"未找到限时事件表: {eventExcelPath}");
+ return;
+ }
+
+ using (var package = new ExcelPackage(new FileInfo(eventExcelPath)))
+ {
+ var worksheet = package.Workbook.Worksheets[EVENT_SHEET_NAME];
+ if (worksheet == null)
+ {
+ Debug.LogWarning($"限时事件表中未找到Sheet: {EVENT_SHEET_NAME}");
+ return;
+ }
+
+ // 查找Id和Name列
+ int idCol = -1, nameCol = -1;
+ int columnCount = worksheet.Dimension.Columns;
+
+ for (int col = 1; col <= columnCount; col++)
+ {
+ string header = worksheet.Cells[1, col].Text;
+ if (header == "Id") idCol = col;
+ else if (header == "Name") nameCol = col;
+ }
+
+ if (idCol < 0 || nameCol < 0)
+ {
+ Debug.LogWarning("限时事件表结构不正确");
+ return;
+ }
+
+ // 读取数据(从第3行开始)
+ int rowCount = worksheet.Dimension.Rows;
+ for (int row = 3; row <= rowCount; row++)
+ {
+ string idText = worksheet.Cells[row, idCol].Text;
+ if (string.IsNullOrEmpty(idText)) continue;
+
+ if (int.TryParse(idText, out int id))
+ {
+ string name = worksheet.Cells[row, nameCol].Text;
+ limitedTimeEventDict[id] = name;
+ }
+ }
+ }
+ }
+
+ ///
+ /// 加载Avatar数据
+ ///
+ private void LoadAvatarData()
+ {
+ avatarDict.Clear();
+
+ string avatarExcelPath = Path.Combine(docsRootPath, "config", AVATAR_EXCEL_NAME);
+ if (!File.Exists(avatarExcelPath))
+ {
+ Debug.LogWarning($"未找到Avatar表: {avatarExcelPath}");
+ return;
+ }
+
+ using (var package = new ExcelPackage(new FileInfo(avatarExcelPath)))
+ {
+ var worksheet = package.Workbook.Worksheets[AVATAR_SHEET_NAME];
+ if (worksheet == null)
+ {
+ Debug.LogWarning($"Avatar表中未找到Sheet: {AVATAR_SHEET_NAME}");
+ return;
+ }
+
+ // 查找列
+ int idCol = -1, nameKeyCol = -1, iconCol = -1;
+ int columnCount = worksheet.Dimension.Columns;
+
+ for (int col = 1; col <= columnCount; col++)
+ {
+ string header = worksheet.Cells[1, col].Text;
+ if (header == "Id") idCol = col;
+ else if (header == "NameKey") nameKeyCol = col;
+ else if (header == "Icon") iconCol = col;
+ }
+
+ if (idCol < 0 || nameKeyCol < 0 || iconCol < 0)
+ {
+ Debug.LogWarning("Avatar表结构不正确");
+ return;
+ }
+
+ // 读取数据(从第3行开始)
+ int rowCount = worksheet.Dimension.Rows;
+ for (int row = 3; row <= rowCount; row++)
+ {
+ string idText = worksheet.Cells[row, idCol].Text;
+ if (string.IsNullOrEmpty(idText)) continue;
+
+ if (int.TryParse(idText, out int id))
+ {
+ string nameKey = worksheet.Cells[row, nameKeyCol].Text;
+ string iconText = worksheet.Cells[row, iconCol].Text;
+ int.TryParse(iconText, out int iconId);
+
+ avatarDict[id] = new AvatarData
+ {
+ Id = id,
+ NameKey = nameKey,
+ IconId = iconId
+ };
+ }
+ }
+ }
+ }
+
+ ///
+ /// 加载Face数据
+ ///
+ private void LoadFaceData()
+ {
+ faceDict.Clear();
+
+ string faceExcelPath = Path.Combine(docsRootPath, "config", FACE_EXCEL_NAME);
+ if (!File.Exists(faceExcelPath))
+ {
+ Debug.LogWarning($"未找到Face表: {faceExcelPath}");
+ return;
+ }
+
+ using (var package = new ExcelPackage(new FileInfo(faceExcelPath)))
+ {
+ var worksheet = package.Workbook.Worksheets[FACE_SHEET_NAME];
+ if (worksheet == null)
+ {
+ Debug.LogWarning($"Face表中未找到Sheet: {FACE_SHEET_NAME}");
+ return;
+ }
+
+ // 查找列
+ int idCol = -1, nameKeyCol = -1, iconCol = -1;
+ int columnCount = worksheet.Dimension.Columns;
+
+ for (int col = 1; col <= columnCount; col++)
+ {
+ string header = worksheet.Cells[1, col].Text;
+ if (header == "Id") idCol = col;
+ else if (header == "NameKey") nameKeyCol = col;
+ else if (header == "Icon") iconCol = col;
+ }
+
+ if (idCol < 0 || nameKeyCol < 0 || iconCol < 0)
+ {
+ Debug.LogWarning("Face表结构不正确");
+ return;
+ }
+
+ // 读取数据(从第3行开始)
+ int rowCount = worksheet.Dimension.Rows;
+ for (int row = 3; row <= rowCount; row++)
+ {
+ string idText = worksheet.Cells[row, idCol].Text;
+ if (string.IsNullOrEmpty(idText)) continue;
+
+ if (int.TryParse(idText, out int id))
+ {
+ string nameKey = worksheet.Cells[row, nameKeyCol].Text;
+ string iconText = worksheet.Cells[row, iconCol].Text;
+ int.TryParse(iconText, out int iconId);
+
+ faceDict[id] = new FaceData
+ {
+ Id = id,
+ NameKey = nameKey,
+ IconId = iconId
+ };
+ }
+ }
+ }
+ }
+
+ ///
+ /// 加载Emoji数据
+ ///
+ private void LoadEmojiData()
+ {
+ emojiDict.Clear();
+
+ string emojiExcelPath = Path.Combine(docsRootPath, "config", EMOJI_EXCEL_NAME);
+ if (!File.Exists(emojiExcelPath))
+ {
+ Debug.LogWarning($"未找到Emoji表: {emojiExcelPath}");
+ return;
+ }
+
+ using (var package = new ExcelPackage(new FileInfo(emojiExcelPath)))
+ {
+ var worksheet = package.Workbook.Worksheets[EMOJI_SHEET_NAME];
+ if (worksheet == null)
+ {
+ Debug.LogWarning($"Emoji表中未找到Sheet: {EMOJI_SHEET_NAME}");
+ return;
+ }
+
+ // 查找列
+ int idCol = -1, nameKeyCol = -1, iconCol = -1;
+ int columnCount = worksheet.Dimension.Columns;
+
+ for (int col = 1; col <= columnCount; col++)
+ {
+ string header = worksheet.Cells[1, col].Text;
+ if (header == "Id") idCol = col;
+ else if (header == "NameKey") nameKeyCol = col;
+ else if (header == "Icon") iconCol = col;
+ }
+
+ if (idCol < 0 || nameKeyCol < 0 || iconCol < 0)
+ {
+ Debug.LogWarning("Emoji表结构不正确");
+ return;
+ }
+
+ // 读取数据(从第3行开始)
+ int rowCount = worksheet.Dimension.Rows;
+ for (int row = 3; row <= rowCount; row++)
+ {
+ string idText = worksheet.Cells[row, idCol].Text;
+ if (string.IsNullOrEmpty(idText)) continue;
+
+ if (int.TryParse(idText, out int id))
+ {
+ string nameKey = worksheet.Cells[row, nameKeyCol].Text;
+ string iconText = worksheet.Cells[row, iconCol].Text;
+ int.TryParse(iconText, out int iconId);
+
+ emojiDict[id] = new EmojiData
+ {
+ Id = id,
+ NameKey = nameKey,
+ IconId = iconId
+ };
+ }
+ }
+ }
+ }
+
+ ///
+ /// 加载Item.xlsx
+ ///
+ private void LoadItemExcel()
+ {
+ allItemDataList.Clear();
+
+ string itemExcelPath = Path.Combine(docsRootPath, "config", ITEM_EXCEL_NAME);
+ if (!File.Exists(itemExcelPath))
+ {
+ throw new Exception($"未找到Item配置文件: {itemExcelPath}");
+ }
+
+ using (var package = new ExcelPackage(new FileInfo(itemExcelPath)))
+ {
+ var worksheet = package.Workbook.Worksheets[ITEM_SHEET_NAME];
+ if (worksheet == null)
+ {
+ throw new Exception($"Item.xlsx中未找到Sheet: {ITEM_SHEET_NAME}");
+ }
+
+ // 读取表头(第1行)
+ int idCol = -1, nameCol = -1, iTypeCol = -1, effectCol = -1, resCol = -1;
+ int columnCount = worksheet.Dimension.Columns;
+
+ for (int col = 1; col <= columnCount; col++)
+ {
+ string header = worksheet.Cells[1, col].Text;
+ if (header == "Id") idCol = col;
+ else if (header == "Name") nameCol = col;
+ else if (header == "IType") iTypeCol = col;
+ else if (header == "Effect") effectCol = col;
+ else if (header == "Res") resCol = col;
+ }
+
+ if (idCol < 0 || nameCol < 0 || iTypeCol < 0)
+ {
+ throw new Exception("Item.xlsx表结构不正确,缺少必要列(Id/Name/IType)");
+ }
+
+ // 读取数据(从第3行开始)
+ int rowCount = worksheet.Dimension.Rows;
+ for (int row = 3; row <= rowCount; row++)
+ {
+ string idText = worksheet.Cells[row, idCol].Text;
+ if (string.IsNullOrEmpty(idText)) continue;
+
+ if (!int.TryParse(idText, out int id)) continue;
+
+ string name = worksheet.Cells[row, nameCol].Text;
+ string iTypeText = worksheet.Cells[row, iTypeCol].Text;
+ string effect = effectCol > 0 ? worksheet.Cells[row, effectCol].Text : "";
+ string res = resCol > 0 ? worksheet.Cells[row, resCol].Text : "";
+
+ if (!int.TryParse(iTypeText, out int iType)) continue;
+
+ var itemData = new ItemData
+ {
+ Id = id,
+ Name = name,
+ IType = iType,
+ Effect = effect,
+ Res = res
+ };
+
+ allItemDataList.Add(itemData);
+ }
+ }
+ }
+
+ ///
+ /// 绘制筛选和数据编辑器
+ ///
+ private void DrawFilterAndDataEditor()
+ {
+ EditorGUILayout.BeginVertical("box");
+
+ // IType筛选
+ EditorGUILayout.LabelField("Item类型筛选", EditorStyles.boldLabel);
+ EditorGUILayout.BeginHorizontal();
+
+ // 创建下拉列表选项
+ var typeOptions = new List { "请选择类型..." };
+ var typeValues = new List { -1 };
+
+ // 排除103、104、108和97、98、111-115
+ foreach (var kvp in ITEM_TYPES.OrderBy(x => x.Key))
+ {
+ // 跳过不需要显示的类型
+ if (kvp.Key == 103 || kvp.Key == 104 || kvp.Key == 108 ||
+ kvp.Key == 97 || kvp.Key == 98 ||
+ (kvp.Key >= 111 && kvp.Key <= 115))
+ {
+ continue;
+ }
+
+ typeOptions.Add(kvp.Value);
+ typeValues.Add(kvp.Key);
+ }
+
+ int currentIndex = typeValues.IndexOf(selectedIType);
+ if (currentIndex < 0) currentIndex = 0;
+
+ int newIndex = EditorGUILayout.Popup("选择类型", currentIndex, typeOptions.ToArray());
+ if (newIndex != currentIndex)
+ {
+ selectedIType = typeValues[newIndex];
+ FilterItemsByType();
+ currentPage = 0;
+ }
+
+ if (selectedIType > 0)
+ {
+ EditorGUILayout.LabelField($"当前类型ID: {selectedIType}", GUILayout.Width(150));
+ }
+
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.EndVertical();
+
+ EditorGUILayout.Space(5);
+
+ // 如果已选择类型,显示数据列表
+ if (selectedIType > 0)
+ {
+ DrawItemDataList();
+ }
+ else
+ {
+ EditorGUILayout.HelpBox("请先选择Item类型进行筛选", MessageType.Info);
+ }
+ }
+
+ ///
+ /// 根据IType筛选数据
+ ///
+ private void FilterItemsByType()
+ {
+ filteredItemDataList = allItemDataList.Where(x => x.IType == selectedIType).ToList();
+ totalPages = Mathf.CeilToInt((float)filteredItemDataList.Count / itemsPerPage);
+ }
+
+ ///
+ /// 绘制Item数据列表
+ ///
+ private void DrawItemDataList()
+ {
+ EditorGUILayout.BeginVertical("box");
+
+ // 顶部工具栏
+ EditorGUILayout.BeginHorizontal();
+ EditorGUILayout.LabelField($"数据列表(共 {filteredItemDataList.Count} 条)", EditorStyles.boldLabel);
+ GUILayout.FlexibleSpace();
+
+ GUI.backgroundColor = Color.cyan;
+ if (GUILayout.Button("+ 添加Item", GUILayout.Height(25), GUILayout.Width(100)))
+ {
+ AddNewItem();
+ }
+ GUI.backgroundColor = Color.white;
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.Space(3);
+
+ // 分页控制
+ DrawPagination();
+
+ EditorGUILayout.Space(3);
+
+ scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.Height(400));
+
+ // 表头
+ DrawTableHeader();
+
+ // 数据行
+ int startIndex = currentPage * itemsPerPage;
+ int endIndex = Mathf.Min(startIndex + itemsPerPage, filteredItemDataList.Count);
+
+ for (int i = startIndex; i < endIndex; i++)
+ {
+ DrawItemDataRow(filteredItemDataList[i]);
+ }
+
+ EditorGUILayout.EndScrollView();
+
+ EditorGUILayout.Space(5);
+
+ // 分页控制(底部)
+ DrawPagination();
+
+ EditorGUILayout.EndVertical();
+
+ EditorGUILayout.Space(10);
+
+ // 保存按钮
+ EditorGUILayout.BeginHorizontal();
+ GUILayout.FlexibleSpace();
+
+ GUI.backgroundColor = Color.green;
+ if (GUILayout.Button("保存配置到Excel", GUILayout.Height(30), GUILayout.Width(200)))
+ {
+ SaveData();
+ }
+ GUI.backgroundColor = Color.white;
+
+ GUILayout.FlexibleSpace();
+ EditorGUILayout.EndHorizontal();
+ }
+
+ ///
+ /// 绘制分页控制
+ ///
+ private void DrawPagination()
+ {
+ EditorGUILayout.BeginHorizontal();
+
+ EditorGUILayout.LabelField("每页显示:", GUILayout.Width(70));
+ itemsPerPage = EditorGUILayout.IntField(itemsPerPage, GUILayout.Width(50));
+ itemsPerPage = Mathf.Max(1, itemsPerPage);
+
+ GUILayout.Space(20);
+
+ GUI.enabled = currentPage > 0;
+ if (GUILayout.Button("第一页", GUILayout.Width(60)))
+ {
+ currentPage = 0;
+ }
+ if (GUILayout.Button("上一页", GUILayout.Width(60)))
+ {
+ currentPage--;
+ }
+ GUI.enabled = true;
+
+ EditorGUILayout.LabelField($"第 {currentPage + 1} / {Mathf.Max(1, totalPages)} 页", GUILayout.Width(100));
+
+ int newPage = EditorGUILayout.IntField(currentPage + 1, GUILayout.Width(50)) - 1;
+ if (newPage != currentPage && newPage >= 0 && newPage < totalPages)
+ {
+ currentPage = newPage;
+ }
+
+ GUI.enabled = currentPage < totalPages - 1;
+ if (GUILayout.Button("下一页", GUILayout.Width(60)))
+ {
+ currentPage++;
+ }
+ if (GUILayout.Button("最后一页", GUILayout.Width(70)))
+ {
+ currentPage = totalPages - 1;
+ }
+ GUI.enabled = true;
+
+ GUILayout.FlexibleSpace();
+ EditorGUILayout.EndHorizontal();
+ }
+
+ ///
+ /// 绘制表头
+ ///
+ private void DrawTableHeader()
+ {
+ EditorGUILayout.BeginHorizontal("box");
+ EditorGUILayout.LabelField("Id", EditorStyles.boldLabel, GUILayout.Width(50));
+ EditorGUILayout.LabelField("Name", EditorStyles.boldLabel, GUILayout.Width(120));
+ EditorGUILayout.LabelField("IType", EditorStyles.boldLabel, GUILayout.Width(50));
+ EditorGUILayout.LabelField("Effect", EditorStyles.boldLabel, GUILayout.Width(200));
+ EditorGUILayout.LabelField("Res", EditorStyles.boldLabel, GUILayout.Width(200));
+ EditorGUILayout.LabelField("预览", EditorStyles.boldLabel, GUILayout.Width(100));
+ EditorGUILayout.LabelField("操作", EditorStyles.boldLabel, GUILayout.Width(60));
+ EditorGUILayout.EndHorizontal();
+ }
+
+ ///
+ /// 绘制单行数据
+ ///
+ private void DrawItemDataRow(ItemData data)
+ {
+ EditorGUILayout.BeginHorizontal("box");
+
+ // Id(可编辑)
+ data.Id = EditorGUILayout.IntField(data.Id, GUILayout.Width(50));
+
+ // Name(可编辑)
+ data.Name = EditorGUILayout.TextField(data.Name, GUILayout.Width(120));
+
+ // IType(只读)
+ GUI.enabled = false;
+ EditorGUILayout.IntField(data.IType, GUILayout.Width(50));
+ GUI.enabled = true;
+
+ // Effect(根据IType不同显示不同内容)
+ DrawEffectField(data);
+
+ // Res(资源选择)
+ DrawResField(data);
+
+ // 预览
+ DrawResPreview(data);
+
+ // 删除按钮
+ GUI.backgroundColor = Color.red;
+ if (GUILayout.Button("删除", GUILayout.Width(60)))
+ {
+ if (EditorUtility.DisplayDialog("确认删除", $"确定要删除ID为{data.Id}的Item吗?", "删除", "取消"))
+ {
+ filteredItemDataList.Remove(data);
+ allItemDataList.Remove(data);
+ FilterItemsByType(); // 重新计算分页
+ }
+ }
+ GUI.backgroundColor = Color.white;
+
+ EditorGUILayout.EndHorizontal();
+ }
+
+ ///
+ /// 绘制Effect字段(统一文本编辑)
+ ///
+ private void DrawEffectField(ItemData data)
+ {
+ EditorGUILayout.BeginVertical(GUILayout.Width(200));
+
+ // 当前使用统一的文本编辑框处理所有类型
+ EditorGUI.BeginChangeCheck();
+ string newEffect = EditorGUILayout.TextField(data.Effect ?? "", GUILayout.Width(200));
+ if (EditorGUI.EndChangeCheck())
+ {
+ data.Effect = newEffect;
+ }
+
+ /*
+ // 以下是之前按类型特殊处理的代码,已注释保留,方便之后还原特殊处理
+ switch (data.IType)
+ {
+ case 100: // 棋子 - 不显示
+ case 1: // 能量
+ case 2: // 星星
+ case 3: // 钻石
+ EditorGUILayout.LabelField("-", GUILayout.Width(200));
+ break;
+
+ case 101: // 卡包 - 只读
+ case 106: // 活动代币 - 没有Effect
+ case 107: // 竞赛游戏代币 - 没有Effect
+ case 99: // 背包道具 - 没有Effect
+ EditorGUILayout.LabelField(string.IsNullOrEmpty(data.Effect) ? "-" : data.Effect, GUILayout.Width(200));
+ break;
+
+ case 102: // 限时事件
+ DrawLimitedTimeEventEffect(data);
+ break;
+
+ case 105: // 头像框 - 已改用Res字段
+ EditorGUILayout.LabelField("请使用Res字段配置", GUILayout.Width(200));
+ break;
+
+ case 110: // 头像 - 已改用Res字段
+ EditorGUILayout.LabelField("请使用Res字段配置", GUILayout.Width(200));
+ break;
+
+ case 109: // 表情 - 已改用Res字段
+ EditorGUILayout.LabelField("请使用Res字段配置", GUILayout.Width(200));
+ break;
+
+ default:
+ data.Effect = EditorGUILayout.TextField(data.Effect, GUILayout.Width(200));
+ break;
+ }
+ */
+
+ EditorGUILayout.EndVertical();
+ }
+
+ ///
+ /// 绘制限时事件Effect
+ ///
+ private void DrawLimitedTimeEventEffect(ItemData data)
+ {
+ // Effect格式: "eventId,seconds"
+ string[] parts = data.Effect.Split(',');
+ int eventId = 0;
+ int seconds = 0;
+
+ if (parts.Length >= 1) int.TryParse(parts[0], out eventId);
+ if (parts.Length >= 2) int.TryParse(parts[1], out seconds);
+
+ EditorGUILayout.BeginHorizontal();
+
+ // 事件选择下拉框
+ var eventOptions = new List { "请选择..." };
+ var eventIds = new List { 0 };
+
+ foreach (var kvp in limitedTimeEventDict.OrderBy(x => x.Key))
+ {
+ eventOptions.Add(kvp.Value);
+ eventIds.Add(kvp.Key);
+ }
+
+ int currentIndex = eventIds.IndexOf(eventId);
+ if (currentIndex < 0) currentIndex = 0;
+
+ int newIndex = EditorGUILayout.Popup(currentIndex, eventOptions.ToArray(), GUILayout.Width(120));
+ int newEventId = eventIds[newIndex];
+
+ // 秒数输入
+ int newSeconds = EditorGUILayout.IntField(seconds, GUILayout.Width(60));
+
+ // 显示时间格式
+ int minutes = newSeconds / 60;
+ int secs = newSeconds % 60;
+ GUI.enabled = false;
+ EditorGUILayout.TextField($"{minutes:D2}:{secs:D2}", GUILayout.Width(50));
+ GUI.enabled = true;
+
+ EditorGUILayout.EndHorizontal();
+
+ // 验证提示
+ if (newEventId > 0 && !limitedTimeEventDict.ContainsKey(newEventId))
+ {
+ EditorGUILayout.HelpBox("没有这个限时事件", MessageType.Error);
+ }
+
+ // 更新数据
+ data.Effect = $"{newEventId},{newSeconds}";
+ }
+
+ ///
+ /// 绘制头像框Effect
+ ///
+ private void DrawHeadFrameEffect(ItemData data)
+ {
+ // Effect格式: "avatarId,0"
+ string[] parts = string.IsNullOrEmpty(data.Effect) ? new string[0] : data.Effect.Split(',');
+ int avatarId = -1;
+
+ if (parts.Length >= 1) int.TryParse(parts[0], out avatarId);
+
+ // 创建下拉选项
+ var options = new List { "请选择..." };
+ var ids = new List { -1 };
+
+ foreach (var kvp in avatarDict.OrderBy(x => x.Key))
+ {
+ string name = kvp.Value.NameKey;
+ if (languageDict.ContainsKey(kvp.Value.NameKey) && languageDict[kvp.Value.NameKey].ContainsKey("zh_CN"))
+ {
+ name = languageDict[kvp.Value.NameKey]["zh_CN"];
+ }
+ options.Add($"{name} (ID:{kvp.Key})");
+ ids.Add(kvp.Key);
+ }
+
+ int currentIndex = ids.IndexOf(avatarId);
+ if (currentIndex < 0) currentIndex = 0;
+
+ EditorGUI.BeginChangeCheck();
+ int newIndex = EditorGUILayout.Popup(currentIndex, options.ToArray(), GUILayout.Width(200));
+ if (EditorGUI.EndChangeCheck())
+ {
+ int newAvatarId = ids[newIndex];
+ data.Effect = $"{newAvatarId},0";
+ }
+ }
+
+ ///
+ /// 绘制头像Effect
+ ///
+ private void DrawFaceEffect(ItemData data)
+ {
+ // Effect格式: "faceId,0"
+ string[] parts = string.IsNullOrEmpty(data.Effect) ? new string[0] : data.Effect.Split(',');
+ int faceId = -1;
+
+ if (parts.Length >= 1) int.TryParse(parts[0], out faceId);
+
+ // 创建下拉选项
+ var options = new List { "请选择..." };
+ var ids = new List { -1 };
+
+ foreach (var kvp in faceDict.OrderBy(x => x.Key))
+ {
+ string name = kvp.Value.NameKey;
+ if (languageDict.ContainsKey(kvp.Value.NameKey) && languageDict[kvp.Value.NameKey].ContainsKey("zh_CN"))
+ {
+ name = languageDict[kvp.Value.NameKey]["zh_CN"];
+ }
+ options.Add($"{name} (ID:{kvp.Key})");
+ ids.Add(kvp.Key);
+ }
+
+ int currentIndex = ids.IndexOf(faceId);
+ if (currentIndex < 0) currentIndex = 0;
+
+ EditorGUI.BeginChangeCheck();
+ int newIndex = EditorGUILayout.Popup(currentIndex, options.ToArray(), GUILayout.Width(200));
+ if (EditorGUI.EndChangeCheck())
+ {
+ int newFaceId = ids[newIndex];
+ data.Effect = $"{newFaceId},0";
+ }
+ }
+
+ ///
+ /// 绘制表情Effect
+ ///
+ private void DrawEmojiEffect(ItemData data)
+ {
+ // Effect格式: "emojiId,0"
+ string[] parts = string.IsNullOrEmpty(data.Effect) ? new string[0] : data.Effect.Split(',');
+ int emojiId = -1;
+
+ if (parts.Length >= 1) int.TryParse(parts[0], out emojiId);
+
+ // 创建下拉选项
+ var options = new List { "请选择..." };
+ var ids = new List { -1 };
+
+ foreach (var kvp in emojiDict.OrderBy(x => x.Key))
+ {
+ string name = kvp.Value.NameKey;
+ if (languageDict.ContainsKey(kvp.Value.NameKey) && languageDict[kvp.Value.NameKey].ContainsKey("zh_CN"))
+ {
+ name = languageDict[kvp.Value.NameKey]["zh_CN"];
+ }
+ options.Add($"{name} (ID:{kvp.Key})");
+ ids.Add(kvp.Key);
+ }
+
+ int currentIndex = ids.IndexOf(emojiId);
+ if (currentIndex < 0) currentIndex = 0;
+
+ EditorGUI.BeginChangeCheck();
+ int newIndex = EditorGUILayout.Popup(currentIndex, options.ToArray(), GUILayout.Width(200));
+ if (EditorGUI.EndChangeCheck())
+ {
+ int newEmojiId = ids[newIndex];
+ data.Effect = $"{newEmojiId},0";
+ }
+ }
+
+ ///
+ /// 绘制Res字段(三级选择:组->表->Item)
+ ///
+ private void DrawResField(ItemData data)
+ {
+ EditorGUILayout.BeginVertical(GUILayout.Width(200));
+
+ // Res格式: "TableId,ItemId"
+ string[] parts = string.IsNullOrEmpty(data.Res) ? new string[0] : data.Res.Split(',');
+ int tableId = -1;
+ int itemId = -1;
+
+ if (parts.Length > 0) int.TryParse(parts[0], out tableId);
+ if (parts.Length > 1) int.TryParse(parts[1], out itemId);
+
+ // 查找当前表格和所属文件夹
+ ArtTableSO currentTable = null;
+ string currentFolder = "";
+
+ // 优先使用缓存的组选择
+ if (resSelectedFolderCache.ContainsKey(data.Id))
+ {
+ currentFolder = resSelectedFolderCache[data.Id];
+ }
+
+ // 如果有Res数据,从Res推导组和表
+ if (tableId >= 0)
+ {
+ currentTable = allArtTables.FirstOrDefault(x => x.TableId == tableId);
+ if (currentTable != null)
+ {
+ string tablePath = AssetDatabase.GetAssetPath(currentTable);
+ string folderFromRes = GetRelativeFolder(tablePath);
+ // 如果没有缓存或缓存不一致,更新缓存
+ if (string.IsNullOrEmpty(currentFolder) || currentFolder != folderFromRes)
+ {
+ currentFolder = folderFromRes;
+ resSelectedFolderCache[data.Id] = currentFolder;
+ }
+ }
+ }
+
+ // 第一级:选择组
+ var folderOptions = new List { "未选择" };
+ folderOptions.AddRange(artTablesByFolder.Keys.OrderBy(k => k));
+
+ int currentFolderIndex = 0;
+ if (!string.IsNullOrEmpty(currentFolder))
+ {
+ currentFolderIndex = folderOptions.IndexOf(currentFolder);
+ if (currentFolderIndex < 0) currentFolderIndex = 0;
+ }
+
+ EditorGUI.BeginChangeCheck();
+ int newFolderIndex = EditorGUILayout.Popup(currentFolderIndex, folderOptions.ToArray(), GUILayout.Width(200));
+ bool folderChanged = EditorGUI.EndChangeCheck();
+
+ if (folderChanged)
+ {
+ if (newFolderIndex > 0 && newFolderIndex < folderOptions.Count)
+ {
+ currentFolder = folderOptions[newFolderIndex];
+ // 保存到缓存
+ resSelectedFolderCache[data.Id] = currentFolder;
+ // 组改变时,清空表和Item选择,并清空Res数据
+ currentTable = null;
+ tableId = -1;
+ itemId = -1;
+ data.Res = ""; // 清空Res,避免旧数据干扰
+ }
+ else if (newFolderIndex == 0)
+ {
+ // 选择"未选择",清空所有
+ currentFolder = "";
+ if (resSelectedFolderCache.ContainsKey(data.Id))
+ {
+ resSelectedFolderCache.Remove(data.Id);
+ }
+ currentTable = null;
+ data.Res = "";
+ EditorGUILayout.EndVertical();
+ return;
+ }
+ }
+ else if (newFolderIndex > 0 && newFolderIndex < folderOptions.Count)
+ {
+ currentFolder = folderOptions[newFolderIndex];
+ }
+ else if (newFolderIndex == 0)
+ {
+ // 当前是"未选择"状态
+ EditorGUILayout.EndVertical();
+ return;
+ }
+
+ // 第二级:选择表
+ List tablesInFolder = artTablesByFolder.ContainsKey(currentFolder)
+ ? artTablesByFolder[currentFolder]
+ : new List();
+
+ var tableOptions = new List { "未选择" };
+ tableOptions.AddRange(tablesInFolder.Select(t => $"{t.TableName}(ID:{t.TableId})"));
+
+ int currentTableIndex = 0;
+ if (currentTable != null && !folderChanged)
+ {
+ int foundIndex = tablesInFolder.FindIndex(t => t.TableId == currentTable.TableId);
+ if (foundIndex >= 0)
+ {
+ currentTableIndex = foundIndex + 1; // +1 因为有"未选择"选项
+ }
+ }
+
+ EditorGUI.BeginChangeCheck();
+ int newTableIndex = EditorGUILayout.Popup(currentTableIndex, tableOptions.ToArray(), GUILayout.Width(200));
+ bool tableSelectionChanged = EditorGUI.EndChangeCheck();
+
+ if (tableSelectionChanged)
+ {
+ if (newTableIndex > 0 && newTableIndex <= tablesInFolder.Count)
+ {
+ currentTable = tablesInFolder[newTableIndex - 1]; // -1 因为有"未选择"选项
+ // 表格改变时,清空Item选择
+ data.Res = $"{currentTable.TableId},-1";
+ itemId = -1;
+ }
+ else if (newTableIndex == 0)
+ {
+ // 选择"未选择"
+ currentTable = null;
+ data.Res = "";
+ EditorGUILayout.EndVertical();
+ return;
+ }
+ }
+ else if (newTableIndex > 0 && newTableIndex <= tablesInFolder.Count)
+ {
+ currentTable = tablesInFolder[newTableIndex - 1];
+ }
+ else
+ {
+ // 当前表是"未选择"状态
+ EditorGUILayout.EndVertical();
+ return;
+ }
+
+ // 第三级:选择Item
+ if (currentTable != null && currentTable.Items != null && currentTable.Items.Count > 0)
+ {
+ var itemOptions = new List { "未选择" };
+ itemOptions.AddRange(currentTable.Items.Select(item => $"{item.Name}(ID:{item.Id})"));
+
+ int currentItemIndex = 0;
+ if (itemId >= 0 && !tableSelectionChanged)
+ {
+ int foundIndex = currentTable.Items.FindIndex(item => item.Id == itemId);
+ if (foundIndex >= 0)
+ {
+ currentItemIndex = foundIndex + 1; // +1 因为有"未选择"选项
+ }
+ }
+
+ EditorGUI.BeginChangeCheck();
+ int newItemIndex = EditorGUILayout.Popup(currentItemIndex, itemOptions.ToArray(), GUILayout.Width(200));
+ if (EditorGUI.EndChangeCheck())
+ {
+ if (newItemIndex > 0 && newItemIndex <= currentTable.Items.Count)
+ {
+ data.Res = $"{currentTable.TableId},{currentTable.Items[newItemIndex - 1].Id}"; // -1 因为有"未选择"选项
+ }
+ else if (newItemIndex == 0)
+ {
+ // 选择"未选择"
+ data.Res = $"{currentTable.TableId},-1";
+ }
+ }
+ }
+
+ EditorGUILayout.EndVertical();
+ }
+
+ ///
+ /// 绘制资源预览(统一使用Res字段)
+ ///
+ private void DrawResPreview(ItemData data)
+ {
+ Sprite previewSprite = null;
+ string tipMessage = "";
+
+ // 统一从Res字段获取预览
+ previewSprite = GetResPreview(data);
+
+ if (previewSprite != null)
+ {
+ Rect rect = GUILayoutUtility.GetRect(80, 80, GUILayout.Width(100));
+ GUI.DrawTexture(rect, previewSprite.texture, ScaleMode.ScaleToFit);
+ }
+ else
+ {
+ if (!string.IsNullOrEmpty(tipMessage))
+ {
+ GUILayout.Box(tipMessage, GUILayout.Width(100), GUILayout.Height(80));
+ }
+ else
+ {
+ GUILayout.Box("无预览", GUILayout.Width(100), GUILayout.Height(80));
+ }
+ }
+ }
+
+ ///
+ /// 获取头像框预览
+ ///
+ private Sprite GetHeadFramePreview(ItemData data)
+ {
+ if (headFrameResourceSO == null) return null;
+ if (string.IsNullOrEmpty(data.Effect)) return null;
+
+ string[] parts = data.Effect.Split(',');
+ if (parts.Length < 1 || !int.TryParse(parts[0], out int avatarId)) return null;
+ if (avatarId < 0) return null;
+
+ if (!avatarDict.ContainsKey(avatarId)) return null;
+
+ int iconId = avatarDict[avatarId].IconId;
+ var item = headFrameResourceSO.Items.FirstOrDefault(x => x.Id == iconId);
+
+ return item?.Sprite;
+ }
+
+ ///
+ /// 获取头像预览
+ ///
+ private Sprite GetFacePreview(ItemData data)
+ {
+ if (headResourceSO == null) return null;
+ if (string.IsNullOrEmpty(data.Effect)) return null;
+
+ string[] parts = data.Effect.Split(',');
+ if (parts.Length < 1 || !int.TryParse(parts[0], out int faceId)) return null;
+ if (faceId < 0) return null;
+
+ if (!faceDict.ContainsKey(faceId)) return null;
+
+ int iconId = faceDict[faceId].IconId;
+ var item = headResourceSO.Items.FirstOrDefault(x => x.Id == iconId);
+
+ return item?.Sprite;
+ }
+
+ ///
+ /// 获取表情预览
+ ///
+ private Sprite GetEmojiPreview(ItemData data)
+ {
+ if (emojiResourceSO == null) return null;
+ if (string.IsNullOrEmpty(data.Effect)) return null;
+
+ string[] parts = data.Effect.Split(',');
+ if (parts.Length < 1 || !int.TryParse(parts[0], out int emojiId)) return null;
+ if (emojiId < 0) return null;
+
+ if (!emojiDict.ContainsKey(emojiId)) return null;
+
+ int iconId = emojiDict[emojiId].IconId;
+ var item = emojiResourceSO.Items.FirstOrDefault(x => x.Id == iconId);
+
+ return item?.Sprite;
+ }
+
+ ///
+ /// 获取Res资源预览
+ ///
+ private Sprite GetResPreview(ItemData data)
+ {
+ if (string.IsNullOrEmpty(data.Res)) return null;
+
+ string[] parts = data.Res.Split(',');
+ if (parts.Length < 2) return null;
+
+ if (!int.TryParse(parts[0], out int tableId)) return null;
+ if (!int.TryParse(parts[1], out int itemId)) return null;
+
+ var table = allArtTables.FirstOrDefault(x => x.TableId == tableId);
+ if (table == null) return null;
+
+ var item = table.Items.FirstOrDefault(x => x.Id == itemId);
+ return item?.Sprite;
+ }
+
+ ///
+ /// 添加新Item
+ ///
+ private void AddNewItem()
+ {
+ // 找到当前IType的最大ID
+ int maxId = 0;
+ foreach (var item in allItemDataList.Where(x => x.IType == selectedIType))
+ {
+ if (item.Id > maxId) maxId = item.Id;
+ }
+
+ // 自增ID并检查重复
+ int newId = maxId + 1;
+ while (allItemDataList.Any(x => x.Id == newId))
+ {
+ newId++;
+ }
+
+ var newItem = new ItemData
+ {
+ Id = newId,
+ Name = $"新Item_{newId}",
+ IType = selectedIType,
+ Effect = "",
+ Res = ""
+ };
+
+ allItemDataList.Add(newItem);
+ FilterItemsByType();
+
+ // 跳转到最后一页
+ currentPage = totalPages - 1;
+ }
+
+ ///
+ /// 保存数据到Excel
+ ///
+ private void SaveData()
+ {
+ try
+ {
+ // ID重复检查
+ var duplicateIds = allItemDataList.GroupBy(x => x.Id)
+ .Where(g => g.Count() > 1)
+ .Select(g => g.Key)
+ .ToList();
+
+ if (duplicateIds.Count > 0)
+ {
+ EditorUtility.DisplayDialog("错误",
+ $"存在重复的ID: {string.Join(", ", duplicateIds)}\n请修正后再保存",
+ "确定");
+ return;
+ }
+
+ // 验证限时事件
+ foreach (var item in allItemDataList.Where(x => x.IType == 102))
+ {
+ if (!string.IsNullOrEmpty(item.Effect))
+ {
+ string[] parts = item.Effect.Split(',');
+ if (parts.Length >= 1 && int.TryParse(parts[0], out int eventId))
+ {
+ if (eventId > 0 && !limitedTimeEventDict.ContainsKey(eventId))
+ {
+ EditorUtility.DisplayDialog("错误",
+ $"ID {item.Id} 的限时事件ID {eventId} 不存在\n请修正后再保存",
+ "确定");
+ return;
+ }
+ }
+ }
+ }
+
+ string itemExcelPath = Path.Combine(docsRootPath, "config", ITEM_EXCEL_NAME);
+
+ using (var package = new ExcelPackage(new FileInfo(itemExcelPath)))
+ {
+ var worksheet = package.Workbook.Worksheets[ITEM_SHEET_NAME];
+
+ // 找到列索引
+ int idCol = -1, nameCol = -1, iTypeCol = -1, effectCol = -1, resCol = -1;
+ int columnCount = worksheet.Dimension.Columns;
+
+ for (int col = 1; col <= columnCount; col++)
+ {
+ string header = worksheet.Cells[1, col].Text;
+ if (header == "Id") idCol = col;
+ else if (header == "Name") nameCol = col;
+ else if (header == "IType") iTypeCol = col;
+ else if (header == "Effect") effectCol = col;
+ else if (header == "Res") resCol = col;
+ }
+
+ // 创建Id到行号的映射(读取现有数据)
+ var existingRowMap = new Dictionary();
+ int existingRowCount = worksheet.Dimension.Rows;
+
+ for (int row = 3; row <= existingRowCount; row++)
+ {
+ string idText = worksheet.Cells[row, idCol].Text;
+ if (!string.IsNullOrEmpty(idText) && int.TryParse(idText, out int id))
+ {
+ existingRowMap[id] = row;
+ }
+ }
+
+ // 更新或新增数据
+ int nextNewRow = existingRowCount + 1;
+
+ foreach (var item in allItemDataList.OrderBy(x => x.Id))
+ {
+ int targetRow;
+
+ // 如果该ID已存在,更新对应行;否则添加新行
+ if (existingRowMap.ContainsKey(item.Id))
+ {
+ targetRow = existingRowMap[item.Id];
+ }
+ else
+ {
+ targetRow = nextNewRow;
+ nextNewRow++;
+ }
+
+ // 只更新我们管理的列
+ worksheet.Cells[targetRow, idCol].Value = item.Id;
+ worksheet.Cells[targetRow, nameCol].Value = item.Name;
+ worksheet.Cells[targetRow, iTypeCol].Value = item.IType;
+
+ if (effectCol > 0)
+ {
+ worksheet.Cells[targetRow, effectCol].Value = item.Effect;
+ }
+
+ if (resCol > 0)
+ {
+ worksheet.Cells[targetRow, resCol].Value = item.Res;
+ }
+ }
+
+ // 删除已不存在的数据行(只清空我们管理的列)
+ var currentIds = allItemDataList.Select(x => x.Id).ToHashSet();
+ for (int row = 3; row <= existingRowCount; row++)
+ {
+ string idText = worksheet.Cells[row, idCol].Text;
+ if (!string.IsNullOrEmpty(idText) && int.TryParse(idText, out int id))
+ {
+ if (!currentIds.Contains(id))
+ {
+ // 只清空我们管理的列,保留其他列
+ worksheet.Cells[row, idCol].Value = null;
+ worksheet.Cells[row, nameCol].Value = null;
+ worksheet.Cells[row, iTypeCol].Value = null;
+ if (effectCol > 0) worksheet.Cells[row, effectCol].Value = null;
+ if (resCol > 0) worksheet.Cells[row, resCol].Value = null;
+ }
+ }
+ }
+
+ package.Save();
+ }
+
+ EditorUtility.DisplayDialog("成功", "配置已保存到Excel文件", "确定");
+ }
+ catch (Exception e)
+ {
+ EditorUtility.DisplayDialog("错误", $"保存失败: {e.Message}\n{e.StackTrace}", "确定");
+ }
+ }
+
+ ///
+ /// Item数据类
+ ///
+ private class ItemData
+ {
+ public int Id;
+ public string Name;
+ public int IType;
+ public string Effect;
+ public string Res;
+ }
+
+ ///
+ /// Avatar数据类
+ ///
+ private class AvatarData
+ {
+ public int Id;
+ public string NameKey;
+ public int IconId;
+ }
+
+ ///
+ /// Face数据类
+ ///
+ private class FaceData
+ {
+ public int Id;
+ public string NameKey;
+ public int IconId;
+ }
+
+ ///
+ /// Emoji数据类
+ ///
+ private class EmojiData
+ {
+ public int Id;
+ public string NameKey;
+ public int IconId;
+ }
+ }
+}
diff --git a/Scripts/Editor/Design_Tools/ItemConfigEditor.cs.meta b/Scripts/Editor/Design_Tools/ItemConfigEditor.cs.meta
new file mode 100644
index 0000000..873c7ac
--- /dev/null
+++ b/Scripts/Editor/Design_Tools/ItemConfigEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2cbfd5e054af7604e837e9ececb2a15c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/Design_Tools/Scene.meta b/Scripts/Editor/Design_Tools/Scene.meta
new file mode 100644
index 0000000..beb91bc
--- /dev/null
+++ b/Scripts/Editor/Design_Tools/Scene.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 0f94b023b059a5349a6ecd1811ad9334
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/Design_Tools/Scene/IndoorProgressEditor.cs b/Scripts/Editor/Design_Tools/Scene/IndoorProgressEditor.cs
new file mode 100644
index 0000000..bd3e9df
--- /dev/null
+++ b/Scripts/Editor/Design_Tools/Scene/IndoorProgressEditor.cs
@@ -0,0 +1,1585 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using UnityEditor;
+using UnityEngine;
+using OfficeOpenXml;
+using ArtResource;
+using Debug = UnityEngine.Debug;
+
+namespace DesignTools.Scene
+{
+ ///
+ /// 场景进度奖励配置Editor工具
+ /// 读取Docs/config/IndoorProgress.xlsx和Item.xlsx
+ ///
+ public class IndoorProgressEditor : EditorWindow
+ {
+ private const string ART_SO_PATH = "Assets/Art_SubModule/Art_SO";
+ private const string INDOOR_PROGRESS_EXCEL_NAME = "IndoorProgress.xlsx";
+ private const string ITEM_EXCEL_NAME = "Item.xlsx";
+ private const string INDOOR_PROGRESS_SHEET_NAME = "IndoorProgress";
+ private const string ITEM_SHEET_NAME = "Item";
+ private const string DOCS_PATH_PREF_KEY = "IndoorProgressEditor_DocsPath";
+ private const string DESIGN_SUBMODULE_PATH = "Assets/Design_SubModule";
+
+ // IType类型定义(从ItemConfigEditor复制)
+ private static readonly Dictionary ITEM_TYPES = new Dictionary
+ {
+ { 1, "能量" },
+ { 2, "星星" },
+ { 3, "钻石" },
+ { 97, "Playroom宠物道具" },
+ { 98, "卡牌" },
+ { 99, "背包道具" },
+ { 100, "棋子" },
+ { 101, "卡包" },
+ { 102, "限时事件" },
+ { 103, "小猪存钱罐" },
+ { 104, "万能卡" },
+ { 105, "头像框" },
+ { 106, "活动代币" },
+ { 107, "竞赛游戏代币" },
+ { 108, "Pet Playroom拜访道具" },
+ { 109, "表情" },
+ { 110, "头像" },
+ { 111, "Playroom装饰" },
+ { 112, "Playroom服装" },
+ { 113, "Playroom装饰套装" },
+ { 114, "Playroom服装套装" },
+ { 115, "Playroom道具宝箱" },
+ { 116, "活动通行证代币道具" }
+ };
+
+ // 特殊Item的映射关系
+ private static readonly Dictionary MY_ITEM_DICT = new Dictionary()
+ {
+ [100001] = "Energy",
+ [100002] = "Star",
+ [100003] = "Diamond",
+ [100004] = "Cardpack1",
+ [100005] = "Cardpack2",
+ [100006] = "Cardpack3",
+ [100007] = "Cardpack4",
+ [100008] = "Cardpack5",
+ [100011] = "LimitEvent4",
+ [100012] = "LimitEvent2",
+ [100013] = "LimitEvent3",
+ [100014] = "LimitEvent1",
+ [100015] = "LimitEvent5",
+ [100016] = "LimitEvent6",
+ [100017] = "LimitEvent7",
+ [100018] = "LimitEvent9",
+ [100019] = "LimitEvent10",
+ [100020] = "LimitEvent8",
+ [100022] = "PurplePig",
+ [101149] = "LimitEvent2",
+ [101150] = "LimitEvent3",
+ [101151] = "LimitEvent4",
+ [1021] = "705",
+ [1006] = "702",
+ [1007] = "703",
+ [100035] = "Battery",
+ [101445] = "BagItemBox1",
+ [101446] = "BagItemBox2",
+ [101447] = "BagItemBox3",
+ };
+
+ private string docsRootPath = "";
+ private List allProgressDataList = new List();
+ private List filteredProgressDataList = new List();
+ private Dictionary itemDict = new Dictionary(); // Id -> ItemData
+ private List allArtTables = new List();
+
+ private Vector2 scrollPosition;
+ private bool isDataLoaded = false;
+ private int selectedScene = -1;
+ private string sceneInputText = "";
+
+ // 道具奖励临时编辑状态(按数据ID存储)
+ private Dictionary tempItemRewardTypeIndex = new Dictionary();
+
+ // 区域奖励临时编辑状态(按数据ID存储)
+ private Dictionary tempAreaRewardTypeIndex = new Dictionary();
+ private Dictionary tempAreaRewardItemId = new Dictionary();
+ private Dictionary tempAreaRewardNum = new Dictionary();
+
+ [MenuItem("策划工具/场景/场景进度奖励")]
+ public static void ShowWindow()
+ {
+ var window = GetWindow("场景进度奖励配置");
+ window.minSize = new Vector2(1400, 700);
+ window.Show();
+ }
+
+ private void OnEnable()
+ {
+ // 读取上次保存的路径
+ if (EditorPrefs.HasKey(DOCS_PATH_PREF_KEY))
+ {
+ docsRootPath = EditorPrefs.GetString(DOCS_PATH_PREF_KEY);
+ }
+ }
+
+ private void OnGUI()
+ {
+ EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+ EditorGUILayout.LabelField("场景进度奖励配置工具", EditorStyles.boldLabel);
+ GUILayout.FlexibleSpace();
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.Space(5);
+
+ // Docs路径选择
+ EditorGUILayout.BeginVertical("box");
+ EditorGUILayout.LabelField("Docs项目根目录", EditorStyles.boldLabel);
+
+ EditorGUILayout.BeginHorizontal();
+ docsRootPath = EditorGUILayout.TextField("路径", docsRootPath);
+ if (GUILayout.Button("选择文件夹", GUILayout.Width(100)))
+ {
+ string selectedPath = EditorUtility.OpenFolderPanel("选择Docs项目根目录", "", "");
+ if (!string.IsNullOrEmpty(selectedPath))
+ {
+ docsRootPath = selectedPath;
+ EditorPrefs.SetString(DOCS_PATH_PREF_KEY, docsRootPath);
+ }
+ }
+ EditorGUILayout.EndHorizontal();
+
+ if (GUILayout.Button("加载配置数据", GUILayout.Height(30)))
+ {
+ LoadData();
+ }
+
+ EditorGUILayout.EndVertical();
+
+ EditorGUILayout.Space(5);
+
+ // 数据编辑区域
+ if (isDataLoaded)
+ {
+ DrawSceneFilterAndDataEditor();
+ }
+ else
+ {
+ EditorGUILayout.HelpBox("请先选择Docs根目录并加载配置数据", MessageType.Info);
+ }
+ }
+
+ ///
+ /// 加载配置数据
+ ///
+ private void LoadData()
+ {
+ if (string.IsNullOrEmpty(docsRootPath))
+ {
+ EditorUtility.DisplayDialog("错误", "请先选择Docs根目录", "确定");
+ return;
+ }
+
+ try
+ {
+ // 检查并更新Docs仓库
+ if (!CheckAndUpdateDocsRepository())
+ {
+ return;
+ }
+
+ // 检查并切换Design_SubModule到main分支
+ if (!CheckAndSwitchDesignSubModuleBranch())
+ {
+ return;
+ }
+
+ // 加载所有Art_SO资源
+ LoadAllArtTableSO();
+
+ // 加载Item数据
+ LoadItemData();
+
+ // 加载IndoorProgress数据
+ LoadIndoorProgressData();
+
+ isDataLoaded = true;
+ selectedScene = -1;
+ sceneInputText = "";
+ filteredProgressDataList.Clear();
+
+ EditorUtility.DisplayDialog("成功", "配置数据加载完成!", "确定");
+ }
+ catch (Exception e)
+ {
+ EditorUtility.DisplayDialog("错误", $"加载数据失败: {e.Message}\n{e.StackTrace}", "确定");
+ isDataLoaded = false;
+ }
+ }
+
+ ///
+ /// 执行Git命令
+ ///
+ private string ExecuteGitCommand(string workingDirectory, string arguments, out bool success)
+ {
+ try
+ {
+ var processInfo = new ProcessStartInfo
+ {
+ FileName = "git",
+ Arguments = arguments,
+ WorkingDirectory = workingDirectory,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using (var process = Process.Start(processInfo))
+ {
+ string output = process.StandardOutput.ReadToEnd();
+ string error = process.StandardError.ReadToEnd();
+ process.WaitForExit();
+
+ success = process.ExitCode == 0;
+ return success ? output : error;
+ }
+ }
+ catch (Exception e)
+ {
+ success = false;
+ return $"执行Git命令失败: {e.Message}";
+ }
+ }
+
+ ///
+ /// 检查并更新Docs仓库
+ ///
+ private bool CheckAndUpdateDocsRepository()
+ {
+ string gitPath = Path.Combine(docsRootPath, ".git");
+ if (!Directory.Exists(gitPath))
+ {
+ EditorUtility.DisplayDialog("错误",
+ $"所选目录不是Git仓库:{docsRootPath}\n请确保选择的是Docs项目的根目录。",
+ "确定");
+ return false;
+ }
+
+ string fetchResult = ExecuteGitCommand(docsRootPath, "fetch", out bool fetchSuccess);
+
+ if (!fetchSuccess)
+ {
+ bool continueAnyway = EditorUtility.DisplayDialog("警告",
+ $"Git fetch失败:{fetchResult}\n\n是否继续?",
+ "继续", "取消");
+ return continueAnyway;
+ }
+
+ string statusResult = ExecuteGitCommand(docsRootPath, "status", out bool statusSuccess);
+
+ if (!statusSuccess)
+ {
+ EditorUtility.DisplayDialog("错误",
+ $"Git status检查失败:{statusResult}",
+ "确定");
+ return false;
+ }
+
+ if (statusResult.Contains("Changes not staged") || statusResult.Contains("Changes to be committed"))
+ {
+ bool proceed = EditorUtility.DisplayDialog("提示",
+ "Docs仓库有未提交的修改。\n\n建议先提交或暂存这些修改。\n\n是否继续?",
+ "继续", "取消");
+
+ if (!proceed)
+ {
+ return false;
+ }
+ }
+
+ if (statusResult.Contains("Your branch is behind"))
+ {
+ bool doPull = EditorUtility.DisplayDialog("提示",
+ "Docs仓库有远程更新。\n\n是否执行git pull拉取最新代码?\n(建议执行以获取最新配置)",
+ "执行Pull", "跳过");
+
+ if (doPull)
+ {
+ string pullResult = ExecuteGitCommand(docsRootPath, "pull", out bool pullSuccess);
+
+ if (!pullSuccess)
+ {
+ if (pullResult.Contains("CONFLICT") || pullResult.Contains("conflict"))
+ {
+ EditorUtility.DisplayDialog("错误",
+ $"Git pull遇到冲突:\n{pullResult}\n\n请先在SourceTree或其他Git工具中解决冲突。",
+ "确定");
+ }
+ else
+ {
+ EditorUtility.DisplayDialog("错误",
+ $"Git pull失败:{pullResult}",
+ "确定");
+ }
+ return false;
+ }
+
+ EditorUtility.DisplayDialog("成功", "已拉取最新代码!", "确定");
+ }
+ }
+ else
+ {
+ Debug.Log("Docs仓库已是最新");
+ }
+
+ return true;
+ }
+
+ ///
+ /// 检查并切换Design_SubModule到main分支
+ ///
+ private bool CheckAndSwitchDesignSubModuleBranch()
+ {
+ string designSubModulePath = Path.Combine(Application.dataPath.Replace("/Assets", ""), DESIGN_SUBMODULE_PATH);
+ designSubModulePath = designSubModulePath.Replace("\\", "/");
+
+ if (!Directory.Exists(designSubModulePath))
+ {
+ EditorUtility.DisplayDialog("错误",
+ $"Design_SubModule目录不存在:{designSubModulePath}",
+ "确定");
+ return false;
+ }
+
+ string branchResult = ExecuteGitCommand(designSubModulePath, "rev-parse --abbrev-ref HEAD", out bool branchSuccess);
+
+ if (!branchSuccess)
+ {
+ EditorUtility.DisplayDialog("错误",
+ $"无法获取Design_SubModule当前分支:{branchResult}",
+ "确定");
+ return false;
+ }
+
+ string currentBranch = branchResult.Trim();
+
+ if (currentBranch != "main")
+ {
+ bool doCheckout = EditorUtility.DisplayDialog("提示",
+ $"Design_SubModule当前在 '{currentBranch}' 分支。\n\n是否切换到main分支?",
+ "切换到main", "取消");
+
+ if (doCheckout)
+ {
+ string checkoutResult = ExecuteGitCommand(designSubModulePath, "checkout main", out bool checkoutSuccess);
+
+ if (!checkoutSuccess)
+ {
+ EditorUtility.DisplayDialog("错误",
+ $"切换到main分支失败:{checkoutResult}",
+ "确定");
+ return false;
+ }
+
+ EditorUtility.DisplayDialog("成功", "已切换到main分支!", "确定");
+ }
+ else
+ {
+ return false;
+ }
+ }
+ else
+ {
+ Debug.Log("Design_SubModule已在main分支");
+ }
+
+ return true;
+ }
+
+ ///
+ /// 加载所有Art_SO资源
+ ///
+ private void LoadAllArtTableSO()
+ {
+ allArtTables.Clear();
+
+ string[] guids = AssetDatabase.FindAssets("t:ArtTableSO", new[] { ART_SO_PATH });
+ foreach (string guid in guids)
+ {
+ string path = AssetDatabase.GUIDToAssetPath(guid);
+ ArtTableSO artTable = AssetDatabase.LoadAssetAtPath(path);
+ if (artTable != null)
+ {
+ allArtTables.Add(artTable);
+ }
+ }
+
+ Debug.Log($"加载了 {allArtTables.Count} 个ArtTableSO资源");
+ }
+
+ ///
+ /// 加载Item数据
+ ///
+ private void LoadItemData()
+ {
+ itemDict.Clear();
+
+ string itemExcelPath = Path.Combine(docsRootPath, "config", ITEM_EXCEL_NAME);
+ if (!File.Exists(itemExcelPath))
+ {
+ throw new Exception($"未找到Item配置文件: {itemExcelPath}");
+ }
+
+ using (var package = new ExcelPackage(new FileInfo(itemExcelPath)))
+ {
+ var worksheet = package.Workbook.Worksheets[ITEM_SHEET_NAME];
+ if (worksheet == null)
+ {
+ throw new Exception($"Item.xlsx中未找到Sheet: {ITEM_SHEET_NAME}");
+ }
+
+ // 读取表头(第1行)
+ int idCol = -1, nameCol = -1, iTypeCol = -1, resCol = -1;
+ int columnCount = worksheet.Dimension.Columns;
+
+ for (int col = 1; col <= columnCount; col++)
+ {
+ string header = worksheet.Cells[1, col].Text;
+ if (header == "Id") idCol = col;
+ else if (header == "Name") nameCol = col;
+ else if (header == "IType") iTypeCol = col;
+ else if (header == "Res") resCol = col;
+ }
+
+ if (idCol < 0 || nameCol < 0 || iTypeCol < 0)
+ {
+ throw new Exception("Item.xlsx表结构不正确,缺少必要列(Id/Name/IType)");
+ }
+
+ // 读取数据(从第3行开始)
+ int rowCount = worksheet.Dimension.Rows;
+ for (int row = 3; row <= rowCount; row++)
+ {
+ string idText = worksheet.Cells[row, idCol].Text;
+ if (string.IsNullOrEmpty(idText)) continue;
+
+ if (!int.TryParse(idText, out int id)) continue;
+
+ string name = worksheet.Cells[row, nameCol].Text;
+ string iTypeText = worksheet.Cells[row, iTypeCol].Text;
+ string res = resCol > 0 ? worksheet.Cells[row, resCol].Text : "";
+
+ if (!int.TryParse(iTypeText, out int iType)) continue;
+
+ itemDict[id] = new ItemData
+ {
+ Id = id,
+ Name = name,
+ IType = iType,
+ Res = res
+ };
+ }
+ }
+
+ Debug.Log($"加载了 {itemDict.Count} 个Item数据");
+ }
+
+ ///
+ /// 加载IndoorProgress数据
+ ///
+ private void LoadIndoorProgressData()
+ {
+ allProgressDataList.Clear();
+
+ string progressExcelPath = Path.Combine(docsRootPath, "config", INDOOR_PROGRESS_EXCEL_NAME);
+ if (!File.Exists(progressExcelPath))
+ {
+ throw new Exception($"未找到IndoorProgress配置文件: {progressExcelPath}");
+ }
+
+ using (var package = new ExcelPackage(new FileInfo(progressExcelPath)))
+ {
+ var worksheet = package.Workbook.Worksheets[INDOOR_PROGRESS_SHEET_NAME];
+ if (worksheet == null)
+ {
+ throw new Exception($"IndoorProgress.xlsx中未找到Sheet: {INDOOR_PROGRESS_SHEET_NAME}");
+ }
+
+ // 读取表头(第1行)
+ int idCol = -1, sceneCol = -1, lvCol = -1, itemCol = -1, emitCol = -1,
+ rewardCol = -1, bigRewardCol = -1, areaRewardCol = -1, partCol = -1;
+ int columnCount = worksheet.Dimension.Columns;
+
+ for (int col = 1; col <= columnCount; col++)
+ {
+ string header = worksheet.Cells[1, col].Text;
+ if (header == "Id") idCol = col;
+ else if (header == "Scene") sceneCol = col;
+ else if (header == "Lv") lvCol = col;
+ else if (header == "Item") itemCol = col;
+ else if (header == "Emit") emitCol = col;
+ else if (header == "Reward") rewardCol = col;
+ else if (header == "BigReward") bigRewardCol = col;
+ else if (header == "AreaReward") areaRewardCol = col;
+ else if (header == "Part") partCol = col;
+ }
+
+ if (idCol < 0 || sceneCol < 0 || lvCol < 0)
+ {
+ throw new Exception("IndoorProgress.xlsx表结构不正确,缺少必要列(Id/Scene/Lv)");
+ }
+
+ // 读取数据(从第3行开始)
+ int rowCount = worksheet.Dimension.Rows;
+ for (int row = 3; row <= rowCount; row++)
+ {
+ string idText = worksheet.Cells[row, idCol].Text;
+ if (string.IsNullOrEmpty(idText)) continue;
+
+ if (!int.TryParse(idText, out int id)) continue;
+
+ string sceneText = worksheet.Cells[row, sceneCol].Text;
+ string lvText = worksheet.Cells[row, lvCol].Text;
+
+ if (!int.TryParse(sceneText, out int scene)) continue;
+ if (!int.TryParse(lvText, out int lv)) continue;
+
+ string item = itemCol > 0 ? worksheet.Cells[row, itemCol].Text : "";
+ string emit = emitCol > 0 ? worksheet.Cells[row, emitCol].Text : "";
+ string reward = rewardCol > 0 ? worksheet.Cells[row, rewardCol].Text : "";
+ string bigReward = bigRewardCol > 0 ? worksheet.Cells[row, bigRewardCol].Text : "";
+ string areaReward = areaRewardCol > 0 ? worksheet.Cells[row, areaRewardCol].Text : "";
+ string partText = partCol > 0 ? worksheet.Cells[row, partCol].Text : "";
+
+ int.TryParse(partText, out int part);
+
+ var progressData = new IndoorProgressData
+ {
+ Id = id,
+ Scene = scene,
+ Lv = lv,
+ Item = item,
+ Emit = emit,
+ Reward = reward,
+ BigReward = bigReward,
+ AreaReward = areaReward,
+ Part = part
+ };
+
+ allProgressDataList.Add(progressData);
+ }
+ }
+
+ Debug.Log($"加载了 {allProgressDataList.Count} 条IndoorProgress数据");
+ }
+
+ ///
+ /// 绘制场景筛选和数据编辑器
+ ///
+ private void DrawSceneFilterAndDataEditor()
+ {
+ EditorGUILayout.BeginVertical("box");
+
+ // 场景筛选
+ EditorGUILayout.LabelField("场景筛选", EditorStyles.boldLabel);
+ EditorGUILayout.BeginHorizontal();
+
+ sceneInputText = EditorGUILayout.TextField("输入场景ID", sceneInputText, GUILayout.Width(200));
+
+ if (GUILayout.Button("确认加载场景", GUILayout.Width(120)))
+ {
+ if (int.TryParse(sceneInputText, out int sceneId))
+ {
+ selectedScene = sceneId;
+ FilterProgressByScene();
+ }
+ else
+ {
+ EditorUtility.DisplayDialog("错误", "请输入有效的场景ID(数字)", "确定");
+ }
+ }
+
+ if (selectedScene > 0)
+ {
+ EditorGUILayout.LabelField($"当前场景: {selectedScene}", EditorStyles.boldLabel, GUILayout.Width(150));
+ }
+
+ EditorGUILayout.EndHorizontal();
+ EditorGUILayout.EndVertical();
+
+ if (selectedScene > 0 && filteredProgressDataList.Count > 0)
+ {
+ EditorGUILayout.Space(10);
+
+ // 新增场景按钮
+ EditorGUILayout.BeginHorizontal();
+ if (GUILayout.Button("+ 新增场景", GUILayout.Height(30), GUILayout.Width(150)))
+ {
+ AddNewScene();
+ }
+ GUILayout.FlexibleSpace();
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.Space(5);
+
+ // 数据列表
+ scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
+
+ EditorGUILayout.BeginVertical("box");
+ DrawProgressDataList();
+ EditorGUILayout.EndVertical();
+
+ EditorGUILayout.EndScrollView();
+
+ EditorGUILayout.Space(10);
+
+ // 保存按钮
+ EditorGUILayout.BeginHorizontal();
+ GUILayout.FlexibleSpace();
+ if (GUILayout.Button("保存配置到Excel", GUILayout.Height(40), GUILayout.Width(200)))
+ {
+ SaveData();
+ }
+ GUILayout.FlexibleSpace();
+ EditorGUILayout.EndHorizontal();
+ }
+ else if (selectedScene > 0)
+ {
+ EditorGUILayout.Space(10);
+ EditorGUILayout.HelpBox($"场景 {selectedScene} 没有数据", MessageType.Warning);
+
+ // 新增场景按钮
+ EditorGUILayout.BeginHorizontal();
+ if (GUILayout.Button("+ 新增场景", GUILayout.Height(30), GUILayout.Width(150)))
+ {
+ AddNewScene();
+ }
+ GUILayout.FlexibleSpace();
+ EditorGUILayout.EndHorizontal();
+ }
+ else
+ {
+ EditorGUILayout.Space(10);
+ EditorGUILayout.HelpBox("请输入场景ID并点击确认加载", MessageType.Info);
+ }
+ }
+
+ ///
+ /// 根据场景筛选数据
+ ///
+ private void FilterProgressByScene()
+ {
+ filteredProgressDataList = allProgressDataList
+ .Where(x => x.Scene == selectedScene)
+ .OrderBy(x => x.Lv)
+ .ToList();
+ }
+
+ ///
+ /// 绘制进度数据列表
+ ///
+ private void DrawProgressDataList()
+ {
+ // 表头
+ EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+ EditorGUILayout.LabelField("ID", EditorStyles.boldLabel, GUILayout.Width(40));
+ EditorGUILayout.LabelField("Lv", EditorStyles.boldLabel, GUILayout.Width(40));
+ EditorGUILayout.LabelField("道具奖励", EditorStyles.boldLabel, GUILayout.Width(450));
+ EditorGUILayout.LabelField("区域奖励", EditorStyles.boldLabel, GUILayout.Width(450));
+ EditorGUILayout.LabelField("零件", EditorStyles.boldLabel, GUILayout.Width(60));
+ EditorGUILayout.LabelField("操作", EditorStyles.boldLabel, GUILayout.Width(60));
+ EditorGUILayout.EndHorizontal();
+
+ // 数据行
+ for (int i = 0; i < filteredProgressDataList.Count; i++)
+ {
+ DrawProgressDataRow(filteredProgressDataList[i], i);
+ }
+ }
+
+ ///
+ /// 绘制单行进度数据
+ ///
+ private void DrawProgressDataRow(IndoorProgressData data, int index)
+ {
+ EditorGUILayout.BeginHorizontal("box");
+
+ // ID(不可编辑)
+ EditorGUILayout.LabelField(data.Id.ToString(), GUILayout.Width(40));
+
+ // Lv(可编辑)
+ EditorGUI.BeginChangeCheck();
+ int newLv = EditorGUILayout.IntField(data.Lv, GUILayout.Width(40));
+ if (EditorGUI.EndChangeCheck())
+ {
+ data.Lv = newLv;
+ }
+
+ // 道具奖励(Item/Emit/Reward一体)
+ EditorGUILayout.BeginVertical(GUILayout.Width(450));
+ DrawItemRewardEditor(data);
+ EditorGUILayout.EndVertical();
+
+ // 区域奖励(AreaReward/BigReward)
+ EditorGUILayout.BeginVertical(GUILayout.Width(450));
+ DrawAreaRewardEditor(data);
+ EditorGUILayout.EndVertical();
+
+ // 零件数量
+ EditorGUI.BeginChangeCheck();
+ int newPart = EditorGUILayout.IntField(data.Part, GUILayout.Width(60));
+ if (EditorGUI.EndChangeCheck())
+ {
+ data.Part = newPart;
+ }
+
+ // 操作按钮
+ if (GUILayout.Button("删除", GUILayout.Width(60)))
+ {
+ if (EditorUtility.DisplayDialog("确认删除",
+ $"确定要删除ID {data.Id} (场景{data.Scene}-Lv{data.Lv}) 的数据吗?",
+ "删除", "取消"))
+ {
+ filteredProgressDataList.RemoveAt(index);
+ allProgressDataList.Remove(data);
+ }
+ }
+
+ EditorGUILayout.EndHorizontal();
+ }
+
+ ///
+ /// 绘制道具奖励编辑器(Item/Emit/Reward)
+ ///
+ private void DrawItemRewardEditor(IndoorProgressData data)
+ {
+ // 解析当前数据
+ var itemReward = ParseItemReward(data.Item);
+
+ EditorGUILayout.BeginHorizontal();
+
+ // 选择IType
+ var typeOptions = new List { "无" };
+ var typeValues = new List { -1 };
+
+ foreach (var kvp in ITEM_TYPES.OrderBy(x => x.Key))
+ {
+ typeOptions.Add($"{kvp.Value} ({kvp.Key})");
+ typeValues.Add(kvp.Key);
+ }
+
+ // 初始化临时状态:如果有数据就用数据的类型,如果没数据就用临时保存的选择
+ if (!tempItemRewardTypeIndex.ContainsKey(data.Id))
+ {
+ if (itemReward.HasValue)
+ {
+ int itemType = GetItemType(itemReward.Value.Id);
+ tempItemRewardTypeIndex[data.Id] = typeValues.IndexOf(itemType);
+ }
+ else
+ {
+ tempItemRewardTypeIndex[data.Id] = 0; // 默认"无"
+ }
+ }
+ else if (itemReward.HasValue)
+ {
+ // 如果有数据,同步临时状态
+ int itemType = GetItemType(itemReward.Value.Id);
+ int typeIndex = typeValues.IndexOf(itemType);
+ if (typeIndex >= 0)
+ {
+ tempItemRewardTypeIndex[data.Id] = typeIndex;
+ }
+ }
+
+ int currentTypeIndex = tempItemRewardTypeIndex[data.Id];
+ if (currentTypeIndex < 0) currentTypeIndex = 0;
+
+ EditorGUI.BeginChangeCheck();
+ int newTypeIndex = EditorGUILayout.Popup(currentTypeIndex, typeOptions.ToArray(), GUILayout.Width(120));
+ bool typeChanged = EditorGUI.EndChangeCheck();
+
+ if (typeChanged)
+ {
+ tempItemRewardTypeIndex[data.Id] = newTypeIndex;
+ }
+
+ if (newTypeIndex == 0)
+ {
+ // 选择"无"
+ if (itemReward.HasValue)
+ {
+ data.Item = "";
+ data.Emit = "";
+ data.Reward = "";
+ }
+ }
+ else
+ {
+ int selectedType = typeValues[newTypeIndex];
+
+ // 如果类型改变,且当前有数据但类型不匹配,清空数据
+ if (typeChanged && itemReward.HasValue && GetItemType(itemReward.Value.Id) != selectedType)
+ {
+ data.Item = "";
+ data.Emit = "";
+ data.Reward = "";
+ itemReward = null;
+ }
+
+ // 根据类型筛选Item
+ var itemsOfType = itemDict.Values.Where(x => x.IType == selectedType).ToList();
+
+ if (itemsOfType.Count > 0)
+ {
+ // IType=100(棋子)使用输入框+刷新按钮,其他使用下拉菜单
+ if (selectedType == 100)
+ {
+ // 输入ID
+ EditorGUILayout.LabelField("ID:", GUILayout.Width(25));
+
+ // 当前ID:如果类型改变,重置为0;否则使用现有值
+ int inputId = 0;
+ if (!typeChanged && itemReward.HasValue && GetItemType(itemReward.Value.Id) == 100)
+ {
+ inputId = itemReward.Value.Id;
+ }
+
+ EditorGUI.BeginChangeCheck();
+ inputId = EditorGUILayout.IntField(inputId, GUILayout.Width(60));
+ bool idChanged = EditorGUI.EndChangeCheck();
+
+ // 数量
+ EditorGUILayout.LabelField("数量:", GUILayout.Width(35));
+ int num = itemReward.HasValue ? itemReward.Value.Num : 1;
+ EditorGUI.BeginChangeCheck();
+ num = EditorGUILayout.IntField(num, GUILayout.Width(40));
+ bool numChanged = EditorGUI.EndChangeCheck();
+
+ // 刷新按钮
+ if (GUILayout.Button("刷新", GUILayout.Width(50)))
+ {
+ if (inputId > 0 && itemDict.ContainsKey(inputId) && itemDict[inputId].IType == 100)
+ {
+ UpdateItemReward(data, inputId, num);
+ }
+ else
+ {
+ EditorUtility.DisplayDialog("错误", $"ID {inputId} 不存在或不是棋子类型", "确定");
+ }
+ }
+
+ // 如果ID或数量改变,自动更新
+ if (idChanged || numChanged)
+ {
+ if (inputId > 0 && itemDict.ContainsKey(inputId) && itemDict[inputId].IType == 100)
+ {
+ UpdateItemReward(data, inputId, num);
+ }
+ }
+
+ // 显示预览图
+ if (inputId > 0 && itemDict.TryGetValue(inputId, out var item) && item.IType == 100)
+ {
+ DrawItemPreview(item);
+ }
+ else
+ {
+ EditorGUILayout.LabelField("暂无预览", GUILayout.Width(60));
+ }
+ }
+ else
+ {
+ // 其他类型使用下拉菜单
+ var itemOptions = itemsOfType.Select(x => $"{x.Name} ({x.Id})").ToList();
+ var itemIds = itemsOfType.Select(x => x.Id).ToList();
+
+ int currentItemIndex = 0;
+ if (!typeChanged && itemReward.HasValue)
+ {
+ currentItemIndex = itemIds.IndexOf(itemReward.Value.Id);
+ if (currentItemIndex < 0) currentItemIndex = 0;
+ }
+
+ EditorGUI.BeginChangeCheck();
+ int newItemIndex = EditorGUILayout.Popup(currentItemIndex, itemOptions.ToArray(), GUILayout.Width(180));
+ int selectedItemId = itemIds[newItemIndex];
+
+ // 数量
+ int num = itemReward.HasValue ? itemReward.Value.Num : 1;
+ num = EditorGUILayout.IntField(num, GUILayout.Width(40));
+
+ // 更新数据
+ if (EditorGUI.EndChangeCheck() || typeChanged || !itemReward.HasValue)
+ {
+ UpdateItemReward(data, selectedItemId, num);
+ }
+
+ // 显示预览图
+ if (itemDict.TryGetValue(selectedItemId, out var item2))
+ {
+ DrawItemPreview(item2);
+ }
+ }
+ }
+ else
+ {
+ EditorGUILayout.LabelField("该类型无可用Item", GUILayout.Width(150));
+ }
+ }
+
+ EditorGUILayout.EndHorizontal();
+ }
+
+ ///
+ /// 绘制区域奖励编辑器(AreaReward/BigReward)
+ ///
+ private void DrawAreaRewardEditor(IndoorProgressData data)
+ {
+ // 判断当前Lv是否可以配置AreaReward
+ bool canConfigArea = CanConfigAreaReward(data.Scene, data.Lv);
+
+ if (!canConfigArea)
+ {
+ // 不显示任何内容,保持空白
+ return;
+ }
+
+ // 解析当前AreaReward
+ var areaRewards = ParseAreaReward(data.AreaReward);
+
+ EditorGUILayout.BeginVertical();
+
+ // 显示现有奖励
+ for (int i = 0; i < areaRewards.Count; i++)
+ {
+ EditorGUILayout.BeginHorizontal();
+
+ var reward = areaRewards[i];
+
+ // 显示Item信息和图片
+ if (itemDict.TryGetValue(reward.Id, out var rewardItem))
+ {
+ EditorGUILayout.LabelField($"{rewardItem.Name}", GUILayout.Width(100));
+
+ // 显示预览图
+ DrawItemPreview(rewardItem);
+ }
+ else
+ {
+ EditorGUILayout.LabelField($"ID:{reward.Id}", GUILayout.Width(100));
+ EditorGUILayout.LabelField("无图", GUILayout.Width(50));
+ }
+
+ // 数量
+ EditorGUILayout.LabelField("x", GUILayout.Width(10));
+ EditorGUI.BeginChangeCheck();
+ int newNum = EditorGUILayout.IntField(reward.Num, GUILayout.Width(40));
+ if (EditorGUI.EndChangeCheck())
+ {
+ areaRewards[i] = new ItemReward { Id = reward.Id, Num = newNum };
+ UpdateAreaReward(data, areaRewards);
+ }
+
+ // 删除按钮
+ if (GUILayout.Button("-", GUILayout.Width(30)))
+ {
+ areaRewards.RemoveAt(i);
+ UpdateAreaReward(data, areaRewards);
+ break;
+ }
+
+ EditorGUILayout.EndHorizontal();
+ }
+
+ // 添加新奖励UI
+ DrawAddAreaRewardUI(data, areaRewards);
+
+ EditorGUILayout.EndVertical();
+ }
+
+ ///
+ /// 判断是否可以配置AreaReward
+ ///
+ private bool CanConfigAreaReward(int scene, int lv)
+ {
+ if (scene == 1)
+ {
+ // 场景1: 20, 29, 36, 44
+ return lv == 20 || lv == 29 || lv == 36 || lv == 44;
+ }
+ else if (scene >= 2 && scene <= 5)
+ {
+ // 场景2-5: 20
+ return lv == 20;
+ }
+ else
+ {
+ // 其他场景: 25
+ return lv == 25;
+ }
+ }
+
+ ///
+ /// 绘制添加区域奖励UI
+ ///
+ private void DrawAddAreaRewardUI(IndoorProgressData data, List areaRewards)
+ {
+ EditorGUILayout.BeginHorizontal();
+
+ // 选择IType
+ var typeOptions = new List { "选择类型..." };
+ var typeValues = new List { -1 };
+
+ foreach (var kvp in ITEM_TYPES.OrderBy(x => x.Key))
+ {
+ typeOptions.Add($"{kvp.Value} ({kvp.Key})");
+ typeValues.Add(kvp.Key);
+ }
+
+ // 使用临时变量存储当前选择
+ if (!tempAreaRewardTypeIndex.ContainsKey(data.Id))
+ {
+ tempAreaRewardTypeIndex[data.Id] = 0;
+ }
+ if (!tempAreaRewardItemId.ContainsKey(data.Id))
+ {
+ tempAreaRewardItemId[data.Id] = 0;
+ }
+ if (!tempAreaRewardNum.ContainsKey(data.Id))
+ {
+ tempAreaRewardNum[data.Id] = 1;
+ }
+
+ int currentTypeIndex = tempAreaRewardTypeIndex[data.Id];
+
+ EditorGUI.BeginChangeCheck();
+ int newTypeIndex = EditorGUILayout.Popup(currentTypeIndex, typeOptions.ToArray(), GUILayout.Width(120));
+ bool typeChanged = EditorGUI.EndChangeCheck();
+
+ if (typeChanged)
+ {
+ tempAreaRewardTypeIndex[data.Id] = newTypeIndex;
+ tempAreaRewardItemId[data.Id] = 0; // 重置ItemId
+ }
+
+ if (newTypeIndex > 0)
+ {
+ int selectedType = typeValues[newTypeIndex];
+
+ // 根据类型筛选Item
+ var itemsOfType = itemDict.Values.Where(x => x.IType == selectedType).ToList();
+
+ if (itemsOfType.Count > 0)
+ {
+ // IType=100(棋子)使用输入框+添加按钮
+ if (selectedType == 100)
+ {
+ EditorGUILayout.LabelField("ID:", GUILayout.Width(25));
+ int inputId = tempAreaRewardItemId[data.Id];
+ inputId = EditorGUILayout.IntField(inputId, GUILayout.Width(60));
+ tempAreaRewardItemId[data.Id] = inputId;
+
+ EditorGUILayout.LabelField("数量:", GUILayout.Width(35));
+ int num = tempAreaRewardNum[data.Id];
+ num = EditorGUILayout.IntField(num, GUILayout.Width(40));
+ tempAreaRewardNum[data.Id] = num;
+
+ if (GUILayout.Button("添加", GUILayout.Width(50)))
+ {
+ if (itemDict.ContainsKey(inputId) && itemDict[inputId].IType == 100)
+ {
+ areaRewards.Add(new ItemReward { Id = inputId, Num = num });
+ UpdateAreaReward(data, areaRewards);
+ // 重置
+ tempAreaRewardTypeIndex[data.Id] = 0;
+ tempAreaRewardItemId[data.Id] = 0;
+ tempAreaRewardNum[data.Id] = 1;
+ }
+ else
+ {
+ EditorUtility.DisplayDialog("错误", $"ID {inputId} 不存在或不是棋子类型", "确定");
+ }
+ }
+
+ // 显示预览图
+ if (itemDict.TryGetValue(inputId, out var previewItem) && previewItem.IType == 100)
+ {
+ DrawItemPreview(previewItem);
+ }
+ else
+ {
+ EditorGUILayout.LabelField("暂无预览", GUILayout.Width(60));
+ }
+ }
+ else
+ {
+ // 其他类型使用下拉菜单
+ var itemOptions = itemsOfType.Select(x => $"{x.Name} ({x.Id})").ToList();
+ var itemIds = itemsOfType.Select(x => x.Id).ToList();
+
+ int savedItemId = tempAreaRewardItemId[data.Id];
+ int currentItemIndex = itemIds.IndexOf(savedItemId);
+ if (currentItemIndex < 0) currentItemIndex = 0;
+
+ EditorGUI.BeginChangeCheck();
+ int newItemIndex = EditorGUILayout.Popup(currentItemIndex, itemOptions.ToArray(), GUILayout.Width(150));
+ if (EditorGUI.EndChangeCheck())
+ {
+ tempAreaRewardItemId[data.Id] = itemIds[newItemIndex];
+ }
+
+ int selectedItemId = itemIds[newItemIndex];
+
+ // 数量
+ int num = tempAreaRewardNum[data.Id];
+ num = EditorGUILayout.IntField(num, GUILayout.Width(40));
+ tempAreaRewardNum[data.Id] = num;
+
+ if (GUILayout.Button("添加", GUILayout.Width(50)))
+ {
+ areaRewards.Add(new ItemReward { Id = selectedItemId, Num = num });
+ UpdateAreaReward(data, areaRewards);
+ // 重置
+ tempAreaRewardTypeIndex[data.Id] = 0;
+ tempAreaRewardItemId[data.Id] = 0;
+ tempAreaRewardNum[data.Id] = 1;
+ }
+
+ // 显示预览图
+ if (itemDict.TryGetValue(selectedItemId, out var previewItem2))
+ {
+ DrawItemPreview(previewItem2);
+ }
+ }
+ }
+ else
+ {
+ EditorGUILayout.LabelField("该类型无可用Item", GUILayout.Width(120));
+ }
+ }
+
+ EditorGUILayout.EndHorizontal();
+ }
+
+ ///
+ /// 解析Item奖励
+ ///
+ private ItemReward? ParseItemReward(string itemJson)
+ {
+ if (string.IsNullOrEmpty(itemJson)) return null;
+
+ try
+ {
+ // 格式: [{"Id":83,"Num":1}]
+ itemJson = itemJson.Trim();
+ if (!itemJson.StartsWith("[") || !itemJson.EndsWith("]"))
+ return null;
+
+ itemJson = itemJson.Substring(1, itemJson.Length - 2); // 去掉[]
+
+ // 简单解析
+ var idMatch = System.Text.RegularExpressions.Regex.Match(itemJson, @"""Id"":(\d+)");
+ var numMatch = System.Text.RegularExpressions.Regex.Match(itemJson, @"""Num"":(\d+)");
+
+ if (idMatch.Success && numMatch.Success)
+ {
+ return new ItemReward
+ {
+ Id = int.Parse(idMatch.Groups[1].Value),
+ Num = int.Parse(numMatch.Groups[1].Value)
+ };
+ }
+ }
+ catch { }
+
+ return null;
+ }
+
+ ///
+ /// 解析区域奖励
+ ///
+ private List ParseAreaReward(string areaRewardJson)
+ {
+ var result = new List();
+
+ if (string.IsNullOrEmpty(areaRewardJson)) return result;
+
+ try
+ {
+ // 格式: [{"Id":100001,"Num":25}] 或 [{"Id":100001,"Num":50},{"Id":100021,"Num":1}]
+ var matches = System.Text.RegularExpressions.Regex.Matches(areaRewardJson, @"\{""Id"":(\d+),""Num"":(\d+)\}");
+
+ foreach (System.Text.RegularExpressions.Match match in matches)
+ {
+ result.Add(new ItemReward
+ {
+ Id = int.Parse(match.Groups[1].Value),
+ Num = int.Parse(match.Groups[2].Value)
+ });
+ }
+ }
+ catch { }
+
+ return result;
+ }
+
+ ///
+ /// 获取Item的类型
+ ///
+ private int GetItemType(int itemId)
+ {
+ if (itemDict.TryGetValue(itemId, out var item))
+ {
+ return item.IType;
+ }
+ return -1;
+ }
+
+ ///
+ /// 更新Item奖励
+ ///
+ private void UpdateItemReward(IndoorProgressData data, int itemId, int num)
+ {
+ // Item格式: [{"Id":83,"Num":1}]
+ data.Item = $"[{{\"Id\":{itemId},\"Num\":{num}}}]";
+
+ // Emit格式: 83
+ data.Emit = itemId.ToString();
+
+ // Reward格式: 83=1
+ data.Reward = $"{itemId}={num}";
+ }
+
+ ///
+ /// 更新区域奖励
+ ///
+ private void UpdateAreaReward(IndoorProgressData data, List areaRewards)
+ {
+ if (areaRewards.Count == 0)
+ {
+ data.AreaReward = "";
+ data.BigReward = "";
+
+ // 如果这是最后一个AreaReward位置,需要清空Lv=1的BigReward
+ if (IsLastAreaRewardPosition(data.Scene, data.Lv))
+ {
+ var lv1Data = allProgressDataList.FirstOrDefault(x => x.Scene == data.Scene && x.Lv == 1);
+ if (lv1Data != null)
+ {
+ lv1Data.BigReward = "";
+ }
+ }
+ return;
+ }
+
+ // AreaReward格式: [{"Id":100001,"Num":25}] 或 [{"Id":100001,"Num":50},{"Id":100021,"Num":1}]
+ var areaRewardItems = areaRewards.Select(r => $"{{\"Id\":{r.Id},\"Num\":{r.Num}}}");
+ data.AreaReward = $"[{string.Join(",", areaRewardItems)}]";
+
+ // 当前行的BigReward清空(只有Lv=1才有BigReward)
+ data.BigReward = "";
+
+ // 如果这是最后一个AreaReward位置,更新Lv=1的BigReward
+ if (IsLastAreaRewardPosition(data.Scene, data.Lv))
+ {
+ var lv1Data = allProgressDataList.FirstOrDefault(x => x.Scene == data.Scene && x.Lv == 1);
+ if (lv1Data != null)
+ {
+ // BigReward格式: Energy=25 或 Energy=50,PurplePig=1,101451=1
+ var bigRewardItems = new List();
+ bool hasUnmappedItem = false;
+
+ foreach (var reward in areaRewards)
+ {
+ if (MY_ITEM_DICT.TryGetValue(reward.Id, out string mappedName))
+ {
+ bigRewardItems.Add($"{mappedName}={reward.Num}");
+ }
+ else
+ {
+ bigRewardItems.Add($"{reward.Id}={reward.Num}");
+ hasUnmappedItem = true;
+ }
+ }
+
+ lv1Data.BigReward = string.Join(",", bigRewardItems);
+
+ // 如果有未映射的Item,显示警告
+ if (hasUnmappedItem)
+ {
+ Debug.LogWarning($"场景{data.Scene}的AreaReward包含未映射的ItemId,可能会出问题!");
+ }
+ }
+ }
+ }
+
+ ///
+ /// 判断当前Lv是否为最后一个AreaReward位置
+ ///
+ private bool IsLastAreaRewardPosition(int scene, int lv)
+ {
+ if (scene == 1)
+ {
+ // 场景1的最后一个是44
+ return lv == 44;
+ }
+ else if (scene >= 2 && scene <= 5)
+ {
+ // 场景2-5的最后一个是20
+ return lv == 20;
+ }
+ else
+ {
+ // 其他场景的最后一个是25
+ return lv == 25;
+ }
+ }
+
+ ///
+ /// 绘制Item预览
+ ///
+ private void DrawItemPreview(ItemData item)
+ {
+ if (string.IsNullOrEmpty(item.Res))
+ {
+ EditorGUILayout.LabelField("无图", GUILayout.Width(50));
+ return;
+ }
+
+ // 解析Res格式: "tableId,itemId"
+ var parts = item.Res.Split(',');
+ if (parts.Length != 2)
+ {
+ EditorGUILayout.LabelField("无图", GUILayout.Width(50));
+ return;
+ }
+
+ if (!int.TryParse(parts[0], out int tableId)) return;
+ if (!int.TryParse(parts[1], out int itemId)) return;
+
+ // 查找对应的ArtTableSO
+ var artTable = allArtTables.FirstOrDefault(x => x.TableId == tableId);
+ if (artTable == null)
+ {
+ EditorGUILayout.LabelField("无图", GUILayout.Width(50));
+ return;
+ }
+
+ var artItem = artTable.Items.FirstOrDefault(x => x.Id == itemId);
+ if (artItem == null || artItem.Sprite == null)
+ {
+ EditorGUILayout.LabelField("无图", GUILayout.Width(50));
+ return;
+ }
+
+ // 显示预览图
+ Rect rect = GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
+ GUI.DrawTexture(rect, artItem.Sprite.texture, ScaleMode.ScaleToFit);
+ }
+
+ ///
+ /// 新增场景
+ ///
+ private void AddNewScene()
+ {
+ // 找到最大的Scene ID
+ int maxScene = allProgressDataList.Count > 0 ? allProgressDataList.Max(x => x.Scene) : 0;
+ int newScene = maxScene + 1;
+
+ // 找到最大的ID
+ int maxId = allProgressDataList.Count > 0 ? allProgressDataList.Max(x => x.Id) : 0;
+
+ // 创建25个新记录
+ for (int lv = 1; lv <= 25; lv++)
+ {
+ maxId++;
+
+ var newData = new IndoorProgressData
+ {
+ Id = maxId,
+ Scene = newScene,
+ Lv = lv,
+ Item = "",
+ Emit = "",
+ Reward = "",
+ BigReward = "",
+ AreaReward = "",
+ Part = 0
+ };
+
+ // 如果是Lv25,添加默认的AreaReward
+ if (lv == 25)
+ {
+ // 默认给25能量
+ newData.AreaReward = "[{\"Id\":100001,\"Num\":25}]";
+ newData.BigReward = "Energy=25";
+ }
+
+ allProgressDataList.Add(newData);
+ }
+
+ // 切换到新场景
+ selectedScene = newScene;
+ sceneInputText = newScene.ToString();
+ FilterProgressByScene();
+
+ EditorUtility.DisplayDialog("成功", $"已创建场景 {newScene},包含25个步骤(Lv 1-25)", "确定");
+ }
+
+ ///
+ /// 保存数据
+ ///
+ private void SaveData()
+ {
+ try
+ {
+ string progressExcelPath = Path.Combine(docsRootPath, "config", INDOOR_PROGRESS_EXCEL_NAME);
+ if (!File.Exists(progressExcelPath))
+ {
+ EditorUtility.DisplayDialog("错误", $"未找到IndoorProgress配置文件: {progressExcelPath}", "确定");
+ return;
+ }
+
+ using (var package = new ExcelPackage(new FileInfo(progressExcelPath)))
+ {
+ var worksheet = package.Workbook.Worksheets[INDOOR_PROGRESS_SHEET_NAME];
+ if (worksheet == null)
+ {
+ EditorUtility.DisplayDialog("错误", $"IndoorProgress.xlsx中未找到Sheet: {INDOOR_PROGRESS_SHEET_NAME}", "确定");
+ return;
+ }
+
+ // 查找列索引
+ int idCol = -1, sceneCol = -1, lvCol = -1, itemCol = -1, emitCol = -1,
+ rewardCol = -1, bigRewardCol = -1, areaRewardCol = -1, partCol = -1;
+ int columnCount = worksheet.Dimension.Columns;
+
+ for (int col = 1; col <= columnCount; col++)
+ {
+ string header = worksheet.Cells[1, col].Text;
+ if (header == "Id") idCol = col;
+ else if (header == "Scene") sceneCol = col;
+ else if (header == "Lv") lvCol = col;
+ else if (header == "Item") itemCol = col;
+ else if (header == "Emit") emitCol = col;
+ else if (header == "Reward") rewardCol = col;
+ else if (header == "BigReward") bigRewardCol = col;
+ else if (header == "AreaReward") areaRewardCol = col;
+ else if (header == "Part") partCol = col;
+ }
+
+ // 更新和删除数据(从第3行开始)
+ int rowCount = worksheet.Dimension.Rows;
+ var processedIds = new HashSet();
+
+ // 第一遍:更新现有行或标记删除
+ for (int row = rowCount; row >= 3; row--)
+ {
+ string idText = worksheet.Cells[row, idCol].Text;
+ if (string.IsNullOrEmpty(idText)) continue;
+
+ if (!int.TryParse(idText, out int id)) continue;
+
+ // 查找对应的数据
+ var progressData = allProgressDataList.Find(x => x.Id == id);
+ if (progressData != null)
+ {
+ // 更新数据
+ worksheet.Cells[row, sceneCol].Value = progressData.Scene;
+ worksheet.Cells[row, lvCol].Value = progressData.Lv;
+ worksheet.Cells[row, itemCol].Value = progressData.Item;
+ worksheet.Cells[row, emitCol].Value = progressData.Emit;
+ worksheet.Cells[row, rewardCol].Value = progressData.Reward;
+ worksheet.Cells[row, bigRewardCol].Value = progressData.BigReward;
+ worksheet.Cells[row, areaRewardCol].Value = progressData.AreaReward;
+ worksheet.Cells[row, partCol].Value = progressData.Part > 0 ? progressData.Part.ToString() : "";
+
+ processedIds.Add(id);
+ }
+ else
+ {
+ // 删除行
+ worksheet.DeleteRow(row);
+ }
+ }
+
+ // 第二遍:添加新行
+ int currentRow = worksheet.Dimension?.Rows ?? 2;
+ foreach (var progressData in allProgressDataList.OrderBy(x => x.Id))
+ {
+ if (!processedIds.Contains(progressData.Id))
+ {
+ // 这是新增的数据
+ currentRow++;
+ worksheet.Cells[currentRow, idCol].Value = progressData.Id;
+ worksheet.Cells[currentRow, sceneCol].Value = progressData.Scene;
+ worksheet.Cells[currentRow, lvCol].Value = progressData.Lv;
+ worksheet.Cells[currentRow, itemCol].Value = progressData.Item;
+ worksheet.Cells[currentRow, emitCol].Value = progressData.Emit;
+ worksheet.Cells[currentRow, rewardCol].Value = progressData.Reward;
+ worksheet.Cells[currentRow, bigRewardCol].Value = progressData.BigReward;
+ worksheet.Cells[currentRow, areaRewardCol].Value = progressData.AreaReward;
+ worksheet.Cells[currentRow, partCol].Value = progressData.Part > 0 ? progressData.Part.ToString() : "";
+ }
+ }
+
+ // 保存文件
+ package.Save();
+ }
+
+ // 提示成功并提醒推送
+ EditorUtility.DisplayDialog("保存成功",
+ "配置已保存到Excel文件!\n\n" +
+ "⚠️ 重要提醒:\n" +
+ "请及时在SourceTree或GitHubDesktop中:\n" +
+ "1. 提交(Commit)本次修改\n" +
+ "2. 推送(Push)到远程仓库\n\n" +
+ "避免与其他策划产生冲突!",
+ "我知道了");
+ }
+ catch (Exception e)
+ {
+ EditorUtility.DisplayDialog("错误", $"保存数据失败: {e.Message}\n{e.StackTrace}", "确定");
+ }
+ }
+
+ ///
+ /// 场景进度数据类
+ ///
+ private class IndoorProgressData
+ {
+ public int Id;
+ public int Scene;
+ public int Lv;
+ public string Item;
+ public string Emit;
+ public string Reward;
+ public string BigReward;
+ public string AreaReward;
+ public int Part;
+ }
+
+ ///
+ /// Item数据类
+ ///
+ private class ItemData
+ {
+ public int Id;
+ public string Name;
+ public int IType;
+ public string Res;
+ }
+
+ ///
+ /// Item奖励结构
+ ///
+ private struct ItemReward
+ {
+ public int Id;
+ public int Num;
+ }
+ }
+}
diff --git a/Scripts/Editor/Design_Tools/Scene/IndoorProgressEditor.cs.meta b/Scripts/Editor/Design_Tools/Scene/IndoorProgressEditor.cs.meta
new file mode 100644
index 0000000..bb67e48
--- /dev/null
+++ b/Scripts/Editor/Design_Tools/Scene/IndoorProgressEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 15c94f7123a92774084366377ef07164
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: