using System; using System.Collections.Generic; using System.Data; using System.IO; using System.Linq; using ClosedXML.Excel; namespace TfsExcelImporter { /// 一行待导入的工作项数据(列顺序与 Excel 模板一致)。 public class WorkItemRow { public string Title { get; set; } public string State { get; set; } public string AssignedTo { get; set; } public string FinancialProject { get; set; } public string Completed { get; set; } public string LogDate { get; set; } public string CreatedDate { get; set; } public string ClosedDate { get; set; } public string PbiId { get; set; } public string Notes { get; set; } } /// /// Excel 读取与模板生成。 /// 模板列: Title | State | Assigned To | FinancialProject | Completed | LogDate | Created Date | Closed Date | PBI Id | Notes /// public static class ExcelService { public static readonly string[] Headers = { "Title", "State", "Assigned To", "FinancialProject", "Completed", "LogDate", "Created Date", "Closed Date", "PBI Id", "Notes" }; private static readonly string[] DateFormats = { "yyyy-MM-dd HH:mm:ss", "yyyy/MM/dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm", "yyyy-MM-dd", "yyyy/MM/dd" }; // --------------------------------------------------------- 生成模板 public static void GenerateTemplate(string path) { using (var wb = new XLWorkbook()) { var ws = wb.Worksheets.Add("工作项"); for (int i = 0; i < Headers.Length; i++) { var cell = ws.Cell(1, i + 1); cell.Value = Headers[i]; cell.Style.Font.Bold = true; cell.Style.Fill.BackgroundColor = XLColor.LightGray; } // 示例行(To Do / Done 各一,帮助理解格式) var sample1 = new WorkItemRow { Title = "20261012 - 示例:货载报告解析并上传凭证受托报告相关内容", State = "To Do", AssignedTo = "李晓柯", FinancialProject = "信托 - 上海信托", Completed = "5", LogDate = "2026-10-12", CreatedDate = "2026-10-30 11:14:41", ClosedDate = "", Notes = "示例行,导入前请删除" }; var sample2 = new WorkItemRow { Title = "20261009 示例:受托报告bug修复", State = "Done", AssignedTo = "陈卫宇", FinancialProject = "信托 - 上海信托", Completed = "8", LogDate = "2026-10-09", CreatedDate = "2026-10-10 18:20:00", ClosedDate = "2026-10-30 18:00:00", Notes = "示例行,导入前请删除" }; WriteRow(ws, 2, sample1); WriteRow(ws, 3, sample2); ws.Columns().AdjustToContents(); ws.SheetView.FreezeRows(1); wb.SaveAs(path); } } private static void WriteRow(IXLWorksheet ws, int r, WorkItemRow row) { ws.Cell(r, 1).Value = row.Title ?? ""; ws.Cell(r, 2).Value = row.State ?? ""; ws.Cell(r, 3).Value = row.AssignedTo ?? ""; ws.Cell(r, 4).Value = row.FinancialProject ?? ""; ws.Cell(r, 5).Value = row.Completed ?? ""; ws.Cell(r, 6).Value = row.LogDate ?? ""; ws.Cell(r, 7).Value = row.CreatedDate ?? ""; ws.Cell(r, 8).Value = row.ClosedDate ?? ""; ws.Cell(r, 9).Value = row.PbiId ?? ""; ws.Cell(r, 10).Value = row.Notes ?? ""; } // --------------------------------------------------------- 读取数据 /// 读取 Excel(或同格式 CSV 由用户另存为 xlsx)数据行,跳过空标题行。 public static List ReadRows(string path) { var rows = new List(); using (var wb = new XLWorkbook(path)) { var ws = wb.Worksheets.First(); var range = ws.RangeUsed(); if (range == null) return rows; // 按表头名定位列(顺序不限),找不到的列按 Headers 顺序兜底 var headerMap = MapHeaders(ws, range); for (int r = range.FirstRow().RowNumber() + 1; r <= range.LastRow().RowNumber(); r++) { var title = CellText(ws, r, headerMap, "Title"); if (string.IsNullOrWhiteSpace(title)) continue; rows.Add(new WorkItemRow { Title = title.Trim(), State = CellText(ws, r, headerMap, "State"), AssignedTo = CellText(ws, r, headerMap, "Assigned To"), FinancialProject = CellText(ws, r, headerMap, "FinancialProject"), Completed = CellText(ws, r, headerMap, "Completed"), LogDate = CellText(ws, r, headerMap, "LogDate"), CreatedDate = CellText(ws, r, headerMap, "Created Date"), ClosedDate = CellText(ws, r, headerMap, "Closed Date"), PbiId = CellText(ws, r, headerMap, "PBI Id"), Notes = CellText(ws, r, headerMap, "Notes") }); } } return rows; } private static Dictionary MapHeaders(IXLWorksheet ws, IXLRange range) { var map = new Dictionary(StringComparer.OrdinalIgnoreCase); var firstRow = range.FirstRow().RowNumber(); int lastCol = range.LastColumn().ColumnNumber(); for (int c = 1; c <= lastCol; c++) { var text = ws.Cell(firstRow, c).GetString().Trim(); if (text.Length == 0) continue; // 归一化:去掉空格/下划线,兼容 "Assigned To" / "AssignedTo" / "Created Date" 等 var key = text.Replace(" ", "").Replace("_", ""); if (!map.ContainsKey(key)) map[key] = c; } // 兜底:模板标准列名 foreach (var h in Headers) { var key = h.Replace(" ", ""); if (!map.ContainsKey(key)) { int idx = Array.IndexOf(Headers, h) + 1; if (idx <= lastCol) map[key] = idx; } } return map; } private static string CellText(IXLWorksheet ws, int row, Dictionary map, string header) { int col; if (!map.TryGetValue(header.Replace(" ", ""), out col)) return ""; var cell = ws.Cell(row, col); if (cell.IsEmpty()) return ""; if (cell.DataType == XLDataType.DateTime) return cell.GetDateTime().ToString("yyyy-MM-dd HH:mm:ss"); if (cell.DataType == XLDataType.Number) { double d; if (double.TryParse(cell.GetString(), out d)) { // 处理 Excel 日期序列号(如 46300.0) if (d > 20000 && d < 80000 && Math.Abs(d - Math.Floor(d)) < 0.001) { try { return DateTime.FromOADate(d).ToString("yyyy-MM-dd HH:mm:ss"); } catch { } } return d.ToString("0.####"); } } return cell.GetString().Trim(); } // --------------------------------------------------------- 时间转换 /// 把单元格文本转成 TFS 接受的 ISO 时间;空值返回 null;解析失败抛异常。 public static string ToIsoDateTime(string value) { if (string.IsNullOrWhiteSpace(value)) return null; value = value.Trim(); foreach (var fmt in DateFormats) { DateTime dt; if (DateTime.TryParseExact(value, fmt, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out dt)) return dt.ToString("yyyy-MM-ddTHH:mm:ss"); } DateTime any; if (DateTime.TryParse(value, out any)) return any.ToString("yyyy-MM-ddTHH:mm:ss"); throw new FormatException("无法解析时间: " + value); } /// Completed 数值:整数去小数点,非法值返回 null。 public static object ToNumber(string value) { if (string.IsNullOrWhiteSpace(value)) return null; double d; if (!double.TryParse(value.Trim(), out d)) throw new FormatException("无法解析数值: " + value); return Math.Abs(d - Math.Floor(d)) < 0.0001 ? (object)(int)Math.Floor(d) : d; } } }