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
+52 -21
View File
@@ -23,9 +23,10 @@ namespace TfsExcelImporter
private ComboBox _cmbIteration = new ComboBox();
private ComboBox _cmbArea = new ComboBox();
private TextBox _txtExcel = new TextBox();
private TextBox _txtParentPbi = new TextBox();
private ComboBox _cmbParent = new ComboBox();
private Button _btnLoadIterations = new Button();
private Button _btnLoadAreas = new Button();
private Button _btnLoadPbi = new Button();
private Button _btnTemplate = new Button();
private CheckBox _chkSkip = new CheckBox();
@@ -341,20 +342,25 @@ namespace TfsExcelImporter
_cardData.SummaryDots = true; // 头部摘要按「圆点 + 短语」分段,五项状态一眼可辨
_cmbIteration.DropDownStyle = ComboBoxStyle.DropDown; // 允许手填完整路径
_cmbArea.DropDownStyle = ComboBoxStyle.DropDown; // 同上:列表拿不到时仍能手填
_cmbParent.DropDownStyle = ComboBoxStyle.DropDown; // 可直接输 Id,也可从列表挑
// 不做自定义 DropDownWidth:字段已和迭代同宽,下拉面板跟着控件宽走,
// 形态与「目标迭代」完全一致,也不再有宽度写死后与实际布局对不上的问题。
_fIteration = NewField(_cmbIteration, 500, false);
_fArea = NewField(_cmbArea, 500, true);
_fExcel = NewField(_txtExcel, 500, true);
_fParent = NewField(_txtParentPbi, 150, true);
_fParent = NewField(_cmbParent, 500, true);
_fIteration.SetCue("选择或手填完整迭代路径,如 Tools_Dev\\Sprint 1");
// Area 现在是必填:留空会导致团队视图看不到新导入的工作项,所以提示里直接给写法
_fArea.SetCue("选择或手填完整 Area 路径,如 Tools_Dev\\DataTechnologies");
_fExcel.SetCue("选择 .xlsx 文件,或点右侧文件夹图标浏览");
_fParent.SetCue("如 219975");
// 行尾不再放说明文字,规则并进 cue:字段宽度与迭代一致后,这里放得下完整提示
_fParent.SetCue("选择或手填父 PBI Id,如 219975;Excel 里加 PBI Id 列可逐行覆盖");
_btnLoadIterations = NewButton("加载迭代列表", BtnKind.Secondary, IconKind.Fetch, 130);
_btnLoadAreas = NewButton("加载 Area 列表", BtnKind.Secondary, IconKind.Fetch, 130);
_btnLoadPbi = NewButton("加载 PBI 列表", BtnKind.Secondary, IconKind.Fetch, 130);
_btnTemplate = NewButton("导出 Excel 模板", BtnKind.Secondary, IconKind.Doc, 142);
// 行尾两个按钮宽度取齐(取两者较宽的一个):右缘本来就都由 AnchorRight 贴在 M 上,
// 宽度一致后左缘也落在同一条竖线上,两个按钮才成为一列;同时两行 Track 给右侧留的
@@ -362,8 +368,8 @@ namespace TfsExcelImporter
int wBtn = Math.Max(_btnLoadIterations.Width, _btnTemplate.Width);
_btnLoadIterations.Width = wBtn;
_btnLoadAreas.Width = wBtn;
_btnLoadPbi.Width = wBtn;
_btnTemplate.Width = wBtn;
_chkSkip = NewCheck("跳过已存在标题", "");
// 四行字段对齐同一条左缘:标签列宽取最大值;必填项的红色 * 也留进列宽里,
// 否则带 * 的行会把输入框往右推,四行左缘就不再是一条竖线。
@@ -383,12 +389,14 @@ namespace TfsExcelImporter
_fExcel.AddSuffix(IconKind.Folder, BrowseExcel);
AnchorRight(_btnTemplate, _cardData, M, _fExcel.Top);
Track(_fExcel, _cardData, wBtn + G + M, 0);
y = Row(_cardData, y, 42, new Lbl("父 PBI Id", wLbl), _fParent,
new Hint("Task 默认挂到该 backlog item;Excel 里加 PBI Id 列可逐行覆盖"));
// 勾选项不再贴着标签起头:整体右移到上面三行输入框的左缘(跳过标签列)。
// 只有「去重」这一个开关;字段校验不可绕过(OM 14.x 无 BypassRules),失败时由
// TfsClient 输出可操作的修复提示。
y = Row(_cardData, y, 32, _fParent.Left - M, _chkSkip);
// 父项行结构与上面三行完全一致:字段吃满宽度 + 行尾按钮右对齐。
// Id 本身不可读,列出来才敢确认挂的是哪条。
y = Row(_cardData, y, 52, new Lbl("父 PBI Id", wLbl), _fParent);
AnchorRight(_btnLoadPbi, _cardData, M, _fParent.Top);
Track(_fParent, _cardData, wBtn + G + M, 0);
// 「跳过已存在标题」不在这张卡片里:它是「开始导入」这个动作的开关,已挪到步骤 4 操作条
// 紧贴主按钮(卡片收起后只看得到一行摘要,勾没勾都看不见,容易被当成「没勾」)。
// 字段校验不可绕过(OM 14.x 无 BypassRules),失败时由 TfsClient 输出可操作的修复提示。
_errIteration = AddErrorUnder(_cardData, _fIteration);
_errArea = AddErrorUnder(_cardData, _fArea);
@@ -440,6 +448,10 @@ namespace TfsExcelImporter
// 148 已够图标+文字(实际量出约 130),省下的宽度留给右侧状态文字与进度条
_btnImport = NewButton("开始导入 TFS", BtnKind.Primary, IconKind.ArrowUp, 148);
// 主/次按钮同高(层级靠主色表达,不再用 38px 拔高),避免操作条里两个按钮大小不齐
// 去重开关紧贴主按钮右侧:它是「开始导入」这一个动作的开关(一次导入只在开始时读一次),
// 挂在按钮旁边才不会被误读成某个字段的属性。
_chkSkip = NewCheck("跳过已存在标题", "");
_pbar = new ProgressTrack
{
Minimum = 0,
@@ -463,6 +475,7 @@ namespace TfsExcelImporter
_actionBar.Controls.Add(_badgeRun);
_actionBar.Controls.Add(_btnPreview);
_actionBar.Controls.Add(_btnImport);
_actionBar.Controls.Add(_chkSkip);
_actionBar.Controls.Add(_pbar);
_actionBar.Controls.Add(_chipOk);
_actionBar.Controls.Add(_chipSkip);
@@ -484,19 +497,36 @@ namespace TfsExcelImporter
Theme.Place(_btnImport, _btnPreview.Right + G, (_actionBar.Height - _btnImport.Height) / 2,
_btnImport.Width, _btnImport.Height);
// 去重开关:与两个按钮同一条中线,纵向按勾选框自身高度居中
int chkW, chkH;
CheckBoxSize(_chkSkip, Theme.Body, out chkW, out chkH);
Theme.Place(_chkSkip, _btnImport.Right + Theme.S16, (_actionBar.Height - chkH) / 2, chkW, chkH);
Theme.Place(_chipFail, _actionBar.Width - _chipFail.Width - M - Theme.Px(2), cy, _chipFail.Width, _chipFail.Height);
Theme.Place(_chipSkip, _chipFail.Left - G - _chipSkip.Width, cy, _chipSkip.Width, _chipSkip.Height);
Theme.Place(_chipOk, _chipSkip.Left - G - _chipOk.Width, cy, _chipOk.Width, _chipOk.Height);
// 状态文字按最长常规文案实测给宽,且只给一行的高度:
// 宽度够了不折行,万一更长的文案也只省略尾巴,不会把计数拆到第二行。
int statW = StatusLabelWidth();
// 进度条至少要 120px 才像一条进度条(再窄就只是一段杂线):先按最长常规文案给状态文字留宽,
// 宽度不够时再收窄状态文字(下限 120)——长文案本来就靠省略尾巴兜底,关键是别把进度条挤没。
int left = _chkSkip.Right + Theme.S16;
int statRoom = _chipOk.Left - G - left - Theme.Px(120) - G;
int statW = Math.Min(StatusLabelWidth(), Math.Max(Theme.Px(120), statRoom));
int statH = Theme.Caption.Height + Theme.Px(4);
Theme.Place(_lblStatus, _chipOk.Left - statW - G, (_actionBar.Height - statH) / 2, statW, statH);
int left = _btnImport.Right + Theme.S16;
Theme.Place(_lblStatus, _chipOk.Left - G - statW, (_actionBar.Height - statH) / 2, statW, statH);
int barW = Math.Max(Theme.Px(40), _lblStatus.Left - G - left);
Theme.Place(_pbar, left, (_actionBar.Height - _pbar.Height) / 2, barW, _pbar.Height);
}
/// <summary>
/// net48 的 CheckBox 没有 PreferredWidth/Height 的稳定值:宽度自己量(文字 + 勾选框),
/// 高度取原生 PreferredHeight 并留 18px 下限,调用方据此按中线精确居中。
/// </summary>
private static void CheckBoxSize(CheckBox chk, Font font, out int width, out int height)
{
var pref = chk.GetPreferredSize(Size.Empty);
height = Math.Max(Theme.Px(18), pref.Height);
width = Math.Max(Theme.TextWidth(chk.Text, font) + Theme.Px(22), pref.Width);
}
/// <summary>
/// 操作条右侧状态文字的最大显示宽度:取各操作最长文案的实测宽度(160~280 之间夹取),
/// 保证常规状态既不折行、也不省略尾部计数。更长的文案(如「Area 修正完成…(详见日志)」)
@@ -555,11 +585,9 @@ namespace TfsExcelImporter
_btnOpenReport.Width, _btnOpenReport.Height);
Theme.Place(_btnClearLog, _btnOpenReport.Left - G - _btnClearLog.Width, cy - _btnClearLog.Height / 2,
_btnClearLog.Width, _btnClearLog.Height);
// net48 的 CheckBox 没有 PreferredWidth,只能自己量文字宽度(外加勾选框);
// 高度取原生 PreferredHeight,整体压在中线上
var pref = _chkAutoScroll.GetPreferredSize(Size.Empty);
int chkH = Math.Max(Theme.Px(18), pref.Height);
int chkW = Math.Max(Theme.TextWidth(_chkAutoScroll.Text, Theme.Caption) + Theme.Px(22), pref.Width);
// net48 的 CheckBox 没有 PreferredWidth:宽高都由 CheckBoxSize 量出来,整体压在中线上
int chkW, chkH;
CheckBoxSize(_chkAutoScroll, Theme.Caption, out chkW, out chkH);
Theme.Place(_chkAutoScroll, _btnClearLog.Left - chkW - G, cy - chkH / 2, chkW, chkH);
});
}
@@ -961,7 +989,7 @@ namespace TfsExcelImporter
string.IsNullOrWhiteSpace(_cmbArea.Text) ? "Area 未填" : "Area " + _cmbArea.Text.Trim(),
string.IsNullOrEmpty(fileName) ? "未选 Excel" : fileName,
_rows.Count > 0 ? _rows.Count + " 行" : "未预览",
string.IsNullOrWhiteSpace(_txtParentPbi.Text) ? "父 PBI 未填" : "父 PBI #" + _txtParentPbi.Text.Trim()
string.IsNullOrWhiteSpace(_cmbParent.Text) ? "父 PBI 未填" : "父 PBI #" + _cmbParent.Text.Trim()
}));
_cardPreview.SetSummary(_rows.Count > 0
? "共 " + _rows.Count + " 行,可直接开始导入"
@@ -1027,8 +1055,11 @@ namespace TfsExcelImporter
_btnTest.Enabled = !busy;
_btnLoadIterations.Enabled = !busy;
_btnLoadAreas.Enabled = !busy;
_btnLoadPbi.Enabled = !busy;
_btnFixArea.Enabled = !busy;
_btnLinkParent.Enabled = !busy;
// 去重开关在导入开始时只读一次:跑起来再改会误导(看着改了,本轮其实不生效),直接置灰
_chkSkip.Enabled = !busy;
_fExcel.SetSuffixEnabled(!busy); // 「浏览」已并入输入框尾部,随忙等一起禁用
_btnTemplate.Enabled = !busy;
}