432 lines
17 KiB
C#
432 lines
17 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// PDF 书籍维护页:左侧多级目录树 + 右侧富文本内容编辑(UMEditor,可输入文字、上传图片)。
|
||
/// 章节与内容全部手动录入维护:新增书籍 / 新增·编辑·删除章节 / 保存章节富文本内容。
|
||
/// 数据存 Sys_PdfBook / Sys_PdfChapter(内容为 HTML;上传图片在保存时转为 base64 内嵌,正文自包含)。
|
||
/// </summary>
|
||
public partial class PdfBookView : PageBase
|
||
{
|
||
#region 页面加载
|
||
/// <summary>
|
||
/// 页面加载。
|
||
/// </summary>
|
||
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 绑定书籍
|
||
/// <summary>
|
||
/// 绑定书籍下拉框。
|
||
/// </summary>
|
||
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 目录树
|
||
/// <summary>
|
||
/// 初始化目录树(按父章节/层级递归建树,支持多级目录)。
|
||
/// </summary>
|
||
private void InitTree(string bookId)
|
||
{
|
||
this.trCatalog.Nodes.Clear();
|
||
this.trCatalog.SelectedNodeID = "";
|
||
var chapters = PdfBookService.GetChapters(bookId);
|
||
|
||
// 1) 先为每个章节创建节点
|
||
var nodeMap = new Dictionary<string, FineUIPro.TreeNode>();
|
||
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<FineUIPro.TreeNode>();
|
||
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 下拉切换
|
||
/// <summary>
|
||
/// 切换书籍。
|
||
/// </summary>
|
||
protected void drpBook_SelectedIndexChanged(object sender, EventArgs e)
|
||
{
|
||
InitTree(this.drpBook.SelectedValue);
|
||
this.htmlContent.Text = "";
|
||
this.hfCurrentChapterId.Text = "";
|
||
this.lblProgress.Text = "";
|
||
}
|
||
#endregion
|
||
|
||
#region 目录点击 → 加载章节内容
|
||
/// <summary>
|
||
/// 点击目录节点,把章节富文本内容加载到编辑器。
|
||
/// 所有章节(含带子章节的目录章节)均可编辑自身正文,点击即加载。
|
||
/// </summary>
|
||
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 保存章节内容(富文本)
|
||
/// <summary>
|
||
/// 保存当前章节的富文本内容(HTML;本站上传图片引用自动转为 base64 内嵌存储)。
|
||
/// </summary>
|
||
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");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 把 HTML 中指向本站 UMeditor 上传相关路径(/res/umeditor/net/..,含 ../../../upload 形态)的图片引用替换为 base64 内嵌。
|
||
/// 已是 data: 的图片与外部 http(s) 图片原样保留;路径非图片扩展名或文件不存在时不做替换(回退原引用)。
|
||
/// </summary>
|
||
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 书籍新增
|
||
/// <summary>
|
||
/// 打开新增书籍弹窗。
|
||
/// </summary>
|
||
protected void btnAddBook_Click(object sender, EventArgs e)
|
||
{
|
||
this.txtBookTitle.Text = "";
|
||
this.WindowBook.Hidden = false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存新增书籍。
|
||
/// </summary>
|
||
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 + "》,请继续添加章节。";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 关闭新增书籍弹窗。
|
||
/// </summary>
|
||
protected void btnCloseBook_Click(object sender, EventArgs e)
|
||
{
|
||
this.WindowBook.Hidden = true;
|
||
}
|
||
#endregion
|
||
|
||
#region 章节新增/编辑/删除
|
||
/// <summary>
|
||
/// 打开新增章节弹窗(父级=目录树选中节点,未选中则为顶级章节)。
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 打开编辑章节弹窗(修改选中章节名称)。
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存章节(新增:挂在选中节点下,层级=父级+1;编辑:仅改名)。
|
||
/// 保存后保持选中刚保存的章节:叶子章节加载其内容,目录章节仅刷新选中、不动编辑器。
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除选中章节及其全部子章节。
|
||
/// </summary>
|
||
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<string> { 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) + " 个)。";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 关闭章节弹窗。
|
||
/// </summary>
|
||
protected void btnCloseChapter_Click(object sender, EventArgs e)
|
||
{
|
||
this.WindowChapter.Hidden = true;
|
||
}
|
||
#endregion
|
||
|
||
#region 工具
|
||
/// <summary>SQL 字符串转义。</summary>
|
||
private static string SqlEsc(string s)
|
||
{
|
||
return (s ?? "").Replace("'", "''");
|
||
}
|
||
|
||
/// <summary>生成业务主键(前缀+时间+随机)。</summary>
|
||
private static string NewId(string prefix)
|
||
{
|
||
return prefix + DateTime.Now.ToString("yyyyMMddHHmmss")
|
||
+ Guid.NewGuid().ToString("N").Substring(0, 4);
|
||
}
|
||
#endregion
|
||
}
|
||
}
|