feat(wpf): 添加数据导入功能

This commit is contained in:
ssss
2026-07-03 10:49:23 +08:00
parent d1a89cb86a
commit 6e183f84e4
10 changed files with 314 additions and 21 deletions

View File

@@ -1,8 +1,10 @@
<Application x:Class="WCTDataMiner.Wpf.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WCTDataMiner.Wpf">
xmlns:local="clr-namespace:WCTDataMiner.Wpf"
xmlns:converters="clr-namespace:WCTDataMiner.Wpf.Converters">
<Application.Resources>
<converters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
<converters:InverseBooleanConverter x:Key="InverseBooleanConverter"/>
</Application.Resources>
</Application>

View File

@@ -29,12 +29,14 @@ public partial class App : Application
services.AddTransient<ScenarioListViewModel>();
services.AddTransient<PlossChartViewModel>();
services.AddTransient<QfodChartViewModel>();
services.AddTransient<ImportViewModel>();
// 注册 Views
services.AddTransient<MainWindow>();
services.AddTransient<ScenarioListView>();
services.AddTransient<PlossChartView>();
services.AddTransient<QfodChartView>();
services.AddTransient<ImportView>();
})
.Build();

View File

@@ -0,0 +1,53 @@
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace WCTDataMiner.Wpf.Converters;
/// <summary>
/// Boolean to Visibility 转换器
/// </summary>
public class BooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return boolValue ? Visibility.Visible : Visibility.Collapsed;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Visibility visibility)
{
return visibility == Visibility.Visible;
}
return false;
}
}
/// <summary>
/// 反转 Boolean 转换器
/// </summary>
public class InverseBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return !boolValue;
}
return true;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return !boolValue;
}
return false;
}
}

View File

@@ -3,16 +3,11 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WCTDataMiner.Wpf"
xmlns:views="clr-namespace:WCTDataMiner.Wpf.Views"
xmlns:viewmodels="clr-namespace:WCTDataMiner.Wpf.ViewModels"
xmlns:lvc="clr-namespace:LiveChartsCore.SkiaSharpView.WPF;assembly=LiveChartsCore.SkiaSharpView.WPF"
mc:Ignorable="d"
Title="WCTDataMiner - FOD 数据分析工具" Height="600" Width="900"
WindowStartupLocation="CenterScreen">
<Window.DataContext>
<viewmodels:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
@@ -25,6 +20,10 @@
<StackPanel Margin="10">
<TextBlock Text="导航" FontSize="16" FontWeight="Bold" Margin="0,0,0,15"/>
<Button Content="数据导入" Command="{Binding NavigateToImportCommand}"
Margin="0,5" Padding="10,5" HorizontalAlignment="Stretch"
Background="#4CAF50" Foreground="White" FontWeight="Bold"/>
<Button Content="场景列表" Command="{Binding NavigateToScenariosCommand}"
Margin="0,5" Padding="10,5" HorizontalAlignment="Stretch"/>
@@ -51,6 +50,9 @@
<!-- 右侧内容区域 -->
<ContentControl Grid.Column="1" Content="{Binding CurrentView}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type viewmodels:ImportViewModel}">
<views:ImportView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:ScenarioListViewModel}">
<views:ScenarioListView/>
</DataTemplate>

View File

@@ -1,13 +1,5 @@
using System.Text;
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;
using WCTDataMiner.Wpf.ViewModels;
namespace WCTDataMiner.Wpf;
@@ -16,8 +8,9 @@ namespace WCTDataMiner.Wpf;
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
public MainWindow(MainViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}

View File

@@ -0,0 +1,149 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Win32;
using System.Collections.ObjectModel;
using WCTDataMiner.Core.Services;
namespace WCTDataMiner.Wpf.ViewModels;
/// <summary>
/// 数据导入 ViewModel
/// </summary>
public partial class ImportViewModel : ObservableObject
{
private readonly ParseService _parseService;
[ObservableProperty]
private string _selectedPath = "";
[ObservableProperty]
private bool _isRecursive;
[ObservableProperty]
private bool _isImporting;
[ObservableProperty]
private int _progressValue;
[ObservableProperty]
private string _statusMessage = "请选择日志文件或目录";
[ObservableProperty]
private ObservableCollection<ImportResultItem> _importResults = new();
public ImportViewModel(ParseService parseService)
{
_parseService = parseService;
}
[RelayCommand]
private void SelectFile()
{
var dialog = new OpenFileDialog
{
Title = "选择日志文件",
Filter = "日志文件 (*.log)|*.log|所有文件 (*.*)|*.*",
Multiselect = false
};
if (dialog.ShowDialog() == true)
{
SelectedPath = dialog.FileName;
StatusMessage = $"已选择文件: {System.IO.Path.GetFileName(SelectedPath)}";
}
}
[RelayCommand]
private void SelectFolder()
{
var dialog = new OpenFolderDialog
{
Title = "选择日志目录",
Multiselect = false
};
if (dialog.ShowDialog() == true)
{
SelectedPath = dialog.FolderName;
StatusMessage = $"已选择目录: {System.IO.Path.GetFileName(SelectedPath)}";
}
}
[RelayCommand]
private async Task ImportAsync()
{
if (string.IsNullOrEmpty(SelectedPath))
{
StatusMessage = "请先选择文件或目录";
return;
}
IsImporting = true;
ImportResults.Clear();
StatusMessage = "正在导入...";
try
{
if (System.IO.File.Exists(SelectedPath))
{
// 导入单个文件
var report = await _parseService.ParseFileAsync(SelectedPath);
ImportResults.Add(new ImportResultItem(report));
StatusMessage = $"导入完成: Qfod={report.QfodCount}, Ploss={report.PlossCount}, 错误={report.ErrorCount}";
}
else if (System.IO.Directory.Exists(SelectedPath))
{
// 导入目录
var reports = await _parseService.ParseDirectoryAsync(SelectedPath, IsRecursive);
int totalQfod = 0, totalPloss = 0, totalErrors = 0;
foreach (var report in reports)
{
ImportResults.Add(new ImportResultItem(report));
totalQfod += report.QfodCount;
totalPloss += report.PlossCount;
totalErrors += report.ErrorCount;
ProgressValue = (int)((double)ImportResults.Count / reports.Count * 100);
}
StatusMessage = $"导入完成: {reports.Count} 个文件, Qfod={totalQfod}, Ploss={totalPloss}, 错误={totalErrors}";
}
else
{
StatusMessage = "路径不存在";
}
}
catch (Exception ex)
{
StatusMessage = $"导入失败: {ex.Message}";
}
finally
{
IsImporting = false;
ProgressValue = 100;
}
}
}
/// <summary>
/// 导入结果项
/// </summary>
public class ImportResultItem
{
public string FileName { get; }
public int QfodCount { get; }
public int PlossCount { get; }
public int ErrorCount { get; }
public string Status { get; }
public bool IsSuccess { get; }
public ImportResultItem(ParseReport report)
{
FileName = report.FileName;
QfodCount = report.QfodCount;
PlossCount = report.PlossCount;
ErrorCount = report.ErrorCount;
IsSuccess = report.IsSuccess;
Status = report.IsSuccess ? "成功" : report.ErrorMessage ?? "失败";
}
}

View File

@@ -8,6 +8,9 @@ namespace WCTDataMiner.Wpf.ViewModels;
/// </summary>
public partial class MainViewModel : ObservableObject
{
private readonly ImportViewModel _importViewModel;
private readonly ScenarioListViewModel _scenarioListViewModel;
[ObservableProperty]
private ObservableObject _currentView = null!;
@@ -29,15 +32,23 @@ public partial class MainViewModel : ObservableObject
public List<string> TxSoftwares { get; } = new() { "全部", "hex2_1", "hex2_2" };
public List<string> RxTypes { get; } = new() { "全部", "iPhone15", "iPhone16", "Android" };
public MainViewModel()
public MainViewModel(ImportViewModel importViewModel, ScenarioListViewModel scenarioListViewModel)
{
_currentView = new ScenarioListViewModel();
_importViewModel = importViewModel;
_scenarioListViewModel = scenarioListViewModel;
_currentView = _scenarioListViewModel;
}
[RelayCommand]
private void NavigateToImport()
{
CurrentView = _importViewModel;
}
[RelayCommand]
private void NavigateToScenarios()
{
CurrentView = new ScenarioListViewModel();
CurrentView = _scenarioListViewModel;
}
[RelayCommand]

View File

@@ -0,0 +1,63 @@
<UserControl x:Class="WCTDataMiner.Wpf.Views.ImportView"
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:local="clr-namespace:WCTDataMiner.Wpf.Views"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="600">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 标题 -->
<TextBlock Grid.Row="0" Text="数据导入" FontSize="20" FontWeight="Bold" Margin="0,0,0,20"/>
<!-- 选择路径 -->
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,15">
<Button Content="选择文件" Command="{Binding SelectFileCommand}"
Width="100" Margin="0,0,10,0" Padding="10,5"/>
<Button Content="选择目录" Command="{Binding SelectFolderCommand}"
Width="100" Margin="0,0,10,0" Padding="10,5"/>
<TextBox Text="{Binding SelectedPath, UpdateSourceTrigger=PropertyChanged}"
Width="300" IsReadOnly="True" VerticalContentAlignment="Center"/>
</StackPanel>
<!-- 选项和导入按钮 -->
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,0,0,15">
<CheckBox Content="递归搜索子目录" IsChecked="{Binding IsRecursive}"
Margin="0,0,20,0" VerticalContentAlignment="Center"/>
<Button Content="开始导入" Command="{Binding ImportCommand}"
Width="120" Padding="15,8" FontWeight="Bold"
IsEnabled="{Binding IsImporting, Converter={StaticResource InverseBooleanConverter}}"/>
</StackPanel>
<!-- 进度和状态 -->
<StackPanel Grid.Row="3" Margin="0,0,0,15">
<ProgressBar Height="20" Value="{Binding ProgressValue}" Maximum="100"
Visibility="{Binding IsImporting, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<TextBlock Text="{Binding StatusMessage}" FontSize="14" Margin="0,5,0,0"/>
</StackPanel>
<!-- 导入结果列表 -->
<DataGrid Grid.Row="4" ItemsSource="{Binding ImportResults}"
AutoGenerateColumns="False"
IsReadOnly="True"
GridLinesVisibility="Horizontal"
AlternatingRowBackground="#F9F9F9"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="文件名" Binding="{Binding FileName}" Width="*"/>
<DataGridTextColumn Header="Qfod" Binding="{Binding QfodCount}" Width="80"/>
<DataGridTextColumn Header="Ploss" Binding="{Binding PlossCount}" Width="80"/>
<DataGridTextColumn Header="错误" Binding="{Binding ErrorCount}" Width="80"/>
<DataGridTextColumn Header="状态" Binding="{Binding Status}" Width="100"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>

View File

@@ -0,0 +1,12 @@
using System.Windows;
using System.Windows.Controls;
namespace WCTDataMiner.Wpf.Views;
public partial class ImportView : UserControl
{
public ImportView()
{
InitializeComponent();
}
}

View File

@@ -26,4 +26,10 @@
<ProjectReference Include="..\WCTDataMiner.Core\WCTDataMiner.Core.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>