refactor: 更新 README 和代码以支持默认模板导出功能
- README.md: 修改数据源描述,明确默认模板随程序发布,并更新操作流程中的按钮名称。 - ExcelService.cs: 重命名模板生成方法为 CopyTemplate,简化模板导出逻辑。 - MainForm.cs: 更新按钮文本和事件处理,改为导出默认模板。 - Config.cs 和 config.json: 添加 OriginalEstimateRef 字段以支持新功能。 - 删除不再使用的 build_template.ps1 脚本及相关模板部件。
This commit is contained in:
@@ -36,6 +36,8 @@ namespace TfsExcelImporter
|
||||
public string FinancialProjectRef { get; set; } = "";
|
||||
public string CompletedRef { get; set; } = "";
|
||||
public string LogDateRef { get; set; } = "";
|
||||
// Original Estimate 导入时取 Completed 的值;引用名留空则按显示名解析
|
||||
public string OriginalEstimateRef { get; set; } = "";
|
||||
|
||||
public string LastExcelPath { get; set; } = "";
|
||||
|
||||
|
||||
@@ -16,22 +16,23 @@ namespace TfsExcelImporter
|
||||
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; }
|
||||
public string Description { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excel 读取与模板生成。
|
||||
/// 模板列: Title | State | Assigned To | FinancialProject | Completed | LogDate | Created Date | Closed Date | PBI Id | Notes
|
||||
/// Excel 读取与默认模板。模板内容(列宽、示例行、样式)只在
|
||||
/// templates\TFS导入模板.xlsx 里维护,随构建复制到 exe 目录后原样导出。
|
||||
/// 列: Title | State | Assigned To | FinancialProject | Completed | LogDate | PBI Id | Description
|
||||
/// </summary>
|
||||
public static class ExcelService
|
||||
{
|
||||
public const string TemplateFileName = "TFS导入模板.xlsx";
|
||||
|
||||
public static readonly string[] Headers =
|
||||
{
|
||||
"Title", "State", "Assigned To", "FinancialProject",
|
||||
"Completed", "LogDate", "Created Date", "Closed Date", "PBI Id", "Notes"
|
||||
"Completed", "LogDate", "PBI Id", "Description"
|
||||
};
|
||||
|
||||
private static readonly string[] DateFormats =
|
||||
@@ -41,68 +42,15 @@ namespace TfsExcelImporter
|
||||
"yyyy-MM-dd", "yyyy/MM/dd"
|
||||
};
|
||||
|
||||
// --------------------------------------------------------- 生成模板
|
||||
// --------------------------------------------------------- 默认模板
|
||||
|
||||
public static void GenerateTemplate(string path)
|
||||
/// <summary>把随程序发布的默认模板原样复制到目标路径。</summary>
|
||||
public static void CopyTemplate(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 ?? "";
|
||||
var src = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, TemplateFileName);
|
||||
if (!File.Exists(src))
|
||||
throw new FileNotFoundException("未找到默认模板文件: " + src, src);
|
||||
File.Copy(src, path, true);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------- 读取数据
|
||||
@@ -132,10 +80,8 @@ namespace TfsExcelImporter
|
||||
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")
|
||||
Description = CellText(ws, r, headerMap, "Description")
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -151,7 +97,7 @@ namespace TfsExcelImporter
|
||||
{
|
||||
var text = ws.Cell(firstRow, c).GetString().Trim();
|
||||
if (text.Length == 0) continue;
|
||||
// 归一化:去掉空格/下划线,兼容 "Assigned To" / "AssignedTo" / "Created Date" 等
|
||||
// 归一化:去掉空格/下划线,兼容 "Assigned To" / "AssignedTo" / "PBI Id" 等
|
||||
var key = text.Replace(" ", "").Replace("_", "");
|
||||
if (!map.ContainsKey(key)) map[key] = c;
|
||||
}
|
||||
@@ -159,11 +105,12 @@ namespace TfsExcelImporter
|
||||
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;
|
||||
}
|
||||
if (map.ContainsKey(key)) continue;
|
||||
int idx = Array.IndexOf(Headers, h) + 1;
|
||||
if (idx > lastCol) continue;
|
||||
// 该列已有别的表头占用(如旧版模板的 Created Date 列)时不套用位置,避免读错列
|
||||
bool taken = map.Values.Any(c => c == idx);
|
||||
if (!taken) map[key] = idx;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -144,10 +144,10 @@ namespace TfsExcelImporter
|
||||
_btnBrowse.Click += (s, e) => BrowseExcel();
|
||||
grpImport.Controls.Add(_btnBrowse);
|
||||
|
||||
_btnTemplate.Text = "生成Excel模板";
|
||||
_btnTemplate.Text = "导出Excel模板";
|
||||
_btnTemplate.Location = new Point(760, 58); _btnTemplate.Size = new Size(110, 29);
|
||||
_btnTemplate.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||||
_btnTemplate.Click += (s, e) => GenerateTemplate();
|
||||
_btnTemplate.Click += (s, e) => ExportTemplate();
|
||||
grpImport.Controls.Add(_btnTemplate);
|
||||
|
||||
_chkSkip.Text = "跳过已存在标题";
|
||||
@@ -155,7 +155,7 @@ namespace TfsExcelImporter
|
||||
_chkSkip.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||||
grpImport.Controls.Add(_chkSkip);
|
||||
|
||||
_chkBypass.Text = "bypassRules(可写系统日期)";
|
||||
_chkBypass.Text = "bypassRules(绕过字段规则)";
|
||||
_chkBypass.Location = new Point(886, 60); _chkBypass.Size = new Size(150, 24);
|
||||
_chkBypass.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||||
grpImport.Controls.Add(_chkBypass);
|
||||
@@ -357,25 +357,25 @@ namespace TfsExcelImporter
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateTemplate()
|
||||
private void ExportTemplate()
|
||||
{
|
||||
using (var dlg = new SaveFileDialog())
|
||||
{
|
||||
dlg.Filter = "Excel 文件 (*.xlsx)|*.xlsx";
|
||||
dlg.FileName = "TFS导入模板.xlsx";
|
||||
dlg.Title = "保存 Excel 模板";
|
||||
dlg.FileName = ExcelService.TemplateFileName;
|
||||
dlg.Title = "导出默认模板";
|
||||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||||
try
|
||||
{
|
||||
ExcelService.GenerateTemplate(dlg.FileName);
|
||||
Log("模板已生成: " + dlg.FileName);
|
||||
MessageBox.Show(this, "模板已生成:\n" + dlg.FileName, "完成",
|
||||
ExcelService.CopyTemplate(dlg.FileName);
|
||||
Log("模板已导出: " + dlg.FileName);
|
||||
MessageBox.Show(this, "模板已导出:\n" + dlg.FileName, "完成",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log("生成模板失败: " + ex.Message);
|
||||
MessageBox.Show(this, "生成模板失败:\n" + ex.Message, "错误",
|
||||
Log("导出模板失败: " + ex.Message);
|
||||
MessageBox.Show(this, "导出模板失败:\n" + ex.Message, "错误",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
@@ -402,9 +402,8 @@ namespace TfsExcelImporter
|
||||
财务项目 = r.FinancialProject,
|
||||
r.Completed,
|
||||
LogDate = r.LogDate,
|
||||
Created = r.CreatedDate,
|
||||
Closed = r.ClosedDate,
|
||||
r.Notes
|
||||
r.PbiId,
|
||||
r.Description
|
||||
}).ToList();
|
||||
_lblStatus.Text = $"已读取 {_rows.Count} 行";
|
||||
Log($"读取 {path} 共 {_rows.Count} 行");
|
||||
@@ -563,13 +562,12 @@ namespace TfsExcelImporter
|
||||
var assignedRef = "System.AssignedTo";
|
||||
var iterationRef = "System.IterationPath";
|
||||
var areaRef = "System.AreaPath";
|
||||
var createdRef = "System.CreatedDate";
|
||||
var closedRef = "Microsoft.VSTS.Common.ClosedDate";
|
||||
|
||||
var finRef = TfsClient.ResolveRef(fieldMap, "FinancialProject", _cfg.FinancialProjectRef);
|
||||
var compRef = TfsClient.ResolveRef(fieldMap, "Completed", _cfg.CompletedRef);
|
||||
var logRef = TfsClient.ResolveRef(fieldMap, "LogDate", _cfg.LogDateRef);
|
||||
Log($"字段: FinancialProject->{finRef ?? "(未找到)"} Completed->{compRef ?? "(未找到)"} LogDate->{logRef ?? "(未找到)"}");
|
||||
var origEstRef = TfsClient.ResolveRef(fieldMap, "Original Estimate", _cfg.OriginalEstimateRef);
|
||||
Log($"字段: FinancialProject->{finRef ?? "(未找到)"} Completed->{compRef ?? "(未找到)"} LogDate->{logRef ?? "(未找到)"} OriginalEstimate->{origEstRef ?? "(未找到)"}");
|
||||
Log($"父PBI: '{(string.IsNullOrWhiteSpace(_cfg.ParentPbiId) ? "(未填)" : _cfg.ParentPbiId)}'——Excel 每行可用 PBI Id 列覆盖");
|
||||
|
||||
HashSet<string> existing = null;
|
||||
@@ -612,25 +610,17 @@ namespace TfsExcelImporter
|
||||
if (completed != null && compRef != null)
|
||||
fields.Add(new KeyValuePair<string, object>(compRef, completed));
|
||||
|
||||
// Notes -> Description(该 Task 模板必填)
|
||||
if (!string.IsNullOrWhiteSpace(row.Notes))
|
||||
fields.Add(new KeyValuePair<string, object>("System.Description", row.Notes.Trim()));
|
||||
// Original Estimate 默认取 Completed 的值
|
||||
if (completed != null && origEstRef != null)
|
||||
fields.Add(new KeyValuePair<string, object>(origEstRef, completed));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(row.Description))
|
||||
fields.Add(new KeyValuePair<string, object>("System.Description", row.Description.Trim()));
|
||||
|
||||
var logDate = ExcelService.ToIsoDateTime(row.LogDate);
|
||||
if (logDate != null && logRef != null)
|
||||
fields.Add(new KeyValuePair<string, object>(logRef, logDate));
|
||||
|
||||
// 系统日期字段:bypassRules 时尝试写入
|
||||
if (_chkBypass.Checked)
|
||||
{
|
||||
var created = ExcelService.ToIsoDateTime(row.CreatedDate);
|
||||
if (created != null)
|
||||
fields.Add(new KeyValuePair<string, object>(createdRef, created));
|
||||
var closed = ExcelService.ToIsoDateTime(row.ClosedDate);
|
||||
if (closed != null)
|
||||
fields.Add(new KeyValuePair<string, object>(closedRef, closed));
|
||||
}
|
||||
|
||||
// 父 PBI:行级 PBI Id 列优先,其次界面默认值
|
||||
var pbiText = !string.IsNullOrWhiteSpace(row.PbiId) ? row.PbiId.Trim() : _cfg.ParentPbiId;
|
||||
int? parentPbi = int.TryParse(pbiText, out var pbiNum) ? pbiNum : (int?)null;
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
<None Update="config.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<!-- 默认导入模板:随 exe 一起发布,「导出Excel模板」原样复制这份文件 -->
|
||||
<Content Include="..\templates\TFS导入模板.xlsx" Link="TFS导入模板.xlsx">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"FinancialProjectRef": "",
|
||||
"CompletedRef": "",
|
||||
"LogDateRef": "",
|
||||
"OriginalEstimateRef": "",
|
||||
"LastExcelPath": "",
|
||||
"ParentPbiId": ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user