ソースを参照

新增锁付结果查询与导出功能,优化相机分析

- 新增 LockResult 数据库存储、分页/导出查询接口及索引
- 统计界面增加锁付结果查询页面,支持条件筛选与CSV导出
- LockResult 模型扩展主键、图片路径等字段
- 导航增加 MainTabSwitchNotification 事件,避免重复导航
- ImageHelper 增加 ICogImage 与 Bitmap/Mat/BitmapImage 互转方法
- 调试界面支持相机列表选择与实时采集分析,隐藏“打开图像”入口
- 相关 XAML、csproj 文件同步更新,完善界面与引用
孝锋 徐 8 ヶ月 前
コミット
7dc9001b6d

+ 91 - 1
TeamAAS-VM/Core/ImageHelper.cs

@@ -1,10 +1,14 @@
-using System;
+using Cognex.VisionPro;
+using OpenCvSharp;
+using System;
 using System.Collections.Generic;
 using System.Drawing;
 using System.Drawing.Imaging;
+using System.IO;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
+using System.Windows.Media.Imaging;
 
 namespace TeamAAS_VP.Core
 {
@@ -41,5 +45,91 @@ namespace TeamAAS_VP.Core
             }
             return null;
         }
+
+        public static Bitmap ICogImageConvertBitmap(ICogImage image)
+        {
+            return image.ToBitmap();
+        }
+
+        public static Mat ICogImageConvertMat(ICogImage image)
+        {
+            var im = image.ToBitmap();
+            Mat im1 = OpenCvSharp.Extensions.BitmapConverter.ToMat(im);
+            im.Dispose();
+            return im1;
+        }
+
+        public static Bitmap MatConvertBitmap(Mat image)
+        {
+            return OpenCvSharp.Extensions.BitmapConverter.ToBitmap(image);
+        }
+
+        public static ICogImage MatConvertICogImage(Mat image, bool color = false)
+        {
+            var im = OpenCvSharp.Extensions.BitmapConverter.ToBitmap(image);
+            ICogImage cogImage = new CogImage8Grey(im);
+            if (color)
+            {
+                cogImage = new CogImage24PlanarColor(im);
+            }
+            im.Dispose();
+            return cogImage;
+        }
+
+        public static BitmapImage ConvertToBitmapImage(System.Drawing.Bitmap bitmap)
+        {
+            using (MemoryStream memoryStream = new MemoryStream())
+            {
+                // 将 System.Drawing.Bitmap 保存到内存流中
+                bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
+
+                // 重置内存流的位置,以便从头开始读取
+                memoryStream.Seek(0, SeekOrigin.Begin);
+
+                // 创建 BitmapImage 对象并加载内存流中的图像数据
+                BitmapImage bitmapImage = new BitmapImage();
+                bitmapImage.BeginInit();
+                bitmapImage.StreamSource = memoryStream;
+                bitmapImage.EndInit();
+
+                return bitmapImage;
+            }
+        }
+
+        public static BitmapImage ConvertToBitmapImage(ICogImage image)
+        {
+            var bitmap = image.ToBitmap();
+            using (MemoryStream memoryStream = new MemoryStream())
+            {
+                // 将 System.Drawing.Bitmap 保存到内存流中
+                bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
+
+                // 重置内存流的位置,以便从头开始读取
+                memoryStream.Seek(0, SeekOrigin.Begin);
+
+                // 创建 BitmapImage 对象并加载内存流中的图像数据
+                BitmapImage bitmapImage = new BitmapImage();
+                bitmapImage.BeginInit();
+                bitmapImage.StreamSource = memoryStream;
+                bitmapImage.EndInit();
+                bitmap.Dispose();
+                return bitmapImage;
+            }
+        }
+
+        public static BitmapImage BitmapToBitmapImage(System.Drawing.Bitmap bitmap)
+        {
+            BitmapImage bitmapImage = new BitmapImage();
+            using (System.IO.MemoryStream ms = new System.IO.MemoryStream())
+            {
+                bitmap.Save(ms, bitmap.RawFormat);
+                bitmapImage.BeginInit();
+                bitmapImage.StreamSource = ms;
+                bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
+                bitmapImage.EndInit();
+                bitmapImage.Freeze();
+            }
+            return bitmapImage;
+        }
     }
 }

+ 1 - 0
TeamAAS-VM/Core/Management.cs

@@ -1325,6 +1325,7 @@ namespace TeamAAS_VP.Core
                             lockResult.OperatorName = "";
                             lockResult.WaveDatas= ScrewDriver.WaveDatas;
                             _eventAggregator.GetEvent<LockFinishNotification>().Publish(lockResult);
+                            await _systemDatabaseService.RecordLockResultAsync(lockResult);
                         }
                         else
                         {

+ 24 - 2
TeamAAS-VM/Data/DatabaseInitializer.cs

@@ -26,7 +26,7 @@ namespace TeamAAS_VP.Data
                 // 创建数据库(如果不存在)
                 _db.DbMaintenance.CreateDatabase();
                 // 创建表
-                _db.CodeFirst.InitTables(typeof(User), typeof(ProductionRecord), typeof(UserLoginRecord), typeof(AlarmRecord));
+                _db.CodeFirst.InitTables(typeof(User), typeof(ProductionRecord), typeof(UserLoginRecord), typeof(AlarmRecord), typeof(LockResult));
 
                 // 创建索引
                 CreateIndexes();
@@ -55,7 +55,29 @@ namespace TeamAAS_VP.Data
 
         private void CreateIndexes()
         {
-            // 索引创建逻辑(同上一个回答)
+            try
+            {
+                // Ensure LockResult indexes for common queries
+                // 单列索引:Timestamp, ProductNumber, LockPassed
+                try { _db.DbMaintenance.CreateIndex("LockResult", new string[] { "Timestamp" }, "IX_LockResult_Timestamp", false); } catch { }
+                try { _db.DbMaintenance.CreateIndex("LockResult", new string[] { "ProductNumber" }, "IX_LockResult_ProductNumber", false); } catch { }
+                try { _db.DbMaintenance.CreateIndex("LockResult", new string[] { "LockPassed" }, "IX_LockResult_LockPassed", false); } catch { }
+
+                // 新增索引:Number, FeederNumber, NozzleNumber
+                try { _db.DbMaintenance.CreateIndex("LockResult", new string[] { "Number" }, "IX_LockResult_Number", false); } catch { }
+                try { _db.DbMaintenance.CreateIndex("LockResult", new string[] { "FeederNumber" }, "IX_LockResult_FeederNumber", false); } catch { }
+                try { _db.DbMaintenance.CreateIndex("LockResult", new string[] { "NozzleNumber" }, "IX_LockResult_NozzleNumber", false); } catch { }
+
+                // 复合索引:ProductNumber + Timestamp(支持按产品+时间范围查询)
+                try { _db.DbMaintenance.CreateIndex("LockResult", new string[] { "ProductNumber", "Timestamp" }, "IX_LockResult_ProductNumber_Timestamp", false); } catch { }
+
+                // 你可以在这里为其他表添加索引,例如 ProductionRecord 的 Timestamp 或 ProductName
+                try { _db.DbMaintenance.CreateIndex("ProductionRecord", new string[] { "ProductName", "Timestamp" }, "IX_ProductionRecord_ProductName_Timestamp", false); } catch { }
+            }
+            catch
+            {
+                // 忽略索引创建中的错误(如已存在),调用方仍然可以继续
+            }
         }
     }
 }

+ 48 - 56
TeamAAS-VM/Data/SystemDatabaseService.cs

@@ -1,56 +1,3 @@
-/*
-  设计计划(伪代码) - 详尽步骤说明
-
-  目标:
-    - 为 SystemDatabaseService 类添加完整的文档注释(XML 注释),
-      以便在 IDE 中显示方法契约和用途。
-    - 保持现有实现逻辑不变,增加必要的参数检查(轻量级)以提高健壮性,
-      但不改变公开方法签名,确保兼容 ISystemDatabaseService。
-    - 将伪代码通过文件顶部注释包含在文件内(满足“先描述计划,再给代码”的要求)。
-
-  伪代码步骤:
-    1. 在文件顶部插入多行注释,包含本次修改的目的和每个主要动作的摘要。
-    2. 为类 `SystemDatabaseService` 添加 XML 文档注释,说明职责和依赖。
-    3. 为每个公共方法添加 XML 注释(summary, param, returns)。
-       - AddUserAsync(User user)
-         - 验证 user 不为 null(如为 null 则抛出 ArgumentNullException)
-         - 调用数据库插入并返回 user
-       - DeleteUserAsync(int userId)
-         - 直接调用 Deleteable 并返回是否删除到记录
-       - GetAllUsersAsync()
-         - 返回所有用户列表
-       - GetUserByIdAsync(int userId)
-         - 返回单一用户(InSingleAsync)
-       - UpdateUserAsync(User user)
-         - 验证 user 不为 null(如为 null 则抛出 ArgumentNullException)
-         - 更新并返回 user
-       - AuthenticateUserAsync(string username, string password)
-         - 验证输入参数不为 null 或空(轻量级)
-         - 使用 Queryable 查询并返回匹配用户(FirstAsync)
-       - RecordProductionAsync(ProductionRecord record)
-         - 验证 record 不为 null(如为 null 则抛出 ArgumentNullException)
-         - 确保 record.Id 非空(Guid),插入记录
-       - ResetProductionAsync(string productName = null)
-         - 如果 productName 为空,清空表,否则按产品名删除
-       - GetTotalProductionAsync(string productName, IEnumerable<string> categories = null)
-         - 构造查询,按类别可选过滤,求和数量并返回
-       - GetProductionStatsAsync(ProductionStatQuery query)
-         - 验证 query 不为 null(如为 null 则抛出 ArgumentNullException)
-         - 基于 query 参数构造查询(产品名、时间范围、类别)
-         - 拉取列表后根据 GroupBy(hour/day/week/month/total)分组聚合
-         - 返回聚合结果集合
-       - 私有方法 FirstDayOfWeek(DateTime dt)
-         - 计算给定日期的本周第一天(星期一),用于 week 分组
-
-    4. 确保所有新增的参数检查使用 .NET Framework 4.8 可用的异常类型。
-    5. 保持现有第三方 API(SqlSugar)调用不变,避免引入新依赖。
-    6. 生成的代码应能在 VS 中正确显示注释并通过编译(不改变方法签名)。
-
-  备注:
-    - 不会尝试修改或新增接口方法签名(以确保与 ISystemDatabaseService 保持兼容)。
-    - 本次变更侧重于文档注释与轻量的输入验证,避免对业务逻辑做重大调整。
-*/
-
 using SqlSugar;
 using System;
 using System.Collections.Generic;
@@ -291,7 +238,7 @@ namespace TeamAAS_VP.Data
         // Production records
 
         /// <summary>
-        /// 记录一次生产事件。如果 <see cref="ProductionRecord.Id"/> 为 <see cref="Guid.Empty"/>
+        /// 记录一次生产事件。如果 <see cref="ProductionRecord.Id"/> 为 <see cref="Guid.Empty"/>
         /// 则会自动生成新的 Guid。
         /// </summary>
         /// <param name="record">生产记录对象,不能为空。</param>
@@ -323,8 +270,8 @@ namespace TeamAAS_VP.Data
         }
 
         /// <summary>
-        /// 重置生产记录。若未传入 <paramref name="productName"/>,则清空所有生产记录;
-        /// 否则仅删除指定产品名的记录。
+        /// 重置生产记录。若未传入 <paramref name="productName"/>
+        /// 则清空所有生产记录;否则仅删除指定产品名的记录。
         /// </summary>
         /// <param name="productName">可选的产品名过滤。</param>
         /// <returns>异步任务。</returns>
@@ -653,6 +600,51 @@ namespace TeamAAS_VP.Data
             if (!string.IsNullOrEmpty(source)) q = q.Where(a => a.Source == source);
             return await q.OrderBy(a => a.Time, OrderByType.Desc).ToListAsync();
         }
+
         #endregion
+
+        // Lock result methods
+        /// <summary>
+        /// 记录单次锁付结果(WaveDatas 不写入数据库)
+        /// </summary>
+        /// <param name="result"></param>
+        public async Task RecordLockResultAsync(LockResult result)
+        {
+            if (result == null) throw new ArgumentNullException(nameof(result));
+            if (result.Timestamp == default(DateTime)) result.Timestamp = DateTime.UtcNow;
+            await _db.Insertable(result).ExecuteCommandAsync();
+        }
+
+        /// <summary>
+        /// 分页查询锁付结果
+        /// </summary>
+        public async Task<(IEnumerable<LockResult> Items, int TotalCount)> QueryLockResultsAsync(DateTime start, DateTime end, int pageIndex, int pageSize, string productNumber = null, int? screwNumber = null, bool? lockPassed = null)
+        {
+            if (pageIndex < 1) pageIndex = 1;
+            if (pageSize < 1) pageSize = 20;
+
+            var q = _db.Queryable<LockResult>().Where(r => r.Timestamp >= start && r.Timestamp < end);
+            if (!string.IsNullOrEmpty(productNumber)) q = q.Where(r => r.ProductNumber == productNumber);
+            if (screwNumber.HasValue) q = q.Where(r => r.Number == screwNumber.Value);
+            if (lockPassed.HasValue) q = q.Where(r => r.LockPassed == lockPassed.Value);
+
+            var total = await q.CountAsync();
+            var list = await q.OrderBy(r => r.Timestamp, OrderByType.Desc).ToPageListAsync(pageIndex, pageSize);
+            return (list, total);
+        }
+
+        /// <summary>
+        /// 查询所有符合条件的锁付结果(不分页)。
+        /// </summary>
+        public async Task<IEnumerable<LockResult>> QueryAllLockResultsAsync(DateTime start, DateTime end, string productNumber = null, int? screwNumber = null, bool? lockPassed = null)
+        {
+            var q = _db.Queryable<LockResult>().Where(r => r.Timestamp >= start && r.Timestamp < end);
+            if (!string.IsNullOrEmpty(productNumber)) q = q.Where(r => r.ProductNumber == productNumber);
+            if (screwNumber.HasValue) q = q.Where(r => r.Number == screwNumber.Value);
+            if (lockPassed.HasValue) q = q.Where(r => r.LockPassed == lockPassed.Value);
+
+            var list = await q.OrderBy(r => r.Timestamp, OrderByType.Desc).ToListAsync();
+            return list;
+        }
     }
 }

+ 14 - 0
TeamAAS-VM/Events/MainTabSwitchNotification.cs

@@ -0,0 +1,14 @@
+using Prism.Events;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using TeamAAS_VP.Models.Robot;
+
+namespace TeamAAS_VP.Events
+{
+    public class MainTabSwitchNotification : PubSubEvent<int>
+    {
+    }
+}

+ 33 - 0
TeamAAS-VM/Interfaces/ISystemDatabaseService.cs

@@ -284,5 +284,38 @@ namespace TeamAAS_VP.Interfaces
         /// <param name="source">可选的报警来源过滤,传入 null 表示不过滤。</param>
         /// <returns>满足条件的报警记录集合。</returns>
         Task<IEnumerable<Models.AlarmRecord>> QueryAlarmsAsync(DateTime start, DateTime end, string code = null, string source = null);
+
+        // Lock results
+
+        /// <summary>
+        /// 记录单次锁付结果(波形数据 WaveDatas 不存入数据库)
+        /// </summary>
+        /// <param name="result">要存储的 LockResult 对象</param>
+        Task RecordLockResultAsync(LockResult result);
+
+        /// <summary>
+        /// 分页查询锁付结果。
+        /// 返回符合时间范围及可选过滤条件的记录集合和总记录数。
+        /// </summary>
+        /// <param name="start">起始时间(含)</param>
+        /// <param name="end">结束时间(不含)</param>
+        /// <param name="pageIndex">页号(从 1 开始)</param>
+        /// <param name="pageSize">每页大小</param>
+        /// <param name="productNumber">可选的产品编号过滤</param>
+        /// <param name="screwNumber">可选的螺丝编号(Number)过滤</param>
+        /// <param name="lockPassed">可选的是否通过过滤</param>
+        /// <returns>返回一个元组,包含记录集合和总记录数</returns>
+        Task<(IEnumerable<LockResult> Items, int TotalCount)> QueryLockResultsAsync(DateTime start, DateTime end, int pageIndex, int pageSize, string productNumber = null, int? screwNumber = null, bool? lockPassed = null);
+
+        /// <summary>
+        /// 查询所有符合条件的锁付结果(不分页)。
+        /// </summary>
+        /// <param name="start">起始时间(含)</param>
+        /// <param name="end">结束时间(不含)</param>
+        /// <param name="productNumber">可选的产品编号过滤</param>
+        /// <param name="screwNumber">可选的螺丝编号过滤</param>
+        /// <param name="lockPassed">可选的是否通过过滤</param>
+        /// <returns>符合条件的锁付结果集合。</returns>
+        Task<IEnumerable<LockResult>> QueryAllLockResultsAsync(DateTime start, DateTime end, string productNumber = null, int? screwNumber = null, bool? lockPassed = null);
     }
 }

+ 39 - 2
TeamAAS-VM/Models/LockResult.cs

@@ -2,11 +2,15 @@
 using System;
 using System.Collections.Generic;
 using static TeamAAS_VP.Core.ScrewDriver.XYD_ScrewDriver;
+using SqlSugar;
 
 namespace TeamAAS_VP.Models
 {
     public class LockResult : BindableBase
     {
+        [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
+        public int Id { get; set; }
+
         private int __Number;
         /// <summary>
         /// 螺丝编号
@@ -226,9 +230,20 @@ namespace TeamAAS_VP.Models
             set { SetProperty(ref _OperatorName, value); }
         }
 
+        private string _ProductImagePath;
+        /// <summary>
+        /// 产品图片路径
+        /// </summary>
+        public string ProductImagePath
+        {
+            get { return _ProductImagePath; }
+            set { SetProperty(ref _ProductImagePath, value); }
+        }
+
         /// <summary>
         /// 锁付曲线数据列表
         /// </summary>
+        [SugarColumn(IsIgnore = true)]
         public List<WaveData> WaveDatas { get; set; } = new List<WaveData>();
 
         public LockResult()
@@ -237,7 +252,7 @@ namespace TeamAAS_VP.Models
             Timestamp = DateTime.Now;
         }
 
-        public LockResult(int number, string productNumber, float lockTurns, float lockTorque,bool lockPassed)
+        public LockResult(int number, string productNumber, float lockTurns, float lockTorque, bool lockPassed)
         {
             Number = number;
             ProductNumber = productNumber;
@@ -270,10 +285,32 @@ namespace TeamAAS_VP.Models
                 EndPositionZ = this.EndPositionZ,
                 LockDescentSpeed = this.LockDescentSpeed,
                 FeederNumber = this.FeederNumber,
-                NozzleNumber= this.NozzleNumber,
+                NozzleNumber = this.NozzleNumber,
                 WaveDatas = new List<WaveData>(this.WaveDatas)
             };
         }
 
+        /// <summary>
+        /// 将当前的锁付结果结果及波形数据存储至.csv文件中
+        /// </summary>
+        /// <param name="filePath"></param>
+        public void SaveToCsv(string filePath)
+        {
+            using (var writer = new System.IO.StreamWriter(filePath))
+            {
+                // 写入锁付结果信息
+                writer.WriteLine("Number,ProductNumber,Timestamp,LockTurns,LockTorque,LockDuration,LockPassed,ScrewdriverProgramNumber,Pressure,TargetTorque,TargetTurnsUpperLimit,TargetTurnsLowerLimit,TargetLockSpeed,StartPositionX,StartPositionY,StartPositionZ,EndPositionZ,LockDescentSpeed,FeederNumber,NozzleNumber");
+                writer.WriteLine($"{Number},{ProductNumber},{Timestamp},{LockTurns},{LockTorque},{LockDuration},{LockPassed},{ScrewdriverProgramNumber},{Pressure},{TargetTorque},{TargetTurnsUpperLimit},{TargetTurnsLowerLimit},{TargetLockSpeed},{StartPositionX},{StartPositionY},{StartPositionZ},{EndPositionZ},{LockDescentSpeed},{FeederNumber},{NozzleNumber}");
+                // 写入波形数据
+                writer.WriteLine();
+                writer.WriteLine("WaveDataIndex,LockAngle,Torque,Turns,Speed,Slopes");
+                for (int i = 0; i < WaveDatas.Count; i++)
+                {
+                    var waveData = WaveDatas[i];
+                    writer.WriteLine($"{i},{waveData.LockAngle},{waveData.Torque},{waveData.Turns},{waveData.Speed},{waveData.Slopes}");
+                }
+            }
+        }
+
     }
 }

+ 9 - 0
TeamAAS-VM/TeamAAS-VP.csproj

@@ -526,6 +526,8 @@
     <Compile Include="Controls\FeederSystemParam.xaml.cs">
       <DependentUpon>FeederSystemParam.xaml</DependentUpon>
     </Compile>
+    <Compile Include="Events\MainTabSwitchNotification.cs" />
+    <Compile Include="ViewModels\Statistics\LockResultRecoredQueryViewModel.cs" />
     <Compile Include="Views\DebugMod\FocusAnalyzerl.xaml.cs">
       <DependentUpon>FocusAnalyzerl.xaml</DependentUpon>
     </Compile>
@@ -1023,6 +1025,9 @@
     <Compile Include="Views\Statistics\AlarmQuery.xaml.cs">
       <DependentUpon>AlarmQuery.xaml</DependentUpon>
     </Compile>
+    <Compile Include="Views\Statistics\LockResultRecoredQuery.xaml.cs">
+      <DependentUpon>LockResultRecoredQuery.xaml</DependentUpon>
+    </Compile>
     <Compile Include="Views\Statistics\ProductionStatement.xaml.cs">
       <DependentUpon>ProductionStatement.xaml</DependentUpon>
     </Compile>
@@ -1379,6 +1384,10 @@
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>
     </Page>
+    <Page Include="Views\Statistics\LockResultRecoredQuery.xaml">
+      <SubType>Designer</SubType>
+      <Generator>MSBuild:Compile</Generator>
+    </Page>
     <Page Include="Views\Statistics\ProductionStatement.xaml">
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>

+ 113 - 5
TeamAAS-VM/ViewModels/DebugMod/FocusAnalyzerViewModel.cs

@@ -1,4 +1,5 @@
-using OpenCvSharp;
+using Cognex.VisionPro;
+using OpenCvSharp;
 using OpenCvSharp.WpfExtensions;
 using OxyPlot;
 using OxyPlot.Axes;
@@ -15,6 +16,7 @@ using System.Collections.ObjectModel;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
+using System.Windows.Forms;
 using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
@@ -22,8 +24,10 @@ using System.Windows.Threading;
 using TeamAAS_VP.Core;
 using TeamAAS_VP.Core.Lights;
 using TeamAAS_VP.Enums;
+using TeamAAS_VP.Events;
 using TeamAAS_VP.Interfaces;
 using TeamAAS_VP.Models;
+using TeamAAS_VP.Resources.Languages;
 using TeamAAS_VP.Services;
 using static TeamAAS_VP.ViewModels.DebugMod.LightManualViewModel;
 
@@ -187,6 +191,48 @@ namespace TeamAAS_VP.ViewModels.DebugMod
         /// </summary>
         public ObservableCollection<AnalysisMode> AnalysisModes { get; }
 
+        private ObservableCollection<CameraInfo> _CameraList;
+        public ObservableCollection<CameraInfo> CameraList
+        {
+            get { return _CameraList; }
+            set { SetProperty(ref _CameraList, value); }
+        }
+
+        private CameraInfo _SelectedCamera;
+        public CameraInfo SelectedCamera
+        {
+            get { return _SelectedCamera; }
+            set { SetProperty(ref _SelectedCamera, value);
+                if (value!=null)
+                {
+                    if (Camera != null)
+                    {
+                        Camera.ImageCallbackEvent -= Camera_ImageCallbackEvent;
+                        //Camera.Grab();
+                    }
+                    Camera = _cameraService.GetCamera(value.Id);
+                    Camera.ImageCallbackEvent += Camera_ImageCallbackEvent;
+                    try
+                    {
+                        var image = Camera.Grab();
+                        _currentMat = ImageHelper.ICogImageConvertMat(image);
+                        // 转换为 WPF 可显示的位图并更新 UI 绑定
+                        CurrentImage = _currentMat.ToBitmapSource();
+                    }
+                    catch (Exception)
+                    {
+
+                    }
+                }
+                else
+                {
+                    Camera = null;
+                }
+            }
+        }
+
+        private ICamera Camera;
+
         #endregion
 
         #region 命令
@@ -255,7 +301,7 @@ namespace TeamAAS_VP.ViewModels.DebugMod
 
             // 初始化命令并绑定 CanExecute/ObservesProperty
             LoadImageCommand = new DelegateCommand(OnLoadImage);
-            StartAnalysisCommand = new DelegateCommand(OnStartAnalysis, CanStartAnalysis).ObservesProperty(() => CurrentImage);
+            StartAnalysisCommand = new DelegateCommand(OnStartAnalysis, CanStartAnalysis).ObservesProperty(()=>SelectedCamera).ObservesProperty(() => IsAnalyzing);
             StopAnalysisCommand = new DelegateCommand(OnStopAnalysis, () => IsAnalyzing).ObservesProperty(() => IsAnalyzing);
             ResetCommand = new DelegateCommand(OnReset);
             ExportCommand = new DelegateCommand(OnExport, () => _resultHistory.Count > 0);
@@ -272,6 +318,25 @@ namespace TeamAAS_VP.ViewModels.DebugMod
             _analysisTimer.Tick += OnAnalysisTick;
 
             StatusText = "就绪";
+
+            _eventAggregator.GetEvent<MainTabSwitchNotification>().Subscribe((index) =>
+            {
+                if (index != 2)
+                {
+                    try
+                    {
+                        if (Camera != null)
+                        {
+                            Camera.ImageCallbackEvent -= Camera_ImageCallbackEvent;
+                            SelectedCamera = null;
+                        }
+                    }
+                    catch (Exception)
+                    {
+
+                    }
+                }
+            });
         }
 
         #region 方法
@@ -401,7 +466,8 @@ namespace TeamAAS_VP.ViewModels.DebugMod
         /// <returns>如果可以开始分析返回 true,否则 false。</returns>
         private bool CanStartAnalysis()
         {
-            return CurrentImage != null && !IsAnalyzing;
+            return SelectedCamera != null && !IsAnalyzing;
+            //return CurrentImage != null && !IsAnalyzing;
         }
 
         /// <summary>
@@ -411,6 +477,11 @@ namespace TeamAAS_VP.ViewModels.DebugMod
         /// </summary>
         private void OnStartAnalysis()
         {
+            if (Camera==null)
+            {
+                return;
+            }
+
             IsAnalyzing = true;
             _frameCount = 0;
             _bestSharpness = 0;
@@ -419,10 +490,15 @@ namespace TeamAAS_VP.ViewModels.DebugMod
 
             if (Parameters.EnableRealtimeAnalysis)
             {
-                _analysisTimer.Start();
+                //_analysisTimer.Start();
+                Camera.StartGrabbing();
             }
             else
             {
+                var image = Camera.Grab();
+                _currentMat = ImageHelper.ICogImageConvertMat(image);
+                // 转换为 WPF 可显示的位图并更新 UI 绑定
+                CurrentImage = _currentMat.ToBitmapSource();
                 AnalyzeCurrentImage();
                 IsAnalyzing = false;
                 StatusText = "分析完成";
@@ -434,7 +510,12 @@ namespace TeamAAS_VP.ViewModels.DebugMod
         /// </summary>
         private void OnStopAnalysis()
         {
-            _analysisTimer.Stop();
+            if (Camera == null)
+            {
+                return;
+            }
+            Camera.StopGrabbing();
+            //_analysisTimer.Stop();
             IsAnalyzing = false;
             StatusText = "已停止";
         }
@@ -644,6 +725,23 @@ namespace TeamAAS_VP.ViewModels.DebugMod
             PlotModel.InvalidatePlot(true);
         }
 
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="image"></param>
+        /// <param name="totaltime"></param>
+        /// <param name="errormessage"></param>
+        private void Camera_ImageCallbackEvent(ICogImage image, TimeSpan totaltime, string errormessage)
+        {
+            _currentMat= ImageHelper.ICogImageConvertMat(image);
+            App.Current.Dispatcher.Invoke(() =>
+            {
+                // 转换为 WPF 可显示的位图并更新 UI 绑定
+                CurrentImage = _currentMat.ToBitmapSource();
+            });
+            
+            AnalyzeCurrentImage();
+        }
         #endregion
 
         #region 继承
@@ -663,6 +761,7 @@ namespace TeamAAS_VP.ViewModels.DebugMod
         public async void OnNavigatedTo(NavigationContext navigationContext)
         {
             // TODO: 需要时从灯管理服务加载通道或其他初始化逻辑
+            CameraList = new ObservableCollection<CameraInfo>(_configService.GetAllCameras());
         }
 
         /// <summary>
@@ -680,6 +779,15 @@ namespace TeamAAS_VP.ViewModels.DebugMod
         public void OnNavigatedFrom(NavigationContext navigationContext)
         {
             // nothing
+            if (Camera != null)
+            {
+                if (IsAnalyzing)
+                {
+                    OnStopAnalysis();
+                }
+                Camera.ImageCallbackEvent -= Camera_ImageCallbackEvent;
+                //Camera.Grab();
+            }
         }
         #endregion
     }

+ 17 - 0
TeamAAS-VM/ViewModels/MainWindowViewModel.cs

@@ -163,37 +163,54 @@ namespace TeamAAS_VP.ViewModels
         }
 
         #region 方法
+        //上一次导航的名称
+        string lastNavigationName = string.Empty;
         private void NavigationVoid(string obj)
         {
+            if(!string.IsNullOrEmpty(lastNavigationName))
+            {
+                if (lastNavigationName == obj)
+                {
+                    return;
+                }
+            }
+            lastNavigationName = obj;
             switch (obj)
             {
                 case "Home":
                     _regionManager.RequestNavigate("MainRegionContext", "HomeView");
                     NavigationIndex = 0;
+                    _eventAggregator.GetEvent<MainTabSwitchNotification>().Publish(NavigationIndex);
                     break;
                 case "Products":
                     _regionManager.RequestNavigate("MainRegionContext", "ProductsView");
                     NavigationIndex = 1;
+                    _eventAggregator.GetEvent<MainTabSwitchNotification>().Publish(NavigationIndex);
                     break;
                 case "Debug":
                     _regionManager.RequestNavigate("MainRegionContext", "DebugView");
                     NavigationIndex = 2;
+                    _eventAggregator.GetEvent<MainTabSwitchNotification>().Publish(NavigationIndex);
                     break;
                 case "Setting":
                     _regionManager.RequestNavigate("MainRegionContext", "SettingView");
                     NavigationIndex = 3;
+                    _eventAggregator.GetEvent<MainTabSwitchNotification>().Publish(NavigationIndex);
                     break;
                 case "Calib":
                     _regionManager.RequestNavigate("MainRegionContext", "CalibrationView");
                     NavigationIndex = 5;
+                    _eventAggregator.GetEvent<MainTabSwitchNotification>().Publish(NavigationIndex);
                     break;
                 case "Statistics":
                     _regionManager.RequestNavigate("MainRegionContext", "StatisticsView");
                     NavigationIndex = 6;
+                    _eventAggregator.GetEvent<MainTabSwitchNotification>().Publish(NavigationIndex);
                     break;
                 case "User":
                     _regionManager.RequestNavigate("MainRegionContext", "UserView");
                     NavigationIndex = 7;
+                    _eventAggregator.GetEvent<MainTabSwitchNotification>().Publish(NavigationIndex);
                     break;
                 default:
                     break;

+ 268 - 0
TeamAAS-VM/ViewModels/Statistics/LockResultRecoredQueryViewModel.cs

@@ -0,0 +1,268 @@
+using Prism.Commands;
+using Prism.Mvvm;
+using System;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using TeamAAS_VP.Interfaces;
+using TeamAAS_VP.Models;
+using TeamAAS_VP.Models.Product;
+
+namespace TeamAAS_VP.ViewModels.Statistics
+{
+    public class LockResultRecoredQueryViewModel : BindableBase
+    {
+        private readonly IProductService _productService;
+        private readonly ISystemDatabaseService _systemDatabaseService;
+
+        public LockResultRecoredQueryViewModel(IProductService productService, ISystemDatabaseService systemDatabaseService)
+        {
+            _productService = productService;
+            _systemDatabaseService = systemDatabaseService;
+
+            PageSize = 20;
+            PageIndex = 1;
+
+            SearchCommand = new DelegateCommand(async () => await ExecuteSearchCommand(), CanSearch).ObservesProperty(() => SelectedProduct).ObservesProperty(() => StartDate).ObservesProperty(() => EndDate).ObservesProperty(() => IsSearching);
+            PrevPageCommand = new DelegateCommand(async () => { if (PageIndex>1) { PageIndex--; await ExecuteSearchCommand(); } }).ObservesProperty(() => PageIndex);
+            NextPageCommand = new DelegateCommand(async () => { if (PageIndex * PageSize < TotalCount) { PageIndex++; await ExecuteSearchCommand(); } }).ObservesProperty(() => PageIndex).ObservesProperty(() => TotalCount);
+            ExportCommand = new DelegateCommand(async () => await ExecuteExportCommand(), CanExport).ObservesProperty(() => Results).ObservesProperty(() => IsSearching);
+
+            AllProduct = new ObservableCollection<ProductModel>(_productService.GetAllProducts());
+            StartDate = DateTime.Now.Date;
+            EndDate = DateTime.Now.Date.AddDays(1);
+        }
+
+        #region Properties
+        private ObservableCollection<ProductModel> _AllProduct;
+        public ObservableCollection<ProductModel> AllProduct
+        {
+            get { return _AllProduct; }
+            set { SetProperty(ref _AllProduct, value); }
+        }
+
+        private ProductModel _SelectedProduct;
+        public ProductModel SelectedProduct
+        {
+            get { return _SelectedProduct; }
+            set { SetProperty(ref _SelectedProduct, value); }
+        }
+
+        private DateTime? _StartDate;
+        public DateTime? StartDate
+        {
+            get { return _StartDate; }
+            set { SetProperty(ref _StartDate, value); }
+        }
+
+        private DateTime? _EndDate;
+        public DateTime? EndDate
+        {
+            get { return _EndDate; }
+            set { SetProperty(ref _EndDate, value); }
+        }
+
+        private int? _ScrewNumber;
+        public int? ScrewNumber
+        {
+            get { return _ScrewNumber; }
+            set { SetProperty(ref _ScrewNumber, value); }
+        }
+
+        private string _ProductNumber;
+        public string ProductNumber
+        {
+            get { return _ProductNumber; }
+            set { SetProperty(ref _ProductNumber, value); }
+        }
+
+        private ObservableCollection<LockResult> _Results = new ObservableCollection<LockResult>();
+        public ObservableCollection<LockResult> Results
+        {
+            get { return _Results; }
+            set { SetProperty(ref _Results, value); }
+        }
+
+        private int _PageIndex;
+        public int PageIndex
+        {
+            get { return _PageIndex; }
+            set { SetProperty(ref _PageIndex, value); }
+        }
+
+        private int _PageSize;
+        public int PageSize
+        {
+            get { return _PageSize; }
+            set { SetProperty(ref _PageSize, value); }
+        }
+
+        private int _TotalCount;
+        public int TotalCount
+        {
+            get { return _TotalCount; }
+            set { SetProperty(ref _TotalCount, value); }
+        }
+
+        private bool _IsSearching = false;
+        public bool IsSearching
+        {
+            get { return _IsSearching; }
+            set { SetProperty(ref _IsSearching, value); }
+        }
+        #endregion
+
+        #region Commands
+        public DelegateCommand SearchCommand { get; private set; }
+        public DelegateCommand PrevPageCommand { get; private set; }
+        public DelegateCommand NextPageCommand { get; private set; }
+        public DelegateCommand ExportCommand { get; private set; }
+        #endregion
+
+        private bool CanSearch()
+        {
+            if (IsSearching) return false;
+            if (SelectedProduct == null && string.IsNullOrWhiteSpace(ProductNumber) && !ScrewNumber.HasValue)
+                return false;
+            if (!StartDate.HasValue || !EndDate.HasValue) return false;
+            return true;
+        }
+
+        private bool CanExport()
+        {
+            // allow export even if current page is empty; user may want to export all matching records
+            return !IsSearching && StartDate.HasValue && EndDate.HasValue && (SelectedProduct != null || !string.IsNullOrWhiteSpace(ProductNumber) || ScrewNumber.HasValue);
+        }
+
+        private async Task ExecuteSearchCommand()
+        {
+            try
+            {
+                IsSearching = true;
+
+                // determine time range (use full days)
+                var start = StartDate.HasValue ? StartDate.Value.Date : DateTime.MinValue;
+                var end = EndDate.HasValue ? EndDate.Value.Date.AddDays(1) : DateTime.MaxValue;
+
+                string productNumberFilter = null;
+                if (!string.IsNullOrWhiteSpace(ProductNumber)) productNumberFilter = ProductNumber.Trim();
+                // if SelectedProduct is selected, use its Name as productNumber? The user requested select product name then time range; productNumber precise search should still be supported
+                if (SelectedProduct != null && string.IsNullOrEmpty(productNumberFilter))
+                {
+                    // assume SelectedProduct.Name correspond to ProductNumber field used when storing
+                    productNumberFilter = SelectedProduct.Name;
+                }
+
+                int pageIndex = PageIndex < 1 ? 1 : PageIndex;
+                int pageSize = PageSize < 1 ? 20 : PageSize;
+
+                var res = await _systemDatabaseService.QueryLockResultsAsync(start, end, pageIndex, pageSize, productNumberFilter, ScrewNumber);
+                Results = new ObservableCollection<LockResult>(res.Items);
+                TotalCount = res.TotalCount;
+            }
+            catch (Exception ex)
+            {
+                MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+            }
+            finally
+            {
+                IsSearching = false;
+            }
+        }
+
+        private async Task ExecuteExportCommand()
+        {
+            try
+            {
+                IsSearching = true;
+
+                // determine time range
+                var start = StartDate.HasValue ? StartDate.Value.Date : DateTime.MinValue;
+                var end = EndDate.HasValue ? EndDate.Value.Date.AddDays(1) : DateTime.MaxValue;
+
+                string productNumberFilter = null;
+                if (!string.IsNullOrWhiteSpace(ProductNumber)) productNumberFilter = ProductNumber.Trim();
+                if (SelectedProduct != null && string.IsNullOrEmpty(productNumberFilter))
+                {
+                    productNumberFilter = SelectedProduct.Name;
+                }
+
+                // fetch all matching records (not paged)
+                var all = await _systemDatabaseService.QueryAllLockResultsAsync(start, end, productNumberFilter, ScrewNumber);
+                if (all == null || !all.Any())
+                {
+                    MessageBox.Show("没有找到符合条件的记录。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
+                    return;
+                }
+
+                var saveFileDialog = new Microsoft.Win32.SaveFileDialog
+                {
+                    FileName = $"LockResults_{DateTime.Now.ToString("yyyyMMddHHmmss")}.csv",
+                    DefaultExt = ".csv",
+                    Filter = "CSV Files (*.csv)|*.csv"
+                };
+                var result = saveFileDialog.ShowDialog();
+                if (result != true) return;
+                var path = saveFileDialog.FileName;
+
+                // write in streaming manner to support large sets
+                using (var sw = new StreamWriter(path, false, Encoding.UTF8))
+                {
+                    sw.WriteLine("Id,Number,ProductNumber,Timestamp,LockTurns,LockTorque,LockDuration,LockPassed,ResultDetails,ScrewdriverProgramNumber,Pressure,TargetTorque,TargetTurnsUpperLimit,TargetTurnsLowerLimit,TargetLockSpeed,StartPositionX,StartPositionY,StartPositionZ,EndPositionZ,LockDescentSpeed,FeederNumber,NozzleNumber,OperatorName,ProductImagePath");
+                    foreach (var r in all)
+                    {
+                        var line = string.Join(",",
+                            EscapeCsv(r.Id.ToString()),
+                            EscapeCsv(r.Number.ToString()),
+                            EscapeCsv(r.ProductNumber),
+                            EscapeCsv(r.Timestamp.ToString("yyyy-MM-dd HH:mm:ss")),
+                            EscapeCsv(r.LockTurns.ToString()),
+                            EscapeCsv(r.LockTorque.ToString()),
+                            EscapeCsv(r.LockDuration.ToString()),
+                            EscapeCsv(r.LockPassed ? "1" : "0"),
+                            EscapeCsv(r.ResultDetails),
+                            EscapeCsv(r.ScrewdriverProgramNumber.ToString()),
+                            EscapeCsv(r.Pressure.ToString()),
+                            EscapeCsv(r.TargetTorque.ToString()),
+                            EscapeCsv(r.TargetTurnsUpperLimit.ToString()),
+                            EscapeCsv(r.TargetTurnsLowerLimit.ToString()),
+                            EscapeCsv(r.TargetLockSpeed.ToString()),
+                            EscapeCsv(r.StartPositionX.ToString()),
+                            EscapeCsv(r.StartPositionY.ToString()),
+                            EscapeCsv(r.StartPositionZ.ToString()),
+                            EscapeCsv(r.EndPositionZ.ToString()),
+                            EscapeCsv(r.LockDescentSpeed.ToString()),
+                            EscapeCsv(r.FeederNumber.ToString()),
+                            EscapeCsv(r.NozzleNumber),
+                            EscapeCsv(r.OperatorName),
+                            EscapeCsv(r.ProductImagePath)
+                        );
+                        sw.WriteLine(line);
+                    }
+                }
+            }
+            catch (Exception ex)
+            {
+                MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+            }
+            finally
+            {
+                IsSearching = false;
+            }
+            await Task.CompletedTask;
+        }
+
+        private string EscapeCsv(string s)
+        {
+            if (s == null) return "";
+            if (s.Contains(",") || s.Contains("\"") || s.Contains("\r") || s.Contains("\n"))
+            {
+                return "\"" + s.Replace("\"", "\"\"") + "\"";
+            }
+            return s;
+        }
+    }
+}

+ 30 - 5
TeamAAS-VM/Views/DebugMod/FocusAnalyzerl.xaml

@@ -140,11 +140,36 @@
                 <Button Content="打开图像"
                         Command="{Binding LoadImageCommand}"
                         Padding="15,5"
-                        Margin="0,0,8,0" />
+                        Margin="0,0,8,0" 
+                        d:Visibility="Visible"
+                        Visibility="Collapsed" />
                 <Button Content="检测棋盘格"
                         Command="{Binding DetectChessboardCommand}"
                         Padding="15,5"
-                        Margin="0,0,8,0" />
+                        Margin="0,0,8,0"
+                        d:Visibility="Visible"
+                        Visibility="Collapsed" />
+                <TextBlock Text="相机列表:"
+                        Margin="0,0,8,0" VerticalAlignment="Center" />
+                <ComboBox ItemsSource="{Binding CameraList}"
+                          SelectedItem="{Binding SelectedCamera}"
+                          MinWidth="100"
+                          Margin="0,0,8,0"
+                          IsEnabled="{Binding IsAnalyzing,Converter={StaticResource InvertBooleanConverter}}">
+                    <ComboBox.ItemTemplate>
+                        <DataTemplate>
+                            <StackPanel Orientation="Horizontal">
+                                <TextBlock Text="{Binding CameraNo}"
+                                           VerticalAlignment="Center" />
+                                <TextBlock Text="."
+                                           Margin="3,0"
+                                           VerticalAlignment="Center" />
+                                <TextBlock Text="{Binding CameraName}"
+                                           VerticalAlignment="Center" />
+                            </StackPanel>
+                        </DataTemplate>
+                    </ComboBox.ItemTemplate>
+                </ComboBox>
                 <Separator Style="{StaticResource {x:Static ToolBar.SeparatorStyleKey}}"
                            Margin="8,0" />
                 <TextBlock Text="{Binding StatusText}"
@@ -231,7 +256,7 @@
                         </Canvas>
 
                         <!-- 无图像提示 -->
-                        <Border Background="#F9F9F9"
+                        <!--<Border Background="#F9F9F9"
                                 BorderBrush="#E0E0E0"
                                 BorderThickness="2"
                                 CornerRadius="8"
@@ -252,7 +277,7 @@
                                         Margin="0,16,0,0"
                                         Padding="20,8" />
                             </StackPanel>
-                        </Border>
+                        </Border>-->
 
                         <!-- 操作提示 -->
                         <Border Background="#80000000"
@@ -332,9 +357,9 @@
                                     Height="30"
                                     Margin="10,0,0,0"
                                     Padding="0"
-                                    Content="&#xE14C;"
                                     ToolTip="清除ROI"
                                     Command="{Binding ClearRoiCommand}">
+                                <materialDesign:PackIcon Kind="ClearCircle" />
                             </Button>
                         </StackPanel>
 

+ 88 - 0
TeamAAS-VM/Views/Statistics/LockResultRecoredQuery.xaml

@@ -0,0 +1,88 @@
+<UserControl x:Class="TeamAAS_VP.Views.Statistics.LockResultRecoredQuery"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:prism="http://prismlibrary.com/"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:vm="clr-namespace:TeamAAS_VP.ViewModels.Statistics"
+             xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
+             xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
+             xmlns:lex="http://wpflocalizeextension.codeplex.com"
+             lex:LocalizeDictionary.DesignCulture="zh-CN"
+             lex:ResxLocalizationProvider.DefaultAssembly="TeamAAS-VP"
+             lex:ResxLocalizationProvider.DefaultDictionary="Lang"
+             prism:ViewModelLocator.AutoWireViewModel="True"
+             HorizontalAlignment="Stretch"
+             VerticalAlignment="Stretch"
+             FontFamily="{DynamicResource DefaultFont}"
+             mc:Ignorable="d"
+             d:DataContext="{d:DesignInstance Type=vm:LockResultRecoredQueryViewModel}"
+             d:Height="600"
+             d:Width="1024"
+             d:Background="White">
+    <Grid Margin="8">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="*" />
+            <RowDefinition Height="Auto" />
+        </Grid.RowDefinitions>
+        
+        <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,0,8">
+            <TextBlock Text="产品:" VerticalAlignment="Center" Margin="0,0,8,0" />
+            <ComboBox Width="200" ItemsSource="{Binding AllProduct}" DisplayMemberPath="Name" SelectedItem="{Binding SelectedProduct}" />
+
+            <TextBlock Text="产品编号:" VerticalAlignment="Center" Margin="16,0,8,0" />
+            <TextBox Width="150" Text="{Binding ProductNumber, UpdateSourceTrigger=PropertyChanged}" />
+
+            <TextBlock Text="螺丝编号:" VerticalAlignment="Center" Margin="16,0,8,0" />
+            <TextBox Width="80" Text="{Binding ScrewNumber, UpdateSourceTrigger=PropertyChanged}" />
+
+            <TextBlock Text="开始:" VerticalAlignment="Center" Margin="16,0,8,0" />
+            <DatePicker SelectedDate="{Binding StartDate}" />
+            <TextBlock Text="结束:" VerticalAlignment="Center" Margin="8,0,8,0" />
+            <DatePicker SelectedDate="{Binding EndDate}" />
+
+            <Button Content="查询" Command="{Binding SearchCommand}" Margin="12,0,0,0" />
+            <Button Content="导出当前页" Command="{Binding ExportCommand}" Margin="6,0,0,0" />
+        </StackPanel>
+
+        <DataGrid Grid.Row="1" ItemsSource="{Binding Results}" AutoGenerateColumns="False" CanUserAddRows="False" IsReadOnly="True">
+            <DataGrid.Columns>
+                <DataGridTextColumn Header="Id" Binding="{Binding Id}" Width="60" />
+                <DataGridTextColumn Header="螺丝编号" Binding="{Binding Number}" Width="80" />
+                <DataGridTextColumn Header="产品编号" Binding="{Binding ProductNumber}" Width="150" />
+                <DataGridTextColumn Header="时间" Binding="{Binding Timestamp}" Width="160" />
+                <DataGridTextColumn Header="扭矩" Binding="{Binding LockTorque}" Width="80" />
+                <DataGridTextColumn Header="圈数" Binding="{Binding LockTurns}" Width="80" />
+                <DataGridTextColumn Header="通过" Binding="{Binding LockPassed}" Width="60" />
+                <DataGridTextColumn Header="供料器" Binding="{Binding FeederNumber}" Width="80" />
+                <DataGridTextColumn Header="披头" Binding="{Binding NozzleNumber}" Width="80" />
+                <DataGridTextColumn Header="结果详情" Binding="{Binding ResultDetails}" Width="*" />
+            </DataGrid.Columns>
+        </DataGrid>
+        <Button Margin="16"
+                HorizontalAlignment="Left"
+                VerticalAlignment="Center"
+                Grid.RowSpan="2"
+                Command="{x:Static materialDesign:Transitioner.MovePreviousCommand}"
+                Style="{StaticResource MaterialDesignFloatingActionMiniButton}">
+            <materialDesign:PackIcon Kind="StepBackward" />
+        </Button>
+        <Button Margin="16"
+                HorizontalAlignment="Right"
+                VerticalAlignment="Center"
+                Grid.RowSpan="2"
+                Command="{x:Static materialDesign:Transitioner.MoveNextCommand}"
+                Style="{StaticResource MaterialDesignFloatingActionSecondaryButton}">
+            <materialDesign:PackIcon Kind="StepForward" />
+        </Button>
+        <StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,8,0,0">
+            <Button Content="上一页" Command="{Binding PrevPageCommand}" Margin="4" />
+            <TextBlock VerticalAlignment="Center" Margin="4">第</TextBlock>
+            <TextBox Width="50" Text="{Binding PageIndex, UpdateSourceTrigger=PropertyChanged}" HorizontalContentAlignment="Center" />
+            <TextBlock VerticalAlignment="Center" Margin="4">页 / 共</TextBlock>
+            <TextBlock VerticalAlignment="Center" Text="{Binding TotalCount}" Margin="4" />
+            <Button Content="下一页" Command="{Binding NextPageCommand}" Margin="4" />
+        </StackPanel>
+    </Grid>
+</UserControl>

+ 15 - 0
TeamAAS-VM/Views/Statistics/LockResultRecoredQuery.xaml.cs

@@ -0,0 +1,15 @@
+using System.Windows.Controls;
+
+namespace TeamAAS_VP.Views.Statistics
+{
+    /// <summary>
+    /// Interaction logic for LockResultRecoredQuery
+    /// </summary>
+    public partial class LockResultRecoredQuery : UserControl
+    {
+        public LockResultRecoredQuery()
+        {
+            InitializeComponent();
+        }
+    }
+}

+ 3 - 0
TeamAAS-VM/Views/StatisticsView.xaml

@@ -25,10 +25,13 @@
             <materialDesign:TransitionerSlide OpeningEffect="{materialDesign:TransitionEffect FadeIn}">
                 <local:ProductionStatement />
             </materialDesign:TransitionerSlide>
+
+            <local:LockResultRecoredQuery />
             
             <local:StatisticalQuery />
 
             <local:AlarmQuery />
+            
         </materialDesign:Transitioner>
     </Grid>
 </UserControl>