1354 lines
62 KiB
C#
1354 lines
62 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Data;
|
|
using System.Diagnostics;
|
|
using System.Drawing;
|
|
using System.IO;
|
|
using System.Windows.Forms;
|
|
using Microsoft.Win32;
|
|
using Smart.CustomComponents;
|
|
|
|
namespace 成套报价软件
|
|
{
|
|
/// <summary>主窗口 拆分文件:由 主窗口.cs 按模块拆出,同一 partial class。</summary>
|
|
public partial class MainForm
|
|
{
|
|
// ============================================================
|
|
// 项目导入/导出
|
|
// · 导出:把选中项目的独立库(项目<编号>.db)和元数据(项目列表那一行)
|
|
// 打包成一个 .zip 文件,带元数据 meta.json
|
|
// · 导入:解压 .zip → 项目编号冲突时自动改名 → db 放到 sjk\bjb\ →
|
|
// 元数据 INSERT 到项目列表 → 刷新项目表显示
|
|
// ============================================================
|
|
|
|
// <summary>导出项目:读项目表 → 弹列表让用户选 → 打包导出。</summary>
|
|
private void button7_Click(object sender, EventArgs e)
|
|
{
|
|
string pid = "";
|
|
string dbPath = "";
|
|
string dstDb = "";
|
|
string zipPath = "";
|
|
try
|
|
{
|
|
Log("[项目导出] ===== 开始导出 =====");
|
|
|
|
// ① 先从项目表读取所有项目
|
|
Log("[项目导出] 步骤1: 读项目表");
|
|
var projects = ReadAllProjectsFromDatabase();
|
|
if (projects == null || projects.Count == 0)
|
|
{
|
|
MessageBox.Show(this, "项目列表为空,无项目可导出。",
|
|
"导出项目", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
return;
|
|
}
|
|
Log("[项目导出] 步骤1完成: 共 " + projects.Count + " 个项目");
|
|
|
|
// ② 弹出列表让用户选择要导出的项目(支持多选)
|
|
Log("[项目导出] 步骤2: 选择项目(多选)");
|
|
var selectedPids = new System.Collections.Generic.List<string>();
|
|
if (projects.Count == 1)
|
|
{
|
|
var first = projects[0];
|
|
string singlePid = first.ContainsKey("项目编号") ? (first["项目编号"] ?? "").ToString() : "";
|
|
if (!string.IsNullOrWhiteSpace(singlePid)) selectedPids.Add(singlePid);
|
|
}
|
|
else
|
|
{
|
|
selectedPids = PickProjectDialogMulti(projects);
|
|
if (selectedPids == null || selectedPids.Count == 0)
|
|
{
|
|
Log("[项目导出] 用户取消选择");
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (selectedPids == null || selectedPids.Count == 0)
|
|
{
|
|
MessageBox.Show(this, "未能确定要导出的项目编号。",
|
|
"导出项目", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return;
|
|
}
|
|
Log("[项目导出] 步骤2完成: 选中 " + selectedPids.Count + " 个项目");
|
|
|
|
// 先把当前项目所有未保存的修改写库
|
|
Log("[项目导出] 步骤3: SaveAllToDatabase");
|
|
SaveAllToDatabase();
|
|
Log("[项目导出] 步骤3完成");
|
|
|
|
// ★ 选择保存位置:单个项目弹文件对话框,多个项目弹文件夹对话框
|
|
string singleZipPath = "";
|
|
|
|
if (selectedPids.Count == 1)
|
|
{
|
|
// ===== 单项目导出:弹保存文件对话框(保持原有行为)=====
|
|
pid = selectedPids[0];
|
|
dbPath = GetQuoteDbPath(pid);
|
|
if (!System.IO.File.Exists(dbPath))
|
|
{
|
|
MessageBox.Show(this, "项目数据库文件不存在:\n" + dbPath,
|
|
"导出项目", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return;
|
|
}
|
|
|
|
var meta = ReadProjectMetaData(pid);
|
|
if (meta == null)
|
|
{
|
|
MessageBox.Show(this, "项目元数据读取失败,项目编号:" + pid,
|
|
"导出项目", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return;
|
|
}
|
|
|
|
string projName = meta.ContainsKey("项目名称") ? (meta["项目名称"] ?? "").ToString() : "";
|
|
string safeName = MakeSafeFileName(projName);
|
|
if (string.IsNullOrWhiteSpace(safeName)) safeName = MakeSafeFileName(pid);
|
|
string defaultFileName = safeName + ".zip";
|
|
|
|
string initialDir = "";
|
|
try
|
|
{
|
|
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
|
if (!string.IsNullOrEmpty(desktop) && System.IO.Directory.Exists(desktop))
|
|
initialDir = desktop;
|
|
}
|
|
catch { }
|
|
if (string.IsNullOrEmpty(initialDir))
|
|
{
|
|
try
|
|
{
|
|
string docs = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
|
if (!string.IsNullOrEmpty(docs) && System.IO.Directory.Exists(docs))
|
|
initialDir = docs;
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
using (var sfd = new SaveFileDialog
|
|
{
|
|
Title = "导出项目",
|
|
Filter = "项目包 (*.zip)|*.zip",
|
|
FileName = defaultFileName,
|
|
RestoreDirectory = true,
|
|
})
|
|
{
|
|
if (!string.IsNullOrEmpty(initialDir))
|
|
sfd.InitialDirectory = initialDir;
|
|
|
|
if (sfd.ShowDialog(this) != DialogResult.OK)
|
|
{
|
|
Log("[项目导出] 用户取消保存");
|
|
return;
|
|
}
|
|
singleZipPath = sfd.FileName;
|
|
}
|
|
|
|
// 导出单个
|
|
var pidsToExport = new System.Collections.Generic.List<string>();
|
|
pidsToExport.Add(pid);
|
|
int okCount = ExportProjectsToZip(pidsToExport, null, singleZipPath);
|
|
if (okCount > 0)
|
|
{
|
|
MessageBox.Show(this,
|
|
"导出成功!\n\n文件:" + singleZipPath,
|
|
"导出项目", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// ===== 多项目导出:打成一个 zip 包,里面每个项目一个子文件夹 =====
|
|
string initialDir = "";
|
|
try
|
|
{
|
|
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
|
if (!string.IsNullOrEmpty(desktop) && System.IO.Directory.Exists(desktop))
|
|
initialDir = desktop;
|
|
}
|
|
catch { }
|
|
|
|
string multiZipPath = "";
|
|
using (var sfd = new SaveFileDialog
|
|
{
|
|
Title = "导出 " + selectedPids.Count + " 个项目到一个压缩包",
|
|
Filter = "项目包 (*.zip)|*.zip",
|
|
FileName = selectedPids.Count + "个项目_" + DateTime.Now.ToString("yyyyMMdd") + ".zip",
|
|
RestoreDirectory = true,
|
|
})
|
|
{
|
|
if (!string.IsNullOrEmpty(initialDir))
|
|
sfd.InitialDirectory = initialDir;
|
|
|
|
if (sfd.ShowDialog(this) != DialogResult.OK)
|
|
{
|
|
Log("[项目导出] 用户取消保存");
|
|
return;
|
|
}
|
|
multiZipPath = sfd.FileName;
|
|
}
|
|
|
|
// 批量导出到一个 zip(每个项目一个子文件夹)
|
|
int okCount = ExportProjectsToSingleZip(selectedPids, multiZipPath);
|
|
MessageBox.Show(this,
|
|
"批量导出完成!\n\n成功导出 " + okCount + " / " + selectedPids.Count + " 个项目\n压缩包:" + multiZipPath,
|
|
"导出项目", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
}
|
|
Log("[项目导出] ===== 导出成功 =====");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log("[项目导出异常] 失败步骤位置: pid=[" + pid + "] dbPath=[" + dbPath +
|
|
"] dstDb=[" + dstDb + "] zipPath=[" + zipPath + "]");
|
|
Log("[项目导出异常] " + ex.GetType().Name + ": " + ex.Message);
|
|
Log("[项目导出异常] 堆栈:\n" + ex.StackTrace);
|
|
MessageBox.Show(this,
|
|
"导出失败:\n" + ex.Message +
|
|
"\n\n异常类型: " + ex.GetType().Name +
|
|
"\n\n=== 详细堆栈 ===\n" + ex.StackTrace,
|
|
"导出项目", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 把多个项目打到同一个 .zip 包里,每个项目一个子文件夹(以项目名称命名)。
|
|
/// 返回成功导出的数量。
|
|
/// </summary>
|
|
private int ExportProjectsToSingleZip(System.Collections.Generic.List<string> pids, string zipPath)
|
|
{
|
|
int okCount = 0;
|
|
// 临时根目录
|
|
string tempRoot = System.IO.Path.Combine(System.IO.Path.GetTempPath(),
|
|
"成套报价_批量导出_" + System.Guid.NewGuid().ToString("N"));
|
|
System.IO.Directory.CreateDirectory(tempRoot);
|
|
|
|
// 记录子文件夹名,用于去重(项目名可能重复)
|
|
var usedNames = new System.Collections.Generic.HashSet<string>(System.StringComparer.Ordinal);
|
|
|
|
try
|
|
{
|
|
foreach (string currentPid in pids)
|
|
{
|
|
try
|
|
{
|
|
string currentDbPath = GetQuoteDbPath(currentPid);
|
|
if (!System.IO.File.Exists(currentDbPath))
|
|
{
|
|
Log("[项目导出] 跳过(数据库不存在): " + currentPid);
|
|
continue;
|
|
}
|
|
|
|
var meta = ReadProjectMetaData(currentPid);
|
|
if (meta == null)
|
|
{
|
|
Log("[项目导出] 跳过(元数据读取失败): " + currentPid);
|
|
continue;
|
|
}
|
|
|
|
// 子文件夹名 = 项目名称(去重)
|
|
string projName = meta.ContainsKey("项目名称") ? (meta["项目名称"] ?? "").ToString() : "";
|
|
string folderName = MakeSafeFileName(projName);
|
|
if (string.IsNullOrWhiteSpace(folderName)) folderName = MakeSafeFileName(currentPid);
|
|
// 去重:同名项目加编号
|
|
if (usedNames.Contains(folderName))
|
|
{
|
|
string baseFolder = folderName;
|
|
int dup = 1;
|
|
while (usedNames.Contains(folderName))
|
|
{
|
|
folderName = baseFolder + "(" + dup + ")";
|
|
dup++;
|
|
}
|
|
}
|
|
usedNames.Add(folderName);
|
|
|
|
// 创建子文件夹
|
|
string subDir = System.IO.Path.Combine(tempRoot, folderName);
|
|
System.IO.Directory.CreateDirectory(subDir);
|
|
|
|
// 复制 db
|
|
string dstDbFile = System.IO.Path.Combine(subDir, "项目" + MakeSafeFileName(currentPid) + ".db");
|
|
BackupSQLiteDb(currentDbPath, dstDbFile);
|
|
|
|
// 写 meta.json
|
|
var sb = new System.Text.StringBuilder();
|
|
sb.Append("{\n \"项目编号\": \"");
|
|
sb.Append(EscapeJsonString(currentPid));
|
|
sb.Append("\",\n \"字段\": {");
|
|
bool firstKv = true;
|
|
foreach (var kv in meta)
|
|
{
|
|
if (firstKv) firstKv = false; else sb.Append(",");
|
|
sb.Append("\n \"");
|
|
sb.Append(EscapeJsonString(kv.Key));
|
|
sb.Append("\": \"");
|
|
sb.Append(EscapeJsonString((kv.Value ?? "").ToString()));
|
|
sb.Append("\"");
|
|
}
|
|
sb.Append("\n }\n}");
|
|
|
|
string metaPath = System.IO.Path.Combine(subDir, "meta.json");
|
|
System.IO.File.WriteAllText(metaPath, sb.ToString(), new System.Text.UTF8Encoding(false));
|
|
|
|
okCount++;
|
|
Log("[项目导出] 批量: " + currentPid + " → 子文件夹[" + folderName + "]");
|
|
}
|
|
catch (Exception exProj)
|
|
{
|
|
Log("[项目导出] 单个项目导出失败 [" + currentPid + "]: " + exProj.Message);
|
|
}
|
|
}
|
|
|
|
// 全部完成后打成一个 zip
|
|
if (okCount > 0)
|
|
{
|
|
if (System.IO.File.Exists(zipPath))
|
|
System.IO.File.Delete(zipPath);
|
|
System.IO.Compression.ZipFile.CreateFromDirectory(tempRoot, zipPath);
|
|
Log("[项目导出] 批量打包完成: " + okCount + " 个项目 → " + zipPath);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
try { System.IO.Directory.Delete(tempRoot, true); } catch { }
|
|
}
|
|
return okCount;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 把多个项目逐个导出为独立的 .zip 文件。
|
|
/// 导出到 exportDir 目录(多项目),或 singleZipPath(单项目)。
|
|
/// 返回成功导出的数量。
|
|
/// </summary>
|
|
// ---- 项目云窗体(项目云.cs)用的转发包装:原方法 private,包装成 public 供跨类调用 ----
|
|
public System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>> ReadAllProjectsFromDatabaseForCloud()
|
|
{ return ReadAllProjectsFromDatabase(); }
|
|
public string GetQuoteDbPathForCloud(string pid) { return GetQuoteDbPath(pid); }
|
|
public System.Collections.Generic.Dictionary<string, object> ReadProjectMetaDataForCloud(string pid)
|
|
{ return ReadProjectMetaData(pid); }
|
|
public string MakeSafeFileNameForCloud(string s) { return MakeSafeFileName(s); }
|
|
public string EscapeJsonStringForCloud(string s) { return EscapeJsonString(s); }
|
|
public void BackupSQLiteDbForCloud(string src, string dst) { BackupSQLiteDb(src, dst); }
|
|
public string ImportProjectFromZipForCloud(string zipPath) { return ImportProjectFromZip(zipPath); }
|
|
|
|
private int ExportProjectsToZip(System.Collections.Generic.List<string> pids,
|
|
string exportDir, string singleZipPath)
|
|
{ int okCount = 0;
|
|
foreach (string currentPid in pids)
|
|
{
|
|
try
|
|
{
|
|
string currentDbPath = GetQuoteDbPath(currentPid);
|
|
if (!System.IO.File.Exists(currentDbPath))
|
|
{
|
|
Log("[项目导出] 跳过(数据库不存在): " + currentPid);
|
|
continue;
|
|
}
|
|
|
|
var meta = ReadProjectMetaData(currentPid);
|
|
if (meta == null)
|
|
{
|
|
Log("[项目导出] 跳过(元数据读取失败): " + currentPid);
|
|
continue;
|
|
}
|
|
|
|
string projName = meta.ContainsKey("项目名称") ? (meta["项目名称"] ?? "").ToString() : "";
|
|
string safeName = MakeSafeFileName(projName);
|
|
if (string.IsNullOrWhiteSpace(safeName)) safeName = MakeSafeFileName(currentPid);
|
|
|
|
// 确定 zip 路径
|
|
string currentZipPath;
|
|
if (!string.IsNullOrEmpty(singleZipPath))
|
|
currentZipPath = singleZipPath;
|
|
else
|
|
currentZipPath = System.IO.Path.Combine(exportDir, safeName + ".zip");
|
|
|
|
// 临时目录打包
|
|
string tempDir = System.IO.Path.Combine(System.IO.Path.GetTempPath(),
|
|
"成套报价_导出_" + System.Guid.NewGuid().ToString("N"));
|
|
System.IO.Directory.CreateDirectory(tempDir);
|
|
|
|
try
|
|
{
|
|
// 复制 db
|
|
string dstDbFile = System.IO.Path.Combine(tempDir, "项目" + MakeSafeFileName(currentPid) + ".db");
|
|
BackupSQLiteDb(currentDbPath, dstDbFile);
|
|
|
|
// 写 meta.json
|
|
var sb = new System.Text.StringBuilder();
|
|
sb.Append("{\n \"项目编号\": \"");
|
|
sb.Append(EscapeJsonString(currentPid));
|
|
sb.Append("\",\n \"字段\": {");
|
|
bool firstKv = true;
|
|
foreach (var kv in meta)
|
|
{
|
|
if (firstKv) firstKv = false; else sb.Append(",");
|
|
sb.Append("\n \"");
|
|
sb.Append(EscapeJsonString(kv.Key));
|
|
sb.Append("\": \"");
|
|
sb.Append(EscapeJsonString((kv.Value ?? "").ToString()));
|
|
sb.Append("\"");
|
|
}
|
|
sb.Append("\n }\n}");
|
|
|
|
string metaPath = System.IO.Path.Combine(tempDir, "meta.json");
|
|
System.IO.File.WriteAllText(metaPath, sb.ToString(), new System.Text.UTF8Encoding(false));
|
|
|
|
// 打包 zip
|
|
if (System.IO.File.Exists(currentZipPath))
|
|
System.IO.File.Delete(currentZipPath);
|
|
System.IO.Compression.ZipFile.CreateFromDirectory(tempDir, currentZipPath);
|
|
|
|
okCount++;
|
|
Log("[项目导出] 成功: " + currentPid + " → " + currentZipPath);
|
|
}
|
|
finally
|
|
{
|
|
try { System.IO.Directory.Delete(tempDir, true); } catch { }
|
|
}
|
|
}
|
|
catch (Exception exProj)
|
|
{
|
|
Log("[项目导出] 单个项目导出失败 [" + currentPid + "]: " + exProj.Message);
|
|
}
|
|
}
|
|
return okCount;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 复制 SQLite 数据库文件到目标路径,解决"没有权限/被占用"问题。
|
|
/// 主路径:用 SQL ATTACH + CREATE TABLE AS SELECT 在 SQL 层复制,完全绕开文件锁
|
|
/// 兜底:File.Copy + ClearAllPools(以防 SQL 复制失败)
|
|
/// </summary>
|
|
private void BackupSQLiteDb(string srcDbPath, string dstDbPath)
|
|
{
|
|
// 清掉旧的目标文件(失败也无所谓,SQL CREATE 会覆盖)
|
|
TryDeleteFile(dstDbPath);
|
|
|
|
// ① 主路径:SQL 层复制(完全绕开文件锁)
|
|
Exception sqlErr = null;
|
|
try
|
|
{
|
|
// 用独立的 Pooling=False 连接,不参与主程序的连接池。
|
|
// 注意:不能加 ReadOnly=True — 只读连接上 ATTACH 新建目标库会被拒
|
|
// (unable to open database),导致总是掉进 File.Copy 兜底。
|
|
using (var srcConn = new System.Data.SQLite.SQLiteConnection(
|
|
"Data Source=" + srcDbPath + ";Version=3;Pooling=False;"))
|
|
{
|
|
srcConn.Open();
|
|
|
|
// 把目标库 ATTACH 上来,别名 dst
|
|
using (var att = new System.Data.SQLite.SQLiteCommand(
|
|
"ATTACH DATABASE @path AS dst;", srcConn))
|
|
{
|
|
att.Parameters.AddWithValue("@path", dstDbPath);
|
|
att.ExecuteNonQuery();
|
|
}
|
|
|
|
try
|
|
{
|
|
// 读源库所有表(排除 sqlite_ 系统表)
|
|
var tableNames = new System.Collections.Generic.List<string>();
|
|
using (var sel = new System.Data.SQLite.SQLiteCommand(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';", srcConn))
|
|
using (var rd = sel.ExecuteReader())
|
|
{
|
|
while (rd.Read())
|
|
{
|
|
string nm = rd.GetString(0);
|
|
if (!string.IsNullOrEmpty(nm))
|
|
tableNames.Add(nm);
|
|
}
|
|
}
|
|
|
|
// 逐表复制(结构 + 数据 + 自增主键)
|
|
foreach (string tbl in tableNames)
|
|
{
|
|
string safe = tbl.Replace("\"", "\"\"");
|
|
string main = "main.\"" + safe + "\"";
|
|
string dst = "dst.\"" + safe + "\"";
|
|
|
|
// 1) 拿源表的 CREATE 语句,改个名建到 dst 下
|
|
string createSql = null;
|
|
using (var selSql = new System.Data.SQLite.SQLiteCommand(
|
|
"SELECT sql FROM sqlite_master WHERE type='table' AND name=@n;", srcConn))
|
|
{
|
|
selSql.Parameters.AddWithValue("@n", tbl);
|
|
object o = selSql.ExecuteScalar();
|
|
createSql = (o == null || o == DBNull.Value) ? null : o.ToString();
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(createSql))
|
|
{
|
|
// 把 CREATE TABLE "原表名" 替换成 CREATE TABLE dst."原表名"
|
|
// 简单处理:在 CREATE TABLE 后插入 dst.
|
|
string dstCreateSql = createSql.Replace(
|
|
"CREATE TABLE \"" + safe + "\"",
|
|
"CREATE TABLE dst.\"" + safe + "\"");
|
|
// 兜底替换:可能 sqlite_master 存的是不带引号的形式
|
|
if (dstCreateSql == createSql)
|
|
{
|
|
dstCreateSql = createSql.Replace(
|
|
"CREATE TABLE " + tbl,
|
|
"CREATE TABLE dst.\"" + safe + "\"");
|
|
}
|
|
using (var ct = new System.Data.SQLite.SQLiteCommand(dstCreateSql, srcConn))
|
|
ct.ExecuteNonQuery();
|
|
|
|
// 2) INSERT 数据
|
|
using (var ins = new System.Data.SQLite.SQLiteCommand(
|
|
"INSERT INTO " + dst + " SELECT * FROM " + main + ";", srcConn))
|
|
ins.ExecuteNonQuery();
|
|
}
|
|
else
|
|
{
|
|
// 兜底:CREATE AS SELECT(自增主键不会保留,但本场景没主键也无所谓)
|
|
using (var cp = new System.Data.SQLite.SQLiteCommand(
|
|
"CREATE TABLE " + dst + " AS SELECT * FROM " + main + ";", srcConn))
|
|
cp.ExecuteNonQuery();
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
try
|
|
{
|
|
using (var det = new System.Data.SQLite.SQLiteCommand("DETACH DATABASE dst;", srcConn))
|
|
det.ExecuteNonQuery();
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
return; // 成功
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
sqlErr = ex;
|
|
Log("[项目复制] SQL 复制失败,改用 File.Copy: " + ex.Message);
|
|
TryDeleteFile(dstDbPath); // 清理半成品
|
|
}
|
|
|
|
// ② 兜底:File.Copy + ClearAllPools 重试 3 次
|
|
for (int attempt = 1; attempt <= 3; attempt++)
|
|
{
|
|
try
|
|
{
|
|
System.Data.SQLite.SQLiteConnection.ClearAllPools();
|
|
GC.Collect();
|
|
GC.WaitForPendingFinalizers();
|
|
System.Threading.Thread.Sleep(50 * attempt);
|
|
System.IO.File.Copy(srcDbPath, dstDbPath, true);
|
|
return;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log("[项目复制] File.Copy 第 " + attempt + " 次失败: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
throw new Exception("无法复制数据库文件。\n" +
|
|
"SQL 复制失败: " + (sqlErr == null ? "(未知)" : sqlErr.Message) + "\n" +
|
|
"File.Copy 也失败(权限/占用),请关闭其他打开此项目的程序后重试。");
|
|
}
|
|
|
|
/// <summary>尝试删除文件,删不掉就改名,实在不行就忽略。</summary>
|
|
private static void TryDeleteFile(string path)
|
|
{
|
|
if (!System.IO.File.Exists(path)) return;
|
|
try { System.IO.File.Delete(path); return; } catch { }
|
|
try
|
|
{
|
|
string tmp = path + ".old_" + Guid.NewGuid().ToString("N").Substring(0, 6);
|
|
System.IO.File.Move(path, tmp);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
/// <summary>从项目表读取所有项目(每个项目是一个字段字典)。</summary>
|
|
private System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>> ReadAllProjectsFromDatabase()
|
|
{
|
|
var list = new System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>>();
|
|
try
|
|
{
|
|
string dbPath = GetDbFullPath(ProjectsDbPath);
|
|
if (!System.IO.File.Exists(dbPath)) return list;
|
|
|
|
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
|
|
{
|
|
conn.Open();
|
|
using (var cmd = new System.Data.SQLite.SQLiteCommand(
|
|
"SELECT * FROM " + ProjectsTableName + " ORDER BY rowid;", conn))
|
|
using (var rd = cmd.ExecuteReader())
|
|
{
|
|
while (rd.Read())
|
|
{
|
|
var dict = new System.Collections.Generic.Dictionary<string, object>(StringComparer.Ordinal);
|
|
for (int i = 0; i < rd.FieldCount; i++)
|
|
{
|
|
string col = rd.GetName(i);
|
|
object v = rd.GetValue(i);
|
|
if (v == null || v == DBNull.Value) v = "";
|
|
dict[col] = v;
|
|
}
|
|
list.Add(dict);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log("[项目导出] 读项目表失败: " + ex.Message);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 弹出一个简单对话框,让用户从项目列表中选一个项目。
|
|
/// 返回选中的"项目编号",用户取消返回空字符串。
|
|
/// </summary>
|
|
/// <summary>
|
|
/// 弹出项目选择对话框(支持多选),返回选中的项目编号列表。
|
|
/// </summary>
|
|
private System.Collections.Generic.List<string> PickProjectDialogMulti(System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>> projects)
|
|
{
|
|
var result = new System.Collections.Generic.List<string>();
|
|
try
|
|
{
|
|
using (var dlg = new System.Windows.Forms.Form
|
|
{
|
|
Text = "选择要导出的项目(可多选)",
|
|
StartPosition = FormStartPosition.CenterParent,
|
|
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog,
|
|
MaximizeBox = false,
|
|
MinimizeBox = false,
|
|
ClientSize = new System.Drawing.Size(520, 380),
|
|
})
|
|
{
|
|
var lbl = new System.Windows.Forms.Label
|
|
{
|
|
Text = "请选择要导出的项目(Ctrl/Shift 可多选):",
|
|
Location = new System.Drawing.Point(12, 12),
|
|
Size = new System.Drawing.Size(496, 23),
|
|
TextAlign = System.Drawing.ContentAlignment.MiddleLeft,
|
|
};
|
|
dlg.Controls.Add(lbl);
|
|
|
|
var lv = new System.Windows.Forms.ListView
|
|
{
|
|
Location = new System.Drawing.Point(12, 40),
|
|
Size = new System.Drawing.Size(496, 280),
|
|
View = System.Windows.Forms.View.Details,
|
|
FullRowSelect = true,
|
|
MultiSelect = true, // ★ 允许多选
|
|
HideSelection = false,
|
|
};
|
|
lv.Columns.Add("项目编号", 100);
|
|
lv.Columns.Add("项目名称", 160);
|
|
lv.Columns.Add("客户名称", 130);
|
|
lv.Columns.Add("项目日期", 90);
|
|
|
|
foreach (var p in projects)
|
|
{
|
|
string bh = p.ContainsKey("项目编号") ? (p["项目编号"] ?? "").ToString() : "";
|
|
string mc = p.ContainsKey("项目名称") ? (p["项目名称"] ?? "").ToString() : "";
|
|
string kh = p.ContainsKey("客户名称") ? (p["客户名称"] ?? "").ToString() : "";
|
|
string rq = p.ContainsKey("项目日期") ? (p["项目日期"] ?? "").ToString() : "";
|
|
|
|
var item = new System.Windows.Forms.ListViewItem(bh);
|
|
item.SubItems.Add(mc);
|
|
item.SubItems.Add(kh);
|
|
item.SubItems.Add(rq);
|
|
lv.Items.Add(item);
|
|
}
|
|
|
|
if (lv.Items.Count > 0)
|
|
lv.Items[0].Selected = true;
|
|
|
|
dlg.Controls.Add(lv);
|
|
|
|
// "全选"按钮
|
|
var btnAll = new System.Windows.Forms.Button
|
|
{
|
|
Text = "全选",
|
|
Location = new System.Drawing.Point(12, 330),
|
|
Size = new System.Drawing.Size(75, 28),
|
|
};
|
|
btnAll.Click += (s2, e2) => { foreach (System.Windows.Forms.ListViewItem it in lv.Items) it.Selected = true; };
|
|
dlg.Controls.Add(btnAll);
|
|
|
|
var btnOk = new System.Windows.Forms.Button
|
|
{
|
|
Text = "导出选中项",
|
|
DialogResult = System.Windows.Forms.DialogResult.OK,
|
|
Location = new System.Drawing.Point(330, 330),
|
|
Size = new System.Drawing.Size(85, 28),
|
|
};
|
|
dlg.Controls.Add(btnOk);
|
|
|
|
var btnCancel = new System.Windows.Forms.Button
|
|
{
|
|
Text = "取消",
|
|
DialogResult = System.Windows.Forms.DialogResult.Cancel,
|
|
Location = new System.Drawing.Point(423, 330),
|
|
Size = new System.Drawing.Size(85, 28),
|
|
};
|
|
dlg.Controls.Add(btnCancel);
|
|
|
|
dlg.AcceptButton = btnOk;
|
|
dlg.CancelButton = btnCancel;
|
|
|
|
if (dlg.ShowDialog(this) == System.Windows.Forms.DialogResult.OK)
|
|
{
|
|
if (lv.SelectedItems != null)
|
|
{
|
|
foreach (System.Windows.Forms.ListViewItem it in lv.SelectedItems)
|
|
{
|
|
result.Add(it.Text); // 第 0 列就是项目编号
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log("[项目导出] 弹选择对话框失败: " + ex.Message);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private string PickProjectDialog(System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>> projects)
|
|
{
|
|
try
|
|
{
|
|
// 用一个临时 Form 做选择对话框
|
|
using (var dlg = new System.Windows.Forms.Form
|
|
{
|
|
Text = "选择要导出的项目",
|
|
StartPosition = FormStartPosition.CenterParent,
|
|
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog,
|
|
MaximizeBox = false,
|
|
MinimizeBox = false,
|
|
ClientSize = new System.Drawing.Size(520, 360),
|
|
})
|
|
{
|
|
var lbl = new System.Windows.Forms.Label
|
|
{
|
|
Text = "请选择要导出的项目:",
|
|
Location = new System.Drawing.Point(12, 12),
|
|
Size = new System.Drawing.Size(496, 23),
|
|
TextAlign = System.Drawing.ContentAlignment.MiddleLeft,
|
|
};
|
|
dlg.Controls.Add(lbl);
|
|
|
|
var lv = new System.Windows.Forms.ListView
|
|
{
|
|
Location = new System.Drawing.Point(12, 40),
|
|
Size = new System.Drawing.Size(496, 260),
|
|
View = System.Windows.Forms.View.Details,
|
|
FullRowSelect = true,
|
|
MultiSelect = false,
|
|
HideSelection = false,
|
|
};
|
|
lv.Columns.Add("项目编号", 100);
|
|
lv.Columns.Add("项目名称", 160);
|
|
lv.Columns.Add("客户名称", 130);
|
|
lv.Columns.Add("项目日期", 90);
|
|
|
|
foreach (var p in projects)
|
|
{
|
|
string bh = p.ContainsKey("项目编号") ? (p["项目编号"] ?? "").ToString() : "";
|
|
string mc = p.ContainsKey("项目名称") ? (p["项目名称"] ?? "").ToString() : "";
|
|
string kh = p.ContainsKey("客户名称") ? (p["客户名称"] ?? "").ToString() : "";
|
|
string rq = p.ContainsKey("项目日期") ? (p["项目日期"] ?? "").ToString() : "";
|
|
|
|
var item = new System.Windows.Forms.ListViewItem(bh);
|
|
item.SubItems.Add(mc);
|
|
item.SubItems.Add(kh);
|
|
item.SubItems.Add(rq);
|
|
lv.Items.Add(item);
|
|
}
|
|
|
|
if (lv.Items.Count > 0)
|
|
lv.Items[0].Selected = true;
|
|
|
|
dlg.Controls.Add(lv);
|
|
|
|
var btnOk = new System.Windows.Forms.Button
|
|
{
|
|
Text = "导出选中项",
|
|
DialogResult = System.Windows.Forms.DialogResult.OK,
|
|
Location = new System.Drawing.Point(330, 310),
|
|
Size = new System.Drawing.Size(85, 28),
|
|
};
|
|
dlg.Controls.Add(btnOk);
|
|
|
|
var btnCancel = new System.Windows.Forms.Button
|
|
{
|
|
Text = "取消",
|
|
DialogResult = System.Windows.Forms.DialogResult.Cancel,
|
|
Location = new System.Drawing.Point(423, 310),
|
|
Size = new System.Drawing.Size(85, 28),
|
|
};
|
|
dlg.Controls.Add(btnCancel);
|
|
|
|
dlg.AcceptButton = btnOk;
|
|
dlg.CancelButton = btnCancel;
|
|
|
|
// 双击列表项也相当于点确定
|
|
lv.DoubleClick += (s2, e2) => { dlg.DialogResult = System.Windows.Forms.DialogResult.OK; };
|
|
|
|
if (dlg.ShowDialog(this) == System.Windows.Forms.DialogResult.OK)
|
|
{
|
|
if (lv.SelectedItems != null && lv.SelectedItems.Count > 0)
|
|
{
|
|
return lv.SelectedItems[0].Text; // 第 0 列就是项目编号
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log("[项目导出] 弹选择对话框失败: " + ex.Message);
|
|
}
|
|
return "";
|
|
}
|
|
|
|
// <summary>导入项目:从 .zip 包导入一个项目(冲突时自动改名)。</summary>
|
|
private void button8_Click(object sender, EventArgs e)
|
|
{
|
|
try
|
|
{
|
|
// 默认打开桌面(避免 exe 所在的源码目录无权限)
|
|
string initialDir = "";
|
|
try
|
|
{
|
|
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
|
if (!string.IsNullOrEmpty(desktop) && System.IO.Directory.Exists(desktop))
|
|
initialDir = desktop;
|
|
}
|
|
catch { }
|
|
|
|
using (var ofd = new OpenFileDialog
|
|
{
|
|
Title = "导入项目(可多选)",
|
|
Filter = "项目包 (*.zip)|*.zip",
|
|
RestoreDirectory = true,
|
|
Multiselect = true, // ★ 允许多选 zip 文件
|
|
})
|
|
{
|
|
if (!string.IsNullOrEmpty(initialDir))
|
|
ofd.InitialDirectory = initialDir;
|
|
|
|
if (ofd.ShowDialog(this) != DialogResult.OK) return;
|
|
|
|
int totalOk = 0;
|
|
int totalFail = 0;
|
|
var importedPids = new System.Collections.Generic.List<string>();
|
|
|
|
foreach (string zipPath in ofd.FileNames)
|
|
{
|
|
// 每个 zip 里可能有一个或多个项目(子文件夹)
|
|
var pids = ImportAllProjectsFromZip(zipPath);
|
|
if (pids != null && pids.Count > 0)
|
|
{
|
|
totalOk += pids.Count;
|
|
importedPids.AddRange(pids);
|
|
}
|
|
else
|
|
{
|
|
totalFail++;
|
|
}
|
|
}
|
|
|
|
// 刷新项目列表
|
|
if (totalOk > 0) ReloadProjectsFromDatabase();
|
|
|
|
string msg;
|
|
if (ofd.FileNames.Length == 1 && totalOk == 1)
|
|
{
|
|
msg = "导入成功!\n项目编号:" + importedPids[0];
|
|
}
|
|
else
|
|
{
|
|
msg = "批量导入完成!\n\n成功导入 " + totalOk + " 个项目";
|
|
if (totalFail > 0) msg += ",失败 " + totalFail + " 个压缩包";
|
|
msg += "\n导入的项目:\n" + string.Join("\n", importedPids.ToArray());
|
|
}
|
|
MessageBox.Show(this, msg,
|
|
"导入项目", MessageBoxButtons.OK,
|
|
totalOk > 0 ? MessageBoxIcon.Information : MessageBoxIcon.Warning);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show(this, "导入失败:\n" + ex.Message,
|
|
"导入项目", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
Log("[项目导入异常] " + ex.Message);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从 zip 包导入所有项目(支持 zip 根目录单个项目 + 子文件夹多个项目)。
|
|
/// 返回成功导入的项目编号列表。
|
|
/// </summary>
|
|
private System.Collections.Generic.List<string> ImportAllProjectsFromZip(string zipPath)
|
|
{
|
|
var result = new System.Collections.Generic.List<string>();
|
|
if (!System.IO.File.Exists(zipPath))
|
|
{
|
|
MessageBox.Show(this, "项目包文件不存在:\n" + zipPath,
|
|
"导入项目", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return result;
|
|
}
|
|
|
|
// 解压到临时目录
|
|
string tempDir = System.IO.Path.Combine(System.IO.Path.GetTempPath(),
|
|
"成套报价_导入_" + System.Guid.NewGuid().ToString("N"));
|
|
System.IO.Directory.CreateDirectory(tempDir);
|
|
try
|
|
{
|
|
System.IO.Compression.ZipFile.ExtractToDirectory(zipPath, tempDir);
|
|
|
|
// 扫描所有含 meta.json 的目录(根目录 + 所有子文件夹)
|
|
var metaDirs = new System.Collections.Generic.List<string>();
|
|
// ① 根目录
|
|
if (System.IO.File.Exists(System.IO.Path.Combine(tempDir, "meta.json")))
|
|
metaDirs.Add(tempDir);
|
|
// ② 所有子文件夹
|
|
foreach (var sub in System.IO.Directory.GetDirectories(tempDir, "*", System.IO.SearchOption.AllDirectories))
|
|
{
|
|
if (System.IO.File.Exists(System.IO.Path.Combine(sub, "meta.json")))
|
|
metaDirs.Add(sub);
|
|
}
|
|
|
|
if (metaDirs.Count == 0)
|
|
{
|
|
MessageBox.Show(this, "无效的项目包:缺少 meta.json\n" + zipPath,
|
|
"导入项目", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return result;
|
|
}
|
|
|
|
Log("[项目导入] " + zipPath + " 中发现 " + metaDirs.Count + " 个项目");
|
|
|
|
// 逐个导入
|
|
foreach (string dir in metaDirs)
|
|
{
|
|
string pid = ImportSingleProjectFromDir(dir);
|
|
if (!string.IsNullOrEmpty(pid)) result.Add(pid);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
try { System.IO.Directory.Delete(tempDir, true); } catch { }
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>从指定目录导入单个项目(读 meta.json + db),返回新项目编号。</summary>
|
|
private string ImportSingleProjectFromDir(string dir)
|
|
{
|
|
try
|
|
{
|
|
string metaPath = System.IO.Path.Combine(dir, "meta.json");
|
|
if (!System.IO.File.Exists(metaPath)) return null;
|
|
|
|
string metaJson = System.IO.File.ReadAllText(metaPath, new System.Text.UTF8Encoding(false));
|
|
var fields = ParseSimpleJsonFields(metaJson);
|
|
if (fields == null || fields.Count == 0) return null;
|
|
|
|
string origPid = fields.ContainsKey("项目编号") ? (fields["项目编号"] ?? "").ToString() : "";
|
|
if (string.IsNullOrWhiteSpace(origPid)) origPid = "未命名";
|
|
|
|
// 找 db 文件
|
|
string[] dbFiles = System.IO.Directory.GetFiles(dir, "*.db");
|
|
if (dbFiles.Length == 0) return null;
|
|
string srcDbPath = dbFiles[0];
|
|
|
|
// 项目编号冲突时改名
|
|
string newPid = origPid;
|
|
int suffix = 1;
|
|
while (ProjectExists(newPid) || System.IO.File.Exists(GetQuoteDbPath(newPid)))
|
|
{
|
|
newPid = origPid + "_" + suffix.ToString();
|
|
suffix++;
|
|
if (suffix > 999) break;
|
|
}
|
|
|
|
// 复制 db
|
|
string dstDbPath = GetQuoteDbPath(newPid);
|
|
string dstDir = System.IO.Path.GetDirectoryName(dstDbPath);
|
|
if (!System.IO.Directory.Exists(dstDir))
|
|
System.IO.Directory.CreateDirectory(dstDir);
|
|
BackupSQLiteDb(srcDbPath, dstDbPath);
|
|
|
|
// 改写库里的项目编号
|
|
try
|
|
{
|
|
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dstDbPath)))
|
|
{
|
|
conn.Open();
|
|
using (var chk = new System.Data.SQLite.SQLiteCommand(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='项目信息';", conn))
|
|
{
|
|
object o = chk.ExecuteScalar();
|
|
if (o != null)
|
|
{
|
|
using (var upd = new System.Data.SQLite.SQLiteCommand(
|
|
"UPDATE 项目信息 SET 项目编号 = @p;", conn))
|
|
{
|
|
upd.Parameters.AddWithValue("@p", newPid);
|
|
upd.ExecuteNonQuery();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
// 插入项目列表
|
|
fields["项目编号"] = newPid;
|
|
InsertProjectRow(fields);
|
|
|
|
Log("[项目导入] " + origPid + " -> " + newPid +
|
|
(newPid != origPid ? " (原编号:" + origPid + ")" : ""));
|
|
return newPid;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log("[项目导入] 单个项目导入失败: " + ex.Message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>从 zip 包导入项目,返回新项目编号(失败返回 null)。</summary>
|
|
private string ImportProjectFromZip(string zipPath)
|
|
{
|
|
try
|
|
{
|
|
if (!System.IO.File.Exists(zipPath))
|
|
{
|
|
MessageBox.Show(this, "项目包文件不存在:\n" + zipPath,
|
|
"导入项目", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return null;
|
|
}
|
|
|
|
// 解压到临时目录
|
|
string tempDir = System.IO.Path.Combine(System.IO.Path.GetTempPath(),
|
|
"成套报价_导入_" + Guid.NewGuid().ToString("N"));
|
|
System.IO.Directory.CreateDirectory(tempDir);
|
|
try
|
|
{
|
|
System.IO.Compression.ZipFile.ExtractToDirectory(zipPath, tempDir);
|
|
|
|
// 读 meta.json
|
|
string metaPath = System.IO.Path.Combine(tempDir, "meta.json");
|
|
if (!System.IO.File.Exists(metaPath))
|
|
{
|
|
MessageBox.Show(this, "无效的项目包:缺少 meta.json",
|
|
"导入项目", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return null;
|
|
}
|
|
string metaJson = System.IO.File.ReadAllText(metaPath, new System.Text.UTF8Encoding(false));
|
|
var fields = ParseSimpleJsonFields(metaJson);
|
|
if (fields == null || fields.Count == 0)
|
|
{
|
|
MessageBox.Show(this, "无效的项目包:meta.json 解析失败",
|
|
"导入项目", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return null;
|
|
}
|
|
|
|
// 原 pid(可能跟现有冲突,后面要改名)
|
|
string origPid = fields.ContainsKey("项目编号") ? (fields["项目编号"] ?? "").ToString() : "";
|
|
if (string.IsNullOrWhiteSpace(origPid))
|
|
origPid = "未命名";
|
|
|
|
// 找包里的 db 文件(只取第一个 .db)
|
|
string[] dbFiles = System.IO.Directory.GetFiles(tempDir, "*.db");
|
|
if (dbFiles.Length == 0)
|
|
{
|
|
MessageBox.Show(this, "无效的项目包:缺少 .db 文件",
|
|
"导入项目", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return null;
|
|
}
|
|
string srcDbPath = dbFiles[0];
|
|
|
|
// 项目编号冲突时改名:在末尾加 _1, _2, ...
|
|
string newPid = origPid;
|
|
int suffix = 1;
|
|
while (ProjectExists(newPid) || System.IO.File.Exists(GetQuoteDbPath(newPid)))
|
|
{
|
|
newPid = origPid + "_" + suffix.ToString();
|
|
suffix++;
|
|
if (suffix > 999) break; // 防止死循环
|
|
}
|
|
|
|
// 把 db 复制到 sjk\bjb\项目<newPid>.db
|
|
string dstDbPath = GetQuoteDbPath(newPid);
|
|
string dstDir = System.IO.Path.GetDirectoryName(dstDbPath);
|
|
if (!System.IO.Directory.Exists(dstDir))
|
|
System.IO.Directory.CreateDirectory(dstDir);
|
|
// 用 SQLite Backup API 复制(避免目标路径被连接池锁住)
|
|
BackupSQLiteDb(srcDbPath, dstDbPath);
|
|
|
|
// 改写库里的"项目信息"表的项目编号字段(同步成新 pid)
|
|
try
|
|
{
|
|
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dstDbPath)))
|
|
{
|
|
conn.Open();
|
|
using (var chk = new System.Data.SQLite.SQLiteCommand(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='项目信息';", conn))
|
|
{
|
|
object o = chk.ExecuteScalar();
|
|
if (o != null)
|
|
{
|
|
using (var upd = new System.Data.SQLite.SQLiteCommand(
|
|
"UPDATE 项目信息 SET 项目编号 = @p;", conn))
|
|
{
|
|
upd.Parameters.AddWithValue("@p", newPid);
|
|
upd.ExecuteNonQuery();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch { /* 项目信息表更新失败不阻断 */ }
|
|
|
|
// 把项目元数据 INSERT 到项目列表(用新 pid)
|
|
fields["项目编号"] = newPid;
|
|
InsertProjectRow(fields);
|
|
|
|
// 刷新项目列表显示
|
|
ReloadProjectsFromDatabase();
|
|
|
|
Log("[项目导入] " + zipPath + " -> " + newPid +
|
|
(newPid != origPid ? " (原编号:" + origPid + ")" : ""));
|
|
return newPid;
|
|
}
|
|
finally
|
|
{
|
|
try { System.IO.Directory.Delete(tempDir, true); } catch { }
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show(this, "导入失败:\n" + ex.Message,
|
|
"导入项目", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
Log("[项目导入异常] " + ex.Message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 取当前要导出/导入操作的项目编号。
|
|
/// 优先级:① 项目列表(advancedDataGridView6)中用户选中的行
|
|
/// ② 已双击进入的当前项目(_currentProjectId)
|
|
/// </summary>
|
|
private string GetCurrentProjectIdFromGrid()
|
|
{
|
|
try
|
|
{
|
|
var grid = this.advancedDataGridView6;
|
|
if (grid != null && grid.Columns.Contains("项目编号"))
|
|
{
|
|
// ① CurrentRow(无论 SelectionMode,只要用户在 grid 上点击过,这就是当前行)
|
|
if (grid.CurrentRow != null && grid.CurrentRow.Index >= 0
|
|
&& grid.CurrentRow.Index < grid.Rows.Count)
|
|
{
|
|
object v = grid.CurrentRow.Cells["项目编号"].Value;
|
|
if (v != null && v != DBNull.Value)
|
|
{
|
|
string s = v.ToString();
|
|
if (!string.IsNullOrWhiteSpace(s)) return s;
|
|
}
|
|
}
|
|
|
|
// ② SelectedRows(RowHeaderSelect 模式下点行头会进 SelectedRows)
|
|
if (grid.SelectedRows != null && grid.SelectedRows.Count > 0)
|
|
{
|
|
foreach (DataGridViewRow sr in grid.SelectedRows)
|
|
{
|
|
if (sr.Index < 0) continue;
|
|
object v = sr.Cells["项目编号"].Value;
|
|
if (v != null && v != DBNull.Value)
|
|
{
|
|
string s = v.ToString();
|
|
if (!string.IsNullOrWhiteSpace(s)) return s;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ③ SelectedCells 回退:单元格选中模式下,从选中单元格反查所属行
|
|
if (grid.SelectedCells != null && grid.SelectedCells.Count > 0)
|
|
{
|
|
int rowIdx = grid.SelectedCells[0].RowIndex;
|
|
if (rowIdx >= 0 && rowIdx < grid.Rows.Count)
|
|
{
|
|
object v = grid.Rows[rowIdx].Cells["项目编号"].Value;
|
|
if (v != null && v != DBNull.Value)
|
|
{
|
|
string s = v.ToString();
|
|
if (!string.IsNullOrWhiteSpace(s)) return s;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ④ FirstDisplayedScrollingRowIndex(用户滚动到的那行)
|
|
if (grid.FirstDisplayedScrollingRowIndex >= 0
|
|
&& grid.FirstDisplayedScrollingRowIndex < grid.Rows.Count)
|
|
{
|
|
object v = grid.Rows[grid.FirstDisplayedScrollingRowIndex].Cells["项目编号"].Value;
|
|
if (v != null && v != DBNull.Value)
|
|
{
|
|
string s = v.ToString();
|
|
if (!string.IsNullOrWhiteSpace(s)) return s;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
// ⑤ 兜底:已经双击进入的项目
|
|
if (!string.IsNullOrWhiteSpace(_currentProjectId))
|
|
return _currentProjectId;
|
|
|
|
return "";
|
|
}
|
|
|
|
/// <summary>从项目表读取指定项目编号那一行的所有字段(字典)。</summary>
|
|
private System.Collections.Generic.Dictionary<string, object> ReadProjectMetaData(string pid)
|
|
{
|
|
var dict = new System.Collections.Generic.Dictionary<string, object>(StringComparer.Ordinal);
|
|
try
|
|
{
|
|
string dbPath = GetDbFullPath(ProjectsDbPath);
|
|
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
|
|
{
|
|
conn.Open();
|
|
using (var cmd = new System.Data.SQLite.SQLiteCommand(
|
|
"SELECT * FROM " + ProjectsTableName + " WHERE 项目编号 = @p LIMIT 1;", conn))
|
|
{
|
|
cmd.Parameters.AddWithValue("@p", pid);
|
|
using (var rd = cmd.ExecuteReader())
|
|
{
|
|
if (!rd.Read()) return null;
|
|
for (int i = 0; i < rd.FieldCount; i++)
|
|
{
|
|
string col = rd.GetName(i);
|
|
object v = rd.GetValue(i);
|
|
if (v == null || v == DBNull.Value) v = "";
|
|
dict[col] = v;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch { return null; }
|
|
return dict;
|
|
}
|
|
|
|
/// <summary>把项目元数据字段字典插入到项目表(过滤不存在的列)。</summary>
|
|
private void InsertProjectRow(System.Collections.Generic.Dictionary<string, string> fields)
|
|
{
|
|
try
|
|
{
|
|
string dbPath = GetDbFullPath(ProjectsDbPath);
|
|
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
|
|
{
|
|
conn.Open();
|
|
// 读项目表实际有的列
|
|
var tableCols = new System.Collections.Generic.HashSet<string>(StringComparer.Ordinal);
|
|
using (var infoCmd = new System.Data.SQLite.SQLiteCommand(
|
|
"PRAGMA table_info(" + ProjectsTableName + ");", conn))
|
|
using (var rd = infoCmd.ExecuteReader())
|
|
{
|
|
while (rd.Read())
|
|
tableCols.Add(rd.GetString(1));
|
|
}
|
|
|
|
// 只插入项目表有的列
|
|
var colNames = new System.Collections.Generic.List<string>();
|
|
var paramNames = new System.Collections.Generic.List<string>();
|
|
foreach (var kv in fields)
|
|
{
|
|
if (tableCols.Contains(kv.Key))
|
|
{
|
|
colNames.Add(kv.Key);
|
|
paramNames.Add("@" + kv.Key);
|
|
}
|
|
}
|
|
if (colNames.Count == 0) return;
|
|
|
|
string sql = "INSERT INTO " + ProjectsTableName + " (" +
|
|
string.Join(", ", colNames.ToArray()) + ") VALUES (" +
|
|
string.Join(", ", paramNames.ToArray()) + ");";
|
|
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
|
|
{
|
|
foreach (string col in colNames)
|
|
cmd.Parameters.AddWithValue("@" + col, fields[col] ?? "");
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log("[项目导入] 插入项目表失败: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
/// <summary>把字符串转成合法文件名(替换非法字符为下划线)。</summary>
|
|
private static string MakeSafeFileName(string s)
|
|
{
|
|
if (string.IsNullOrEmpty(s)) return "未命名";
|
|
string r = s;
|
|
char[] invalid = System.IO.Path.GetInvalidFileNameChars();
|
|
foreach (char c in invalid) r = r.Replace(c, '_');
|
|
return r;
|
|
}
|
|
|
|
/// <summary>简单的 JSON 字符串转义。</summary>
|
|
private static string EscapeJsonString(string s)
|
|
{
|
|
if (s == null) return "";
|
|
return s.Replace("\\", "\\\\").Replace("\"", "\\\"")
|
|
.Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 极简 JSON 解析:只支持读 { "字段": { "k":"v", "k2":"v2" }, "项目编号":"xxx" } 这种结构。
|
|
/// 返回 "字段" 字典 + "项目编号" 顶层键合并到一起(用于导入元数据)。
|
|
/// 不支持数组、嵌套对象等(本场景够用)。
|
|
/// </summary>
|
|
private static System.Collections.Generic.Dictionary<string, string> ParseSimpleJsonFields(string json)
|
|
{
|
|
var dict = new System.Collections.Generic.Dictionary<string, string>(StringComparer.Ordinal);
|
|
if (string.IsNullOrWhiteSpace(json)) return dict;
|
|
try
|
|
{
|
|
// 用正则提取所有 "键": "值" 配对(不区分是否嵌套,统统拍平)
|
|
// 值里可能含转义 \" \\ 等,贪婪到第一个未转义的 "
|
|
string pattern = "\"((?:[^\"\\\\]|\\\\.)*)\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"";
|
|
var rx = new System.Text.RegularExpressions.Regex(pattern);
|
|
var matches = rx.Matches(json);
|
|
foreach (System.Text.RegularExpressions.Match m in matches)
|
|
{
|
|
string key = UnescapeJsonString(m.Groups[1].Value);
|
|
string val = UnescapeJsonString(m.Groups[2].Value);
|
|
if (!dict.ContainsKey(key))
|
|
dict[key] = val;
|
|
}
|
|
return dict;
|
|
}
|
|
catch
|
|
{
|
|
return dict;
|
|
}
|
|
}
|
|
|
|
/// <summary>反转 JSON 字符串的转义。</summary>
|
|
private static string UnescapeJsonString(string s)
|
|
{
|
|
if (string.IsNullOrEmpty(s)) return s;
|
|
return s.Replace("\\\"", "\"").Replace("\\\\", "\\")
|
|
.Replace("\\n", "\n").Replace("\\r", "\r").Replace("\\t", "\t");
|
|
}
|
|
|
|
}
|
|
}
|