refactor: 更新 README 和代码以支持默认模板导出功能
- README.md: 修改数据源描述,明确默认模板随程序发布,并更新操作流程中的按钮名称。 - ExcelService.cs: 重命名模板生成方法为 CopyTemplate,简化模板导出逻辑。 - MainForm.cs: 更新按钮文本和事件处理,改为导出默认模板。 - Config.cs 和 config.json: 添加 OriginalEstimateRef 字段以支持新功能。 - 删除不再使用的 build_template.ps1 脚本及相关模板部件。
This commit is contained in:
@@ -18,7 +18,7 @@ TFS 2013 工作项批量导入工具集,用于把 Excel/CSV 里的工时台账
|
||||
| --- | --- | --- |
|
||||
| 通信协议 | TFS Client OM(VS/Team Explorer 同款内部 SOAP) | REST `/_apis/wit/*` |
|
||||
| 适用 | **本环境可用**。该 TFS 2013 实例只有 `/_apis/projects` 通,`wit/fields`、`wit/classificationnodes`、`wit/wiql` 全部 404 | 服务器开放了 wit REST 路由时使用 |
|
||||
| 数据源 | `.xlsx`(内置模板) | `data/work_items.csv`(GBK/UTF-8 均可,需自备) |
|
||||
| 数据源 | `.xlsx`(默认模板随程序发布) | `data/work_items.csv`(GBK/UTF-8 均可,需自备) |
|
||||
| 界面 | 图形界面,预览后导入 | 命令行,`--dry-run` 先校验 |
|
||||
|
||||
Python 版在本环境会因 REST 路由 404 而失败,保留用于其它 TFS 实例或接口可用时的批量脚本化导入。
|
||||
@@ -33,14 +33,12 @@ TFSImport/
|
||||
├── data/
|
||||
│ └── work_items.example.csv # 列结构与示例行(UTF-8 BOM);真实台账不入库
|
||||
├── templates/
|
||||
│ ├── TFS导入模板.xlsx # Excel 模板成品
|
||||
│ ├── build_template.ps1 # 由 _parts 里的 XML 打包生成 xlsx
|
||||
│ └── _parts/ # xlsx 的 OOXML 源文件
|
||||
│ └── TFS导入模板.xlsx # 默认模板:csproj 随构建拷到 exe 目录,「导出Excel模板」原样复制它
|
||||
└── TfsExcelImporter/ # C# WinForms 桌面版(主用)
|
||||
├── TfsExcelImporter.csproj # net48 + ClosedXML + TFS ExtendedClient 14.*
|
||||
├── MainForm.cs # 界面与导入/修 Area 流程
|
||||
├── TfsClient.cs # TFS Client OM 封装
|
||||
├── ExcelService.cs # 读 Excel、生成模板、时间/数值归一
|
||||
├── ExcelService.cs # 读 Excel、导出默认模板、时间/数值归一
|
||||
├── Config.cs # exe 同目录 config.json 读写
|
||||
└── config.json # 桌面版配置
|
||||
```
|
||||
@@ -55,11 +53,13 @@ dotnet build TfsExcelImporter.sln -c Release
|
||||
TfsExcelImporter\bin\Release\net48\TfsExcelImporter.exe
|
||||
```
|
||||
|
||||
「导出Excel模板」复制的是 exe 同目录的 `TFS导入模板.xlsx`(构建时自动拷过去);单独分发 exe 时要带上这个文件,否则该按钮会报「未找到默认模板文件」。
|
||||
|
||||
操作流程:
|
||||
|
||||
1. 填「TFS 连接」区:URL、项目、工作项类型、认证方式与域账号;内网保持「使用系统代理」**不勾选**。
|
||||
2. 点「加载迭代列表」,选择目标迭代。
|
||||
3. 点「生成Excel模板」拿到表头,或「浏览...」选择已有 `.xlsx`;点「预览数据」核对。
|
||||
3. 点「导出Excel模板」拿到默认模板,或「浏览...」选择已有 `.xlsx`;点「预览数据」核对。
|
||||
4. 需要挂父项时填「父 PBI Id」(默认全部挂同一个;Excel 里加 `PBI Id` 列可逐行指定不同父项)。
|
||||
5. 点「开始导入 TFS」,日志区逐行输出 `OK/SKIP/FAIL` 与工作项 ID。
|
||||
6. 历史数据 Area 不对时用「按迭代修Area」批量纠正。
|
||||
@@ -89,14 +89,28 @@ python tfs_import.py --input other.csv --limit 5
|
||||
| `auth.mode` | `ntlm`(域账号,典型)/ `basic` / `default`(当前 Windows 凭据,仅桌面版) |
|
||||
| `field_refs` | 语义名 → 字段引用名;自定义字段留空时按 `resolve_fields_by_display_name` 的显示名在服务器字段表里查(支持前缀模糊匹配,如 `Completed` → `Completed Work`) |
|
||||
| `parent_title` / `parent_id` | 父工作项,二选一;按标题匹配不到会直接报错并提示改用 `parent_id` |
|
||||
| `bypass_rules` | 是否尝试写入 `CreatedDate`/`ClosedDate` 等系统日期 |
|
||||
| `bypass_rules` | 是否请求绕过字段校验规则;桌面版走 Client OM(14.x 无 `SaveFlags.BypassRules`),实际由服务器规则决定 |
|
||||
| `skip_existing_titles` | 同项目/同类型/同迭代下按标题去重 |
|
||||
|
||||
> 配置里含明文密码,请勿提交到共享仓库;`config.json` 保持空值,凭据本地填写。
|
||||
|
||||
## Excel / CSV 列
|
||||
|
||||
`Title`、`State`、`Assigned To`、`FinancialProject`、`Completed`、`LogDate`、`Created Date`、`Closed Date`、`PBI Id`、`Notes`
|
||||
桌面版 Excel 模板 = 仓库里的 `templates\TFS导入模板.xlsx`,界面「导出Excel模板」直接复制它:
|
||||
|
||||
`Title`、`State`、`Assigned To`、`FinancialProject`、`Completed`、`LogDate`、`PBI Id`、`Description`
|
||||
|
||||
写入 TFS 的对应关系:
|
||||
|
||||
| 模板列 | 目标字段 |
|
||||
| --- | --- |
|
||||
| `Description` | `System.Description` |
|
||||
| `Completed` | 显示名 `Completed`(模糊匹配到 `Completed Work`)解析出的字段 |
|
||||
| —(无列) | `Original Estimate` 取该行 `Completed` 的值;引用名留空则按显示名 `Original Estimate` 解析,可写在 `OriginalEstimateRef` 里固定 |
|
||||
|
||||
系统日期列(`Created Date` / `Closed Date`)已从模板移除:`System.CreatedDate` 由服务器按导入时间填写,`Microsoft.VSTS.Common.ClosedDate` 不再写入、保持为空。
|
||||
|
||||
Python 版仍读 `data/work_items.example.csv` 的旧列(含 `CreatedDate`、`ClosedDate`、`Notes`),未随模板调整。
|
||||
|
||||
真实台账含人员与项目信息,不入库;本地准备数据文件:
|
||||
|
||||
@@ -104,7 +118,7 @@ python tfs_import.py --input other.csv --limit 5
|
||||
copy data\work_items.example.csv data\work_items.csv # Python 版默认读这个路径,已在 .gitignore 里
|
||||
```
|
||||
|
||||
- 表头按名匹配,顺序不限,`AssignedTo` / `Assigned To` 等价;缺列自动按模板顺序兜底。
|
||||
- 表头按名匹配,顺序不限,`AssignedTo` / `Assigned To` 等价;缺列按模板顺序兜底,但该列已被别的表头占用时不套用位置(旧版模板文件不会串列)。
|
||||
- 空 `Title` 的行直接跳过;`State` 为空时用 `default_state`;未识别的列(如 `Group`)忽略。
|
||||
- 日期支持 `yyyy-MM-dd[ / ][ HH:mm[:ss]]` 等格式,Excel 日期序列号(如 `46300`)会被还原成日期。
|
||||
- CSV 从 Excel 另存时注意编码,脚本按 `utf-8-sig` → `gbk` 依次尝试。
|
||||
@@ -116,7 +130,7 @@ copy data\work_items.example.csv data\work_items.csv # Python 版默认读这
|
||||
- **wit REST 路由全 404**:TFS 2013 RTM/低 Update 不带 `/_apis/wit/*`,`Classification.asmx?WSDL` 也被禁 → 桌面版整体走 Client OM(NuGet `Microsoft.TeamFoundationServer.ExtendedClient` 钉 `14.*`,对应 VS2015 客户端代际;15.x 不兼容 TFS 2013)。
|
||||
- **502 Bad Gateway**:TFS OM 内部用 `HttpWebRequest`,默认跟随 IE 系统代理,代理会拦内网 TFS。桌面版默认把 `WebRequest.DefaultWebProxy` 置空直连;确实要走代理时勾「使用系统代理」并带上 Windows 凭据过代理认证。
|
||||
- **TF400276 / 关闭状态只读**:改已关闭工作项的 Area 会被拒;`FixAreaAsync` 走「重新打开 → 改 Area → 还原状态」的状态循环,中途失败回滚状态。
|
||||
- **`System.CreatedDate` 写不进去**:OM 14.x 没有 `SaveFlags.BypassRules`(15.x 才加),该字段服务器托管;`Microsoft.VSTS.Common.ClosedDate` 在 `State=Done` 时尝试写入,校验或保存失败自动降级为普通保存,保证行不丢。
|
||||
- **`System.CreatedDate` 写不进去**:OM 14.x 没有 `SaveFlags.BypassRules`(15.x 才加),该字段服务器托管;`Microsoft.VSTS.Common.ClosedDate` 在 `State=Done` 时按普通字段可尝试写入。模板已去掉日期列,所以导入不再走这条路——若某模板要求 `Done` 必填 Closed Date,需另加列或改回写日期。
|
||||
- **孤立 Task 在积压工作页不可见**:TFS 2013 的 backlog/任务板按「父项 → 子任务」组织,没挂父链接的 Task 只能在查询里看到,所以导入必须带 `parent_title`/`parent_id`。
|
||||
- **DNS 解析失败 / 超时**:多为 VPN 或网络断开,不是代码问题;桌面版会对这类异常附加排查提示。
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ namespace TfsExcelImporter
|
||||
public string FinancialProjectRef { get; set; } = "";
|
||||
public string CompletedRef { get; set; } = "";
|
||||
public string LogDateRef { get; set; } = "";
|
||||
// Original Estimate 导入时取 Completed 的值;引用名留空则按显示名解析
|
||||
public string OriginalEstimateRef { get; set; } = "";
|
||||
|
||||
public string LastExcelPath { get; set; } = "";
|
||||
|
||||
|
||||
@@ -16,22 +16,23 @@ namespace TfsExcelImporter
|
||||
public string FinancialProject { get; set; }
|
||||
public string Completed { get; set; }
|
||||
public string LogDate { get; set; }
|
||||
public string CreatedDate { get; set; }
|
||||
public string ClosedDate { get; set; }
|
||||
public string PbiId { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public string Description { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excel 读取与模板生成。
|
||||
/// 模板列: Title | State | Assigned To | FinancialProject | Completed | LogDate | Created Date | Closed Date | PBI Id | Notes
|
||||
/// Excel 读取与默认模板。模板内容(列宽、示例行、样式)只在
|
||||
/// templates\TFS导入模板.xlsx 里维护,随构建复制到 exe 目录后原样导出。
|
||||
/// 列: Title | State | Assigned To | FinancialProject | Completed | LogDate | PBI Id | Description
|
||||
/// </summary>
|
||||
public static class ExcelService
|
||||
{
|
||||
public const string TemplateFileName = "TFS导入模板.xlsx";
|
||||
|
||||
public static readonly string[] Headers =
|
||||
{
|
||||
"Title", "State", "Assigned To", "FinancialProject",
|
||||
"Completed", "LogDate", "Created Date", "Closed Date", "PBI Id", "Notes"
|
||||
"Completed", "LogDate", "PBI Id", "Description"
|
||||
};
|
||||
|
||||
private static readonly string[] DateFormats =
|
||||
@@ -41,68 +42,15 @@ namespace TfsExcelImporter
|
||||
"yyyy-MM-dd", "yyyy/MM/dd"
|
||||
};
|
||||
|
||||
// --------------------------------------------------------- 生成模板
|
||||
// --------------------------------------------------------- 默认模板
|
||||
|
||||
public static void GenerateTemplate(string path)
|
||||
/// <summary>把随程序发布的默认模板原样复制到目标路径。</summary>
|
||||
public static void CopyTemplate(string path)
|
||||
{
|
||||
using (var wb = new XLWorkbook())
|
||||
{
|
||||
var ws = wb.Worksheets.Add("工作项");
|
||||
|
||||
for (int i = 0; i < Headers.Length; i++)
|
||||
{
|
||||
var cell = ws.Cell(1, i + 1);
|
||||
cell.Value = Headers[i];
|
||||
cell.Style.Font.Bold = true;
|
||||
cell.Style.Fill.BackgroundColor = XLColor.LightGray;
|
||||
}
|
||||
|
||||
// 示例行(To Do / Done 各一,帮助理解格式)
|
||||
var sample1 = new WorkItemRow
|
||||
{
|
||||
Title = "20261012 - 示例:货载报告解析并上传凭证受托报告相关内容",
|
||||
State = "To Do",
|
||||
AssignedTo = "李晓柯",
|
||||
FinancialProject = "信托 - 上海信托",
|
||||
Completed = "5",
|
||||
LogDate = "2026-10-12",
|
||||
CreatedDate = "2026-10-30 11:14:41",
|
||||
ClosedDate = "",
|
||||
Notes = "示例行,导入前请删除"
|
||||
};
|
||||
var sample2 = new WorkItemRow
|
||||
{
|
||||
Title = "20261009 示例:受托报告bug修复",
|
||||
State = "Done",
|
||||
AssignedTo = "陈卫宇",
|
||||
FinancialProject = "信托 - 上海信托",
|
||||
Completed = "8",
|
||||
LogDate = "2026-10-09",
|
||||
CreatedDate = "2026-10-10 18:20:00",
|
||||
ClosedDate = "2026-10-30 18:00:00",
|
||||
Notes = "示例行,导入前请删除"
|
||||
};
|
||||
WriteRow(ws, 2, sample1);
|
||||
WriteRow(ws, 3, sample2);
|
||||
|
||||
ws.Columns().AdjustToContents();
|
||||
ws.SheetView.FreezeRows(1);
|
||||
wb.SaveAs(path);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteRow(IXLWorksheet ws, int r, WorkItemRow row)
|
||||
{
|
||||
ws.Cell(r, 1).Value = row.Title ?? "";
|
||||
ws.Cell(r, 2).Value = row.State ?? "";
|
||||
ws.Cell(r, 3).Value = row.AssignedTo ?? "";
|
||||
ws.Cell(r, 4).Value = row.FinancialProject ?? "";
|
||||
ws.Cell(r, 5).Value = row.Completed ?? "";
|
||||
ws.Cell(r, 6).Value = row.LogDate ?? "";
|
||||
ws.Cell(r, 7).Value = row.CreatedDate ?? "";
|
||||
ws.Cell(r, 8).Value = row.ClosedDate ?? "";
|
||||
ws.Cell(r, 9).Value = row.PbiId ?? "";
|
||||
ws.Cell(r, 10).Value = row.Notes ?? "";
|
||||
var src = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, TemplateFileName);
|
||||
if (!File.Exists(src))
|
||||
throw new FileNotFoundException("未找到默认模板文件: " + src, src);
|
||||
File.Copy(src, path, true);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------- 读取数据
|
||||
@@ -132,10 +80,8 @@ namespace TfsExcelImporter
|
||||
FinancialProject = CellText(ws, r, headerMap, "FinancialProject"),
|
||||
Completed = CellText(ws, r, headerMap, "Completed"),
|
||||
LogDate = CellText(ws, r, headerMap, "LogDate"),
|
||||
CreatedDate = CellText(ws, r, headerMap, "Created Date"),
|
||||
ClosedDate = CellText(ws, r, headerMap, "Closed Date"),
|
||||
PbiId = CellText(ws, r, headerMap, "PBI Id"),
|
||||
Notes = CellText(ws, r, headerMap, "Notes")
|
||||
Description = CellText(ws, r, headerMap, "Description")
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -151,7 +97,7 @@ namespace TfsExcelImporter
|
||||
{
|
||||
var text = ws.Cell(firstRow, c).GetString().Trim();
|
||||
if (text.Length == 0) continue;
|
||||
// 归一化:去掉空格/下划线,兼容 "Assigned To" / "AssignedTo" / "Created Date" 等
|
||||
// 归一化:去掉空格/下划线,兼容 "Assigned To" / "AssignedTo" / "PBI Id" 等
|
||||
var key = text.Replace(" ", "").Replace("_", "");
|
||||
if (!map.ContainsKey(key)) map[key] = c;
|
||||
}
|
||||
@@ -159,11 +105,12 @@ namespace TfsExcelImporter
|
||||
foreach (var h in Headers)
|
||||
{
|
||||
var key = h.Replace(" ", "");
|
||||
if (!map.ContainsKey(key))
|
||||
{
|
||||
if (map.ContainsKey(key)) continue;
|
||||
int idx = Array.IndexOf(Headers, h) + 1;
|
||||
if (idx <= lastCol) map[key] = idx;
|
||||
}
|
||||
if (idx > lastCol) continue;
|
||||
// 该列已有别的表头占用(如旧版模板的 Created Date 列)时不套用位置,避免读错列
|
||||
bool taken = map.Values.Any(c => c == idx);
|
||||
if (!taken) map[key] = idx;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -144,10 +144,10 @@ namespace TfsExcelImporter
|
||||
_btnBrowse.Click += (s, e) => BrowseExcel();
|
||||
grpImport.Controls.Add(_btnBrowse);
|
||||
|
||||
_btnTemplate.Text = "生成Excel模板";
|
||||
_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();
|
||||
_btnTemplate.Click += (s, e) => ExportTemplate();
|
||||
grpImport.Controls.Add(_btnTemplate);
|
||||
|
||||
_chkSkip.Text = "跳过已存在标题";
|
||||
@@ -155,7 +155,7 @@ namespace TfsExcelImporter
|
||||
_chkSkip.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||||
grpImport.Controls.Add(_chkSkip);
|
||||
|
||||
_chkBypass.Text = "bypassRules(可写系统日期)";
|
||||
_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);
|
||||
@@ -357,25 +357,25 @@ namespace TfsExcelImporter
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateTemplate()
|
||||
private void ExportTemplate()
|
||||
{
|
||||
using (var dlg = new SaveFileDialog())
|
||||
{
|
||||
dlg.Filter = "Excel 文件 (*.xlsx)|*.xlsx";
|
||||
dlg.FileName = "TFS导入模板.xlsx";
|
||||
dlg.Title = "保存 Excel 模板";
|
||||
dlg.FileName = ExcelService.TemplateFileName;
|
||||
dlg.Title = "导出默认模板";
|
||||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||||
try
|
||||
{
|
||||
ExcelService.GenerateTemplate(dlg.FileName);
|
||||
Log("模板已生成: " + dlg.FileName);
|
||||
MessageBox.Show(this, "模板已生成:\n" + dlg.FileName, "完成",
|
||||
ExcelService.CopyTemplate(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, "错误",
|
||||
Log("导出模板失败: " + ex.Message);
|
||||
MessageBox.Show(this, "导出模板失败:\n" + ex.Message, "错误",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
@@ -402,9 +402,8 @@ namespace TfsExcelImporter
|
||||
财务项目 = r.FinancialProject,
|
||||
r.Completed,
|
||||
LogDate = r.LogDate,
|
||||
Created = r.CreatedDate,
|
||||
Closed = r.ClosedDate,
|
||||
r.Notes
|
||||
r.PbiId,
|
||||
r.Description
|
||||
}).ToList();
|
||||
_lblStatus.Text = $"已读取 {_rows.Count} 行";
|
||||
Log($"读取 {path} 共 {_rows.Count} 行");
|
||||
@@ -563,13 +562,12 @@ namespace TfsExcelImporter
|
||||
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 ?? "(未找到)"}");
|
||||
var origEstRef = TfsClient.ResolveRef(fieldMap, "Original Estimate", _cfg.OriginalEstimateRef);
|
||||
Log($"字段: FinancialProject->{finRef ?? "(未找到)"} Completed->{compRef ?? "(未找到)"} LogDate->{logRef ?? "(未找到)"} OriginalEstimate->{origEstRef ?? "(未找到)"}");
|
||||
Log($"父PBI: '{(string.IsNullOrWhiteSpace(_cfg.ParentPbiId) ? "(未填)" : _cfg.ParentPbiId)}'——Excel 每行可用 PBI Id 列覆盖");
|
||||
|
||||
HashSet<string> existing = null;
|
||||
@@ -612,25 +610,17 @@ namespace TfsExcelImporter
|
||||
if (completed != null && compRef != null)
|
||||
fields.Add(new KeyValuePair<string, object>(compRef, completed));
|
||||
|
||||
// Notes -> Description(该 Task 模板必填)
|
||||
if (!string.IsNullOrWhiteSpace(row.Notes))
|
||||
fields.Add(new KeyValuePair<string, object>("System.Description", row.Notes.Trim()));
|
||||
// Original Estimate 默认取 Completed 的值
|
||||
if (completed != null && origEstRef != null)
|
||||
fields.Add(new KeyValuePair<string, object>(origEstRef, completed));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(row.Description))
|
||||
fields.Add(new KeyValuePair<string, object>("System.Description", row.Description.Trim()));
|
||||
|
||||
var logDate = ExcelService.ToIsoDateTime(row.LogDate);
|
||||
if (logDate != null && logRef != null)
|
||||
fields.Add(new KeyValuePair<string, object>(logRef, logDate));
|
||||
|
||||
// 系统日期字段:bypassRules 时尝试写入
|
||||
if (_chkBypass.Checked)
|
||||
{
|
||||
var created = ExcelService.ToIsoDateTime(row.CreatedDate);
|
||||
if (created != null)
|
||||
fields.Add(new KeyValuePair<string, object>(createdRef, created));
|
||||
var closed = ExcelService.ToIsoDateTime(row.ClosedDate);
|
||||
if (closed != null)
|
||||
fields.Add(new KeyValuePair<string, object>(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;
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
<None Update="config.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<!-- 默认导入模板:随 exe 一起发布,「导出Excel模板」原样复制这份文件 -->
|
||||
<Content Include="..\templates\TFS导入模板.xlsx" Link="TFS导入模板.xlsx">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"FinancialProjectRef": "",
|
||||
"CompletedRef": "",
|
||||
"LogDateRef": "",
|
||||
"OriginalEstimateRef": "",
|
||||
"LastExcelPath": "",
|
||||
"ParentPbiId": ""
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||
<Default Extension="xml" ContentType="application/xml"/>
|
||||
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
|
||||
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
|
||||
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
|
||||
</Types>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
|
||||
</Relationships>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
|
||||
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
||||
</Relationships>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||||
<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>
|
||||
<fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>
|
||||
<borders count="1"><border/></borders>
|
||||
<cellStyleXfs count="1"><xf/></cellStyleXfs>
|
||||
<cellXfs count="1"><xf/></cellXfs>
|
||||
</styleSheet>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||||
<sheets>
|
||||
<sheet name="工作项" sheetId="1" r:id="rId1"/>
|
||||
</sheets>
|
||||
</workbook>
|
||||
@@ -1,41 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||||
<sheetData>
|
||||
<row r="1">
|
||||
<c r="A1" t="inlineStr"><is><t>Title</t></is></c>
|
||||
<c r="B1" t="inlineStr"><is><t>State</t></is></c>
|
||||
<c r="C1" t="inlineStr"><is><t>Assigned To</t></is></c>
|
||||
<c r="D1" t="inlineStr"><is><t>FinancialProject</t></is></c>
|
||||
<c r="E1" t="inlineStr"><is><t>Completed</t></is></c>
|
||||
<c r="F1" t="inlineStr"><is><t>LogDate</t></is></c>
|
||||
<c r="G1" t="inlineStr"><is><t>Created Date</t></is></c>
|
||||
<c r="H1" t="inlineStr"><is><t>Closed Date</t></is></c>
|
||||
<c r="I1" t="inlineStr"><is><t>PBI Id</t></is></c>
|
||||
<c r="J1" t="inlineStr"><is><t>Notes</t></is></c>
|
||||
</row>
|
||||
<row r="2">
|
||||
<c r="A2" t="inlineStr"><is><t>20261012 - 示例:货载报告解析并上传凭证受托报告相关内容</t></is></c>
|
||||
<c r="B2" t="inlineStr"><is><t>To Do</t></is></c>
|
||||
<c r="C2" t="inlineStr"><is><t>李晓柯</t></is></c>
|
||||
<c r="D2" t="inlineStr"><is><t>信托 - 上海信托</t></is></c>
|
||||
<c r="E2" t="inlineStr"><is><t>5</t></is></c>
|
||||
<c r="F2" t="inlineStr"><is><t>2026-10-12</t></is></c>
|
||||
<c r="G2" t="inlineStr"><is><t>2026-10-30 11:14:41</t></is></c>
|
||||
<c r="H2" t="inlineStr"><is><t></t></is></c>
|
||||
<c r="I2" t="inlineStr"><is><t>219975</t></is></c>
|
||||
<c r="J2" t="inlineStr"><is><t>示例行,导入前请删除</t></is></c>
|
||||
</row>
|
||||
<row r="3">
|
||||
<c r="A3" t="inlineStr"><is><t>20261009 示例:受托报告bug修复</t></is></c>
|
||||
<c r="B3" t="inlineStr"><is><t>Done</t></is></c>
|
||||
<c r="C3" t="inlineStr"><is><t>陈卫宇</t></is></c>
|
||||
<c r="D3" t="inlineStr"><is><t>信托 - 上海信托</t></is></c>
|
||||
<c r="E3" t="inlineStr"><is><t>8</t></is></c>
|
||||
<c r="F3" t="inlineStr"><is><t>2026-10-09</t></is></c>
|
||||
<c r="G3" t="inlineStr"><is><t>2026-10-10 18:20:00</t></is></c>
|
||||
<c r="H3" t="inlineStr"><is><t>2026-10-30 18:00:00</t></is></c>
|
||||
<c r="I3" t="inlineStr"><is><t>219975</t></is></c>
|
||||
<c r="J3" t="inlineStr"><is><t>示例行,导入前请删除</t></is></c>
|
||||
</row>
|
||||
</sheetData>
|
||||
</worksheet>
|
||||
@@ -1,104 +0,0 @@
|
||||
# Build TFS import xlsx from XML parts as a stored (uncompressed) ZIP.
|
||||
# Pure in-process .NET byte APIs: no external processes, no assembly loading.
|
||||
# Output file name uses ASCII+char trick to stay encoding-safe.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$parts = Join-Path $PSScriptRoot '_parts'
|
||||
$outName = 'TFS' + [char]0x5BFC + [char]0x5165 + [char]0x6A21 + [char]0x677F + '.xlsx'
|
||||
$out = Join-Path $PSScriptRoot $outName
|
||||
|
||||
# ---------- CRC32 (IEEE 802.3) ----------
|
||||
$tbl = New-Object 'System.Int64[]' 256
|
||||
for ($i = 0; $i -lt 256; $i++) {
|
||||
$c = [int64]$i
|
||||
for ($k = 0; $k -lt 8; $k++) {
|
||||
if (($c -band 1) -eq 1) { $c = (3988292384 -bxor ($c -shr 1)) } else { $c = ($c -shr 1) }
|
||||
$c = $c -band 4294967295
|
||||
}
|
||||
$tbl[$i] = $c
|
||||
}
|
||||
|
||||
function Get-Crc32([byte[]]$data) {
|
||||
$crc = [int64]4294967295
|
||||
foreach ($b in $data) {
|
||||
$idx = [int](($crc -bxor [int64]$b) -band 255)
|
||||
$crc = ($tbl[$idx] -bxor ($crc -shr 8)) -band 4294967295
|
||||
}
|
||||
return ($crc -bxor 4294967295)
|
||||
}
|
||||
|
||||
# ---------- entry list (source relative path, zip entry name) ----------
|
||||
$entries = @(
|
||||
@{ src = '[Content_Types].xml'; zip = '[Content_Types].xml' },
|
||||
@{ src = '_rels\.rels'; zip = '_rels/.rels' },
|
||||
@{ src = 'xl\workbook.xml'; zip = 'xl/workbook.xml' },
|
||||
@{ src = 'xl\_rels\workbook.xml.rels'; zip = 'xl/_rels/workbook.xml.rels' },
|
||||
@{ src = 'xl\styles.xml'; zip = 'xl/styles.xml' },
|
||||
@{ src = 'xl\worksheets\sheet1.xml'; zip = 'xl/worksheets/sheet1.xml' }
|
||||
)
|
||||
|
||||
$ms = New-Object System.IO.MemoryStream
|
||||
$central = New-Object System.Collections.ArrayList
|
||||
|
||||
foreach ($e in $entries) {
|
||||
$srcPath = Join-Path $parts $e['src']
|
||||
$bytes = [System.IO.File]::ReadAllBytes($srcPath)
|
||||
$nameBytes = [System.Text.Encoding]::ASCII.GetBytes([string]$e['zip'])
|
||||
$crc = Get-Crc32 $bytes
|
||||
$offset = $ms.Position
|
||||
|
||||
$ms.Write([BitConverter]::GetBytes([uint32]0x04034b50), 0, 4) # local header signature
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]20), 0, 2) # version needed
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # flags
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # method: stored
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # mod time
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]33), 0, 2) # mod date 1980-01-01
|
||||
$ms.Write([BitConverter]::GetBytes([uint32]$crc), 0, 4)
|
||||
$ms.Write([BitConverter]::GetBytes([uint32]$bytes.Length), 0, 4)
|
||||
$ms.Write([BitConverter]::GetBytes([uint32]$bytes.Length), 0, 4)
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]$nameBytes.Length), 0, 2)
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # extra len
|
||||
$ms.Write($nameBytes, 0, $nameBytes.Length)
|
||||
$ms.Write($bytes, 0, $bytes.Length)
|
||||
|
||||
[void]$central.Add(@{ name = $nameBytes; crc = $crc; size = $bytes.Length; offset = $offset })
|
||||
}
|
||||
|
||||
# ---------- central directory ----------
|
||||
$cdStart = $ms.Position
|
||||
foreach ($c in $central) {
|
||||
$ms.Write([BitConverter]::GetBytes([uint32]0x02014b50), 0, 4) # central header signature
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]20), 0, 2) # made by
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]20), 0, 2) # version needed
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # flags
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # method
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # time
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]33), 0, 2) # date
|
||||
$ms.Write([BitConverter]::GetBytes([uint32]$c['crc']), 0, 4)
|
||||
$ms.Write([BitConverter]::GetBytes([uint32]$c['size']), 0, 4)
|
||||
$ms.Write([BitConverter]::GetBytes([uint32]$c['size']), 0, 4)
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]$c['name'].Length), 0, 2)
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # extra len
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # comment len
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # disk start
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # internal attrs
|
||||
$ms.Write([BitConverter]::GetBytes([uint32]0), 0, 4) # external attrs
|
||||
$ms.Write([BitConverter]::GetBytes([uint32]$c['offset']), 0, 4)
|
||||
$ms.Write($c['name'], 0, $c['name'].Length)
|
||||
}
|
||||
$cdSize = $ms.Position - $cdStart
|
||||
|
||||
# ---------- end of central directory ----------
|
||||
$ms.Write([BitConverter]::GetBytes([uint32]0x06054b50), 0, 4)
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # disk number
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # cd disk
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]$central.Count), 0, 2)
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]$central.Count), 0, 2)
|
||||
$ms.Write([BitConverter]::GetBytes([uint32]$cdSize), 0, 4)
|
||||
$ms.Write([BitConverter]::GetBytes([uint32]$cdStart), 0, 4)
|
||||
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # comment len
|
||||
|
||||
[System.IO.File]::WriteAllBytes($out, $ms.ToArray())
|
||||
$ms.Dispose()
|
||||
|
||||
Write-Output ('CREATED ' + $out + ' size=' + (New-Object System.IO.FileInfo $out).Length)
|
||||
Reference in New Issue
Block a user