Justin 1 năm trước cách đây
mục cha
commit
0fcbc18a51

+ 87 - 6
StationProbe/Database.cs

@@ -4,7 +4,7 @@ using SQLite;
 
 namespace StationProbe
 {
-    internal class Database:IDisposable
+    internal class Database : IDisposable
     {
         private readonly SQLiteConnection _db;
 
@@ -19,7 +19,7 @@ namespace StationProbe
         public Database()
         {
             var drives = System.IO.DriveInfo.GetDrives();
-            var largestDriver = drives.OrderByDescending(x=>x.TotalFreeSpace).First();
+            var largestDriver = drives.OrderByDescending(x => x.TotalFreeSpace).First();
             _db = new SQLiteConnection($"{largestDriver.Name}database.db");
             _db.CreateTable<Image>();
             _db.CreateTable<Exam>();
@@ -53,7 +53,7 @@ namespace StationProbe
 
         public BatchTask? GetBatchTask(int id)
         {
-            return _db.Table<BatchTask>().FirstOrDefault(x=>x.Id == id);
+            return _db.Table<BatchTask>().FirstOrDefault(x => x.Id == id);
         }
 
         public void UpdateBatchTask(BatchTask batchTask)
@@ -63,17 +63,98 @@ namespace StationProbe
 
         public Exam[] GetExams(int batchTaskId, int page)
         {
-            return _db.Table<Exam>().Where(x=>x.BatchTaskId == batchTaskId && x.PageIndex == page).OrderBy(x=>x.Id).ToArray();
+            return _db.Table<Exam>().Where(x => x.BatchTaskId == batchTaskId && x.PageIndex == page).OrderBy(x => x.Id).ToArray();
         }
 
         public Image[] GetImages(string examId)
         {
-            return _db.Table<Image>().Where(x => x.ExamId == examId).OrderBy(x=>x.ImageIndex).ToArray();
+            return _db.Table<Image>().Where(x => x.ExamId == examId).OrderBy(x => x.ImageIndex).ToArray();
         }
 
         public Exam? GetLastExam(int batchTaskId)
         {
-            return _db.Table<Exam>().Where(x => x.BatchTaskId == batchTaskId).OrderByDescending(x=>x.ExamIndex).FirstOrDefault();
+            return _db.Table<Exam>().Where(x => x.BatchTaskId == batchTaskId).OrderByDescending(x => x.ExamIndex).FirstOrDefault();
+        }
+
+        public int GetPageCount(int batchTaskId, string part, string conclusion)
+        {
+            if (!string.IsNullOrEmpty(part) && !string.IsNullOrEmpty(conclusion))
+            {
+                var examCount = _db.Table<Exam>().Count(x => x.BatchTaskId == batchTaskId && x.Report.Contains(part) && x.Report.Contains(conclusion));
+                if(examCount < 15)
+                {
+                    return 1;
+                }
+                if(examCount % 15 == 0)
+                {
+                    return examCount / 15;
+                }else
+                {
+                    return examCount / 15 + 1;
+                }              
+            }
+            else if (!string.IsNullOrEmpty(part))
+            {
+                var examCount = _db.Table<Exam>().Count(x => x.BatchTaskId == batchTaskId && x.Report.Contains(part));
+                if (examCount < 15)
+                {
+                    return 1;
+                }
+                if (examCount % 15 == 0)
+                {
+                    return examCount / 15;
+                }
+                else
+                {
+                    return examCount / 15 + 1;
+                }
+            }
+            else if (!string.IsNullOrEmpty(conclusion))
+            {
+                var examCount = _db.Table<Exam>().Count(x => x.BatchTaskId == batchTaskId && x.Report.Contains(conclusion));
+                if (examCount < 15)
+                {
+                    return 1;
+                }
+                if (examCount % 15 == 0)
+                {
+                    return examCount / 15;
+                }
+                else
+                {
+                    return examCount / 15 + 1;
+                }
+            }
+            else
+            {
+                var lastExam = GetLastExam(batchTaskId);
+                if(lastExam != null)
+                {
+                    return lastExam.PageIndex + 1;
+                }
+                return 0;
+            }
+        }
+
+
+        public Exam[] GetExams(int batchTaskId, int page, string part, string conclusion)
+        {
+            if (!string.IsNullOrEmpty(part) && !string.IsNullOrEmpty(conclusion))
+            {
+                return _db.Table<Exam>().Where(x => x.BatchTaskId == batchTaskId && x.Report.Contains(part) && x.Report.Contains(conclusion)).OrderBy(x => x.Id).Skip(15 * page).Take(15).ToArray();
+            }
+            else if (!string.IsNullOrEmpty(part))
+            {
+                return _db.Table<Exam>().Where(x => x.BatchTaskId == batchTaskId && x.Report.Contains(part)).OrderBy(x => x.Id).Skip(15 * page).Take(15).ToArray();
+            }
+            else if (!string.IsNullOrEmpty(conclusion))
+            {
+                return _db.Table<Exam>().Where(x => x.BatchTaskId == batchTaskId && x.Report.Contains(conclusion)).OrderBy(x => x.Id).Skip(15 * page).Take(15).ToArray();
+            }
+            else
+            {
+                return GetExams(batchTaskId, page);
+            }
         }
 
         public void Dispose()

+ 1 - 1
StationProbe/DingTalk.cs

@@ -46,7 +46,7 @@ namespace StationProbe
                 if (tokenResult.ErrorCode == 0)
                 {
                     var text = new Dictionary<string, object>();
-                    text["content"] = msg;
+                    text["content"] = "[" + DateTime.Now.ToString() + "]" +  msg;
 
                     var message = new Dictionary<string, object>();
                     message["msgtype"] = "text";

+ 2 - 2
StationProbe/ProbeTask.cs

@@ -306,7 +306,7 @@ namespace StationProbe
 
         internal class ExamTask : ProbeTask
         {
-            private static readonly ScreenshotOptions _opt = new ScreenshotOptions() { Type = ScreenshotType.Jpeg, Quality = 90 };
+            private static readonly ScreenshotOptions _opt = new ScreenshotOptions() { Type = ScreenshotType.Jpeg, Quality = 80 };
             private readonly int _batchTaskId;
             private readonly int _pageIndex;
             private readonly int _examIndex;
@@ -322,7 +322,7 @@ namespace StationProbe
 
             private async Task EnterExamAsync(IPage page)
             {
-                await using (var tableElement = await page.QuerySelectorAsync("table[class=\"tb_main\"]"))
+                await using (var tableElement = await page.WaitForSelectorAsync("table[class=\"tb_main\"]"))
                 {
                     var examRecordElements = await tableElement.QuerySelectorAllAsync("tr");
                     var examRecordElement = examRecordElements[_examIndexInPage + 1];

+ 1 - 1
StationProbe/SuperImageTask.cs

@@ -120,7 +120,7 @@ namespace StationProbe
                     var currentExamCountinPage = _pageIndex < (_batchTask.PageCount - 1) ? _examCountInPage : (_batchTask.ExamCount - _pageIndex * _examCountInPage);
                     while (_examIndexInPage < currentExamCountinPage)
                     {
-                        Logger.WriteLine($"Start exam task (exam index: {_examIndexInPage}) in page {_pageIndex}...");
+                        Logger.WriteLine($"Start exam task (exam index: {_examIndexInPage})...");
                         try
                         {
                             outputs = await new ExamTask(_examStore, _batchTask.Id, _pageIndex, _examIndex ,_examIndexInPage).ExecuteAsync(outputs);

+ 30 - 0
TestViewer/Loading.cs

@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Threading;
+
+namespace TestViewer
+{
+    internal class Loading
+    {
+        public static event EventHandler<bool> LoadingChanged;
+
+        public static void Run(Action action)
+        {
+            LoadingChanged?.Invoke(null, true);
+            Task.Run(()=> 
+            {
+                try
+                {
+                    action();
+                }
+                finally
+                {
+                    LoadingChanged?.Invoke(null, false);
+                }
+             });
+        }
+    }
+}

+ 53 - 4
TestViewer/MainWindow.xaml

@@ -6,8 +6,12 @@
         mc:Ignorable="d"
         Title="Exam Database TestViewer" Height="450" Width="800" WindowStartupLocation="CenterScreen" WindowState="Maximized">
     <Grid>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="*"/>
+            <RowDefinition Height="48"/>
+        </Grid.RowDefinitions>
         <Grid.ColumnDefinitions>
-            <ColumnDefinition Width="480"/>
+            <ColumnDefinition Width="282"/>
             <ColumnDefinition Width="*"/>
         </Grid.ColumnDefinitions>
         <Grid Grid.Column="0">
@@ -17,7 +21,40 @@
                 <RowDefinition Height="32"/>
             </Grid.RowDefinitions>
             <ComboBox Grid.Row="0" x:Name="BatchTaskList" VerticalContentAlignment="Center"/>
-            <ListBox Grid.Row="1" x:Name="ExamList"/>
+            <ListBox Grid.Row="1" x:Name="ExamList" HorizontalContentAlignment="Stretch">
+                <ListBox.ItemTemplate>
+                    <DataTemplate DataType="Exam">
+                        <Border Height="64" BorderBrush="Gray" Background="White" BorderThickness="1" CornerRadius="4">
+                            <StackPanel Margin="8">
+                                <StackPanel Orientation="Horizontal">
+                                    <StackPanel Orientation="Horizontal">
+                                        <TextBlock Text="姓名: " FontWeight="Bold"/>
+                                        <TextBlock Text="{Binding PatientName}" Foreground="Blue"/>
+                                    </StackPanel>
+                                    <TextBlock Text="   "/>
+                                    <StackPanel Orientation="Horizontal">
+                                        <TextBlock Text="性别: " FontWeight="Bold"/>
+                                        <TextBlock Text="{Binding PatientSex}"/>
+                                    </StackPanel>
+                                    <TextBlock Text="   "/>
+                                    <StackPanel Orientation="Horizontal">
+                                        <TextBlock Text="年龄: " FontWeight="Bold"/>
+                                        <TextBlock Text="{Binding PatientAge}"/>
+                                    </StackPanel>
+                                </StackPanel>
+                                <StackPanel Orientation="Horizontal">
+                                    <TextBlock Text="检查日期: " FontWeight="Bold"/>
+                                    <TextBlock Text="{Binding ExamDate,StringFormat='{} {0:yyyy-MM-dd HH:mm:ss}'}"/>
+                                </StackPanel>
+                                <StackPanel Orientation="Horizontal">
+                                    <TextBlock Text="创建日期: " FontWeight="Bold"/>
+                                    <TextBlock Text="{Binding CreateTime,StringFormat='{} {0:yyyy-MM-dd HH:mm:ss}'}"/>
+                                </StackPanel>
+                            </StackPanel>
+                        </Border>
+                    </DataTemplate>
+                </ListBox.ItemTemplate>
+            </ListBox>
             <UniformGrid Grid.Row="2" Columns="3">
                 <Button Margin="2" Content="&lt;" Click="OnPreviousPageClick"/>
                 <TextBlock x:Name="PageIndex" Text="1" VerticalAlignment="Center" HorizontalAlignment="Center"/>
@@ -25,8 +62,20 @@
             </UniformGrid>
         </Grid>
         <ScrollViewer x:Name="ExamControllScrollView" Grid.Column="1" >
-            <StackPanel x:Name="ExamContent"/>
+            <WrapPanel x:Name="ExamContent" Orientation="Horizontal"/>
         </ScrollViewer>
-        
+        <StackPanel Orientation="Horizontal" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2">
+            <TextBlock Margin="4" Text="部位:" VerticalAlignment="Center" Width="40" FontSize="16"/>
+            <TextBox x:Name="PartKey" VerticalAlignment="Center" Width="128" FontSize="16"/>
+            <TextBlock VerticalAlignment="Center" Margin="4" Text="结论:" Width="40" FontSize="16"/>
+            <TextBox x:Name="ConclusionKey" VerticalAlignment="Center" Width="128" FontSize="16"/>
+            <Button Margin="4" VerticalAlignment="Center" Width="48" Content="查询" FontSize="16" Click="OnSearchClick"/>
+            <Button Margin="4" VerticalAlignment="Center" Width="48" Content="导出" FontSize="16" Click="OnExportClick"/>
+        </StackPanel>
+        <Border Height="1" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" VerticalAlignment="Top" Background="Gray"/>
+        <Grid x:Name="LoadingMask" Grid.Row="0" Grid.RowSpan="3" Grid.Column="0" Grid.ColumnSpan="2" Visibility="Collapsed">
+            <Border Background="Gray" Opacity="0.5"/>
+            <TextBlock Text="加载中..." FontSize="32" FontWeight="Bold" VerticalAlignment="Center" HorizontalAlignment="Center"/>
+        </Grid>
     </Grid>
 </Window>

+ 341 - 31
TestViewer/MainWindow.xaml.cs

@@ -1,9 +1,19 @@
 using Microsoft.Win32;
 using StationProbe;
+using System;
+using System.Collections.Generic;
 using System.IO;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text.Unicode;
+using System.Threading;
+using System.Threading.Tasks;
 using System.Windows;
 using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
 using System.Windows.Media.Imaging;
+using System.Windows.Threading;
 
 namespace TestViewer
 {
@@ -15,14 +25,17 @@ namespace TestViewer
         private readonly Database _db;
         private int _pageIndex = -1;
         private int _pageCount = 0;
+        private string _partKey = string.Empty;
+        private string _conclusionKey = string.Empty;
 
         public MainWindow()
         {
             InitializeComponent();
+            Loading.LoadingChanged += OnLoadingChanged;
             var fileDialog = new OpenFileDialog();
             fileDialog.Title = "Open database file";
             fileDialog.Filter = "*.db|*.db";
-            if(fileDialog.ShowDialog() == true)
+            if (fileDialog.ShowDialog() == true)
             {
                 _db = new Database(fileDialog.FileName);
                 PageIndex.Text = $"{_pageIndex + 1}/{_pageCount}";
@@ -37,7 +50,19 @@ namespace TestViewer
             else
             {
                 Application.Current.Shutdown(0);
-            }           
+            }
+        }
+
+        private void OnLoadingChanged(object? sender, bool e)
+        {
+            if (e)
+            {
+                Dispatcher.Invoke(() => { LoadingMask.Visibility = Visibility.Visible; });
+            }
+            else
+            {
+                Dispatcher.Invoke(() => { LoadingMask.Visibility = Visibility.Collapsed; });
+            }
         }
 
         private void OnBatchTaskSelectionChanged(object sender, SelectionChangedEventArgs e)
@@ -45,18 +70,20 @@ namespace TestViewer
             var batchTask = BatchTaskList.SelectedValue as BatchTask;
             if (batchTask != null)
             {
-                var lastExam = _db.GetLastExam(batchTask.Id);
-                if (lastExam != null)
+                Loading.Run(() =>
                 {
                     _pageIndex = 0;
-                    _pageCount = lastExam.PageIndex + 1;
-                    var exams = _db.GetExams(batchTask.Id, _pageIndex);
-                    foreach (var exam in exams)
+                    _pageCount = _db.GetPageCount(batchTask.Id, _partKey, _conclusionKey);
+                    var exams = _db.GetExams(batchTask.Id, _pageIndex, _partKey, _conclusionKey);
+                    Dispatcher.Invoke(() =>
                     {
-                        ExamList.Items.Add(exam);
-                    }
-                    PageIndex.Text = $"{_pageIndex + 1}/{_pageCount}";
-                }
+                        foreach (var exam in exams)
+                        {
+                            ExamList.Items.Add(exam);
+                        }
+                        PageIndex.Text = $"{_pageIndex + 1}/{_pageCount}";
+                    });
+                });
             }
         }
 
@@ -69,13 +96,117 @@ namespace TestViewer
                 var exam = ExamList.SelectedValue as Exam;
                 if (exam != null)
                 {
-                    ExamContent.Children.Add(new TextBlock() 
+                    Dictionary<string, string>? reportContent = null;
+                    try
                     {
-                        Margin = new Thickness(16),
-                        Text = exam.Report, 
-                        FontSize = 20, 
-                        TextWrapping = TextWrapping.Wrap 
-                    });
+                        reportContent = JsonSerializer.Deserialize<Dictionary<string, string>>(exam.Report);
+                    }
+                    catch
+                    {
+                        reportContent = null;
+                    }
+                    if (reportContent != null)
+                    {
+                        var partStackPanel = new StackPanel() { Margin = new Thickness(16), Orientation = Orientation.Horizontal };
+                        partStackPanel.Children.Add(new TextBlock()
+                        {
+                            VerticalAlignment = VerticalAlignment.Center,
+                            Text = "部位: ",
+                            FontWeight = FontWeights.Bold,
+                            FontSize = 16,
+                        });
+                        partStackPanel.Children.Add(new TextBlock()
+                        {
+                            VerticalAlignment = VerticalAlignment.Center,
+                            Text = $"{reportContent["Part"]}",
+                            FontSize = 16,
+                        });
+                        ExamContent.Children.Add(partStackPanel);
+
+                        var deviceStackPanel = new StackPanel() { Margin = new Thickness(16), Orientation = Orientation.Horizontal };
+                        deviceStackPanel.Children.Add(new TextBlock()
+                        {
+                            VerticalAlignment = VerticalAlignment.Center,
+                            Text = "设备: ",
+                            FontWeight = FontWeights.Bold,
+                            FontSize = 16,
+                        });
+                        deviceStackPanel.Children.Add(new TextBlock()
+                        {
+                            VerticalAlignment = VerticalAlignment.Center,
+                            Text = $"{reportContent["Device"]}",
+                            FontSize = 16,
+                        });
+                        ExamContent.Children.Add(deviceStackPanel);
+
+                        var conclusionStackPanel = new StackPanel() { Margin = new Thickness(16), Orientation = Orientation.Horizontal };
+                        conclusionStackPanel.Children.Add(new TextBlock()
+                        {
+                            VerticalAlignment = VerticalAlignment.Center,
+                            Text = "结论: ",
+                            FontWeight = FontWeights.Bold,
+                            FontSize = 16,
+                        });
+                        var conclusion = "无结论";
+                        if (!string.IsNullOrEmpty(reportContent["Conclusion"]))
+                        {
+                            conclusion = reportContent["Conclusion"];
+                        }
+                        conclusionStackPanel.Children.Add(new TextBlock()
+                        {
+                            VerticalAlignment = VerticalAlignment.Center,
+                            Text = conclusion,
+                            FontSize = 16,
+                        });
+                        ExamContent.Children.Add(conclusionStackPanel);
+
+                        ExamContent.Children.Add(new Border() { Height = 1, Width = 2000, BorderBrush = Brushes.Gray, Background = Brushes.Gray });
+
+                        ExamContent.Children.Add(new TextBlock()
+                        {
+                            Width = 2000,
+                            Margin = new Thickness(16, 16, 16, 8),
+                            Text = $"超声所见:",
+                            FontSize = 16,
+                            FontWeight = FontWeights.Bold,
+                        });
+
+                        ExamContent.Children.Add(new TextBlock()
+                        {
+                            Margin = new Thickness(16, 8, 16, 16),
+                            Text = reportContent["Summary"].Replace(@"\n", Environment.NewLine),
+                            FontSize = 16,
+                            TextWrapping = TextWrapping.Wrap,
+                        });
+
+                        ExamContent.Children.Add(new TextBlock()
+                        {
+                            Width = 2000,
+                            Margin = new Thickness(16, 16, 16, 8),
+                            Text = $"超声提示:",
+                            FontSize = 16,
+                            FontWeight = FontWeights.Bold,
+                        });
+
+                        ExamContent.Children.Add(new TextBlock()
+                        {
+                            Margin = new Thickness(16, 8, 16, 16),
+                            Text = reportContent["Comment"].Replace(@"\n", Environment.NewLine),
+                            FontSize = 16,
+                            TextWrapping = TextWrapping.Wrap,
+                        });
+                    }
+                    else
+                    {
+                        ExamContent.Children.Add(new TextBlock()
+                        {
+                            Margin = new Thickness(16),
+                            Text = exam.Report,
+                            FontSize = 20,
+                            TextWrapping = TextWrapping.Wrap,
+                        });
+                    }
+                    ExamContent.Children.Add(new Border() { Height = 1, Width = 2000, BorderBrush = Brushes.Gray, Background = Brushes.Gray });
                     var images = _db.GetImages(exam.Id);
                     foreach (var image in images)
                     {
@@ -83,12 +214,45 @@ namespace TestViewer
                         img.BeginInit();
                         img.StreamSource = new MemoryStream(image.Data);
                         img.EndInit();
-                        ExamContent.Children.Add(new System.Windows.Controls.Image() 
-                        { 
-                            Stretch = System.Windows.Media.Stretch.Uniform,
+                        var imageBorder = new Border()
+                        {
                             Margin = new Thickness(16),
-                            Source = img 
-                        });
+                            Width = 200,
+                            Height = 200,
+                            Background = Brushes.Black,
+                            Child = new System.Windows.Controls.Image()
+                            {
+                                Source = img
+                            },
+                            Cursor = Cursors.Hand
+
+                        };
+                        imageBorder.MouseDown += (sender, e) =>
+                        {
+                            var viewImageWindow = new Window
+                            {
+                                Owner = this,
+                                Background = Brushes.Black,
+                                WindowStartupLocation = WindowStartupLocation.CenterOwner
+                            };
+                            var width = img.Width + 16;
+                            var height = img.Height + 16;
+                            if (width >= SystemParameters.PrimaryScreenWidth || height >= SystemParameters.PrimaryScreenHeight)
+                            {
+                                viewImageWindow.WindowState = WindowState.Maximized;
+                            }
+                            else
+                            {
+                                viewImageWindow.Width = width;
+                                viewImageWindow.Height = height;
+                            }
+                            viewImageWindow.Content = new System.Windows.Controls.Image()
+                            {
+                                Source = img
+                            };
+                            viewImageWindow.ShowDialog();
+                        };
+                        ExamContent.Children.Add(imageBorder);
                     }
                 }
             }
@@ -103,12 +267,18 @@ namespace TestViewer
                 {
                     ExamList.Items.Clear();
                     _pageIndex++;
-                    var exams = _db.GetExams(batchTask.Id, _pageIndex);
-                    foreach (var exam in exams)
+                    Loading.Run(() =>
                     {
-                        ExamList.Items.Add(exam);
-                    }
-                    PageIndex.Text = $"{_pageIndex + 1}/{_pageCount}";
+                        var exams = _db.GetExams(batchTask.Id, _pageIndex, _partKey, _conclusionKey);
+                        Dispatcher.Invoke(() =>
+                        {
+                            foreach (var exam in exams)
+                            {
+                                ExamList.Items.Add(exam);
+                            }
+                            PageIndex.Text = $"{_pageIndex + 1}/{_pageCount}";
+                        });
+                    });
                 }
             }
         }
@@ -122,12 +292,152 @@ namespace TestViewer
                 {
                     ExamList.Items.Clear();
                     _pageIndex--;
-                    var exams = _db.GetExams(batchTask.Id, _pageIndex);
-                    foreach (var exam in exams)
+                    Loading.Run(() =>
+                    {
+                        var exams = _db.GetExams(batchTask.Id, _pageIndex, _partKey, _conclusionKey);
+                        Dispatcher.Invoke(() =>
+                        {
+                            foreach (var exam in exams)
+                            {
+                                ExamList.Items.Add(exam);
+                            }
+                            PageIndex.Text = $"{_pageIndex + 1}/{_pageCount}";
+                        });
+                    });
+                }
+            }
+        }
+
+        private void OnSearchClick(object sender, RoutedEventArgs e)
+        {
+            _partKey = PartKey.Text;
+            _conclusionKey = ConclusionKey.Text;
+            var batchTask = BatchTaskList.SelectedValue as BatchTask;
+            if (batchTask != null)
+            {
+                ExamList.Items.Clear();
+                Exam[]? exams = null;
+                Loading.Run(() =>
+                {
+                    _pageCount = _db.GetPageCount(batchTask.Id, _partKey, _conclusionKey);
+                    _pageIndex = 0;
+                    exams = _db.GetExams(batchTask.Id, _pageIndex, _partKey, _conclusionKey);
+                    Dispatcher.Invoke(() =>
                     {
-                        ExamList.Items.Add(exam);
+                        foreach (var exam in exams)
+                        {
+                            ExamList.Items.Add(exam);
+                        }
+                        PageIndex.Text = $"{_pageIndex + 1}/{_pageCount}";
+                    });
+                });
+            }
+        }
+
+        private void OnExportClick(object sender, RoutedEventArgs e)
+        {
+            var partKey = PartKey.Text;
+            var conclusionKey = ConclusionKey.Text;
+            var batchTask = BatchTaskList.SelectedValue as BatchTask;
+            if (batchTask != null)
+            {
+                FolderBrowserEx.FolderBrowserDialog dialog = new FolderBrowserEx.FolderBrowserDialog();
+                var result = dialog.ShowDialog();
+                if (result == System.Windows.Forms.DialogResult.OK)
+                {
+                    var targetFolder = dialog.SelectedFolder;
+                    var imageFolder = Path.Combine(targetFolder, "images");
+                    if (!Directory.Exists(imageFolder))
+                    {
+                        Directory.CreateDirectory(imageFolder);
+                    }
+                    var reportFolder = Path.Combine(targetFolder, "reports");
+                    if (!Directory.Exists(reportFolder))
+                    {
+                        Directory.CreateDirectory(reportFolder);
                     }
-                    PageIndex.Text = $"{_pageIndex + 1}/{_pageCount}";
+                    var progressWindow = new Window
+                    {
+                        Title = "导出中...",
+                        WindowStartupLocation = WindowStartupLocation.CenterOwner,
+                        WindowStyle = WindowStyle.ToolWindow,
+                        ResizeMode = ResizeMode.NoResize,
+                        Width = 320,
+                        Height = 80,
+                        Owner = this
+                    };
+                    var canClose = false;
+                    progressWindow.Closing += (s, e) => { e.Cancel = !canClose; };
+
+
+                    var progressBar = new ProgressBar
+                    {
+                        Minimum = 0,
+                        Maximum = 100,
+                        Value = 0
+                    };
+                    progressWindow.Content = progressBar;
+                    Task.Run(() =>
+                    {
+                        JsonSerializerOptions opts = new JsonSerializerOptions
+                        {
+                            WriteIndented = true,
+                            Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
+                        };
+                        var pageCount = _db.GetPageCount(batchTask.Id, partKey, conclusionKey);
+                        if (pageCount > 0)
+                        {
+                            for (int i = 0; i < pageCount; i++)
+                            {
+                                var exams = _db.GetExams(batchTask.Id, i, partKey, conclusionKey);
+                                for (var j = 0; j < exams.Length; j++)
+                                {
+                                    var exam = exams[j];
+                                    var images = _db.GetImages(exam.Id);
+                                    foreach (var image in images)
+                                    {
+                                        var fileName = Path.Combine(imageFolder, $"{exam.Id}_{image.Id}.jpg");
+                                        File.WriteAllBytes(fileName, image.Data);
+                                    }
+                                    var reportDic = new Dictionary<string, object>
+                                    {
+                                        { "PatientName", exam.PatientName },
+                                        { "PatientSex", exam.PatientSex },
+                                        { "PatientAge", exam.PatientAge },
+                                        { "ExamDate", exam.ExamDate }
+                                    };
+                                    Dictionary<string, string>? reportContent = null;
+                                    try
+                                    {
+                                        reportContent = JsonSerializer.Deserialize<Dictionary<string, string>>(exam.Report);
+                                    }
+                                    catch
+                                    {
+                                        reportContent = null;
+                                    }
+                                    if (reportContent != null)
+                                    {
+                                        reportDic.Add("Report", reportContent);
+                                    }
+                                    else
+                                    {
+                                        reportDic.Add("Report", exam.Report);
+                                    }
+                                    var reportFile = Path.Combine(reportFolder, $"{exam.Id}.txt");
+                                    File.WriteAllText(reportFile, JsonSerializer.Serialize(reportDic, opts));
+                                }
+                                var progress = (i + 1) / (double)pageCount * 100;
+                                Dispatcher.Invoke(() =>
+                                {
+                                    progressWindow.Title = $"导出中...{(int)progress}%";
+                                    progressBar.Value = progress;
+                                });
+                            }
+                            canClose = true;
+                            Dispatcher.Invoke(progressWindow.Close);
+                        }
+                    });
+                    progressWindow.ShowDialog();
                 }
             }
         }

+ 1 - 0
TestViewer/TestViewer.csproj

@@ -13,6 +13,7 @@
   </ItemGroup>
 
   <ItemGroup>
+    <PackageReference Include="FolderBrowserEx" Version="1.0.1" />
     <PackageReference Include="sqlite-net-pcl" Version="1.8.116" />
   </ItemGroup>