using UnityEngine; using UnityEditor; using System; using System.Collections.Generic; using System.Linq; using System.IO; using System.IO.Compression; using System.Xml.Linq; public static class ConfigLinkViewerCallbacks { public static Action ExportConfigCallback; public static Func FallbackExcelPathCallback; } [InitializeOnLoad] public static class ConfigLinkViewerAutoSetup { static ConfigLinkViewerAutoSetup() { TryRegisterExportConfig(); TryRegisterFallbackExcelPath(); } private static void TryRegisterExportConfig() { if (ConfigLinkViewerCallbacks.ExportConfigCallback != null) return; try { var type = FindType("MFrame.ConfigDeal"); if (type == null) return; var method = type.GetMethod("ExportConfig", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); if (method == null) return; ConfigLinkViewerCallbacks.ExportConfigCallback = (Action)Delegate.CreateDelegate(typeof(Action), method); } catch { } } private static void TryRegisterFallbackExcelPath() { if (ConfigLinkViewerCallbacks.FallbackExcelPathCallback != null) return; try { var pathConfType = FindType("MFrame.PathConf"); var editorCfType = FindType("MFrame.ConfigEditorCf"); if (pathConfType == null || editorCfType == null) return; var rootField = pathConfType.GetField("project_root", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); if (rootField == null) return; var insProp = editorCfType.GetProperty("ins", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); if (insProp == null) return; var ins = insProp.GetValue(null); if (ins == null) return; var excelField = ins.GetType().GetField("excel_path", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); if (excelField == null) return; ConfigLinkViewerCallbacks.FallbackExcelPathCallback = () => { var root = rootField.GetValue(null) as string; var inst = insProp.GetValue(null); var ep = inst != null ? excelField.GetValue(inst) as string : null; return root + ep; }; } catch { } } private static Type FindType(string fullName) { foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) { var t = asm.GetType(fullName); if (t != null) return t; } return null; } } public class ConfigLinkViewerWindow : EditorWindow { private enum Tab { View, Config } private Tab currentTab = Tab.View; private string selectedTableName; private Dictionary expandedRelations = new Dictionary(); private Vector2 relationScrollPos; private Vector2 detailScrollPos; private Vector2 configScrollPos; 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() { DrawTabBar(); EditorGUILayout.Space(); float windowWidth = position.width; float windowHeight = position.height; if (currentTab == Tab.View) { DrawViewTab(windowWidth, windowHeight); } else { DrawConfigTab(windowWidth, windowHeight); DrawLuoTianyiOverlay(windowWidth, windowHeight); } } private void DrawTabBar() { EditorGUILayout.BeginHorizontal("Toolbar"); GUILayout.FlexibleSpace(); GUIStyle tabLeft, tabRight; if (currentTab == Tab.View) { tabLeft = new GUIStyle("ToolbarButton") { fontStyle = FontStyle.Bold }; tabRight = new GUIStyle("ToolbarButton"); } else { tabLeft = new GUIStyle("ToolbarButton"); tabRight = new GUIStyle("ToolbarButton") { fontStyle = FontStyle.Bold }; } if (GUILayout.Button("查看", tabLeft, GUILayout.Width(60))) { currentTab = Tab.View; } if (GUILayout.Button("配置", tabRight, GUILayout.Width(60))) { currentTab = Tab.Config; } GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); } private void DrawViewTab(float windowWidth, float windowHeight) { DrawViewHeader(); EditorGUILayout.Space(); DrawTableSelector(windowWidth, windowHeight); } private void DrawViewHeader() { 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 += () => { if (ConfigLinkViewerCallbacks.ExportConfigCallback != null) { ConfigLinkViewerCallbacks.ExportConfigCallback(); } else { EditorUtility.DisplayDialog("提示", "未注册导出配置回调。\n请在项目初始化代码中设置 ConfigLinkViewerCallbacks.ExportConfigCallback。", "确定"); } }; } if (GUILayout.Button("刷新", GUILayout.Height(22), GUILayout.Width(60))) { ConfigLinkDatabase.ClearCache(); } EditorGUILayout.EndHorizontal(); string configStatus = ConfigLinkDatabase.HasConfigFile() ? "已加载 (ConfigLinkData.json)" : "未找到配置文件,请切换到[配置]标签页生成"; var statusStyle = new GUIStyle(EditorStyles.miniLabel) { richText = true }; EditorGUILayout.LabelField($"配置状态: {configStatus}", statusStyle); EditorGUILayout.EndVertical(); } private void DrawConfigTab(float windowWidth, float windowHeight) { configScrollPos = EditorGUILayout.BeginScrollView(configScrollPos); EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField("项目配置", new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 }); EditorGUILayout.Space(); DrawProjectSettings(); EditorGUILayout.Space(); DrawExcelPathSettings(windowWidth); EditorGUILayout.Space(); EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField("配置数据生成", EditorStyles.boldLabel); EditorGUILayout.LabelField("首次使用或跨项目时,需要先生成配置数据。", EditorStyles.miniLabel); EditorGUILayout.Space(); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("生成配置", GUILayout.Height(30))) { EditorApplication.delayCall += () => { ConfigLinkDatabase.GenerateConfigFromExcelFolder(); }; } if (GUILayout.Button("补全配置", GUILayout.Height(30))) { EditorApplication.delayCall += RunAIConfigScript; } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); EditorGUILayout.LabelField("生成配置:从 Excel 文件夹扫描生成 ConfigLinkData.json 骨架", EditorStyles.miniLabel); EditorGUILayout.LabelField("补全配置:运行 Python 脚本自动分析引用关系,补全字段关联", EditorStyles.miniLabel); EditorGUILayout.EndVertical(); EditorGUILayout.EndVertical(); EditorGUILayout.EndScrollView(); } private GUIStyle _artStyle; private GUIStyle GetArtStyle(int fontSize) { if (_artStyle == null || _artStyle.fontSize != fontSize) { _artStyle = new GUIStyle(EditorStyles.label) { alignment = TextAnchor.MiddleCenter, fontSize = fontSize, richText = true, wordWrap = false, clipping = TextClipping.Overflow, normal = { textColor = Color.white }, }; var font = Font.CreateDynamicFontFromOSFont("Consolas", fontSize); if (font != null) _artStyle.font = font; } return _artStyle; } private void DrawLuoTianyiOverlay(float windowWidth, float windowHeight) { float scale = Mathf.Clamp(Mathf.Min(windowWidth, windowHeight) / 600f, 0.35f, 0.7f); int fontSize = Mathf.RoundToInt(9 * scale); float lineHeight = fontSize * 1.1f; string[] lines = { " _________________", " ____/:::::::::::::::::\\_____", " __/::::::::::::::::::::::::::::\\___", " _/:::::::::::::::::::::::::::::::::::\\__", " _/::::::::::::::::::::::::::::::::::::::::\\_", " /::::::::::::::::::::::::::::::::::::::::::::\\", " |::::::::::::::::::::::::::::::::::::::::::::::\\", " /::::::::::::::::::::::::::::::::::::::::::::::::\\", " |:::/.:::::::;:::::::::::::::::::::::::::::::::::::|", " /:::/.:::::::/..:::::::::::::::::::::::::::::::::::::\\", " |:::|.::::::;/.::::::::::::::::::::::::::::::::::::::::|", " |::/.::::::/..:::::::;;'.::::::::::::::::::::::::::::::|", " |:|.::::/./.::::::;;/..:::::::::::::::::::::::::::::::::|", " `:|.:::|.|.:::::;/..;;;;;;-'.:;;;-':::::::::::::::::::::|", " \\|.:::|.|.:::;/.;;/ -..::'''...:::::::::::::::::::::::|", " \\;;::|.|.::/.;/--__ |::::::::::::::::::::::::::::|", " \\;;\\\\::/|/ =-__ --_ /::::::::::::::::::::::::::::::", " \\/ /| -._ |.::::::::::::::::::::::::::::::", " _.' /// /- ||::::::::::::::::::::::::::::::", " _.-' //' ||::::::::::::::::::::::::::::::", " | - `|::::::::::::::::::::::::::::::", " \\ \\:::::::::::::::::::::::::::::", " | \\:::::::::::::::::::::", " / __/:::::::::::::::::::::::::::", " \\ __/::;::;;:::::: ::::::::::", " |` /;;;;/::| \\:::: :___: :::::::::", " \\ |'_,::::/ \\ |:::: .| |`. :::::::::", " / _/::::::/ / /:::: | \\.' | ::. .::::", " | /.::;;:-'_)/_/::::: `._| .' ::..::::", " ----.__ | |.::| \\___/::::::: ::..::::", " :::::::`----\\_____ \\:::\\.-'::::::::::: ___:___ ::..::::", " ;;;;;:::::::::::::`------ \\:::::::::::::::: ___|___ ::..::::", " `-------:::::::\\ /:::::::::::::::: __| ::..::::", " ___.--------'::::::::\\ |::::::::::::::::: | |-. ::..::::", " :::::;;;:--:::::::::::| /::::::::::::::::: --' ' ::..::::", " ----' _,-:.:::::::::::\\ |.::::::::::::::::: ::..::::", " __/.::::::::::::::::| |.::::::::::::::::: : : ::..::::", " __/.:::;;::::::::;/.:::| |.::::::::::::::::: | : ::. .::::", " /.::::;/ /.:::::;/ |.::::| \\_.::::::::::::::: | . | :::::::::", " :::::/ /.:::::/ /.:::::| \\__.:::::::::::: `.' ' :::::::::", " ::::| |.:::::/ /.:::::.| \\,:::::::::::: ::::::::::", " ::::| |.::::| |.:::::/| __/::::::::::::::::::::::::::::", " \\.:::\\ \\.:::| |.::::||| __.--:::::::::::::::::::::::::::::", " \\.:::\\_ \\.:::\\ \\.:::'/.:::::::::::::::::::::::::::::::::", " \\.::::\\ \\.:::\\ \\.::::::::::::::::::::::::::::::::::::::::::" }; float boxW = Mathf.Clamp(windowWidth * 0.45f, 300f, 500f); float boxH = lines.Length * lineHeight + 16; float boxX = windowWidth - boxW - 10; float boxY = windowHeight - boxH - 10; var bgColor = new Color(0.12f, 0.12f, 0.16f, 0.9f); EditorGUI.DrawRect(new Rect(boxX, boxY, boxW, boxH), bgColor); var borderColor = new Color(0f, 0.75f, 1f, 0.4f); EditorGUI.DrawRect(new Rect(boxX, boxY, boxW, 1), borderColor); EditorGUI.DrawRect(new Rect(boxX, boxY + boxH - 1, boxW, 1), borderColor); EditorGUI.DrawRect(new Rect(boxX, boxY, 1, boxH), borderColor); EditorGUI.DrawRect(new Rect(boxX + boxW - 1, boxY, 1, boxH), borderColor); GUIStyle artStyle = new GUIStyle(EditorStyles.label) { alignment = TextAnchor.MiddleCenter, fontSize = fontSize, richText = false, wordWrap = false, clipping = TextClipping.Overflow, }; artStyle.normal.textColor = new Color(0f, 1f, 1f); float y = boxY + 8; for (int i = 0; i < lines.Length; i++) { var lineRect = new Rect(boxX + 4, y, boxW - 8, lineHeight); GUI.Label(lineRect, lines[i], artStyle); y += lineHeight; } } private void DrawProjectSettings() { EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField("路径设置", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(); string configPath = ConfigLinkDatabase.GetConfigFolderPath(); EditorGUILayout.LabelField("Config目录:", GUILayout.Width(80)); EditorGUILayout.LabelField(configPath, EditorStyles.textField, GUILayout.ExpandWidth(true)); if (GUILayout.Button("选择", GUILayout.Width(60))) { string selected = EditorUtility.OpenFolderPanel("选择配置导出目录", Application.dataPath, ""); if (!string.IsNullOrEmpty(selected)) { ConfigLinkDatabase.SetConfigFolderPath(selected); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); } private void DrawTableSelector(float windowWidth, float windowHeight) { EditorGUILayout.BeginVertical("box", GUILayout.Height(windowHeight - 60)); EditorGUILayout.BeginHorizontal(); searchFilter = EditorGUILayout.TextField("搜索:", searchFilter, EditorStyles.toolbarSearchField); showOnlyExisting = EditorGUILayout.ToggleLeft("只显示当前项目存在的表", showOnlyExisting); EditorGUILayout.EndHorizontal(); 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(); float innerHeight = windowHeight - 80; // 减去头部和边距 EditorGUILayout.BeginHorizontal(GUILayout.Height(innerHeight)); float leftPanelWidth = Mathf.Clamp(windowWidth * 0.3f, 180f, 300f); float rightPanelWidth = windowWidth - leftPanelWidth - 20f; EditorGUILayout.BeginVertical(GUILayout.Width(leftPanelWidth)); EditorGUILayout.LabelField($"配置表列表 ({filteredTables.Count}):", EditorStyles.miniBoldLabel); float scrollHeight = innerHeight - 40; // 减去标签高度和边距 relationScrollPos = EditorGUILayout.BeginScrollView(relationScrollPos, false, true, GUILayout.Height(scrollHeight)); 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)) { if (selectedTableName != tableKeys[i]) { selectedTableName = tableKeys[i]; expandedRelations.Clear(); } } } EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); EditorGUILayout.BeginVertical(GUILayout.Width(rightPanelWidth)); EditorGUILayout.LabelField("当前表详情:", EditorStyles.miniBoldLabel); detailScrollPos = EditorGUILayout.BeginScrollView(detailScrollPos, false, true, GUILayout.Height(scrollHeight)); DrawCurrentTableInfo(rightPanelWidth); EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); } private void DrawCurrentTableInfo(float panelWidth) { if (string.IsNullOrEmpty(selectedTableName)) { EditorGUILayout.LabelField("请从左侧选择一个配置表", EditorStyles.centeredGreyMiniLabel); return; } var tableInfo = ConfigLinkDatabase.GetTableInfo(selectedTableName); if (tableInfo == null) { EditorGUILayout.LabelField("未找到该表的配置信息", EditorStyles.helpBox); 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); } } } } 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) { EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField("Excel 文件夹", EditorStyles.boldLabel); string currentPath = ConfigLinkDatabase.GetExcelFolderPath(); string displayPath = string.IsNullOrEmpty(currentPath) ? "未设置" : currentPath; EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("路径:", GUILayout.Width(36)); EditorGUILayout.LabelField(displayPath, EditorStyles.textField, GUILayout.ExpandWidth(true)); if (GUILayout.Button("选择", GUILayout.Width(60))) { string selectedPath = EditorUtility.OpenFolderPanel("选择Excel文件夹", "", ""); if (!string.IsNullOrEmpty(selectedPath)) { ConfigLinkDatabase.SetExcelFolderPath(selectedPath); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); } 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 void DrawFormatExample(string format) { string example = ConfigLinkDatabase.FormatRelationValue(format, "1_2_3_4"); if (example != "1_2_3_4") { GUIStyle exampleStyle = new GUIStyle(EditorStyles.miniLabel) { fontStyle = FontStyle.Italic }; EditorGUILayout.LabelField($"示例: {example}", exampleStyle); } } private static void RunAIConfigScript() { string scriptPath = Path.Combine(Application.dataPath, "Editor/ConfigLinkViewer", "generate_config_link_data.py"); if (!File.Exists(scriptPath)) { EditorUtility.DisplayDialog("提示", "未找到脚本文件:\n" + scriptPath, "确定"); return; } string pythonExe = FindPythonExecutable(); if (string.IsNullOrEmpty(pythonExe)) { EditorUtility.DisplayDialog("提示", "未找到 Python 环境,请确保已安装 Python 并添加到 PATH", "确定"); return; } try { string configDir = ConfigLinkDatabase.GetConfigFolderPath(); string tableInfoPath = Path.Combine(Application.dataPath, "Editor/ConfigLinkViewer", "table_info.json"); string outputPath = Path.Combine(Application.dataPath, "Editor/ConfigLinkViewer", "ConfigLinkData.json"); string arguments = $"\"{scriptPath}\" --config-dir \"{Path.Combine(Application.dataPath, configDir)}\" --output \"{outputPath}\""; if (File.Exists(tableInfoPath)) { arguments += $" --table-info \"{tableInfoPath}\""; } var startInfo = new System.Diagnostics.ProcessStartInfo { FileName = pythonExe, Arguments = arguments, WorkingDirectory = Path.GetDirectoryName(scriptPath), UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, StandardOutputEncoding = System.Text.Encoding.UTF8, StandardErrorEncoding = System.Text.Encoding.UTF8, }; using (var process = System.Diagnostics.Process.Start(startInfo)) { string stdout = process.StandardOutput.ReadToEnd(); string stderr = process.StandardError.ReadToEnd(); process.WaitForExit(); if (process.ExitCode == 0) { Debug.Log("[ConfigLinkViewer] 补全配置完成:\n" + stdout); ConfigLinkDatabase.ClearCache(); AssetDatabase.Refresh(); EditorUtility.DisplayDialog("完成", "补全配置已完成\n\n" + stdout, "确定"); } else { Debug.LogError("[ConfigLinkViewer] 补全配置失败:\n" + stderr); EditorUtility.DisplayDialog("失败", "脚本执行失败:\n" + stderr, "确定"); } } } catch (Exception ex) { Debug.LogError("[ConfigLinkViewer] 运行脚本异常: " + ex.Message); EditorUtility.DisplayDialog("异常", "运行脚本时发生异常:\n" + ex.Message, "确定"); } } private static string FindPythonExecutable() { string[] candidates = { "python", "python3", "py" }; foreach (var candidate in candidates) { try { var startInfo = new System.Diagnostics.ProcessStartInfo { FileName = candidate, Arguments = "--version", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, }; using (var process = System.Diagnostics.Process.Start(startInfo)) { process.WaitForExit(); if (process.ExitCode == 0) return candidate; } } catch { } } return null; } private static void CleanExcelEmptyRows() { string excelFolder = ConfigLinkDatabase.GetExcelFolderPath(); if (string.IsNullOrEmpty(excelFolder)) { if (ConfigLinkViewerCallbacks.FallbackExcelPathCallback != null) { excelFolder = ConfigLinkViewerCallbacks.FallbackExcelPathCallback(); } } if (string.IsNullOrEmpty(excelFolder) || !Directory.Exists(excelFolder)) { EditorUtility.DisplayDialog("提示", "Excel文件夹路径未设置或不存在。\n请切换到[配置]标签页设置,或在 ConfigLinkViewerCallbacks.FallbackExcelPathCallback 中注册回退逻辑。", "确定"); 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); } } List sharedStrings = null; using (var archive = ZipFile.Open(filePath, ZipArchiveMode.Read)) { var ssEntry = archive.GetEntry("xl/sharedStrings.xml"); if (ssEntry != null) { using (var stream = ssEntry.Open()) { var ssDoc = XDocument.Load(stream); sharedStrings = ssDoc.Root.Elements(ns + "si") .Select(si => si.Value) .ToList(); } } } var sheetData = doc.Root.Element(ns + "sheetData"); if (sheetData == null) return 0; var rows = sheetData.Elements(ns + "row").ToList(); if (rows.Count <= headerRowCount) return 0; var emptyRows = new List(); for (int i = headerRowCount; i < rows.Count; i++) { var row = rows[i]; var cells = row.Elements(ns + "c").ToList(); if (cells.Count == 0) { emptyRows.Add(row); continue; } bool allEmpty = true; foreach (var cell in cells) { var value = cell.Element(ns + "v"); if (value != null && !string.IsNullOrWhiteSpace(value.Value)) { allEmpty = false; break; } var cellType = cell.Attribute("t")?.Value; if (cellType == "s" && value != null) { if (int.TryParse(value.Value, out int ssIdx) && sharedStrings != null && ssIdx < sharedStrings.Count) { if (!string.IsNullOrWhiteSpace(sharedStrings[ssIdx])) { allEmpty = false; break; } } } } if (allEmpty) { emptyRows.Add(row); } } if (emptyRows.Count == 0) return 0; foreach (var row in emptyRows) { row.Remove(); } using (var archive = ZipFile.Open(filePath, ZipArchiveMode.Update)) { var sheetEntry = archive.GetEntry("xl/worksheets/sheet1.xml"); if (sheetEntry != null) { sheetEntry.Delete(); var newEntry = archive.CreateEntry("xl/worksheets/sheet1.xml"); using (var stream = newEntry.Open()) { doc.Save(stream); } } } return emptyRows.Count; } }