Просмотр исходного кода

1,增加扫码枪
2,增加示意图显示被测量点位的匹配关系

刘锦天 6 месяцев назад
Родитель
Сommit
5809f89824

+ 1 - 0
TeamAAS-VM/App.xaml.cs

@@ -85,6 +85,7 @@ namespace TeamAAS_VP
             containerRegistry.RegisterForNavigation<ProductsView>();
             containerRegistry.RegisterForNavigation<DebugView>();
             containerRegistry.RegisterForNavigation<SettingView>();
+            containerRegistry.RegisterForNavigation<ImageView>();
             containerRegistry.RegisterForNavigation<CalibrationView>();
             containerRegistry.RegisterForNavigation<StatisticsView>();
             containerRegistry.RegisterForNavigation<UserView>();

+ 141 - 17
TeamAAS-VM/Core/Management.cs

@@ -95,6 +95,8 @@ namespace TeamAAS_VP.Core
         #endregion
 
         #region 属性
+        private Guid _defaultScannerId = Guid.Empty;
+        private volatile TaskCompletionSource<string> _scanTcs; // 最简单:只支持单通道“当前一次扫
         public string[] CsvData = new string[50];
         public int[] ResultData = new int[200];
         public string code = "";
@@ -783,8 +785,15 @@ namespace TeamAAS_VP.Core
                 var scanner = await _scannerService.GetScannerAsync(scannerid);
                 //从服务器收到信息。但是一般byteBlock和requestInfo会根据适配器呈现不同的值。
                 var buffer = e.ByteBlock.Span.ToString(scanner.GetEncoding());
-                SendTaskMessage($"{scanner.Name}Received:{buffer}", MessageLevel.Info);
-
+                var reply = (buffer ?? string.Empty).Trim();
+                SendTaskMessage($"{scanner.Name}Received:{reply}", MessageLevel.Info);
+                // 只要有人在等扫码结果,就先完成等待(避免被下面 ExecuteAsync 干扰)
+                var tcs = _scanTcs;
+                if (tcs != null && !string.IsNullOrWhiteSpace(reply))
+                {
+                    // 只完成一次
+                    tcs.TrySetResult(reply);
+                }
                 var _ = _remoteCommandService.ExecuteAsync(buffer, ScannerTCPCollection[scannerid].Token, async msg =>
                 {
                     try
@@ -814,7 +823,7 @@ namespace TeamAAS_VP.Core
             }
             catch (Exception ex)
             {
-                LogHelper.WriteLogError("处理机器人接收的命令时出错!", ex);
+                LogHelper.WriteLogError("处理扫码枪接收的命令时出错!", ex);
                 SendTaskMessage(ex.Message, MessageLevel.Alarm);
             }
         }
@@ -1697,7 +1706,23 @@ namespace TeamAAS_VP.Core
                                 else
                                 {
 
-                                    code = visionResult.X.ToUpper();
+                                    // 发T1并等待回包:回包是 SN 或 NG
+                                    var reply = await ScanT1Async(timeoutMs: 3000);
+                                    if (string.IsNullOrWhiteSpace(reply))
+                                    {
+                                        SendTaskMessage("扫码枪回包超时/空数据", MessageLevel.Alarm);
+                                        await plc.WriteNodeAsync(addressConfig.Out_FixedCameraPickStatus.Address, (short)3);
+                                        return;
+                                    }
+
+                                    if (string.Equals(reply, "NG", StringComparison.OrdinalIgnoreCase))
+                                    {
+                                        SendTaskMessage("扫码枪返回NG", MessageLevel.Alarm);
+                                        await plc.WriteNodeAsync(addressConfig.Out_FixedCameraPickStatus.Address, (short)3);
+                                        return;
+                                    }
+
+                                    code = reply.ToUpperInvariant();
 
                                     SendTaskMessage($"扫码成功,SN:{code}", MessageLevel.Info);
                                     //这里缺少从MES获取产品配方的内容,需要补充
@@ -1814,11 +1839,82 @@ namespace TeamAAS_VP.Core
                                 {
                                     await _remoteCommandService.SetLightBeforePhoto(selectedProcedure);
                                 }
-                                var visionResult = await _remoteCommandService.ExecutePhotoGetSinglePoint2(selectedProcedure, null, null, ctsAll.Token);
-                                if (visionResult.IsSucceed)
+                                if(sysConfig.ScannerUse==false)
+                                {
+                                    var visionResult = await _remoteCommandService.ExecutePhotoGetSinglePoint2(selectedProcedure, null, null, ctsAll.Token);
+                                    if (visionResult.IsSucceed)
+                                    {
+                                        code = visionResult.X;
+                                        ProductMainSN = visionResult.X;
+                                        SendTaskMessage($"扫码成功,SN:{code}", MessageLevel.Info);
+                                        //这里缺少从MES获取产品配方的内容,需要补充
+                                        _LastMesCheckTime[0] = (await _mesService.GetServerTimeAsync(code, 2)).Now;
+                                        //_LastMesCheckTime[1] = DateTime.Now;
+                                        LogHelper.WriteLogMes($"【mes开始QUERY】");
+                                        SendTaskMessage($"mes开始QUERY", MessageLevel.Info);
+                                        (bool isSuccess, string response) = await _mesService.StationGetAsync(code, "", 2);//询问mes
+
+                                        if (isSuccess)
+                                        {
+                                            //产品发生改变并且供料器发生了变化时,需要通知PLC进行取料更改
+
+                                            LogHelper.WriteLogMes($"询问mes成功");
+                                            SendTaskMessage($"询问mes成功:{response}", MessageLevel.Debug);
+                                            if (response != "MES功能未启用!")
+                                            {
+                                                string[] items = response.Split('\n');
+                                                if (items[3].Contains("sn="))
+                                                {
+                                                    ProductMainSN = items[3].Replace("sn=", "");
+                                                    _productService.CurrentProductCode = ProductMainSN;
+                                                }
+                                                SendTaskMessage($"产品主sn:{ProductMainSN}", MessageLevel.Info);
+                                                if (items[5].Contains("wo="))
+                                                {
+                                                    wo = items[5].Replace("wo=", "");
+                                                }
+                                                SendTaskMessage($"产品wo:{wo}", MessageLevel.Info);
+                                            }
+                                            LogHelper.WriteLogMes($"【mes结束QUERY】");
+                                            SendTaskMessage($"mes结束QUERY", MessageLevel.Info);
+                                            await plc.WriteNodeAsync(addressConfig.Out_FixedCameraPickStatus.Address, (Int16)1);
+                                            return;
+                                        }
+                                        else
+                                        {
+                                            //产品发生改变并且供料器发生了变化时,需要通知PLC进行取料更改
+
+                                            await plc.WriteNodeAsync(addressConfig.Out_FixedCameraPickStatus.Address, (Int16)3);
+
+                                            LogHelper.WriteLogMes($"询问mes失败");
+                                            SendTaskMessage($"询问mes失败:{response}", MessageLevel.Error);
+                                        }
+                                    }
+                                    else
+                                    {
+                                        SendTaskMessage($"扫码失败", MessageLevel.Alarm);
+                                        await plc.WriteNodeAsync(addressConfig.Out_FixedCameraPickStatus.Address, (Int16)3);
+                                    }
+                                }
+                                else
                                 {
-                                    code = visionResult.X;
-                                    ProductMainSN = visionResult.X;
+                                    // 发T1并等待回包:回包是 SN 或 NG
+                                    var reply = await ScanT1Async(timeoutMs: 3000);
+                                    if (string.IsNullOrWhiteSpace(reply))
+                                    {
+                                        SendTaskMessage("扫码枪回包超时/空数据", MessageLevel.Alarm);
+                                        await plc.WriteNodeAsync(addressConfig.Out_FixedCameraPickStatus.Address, (short)3);
+                                        return;
+                                    }
+
+                                    if (string.Equals(reply, "NG", StringComparison.OrdinalIgnoreCase))
+                                    {
+                                        SendTaskMessage("扫码枪返回NG", MessageLevel.Alarm);
+                                        await plc.WriteNodeAsync(addressConfig.Out_FixedCameraPickStatus.Address, (short)3);
+                                        return;
+                                    }
+
+                                    code = reply.ToUpperInvariant();
                                     SendTaskMessage($"扫码成功,SN:{code}", MessageLevel.Info);
                                     //这里缺少从MES获取产品配方的内容,需要补充
                                     _LastMesCheckTime[0] = (await _mesService.GetServerTimeAsync(code, 2)).Now;
@@ -1863,17 +1959,8 @@ namespace TeamAAS_VP.Core
                                         SendTaskMessage($"询问mes失败:{response}", MessageLevel.Error);
                                     }
                                 }
-                                else
-                                {
-                                    SendTaskMessage($"扫码失败", MessageLevel.Alarm);
-                                    await plc.WriteNodeAsync(addressConfig.Out_FixedCameraPickStatus.Address, (Int16)3);
-                                }
-
                                 return;
                             }
-
-                            
-
                             foreach (var prcId in procIds)
                             {
                                 if (Is3D(prcId))
@@ -2899,6 +2986,43 @@ namespace TeamAAS_VP.Core
 
         //    return true;
         //}
+        // ====== 新增:最简等待:TCS + 超时(比 SemaphoreSlim + CurrentCount 轮询更轻、更可靠)======
+        private async Task<string> ScanT1Async(int timeoutMs)
+        {
+            var scannerId = GetDefaultScannerId();
+            if (scannerId == Guid.Empty) return null;
+
+            var scanner = _scannerService.GetScanner(scannerId);
+            if (scanner == null || !scanner.IsConnected) return null;
 
+            // 关键:先注册等待,再发送 T1,避免“回包先到”导致丢信号
+            var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
+            _scanTcs = tcs;
+
+            await scanner.SendAsync("T1").ConfigureAwait(false);
+
+            var completed = await Task.WhenAny(tcs.Task, Task.Delay(timeoutMs)).ConfigureAwait(false);
+            if (completed != tcs.Task)
+            {
+                // 超时:清掉本次等待,避免下次误用
+                if (ReferenceEquals(_scanTcs, tcs)) _scanTcs = null;
+                return null;
+            }
+
+            var reply = await tcs.Task.ConfigureAwait(false);
+            if (ReferenceEquals(_scanTcs, tcs)) _scanTcs = null;
+            return reply;
+        }
+        private Guid GetDefaultScannerId()
+        {
+            if (_defaultScannerId != Guid.Empty) return _defaultScannerId;
+
+            var scannerInfo = _configService.GetAllScanners()
+                .OrderBy(s => s.ScannerNo)
+                .FirstOrDefault();
+
+            _defaultScannerId = scannerInfo == null ? Guid.Empty : scannerInfo.Id;
+            return _defaultScannerId;
+        }
     }
 }

+ 18 - 0
TeamAAS-VM/Resources/Languages/Lang.Designer.cs

@@ -2904,6 +2904,24 @@ namespace TeamAAS_VP.Resources.Languages {
             }
         }
         
+        /// <summary>
+        ///   查找类似 扫码枪 的本地化字符串。
+        /// </summary>
+        public static string 扫码枪 {
+            get {
+                return ResourceManager.GetString("扫码枪", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   查找类似 扫码枪列表 的本地化字符串。
+        /// </summary>
+        public static string 扫码枪列表 {
+            get {
+                return ResourceManager.GetString("扫码枪列表", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   查找类似 抛料数 的本地化字符串。
         /// </summary>

+ 6 - 0
TeamAAS-VM/Resources/Languages/Lang.en.resx

@@ -2047,4 +2047,10 @@
   <data name="是否保存扫码枪" type="System.Resources.ResXNullRef, System.Windows.Forms">
     <value />
   </data>
+  <data name="扫码枪" type="System.Resources.ResXNullRef, System.Windows.Forms">
+    <value />
+  </data>
+  <data name="扫码枪列表" type="System.Resources.ResXNullRef, System.Windows.Forms">
+    <value />
+  </data>
 </root>

+ 6 - 0
TeamAAS-VM/Resources/Languages/Lang.ja-JP.resx

@@ -2047,4 +2047,10 @@
   <data name="是否保存扫码枪" type="System.Resources.ResXNullRef, System.Windows.Forms">
     <value />
   </data>
+  <data name="扫码枪" type="System.Resources.ResXNullRef, System.Windows.Forms">
+    <value />
+  </data>
+  <data name="扫码枪列表" type="System.Resources.ResXNullRef, System.Windows.Forms">
+    <value />
+  </data>
 </root>

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

@@ -2195,4 +2195,12 @@
     <value>是否保存扫码枪</value>
     <comment>是否保存扫码枪</comment>
   </data>
+  <data name="扫码枪" xml:space="preserve">
+    <value>扫码枪</value>
+    <comment>扫码枪</comment>
+  </data>
+  <data name="扫码枪列表" xml:space="preserve">
+    <value>扫码枪列表</value>
+    <comment>扫码枪列表</comment>
+  </data>
 </root>

+ 6 - 0
TeamAAS-VM/Resources/Languages/Lang.zh-TW.resx

@@ -2047,4 +2047,10 @@
   <data name="是否保存扫码枪" type="System.Resources.ResXNullRef, System.Windows.Forms">
     <value />
   </data>
+  <data name="扫码枪" type="System.Resources.ResXNullRef, System.Windows.Forms">
+    <value />
+  </data>
+  <data name="扫码枪列表" type="System.Resources.ResXNullRef, System.Windows.Forms">
+    <value />
+  </data>
 </root>

+ 18 - 1
TeamAAS-VM/TeamAAS-VP.csproj

@@ -585,6 +585,7 @@
     <Compile Include="Services\MesService.cs" />
     <Compile Include="Services\ScannerService.cs" />
     <Compile Include="ValueConverter\ImageFileNameFormatConverter.cs" />
+    <Compile Include="ViewModels\ImageViewModel.cs" />
     <Compile Include="ViewModels\Product\PlcPointParamsViewModel.cs" />
     <Compile Include="ViewModels\Product\VisionDynamicAccuracyAnalyzerViewModel.cs" />
     <Compile Include="ViewModels\Setting\AddLightControllerViewModel.cs" />
@@ -956,9 +957,15 @@
     <Compile Include="Views\DebugView.xaml.cs">
       <DependentUpon>DebugView.xaml</DependentUpon>
     </Compile>
+    <Compile Include="Views\Dialogs\TextInputDialog.xaml.cs">
+      <DependentUpon>TextInputDialog.xaml</DependentUpon>
+    </Compile>
     <Compile Include="Views\Home\ShowVisionRender.xaml.cs">
       <DependentUpon>ShowVisionRender.xaml</DependentUpon>
     </Compile>
+    <Compile Include="Views\ImageView.xaml.cs">
+      <DependentUpon>ImageView.xaml</DependentUpon>
+    </Compile>
     <Compile Include="Views\ProductsView.xaml.cs">
       <DependentUpon>ProductsView.xaml</DependentUpon>
     </Compile>
@@ -1265,6 +1272,10 @@
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>
     </Page>
+    <Page Include="Views\Dialogs\TextInputDialog.xaml">
+      <SubType>Designer</SubType>
+      <Generator>MSBuild:Compile</Generator>
+    </Page>
     <Page Include="Views\HomeView.xaml">
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>
@@ -1273,6 +1284,10 @@
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>
     </Page>
+    <Page Include="Views\ImageView.xaml">
+      <SubType>Designer</SubType>
+      <Generator>MSBuild:Compile</Generator>
+    </Page>
     <Page Include="Views\MainWindowView.xaml">
       <Generator>MSBuild:Compile</Generator>
       <SubType>Designer</SubType>
@@ -1638,7 +1653,9 @@
       <Install>false</Install>
     </BootstrapperPackage>
   </ItemGroup>
-  <ItemGroup />
+  <ItemGroup>
+    <Folder Include="ViewModels\Dialogs\" />
+  </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
     <PropertyGroup>

+ 593 - 0
TeamAAS-VM/ViewModels/ImageViewModel.cs

@@ -0,0 +1,593 @@
+using Microsoft.Win32;
+using Prism.Commands;
+using Prism.Mvvm;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Shapes;
+using System.Xml.Serialization;
+using TeamAAS_VP.Views.Dialogs;
+
+namespace TeamAAS_VP.ViewModels
+{
+    public class ImageViewModel : BindableBase
+    {
+        private const string AppFolderName = "TeamAAS-VM";
+        private const string ImageFileName = "LastImage.png";
+        private const string XmlFileName = "LastAnno.xml";
+
+        private string _folder;
+        private string _imagePath;
+        private string _xmlPath;
+
+        private FrameworkElement _renderTarget;
+        private Canvas _canvas;
+
+        private Grid _lastArrowGroup;
+
+        public ImageViewModel()
+        {
+            InitPaths();
+
+            ImportImageCommand = new DelegateCommand(ImportImage);
+            SaveCommand = new DelegateCommand(Save);
+            ClearCommand = new DelegateCommand(Clear);
+
+            Tool = "Rect";
+            ColorName = "Red";
+            Thickness = 2;
+        }
+
+        #region Commands
+
+        public DelegateCommand ImportImageCommand { get; }
+        public DelegateCommand SaveCommand { get; }
+        public DelegateCommand ClearCommand { get; }
+
+        #endregion
+
+        #region Bindable Properties
+
+        private ImageSource _ImageSource;
+        public ImageSource ImageSource
+        {
+            get { return _ImageSource; }
+            set { SetProperty(ref _ImageSource, value); }
+        }
+
+        private int _PixelWidth;
+        public int PixelWidth
+        {
+            get { return _PixelWidth; }
+            set { SetProperty(ref _PixelWidth, value); }
+        }
+
+        private int _PixelHeight;
+        public int PixelHeight
+        {
+            get { return _PixelHeight; }
+            set { SetProperty(ref _PixelHeight, value); }
+        }
+
+        /// <summary>
+        /// Rect / Arrow / Text / Select
+        /// </summary>
+        private string _Tool;
+        public string Tool
+        {
+            get { return _Tool; }
+            set { SetProperty(ref _Tool, value); }
+        }
+
+        private string _ColorName;
+        public string ColorName
+        {
+            get { return _ColorName; }
+            set { SetProperty(ref _ColorName, value); }
+        }
+
+        private double _Thickness;
+        public double Thickness
+        {
+            get { return _Thickness; }
+            set { SetProperty(ref _Thickness, value); }
+        }
+
+        #endregion
+
+        #region Public API (called from ImageView.xaml.cs)
+
+        public void AttachRenderTarget(FrameworkElement renderTarget, Canvas canvas)
+        {
+            _renderTarget = renderTarget;
+            _canvas = canvas;
+        }
+
+        public void LoadLast()
+        {
+            try
+            {
+                if (File.Exists(_imagePath))
+                {
+                    var bmp = LoadBitmap(_imagePath);
+                    ImageSource = bmp;
+                    PixelWidth = bmp.PixelWidth;
+                    PixelHeight = bmp.PixelHeight;
+                }
+
+                if (_canvas != null && File.Exists(_xmlPath))
+                {
+                    var list = ReadXml<List<AnnoItem>>(_xmlPath);
+                    if (list != null)
+                    {
+                        _canvas.Children.Clear();
+                        foreach (var item in list)
+                        {
+                            RestoreItem(item);
+                        }
+                    }
+                }
+            }
+            catch
+            {
+            }
+        }
+
+        public Rectangle CreateRect()
+        {
+            return new Rectangle
+            {
+                Stroke = GetStrokeBrush(),
+                StrokeThickness = Thickness,
+                Fill = Brushes.Transparent
+            };
+        }
+
+        public void CreateTextAt(Canvas canvas, Point pixelPoint)
+        {
+            var dlg = new TextInputDialog();
+            dlg.Owner = Application.Current?.MainWindow;
+            if (dlg.ShowDialog() != true) return;
+
+            var input = dlg.InputText;
+            if (string.IsNullOrWhiteSpace(input)) return;
+
+            var tb = new TextBlock
+            {
+                Text = input,
+                Foreground = GetStrokeBrush(),
+                FontSize = 20,
+                Background = Brushes.Transparent
+            };
+
+            Canvas.SetLeft(tb, pixelPoint.X);
+            Canvas.SetTop(tb, pixelPoint.Y);
+            canvas.Children.Add(tb);
+        }
+
+        public Grid CreateArrowGroup(Point start, Point end)
+        {
+            var brush = GetStrokeBrush();
+
+            var line = new Line
+            {
+                X1 = start.X,
+                Y1 = start.Y,
+                X2 = end.X,
+                Y2 = end.Y,
+                Stroke = brush,
+                StrokeThickness = Thickness
+            };
+
+            var head = new Polygon
+            {
+                Fill = brush,
+                Stroke = brush,
+                StrokeThickness = 1
+            };
+
+            var group = new Grid();
+            group.Children.Add(line);
+            group.Children.Add(head);
+            group.Tag = line;
+
+            _lastArrowGroup = group;
+            UpdateArrowHead(line, head);
+
+            return group;
+        }
+
+        public void UpdateLastArrow(Canvas canvas, Point current)
+        {
+            if (_lastArrowGroup == null) return;
+
+            var line = _lastArrowGroup.Children.OfType<Line>().FirstOrDefault();
+            var head = _lastArrowGroup.Children.OfType<Polygon>().FirstOrDefault();
+            if (line == null || head == null) return;
+
+            line.X2 = current.X;
+            line.Y2 = current.Y;
+
+            UpdateArrowHead(line, head);
+        }
+
+        public void CommitLastArrow(Canvas canvas, Point end)
+        {
+            UpdateLastArrow(canvas, end);
+        }
+
+        #endregion
+
+        #region Command handlers
+
+        private void ImportImage()
+        {
+            var dlg = new OpenFileDialog();
+            dlg.Title = "导入图片";
+            dlg.Filter = "Image Files|*.png;*.jpg;*.jpeg;*.bmp|All Files|*.*";
+            if (dlg.ShowDialog() != true) return;
+
+            var bmp = LoadBitmap(dlg.FileName);
+            ImageSource = bmp;
+
+            PixelWidth = bmp.PixelWidth;
+            PixelHeight = bmp.PixelHeight;
+
+            _canvas?.Children.Clear();
+            _lastArrowGroup = null;
+        }
+
+        private void Clear()
+        {
+            _canvas?.Children.Clear();
+            _lastArrowGroup = null;
+        }
+
+        private void Save()
+        {
+            if (ImageSource == null)
+            {
+                MessageBox.Show("请先导入图片", "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk);
+                return;
+            }
+
+            if (_renderTarget == null || _canvas == null)
+            {
+                MessageBox.Show("保存失败:界面尚未初始化完成", "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk);
+                return;
+            }
+
+            if (PixelWidth <= 0 || PixelHeight <= 0)
+            {
+                MessageBox.Show("图片像素尺寸无效", "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk);
+                return;
+            }
+
+            try
+            {
+                var merged = RenderPixelElement(_renderTarget, PixelWidth, PixelHeight);
+                SavePng(merged, _imagePath);
+
+                var items = DumpAnnoItems();
+                WriteXml(_xmlPath, items);
+
+                MessageBox.Show("保存成功", "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk);
+            }
+            catch (Exception ex)
+            {
+                MessageBox.Show(ex.Message, "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk);
+            }
+        }
+
+        #endregion
+
+        #region Persistence (PNG + XML)
+
+        private void InitPaths()
+        {
+            _folder = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppFolderName);
+            Directory.CreateDirectory(_folder);
+
+            _imagePath = System.IO.Path.Combine(_folder, ImageFileName);
+            _xmlPath = System.IO.Path.Combine(_folder, XmlFileName);
+        }
+
+        private BitmapImage LoadBitmap(string path)
+        {
+            var bmp = new BitmapImage();
+            bmp.BeginInit();
+            bmp.CacheOption = BitmapCacheOption.OnLoad;
+            bmp.UriSource = new Uri(path, UriKind.Absolute);
+            bmp.EndInit();
+            bmp.Freeze();
+            return bmp;
+        }
+
+        private BitmapSource RenderPixelElement(FrameworkElement element, int pixelWidth, int pixelHeight)
+        {
+            element.Measure(new Size(pixelWidth, pixelHeight));
+            element.Arrange(new Rect(0, 0, pixelWidth, pixelHeight));
+            element.UpdateLayout();
+
+            var rtb = new RenderTargetBitmap(pixelWidth, pixelHeight, 96, 96, PixelFormats.Pbgra32);
+            rtb.Render(element);
+            rtb.Freeze();
+            return rtb;
+        }
+
+        private void SavePng(BitmapSource source, string path)
+        {
+            var encoder = new PngBitmapEncoder();
+            encoder.Frames.Add(BitmapFrame.Create(source));
+            using (var fs = File.Create(path))
+            {
+                encoder.Save(fs);
+            }
+        }
+
+        private void WriteXml<T>(string path, T data)
+        {
+            var ser = new XmlSerializer(typeof(T));
+            using (var fs = File.Create(path))
+            {
+                ser.Serialize(fs, data);
+            }
+        }
+
+        private T ReadXml<T>(string path) where T : class
+        {
+            var ser = new XmlSerializer(typeof(T));
+            using (var fs = File.OpenRead(path))
+            {
+                return ser.Deserialize(fs) as T;
+            }
+        }
+
+        #endregion
+
+        #region Dump/Restore items
+
+        private List<AnnoItem> DumpAnnoItems()
+        {
+            var list = new List<AnnoItem>();
+            if (_canvas == null) return list;
+
+            foreach (var child in _canvas.Children)
+            {
+                if (child is Rectangle r)
+                {
+                    list.Add(new AnnoItem
+                    {
+                        Type = "Rect",
+                        X = SafeGetLeft(r),
+                        Y = SafeGetTop(r),
+                        W = r.Width,
+                        H = r.Height,
+                        Color = BrushToColorString(r.Stroke),
+                        Thickness = r.StrokeThickness
+                    });
+                }
+                else if (child is TextBlock t)
+                {
+                    list.Add(new AnnoItem
+                    {
+                        Type = "Text",
+                        X = SafeGetLeft(t),
+                        Y = SafeGetTop(t),
+                        Text = t.Text,
+                        Color = BrushToColorString(t.Foreground),
+                        FontSize = t.FontSize
+                    });
+                }
+                else if (child is Grid g)
+                {
+                    var line = g.Children.OfType<Line>().FirstOrDefault();
+                    if (line != null)
+                    {
+                        list.Add(new AnnoItem
+                        {
+                            Type = "Arrow",
+                            X1 = line.X1,
+                            Y1 = line.Y1,
+                            X2 = line.X2,
+                            Y2 = line.Y2,
+                            Color = BrushToColorString(line.Stroke),
+                            Thickness = line.StrokeThickness
+                        });
+                    }
+                }
+            }
+
+            return list;
+        }
+
+        private void RestoreItem(AnnoItem item)
+        {
+            if (_canvas == null || item == null) return;
+
+            if (item.Type == "Rect")
+            {
+                var rect = new Rectangle
+                {
+                    Stroke = BrushFrom(item.Color),
+                    StrokeThickness = item.Thickness <= 0 ? 2 : item.Thickness,
+                    Fill = Brushes.Transparent,
+                    Width = item.W,
+                    Height = item.H
+                };
+                Canvas.SetLeft(rect, item.X);
+                Canvas.SetTop(rect, item.Y);
+                _canvas.Children.Add(rect);
+                return;
+            }
+
+            if (item.Type == "Text")
+            {
+                var tb = new TextBlock
+                {
+                    Text = item.Text ?? string.Empty,
+                    Foreground = BrushFrom(item.Color),
+                    FontSize = item.FontSize <= 0 ? 20 : item.FontSize,
+                    Background = Brushes.Transparent
+                };
+                Canvas.SetLeft(tb, item.X);
+                Canvas.SetTop(tb, item.Y);
+                _canvas.Children.Add(tb);
+                return;
+            }
+
+            if (item.Type == "Arrow")
+            {
+                var brush = BrushFrom(item.Color);
+
+                var line = new Line
+                {
+                    X1 = item.X1,
+                    Y1 = item.Y1,
+                    X2 = item.X2,
+                    Y2 = item.Y2,
+                    Stroke = brush,
+                    StrokeThickness = item.Thickness <= 0 ? 2 : item.Thickness
+                };
+
+                var head = new Polygon
+                {
+                    Fill = brush,
+                    Stroke = brush,
+                    StrokeThickness = 1
+                };
+
+                var group = new Grid();
+                group.Children.Add(line);
+                group.Children.Add(head);
+                group.Tag = line;
+
+                UpdateArrowHead(line, head);
+
+                _canvas.Children.Add(group);
+            }
+        }
+
+        #endregion
+
+        #region Helpers
+
+        private Brush GetStrokeBrush()
+        {
+            try
+            {
+                var obj = ColorConverter.ConvertFromString(ColorName);
+                if (obj != null)
+                {
+                    return new SolidColorBrush((Color)obj);
+                }
+            }
+            catch { }
+            return Brushes.Red;
+        }
+
+        private Brush BrushFrom(string color)
+        {
+            try
+            {
+                if (!string.IsNullOrWhiteSpace(color))
+                {
+                    var c = (Color)ColorConverter.ConvertFromString(color);
+                    return new SolidColorBrush(c);
+                }
+            }
+            catch { }
+            return Brushes.Red;
+        }
+
+        private string BrushToColorString(Brush brush)
+        {
+            if (brush is SolidColorBrush s)
+            {
+                return s.Color.ToString();
+            }
+            return Colors.Red.ToString();
+        }
+
+        private double SafeGetLeft(UIElement e)
+        {
+            var v = Canvas.GetLeft(e);
+            return double.IsNaN(v) ? 0 : v;
+        }
+
+        private double SafeGetTop(UIElement e)
+        {
+            var v = Canvas.GetTop(e);
+            return double.IsNaN(v) ? 0 : v;
+        }
+
+        private void UpdateArrowHead(Line line, Polygon head)
+        {
+            var dx = line.X2 - line.X1;
+            var dy = line.Y2 - line.Y1;
+            var len = Math.Sqrt(dx * dx + dy * dy);
+            if (len < 1) len = 1;
+
+            var ux = dx / len;
+            var uy = dy / len;
+
+            const double size = 12.0;
+            var px = -uy;
+            var py = ux;
+
+            var tip = new Point(line.X2, line.Y2);
+            var p1 = new Point(line.X2 - ux * size + px * (size / 2), line.Y2 - uy * size + py * (size / 2));
+            var p2 = new Point(line.X2 - ux * size - px * (size / 2), line.Y2 - uy * size - py * (size / 2));
+
+            head.Points.Clear();
+            head.Points.Add(tip);
+            head.Points.Add(p1);
+            head.Points.Add(p2);
+        }
+
+        #endregion
+
+        [Serializable]
+        public class AnnoItem
+        {
+            public string Type { get; set; }
+
+            public double X { get; set; }
+            public double Y { get; set; }
+            public double W { get; set; }
+            public double H { get; set; }
+            public string Text { get; set; }
+            public double FontSize { get; set; }
+
+            public double X1 { get; set; }
+            public double Y1 { get; set; }
+            public double X2 { get; set; }
+            public double Y2 { get; set; }
+
+            public string Color { get; set; }
+            public double Thickness { get; set; }
+        }
+
+        public void RemoveAnnotation(UIElement element)
+        {
+            if (element == null) return;
+            if (_canvas == null) return;
+
+            if (_canvas.Children.Contains(element))
+            {
+                _canvas.Children.Remove(element);
+            }
+
+            // 如果删的是最后一根箭头,清理引用避免后续更新异常
+            if (ReferenceEquals(_lastArrowGroup, element))
+            {
+                _lastArrowGroup = null;
+            }
+        }
+    }
+}

+ 8 - 3
TeamAAS-VM/ViewModels/MainWindowViewModel.cs

@@ -197,19 +197,24 @@ namespace TeamAAS_VP.ViewModels
                     NavigationIndex = 3;
                     _eventAggregator.GetEvent<MainTabSwitchNotification>().Publish(NavigationIndex);
                     break;
+                case "Image":
+                    _regionManager.RequestNavigate("MainRegionContext", "ImageView");
+                    NavigationIndex = 5;
+                    _eventAggregator.GetEvent<MainTabSwitchNotification>().Publish(NavigationIndex);
+                    break;
                 case "Calib":
                     _regionManager.RequestNavigate("MainRegionContext", "CalibrationView");
-                    NavigationIndex = 5;
+                    NavigationIndex = 6;
                     _eventAggregator.GetEvent<MainTabSwitchNotification>().Publish(NavigationIndex);
                     break;
                 case "Statistics":
                     _regionManager.RequestNavigate("MainRegionContext", "StatisticsView");
-                    NavigationIndex = 6;
+                    NavigationIndex = 7;
                     _eventAggregator.GetEvent<MainTabSwitchNotification>().Publish(NavigationIndex);
                     break;
                 case "User":
                     _regionManager.RequestNavigate("MainRegionContext", "UserView");
-                    NavigationIndex = 7;
+                    NavigationIndex = 8;
                     _eventAggregator.GetEvent<MainTabSwitchNotification>().Publish(NavigationIndex);
                     break;
                 default:

+ 23 - 0
TeamAAS-VM/Views/Dialogs/TextInputDialog.xaml

@@ -0,0 +1,23 @@
+<Window x:Class="TeamAAS_VP.Views.Dialogs.TextInputDialog"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        Title="AAS" SizeToContent="WidthAndHeight"
+        WindowStartupLocation="CenterOwner"
+        ResizeMode="NoResize"
+        MinWidth="320">
+    <Grid Margin="12">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto"/>
+            <RowDefinition Height="Auto"/>
+            <RowDefinition Height="Auto"/>
+        </Grid.RowDefinitions>
+
+        <TextBlock Text="输入文字:" Margin="0,0,0,6"/>
+        <TextBox x:Name="InputBox" Grid.Row="1" MinWidth="280" Margin="0,0,0,10"/>
+
+        <StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right">
+            <Button Content="确定" Width="80" Margin="0,0,8,0" Click="Ok_Click"/>
+            <Button Content="取消" Width="80" Click="Cancel_Click"/>
+        </StackPanel>
+    </Grid>
+</Window>

+ 30 - 0
TeamAAS-VM/Views/Dialogs/TextInputDialog.xaml.cs

@@ -0,0 +1,30 @@
+using System.Windows;
+
+namespace TeamAAS_VP.Views.Dialogs
+{
+    public partial class TextInputDialog : Window
+    {
+        public TextInputDialog()
+        {
+            InitializeComponent();
+        }
+
+        public string InputText
+        {
+            get { return InputBox.Text; }
+            set { InputBox.Text = value; }
+        }
+
+        private void Ok_Click(object sender, RoutedEventArgs e)
+        {
+            DialogResult = true;
+            Close();
+        }
+
+        private void Cancel_Click(object sender, RoutedEventArgs e)
+        {
+            DialogResult = false;
+            Close();
+        }
+    }
+}

+ 70 - 0
TeamAAS-VM/Views/ImageView.xaml

@@ -0,0 +1,70 @@
+<UserControl x:Class="TeamAAS_VP.Views.ImageView"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:prism="http://prismlibrary.com/"
+             mc:Ignorable="d"
+             prism:ViewModelLocator.AutoWireViewModel="True"
+             d:DesignHeight="450" d:DesignWidth="800">
+    <Grid>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto"/>
+            <RowDefinition Height="*"/>
+        </Grid.RowDefinitions>
+
+        <StackPanel Orientation="Horizontal" Margin="8">
+            <Button Content="导入图片" Command="{Binding ImportImageCommand}" Margin="0,0,8,0"/>
+            <Button Content="保存" Command="{Binding SaveCommand}" Margin="0,0,8,0"/>
+            <Button Content="清空批注" Command="{Binding ClearCommand}" Margin="0,0,16,0"/>
+
+            <TextBlock VerticalAlignment="Center" Text="工具:" Margin="0,0,4,0"/>
+            <ComboBox Width="120" SelectedValue="{Binding Tool}" SelectedValuePath="Tag">
+                <ComboBoxItem Content="矩形" Tag="Rect"/>
+                <ComboBoxItem Content="箭头" Tag="Arrow"/>
+                <ComboBoxItem Content="文字" Tag="Text"/>
+                <ComboBoxItem Content="选择/移动" Tag="Select"/>
+            </ComboBox>
+
+            <TextBlock VerticalAlignment="Center" Text="颜色:" Margin="16,0,4,0"/>
+            <ComboBox Width="110" SelectedValue="{Binding ColorName}">
+                <ComboBoxItem Content="Red"/>
+                <ComboBoxItem Content="Yellow"/>
+                <ComboBoxItem Content="Lime"/>
+                <ComboBoxItem Content="DodgerBlue"/>
+                <ComboBoxItem Content="White"/>
+                <ComboBoxItem Content="Black"/>
+            </ComboBox>
+
+            <TextBlock VerticalAlignment="Center" Text="线宽:" Margin="16,0,4,0"/>
+            <Slider Width="120" Minimum="1" Maximum="10" Value="{Binding Thickness}"/>
+        </StackPanel>
+
+        <Border Grid.Row="1" Margin="8" BorderBrush="#33000000" BorderThickness="1" Background="Black">
+            <!-- 显示缩放层:只用于显示,不参与保存 -->
+            <Viewbox x:Name="DisplayViewbox" Stretch="Uniform">
+                <!-- 像素层:尺寸=原图像素,批注也在这个坐标系 -->
+                <Grid x:Name="RootPixelRender"
+                      Width="{Binding PixelWidth}"
+                      Height="{Binding PixelHeight}"
+                      Background="Black">
+                    <Image x:Name="Img"
+                           Source="{Binding ImageSource}"
+                           Width="{Binding PixelWidth}"
+                           Height="{Binding PixelHeight}"
+                           Stretch="Fill" />
+                    <Canvas x:Name="AnnoCanvas"
+                            Width="{Binding PixelWidth}"
+                            Height="{Binding PixelHeight}"
+                            Background="Transparent"
+                            Focusable="True"
+                            MouseLeftButtonDown="AnnoCanvas_MouseLeftButtonDown"
+                            MouseMove="AnnoCanvas_MouseMove"
+                            MouseLeftButtonUp="AnnoCanvas_MouseLeftButtonUp"
+                            MouseRightButtonDown="AnnoCanvas_MouseRightButtonDown"
+                            KeyDown="AnnoCanvas_KeyDown"/>
+                </Grid>
+            </Viewbox>
+        </Border>
+    </Grid>
+</UserControl>

+ 260 - 0
TeamAAS-VM/Views/ImageView.xaml.cs

@@ -0,0 +1,260 @@
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Shapes;
+using TeamAAS_VP.ViewModels;
+
+namespace TeamAAS_VP.Views
+{
+    public partial class ImageView : UserControl
+    {
+        private Point _start;
+        private Shape _previewShape;
+        private bool _isDown;
+
+        private UIElement _dragTarget;
+        private Point _dragOffset;
+
+        // 新增:记录当前选中的批注元素
+        private UIElement _selectedAnnotation;
+
+        public ImageView()
+        {
+            InitializeComponent();
+            Loaded += ImageView_Loaded;
+        }
+
+        private void ImageView_Loaded(object sender, RoutedEventArgs e)
+        {
+            var vm = DataContext as ImageViewModel;
+            vm?.AttachRenderTarget(RootPixelRender, AnnoCanvas);
+            vm?.LoadLast();
+
+            // 让 KeyDown 可用
+            AnnoCanvas.Focus();
+        }
+
+        private ImageViewModel Vm => DataContext as ImageViewModel;
+
+        private Point GetPixelPoint(MouseEventArgs e)
+        {
+            var p = e.GetPosition(AnnoCanvas);
+
+            var vm = Vm;
+            if (vm == null || vm.PixelWidth <= 0 || vm.PixelHeight <= 0)
+            {
+                return p;
+            }
+
+            var actualW = AnnoCanvas.ActualWidth;
+            var actualH = AnnoCanvas.ActualHeight;
+            if (actualW <= 0 || actualH <= 0) return p;
+
+            var scaleX = actualW / vm.PixelWidth;
+            var scaleY = actualH / vm.PixelHeight;
+
+            if (scaleX <= 0) scaleX = 1;
+            if (scaleY <= 0) scaleY = 1;
+
+            return new Point(p.X / scaleX, p.Y / scaleY);
+        }
+
+        private UIElement GetAnnotationElementFromOriginalSource(object originalSource)
+        {
+            // 右键/左键命中的可能是:Rectangle/TextBlock/Grid/Line/Polygon 等子元素
+            var element = originalSource as DependencyObject;
+            while (element != null)
+            {
+                if (element is Rectangle) return (UIElement)element;
+                if (element is TextBlock) return (UIElement)element;
+                if (element is Grid) return (UIElement)element;
+
+                // 命中箭头内部:Line/Polygon -> 往上找 Grid
+                element = System.Windows.Media.VisualTreeHelper.GetParent(element);
+            }
+
+            return null;
+        }
+
+        private void SelectAnnotation(UIElement element)
+        {
+            _selectedAnnotation = element;
+
+            // 简单选中效果(可选):给选中项加虚线描边
+            // 只处理 Rectangle / Grid(箭头) 的视觉效果;TextBlock 不做边框避免影响显示
+            foreach (var child in AnnoCanvas.Children)
+            {
+                if (child is Rectangle rr)
+                {
+                    rr.StrokeDashArray = null;
+                }
+            }
+
+            if (_selectedAnnotation is Rectangle r)
+            {
+                r.StrokeDashArray = new System.Windows.Media.DoubleCollection() { 2, 2 };
+            }
+        }
+
+        private void DeleteSelectedAnnotation()
+        {
+            if (Vm == null) return;
+            if (_selectedAnnotation == null) return;
+
+            Vm.RemoveAnnotation(_selectedAnnotation);
+            _selectedAnnotation = null;
+        }
+
+        private void AnnoCanvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
+        {
+            if (Vm == null) return;
+
+            var hit = GetAnnotationElementFromOriginalSource(e.OriginalSource);
+            if (hit == null || hit == AnnoCanvas)
+            {
+                _selectedAnnotation = null;
+                return;
+            }
+
+            SelectAnnotation(hit);
+            AnnoCanvas.Focus();
+
+            var menu = new ContextMenu();
+            var deleteItem = new MenuItem() { Header = "删除" };
+            deleteItem.Click += (s, ev) => DeleteSelectedAnnotation();
+            menu.Items.Add(deleteItem);
+
+            // 打开菜单
+            menu.PlacementTarget = AnnoCanvas;
+            menu.IsOpen = true;
+
+            e.Handled = true;
+        }
+
+        private void AnnoCanvas_KeyDown(object sender, KeyEventArgs e)
+        {
+            if (e.Key == Key.Delete)
+            {
+                DeleteSelectedAnnotation();
+                e.Handled = true;
+            }
+        }
+
+        private void AnnoCanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+        {
+            if (Vm == null) return;
+
+            _isDown = true;
+            _start = GetPixelPoint(e);
+            AnnoCanvas.CaptureMouse();
+
+            // Select 模式:左键点中即选中
+            if (Vm.Tool == "Select")
+            {
+                var hit = GetAnnotationElementFromOriginalSource(e.OriginalSource);
+                if (hit != null && hit != AnnoCanvas)
+                {
+                    SelectAnnotation(hit);
+                    _dragTarget = hit;
+
+                    var p = _start;
+                    var left = Canvas.GetLeft(_dragTarget);
+                    var top = Canvas.GetTop(_dragTarget);
+                    if (double.IsNaN(left)) left = 0;
+                    if (double.IsNaN(top)) top = 0;
+
+                    _dragOffset = new Point(p.X - left, p.Y - top);
+                }
+                else
+                {
+                    _selectedAnnotation = null;
+                    _dragTarget = null;
+                }
+                return;
+            }
+
+            if (Vm.Tool == "Rect")
+            {
+                var rect = Vm.CreateRect();
+                Canvas.SetLeft(rect, _start.X);
+                Canvas.SetTop(rect, _start.Y);
+                rect.Width = 1;
+                rect.Height = 1;
+
+                _previewShape = rect;
+                AnnoCanvas.Children.Add(rect);
+                return;
+            }
+
+            if (Vm.Tool == "Arrow")
+            {
+                var g = Vm.CreateArrowGroup(_start, _start);
+                AnnoCanvas.Children.Add(g);
+                _previewShape = g.Tag as Shape;
+                return;
+            }
+
+            if (Vm.Tool == "Text")
+            {
+                Vm.CreateTextAt(AnnoCanvas, _start);
+                _isDown = false;
+                AnnoCanvas.ReleaseMouseCapture();
+            }
+        }
+
+        private void AnnoCanvas_MouseMove(object sender, MouseEventArgs e)
+        {
+            if (!_isDown || Vm == null) return;
+
+            var p = GetPixelPoint(e);
+
+            if (Vm.Tool == "Select")
+            {
+                if (_dragTarget != null && _dragTarget != AnnoCanvas && e.LeftButton == MouseButtonState.Pressed)
+                {
+                    Canvas.SetLeft(_dragTarget, p.X - _dragOffset.X);
+                    Canvas.SetTop(_dragTarget, p.Y - _dragOffset.Y);
+                }
+                return;
+            }
+
+            if (Vm.Tool == "Rect" && _previewShape is Rectangle r)
+            {
+                var x = Math.Min(p.X, _start.X);
+                var y = Math.Min(p.Y, _start.Y);
+                var w = Math.Abs(p.X - _start.X);
+                var h = Math.Abs(p.Y - _start.Y);
+
+                Canvas.SetLeft(r, x);
+                Canvas.SetTop(r, y);
+                r.Width = Math.Max(1, w);
+                r.Height = Math.Max(1, h);
+                return;
+            }
+
+            if (Vm.Tool == "Arrow")
+            {
+                Vm.UpdateLastArrow(AnnoCanvas, p);
+            }
+        }
+
+        private void AnnoCanvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
+        {
+            if (Vm == null) return;
+
+            _isDown = false;
+            AnnoCanvas.ReleaseMouseCapture();
+
+            var p = GetPixelPoint(e);
+
+            if (Vm.Tool == "Arrow")
+            {
+                Vm.CommitLastArrow(AnnoCanvas, p);
+            }
+
+            _previewShape = null;
+            _dragTarget = null;
+        }
+    }
+}

+ 22 - 8
TeamAAS-VM/Views/MainWindowView.xaml

@@ -58,6 +58,7 @@
                     <ColumnDefinition Width="100" />
                     <ColumnDefinition Width="100" />
                     <ColumnDefinition Width="100" />
+                    <ColumnDefinition Width="100" />
                 </Grid.ColumnDefinitions>
                 <Grid.Resources>
 
@@ -124,43 +125,56 @@
                                FontWeight="Bold"/>
                 </materialDesign:Card>
                 <Button Grid.Column="5"
+        Style="{StaticResource MaterialDesignIconButton}"
+        ToolTip="{lex:Loc 图片}"
+        Width="90"
+        Height="90"
+        Background="{Binding NavigationIndex,Converter={StaticResource _NavigationState},ConverterParameter=5}"
+        Command="{Binding NavigationCommand}"
+        CommandParameter="Image">
+                    <materialDesign:PackIcon Kind="Image"
+                             Width="70"
+                             Height="70"
+                             Foreground="{Binding NavigationIndex,Converter={StaticResource _NavigationState1},ConverterParameter=5}" />
+                </Button>
+                <Button Grid.Column="6"
                         Style="{StaticResource MaterialDesignIconButton}"
                         ToolTip="{lex:Loc 校准}"
                         Width="90"
                         Height="90"
-                        Background="{Binding NavigationIndex,Converter={StaticResource _NavigationState},ConverterParameter=5}"
+                        Background="{Binding NavigationIndex,Converter={StaticResource _NavigationState},ConverterParameter=6}"
                         Command="{Binding NavigationCommand}"
                         CommandParameter="Calib">
                     <materialDesign:PackIcon Kind="CrosshairsGps"
                                              Width="70"
                                              Height="70"
-                                             Foreground="{Binding NavigationIndex,Converter={StaticResource _NavigationState1},ConverterParameter=5}" />
+                                             Foreground="{Binding NavigationIndex,Converter={StaticResource _NavigationState1},ConverterParameter=6}" />
                 </Button>
-                <Button Grid.Column="6"
+                <Button Grid.Column="7"
                         Style="{StaticResource MaterialDesignIconButton}"
                         ToolTip="{lex:Loc 统计}"
                         Width="90"
                         Height="90"
-                        Background="{Binding NavigationIndex,Converter={StaticResource _NavigationState},ConverterParameter=6}"
+                        Background="{Binding NavigationIndex,Converter={StaticResource _NavigationState},ConverterParameter=7}"
                         Command="{Binding NavigationCommand}"
                         CommandParameter="Statistics">
                     <materialDesign:PackIcon Kind="ChartBellCurveCumulative"
                                              Width="70"
                                              Height="70"
-                                             Foreground="{Binding NavigationIndex,Converter={StaticResource _NavigationState1},ConverterParameter=6}" />
+                                             Foreground="{Binding NavigationIndex,Converter={StaticResource _NavigationState1},ConverterParameter=7}" />
                 </Button>
-                <Button Grid.Column="7"
+                <Button Grid.Column="8"
                         Style="{StaticResource MaterialDesignIconButton}"
                         ToolTip="{lex:Loc 用户}"
                         Width="90"
                         Height="90"
-                        Background="{Binding NavigationIndex,Converter={StaticResource _NavigationState},ConverterParameter=7}"
+                        Background="{Binding NavigationIndex,Converter={StaticResource _NavigationState},ConverterParameter=8}"
                         Command="{Binding NavigationCommand}"
                         CommandParameter="User">
                     <materialDesign:PackIcon Kind="AccountMultipleOutline"
                                              Width="70"
                                              Height="70"
-                                             Foreground="{Binding NavigationIndex,Converter={StaticResource _NavigationState1},ConverterParameter=7}" />
+                                             Foreground="{Binding NavigationIndex,Converter={StaticResource _NavigationState1},ConverterParameter=8}" />
                 </Button>
             </Grid>