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; avatarDict[id] = new AvatarData { Id = id, NameKey = nameKey, IconName = iconText ?? "" }; } } } } /// /// 加载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; faceDict[id] = new FaceData { Id = id, NameKey = nameKey, IconName = iconText ?? "" }; } } } } /// /// 加载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; emojiDict[id] = new EmojiData { Id = id, NameKey = nameKey, IconName = iconText ?? "" }; } } } } /// /// 加载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格式: "TableName,ItemName" string[] parts = string.IsNullOrEmpty(data.Res) ? new string[0] : data.Res.Split(','); string tableName = parts.Length > 0 ? parts[0] : ""; string itemName = parts.Length > 1 ? parts[1] : ""; // 查找当前表格和所属文件夹 ArtTableSO currentTable = null; string currentFolder = ""; // 优先使用缓存的组选择 if (resSelectedFolderCache.ContainsKey(data.Id)) { currentFolder = resSelectedFolderCache[data.Id]; } // 如果有Res数据,从Res推导组和表 if (!string.IsNullOrEmpty(tableName)) { currentTable = allArtTables.FirstOrDefault(x => x.TableName == tableName); 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; tableName = ""; itemName = ""; 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.TableName == currentTable.TableName); 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.TableName},"; itemName = ""; } 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 (!string.IsNullOrEmpty(itemName) && !tableSelectionChanged) { int foundIndex = currentTable.Items.FindIndex(item => item.Name == itemName); 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.TableName},{currentTable.Items[newItemIndex - 1].Name}"; } else if (newItemIndex == 0) { // 选择"未选择" data.Res = $"{currentTable.TableName},"; } } } 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; string iconName = avatarDict[avatarId].IconName; if (string.IsNullOrEmpty(iconName)) return null; var item = headFrameResourceSO.Items.FirstOrDefault(x => x.Name == iconName); 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; string iconName = faceDict[faceId].IconName; if (string.IsNullOrEmpty(iconName)) return null; var item = headResourceSO.Items.FirstOrDefault(x => x.Name == iconName); 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; string iconName = emojiDict[emojiId].IconName; if (string.IsNullOrEmpty(iconName)) return null; var item = emojiResourceSO.Items.FirstOrDefault(x => x.Name == iconName); 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; string tableName = parts[0]; string artItemName = parts[1]; if (string.IsNullOrEmpty(tableName) || string.IsNullOrEmpty(artItemName)) return null; var table = allArtTables.FirstOrDefault(x => x.TableName == tableName); if (table == null) return null; var item = table.Items.FirstOrDefault(x => x.Name == artItemName); 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; } /// /// 根据Res字段获取完整资源路径 /// private string GetFullResourcePath(string res) { if (string.IsNullOrEmpty(res)) return ""; string[] parts = res.Split(','); if (parts.Length < 2) return ""; string tableName = parts[0]; string artItemName = parts[1]; if (string.IsNullOrEmpty(tableName) || string.IsNullOrEmpty(artItemName)) return ""; var table = allArtTables.FirstOrDefault(x => x.TableName == tableName); if (table == null) return ""; var item = table.Items.FirstOrDefault(x => x.Name == artItemName); if (item == null) return ""; if (item.Sprite == null) return ""; return AssetDatabase.GetAssetPath(item.Sprite); } /// /// 保存数据到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, fullResourcePathCol = -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; else if (header == "FullResourcePath") fullResourcePathCol = 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; } // 写入完整资源路径 if (fullResourcePathCol > 0) { string fullPath = GetFullResourcePath(item.Res); worksheet.Cells[targetRow, fullResourcePathCol].Value = fullPath; } } // 删除已不存在的数据行(只清空我们管理的列) 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; if (fullResourcePathCol > 0) worksheet.Cells[row, fullResourcePathCol].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 string IconName = ""; } /// /// Face数据类 /// private class FaceData { public int Id; public string NameKey; public string IconName = ""; } /// /// Emoji数据类 /// private class EmojiData { public int Id; public string NameKey; public string IconName = ""; } } }