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 成套报价软件 { /// 主窗口 拆分文件:由 主窗口.cs 按模块拆出,同一 partial class。 public partial class MainForm { // ============================================================ // 项目导入/导出 // · 导出:把选中项目的独立库(项目<编号>.db)和元数据(项目列表那一行) // 打包成一个 .zip 文件,带元数据 meta.json // · 导入:解压 .zip → 项目编号冲突时自动改名 → db 放到 sjk\bjb\ → // 元数据 INSERT 到项目列表 → 刷新项目表显示 // ============================================================ // 导出项目:读项目表 → 弹列表让用户选 → 打包导出。 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(); 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(); 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); } } /// /// 把多个项目打到同一个 .zip 包里,每个项目一个子文件夹(以项目名称命名)。 /// 返回成功导出的数量。 /// private int ExportProjectsToSingleZip(System.Collections.Generic.List 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(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; } /// /// 把多个项目逐个导出为独立的 .zip 文件。 /// 导出到 exportDir 目录(多项目),或 singleZipPath(单项目)。 /// 返回成功导出的数量。 /// // ---- 项目云窗体(项目云.cs)用的转发包装:原方法 private,包装成 public 供跨类调用 ---- public System.Collections.Generic.List> ReadAllProjectsFromDatabaseForCloud() { return ReadAllProjectsFromDatabase(); } public string GetQuoteDbPathForCloud(string pid) { return GetQuoteDbPath(pid); } public System.Collections.Generic.Dictionary 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 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; } /// /// 复制 SQLite 数据库文件到目标路径,解决"没有权限/被占用"问题。 /// 主路径:用 SQL ATTACH + CREATE TABLE AS SELECT 在 SQL 层复制,完全绕开文件锁 /// 兜底:File.Copy + ClearAllPools(以防 SQL 复制失败) /// 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(); 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 也失败(权限/占用),请关闭其他打开此项目的程序后重试。"); } /// 尝试删除文件,删不掉就改名,实在不行就忽略。 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 { } } /// 从项目表读取所有项目(每个项目是一个字段字典)。 private System.Collections.Generic.List> ReadAllProjectsFromDatabase() { var list = new System.Collections.Generic.List>(); 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(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; } /// /// 弹出一个简单对话框,让用户从项目列表中选一个项目。 /// 返回选中的"项目编号",用户取消返回空字符串。 /// /// /// 弹出项目选择对话框(支持多选),返回选中的项目编号列表。 /// private System.Collections.Generic.List PickProjectDialogMulti(System.Collections.Generic.List> projects) { var result = new System.Collections.Generic.List(); 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> 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 ""; } // 导入项目:从 .zip 包导入一个项目(冲突时自动改名)。 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(); 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); } } /// /// 从 zip 包导入所有项目(支持 zip 根目录单个项目 + 子文件夹多个项目)。 /// 返回成功导入的项目编号列表。 /// private System.Collections.Generic.List ImportAllProjectsFromZip(string zipPath) { var result = new System.Collections.Generic.List(); 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(); // ① 根目录 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; } /// 从指定目录导入单个项目(读 meta.json + db),返回新项目编号。 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; } } /// 从 zip 包导入项目,返回新项目编号(失败返回 null)。 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\项目.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; } } /// /// 取当前要导出/导入操作的项目编号。 /// 优先级:① 项目列表(advancedDataGridView6)中用户选中的行 /// ② 已双击进入的当前项目(_currentProjectId) /// 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 ""; } /// 从项目表读取指定项目编号那一行的所有字段(字典)。 private System.Collections.Generic.Dictionary ReadProjectMetaData(string pid) { var dict = new System.Collections.Generic.Dictionary(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; } /// 把项目元数据字段字典插入到项目表(过滤不存在的列)。 private void InsertProjectRow(System.Collections.Generic.Dictionary 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(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(); var paramNames = new System.Collections.Generic.List(); 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); } } /// 把字符串转成合法文件名(替换非法字符为下划线)。 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; } /// 简单的 JSON 字符串转义。 private static string EscapeJsonString(string s) { if (s == null) return ""; return s.Replace("\\", "\\\\").Replace("\"", "\\\"") .Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t"); } /// /// 极简 JSON 解析:只支持读 { "字段": { "k":"v", "k2":"v2" }, "项目编号":"xxx" } 这种结构。 /// 返回 "字段" 字典 + "项目编号" 顶层键合并到一起(用于导入元数据)。 /// 不支持数组、嵌套对象等(本场景够用)。 /// private static System.Collections.Generic.Dictionary ParseSimpleJsonFields(string json) { var dict = new System.Collections.Generic.Dictionary(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; } } /// 反转 JSON 字符串的转义。 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"); } } }