feat: 增强数据导入功能与用户界面优化

- 更新 README.md,强调 Area 路径为必填项,并增加加载 Area 列表的功能,确保用户在导入时不会遗漏必要信息。
- 修改 Config.cs 和 config.json,调整 SkipExistingTitles 默认值为 false,以便用户首次导入时能看到真实冲突和报错。
- 在 MainForm.cs 中添加加载 Area 列表的按钮,优化用户交互体验。
- 更新 ExcelService.cs,增强模板导出逻辑,确保用户能够更方便地使用自定义模板。
- 调整界面布局,确保 Area 路径与迭代路径的选择更加直观,提升整体用户体验。
This commit is contained in:
chenshoulin
2026-11-02 10:40:52 +08:00
parent ffc210f827
commit 14f5e32262
7 changed files with 409 additions and 91 deletions
+32 -12
View File
@@ -88,10 +88,30 @@ namespace TfsExcelImporter
return null;
}
// ------------------------------------------------------------- 迭代
// ------------------------------------------------------------- 迭代 / Area
/// <summary>取项目下全部迭代完整路径(Tools_Dev\2026年Sprint\2026年Sprint10 形式),按路径排序。</summary>
public async Task<List<string>> GetIterationPathsAsync(string project, Action<string> log = null)
public Task<List<string>> GetIterationPathsAsync(string project, Action<string> log = null)
{
return GetClassificationPathsAsync(project, "ProjectLifecycle", "Iteration", "迭代", log);
}
/// <summary>
/// 取项目下全部 Area 完整路径(Tools_Dev\DataTechnologies 形式),按路径排序。
/// Area 与迭代同属分类结构(ICommonStructureService),只是根节点不同:
/// TFS 2013 里 Area 的 StructureType 固定为 ProjectModelHierarchy,迭代是 ProjectLifecycle。
/// </summary>
public Task<List<string>> GetAreaPathsAsync(string project, Action<string> log = null)
{
return GetClassificationPathsAsync(project, "ProjectModelHierarchy", "Area", "Area", log);
}
/// <summary>
/// 通用分类树抓取:按 StructureType 挑根节点(拿不到就用名字模糊匹配兜底),
/// GetNodesXml 深度展开整棵树,再取每个节点的 Path 属性。迭代与 Area 共用这一条通道。
/// </summary>
private async Task<List<string>> GetClassificationPathsAsync(string project, string structureType,
string nameHint, string label, Action<string> log)
{
var list = await Task.Run(() =>
{
@@ -101,13 +121,13 @@ namespace TfsExcelImporter
throw new InvalidOperationException("项目不存在或无权访问: " + project);
var structures = css.ListStructures(proj.Uri);
var iterations = structures.FirstOrDefault(n => n.StructureType == "ProjectLifecycle")
?? structures.FirstOrDefault(n => (n.Name ?? "").IndexOf("Iteration", StringComparison.OrdinalIgnoreCase) >= 0);
if (iterations == null)
throw new InvalidOperationException("未找到 Iterations(ProjectLifecycle) 结构");
var root = structures.FirstOrDefault(n => string.Equals(n.StructureType, structureType, StringComparison.OrdinalIgnoreCase))
?? structures.FirstOrDefault(n => (n.Name ?? "").IndexOf(nameHint, StringComparison.OrdinalIgnoreCase) >= 0);
if (root == null)
throw new InvalidOperationException($"未找到 {label} 结构(StructureType={structureType})");
// GetNodesXml 深度展开全树;Node 元素的 Path 属性为分类节点路径
var tree = css.GetNodesXml(new[] { iterations.Uri }, true);
// 深度展开全树;Node 元素的 Path 属性为分类节点路径
var tree = css.GetNodesXml(new[] { root.Uri }, true);
var raw = new List<string>();
if (tree != null)
{
@@ -120,20 +140,20 @@ namespace TfsExcelImporter
return raw;
});
var result = list.Select(p => NormalizeIterationPath(p, project))
var result = list.Select(p => NormalizeNodePath(p, project))
.Where(p => p != null)
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)
.ToList();
log?.Invoke($"迭代列表来源: TFS OM ICommonStructureService({result.Count} 个)");
log?.Invoke($"{label}列表来源: TFS OM ICommonStructureService({result.Count} 个)");
return result;
}
/// <summary>
/// 节点路径归一化为工作项 IterationPath(项目\子...)。
/// 分类节点路径归一化为工作项字段值(项目\子...),迭代与 Area 共用。
/// 兼容:含/不含项目名前缀、含/不含结构根段(Iteration/Iterations/Area/Areas)。
/// </summary>
private static string NormalizeIterationPath(string nodePath, string project)
private static string NormalizeNodePath(string nodePath, string project)
{
if (string.IsNullOrWhiteSpace(nodePath)) return null;
var segs = nodePath.Split('\\').Select(s => s.Trim()).Where(s => s.Length > 0).ToList();