using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using UnityEditor; using UnityEngine; using OfficeOpenXml; using Debug = UnityEngine.Debug; namespace DesignTools.Friends { /// /// 好友邀请奖励配置工具 /// 读取 Docs/config/Invite.xlsx 的 Reward Sheet,仅支持编辑固定 4 档邀请奖励 /// public class InviteRewardEditor : BaseDesignToolEditor { private const string INVITE_EXCEL_NAME = "Invite.xlsx"; private const string REWARD_SHEET_NAME = "Reward"; private const int COL_ID = 1; private const int COL_NEED = 2; private const int COL_ITEMS = 3; private const int FIXED_STAGE_COUNT = 4; private readonly List rewardDataList = new List(); [MenuItem("策划工具/好友/邀请奖励")] public static void ShowWindow() { var window = GetWindow("邀请奖励配置"); window.minSize = window.GetMinWindowSize(); window.Show(); } protected override string GetDocsPathPrefKey() { return "InviteRewardEditor_DocsPath"; } protected override string GetWindowTitle() { return "邀请奖励配置工具"; } protected override Vector2 GetMinWindowSize() { return new Vector2(820, 520); } protected override void LoadConfigData() { LoadInviteRewardExcel(); } protected override void DrawDataEditor() { DrawTips(); EditorGUILayout.Space(6); DrawRewardTable(); } protected override void SaveDataToExcel() { SaveToExcel(); } private void DrawTips() { EditorGUILayout.BeginVertical("box"); EditorGUILayout.HelpBox("如果要添加阶段数量请联系开发组", MessageType.Warning); if (rewardDataList.Count != FIXED_STAGE_COUNT) { EditorGUILayout.HelpBox( $"当前 Reward Sheet 读取到 {rewardDataList.Count} 行有效数据,工具仅支持固定 {FIXED_STAGE_COUNT} 行配置。", MessageType.Error); } bool hasParseError = false; for (int i = 0; i < rewardDataList.Count; i++) { if (!rewardDataList[i].IsItemsValid) { hasParseError = true; break; } } if (hasParseError) { EditorGUILayout.HelpBox("存在无法解析的奖励数据,请先检查 Excel 中 Items 列格式,必须为 [{\"Id\":100001,\"Num\":50}] 这种结构。", MessageType.Error); } EditorGUILayout.EndVertical(); } private void DrawRewardTable() { EditorGUILayout.BeginVertical("box"); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("阶段", EditorStyles.boldLabel, GUILayout.Width(80)); EditorGUILayout.LabelField("需要邀请人数", EditorStyles.boldLabel, GUILayout.Width(180)); EditorGUILayout.LabelField("奖励数量", EditorStyles.boldLabel, GUILayout.Width(180)); GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(4); scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); for (int i = 0; i < rewardDataList.Count; i++) { InviteRewardData data = rewardDataList[i]; EditorGUILayout.BeginVertical("box"); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField($"第 {i + 1} 档", GUILayout.Width(80)); data.Need = EditorGUILayout.IntField(data.Need, GUILayout.Width(180)); data.RewardNum = EditorGUILayout.IntField(data.RewardNum, GUILayout.Width(180)); GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); GUILayout.Space(84); EditorGUILayout.LabelField($"配置Id: {data.Id}", EditorStyles.miniLabel, GUILayout.Width(120)); EditorGUILayout.LabelField($"奖励ItemId: {data.RewardItemId}", EditorStyles.miniLabel, GUILayout.Width(160)); if (!data.IsItemsValid) { EditorGUILayout.LabelField("Items 格式异常", EditorStyles.miniBoldLabel, GUILayout.Width(100)); } GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); } EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); } private void LoadInviteRewardExcel() { rewardDataList.Clear(); string excelPath = GetDocsConfigFilePath(INVITE_EXCEL_NAME); if (!File.Exists(excelPath)) { throw new FileNotFoundException($"未找到 {INVITE_EXCEL_NAME}", excelPath); } using (var package = new ExcelPackage(new FileInfo(excelPath))) { var worksheet = package.Workbook.Worksheets[REWARD_SHEET_NAME]; if (worksheet == null) { throw new Exception($"未找到 Sheet: {REWARD_SHEET_NAME}"); } int rowCount = worksheet.Dimension?.Rows ?? 0; for (int row = 3; row <= rowCount; row++) { string idText = worksheet.Cells[row, COL_ID].Text.Trim(); if (string.IsNullOrEmpty(idText)) { continue; } if (!int.TryParse(idText, out int id)) { Debug.LogWarning($"Invite.xlsx/{REWARD_SHEET_NAME} 第 {row} 行 Id 不是有效整数,已跳过"); continue; } InviteRewardData data = new InviteRewardData { Id = id, Need = ParseInt(worksheet.Cells[row, COL_NEED].Text.Trim()), ItemsRawText = worksheet.Cells[row, COL_ITEMS].Text.Trim(), RowIndex = row }; data.IsItemsValid = TryParseItems(data.ItemsRawText, out int rewardItemId, out int rewardNum); data.RewardItemId = rewardItemId; data.RewardNum = rewardNum; rewardDataList.Add(data); } } if (rewardDataList.Count == 0) { throw new Exception("未读取到任何邀请奖励数据"); } } private void SaveToExcel() { if (!ValidateRewardData(out string errorMessage)) { EditorUtility.DisplayDialog("校验失败", errorMessage, "确定"); return; } try { string excelPath = GetDocsConfigFilePath(INVITE_EXCEL_NAME); using (var package = new ExcelPackage(new FileInfo(excelPath))) { var worksheet = package.Workbook.Worksheets[REWARD_SHEET_NAME]; if (worksheet == null) { EditorUtility.DisplayDialog("错误", $"未找到 Sheet: {REWARD_SHEET_NAME}", "确定"); return; } foreach (InviteRewardData data in rewardDataList) { int row = data.RowIndex; worksheet.Cells[row, COL_ID].Value = data.Id; worksheet.Cells[row, COL_NEED].Value = data.Need; worksheet.Cells[row, COL_ITEMS].Value = BuildItemsJson(data.RewardItemId, data.RewardNum); } package.Save(); } EditorUtility.DisplayDialog("成功", "邀请奖励配置已保存到 Excel!", "确定"); Debug.Log("邀请奖励配置已保存"); } catch (Exception e) { EditorUtility.DisplayDialog("错误", $"保存失败: {e.Message}", "确定"); Debug.LogError($"保存邀请奖励配置失败: {e}"); } } private bool ValidateRewardData(out string errorMessage) { if (rewardDataList.Count != FIXED_STAGE_COUNT) { errorMessage = $"当前 Reward Sheet 不是固定的 {FIXED_STAGE_COUNT} 行配置,不能通过该工具保存。\n\n如果要添加阶段数量请联系开发组"; return false; } int previousNeed = int.MinValue; for (int i = 0; i < rewardDataList.Count; i++) { InviteRewardData data = rewardDataList[i]; if (!data.IsItemsValid || data.RewardItemId <= 0) { errorMessage = $"第 {i + 1} 档奖励数据格式异常,无法保存。"; return false; } if (data.Need < 0) { errorMessage = $"第 {i + 1} 档的需要邀请人数不能小于 0。"; return false; } if (data.RewardNum < 0) { errorMessage = $"第 {i + 1} 档的奖励数量必须是整数,且不能小于 0。"; return false; } if (data.Need <= previousNeed) { errorMessage = "需要邀请人数必须按从小到大的顺序填写。"; return false; } previousNeed = data.Need; } errorMessage = string.Empty; return true; } private bool TryParseItems(string itemsText, out int itemId, out int rewardNum) { itemId = 0; rewardNum = 0; if (string.IsNullOrWhiteSpace(itemsText)) { return false; } Match idMatch = Regex.Match(itemsText, @"""Id""\s*:\s*(\d+)"); Match numMatch = Regex.Match(itemsText, @"""Num""\s*:\s*(-?\d+)"); if (!idMatch.Success || !numMatch.Success) { return false; } return int.TryParse(idMatch.Groups[1].Value, out itemId) && int.TryParse(numMatch.Groups[1].Value, out rewardNum); } private string BuildItemsJson(int itemId, int rewardNum) { return $"[{{\"Id\":{itemId},\"Num\":{rewardNum}}}]"; } private int ParseInt(string text) { return int.TryParse(text, out int value) ? value : 0; } } [Serializable] public class InviteRewardData { public int Id; public int Need; public int RewardItemId; public int RewardNum; public bool IsItemsValid; public int RowIndex; public string ItemsRawText; } }