using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace TfsExcelImporter { public class MainForm : Form { // ------------------------------------------------------------- 控件 private readonly TextBox _txtUrl = new TextBox(); private readonly TextBox _txtProject = new TextBox(); private readonly TextBox _txtType = new TextBox(); private readonly ComboBox _cmbAuth = new ComboBox(); private readonly TextBox _txtUser = new TextBox(); private readonly TextBox _txtPwd = new TextBox { UseSystemPasswordChar = true }; private readonly TextBox _txtDomain = new TextBox(); private readonly CheckBox _chkProxy = new CheckBox(); private readonly ComboBox _cmbIteration = new ComboBox(); private readonly Button _btnLoadIterations = new Button(); private readonly TextBox _txtExcel = new TextBox(); private readonly Button _btnBrowse = new Button(); private readonly Button _btnTemplate = new Button(); private readonly CheckBox _chkSkip = new CheckBox(); private readonly CheckBox _chkBypass = new CheckBox(); private readonly TextBox _txtParentPbi = new TextBox(); private readonly DataGridView _grid = new DataGridView(); private readonly Button _btnPreview = new Button(); private readonly Button _btnImport = new Button(); private readonly Button _btnFixArea = new Button(); private readonly Button _btnLinkParent = new Button(); private readonly TextBox _txtLog = new TextBox(); private readonly Label _lblStatus = new Label(); private AppConfig _cfg; private List _rows = new List(); public MainForm() { _cfg = AppConfig.Load(); BuildUi(); LoadConfigToUi(); } // ------------------------------------------------------------- UI 构建 private void BuildUi() { Text = "TFS 2013 工作项 Excel 导入工具"; ClientSize = new Size(1060, 720); MinimumSize = new Size(960, 640); StartPosition = FormStartPosition.CenterScreen; Font = new Font("Microsoft YaHei UI", 9F); int y = 12, left = 12, lblW = 84; // --- 服务器配置区 --- var grpServer = new GroupBox { Text = "TFS 连接", Location = new Point(left, y), Size = new Size(1036, 120), Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right }; Controls.Add(grpServer); AddLabel(grpServer, "服务器URL:", 12, 24, lblW); _txtUrl.Location = new Point(100, 22); _txtUrl.Size = new Size(460, 27); _txtUrl.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; grpServer.Controls.Add(_txtUrl); _chkProxy.Text = "使用系统代理"; _chkProxy.Location = new Point(564, 24); _chkProxy.Size = new Size(110, 24); _chkProxy.Anchor = AnchorStyles.Top | AnchorStyles.Right; _chkProxy.Checked = false; // 内网 TFS 默认直连 grpServer.Controls.Add(_chkProxy); AddLabel(grpServer, "团队项目:", 680, 24, lblW); _txtProject.Location = new Point(768, 22); _txtProject.Size = new Size(150, 27); _txtProject.Anchor = AnchorStyles.Top | AnchorStyles.Right; grpServer.Controls.Add(_txtProject); AddLabel(grpServer, "工作项类型:", 12, 58, lblW); _txtType.Location = new Point(100, 56); _txtType.Size = new Size(220, 27); grpServer.Controls.Add(_txtType); AddLabel(grpServer, "认证方式:", 340, 58, lblW); _cmbAuth.Location = new Point(428, 56); _cmbAuth.Size = new Size(110, 27); _cmbAuth.DropDownStyle = ComboBoxStyle.DropDownList; _cmbAuth.Items.AddRange(new object[] { "ntlm", "basic", "default" }); _cmbAuth.SelectedIndexChanged += (s, e) => UpdateAuthUi(); grpServer.Controls.Add(_cmbAuth); AddLabel(grpServer, "用户:", 556, 58, 44); _txtUser.Location = new Point(604, 56); _txtUser.Size = new Size(110, 27); grpServer.Controls.Add(_txtUser); AddLabel(grpServer, "密码:", 726, 58, 44); _txtPwd.Location = new Point(774, 56); _txtPwd.Size = new Size(110, 27); grpServer.Controls.Add(_txtPwd); AddLabel(grpServer, "域:", 896, 58, 28); _txtDomain.Location = new Point(928, 56); _txtDomain.Size = new Size(94, 27); _txtDomain.Anchor = AnchorStyles.Top | AnchorStyles.Right; grpServer.Controls.Add(_txtDomain); y += 128; // --- 导入选区 --- var grpImport = new GroupBox { Text = "导入选区", Location = new Point(left, y), Size = new Size(1036, 120), Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right }; Controls.Add(grpImport); AddLabel(grpImport, "目标Iteration:", 12, 26, 96); _cmbIteration.Location = new Point(112, 23); _cmbIteration.Size = new Size(560, 27); _cmbIteration.DropDownStyle = ComboBoxStyle.DropDown; // 允许手填 _cmbIteration.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; grpImport.Controls.Add(_cmbIteration); _btnLoadIterations.Text = "加载迭代列表"; _btnLoadIterations.Location = new Point(682, 22); _btnLoadIterations.Size = new Size(110, 29); _btnLoadIterations.Anchor = AnchorStyles.Top | AnchorStyles.Right; _btnLoadIterations.Click += async (s, e) => await LoadIterationsAsync(); grpImport.Controls.Add(_btnLoadIterations); AddLabel(grpImport, "Excel 文件:", 12, 62, 96); _txtExcel.Location = new Point(112, 59); _txtExcel.Size = new Size(560, 27); _txtExcel.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; grpImport.Controls.Add(_txtExcel); _btnBrowse.Text = "浏览..."; _btnBrowse.Location = new Point(682, 58); _btnBrowse.Size = new Size(70, 29); _btnBrowse.Anchor = AnchorStyles.Top | AnchorStyles.Right; _btnBrowse.Click += (s, e) => BrowseExcel(); grpImport.Controls.Add(_btnBrowse); _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(); grpImport.Controls.Add(_btnTemplate); _chkSkip.Text = "跳过已存在标题"; _chkSkip.Location = new Point(886, 26); _chkSkip.Size = new Size(130, 24); _chkSkip.Anchor = AnchorStyles.Top | AnchorStyles.Right; grpImport.Controls.Add(_chkSkip); _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); // 父 PBI:全部 Task 默认挂接的 Product Backlog Item(Excel 每行 PBI Id 列可覆盖) AddLabel(grpImport, "父PBI Id:", 12, 92, 96); _txtParentPbi.Location = new Point(112, 89); _txtParentPbi.Size = new Size(200, 27); grpImport.Controls.Add(_txtParentPbi); var lblPbiHint = new Label { Text = "Task 将挂接为该 Product Backlog Item 的子项;Excel 加 PBI Id 列可逐行指定不同父项", Location = new Point(320, 92), Size = new Size(560, 22), TextAlign = ContentAlignment.MiddleLeft, ForeColor = SystemColors.GrayText }; grpImport.Controls.Add(lblPbiHint); y += 128; // --- 预览表格 --- _grid.Location = new Point(left, y); _grid.Size = new Size(1036, 300); _grid.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom; _grid.ReadOnly = true; _grid.AllowUserToAddRows = false; _grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.DisplayedCells; _grid.RowHeadersVisible = false; _grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect; Controls.Add(_grid); y += 308; // --- 操作按钮 --- _btnPreview.Text = "预览数据"; _btnPreview.Location = new Point(left, y); _btnPreview.Size = new Size(110, 32); _btnPreview.Anchor = AnchorStyles.Bottom | AnchorStyles.Left; _btnPreview.Click += (s, e) => PreviewData(); Controls.Add(_btnPreview); _btnImport.Text = "开始导入 TFS"; _btnImport.Location = new Point(left + 122, y); _btnImport.Size = new Size(130, 32); _btnImport.Anchor = AnchorStyles.Bottom | AnchorStyles.Left; _btnImport.BackColor = Color.FromArgb(0, 120, 215); _btnImport.ForeColor = Color.White; _btnImport.Click += async (s, e) => await ImportAsync(); Controls.Add(_btnImport); _btnFixArea.Text = "按迭代修Area"; _btnFixArea.Location = new Point(left + 264, y); _btnFixArea.Size = new Size(130, 32); _btnFixArea.Anchor = AnchorStyles.Bottom | AnchorStyles.Left; _btnFixArea.Click += async (s, e) => await FixAreaAsync(); Controls.Add(_btnFixArea); _btnLinkParent.Text = "补挂父项"; _btnLinkParent.Location = new Point(left + 406, y); _btnLinkParent.Size = new Size(130, 32); _btnLinkParent.Anchor = AnchorStyles.Bottom | AnchorStyles.Left; _btnLinkParent.Click += async (s, e) => await LinkParentAsync(); Controls.Add(_btnLinkParent); _lblStatus.Text = "就绪"; _lblStatus.Location = new Point(left + 548, y + 6); _lblStatus.Size = new Size(400, 22); _lblStatus.Anchor = AnchorStyles.Bottom | AnchorStyles.Left; Controls.Add(_lblStatus); y += 40; // --- 日志 --- _txtLog.Location = new Point(left, y); _txtLog.Size = new Size(1036, 120); _txtLog.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; _txtLog.Multiline = true; _txtLog.ScrollBars = ScrollBars.Vertical; _txtLog.ReadOnly = true; Controls.Add(_txtLog); } private static void AddLabel(Control parent, string text, int x, int y, int w) { var lbl = new Label { Text = text, Location = new Point(x, y), Size = new Size(w, 22), TextAlign = ContentAlignment.MiddleLeft }; parent.Controls.Add(lbl); } private void UpdateAuthUi() { bool needCreds = _cmbAuth.SelectedItem != null && _cmbAuth.SelectedItem.ToString() != "default"; _txtUser.Enabled = needCreds; _txtPwd.Enabled = needCreds; _txtDomain.Enabled = needCreds; } // ------------------------------------------------------------- 配置互转 private void LoadConfigToUi() { _txtUrl.Text = _cfg.TfsUrl; _txtProject.Text = _cfg.Project; _txtType.Text = _cfg.WorkItemType; _cmbAuth.SelectedItem = _cfg.AuthMode; if (_cmbAuth.SelectedItem == null) _cmbAuth.SelectedItem = "ntlm"; _txtUser.Text = _cfg.Username; _txtPwd.Text = _cfg.Password; _txtDomain.Text = _cfg.Domain; _chkProxy.Checked = _cfg.UseSystemProxy; _cmbIteration.Text = _cfg.IterationPath; _txtExcel.Text = _cfg.LastExcelPath; _chkSkip.Checked = _cfg.SkipExistingTitles; _chkBypass.Checked = _cfg.BypassRules; _txtParentPbi.Text = _cfg.ParentPbiId; UpdateAuthUi(); } private void SaveUiToConfig() { _cfg.TfsUrl = _txtUrl.Text.Trim(); _cfg.Project = _txtProject.Text.Trim(); _cfg.WorkItemType = _txtType.Text.Trim(); _cfg.AuthMode = _cmbAuth.SelectedItem != null ? _cmbAuth.SelectedItem.ToString() : "ntlm"; _cfg.Username = _txtUser.Text.Trim(); _cfg.Password = _txtPwd.Text; _cfg.Domain = _txtDomain.Text.Trim(); _cfg.UseSystemProxy = _chkProxy.Checked; _cfg.IterationPath = _cmbIteration.Text.Trim(); _cfg.LastExcelPath = _txtExcel.Text.Trim(); _cfg.SkipExistingTitles = _chkSkip.Checked; _cfg.BypassRules = _chkBypass.Checked; _cfg.ParentPbiId = _txtParentPbi.Text.Trim(); _cfg.Save(); } // ------------------------------------------------------------- 日志 private void Log(string msg) { if (InvokeRequired) { BeginInvoke(new Action(() => Log(msg))); return; } _txtLog.AppendText($"[{DateTime.Now:HH:mm:ss}] {msg}{Environment.NewLine}"); } // ------------------------------------------------------------- 迭代加载 private async Task LoadIterationsAsync() { SaveUiToConfig(); _btnLoadIterations.Enabled = false; try { using (var client = new TfsClient(_cfg)) { Log("测试连接..."); Log(await client.TestConnectionAsync()); // 提前探测导入所需字段接口,避免导入时才发现路由缺失 try { var fm = await client.GetFieldMapAsync(); Log($"字段接口可用(字段 {fm.Count} 个)"); } catch (Exception ex) { Log("⚠ 字段接口(_apis/wit/fields)不可用,导入可能受影响: " + ex.Message.Split('\n')[0]); } var paths = await client.GetIterationPathsAsync(_cfg.Project, Log); _cmbIteration.Items.Clear(); foreach (var p in paths) _cmbIteration.Items.Add(p); if (_cmbIteration.Items.Count > 0 && string.IsNullOrWhiteSpace(_cmbIteration.Text)) _cmbIteration.SelectedIndex = 0; // 优先选中配置中的迭代 for (int i = 0; i < _cmbIteration.Items.Count; i++) { if (string.Equals(_cmbIteration.Items[i].ToString(), _cfg.IterationPath, StringComparison.OrdinalIgnoreCase)) { _cmbIteration.SelectedIndex = i; break; } } Log($"已加载 {_cmbIteration.Items.Count} 个迭代路径"); } } catch (Exception ex) { Log("加载迭代失败: " + ex.Message); MessageBox.Show(this, "加载迭代失败:\n" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { _btnLoadIterations.Enabled = true; } } // ------------------------------------------------------------- Excel private void BrowseExcel() { using (var dlg = new OpenFileDialog()) { dlg.Filter = "Excel 文件 (*.xlsx;*.xlsm)|*.xlsx;*.xlsm|所有文件 (*.*)|*.*"; dlg.Title = "选择要导入的 Excel 文件"; if (!string.IsNullOrEmpty(_txtExcel.Text)) { try { dlg.InitialDirectory = Path.GetDirectoryName(_txtExcel.Text); } catch { } } if (dlg.ShowDialog(this) != DialogResult.OK) return; _txtExcel.Text = dlg.FileName; SaveUiToConfig(); PreviewData(); } } private void GenerateTemplate() { using (var dlg = new SaveFileDialog()) { dlg.Filter = "Excel 文件 (*.xlsx)|*.xlsx"; dlg.FileName = "TFS导入模板.xlsx"; dlg.Title = "保存 Excel 模板"; if (dlg.ShowDialog(this) != DialogResult.OK) return; try { ExcelService.GenerateTemplate(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, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } private void PreviewData() { var path = _txtExcel.Text.Trim(); if (string.IsNullOrEmpty(path) || !File.Exists(path)) { MessageBox.Show(this, "请先选择有效的 Excel 文件", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } try { _rows = ExcelService.ReadRows(path); _grid.DataSource = null; _grid.DataSource = _rows.Select((r, i) => new { 行号 = i + 1, r.Title, r.State, 指派 = r.AssignedTo, 财务项目 = r.FinancialProject, r.Completed, LogDate = r.LogDate, Created = r.CreatedDate, Closed = r.ClosedDate, r.Notes }).ToList(); _lblStatus.Text = $"已读取 {_rows.Count} 行"; Log($"读取 {path} 共 {_rows.Count} 行"); SaveUiToConfig(); } catch (Exception ex) { Log("读取 Excel 失败: " + ex.Message); MessageBox.Show(this, "读取 Excel 失败:\n" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // ------------------------------------------------------------- Area 修正 private async Task FixAreaAsync() { SaveUiToConfig(); if (string.IsNullOrWhiteSpace(_cfg.IterationPath)) { MessageBox.Show(this, "请先选择目标 Iteration", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (string.IsNullOrWhiteSpace(_cfg.AreaPath)) { MessageBox.Show(this, "请先在 config.json 中设置 AreaPath(如 Tools_Dev\\DataTechnologies)", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } var confirm = MessageBox.Show(this, $"将把 迭代「{_cfg.IterationPath}」下 类型={_cfg.WorkItemType} 且 Area≠{_cfg.AreaPath} 的工作项\nArea 改为 {_cfg.AreaPath}。\n\n注意:已关闭(如 Done)的工作项会被临时重新打开,改完 Area 后自动还原状态。\n\n确认执行?", "确认", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (confirm != DialogResult.Yes) return; _btnFixArea.Enabled = false; try { using (var client = new TfsClient(_cfg)) { var r = await client.FixAreaAsync(_cfg.Project, _cfg.IterationPath, _cfg.WorkItemType, _cfg.AreaPath, Log); var summary = r.Item2 > 0 ? $"Area 修正完成:成功 {r.Item1} 条,失败 {r.Item2} 条(详见日志)" : $"Area 修正完成,共 {r.Item1} 条"; _lblStatus.Text = summary; Log(summary); MessageBox.Show(this, summary, r.Item2 > 0 ? "部分完成" : "完成", MessageBoxButtons.OK, r.Item2 > 0 ? MessageBoxIcon.Warning : MessageBoxIcon.Information); } } catch (Exception ex) { Log("Area 修正失败: " + ex.Message); MessageBox.Show(this, "Area 修正失败:\n" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { _btnFixArea.Enabled = true; } } // ------------------------------------------------------------- 补挂父项 private async Task LinkParentAsync() { SaveUiToConfig(); if (string.IsNullOrWhiteSpace(_cfg.IterationPath)) { MessageBox.Show(this, "请先选择目标 Iteration", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (!int.TryParse(_cfg.ParentPbiId, out var parentId)) { MessageBox.Show(this, "请先在「父PBI Id」输入框填入父工作项 Id(纯数字)", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } var confirm = MessageBox.Show(this, $"将把 迭代「{_cfg.IterationPath}」下 类型={_cfg.WorkItemType} 且尚无父项的工作项\n补挂为 #{parentId} 的子项。\n\n已有父项的自动跳过;已关闭的工作项会临时重新打开,挂完自动还原状态。\n\n确认执行?", "确认", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (confirm != DialogResult.Yes) return; _btnLinkParent.Enabled = false; try { using (var client = new TfsClient(_cfg)) { var r = await client.LinkExistingAsync(_cfg.Project, _cfg.IterationPath, _cfg.WorkItemType, parentId, Log); var summary = $"补挂完成: 挂接 {r.Item1} / 已有父跳过 {r.Item2} / 失败 {r.Item3}"; _lblStatus.Text = summary; Log(summary); MessageBox.Show(this, summary + "\n挂接后在积压工作页展开父项即可看到子项。", r.Item3 > 0 ? "部分完成" : "完成", MessageBoxButtons.OK, r.Item3 > 0 ? MessageBoxIcon.Warning : MessageBoxIcon.Information); } } catch (Exception ex) { Log("补挂父项失败: " + ex.Message); MessageBox.Show(this, "补挂父项失败:\n" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { _btnLinkParent.Enabled = true; } } // ------------------------------------------------------------- 导入 private async Task ImportAsync() { SaveUiToConfig(); if (string.IsNullOrWhiteSpace(_cfg.IterationPath)) { MessageBox.Show(this, "请先选择目标 Iteration(可点击“加载迭代列表”)", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } var path = _txtExcel.Text.Trim(); if (string.IsNullOrEmpty(path) || !File.Exists(path)) { MessageBox.Show(this, "请先选择有效的 Excel 文件", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (_rows.Count == 0) { try { _rows = ExcelService.ReadRows(path); } catch (Exception ex) { MessageBox.Show(this, "读取 Excel 失败:\n" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } } if (_rows.Count == 0) { MessageBox.Show(this, "Excel 中没有可导入的数据行", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } _btnImport.Enabled = false; _btnPreview.Enabled = false; int ok = 0, skip = 0, fail = 0; var report = new StringBuilder(); report.AppendLine("行号,Title,结果,WorkItemId,错误"); try { using (var client = new TfsClient(_cfg)) { Log("测试连接..."); Log(await client.TestConnectionAsync()); // 字段解析 var fieldMap = await client.GetFieldMapAsync(); var titleRef = "System.Title"; var stateRef = "System.State"; 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 ?? "(未找到)"}"); Log($"父PBI: '{(string.IsNullOrWhiteSpace(_cfg.ParentPbiId) ? "(未填)" : _cfg.ParentPbiId)}'——Excel 每行可用 PBI Id 列覆盖"); HashSet existing = null; if (_chkSkip.Checked) { Log("查询已有工作项标题..."); existing = await client.GetExistingTitlesAsync(_cfg.Project, _cfg.IterationPath, _cfg.WorkItemType); Log($"服务器已有 {existing.Count} 条"); } for (int i = 0; i < _rows.Count; i++) { var row = _rows[i]; var line = i + 2; // Excel 实际行号(含表头) try { if (existing != null && existing.Contains(row.Title)) { skip++; report.AppendLine($"{line},\"{row.Title}\",SKIP,,标题已存在"); Log($"[{i + 1}/{_rows.Count}] SKIP {row.Title}"); continue; } var fields = new List> { new KeyValuePair(titleRef, row.Title), new KeyValuePair(stateRef, string.IsNullOrWhiteSpace(row.State) ? _cfg.DefaultState : row.State.Trim()), new KeyValuePair(iterationRef, _cfg.IterationPath) }; if (!string.IsNullOrWhiteSpace(row.AssignedTo)) fields.Add(new KeyValuePair(assignedRef, row.AssignedTo.Trim())); if (!string.IsNullOrWhiteSpace(_cfg.AreaPath)) fields.Add(new KeyValuePair(areaRef, _cfg.AreaPath.Trim())); if (!string.IsNullOrWhiteSpace(row.FinancialProject) && finRef != null) fields.Add(new KeyValuePair(finRef, row.FinancialProject.Trim())); var completed = ExcelService.ToNumber(row.Completed); if (completed != null && compRef != null) fields.Add(new KeyValuePair(compRef, completed)); // Notes -> Description(该 Task 模板必填) if (!string.IsNullOrWhiteSpace(row.Notes)) fields.Add(new KeyValuePair("System.Description", row.Notes.Trim())); var logDate = ExcelService.ToIsoDateTime(row.LogDate); if (logDate != null && logRef != null) fields.Add(new KeyValuePair(logRef, logDate)); // 系统日期字段:bypassRules 时尝试写入 if (_chkBypass.Checked) { var created = ExcelService.ToIsoDateTime(row.CreatedDate); if (created != null) fields.Add(new KeyValuePair(createdRef, created)); var closed = ExcelService.ToIsoDateTime(row.ClosedDate); if (closed != null) fields.Add(new KeyValuePair(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; var id = await client.CreateWorkItemAsync(_cfg.Project, _cfg.WorkItemType, fields, _chkBypass.Checked, parentPbi); ok++; report.AppendLine($"{line},\"{row.Title}\",OK,{id},"); Log($"[{i + 1}/{_rows.Count}] OK #{id} {row.Title}"); } catch (Exception ex) { fail++; report.AppendLine($"{line},\"{row.Title}\",FAIL,,\"{ex.Message.Replace("\"", "'")}\""); Log($"[{i + 1}/{_rows.Count}] FAIL {row.Title} -> {ex.Message}"); } _lblStatus.Text = $"导入中 {i + 1}/{_rows.Count}..."; } } } catch (Exception ex) { Log("导入中断: " + ex.Message); MessageBox.Show(this, "导入中断:\n" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { _btnImport.Enabled = true; _btnPreview.Enabled = true; try { var reportPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "import_report_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".csv"); File.WriteAllText(reportPath, report.ToString(), Encoding.UTF8); Log($"报告已保存: {reportPath}"); } catch (Exception ex) { Log("写报告失败: " + ex.Message); } _lblStatus.Text = $"完成: 成功 {ok} / 跳过 {skip} / 失败 {fail}"; Log($"汇总: 成功 {ok} / 跳过 {skip} / 失败 {fail}"); MessageBox.Show(this, $"导入完成。\n成功 {ok} / 跳过 {skip} / 失败 {fail}\n详见日志与报告 CSV。", "完成", MessageBoxButtons.OK, MessageBoxIcon.Information); } } } }