using System; using System.Collections.Generic; using System.Data; using System.Data.SqlClient; using System.Text; using System.Text.RegularExpressions; using BLL; namespace FineUIPro.Web.HSSE.PDFDOC { /// /// PDF 书籍维护页:左侧多级目录树 + 右侧富文本内容编辑(UMEditor,可输入文字、上传图片)。 /// 章节与内容全部手动录入维护:新增书籍 / 新增·编辑·删除章节 / 保存章节富文本内容。 /// 数据存 Sys_PdfBook / Sys_PdfChapter(内容为 HTML;上传图片在保存时转为 base64 内嵌,正文自包含)。 /// public partial class PdfBookView : PageBase { #region 页面加载 /// /// 页面加载。 /// protected void Page_Load(object sender, EventArgs e) { // 显式指定 UTF-8 编码,避免个别环境下中文乱码 this.Response.ContentEncoding = System.Text.Encoding.UTF8; this.Response.Charset = "utf-8"; if (!IsPostBack) { BindBooks(); if (this.drpBook.Items.Count > 0) { this.drpBook.SelectedIndex = 0; InitTree(this.drpBook.SelectedValue); } this.lblProgress.Text = ""; } } #endregion #region 绑定书籍 /// /// 绑定书籍下拉框。 /// private void BindBooks() { this.drpBook.Items.Clear(); foreach (PdfBook book in PdfBookService.GetBooks()) { this.drpBook.Items.Add(new FineUIPro.ListItem(book.Title, book.BookId)); } } #endregion #region 目录树 /// /// 初始化目录树(按父章节/层级递归建树,支持多级目录)。 /// private void InitTree(string bookId) { this.trCatalog.Nodes.Clear(); this.trCatalog.SelectedNodeID = ""; var chapters = PdfBookService.GetChapters(bookId); // 1) 先为每个章节创建节点 var nodeMap = new Dictionary(); foreach (PdfChapter c in chapters) { var node = new FineUIPro.TreeNode { Text = c.Title, NodeID = c.ChapterId, CommandName = "chapter", CommandArgument = c.ChapterId, ToolTip = c.PageStart > 0 ? ("第" + c.PageStart + " - " + c.PageEnd + " 页") : "", EnableClickEvent = true, Expanded = true // 页面默认全部展开 }; nodeMap[c.ChapterId] = node; } // 2) 按父章节挂接,没有父级(或父级不在列表)的作为顶级 var roots = new List(); foreach (PdfChapter c in chapters) { var node = nodeMap[c.ChapterId]; if (!string.IsNullOrWhiteSpace(c.ParentChapterId) && nodeMap.ContainsKey(c.ParentChapterId)) { nodeMap[c.ParentChapterId].Nodes.Add(node); } else { roots.Add(node); } } // 3) 顶级节点加入树 foreach (var r in roots) this.trCatalog.Nodes.Add(r); } #endregion #region 下拉切换 /// /// 切换书籍。 /// protected void drpBook_SelectedIndexChanged(object sender, EventArgs e) { InitTree(this.drpBook.SelectedValue); this.htmlContent.Text = ""; this.hfCurrentChapterId.Text = ""; this.lblProgress.Text = ""; } #endregion #region 目录点击 → 加载章节内容 /// /// 点击目录节点,把章节富文本内容加载到编辑器。 /// 所有章节(含带子章节的目录章节)均可编辑自身正文,点击即加载。 /// protected void trCatalog_NodeCommand(object sender, FineUIPro.TreeCommandEventArgs e) { if (e.Node == null) return; string chapterId = e.Node.CommandArgument; if (string.IsNullOrWhiteSpace(chapterId)) chapterId = e.Node.NodeID; PdfChapterDetail detail = PdfBookService.GetChapterContent(chapterId); this.htmlContent.Text = detail == null ? "" : (detail.Content ?? ""); this.hfCurrentChapterId.Text = chapterId; this.trCatalog.SelectedNodeID = e.Node.NodeID; this.lblProgress.Text = "正在编辑:" + (detail == null ? e.Node.Text : detail.Title); } #endregion #region 保存章节内容(富文本) /// /// 保存当前章节的富文本内容(HTML;本站上传图片引用自动转为 base64 内嵌存储)。 /// protected void btnSaveContent_Click(object sender, EventArgs e) { string chapterId = this.hfCurrentChapterId.Text; if (string.IsNullOrWhiteSpace(chapterId)) { FineUIPro.Alert.Show("请先在左侧目录选择章节。"); return; } // 本地上传图片转 base64 后入库,正文自包含(不依赖 upload 目录文件) string html = EmbedLocalImagesAsBase64(this.htmlContent.Text); SQLHelper.ExecutSql("UPDATE dbo.Sys_PdfChapter SET Text=N'" + SqlEsc(html) + "' WHERE ChapterId='" + SqlEsc(chapterId) + "'"); this.lblProgress.Text = "内容已保存:" + DateTime.Now.ToString("HH:mm:ss"); } /// /// 把 HTML 中指向本站 UMeditor 上传相关路径(/res/umeditor/net/..,含 ../../../upload 形态)的图片引用替换为 base64 内嵌。 /// 已是 data: 的图片与外部 http(s) 图片原样保留;路径非图片扩展名或文件不存在时不做替换(回退原引用)。 /// private static readonly Regex LocalUploadImgSrcRegex = new Regex( "src\\s*=\\s*([\"'])([^\"']*?/res/umeditor/net/[^\"']*?)\\1", RegexOptions.IgnoreCase | RegexOptions.Compiled); private string EmbedLocalImagesAsBase64(string html) { if (string.IsNullOrEmpty(html) || html.IndexOf("/res/umeditor/net/", StringComparison.OrdinalIgnoreCase) < 0) return html; return LocalUploadImgSrcRegex.Replace(html, delegate(Match m) { string quote = m.Groups[1].Value; string raw = m.Groups[2].Value; int i = raw.IndexOf("/res/umeditor/net/", StringComparison.OrdinalIgnoreCase); if (i < 0) return m.Value; string virtualPath = raw.Substring(i); int q = virtualPath.IndexOf('?'); if (q >= 0) virtualPath = virtualPath.Substring(0, q); string extCheck = System.IO.Path.GetExtension(virtualPath).ToLowerInvariant(); if (extCheck != ".png" && extCheck != ".jpg" && extCheck != ".jpeg" && extCheck != ".gif" && extCheck != ".bmp") return m.Value; try { string physical = Server.MapPath(virtualPath); if (!System.IO.File.Exists(physical)) return m.Value; byte[] bytes = System.IO.File.ReadAllBytes(physical); if (bytes.Length == 0) return m.Value; string ext = System.IO.Path.GetExtension(physical).ToLowerInvariant(); string mime = ext == ".png" ? "image/png" : ext == ".gif" ? "image/gif" : ext == ".bmp" ? "image/bmp" : "image/jpeg"; return "src=" + quote + "data:" + mime + ";base64," + Convert.ToBase64String(bytes) + quote; } catch { return m.Value; } }); } #endregion #region 书籍新增 /// /// 打开新增书籍弹窗。 /// protected void btnAddBook_Click(object sender, EventArgs e) { this.txtBookTitle.Text = ""; this.WindowBook.Hidden = false; } /// /// 保存新增书籍。 /// protected void btnSaveBook_Click(object sender, EventArgs e) { string title = this.txtBookTitle.Text.Trim(); if (title.Length == 0) { FineUIPro.Alert.Show("请输入书名。"); return; } string bookId = NewId("PB"); SQLHelper.ExecutSql( "INSERT INTO dbo.Sys_PdfBook (BookId, Title, FilePath, TotalPages, IndexStatus, CreateUser, CreateTime) VALUES ('" + SqlEsc(bookId) + "',N'" + SqlEsc(title) + "',NULL,0,2,N'test',GETDATE())"); this.WindowBook.Hidden = true; BindBooks(); this.drpBook.SelectedValue = bookId; InitTree(bookId); this.htmlContent.Text = ""; this.hfCurrentChapterId.Text = ""; this.lblProgress.Text = "已新增书籍:《" + title + "》,请继续添加章节。"; } /// /// 关闭新增书籍弹窗。 /// protected void btnCloseBook_Click(object sender, EventArgs e) { this.WindowBook.Hidden = true; } #endregion #region 章节新增/编辑/删除 /// /// 打开新增章节弹窗(父级=目录树选中节点,未选中则为顶级章节)。 /// protected void btnAddChapter_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(this.drpBook.SelectedValue)) { FineUIPro.Alert.Show("请先新增/选择书籍。"); return; } this.hfEditChapterId.Text = ""; this.txtChapterTitle.Text = ""; string parentId = this.trCatalog.SelectedNodeID; this.lblParentChapter.Text = string.IsNullOrEmpty(parentId) ? "顶级章节" : "选中节点的子章节"; this.WindowChapter.Title = "新增章节"; this.WindowChapter.Hidden = false; } /// /// 打开编辑章节弹窗(修改选中章节名称)。 /// protected void btnEditChapter_Click(object sender, EventArgs e) { string chapterId = this.trCatalog.SelectedNodeID; if (string.IsNullOrEmpty(chapterId)) { FineUIPro.Alert.Show("请先在目录中选择要编辑的章节。"); return; } DataTable dt = SQLHelper.GetDataTableRunText( "SELECT ChapterTitle FROM dbo.Sys_PdfChapter WHERE ChapterId=@id", new[] { new SqlParameter("@id", chapterId) }); if (dt == null || dt.Rows.Count == 0) { FineUIPro.Alert.Show("未找到该章节。"); return; } this.hfEditChapterId.Text = chapterId; this.txtChapterTitle.Text = dt.Rows[0]["ChapterTitle"].ToString(); this.lblParentChapter.Text = "修改章节名称"; this.WindowChapter.Title = "编辑章节"; this.WindowChapter.Hidden = false; } /// /// 保存章节(新增:挂在选中节点下,层级=父级+1;编辑:仅改名)。 /// 保存后保持选中刚保存的章节:叶子章节加载其内容,目录章节仅刷新选中、不动编辑器。 /// protected void btnSaveChapter_Click(object sender, EventArgs e) { string title = this.txtChapterTitle.Text.Trim(); if (title.Length == 0) { FineUIPro.Alert.Show("请输入章节名称。"); return; } string bookId = this.drpBook.SelectedValue; string affectedId; if (this.hfEditChapterId.Text != "") { // 编辑:仅改名称 affectedId = this.hfEditChapterId.Text; SQLHelper.ExecutSql("UPDATE dbo.Sys_PdfChapter SET ChapterTitle=N'" + SqlEsc(title) + "' WHERE ChapterId='" + SqlEsc(affectedId) + "'"); } else { // 新增:父级为目录树选中节点 string parentId = this.trCatalog.SelectedNodeID; int level = 1; if (!string.IsNullOrEmpty(parentId)) { int parentLevel = SQLHelper.GetIntValue( "SELECT ISNULL([Level],1) FROM dbo.Sys_PdfChapter WHERE ChapterId='" + SqlEsc(parentId) + "'"); level = parentLevel + 1; } int sort = SQLHelper.GetIntValue( "SELECT ISNULL(MAX(SortNo),0) FROM dbo.Sys_PdfChapter WHERE BookId='" + SqlEsc(bookId) + "'") + 1; affectedId = NewId("CH"); SQLHelper.ExecutSql( "INSERT INTO dbo.Sys_PdfChapter (ChapterId, BookId, ParentChapterId, [Level], ChapterTitle, PageStart, PageEnd, Text, SortNo) VALUES ('" + SqlEsc(affectedId) + "','" + SqlEsc(bookId) + "'," + (string.IsNullOrEmpty(parentId) ? "NULL" : "'" + SqlEsc(parentId) + "'") + "," + level + ",N'" + SqlEsc(title) + "',NULL,NULL,N''," + sort + ")"); } this.WindowChapter.Hidden = true; InitTree(bookId); // 保持选中刚保存的章节并加载其正文(目录章节同样可编辑正文) this.trCatalog.SelectedNodeID = affectedId; PdfChapterDetail detail = PdfBookService.GetChapterContent(affectedId); this.htmlContent.Text = detail == null ? "" : (detail.Content ?? ""); this.hfCurrentChapterId.Text = affectedId; this.lblProgress.Text = "章节已保存,正在编辑:" + (detail == null ? title : detail.Title); } /// /// 删除选中章节及其全部子章节。 /// protected void btnDelChapter_Click(object sender, EventArgs e) { string chapterId = this.trCatalog.SelectedNodeID; if (string.IsNullOrEmpty(chapterId)) { FineUIPro.Alert.Show("请先在目录中选择要删除的章节。"); return; } string bookId = this.drpBook.SelectedValue; var all = PdfBookService.GetChapters(bookId); // 递归收集要删除的章节(自身+全部后代) var ids = new List { chapterId }; bool changed = true; while (changed) { changed = false; foreach (PdfChapter c in all) { if (!ids.Contains(c.ChapterId) && ids.Contains(c.ParentChapterId ?? "")) { ids.Add(c.ChapterId); changed = true; } } } var sb = new StringBuilder(); foreach (string id in ids) { if (sb.Length > 0) sb.Append(","); sb.Append("'").Append(id.Replace("'", "''")).Append("'"); } SQLHelper.ExecutSql("DELETE FROM dbo.Sys_PdfChapter WHERE ChapterId IN (" + sb + ")"); InitTree(bookId); this.htmlContent.Text = ""; this.hfCurrentChapterId.Text = ""; this.lblProgress.Text = "章节已删除(含子章节 " + (ids.Count - 1) + " 个)。"; } /// /// 关闭章节弹窗。 /// protected void btnCloseChapter_Click(object sender, EventArgs e) { this.WindowChapter.Hidden = true; } #endregion #region 工具 /// SQL 字符串转义。 private static string SqlEsc(string s) { return (s ?? "").Replace("'", "''"); } /// 生成业务主键(前缀+时间+随机)。 private static string NewId(string prefix) { return prefix + DateTime.Now.ToString("yyyyMMddHHmmss") + Guid.NewGuid().ToString("N").Substring(0, 4); } #endregion } }