Files
Planner_Tools/Unity/配置表联动查看器/ConfigLinkViewer/ConfigLinkViewerWindow.cs
2026-05-29 19:44:56 +08:00

507 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, bool> expandedRelations = new Dictionary<string, bool>();
private Vector2 relationScrollPos;
private string searchFilter = "";
private bool showOnlyExisting = true;
private bool showReverseRelations = false;
[MenuItem("Tools/配置表联动查看器")]
public static void ShowWindow()
{
var window = GetWindow<ConfigLinkViewerWindow>("配置表联动");
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<XElement>();
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<string, (string example, string desc)> FormatExamples = new Dictionary<string, (string, string)>
{
{ "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);
}
}
}