Files
TFSImport/TfsExcelImporter/ExcelService.cs
T
chenshoulin 8d1af0d11c chore: 初始化 TFS 2013 工作项批量导入工具集
- TfsExcelImporter: WinForms 桌面版,走 TFS Client OM(本环境 wit REST 路由 404,OM 是唯一可行通道)
- tfs_import.py: REST 版命令行导入器,供 wit 接口可用的 TFS 实例使用
- templates: Excel 导入模板及其 OOXML 源部件
- README: 两套工具定位、配置、环境坑点与验证方式
- 真实台账与本机工作笔记不入库,仅提交 work_items.example.csv 说明列结构
2026-11-01 11:21:20 +08:00

227 lines
9.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using ClosedXML.Excel;
namespace TfsExcelImporter
{
/// <summary>一行待导入的工作项数据(列顺序与 Excel 模板一致)。</summary>
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; }
}
/// <summary>
/// Excel 读取与模板生成。
/// 模板列: Title | State | Assigned To | FinancialProject | Completed | LogDate | Created Date | Closed Date | PBI Id | Notes
/// </summary>
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 ?? "";
}
// --------------------------------------------------------- 读取数据
/// <summary>读取 Excel(或同格式 CSV 由用户另存为 xlsx)数据行,跳过空标题行。</summary>
public static List<WorkItemRow> ReadRows(string path)
{
var rows = new List<WorkItemRow>();
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<string, int> MapHeaders(IXLWorksheet ws, IXLRange range)
{
var map = new Dictionary<string, int>(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<string, int> 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();
}
// --------------------------------------------------------- 时间转换
/// <summary>把单元格文本转成 TFS 接受的 ISO 时间;空值返回 null;解析失败抛异常。</summary>
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);
}
/// <summary>Completed 数值:整数去小数点,非法值返回 null。</summary>
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;
}
}
}