Bladeren bron

【+】
1.考试成绩导出功能加入
2.考试成绩数量限制到20个功能加入
1).使用说明
为防止更新框架后StreamingAssets下相关配置确实,请先导入框架下的”考试成绩输出功能配置“包

lxd 1 maand geleden
bovenliggende
commit
2ca7b908e5
29 gewijzigde bestanden met toevoegingen van 1381 en 58 verwijderingen
  1. 30 11
      Framework/Scripts/Proxys/ExamProxy.cs
  2. 0 14
      Framework/Scripts/Test.cs
  3. 29 2
      Framework/Scripts/UI/PCController/ExamManagerForPC.cs
  4. 55 0
      Framework/Toolkits/FileKits/FileToolkit.cs
  5. 8 0
      Framework/Toolkits/FolderBrowserHelper.meta
  6. 251 0
      Framework/Toolkits/FolderBrowserHelper/FolderBrowserHelper.cs
  7. 1 1
      Framework/Toolkits/FolderBrowserHelper/FolderBrowserHelper.cs.meta
  8. 8 0
      Framework/Toolkits/PDFkit.meta
  9. 8 0
      Framework/Toolkits/PDFkit/Plugins.meta
  10. BIN
      Framework/Toolkits/PDFkit/Plugins/itextsharp.dll
  11. 33 0
      Framework/Toolkits/PDFkit/Plugins/itextsharp.dll.meta
  12. 306 0
      Framework/Toolkits/PDFkit/SimplePDFReport.cs
  13. 11 0
      Framework/Toolkits/PDFkit/SimplePDFReport.cs.meta
  14. 1 1
      OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel.Designer.cs
  15. 1 1
      OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/ExamInfo.Designer.cs
  16. 1 1
      OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/OperateStep.Designer.cs
  17. 1 1
      OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/OperateStep/FistStepItem.Designer.cs
  18. 1 1
      OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/OperateStep/SecondStepItem.Designer.cs
  19. 1 1
      OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/PracticeResult.Designer.cs
  20. 3 1
      OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/ScoreInfo.Designer.cs
  21. 350 11
      OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/ScoreInfo.cs
  22. 1 1
      OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/StartTips.Designer.cs
  23. 1 1
      OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/StudyPanel.Designer.cs
  24. 1 1
      OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/SubmitResult.Designer.cs
  25. 0 2
      OperationUIFrame/OperationUIFrameV2/Scripts/UI/ScoreInfoItem.cs
  26. 264 7
      OperationUIFrame/OperationUIFrameV2/UIPrefabs/PC_OperatePanel.prefab
  27. 8 0
      StreamingAssets.meta
  28. BIN
      考试成绩输出功能配置.unitypackage
  29. 7 0
      考试成绩输出功能配置.unitypackage.meta

+ 30 - 11
Framework/Scripts/Proxys/ExamProxy.cs

@@ -16,6 +16,13 @@ public class ExamProxy : DataProxy
     /// </summary>
     public System.DateTime startTime;
 
+    /// <summary>
+    /// 考试用时
+    /// </summary>
+    public int examTime;
+
+    public List<ExamScoreInfo> examScoreInfos;
+
     /// <summary>
     /// 开始考试
     /// </summary>
@@ -25,6 +32,7 @@ public class ExamProxy : DataProxy
         curse = GlobalConfig.m_SelectDevice;
     }
 
+
     /// <summary>
     /// 上传考试结果
     /// </summary>
@@ -32,17 +40,19 @@ public class ExamProxy : DataProxy
     {
         UserProxy userProxy = DAL.Instance.Get<UserProxy>();
 
-        OperateResponse operateResponse = GrpcChannelContronller.Instance.client.StudentScoreAdd(new StudentScore()
-        {
-            Answertime = (int)TimestampConvert.ConverOldTiemAndNewTiemDuration(startTime, System.DateTime.Now),
-            CourseName = curse,
-            Name = userProxy.userInfo.userName,
-            Score = score,
-            Starttime = GetCurrentTime(startTime),
-            PhoneNumber = userProxy.userInfo.phoneNumber
-        });
-
-        return operateResponse.Result;
+        //OperateResponse operateResponse = GrpcChannelContronller.Instance.client.StudentScoreAdd(new StudentScore()
+        //{
+        //    Answertime = (int)TimestampConvert.ConverOldTiemAndNewTiemDuration(startTime, System.DateTime.Now),
+        //    CourseName = curse,
+        //    Name = userProxy.userInfo.userName,
+        //    Score = score,
+        //    Starttime = GetCurrentTime(startTime),
+        //    PhoneNumber = userProxy.userInfo.phoneNumber
+        //});
+
+        //return operateResponse.Result;
+
+        return true;
     }
 
     /// <summary>
@@ -60,3 +70,12 @@ public class ExamProxy : DataProxy
         return TimestampConvert.ConvertDateTimeToLong(dt);
     }
 }
+
+public class ExamScoreInfo
+{
+    public int id;
+
+    public string stepName;
+
+    public float score;
+}

+ 0 - 14
Framework/Scripts/Test.cs

@@ -1,14 +0,0 @@
-using QFramework;
-using System.Collections;
-using System.Collections.Generic;
-using UnityEngine;
-
-public class Test : MonoBehaviour
-{
-    void Start()
-    {
-        ResKit.Init();
-
-        UIKit.OpenPanel<ToolLibraryForm>();
-    }
-}

+ 29 - 2
Framework/Scripts/UI/PCController/ExamManagerForPC.cs

@@ -391,7 +391,32 @@ public class ExamManagerForPC : MonoBehaviour
     public void ExamFinish()
     {
         m_EndTime = DateTime.Now;
-        CountExamDuration();
+      
+        ExamProxy examProxy = DAL.Instance.Get<ExamProxy>();
+
+        examProxy.examTime = CountExamDuration();
+
+        List<ExamScoreInfo> examScoreInfos = new List<ExamScoreInfo>();
+
+        foreach (var item in m_examProcessElements)
+        {
+            ExamScoreInfo examScoreInfo = new ExamScoreInfo();
+
+            examScoreInfo.id = item.id;
+            examScoreInfo.stepName = item.stepName;
+
+            if (item.finish && item.result)
+            {
+                examScoreInfo.score = item.scores;
+            }else
+            {
+                examScoreInfo.score = -item.scores;
+            }
+
+            examScoreInfos.Add(examScoreInfo);
+        }
+
+        examProxy.examScoreInfos = examScoreInfos;
 
         if (UIKit.GetPanel<PC_OperatePanel>())
         {
@@ -413,12 +438,14 @@ public class ExamManagerForPC : MonoBehaviour
     /// <summary>
     /// 统计考试时长
     /// </summary>
-    private void CountExamDuration()
+    private int CountExamDuration()
     {
         // 记录结束时的时间
         DateTime finishTime = DateTime.Now;
 
         m_RecordExamDuration = (int)(finishTime - m_ExamProxy.startTime).TotalSeconds;
+
+        return m_RecordExamDuration;
     }
 
     private void OnDisable()

+ 55 - 0
Framework/Toolkits/FileKits/FileToolkit.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using UnityEngine;
 using System.IO;
 using SFB;
+using System;
 
 public class FileToolkit 
 {
@@ -82,4 +83,58 @@ public class FileToolkit
 	    string[] paths = StandaloneFileBrowser.OpenFilePanel(" 上传文件", "", extensions.ToArray(), true);
 		return paths;
 	}
+    
+	/// <summary>
+	/// 获取文件的创建时间
+	/// </summary>
+	/// <param name="filePath"></param>
+	/// <returns></returns>
+	public static DateTime GetFileCreateTime(string filePath)
+    {
+		return File.GetCreationTime(filePath);
+    }
+
+	/// <summary>
+	/// 对比文件创建前后
+	/// </summary>
+	/// <param name="filePath1"></param>
+	/// <param name="filePath2"></param>
+	/// <returns>True为FilePath1靠前</returns>
+	public static bool CompareBeforAndAfterCreateTime(string filePath1,string filePath2)
+    {
+		DateTime dateTime1 = GetFileCreateTime(filePath1);
+
+		DateTime dateTime2 = GetFileCreateTime(filePath2);
+
+		return dateTime1 > dateTime2;
+	}
+
+	/// <summary>
+	/// 搜索指定条件的文件,并返回文件路径
+	/// </summary>
+	/// <param name="targetPath"></param>
+	/// <param name="searchPattern"></param>
+	/// <returns></returns>
+    public static string[] SearchFilePaths(string targetPath,string searchPattern)
+    {
+		DirectoryInfo directoryInfo = new DirectoryInfo(targetPath);
+
+		List<string> tmpFilePaths = new List<string>();
+
+        if (!directoryInfo.Exists)
+        {
+			Debug.LogError(targetPath + "路径不存在!!!!");
+        }else
+        {
+			FileInfo[] fileInfos = directoryInfo.GetFiles(searchPattern);
+
+            foreach (var item in fileInfos)
+            {
+				tmpFilePaths.Add(item.FullName);
+            }
+        }
+
+		return tmpFilePaths.ToArray();
+    }
+
 }

+ 8 - 0
Framework/Toolkits/FolderBrowserHelper.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d14c0fbc2f7a253469bb4b731cf4ee5c
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 251 - 0
Framework/Toolkits/FolderBrowserHelper/FolderBrowserHelper.cs

@@ -0,0 +1,251 @@
+using SFB;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Data;
+using System.IO;
+using System.Runtime.InteropServices;
+using UnityEditor;
+using UnityEngine;
+using UnityEngine.Networking;
+
+#region 调用系统窗口选择文件夹或文件
+public class FileOpMsg
+{
+    public bool success;
+    public string msg;
+}
+
+#region 结构体
+[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
+public class OpenDialogFile
+{
+    public int structSize = 0;
+    public IntPtr dlgOwner = IntPtr.Zero;
+    public IntPtr instance = IntPtr.Zero;
+    public String filter = null;
+    public String customFilter = null;
+    public int maxCustFilter = 0;
+    public int filterIndex = 0;
+    public String file = null;
+    public int maxFile = 0;
+    public String fileTitle = null;
+    public int maxFileTitle = 0;
+    public String initialDir = null;
+    public String title = null;
+    public int flags = 0;
+    public short fileOffset = 0;
+    public short fileExtension = 0;
+    public String defExt = null;
+    public IntPtr custData = IntPtr.Zero;
+    public IntPtr hook = IntPtr.Zero;
+    public String templateName = null;
+    public IntPtr reservedPtr = IntPtr.Zero;
+    public int reservedInt = 0;
+    public int flagsEx = 0;
+}
+
+[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
+public class OpenDialogDir
+{
+    public IntPtr hwndOwner = IntPtr.Zero;
+    public IntPtr pidlRoot = IntPtr.Zero;
+    public String pszDisplayName = null;
+    public String lpszTitle = null;
+    public UInt32 ulFlags = 0;
+    public IntPtr lpfn = IntPtr.Zero;
+    public IntPtr lParam = IntPtr.Zero;
+    public int iImage = 0;
+}
+
+[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
+public class OpenFileName
+{
+    public int structSize = 0;
+    //public IntPtr hwndOwner = IntPtr.Zero;
+    public IntPtr dlgOwner = IntPtr.Zero;
+    public IntPtr instance = IntPtr.Zero;
+    public String filter = null;
+    public String customFilter = null;
+    public int maxCustFilter = 0;
+    public int filterIndex = 0;
+    public String file = null;
+    public int maxFile = 0;
+    public String fileTitle = null;
+    public int maxFileTitle = 0;
+    public String initialDir = null;
+    public String title = null;
+    public int flags = 0;
+    public short fileOffset = 0;
+    public short fileExtension = 0;
+    public String defExt = null;
+    public IntPtr custData = IntPtr.Zero;
+    public IntPtr hook = IntPtr.Zero;
+    public String templateName = null;
+    public IntPtr reservedPtr = IntPtr.Zero;
+    public int reservedInt = 0;
+    public int flagsEx = 0;
+}
+
+#endregion
+
+public class FolderBrowserHelper
+{
+
+    #region 引用windows的dll文件
+    //[DllImport("user32.dll", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto)]
+    //private static extern bool ClipCursor([In, Out] Rect rect);
+
+    [DllImport("Comdlg32.dll", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto)]
+    private static extern bool GetOpenFileName([In, Out] OpenFileName ofn);
+
+    [DllImport("Comdlg32.dll", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto)]
+    private static extern bool GetSaveFileName([In, Out] OpenFileName ofn);
+
+    [DllImport("Comdlg32.dll", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto)]
+    private static extern bool GetOpenFileName([In, Out] OpenDialogFile ofn);
+
+    [DllImport("Comdlg32.dll", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto)]
+    private static extern bool GetSaveFileName([In, Out] OpenDialogFile ofn);
+
+    [DllImport("shell32.dll", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto)]
+    private static extern IntPtr SHBrowseForFolder([In, Out] OpenDialogDir ofn);
+
+    [DllImport("shell32.dll", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto)]
+    private static extern bool SHGetPathFromIDList([In] IntPtr pidl, [In, Out] char[] fileName);
+    #endregion
+
+    #region 文件操作方法
+    public const string IMAGEFILTER = "图片文件(*.jpg;*.png)\0*.jpg;*.png";
+    public const string ALLFILTER = "所有文件(*.*)\0*.*";
+
+    /// <summary>
+    /// 选择文件
+    /// </summary>
+    /// <param name="callback">返回选择文件夹的路径</param>
+    /// <param name="filter">文件类型筛选器</param>
+    public static void SelectFile(Action<FileOpMsg> callback, string filter = ALLFILTER)
+    {
+        Debug.Log("open 00");
+        FileOpMsg fileOpMsg = new FileOpMsg();
+        try
+        {
+            OpenFileName openFileName = new OpenFileName();
+            openFileName.structSize = Marshal.SizeOf(openFileName);
+            //openFileName.hwndOwner = WindowsTools.ProcessHandle;
+            openFileName.filter = filter;
+            openFileName.file = new string(new char[256]);
+            openFileName.maxFile = openFileName.file.Length;
+            openFileName.fileTitle = new string(new char[64]);
+            openFileName.maxFileTitle = openFileName.fileTitle.Length;
+            openFileName.title = "选择文件";
+            openFileName.flags = 0x00080000 | 0x00001000 | 0x00000800 | 0x00000008;
+            if (GetSaveFileName(openFileName))
+            {
+                string filepath = openFileName.file; //选择的文件路径;  
+                if (File.Exists(filepath))
+                {
+                    fileOpMsg.success = true;
+                    fileOpMsg.msg = filepath;
+                    callback?.Invoke(fileOpMsg);
+                    return;
+                }
+            }
+        }
+        catch (Exception e)
+        {
+            fileOpMsg.success = false;
+            fileOpMsg.msg = e.ToString();
+            callback?.Invoke(fileOpMsg);
+            Debug.LogError(fileOpMsg.msg);
+            return;
+        }
+
+        fileOpMsg.success = true;
+        fileOpMsg.msg = string.Empty;
+        callback?.Invoke(fileOpMsg);
+    }
+
+    /// <summary>
+    /// 调用WindowsExploer并返回所选文件夹路径
+    /// </summary>
+    /// <param name="dialogtitle">打开对话框的标题</param>
+    /// <returns>所选文件夹路径</returns>
+    public static string GetPathFromWindowsExplorer(string dialogtitle = "请选择下载路径")
+    {
+        try
+        {
+            OpenDialogDir ofn2 = new OpenDialogDir();
+            ofn2.pszDisplayName = new string(new char[2048]);// 存放目录路径缓冲区  
+            ofn2.lpszTitle = dialogtitle; // 标题  
+            ofn2.ulFlags = 0x00000040; // 新的样式,带编辑框  
+            IntPtr pidlPtr = SHBrowseForFolder(ofn2);
+
+            char[] charArray = new char[2048];
+            for (int i = 0; i < 2048; i++)
+            {
+                charArray[i] = '\0';
+            }
+
+            SHGetPathFromIDList(pidlPtr, charArray);
+            string fullDirPath = new string(charArray);
+            fullDirPath = fullDirPath.Substring(0, fullDirPath.IndexOf('\0'));
+            return fullDirPath;
+        }
+        catch (Exception e)
+        {
+            Debug.LogError(e);
+        }
+
+        return string.Empty;
+    }
+
+
+    /// <summary>
+    /// 选择文件
+    /// </summary>
+    /// <param name="title"></param>
+    /// <param name="directory"></param>
+    /// <param name="filterName"></param>
+    /// <param name="filterExtensions"></param>
+    /// <returns></returns>
+    public static string[] SelectFile(string title,string directory,string filterName, params string[] filterExtensions)
+    {
+        List<ExtensionFilter> extensions = new List<ExtensionFilter>();
+
+        extensions.Add(new ExtensionFilter(filterName, filterExtensions));
+
+        return StandaloneFileBrowser.OpenFilePanel(title,directory, extensions.ToArray(),false);
+    }
+
+    /// <summary>
+    /// 多选文件
+    /// </summary>
+    /// <param name="filterName"></param>
+    /// <param name="mutiltSelect"></param>
+    /// <param name="filterExtensions"></param>
+    /// <returns></returns>
+    public static List<string> OpenProject(string filterName, bool mutiltSelect, params string[] filterExtensions)
+    {
+        List<string> list = new List<string>();
+        List<ExtensionFilter> extensions = new List<ExtensionFilter>();
+
+        extensions.Add(new ExtensionFilter(filterName, filterExtensions));
+        string[] paths = StandaloneFileBrowser.OpenFilePanel(" 上传文件", "", extensions.ToArray(), mutiltSelect);
+
+        foreach (string Wenjian in paths)
+        {
+            //路径
+            string pathName = Path.GetDirectoryName(Wenjian);
+            //带扩展名的的指定路径文件名
+            string fileName = Path.GetFileName(Wenjian);
+
+            list.Add(pathName + "/" + fileName);
+        }
+        return list;
+    }
+
+    #endregion
+
+}
+#endregion

+ 1 - 1
Framework/Scripts/Test.cs.meta → Framework/Toolkits/FolderBrowserHelper/FolderBrowserHelper.cs.meta

@@ -1,5 +1,5 @@
 fileFormatVersion: 2
-guid: 80a2bf6553627e04bafe20613013fbf9
+guid: a4feb810ce5d07c4eba11df1a58e7e03
 MonoImporter:
   externalObjects: {}
   serializedVersion: 2

+ 8 - 0
Framework/Toolkits/PDFkit.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 8cf4c28b2b9d12349840569b332bb3bd
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Framework/Toolkits/PDFkit/Plugins.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 702ef75ae606afe41a4a5f7c924245dd
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
Framework/Toolkits/PDFkit/Plugins/itextsharp.dll


+ 33 - 0
Framework/Toolkits/PDFkit/Plugins/itextsharp.dll.meta

@@ -0,0 +1,33 @@
+fileFormatVersion: 2
+guid: 249c4d927eb51a54e8bf4b9511062bb5
+PluginImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  defineConstraints: []
+  isPreloaded: 0
+  isOverridable: 0
+  isExplicitlyReferenced: 0
+  validateReferences: 1
+  platformData:
+  - first:
+      Any: 
+    second:
+      enabled: 1
+      settings: {}
+  - first:
+      Editor: Editor
+    second:
+      enabled: 0
+      settings:
+        DefaultValueInitialized: true
+  - first:
+      Windows Store Apps: WindowsStoreApps
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 306 - 0
Framework/Toolkits/PDFkit/SimplePDFReport.cs

@@ -0,0 +1,306 @@
+using UnityEngine;
+using System.IO;
+using iTextSharp.text;
+using iTextSharp.text.pdf;
+using System.Collections.Generic;
+using UnityEngine.UI;
+using PdfFont = iTextSharp.text.Font;
+using QFramework;
+using System.IO;
+
+public class SimplePDFReport : MonoBehaviour
+{
+    UserProxy userProxy = DAL.Instance.Get<UserProxy>();
+    [Header("UI绑定")]
+    public Button Btn_Export;
+
+    [Header("基础信息")]
+    public string studentName = "Chiva";
+    private string experimentName = OperateSetting.Instance.m_CourseName;
+    public string date = "2025-10-16";
+
+
+    // 可外部替换 Logo
+    public string logoFileName = "logo.png"; // 放在 StreamingAssets 下
+
+    private ScoreInfo scorePanel; // 动态获取成绩信息
+    private List<StepScore> stepScores = new List<StepScore>();
+    private float totalScore;
+    private string useTime;
+  
+
+
+    [System.Serializable]
+    public class StepScore
+    {
+        public int index;
+        public string stepName;
+        public float score;
+    }
+
+    private void Start()
+    {
+        studentName = userProxy.userInfo.userName;
+        Btn_Export.onClick.AddListener(ExportPDF);
+    }
+
+
+    [ContextMenu("导出成绩单 PDF")]
+    public void ExportPDF()
+    {
+        scorePanel = UIKit.GetPanel<PC_OperatePanel>().ScoreInfo;
+        if (scorePanel == null)
+        {
+            Debug.LogError("❌ 未找到 ScoreInfo 面板,请确保在当前场景中存在。");
+            return;
+        }
+        totalScore = float.Parse(scorePanel.Score.text);
+        useTime = scorePanel.UseTime.text;
+        date = System.DateTime.Now.ToString("yyyy-MM-dd");
+
+        // 生成步骤表数据
+        Transform content = scorePanel.Content;
+        stepScores.Clear();
+        for (int i = 0; i < content.childCount; i++)
+        {
+            var item = content.GetChild(i).GetComponent<ScoreInfoItem>();
+            if (item != null)
+            {
+                stepScores.Add(new StepScore()
+                {
+                    //index = i + 1,
+                    index = int.Parse(item.stepIdText.text),
+                    stepName = item.stepDescriptText.text,  // 步骤名
+                    score = float.Parse(item.scoreSituationText.text) // 分数
+                });
+            }
+        }
+
+        // ====== PDF导出路径 ======
+        string tmpSavePath = FolderBrowserHelper.GetPathFromWindowsExplorer("请选择成绩导出位置");
+
+        string savePath = Path.Combine(Application.dataPath, $"{studentName}_成绩单.pdf");
+        if (File.Exists(savePath)) File.Delete(savePath);
+        Document doc = new Document(PageSize.A4, 60, 60, 80, 60); // 上边距略大,留页眉空间
+        PdfWriter writer = PdfWriter.GetInstance(doc, new FileStream(savePath, FileMode.Create));
+
+        // ===== 页脚和页码 =====
+        writer.PageEvent = new PdfPageEvents();
+
+        doc.Open();
+
+        // ====== 字体设置 ======
+        string fontPath = Path.Combine(Application.dataPath, "Fonts/SIMHEI.ttf");
+        if (!File.Exists(fontPath))
+        {
+            Debug.LogError("字体文件未找到:" + fontPath);
+            return;
+        }
+
+        BaseFont bfChinese = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
+        PdfFont smallFont = new PdfFont(bfChinese, 10) { Color = new BaseColor(100, 100, 100) }; // 灰色
+        PdfFont normalFont = new PdfFont(bfChinese, 12);
+        PdfFont boldFont = new PdfFont(bfChinese, 12, PdfFont.BOLD);
+        PdfFont titleFont = new PdfFont(bfChinese, 30, PdfFont.BOLD);
+
+        PdfContentByte cb = writer.DirectContent;
+
+        // ===== 横线 =====
+        float lineY = doc.Top - 20f;
+        cb.MoveTo(doc.Left, lineY);
+        cb.LineTo(doc.Right, lineY);
+        cb.Stroke();
+
+        // ===== 页眉在横线之上 =====
+        string headerText = "虚拟仿真实验中心";
+        ColumnText.ShowTextAligned(
+            cb,
+            Element.ALIGN_CENTER,
+            new Phrase(headerText, smallFont),
+            (doc.Left + doc.Right) / 2,
+            lineY + 10,  // 横线上方 10pt
+            0
+        );
+
+        // ===== LOGO 横线上方左侧 =====
+        string logoPath = Path.Combine(Application.streamingAssetsPath, logoFileName);
+
+        // 统一去除 URL 前缀(特别是 file://)
+        if (logoPath.StartsWith("file://"))
+            logoPath = logoPath.Replace("file://", "");
+
+        if (!File.Exists(logoPath))
+        {
+            // Unity Editor 下 StreamingAssets 在项目路径内
+            logoPath = Path.Combine(Application.dataPath, "StreamingAssets", logoFileName);
+        }
+        float logoBottomY = lineY; // 用于计算标题位置
+        if (File.Exists(logoPath))
+        {
+            try
+            {
+                byte[] logoBytes = File.ReadAllBytes(logoPath);
+                iTextSharp.text.Image logo = iTextSharp.text.Image.GetInstance(logoBytes);
+
+                // 👉 只固定高度,让宽度按比例缩放
+                float targetHeight = 50f;  // 设定统一显示宽度
+                float scaleFactor = targetHeight / logo.Height; // 缩放比例
+                logo.ScalePercent(scaleFactor * 100); // iTextSharp 用百分比
+
+                // 保证顶部距离横线 5pt
+                float logoTopY = lineY + 5f + logo.ScaledHeight;
+                float logoPosY = logoTopY - logo.ScaledHeight;
+                logoBottomY = logoPosY;
+
+                // 让左下角对齐页边距
+                logo.SetAbsolutePosition(doc.Left, logoPosY);
+                doc.Add(logo);
+
+                Debug.Log($"✅ Logo 添加成功(原始大小: {logo.Width}x{logo.Height}, 缩放比例: {scaleFactor:F2})");
+            }
+            catch (System.Exception e)
+            {
+                Debug.LogError("❌ 读取 Logo 出错:" + e.Message);
+            }
+        }
+        else
+        {
+            Debug.LogWarning("⚠️ 未找到 Logo 文件:" + logoPath);
+        }
+
+        // ===== 主标题(单独一行居中) =====
+        Paragraph title = new Paragraph("成 绩 单", titleFont);
+        title.Alignment = Element.ALIGN_CENTER;
+        title.SpacingBefore = 30f;
+        title.SpacingAfter = 20f;
+        doc.Add(title);
+
+        // ===== 基本信息表格 =====
+        PdfPTable infoTable = new PdfPTable(4);
+        infoTable.WidthPercentage = 100;
+        infoTable.SpacingBefore = 10f;
+        infoTable.SpacingAfter = 15f;
+        infoTable.SetWidths(new float[] { 1.2f, 2f, 1.2f, 2f });
+
+        AddCell(infoTable, "姓名", boldFont, new BaseColor(235, 235, 235));
+        AddCell(infoTable, studentName, normalFont);
+        AddCell(infoTable, "实验日期", boldFont, new BaseColor(235, 235, 235));
+        AddCell(infoTable, date, normalFont);
+
+        AddCell(infoTable, "实验名称", boldFont, new BaseColor(235, 235, 235));
+        AddCell(infoTable, experimentName, normalFont);
+        AddCell(infoTable, "总成绩", boldFont, new BaseColor(235, 235, 235));
+        AddCell(infoTable, totalScore.ToString("F1") + " 分", normalFont);
+
+        AddCell(infoTable, "用时", boldFont, new BaseColor(235, 235, 235));
+        AddCell(infoTable, useTime, normalFont);
+        AddCell(infoTable, "", normalFont);
+        AddCell(infoTable, "", normalFont);
+
+        doc.Add(infoTable);
+
+        // ===== 操作步骤表格 =====
+        PdfPTable stepTable = new PdfPTable(3);
+        stepTable.WidthPercentage = 100;
+        stepTable.SetWidths(new float[] { 1f, 3f, 1f });
+
+        AddHeaderCell(stepTable, "序号", boldFont);
+        AddHeaderCell(stepTable, "操作步骤", boldFont);
+        AddHeaderCell(stepTable, "分数", boldFont);
+
+        foreach (var s in stepScores)
+        {
+            AddCell(stepTable, s.index.ToString(), normalFont);
+            AddCell(stepTable, s.stepName, normalFont);
+            AddCell(stepTable, s.score.ToString("F1"), normalFont);
+        }
+
+        doc.Add(stepTable);
+
+        // ===== 评语 =====
+        doc.Add(new Paragraph("\n"));
+        string commentText = GenerateComment(totalScore);
+        Paragraph comment = new Paragraph($"评语:{commentText}", normalFont);
+        comment.SpacingBefore = 10;
+        doc.Add(comment);
+
+        doc.Close();
+        writer.Close();
+
+        Debug.Log($"✅ 成绩单导出成功:{savePath}");
+    }
+    private string GenerateComment(float score)
+    {
+        if (score >= 90f)
+            return "表现优秀,操作熟练,理解深入。";
+        else if (score >= 75f)
+            return "完成良好,掌握较为扎实,可继续巩固。";
+        else if (score >= 60f)
+            return "基本达标,但仍需加强理解与操作细节。";
+        else
+            return "需改进,建议复习实验步骤并提高熟练度。";
+    }
+
+    private void AddHeaderCell(PdfPTable table, string text, PdfFont font)
+    {
+        PdfPCell cell = new PdfPCell(new Phrase(text, font));
+        cell.BackgroundColor = new BaseColor(220, 220, 220);
+        cell.HorizontalAlignment = Element.ALIGN_CENTER;
+        cell.MinimumHeight = 25;
+        table.AddCell(cell);
+    }
+
+    private void AddCell(PdfPTable table, string text, PdfFont font, BaseColor bg = null)
+    {
+        PdfPCell cell = new PdfPCell(new Phrase(text, font));
+        cell.HorizontalAlignment = Element.ALIGN_CENTER;
+        cell.VerticalAlignment = Element.ALIGN_MIDDLE;
+        cell.MinimumHeight = 22;
+        if (bg != null) cell.BackgroundColor = bg;
+        table.AddCell(cell);
+    }
+
+    // ===== 页脚页码事件类 =====
+    private class PdfPageEvents : PdfPageEventHelper
+    {
+        BaseFont bf;
+        PdfFont footerFont;
+
+        public PdfPageEvents()
+        {
+            string fontPath = Path.Combine(Application.dataPath, "Fonts/simhei.ttf");
+            if (File.Exists(fontPath))
+            {
+                bf = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
+                footerFont = new PdfFont(bf, 10);
+            }
+        }
+
+        public override void OnEndPage(PdfWriter writer, Document document)
+        {
+            int pageN = writer.PageNumber;
+            string text = "第 " + pageN + " 页 / 共 " + writer.PageNumber + " 页";
+
+            PdfContentByte cb = writer.DirectContent;
+            // 右下页码
+            ColumnText.ShowTextAligned(
+                cb,
+                Element.ALIGN_RIGHT,
+                new Phrase(text, footerFont),
+                document.Right,
+                document.Bottom - 20,
+                0
+            );
+
+            // 左下固定文字
+            ColumnText.ShowTextAligned(
+                cb,
+                Element.ALIGN_LEFT,
+                new Phrase("虚拟仿真实验考核系统 © 2025", footerFont),
+                document.Left,
+                document.Bottom - 20,
+                0
+            );
+        }
+    }
+}

+ 11 - 0
Framework/Toolkits/PDFkit/SimplePDFReport.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6107f9028d64bbb4fa8cc04f9ccb5724
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 1 - 1
OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel.Designer.cs

@@ -5,7 +5,7 @@ using QFramework;
 
 namespace QFramework
 {
-	// Generate Id:21d1736e-dcb2-4727-aa2b-5bf55ead9ad0
+	// Generate Id:744a9722-8192-48e6-a4ef-34a94935b876
 	public partial class PC_OperatePanel
 	{
 		public const string Name = "PC_OperatePanel";

+ 1 - 1
OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/ExamInfo.Designer.cs

@@ -1,5 +1,5 @@
 /****************************************************************************
- * 2025.7 CHIVA
+ * 2025.10 LXD
  ****************************************************************************/
 
 using UnityEngine;

+ 1 - 1
OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/OperateStep.Designer.cs

@@ -1,5 +1,5 @@
 /****************************************************************************
- * 2025.7 CHIVA
+ * 2025.10 LXD
  ****************************************************************************/
 
 using UnityEngine;

+ 1 - 1
OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/OperateStep/FistStepItem.Designer.cs

@@ -1,5 +1,5 @@
 /****************************************************************************
- * 2025.7 CHIVA
+ * 2025.10 LXD
  ****************************************************************************/
 
 using UnityEngine;

+ 1 - 1
OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/OperateStep/SecondStepItem.Designer.cs

@@ -1,5 +1,5 @@
 /****************************************************************************
- * 2025.7 CHIVA
+ * 2025.10 LXD
  ****************************************************************************/
 
 using UnityEngine;

+ 1 - 1
OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/PracticeResult.Designer.cs

@@ -1,5 +1,5 @@
 /****************************************************************************
- * 2025.7 CHIVA
+ * 2025.10 LXD
  ****************************************************************************/
 
 using UnityEngine;

+ 3 - 1
OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/ScoreInfo.Designer.cs

@@ -1,5 +1,5 @@
 /****************************************************************************
- * 2025.7 CHIVA
+ * 2025.10 LXD
  ****************************************************************************/
 
 using UnityEngine;
@@ -16,6 +16,7 @@ namespace QFramework
 		[SerializeField] public UnityEngine.RectTransform Item;
 		[SerializeField] public UnityEngine.RectTransform Content;
 		[SerializeField] public UnityEngine.UI.Button ConfirmBtn;
+		[SerializeField] public UnityEngine.UI.Button ExportBtn;
 
 		public void Clear()
 		{
@@ -25,6 +26,7 @@ namespace QFramework
 			Item = null;
 			Content = null;
 			ConfirmBtn = null;
+			ExportBtn = null;
 		}
 
 		public override string ComponentName

+ 350 - 11
OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/ScoreInfo.cs

@@ -1,4 +1,4 @@
-/****************************************************************************
+/****************************************************************************
  * 2023.11 DESKTOP-DL7CJI0
  ****************************************************************************/
 
@@ -8,23 +8,29 @@ using UnityEngine;
 using UnityEngine.UI;
 using QFramework;
 using ChivaXR.Op;
+using iTextSharp.text.pdf;
+using iTextSharp.text;
+using System.IO;
+using PdfFont = iTextSharp.text.Font;
 
 namespace QFramework
 {
 	public partial class ScoreInfo : UIElement
-	{		
-		void Start()
+	{
+        void Start()
 		{
 			ConfirmBtn.gameObject.SetActive(true);
 
             ConfirmBtn.onClick.AddListener(OnConfirmBtnClick);
+
+            ExportBtn.onClick.AddListener(OnExportPDFClick);
         }
 
 		public void SetInfo(List<ExamProcessElement> elements, float score, int useTime)
 		{
 			int remain = useTime % 60;
 
-			UseTime.text = (useTime/60).ToString() + "·ÖÖÓ" + remain + "Ãë";
+			UseTime.text = (useTime/60).ToString() + "分钟" + remain + "秒";
 			Score.text = score.ToString();
 			Score_Shadow.text = score.ToString();
 
@@ -42,20 +48,353 @@ namespace QFramework
 			}
 		}
 
-        void Update()
-        {
-   
-        }
-
 		public void OnConfirmBtnClick()
 		{
             //UIKit.GetPanel<PC_OperatePanel>().ScoreInfo.gameObject.SetActive(false);
-			//¹Ø±Õ²Ëµ¥Í˳öϵͳ
+			//关闭��退出系统
 			Application.Quit();
         }
 
         protected override void OnBeforeDestroy()
 		{
 		}
-	}
+
+        public void OnExportPDFClick()
+        {
+            UserProxy tmpUserProxy = DAL.Instance.Get<UserProxy>();
+            ExamProxy tmpExamProxy = DAL.Instance.Get<ExamProxy>();
+            ScoreInfo scorePanel = UIKit.GetPanel<PC_OperatePanel>().ScoreInfo;
+
+            if (scorePanel == null)
+            {
+                Debug.LogError("� 未找到 ScoreInfo ��,请确�在当�场景中存在。");
+                return;
+            }
+
+            // ====== PDF导出路径 ======
+            //选择的路径--如果没有选择路径那就使用默认路径
+            string tmpSelectSavePath = FolderBrowserHelper.GetPathFromWindowsExplorer("请选择�绩导出�置");
+
+            string tmpSavePath = string.Empty;
+
+            if (string.IsNullOrEmpty(tmpSelectSavePath))
+            {
+                tmpSelectSavePath = Path.Combine(Application.streamingAssetsPath, "考试�绩");
+
+                tmpSavePath = Path.Combine(Application.streamingAssetsPath + "/考试�绩", $"{tmpUserProxy.userInfo.userName}_{DateTime.Now:yyyy年MM月dd日HH时mm分}_�绩�.pdf");
+            }
+            else
+            {
+                tmpSavePath = Path.Combine(tmpSelectSavePath, $"{tmpUserProxy.userInfo.userName}_{DateTime.Now:yyyy年MM月dd日HH时mm分}_�绩�.pdf");
+            }
+            
+            Document doc = new Document(PageSize.A4, 60, 60, 80, 60); //上边�略大,留页眉空间
+            PdfWriter writer = PdfWriter.GetInstance(doc, new FileStream(tmpSavePath, FileMode.CreateNew));
+
+            // ===== 页脚和页� =====
+            writer.PageEvent = new PdfPageEvents();
+
+            doc.Open();
+
+            // ====== 字体设置 ======
+            string fontPath = Path.Combine(Application.streamingAssetsPath, "Fonts/�绩字体.ttf");
+            if (!File.Exists(fontPath))
+            {
+                Debug.LogError("字体文件未找到:" + fontPath);
+                return;
+            }
+
+            BaseFont bfChinese = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
+            PdfFont smallFont = new PdfFont(bfChinese, 10) { Color = new BaseColor(100, 100, 100) }; // �色
+            PdfFont normalFont = new PdfFont(bfChinese, 12);
+            PdfFont boldFont = new PdfFont(bfChinese, 12, PdfFont.BOLD);
+            PdfFont titleFont = new PdfFont(bfChinese, 30, PdfFont.BOLD);
+
+            PdfContentByte cb = writer.DirectContent;
+
+            // ===== 横线 =====
+            float lineY = doc.Top - 20f;
+            cb.MoveTo(doc.Left, lineY);
+            cb.LineTo(doc.Right, lineY);
+            cb.Stroke();
+
+            // ===== 页眉在横线之上 =====
+            string headerText = "虚拟仿真实验中心";
+            ColumnText.ShowTextAligned(
+                cb,
+                Element.ALIGN_CENTER,
+                new Phrase(headerText, smallFont),
+                (doc.Left + doc.Right) / 2,
+                lineY + 10,  // 横线上方 10pt
+                0
+            );
+
+            // ===== LOGO 横线上方左侧 =====
+            string logoPath = Path.Combine(Application.streamingAssetsPath, "Logos", "�绩logo.png");
+
+            // 统一去除 URL �缀(特别是 file://)
+            if (logoPath.StartsWith("file://"))
+                logoPath = logoPath.Replace("file://", "");
+
+            if (!File.Exists(logoPath))
+            {
+                // Unity Editor 下 StreamingAssets 在项目路径内
+                logoPath = Path.Combine(Application.streamingAssetsPath, "Logos", "�绩logo.png");
+            }
+            float logoBottomY = lineY; // 用于计算标题�置
+            if (File.Exists(logoPath))
+            {
+                try
+                {
+                    byte[] logoBytes = File.ReadAllBytes(logoPath);
+                    iTextSharp.text.Image logo = iTextSharp.text.Image.GetInstance(logoBytes);
+
+                    // 👉 �固定高度,让宽度按比例缩放
+                    float targetHeight = 50f;  // 设定统一显示宽度
+                    float scaleFactor = targetHeight / logo.Height; // 缩放比例
+                    logo.ScalePercent(scaleFactor * 100); // iTextSharp 用百分比
+
+                    // ��顶部�离横线 5pt
+                    float logoTopY = lineY + 5f + logo.ScaledHeight;
+                    float logoPosY = logoTopY - logo.ScaledHeight;
+                    logoBottomY = logoPosY;
+
+                    // 让左下角对�页边�
+                    logo.SetAbsolutePosition(doc.Left, logoPosY);
+                    doc.Add(logo);
+
+                    Debug.Log($"✅ Logo 添加�功(原始大�: {logo.Width}x{logo.Height}, 缩放比例: {scaleFactor:F2})");
+                }
+                catch (System.Exception e)
+                {
+                    Debug.LogError("� 读� Logo 出错:" + e.Message);
+                }
+            }
+            else
+            {
+                Debug.LogWarning("⚠� 未找到 Logo 文件:" + logoPath);
+            }
+
+            // ===== 主标题(�独一行居中) =====
+            Paragraph title = new Paragraph("� 绩 �", titleFont);
+            title.Alignment = Element.ALIGN_CENTER;
+            title.SpacingBefore = 30f;
+            title.SpacingAfter = 20f;
+            doc.Add(title);
+
+            // ===== 基本信�表格 =====
+            PdfPTable infoTable = new PdfPTable(4);
+            infoTable.WidthPercentage = 100;
+            infoTable.SpacingBefore = 10f;
+            infoTable.SpacingAfter = 15f;
+            infoTable.SetWidths(new float[] { 1.2f, 2f, 1.2f, 2f });
+
+            AddCell(infoTable, "姓�", boldFont, new BaseColor(235, 235, 235));
+            AddCell(infoTable, tmpUserProxy.userInfo.userName, normalFont);
+            AddCell(infoTable, "实验日期", boldFont, new BaseColor(235, 235, 235));
+            AddCell(infoTable, DateTime.Now.ToString("yyyy年MM月dd日HH时"), normalFont);
+
+            AddCell(infoTable, "实验�称", boldFont, new BaseColor(235, 235, 235));
+            AddCell(infoTable, OperateSetting.Instance.m_CourseName, normalFont);
+            AddCell(infoTable, "总�绩", boldFont, new BaseColor(235, 235, 235));
+            AddCell(infoTable, float.Parse(scorePanel.Score.text).ToString("F1") + " 分", normalFont);
+
+            AddCell(infoTable, "用时", boldFont, new BaseColor(235, 235, 235));
+            AddCell(infoTable, scorePanel.UseTime.text, normalFont);
+            AddCell(infoTable, "", normalFont);
+            AddCell(infoTable, "", normalFont);
+
+            doc.Add(infoTable);
+
+            // ===== �作步骤表格 =====
+            PdfPTable stepTable = new PdfPTable(3);
+            stepTable.WidthPercentage = 100;
+            stepTable.SetWidths(new float[] { 1f, 3f, 1f });
+
+            AddHeaderCell(stepTable, "��", boldFont);
+            AddHeaderCell(stepTable, "�作步骤", boldFont);
+            AddHeaderCell(stepTable, "分数", boldFont);
+
+            foreach (var examInfo in tmpExamProxy.examScoreInfos)
+            {
+                AddCell(stepTable, examInfo.id.ToString(), normalFont);
+                AddCell(stepTable, examInfo.stepName, normalFont);
+
+                string score = examInfo.score < 0 ? "0" : examInfo.score.ToString("F1");
+
+                AddCell(stepTable, score.ToString(), normalFont);
+            }
+
+            doc.Add(stepTable);
+
+            // ===== 评语 =====
+            doc.Add(new Paragraph("\n"));
+            string commentText = GenerateComment(float.Parse(scorePanel.Score.text));
+            Paragraph comment = new Paragraph($"评语:{commentText}", normalFont);
+            comment.SpacingBefore = 10;
+            doc.Add(comment);
+
+            doc.Close();
+            writer.Close();
+            Debug.Log($"✅ �绩�导出�功:{tmpSavePath}");
+
+            CheckGradeCount(tmpSelectSavePath,20, "*�绩�.pdf");
+
+        }
+
+        /// <summary>
+        /// 生�评语 
+        /// </summary>
+        /// <param name="score"></param>
+        /// <returns></returns>
+        private string GenerateComment(float score)
+        {
+            if (score >= 90f)
+                return "表现优秀,�作熟练,�解深入。";
+            else if (score >= 75f)
+                return "完�良好,掌�较为扎实,�继续巩固。";
+            else if (score >= 60f)
+                return "基本达标,但�需加强�解与�作细节。";
+            else
+                return "需改进,建议�习实验步骤并�高熟练度。";
+        }
+
+        private  void AddHeaderCell(PdfPTable table, string text, PdfFont font)
+        {
+            PdfPCell cell = new PdfPCell(new Phrase(text, font));
+            cell.BackgroundColor = new BaseColor(220, 220, 220);
+            cell.HorizontalAlignment = Element.ALIGN_CENTER;
+            cell.MinimumHeight = 25;
+            table.AddCell(cell);
+        }
+
+        private  void AddCell(PdfPTable table, string text, PdfFont font, BaseColor bg = null)
+        {
+            PdfPCell cell = new PdfPCell(new Phrase(text, font));
+            cell.HorizontalAlignment = Element.ALIGN_CENTER;
+            cell.VerticalAlignment = Element.ALIGN_MIDDLE;
+            cell.MinimumHeight = 22;
+            if (bg != null) cell.BackgroundColor = bg;
+            table.AddCell(cell);
+        }
+
+        // ===== 页脚页�事件类 =====
+        private class PdfPageEvents : PdfPageEventHelper
+        {
+            private PdfTemplate totalPages;
+            private BaseFont baseFont;
+            private PdfFont pdfFont;
+            public override void OnOpenDocument(PdfWriter writer, Document document)
+            {
+                string fontPath = Path.Combine(Application.streamingAssetsPath, "Fonts/�绩字体.ttf");
+
+                baseFont = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
+                pdfFont = new PdfFont(baseFont,10);
+
+                totalPages = writer.DirectContent.CreateTemplate(100, 100);
+            }
+
+            public override void OnEndPage(PdfWriter writer, Document document)
+            {
+                base.OnEndPage(writer, document);
+
+                // 添加页眉
+                //AddHeader(writer, document);
+
+                // 添加页脚
+                AddFooter(writer, document);
+            }
+
+            private void AddHeader(PdfWriter writer, Document document)
+            {
+                PdfPTable headerTable = new PdfPTable(1);
+                headerTable.TotalWidth = document.PageSize.Width - document.LeftMargin - document.RightMargin;
+
+                PdfPCell cell = new PdfPCell(new Phrase("这是页眉内容", pdfFont));
+                cell.Border = Rectangle.BOTTOM_BORDER;
+                cell.BorderColor = BaseColor.GRAY;
+                cell.BorderWidth = 1f;
+                cell.PaddingBottom = 5f;
+                cell.HorizontalAlignment = Element.ALIGN_CENTER;
+
+                headerTable.AddCell(cell);
+                headerTable.WriteSelectedRows(0, -1, document.LeftMargin, document.PageSize.Height - document.TopMargin + 10,
+                                             writer.DirectContent);
+            }
+
+            private void AddFooter(PdfWriter writer, Document document)
+            {
+                PdfPTable footerTable = new PdfPTable(2);
+                footerTable.TotalWidth = document.PageSize.Width - document.LeftMargin - document.RightMargin;
+
+                // 左侧文本
+                PdfPCell leftCell = new PdfPCell(new Phrase("虚拟仿真实验考核系统 © 2025", pdfFont));
+                leftCell.Border = Rectangle.TOP_BORDER;
+                leftCell.BorderColor = BaseColor.GRAY;
+                leftCell.HorizontalAlignment = Element.ALIGN_LEFT;
+
+                // 居中页�
+                string pageText = $"第 {writer.PageNumber} 页";
+                PdfPCell centerCell = new PdfPCell(new Phrase(pageText, pdfFont));
+                centerCell.Border = Rectangle.TOP_BORDER;
+                centerCell.BorderColor = BaseColor.GRAY;
+                centerCell.HorizontalAlignment = Element.ALIGN_RIGHT;
+
+                // �侧总页数
+                //PdfPCell rightCell = new PdfPCell(new Phrase("总页数:", pdfFont));
+                //rightCell.Border = Rectangle.TOP_BORDER;
+                //rightCell.BorderColor = BaseColor.GRAY;
+                //rightCell.HorizontalAlignment = Element.ALIGN_RIGHT;
+
+                footerTable.AddCell(leftCell);
+                footerTable.AddCell(centerCell);
+                //footerTable.AddCell(rightCell);
+
+                footerTable.WriteSelectedRows(0, -1, document.LeftMargin, document.BottomMargin - 10,
+                                             writer.DirectContent);
+            }
+
+            public override void OnCloseDocument(PdfWriter writer, Document document)
+            {
+                base.OnCloseDocument(writer, document);
+
+                // 在文档关闭时写入总页数
+                //ColumnText.ShowTextAligned(totalPages, Element.ALIGN_LEFT,
+                //    new Phrase((writer.PageNumber - 1).ToString(), pdfFont),
+                //    2, 2, 0);
+            }
+        }
+
+        /// <summary>
+        /// 检查�绩数�
+        /// </summary>
+        /// <param name="path">路径</param>
+        /// <param name="quantityLimit">�制数�</param>
+        /// <param name="searchPattern">�索�件</param>
+        private void CheckGradeCount(string path, int quantityLimit, string searchPattern)
+        {
+            string[] filePaths = FileToolkit.SearchFilePaths(path, searchPattern);
+
+            if (filePaths.Length > quantityLimit)
+            {
+                for (int i = 0; i < filePaths.Length - 1; i++)
+                {
+                    for (int j = 0; j < filePaths.Length - i - 1; j++)
+                    {
+                        if (FileToolkit.CompareBeforAndAfterCreateTime(filePaths[j], filePaths[j + 1]))
+                        {
+                            // 交� arr[j] 和 arr[j + 1]
+                            string temp = filePaths[j];
+                            filePaths[j] = filePaths[j + 1];
+                            filePaths[j + 1] = temp;
+
+                        }
+                    }
+                }
+                Debug.Log("删除�绩" + filePaths[0]);
+                File.Delete(filePaths[0]);
+            }
+        }
+
+    }
 }

+ 1 - 1
OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/StartTips.Designer.cs

@@ -1,5 +1,5 @@
 /****************************************************************************
- * 2025.7 CHIVA
+ * 2025.10 LXD
  ****************************************************************************/
 
 using UnityEngine;

+ 1 - 1
OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/StudyPanel.Designer.cs

@@ -1,5 +1,5 @@
 /****************************************************************************
- * 2025.7 CHIVA
+ * 2025.10 LXD
  ****************************************************************************/
 
 using UnityEngine;

+ 1 - 1
OperationUIFrame/OperationUIFrameV2/Scripts/UI/PC_OperatePanel/SubmitResult.Designer.cs

@@ -1,5 +1,5 @@
 /****************************************************************************
- * 2025.7 CHIVA
+ * 2025.10 LXD
  ****************************************************************************/
 
 using UnityEngine;

+ 0 - 2
OperationUIFrame/OperationUIFrameV2/Scripts/UI/ScoreInfoItem.cs

@@ -9,12 +9,10 @@ using I2.Loc;
 
 public class ScoreInfoItem : MonoBehaviour
 {
-
 	/// <summary>
 	/// 底图
 	/// </summary>
 	public GameObject m_BaseMap;
-	
 	/// <summary>
 	/// 步骤id
 	/// </summary>

+ 264 - 7
OperationUIFrame/OperationUIFrameV2/UIPrefabs/PC_OperatePanel.prefab

@@ -3358,6 +3358,123 @@ MonoBehaviour:
   m_Name: 
   m_EditorClassIdentifier: 
   scrollview: {fileID: 270643271180574912}
+--- !u!1 &887436751131807749
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 1699269170352624347}
+  - component: {fileID: 2776126869527016128}
+  - component: {fileID: 6493044182731787802}
+  - component: {fileID: 5113388517272013039}
+  m_Layer: 5
+  m_Name: Text
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!224 &1699269170352624347
+RectTransform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 887436751131807749}
+  m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_Children: []
+  m_Father: {fileID: 5499257648862480122}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+  m_AnchorMin: {x: 0, y: 0}
+  m_AnchorMax: {x: 1, y: 1}
+  m_AnchoredPosition: {x: 0, y: 0}
+  m_SizeDelta: {x: 0, y: 0}
+  m_Pivot: {x: 0.5, y: 0.5}
+--- !u!222 &2776126869527016128
+CanvasRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 887436751131807749}
+  m_CullTransparentMesh: 0
+--- !u!114 &6493044182731787802
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 887436751131807749}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Material: {fileID: 0}
+  m_Color: {r: 1, g: 1, b: 1, a: 1}
+  m_RaycastTarget: 1
+  m_Maskable: 1
+  m_OnCullStateChanged:
+    m_PersistentCalls:
+      m_Calls: []
+  m_FontData:
+    m_Font: {fileID: 12800000, guid: 40655297447ed2e46a2d4b5c829580da, type: 3}
+    m_FontSize: 20
+    m_FontStyle: 0
+    m_BestFit: 0
+    m_MinSize: 2
+    m_MaxSize: 40
+    m_Alignment: 4
+    m_AlignByGeometry: 0
+    m_RichText: 1
+    m_HorizontalOverflow: 0
+    m_VerticalOverflow: 0
+    m_LineSpacing: 1
+  m_Text: "\u5BFC\u51FA\u6210\u7EE9"
+--- !u!114 &5113388517272013039
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 887436751131807749}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 344445a89b4f74a0e9a0a766903df87e, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  mTerm: exportGrades
+  mTermSecondary: 
+  PrimaryTermModifier: 0
+  SecondaryTermModifier: 0
+  TermPrefix: 
+  TermSuffix: 
+  LocalizeOnAwake: 1
+  IgnoreRTL: 0
+  MaxCharactersInRTL: 0
+  IgnoreNumbersInRTL: 1
+  CorrectAlignmentForRTL: 1
+  AddSpacesToJoinedLanguages: 0
+  AllowLocalizedParameters: 1
+  TranslatedObjects: []
+  LocalizeEvent:
+    m_PersistentCalls:
+      m_Calls: []
+  AlwaysForceLocalize: 0
+  LocalizeCallBack:
+    Target: {fileID: 0}
+    MethodName: 
+  mGUI_ShowReferences: 0
+  mGUI_ShowTems: 1
+  mGUI_ShowCallback: 0
+  mLocalizeTarget: {fileID: 0}
+  mLocalizeTargetName: I2.Loc.LocalizeTarget_UnityUI_Text
 --- !u!1 &888655909969223658
 GameObject:
   m_ObjectHideFlags: 0
@@ -3596,6 +3713,7 @@ RectTransform:
   - {fileID: 3524390655375017021}
   - {fileID: 3631619018895126077}
   - {fileID: 9220195329770722693}
+  - {fileID: 5499257648862480122}
   m_Father: {fileID: 5937661528918436544}
   m_RootOrder: 4
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
@@ -3659,6 +3777,7 @@ MonoBehaviour:
   Item: {fileID: 2201595904997836746}
   Content: {fileID: 134708385191675878}
   ConfirmBtn: {fileID: 4238464256931499907}
+  ExportBtn: {fileID: 3101506558637669177}
 --- !u!1 &995660817717548131
 GameObject:
   m_ObjectHideFlags: 0
@@ -7657,6 +7776,144 @@ MonoBehaviour:
   mGUI_ShowCallback: 0
   mLocalizeTarget: {fileID: 0}
   mLocalizeTargetName: I2.Loc.LocalizeTarget_UnityUI_Text
+--- !u!1 &2747890237582618001
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 5499257648862480122}
+  - component: {fileID: 3848132266616053872}
+  - component: {fileID: 5081385360510800044}
+  - component: {fileID: 3101506558637669177}
+  - component: {fileID: 4281631426592982633}
+  m_Layer: 5
+  m_Name: ExportBtn
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!224 &5499257648862480122
+RectTransform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 2747890237582618001}
+  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_Children:
+  - {fileID: 1699269170352624347}
+  m_Father: {fileID: 1032962305134131580}
+  m_RootOrder: 7
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+  m_AnchorMin: {x: 0.5, y: 0.5}
+  m_AnchorMax: {x: 0.5, y: 0.5}
+  m_AnchoredPosition: {x: 60, y: -381.8}
+  m_SizeDelta: {x: 129, y: 36}
+  m_Pivot: {x: 0.5, y: 0.5}
+--- !u!222 &3848132266616053872
+CanvasRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 2747890237582618001}
+  m_CullTransparentMesh: 0
+--- !u!114 &5081385360510800044
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 2747890237582618001}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Material: {fileID: 0}
+  m_Color: {r: 1, g: 1, b: 1, a: 1}
+  m_RaycastTarget: 1
+  m_Maskable: 1
+  m_OnCullStateChanged:
+    m_PersistentCalls:
+      m_Calls: []
+  m_Sprite: {fileID: 21300000, guid: 0b2112719e2768542b537d84dd93109e, type: 3}
+  m_Type: 0
+  m_PreserveAspect: 0
+  m_FillCenter: 1
+  m_FillMethod: 4
+  m_FillAmount: 1
+  m_FillClockwise: 1
+  m_FillOrigin: 0
+  m_UseSpriteMesh: 0
+  m_PixelsPerUnitMultiplier: 1
+--- !u!114 &3101506558637669177
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 2747890237582618001}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Navigation:
+    m_Mode: 3
+    m_SelectOnUp: {fileID: 0}
+    m_SelectOnDown: {fileID: 0}
+    m_SelectOnLeft: {fileID: 0}
+    m_SelectOnRight: {fileID: 0}
+  m_Transition: 2
+  m_Colors:
+    m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
+    m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+    m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+    m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+    m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
+    m_ColorMultiplier: 1
+    m_FadeDuration: 0.1
+  m_SpriteState:
+    m_HighlightedSprite: {fileID: 21300000, guid: 596ea1177603dd346b27998610b571ca,
+      type: 3}
+    m_PressedSprite: {fileID: 21300000, guid: 596ea1177603dd346b27998610b571ca, type: 3}
+    m_SelectedSprite: {fileID: 21300000, guid: 596ea1177603dd346b27998610b571ca, type: 3}
+    m_DisabledSprite: {fileID: 21300000, guid: 0b2112719e2768542b537d84dd93109e, type: 3}
+  m_AnimationTriggers:
+    m_NormalTrigger: Normal
+    m_HighlightedTrigger: Highlighted
+    m_PressedTrigger: Pressed
+    m_SelectedTrigger: Selected
+    m_DisabledTrigger: Disabled
+  m_Interactable: 1
+  m_TargetGraphic: {fileID: 5081385360510800044}
+  m_OnClick:
+    m_PersistentCalls:
+      m_Calls: []
+--- !u!114 &4281631426592982633
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 2747890237582618001}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 0d51f3a7c41ab0346b49ae50d456bece, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  MarkType: 0
+  CustomComponentName: ConfirmBtn
+  ComponentGeneratePath: 
+  CustomComment: 
+  mComponentName: UnityEngine.UI.Button
 --- !u!1 &2761693071561861445
 GameObject:
   m_ObjectHideFlags: 0
@@ -9142,7 +9399,7 @@ RectTransform:
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
   m_AnchorMin: {x: 0.5, y: 0.5}
   m_AnchorMax: {x: 0.5, y: 0.5}
-  m_AnchoredPosition: {x: 0, y: -381.8}
+  m_AnchoredPosition: {x: -84.099976, y: -381.8}
   m_SizeDelta: {x: 129, y: 36}
   m_Pivot: {x: 0.5, y: 0.5}
 --- !u!222 &7327826225571033312
@@ -9590,7 +9847,7 @@ RectTransform:
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
   m_AnchorMin: {x: 1, y: 1}
   m_AnchorMax: {x: 1, y: 1}
-  m_AnchoredPosition: {x: -371.19992, y: -56.200012}
+  m_AnchoredPosition: {x: -371.19995, y: -144.44617}
   m_SizeDelta: {x: 61.586914, y: 30}
   m_Pivot: {x: 0.5, y: 0.5}
 --- !u!222 &8035942305638376360
@@ -10772,7 +11029,7 @@ RectTransform:
   m_PrefabInstance: {fileID: 0}
   m_PrefabAsset: {fileID: 0}
   m_GameObject: {fileID: 4125604218355242711}
-  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+  m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
   m_LocalPosition: {x: 0, y: 0, z: 0}
   m_LocalScale: {x: 1, y: 1, z: 1}
   m_Children: []
@@ -10781,7 +11038,7 @@ RectTransform:
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
   m_AnchorMin: {x: 0, y: 0.5}
   m_AnchorMax: {x: 0, y: 0.5}
-  m_AnchoredPosition: {x: 133.07, y: -8.25}
+  m_AnchoredPosition: {x: 133.07, y: -15.670044}
   m_SizeDelta: {x: 231.07074, y: 38.856934}
   m_Pivot: {x: 0.5, y: 0.5}
 --- !u!222 &2220818534546923475
@@ -15420,8 +15677,8 @@ RectTransform:
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
   m_AnchorMin: {x: 0, y: 0.9}
   m_AnchorMax: {x: 1, y: 1}
-  m_AnchoredPosition: {x: -2.5, y: -91.42}
-  m_SizeDelta: {x: -734.9946, y: -182.84033}
+  m_AnchoredPosition: {x: -2.5, y: -84}
+  m_SizeDelta: {x: -734.9946, y: -21.18805}
   m_Pivot: {x: 0.5, y: 0.5}
 --- !u!1 &6850574410406680057
 GameObject:
@@ -18268,7 +18525,7 @@ RectTransform:
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
   m_AnchorMin: {x: 1, y: 1}
   m_AnchorMax: {x: 1, y: 1}
-  m_AnchoredPosition: {x: -158.16, y: 23.9}
+  m_AnchoredPosition: {x: -158.16003, y: -64.34616}
   m_SizeDelta: {x: 130.6803, y: 30}
   m_Pivot: {x: 0.5, y: 0.5}
 --- !u!222 &757613297832187400

+ 8 - 0
StreamingAssets.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 7c1271fd5c2c03745940fa7bb4ff60a5
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
考试成绩输出功能配置.unitypackage


+ 7 - 0
考试成绩输出功能配置.unitypackage.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: bc048e781bbc81b4fb59b2d144362afc
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: