工具移动位置

This commit is contained in:
zhang hongbo 2026-02-10 17:57:55 +08:00
parent 6bfb4a06e8
commit e8797132b4
28 changed files with 2 additions and 10703 deletions

@ -1 +1 @@
Subproject commit ea8f052587557afd8afd3760e73a6d33d2818f03
Subproject commit 103b593420199e5e73302d3eae2d932c54a56e8b

@ -1 +1 @@
Subproject commit 59298e4d8f9f439e9caa9ea1f3711b7547e2fb87
Subproject commit dc5e4584757379eaabdd31a3aedade4498d7b4dd

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 06b84cf237692794f9d12d97e15f4e8e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 6e1a602a557cd7a48ab28f8852a81a48
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 372054b25043e444a98bee8c1257039b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 36e4dd8ddc0b69341aa541d3caa4105d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,421 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using OfficeOpenXml;
namespace ThriftPipelineTools
{
/// <summary>
/// Excel 查看器 - 用于学习和测试 EPPlus 的 Excel 读取功能
///
/// 功能:
/// 1. 选择 Excel 文件 (.xlsx)
/// 2. 加载并显示所有工作表 (Sheet)
/// 3. 点击工作表查看数据内容
/// 4. 实时预览 Excel 数据结构
///
/// 学习目的:
/// - 理解 EPPlus 的基本用法
/// - 了解 Excel 文件的结构Workbook、Worksheet、Cells
/// - 掌握单元格数据的读取方法
/// </summary>
public class EPPlusTestEditor : EditorWindow
{
// ========== 文件路径 ==========
private string excelFilePath = ""; // 选中的Excel文件路径
// ========== Excel 数据 ==========
private List<string> sheetNames = new List<string>(); // 所有工作表名称
private int selectedSheetIndex = 0; // 当前选中的工作表索引
private string[,] sheetData = null; // 当前工作表的数据(二维数组)
private int maxRows = 0; // 当前工作表的最大行数
private int maxCols = 0; // 当前工作表的最大列数
// ========== UI 状态 ==========
private Vector2 sheetListScrollPosition; // 工作表列表滚动位置
private Vector2 dataScrollPosition; // 数据表格滚动位置
private bool isFileLoaded = false; // 是否已加载文件
private string statusMessage = "请选择一个 Excel 文件"; // 状态消息
// ========== UI 配置 ==========
private const float CELL_WIDTH = 120f; // 单元格显示宽度
private const float CELL_HEIGHT = 20f; // 单元格显示高度
private const int MAX_DISPLAY_ROWS = 100; // 最多显示行数(避免性能问题)
private const int MAX_DISPLAY_COLS = 50; // 最多显示列数
/// <summary>
/// Unity 菜单项:打开 Excel 查看器窗口
/// </summary>
[MenuItem("蹊径/Thrift/Excel 查看器")]
public static void ShowWindow()
{
var window = GetWindow<EPPlusTestEditor>("Excel 查看器");
window.minSize = new Vector2(800, 600);
window.Show();
}
/// <summary>
/// 绘制 Editor 窗口 UI
/// </summary>
private void OnGUI()
{
EditorGUILayout.Space(10);
// ===== 标题区域 =====
EditorGUILayout.LabelField("EPPlus Excel 读取测试工具", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"此工具用于测试 EPPlus 读取 Excel 文件的功能\n" +
"1. 选择 .xlsx 文件\n" +
"2. 点击「加载文件」查看所有工作表\n" +
"3. 选择工作表查看数据内容",
MessageType.Info
);
EditorGUILayout.Space(10);
// ===== 文件选择区域 =====
DrawFileSelection();
EditorGUILayout.Space(10);
// ===== 状态信息 =====
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("状态:", statusMessage, EditorStyles.miniLabel);
if (isFileLoaded)
{
EditorGUILayout.LabelField($"工作表数量: {sheetNames.Count}", EditorStyles.miniLabel);
if (sheetData != null)
{
EditorGUILayout.LabelField($"当前表格: {maxRows} 行 × {maxCols} 列", EditorStyles.miniLabel);
}
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(10);
// ===== 主内容区域 =====
if (isFileLoaded)
{
DrawMainContent();
}
}
/// <summary>
/// 绘制文件选择区域
/// </summary>
private void DrawFileSelection()
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("Excel 文件选择", EditorStyles.boldLabel);
// 文件路径输入框和浏览按钮
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("文件路径:", GUILayout.Width(70));
excelFilePath = EditorGUILayout.TextField(excelFilePath);
if (GUILayout.Button("浏览...", GUILayout.Width(70)))
{
string selected = EditorUtility.OpenFilePanel("选择 Excel 文件", "", "xlsx");
if (!string.IsNullOrEmpty(selected))
{
excelFilePath = selected;
}
}
EditorGUILayout.EndHorizontal();
// 加载按钮
EditorGUILayout.BeginHorizontal();
GUI.enabled = !string.IsNullOrEmpty(excelFilePath) && File.Exists(excelFilePath);
if (GUILayout.Button("🔄 加载文件", GUILayout.Height(30)))
{
LoadExcelFile();
}
// 刷新按钮(仅在已加载时显示)
if (isFileLoaded)
{
if (GUILayout.Button("🔃 刷新数据", GUILayout.Height(30)))
{
LoadExcelFile();
}
}
GUI.enabled = true;
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
/// <summary>
/// 绘制主要内容区域(工作表列表 + 数据表格)
/// </summary>
private void DrawMainContent()
{
EditorGUILayout.BeginHorizontal();
// ===== 左侧:工作表列表 =====
EditorGUILayout.BeginVertical("box", GUILayout.Width(200));
EditorGUILayout.LabelField("工作表列表", EditorStyles.boldLabel);
sheetListScrollPosition = EditorGUILayout.BeginScrollView(
sheetListScrollPosition,
GUILayout.ExpandHeight(true)
);
for (int i = 0; i < sheetNames.Count; i++)
{
// 高亮显示选中的工作表
bool isSelected = (i == selectedSheetIndex);
GUI.backgroundColor = isSelected ? Color.cyan : Color.white;
if (GUILayout.Button($"📄 {sheetNames[i]}", GUILayout.Height(25)))
{
selectedSheetIndex = i;
LoadSheetData(i);
}
GUI.backgroundColor = Color.white;
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
// ===== 右侧:数据表格 =====
EditorGUILayout.BeginVertical("box", GUILayout.ExpandWidth(true));
if (sheetData != null)
{
EditorGUILayout.LabelField(
$"工作表: {sheetNames[selectedSheetIndex]}",
EditorStyles.boldLabel
);
DrawDataTable();
}
else
{
EditorGUILayout.LabelField("请选择一个工作表查看数据", EditorStyles.centeredGreyMiniLabel);
}
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 绘制数据表格
/// </summary>
private void DrawDataTable()
{
if (sheetData == null || maxRows == 0 || maxCols == 0)
return;
dataScrollPosition = EditorGUILayout.BeginScrollView(
dataScrollPosition,
GUILayout.ExpandWidth(true),
GUILayout.ExpandHeight(true)
);
// 限制显示的行列数
int displayRows = Mathf.Min(maxRows, MAX_DISPLAY_ROWS);
int displayCols = Mathf.Min(maxCols, MAX_DISPLAY_COLS);
// 提示信息(如果数据被截断)
if (maxRows > MAX_DISPLAY_ROWS || maxCols > MAX_DISPLAY_COLS)
{
EditorGUILayout.HelpBox(
$"数据过大,仅显示前 {displayRows} 行 × {displayCols} 列",
MessageType.Warning
);
}
// 使用 GUILayout 绘制表格
EditorGUILayout.BeginVertical();
for (int row = 0; row < displayRows; row++)
{
EditorGUILayout.BeginHorizontal();
// 行号
EditorGUILayout.LabelField(
$"{row + 1}",
GUILayout.Width(40)
);
// 每列的数据
for (int col = 0; col < displayCols; col++)
{
string cellValue = sheetData[row, col] ?? "";
// 第一行(表头)使用不同样式
if (row == 0)
{
GUI.backgroundColor = new Color(0.8f, 0.9f, 1f);
EditorGUILayout.LabelField(
cellValue,
EditorStyles.boldLabel,
GUILayout.Width(CELL_WIDTH),
GUILayout.Height(CELL_HEIGHT)
);
GUI.backgroundColor = Color.white;
}
else
{
EditorGUILayout.LabelField(
cellValue,
GUILayout.Width(CELL_WIDTH),
GUILayout.Height(CELL_HEIGHT)
);
}
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
EditorGUILayout.EndScrollView();
}
/// <summary>
/// 加载 Excel 文件,获取所有工作表名称
/// </summary>
private void LoadExcelFile()
{
try
{
if (!File.Exists(excelFilePath))
{
statusMessage = "❌ 文件不存在!";
Debug.LogError($"文件不存在: {excelFilePath}");
return;
}
// 清空之前的数据
sheetNames.Clear();
sheetData = null;
selectedSheetIndex = 0;
// 使用 EPPlus 读取 Excel
FileInfo fileInfo = new FileInfo(excelFilePath);
using (ExcelPackage package = new ExcelPackage(fileInfo))
{
// 获取所有工作表名称
// 注意使用名称访问更可靠Worksheets集合的索引可能不是从0开始
Debug.Log($"[EPPlus测试] Worksheets.Count = {package.Workbook.Worksheets.Count}");
foreach (ExcelWorksheet worksheet in package.Workbook.Worksheets)
{
sheetNames.Add(worksheet.Name);
Debug.Log($"[EPPlus测试] 发现工作表: '{worksheet.Name}'");
}
if (sheetNames.Count == 0)
{
statusMessage = "⚠️ Excel 文件中没有工作表";
isFileLoaded = false;
return;
}
isFileLoaded = true;
statusMessage = $"✅ 成功加载文件,共 {sheetNames.Count} 个工作表";
// 自动加载第一个工作表
LoadSheetData(0);
Debug.Log($"[EPPlus测试] 成功加载: {Path.GetFileName(excelFilePath)}");
Debug.Log($"[EPPlus测试] 工作表列表: {string.Join(", ", sheetNames)}");
}
}
catch (Exception ex)
{
statusMessage = $"❌ 加载失败: {ex.Message}";
isFileLoaded = false;
Debug.LogError($"[EPPlus测试] 加载失败: {ex.Message}\n{ex.StackTrace}");
EditorUtility.DisplayDialog("加载失败", ex.Message, "确定");
}
}
/// <summary>
/// 加载指定工作表的数据
/// 使用工作表名称来访问避免索引问题EPPlus的Worksheets集合索引可能不连续
/// </summary>
private void LoadSheetData(int sheetIndex)
{
try
{
if (sheetIndex < 0 || sheetIndex >= sheetNames.Count)
return;
// 获取要加载的工作表名称
string sheetName = sheetNames[sheetIndex];
FileInfo fileInfo = new FileInfo(excelFilePath);
using (ExcelPackage package = new ExcelPackage(fileInfo))
{
// 使用名称访问工作表(更可靠,避免索引问题)
ExcelWorksheet worksheet = package.Workbook.Worksheets[sheetName];
if (worksheet == null)
{
statusMessage = $"⚠️ 无法读取工作表: {sheetName}";
Debug.LogError($"[EPPlus测试] 工作表 '{sheetName}' 不存在");
return;
}
// 获取有效数据范围
if (worksheet.Dimension == null)
{
statusMessage = $"⚠️ 工作表 '{sheetName}' 为空";
sheetData = null;
maxRows = 0;
maxCols = 0;
Debug.LogWarning($"[EPPlus测试] 工作表 '{sheetName}' 没有数据");
return;
}
maxRows = worksheet.Dimension.End.Row;
maxCols = worksheet.Dimension.End.Column;
// 创建二维数组存储数据
sheetData = new string[maxRows, maxCols];
// 读取所有单元格数据
// 注意EPPlus 的索引从 1 开始,不是 0
for (int row = 1; row <= maxRows; row++)
{
for (int col = 1; col <= maxCols; col++)
{
var cell = worksheet.Cells[row, col];
sheetData[row - 1, col - 1] = cell.Value?.ToString() ?? "";
}
}
statusMessage = $"✅ 已加载工作表: {sheetName} ({maxRows}行 × {maxCols}列)";
// 重置滚动位置
dataScrollPosition = Vector2.zero;
Debug.Log($"[EPPlus测试] 成功加载工作表: '{sheetName}'");
Debug.Log($"[EPPlus测试] 数据范围: {maxRows} 行 × {maxCols} 列");
// 打印前几行数据用于调试
if (maxRows > 0 && maxCols > 0)
{
Debug.Log($"[EPPlus测试] 第1行第1列: {sheetData[0, 0]}");
if (maxRows > 1)
{
Debug.Log($"[EPPlus测试] 第2行第1列: {sheetData[1, 0]}");
}
}
}
}
catch (Exception ex)
{
statusMessage = $"❌ 读取工作表失败: {ex.Message}";
Debug.LogError($"[EPPlus测试] 读取工作表失败: {ex.Message}\n{ex.StackTrace}");
}
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 8d6ea734388f0a7458a42549089e7c89
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 98ccc8fb1ea8ccc4bb42355ce83ffeed
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,5 +0,0 @@
# 忽略所有日志文件
*
# 但保留 .gitignore 自身
!.gitignore

View File

@ -1,715 +0,0 @@
using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OfficeOpenXml;
using Thrift.Protocol;
using Thrift.Transport.Client;
using System.Threading;
namespace ThriftPipelineTools
{
/// <summary>
/// 快速生成 Bytes 工具(简化版)
/// 只生成 AllConfigs.bytes不生成 C# 和 DR
/// 适用于表结构未变化,只需要更新数据的场景
/// </summary>
public class ThriftBytesGeneratorEditor : EditorWindow
{
// ========== 版本信息 ==========
private const string VERSION = "v1.0.0 (Bytes Only)";
// ========== 配置Key使用EditorPrefs ==========
private const string PREF_KEY_DOCS_PATH = "ThriftPipeline_DocsPath";
// ========== 路径配置 ==========
private string docsProjectPath = ""; // Docs 项目路径
// ========== UI 状态 ==========
private Vector2 scrollPosition;
private Vector2 logScrollPosition;
private StringBuilder logBuilder = new StringBuilder();
private bool isExecuting = false;
// ========== 进度控制 ==========
private float progress = 0f;
private string currentStep = "";
[MenuItem("蹊径/Thrift/快速生成 Bytes简化版")]
public static void ShowWindow()
{
var window = GetWindow<ThriftBytesGeneratorEditor>("快速生成 Bytes");
window.minSize = new Vector2(800, 600);
window.Show();
}
private void OnEnable()
{
LoadConfig();
}
private void OnGUI()
{
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
EditorGUILayout.Space(10);
EditorGUILayout.LabelField($"快速生成 Bytes 工具 - {VERSION}", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"此工具只生成 AllConfigs.bytes 文件,不生成 C# 和 DR\n" +
"适用于:表结构未变化,只需要更新配置数据的场景\n" +
"⚡ 执行速度更快,跳过编译和代码生成步骤",
MessageType.Info
);
EditorGUILayout.Space(10);
DrawPathConfiguration();
EditorGUILayout.Space(10);
DrawExecutionSection();
EditorGUILayout.Space(10);
DrawLogSection();
EditorGUILayout.EndScrollView();
}
/// <summary>
/// 绘制路径配置区域
/// </summary>
private void DrawPathConfiguration()
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("路径配置", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"只需配置 Docs 项目路径(包含 Excel 配置和 cfg_txt.json",
MessageType.None
);
// Docs 项目路径
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Docs 项目路径:", GUILayout.Width(120));
docsProjectPath = EditorGUILayout.TextField(docsProjectPath);
if (GUILayout.Button("选择", GUILayout.Width(60)))
{
string selected = EditorUtility.OpenFolderPanel("选择 Docs 项目路径", docsProjectPath, "");
if (!string.IsNullOrEmpty(selected))
{
docsProjectPath = selected;
SaveConfig();
}
}
EditorGUILayout.EndHorizontal();
// 显示派生路径预览
if (!string.IsNullOrEmpty(docsProjectPath))
{
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("派生路径预览:", EditorStyles.miniLabel);
var paths = GetDerivedPaths();
EditorGUILayout.LabelField($" cfg_txt.json: {paths["cfg_json"]}", EditorStyles.miniLabel);
EditorGUILayout.LabelField($" Config 目录: {paths["config_dir"]}", EditorStyles.miniLabel);
EditorGUILayout.LabelField($" 输出到 Unity: {paths["unity_bytes_dir"]}", EditorStyles.miniLabel);
}
EditorGUILayout.EndVertical();
}
/// <summary>
/// 绘制执行区域
/// </summary>
private void DrawExecutionSection()
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("执行控制", EditorStyles.boldLabel);
GUI.enabled = !isExecuting && ValidatePaths();
if (GUILayout.Button("⚡ 快速生成 AllConfigs.bytes", GUILayout.Height(40)))
{
GenerateBytes();
}
GUI.enabled = true;
if (isExecuting)
{
EditorGUILayout.Space(5);
EditorGUILayout.LabelField($"当前步骤: {currentStep}");
EditorGUI.ProgressBar(EditorGUILayout.GetControlRect(GUILayout.Height(20)), progress, "执行中...");
}
EditorGUILayout.EndVertical();
}
/// <summary>
/// 绘制日志区域
/// </summary>
private void DrawLogSection()
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("执行日志", EditorStyles.boldLabel);
logScrollPosition = EditorGUILayout.BeginScrollView(
logScrollPosition,
GUILayout.Height(300)
);
EditorGUILayout.TextArea(logBuilder.ToString(), GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
if (GUILayout.Button("清空日志"))
{
logBuilder.Clear();
}
EditorGUILayout.EndVertical();
}
/// <summary>
/// 加载配置(使用 EditorPrefs本地保存不会被git提交
/// </summary>
private void LoadConfig()
{
try
{
docsProjectPath = EditorPrefs.GetString(PREF_KEY_DOCS_PATH, "");
}
catch (Exception ex)
{
Debug.LogError($"加载配置失败: {ex.Message}");
}
}
/// <summary>
/// 保存配置(使用 EditorPrefs本地保存不会被git提交
/// </summary>
private void SaveConfig()
{
try
{
EditorPrefs.SetString(PREF_KEY_DOCS_PATH, docsProjectPath);
}
catch (Exception ex)
{
Debug.LogError($"保存配置失败: {ex.Message}");
}
}
/// <summary>
/// 获取所有派生路径
/// </summary>
private Dictionary<string, string> GetDerivedPaths()
{
var paths = new Dictionary<string, string>();
// Docs 相关路径
if (!string.IsNullOrEmpty(docsProjectPath))
{
paths["cfg_json"] = Path.Combine(docsProjectPath, "tool", "cfg", "cfg_txt.json");
paths["config_dir"] = Path.Combine(docsProjectPath, "config");
}
// Unity 项目相关路径(当前项目)
string assetsPath = Application.dataPath;
paths["unity_bytes_dir"] = Path.Combine(assetsPath, "Design_SubModule", "ConfigData");
return paths;
}
/// <summary>
/// 验证路径
/// </summary>
private bool ValidatePaths()
{
if (string.IsNullOrEmpty(docsProjectPath))
return false;
var paths = GetDerivedPaths();
if (!File.Exists(paths["cfg_json"]))
return false;
if (!Directory.Exists(paths["config_dir"]))
return false;
return true;
}
/// <summary>
/// 日志输出
/// </summary>
private void Log(string message)
{
logBuilder.AppendLine(message);
Repaint();
}
/// <summary>
/// 生成 Bytes 文件
/// </summary>
private async void GenerateBytes()
{
isExecuting = true;
logBuilder.Clear();
progress = 0f;
DateTime startTime = DateTime.Now;
Log("================================================================================");
Log($"开始生成 AllConfigs.bytes ({VERSION})");
Log("================================================================================");
Log("");
try
{
var paths = GetDerivedPaths();
currentStep = "读取配置并生成 bytes";
progress = 0.3f;
Repaint();
if (!await GenerateBytesFile(paths))
{
Log("[FAIL] 生成失败");
EditorUtility.DisplayDialog("失败", "生成 AllConfigs.bytes 失败,请查看日志", "确定");
return;
}
progress = 1f;
currentStep = "完成";
Repaint();
DateTime endTime = DateTime.Now;
TimeSpan duration = endTime - startTime;
Log("");
Log("================================================================================");
Log($"✅ 生成完成!");
Log($" 耗时: {duration.TotalSeconds:F2} 秒");
Log($" 输出: {paths["unity_bytes_dir"]}/AllConfigs.bytes");
Log("================================================================================");
EditorUtility.DisplayDialog("成功", $"AllConfigs.bytes 生成完成!\n耗时: {duration.TotalSeconds:F2} 秒", "确定");
}
catch (Exception ex)
{
Log($"\n[FAIL] 执行失败: {ex.Message}");
Log(ex.StackTrace);
EditorUtility.DisplayDialog("错误", $"执行失败:\n{ex.Message}", "确定");
}
finally
{
isExecuting = false;
AssetDatabase.Refresh();
Repaint();
}
}
/// <summary>
/// 生成 Bytes 文件
/// </summary>
private async Task<bool> GenerateBytesFile(Dictionary<string, string> paths)
{
Log("【生成 AllConfigs.bytes】");
Log("--------------------------------------------------------------------------------");
try
{
string cfgJsonPath = paths["cfg_json"];
string configDir = paths["config_dir"];
string bytesOutputDir = paths["unity_bytes_dir"];
// 创建输出目录
if (!Directory.Exists(bytesOutputDir))
Directory.CreateDirectory(bytesOutputDir);
// 加载程序集
Log(" [1/3] 加载程序集");
System.Reflection.Assembly assembly = null;
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
if (asm.GetType("Byway.Thrift.Data.AllConfigs") != null)
{
assembly = asm;
break;
}
}
if (assembly == null)
{
Log("[FAIL] 找不到 Byway.Thrift.Data.AllConfigs 程序集");
Log(" 请确保已经执行过完整流程,生成了 C# 类");
return false;
}
Log(" [OK] 程序集加载成功");
// 读取配置
Log(" [2/3] 读取配置并填充数据");
string cfgText = File.ReadAllText(cfgJsonPath);
var cfg = JsonConvert.DeserializeObject<JObject>(cfgText);
var fileList = cfg["file_list"] as JArray;
// 创建AllConfigs实例
var allConfigsType = assembly.GetType("Byway.Thrift.Data.AllConfigs");
object allConfigsInstance = Activator.CreateInstance(allConfigsType);
int successCount = 0;
int processedCount = 0;
// 填充每个配置
foreach (JObject configItem in fileList)
{
string inFile = configItem["in_file"]?.ToString();
string outFile = configItem["out_file"]?.ToString();
string sheetName = configItem["sheet_name"]?.ToString();
var columnTypes = configItem["coloum_type"] as JArray;
if (string.IsNullOrEmpty(inFile) || string.IsNullOrEmpty(outFile))
continue;
string structName = outFile.Replace(".txt", "");
string excelPath = Path.Combine(configDir, inFile);
if (!File.Exists(excelPath))
{
Log($" [{++processedCount}] [SKIP] {structName} - Excel文件不存在");
continue;
}
Log($" [{++processedCount}] 处理: {structName}");
if (FillThriftObject(allConfigsInstance, allConfigsType, structName,
excelPath, sheetName, columnTypes, assembly))
{
successCount++;
}
}
// 序列化为bytes
Log($"\n [3/3] 序列化 AllConfigs.bytes (已设置 {successCount} 个配置)");
// 检查有多少配置实际被设置
int setCount = 0;
var issetField = allConfigsType.GetField("__isset",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
if (issetField != null)
{
var issetObj = issetField.GetValue(allConfigsInstance);
var issetType = issetObj.GetType();
foreach (var prop in allConfigsType.GetProperties())
{
if (prop.Name != "Isset" && prop.PropertyType.Namespace == "Byway.Thrift.Data")
{
var issetProp = issetType.GetProperty(prop.Name);
if (issetProp != null && (bool)issetProp.GetValue(issetObj))
setCount++;
}
}
}
Log($" __isset 标志统计: {setCount} 个配置被标记为已设置");
using (var memoryStream = new MemoryStream())
{
using (var transport = new TStreamTransport(memoryStream, memoryStream, new Thrift.TConfiguration()))
{
using (var protocol = new TBinaryProtocol(transport))
{
var writeMethod = allConfigsType.GetMethod("WriteAsync");
if (writeMethod != null)
{
var task = (Task)writeMethod.Invoke(allConfigsInstance,
new object[] { protocol, CancellationToken.None });
await task;
byte[] bytes = memoryStream.ToArray();
string allConfigsBytesPath = Path.Combine(bytesOutputDir, "AllConfigs.bytes");
File.WriteAllBytes(allConfigsBytesPath, bytes);
Log($" [OK] 生成成功 ({bytes.Length} bytes / {bytes.Length / 1024.0:F2} KB)");
if (bytes.Length < 1000)
{
Log($" [WARNING] 文件太小 ({bytes.Length} bytes),可能大部分配置未序列化!");
}
}
}
}
}
Log($"\n生成完成: 成功 {successCount} 个配置");
return true;
}
catch (Exception ex)
{
Log($"[FAIL] 生成失败: {ex.Message}");
Log(ex.StackTrace);
return false;
}
}
/// <summary>
/// 填充Thrift对象数据
/// </summary>
private bool FillThriftObject(object allConfigsInstance, Type allConfigsType, string structName,
string excelPath, string sheetName, JArray columnTypes, System.Reflection.Assembly assembly)
{
try
{
var configType = assembly.GetType($"Byway.Thrift.Data.{structName}");
var itemType = assembly.GetType($"Byway.Thrift.Data.{structName}Item");
if (configType == null || itemType == null)
{
Log($" [ERROR] 找不到类型: {structName} 或 {structName}Item");
return false;
}
object configInstance = Activator.CreateInstance(configType);
var dictType = typeof(Dictionary<,>).MakeGenericType(typeof(int), itemType);
var dictInstance = Activator.CreateInstance(dictType);
// 读取Excel
FileInfo fileInfo = new FileInfo(excelPath);
using (ExcelPackage package = new ExcelPackage(fileInfo))
{
ExcelWorksheet worksheet = null;
if (!string.IsNullOrEmpty(sheetName) && package.Workbook.Worksheets[sheetName] != null)
worksheet = package.Workbook.Worksheets[sheetName];
else
worksheet = package.Workbook.Worksheets[0];
if (worksheet == null || worksheet.Dimension == null)
{
Log($" [ERROR] 工作表为空");
return false;
}
int maxRows = worksheet.Dimension.End.Row;
int maxCols = worksheet.Dimension.End.Column;
// 读取表头第1行
List<string> headers = new List<string>();
List<int> validColumns = new List<int>();
int maxColumns = columnTypes?.Count ?? maxCols;
for (int col = 1; col <= maxCols && headers.Count < maxColumns; col++)
{
string header = worksheet.Cells[1, col].Value?.ToString() ?? "";
if (!string.IsNullOrWhiteSpace(header))
{
headers.Add(header);
validColumns.Add(col);
}
}
// 读取数据从第3行开始
int filledItemCount = 0;
for (int row = 3; row <= maxRows; row++)
{
bool isEmptyRow = true;
for (int i = 0; i < validColumns.Count; i++)
{
if (worksheet.Cells[row, validColumns[i]].Value != null)
{
isEmptyRow = false;
break;
}
}
if (isEmptyRow)
continue;
object itemInstance = Activator.CreateInstance(itemType);
int? idValue = null;
for (int i = 0; i < headers.Count; i++)
{
string originalHeader = headers[i];
string fieldName = SanitizeFieldName(originalHeader);
int colIndex = validColumns[i];
var cell = worksheet.Cells[row, colIndex];
object cellValue = cell.Value;
var property = itemType.GetProperty(fieldName);
if (property != null)
{
try
{
object convertedValue;
if (cellValue == null)
{
if (property.PropertyType == typeof(string))
{
convertedValue = "";
}
else if (property.PropertyType.IsValueType)
{
convertedValue = Activator.CreateInstance(property.PropertyType);
}
else
{
continue;
}
}
else
{
convertedValue = ConvertCellValue(cellValue, property.PropertyType);
}
property.SetValue(itemInstance, convertedValue);
if (fieldName.ToLower() == "id" && convertedValue is int)
idValue = (int)convertedValue;
}
catch (Exception ex)
{
Log($" [WARN] 行{row} 字段{fieldName}转换失败: {ex.Message}");
}
}
}
if (idValue.HasValue)
{
var addMethod = dictType.GetMethod("Add");
addMethod.Invoke(dictInstance, new object[] { idValue.Value, itemInstance });
filledItemCount++;
}
}
Log($" 读取了 {filledItemCount} 条数据");
}
// 设置字典属性
string dictPropertyName = char.ToUpper(structName[0]) + structName.Substring(1).ToLower() + "s";
var dictProperty = configType.GetProperty(dictPropertyName);
if (dictProperty != null)
{
dictProperty.SetValue(configInstance, dictInstance);
}
else
{
Log($" [ERROR] 未找到字典属性: {dictPropertyName}");
return false;
}
// 设置到AllConfigs
var allConfigProperty = allConfigsType.GetProperty(structName);
if (allConfigProperty != null)
{
allConfigProperty.SetValue(allConfigsInstance, configInstance);
var verifyValue = allConfigProperty.GetValue(allConfigsInstance);
if (verifyValue == null)
{
Log($" [ERROR] 设置到AllConfigs后为null!");
return false;
}
Log($" [OK] 成功");
}
else
{
Log($" [ERROR] AllConfigs中未找到属性: {structName}");
return false;
}
return true;
}
catch (Exception ex)
{
Log($" [ERROR] {structName}: {ex.Message}");
return false;
}
}
/// <summary>
/// 清理并转换字段名为PascalCase
/// </summary>
private string SanitizeFieldName(string fieldName)
{
if (string.IsNullOrWhiteSpace(fieldName))
return "Field_unknown";
StringBuilder sb = new StringBuilder();
foreach (char c in fieldName)
{
if (char.IsLetterOrDigit(c) || c == '_')
sb.Append(c);
else if (c == ' ' || c == '-')
sb.Append('_');
}
string cleaned = sb.ToString();
if (string.IsNullOrEmpty(cleaned))
return "Field_unknown";
if (cleaned.Length > 0)
{
return char.ToUpper(cleaned[0]) + cleaned.Substring(1);
}
return cleaned;
}
/// <summary>
/// 转换单元格值
/// </summary>
private object ConvertCellValue(object cellValue, Type targetType)
{
try
{
if (targetType == typeof(int))
{
if (cellValue is double d)
return (int)d;
return Convert.ToInt32(cellValue);
}
else if (targetType == typeof(long))
{
if (cellValue is double d)
return (long)d;
return Convert.ToInt64(cellValue);
}
else if (targetType == typeof(double))
{
return Convert.ToDouble(cellValue);
}
else if (targetType == typeof(bool))
{
return Convert.ToBoolean(cellValue);
}
else if (targetType == typeof(string))
{
return cellValue.ToString();
}
return cellValue;
}
catch
{
if (targetType == typeof(int)) return 0;
if (targetType == typeof(long)) return 0L;
if (targetType == typeof(double)) return 0.0;
if (targetType == typeof(bool)) return false;
if (targetType == typeof(string)) return "";
return null;
}
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 853ea353631081f4693099a548199b77
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: af282e3c12494574f9075f3ec1656b3f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 5700364c75144e9418afa8c01c54ec6d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 878d1e2621294df439f0cf7a06d20f32
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,937 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using OfficeOpenXml;
using ArtResource;
using Debug = UnityEngine.Debug;
namespace DesignTools.Collections
{
/// <summary>
/// 表情配置Editor工具
/// 读取Docs/config/Emoji.xlsx关联Art_SO/Collections/EmojiResource.asset
/// </summary>
public class EmojiConfigEditor : EditorWindow
{
private const string EMOJI_SO_PATH = "Assets/Art_SubModule/Art_SO/Collections";
private const string EMOJI_SO_NAME = "EmojiResource";
private const string EMOJI_EXCEL_NAME = "Emoji.xlsx";
private const string LANGUAGE_EXCEL_NAME = "AllLanguage.xlsx";
private const string EMOJI_SHEET_NAME = "Emoji";
private const string LANGUAGE_SHEET_NAME = "client";
private const string DOCS_PATH_PREF_KEY = "EmojiConfigEditor_DocsPath";
private const string DESIGN_SUBMODULE_PATH = "Assets/Design_SubModule";
private string docsRootPath = "";
private List<EmojiData> emojiDataList = new List<EmojiData>();
private ArtTableSO emojiTableSO;
private Dictionary<string, Dictionary<string, string>> languageDict = new Dictionary<string, Dictionary<string, string>>();
private Vector2 scrollPosition;
private bool isDataLoaded = false;
private string pendingTooltipText = "";
[MenuItem("策划工具/收藏品/表情")]
public static void ShowWindow()
{
var window = GetWindow<EmojiConfigEditor>("表情配置");
window.minSize = new Vector2(1000, 600);
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("表情配置工具", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
// Docs路径选择
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("Docs项目根目录", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
docsRootPath = EditorGUILayout.TextField("路径", docsRootPath);
if (GUILayout.Button("选择文件夹", GUILayout.Width(100)))
{
string selectedPath = EditorUtility.OpenFolderPanel("选择Docs项目根目录", "", "");
if (!string.IsNullOrEmpty(selectedPath))
{
docsRootPath = selectedPath;
EditorPrefs.SetString(DOCS_PATH_PREF_KEY, docsRootPath);
}
}
EditorGUILayout.EndHorizontal();
if (GUILayout.Button("加载配置数据", GUILayout.Height(30)))
{
LoadData();
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(5);
// 数据编辑区域
if (isDataLoaded)
{
DrawDataEditor();
}
else
{
EditorGUILayout.HelpBox("请先选择Docs根目录并加载配置数据", MessageType.Info);
}
}
/// <summary>
/// 加载配置数据
/// </summary>
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;
}
// 校验Emoji SO是否存在
string emojiSOPath = Path.Combine(EMOJI_SO_PATH, $"{EMOJI_SO_NAME}.asset");
emojiTableSO = AssetDatabase.LoadAssetAtPath<ArtTableSO>(emojiSOPath);
if (emojiTableSO == null)
{
EditorUtility.DisplayDialog("错误",
$"未找到表情资源配置\n路径: {emojiSOPath}\n\n请先在美术资源配置工具中创建",
"确定");
return;
}
if (emojiTableSO.Items == null || emojiTableSO.Items.Count == 0)
{
EditorUtility.DisplayDialog("错误", "表情资源配置数据为空", "确定");
return;
}
// 加载语言表
LoadLanguageData();
// 加载Emoji.xlsx
LoadEmojiExcel();
isDataLoaded = true;
EditorUtility.DisplayDialog("成功", "配置数据加载成功!", "确定");
}
catch (Exception e)
{
EditorUtility.DisplayDialog("错误", $"加载数据失败: {e.Message}\n{e.StackTrace}", "确定");
isDataLoaded = false;
}
}
/// <summary>
/// 执行Git命令
/// </summary>
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}";
}
}
/// <summary>
/// 检查并更新Docs仓库
/// </summary>
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;
}
/// <summary>
/// 检查并切换Design_SubModule到main分支
/// </summary>
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 分支");
EditorUtility.DisplayDialog("分支切换成功",
$"Design_SubModule已从 {currentBranch} 切换到 main 分支",
"确定");
}
else
{
EditorUtility.ClearProgressBar();
Debug.Log("Design_SubModule已在main分支");
}
return true;
}
/// <summary>
/// 加载语言数据
/// </summary>
private void LoadLanguageData()
{
languageDict.Clear();
string languageExcelPath = Path.Combine(docsRootPath, "config", LANGUAGE_EXCEL_NAME);
if (!File.Exists(languageExcelPath))
{
Debug.LogWarning($"未找到语言表: {languageExcelPath}");
return;
}
using (var package = new ExcelPackage(new FileInfo(languageExcelPath)))
{
var worksheet = package.Workbook.Worksheets[LANGUAGE_SHEET_NAME];
if (worksheet == null)
{
Debug.LogWarning($"语言表中未找到Sheet: {LANGUAGE_SHEET_NAME}");
return;
}
// 查找Key列和语言列
int keyColumnIndex = -1;
int zhCNColumnIndex = -1;
int enUSColumnIndex = -1;
int ptBRColumnIndex = -1;
int columnCount = worksheet.Dimension.Columns;
for (int col = 1; col <= columnCount; col++)
{
string header = worksheet.Cells[1, col].Text;
if (header == "key")
{
keyColumnIndex = col;
}
else if (header == "zh_CN")
{
zhCNColumnIndex = col;
}
else if (header == "en_US")
{
enUSColumnIndex = col;
}
else if (header == "pt_BR")
{
ptBRColumnIndex = col;
}
}
if (keyColumnIndex < 0)
{
Debug.LogWarning("语言表中未找到Key列");
return;
}
// 读取数据从第3行开始
int rowCount = worksheet.Dimension.Rows;
for (int row = 3; row <= rowCount; row++)
{
string key = worksheet.Cells[row, keyColumnIndex].Text;
if (string.IsNullOrEmpty(key)) continue;
if (!languageDict.ContainsKey(key))
{
languageDict[key] = new Dictionary<string, string>();
}
if (zhCNColumnIndex > 0)
{
languageDict[key]["zh_CN"] = worksheet.Cells[row, zhCNColumnIndex].Text;
}
if (enUSColumnIndex > 0)
{
languageDict[key]["en_US"] = worksheet.Cells[row, enUSColumnIndex].Text;
}
if (ptBRColumnIndex > 0)
{
languageDict[key]["pt_BR"] = worksheet.Cells[row, ptBRColumnIndex].Text;
}
}
}
}
/// <summary>
/// 加载Emoji.xlsx
/// </summary>
private void LoadEmojiExcel()
{
emojiDataList.Clear();
string emojiExcelPath = Path.Combine(docsRootPath, "config", EMOJI_EXCEL_NAME);
if (!File.Exists(emojiExcelPath))
{
throw new Exception($"未找到Emoji配置文件: {emojiExcelPath}");
}
using (var package = new ExcelPackage(new FileInfo(emojiExcelPath)))
{
var worksheet = package.Workbook.Worksheets[EMOJI_SHEET_NAME];
if (worksheet == null)
{
throw new Exception($"Emoji.xlsx中未找到Sheet: {EMOJI_SHEET_NAME}");
}
// 读取表头第1行
int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1;
int columnCount = worksheet.Dimension.Columns;
for (int col = 1; col <= columnCount; col++)
{
string header = worksheet.Cells[1, col].Text;
switch (header)
{
case "Id":
idCol = col;
break;
case "NameKey":
nameKeyCol = col;
break;
case "Init":
initCol = col;
break;
case "Icon":
iconCol = col;
break;
}
}
if (idCol < 0 || nameKeyCol < 0 || initCol < 0 || iconCol < 0)
{
throw new Exception("Emoji.xlsx表结构不正确缺少必要列Id/NameKey/Init/Icon");
}
// 读取数据从第3行开始
int rowCount = worksheet.Dimension.Rows;
for (int row = 3; row <= rowCount; row++)
{
string idText = worksheet.Cells[row, idCol].Text;
if (string.IsNullOrEmpty(idText)) continue;
if (!int.TryParse(idText, out int id)) continue;
string nameKey = worksheet.Cells[row, nameKeyCol].Text;
string initText = worksheet.Cells[row, initCol].Text;
string iconText = worksheet.Cells[row, iconCol].Text;
int init = 0;
if (!string.IsNullOrEmpty(initText))
{
int.TryParse(initText, out init);
}
int iconId = -1;
if (!string.IsNullOrEmpty(iconText))
{
if (!int.TryParse(iconText, out iconId))
{
iconId = -1;
}
}
var emojiData = new EmojiData
{
Id = id,
NameKey = nameKey,
Init = init,
IconId = iconId
};
emojiDataList.Add(emojiData);
}
}
}
/// <summary>
/// 绘制数据编辑器
/// </summary>
private void DrawDataEditor()
{
pendingTooltipText = "";
EditorGUILayout.BeginVertical("box");
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"表情数据列表(共 {emojiDataList.Count} 条)", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
GUI.backgroundColor = Color.cyan;
if (GUILayout.Button("+ 添加表情", GUILayout.Height(25), GUILayout.Width(100)))
{
AddNewEmoji();
}
GUI.backgroundColor = Color.white;
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(3);
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
// 表头
EditorGUILayout.BeginHorizontal("box");
EditorGUILayout.LabelField("Id", EditorStyles.boldLabel, GUILayout.Width(50));
EditorGUILayout.LabelField("NameKey", EditorStyles.boldLabel, GUILayout.Width(150));
EditorGUILayout.LabelField("中文名称", EditorStyles.boldLabel, GUILayout.Width(120));
EditorGUILayout.LabelField("Init", EditorStyles.boldLabel, GUILayout.Width(50));
EditorGUILayout.LabelField("Icon", EditorStyles.boldLabel, GUILayout.Width(200));
EditorGUILayout.LabelField("预览", EditorStyles.boldLabel, GUILayout.Width(100));
EditorGUILayout.LabelField("操作", EditorStyles.boldLabel, GUILayout.Width(60));
EditorGUILayout.EndHorizontal();
// 数据行
for (int i = 0; i < emojiDataList.Count; i++)
{
DrawEmojiDataRow(emojiDataList[i]);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
EditorGUILayout.Space(10);
// 保存按钮
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
GUI.backgroundColor = Color.green;
if (GUILayout.Button("保存配置到Excel", GUILayout.Height(30), GUILayout.Width(200)))
{
SaveData();
}
GUI.backgroundColor = Color.white;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
// 在所有内容绘制完后绘制tooltip确保在最上层
if (!string.IsNullOrEmpty(pendingTooltipText))
{
Vector2 tooltipSize = GUI.skin.box.CalcSize(new GUIContent(pendingTooltipText));
tooltipSize.x += 10;
tooltipSize.y += 10;
Vector2 mousePos = Event.current.mousePosition;
Rect tooltipRect = new Rect(
mousePos.x + 1,
mousePos.y + 1,
tooltipSize.x,
tooltipSize.y
);
GUI.Box(tooltipRect, pendingTooltipText);
}
}
/// <summary>
/// 绘制单行数据
/// </summary>
private void DrawEmojiDataRow(EmojiData data)
{
EditorGUILayout.BeginHorizontal("box");
// Id可编辑
data.Id = EditorGUILayout.IntField(data.Id, GUILayout.Width(50));
// NameKey可编辑
data.NameKey = EditorGUILayout.TextField(data.NameKey, GUILayout.Width(150));
// 中文名称(只读预览,带即时多语言显示)
GUI.enabled = false;
string zhName = "未找到语言Key";
if (languageDict.ContainsKey(data.NameKey))
{
var langs = languageDict[data.NameKey];
if (langs.ContainsKey("zh_CN"))
{
zhName = langs["zh_CN"];
}
}
Rect nameRect = GUILayoutUtility.GetRect(new GUIContent(zhName), GUI.skin.textField, GUILayout.Width(120));
EditorGUI.TextField(nameRect, zhName);
// 检测鼠标悬停并准备tooltip内容
if (nameRect.Contains(Event.current.mousePosition) && languageDict.ContainsKey(data.NameKey))
{
var langs = languageDict[data.NameKey];
List<string> tooltipLines = new List<string>();
if (langs.ContainsKey("zh_CN") && !string.IsNullOrEmpty(langs["zh_CN"]))
{
tooltipLines.Add($"[中文] {langs["zh_CN"]}");
}
if (langs.ContainsKey("en_US") && !string.IsNullOrEmpty(langs["en_US"]))
{
tooltipLines.Add($"[English] {langs["en_US"]}");
}
if (langs.ContainsKey("pt_BR") && !string.IsNullOrEmpty(langs["pt_BR"]))
{
tooltipLines.Add($"[Português] {langs["pt_BR"]}");
}
if (tooltipLines.Count > 0)
{
pendingTooltipText = string.Join("\n", tooltipLines);
Repaint();
}
}
GUI.enabled = true;
// InitCheckbox
bool initChecked = data.Init == 1;
bool newInitChecked = EditorGUILayout.Toggle(initChecked, GUILayout.Width(50));
data.Init = newInitChecked ? 1 : 0;
// Icon下拉列表
var iconItems = emojiTableSO.Items;
var iconNamesList = new List<string> { "未选择" };
iconNamesList.AddRange(iconItems.Select(x => x.Name));
var iconNames = iconNamesList.ToArray();
int currentIndex = 0;
var currentItem = iconItems.Find(x => x.Id == data.IconId);
if (currentItem != null)
{
int itemIndex = iconItems.IndexOf(currentItem);
if (itemIndex >= 0)
{
currentIndex = itemIndex + 1;
}
}
EditorGUI.BeginChangeCheck();
int newIndex = EditorGUILayout.Popup(currentIndex, iconNames, GUILayout.Width(200));
if (EditorGUI.EndChangeCheck())
{
if (newIndex == 0)
{
data.IconId = -1;
}
else if (newIndex > 0 && newIndex <= iconItems.Count)
{
data.IconId = iconItems[newIndex - 1].Id;
}
}
// 预览
if (data.IconId >= 0)
{
var item = iconItems.Find(x => x.Id == data.IconId);
if (item != null)
{
Sprite sprite = item.Sprite;
if (sprite != null && sprite.texture != null)
{
Rect previewRect = GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
EditorGUI.DrawRect(previewRect, new Color(0.5f, 0.5f, 0.5f, 1f));
Rect texCoords = sprite.textureRect;
Texture2D tex = sprite.texture;
Rect normalizedCoords = new Rect(
texCoords.x / tex.width,
texCoords.y / tex.height,
texCoords.width / tex.width,
texCoords.height / tex.height
);
float aspect = texCoords.width / texCoords.height;
Rect drawRect = previewRect;
if (aspect > 1f)
{
float height = drawRect.width / aspect;
drawRect.y += (drawRect.height - height) * 0.5f;
drawRect.height = height;
}
else
{
float width = drawRect.height * aspect;
drawRect.x += (drawRect.width - width) * 0.5f;
drawRect.width = width;
}
GUI.DrawTextureWithTexCoords(drawRect, tex, normalizedCoords, true);
if (!string.IsNullOrEmpty(item.Desc))
{
GUI.Label(previewRect, new GUIContent("", item.Desc));
}
}
else
{
GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
GUILayout.Space(-50);
EditorGUILayout.LabelField("无图片", GUILayout.Width(50));
}
}
else
{
GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
GUILayout.Space(-50);
EditorGUILayout.LabelField("未选择", GUILayout.Width(50));
}
}
else
{
GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
GUILayout.Space(-50);
EditorGUILayout.LabelField("未选择", GUILayout.Width(50));
}
// 删除按钮
GUI.backgroundColor = Color.red;
if (GUILayout.Button("删除", GUILayout.Width(60)))
{
if (EditorUtility.DisplayDialog("确认删除",
$"确定要删除表情 {data.Id} ({data.NameKey}) 吗?",
"删除", "取消"))
{
emojiDataList.Remove(data);
}
}
GUI.backgroundColor = Color.white;
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 添加新表情
/// </summary>
private void AddNewEmoji()
{
int newId = 1;
if (emojiDataList.Count > 0)
{
newId = emojiDataList.Max(x => x.Id) + 1;
}
var newEmoji = new EmojiData
{
Id = newId,
NameKey = "",
Init = 0,
IconId = -1
};
emojiDataList.Add(newEmoji);
scrollPosition = new Vector2(0, float.MaxValue);
}
/// <summary>
/// 保存数据到Excel
/// </summary>
private void SaveData()
{
try
{
string emojiExcelPath = Path.Combine(docsRootPath, "config", EMOJI_EXCEL_NAME);
if (!File.Exists(emojiExcelPath))
{
EditorUtility.DisplayDialog("错误", $"未找到Emoji配置文件: {emojiExcelPath}", "确定");
return;
}
using (var package = new ExcelPackage(new FileInfo(emojiExcelPath)))
{
var worksheet = package.Workbook.Worksheets[EMOJI_SHEET_NAME];
if (worksheet == null)
{
EditorUtility.DisplayDialog("错误", $"Emoji.xlsx中未找到Sheet: {EMOJI_SHEET_NAME}", "确定");
return;
}
// 查找列索引
int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1;
int columnCount = worksheet.Dimension.Columns;
for (int col = 1; col <= columnCount; col++)
{
string header = worksheet.Cells[1, col].Text;
switch (header)
{
case "Id":
idCol = col;
break;
case "NameKey":
nameKeyCol = col;
break;
case "Init":
initCol = col;
break;
case "Icon":
iconCol = col;
break;
}
}
// 更新和删除数据从第3行开始
int rowCount = worksheet.Dimension.Rows;
var processedIds = new HashSet<int>();
// 第一遍:更新现有行或删除
for (int row = rowCount; row >= 3; row--)
{
string idText = worksheet.Cells[row, idCol].Text;
if (string.IsNullOrEmpty(idText)) continue;
if (!int.TryParse(idText, out int id)) continue;
var emojiData = emojiDataList.Find(x => x.Id == id);
if (emojiData != null)
{
worksheet.Cells[row, nameKeyCol].Value = emojiData.NameKey;
worksheet.Cells[row, initCol].Value = emojiData.Init;
if (emojiData.IconId < 0)
{
worksheet.Cells[row, iconCol].Value = "";
}
else
{
worksheet.Cells[row, iconCol].Value = emojiData.IconId;
}
processedIds.Add(id);
}
else
{
worksheet.DeleteRow(row);
}
}
// 第二遍:添加新行
int currentRow = worksheet.Dimension?.Rows ?? 2;
foreach (var emojiData in emojiDataList)
{
if (!processedIds.Contains(emojiData.Id))
{
currentRow++;
worksheet.Cells[currentRow, idCol].Value = emojiData.Id;
worksheet.Cells[currentRow, nameKeyCol].Value = emojiData.NameKey;
worksheet.Cells[currentRow, initCol].Value = emojiData.Init;
if (emojiData.IconId < 0)
{
worksheet.Cells[currentRow, iconCol].Value = "";
}
else
{
worksheet.Cells[currentRow, iconCol].Value = emojiData.IconId;
}
}
}
package.Save();
}
// 提示成功并提醒推送
bool understood = EditorUtility.DisplayDialog("保存成功",
"配置已保存到Excel文件\n\n" +
"⚠️ 重要提醒:\n" +
"请及时在SourceTree或GitHubDesktop中\n" +
"1. 提交(Commit)本次修改\n" +
"2. 推送(Push)到远程仓库\n\n" +
"避免与其他策划产生冲突!",
"我知道了");
}
catch (Exception e)
{
EditorUtility.DisplayDialog("错误", $"保存数据失败: {e.Message}\n{e.StackTrace}", "确定");
}
}
/// <summary>
/// 表情数据类
/// </summary>
private class EmojiData
{
public int Id;
public string NameKey;
public int Init;
public int IconId;
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: a9ae11b153bc3454cab7be603efdb90b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,987 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using OfficeOpenXml;
using ArtResource;
using Debug = UnityEngine.Debug;
namespace DesignTools.Collections
{
/// <summary>
/// 头像配置Editor工具
/// 读取Docs/config/Face.xlsx关联Art_SO/HeadResources.asset
/// </summary>
public class HeadConfigEditor : EditorWindow
{
private const string HEAD_SO_PATH = "Assets/Art_SubModule/Art_SO";
private const string FACE_EXCEL_NAME = "Face.xlsx";
private const string LANGUAGE_EXCEL_NAME = "AllLanguage.xlsx";
private const string FACE_SHEET_NAME = "Face";
private const string LANGUAGE_SHEET_NAME = "client";
private const string DOCS_PATH_PREF_KEY = "HeadConfigEditor_DocsPath";
private const string DESIGN_SUBMODULE_PATH = "Assets/Design_SubModule";
private string docsRootPath = "";
private List<FaceData> faceDataList = new List<FaceData>();
private ArtTableSO headTableSO;
private Dictionary<string, Dictionary<string, string>> languageDict = new Dictionary<string, Dictionary<string, string>>();
private Vector2 scrollPosition;
private bool isDataLoaded = false;
private string pendingTooltipText = "";
[MenuItem("策划工具/收藏品/头像")]
public static void ShowWindow()
{
var window = GetWindow<HeadConfigEditor>("头像配置");
window.minSize = new Vector2(900, 600);
window.Show();
}
private void OnEnable()
{
// 设置EPPlus许可证
// ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
// 读取上次保存的路径
if (EditorPrefs.HasKey(DOCS_PATH_PREF_KEY))
{
docsRootPath = EditorPrefs.GetString(DOCS_PATH_PREF_KEY);
}
}
private void OnGUI()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
EditorGUILayout.LabelField("头像配置工具", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
// Docs路径选择
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("Docs项目根目录", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
docsRootPath = EditorGUILayout.TextField("路径", docsRootPath);
if (GUILayout.Button("选择文件夹", GUILayout.Width(100)))
{
string selectedPath = EditorUtility.OpenFolderPanel("选择Docs项目根目录", "", "");
if (!string.IsNullOrEmpty(selectedPath))
{
docsRootPath = selectedPath;
EditorPrefs.SetString(DOCS_PATH_PREF_KEY, docsRootPath);
}
}
EditorGUILayout.EndHorizontal();
if (GUILayout.Button("加载配置数据", GUILayout.Height(30)))
{
LoadData();
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(5);
// 数据编辑区域
if (isDataLoaded)
{
DrawDataEditor();
}
else
{
EditorGUILayout.HelpBox("请先选择Docs根目录并加载配置数据", MessageType.Info);
}
}
/// <summary>
/// 加载配置数据
/// </summary>
private void LoadData()
{
try
{
// 校验路径
if (string.IsNullOrEmpty(docsRootPath) || !Directory.Exists(docsRootPath))
{
EditorUtility.DisplayDialog("错误", "请选择有效的Docs根目录", "确定");
return;
}
// 1. 检查Docs是否为Git仓库并更新
if (!CheckAndUpdateDocsRepository())
{
return; // 检查失败,提示已在方法内显示
}
// 2. 检查Design_SubModule分支
if (!CheckAndSwitchDesignSubModuleBranch())
{
return; // 检查失败,提示已在方法内显示
}
// 校验Head SO是否存在
string[] soGuids = AssetDatabase.FindAssets("t:ArtTableSO", new[] { HEAD_SO_PATH });
if (soGuids.Length == 0)
{
EditorUtility.DisplayDialog("错误", $"未在 {HEAD_SO_PATH} 目录下找到头像资源配置ArtTableSO\n请先在美术资源配置工具中创建", "确定");
return;
}
// 查找名称为"HeadResource"的SO
ArtTableSO foundSO = null;
foreach (var guid in soGuids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
var so = AssetDatabase.LoadAssetAtPath<ArtTableSO>(path);
if (so != null && so.TableName == "HeadResource")
{
foundSO = so;
break;
}
}
if (foundSO == null)
{
EditorUtility.DisplayDialog("错误", "无法加载头像资源配置", "确定");
return;
}
headTableSO = foundSO;
if (headTableSO.Items == null || headTableSO.Items.Count == 0)
{
EditorUtility.DisplayDialog("错误", "头像资源配置数据为空", "确定");
return;
}
// 加载语言表
LoadLanguageData();
// 加载Face.xlsx
LoadFaceExcel();
isDataLoaded = true;
EditorUtility.DisplayDialog("成功", "配置数据加载成功!", "确定");
}
catch (Exception e)
{
EditorUtility.DisplayDialog("错误", $"加载数据失败: {e.Message}\n{e.StackTrace}", "确定");
isDataLoaded = false;
}
}
/// <summary>
/// 执行Git命令
/// </summary>
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}";
}
}
/// <summary>
/// 检查并更新Docs仓库
/// </summary>
private bool CheckAndUpdateDocsRepository()
{
// 检查是否是Git仓库
string gitPath = Path.Combine(docsRootPath, ".git");
if (!Directory.Exists(gitPath))
{
EditorUtility.DisplayDialog("错误",
$"Docs目录不是Git仓库\n路径: {docsRootPath}\n\n请确保Docs项目已正确克隆",
"确定");
return false;
}
EditorUtility.DisplayProgressBar("检查更新", "正在检查Docs仓库远程更新...", 0.3f);
// 执行git fetch检查远程更新
string fetchResult = ExecuteGitCommand(docsRootPath, "fetch", out bool fetchSuccess);
if (!fetchSuccess)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("Git Fetch失败",
$"无法检查远程更新:\n{fetchResult}\n\n请在SourceTree或GitHubDesktop中检查网络连接和仓库状态",
"确定");
return false;
}
EditorUtility.DisplayProgressBar("检查更新", "正在检查是否有新提交...", 0.6f);
// 检查本地和远程的差异
string statusResult = ExecuteGitCommand(docsRootPath, "status -uno", out bool statusSuccess);
if (!statusSuccess)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("Git Status失败",
$"无法获取仓库状态:\n{statusResult}\n\n请在SourceTree或GitHubDesktop中检查仓库状态",
"确定");
return false;
}
// 检查是否有未提交的更改
if (statusResult.Contains("Changes not staged") || statusResult.Contains("Changes to be committed"))
{
EditorUtility.ClearProgressBar();
bool proceed = EditorUtility.DisplayDialog("警告:有未提交的更改",
"Docs仓库中有未提交的更改这可能导致拉取时产生冲突。\n\n建议先提交或暂存这些更改。\n\n是否继续加载配置不推荐",
"继续(不推荐)", "取消,前往处理");
if (!proceed)
{
Debug.Log("请在SourceTree或GitHubDesktop中处理未提交的更改");
return false;
}
}
// 检查是否behind远程
if (statusResult.Contains("Your branch is behind"))
{
EditorUtility.DisplayProgressBar("更新中", "正在从远程拉取最新代码...", 0.8f);
string pullResult = ExecuteGitCommand(docsRootPath, "pull", out bool pullSuccess);
EditorUtility.ClearProgressBar();
if (!pullSuccess)
{
// 检查是否是合并冲突
if (pullResult.Contains("CONFLICT") || pullResult.Contains("conflict"))
{
EditorUtility.DisplayDialog("拉取失败:存在冲突",
$"拉取远程更新时发生冲突:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中解决冲突后再操作",
"确定");
}
else
{
EditorUtility.DisplayDialog("拉取失败",
$"无法拉取远程更新:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中检查并解决问题",
"确定");
}
return false;
}
Debug.Log($"Docs仓库已更新到最新版本:\n{pullResult}");
EditorUtility.DisplayDialog("更新成功", "Docs仓库已更新到最新版本", "确定");
}
else
{
EditorUtility.ClearProgressBar();
Debug.Log("Docs仓库已是最新版本");
}
return true;
}
/// <summary>
/// 检查并切换Design_SubModule到main分支
/// </summary>
private bool CheckAndSwitchDesignSubModuleBranch()
{
string designSubModulePath = Path.Combine(Application.dataPath, "..", DESIGN_SUBMODULE_PATH);
designSubModulePath = Path.GetFullPath(designSubModulePath);
// 检查Design_SubModule是否存在
if (!Directory.Exists(designSubModulePath))
{
EditorUtility.DisplayDialog("错误",
$"Design_SubModule目录不存在:\n{designSubModulePath}\n\n请确保子模块已正确初始化",
"确定");
return false;
}
EditorUtility.DisplayProgressBar("检查分支", "正在检查Design_SubModule分支...", 0.5f);
// 检查当前分支
string branchResult = ExecuteGitCommand(designSubModulePath, "branch --show-current", out bool branchSuccess);
if (!branchSuccess)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("Git Branch失败",
$"无法获取当前分支:\n{branchResult}\n\n请在SourceTree或GitHubDesktop中检查Design_SubModule状态",
"确定");
return false;
}
string currentBranch = branchResult.Trim();
if (currentBranch != "main")
{
EditorUtility.DisplayProgressBar("切换分支", "正在切换到main分支...", 0.8f);
string checkoutResult = ExecuteGitCommand(designSubModulePath, "checkout main", out bool checkoutSuccess);
EditorUtility.ClearProgressBar();
if (!checkoutSuccess)
{
EditorUtility.DisplayDialog("切换分支失败",
$"无法切换到main分支:\n{checkoutResult}\n\n当前分支: {currentBranch}\n\n请在SourceTree或GitHubDesktop中手动切换到main分支",
"确定");
return false;
}
Debug.Log($"Design_SubModule已从 {currentBranch} 切换到 main 分支");
EditorUtility.DisplayDialog("分支切换成功",
$"Design_SubModule已从 {currentBranch} 切换到 main 分支",
"确定");
}
else
{
EditorUtility.ClearProgressBar();
Debug.Log("Design_SubModule已在main分支");
}
return true;
}
/// <summary>
/// 加载语言数据
/// </summary>
private void LoadLanguageData()
{
languageDict.Clear();
string languageExcelPath = Path.Combine(docsRootPath, "config", LANGUAGE_EXCEL_NAME);
if (!File.Exists(languageExcelPath))
{
Debug.LogWarning($"未找到语言表: {languageExcelPath}");
return;
}
using (var package = new ExcelPackage(new FileInfo(languageExcelPath)))
{
var worksheet = package.Workbook.Worksheets[LANGUAGE_SHEET_NAME];
if (worksheet == null)
{
Debug.LogWarning($"语言表中未找到Sheet: {LANGUAGE_SHEET_NAME}");
return;
}
// 查找Key列和语言列
int keyColumnIndex = -1;
int zhCNColumnIndex = -1;
int enUSColumnIndex = -1;
int ptBRColumnIndex = -1;
int columnCount = worksheet.Dimension.Columns;
for (int col = 1; col <= columnCount; col++)
{
string header = worksheet.Cells[1, col].Text;
if (header == "key")
{
keyColumnIndex = col;
}
else if (header == "zh_CN")
{
zhCNColumnIndex = col;
}
else if (header == "en_US")
{
enUSColumnIndex = col;
}
else if (header == "pt_BR")
{
ptBRColumnIndex = col;
}
}
if (keyColumnIndex < 0)
{
Debug.LogWarning("语言表中未找到Key列");
return;
}
// 读取数据从第3行开始
int rowCount = worksheet.Dimension.Rows;
for (int row = 3; row <= rowCount; row++)
{
string key = worksheet.Cells[row, keyColumnIndex].Text;
if (string.IsNullOrEmpty(key)) continue;
if (!languageDict.ContainsKey(key))
{
languageDict[key] = new Dictionary<string, string>();
}
if (zhCNColumnIndex > 0)
{
languageDict[key]["zh_CN"] = worksheet.Cells[row, zhCNColumnIndex].Text;
}
if (enUSColumnIndex > 0)
{
languageDict[key]["en_US"] = worksheet.Cells[row, enUSColumnIndex].Text;
}
if (ptBRColumnIndex > 0)
{
languageDict[key]["pt_BR"] = worksheet.Cells[row, ptBRColumnIndex].Text;
}
}
}
}
/// <summary>
/// 加载Face.xlsx
/// </summary>
private void LoadFaceExcel()
{
faceDataList.Clear();
string faceExcelPath = Path.Combine(docsRootPath, "config", FACE_EXCEL_NAME);
if (!File.Exists(faceExcelPath))
{
throw new Exception($"未找到Face配置文件: {faceExcelPath}");
}
using (var package = new ExcelPackage(new FileInfo(faceExcelPath)))
{
var worksheet = package.Workbook.Worksheets[FACE_SHEET_NAME];
if (worksheet == null)
{
throw new Exception($"Face.xlsx中未找到Sheet: {FACE_SHEET_NAME}");
}
// 读取表头第1行
int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1;
int columnCount = worksheet.Dimension.Columns;
for (int col = 1; col <= columnCount; col++)
{
string header = worksheet.Cells[1, col].Text;
switch (header)
{
case "Id":
idCol = col;
break;
case "NameKey":
nameKeyCol = col;
break;
case "Init":
initCol = col;
break;
case "Icon":
iconCol = col;
break;
}
}
if (idCol < 0 || nameKeyCol < 0 || initCol < 0 || iconCol < 0)
{
throw new Exception("Face.xlsx表结构不正确缺少必要列Id/NameKey/Init/Icon");
}
// 读取数据从第3行开始
int rowCount = worksheet.Dimension.Rows;
for (int row = 3; row <= rowCount; row++)
{
string idText = worksheet.Cells[row, idCol].Text;
if (string.IsNullOrEmpty(idText)) continue;
if (!int.TryParse(idText, out int id)) continue;
string nameKey = worksheet.Cells[row, nameKeyCol].Text;
string initText = worksheet.Cells[row, initCol].Text;
string iconText = worksheet.Cells[row, iconCol].Text;
int init = 0;
if (!string.IsNullOrEmpty(initText))
{
int.TryParse(initText, out init);
}
int iconId = -1; // 默认为未选择
if (!string.IsNullOrEmpty(iconText))
{
if (!int.TryParse(iconText, out iconId))
{
iconId = -1; // 解析失败设为未选择
}
}
var faceData = new FaceData
{
Id = id,
NameKey = nameKey,
Init = init,
IconId = iconId
};
faceDataList.Add(faceData);
}
}
}
/// <summary>
/// 绘制数据编辑器
/// </summary>
private void DrawDataEditor()
{
pendingTooltipText = "";
EditorGUILayout.BeginVertical("box");
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"头像数据列表(共 {faceDataList.Count} 条)", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
GUI.backgroundColor = Color.cyan;
if (GUILayout.Button("+ 添加头像", GUILayout.Height(25), GUILayout.Width(100)))
{
AddNewFace();
}
GUI.backgroundColor = Color.white;
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(3);
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
// 表头
EditorGUILayout.BeginHorizontal("box");
EditorGUILayout.LabelField("Id", EditorStyles.boldLabel, GUILayout.Width(50));
EditorGUILayout.LabelField("NameKey", EditorStyles.boldLabel, GUILayout.Width(150));
EditorGUILayout.LabelField("中文名称", EditorStyles.boldLabel, GUILayout.Width(120));
EditorGUILayout.LabelField("Init", EditorStyles.boldLabel, GUILayout.Width(50));
EditorGUILayout.LabelField("Icon", EditorStyles.boldLabel, GUILayout.Width(200));
EditorGUILayout.LabelField("预览", EditorStyles.boldLabel, GUILayout.Width(100));
EditorGUILayout.LabelField("操作", EditorStyles.boldLabel, GUILayout.Width(60));
EditorGUILayout.EndHorizontal();
// 数据行
for (int i = 0; i < faceDataList.Count; i++)
{
DrawFaceDataRow(faceDataList[i]);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
EditorGUILayout.Space(10);
// 保存按钮
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
GUI.backgroundColor = Color.green;
if (GUILayout.Button("保存配置到Excel", GUILayout.Height(30), GUILayout.Width(200)))
{
SaveData();
}
GUI.backgroundColor = Color.white;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
// 在所有内容绘制完后绘制tooltip确保在最上层
if (!string.IsNullOrEmpty(pendingTooltipText))
{
Vector2 tooltipSize = GUI.skin.box.CalcSize(new GUIContent(pendingTooltipText));
tooltipSize.x += 10;
tooltipSize.y += 10;
// 使用当前鼠标位置,而不是之前记录的位置
Vector2 mousePos = Event.current.mousePosition;
Rect tooltipRect = new Rect(
mousePos.x + 1,
mousePos.y + 1,
tooltipSize.x,
tooltipSize.y
);
GUI.Box(tooltipRect, pendingTooltipText);
}
}
/// <summary>
/// 绘制单行数据
/// </summary>
private void DrawFaceDataRow(FaceData data)
{
EditorGUILayout.BeginHorizontal("box");
// Id只读
GUI.enabled = false;
EditorGUILayout.IntField(data.Id, GUILayout.Width(50));
GUI.enabled = true;
// NameKey可编辑
data.NameKey = EditorGUILayout.TextField(data.NameKey, GUILayout.Width(150));
// 中文名称(只读预览,带即时多语言显示)
GUI.enabled = false;
string zhName = "未找到语言Key";
if (languageDict.ContainsKey(data.NameKey))
{
var langs = languageDict[data.NameKey];
if (langs.ContainsKey("zh_CN"))
{
zhName = langs["zh_CN"];
}
}
Rect nameRect = GUILayoutUtility.GetRect(new GUIContent(zhName), GUI.skin.textField, GUILayout.Width(120));
EditorGUI.TextField(nameRect, zhName);
// 检测鼠标悬停并准备tooltip内容不立即绘制
if (nameRect.Contains(Event.current.mousePosition) && languageDict.ContainsKey(data.NameKey))
{
var langs = languageDict[data.NameKey];
List<string> tooltipLines = new List<string>();
if (langs.ContainsKey("zh_CN") && !string.IsNullOrEmpty(langs["zh_CN"]))
{
tooltipLines.Add($"[中文] {langs["zh_CN"]}");
}
if (langs.ContainsKey("en_US") && !string.IsNullOrEmpty(langs["en_US"]))
{
tooltipLines.Add($"[English] {langs["en_US"]}");
}
if (langs.ContainsKey("pt_BR") && !string.IsNullOrEmpty(langs["pt_BR"]))
{
tooltipLines.Add($"[Português] {langs["pt_BR"]}");
}
if (tooltipLines.Count > 0)
{
pendingTooltipText = string.Join("\n", tooltipLines);
Repaint();
}
}
GUI.enabled = true;
// InitCheckbox
bool initChecked = data.Init == 1;
bool newInitChecked = EditorGUILayout.Toggle(initChecked, GUILayout.Width(50));
data.Init = newInitChecked ? 1 : 0;
// Icon下拉列表包含未选择选项
var iconItems = headTableSO.Items;
var iconNamesList = new List<string> { "未选择" };
iconNamesList.AddRange(iconItems.Select(x => x.Name));
var iconNames = iconNamesList.ToArray();
// 查找当前选中的索引
int currentIndex = 0; // 默认为"未选择"
var currentItem = iconItems.Find(x => x.Id == data.IconId);
if (currentItem != null)
{
// 找到了对应的Item索引需要+1因为第0项是"未选择"
int itemIndex = iconItems.IndexOf(currentItem);
if (itemIndex >= 0)
{
currentIndex = itemIndex + 1;
}
}
EditorGUI.BeginChangeCheck();
int newIndex = EditorGUILayout.Popup(currentIndex, iconNames, GUILayout.Width(200));
if (EditorGUI.EndChangeCheck())
{
if (newIndex == 0)
{
// 选择了"未选择"
data.IconId = -1;
}
else if (newIndex > 0 && newIndex <= iconItems.Count)
{
// 选择了具体的Icon索引需要-1
data.IconId = iconItems[newIndex - 1].Id;
}
}
// 预览和Desc提示
if (data.IconId >= 0)
{
var item = iconItems.Find(x => x.Id == data.IconId);
if (item != null)
{
// 直接使用Sprite引用
Sprite sprite = item.Sprite;
if (sprite != null && sprite.texture != null)
{
// 正确处理图集sprite的预览
Rect previewRect = GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
// 绘制背景
EditorGUI.DrawRect(previewRect, new Color(0.5f, 0.5f, 0.5f, 1f));
Rect texCoords = sprite.textureRect;
Texture2D tex = sprite.texture;
// 归一化UV坐标
Rect normalizedCoords = new Rect(
texCoords.x / tex.width,
texCoords.y / tex.height,
texCoords.width / tex.width,
texCoords.height / tex.height
);
// 计算保持宽高比的显示区域
float aspect = texCoords.width / texCoords.height;
Rect drawRect = previewRect;
if (aspect > 1f)
{
float height = drawRect.width / aspect;
drawRect.y += (drawRect.height - height) * 0.5f;
drawRect.height = height;
}
else
{
float width = drawRect.height * aspect;
drawRect.x += (drawRect.width - width) * 0.5f;
drawRect.width = width;
}
GUI.DrawTextureWithTexCoords(drawRect, tex, normalizedCoords, true);
// 添加Tooltip
if (!string.IsNullOrEmpty(item.Desc))
{
GUI.Label(previewRect, new GUIContent("", item.Desc));
}
}
else
{
GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
GUILayout.Space(-50);
EditorGUILayout.LabelField("无图片", GUILayout.Width(50));
}
}
else
{
// ID存在但找不到对应的Item
GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
GUILayout.Space(-50);
EditorGUILayout.LabelField("未选择", GUILayout.Width(50));
}
}
else
{
// IconId < 0未选择状态
GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
GUILayout.Space(-50);
EditorGUILayout.LabelField("未选择", GUILayout.Width(50));
}
// 删除按钮
GUI.backgroundColor = Color.red;
if (GUILayout.Button("删除", GUILayout.Width(60)))
{
if (EditorUtility.DisplayDialog("确认删除",
$"确定要删除头像 {data.Id} ({data.NameKey}) 吗?",
"删除", "取消"))
{
faceDataList.Remove(data);
}
}
GUI.backgroundColor = Color.white;
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 添加新头像
/// </summary>
private void AddNewFace()
{
// 计算新ID当前最大ID + 1
int newId = 1;
if (faceDataList.Count > 0)
{
newId = faceDataList.Max(x => x.Id) + 1;
}
var newFace = new FaceData
{
Id = newId,
NameKey = "",
Init = 0,
IconId = -1
};
faceDataList.Add(newFace);
// 滚动到底部
scrollPosition = new Vector2(0, float.MaxValue);
}
/// <summary>
/// 保存数据到Excel
/// </summary>
private void SaveData()
{
try
{
string faceExcelPath = Path.Combine(docsRootPath, "config", FACE_EXCEL_NAME);
if (!File.Exists(faceExcelPath))
{
EditorUtility.DisplayDialog("错误", $"未找到Face配置文件: {faceExcelPath}", "确定");
return;
}
using (var package = new ExcelPackage(new FileInfo(faceExcelPath)))
{
var worksheet = package.Workbook.Worksheets[FACE_SHEET_NAME];
if (worksheet == null)
{
EditorUtility.DisplayDialog("错误", $"Face.xlsx中未找到Sheet: {FACE_SHEET_NAME}", "确定");
return;
}
// 查找列索引
int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1;
int columnCount = worksheet.Dimension.Columns;
for (int col = 1; col <= columnCount; col++)
{
string header = worksheet.Cells[1, col].Text;
switch (header)
{
case "Id":
idCol = col;
break;
case "NameKey":
nameKeyCol = col;
break;
case "Init":
initCol = col;
break;
case "Icon":
iconCol = col;
break;
}
}
// 更新和删除数据从第3行开始
int rowCount = worksheet.Dimension.Rows;
var processedIds = new HashSet<int>();
// 第一遍:更新现有行或标记删除
for (int row = rowCount; row >= 3; row--)
{
string idText = worksheet.Cells[row, idCol].Text;
if (string.IsNullOrEmpty(idText)) continue;
if (!int.TryParse(idText, out int id)) continue;
// 查找对应的FaceData
var faceData = faceDataList.Find(x => x.Id == id);
if (faceData != null)
{
// 更新数据
worksheet.Cells[row, nameKeyCol].Value = faceData.NameKey;
worksheet.Cells[row, initCol].Value = faceData.Init;
// IconId为-1时写入空字符串否则写入实际值
if (faceData.IconId < 0)
{
worksheet.Cells[row, iconCol].Value = "";
}
else
{
worksheet.Cells[row, iconCol].Value = faceData.IconId;
}
processedIds.Add(id);
}
else
{
// 删除行
worksheet.DeleteRow(row);
}
}
// 第二遍:添加新行
int currentRow = worksheet.Dimension?.Rows ?? 2;
foreach (var faceData in faceDataList)
{
if (!processedIds.Contains(faceData.Id))
{
// 这是新增的数据
currentRow++;
worksheet.Cells[currentRow, idCol].Value = faceData.Id;
worksheet.Cells[currentRow, nameKeyCol].Value = faceData.NameKey;
worksheet.Cells[currentRow, initCol].Value = faceData.Init;
if (faceData.IconId < 0)
{
worksheet.Cells[currentRow, iconCol].Value = "";
}
else
{
worksheet.Cells[currentRow, iconCol].Value = faceData.IconId;
}
}
}
// 保存文件
package.Save();
}
// 提示成功并提醒推送
bool understood = EditorUtility.DisplayDialog("保存成功",
"配置已保存到Excel文件\n\n" +
"⚠️ 重要提醒:\n" +
"请及时在SourceTree或GitHubDesktop中\n" +
"1. 提交(Commit)本次修改\n" +
"2. 推送(Push)到远程仓库\n\n" +
"避免与其他策划产生冲突!",
"我知道了");
}
catch (Exception e)
{
EditorUtility.DisplayDialog("错误", $"保存数据失败: {e.Message}\n{e.StackTrace}", "确定");
}
}
/// <summary>
/// 头像数据类
/// </summary>
private class FaceData
{
public int Id;
public string NameKey;
public int Init;
public int IconId;
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: b3c6d1239cdbf0748b5fb75f163c3121
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,965 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using OfficeOpenXml;
using ArtResource;
using Debug = UnityEngine.Debug;
namespace DesignTools.Collections
{
/// <summary>
/// 头像框配置Editor工具
/// 读取Docs/config/Avatar.xlsx关联Art_SO/Collections/HeadFrameResource.asset
/// </summary>
public class HeadFrameConfigEditor : EditorWindow
{
private const string HEADFRAME_SO_PATH = "Assets/Art_SubModule/Art_SO/Collections";
private const string HEADFRAME_SO_NAME = "HeadFrameResource";
private const string AVATAR_EXCEL_NAME = "Avatar.xlsx";
private const string LANGUAGE_EXCEL_NAME = "AllLanguage.xlsx";
private const string AVATAR_SHEET_NAME = "Avatar";
private const string LANGUAGE_SHEET_NAME = "client";
private const string DOCS_PATH_PREF_KEY = "HeadFrameConfigEditor_DocsPath";
private const string DESIGN_SUBMODULE_PATH = "Assets/Design_SubModule";
private string docsRootPath = "";
private List<HeadFrameData> headFrameDataList = new List<HeadFrameData>();
private ArtTableSO headFrameTableSO;
private Dictionary<string, Dictionary<string, string>> languageDict = new Dictionary<string, Dictionary<string, string>>();
private Vector2 scrollPosition;
private bool isDataLoaded = false;
private string pendingTooltipText = "";
[MenuItem("策划工具/收藏品/头像框")]
public static void ShowWindow()
{
var window = GetWindow<HeadFrameConfigEditor>("头像框配置");
window.minSize = new Vector2(1000, 600);
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("头像框配置工具", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
// Docs路径选择
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("Docs项目根目录", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
docsRootPath = EditorGUILayout.TextField("路径", docsRootPath);
if (GUILayout.Button("选择文件夹", GUILayout.Width(100)))
{
string selectedPath = EditorUtility.OpenFolderPanel("选择Docs项目根目录", "", "");
if (!string.IsNullOrEmpty(selectedPath))
{
docsRootPath = selectedPath;
EditorPrefs.SetString(DOCS_PATH_PREF_KEY, docsRootPath);
}
}
EditorGUILayout.EndHorizontal();
if (GUILayout.Button("加载配置数据", GUILayout.Height(30)))
{
LoadData();
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(5);
// 数据编辑区域
if (isDataLoaded)
{
DrawDataEditor();
}
else
{
EditorGUILayout.HelpBox("请先选择Docs根目录并加载配置数据", MessageType.Info);
}
}
/// <summary>
/// 加载配置数据
/// </summary>
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;
}
// 校验HeadFrame SO是否存在
string headFrameSOPath = Path.Combine(HEADFRAME_SO_PATH, $"{HEADFRAME_SO_NAME}.asset");
headFrameTableSO = AssetDatabase.LoadAssetAtPath<ArtTableSO>(headFrameSOPath);
if (headFrameTableSO == null)
{
EditorUtility.DisplayDialog("错误",
$"未找到头像框资源配置\n路径: {headFrameSOPath}\n\n请先在美术资源配置工具中创建",
"确定");
return;
}
if (headFrameTableSO.Items == null || headFrameTableSO.Items.Count == 0)
{
EditorUtility.DisplayDialog("错误", "头像框资源配置数据为空", "确定");
return;
}
// 加载语言表
LoadLanguageData();
// 加载Avatar.xlsx
LoadAvatarExcel();
isDataLoaded = true;
EditorUtility.DisplayDialog("成功", "配置数据加载成功!", "确定");
}
catch (Exception e)
{
EditorUtility.DisplayDialog("错误", $"加载数据失败: {e.Message}\n{e.StackTrace}", "确定");
isDataLoaded = false;
}
}
/// <summary>
/// 执行Git命令
/// </summary>
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}";
}
}
/// <summary>
/// 检查并更新Docs仓库
/// </summary>
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;
}
/// <summary>
/// 检查并切换Design_SubModule到main分支
/// </summary>
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 分支");
EditorUtility.DisplayDialog("分支切换成功",
$"Design_SubModule已从 {currentBranch} 切换到 main 分支",
"确定");
}
else
{
EditorUtility.ClearProgressBar();
Debug.Log("Design_SubModule已在main分支");
}
return true;
}
/// <summary>
/// 加载语言数据
/// </summary>
private void LoadLanguageData()
{
languageDict.Clear();
string languageExcelPath = Path.Combine(docsRootPath, "config", LANGUAGE_EXCEL_NAME);
if (!File.Exists(languageExcelPath))
{
Debug.LogWarning($"未找到语言表: {languageExcelPath}");
return;
}
using (var package = new ExcelPackage(new FileInfo(languageExcelPath)))
{
var worksheet = package.Workbook.Worksheets[LANGUAGE_SHEET_NAME];
if (worksheet == null)
{
Debug.LogWarning($"语言表中未找到Sheet: {LANGUAGE_SHEET_NAME}");
return;
}
// 查找Key列和语言列
int keyColumnIndex = -1;
int zhCNColumnIndex = -1;
int enUSColumnIndex = -1;
int ptBRColumnIndex = -1;
int columnCount = worksheet.Dimension.Columns;
for (int col = 1; col <= columnCount; col++)
{
string header = worksheet.Cells[1, col].Text;
if (header == "key")
{
keyColumnIndex = col;
}
else if (header == "zh_CN")
{
zhCNColumnIndex = col;
}
else if (header == "en_US")
{
enUSColumnIndex = col;
}
else if (header == "pt_BR")
{
ptBRColumnIndex = col;
}
}
if (keyColumnIndex < 0)
{
Debug.LogWarning("语言表中未找到Key列");
return;
}
// 读取数据从第3行开始
int rowCount = worksheet.Dimension.Rows;
for (int row = 3; row <= rowCount; row++)
{
string key = worksheet.Cells[row, keyColumnIndex].Text;
if (string.IsNullOrEmpty(key)) continue;
if (!languageDict.ContainsKey(key))
{
languageDict[key] = new Dictionary<string, string>();
}
if (zhCNColumnIndex > 0)
{
languageDict[key]["zh_CN"] = worksheet.Cells[row, zhCNColumnIndex].Text;
}
if (enUSColumnIndex > 0)
{
languageDict[key]["en_US"] = worksheet.Cells[row, enUSColumnIndex].Text;
}
if (ptBRColumnIndex > 0)
{
languageDict[key]["pt_BR"] = worksheet.Cells[row, ptBRColumnIndex].Text;
}
}
}
}
/// <summary>
/// 加载Avatar.xlsx
/// </summary>
private void LoadAvatarExcel()
{
headFrameDataList.Clear();
string avatarExcelPath = Path.Combine(docsRootPath, "config", AVATAR_EXCEL_NAME);
if (!File.Exists(avatarExcelPath))
{
throw new Exception($"未找到Avatar配置文件: {avatarExcelPath}");
}
using (var package = new ExcelPackage(new FileInfo(avatarExcelPath)))
{
var worksheet = package.Workbook.Worksheets[AVATAR_SHEET_NAME];
if (worksheet == null)
{
throw new Exception($"Avatar.xlsx中未找到Sheet: {AVATAR_SHEET_NAME}");
}
// 读取表头第1行
int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1, frameImageScaleCol = -1;
int columnCount = worksheet.Dimension.Columns;
for (int col = 1; col <= columnCount; col++)
{
string header = worksheet.Cells[1, col].Text;
switch (header)
{
case "Id":
idCol = col;
break;
case "NameKey":
nameKeyCol = col;
break;
case "Init":
initCol = col;
break;
case "Icon":
iconCol = col;
break;
case "FrameImageScale":
frameImageScaleCol = col;
break;
}
}
if (idCol < 0 || nameKeyCol < 0 || initCol < 0 || iconCol < 0 || frameImageScaleCol < 0)
{
throw new Exception("Avatar.xlsx表结构不正确缺少必要列Id/NameKey/Init/Icon/FrameImageScale");
}
// 读取数据从第3行开始
int rowCount = worksheet.Dimension.Rows;
for (int row = 3; row <= rowCount; row++)
{
string idText = worksheet.Cells[row, idCol].Text;
if (string.IsNullOrEmpty(idText)) continue;
if (!int.TryParse(idText, out int id)) continue;
string nameKey = worksheet.Cells[row, nameKeyCol].Text;
string initText = worksheet.Cells[row, initCol].Text;
string iconText = worksheet.Cells[row, iconCol].Text;
string frameImageScaleText = worksheet.Cells[row, frameImageScaleCol].Text;
int init = 0;
if (!string.IsNullOrEmpty(initText))
{
int.TryParse(initText, out init);
}
int iconId = -1;
if (!string.IsNullOrEmpty(iconText))
{
if (!int.TryParse(iconText, out iconId))
{
iconId = -1;
}
}
float frameImageScale = 1.0f;
if (!string.IsNullOrEmpty(frameImageScaleText))
{
if (!float.TryParse(frameImageScaleText, out frameImageScale))
{
frameImageScale = 1.0f;
}
}
var frameData = new HeadFrameData
{
Id = id,
NameKey = nameKey,
Init = init,
IconId = iconId,
FrameImageScale = frameImageScale
};
headFrameDataList.Add(frameData);
}
}
}
/// <summary>
/// 绘制数据编辑器
/// </summary>
private void DrawDataEditor()
{
pendingTooltipText = "";
EditorGUILayout.BeginVertical("box");
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"头像框数据列表(共 {headFrameDataList.Count} 条)", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
GUI.backgroundColor = Color.cyan;
if (GUILayout.Button("+ 添加头像框", GUILayout.Height(25), GUILayout.Width(100)))
{
AddNewHeadFrame();
}
GUI.backgroundColor = Color.white;
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(3);
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
// 表头
EditorGUILayout.BeginHorizontal("box");
EditorGUILayout.LabelField("Id", EditorStyles.boldLabel, GUILayout.Width(50));
EditorGUILayout.LabelField("NameKey", EditorStyles.boldLabel, GUILayout.Width(150));
EditorGUILayout.LabelField("中文名称", EditorStyles.boldLabel, GUILayout.Width(120));
EditorGUILayout.LabelField("Init", EditorStyles.boldLabel, GUILayout.Width(50));
EditorGUILayout.LabelField("Icon", EditorStyles.boldLabel, GUILayout.Width(200));
EditorGUILayout.LabelField("预览", EditorStyles.boldLabel, GUILayout.Width(100));
EditorGUILayout.LabelField("缩放", EditorStyles.boldLabel, GUILayout.Width(80));
EditorGUILayout.LabelField("操作", EditorStyles.boldLabel, GUILayout.Width(60));
EditorGUILayout.EndHorizontal();
// 数据行
for (int i = 0; i < headFrameDataList.Count; i++)
{
DrawHeadFrameDataRow(headFrameDataList[i]);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
EditorGUILayout.Space(10);
// 保存按钮
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
GUI.backgroundColor = Color.green;
if (GUILayout.Button("保存配置到Excel", GUILayout.Height(30), GUILayout.Width(200)))
{
SaveData();
}
GUI.backgroundColor = Color.white;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
// 在所有内容绘制完后绘制tooltip确保在最上层
if (!string.IsNullOrEmpty(pendingTooltipText))
{
Vector2 tooltipSize = GUI.skin.box.CalcSize(new GUIContent(pendingTooltipText));
tooltipSize.x += 10;
tooltipSize.y += 10;
// 使用当前鼠标位置,而不是之前记录的位置
Vector2 mousePos = Event.current.mousePosition;
Rect tooltipRect = new Rect(
mousePos.x + 1,
mousePos.y + 1,
tooltipSize.x,
tooltipSize.y
);
GUI.Box(tooltipRect, pendingTooltipText);
}
}
/// <summary>
/// 绘制单行数据
/// </summary>
private void DrawHeadFrameDataRow(HeadFrameData data)
{
EditorGUILayout.BeginHorizontal("box");
// Id可编辑
data.Id = EditorGUILayout.IntField(data.Id, GUILayout.Width(50));
// NameKey可编辑
data.NameKey = EditorGUILayout.TextField(data.NameKey, GUILayout.Width(150));
// 中文名称(只读预览,带即时多语言显示)
GUI.enabled = false;
string zhName = "未找到语言Key";
if (languageDict.ContainsKey(data.NameKey))
{
var langs = languageDict[data.NameKey];
if (langs.ContainsKey("zh_CN"))
{
zhName = langs["zh_CN"];
}
}
Rect nameRect = GUILayoutUtility.GetRect(new GUIContent(zhName), GUI.skin.textField, GUILayout.Width(120));
EditorGUI.TextField(nameRect, zhName);
// 检测鼠标悬停并准备tooltip内容不立即绘制
if (nameRect.Contains(Event.current.mousePosition) && languageDict.ContainsKey(data.NameKey))
{
var langs = languageDict[data.NameKey];
List<string> tooltipLines = new List<string>();
if (langs.ContainsKey("zh_CN") && !string.IsNullOrEmpty(langs["zh_CN"]))
{
tooltipLines.Add($"[中文] {langs["zh_CN"]}");
}
if (langs.ContainsKey("en_US") && !string.IsNullOrEmpty(langs["en_US"]))
{
tooltipLines.Add($"[English] {langs["en_US"]}");
}
if (langs.ContainsKey("pt_BR") && !string.IsNullOrEmpty(langs["pt_BR"]))
{
tooltipLines.Add($"[Português] {langs["pt_BR"]}");
}
if (tooltipLines.Count > 0)
{
pendingTooltipText = string.Join("\n", tooltipLines);
Repaint();
}
}
GUI.enabled = true;
// InitCheckbox
bool initChecked = data.Init == 1;
bool newInitChecked = EditorGUILayout.Toggle(initChecked, GUILayout.Width(50));
data.Init = newInitChecked ? 1 : 0;
// Icon下拉列表
var iconItems = headFrameTableSO.Items;
var iconNamesList = new List<string> { "未选择" };
iconNamesList.AddRange(iconItems.Select(x => x.Name));
var iconNames = iconNamesList.ToArray();
int currentIndex = 0;
var currentItem = iconItems.Find(x => x.Id == data.IconId);
if (currentItem != null)
{
int itemIndex = iconItems.IndexOf(currentItem);
if (itemIndex >= 0)
{
currentIndex = itemIndex + 1;
}
}
EditorGUI.BeginChangeCheck();
int newIndex = EditorGUILayout.Popup(currentIndex, iconNames, GUILayout.Width(200));
if (EditorGUI.EndChangeCheck())
{
if (newIndex == 0)
{
data.IconId = -1;
}
else if (newIndex > 0 && newIndex <= iconItems.Count)
{
data.IconId = iconItems[newIndex - 1].Id;
}
}
// 预览
if (data.IconId >= 0)
{
var item = iconItems.Find(x => x.Id == data.IconId);
if (item != null)
{
Sprite sprite = item.Sprite;
if (sprite != null && sprite.texture != null)
{
Rect previewRect = GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
EditorGUI.DrawRect(previewRect, new Color(0.5f, 0.5f, 0.5f, 1f));
Rect texCoords = sprite.textureRect;
Texture2D tex = sprite.texture;
Rect normalizedCoords = new Rect(
texCoords.x / tex.width,
texCoords.y / tex.height,
texCoords.width / tex.width,
texCoords.height / tex.height
);
float aspect = texCoords.width / texCoords.height;
Rect drawRect = previewRect;
if (aspect > 1f)
{
float height = drawRect.width / aspect;
drawRect.y += (drawRect.height - height) * 0.5f;
drawRect.height = height;
}
else
{
float width = drawRect.height * aspect;
drawRect.x += (drawRect.width - width) * 0.5f;
drawRect.width = width;
}
GUI.DrawTextureWithTexCoords(drawRect, tex, normalizedCoords, true);
if (!string.IsNullOrEmpty(item.Desc))
{
GUI.Label(previewRect, new GUIContent("", item.Desc));
}
}
else
{
GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
GUILayout.Space(-50);
EditorGUILayout.LabelField("无图片", GUILayout.Width(50));
}
}
else
{
GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
GUILayout.Space(-50);
EditorGUILayout.LabelField("未选择", GUILayout.Width(50));
}
}
else
{
GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
GUILayout.Space(-50);
EditorGUILayout.LabelField("未选择", GUILayout.Width(50));
}
// FrameImageScalefloat输入框
data.FrameImageScale = EditorGUILayout.FloatField(data.FrameImageScale, GUILayout.Width(80));
// 删除按钮
GUI.backgroundColor = Color.red;
if (GUILayout.Button("删除", GUILayout.Width(60)))
{
if (EditorUtility.DisplayDialog("确认删除",
$"确定要删除头像框 {data.Id} ({data.NameKey}) 吗?",
"删除", "取消"))
{
headFrameDataList.Remove(data);
}
}
GUI.backgroundColor = Color.white;
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 添加新头像框
/// </summary>
private void AddNewHeadFrame()
{
int newId = 1;
if (headFrameDataList.Count > 0)
{
newId = headFrameDataList.Max(x => x.Id) + 1;
}
var newFrame = new HeadFrameData
{
Id = newId,
NameKey = "",
Init = 0,
IconId = -1,
FrameImageScale = 1.0f
};
headFrameDataList.Add(newFrame);
scrollPosition = new Vector2(0, float.MaxValue);
}
/// <summary>
/// 保存数据到Excel
/// </summary>
private void SaveData()
{
try
{
string avatarExcelPath = Path.Combine(docsRootPath, "config", AVATAR_EXCEL_NAME);
if (!File.Exists(avatarExcelPath))
{
EditorUtility.DisplayDialog("错误", $"未找到Avatar配置文件: {avatarExcelPath}", "确定");
return;
}
using (var package = new ExcelPackage(new FileInfo(avatarExcelPath)))
{
var worksheet = package.Workbook.Worksheets[AVATAR_SHEET_NAME];
if (worksheet == null)
{
EditorUtility.DisplayDialog("错误", $"Avatar.xlsx中未找到Sheet: {AVATAR_SHEET_NAME}", "确定");
return;
}
// 查找列索引
int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1, frameImageScaleCol = -1;
int columnCount = worksheet.Dimension.Columns;
for (int col = 1; col <= columnCount; col++)
{
string header = worksheet.Cells[1, col].Text;
switch (header)
{
case "Id":
idCol = col;
break;
case "NameKey":
nameKeyCol = col;
break;
case "Init":
initCol = col;
break;
case "Icon":
iconCol = col;
break;
case "FrameImageScale":
frameImageScaleCol = col;
break;
}
}
// 更新和删除数据从第3行开始
int rowCount = worksheet.Dimension.Rows;
var processedIds = new HashSet<int>();
// 第一遍:更新现有行或删除
for (int row = rowCount; row >= 3; row--)
{
string idText = worksheet.Cells[row, idCol].Text;
if (string.IsNullOrEmpty(idText)) continue;
if (!int.TryParse(idText, out int id)) continue;
var frameData = headFrameDataList.Find(x => x.Id == id);
if (frameData != null)
{
worksheet.Cells[row, nameKeyCol].Value = frameData.NameKey;
worksheet.Cells[row, initCol].Value = frameData.Init;
if (frameData.IconId < 0)
{
worksheet.Cells[row, iconCol].Value = "";
}
else
{
worksheet.Cells[row, iconCol].Value = frameData.IconId;
}
worksheet.Cells[row, frameImageScaleCol].Value = frameData.FrameImageScale;
processedIds.Add(id);
}
else
{
worksheet.DeleteRow(row);
}
}
// 第二遍:添加新行
int currentRow = worksheet.Dimension?.Rows ?? 2;
foreach (var frameData in headFrameDataList)
{
if (!processedIds.Contains(frameData.Id))
{
currentRow++;
worksheet.Cells[currentRow, idCol].Value = frameData.Id;
worksheet.Cells[currentRow, nameKeyCol].Value = frameData.NameKey;
worksheet.Cells[currentRow, initCol].Value = frameData.Init;
if (frameData.IconId < 0)
{
worksheet.Cells[currentRow, iconCol].Value = "";
}
else
{
worksheet.Cells[currentRow, iconCol].Value = frameData.IconId;
}
worksheet.Cells[currentRow, frameImageScaleCol].Value = frameData.FrameImageScale;
}
}
package.Save();
}
// 提示成功并提醒推送
bool understood = EditorUtility.DisplayDialog("保存成功",
"配置已保存到Excel文件\n\n" +
"⚠️ 重要提醒:\n" +
"请及时在SourceTree或GitHubDesktop中\n" +
"1. 提交(Commit)本次修改\n" +
"2. 推送(Push)到远程仓库\n\n" +
"避免与其他策划产生冲突!",
"我知道了");
}
catch (Exception e)
{
EditorUtility.DisplayDialog("错误", $"保存数据失败: {e.Message}\n{e.StackTrace}", "确定");
}
}
/// <summary>
/// 头像框数据类
/// </summary>
private class HeadFrameData
{
public int Id;
public string NameKey;
public int Init;
public int IconId;
public float FrameImageScale;
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 370961b2539ed9d49ade3ec27aedd25e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 2cbfd5e054af7604e837e9ececb2a15c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 0f94b023b059a5349a6ecd1811ad9334
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 15c94f7123a92774084366377ef07164
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: