feat: 增加父 PBI 列表加载功能与界面优化

- 更新 README.md,明确父 PBI Id 的选择方式,增加加载 PBI 列表的功能,提升用户在导入时的便利性。
- 在 MainForm.cs 中添加加载 PBI 列表的按钮,优化父项选择逻辑,确保用户能够方便地选择合适的父项。
- 修改 MainFormUi.cs,将父 PBI Id 输入框替换为下拉框,支持从列表中选择,提升用户体验。
- 在 TfsClient.cs 中实现获取父项候选的逻辑,确保用户能够根据项目和迭代路径获取相关的工作项信息。
- 调整界面布局,确保父 PBI 选择与其他输入项一致,提升整体视觉效果。
This commit is contained in:
chenshoulin
2026-11-02 13:07:58 +08:00
parent 14f5e32262
commit fe23e1c64a
4 changed files with 273 additions and 48 deletions
+91
View File
@@ -169,6 +169,97 @@ namespace TfsExcelImporter
return project + "\\" + string.Join("\\", segs);
}
// ------------------------------------------------------------- 父项(backlog item)
/// <summary>下拉用的轻量工作项快照:只要 Id / 标题 / 类型 / 状态 / 迭代。</summary>
public class WorkItemBrief
{
public int Id;
public string Title;
public string Type;
public string State;
public string IterationPath;
/// <summary>
/// 列表里给人看的文本:Id 打头,后面是标题;状态只在「非 New」时追加。
/// 新建未审批的 PBI 全是 New,逐条显示纯属噪音——真正要看的是 Done/Closed 这类
/// 挂上去也展不开的父项,让异常状态自己显形就够了。
/// </summary>
public override string ToString()
{
var tail = string.IsNullOrWhiteSpace(State)
|| string.Equals(State, "New", StringComparison.OrdinalIgnoreCase)
? "" : " · " + State;
return "#" + Id + " " + (Title ?? "") + tail;
}
}
/// <summary>
/// 查可当父项用的工作项(默认 Product Backlog Item)。
/// 先按目标迭代过滤;该迭代下一条都没有时放宽到全项目——迭代里还没建 PBI 是常态,
/// 直接返回空列表会让人以为「接口没通」。
/// </summary>
public async Task<List<WorkItemBrief>> GetBacklogItemsAsync(string project, string iterationPath,
Action<string> log = null)
{
var scoped = await QueryWorkItemsAsync(project, iterationPath, "Product Backlog Item");
if (scoped.Count > 0)
{
log?.Invoke($"父项候选: 迭代「{iterationPath}」下的 Product Backlog Item {scoped.Count} 条");
return scoped;
}
log?.Invoke("父项候选: 该迭代下没有 Product Backlog Item,放宽到全项目");
var all = await QueryWorkItemsAsync(project, null, "Product Backlog Item");
if (all.Count > 0)
{
log?.Invoke($"父项候选: 全项目 Product Backlog Item {all.Count} 条");
return all;
}
// 非敏捷模板(CMMI/Scrum 的老项目)没有 PBI 类型:退化为「除 Task 外的所有工作项」
var fallback = await QueryWorkItemsAsync(project, iterationPath, null, excludeType: "Task");
log?.Invoke($"父项候选: 未找到 Product Backlog Item,退回「非 Task 工作项」{fallback.Count} 条");
return fallback;
}
/// <summary>
/// WIQL 取工作项快照。workItemType 为空时按 excludeType 反向过滤(排除导入类型本身)。
/// iterationPath 为空时不限迭代。
/// </summary>
private async Task<List<WorkItemBrief>> QueryWorkItemsAsync(string project, string iterationPath,
string workItemType, string excludeType = null)
{
return await Task.Run(() =>
{
var wiql = new StringBuilder(
"SELECT [System.Id], [System.Title], [System.WorkItemType], [System.State], [System.IterationPath] " +
"FROM WorkItems WHERE ");
wiql.Append($"[System.TeamProject] = '{EscapeWiql(project)}'");
if (!string.IsNullOrWhiteSpace(iterationPath))
wiql.Append($" AND [System.IterationPath] = '{EscapeWiql(iterationPath)}'");
if (!string.IsNullOrWhiteSpace(workItemType))
wiql.Append($" AND [System.WorkItemType] = '{EscapeWiql(workItemType)}'");
else if (!string.IsNullOrWhiteSpace(excludeType))
wiql.Append($" AND [System.WorkItemType] <> '{EscapeWiql(excludeType)}'");
wiql.Append(" ORDER BY [System.Id]");
var list = new List<WorkItemBrief>();
foreach (WorkItem wi in Store.Query(wiql.ToString()))
{
list.Add(new WorkItemBrief
{
Id = wi.Id,
Title = wi.Title,
Type = Convert.ToString(wi.Fields["System.WorkItemType"].Value),
State = Convert.ToString(wi.Fields["System.State"].Value),
IterationPath = Convert.ToString(wi.Fields["System.IterationPath"].Value)
});
}
return list;
});
}
// ------------------------------------------------------------- 查重
/// <summary>查询指定迭代下同类型工作项的标题集合。</summary>