feat: 增强数据导入功能与用户界面优化
- 更新 README.md,强调 Area 路径为必填项,并增加加载 Area 列表的功能,确保用户在导入时不会遗漏必要信息。 - 修改 Config.cs 和 config.json,调整 SkipExistingTitles 默认值为 false,以便用户首次导入时能看到真实冲突和报错。 - 在 MainForm.cs 中添加加载 Area 列表的按钮,优化用户交互体验。 - 更新 ExcelService.cs,增强模板导出逻辑,确保用户能够更方便地使用自定义模板。 - 调整界面布局,确保 Area 路径与迭代路径的选择更加直观,提升整体用户体验。
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using ClosedXML.Excel;
|
||||
|
||||
namespace TfsExcelImporter
|
||||
@@ -45,35 +46,129 @@ namespace TfsExcelImporter
|
||||
|
||||
// --------------------------------------------------------- 默认模板
|
||||
|
||||
/// <summary>
|
||||
/// 把默认模板原样复制到目标路径。优先用 exe 同目录的同名文件(可由现场替换),
|
||||
/// 没有则从 exe 内嵌资源释放——单文件便携分发时无需附带模板。
|
||||
/// </summary>
|
||||
public static void CopyTemplate(string path)
|
||||
/// <summary>exe 同目录的模板路径(现场可替换,存在时优先于内嵌模板)。</summary>
|
||||
public static string LocalTemplatePath
|
||||
{
|
||||
var src = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, TemplateFileName);
|
||||
if (File.Exists(src))
|
||||
get { return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, TemplateFileName); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// exe 同目录的模板是否还是构建时那份(与内嵌模板内容一致)。
|
||||
/// 现场替换过、或被填过数据都算"不一致",导出前应由调用方确认来源。
|
||||
/// </summary>
|
||||
public static bool LocalTemplateIsBuiltIn()
|
||||
{
|
||||
var local = LocalTemplatePath;
|
||||
if (!File.Exists(local)) return true;
|
||||
try
|
||||
{
|
||||
File.Copy(src, path, true);
|
||||
return;
|
||||
var embed = ReadEmbeddedTemplate();
|
||||
return embed != null && SameDigest(embed, File.ReadAllBytes(local));
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把默认模板原样写到目标路径,返回模板来源描述(用于日志)。
|
||||
/// 优先用 exe 同目录的同名文件(现场可替换),forceEmbedded 或该文件不存在时用内嵌资源。
|
||||
///
|
||||
/// 模板源只有这两个确定来源,绝不按"相近文件名"去猜;目标就是调用方选定的那一个路径。
|
||||
/// 写出后再比对一次内容,避免"报成功但文件不是模板"这类静默错。
|
||||
/// </summary>
|
||||
public static string CopyTemplate(string path, bool forceEmbedded = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentException("导出路径为空", nameof(path));
|
||||
|
||||
var dst = Path.GetFullPath(path);
|
||||
var src = LocalTemplatePath;
|
||||
|
||||
if (!forceEmbedded && File.Exists(src))
|
||||
{
|
||||
var srcFull = Path.GetFullPath(src);
|
||||
// 目标就是源本身:File.Copy 会抛"源和目标是同一个文件",先给出可读提示
|
||||
if (string.Equals(srcFull, dst, StringComparison.OrdinalIgnoreCase))
|
||||
throw new IOException(
|
||||
"导出路径与模板源文件相同(" + srcFull + "),请换一个文件名或目录。");
|
||||
|
||||
File.Copy(srcFull, dst, true);
|
||||
AssertSameContent(srcFull, dst);
|
||||
return "exe 同目录 " + TemplateFileName;
|
||||
}
|
||||
|
||||
var embed = ReadEmbeddedTemplate();
|
||||
if (embed == null)
|
||||
throw new FileNotFoundException(
|
||||
"未找到默认模板:exe 内嵌资源与同目录 " + TemplateFileName + " 均缺失", src);
|
||||
|
||||
// 先写临时文件再替换:中途失败不会把目标位置已有的文件写坏
|
||||
var tmp = dst + "." + Guid.NewGuid().ToString("N") + ".tmp";
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes(tmp, embed);
|
||||
File.Copy(tmp, dst, true);
|
||||
AssertSameContent(tmp, dst);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (File.Exists(tmp)) File.Delete(tmp); } catch { }
|
||||
}
|
||||
return "exe 内嵌模板";
|
||||
}
|
||||
|
||||
private static byte[] ReadEmbeddedTemplate()
|
||||
{
|
||||
var asm = typeof(ExcelService).Assembly;
|
||||
// 嵌入名取决于 Link 名:默认 <RootNamespace>.<文件名>
|
||||
var name = asm.GetManifestResourceNames()
|
||||
.FirstOrDefault(n => n.EndsWith(TemplateFileName, StringComparison.OrdinalIgnoreCase));
|
||||
if (name == null)
|
||||
throw new FileNotFoundException(
|
||||
"未找到默认模板:exe 内嵌资源与同目录 " + TemplateFileName + " 均缺失", src);
|
||||
if (name == null) return null;
|
||||
|
||||
using (var input = asm.GetManifestResourceStream(name))
|
||||
{
|
||||
if (input == null) throw new FileNotFoundException("无法读取内嵌模板资源: " + name, src);
|
||||
using (var output = File.Create(path))
|
||||
input.CopyTo(output);
|
||||
if (input == null) return null;
|
||||
using (var buffer = new MemoryStream())
|
||||
{
|
||||
input.CopyTo(buffer);
|
||||
return buffer.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>写出后比对内容,避免"导出成功但文件是旧的/被别的数据占了"这类静默错。</summary>
|
||||
private static void AssertSameContent(string expected, string actual)
|
||||
{
|
||||
var a = new FileInfo(expected);
|
||||
var b = new FileInfo(actual);
|
||||
if (!b.Exists || b.Length != a.Length || !SameDigest(expected, actual))
|
||||
throw new IOException("模板写出后校验不一致,目标文件内容与模板不同:" + actual);
|
||||
}
|
||||
|
||||
private static bool SameDigest(string x, string y)
|
||||
{
|
||||
return SameDigest(File.ReadAllBytes(x), File.ReadAllBytes(y));
|
||||
}
|
||||
|
||||
private static bool SameDigest(byte[] x, byte[] y)
|
||||
{
|
||||
if (x == null || y == null || x.Length != y.Length) return false;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
byte[] hx = sha.ComputeHash(x);
|
||||
byte[] hy = sha.ComputeHash(y);
|
||||
for (int i = 0; i < hx.Length; i++)
|
||||
if (hx[i] != hy[i]) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>数据行数(Title 非空);文件被占用/损坏时返回 -1,调用方据此区分"读不出"与"空表"。</summary>
|
||||
public static int CountDataRows(string path)
|
||||
{
|
||||
try { return ReadRows(path).Count; }
|
||||
catch { return -1; }
|
||||
}
|
||||
|
||||
// --------------------------------------------------------- 读取数据
|
||||
|
||||
/// <summary>读取 Excel(或同格式 CSV 由用户另存为 xlsx)数据行,跳过空标题行。</summary>
|
||||
|
||||
Reference in New Issue
Block a user