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); } string iconName = iconText ?? ""; var faceData = new FaceData { Id = id, NameKey = nameKey, Init = init, IconName = iconName }; 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; // 默认为"未选择" if (!string.IsNullOrEmpty(data.IconName)) { var currentItem = iconItems.Find(x => x.Name == data.IconName); 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.IconName = ""; } else if (newIndex > 0 && newIndex <= iconItems.Count) { // 选择了具体的Icon(索引需要-1) data.IconName = iconItems[newIndex - 1].Name; } } // 预览和Desc提示 if (!string.IsNullOrEmpty(data.IconName)) { var item = iconItems.Find(x => x.Name == data.IconName); 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 { // IconName为空,未选择状态 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, IconName = "" }; 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; worksheet.Cells[row, iconCol].Value = faceData.IconName ?? ""; 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; worksheet.Cells[currentRow, iconCol].Value = faceData.IconName ?? ""; } } // 保存文件 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 string IconName = ""; } } }