1795 lines
67 KiB
C#
1795 lines
67 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// Item配置Editor工具
|
||
/// 读取Docs/config/Item.xlsx,关联所有Art_SO资源
|
||
/// </summary>
|
||
public class ItemConfigEditor : EditorWindow
|
||
{
|
||
private const string ART_SO_PATH = "Assets/Art_SubModule/Art_SO";
|
||
private const string ITEM_EXCEL_NAME = "Item.xlsx";
|
||
private const string LIMITED_TIME_EVENT_EXCEL_NAME = "LimitedTimeEvent.xlsx";
|
||
private const string AVATAR_EXCEL_NAME = "Avatar.xlsx";
|
||
private const string FACE_EXCEL_NAME = "Face.xlsx";
|
||
private const string EMOJI_EXCEL_NAME = "Emoji.xlsx";
|
||
private const string LANGUAGE_EXCEL_NAME = "AllLanguage.xlsx";
|
||
private const string ITEM_SHEET_NAME = "Item";
|
||
private const string EVENT_SHEET_NAME = "Event";
|
||
private const string AVATAR_SHEET_NAME = "Avatar";
|
||
private const string FACE_SHEET_NAME = "Face";
|
||
private const string EMOJI_SHEET_NAME = "Emoji";
|
||
private const string LANGUAGE_SHEET_NAME = "client";
|
||
private const string DOCS_PATH_PREF_KEY = "ItemConfigEditor_DocsPath";
|
||
private const string DESIGN_SUBMODULE_PATH = "Assets/Design_SubModule";
|
||
|
||
// IType类型定义
|
||
private static readonly Dictionary<int, string> ITEM_TYPES = new Dictionary<int, string>
|
||
{
|
||
{ 1, "能量" },
|
||
{ 2, "星星" },
|
||
{ 3, "钻石" },
|
||
{ 97, "Playroom宠物道具" },
|
||
{ 98, "卡牌" },
|
||
{ 99, "背包道具" },
|
||
{ 100, "棋子" },
|
||
{ 101, "卡包" },
|
||
{ 102, "限时事件" },
|
||
{ 103, "小猪存钱罐" },
|
||
{ 104, "万能卡" },
|
||
{ 105, "头像框" },
|
||
{ 106, "活动代币" },
|
||
{ 107, "竞赛游戏代币" },
|
||
{ 108, "Pet Playroom拜访道具" },
|
||
{ 109, "表情" },
|
||
{ 110, "头像" },
|
||
{ 111, "Playroom装饰" },
|
||
{ 112, "Playroom服装" },
|
||
{ 113, "Playroom装饰套装" },
|
||
{ 114, "Playroom服装套装" },
|
||
{ 115, "Playroom道具宝箱" },
|
||
{ 116, "活动通行证代币道具" }
|
||
};
|
||
|
||
private string docsRootPath = "";
|
||
private List<ItemData> allItemDataList = new List<ItemData>();
|
||
private List<ItemData> filteredItemDataList = new List<ItemData>();
|
||
private Dictionary<int, string> limitedTimeEventDict = new Dictionary<int, string>(); // Id -> Name
|
||
private Dictionary<int, AvatarData> avatarDict = new Dictionary<int, AvatarData>(); // Id -> AvatarData
|
||
private Dictionary<int, FaceData> faceDict = new Dictionary<int, FaceData>(); // Id -> FaceData
|
||
private Dictionary<int, EmojiData> emojiDict = new Dictionary<int, EmojiData>(); // Id -> EmojiData
|
||
private Dictionary<string, Dictionary<string, string>> languageDict = new Dictionary<string, Dictionary<string, string>>();
|
||
private List<ArtTableSO> allArtTables = new List<ArtTableSO>();
|
||
private Dictionary<string, List<ArtTableSO>> artTablesByFolder = new Dictionary<string, List<ArtTableSO>>(); // 按文件夹分组的表格
|
||
|
||
private ArtTableSO headFrameResourceSO;
|
||
private ArtTableSO headResourceSO;
|
||
private ArtTableSO emojiResourceSO;
|
||
|
||
// Res选择状态缓存 (ItemData的Id -> 选择的组名)
|
||
private Dictionary<int, string> resSelectedFolderCache = new Dictionary<int, string>();
|
||
|
||
private Vector2 scrollPosition;
|
||
private bool isDataLoaded = false;
|
||
|
||
// 筛选和分页
|
||
private int selectedIType = -1;
|
||
private int currentPage = 0;
|
||
private int itemsPerPage = 20;
|
||
private int totalPages = 0;
|
||
|
||
[MenuItem("策划工具/Item配置")]
|
||
public static void ShowWindow()
|
||
{
|
||
var window = GetWindow<ItemConfigEditor>("Item配置");
|
||
window.minSize = new Vector2(1200, 700);
|
||
window.Show();
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
// 读取上次保存的路径
|
||
if (EditorPrefs.HasKey(DOCS_PATH_PREF_KEY))
|
||
{
|
||
docsRootPath = EditorPrefs.GetString(DOCS_PATH_PREF_KEY);
|
||
}
|
||
}
|
||
|
||
private void OnGUI()
|
||
{
|
||
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
|
||
EditorGUILayout.LabelField("Item配置工具", EditorStyles.boldLabel);
|
||
GUILayout.FlexibleSpace();
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
EditorGUILayout.Space(5);
|
||
|
||
// Docs路径选择
|
||
EditorGUILayout.BeginVertical("box");
|
||
EditorGUILayout.LabelField("Docs项目根目录", EditorStyles.boldLabel);
|
||
|
||
EditorGUILayout.BeginHorizontal();
|
||
docsRootPath = EditorGUILayout.TextField("路径", docsRootPath);
|
||
if (GUILayout.Button("选择文件夹", GUILayout.Width(100)))
|
||
{
|
||
string selectedPath = EditorUtility.OpenFolderPanel("选择Docs项目根目录", "", "");
|
||
if (!string.IsNullOrEmpty(selectedPath))
|
||
{
|
||
docsRootPath = selectedPath;
|
||
EditorPrefs.SetString(DOCS_PATH_PREF_KEY, docsRootPath);
|
||
}
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
if (GUILayout.Button("加载配置数据", GUILayout.Height(30)))
|
||
{
|
||
LoadData();
|
||
}
|
||
|
||
EditorGUILayout.EndVertical();
|
||
|
||
EditorGUILayout.Space(5);
|
||
|
||
// 数据编辑区域
|
||
if (isDataLoaded)
|
||
{
|
||
DrawFilterAndDataEditor();
|
||
}
|
||
else
|
||
{
|
||
EditorGUILayout.HelpBox("请先选择Docs根目录并加载配置数据", MessageType.Info);
|
||
}
|
||
}
|
||
|
||
/// <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;
|
||
}
|
||
|
||
// 3. 加载所有Art_SO资源
|
||
LoadAllArtTableSO();
|
||
|
||
// 4. 加载语言表
|
||
LoadLanguageData();
|
||
|
||
// 5. 加载LimitedTimeEvent表
|
||
LoadLimitedTimeEventData();
|
||
|
||
// 6. 加载Avatar表
|
||
LoadAvatarData();
|
||
|
||
// 7. 加载Face表
|
||
LoadFaceData();
|
||
|
||
// 8. 加载Emoji表
|
||
LoadEmojiData();
|
||
|
||
// 9. 加载Item表
|
||
LoadItemExcel();
|
||
|
||
isDataLoaded = true;
|
||
EditorUtility.DisplayDialog("成功", "配置数据加载成功!", "确定");
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
EditorUtility.DisplayDialog("错误", $"加载数据失败: {e.Message}\n{e.StackTrace}", "确定");
|
||
isDataLoaded = false;
|
||
}
|
||
}
|
||
|
||
/// <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 分支");
|
||
}
|
||
else
|
||
{
|
||
EditorUtility.ClearProgressBar();
|
||
Debug.Log("Design_SubModule已在main分支");
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 加载所有Art_SO资源
|
||
/// </summary>
|
||
private void LoadAllArtTableSO()
|
||
{
|
||
allArtTables.Clear();
|
||
artTablesByFolder.Clear();
|
||
|
||
string[] guids = AssetDatabase.FindAssets("t:ArtTableSO", new[] { ART_SO_PATH });
|
||
foreach (string guid in guids)
|
||
{
|
||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||
ArtTableSO tableSO = AssetDatabase.LoadAssetAtPath<ArtTableSO>(path);
|
||
if (tableSO != null)
|
||
{
|
||
allArtTables.Add(tableSO);
|
||
|
||
// 按文件夹分组
|
||
string folder = GetRelativeFolder(path);
|
||
if (!artTablesByFolder.ContainsKey(folder))
|
||
{
|
||
artTablesByFolder[folder] = new List<ArtTableSO>();
|
||
}
|
||
artTablesByFolder[folder].Add(tableSO);
|
||
}
|
||
}
|
||
|
||
// 加载特定资源
|
||
headFrameResourceSO = allArtTables.FirstOrDefault(x => x.TableName == "HeadFrameResource");
|
||
headResourceSO = allArtTables.FirstOrDefault(x => x.TableName == "HeadResource");
|
||
emojiResourceSO = allArtTables.FirstOrDefault(x => x.TableName == "EmojiResource");
|
||
|
||
Debug.Log($"加载了 {allArtTables.Count} 个ArtTableSO资源,分为 {artTablesByFolder.Count} 个组");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取相对文件夹路径
|
||
/// </summary>
|
||
private string GetRelativeFolder(string assetPath)
|
||
{
|
||
string relativePath = assetPath.Replace(ART_SO_PATH + "/", "");
|
||
int lastSlash = relativePath.LastIndexOf('/');
|
||
if (lastSlash >= 0)
|
||
{
|
||
return relativePath.Substring(0, lastSlash);
|
||
}
|
||
return "根目录";
|
||
}
|
||
|
||
/// <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 columnCount = worksheet.Dimension.Columns;
|
||
|
||
for (int col = 1; col <= columnCount; col++)
|
||
{
|
||
string header = worksheet.Cells[1, col].Text;
|
||
if (header == "key")
|
||
{
|
||
keyColumnIndex = col;
|
||
}
|
||
else if (header == "zh_CN")
|
||
{
|
||
zhCNColumnIndex = col;
|
||
}
|
||
}
|
||
|
||
if (keyColumnIndex < 0)
|
||
{
|
||
Debug.LogWarning("语言表中未找到Key列");
|
||
return;
|
||
}
|
||
|
||
// 读取数据(从第3行开始)
|
||
int rowCount = worksheet.Dimension.Rows;
|
||
for (int row = 3; row <= rowCount; row++)
|
||
{
|
||
string key = worksheet.Cells[row, keyColumnIndex].Text;
|
||
if (string.IsNullOrEmpty(key)) continue;
|
||
|
||
if (!languageDict.ContainsKey(key))
|
||
{
|
||
languageDict[key] = new Dictionary<string, string>();
|
||
}
|
||
|
||
if (zhCNColumnIndex > 0)
|
||
{
|
||
languageDict[key]["zh_CN"] = worksheet.Cells[row, zhCNColumnIndex].Text;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 加载限时事件数据
|
||
/// </summary>
|
||
private void LoadLimitedTimeEventData()
|
||
{
|
||
limitedTimeEventDict.Clear();
|
||
|
||
string eventExcelPath = Path.Combine(docsRootPath, "config", LIMITED_TIME_EVENT_EXCEL_NAME);
|
||
if (!File.Exists(eventExcelPath))
|
||
{
|
||
Debug.LogWarning($"未找到限时事件表: {eventExcelPath}");
|
||
return;
|
||
}
|
||
|
||
using (var package = new ExcelPackage(new FileInfo(eventExcelPath)))
|
||
{
|
||
var worksheet = package.Workbook.Worksheets[EVENT_SHEET_NAME];
|
||
if (worksheet == null)
|
||
{
|
||
Debug.LogWarning($"限时事件表中未找到Sheet: {EVENT_SHEET_NAME}");
|
||
return;
|
||
}
|
||
|
||
// 查找Id和Name列
|
||
int idCol = -1, nameCol = -1;
|
||
int columnCount = worksheet.Dimension.Columns;
|
||
|
||
for (int col = 1; col <= columnCount; col++)
|
||
{
|
||
string header = worksheet.Cells[1, col].Text;
|
||
if (header == "Id") idCol = col;
|
||
else if (header == "Name") nameCol = col;
|
||
}
|
||
|
||
if (idCol < 0 || nameCol < 0)
|
||
{
|
||
Debug.LogWarning("限时事件表结构不正确");
|
||
return;
|
||
}
|
||
|
||
// 读取数据(从第3行开始)
|
||
int rowCount = worksheet.Dimension.Rows;
|
||
for (int row = 3; row <= rowCount; row++)
|
||
{
|
||
string idText = worksheet.Cells[row, idCol].Text;
|
||
if (string.IsNullOrEmpty(idText)) continue;
|
||
|
||
if (int.TryParse(idText, out int id))
|
||
{
|
||
string name = worksheet.Cells[row, nameCol].Text;
|
||
limitedTimeEventDict[id] = name;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 加载Avatar数据
|
||
/// </summary>
|
||
private void LoadAvatarData()
|
||
{
|
||
avatarDict.Clear();
|
||
|
||
string avatarExcelPath = Path.Combine(docsRootPath, "config", AVATAR_EXCEL_NAME);
|
||
if (!File.Exists(avatarExcelPath))
|
||
{
|
||
Debug.LogWarning($"未找到Avatar表: {avatarExcelPath}");
|
||
return;
|
||
}
|
||
|
||
using (var package = new ExcelPackage(new FileInfo(avatarExcelPath)))
|
||
{
|
||
var worksheet = package.Workbook.Worksheets[AVATAR_SHEET_NAME];
|
||
if (worksheet == null)
|
||
{
|
||
Debug.LogWarning($"Avatar表中未找到Sheet: {AVATAR_SHEET_NAME}");
|
||
return;
|
||
}
|
||
|
||
// 查找列
|
||
int idCol = -1, nameKeyCol = -1, iconCol = -1;
|
||
int columnCount = worksheet.Dimension.Columns;
|
||
|
||
for (int col = 1; col <= columnCount; col++)
|
||
{
|
||
string header = worksheet.Cells[1, col].Text;
|
||
if (header == "Id") idCol = col;
|
||
else if (header == "NameKey") nameKeyCol = col;
|
||
else if (header == "Icon") iconCol = col;
|
||
}
|
||
|
||
if (idCol < 0 || nameKeyCol < 0 || iconCol < 0)
|
||
{
|
||
Debug.LogWarning("Avatar表结构不正确");
|
||
return;
|
||
}
|
||
|
||
// 读取数据(从第3行开始)
|
||
int rowCount = worksheet.Dimension.Rows;
|
||
for (int row = 3; row <= rowCount; row++)
|
||
{
|
||
string idText = worksheet.Cells[row, idCol].Text;
|
||
if (string.IsNullOrEmpty(idText)) continue;
|
||
|
||
if (int.TryParse(idText, out int id))
|
||
{
|
||
string nameKey = worksheet.Cells[row, nameKeyCol].Text;
|
||
string iconText = worksheet.Cells[row, iconCol].Text;
|
||
int.TryParse(iconText, out int iconId);
|
||
|
||
avatarDict[id] = new AvatarData
|
||
{
|
||
Id = id,
|
||
NameKey = nameKey,
|
||
IconId = iconId
|
||
};
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 加载Face数据
|
||
/// </summary>
|
||
private void LoadFaceData()
|
||
{
|
||
faceDict.Clear();
|
||
|
||
string faceExcelPath = Path.Combine(docsRootPath, "config", FACE_EXCEL_NAME);
|
||
if (!File.Exists(faceExcelPath))
|
||
{
|
||
Debug.LogWarning($"未找到Face表: {faceExcelPath}");
|
||
return;
|
||
}
|
||
|
||
using (var package = new ExcelPackage(new FileInfo(faceExcelPath)))
|
||
{
|
||
var worksheet = package.Workbook.Worksheets[FACE_SHEET_NAME];
|
||
if (worksheet == null)
|
||
{
|
||
Debug.LogWarning($"Face表中未找到Sheet: {FACE_SHEET_NAME}");
|
||
return;
|
||
}
|
||
|
||
// 查找列
|
||
int idCol = -1, nameKeyCol = -1, iconCol = -1;
|
||
int columnCount = worksheet.Dimension.Columns;
|
||
|
||
for (int col = 1; col <= columnCount; col++)
|
||
{
|
||
string header = worksheet.Cells[1, col].Text;
|
||
if (header == "Id") idCol = col;
|
||
else if (header == "NameKey") nameKeyCol = col;
|
||
else if (header == "Icon") iconCol = col;
|
||
}
|
||
|
||
if (idCol < 0 || nameKeyCol < 0 || iconCol < 0)
|
||
{
|
||
Debug.LogWarning("Face表结构不正确");
|
||
return;
|
||
}
|
||
|
||
// 读取数据(从第3行开始)
|
||
int rowCount = worksheet.Dimension.Rows;
|
||
for (int row = 3; row <= rowCount; row++)
|
||
{
|
||
string idText = worksheet.Cells[row, idCol].Text;
|
||
if (string.IsNullOrEmpty(idText)) continue;
|
||
|
||
if (int.TryParse(idText, out int id))
|
||
{
|
||
string nameKey = worksheet.Cells[row, nameKeyCol].Text;
|
||
string iconText = worksheet.Cells[row, iconCol].Text;
|
||
int.TryParse(iconText, out int iconId);
|
||
|
||
faceDict[id] = new FaceData
|
||
{
|
||
Id = id,
|
||
NameKey = nameKey,
|
||
IconId = iconId
|
||
};
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 加载Emoji数据
|
||
/// </summary>
|
||
private void LoadEmojiData()
|
||
{
|
||
emojiDict.Clear();
|
||
|
||
string emojiExcelPath = Path.Combine(docsRootPath, "config", EMOJI_EXCEL_NAME);
|
||
if (!File.Exists(emojiExcelPath))
|
||
{
|
||
Debug.LogWarning($"未找到Emoji表: {emojiExcelPath}");
|
||
return;
|
||
}
|
||
|
||
using (var package = new ExcelPackage(new FileInfo(emojiExcelPath)))
|
||
{
|
||
var worksheet = package.Workbook.Worksheets[EMOJI_SHEET_NAME];
|
||
if (worksheet == null)
|
||
{
|
||
Debug.LogWarning($"Emoji表中未找到Sheet: {EMOJI_SHEET_NAME}");
|
||
return;
|
||
}
|
||
|
||
// 查找列
|
||
int idCol = -1, nameKeyCol = -1, iconCol = -1;
|
||
int columnCount = worksheet.Dimension.Columns;
|
||
|
||
for (int col = 1; col <= columnCount; col++)
|
||
{
|
||
string header = worksheet.Cells[1, col].Text;
|
||
if (header == "Id") idCol = col;
|
||
else if (header == "NameKey") nameKeyCol = col;
|
||
else if (header == "Icon") iconCol = col;
|
||
}
|
||
|
||
if (idCol < 0 || nameKeyCol < 0 || iconCol < 0)
|
||
{
|
||
Debug.LogWarning("Emoji表结构不正确");
|
||
return;
|
||
}
|
||
|
||
// 读取数据(从第3行开始)
|
||
int rowCount = worksheet.Dimension.Rows;
|
||
for (int row = 3; row <= rowCount; row++)
|
||
{
|
||
string idText = worksheet.Cells[row, idCol].Text;
|
||
if (string.IsNullOrEmpty(idText)) continue;
|
||
|
||
if (int.TryParse(idText, out int id))
|
||
{
|
||
string nameKey = worksheet.Cells[row, nameKeyCol].Text;
|
||
string iconText = worksheet.Cells[row, iconCol].Text;
|
||
int.TryParse(iconText, out int iconId);
|
||
|
||
emojiDict[id] = new EmojiData
|
||
{
|
||
Id = id,
|
||
NameKey = nameKey,
|
||
IconId = iconId
|
||
};
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 加载Item.xlsx
|
||
/// </summary>
|
||
private void LoadItemExcel()
|
||
{
|
||
allItemDataList.Clear();
|
||
|
||
string itemExcelPath = Path.Combine(docsRootPath, "config", ITEM_EXCEL_NAME);
|
||
if (!File.Exists(itemExcelPath))
|
||
{
|
||
throw new Exception($"未找到Item配置文件: {itemExcelPath}");
|
||
}
|
||
|
||
using (var package = new ExcelPackage(new FileInfo(itemExcelPath)))
|
||
{
|
||
var worksheet = package.Workbook.Worksheets[ITEM_SHEET_NAME];
|
||
if (worksheet == null)
|
||
{
|
||
throw new Exception($"Item.xlsx中未找到Sheet: {ITEM_SHEET_NAME}");
|
||
}
|
||
|
||
// 读取表头(第1行)
|
||
int idCol = -1, nameCol = -1, iTypeCol = -1, effectCol = -1, resCol = -1;
|
||
int columnCount = worksheet.Dimension.Columns;
|
||
|
||
for (int col = 1; col <= columnCount; col++)
|
||
{
|
||
string header = worksheet.Cells[1, col].Text;
|
||
if (header == "Id") idCol = col;
|
||
else if (header == "Name") nameCol = col;
|
||
else if (header == "IType") iTypeCol = col;
|
||
else if (header == "Effect") effectCol = col;
|
||
else if (header == "Res") resCol = col;
|
||
}
|
||
|
||
if (idCol < 0 || nameCol < 0 || iTypeCol < 0)
|
||
{
|
||
throw new Exception("Item.xlsx表结构不正确,缺少必要列(Id/Name/IType)");
|
||
}
|
||
|
||
// 读取数据(从第3行开始)
|
||
int rowCount = worksheet.Dimension.Rows;
|
||
for (int row = 3; row <= rowCount; row++)
|
||
{
|
||
string idText = worksheet.Cells[row, idCol].Text;
|
||
if (string.IsNullOrEmpty(idText)) continue;
|
||
|
||
if (!int.TryParse(idText, out int id)) continue;
|
||
|
||
string name = worksheet.Cells[row, nameCol].Text;
|
||
string iTypeText = worksheet.Cells[row, iTypeCol].Text;
|
||
string effect = effectCol > 0 ? worksheet.Cells[row, effectCol].Text : "";
|
||
string res = resCol > 0 ? worksheet.Cells[row, resCol].Text : "";
|
||
|
||
if (!int.TryParse(iTypeText, out int iType)) continue;
|
||
|
||
var itemData = new ItemData
|
||
{
|
||
Id = id,
|
||
Name = name,
|
||
IType = iType,
|
||
Effect = effect,
|
||
Res = res
|
||
};
|
||
|
||
allItemDataList.Add(itemData);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 绘制筛选和数据编辑器
|
||
/// </summary>
|
||
private void DrawFilterAndDataEditor()
|
||
{
|
||
EditorGUILayout.BeginVertical("box");
|
||
|
||
// IType筛选
|
||
EditorGUILayout.LabelField("Item类型筛选", EditorStyles.boldLabel);
|
||
EditorGUILayout.BeginHorizontal();
|
||
|
||
// 创建下拉列表选项
|
||
var typeOptions = new List<string> { "请选择类型..." };
|
||
var typeValues = new List<int> { -1 };
|
||
|
||
// 排除103、104、108和97、98、111-115
|
||
foreach (var kvp in ITEM_TYPES.OrderBy(x => x.Key))
|
||
{
|
||
// 跳过不需要显示的类型
|
||
if (kvp.Key == 103 || kvp.Key == 104 || kvp.Key == 108 ||
|
||
kvp.Key == 97 || kvp.Key == 98 ||
|
||
(kvp.Key >= 111 && kvp.Key <= 115))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
typeOptions.Add(kvp.Value);
|
||
typeValues.Add(kvp.Key);
|
||
}
|
||
|
||
int currentIndex = typeValues.IndexOf(selectedIType);
|
||
if (currentIndex < 0) currentIndex = 0;
|
||
|
||
int newIndex = EditorGUILayout.Popup("选择类型", currentIndex, typeOptions.ToArray());
|
||
if (newIndex != currentIndex)
|
||
{
|
||
selectedIType = typeValues[newIndex];
|
||
FilterItemsByType();
|
||
currentPage = 0;
|
||
}
|
||
|
||
if (selectedIType > 0)
|
||
{
|
||
EditorGUILayout.LabelField($"当前类型ID: {selectedIType}", GUILayout.Width(150));
|
||
}
|
||
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
EditorGUILayout.EndVertical();
|
||
|
||
EditorGUILayout.Space(5);
|
||
|
||
// 如果已选择类型,显示数据列表
|
||
if (selectedIType > 0)
|
||
{
|
||
DrawItemDataList();
|
||
}
|
||
else
|
||
{
|
||
EditorGUILayout.HelpBox("请先选择Item类型进行筛选", MessageType.Info);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据IType筛选数据
|
||
/// </summary>
|
||
private void FilterItemsByType()
|
||
{
|
||
filteredItemDataList = allItemDataList.Where(x => x.IType == selectedIType).ToList();
|
||
totalPages = Mathf.CeilToInt((float)filteredItemDataList.Count / itemsPerPage);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 绘制Item数据列表
|
||
/// </summary>
|
||
private void DrawItemDataList()
|
||
{
|
||
EditorGUILayout.BeginVertical("box");
|
||
|
||
// 顶部工具栏
|
||
EditorGUILayout.BeginHorizontal();
|
||
EditorGUILayout.LabelField($"数据列表(共 {filteredItemDataList.Count} 条)", EditorStyles.boldLabel);
|
||
GUILayout.FlexibleSpace();
|
||
|
||
GUI.backgroundColor = Color.cyan;
|
||
if (GUILayout.Button("+ 添加Item", GUILayout.Height(25), GUILayout.Width(100)))
|
||
{
|
||
AddNewItem();
|
||
}
|
||
GUI.backgroundColor = Color.white;
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
EditorGUILayout.Space(3);
|
||
|
||
// 分页控制
|
||
DrawPagination();
|
||
|
||
EditorGUILayout.Space(3);
|
||
|
||
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.Height(400));
|
||
|
||
// 表头
|
||
DrawTableHeader();
|
||
|
||
// 数据行
|
||
int startIndex = currentPage * itemsPerPage;
|
||
int endIndex = Mathf.Min(startIndex + itemsPerPage, filteredItemDataList.Count);
|
||
|
||
for (int i = startIndex; i < endIndex; i++)
|
||
{
|
||
DrawItemDataRow(filteredItemDataList[i]);
|
||
}
|
||
|
||
EditorGUILayout.EndScrollView();
|
||
|
||
EditorGUILayout.Space(5);
|
||
|
||
// 分页控制(底部)
|
||
DrawPagination();
|
||
|
||
EditorGUILayout.EndVertical();
|
||
|
||
EditorGUILayout.Space(10);
|
||
|
||
// 保存按钮
|
||
EditorGUILayout.BeginHorizontal();
|
||
GUILayout.FlexibleSpace();
|
||
|
||
GUI.backgroundColor = Color.green;
|
||
if (GUILayout.Button("保存配置到Excel", GUILayout.Height(30), GUILayout.Width(200)))
|
||
{
|
||
SaveData();
|
||
}
|
||
GUI.backgroundColor = Color.white;
|
||
|
||
GUILayout.FlexibleSpace();
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 绘制分页控制
|
||
/// </summary>
|
||
private void DrawPagination()
|
||
{
|
||
EditorGUILayout.BeginHorizontal();
|
||
|
||
EditorGUILayout.LabelField("每页显示:", GUILayout.Width(70));
|
||
itemsPerPage = EditorGUILayout.IntField(itemsPerPage, GUILayout.Width(50));
|
||
itemsPerPage = Mathf.Max(1, itemsPerPage);
|
||
|
||
GUILayout.Space(20);
|
||
|
||
GUI.enabled = currentPage > 0;
|
||
if (GUILayout.Button("第一页", GUILayout.Width(60)))
|
||
{
|
||
currentPage = 0;
|
||
}
|
||
if (GUILayout.Button("上一页", GUILayout.Width(60)))
|
||
{
|
||
currentPage--;
|
||
}
|
||
GUI.enabled = true;
|
||
|
||
EditorGUILayout.LabelField($"第 {currentPage + 1} / {Mathf.Max(1, totalPages)} 页", GUILayout.Width(100));
|
||
|
||
int newPage = EditorGUILayout.IntField(currentPage + 1, GUILayout.Width(50)) - 1;
|
||
if (newPage != currentPage && newPage >= 0 && newPage < totalPages)
|
||
{
|
||
currentPage = newPage;
|
||
}
|
||
|
||
GUI.enabled = currentPage < totalPages - 1;
|
||
if (GUILayout.Button("下一页", GUILayout.Width(60)))
|
||
{
|
||
currentPage++;
|
||
}
|
||
if (GUILayout.Button("最后一页", GUILayout.Width(70)))
|
||
{
|
||
currentPage = totalPages - 1;
|
||
}
|
||
GUI.enabled = true;
|
||
|
||
GUILayout.FlexibleSpace();
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 绘制表头
|
||
/// </summary>
|
||
private void DrawTableHeader()
|
||
{
|
||
EditorGUILayout.BeginHorizontal("box");
|
||
EditorGUILayout.LabelField("Id", EditorStyles.boldLabel, GUILayout.Width(50));
|
||
EditorGUILayout.LabelField("Name", EditorStyles.boldLabel, GUILayout.Width(120));
|
||
EditorGUILayout.LabelField("IType", EditorStyles.boldLabel, GUILayout.Width(50));
|
||
EditorGUILayout.LabelField("Effect", EditorStyles.boldLabel, GUILayout.Width(200));
|
||
EditorGUILayout.LabelField("Res", EditorStyles.boldLabel, GUILayout.Width(200));
|
||
EditorGUILayout.LabelField("预览", EditorStyles.boldLabel, GUILayout.Width(100));
|
||
EditorGUILayout.LabelField("操作", EditorStyles.boldLabel, GUILayout.Width(60));
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 绘制单行数据
|
||
/// </summary>
|
||
private void DrawItemDataRow(ItemData data)
|
||
{
|
||
EditorGUILayout.BeginHorizontal("box");
|
||
|
||
// Id(可编辑)
|
||
data.Id = EditorGUILayout.IntField(data.Id, GUILayout.Width(50));
|
||
|
||
// Name(可编辑)
|
||
data.Name = EditorGUILayout.TextField(data.Name, GUILayout.Width(120));
|
||
|
||
// IType(只读)
|
||
GUI.enabled = false;
|
||
EditorGUILayout.IntField(data.IType, GUILayout.Width(50));
|
||
GUI.enabled = true;
|
||
|
||
// Effect(根据IType不同显示不同内容)
|
||
DrawEffectField(data);
|
||
|
||
// Res(资源选择)
|
||
DrawResField(data);
|
||
|
||
// 预览
|
||
DrawResPreview(data);
|
||
|
||
// 删除按钮
|
||
GUI.backgroundColor = Color.red;
|
||
if (GUILayout.Button("删除", GUILayout.Width(60)))
|
||
{
|
||
if (EditorUtility.DisplayDialog("确认删除", $"确定要删除ID为{data.Id}的Item吗?", "删除", "取消"))
|
||
{
|
||
filteredItemDataList.Remove(data);
|
||
allItemDataList.Remove(data);
|
||
FilterItemsByType(); // 重新计算分页
|
||
}
|
||
}
|
||
GUI.backgroundColor = Color.white;
|
||
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 绘制Effect字段
|
||
/// </summary>
|
||
private void DrawEffectField(ItemData data)
|
||
{
|
||
EditorGUILayout.BeginVertical(GUILayout.Width(200));
|
||
|
||
switch (data.IType)
|
||
{
|
||
case 100: // 棋子 - 不显示
|
||
case 1: // 能量
|
||
case 2: // 星星
|
||
case 3: // 钻石
|
||
EditorGUILayout.LabelField("-", GUILayout.Width(200));
|
||
break;
|
||
|
||
case 101: // 卡包 - 只读
|
||
case 106: // 活动代币 - 没有Effect
|
||
case 107: // 竞赛游戏代币 - 没有Effect
|
||
case 99: // 背包道具 - 没有Effect
|
||
EditorGUILayout.LabelField(string.IsNullOrEmpty(data.Effect) ? "-" : data.Effect, GUILayout.Width(200));
|
||
break;
|
||
|
||
case 102: // 限时事件
|
||
DrawLimitedTimeEventEffect(data);
|
||
break;
|
||
|
||
case 105: // 头像框 - 已改用Res字段
|
||
EditorGUILayout.LabelField("请使用Res字段配置", GUILayout.Width(200));
|
||
break;
|
||
|
||
case 110: // 头像 - 已改用Res字段
|
||
EditorGUILayout.LabelField("请使用Res字段配置", GUILayout.Width(200));
|
||
break;
|
||
|
||
case 109: // 表情 - 已改用Res字段
|
||
EditorGUILayout.LabelField("请使用Res字段配置", GUILayout.Width(200));
|
||
break;
|
||
|
||
default:
|
||
data.Effect = EditorGUILayout.TextField(data.Effect, GUILayout.Width(200));
|
||
break;
|
||
}
|
||
|
||
EditorGUILayout.EndVertical();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 绘制限时事件Effect
|
||
/// </summary>
|
||
private void DrawLimitedTimeEventEffect(ItemData data)
|
||
{
|
||
// Effect格式: "eventId,seconds"
|
||
string[] parts = data.Effect.Split(',');
|
||
int eventId = 0;
|
||
int seconds = 0;
|
||
|
||
if (parts.Length >= 1) int.TryParse(parts[0], out eventId);
|
||
if (parts.Length >= 2) int.TryParse(parts[1], out seconds);
|
||
|
||
EditorGUILayout.BeginHorizontal();
|
||
|
||
// 事件选择下拉框
|
||
var eventOptions = new List<string> { "请选择..." };
|
||
var eventIds = new List<int> { 0 };
|
||
|
||
foreach (var kvp in limitedTimeEventDict.OrderBy(x => x.Key))
|
||
{
|
||
eventOptions.Add(kvp.Value);
|
||
eventIds.Add(kvp.Key);
|
||
}
|
||
|
||
int currentIndex = eventIds.IndexOf(eventId);
|
||
if (currentIndex < 0) currentIndex = 0;
|
||
|
||
int newIndex = EditorGUILayout.Popup(currentIndex, eventOptions.ToArray(), GUILayout.Width(120));
|
||
int newEventId = eventIds[newIndex];
|
||
|
||
// 秒数输入
|
||
int newSeconds = EditorGUILayout.IntField(seconds, GUILayout.Width(60));
|
||
|
||
// 显示时间格式
|
||
int minutes = newSeconds / 60;
|
||
int secs = newSeconds % 60;
|
||
GUI.enabled = false;
|
||
EditorGUILayout.TextField($"{minutes:D2}:{secs:D2}", GUILayout.Width(50));
|
||
GUI.enabled = true;
|
||
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
// 验证提示
|
||
if (newEventId > 0 && !limitedTimeEventDict.ContainsKey(newEventId))
|
||
{
|
||
EditorGUILayout.HelpBox("没有这个限时事件", MessageType.Error);
|
||
}
|
||
|
||
// 更新数据
|
||
data.Effect = $"{newEventId},{newSeconds}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 绘制头像框Effect
|
||
/// </summary>
|
||
private void DrawHeadFrameEffect(ItemData data)
|
||
{
|
||
// Effect格式: "avatarId,0"
|
||
string[] parts = string.IsNullOrEmpty(data.Effect) ? new string[0] : data.Effect.Split(',');
|
||
int avatarId = -1;
|
||
|
||
if (parts.Length >= 1) int.TryParse(parts[0], out avatarId);
|
||
|
||
// 创建下拉选项
|
||
var options = new List<string> { "请选择..." };
|
||
var ids = new List<int> { -1 };
|
||
|
||
foreach (var kvp in avatarDict.OrderBy(x => x.Key))
|
||
{
|
||
string name = kvp.Value.NameKey;
|
||
if (languageDict.ContainsKey(kvp.Value.NameKey) && languageDict[kvp.Value.NameKey].ContainsKey("zh_CN"))
|
||
{
|
||
name = languageDict[kvp.Value.NameKey]["zh_CN"];
|
||
}
|
||
options.Add($"{name} (ID:{kvp.Key})");
|
||
ids.Add(kvp.Key);
|
||
}
|
||
|
||
int currentIndex = ids.IndexOf(avatarId);
|
||
if (currentIndex < 0) currentIndex = 0;
|
||
|
||
EditorGUI.BeginChangeCheck();
|
||
int newIndex = EditorGUILayout.Popup(currentIndex, options.ToArray(), GUILayout.Width(200));
|
||
if (EditorGUI.EndChangeCheck())
|
||
{
|
||
int newAvatarId = ids[newIndex];
|
||
data.Effect = $"{newAvatarId},0";
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 绘制头像Effect
|
||
/// </summary>
|
||
private void DrawFaceEffect(ItemData data)
|
||
{
|
||
// Effect格式: "faceId,0"
|
||
string[] parts = string.IsNullOrEmpty(data.Effect) ? new string[0] : data.Effect.Split(',');
|
||
int faceId = -1;
|
||
|
||
if (parts.Length >= 1) int.TryParse(parts[0], out faceId);
|
||
|
||
// 创建下拉选项
|
||
var options = new List<string> { "请选择..." };
|
||
var ids = new List<int> { -1 };
|
||
|
||
foreach (var kvp in faceDict.OrderBy(x => x.Key))
|
||
{
|
||
string name = kvp.Value.NameKey;
|
||
if (languageDict.ContainsKey(kvp.Value.NameKey) && languageDict[kvp.Value.NameKey].ContainsKey("zh_CN"))
|
||
{
|
||
name = languageDict[kvp.Value.NameKey]["zh_CN"];
|
||
}
|
||
options.Add($"{name} (ID:{kvp.Key})");
|
||
ids.Add(kvp.Key);
|
||
}
|
||
|
||
int currentIndex = ids.IndexOf(faceId);
|
||
if (currentIndex < 0) currentIndex = 0;
|
||
|
||
EditorGUI.BeginChangeCheck();
|
||
int newIndex = EditorGUILayout.Popup(currentIndex, options.ToArray(), GUILayout.Width(200));
|
||
if (EditorGUI.EndChangeCheck())
|
||
{
|
||
int newFaceId = ids[newIndex];
|
||
data.Effect = $"{newFaceId},0";
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 绘制表情Effect
|
||
/// </summary>
|
||
private void DrawEmojiEffect(ItemData data)
|
||
{
|
||
// Effect格式: "emojiId,0"
|
||
string[] parts = string.IsNullOrEmpty(data.Effect) ? new string[0] : data.Effect.Split(',');
|
||
int emojiId = -1;
|
||
|
||
if (parts.Length >= 1) int.TryParse(parts[0], out emojiId);
|
||
|
||
// 创建下拉选项
|
||
var options = new List<string> { "请选择..." };
|
||
var ids = new List<int> { -1 };
|
||
|
||
foreach (var kvp in emojiDict.OrderBy(x => x.Key))
|
||
{
|
||
string name = kvp.Value.NameKey;
|
||
if (languageDict.ContainsKey(kvp.Value.NameKey) && languageDict[kvp.Value.NameKey].ContainsKey("zh_CN"))
|
||
{
|
||
name = languageDict[kvp.Value.NameKey]["zh_CN"];
|
||
}
|
||
options.Add($"{name} (ID:{kvp.Key})");
|
||
ids.Add(kvp.Key);
|
||
}
|
||
|
||
int currentIndex = ids.IndexOf(emojiId);
|
||
if (currentIndex < 0) currentIndex = 0;
|
||
|
||
EditorGUI.BeginChangeCheck();
|
||
int newIndex = EditorGUILayout.Popup(currentIndex, options.ToArray(), GUILayout.Width(200));
|
||
if (EditorGUI.EndChangeCheck())
|
||
{
|
||
int newEmojiId = ids[newIndex];
|
||
data.Effect = $"{newEmojiId},0";
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 绘制Res字段(三级选择:组->表->Item)
|
||
/// </summary>
|
||
private void DrawResField(ItemData data)
|
||
{
|
||
EditorGUILayout.BeginVertical(GUILayout.Width(200));
|
||
|
||
// Res格式: "TableId,ItemId"
|
||
string[] parts = string.IsNullOrEmpty(data.Res) ? new string[0] : data.Res.Split(',');
|
||
int tableId = -1;
|
||
int itemId = -1;
|
||
|
||
if (parts.Length > 0) int.TryParse(parts[0], out tableId);
|
||
if (parts.Length > 1) int.TryParse(parts[1], out itemId);
|
||
|
||
// 查找当前表格和所属文件夹
|
||
ArtTableSO currentTable = null;
|
||
string currentFolder = "";
|
||
|
||
// 优先使用缓存的组选择
|
||
if (resSelectedFolderCache.ContainsKey(data.Id))
|
||
{
|
||
currentFolder = resSelectedFolderCache[data.Id];
|
||
}
|
||
|
||
// 如果有Res数据,从Res推导组和表
|
||
if (tableId >= 0)
|
||
{
|
||
currentTable = allArtTables.FirstOrDefault(x => x.TableId == tableId);
|
||
if (currentTable != null)
|
||
{
|
||
string tablePath = AssetDatabase.GetAssetPath(currentTable);
|
||
string folderFromRes = GetRelativeFolder(tablePath);
|
||
// 如果没有缓存或缓存不一致,更新缓存
|
||
if (string.IsNullOrEmpty(currentFolder) || currentFolder != folderFromRes)
|
||
{
|
||
currentFolder = folderFromRes;
|
||
resSelectedFolderCache[data.Id] = currentFolder;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 第一级:选择组
|
||
var folderOptions = new List<string> { "未选择" };
|
||
folderOptions.AddRange(artTablesByFolder.Keys.OrderBy(k => k));
|
||
|
||
int currentFolderIndex = 0;
|
||
if (!string.IsNullOrEmpty(currentFolder))
|
||
{
|
||
currentFolderIndex = folderOptions.IndexOf(currentFolder);
|
||
if (currentFolderIndex < 0) currentFolderIndex = 0;
|
||
}
|
||
|
||
EditorGUI.BeginChangeCheck();
|
||
int newFolderIndex = EditorGUILayout.Popup(currentFolderIndex, folderOptions.ToArray(), GUILayout.Width(200));
|
||
bool folderChanged = EditorGUI.EndChangeCheck();
|
||
|
||
if (folderChanged)
|
||
{
|
||
if (newFolderIndex > 0 && newFolderIndex < folderOptions.Count)
|
||
{
|
||
currentFolder = folderOptions[newFolderIndex];
|
||
// 保存到缓存
|
||
resSelectedFolderCache[data.Id] = currentFolder;
|
||
// 组改变时,清空表和Item选择
|
||
currentTable = null;
|
||
tableId = -1;
|
||
itemId = -1;
|
||
}
|
||
else if (newFolderIndex == 0)
|
||
{
|
||
// 选择"未选择",清空所有
|
||
currentFolder = "";
|
||
if (resSelectedFolderCache.ContainsKey(data.Id))
|
||
{
|
||
resSelectedFolderCache.Remove(data.Id);
|
||
}
|
||
currentTable = null;
|
||
data.Res = "";
|
||
EditorGUILayout.EndVertical();
|
||
return;
|
||
}
|
||
}
|
||
else if (newFolderIndex > 0 && newFolderIndex < folderOptions.Count)
|
||
{
|
||
currentFolder = folderOptions[newFolderIndex];
|
||
}
|
||
else if (newFolderIndex == 0)
|
||
{
|
||
// 当前是"未选择"状态
|
||
EditorGUILayout.EndVertical();
|
||
return;
|
||
}
|
||
|
||
// 第二级:选择表
|
||
List<ArtTableSO> tablesInFolder = artTablesByFolder.ContainsKey(currentFolder)
|
||
? artTablesByFolder[currentFolder]
|
||
: new List<ArtTableSO>();
|
||
|
||
var tableOptions = new List<string> { "未选择" };
|
||
tableOptions.AddRange(tablesInFolder.Select(t => $"{t.TableName}(ID:{t.TableId})"));
|
||
|
||
int currentTableIndex = 0;
|
||
if (currentTable != null && !folderChanged)
|
||
{
|
||
int foundIndex = tablesInFolder.FindIndex(t => t.TableId == currentTable.TableId);
|
||
if (foundIndex >= 0)
|
||
{
|
||
currentTableIndex = foundIndex + 1; // +1 因为有"未选择"选项
|
||
}
|
||
}
|
||
|
||
EditorGUI.BeginChangeCheck();
|
||
int newTableIndex = EditorGUILayout.Popup(currentTableIndex, tableOptions.ToArray(), GUILayout.Width(200));
|
||
bool tableSelectionChanged = EditorGUI.EndChangeCheck();
|
||
|
||
if (tableSelectionChanged)
|
||
{
|
||
if (newTableIndex > 0 && newTableIndex <= tablesInFolder.Count)
|
||
{
|
||
currentTable = tablesInFolder[newTableIndex - 1]; // -1 因为有"未选择"选项
|
||
// 表格改变时,清空Item选择
|
||
data.Res = $"{currentTable.TableId},-1";
|
||
itemId = -1;
|
||
}
|
||
else if (newTableIndex == 0)
|
||
{
|
||
// 选择"未选择"
|
||
currentTable = null;
|
||
data.Res = "";
|
||
EditorGUILayout.EndVertical();
|
||
return;
|
||
}
|
||
}
|
||
else if (newTableIndex > 0 && newTableIndex <= tablesInFolder.Count)
|
||
{
|
||
currentTable = tablesInFolder[newTableIndex - 1];
|
||
}
|
||
else
|
||
{
|
||
// 当前表是"未选择"状态
|
||
EditorGUILayout.EndVertical();
|
||
return;
|
||
}
|
||
|
||
// 第三级:选择Item
|
||
if (currentTable != null && currentTable.Items != null && currentTable.Items.Count > 0)
|
||
{
|
||
var itemOptions = new List<string> { "未选择" };
|
||
itemOptions.AddRange(currentTable.Items.Select(item => $"{item.Name}(ID:{item.Id})"));
|
||
|
||
int currentItemIndex = 0;
|
||
if (itemId >= 0 && !tableSelectionChanged)
|
||
{
|
||
int foundIndex = currentTable.Items.FindIndex(item => item.Id == itemId);
|
||
if (foundIndex >= 0)
|
||
{
|
||
currentItemIndex = foundIndex + 1; // +1 因为有"未选择"选项
|
||
}
|
||
}
|
||
|
||
EditorGUI.BeginChangeCheck();
|
||
int newItemIndex = EditorGUILayout.Popup(currentItemIndex, itemOptions.ToArray(), GUILayout.Width(200));
|
||
if (EditorGUI.EndChangeCheck())
|
||
{
|
||
if (newItemIndex > 0 && newItemIndex <= currentTable.Items.Count)
|
||
{
|
||
data.Res = $"{currentTable.TableId},{currentTable.Items[newItemIndex - 1].Id}"; // -1 因为有"未选择"选项
|
||
}
|
||
else if (newItemIndex == 0)
|
||
{
|
||
// 选择"未选择"
|
||
data.Res = $"{currentTable.TableId},-1";
|
||
}
|
||
}
|
||
}
|
||
|
||
EditorGUILayout.EndVertical();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 绘制资源预览(统一使用Res字段)
|
||
/// </summary>
|
||
private void DrawResPreview(ItemData data)
|
||
{
|
||
Sprite previewSprite = null;
|
||
string tipMessage = "";
|
||
|
||
// 统一从Res字段获取预览
|
||
previewSprite = GetResPreview(data);
|
||
|
||
if (previewSprite != null)
|
||
{
|
||
Rect rect = GUILayoutUtility.GetRect(80, 80, GUILayout.Width(100));
|
||
GUI.DrawTexture(rect, previewSprite.texture, ScaleMode.ScaleToFit);
|
||
}
|
||
else
|
||
{
|
||
if (!string.IsNullOrEmpty(tipMessage))
|
||
{
|
||
GUILayout.Box(tipMessage, GUILayout.Width(100), GUILayout.Height(80));
|
||
}
|
||
else
|
||
{
|
||
GUILayout.Box("无预览", GUILayout.Width(100), GUILayout.Height(80));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取头像框预览
|
||
/// </summary>
|
||
private Sprite GetHeadFramePreview(ItemData data)
|
||
{
|
||
if (headFrameResourceSO == null) return null;
|
||
if (string.IsNullOrEmpty(data.Effect)) return null;
|
||
|
||
string[] parts = data.Effect.Split(',');
|
||
if (parts.Length < 1 || !int.TryParse(parts[0], out int avatarId)) return null;
|
||
if (avatarId < 0) return null;
|
||
|
||
if (!avatarDict.ContainsKey(avatarId)) return null;
|
||
|
||
int iconId = avatarDict[avatarId].IconId;
|
||
var item = headFrameResourceSO.Items.FirstOrDefault(x => x.Id == iconId);
|
||
|
||
return item?.Sprite;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取头像预览
|
||
/// </summary>
|
||
private Sprite GetFacePreview(ItemData data)
|
||
{
|
||
if (headResourceSO == null) return null;
|
||
if (string.IsNullOrEmpty(data.Effect)) return null;
|
||
|
||
string[] parts = data.Effect.Split(',');
|
||
if (parts.Length < 1 || !int.TryParse(parts[0], out int faceId)) return null;
|
||
if (faceId < 0) return null;
|
||
|
||
if (!faceDict.ContainsKey(faceId)) return null;
|
||
|
||
int iconId = faceDict[faceId].IconId;
|
||
var item = headResourceSO.Items.FirstOrDefault(x => x.Id == iconId);
|
||
|
||
return item?.Sprite;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取表情预览
|
||
/// </summary>
|
||
private Sprite GetEmojiPreview(ItemData data)
|
||
{
|
||
if (emojiResourceSO == null) return null;
|
||
if (string.IsNullOrEmpty(data.Effect)) return null;
|
||
|
||
string[] parts = data.Effect.Split(',');
|
||
if (parts.Length < 1 || !int.TryParse(parts[0], out int emojiId)) return null;
|
||
if (emojiId < 0) return null;
|
||
|
||
if (!emojiDict.ContainsKey(emojiId)) return null;
|
||
|
||
int iconId = emojiDict[emojiId].IconId;
|
||
var item = emojiResourceSO.Items.FirstOrDefault(x => x.Id == iconId);
|
||
|
||
return item?.Sprite;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取Res资源预览
|
||
/// </summary>
|
||
private Sprite GetResPreview(ItemData data)
|
||
{
|
||
if (string.IsNullOrEmpty(data.Res)) return null;
|
||
|
||
string[] parts = data.Res.Split(',');
|
||
if (parts.Length < 2) return null;
|
||
|
||
if (!int.TryParse(parts[0], out int tableId)) return null;
|
||
if (!int.TryParse(parts[1], out int itemId)) return null;
|
||
|
||
var table = allArtTables.FirstOrDefault(x => x.TableId == tableId);
|
||
if (table == null) return null;
|
||
|
||
var item = table.Items.FirstOrDefault(x => x.Id == itemId);
|
||
return item?.Sprite;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加新Item
|
||
/// </summary>
|
||
private void AddNewItem()
|
||
{
|
||
// 找到当前IType的最大ID
|
||
int maxId = 0;
|
||
foreach (var item in allItemDataList.Where(x => x.IType == selectedIType))
|
||
{
|
||
if (item.Id > maxId) maxId = item.Id;
|
||
}
|
||
|
||
// 自增ID并检查重复
|
||
int newId = maxId + 1;
|
||
while (allItemDataList.Any(x => x.Id == newId))
|
||
{
|
||
newId++;
|
||
}
|
||
|
||
var newItem = new ItemData
|
||
{
|
||
Id = newId,
|
||
Name = $"新Item_{newId}",
|
||
IType = selectedIType,
|
||
Effect = "",
|
||
Res = ""
|
||
};
|
||
|
||
allItemDataList.Add(newItem);
|
||
FilterItemsByType();
|
||
|
||
// 跳转到最后一页
|
||
currentPage = totalPages - 1;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存数据到Excel
|
||
/// </summary>
|
||
private void SaveData()
|
||
{
|
||
try
|
||
{
|
||
// ID重复检查
|
||
var duplicateIds = allItemDataList.GroupBy(x => x.Id)
|
||
.Where(g => g.Count() > 1)
|
||
.Select(g => g.Key)
|
||
.ToList();
|
||
|
||
if (duplicateIds.Count > 0)
|
||
{
|
||
EditorUtility.DisplayDialog("错误",
|
||
$"存在重复的ID: {string.Join(", ", duplicateIds)}\n请修正后再保存",
|
||
"确定");
|
||
return;
|
||
}
|
||
|
||
// 验证限时事件
|
||
foreach (var item in allItemDataList.Where(x => x.IType == 102))
|
||
{
|
||
if (!string.IsNullOrEmpty(item.Effect))
|
||
{
|
||
string[] parts = item.Effect.Split(',');
|
||
if (parts.Length >= 1 && int.TryParse(parts[0], out int eventId))
|
||
{
|
||
if (eventId > 0 && !limitedTimeEventDict.ContainsKey(eventId))
|
||
{
|
||
EditorUtility.DisplayDialog("错误",
|
||
$"ID {item.Id} 的限时事件ID {eventId} 不存在\n请修正后再保存",
|
||
"确定");
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
string itemExcelPath = Path.Combine(docsRootPath, "config", ITEM_EXCEL_NAME);
|
||
|
||
using (var package = new ExcelPackage(new FileInfo(itemExcelPath)))
|
||
{
|
||
var worksheet = package.Workbook.Worksheets[ITEM_SHEET_NAME];
|
||
|
||
// 找到列索引
|
||
int idCol = -1, nameCol = -1, iTypeCol = -1, effectCol = -1, resCol = -1;
|
||
int columnCount = worksheet.Dimension.Columns;
|
||
|
||
for (int col = 1; col <= columnCount; col++)
|
||
{
|
||
string header = worksheet.Cells[1, col].Text;
|
||
if (header == "Id") idCol = col;
|
||
else if (header == "Name") nameCol = col;
|
||
else if (header == "IType") iTypeCol = col;
|
||
else if (header == "Effect") effectCol = col;
|
||
else if (header == "Res") resCol = col;
|
||
}
|
||
|
||
// 创建Id到行号的映射(读取现有数据)
|
||
var existingRowMap = new Dictionary<int, int>();
|
||
int existingRowCount = worksheet.Dimension.Rows;
|
||
|
||
for (int row = 3; row <= existingRowCount; row++)
|
||
{
|
||
string idText = worksheet.Cells[row, idCol].Text;
|
||
if (!string.IsNullOrEmpty(idText) && int.TryParse(idText, out int id))
|
||
{
|
||
existingRowMap[id] = row;
|
||
}
|
||
}
|
||
|
||
// 更新或新增数据
|
||
int nextNewRow = existingRowCount + 1;
|
||
|
||
foreach (var item in allItemDataList.OrderBy(x => x.Id))
|
||
{
|
||
int targetRow;
|
||
|
||
// 如果该ID已存在,更新对应行;否则添加新行
|
||
if (existingRowMap.ContainsKey(item.Id))
|
||
{
|
||
targetRow = existingRowMap[item.Id];
|
||
}
|
||
else
|
||
{
|
||
targetRow = nextNewRow;
|
||
nextNewRow++;
|
||
}
|
||
|
||
// 只更新我们管理的列
|
||
worksheet.Cells[targetRow, idCol].Value = item.Id;
|
||
worksheet.Cells[targetRow, nameCol].Value = item.Name;
|
||
worksheet.Cells[targetRow, iTypeCol].Value = item.IType;
|
||
|
||
if (effectCol > 0)
|
||
{
|
||
worksheet.Cells[targetRow, effectCol].Value = item.Effect;
|
||
}
|
||
|
||
if (resCol > 0)
|
||
{
|
||
worksheet.Cells[targetRow, resCol].Value = item.Res;
|
||
}
|
||
}
|
||
|
||
// 删除已不存在的数据行(只清空我们管理的列)
|
||
var currentIds = allItemDataList.Select(x => x.Id).ToHashSet();
|
||
for (int row = 3; row <= existingRowCount; row++)
|
||
{
|
||
string idText = worksheet.Cells[row, idCol].Text;
|
||
if (!string.IsNullOrEmpty(idText) && int.TryParse(idText, out int id))
|
||
{
|
||
if (!currentIds.Contains(id))
|
||
{
|
||
// 只清空我们管理的列,保留其他列
|
||
worksheet.Cells[row, idCol].Value = null;
|
||
worksheet.Cells[row, nameCol].Value = null;
|
||
worksheet.Cells[row, iTypeCol].Value = null;
|
||
if (effectCol > 0) worksheet.Cells[row, effectCol].Value = null;
|
||
if (resCol > 0) worksheet.Cells[row, resCol].Value = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
package.Save();
|
||
}
|
||
|
||
EditorUtility.DisplayDialog("成功", "配置已保存到Excel文件", "确定");
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
EditorUtility.DisplayDialog("错误", $"保存失败: {e.Message}\n{e.StackTrace}", "确定");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Item数据类
|
||
/// </summary>
|
||
private class ItemData
|
||
{
|
||
public int Id;
|
||
public string Name;
|
||
public int IType;
|
||
public string Effect;
|
||
public string Res;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Avatar数据类
|
||
/// </summary>
|
||
private class AvatarData
|
||
{
|
||
public int Id;
|
||
public string NameKey;
|
||
public int IconId;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Face数据类
|
||
/// </summary>
|
||
private class FaceData
|
||
{
|
||
public int Id;
|
||
public string NameKey;
|
||
public int IconId;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Emoji数据类
|
||
/// </summary>
|
||
private class EmojiData
|
||
{
|
||
public int Id;
|
||
public string NameKey;
|
||
public int IconId;
|
||
}
|
||
}
|
||
}
|