SimplePDFReport.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. using UnityEngine;
  2. using System.IO;
  3. using iTextSharp.text;
  4. using iTextSharp.text.pdf;
  5. using System.Collections.Generic;
  6. using UnityEngine.UI;
  7. using PdfFont = iTextSharp.text.Font;
  8. using QFramework;
  9. using System.IO;
  10. public class SimplePDFReport : MonoBehaviour
  11. {
  12. UserProxy userProxy = DAL.Instance.Get<UserProxy>();
  13. [Header("UI绑定")]
  14. public Button Btn_Export;
  15. [Header("基础信息")]
  16. public string studentName = "Chiva";
  17. private string experimentName = OperateSetting.Instance.m_CourseName;
  18. public string date = "2025-10-16";
  19. // 可外部替换 Logo
  20. public string logoFileName = "logo.png"; // 放在 StreamingAssets 下
  21. private ScoreInfo scorePanel; // 动态获取成绩信息
  22. private List<StepScore> stepScores = new List<StepScore>();
  23. private float totalScore;
  24. private string useTime;
  25. [System.Serializable]
  26. public class StepScore
  27. {
  28. public int index;
  29. public string stepName;
  30. public float score;
  31. }
  32. private void Start()
  33. {
  34. studentName = userProxy.userInfo.userName;
  35. Btn_Export.onClick.AddListener(ExportPDF);
  36. }
  37. [ContextMenu("导出成绩单 PDF")]
  38. public void ExportPDF()
  39. {
  40. scorePanel = UIKit.GetPanel<PC_OperatePanel>().ScoreInfo;
  41. if (scorePanel == null)
  42. {
  43. Debug.LogError("❌ 未找到 ScoreInfo 面板,请确保在当前场景中存在。");
  44. return;
  45. }
  46. totalScore = float.Parse(scorePanel.Score.text);
  47. useTime = scorePanel.UseTime.text;
  48. date = System.DateTime.Now.ToString("yyyy-MM-dd");
  49. // 生成步骤表数据
  50. Transform content = scorePanel.Content;
  51. stepScores.Clear();
  52. for (int i = 0; i < content.childCount; i++)
  53. {
  54. var item = content.GetChild(i).GetComponent<ScoreInfoItem>();
  55. if (item != null)
  56. {
  57. stepScores.Add(new StepScore()
  58. {
  59. //index = i + 1,
  60. index = int.Parse(item.stepIdText.text),
  61. stepName = item.stepDescriptText.text, // 步骤名
  62. score = float.Parse(item.scoreSituationText.text) // 分数
  63. });
  64. }
  65. }
  66. // ====== PDF导出路径 ======
  67. string tmpSavePath = FolderBrowserHelper.GetPathFromWindowsExplorer("请选择成绩导出位置");
  68. string savePath = Path.Combine(Application.dataPath, $"{studentName}_成绩单.pdf");
  69. if (File.Exists(savePath)) File.Delete(savePath);
  70. Document doc = new Document(PageSize.A4, 60, 60, 80, 60); // 上边距略大,留页眉空间
  71. PdfWriter writer = PdfWriter.GetInstance(doc, new FileStream(savePath, FileMode.Create));
  72. // ===== 页脚和页码 =====
  73. writer.PageEvent = new PdfPageEvents();
  74. doc.Open();
  75. // ====== 字体设置 ======
  76. string fontPath = Path.Combine(Application.dataPath, "Fonts/SIMHEI.ttf");
  77. if (!File.Exists(fontPath))
  78. {
  79. Debug.LogError("字体文件未找到:" + fontPath);
  80. return;
  81. }
  82. BaseFont bfChinese = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
  83. PdfFont smallFont = new PdfFont(bfChinese, 10) { Color = new BaseColor(100, 100, 100) }; // 灰色
  84. PdfFont normalFont = new PdfFont(bfChinese, 12);
  85. PdfFont boldFont = new PdfFont(bfChinese, 12, PdfFont.BOLD);
  86. PdfFont titleFont = new PdfFont(bfChinese, 30, PdfFont.BOLD);
  87. PdfContentByte cb = writer.DirectContent;
  88. // ===== 横线 =====
  89. float lineY = doc.Top - 20f;
  90. cb.MoveTo(doc.Left, lineY);
  91. cb.LineTo(doc.Right, lineY);
  92. cb.Stroke();
  93. // ===== 页眉在横线之上 =====
  94. string headerText = "虚拟仿真实验中心";
  95. ColumnText.ShowTextAligned(
  96. cb,
  97. Element.ALIGN_CENTER,
  98. new Phrase(headerText, smallFont),
  99. (doc.Left + doc.Right) / 2,
  100. lineY + 10, // 横线上方 10pt
  101. 0
  102. );
  103. // ===== LOGO 横线上方左侧 =====
  104. string logoPath = Path.Combine(Application.streamingAssetsPath, logoFileName);
  105. // 统一去除 URL 前缀(特别是 file://)
  106. if (logoPath.StartsWith("file://"))
  107. logoPath = logoPath.Replace("file://", "");
  108. if (!File.Exists(logoPath))
  109. {
  110. // Unity Editor 下 StreamingAssets 在项目路径内
  111. logoPath = Path.Combine(Application.dataPath, "StreamingAssets", logoFileName);
  112. }
  113. float logoBottomY = lineY; // 用于计算标题位置
  114. if (File.Exists(logoPath))
  115. {
  116. try
  117. {
  118. byte[] logoBytes = File.ReadAllBytes(logoPath);
  119. iTextSharp.text.Image logo = iTextSharp.text.Image.GetInstance(logoBytes);
  120. // 👉 只固定高度,让宽度按比例缩放
  121. float targetHeight = 50f; // 设定统一显示宽度
  122. float scaleFactor = targetHeight / logo.Height; // 缩放比例
  123. logo.ScalePercent(scaleFactor * 100); // iTextSharp 用百分比
  124. // 保证顶部距离横线 5pt
  125. float logoTopY = lineY + 5f + logo.ScaledHeight;
  126. float logoPosY = logoTopY - logo.ScaledHeight;
  127. logoBottomY = logoPosY;
  128. // 让左下角对齐页边距
  129. logo.SetAbsolutePosition(doc.Left, logoPosY);
  130. doc.Add(logo);
  131. Debug.Log($"✅ Logo 添加成功(原始大小: {logo.Width}x{logo.Height}, 缩放比例: {scaleFactor:F2})");
  132. }
  133. catch (System.Exception e)
  134. {
  135. Debug.LogError("❌ 读取 Logo 出错:" + e.Message);
  136. }
  137. }
  138. else
  139. {
  140. Debug.LogWarning("⚠️ 未找到 Logo 文件:" + logoPath);
  141. }
  142. // ===== 主标题(单独一行居中) =====
  143. Paragraph title = new Paragraph("成 绩 单", titleFont);
  144. title.Alignment = Element.ALIGN_CENTER;
  145. title.SpacingBefore = 30f;
  146. title.SpacingAfter = 20f;
  147. doc.Add(title);
  148. // ===== 基本信息表格 =====
  149. PdfPTable infoTable = new PdfPTable(4);
  150. infoTable.WidthPercentage = 100;
  151. infoTable.SpacingBefore = 10f;
  152. infoTable.SpacingAfter = 15f;
  153. infoTable.SetWidths(new float[] { 1.2f, 2f, 1.2f, 2f });
  154. AddCell(infoTable, "姓名", boldFont, new BaseColor(235, 235, 235));
  155. AddCell(infoTable, studentName, normalFont);
  156. AddCell(infoTable, "实验日期", boldFont, new BaseColor(235, 235, 235));
  157. AddCell(infoTable, date, normalFont);
  158. AddCell(infoTable, "实验名称", boldFont, new BaseColor(235, 235, 235));
  159. AddCell(infoTable, experimentName, normalFont);
  160. AddCell(infoTable, "总成绩", boldFont, new BaseColor(235, 235, 235));
  161. AddCell(infoTable, totalScore.ToString("F1") + " 分", normalFont);
  162. AddCell(infoTable, "用时", boldFont, new BaseColor(235, 235, 235));
  163. AddCell(infoTable, useTime, normalFont);
  164. AddCell(infoTable, "", normalFont);
  165. AddCell(infoTable, "", normalFont);
  166. doc.Add(infoTable);
  167. // ===== 操作步骤表格 =====
  168. PdfPTable stepTable = new PdfPTable(3);
  169. stepTable.WidthPercentage = 100;
  170. stepTable.SetWidths(new float[] { 1f, 3f, 1f });
  171. AddHeaderCell(stepTable, "序号", boldFont);
  172. AddHeaderCell(stepTable, "操作步骤", boldFont);
  173. AddHeaderCell(stepTable, "分数", boldFont);
  174. foreach (var s in stepScores)
  175. {
  176. AddCell(stepTable, s.index.ToString(), normalFont);
  177. AddCell(stepTable, s.stepName, normalFont);
  178. AddCell(stepTable, s.score.ToString("F1"), normalFont);
  179. }
  180. doc.Add(stepTable);
  181. // ===== 评语 =====
  182. doc.Add(new Paragraph("\n"));
  183. string commentText = GenerateComment(totalScore);
  184. Paragraph comment = new Paragraph($"评语:{commentText}", normalFont);
  185. comment.SpacingBefore = 10;
  186. doc.Add(comment);
  187. doc.Close();
  188. writer.Close();
  189. Debug.Log($"✅ 成绩单导出成功:{savePath}");
  190. }
  191. private string GenerateComment(float score)
  192. {
  193. if (score >= 90f)
  194. return "表现优秀,操作熟练,理解深入。";
  195. else if (score >= 75f)
  196. return "完成良好,掌握较为扎实,可继续巩固。";
  197. else if (score >= 60f)
  198. return "基本达标,但仍需加强理解与操作细节。";
  199. else
  200. return "需改进,建议复习实验步骤并提高熟练度。";
  201. }
  202. private void AddHeaderCell(PdfPTable table, string text, PdfFont font)
  203. {
  204. PdfPCell cell = new PdfPCell(new Phrase(text, font));
  205. cell.BackgroundColor = new BaseColor(220, 220, 220);
  206. cell.HorizontalAlignment = Element.ALIGN_CENTER;
  207. cell.MinimumHeight = 25;
  208. table.AddCell(cell);
  209. }
  210. private void AddCell(PdfPTable table, string text, PdfFont font, BaseColor bg = null)
  211. {
  212. PdfPCell cell = new PdfPCell(new Phrase(text, font));
  213. cell.HorizontalAlignment = Element.ALIGN_CENTER;
  214. cell.VerticalAlignment = Element.ALIGN_MIDDLE;
  215. cell.MinimumHeight = 22;
  216. if (bg != null) cell.BackgroundColor = bg;
  217. table.AddCell(cell);
  218. }
  219. // ===== 页脚页码事件类 =====
  220. private class PdfPageEvents : PdfPageEventHelper
  221. {
  222. BaseFont bf;
  223. PdfFont footerFont;
  224. public PdfPageEvents()
  225. {
  226. string fontPath = Path.Combine(Application.dataPath, "Fonts/simhei.ttf");
  227. if (File.Exists(fontPath))
  228. {
  229. bf = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
  230. footerFont = new PdfFont(bf, 10);
  231. }
  232. }
  233. public override void OnEndPage(PdfWriter writer, Document document)
  234. {
  235. int pageN = writer.PageNumber;
  236. string text = "第 " + pageN + " 页 / 共 " + writer.PageNumber + " 页";
  237. PdfContentByte cb = writer.DirectContent;
  238. // 右下页码
  239. ColumnText.ShowTextAligned(
  240. cb,
  241. Element.ALIGN_RIGHT,
  242. new Phrase(text, footerFont),
  243. document.Right,
  244. document.Bottom - 20,
  245. 0
  246. );
  247. // 左下固定文字
  248. ColumnText.ShowTextAligned(
  249. cb,
  250. Element.ALIGN_LEFT,
  251. new Phrase("虚拟仿真实验考核系统 © 2025", footerFont),
  252. document.Left,
  253. document.Bottom - 20,
  254. 0
  255. );
  256. }
  257. }
  258. }