diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.ExcelIO.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.ExcelIO.cs
new file mode 100644
index 0000000..d34ff18
--- /dev/null
+++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.ExcelIO.cs
@@ -0,0 +1,433 @@
+using System;
+using System.IO;
+
+using OfficeOpenXml;
+using OfficeOpenXml.Style;
+
+using UnityEngine;
+
+using XericUI.XTable.Core;
+
+namespace XericUI.XTable.Rendering.Component
+{
+ ///
+ /// XTable Excel 导入导出——通过 EPPlus 实现 .xlsx 文件的读写。
+ ///
+ public partial class XericUIActionTable
+ {
+ #region 导入
+
+ ///
+ /// 从 Excel .xlsx 文件加载数据到表格。
+ /// 支持文本、合并单元格、列宽、行高的导入。
+ ///
+ /// Excel 文件路径
+ /// 工作表索引(0-based),默认 0 即第一个工作表
+ public void LoadFromExcelFile(string filePath, int sheetIndex = 0)
+ {
+ if (string.IsNullOrEmpty(filePath))
+ {
+ Debug.LogWarning("[XTable] LoadFromExcelFile: 文件路径为空");
+ return;
+ }
+
+ if (!File.Exists(filePath))
+ {
+ Debug.LogWarning($"[XTable] LoadFromExcelFile: 文件不存在: {filePath}");
+ return;
+ }
+
+ try
+ {
+ using (var package = new ExcelPackage(new FileInfo(filePath)))
+ {
+ if (sheetIndex < 0 || sheetIndex >= package.Workbook.Worksheets.Count)
+ {
+ Debug.LogWarning($"[XTable] LoadFromExcelFile: 工作表索引 {sheetIndex} 超出范围(共 {package.Workbook.Worksheets.Count} 个工作表)");
+ return;
+ }
+ ExcelWorksheet ws = package.Workbook.Worksheets[sheetIndex];
+ if (ws == null)
+ {
+ Debug.LogWarning("[XTable] LoadFromExcelFile: 工作簿中没有工作表");
+ return;
+ }
+
+ // 确定行列数
+ int rows = 0, cols = 0;
+ if (ws.Dimension != null)
+ {
+ rows = ws.Dimension.Rows;
+ cols = ws.Dimension.Columns;
+ }
+
+ if (rows <= 0 || cols <= 0)
+ {
+ Debug.LogWarning("[XTable] LoadFromExcelFile: Excel 文件为空或无法识别行列数");
+ return;
+ }
+
+ // 确保数据层存在
+ if (m_TableData == null)
+ m_TableData = new XTableData();
+
+ // 设置行高
+ m_RowHeights = new float[rows];
+ for (int r = 0; r < rows; r++)
+ {
+ float h = (float)ws.Row(r + 1).Height;
+ m_RowHeights[r] = h > 0 ? h : Config.DefaultCellHeight;
+ }
+
+ // 设置列宽
+ m_ColWidths = new float[cols];
+ for (int c = 0; c < cols; c++)
+ {
+ float w = (float)ws.Column(c + 1).Width;
+ m_ColWidths[c] = w > 0 ? w : Config.DefaultCellWidth;
+ }
+
+ // 读取单元格文本
+ for (int r = 0; r < rows; r++)
+ {
+ for (int c = 0; c < cols; c++)
+ {
+ object val = ws.Cells[r + 1, c + 1].Value;
+ if (val != null)
+ {
+ string text = val.ToString();
+ if (!string.IsNullOrEmpty(text))
+ {
+ var cell = m_TableData.GetOrCreateCell(r, c);
+ cell.Text = text;
+ }
+ }
+ }
+ }
+
+ // 读取合并单元格
+ if (ws.MergedCells.Count > 0)
+ {
+ m_TableData.ClearAllMerges();
+ foreach (string mergeAddr in ws.MergedCells)
+ {
+ try
+ {
+ var addr = new ExcelAddressBase(mergeAddr);
+ int startR = addr.Start.Row - 1; // 1-based → 0-based
+ int startC = addr.Start.Column - 1;
+ int endR = addr.End.Row - 1;
+ int endC = addr.End.Column - 1;
+ int rowSpan = endR - startR + 1;
+ int colSpan = endC - startC + 1;
+
+ if (rowSpan > 1 || colSpan > 1)
+ {
+ if (endR < rows && endC < cols)
+ m_TableData.MergeCells(startR, startC, rowSpan, colSpan);
+ }
+ }
+ catch
+ {
+ // 忽略无法解析的合并地址
+ }
+ }
+ }
+
+ // 标记脏并刷新
+ MarkDirty(TableDirtyType.DataChanged | TableDirtyType.LayoutChanged);
+ RecalculateTotalSize();
+ RefreshView();
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogError($"[XTable] LoadFromExcelFile 失败: {ex.Message}\n{ex.StackTrace}");
+ }
+ }
+
+ #endregion
+
+ #region 导出
+
+ ///
+ /// 将表格数据导出为 Excel .xlsx 文件。
+ /// 支持文本、合并单元格、列宽、行高、样式(颜色/字体/填充)、边框缓存的导出。
+ ///
+ /// Excel 文件路径
+ /// 工作表名称,默认 "Sheet1"
+ public void SaveToExcelFile(string filePath, string sheetName = "Sheet1")
+ {
+ if (string.IsNullOrEmpty(filePath))
+ {
+ Debug.LogWarning("[XTable] SaveToExcelFile: 文件路径为空");
+ return;
+ }
+
+ // 确保目录存在
+ string dir = Path.GetDirectoryName(filePath);
+ if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
+ Directory.CreateDirectory(dir);
+
+ try
+ {
+ using (var package = new ExcelPackage())
+ {
+ if (string.IsNullOrEmpty(sheetName))
+ sheetName = "Sheet1";
+ ExcelWorksheet ws = package.Workbook.Worksheets.Add(sheetName);
+
+ int rows = m_RowHeights?.Length ?? 0;
+ int cols = m_ColWidths?.Length ?? 0;
+
+ if (rows == 0 || cols == 0)
+ {
+ // 空表格:创建空文件
+ package.SaveAs(new FileInfo(filePath));
+ return;
+ }
+
+ // 设置列宽
+ for (int c = 0; c < cols; c++)
+ ws.Column(c + 1).Width = m_ColWidths[c];
+
+ // 设置行高
+ for (int r = 0; r < rows; r++)
+ {
+ float h = m_RowHeights[r];
+ if (h > 0)
+ ws.Row(r + 1).Height = h;
+ }
+
+ // 写入单元格数据和样式
+ for (int r = 0; r < rows; r++)
+ {
+ for (int c = 0; c < cols; c++)
+ {
+ // 跳过被合并覆盖的单元格(稍后通过合并区域统一处理)
+ if (m_TableData != null && m_TableData.IsCellMerged(r, c))
+ continue;
+
+ var cell = m_TableData?.GetCell(r, c);
+ if (cell == null) continue;
+
+ var excelCell = ws.Cells[r + 1, c + 1];
+
+ // 写入文本
+ if (!string.IsNullOrEmpty(cell.Text))
+ excelCell.Value = cell.Text;
+
+ // 应用样式
+ string ns = !string.IsNullOrEmpty(cell.StyleNamespace)
+ ? cell.StyleNamespace
+ : m_DefaultStyleNamespace;
+ if (!string.IsNullOrEmpty(ns))
+ ApplyStyleToExcelCell(excelCell, ns);
+ }
+ }
+
+ // 写入合并单元格
+ if (m_TableData != null)
+ {
+ WriteMergedCellsToExcel(ws, rows, cols);
+ }
+
+ // 写入边框缓存
+ if (m_TableData != null)
+ {
+ WriteBorderCacheToExcel(ws, rows, cols);
+ }
+
+ // 写入表格外边框
+ if (m_TableData != null && m_TableData.OuterBorderNs != null && m_TableData.OuterBorderNs.Count > 0)
+ {
+ var outerRange = ws.Cells[1, 1, rows, cols];
+ var border = outerRange.Style.Border;
+ border.BorderAround(ExcelBorderStyle.Thin,
+ System.Drawing.Color.FromArgb(180, 180, 180));
+ }
+
+ // 保存
+ package.SaveAs(new FileInfo(filePath));
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogError($"[XTable] SaveToExcelFile 失败: {ex.Message}\n{ex.StackTrace}");
+ }
+ }
+
+ #endregion
+
+ #region 辅助方法
+
+ ///
+ /// 将 XTable 的 StyleNamespace 映射为 Excel 单元格样式(字体颜色、背景、字号、对齐)。
+ ///
+ private void ApplyStyleToExcelCell(ExcelRange excelCell, string ns)
+ {
+ if (string.IsNullOrEmpty(ns)) return;
+
+ var style = excelCell.Style;
+
+ // 字体颜色
+ Color fgColor = GetCellFgColor(ns);
+ style.Font.Color.SetColor(System.Drawing.Color.FromArgb(
+ (byte)(fgColor.a * 255),
+ (byte)(fgColor.r * 255),
+ (byte)(fgColor.g * 255),
+ (byte)(fgColor.b * 255)));
+
+ // 背景填充
+ Color bgColor = GetCellBgColor(ns);
+ style.Fill.PatternType = ExcelFillStyle.Solid;
+ style.Fill.BackgroundColor.SetColor(System.Drawing.Color.FromArgb(
+ (byte)(bgColor.a * 255),
+ (byte)(bgColor.r * 255),
+ (byte)(bgColor.g * 255),
+ (byte)(bgColor.b * 255)));
+
+ // 字体大小
+ int fontSize = GetCellFontSize(ns);
+ if (fontSize > 0)
+ style.Font.Size = fontSize;
+
+ // 对齐
+ style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
+ style.VerticalAlignment = ExcelVerticalAlignment.Center;
+ }
+
+ ///
+ /// 将表格中的合并描述写入 Excel 工作表。
+ ///
+ private void WriteMergedCellsToExcel(ExcelWorksheet ws, int rows, int cols)
+ {
+ if (m_TableData == null) return;
+
+ foreach (var kv in m_TableData.BlockMap)
+ {
+ var block = kv.Value;
+ if (block.MergeDescriptors == null || block.MergeDescriptors.Count == 0) continue;
+
+ foreach (var desc in block.MergeDescriptors)
+ {
+ if (desc.IsCrossBlock) continue; // 跨块合并暂不处理
+
+ // 计算全局坐标
+ int blockRow = block.BlockRow;
+ int blockCol = block.BlockCol;
+ int globalStartRow = blockRow * m_TableData.BlockSizeY + desc.LocalStartRow;
+ int globalStartCol = blockCol * m_TableData.BlockSizeX + desc.LocalStartCol;
+
+ int endR = globalStartRow + desc.MergeRowSpan - 1;
+ int endC = globalStartCol + desc.MergeColSpan - 1;
+
+ // 检查边界
+ if (endR >= rows || endC >= cols) continue;
+
+ var range = ws.Cells[globalStartRow + 1, globalStartCol + 1, endR + 1, endC + 1];
+ range.Merge = true;
+ }
+ }
+ }
+
+ ///
+ /// 将表格的边框缓存(右侧/底部边框样式)写入 Excel 单元格边框。
+ ///
+ private void WriteBorderCacheToExcel(ExcelWorksheet ws, int rows, int cols)
+ {
+ if (m_TableData == null) return;
+
+ for (int r = 0; r < rows; r++)
+ {
+ for (int c = 0; c < cols; c++)
+ {
+ // 跳过被合并覆盖的单元格
+ if (m_TableData.IsCellMerged(r, c)) continue;
+
+ var excelCell = ws.Cells[r + 1, c + 1];
+
+ // 右侧边框
+ string rightNs = m_TableData.GetBorderRightNs(r, c);
+ if (!string.IsNullOrEmpty(rightNs))
+ {
+ Color color = GetCellBorderColor(rightNs);
+ float width = GetCellBorderWidth(rightNs);
+ excelCell.Style.Border.Right.Style = BorderWidthToStyle(width);
+ excelCell.Style.Border.Right.Color.SetColor(
+ System.Drawing.Color.FromArgb(
+ (byte)(color.a * 255),
+ (byte)(color.r * 255),
+ (byte)(color.g * 255),
+ (byte)(color.b * 255)));
+ }
+
+ // 底部边框
+ string bottomNs = m_TableData.GetBorderBottomNs(r, c);
+ if (!string.IsNullOrEmpty(bottomNs))
+ {
+ Color color = GetCellBorderColor(bottomNs);
+ float width = GetCellBorderWidth(bottomNs);
+ excelCell.Style.Border.Bottom.Style = BorderWidthToStyle(width);
+ excelCell.Style.Border.Bottom.Color.SetColor(
+ System.Drawing.Color.FromArgb(
+ (byte)(color.a * 255),
+ (byte)(color.r * 255),
+ (byte)(color.g * 255),
+ (byte)(color.b * 255)));
+ }
+ }
+ }
+
+ // 顶部外框(第一行顶部)
+ for (int c = 0; c < cols; c++)
+ {
+ string topNs = m_TableData.GetBorderBottomNs(-1, c);
+ if (!string.IsNullOrEmpty(topNs))
+ {
+ Color color = GetCellBorderColor(topNs);
+ float width = GetCellBorderWidth(topNs);
+ ws.Cells[1, c + 1].Style.Border.Top.Style = BorderWidthToStyle(width);
+ ws.Cells[1, c + 1].Style.Border.Top.Color.SetColor(
+ System.Drawing.Color.FromArgb(
+ (byte)(color.a * 255),
+ (byte)(color.r * 255),
+ (byte)(color.g * 255),
+ (byte)(color.b * 255)));
+ }
+ }
+
+ // 左侧外框(第一列左侧)
+ for (int r = 0; r < rows; r++)
+ {
+ string leftNs = m_TableData.GetBorderRightNs(r, -1);
+ if (!string.IsNullOrEmpty(leftNs))
+ {
+ Color color = GetCellBorderColor(leftNs);
+ float width = GetCellBorderWidth(leftNs);
+ ws.Cells[r + 1, 1].Style.Border.Left.Style = BorderWidthToStyle(width);
+ ws.Cells[r + 1, 1].Style.Border.Left.Color.SetColor(
+ System.Drawing.Color.FromArgb(
+ (byte)(color.a * 255),
+ (byte)(color.r * 255),
+ (byte)(color.g * 255),
+ (byte)(color.b * 255)));
+ }
+ }
+ }
+
+ ///
+ /// 将边框宽度值映射到 Excel 边框样式枚举。
+ ///
+ private static ExcelBorderStyle BorderWidthToStyle(float width)
+ {
+ if (width <= 0.5f) return ExcelBorderStyle.Hair;
+ if (width <= 1.0f) return ExcelBorderStyle.Thin;
+ if (width <= 1.5f) return ExcelBorderStyle.Medium;
+ if (width <= 2.0f) return ExcelBorderStyle.Medium;
+ return ExcelBorderStyle.Thick;
+ }
+
+ #endregion
+ }
+}