using UnityEngine; using UnityEditor; using System; using System.Collections.Generic; using System.Linq; using System.IO; using System.IO.Compression; using System.Text.RegularExpressions; using System.Xml.Linq; public class ConfigLinkViewerWindow : EditorWindow { private string selectedTableName; private Dictionary expandedRelations = new Dictionary(); private Vector2 relationScrollPos; private string searchFilter = ""; private bool showOnlyExisting = true; private bool showReverseRelations = false; [MenuItem("Tools/配置表联动查看器")] public static void ShowWindow() { var window = GetWindow("配置表联动"); window.minSize = new Vector2(900, 600); window.position = new Rect(100, 100, 900, 600); } private void OnGUI() { float windowWidth = position.width; float windowHeight = position.height; DrawHeader(); EditorGUILayout.Space(); DrawTableSelector(windowWidth, windowHeight); } private void DrawHeader() { EditorGUILayout.BeginVertical("box"); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("配置表联动关系查看器", new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 }, GUILayout.ExpandWidth(true)); if (GUILayout.Button("清理Excel空行", GUILayout.Height(22), GUILayout.Width(100))) { EditorApplication.delayCall += CleanExcelEmptyRows; } if (GUILayout.Button("导出配置", GUILayout.Height(22), GUILayout.Width(80))) { EditorApplication.delayCall += MFrame.ConfigDeal.ExportConfig; } EditorGUILayout.EndHorizontal(); EditorGUILayout.LabelField("选择配置表查看其与其他表的关联关系", EditorStyles.miniLabel); EditorGUILayout.EndVertical(); } private void DrawTableSelector(float windowWidth, float windowHeight) { EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField("选择配置表:", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(); searchFilter = EditorGUILayout.TextField("搜索:", searchFilter, EditorStyles.toolbarSearchField); showOnlyExisting = EditorGUILayout.ToggleLeft("只显示当前项目存在的表", showOnlyExisting); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); DrawExcelPathSettings(windowWidth); var allTables = showOnlyExisting ? ConfigLinkDatabase.GetExistingTables() : ConfigLinkDatabase.GetAllTableInfo(); var filteredTables = string.IsNullOrEmpty(searchFilter) ? allTables : allTables.Where(t => t.displayName.Contains(searchFilter) || t.tableName.Contains(searchFilter) || t.description.Contains(searchFilter)).ToList(); var displayNames = filteredTables.Select(x => { string existMark = showOnlyExisting ? "" : (x.isExistInProject ? "[存在]" : "[缺失]"); return $"{x.displayName} ({x.tableName}) {existMark}"; }).ToArray(); var tableKeys = filteredTables.Select(x => x.tableName).ToArray(); EditorGUILayout.Space(); EditorGUILayout.BeginHorizontal(); float leftPanelWidth = Mathf.Clamp(windowWidth * 0.3f, 180f, 300f); float rightPanelWidth = windowWidth - leftPanelWidth - 20f; float panelHeight = windowHeight - 150f; EditorGUILayout.BeginVertical(GUILayout.Width(leftPanelWidth), GUILayout.Height(panelHeight)); EditorGUILayout.LabelField($"配置表列表 ({filteredTables.Count}):", EditorStyles.miniBoldLabel); relationScrollPos = EditorGUILayout.BeginScrollView(relationScrollPos, false, true, GUILayout.ExpandHeight(true)); GUIStyle listButtonStyle = new GUIStyle(EditorStyles.label) { wordWrap = false, stretchWidth = true }; for (int i = 0; i < displayNames.Length; i++) { bool isSelected = tableKeys[i] == selectedTableName; GUIStyle buttonStyle = isSelected ? "Button" : listButtonStyle; if (GUILayout.Button(displayNames[i], buttonStyle)) { selectedTableName = tableKeys[i]; expandedRelations.Clear(); } } EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); EditorGUILayout.BeginVertical("box", GUILayout.Width(rightPanelWidth), GUILayout.Height(panelHeight)); DrawCurrentTableInfo(rightPanelWidth); EditorGUILayout.EndVertical(); EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); } private void DrawCurrentTableInfo(float panelWidth) { EditorGUILayout.BeginScrollView(Vector2.zero, false, true, GUILayout.ExpandHeight(true)); if (string.IsNullOrEmpty(selectedTableName)) { EditorGUILayout.LabelField("请从左侧选择一个配置表", EditorStyles.centeredGreyMiniLabel); EditorGUILayout.EndScrollView(); return; } var tableInfo = ConfigLinkDatabase.GetTableInfo(selectedTableName); if (tableInfo == null) { EditorGUILayout.LabelField("未找到该表的配置信息", EditorStyles.helpBox); EditorGUILayout.EndScrollView(); return; } EditorGUILayout.LabelField($"当前表: {tableInfo.displayName}", EditorStyles.boldLabel); EditorGUILayout.LabelField($"表名: {tableInfo.tableName}", EditorStyles.miniLabel); EditorGUILayout.LabelField($"描述: {tableInfo.description}", EditorStyles.miniLabel); EditorGUILayout.LabelField($"状态: {(tableInfo.isExistInProject ? "存在于当前项目" : "当前项目不存在")}", tableInfo.isExistInProject ? EditorStyles.miniLabel : EditorStyles.helpBox); EditorGUILayout.Space(); DrawOpenExcelButtons(panelWidth); EditorGUILayout.Space(); showReverseRelations = EditorGUILayout.ToggleLeft("显示反向引用", showReverseRelations); EditorGUILayout.Space(); if (showReverseRelations) { DrawReverseRelations(panelWidth); } else { if (tableInfo.relations.Count == 0) { EditorGUILayout.LabelField("该表暂无联动关系", EditorStyles.miniLabel); } else { EditorGUILayout.LabelField($"联动关系 ({tableInfo.relations.Count}):", EditorStyles.boldLabel); foreach (var relation in tableInfo.relations) { DrawRelation(relation, panelWidth); } } } EditorGUILayout.EndScrollView(); } private void DrawOpenExcelButtons(float panelWidth) { EditorGUILayout.BeginHorizontal(); string excelPath = ConfigLinkDatabase.GetExcelFilePath(selectedTableName); bool canOpen = !string.IsNullOrEmpty(excelPath); if (canOpen) { if (GUILayout.Button("打开表格", GUILayout.Height(30), GUILayout.Width(panelWidth / 2 - 5))) { OpenExcelFile(selectedTableName); } } else { EditorGUILayout.LabelField("未找到Excel文件", EditorStyles.helpBox, GUILayout.Height(30), GUILayout.Width(panelWidth / 2 - 5)); } if (GUILayout.Button("批量打开关联表", GUILayout.Height(30), GUILayout.Width(panelWidth / 2 - 5))) { OpenRelatedTables(selectedTableName); } EditorGUILayout.EndHorizontal(); } private void OpenExcelFile(string tableName) { string excelPath = ConfigLinkDatabase.GetExcelFilePath(tableName); if (!string.IsNullOrEmpty(excelPath) && File.Exists(excelPath)) { System.Diagnostics.Process.Start(excelPath); } } private void OpenRelatedTables(string tableName) { var relatedTables = ConfigLinkDatabase.GetRelatedTableNames(tableName); foreach (var relatedTable in relatedTables) { OpenExcelFile(relatedTable); } } private void DrawReverseRelations(float panelWidth) { var reverseRels = ConfigLinkDatabase.GetReverseRelations(selectedTableName); if (reverseRels.Count == 0) { EditorGUILayout.LabelField("暂无其他表引用该表", EditorStyles.miniLabel); } else { EditorGUILayout.LabelField($"反向引用 ({reverseRels.Count}):", EditorStyles.boldLabel); foreach (var rel in reverseRels) { bool sourceExist = ConfigLinkDatabase.IsTableExist(rel.sourceTable); EditorGUILayout.BeginVertical("frameBox", GUILayout.Width(panelWidth - 10)); GUIStyle headerStyle = new GUIStyle(sourceExist ? EditorStyles.boldLabel : EditorStyles.helpBox) { wordWrap = false }; EditorGUILayout.LabelField($"{rel.sourceDisplayName} ({rel.sourceTable}) → {rel.fieldName}", headerStyle); GUIStyle labelStyle = new GUIStyle(EditorStyles.miniLabel) { wordWrap = false }; EditorGUILayout.LabelField($"说明: {rel.description}", labelStyle); if (!string.IsNullOrEmpty(rel.relationFormat)) { EditorGUILayout.LabelField($"格式: {rel.relationFormat}", labelStyle); } if (sourceExist && GUILayout.Button($"查看 {rel.sourceTable} 表", EditorStyles.miniButton)) { selectedTableName = rel.sourceTable; expandedRelations.Clear(); } EditorGUILayout.EndVertical(); EditorGUILayout.Space(2); } } } private void DrawExcelPathSettings(float windowWidth) { string currentPath = ConfigLinkDatabase.GetExcelFolderPath(); string displayPath = string.IsNullOrEmpty(currentPath) ? "未设置" : currentPath; EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Excel文件夹:", GUILayout.Width(70)); EditorGUILayout.LabelField(displayPath, EditorStyles.textField, GUILayout.ExpandWidth(true)); if (GUILayout.Button("选择文件夹", GUILayout.Width(80))) { string selectedPath = EditorUtility.OpenFolderPanel("选择Excel文件夹", "", ""); if (!string.IsNullOrEmpty(selectedPath)) { ConfigLinkDatabase.SetExcelFolderPath(selectedPath); } } EditorGUILayout.EndHorizontal(); } private void DrawRelation(FieldRelation relation, float panelWidth) { string key = $"{selectedTableName}_{relation.fieldName}_{relation.targetTable}"; if (!expandedRelations.ContainsKey(key)) { expandedRelations[key] = false; } bool targetExist = ConfigLinkDatabase.IsTableExist(relation.targetTable); string existMark = targetExist ? "" : "[目标表缺失]"; EditorGUILayout.BeginVertical("frameBox", GUILayout.Width(panelWidth - 10)); GUIStyle foldoutStyle = new GUIStyle(EditorStyles.foldout) { wordWrap = false, richText = true }; expandedRelations[key] = EditorGUILayout.Foldout(expandedRelations[key], $"{relation.fieldName} → {relation.targetTable}.{relation.targetField} {existMark}", true, foldoutStyle); if (expandedRelations[key]) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.Space(); EditorGUILayout.BeginVertical("helpBox", GUILayout.Width(panelWidth - 30)); GUIStyle labelStyle = new GUIStyle(EditorStyles.miniLabel) { wordWrap = false, richText = true }; EditorGUILayout.LabelField($"目标表: {relation.targetTable} {(targetExist ? "" : "(当前项目不存在)")}", labelStyle); EditorGUILayout.LabelField($"目标字段: {relation.targetField}", labelStyle); if (!string.IsNullOrEmpty(relation.relationFormat)) { EditorGUILayout.LabelField($"格式: {relation.relationFormat}", labelStyle); DrawFormatExample(relation.relationFormat); } EditorGUILayout.LabelField($"说明: {relation.description}", labelStyle); if (targetExist && GUILayout.Button($"查看 {relation.targetTable} 表", EditorStyles.miniButton)) { selectedTableName = relation.targetTable; expandedRelations.Clear(); } EditorGUILayout.EndVertical(); EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndVertical(); EditorGUILayout.Space(2); } private static void CleanExcelEmptyRows() { string excelFolder = ConfigLinkDatabase.GetExcelFolderPath(); if (string.IsNullOrEmpty(excelFolder)) { var acf = MFrame.ConfigEditorCf.ins; if (acf != null) { excelFolder = MFrame.PathConf.project_root + acf.excel_path; } } if (string.IsNullOrEmpty(excelFolder) || !Directory.Exists(excelFolder)) { EditorUtility.DisplayDialog("提示", "Excel文件夹路径未设置或不存在,请先设置Excel文件夹", "确定"); return; } var files = Directory.GetFiles(excelFolder, "*.xlsx", SearchOption.AllDirectories) .Where(f => !Path.GetFileName(f).StartsWith("~$")) .ToArray(); if (files.Length == 0) { EditorUtility.DisplayDialog("提示", "未找到Excel文件", "确定"); return; } int totalCleaned = 0; int cleanedFileCount = 0; try { foreach (var file in files) { try { int removedCount = CleanSingleExcelFile(file); if (removedCount > 0) { totalCleaned += removedCount; cleanedFileCount++; } } catch (Exception ex) { Debug.LogError($"[清理空行] 处理文件失败: {Path.GetFileName(file)}, 错误: {ex.Message}"); } } } finally { AssetDatabase.Refresh(); } if (cleanedFileCount > 0) { EditorUtility.DisplayDialog("清理完成", $"扫描 {files.Length} 个文件\n清理了 {cleanedFileCount} 个文件中共 {totalCleaned} 行空数据", "确定"); } else { EditorUtility.DisplayDialog("清理完成", $"扫描 {files.Length} 个文件,未发现需要清理的空行", "确定"); } } private static int CleanSingleExcelFile(string filePath) { const int headerRowCount = 3; XNamespace ns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"; XDocument doc; using (var archive = ZipFile.Open(filePath, ZipArchiveMode.Read)) { var sheetEntry = archive.GetEntry("xl/worksheets/sheet1.xml"); if (sheetEntry == null) return 0; using (var stream = sheetEntry.Open()) { doc = XDocument.Load(stream); } } var sheetData = doc.Root?.Element(ns + "sheetData"); if (sheetData == null) return 0; var rows = sheetData.Elements(ns + "row").ToList(); var rowsToRemove = new List(); foreach (var row in rows) { if (!int.TryParse(row.Attribute("r")?.Value, out int rowNum)) continue; if (rowNum <= headerRowCount) continue; var cells = row.Elements(ns + "c").ToList(); if (cells.Count == 0) { rowsToRemove.Add(row); continue; } var firstCell = cells[0]; var valueElement = firstCell.Element(ns + "v"); string val = valueElement?.Value?.Trim(); if (string.IsNullOrEmpty(val) || !int.TryParse(val, out _)) { rowsToRemove.Add(row); } } if (rowsToRemove.Count == 0) return 0; foreach (var row in rowsToRemove) { row.Remove(); } var remainingRows = sheetData.Elements(ns + "row").ToList(); int newRowIndex = 1; foreach (var row in remainingRows) { row.SetAttributeValue("r", newRowIndex); foreach (var cell in row.Elements(ns + "c")) { string cellRef = cell.Attribute("r")?.Value; if (!string.IsNullOrEmpty(cellRef)) { string colPart = Regex.Replace(cellRef, @"\d", ""); cell.SetAttributeValue("r", colPart + newRowIndex); } } newRowIndex++; } using (var archive = ZipFile.Open(filePath, ZipArchiveMode.Update)) { var entry = archive.GetEntry("xl/worksheets/sheet1.xml"); if (entry != null) entry.Delete(); var newEntry = archive.CreateEntry("xl/worksheets/sheet1.xml"); using (var stream = newEntry.Open()) { doc.Save(stream); } } Debug.Log($"[清理空行] {Path.GetFileName(filePath)}: 删除了 {rowsToRemove.Count} 行空数据"); return rowsToRemove.Count; } private static readonly Dictionary FormatExamples = new Dictionary { { "item_id_num", ("1001_10_5", "类型:1001, ID:10, 数量:5") }, { "type_id_num", ("1001_10_5", "类型:1001, ID:10, 数量:5") }, { "id_pos_lv", ("100_1_30", "ID:100, 位置:1, 等级:30") }, { "id_lv_num", ("100_30_5", "ID:100, 等级:30, 数量:5") }, { "id_lv_num_time",("100_30_5_120","ID:100, 等级:30, 数量:5, 时间:120秒") }, { "buffid_lv", ("5_3", "BuffID:5, 等级:3") }, { "rune_id_num", ("10_2", "符文ID:10, 数量:2") }, { "equip_id_num", ("20_1", "装备ID:20, 数量:1") }, }; private void DrawFormatExample(string format) { if (FormatExamples.TryGetValue(format.ToLower(), out var info)) { var style = new GUIStyle(EditorStyles.miniLabel) { wordWrap = false, richText = true }; EditorGUILayout.LabelField($"示例: {info.example} → {info.desc}", style); } } }