Browse Source

新增组装/拍照过程记录及统计查询功能

本次提交实现了组装过程记录(AssemblyRecord)和拍照过程记录(PhotoCaptureRecord)两大数据表及其服务接口,支持自动记录、查询、分页与导出。扩展了生产记录的查询与导出能力,新增了组装、拍照、生产记录的统计查询页面。视觉与组装流程集成了自动数据采集,主界面支持组装结果实时展示。优化了图片保存路径,提升了系统对生产过程的追溯与数据分析能力。
孝锋 徐 7 months ago
parent
commit
eb2f8cb8e7
31 changed files with 1983 additions and 105 deletions
  1. 64 27
      TeamAAS-VM/Core/Management.cs
  2. 26 10
      TeamAAS-VM/Core/RectangleCenterCalculator.cs
  3. 5 2
      TeamAAS-VM/Core/ScriptHelper.cs
  4. 13 1
      TeamAAS-VM/Data/DatabaseInitializer.cs
  5. 115 0
      TeamAAS-VM/Data/SystemDatabaseService.cs
  6. 5 0
      TeamAAS-VM/Enums/ProcedureSaveImagePathModel.cs
  7. 4 0
      TeamAAS-VM/Events/LockFinishNotification.cs
  8. 3 1
      TeamAAS-VM/Interfaces/IRemoteCommandService.cs
  9. 76 0
      TeamAAS-VM/Interfaces/ISystemDatabaseService.cs
  10. 132 0
      TeamAAS-VM/Models/AssemblyRecord.cs
  11. 87 0
      TeamAAS-VM/Models/PhotoCaptureRecord.cs
  12. 3 0
      TeamAAS-VM/Resources/Languages/Lang.resx
  13. 94 6
      TeamAAS-VM/Services/RemoteCommandService.cs
  14. 26 0
      TeamAAS-VM/TeamAAS-VP.csproj
  15. 29 2
      TeamAAS-VM/ViewModels/HomeViewModel.cs
  16. 4 4
      TeamAAS-VM/ViewModels/Product/PlcPointParamsViewModel.cs
  17. 260 0
      TeamAAS-VM/ViewModels/Statistics/AssemblyRecordQueryViewModel.cs
  18. 1 1
      TeamAAS-VM/ViewModels/Statistics/LockResultRecoredQueryViewModel.cs
  19. 191 0
      TeamAAS-VM/ViewModels/Statistics/PhotoCaptureQueryViewModel.cs
  20. 167 0
      TeamAAS-VM/ViewModels/Statistics/ProductionRecordQueryViewModel.cs
  21. 19 15
      TeamAAS-VM/ViewModels/Statistics/ProductionStatementViewModel.cs
  22. 64 10
      TeamAAS-VM/Views/Home/ShowVisionRender.xaml.cs
  23. 15 19
      TeamAAS-VM/Views/HomeView.xaml
  24. 164 0
      TeamAAS-VM/Views/Statistics/AssemblyRecordQuery.xaml
  25. 15 0
      TeamAAS-VM/Views/Statistics/AssemblyRecordQuery.xaml.cs
  26. 7 5
      TeamAAS-VM/Views/Statistics/LockResultRecoredQuery.xaml
  27. 190 0
      TeamAAS-VM/Views/Statistics/PhotoCaptureQuery.xaml
  28. 28 0
      TeamAAS-VM/Views/Statistics/PhotoCaptureQuery.xaml.cs
  29. 142 0
      TeamAAS-VM/Views/Statistics/ProductionRecordQuery.xaml
  30. 28 0
      TeamAAS-VM/Views/Statistics/ProductionRecordQuery.xaml.cs
  31. 6 2
      TeamAAS-VM/Views/StatisticsView.xaml

+ 64 - 27
TeamAAS-VM/Core/Management.cs

@@ -120,17 +120,6 @@ namespace TeamAAS_VP.Core
             set { SetProperty(ref _Renders, value); }
         }
 
-        private ProductModel _CurrentProduct;
-
-        /// <summary>
-        /// 当前产品
-        /// </summary>
-        public ProductModel CurrentProduct
-        {
-            get { return _CurrentProduct; }
-            set { SetProperty(ref _CurrentProduct, value); }
-        }
-
         public BgCommunicate BgCommunicate { get; private set; }
 
         public BgModbusTcpCommunicate BgModbusTcpCommunicate { get; private set; }
@@ -1093,6 +1082,18 @@ namespace TeamAAS_VP.Core
         /// 移动向下相机拍照结果
         /// </summary>
         OpenCvSharp.Point3f[] UpCameraPutResults = new OpenCvSharp.Point3f[10];
+        /// <summary>
+        /// 取料移动相机拍照结果
+        /// </summary>
+        OpenCvSharp.Point3f UpCameraPickResult = new OpenCvSharp.Point3f();
+        /// <summary>
+        /// 最终组装位置
+        /// </summary>
+        OpenCvSharp.Point3f PlaceActualCoord = new OpenCvSharp.Point3f();
+        /// <summary>
+        /// 零件SN
+        /// </summary>
+        string PartSN = "";
 
         /// <summary>
         /// 二次定位相机的工具坐标
@@ -1197,7 +1198,7 @@ namespace TeamAAS_VP.Core
                                     }
                                     // 执行视觉任务
                                     var _cts = new CancellationTokenSource();
-                                    var visionResult = await _remoteCommandService.ExecutePhotoGetSinglePoint(process, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token);
+                                    var visionResult = await _remoteCommandService.ExecutePhotoGetSinglePoint(process, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token, cameraIndex, null);
                                     if (isDryMode)
                                     {
                                         await plc.WriteNodeAsync(addressConfig.Out_DownCameraStatus.Address, (Int16)1);
@@ -1239,8 +1240,8 @@ namespace TeamAAS_VP.Core
                                     }
                                     // 执行视觉任务
                                     var _cts = new CancellationTokenSource();
-                                    var task1 = _remoteCommandService.ExecutePhotoGetSinglePoint(process1, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token);
-                                    var task2 = _remoteCommandService.ExecutePhotoGetSinglePoint(process2, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token);
+                                    var task1 = _remoteCommandService.ExecutePhotoGetSinglePoint(process1, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token, 1, null);
+                                    var task2 = _remoteCommandService.ExecutePhotoGetSinglePoint(process2, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token, 2, null);
                                     var visionResults = await Task.WhenAll(task1, task2);
                                     if (isDryMode)
                                     {
@@ -1331,8 +1332,8 @@ namespace TeamAAS_VP.Core
                                     }
                                     // 执行视觉任务
                                     var _cts = new CancellationTokenSource();
-                                    var task1 = _remoteCommandService.ExecutePhotoGetSinglePoint(process1, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token);
-                                    var task2 = _remoteCommandService.ExecutePhotoGetSinglePoint(process2, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token);
+                                    var task1 = _remoteCommandService.ExecutePhotoGetSinglePoint(process1, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token, 1, null);
+                                    var task2 = _remoteCommandService.ExecutePhotoGetSinglePoint(process2, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token, 2, null);
                                     var visionResults = await Task.WhenAll(task1, task2);
                                     if (isDryMode)
                                     {
@@ -1483,7 +1484,7 @@ namespace TeamAAS_VP.Core
                                 }
                                 // 执行视觉任务
                                 var _cts = new CancellationTokenSource();
-                                var visionResult = await _remoteCommandService.ExecutePhotoGetSinglePoint(process, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token);
+                                var visionResult = await _remoteCommandService.ExecutePhotoGetSinglePoint(process, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token, cameraIndex, null);
                                 if (visionResult.IsSucceed)
                                 {
                                     currentProduct.PickPoints[0].X_Position = (float)visionResult.X;
@@ -1519,8 +1520,8 @@ namespace TeamAAS_VP.Core
                                 }
                                 // 执行视觉任务
                                 var _cts = new CancellationTokenSource();
-                                var task1 = _remoteCommandService.ExecutePhotoGetSinglePoint(process1, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token);
-                                var task2 = _remoteCommandService.ExecutePhotoGetSinglePoint(process2, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token);
+                                var task1 = _remoteCommandService.ExecutePhotoGetSinglePoint(process1, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token, 1, null);
+                                var task2 = _remoteCommandService.ExecutePhotoGetSinglePoint(process2, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token, 2, null);
                                 var visionResults = await Task.WhenAll(task1, task2);
                                 if (visionResults[0].IsSucceed && visionResults[1].IsSucceed)
                                 {
@@ -1611,7 +1612,7 @@ namespace TeamAAS_VP.Core
 
                                 // 执行视觉任务
                                 var _cts = new CancellationTokenSource();
-                                var visionResult = await _remoteCommandService.ExecutePhotoGetSinglePoint(process, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token);
+                                var visionResult = await _remoteCommandService.ExecutePhotoGetSinglePoint(process, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token, cameraIndex, null);
                                 if (isDryMode)
                                 {
                                     await plc.WriteNodeAsync(addressConfig.Out_UpCameraPutStatus.Address, (Int16)1);
@@ -1650,8 +1651,8 @@ namespace TeamAAS_VP.Core
                                 }
                                 // 执行视觉任务
                                 var _cts = new CancellationTokenSource();
-                                var task1 = _remoteCommandService.ExecutePhotoGetSinglePoint(process1, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token);
-                                var task2 = _remoteCommandService.ExecutePhotoGetSinglePoint(process2, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token);
+                                var task1 = _remoteCommandService.ExecutePhotoGetSinglePoint(process1, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token, 1, null);
+                                var task2 = _remoteCommandService.ExecutePhotoGetSinglePoint(process2, null, new double[] { currentPosition.X, currentPosition.Y, currentPosition.U }, _cts.Token, 2, null);
                                 var visionResults = await Task.WhenAll(task1, task2);
                                 if (isDryMode)
                                 {
@@ -1855,6 +1856,7 @@ namespace TeamAAS_VP.Core
                             point.X_Position = (float)p0[0];
                             point.Y_Position = (float)p0[1];
                             point.U_Position = (float)angle;
+                            PlaceActualCoord = new OpenCvSharp.Point3f(point.X_Position, point.Y_Position, point.U_Position);
                             SendTaskMessage($"点:1,{point.X_Position:F3},{point.Y_Position:F4},{point.U_Position:F4}", MessageLevel.Info);
                             await point.WriteToPlcAddress(addressConfig.Out_Screw, plc);
                             await plc.WriteNodeAsync(addressConfig.Out_WorkNum.Address, (Int16)1);
@@ -1945,16 +1947,40 @@ namespace TeamAAS_VP.Core
                                     SendTaskMessage($"收到{i}上传本站数据请求...", MessageLevel.Debug);
 
                                     //这里缺少上传MES的本站数据结果及开始和结束时间,需要补充
+                                    double ct = DateTime.UtcNow.Subtract(_LastMesCheckTime[i]).TotalSeconds;
                                     if (i == 0)
                                     {
-                                        CTtime = DateTime.UtcNow.Subtract(_LastMesCheckTime[i]).TotalSeconds;
+                                        CTtime = ct;
                                     }
 
                                     //读取产品结果
                                     Int16 resultIndex = plc.ReadNode<Int16>(string.Format(addressConfig.In_Result.Address, i));
+                                    //读取压力值
+                                    float pressure = plc.ReadNode<float>(string.Format(addressConfig.In_PressureValue.Address, i));
+                                    AssemblyRecord assemblyRecord = new AssemblyRecord();
+                                    assemblyRecord.ProductSN = code;
+                                    assemblyRecord.RecipeName = currentProduct.Name;
+                                    assemblyRecord.PartSN = PartSN;
+                                    assemblyRecord.AssemblyDurationSeconds = ct;
+                                    assemblyRecord.AssemblyPressure = pressure;
+                                    assemblyRecord.DeviceId = i;
+                                    assemblyRecord.DeviceName = $"";
+                                    assemblyRecord.OperatorName = _systemDatabaseService.GetCurrentUser().UserName;
+                                    assemblyRecord.Success = resultIndex == 1 ? true : false;
+                                    assemblyRecord.DownCameraCoord1 = "";
+                                    assemblyRecord.DownCameraCoord2 = "";
+                                    assemblyRecord.DownCameraCoord3 = "";
+                                    assemblyRecord.DownCameraCoord4 = "";
+                                    assemblyRecord.PickMoveCameraCoord = "";
+                                    assemblyRecord.PlaceMoveCameraCoord1 = "";
+                                    assemblyRecord.PlaceMoveCameraCoord2 = "";
+                                    assemblyRecord.PlaceMoveCameraCoord3 = "";
+                                    assemblyRecord.PlaceMoveCameraCoord4 = "";
+                                    assemblyRecord.PlaceActualCoord = "";
 
                                     //目前只记录当前站
                                     if (i == 0)
+                                    {
                                         await _systemDatabaseService.RecordProductionAsync(new ProductionRecord()
                                         {
                                             ProductName = currentProduct.Name,
@@ -1965,6 +1991,17 @@ namespace TeamAAS_VP.Core
                                             Quantity = 1,
                                             Remark = $"TotalSeconds:{DateTime.UtcNow.Subtract(_LastMesCheckTime[i]).TotalSeconds.ToString("F2")}",
                                         });
+                                        assemblyRecord.DownCameraCoord1 = $"{DowmCameraResults[1].X:F3},{DowmCameraResults[1].Y:F3},{DowmCameraResults[1].Z:F3}";
+                                        assemblyRecord.PlaceMoveCameraCoord1 = $"{UpCameraPutResults[1].X:F3},{UpCameraPutResults[1].Y:F3},{UpCameraPutResults[1].Z:F3}";
+                                        assemblyRecord.PlaceMoveCameraCoord2 = $"{UpCameraPutResults[2].X:F3},{UpCameraPutResults[2].Y:F3},{UpCameraPutResults[2].Z:F3}";
+                                        assemblyRecord.PlaceActualCoord = $"{PlaceActualCoord.X:F3},{PlaceActualCoord.Y:F3},{PlaceActualCoord.Z:F3}";
+                                        App.Current.Dispatcher.Invoke(() =>
+                                        {
+                                            _eventAggregator.GetEvent<AssemblyFinishNotification>().Publish(assemblyRecord);
+                                        });
+                                    }
+
+                                    await _systemDatabaseService.RecordAssemblyAsync(assemblyRecord);
 
                                     (bool isSuccess, string response) = await _mesService.SubmitGetAsync(code, resultIndex == 1 ? true : false, _LastMesCheckTime[i], DateTime.UtcNow);
                                     //假设上传成功,这里缺少上传失败的处理
@@ -2276,12 +2313,12 @@ namespace TeamAAS_VP.Core
         {
             try
             {
-                if (CurrentProduct == null) return;
                 //获取当前产品
                 var currentproduct = _productService.GetCurrentProduct();
-                TotalQuantity = await _systemDatabaseService.GetOverallProductionAsync(CurrentProduct.Name);
-                TotalQuantityToday = await _systemDatabaseService.GetTodayProductionAsync(CurrentProduct.Name);
-                CurrentUPH = await _systemDatabaseService.GetLastHourProductionAsync(CurrentProduct.Name);
+                if (currentproduct == null) return;
+                TotalQuantity = await _systemDatabaseService.GetOverallProductionAsync(currentproduct.Name);
+                TotalQuantityToday = await _systemDatabaseService.GetTodayProductionAsync(currentproduct.Name);
+                CurrentUPH = await _systemDatabaseService.GetLastHourProductionAsync(currentproduct.Name);
             }
             catch (Exception)
             {

+ 26 - 10
TeamAAS-VM/Core/RectangleCenterCalculator.cs

@@ -168,10 +168,11 @@ namespace TeamAAS_VP.Core
             // 确保短轴与长轴垂直(处理非正交情况)
             minorAxis = (minorAxis - majorAxis * minorAxis.DotProduct(majorAxis)).Normalize(2);
 
+            int dim = orderedCorners[0].Count;
             // 计算中心点(四个角点的平均)
             var center = Vector<double>.Build.DenseOfEnumerable(
-                Enumerable.Range(0, 3)
-                    .Select(i => orderedCorners.Select(c => c[i]).Average()));
+                            Enumerable.Range(0, dim)
+                            .Select(i => orderedCorners.Select(c => c[i]).Average()));
 
             // 计算长度和宽度
             double length = Distance(topLeft, topRight);
@@ -205,10 +206,11 @@ namespace TeamAAS_VP.Core
         {
             var orderedCorners = ValidateAndOrderCorners(corners);
 
+            int dim = orderedCorners[0].Count;
             // 步骤1:使用所有点进行PCA得到初步估计
             var matrix = Matrix<double>.Build.DenseOfRows(orderedCorners);
             var mean = Vector<double>.Build.DenseOfEnumerable(
-                Enumerable.Range(0, 3).Select(i => matrix.Column(i).Average()));
+                Enumerable.Range(0, dim).Select(i => matrix.Column(i).Average()));
 
             var centered = matrix.Clone();
             for (int i = 0; i < 4; i++)
@@ -233,7 +235,7 @@ namespace TeamAAS_VP.Core
             Vector<double> bestAxis = eigenvectors.Column(0);
             double bestSimilarity = Math.Abs(actualMajorDirection.DotProduct(bestAxis));
 
-            for (int i = 1; i < 3; i++)
+            for (int i = 1; i < eigenvectors.ColumnCount; i++)
             {
                 var axis = eigenvectors.Column(i);
                 double similarity = Math.Abs(actualMajorDirection.DotProduct(axis));
@@ -337,14 +339,28 @@ namespace TeamAAS_VP.Core
         /// </summary>
         private static Vector<double> CalculatePerpendicularAxis(Vector<double> axis)
         {
-            // 找一个不与axis共线的向量
-            Vector<double> temp;
-            if (Math.Abs(axis[0]) < 0.9)
-                temp = Vector<double>.Build.Dense(new[] { 1.0, 0.0, 0.0 });
-            else
-                temp = Vector<double>.Build.Dense(new[] { 0.0, 1.0, 0.0 });
+            if (axis == null) throw new ArgumentNullException(nameof(axis));
+            int n = axis.Count;
+            if (n == 0) throw new ArgumentException("axis must have positive dimension", nameof(axis));
+
+            // 专门处理2D:(-y, x) 是垂直向量
+            if (n == 2)
+            {
+                var perp2 = Vector<double>.Build.DenseOfArray(new[] { -axis[1], axis[0] });
+                if (perp2.L2Norm() < 1e-12) throw new ArgumentException("axis is zero vector", nameof(axis));
+                return perp2.Normalize(2);
+            }
+
+            // 一般n维:选一个与axis不共线的标准基向量
+            int idx = 0;
+            for (int i = 0; i < n; i++)
+            {
+                if (Math.Abs(axis[i]) < 0.9) { idx = i; break; }
+            }
+            var temp = Vector<double>.Build.Dense(n, i => i == idx ? 1.0 : 0.0);
 
             var perpendicular = temp - axis * temp.DotProduct(axis);
+            if (perpendicular.L2Norm() < 1e-12) throw new InvalidOperationException("failed to compute perpendicular vector");
             return perpendicular.Normalize(2);
         }
 

+ 5 - 2
TeamAAS-VM/Core/ScriptHelper.cs

@@ -13,6 +13,7 @@ using System.Windows.Documents;
 using System.Windows.Forms;
 using TeamAAS_VP.Interfaces;
 using TeamAAS_VP.Models;
+using TeamAAS_VP.Services;
 
 namespace TeamAAS_VP.Core
 {
@@ -113,13 +114,14 @@ namespace TeamAAS_VP.Core
         {
             try
             {
+                IProductService productService = container.Resolve<IProductService>();
                 var eval = CSScript.Evaluator.ReferenceDomainAssemblies(DomainAssemblies.AllStatic);
                 IBackgroundScripting scripting = eval.LoadCode<IBackgroundScripting>(strSourceCode);
                 scripting.regionManager = regionManager;
                 scripting.eventAggregator = ea;
                 scripting.container = container;
                 scripting.management = container.Resolve<Management>();
-                scripting.CurrentProduct = scripting.management.CurrentProduct;
+                scripting.CurrentProduct = productService.GetCurrentProduct();
                 return scripting.Execute();
             }
             catch (CSScriptLib.CompilerException e1)
@@ -160,13 +162,14 @@ namespace TeamAAS_VP.Core
         {
             try
             {
+                IProductService productService = container.Resolve<IProductService>();
                 var eval = CSScript.Evaluator.ReferenceDomainAssemblies(DomainAssemblies.AllStatic);
                 IVisionScripting scripting = eval.LoadCode<IVisionScripting>(strSourceCode);
                 scripting.regionManager = regionManager;
                 scripting.eventAggregator = ea;
                 scripting.container= container;
                 scripting.management= container.Resolve<Management>();
-                scripting.CurrentProduct = scripting.management.CurrentProduct;
+                scripting.CurrentProduct = productService.GetCurrentProduct();
                 await scripting.Execute(procedure, items, rst =>
                 {
                     callback.Invoke(rst);

+ 13 - 1
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), typeof(LockResult));
+                _db.CodeFirst.InitTables(typeof(User), typeof(ProductionRecord), typeof(UserLoginRecord), typeof(AlarmRecord), typeof(LockResult), typeof(PhotoCaptureRecord), typeof(AssemblyRecord));
 
                 // 创建索引
                 CreateIndexes();
@@ -73,6 +73,18 @@ namespace TeamAAS_VP.Data
 
                 // 你可以在这里为其他表添加索引,例如 ProductionRecord 的 Timestamp 或 ProductName
                 try { _db.DbMaintenance.CreateIndex("ProductionRecord", new string[] { "ProductName", "Timestamp" }, "IX_ProductionRecord_ProductName_Timestamp", false); } catch { }
+
+                // Photo capture records indexes
+                try { _db.DbMaintenance.CreateIndex("PhotoCaptureRecord", new string[] { "CaptureTime" }, "IX_PhotoCaptureRecord_CaptureTime", false); } catch { }
+                try { _db.DbMaintenance.CreateIndex("PhotoCaptureRecord", new string[] { "RecipeName" }, "IX_PhotoCaptureRecord_RecipeName", false); } catch { }
+                try { _db.DbMaintenance.CreateIndex("PhotoCaptureRecord", new string[] { "ProductSN" }, "IX_PhotoCaptureRecord_ProductSN", false); } catch { }
+
+                // Assembly records indexes
+                try { _db.DbMaintenance.CreateIndex("AssemblyRecord", new string[] { "Timestamp" }, "IX_AssemblyRecord_Timestamp", false); } catch { }
+                try { _db.DbMaintenance.CreateIndex("AssemblyRecord", new string[] { "RecipeName" }, "IX_AssemblyRecord_RecipeName", false); } catch { }
+                try { _db.DbMaintenance.CreateIndex("AssemblyRecord", new string[] { "ProductSN" }, "IX_AssemblyRecord_ProductSN", false); } catch { }
+                try { _db.DbMaintenance.CreateIndex("AssemblyRecord", new string[] { "PartSN" }, "IX_AssemblyRecord_PartSN", false); } catch { }
+
             }
             catch
             {

+ 115 - 0
TeamAAS-VM/Data/SystemDatabaseService.cs

@@ -588,6 +588,20 @@ namespace TeamAAS_VP.Data
             return dict;
         }
 
+        /// <summary>
+        /// 分页查询生产记录
+        /// </summary>
+        public async Task<(IEnumerable<ProductionRecord> Items, int TotalCount)> QueryProductionRecordsPagedAsync(DateTime start, DateTime end, int pageIndex, int pageSize, string productName = null, string category = null)
+        {
+            if (pageIndex < 1) pageIndex = 1;
+            if (pageSize < 1) pageSize = 20;
+            var q = _db.Queryable<ProductionRecord>().Where(r => r.Timestamp >= start && r.Timestamp < end);
+            if (!string.IsNullOrEmpty(productName)) q = q.Where(r => r.ProductName == productName);
+            if (!string.IsNullOrEmpty(category)) q = q.Where(r => r.Category == category);
+            var total = await q.CountAsync();
+            var list = await q.OrderBy(r => r.Timestamp, OrderByType.Desc).ToPageListAsync(pageIndex, pageSize);
+            return (list, total);
+        }
         #endregion
 
         #region 报警记录
@@ -622,6 +636,107 @@ namespace TeamAAS_VP.Data
 
         #endregion
 
+        #region 拍照记录
+        /// <summary>
+        /// 记录产品拍照结果
+        /// </summary>
+        /// <param name="record"></param>
+        /// <returns></returns>
+        /// <exception cref="ArgumentNullException"></exception>
+        public async Task RecordPhotoCaptureAsync(PhotoCaptureRecord record)
+        {
+            if (record == null) throw new ArgumentNullException(nameof(record));
+            if (record.CaptureTime == default(DateTime)) record.CaptureTime = DateTime.UtcNow;
+            record.Id = record.Id == Guid.Empty ? Guid.NewGuid() : record.Id;
+            await _db.Insertable(record).ExecuteCommandAsync();
+        }
+
+        /// <summary>
+        /// 查询拍照记录,支持按配方名、产品SN和时间段过滤,参数均为可选。
+        /// </summary>
+        /// <param name="recipeName"></param>
+        /// <param name="cameraName"></param>
+        /// <param name="productSN"></param>
+        /// <param name="start"></param>
+        /// <param name="end"></param>
+        /// <returns></returns>
+        public async Task<IEnumerable<PhotoCaptureRecord>> QueryPhotoCapturesAsync(string recipeName = null, string cameraName = null, string productSN = null, DateTime? start = null, DateTime? end = null)
+        {
+            var q = _db.Queryable<PhotoCaptureRecord>();
+            if (!string.IsNullOrEmpty(recipeName)) q = q.Where(r => r.RecipeName == recipeName);
+            if (!string.IsNullOrEmpty(productSN)) q = q.Where(r => r.ProductSN == productSN);
+            if (start.HasValue) q = q.Where(r => r.CaptureTime >= start.Value);
+            if (end.HasValue) q = q.Where(r => r.CaptureTime <= end.Value);
+            var list = await q.OrderBy(r => r.CaptureTime, OrderByType.Desc).ToListAsync();
+            return list;
+        }
+
+        /// <summary>
+        /// 分页查询拍照记录
+        /// </summary>
+        public async Task<(IEnumerable<PhotoCaptureRecord> Items, int TotalCount)> QueryPhotoCapturesPagedAsync(string recipeName = null, string cameraName = null, string productSN = null, DateTime? start = null, DateTime? end = null, int pageIndex = 1, int pageSize = 20)
+        {
+            if (pageIndex < 1) pageIndex = 1;
+            if (pageSize < 1) pageSize = 20;
+            var q = _db.Queryable<PhotoCaptureRecord>();
+            if (!string.IsNullOrEmpty(recipeName)) q = q.Where(r => r.RecipeName == recipeName);
+            if (!string.IsNullOrEmpty(cameraName)) q = q.Where(r => r.CameraName == cameraName);
+            if (!string.IsNullOrEmpty(productSN)) q = q.Where(r => r.ProductSN == productSN);
+            if (start.HasValue) q = q.Where(r => r.CaptureTime >= start.Value);
+            if (end.HasValue) q = q.Where(r => r.CaptureTime <= end.Value);
+            var total = await q.CountAsync();
+            var list = await q.OrderBy(r => r.CaptureTime, OrderByType.Desc).ToPageListAsync(pageIndex, pageSize);
+            return (list, total);
+        }
+        #endregion
+
+        #region 组装过程记录
+
+        /// <summary>
+        /// 记录一次组装过程记录
+        /// </summary>
+        /// <param name="record">组装记录对象</param>
+        public async Task RecordAssemblyAsync(AssemblyRecord record)
+        {
+            if (record == null) throw new ArgumentNullException(nameof(record));
+            if (record.Timestamp == default(DateTime)) record.Timestamp = DateTime.UtcNow;
+            record.Id = record.Id == Guid.Empty ? Guid.NewGuid() : record.Id;
+            await _db.Insertable(record).ExecuteCommandAsync();
+        }
+
+        /// <summary>
+        /// 查询组装记录,支持按配方名、产品序列号和时间范围过滤
+        /// </summary>
+        public async Task<IEnumerable<AssemblyRecord>> QueryAssemblyRecordsAsync(string recipeName = null, string productSN = null, DateTime? start = null, DateTime? end = null)
+        {
+            var q = _db.Queryable<AssemblyRecord>();
+            if (!string.IsNullOrEmpty(recipeName)) q = q.Where(r => r.RecipeName == recipeName);
+            if (!string.IsNullOrEmpty(productSN)) q = q.Where(r => r.ProductSN == productSN);
+            if (start.HasValue) q = q.Where(r => r.Timestamp >= start.Value);
+            if (end.HasValue) q = q.Where(r => r.Timestamp < end.Value);
+            var list = await q.OrderBy(r => r.Timestamp, OrderByType.Desc).ToListAsync();
+            return list;
+        }
+
+        /// <summary>
+        /// 分页查询组装记录
+        /// </summary>
+        public async Task<(IEnumerable<AssemblyRecord> Items, int TotalCount)> QueryAssemblyRecordsPagedAsync(string recipeName = null, string productSN = null, DateTime? start = null, DateTime? end = null, int pageIndex = 1, int pageSize = 20)
+        {
+            if (pageIndex < 1) pageIndex = 1;
+            if (pageSize < 1) pageSize = 20;
+            var q = _db.Queryable<AssemblyRecord>();
+            if (!string.IsNullOrEmpty(recipeName)) q = q.Where(r => r.RecipeName == recipeName);
+            if (!string.IsNullOrEmpty(productSN)) q = q.Where(r => r.ProductSN == productSN);
+            if (start.HasValue) q = q.Where(r => r.Timestamp >= start.Value);
+            if (end.HasValue) q = q.Where(r => r.Timestamp < end.Value);
+            var total = await q.CountAsync();
+            var list = await q.OrderBy(r => r.Timestamp, OrderByType.Desc).ToPageListAsync(pageIndex, pageSize);
+            return (list, total);
+        }
+
+        #endregion
+
         // Lock result methods
         /// <summary>
         /// 记录单次锁付结果(WaveDatas 不写入数据库)

+ 5 - 0
TeamAAS-VM/Enums/ProcedureSaveImagePathModel.cs

@@ -43,5 +43,10 @@ namespace TeamAAS_VP.Enums
         [Description("..//yyyy-MM//MM-dd//NG//yyyy-MM-dd HH_mm_ss.png")]
         [Localization(ResourceName = "ProcedureSaveImagePathModel_Month_Day_Ng_Time", ResourceType = typeof(Lang))]
         Month_Day_Ng_Time = 3,
+
+        //年月/日/配方名/产品SN/产品SN+时间
+        [Description("..//yyyy-MM//MM-dd//产品配方名//产品SN//产品SN-yyyy-MM-dd HH_mm_ss.png")]
+        [Localization(ResourceName = "ProcedureSaveImagePathModel_Month_Day_Formula_ProductSN_Time", ResourceType = typeof(Lang))]
+        Month_Day_Formula_ProductSN_Time = 4
     }
 }

+ 4 - 0
TeamAAS-VM/Events/LockFinishNotification.cs

@@ -11,6 +11,10 @@ namespace TeamAAS_VP.Events
     {
     }
 
+    public class AssemblyFinishNotification : Prism.Events.PubSubEvent<AssemblyRecord>
+    {
+    }
+
     //保压完成通知
     public class PressureFinishNotification : Prism.Events.PubSubEvent<(bool IsSuccess, int Index, (DateTime Timestamp, float Value)[] values, string SavePath)>
     {

+ 3 - 1
TeamAAS-VM/Interfaces/IRemoteCommandService.cs

@@ -248,8 +248,10 @@ namespace TeamAAS_VP.Interfaces
         /// <param name="InputTerminal"></param>
         /// <param name="robotCoord"></param>
         /// <param name="cancellationToken"></param>
+        /// <param name="positionIndex"></param>
+        /// <param name="remark"></param>
         /// <returns></returns>
-        Task<(bool IsSucceed, double X, double Y, double U)> ExecutePhotoGetSinglePoint(ProcedureModel procedure, Dictionary<string, string> InputTerminal, double[] robotCoord, CancellationToken cancellationToken);
+        Task<(bool IsSucceed, double X, double Y, double U)> ExecutePhotoGetSinglePoint(ProcedureModel procedure, Dictionary<string, string> InputTerminal, double[] robotCoord, CancellationToken cancellationToken, int positionIndex, string remark);
 
         /// <summary>
         /// 执行相机拍照获取单个点位结果,并且返回图像和图形

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

@@ -68,6 +68,37 @@ namespace TeamAAS_VP.Interfaces
         /// </returns>
         Task<IEnumerable<User>> GetAllUsersAsync();
 
+        // Assembly process records
+
+        /// <summary>
+        /// 将一条组装过程记录写入数据库。
+        /// </summary>
+        /// <param name="record">要记录的 <see cref="AssemblyRecord"/> 实例,包含时间戳、配方名、产品序列号、零件序列号、是否成功、组装用时、各类坐标、操作员、备注等信息。</param>
+        /// <returns>完成写入操作的任务。</returns>
+        Task RecordAssemblyAsync(AssemblyRecord record);
+
+        /// <summary>
+        /// 查询组装记录,支持按配方名、产品序列号和时间段过滤(所有参数可选)。
+        /// </summary>
+        /// <param name="recipeName">可选的配方名称过滤,传入 null 表示不过滤。</param>
+        /// <param name="productSN">可选的产品序列号过滤,传入 null 表示不过滤。</param>
+        /// <param name="start">可选的起始时间(含),传入 null 表示不限制开始时间。</param>
+        /// <param name="end">可选的结束时间(不含),传入 null 表示不限制结束时间。</param>
+        /// <returns>满足条件的组装记录集合;如果没有匹配项,可以返回空集合(建议不返回 null)。</returns>
+        Task<IEnumerable<AssemblyRecord>> QueryAssemblyRecordsAsync(string recipeName = null, string productSN = null, DateTime? start = null, DateTime? end = null);
+
+        /// <summary>
+        /// 分页查询组装记录,支持按配方名、产品序列号和时间段过滤(所有参数可选)。
+        /// </summary>
+        /// <param name="recipeName">可选的配方名称过滤,传入 null 表示不过滤。</param>
+        /// <param name="productSN">可选的产品序列号过滤,传入 null 表示不过滤。</param>
+        /// <param name="start">可选的起始时间(含),传入 null 表示不限制开始时间。</param>
+        /// <param name="end">可选的结束时间(不含),传入 null 表示不限制结束时间。</param>
+        /// <param name="pageIndex">页号(从 1 开始)。</param>
+        /// <param name="pageSize">每页大小。</param>
+        /// <returns>返回一个元组,包含记录集合和总记录数。</returns>
+        Task<(IEnumerable<AssemblyRecord> Items, int TotalCount)> QueryAssemblyRecordsPagedAsync(string recipeName = null, string productSN = null, DateTime? start = null, DateTime? end = null, int pageIndex = 1, int pageSize = 20);
+
         // User authentication
 
         /// <summary>
@@ -272,6 +303,17 @@ namespace TeamAAS_VP.Interfaces
         /// <returns></returns>
         Task<Dictionary<int, Dictionary<string, int>>> GetDailyProductionByHourAndCategoryAsync(string productName);
 
+        /// <summary>
+        /// 分页查询生产记录。
+        /// </summary>
+        /// <param name="start"></param>
+        /// <param name="end"></param>
+        /// <param name="pageIndex"></param>
+        /// <param name="pageSize"></param>
+        /// <param name="productName"></param>
+        /// <param name="category"></param>
+        /// <returns></returns>
+        Task<(IEnumerable<ProductionRecord> Items, int TotalCount)> QueryProductionRecordsPagedAsync(DateTime start, DateTime end, int pageIndex, int pageSize, string productName = null, string category = null);
 
         // Alarm records
 
@@ -329,5 +371,39 @@ namespace TeamAAS_VP.Interfaces
         /// 记录一次螺丝供料器批次更换记录
         /// </summary>
         Task RecordScrewFeederBatchAsync(ScrewFeederBatchRecord record);
+
+        /// <summary>
+        /// 记录产品拍照结果(含图片路径、机器人位置、像素与绝对坐标等)。
+        /// </summary>
+        /// <param name="record"></param>
+        /// <returns></returns>
+        Task RecordPhotoCaptureAsync(PhotoCaptureRecord record);
+
+        /// <summary>
+        /// 查询拍照记录,支持按配方名、产品序列号和时间段过滤(所有参数可选)。
+        /// </summary>
+        /// <param name="recipeName"></param>
+        /// <param name="cameraName"></param>
+        /// <param name="productSN"></param>
+        /// <param name="start"></param>
+        /// <param name="end"></param>
+        /// <returns></returns>
+        Task<IEnumerable<PhotoCaptureRecord>> QueryPhotoCapturesAsync(string recipeName = null, string cameraName = null, string productSN = null, DateTime? start = null, DateTime? end = null);
+
+        /// <summary>
+        /// 分页查询拍照记录,支持按配方名、产品序列号和时间段过滤(所有参数可选)。
+        /// </summary>
+        /// <param name="recipeName"></param>
+        /// <param name="cameraName"></param>
+        /// <param name="productSN"></param>
+        /// <param name="start"></param>
+        /// <param name="end"></param>
+        /// <param name="pageIndex"></param>
+        /// <param name="pageSize"></param>
+        /// <returns></returns>
+        Task<(IEnumerable<PhotoCaptureRecord> Items, int TotalCount)> QueryPhotoCapturesPagedAsync(string recipeName = null, string cameraName = null, string productSN = null, DateTime? start = null, DateTime? end = null, int pageIndex = 1, int pageSize = 20);
+
+        //组装过程记录
+
     }
 }

+ 132 - 0
TeamAAS-VM/Models/AssemblyRecord.cs

@@ -0,0 +1,132 @@
+using System;
+using SqlSugar;
+
+namespace TeamAAS_VP.Models
+{
+    /// <summary>
+    /// 组装记录实体,表示一次组装操作的详细信息(用于持久化到数据库的 AssemblyRecord 表)
+    /// </summary>
+    [SugarTable("AssemblyRecord")]
+    public class AssemblyRecord
+    {
+        /// <summary>
+        /// 主键,唯一标识
+        /// </summary>
+        [SugarColumn(IsPrimaryKey = true)]
+        public Guid Id { get; set; } = Guid.NewGuid();
+
+        /// <summary>
+        /// 时间戳
+        /// </summary>
+        public DateTime Timestamp { get; set; } = DateTime.UtcNow;
+
+        /// <summary>
+        /// 配方名称
+        /// </summary>
+        [SugarColumn(Length = 255, IsNullable = true)]
+        public string RecipeName { get; set; }
+
+        /// <summary>
+        /// 产品序列号
+        /// </summary>
+        [SugarColumn(Length = 255, IsNullable = true)]
+        public string ProductSN { get; set; }
+
+        /// <summary>
+        /// 零件序列号
+        /// </summary>
+        [SugarColumn(Length = 255, IsNullable = true)]
+        public string PartSN { get; set; }
+
+        /// <summary>
+        /// 是否组装成功
+        /// </summary>
+        public bool Success { get; set; }
+
+        /// <summary>
+        /// 组装压力
+        /// </summary>
+        public double AssemblyPressure { get; set; }
+
+        /// <summary>
+        /// 组装用时(秒)
+        /// </summary>
+        public double AssemblyDurationSeconds { get; set; }
+
+        /// <summary>
+        /// 下相机拍摄的坐标1,字符串格式(例如 "x,y,z"),用于简化数据库存储,可为空
+        /// </summary>
+        [SugarColumn(Length = 1000, IsNullable = true)]
+        public string DownCameraCoord1 { get; set; }
+        /// <summary>
+        /// 下相机拍摄的坐标2,字符串格式(例如 "x,y,z"),用于简化数据库存储,可为空
+        /// </summary>
+        [SugarColumn(Length = 1000, IsNullable = true)]
+        public string DownCameraCoord2 { get; set; }
+        /// <summary>
+        /// 下相机拍摄的坐标3,字符串格式(例如 "x,y,z"),用于简化数据库存储,可为空
+        /// </summary>
+        [SugarColumn(Length = 1000, IsNullable = true)]
+        public string DownCameraCoord3 { get; set; }
+        /// <summary>
+        /// 下相机拍摄的坐标4,字符串格式(例如 "x,y,z"),用于简化数据库存储,可为空
+        /// </summary>
+        [SugarColumn(Length = 1000, IsNullable = true)]
+        public string DownCameraCoord4 { get; set; }
+
+        /// <summary>
+        /// 抓取运动过程中的相机坐标,字符串格式(例如 "x,y,z"),可为空
+        /// </summary>
+        [SugarColumn(Length = 1000, IsNullable = true)]
+        public string PickMoveCameraCoord { get; set; }
+
+        /// <summary>
+        /// 放置运动过程的相机坐标1,字符串格式(例如 "x,y,z"),可为空
+        /// </summary>
+        [SugarColumn(Length = 1000, IsNullable = true)]
+        public string PlaceMoveCameraCoord1 { get; set; }
+        /// <summary>
+        /// 放置运动过程的相机坐标2,字符串格式(例如 "x,y,z"),可为空
+        /// </summary>
+        [SugarColumn(Length = 1000, IsNullable = true)]
+        public string PlaceMoveCameraCoord2 { get; set; }
+        /// <summary>
+        /// 放置运动过程的相机坐标3,字符串格式(例如 "x,y,z"),可为空
+        /// </summary>
+        [SugarColumn(Length = 1000, IsNullable = true)]
+        public string PlaceMoveCameraCoord3 { get; set; }
+        /// <summary>
+        /// 放置运动过程的相机坐标4,字符串格式(例如 "x,y,z"),可为空
+        /// </summary>
+        [SugarColumn(Length = 1000, IsNullable = true)]
+        public string PlaceMoveCameraCoord4 { get; set; }
+
+        /// <summary>
+        /// 实际放置坐标,字符串格式(例如 "x,y,z"),可用于记录放置位置的实际测量值
+        /// </summary>
+        [SugarColumn(Length = 1000, IsNullable = true)]
+        public string PlaceActualCoord { get; set; }
+
+        /// <summary>
+        /// 设备名称,可为空
+        /// </summary>
+        [SugarColumn(IsNullable = true, Length = 255)]
+        public string DeviceName { get; set; }
+        /// <summary>
+        /// 设备ID,关联设备的数字标识
+        /// </summary>
+        public int DeviceId { get; set; }
+
+        /// <summary>
+        /// 操作员名称,可为空
+        /// </summary>
+        [SugarColumn(Length = 255, IsNullable = true)]
+        public string OperatorName { get; set; }
+
+        /// <summary>
+        /// 备注信息,可为空
+        /// </summary>
+        [SugarColumn(Length = 1000, IsNullable = true)]
+        public string Remark { get; set; }
+    }
+}

+ 87 - 0
TeamAAS-VM/Models/PhotoCaptureRecord.cs

@@ -0,0 +1,87 @@
+using SqlSugar;
+using System;
+
+namespace TeamAAS_VP.Models
+{
+    [SugarTable("PhotoCaptureRecord")]
+    public class PhotoCaptureRecord
+    {
+        [SugarColumn(IsPrimaryKey = true)]
+        public Guid Id { get; set; } = Guid.NewGuid();
+
+        /// <summary>
+        /// 拍照时间
+        /// </summary>
+        public DateTime CaptureTime { get; set; } = DateTime.UtcNow;
+
+        /// <summary>
+        /// 配方名
+        /// </summary>
+        [SugarColumn(Length = 255, IsNullable = true)]
+        public string RecipeName { get; set; }
+
+        /// <summary>
+        /// 产品序列号
+        /// </summary>
+        [SugarColumn(Length = 255, IsNullable = true)]
+        public string ProductSN { get; set; }
+
+        /// <summary>
+        /// 相机名
+        /// </summary>
+        [SugarColumn(Length = 255, IsNullable = true)]
+        public string CameraName { get; set; }
+
+        /// <summary>
+        /// 流程名
+        /// </summary>
+        [SugarColumn(Length = 255, IsNullable = true)]
+        public string ProcessName { get; set; }
+
+        /// <summary>
+        /// 位置编号
+        /// </summary>
+        public int PositionIndex { get; set; }
+
+        /// <summary>
+        /// 是否成功
+        /// </summary>
+        public bool IsSuccess { get; set; }
+
+        /// <summary>
+        /// 机器人拍照位置
+        /// </summary>
+        [SugarColumn(Length = 255, IsNullable = true)]
+        public string RobotPosition { get; set; }
+
+        /// <summary>
+        /// 像素坐标
+        /// </summary>
+        [SugarColumn(Length = 255, IsNullable = true)]
+        public string PixelPosition { get; set; }
+
+        /// <summary>
+        /// 绝对坐标
+        /// </summary>
+        [SugarColumn(Length = 255, IsNullable = true)]
+        public string AbsolutePosition { get; set; }
+
+        /// <summary>
+        /// 图片路径
+        /// </summary>
+        [SugarColumn(Length = 1000, IsNullable = true)]
+        public string ImagePath { get; set; }
+
+        /// <summary>
+        /// 备注
+        /// </summary>
+        [SugarColumn(Length = 1000, IsNullable = true)]
+        public string Remarks { get; set; }
+
+        /// <summary>
+        /// 当前的操作员名称
+        /// </summary>
+        [SugarColumn(Length = 255, IsNullable = true)]
+        public string OperatorName { get; set; }
+    }
+}

+ 3 - 0
TeamAAS-VM/Resources/Languages/Lang.resx

@@ -2129,4 +2129,7 @@
   <data name="视觉静态精度分析" xml:space="preserve">
     <value>视觉静态精度分析</value>
   </data>
+  <data name="ProcedureSaveImagePathModel_Month_Day_Formula_ProductSN_Time" xml:space="preserve">
+    <value>年月/日/配方名/产品SN/产品SN+时间</value>
+  </data>
 </root>

+ 94 - 6
TeamAAS-VM/Services/RemoteCommandService.cs

@@ -1666,8 +1666,10 @@ namespace TeamAAS_VP.Services
         /// <param name="InputTerminal"></param>
         /// <param name="robotCoord"></param>
         /// <param name="cancellationToken"></param>
+        /// <param name="positionIndex"></param>
+        /// <param name="remark"></param>
         /// <returns></returns>
-        public async Task<(bool IsSucceed, double X, double Y, double U)> ExecutePhotoGetSinglePoint(ProcedureModel procedure, Dictionary<string, string> InputTerminal, double[] robotCoord, CancellationToken cancellationToken)
+        public async Task<(bool IsSucceed, double X, double Y, double U)> ExecutePhotoGetSinglePoint(ProcedureModel procedure, Dictionary<string, string> InputTerminal, double[] robotCoord, CancellationToken cancellationToken, int positionIndex, string remark)
         {
             DateTime nowtime = DateTime.Now;
             await SetLightBeforePhoto(procedure);
@@ -1689,6 +1691,7 @@ namespace TeamAAS_VP.Services
                     if (!outputCollection.Contains("Found"))
                     {
                         SendTaskMessage(Lang.未找到视觉输出结果, MessageLevel.Error);
+                        await RecordPhotoCaptureAsync(procedure, robotCoord, false, new double[] { 0, 0, 0 }, new double[] { 0, 0, 0 }, string.Empty, positionIndex, remark);
                         return (false, 0, 0, 0);
                     }
 
@@ -1713,20 +1716,33 @@ namespace TeamAAS_VP.Services
                     double _pixel_x = double.Parse(posParts[0]);
                     double _pixel_y = double.Parse(posParts[1]);
                     double _pixel_u = double.Parse(posParts[2]);
-                    //转换点位-像素转换成机器人绝对坐标
-                    var calibResult = _calibrationService.ConvertPixelToPosition((_pixel_x, _pixel_y, _pixel_u), robotCoord, calib, RobotBrand.XYZ_Platform);
-                    if (!calibResult.IsSucceed)
+                    if (calib != null)
                     {
-                        return (false, 0, 0, 0);
+                        //转换点位-像素转换成机器人绝对坐标
+                        var calibResult = _calibrationService.ConvertPixelToPosition((_pixel_x, _pixel_y, _pixel_u), robotCoord, calib, RobotBrand.XYZ_Platform);
+                        if (!calibResult.IsSucceed)
+                        {
+                            await RecordPhotoCaptureAsync(procedure, robotCoord, false, new double[] { _pixel_x, _pixel_y, _pixel_u }, new double[] { 0, 0, 0 }, string.Empty, positionIndex, remark);
+                            return (false, 0, 0, 0);
+                        }
+                        await RecordPhotoCaptureAsync(procedure, robotCoord, true, new double[] { _pixel_x, _pixel_y, _pixel_u }, new double[] { calibResult.X, calibResult.Y, calibResult.U }, string.Empty, positionIndex, remark);
+                        return (true, calibResult.X, calibResult.Y, calibResult.U);
+                    }
+                    else
+                    {
+                        await RecordPhotoCaptureAsync(procedure, robotCoord, true, new double[] { _pixel_x, _pixel_y, _pixel_u }, new double[] { 0, 0, 0 }, string.Empty, positionIndex, remark);
+                        return (true, _pixel_x, _pixel_y, _pixel_u);
                     }
-                    return (true, calibResult.X, calibResult.Y, calibResult.U);
+
                 }
                 SendTaskMessage(Lang.多次拍照失败, MessageLevel.Alarm);
+                await RecordPhotoCaptureAsync(procedure, robotCoord, false, new double[] { 0, 0, 0 }, new double[] { 0, 0, 0 }, string.Empty, positionIndex, remark);
                 return (false, 0, 0, 0);
             }
             catch (Exception ex)
             {
                 LogHelper.WriteLogError("执行相机取图并获取单点位时出错!", ex);
+                await RecordPhotoCaptureAsync(procedure, robotCoord, false, new double[] { 0, 0, 0 }, new double[] { 0, 0, 0 }, string.Empty, positionIndex, remark);
                 return (false, 0, 0, 0);
             }
             finally
@@ -1736,6 +1752,78 @@ namespace TeamAAS_VP.Services
             }
         }
 
+        /// <summary>
+        /// 记录拍照结果数据至数据库
+        /// </summary>
+        /// <param name="procedure"></param>
+        /// <param name="robotCoord"></param>
+        /// <param name="isSuccess"></param>
+        /// <param name="pixelPosition"></param>
+        /// <param name="absolutePosition"></param>
+        /// <param name="imagePath"></param>
+        /// <param name="positionIndex"></param>
+        /// <param name="emarks"></param>
+        /// <returns></returns>
+        private async Task RecordPhotoCaptureAsync(ProcedureModel procedure, double[] robotCoord, bool isSuccess, double[] pixelPosition, double[] absolutePosition, string imagePath, int positionIndex, string emarks)
+        {
+            try
+            {
+                //当前的产品配方名
+                string formulaName = _productService.GetCurrentProduct().Name;
+                //如果当前产品名称存在非法字符,则替换为下划线
+                foreach (char c in System.IO.Path.GetInvalidFileNameChars())
+                {
+                    formulaName = formulaName.Replace(c, '_');
+                }
+                //获取当前的产品SN
+                string productSN = _productService.CurrentProductCode;
+                //如果产品SN存在非法字符,则替换为下划线
+                foreach (char c in System.IO.Path.GetInvalidFileNameChars())
+                {
+                    productSN = productSN.Replace(c, '_');
+                }
+                //当前用户
+                string currentUser = _systemDatabaseService.GetCurrentUser()?.UserName;
+
+                //相机名称
+                string cameraName = procedure.CameraName;
+                //视觉流程名称
+                string procedureName = procedure.Name;
+                //如果机器人坐标为空,则获取当前机器人坐标
+                if (robotCoord == null)
+                {
+                    var robot = _robotService.GetAllRobots().FirstOrDefault();
+                    if (robot != null)
+                    {
+                        var pos = await robot.GetRobotPosAsync();
+                        robotCoord = new double[] { pos.X, pos.Y, pos.Z };
+                    }
+                    else
+                    {
+                        robotCoord = new double[] { 0, 0, 0 };
+                    }
+                }
+                PhotoCaptureRecord photoCaptureRecord = new PhotoCaptureRecord();
+                photoCaptureRecord.RecipeName = formulaName;
+                photoCaptureRecord.ProductSN = productSN;
+                photoCaptureRecord.CameraName = cameraName;
+                photoCaptureRecord.ProcessName = procedureName;
+                photoCaptureRecord.RobotPosition = string.Join(",", robotCoord);
+                photoCaptureRecord.IsSuccess = isSuccess;
+                photoCaptureRecord.PixelPosition = string.Join(",", pixelPosition);
+                photoCaptureRecord.AbsolutePosition = string.Join(",", absolutePosition);
+                photoCaptureRecord.ImagePath = imagePath;
+                photoCaptureRecord.Remarks = emarks;
+                photoCaptureRecord.OperatorName = currentUser;
+
+                await _systemDatabaseService.RecordPhotoCaptureAsync(photoCaptureRecord);
+            }
+            catch (Exception ex)
+            {
+                LogHelper.WriteLogError("记录拍照结果数据至数据库时出错", ex);
+            }
+        }
+
         /// <summary>
         /// 执行相机拍照获取单个点位结果,并且返回图像和图形
         /// </summary>

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

@@ -536,14 +536,19 @@
     <Compile Include="Core\StabilityAnalyzer.cs" />
     <Compile Include="Events\MainTabSwitchNotification.cs" />
     <Compile Include="Interfaces\IMesService.cs" />
+    <Compile Include="Models\AssemblyRecord.cs" />
     <Compile Include="Models\DeviceInfo.cs" />
     <Compile Include="Models\Feeder\ScrewFeederInfo.cs" />
+    <Compile Include="Models\PhotoCaptureRecord.cs" />
     <Compile Include="Models\ScrewFeederBatchRecord.cs" />
     <Compile Include="Services\MesService.cs" />
     <Compile Include="ValueConverter\CountToColumnsConverter.cs" />
     <Compile Include="ViewModels\DebugMod\PlcRobotManualStepViewModel.cs" />
     <Compile Include="ViewModels\Product\ImageDisplayViewModel.cs" />
     <Compile Include="ViewModels\Product\PlcPointOffserParamsViewModel.cs" />
+    <Compile Include="ViewModels\Statistics\AssemblyRecordQueryViewModel.cs" />
+    <Compile Include="ViewModels\Statistics\PhotoCaptureQueryViewModel.cs" />
+    <Compile Include="ViewModels\Statistics\ProductionRecordQueryViewModel.cs" />
     <Compile Include="ViewModels\User\CardLoginWindowViewModel.cs" />
     <Compile Include="Views\Product\PlcPointOffserParams.xaml.cs">
       <DependentUpon>PlcPointOffserParams.xaml</DependentUpon>
@@ -1064,9 +1069,18 @@
     <Compile Include="Views\Statistics\AlarmQuery.xaml.cs">
       <DependentUpon>AlarmQuery.xaml</DependentUpon>
     </Compile>
+    <Compile Include="Views\Statistics\AssemblyRecordQuery.xaml.cs">
+      <DependentUpon>AssemblyRecordQuery.xaml</DependentUpon>
+    </Compile>
     <Compile Include="Views\Statistics\LockResultRecoredQuery.xaml.cs">
       <DependentUpon>LockResultRecoredQuery.xaml</DependentUpon>
     </Compile>
+    <Compile Include="Views\Statistics\PhotoCaptureQuery.xaml.cs">
+      <DependentUpon>PhotoCaptureQuery.xaml</DependentUpon>
+    </Compile>
+    <Compile Include="Views\Statistics\ProductionRecordQuery.xaml.cs">
+      <DependentUpon>ProductionRecordQuery.xaml</DependentUpon>
+    </Compile>
     <Compile Include="Views\Statistics\ProductionStatement.xaml.cs">
       <DependentUpon>ProductionStatement.xaml</DependentUpon>
     </Compile>
@@ -1454,10 +1468,22 @@
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>
     </Page>
+    <Page Include="Views\Statistics\AssemblyRecordQuery.xaml">
+      <Generator>MSBuild:Compile</Generator>
+      <SubType>Designer</SubType>
+    </Page>
     <Page Include="Views\Statistics\LockResultRecoredQuery.xaml">
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>
     </Page>
+    <Page Include="Views\Statistics\PhotoCaptureQuery.xaml">
+      <Generator>MSBuild:Compile</Generator>
+      <SubType>Designer</SubType>
+    </Page>
+    <Page Include="Views\Statistics\ProductionRecordQuery.xaml">
+      <Generator>MSBuild:Compile</Generator>
+      <SubType>Designer</SubType>
+    </Page>
     <Page Include="Views\Statistics\ProductionStatement.xaml">
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>

+ 29 - 2
TeamAAS-VM/ViewModels/HomeViewModel.cs

@@ -158,6 +158,16 @@ namespace TeamAAS_VP.ViewModels
             set { SetProperty(ref _LockResults, value); }
         }
 
+        private ObservableCollection<AssemblyRecord> _AssemblyResults=new ObservableCollection<AssemblyRecord>();
+        /// <summary>
+        /// 组装结果列表
+        /// </summary>
+        public ObservableCollection<AssemblyRecord> AssemblyResults
+        {
+            get { return _AssemblyResults; }
+            set { SetProperty(ref _AssemblyResults, value); }
+        }
+
         // 可选:记录传感器 Id -> 索引 的映射,便于外部(如 Management)按 Id 更新
         private readonly Dictionary<string, int> _sensorIdToIndex = new Dictionary<string, int>();
         #endregion
@@ -204,6 +214,7 @@ namespace TeamAAS_VP.ViewModels
             _systemDatabaseService = systemDatabaseService;
             _eventAggregator.GetEvent<LockFinishNotification>().Subscribe(OnLockFinish);
             _eventAggregator.GetEvent<PressureFinishNotification>().Subscribe(OnPressureFinish);
+            _eventAggregator.GetEvent<AssemblyFinishNotification>().Subscribe(OnAssemblyFinish);
 
             //获取所有的压力传感器配置,初始化曲线图
             var systemConfig = _configService.GetSystemConfiguration();
@@ -375,7 +386,8 @@ namespace TeamAAS_VP.ViewModels
         /// </summary>
         async void ExecuteResetCounterCommand()
         {
-            if (management.CurrentProduct == null) return;
+            var currentProduct = _productService.GetCurrentProduct();
+            if (currentProduct == null) return;
             var view = new ShowMessage(Lang.计数清零, Lang.是否重置当前产品的计数);
             //show the dialog
             var result = await DialogHost.Show(view, "RootDialog", null, null, null);
@@ -384,7 +396,7 @@ namespace TeamAAS_VP.ViewModels
                 if (((bool)result))
                 {
                     isCanExecute = false;
-                    await _systemDatabaseService.ResetProductionAsync(management.CurrentProduct.Name);
+                    await _systemDatabaseService.ResetProductionAsync(currentProduct.Name);
                     isCanExecute = true;
                 }
             }
@@ -467,6 +479,21 @@ namespace TeamAAS_VP.ViewModels
 
         }
 
+        /// <summary>
+        /// 锁付完成时调用
+        /// </summary>
+        /// <param name="record"></param>
+        /// <exception cref="NotImplementedException"></exception>
+        private void OnAssemblyFinish(AssemblyRecord record)
+        {
+            AssemblyResults.Add(record);
+            //如果记录数超过1000,则删除最早的记录
+            if (AssemblyResults.Count > 50)
+            {
+                AssemblyResults.RemoveAt(0);
+            }
+        }
+
         /// <summary>
         /// 保压完成时调用
         /// </summary>

+ 4 - 4
TeamAAS-VM/ViewModels/Product/PlcPointParamsViewModel.cs

@@ -1261,8 +1261,8 @@ namespace TeamAAS_VP.ViewModels.Product
                 }
                 // 执行视觉任务
                 var _cts = new CancellationTokenSource();
-                var task1 = _remoteCommandService.ExecutePhotoGetSinglePoint(process1, null, new double[] { rpoint.X, rpoint.Y, rpoint.U }, _cts.Token);
-                var task2 = _remoteCommandService.ExecutePhotoGetSinglePoint(process2, null, new double[] { rpoint.X, rpoint.Y, rpoint.U }, _cts.Token);
+                var task1 = _remoteCommandService.ExecutePhotoGetSinglePointWithImage(process1, null, new double[] { rpoint.X, rpoint.Y, rpoint.U }, _cts.Token);
+                var task2 = _remoteCommandService.ExecutePhotoGetSinglePointWithImage(process2, null, new double[] { rpoint.X, rpoint.Y, rpoint.U }, _cts.Token);
                 var visionResults = await Task.WhenAll(task1, task2);
                 if (visionResults[0].IsSucceed && visionResults[1].IsSucceed)
                 {
@@ -1359,7 +1359,7 @@ namespace TeamAAS_VP.ViewModels.Product
                 }
                 // 执行视觉任务
                 var _cts = new CancellationTokenSource();
-                var visionResult =await _remoteCommandService.ExecutePhotoGetSinglePoint(process1, null, new double[] { rpoint.X, rpoint.Y, rpoint.U }, _cts.Token);
+                var visionResult =await _remoteCommandService.ExecutePhotoGetSinglePointWithImage(process1, null, new double[] { rpoint.X, rpoint.Y, rpoint.U }, _cts.Token);
                 if (visionResult.IsSucceed)
                 {
                     _PutPoints[0] = new OpenCvSharp.Point3f((float)visionResult.X, (float)visionResult.Y, (float)visionResult.U);
@@ -1401,7 +1401,7 @@ namespace TeamAAS_VP.ViewModels.Product
                 }
                 // 执行视觉任务
                 _cts = new CancellationTokenSource();
-                visionResult = await _remoteCommandService.ExecutePhotoGetSinglePoint(process2, null, new double[] { rpoint.X, rpoint.Y, rpoint.U }, _cts.Token);
+                visionResult = await _remoteCommandService.ExecutePhotoGetSinglePointWithImage(process2, null, new double[] { rpoint.X, rpoint.Y, rpoint.U }, _cts.Token);
                 if (visionResult.IsSucceed)
                 {
                     _PutPoints[1] = new OpenCvSharp.Point3f((float)visionResult.X, (float)visionResult.Y, (float)visionResult.U);

+ 260 - 0
TeamAAS-VM/ViewModels/Statistics/AssemblyRecordQueryViewModel.cs

@@ -0,0 +1,260 @@
+using Prism.Commands;
+using Prism.Mvvm;
+using System;
+using System.Collections.Generic;
+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;
+
+namespace TeamAAS_VP.ViewModels.Statistics
+{
+    public class AssemblyRecordQueryViewModel : BindableBase
+    {
+        private readonly IProductService _productService;
+        private readonly ISystemDatabaseService _systemDatabaseService;
+
+        public AssemblyRecordQueryViewModel(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 string _ProductNumber;
+        public string ProductNumber
+        {
+            get { return _ProductNumber; }
+            set { SetProperty(ref _ProductNumber, value); }
+        }
+
+        private ObservableCollection<AssemblyRecord> _Results = new ObservableCollection<AssemblyRecord>();
+        public ObservableCollection<AssemblyRecord> 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))
+                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));
+        }
+
+        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.QueryAssemblyRecordsPagedAsync(productNumberFilter,ProductNumber, start, end, pageIndex, pageSize);
+                Results = new ObservableCollection<AssemblyRecord>(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.QueryAssemblyRecordsAsync(productNumberFilter, ProductNumber, start, end);
+                if (all == null || !all.Any())
+                {
+                    MessageBox.Show("没有找到符合条件的记录。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
+                    return;
+                }
+
+                var saveFileDialog = new Microsoft.Win32.SaveFileDialog
+                {
+                    FileName = $"AssemblyRecords_{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))
+                {
+                    // header matches AssemblyRecord properties shown in the UI, include all coordinate fields
+                    sw.WriteLine("Id,Timestamp,RecipeName,ProductSN,PartSN,Success,AssemblyPressure,AssemblyDurationSeconds,DownCameraCoord1,DownCameraCoord2,DownCameraCoord3,DownCameraCoord4,PickMoveCameraCoord,PlaceMoveCameraCoord1,PlaceMoveCameraCoord2,PlaceMoveCameraCoord3,PlaceMoveCameraCoord4,PlaceActualCoord,DeviceName,DeviceId,OperatorName,Remark");
+                    foreach (var r in all)
+                    {
+                        var line = string.Join(",",
+                            EscapeCsv(r.Id.ToString()),
+                            EscapeCsv(r.Timestamp.ToString("yyyy-MM-dd HH:mm:ss")),
+                            EscapeCsv(r.RecipeName),
+                            EscapeCsv(r.ProductSN),
+                            EscapeCsv(r.PartSN),
+                            EscapeCsv(r.Success ? "1" : "0"),
+                            EscapeCsv(r.AssemblyPressure.ToString()),
+                            EscapeCsv(r.AssemblyDurationSeconds.ToString()),
+                            EscapeCsv(r.DownCameraCoord1),
+                            EscapeCsv(r.DownCameraCoord2),
+                            EscapeCsv(r.DownCameraCoord3),
+                            EscapeCsv(r.DownCameraCoord4),
+                            EscapeCsv(r.PickMoveCameraCoord),
+                            EscapeCsv(r.PlaceMoveCameraCoord1),
+                            EscapeCsv(r.PlaceMoveCameraCoord2),
+                            EscapeCsv(r.PlaceMoveCameraCoord3),
+                            EscapeCsv(r.PlaceMoveCameraCoord4),
+                            EscapeCsv(r.PlaceActualCoord),
+                            EscapeCsv(r.DeviceName),
+                            EscapeCsv(r.DeviceId.ToString()),
+                            EscapeCsv(r.OperatorName),
+                            EscapeCsv(r.Remark)
+                        );
+                        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;
+        }
+    }
+}

+ 1 - 1
TeamAAS-VM/ViewModels/Statistics/LockResultRecoredQueryViewModel.cs

@@ -27,7 +27,7 @@ namespace TeamAAS_VP.ViewModels.Statistics
             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);
+            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);
 

+ 191 - 0
TeamAAS-VM/ViewModels/Statistics/PhotoCaptureQueryViewModel.cs

@@ -0,0 +1,191 @@
+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.Controls;
+using TeamAAS_VP.Interfaces;
+using TeamAAS_VP.Models;
+using TeamAAS_VP.Services;
+
+namespace TeamAAS_VP.ViewModels.Statistics
+{
+    public class PhotoCaptureQueryViewModel : BindableBase
+    {
+        private readonly ISystemDatabaseService _systemDatabaseService;
+        private readonly IProductService _productService;
+        private readonly IConfigService _configService;
+
+        public PhotoCaptureQueryViewModel(ISystemDatabaseService systemDatabaseService, IProductService productService, IConfigService configService)
+        {
+            _systemDatabaseService = systemDatabaseService;
+            _productService = productService;
+            _configService = configService;
+            PageSize = 20; PageIndex = 1;
+            StartDate = DateTime.Now.Date; EndDate = DateTime.Now.Date.AddDays(1);
+            SearchCommand = new DelegateCommand(async () => await ExecuteSearchCommand());
+            PrevPageCommand = new DelegateCommand(async () => { if (PageIndex > 1) { PageIndex--; await ExecuteSearchCommand(); } });
+            NextPageCommand = new DelegateCommand(async () => { if (PageIndex * PageSize < TotalCount) { PageIndex++; await ExecuteSearchCommand(); } });
+            ExportCommand = new DelegateCommand(async () => await ExecuteExportCommand());
+        }
+
+        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 ObservableCollection<CameraInfo> _AllCamera;
+        public ObservableCollection<CameraInfo> AllCamera
+        {
+            get { return _AllCamera; }
+            set { SetProperty(ref _AllCamera, value); }
+        }
+
+        private CameraInfo _SelectedCamera;
+        public CameraInfo SelectedCamera
+        {
+            get { return _SelectedCamera; }
+            set { SetProperty(ref _SelectedCamera, value); }
+        }
+        private string _ProductSN;
+        public string ProductSN
+        {
+            get { return _ProductSN; }
+            set { SetProperty(ref _ProductSN, 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 ObservableCollection<PhotoCaptureRecord> _Results;
+        public ObservableCollection<PhotoCaptureRecord> 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); }
+        }
+
+        public DelegateCommand SearchCommand { get; private set; }
+        public DelegateCommand PrevPageCommand { get; private set; }
+        public DelegateCommand NextPageCommand { get; private set; }
+        public DelegateCommand ExportCommand { get; private set; }
+
+        private DelegateCommand _LoadedCommand;
+        public DelegateCommand LoadedCommand =>
+            _LoadedCommand ?? (_LoadedCommand = new DelegateCommand(ExecuteLoadedCommand));
+        void ExecuteLoadedCommand()
+        {
+            AllProduct = new ObservableCollection<ProductModel>(_productService.GetAllProducts());
+            AllCamera = new ObservableCollection<CameraInfo>(_configService.GetAllCameras());
+        }
+
+        private async Task ExecuteSearchCommand()
+        {
+            try
+            {
+                var start = StartDate.HasValue ? StartDate.Value : (DateTime?)null;
+                var end = EndDate.HasValue ? EndDate.Value : (DateTime?)null;
+                var res = await _systemDatabaseService.QueryPhotoCapturesAsync(SelectedProduct?.Name, SelectedCamera?.CameraName, ProductSN, start, end);
+                Results = new ObservableCollection<PhotoCaptureRecord>(res);
+                TotalCount = Results.Count;
+            }
+            catch (Exception ex)
+            {
+                MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+            }
+        }
+
+        private async Task ExecuteExportCommand()
+        {
+            try
+            {
+                var start = StartDate.HasValue ? StartDate.Value : (DateTime?)null;
+                var end = EndDate.HasValue ? EndDate.Value : (DateTime?)null;
+                var all = await _systemDatabaseService.QueryPhotoCapturesAsync(SelectedProduct?.Name, SelectedCamera?.CameraName, ProductSN, start, end);
+                if (all == null || !all.Any()) { MessageBox.Show("没有找到符合条件的记录", "提示", MessageBoxButton.OK, MessageBoxImage.Information); return; }
+                var dlg = new Microsoft.Win32.SaveFileDialog { FileName = $"PhotoCapture_{DateTime.Now:yyyyMMddHHmmss}.csv", DefaultExt = ".csv", Filter = "CSV Files (*.csv)|*.csv" };
+                if (dlg.ShowDialog() != true) return;
+                var path = dlg.FileName;
+                using (var sw = new StreamWriter(path, false, Encoding.UTF8))
+                {
+                    sw.WriteLine("Id,CaptureTime,RecipeName,ProcessName,CameraName,ProductSN,PositionIndex,IsSuccess,RobotPosition,PixelPosition,AbsolutePosition,ImagePath,OperatorName,Remarks");
+                    foreach (var r in all)
+                    {
+                        sw.WriteLine(string.Join(",",
+                            EscapeCsv(r.Id.ToString()),
+                            EscapeCsv(r.CaptureTime.ToString("yyyy-MM-dd HH:mm:ss")),
+                            EscapeCsv(r.RecipeName),
+                            EscapeCsv(r.ProcessName),
+                            EscapeCsv(r.CameraName),
+                            EscapeCsv(r.ProductSN),
+                            EscapeCsv(r.PositionIndex.ToString()),
+                            EscapeCsv(r.IsSuccess ? "1" : "0"),
+                            EscapeCsv(r.RobotPosition),
+                            EscapeCsv(r.PixelPosition),
+                            EscapeCsv(r.AbsolutePosition),
+                            EscapeCsv(r.ImagePath),
+                            EscapeCsv(r.OperatorName),
+                            EscapeCsv(r.Remarks)
+                        ));
+                    }
+                }
+            }
+            catch (Exception ex)
+            {
+                MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+            }
+        }
+
+        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;
+        }
+    }
+}

+ 167 - 0
TeamAAS-VM/ViewModels/Statistics/ProductionRecordQueryViewModel.cs

@@ -0,0 +1,167 @@
+using Prism.Commands;
+using Prism.Mvvm;
+using System;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using TeamAAS_VP.Interfaces;
+using TeamAAS_VP.Models.Product;
+using TeamAAS_VP.Models;
+
+namespace TeamAAS_VP.ViewModels.Statistics
+{
+    public class ProductionRecordQueryViewModel : BindableBase
+    {
+        private readonly IProductService _productService;
+        private readonly ISystemDatabaseService _systemDatabaseService;
+
+        public ProductionRecordQueryViewModel(IProductService productService, ISystemDatabaseService systemDatabaseService)
+        {
+            _productService = productService;
+            _systemDatabaseService = systemDatabaseService;
+
+            PageSize = 20;
+            PageIndex = 1;
+            AllProduct = new ObservableCollection<ProductModel>(_productService.GetAllProducts());
+            Categories = new ObservableCollection<string>();
+
+            StartDate = DateTime.Now.Date;
+            EndDate = DateTime.Now.Date.AddDays(1);
+
+            SearchCommand = new DelegateCommand(async () => await ExecuteSearchCommand()).ObservesProperty(() => SelectedProduct).ObservesProperty(() => StartDate).ObservesProperty(() => EndDate);
+            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()).ObservesProperty(() => Results);
+        }
+
+        public ObservableCollection<ProductModel> AllProduct { get; set; }
+        public ObservableCollection<string> Categories { get; set; }
+
+        private ProductModel _SelectedProduct;
+        public ProductModel SelectedProduct { get => _SelectedProduct; set { SetProperty(ref _SelectedProduct, value); if (value != null) LoadCategoriesForProduct(value.Name); } }
+
+        private string _SelectedCategory;
+        public string SelectedCategory { get => _SelectedCategory; set { SetProperty(ref _SelectedCategory, value); } }
+
+        private DateTime? _StartDate;
+        public DateTime? StartDate { get => _StartDate; set { SetProperty(ref _StartDate, value); } }
+
+        private DateTime? _EndDate;
+        public DateTime? EndDate { get => _EndDate; set { SetProperty(ref _EndDate, value); } }
+
+        private ObservableCollection<ProductionRecord> _Results = new ObservableCollection<ProductionRecord>();
+        public ObservableCollection<ProductionRecord> Results { get => _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); }
+        }
+
+        public DelegateCommand SearchCommand { get; private set; }
+        public DelegateCommand PrevPageCommand { get; private set; }
+        public DelegateCommand NextPageCommand { get; private set; }
+        public DelegateCommand ExportCommand { get; private set; }
+
+        private DelegateCommand _LoadedCommand;
+        public DelegateCommand LoadedCommand =>
+            _LoadedCommand ?? (_LoadedCommand = new DelegateCommand(ExecuteLoadedCommand));
+        void ExecuteLoadedCommand()
+        {
+            AllProduct = new ObservableCollection<ProductModel>(_productService.GetAllProducts());
+        }
+
+        private async Task ExecuteSearchCommand()
+        {
+            try
+            {
+                var start = StartDate.HasValue ? StartDate.Value.Date : DateTime.MinValue;
+                var end = EndDate.HasValue ? EndDate.Value.Date.AddDays(1) : DateTime.MaxValue;
+                string productName = SelectedProduct?.Name;
+                var res = await _systemDatabaseService.QueryProductionRecordsPagedAsync(start, end, PageIndex, PageSize, productName, SelectedCategory);
+                Results = new ObservableCollection<ProductionRecord>(res.Items);
+                TotalCount = res.TotalCount;
+            }
+            catch (Exception ex)
+            {
+                MessageBox.Show(ex.Message, "´íÎó", MessageBoxButton.OK, MessageBoxImage.Error);
+            }
+        }
+
+        private async Task ExecuteExportCommand()
+        {
+            try
+            {
+                // Export current page Results (paged) to CSV including all ProductionRecord fields
+                var saveFileDialog = new Microsoft.Win32.SaveFileDialog { FileName = $"ProductionRecords_{DateTime.Now:yyyyMMddHHmmss}.csv", DefaultExt = ".csv", Filter = "CSV Files (*.csv)|*.csv" };
+                if (saveFileDialog.ShowDialog() != true) return;
+                var path = saveFileDialog.FileName;
+                using (var sw = new StreamWriter(path, false, Encoding.UTF8))
+                {
+                    sw.WriteLine("Id,Timestamp,ProductName,ProductCode,DeviceId,DeviceName,Category,Quantity,UserName,Remark");
+                    foreach (var r in Results)
+                    {
+                        sw.WriteLine(string.Join(",",
+                            EscapeCsv(r.Id.ToString()),
+                            EscapeCsv(r.Timestamp.ToString("yyyy-MM-dd HH:mm:ss")),
+                            EscapeCsv(r.ProductName),
+                            EscapeCsv(r.ProductCode),
+                            EscapeCsv(r.DeviceId.ToString()),
+                            EscapeCsv(r.DeviceName),
+                            EscapeCsv(r.Category),
+                            EscapeCsv(r.Quantity.ToString()),
+                            EscapeCsv(r.UserName),
+                            EscapeCsv(r.Remark)
+                        ));
+                    }
+                }
+            }
+            catch (Exception ex)
+            {
+                MessageBox.Show(ex.Message, "´íÎó", MessageBoxButton.OK, MessageBoxImage.Error);
+            }
+            await Task.CompletedTask;
+        }
+
+        private async void LoadCategoriesForProduct(string productName)
+        {
+            try
+            {
+                Categories.Clear();
+                var cats = await _systemDatabaseService.GetProductCategoriesAsync(productName);
+                if (cats != null)
+                {
+                    foreach (var c in cats) Categories.Add(c);
+                }
+            }
+            catch { }
+        }
+
+        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;
+        }
+    }
+}

+ 19 - 15
TeamAAS-VM/ViewModels/Statistics/ProductionStatementViewModel.cs

@@ -14,6 +14,7 @@ using TeamAAS_VP.Data;
 using TeamAAS_VP.Interfaces;
 using TeamAAS_VP.Models;
 using TeamAAS_VP.Resources.Languages;
+using TeamAAS_VP.Services;
 using static MaterialDesignThemes.Wpf.Theme.ToolBar;
 
 namespace TeamAAS_VP.ViewModels.Statistics
@@ -22,8 +23,7 @@ namespace TeamAAS_VP.ViewModels.Statistics
     {
         IContainerProvider _container;
         ISystemDatabaseService _systemDatabaseService;
-
-        Management management;
+        IProductService _productService;
 
         #region 属性
         private Int64 _TotalCount;
@@ -119,27 +119,27 @@ namespace TeamAAS_VP.ViewModels.Statistics
 
         #endregion
 
-        public ProductionStatementViewModel(IContainerProvider container, ISystemDatabaseService systemDatabaseService)
+        public ProductionStatementViewModel(IContainerProvider container, ISystemDatabaseService systemDatabaseService, IProductService productService)
         {
             _container= container;
             _systemDatabaseService = systemDatabaseService;
+            _productService = productService;
         }
 
         #region 方法
         async void ExecuteLoadedCommand()
         {
-            if (management == null)
-                management = _container.Resolve<Management>();
-            if (management.CurrentProduct == null) return;
-           await  App.Current.Dispatcher.InvokeAsync(new Action(async () => {
-                YieldChartModel = CreateYieldChartModel(await _systemDatabaseService.GetProductionByCategoriesAsync(management.CurrentProduct.Name));
-               TotalCount = await _systemDatabaseService.GetOverallProductionAsync(management.CurrentProduct.Name);
-                MonthCount = await _systemDatabaseService.GetCurrentMonthProductionAsync(management.CurrentProduct.Name);
-               WeekCount = await _systemDatabaseService.GetCurrentWeekProductionAsync(management.CurrentProduct.Name);
-               DayCount = await _systemDatabaseService.GetTodayProductionAsync(management.CurrentProduct.Name);
-               MonthChartModel = CreateMonth(await _systemDatabaseService.GetMonthlyProductionByDayAndCategoryAsync(management.CurrentProduct.Name));
-                WeekChartModel = CreateWeek(await _systemDatabaseService.GetWeeklyProductionByDayAndCategoryAsync(management.CurrentProduct.Name));
-                DayChartModel = CreateDay(await _systemDatabaseService.GetDailyProductionByHourAndCategoryAsync(management.CurrentProduct.Name));
+            var currentproduct = _productService.GetCurrentProduct();
+            if (currentproduct == null) return;
+            await  App.Current.Dispatcher.InvokeAsync(new Action(async () => {
+                YieldChartModel = CreateYieldChartModel(await _systemDatabaseService.GetProductionByCategoriesAsync(currentproduct.Name));
+               TotalCount = await _systemDatabaseService.GetOverallProductionAsync(currentproduct.Name);
+                MonthCount = await _systemDatabaseService.GetCurrentMonthProductionAsync(currentproduct.Name);
+               WeekCount = await _systemDatabaseService.GetCurrentWeekProductionAsync(currentproduct.Name);
+               DayCount = await _systemDatabaseService.GetTodayProductionAsync(currentproduct.Name);
+               MonthChartModel = CreateMonth(await _systemDatabaseService.GetMonthlyProductionByDayAndCategoryAsync(currentproduct.Name));
+                WeekChartModel = CreateWeek(await _systemDatabaseService.GetWeeklyProductionByDayAndCategoryAsync(currentproduct.Name));
+                DayChartModel = CreateDay(await _systemDatabaseService.GetDailyProductionByHourAndCategoryAsync(currentproduct.Name));
             }));
             
         }
@@ -405,6 +405,10 @@ namespace TeamAAS_VP.ViewModels.Statistics
 
             model.Axes.Add(ay1);
             model.Axes.Add(ax);
+            if (Items.Count==0)
+            {
+                return model;
+            }
 
             foreach (var category in Items.First().Value.Keys)
             {

+ 64 - 10
TeamAAS-VM/Views/Home/ShowVisionRender.xaml.cs

@@ -37,6 +37,8 @@ namespace TeamAAS_VP.Views.Home
         Dictionary<Guid, TextBlock> Titles;
         IEventAggregator _eventAggregator;
         ISystemDatabaseService _systemDatabaseService;
+        IProductService _productService;
+
         public ShowVisionRender()
         {
             InitializeComponent();
@@ -45,6 +47,7 @@ namespace TeamAAS_VP.Views.Home
             viewModel = DataContext as ShowVisionRenderViewModel;
             _eventAggregator = viewModel._eventAggregator;
             _systemDatabaseService= ((Prism.PrismApplicationBase)App.Current).Container.Resolve<ISystemDatabaseService>();
+            _productService = ((Prism.PrismApplicationBase)App.Current).Container.Resolve<IProductService>();
             viewModel.UpdateLayout += ViewModel_UpdateLayout;
             _eventAggregator.GetEvent<RenderUpdateNotification>().Subscribe(UpdateRenderModuleSource);
             _eventAggregator.GetEvent<ProductChangedNotification>().Subscribe(ProductChanged);
@@ -829,16 +832,6 @@ namespace TeamAAS_VP.Views.Home
             }
         }
 
-        /// <summary>
-        /// 保存图像
-        /// </summary>
-        /// <param name="saveImageModel">要保存图像的模式</param>
-        /// <param name="saveImagePathModel">图像保存路径类型</param>
-        /// <param name="path">保存图像的文件夹路径</param>
-        /// <param name="image">原图像</param>
-        /// <param name="reimage">渲染图像</param>
-        /// <param name="result">拍照结果</param>
-        /// <param name="isCompress">是否压缩</param>
         private void SaveImage(ProcedureSaveImageModel saveImageModel, ProcedureSaveImagePathModel saveImagePathModel, string path, ICogImage image, Bitmap reimage, bool result, bool isCompress)
         {
 
@@ -889,6 +882,26 @@ namespace TeamAAS_VP.Views.Home
                                 return;
                             }
                         }
+                        else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Formula_ProductSN_Time)
+                        {
+                            //当前的产品配方名
+                            string formulaName = _productService.GetCurrentProduct().Name;
+                            //如果当前产品名称存在非法字符,则替换为下划线
+                            foreach (char c in Path.GetInvalidFileNameChars())
+                            {
+                                formulaName = formulaName.Replace(c, '_');
+                            }
+                            //获取当前的产品SN
+                            string productSN = _productService.CurrentProductCode;
+                            //如果产品SN存在非法字符,则替换为下划线
+                            foreach (char c in Path.GetInvalidFileNameChars())
+                            {
+                                productSN = productSN.Replace(c, '_');
+                            }
+                            //拍照结果
+                            string resultStr = result ? "OK" : "NG";
+                            Originalpath = $"{path}\\{now.ToString("yyyy-MM")}\\{now.ToString("MM-dd")}\\{formulaName}\\{productSN}\\{productSN}-{now.ToString("yyyy-MM-dd HH_mm_ss")}-{resultStr}.bmp";
+                        }
                         if (!Directory.Exists(Path.GetDirectoryName(Originalpath)))   //判断文件夹是否存在
                         {
                             Directory.CreateDirectory(Path.GetDirectoryName(Originalpath));    //创建文件夹
@@ -936,6 +949,26 @@ namespace TeamAAS_VP.Views.Home
                                 return;
                             }
                         }
+                        else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Formula_ProductSN_Time)
+                        {
+                            //当前的产品配方名
+                            string formulaName = _productService.GetCurrentProduct().Name;
+                            //如果当前产品名称存在非法字符,则替换为下划线
+                            foreach (char c in Path.GetInvalidFileNameChars())
+                            {
+                                formulaName = formulaName.Replace(c, '_');
+                            }
+                            //获取当前的产品SN
+                            string productSN = _productService.CurrentProductCode;
+                            //如果产品SN存在非法字符,则替换为下划线
+                            foreach (char c in Path.GetInvalidFileNameChars())
+                            {
+                                productSN = productSN.Replace(c, '_');
+                            }
+                            //拍照结果
+                            string resultStr = result ? "OK" : "NG";
+                            Recordedpath = $"{path}\\{now.ToString("yyyy-MM")}\\{now.ToString("MM-dd")}\\{formulaName}\\{productSN}\\{productSN}-{now.ToString("yyyy-MM-dd HH_mm_ss")}-{resultStr}.bmp";
+                        }
                         if (!Directory.Exists(Path.GetDirectoryName(Recordedpath)))   //判断文件夹是否存在
                         {
                             Directory.CreateDirectory(Path.GetDirectoryName(Recordedpath));    //创建文件夹
@@ -992,6 +1025,27 @@ namespace TeamAAS_VP.Views.Home
                                 return;
                             }
                         }
+                        else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Formula_ProductSN_Time)
+                        {
+                            //当前的产品配方名
+                            string formulaName = _productService.GetCurrentProduct().Name;
+                            //如果当前产品名称存在非法字符,则替换为下划线
+                            foreach (char c in Path.GetInvalidFileNameChars())
+                            {
+                                formulaName = formulaName.Replace(c, '_');
+                            }
+                            //获取当前的产品SN
+                            string productSN = _productService.CurrentProductCode;
+                            //如果产品SN存在非法字符,则替换为下划线
+                            foreach (char c in Path.GetInvalidFileNameChars())
+                            {
+                                productSN = productSN.Replace(c, '_');
+                            }
+                            //拍照结果
+                            string resultStr = result ? "OK" : "NG";
+                            Originalpath = $"{path}\\Original\\{now.ToString("yyyy-MM")}\\{now.ToString("MM-dd")}\\{formulaName}\\{productSN}\\{productSN}-{now.ToString("yyyy-MM-dd HH_mm_ss")}-{resultStr}.bmp";
+                            Recordedpath = $"{path}\\Recored\\{now.ToString("yyyy-MM")}\\{now.ToString("MM-dd")}\\{formulaName}\\{productSN}\\{productSN}-{now.ToString("yyyy-MM-dd HH_mm_ss")}-{resultStr}.bmp";
+                        }
                         if (!Directory.Exists(Path.GetDirectoryName(Originalpath)))   //判断文件夹是否存在
                         {
                             Directory.CreateDirectory(Path.GetDirectoryName(Originalpath));    //创建文件夹

+ 15 - 19
TeamAAS-VM/Views/HomeView.xaml

@@ -132,7 +132,7 @@
                                    FontSize="{DynamicResource Font.Size.Body3}"
                                    Text="视觉引导定位数据:" />
                         <DataGrid x:Name="lockResultGrid"
-                                  ItemsSource="{Binding LockResults}"
+                                  ItemsSource="{Binding AssemblyResults}"
                                   AutoGenerateColumns="False"
                                   CanUserAddRows="False"
                                   CanUserDeleteRows="False"
@@ -145,31 +145,23 @@
                                   HorizontalAlignment="Stretch"
                                   Margin="0,8,0,0">
                             <DataGrid.Columns>
-                                <DataGridTextColumn Header="#"
-                                                    Binding="{Binding Number}"
-                                                    Width="auto"
-                                                    IsReadOnly="True" />
                                 <DataGridTextColumn Header="产品编号"
-                                                    Binding="{Binding ProductNumber}"
+                                                    Binding="{Binding ProductSN}"
                                                     Width="auto"
                                                     IsReadOnly="True" />
                                 <DataGridTextColumn Header="时间"
                                                     Binding="{Binding Timestamp, StringFormat={}{0:G}}"
                                                     Width="auto"
                                                     IsReadOnly="True" />
-                                <DataGridTextColumn Header="程序号"
-                                                    Binding="{Binding ScrewdriverProgramNumber}"
-                                                    Width="auto"
-                                                    IsReadOnly="True" />
-                                <DataGridTextColumn Header="锁付圈数"
-                                                    Binding="{Binding LockTurns, StringFormat={}{0:F1}}"
+                                <DataGridTextColumn Header="组装压力"
+                                                    Binding="{Binding AssemblyPressure, StringFormat={}{0:F3}}"
                                                     Width="auto"
                                                     IsReadOnly="True" />
-                                <DataGridTextColumn Header="锁付扭矩"
-                                                    Binding="{Binding LockTorque, StringFormat={}{0:F3}}"
+                                <DataGridTextColumn Header="组装用时(s)"
+                                                    Binding="{Binding AssemblyDurationSeconds, StringFormat={}{0:F3}}"
                                                     Width="auto"
                                                     IsReadOnly="True" />
-                                <DataGridTemplateColumn Header="锁付结果"
+                                <DataGridTemplateColumn Header="组装结果"
                                                         Width="auto"
                                                         IsReadOnly="True">
                                     <DataGridTemplateColumn.CellTemplate>
@@ -187,7 +179,7 @@
                                                            FontWeight="Bold" />
                                             </Border>
                                             <DataTemplate.Triggers>
-                                                <DataTrigger Binding="{Binding LockPassed, StringFormat={}{0:F3}}"
+                                                <DataTrigger Binding="{Binding Success, StringFormat={}{0:F3}}"
                                                              Value="True">
                                                     <Setter TargetName="bd"
                                                             Property="Background"
@@ -196,7 +188,7 @@
                                                             Property="Text"
                                                             Value="OK" />
                                                 </DataTrigger>
-                                                <DataTrigger Binding="{Binding LockPassed, StringFormat={}{0:F3}}"
+                                                <DataTrigger Binding="{Binding Success, StringFormat={}{0:F3}}"
                                                              Value="False">
                                                     <Setter TargetName="bd"
                                                             Property="Background"
@@ -209,8 +201,12 @@
                                         </DataTemplate>
                                     </DataGridTemplateColumn.CellTemplate>
                                 </DataGridTemplateColumn>
-                                <DataGridTextColumn Header="压力"
-                                                    Binding="{Binding Pressure, StringFormat={}{0:F3}}"
+                                <DataGridTextColumn Header="下相机工具坐标"
+                                                    Binding="{Binding DownCameraCoord1}"
+                                                    Width="auto"
+                                                    IsReadOnly="True" />
+                                <DataGridTextColumn Header="实际放置坐标"
+                                                    Binding="{Binding PlaceActualCoord}"
                                                     Width="auto"
                                                     IsReadOnly="True" />
                                 <DataGridTextColumn Header="详情"

+ 164 - 0
TeamAAS-VM/Views/Statistics/AssemblyRecordQuery.xaml

@@ -0,0 +1,164 @@
+<UserControl x:Class="TeamAAS_VP.Views.Statistics.AssemblyRecordQuery"
+             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:AssemblyRecordQueryViewModel}"
+             d:Height="600"
+             d:Width="1024"
+             d:Background="White">
+    <Grid Margin="8">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="*" />
+            <RowDefinition Height="Auto" />
+        </Grid.RowDefinitions>
+        <!--标题-->
+        <TextBlock Text="组装记录查询"
+                   FontSize="20"
+                   FontWeight="Bold"
+                   Margin="0,0,0,12" />
+
+        <StackPanel Grid.Row="1"
+                    Orientation="Horizontal"
+                    VerticalAlignment="Center"
+                    Margin="0,0,0,8">
+            <TextBlock Text="产品:"
+                       VerticalAlignment="Center"
+                       Margin="0,0,8,0" />
+            <ComboBox MinWidth="100"
+                      ItemsSource="{Binding AllProduct}"
+                      DisplayMemberPath="Name"
+                      SelectedItem="{Binding SelectedProduct}" />
+
+            <TextBlock Text="产品编号:"
+                       VerticalAlignment="Center"
+                       Margin="16,0,8,0" />
+            <TextBox MinWidth="150"
+                     Text="{Binding ProductNumber, UpdateSourceTrigger=PropertyChanged}" />
+
+            <TextBlock Text="开始:"
+                       VerticalAlignment="Center"
+                       Margin="16,0,8,0" />
+            <DatePicker SelectedDate="{Binding StartDate}"
+                        MinWidth="50" />
+            <TextBlock Text="结束:"
+                       VerticalAlignment="Center"
+                       Margin="8,0,8,0" />
+            <DatePicker SelectedDate="{Binding EndDate}"
+                        MinWidth="50" />
+
+            <Button Content="查询"
+                    Command="{Binding SearchCommand}"
+                    Margin="12,0,0,0" />
+            <Button Content="导出当前页"
+                    Command="{Binding ExportCommand}"
+                    Margin="6,0,0,0" />
+        </StackPanel>
+
+        <DataGrid Grid.Row="2"
+                  ItemsSource="{Binding Results}"
+                  AutoGenerateColumns="False"
+                  CanUserAddRows="False"
+                  IsReadOnly="True">
+            <DataGrid.Columns>
+                <DataGridTextColumn Header="Id"
+                                    Binding="{Binding Id}"
+                                    Width="50" />
+                <DataGridTextColumn Header="时间"
+                                    Binding="{Binding Timestamp, StringFormat=yyyy-MM-dd HH:mm:ss}" />
+                <DataGridTextColumn Header="配方名称"
+                                    Binding="{Binding RecipeName}"/>
+                <DataGridTextColumn Header="产品序列号"
+                                    Binding="{Binding ProductSN}"/>
+                <DataGridTextColumn Header="零件序列号"
+                                    Binding="{Binding PartSN}" />
+                <DataGridTextColumn Header="是否成功"
+                                    Binding="{Binding Success}"/>
+                <DataGridTextColumn Header="组装压力"
+                                    Binding="{Binding AssemblyPressure}" />
+                <DataGridTextColumn Header="用时(秒)"
+                                    Binding="{Binding AssemblyDurationSeconds}"/>
+                <DataGridTextColumn Header="下相机坐标1"
+                                    Binding="{Binding DownCameraCoord1}"/>
+                <DataGridTextColumn Header="下相机坐标2"
+                                    Binding="{Binding DownCameraCoord2}"/>
+                <DataGridTextColumn Header="下相机坐标3"
+                                    Binding="{Binding DownCameraCoord3}"/>
+                <DataGridTextColumn Header="下相机坐标4"
+                                    Binding="{Binding DownCameraCoord4}"/>
+                <DataGridTextColumn Header="抓取运动坐标"
+                                    Binding="{Binding PickMoveCameraCoord}"/>
+                <DataGridTextColumn Header="放置运动坐标1"
+                                    Binding="{Binding PlaceMoveCameraCoord1}"/>
+                <DataGridTextColumn Header="放置运动坐标2"
+                                    Binding="{Binding PlaceMoveCameraCoord2}"/>
+                <DataGridTextColumn Header="放置运动坐标3"
+                                    Binding="{Binding PlaceMoveCameraCoord3}"/>
+                <DataGridTextColumn Header="放置运动坐标4"
+                                    Binding="{Binding PlaceMoveCameraCoord4}"/>
+                <DataGridTextColumn Header="实际放置坐标"
+                                    Binding="{Binding PlaceActualCoord}"/>
+                <DataGridTextColumn Header="设备名称"
+                                    Binding="{Binding DeviceName}" />
+                <DataGridTextColumn Header="设备Id"
+                                    Binding="{Binding DeviceId}"/>
+                <DataGridTextColumn Header="操作员"
+                                    Binding="{Binding OperatorName}"/>
+                <DataGridTextColumn Header="备注"
+                                    Binding="{Binding Remark}"/>
+            </DataGrid.Columns>
+        </DataGrid>
+        <Button Margin="16"
+                HorizontalAlignment="Left"
+                VerticalAlignment="Center"
+                Grid.RowSpan="3"
+                Command="{x:Static materialDesign:Transitioner.MovePreviousCommand}"
+                Style="{StaticResource MaterialDesignFloatingActionMiniButton}">
+            <materialDesign:PackIcon Kind="StepBackward" />
+        </Button>
+        <Button Margin="16"
+                HorizontalAlignment="Right"
+                VerticalAlignment="Center"
+                Grid.RowSpan="3"
+                Command="{x:Static materialDesign:Transitioner.MoveNextCommand}"
+                Style="{StaticResource MaterialDesignFloatingActionSecondaryButton}">
+            <materialDesign:PackIcon Kind="StepForward" />
+        </Button>
+        <StackPanel Grid.Row="3"
+                    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/AssemblyRecordQuery.xaml.cs

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

+ 7 - 5
TeamAAS-VM/Views/Statistics/LockResultRecoredQuery.xaml

@@ -33,7 +33,7 @@
             <TextBlock Text="产品:"
                        VerticalAlignment="Center"
                        Margin="0,0,8,0" />
-            <ComboBox Width="200"
+            <ComboBox MinWidth="100"
                       ItemsSource="{Binding AllProduct}"
                       DisplayMemberPath="Name"
                       SelectedItem="{Binding SelectedProduct}" />
@@ -41,23 +41,25 @@
             <TextBlock Text="产品编号:"
                        VerticalAlignment="Center"
                        Margin="16,0,8,0" />
-            <TextBox Width="150"
+            <TextBox MinWidth="150"
                      Text="{Binding ProductNumber, UpdateSourceTrigger=PropertyChanged}" />
 
             <TextBlock Text="螺丝编号:"
                        VerticalAlignment="Center"
                        Margin="16,0,8,0" />
-            <TextBox Width="80"
+            <TextBox MinWidth="80"
                      Text="{Binding ScrewNumber, UpdateSourceTrigger=PropertyChanged}" />
 
             <TextBlock Text="开始:"
                        VerticalAlignment="Center"
                        Margin="16,0,8,0" />
-            <DatePicker SelectedDate="{Binding StartDate}" />
+            <DatePicker SelectedDate="{Binding StartDate}"
+                        MinWidth="50" />
             <TextBlock Text="结束:"
                        VerticalAlignment="Center"
                        Margin="8,0,8,0" />
-            <DatePicker SelectedDate="{Binding EndDate}" />
+            <DatePicker SelectedDate="{Binding EndDate}"
+                        MinWidth="50" />
 
             <Button Content="查询"
                     Command="{Binding SearchCommand}"

+ 190 - 0
TeamAAS-VM/Views/Statistics/PhotoCaptureQuery.xaml

@@ -0,0 +1,190 @@
+<UserControl x:Class="TeamAAS_VP.Views.Statistics.PhotoCaptureQuery"
+             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:PhotoCaptureQueryViewModel}"
+             d:Height="600"
+             d:Width="1024"
+             d:Background="White">
+    <b:Interaction.Triggers>
+        <b:EventTrigger EventName="Loaded">
+            <b:InvokeCommandAction Command="{Binding LoadedCommand}" />
+        </b:EventTrigger>
+    </b:Interaction.Triggers>
+    <Grid Margin="8">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="*" />
+            <RowDefinition Height="Auto" />
+        </Grid.RowDefinitions>
+        <!--标题-->
+        <TextBlock Text="拍照记录查询"
+                   FontSize="20"
+                   FontWeight="Bold"
+                   Margin="0,0,0,12" />
+
+        <StackPanel Grid.Row="1"
+                    Orientation="Horizontal"
+                    Margin="0,0,0,8">
+            <TextBlock Text="产品"
+                       VerticalAlignment="Center"
+                       Margin="0,0,8,0" />
+            <ComboBox MinWidth="100"
+                      ItemsSource="{Binding AllProduct}"
+                      DisplayMemberPath="Name"
+                      SelectedItem="{Binding SelectedProduct}" />
+            <TextBlock Text="相机"
+                       VerticalAlignment="Center"
+                       Margin="16,0,8,0" />
+            <ComboBox MinWidth="100"
+                      ItemsSource="{Binding AllCamera}"
+                      DisplayMemberPath="CameraName"
+                      SelectedItem="{Binding SelectedCamera}" />
+            <TextBlock Text="产品SN"
+                       VerticalAlignment="Center"
+                       Margin="16,0,8,0" />
+            <TextBox Width="150"
+                     Text="{Binding ProductSN}" />
+            <TextBlock Text="开始"
+                       VerticalAlignment="Center"
+                       Margin="16,0,8,0" />
+            <DatePicker SelectedDate="{Binding StartDate}"
+                        MinWidth="100" />
+            <TextBlock Text="结束"
+                       VerticalAlignment="Center"
+                       Margin="8,0,8,0" />
+            <DatePicker SelectedDate="{Binding EndDate}"
+                        MinWidth="100" />
+            <Button Content="查询"
+                    Command="{Binding SearchCommand}"
+                    Margin="12,0,0,0" />
+            <Button Content="导出当前页"
+                    Command="{Binding ExportCommand}"
+                    Margin="6,0,0,0" />
+        </StackPanel>
+        <DataGrid Grid.Row="2"
+                  ItemsSource="{Binding Results}"
+                  AutoGenerateColumns="False"
+                  CanUserAddRows="False"
+                  IsReadOnly="True">
+            <DataGrid.Columns>
+                <DataGridTextColumn Header="Id"
+                                    Binding="{Binding Id}"
+                                    Width="50" />
+                <DataGridTextColumn Header="CaptureTime"
+                                    Binding="{Binding CaptureTime, StringFormat=yyyy-MM-dd HH:mm:ss}"/>
+                <DataGridTextColumn Header="配方名"
+                                    Binding="{Binding RecipeName}" />
+                <DataGridTextColumn Header="流程名"
+                                    Binding="{Binding ProcessName}" />
+                <DataGridTextColumn Header="相机名"
+                                    Binding="{Binding CameraName}" />
+                <DataGridTextColumn Header="产品SN"
+                                    Binding="{Binding ProductSN}" />
+                <DataGridTextColumn Header="拍照编号"
+                                    Binding="{Binding PositionIndex}" />
+                <!-- Changed to TemplateColumn to show OK/NG with color -->
+                <DataGridTemplateColumn Header="是否成功"
+                                        Width="85">
+                    <DataGridTemplateColumn.CellTemplate>
+                        <DataTemplate>
+                            <Border CornerRadius="4"
+                                    Padding="2"
+                                    HorizontalAlignment="Center"
+                                    VerticalAlignment="Center">
+                                <TextBlock HorizontalAlignment="Center"
+                                           VerticalAlignment="Center"
+                                           Foreground="White"
+                                           FontWeight="Bold"
+                                           TextAlignment="Center">
+                                    <TextBlock.Style>
+                                        <Style TargetType="TextBlock">
+                                            <Setter Property="Text"
+                                                    Value="OK" />
+                                            <Setter Property="Background"
+                                                    Value="Green" />
+                                            <Style.Triggers>
+                                                <DataTrigger Binding="{Binding IsSuccess}"
+                                                             Value="False">
+                                                    <Setter Property="Text"
+                                                            Value="NG" />
+                                                    <Setter Property="Background"
+                                                            Value="Red" />
+                                                </DataTrigger>
+                                            </Style.Triggers>
+                                        </Style>
+                                    </TextBlock.Style>
+                                </TextBlock>
+                            </Border>
+                        </DataTemplate>
+                    </DataGridTemplateColumn.CellTemplate>
+                </DataGridTemplateColumn>
+                <DataGridTextColumn Header="机器人位置"
+                                    Binding="{Binding RobotPosition}" />
+                <DataGridTextColumn Header="像素坐标"
+                                    Binding="{Binding PixelPosition}"/>
+                <DataGridTextColumn Header="绝对坐标"
+                                    Binding="{Binding AbsolutePosition}" />
+                <DataGridTextColumn Header="图片路径"
+                                    Binding="{Binding ImagePath}" />
+                <DataGridTextColumn Header="操作员"
+                                    Binding="{Binding OperatorName}"/>
+                <DataGridTextColumn Header="备注"
+                                    Binding="{Binding Remarks}" />
+            </DataGrid.Columns>
+        </DataGrid>
+        <StackPanel Grid.Row="3"
+                    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>
+
+        <Button Margin="16"
+                HorizontalAlignment="Left"
+                VerticalAlignment="Center"
+                Grid.RowSpan="3"
+                Command="{x:Static materialDesign:Transitioner.MovePreviousCommand}"
+                Style="{StaticResource MaterialDesignFloatingActionMiniButton}">
+            <materialDesign:PackIcon Kind="StepBackward" />
+        </Button>
+        <Button Margin="16"
+                HorizontalAlignment="Right"
+                VerticalAlignment="Center"
+                Grid.RowSpan="3"
+                Command="{x:Static materialDesign:Transitioner.MoveNextCommand}"
+                Style="{StaticResource MaterialDesignFloatingActionSecondaryButton}">
+            <materialDesign:PackIcon Kind="StepForward" />
+        </Button>
+    </Grid>
+</UserControl>

+ 28 - 0
TeamAAS-VM/Views/Statistics/PhotoCaptureQuery.xaml.cs

@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+
+namespace TeamAAS_VP.Views.Statistics
+{
+    /// <summary>
+    /// PhotoCaptureQuery.xaml 的交互逻辑
+    /// </summary>
+    public partial class PhotoCaptureQuery : UserControl
+    {
+        public PhotoCaptureQuery()
+        {
+            InitializeComponent();
+        }
+    }
+}

+ 142 - 0
TeamAAS-VM/Views/Statistics/ProductionRecordQuery.xaml

@@ -0,0 +1,142 @@
+<UserControl x:Class="TeamAAS_VP.Views.Statistics.ProductionRecordQuery"
+             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:ProductionRecordQueryViewModel}"
+             d:Height="600"
+             d:Width="1024"
+             d:Background="White">
+    <b:Interaction.Triggers>
+        <b:EventTrigger EventName="Loaded">
+            <b:InvokeCommandAction Command="{Binding LoadedCommand}" />
+        </b:EventTrigger>
+    </b:Interaction.Triggers>
+    <Grid Margin="8">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="*" />
+            <RowDefinition Height="Auto" />
+        </Grid.RowDefinitions>
+        <!--标题-->
+        <TextBlock Text="生产记录查询"
+                   FontSize="20"
+                   FontWeight="Bold"
+                   Margin="0,0,0,12" />
+
+        <StackPanel Grid.Row="1"
+                    Orientation="Horizontal"
+                    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" />
+            <ComboBox Width="150"
+                      ItemsSource="{Binding Categories}"
+                      SelectedItem="{Binding SelectedCategory}" />
+            <TextBlock Text="开始"
+                       VerticalAlignment="Center"
+                       Margin="16,0,8,0" />
+            <DatePicker SelectedDate="{Binding StartDate}"
+                        MinWidth="100" />
+            <TextBlock Text="结束"
+                       VerticalAlignment="Center"
+                       Margin="8,0,8,0" />
+            <DatePicker SelectedDate="{Binding EndDate}"
+                        MinWidth="100" />
+            <Button Content="查询"
+                    Command="{Binding SearchCommand}"
+                    Margin="12,0,0,0" />
+            <Button Content="导出当前页"
+                    Command="{Binding ExportCommand}"
+                    Margin="6,0,0,0" />
+        </StackPanel>
+        <DataGrid Grid.Row="2"
+                  ItemsSource="{Binding Results}"
+                  AutoGenerateColumns="False"
+                  CanUserAddRows="False"
+                  IsReadOnly="True">
+            <DataGrid.Columns>
+                <DataGridTextColumn Header="时间"
+                                    Binding="{Binding Timestamp, StringFormat=yyyy-MM-dd HH:mm:ss}" />
+                <DataGridTextColumn Header="Id"
+                                    Binding="{Binding Id}"
+                                    Width="50" />
+                <DataGridTextColumn Header="产品"
+                                    Binding="{Binding ProductName}" />
+                <DataGridTextColumn Header="产品编码"
+                                    Binding="{Binding ProductCode}" />
+                <DataGridTextColumn Header="设备Id"
+                                    Binding="{Binding DeviceId}" />
+                <DataGridTextColumn Header="设备名称"
+                                    Binding="{Binding DeviceName}" />
+                <DataGridTextColumn Header="类别"
+                                    Binding="{Binding Category}" />
+                <DataGridTextColumn Header="数量"
+                                    Binding="{Binding Quantity}" />
+                <DataGridTextColumn Header="操作员"
+                                    Binding="{Binding UserName}" />
+                <DataGridTextColumn Header="备注"
+                                    Binding="{Binding Remark}" />
+            </DataGrid.Columns>
+        </DataGrid>
+        <StackPanel Grid.Row="3"
+                    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>
+
+        <Button Margin="16"
+                HorizontalAlignment="Left"
+                VerticalAlignment="Center"
+                Grid.RowSpan="3"
+                Command="{x:Static materialDesign:Transitioner.MovePreviousCommand}"
+                Style="{StaticResource MaterialDesignFloatingActionMiniButton}">
+            <materialDesign:PackIcon Kind="StepBackward" />
+        </Button>
+        <Button Margin="16"
+                HorizontalAlignment="Right"
+                VerticalAlignment="Center"
+                Grid.RowSpan="3"
+                Command="{x:Static materialDesign:Transitioner.MoveNextCommand}"
+                Style="{StaticResource MaterialDesignFloatingActionSecondaryButton}">
+            <materialDesign:PackIcon Kind="StepForward" />
+        </Button>
+    </Grid>
+</UserControl>

+ 28 - 0
TeamAAS-VM/Views/Statistics/ProductionRecordQuery.xaml.cs

@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+
+namespace TeamAAS_VP.Views.Statistics
+{
+    /// <summary>
+    /// ProductionRecordQuery.xaml 的交互逻辑
+    /// </summary>
+    public partial class ProductionRecordQuery : UserControl
+    {
+        public ProductionRecordQuery()
+        {
+            InitializeComponent();
+        }
+    }
+}

+ 6 - 2
TeamAAS-VM/Views/StatisticsView.xaml

@@ -26,8 +26,12 @@
                 <local:ProductionStatement />
             </materialDesign:TransitionerSlide>
 
-            <local:LockResultRecoredQuery />
-            
+            <local:AssemblyRecordQuery />
+
+            <local:PhotoCaptureQuery />
+
+            <local:ProductionRecordQuery />
+
             <local:StatisticalQuery />
 
             <local:AlarmQuery />