feat: 增强数据导入功能与用户界面优化
- 更新 README.md,强调 Area 路径为必填项,并增加加载 Area 列表的功能,确保用户在导入时不会遗漏必要信息。 - 修改 Config.cs 和 config.json,调整 SkipExistingTitles 默认值为 false,以便用户首次导入时能看到真实冲突和报错。 - 在 MainForm.cs 中添加加载 Area 列表的按钮,优化用户交互体验。 - 更新 ExcelService.cs,增强模板导出逻辑,确保用户能够更方便地使用自定义模板。 - 调整界面布局,确保 Area 路径与迭代路径的选择更加直观,提升整体用户体验。
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user