Files
ctbjrj/成套报价软件/MainForm.项目导入导出.cs
T

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");
}
}
}