完善数据报表模块

This commit is contained in:
2026-07-27 13:19:06 +08:00
parent 07218fc737
commit 14d818083d
5 changed files with 452 additions and 47 deletions

View File

@@ -7,6 +7,10 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.104.2" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\ORM\ORM.csproj" /> <ProjectReference Include="..\ORM\ORM.csproj" />
<ProjectReference Include="..\Service\Service.csproj" /> <ProjectReference Include="..\Service\Service.csproj" />

View File

@@ -1,24 +1,16 @@
using Logger; using ClosedXML.Excel;
using MahApps.Metro.Controls; using Logger;
using Microsoft.Win32;
using Model; using Model;
using Model.Entity; using Model.Entity;
using Model.Models; using Model.Models;
using NLog.Targets;
using OxyPlot;
using Service.Interface; using Service.Interface;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Data;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Threading;
using UIShare.GlobalVariable;
using UIShare.UIViewModel;
using UIShare.ViewModelBase; using UIShare.ViewModelBase;
namespace ExportModule.ViewModels namespace ExportModule.ViewModels
@@ -36,56 +28,231 @@ namespace ExportModule.ViewModels
set { SetProperty(ref _testReportModelList, value); } set { SetProperty(ref _testReportModelList, value); }
} }
private DateTime? _startTime;
public DateTime? StartTime
{
get => _startTime;
set => SetProperty(ref _startTime, value);
}
private DateTime? _endTime;
public DateTime? EndTime
{
get => _endTime;
set => SetProperty(ref _endTime, value);
}
private string _selectedScope = "全部";
public string SelectedScope
{
get => _selectedScope;
set => SetProperty(ref _selectedScope, value);
}
public ObservableCollection<string> ScopeOptions { get; } = new()
{
"全部",
"TestCell1", "TestCell2", "TestCell3", "TestCell4",
"TestCell5", "TestCell6", "TestCell7", "TestCell8"
};
private string _statusMessage = "就绪";
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
private int _totalCount;
public int TotalCount
{
get => _totalCount;
set => SetProperty(ref _totalCount, value);
}
#endregion #endregion
#region #region
public ICommand LoadedCommand { get; }
public ICommand QueryCommand { get; }
public ICommand ExportCommand { get; }
#endregion
#endregion
public ICommand LoadedCommand { get; set; }
#region #region
ITestReportService _testReportService { get; set; } private readonly ITestReportService _testReportService;
#endregion #endregion
public ExportViewModel(IContainerProvider containerProvider) : base(containerProvider) public ExportViewModel(IContainerProvider containerProvider) : base(containerProvider)
{ {
_testReportService = containerProvider.Resolve<ITestReportService>(); _testReportService = containerProvider.Resolve<ITestReportService>();
LoadedCommand = new AsyncDelegateCommand(OnLoad); LoadedCommand = new AsyncDelegateCommand(OnLoad);
QueryCommand = new AsyncDelegateCommand(OnQuery);
ExportCommand = new AsyncDelegateCommand(OnExport);
} }
#region #region
/// <summary>
/// 加载全部(无筛选条件)
/// </summary>
private async Task OnLoad() private async Task OnLoad()
{ {
var result=await _testReportService.GetFilterList(); var result = await _testReportService.GetFilterList();
if (result.IsSuccess) if (result.IsSuccess)
{ {
IList<TestReportModel> reportList = result.Data; TestReportModelList = new ObservableCollection<TestReportModel>(result.Data ?? new List<TestReportModel>());
TestReportModelList = new ObservableCollection<TestReportModel>(reportList); TotalCount = TestReportModelList.Count;
ShowInfoMessageBox($"加载完成,共 {TotalCount} 条记录", () => { });
} }
else else
{ {
TestReportModelList = new(); TestReportModelList = new ObservableCollection<TestReportModel>();
TotalCount = 0;
ShowErrorMessageBox($"加载失败:{result.Msg}", () => { });
LoggerHelper.Error(result.Msg);
} }
} }
/// <summary>
/// 查询(根据开始日期、结束日期、台架名称筛选)
/// </summary>
private async Task OnQuery()
{
// 处理结束日期:若用户只选了日期没选时间,默认到当天末尾
DateTime? queryEnd = EndTime;
if (queryEnd.HasValue)
{
queryEnd = queryEnd.Value.Date.AddDays(1).AddSeconds(-1);
}
string? scope = SelectedScope == "全部" ? null : SelectedScope;
var result = await _testReportService.GetFilterList(StartTime, queryEnd, scope);
if (result.IsSuccess)
{
TestReportModelList = new ObservableCollection<TestReportModel>(result.Data ?? new List<TestReportModel>());
TotalCount = TestReportModelList.Count;
ShowInfoMessageBox($"查询完成,共 {TotalCount} 条记录", () => { });
}
else
{
TestReportModelList = new ObservableCollection<TestReportModel>();
TotalCount = 0;
ShowErrorMessageBox($"查询失败:{result.Msg}", () => { });
LoggerHelper.Error(result.Msg);
}
}
/// <summary>
/// 导出 Excel根据当前筛选条件导出完整实体数据
/// </summary>
private async Task OnExport()
{
DateTime? queryEnd = EndTime;
if (queryEnd.HasValue)
{
queryEnd = queryEnd.Value.Date.AddDays(1).AddSeconds(-1);
}
string? scope = SelectedScope == "全部" ? null : SelectedScope;
// 查询全量实体数据
StatusMessage = "正在查询导出数据...";
var entityResult = await _testReportService.GetEntitiesByFilter(StartTime, queryEnd, scope);
if (!entityResult.IsSuccess || entityResult.Data == null || entityResult.Data.Count == 0)
{
ShowErrorMessageBox("没有数据可导出,请先查询或调整筛选条件。", () => { });
return;
}
var dialog = new SaveFileDialog
{
Filter = "Excel 工作簿 (*.xlsx)|*.xlsx|所有文件 (*.*)|*.*",
DefaultExt = ".xlsx",
FileName = $"测试报告_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx"
};
if (dialog.ShowDialog() != true) return;
var entities = entityResult.Data;
StatusMessage = $"正在导出 {entities.Count} 条记录...";
await Task.Run(() => ExportToExcel(dialog.FileName, entities));
ShowInfoMessageBox($"导出完成,共 {entities.Count} 条记录,已保存至:{dialog.FileName}", () => { });
}
#endregion #endregion
#region #region
private static void ExportToExcel(string filePath, List<TestReportEntity> entities)
{
using var workbook = new XLWorkbook();
var ws = workbook.Worksheets.Add("测试报告");
// 表头
string[] headers = {
"Id", "CreateTime", "TestRoundId", "Scope", "FileName",
"StepIndex", "StepName", "StepType", "MethodName", "MethodFullName",
"Result", "RunTimeMs", "OutputValue", "Depth", "IsErrorStep", "LoopRemaining"
};
for (int c = 0; c < headers.Length; c++)
{
ws.Cell(1, c + 1).Value = headers[c];
}
// 标题行样式
var headerRange = ws.Range(1, 1, 1, headers.Length);
headerRange.Style.Font.Bold = true;
headerRange.Style.Fill.BackgroundColor = XLColor.FromArgb(0xECEFF4);
headerRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
headerRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
// 填充数据
for (int i = 0; i < entities.Count; i++)
{
var e = entities[i];
int row = i + 2;
ws.Cell(row, 1).Value = e.Id;
ws.Cell(row, 2).Value = e.CreateTime.ToString("yyyy-MM-dd HH:mm:ss");
ws.Cell(row, 3).Value = e.TestRoundId.ToString();
ws.Cell(row, 4).Value = e.Scope ?? "";
ws.Cell(row, 5).Value = e.FileName ?? "";
ws.Cell(row, 6).Value = e.StepIndex;
ws.Cell(row, 7).Value = e.StepName ?? "";
ws.Cell(row, 8).Value = e.StepType ?? "";
ws.Cell(row, 9).Value = e.MethodName ?? "";
ws.Cell(row, 10).Value = e.MethodFullName ?? "";
ws.Cell(row, 11).Value = e.Result;
ws.Cell(row, 12).Value = e.RunTimeMs;
ws.Cell(row, 13).Value = e.OutputValue ?? "";
ws.Cell(row, 14).Value = e.Depth;
ws.Cell(row, 15).Value = e.IsErrorStep ? "是" : "否";
ws.Cell(row, 16).Value = e.LoopRemaining;
// 数据行边框
var dataRange = ws.Range(row, 1, row, headers.Length);
dataRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
dataRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
}
ws.Columns().AdjustToContents();
workbook.SaveAs(filePath);
}
#endregion #endregion
#region #region
public override void OnNavigatedTo(NavigationContext navigationContext) public override void OnNavigatedTo(NavigationContext navigationContext)
{ {
} }
public void Dispose() public void Dispose()
{ {
} }
#endregion #endregion
} }
} }

View File

@@ -1,20 +1,203 @@
<UserControl x:Class="ExportModule.Views.ExportView" <UserControl x:Class="ExportModule.Views.ExportView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" xmlns:prism="http://prismlibrary.com/"
xmlns:prism="http://prismlibrary.com/" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
prism:ViewModelLocator.AutoWireViewModel="True" prism:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="1080" d:DesignWidth="1920"> mc:Ignorable="d"
<i:Interaction.Triggers> d:DesignHeight="700"
<i:EventTrigger EventName="Loaded"> d:DesignWidth="1200">
<i:InvokeCommandAction Command="{Binding LoadedCommand}"/> <UserControl.Resources>
</i:EventTrigger> <converters:LessThanConverter x:Key="LessThanConverter"/>
</i:Interaction.Triggers> </UserControl.Resources>
<Grid>
<Border Background="#F5F7FA">
</Grid> <Grid x:Name="RootGrid" Margin="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- ========== 标题 ========== -->
<TextBlock Grid.Row="0"
Text="测试报告导出"
FontSize="20" FontWeight="Bold"
Margin="4,4,0,12"/>
<!-- ========== 主内容卡片 ========== -->
<Border Grid.Row="1"
Background="White"
BorderBrush="#DDD" BorderThickness="1"
CornerRadius="4">
<Grid>
<Grid.RowDefinitions>
<!-- 工具栏 -->
<RowDefinition Height="Auto"/>
<!-- 数据表格 -->
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- ====== 工具栏(筛选条件 + 操作按钮) ====== -->
<Border Grid.Row="0"
Background="#ECEFF4"
Padding="12,8"
BorderBrush="#DDD" BorderThickness="0,0,0,1">
<!-- 第一行:筛选条件 -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 开始日期 -->
<TextBlock Grid.Column="0"
Text="开始日期:"
VerticalAlignment="Center"
FontWeight="SemiBold"
Margin="0,0,4,0"/>
<DatePicker Grid.Column="1"
materialDesign:HintAssist.Hint=""
SelectedDate="{Binding StartTime}"
VerticalAlignment="Center"
Margin="0,0,16,0"/>
<!-- 结束日期 -->
<TextBlock Grid.Column="2"
Text="结束日期:"
VerticalAlignment="Center"
FontWeight="SemiBold"
Margin="0,0,4,0"/>
<DatePicker Grid.Column="3"
materialDesign:HintAssist.Hint=""
SelectedDate="{Binding EndTime}"
VerticalAlignment="Center"
Margin="0,0,16,0"/>
<!-- 台架选择 -->
<TextBlock Grid.Column="4"
Text="台架:"
VerticalAlignment="Center"
FontWeight="SemiBold"
Margin="0,0,4,0"/>
<ComboBox Grid.Column="5"
materialDesign:HintAssist.Hint=""
ItemsSource="{Binding ScopeOptions}"
SelectedItem="{Binding SelectedScope}"
VerticalAlignment="Center"
Margin="0,0,16,0"/>
<!-- 操作按钮 -->
<Button Grid.Column="6"
Content="查询"
Command="{Binding QueryCommand}"
Style="{StaticResource MaterialDesignFlatButton}"
Padding="16,6"
Margin="4,0"
VerticalAlignment="Center"/>
<Button Grid.Column="7"
Content="导出 Excel"
Command="{Binding ExportCommand}"
Style="{StaticResource MaterialDesignFlatButton}"
Padding="16,6"
Margin="4,0"
VerticalAlignment="Center"/>
<Button Grid.Column="8"
Content="加载全部"
Command="{Binding LoadedCommand}"
Style="{StaticResource MaterialDesignFlatButton}"
Padding="16,6"
Margin="4,0"
VerticalAlignment="Center"/>
</Grid>
</Border>
<!-- ====== 数据展示区 ====== -->
<DataGrid Grid.Row="1"
ItemsSource="{Binding TestReportModelList}"
AutoGenerateColumns="False"
IsReadOnly="True"
RowHeaderWidth="0"
BorderThickness="0"
Background="White"
AlternatingRowBackground="#F5F7FA"
SelectionMode="Single"
Margin="8">
<DataGrid.Columns>
<DataGridTextColumn Header="TestRoundId"
Width="280"
Binding="{Binding TestRoundId}"/>
<DataGridTextColumn Header="台架名称"
Width="120"
Binding="{Binding Scope}"/>
<DataGridTextColumn Header="开始时间"
Width="180"
Binding="{Binding StartTime, StringFormat={}{0:yyyy-MM-dd HH:mm:ss}}"/>
<DataGridTextColumn Header="结束时间"
Width="180"
Binding="{Binding EndTime, StringFormat={}{0:yyyy-MM-dd HH:mm:ss}}"/>
<DataGridTextColumn Header="文件名"
Width="*"
Binding="{Binding FileName}"/>
</DataGrid.Columns>
<DataGrid.Resources>
<!-- 标题行样式:与项目风格一致 -->
<Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#ECEFF4"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="BorderBrush" Value="#DDD"/>
<Setter Property="BorderThickness" Value="0,0,1,1"/>
<Setter Property="Padding" Value="8,6"/>
</Style>
<Style TargetType="DataGridCell">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="8,4"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</DataGrid.Resources>
</DataGrid>
</Grid>
</Border>
<!-- ========== 状态栏 ========== -->
<Border Grid.Row="2"
Background="#ECEFF4"
Padding="10,6"
Margin="0,8,0,0"
CornerRadius="2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="{Binding StatusMessage}"
Foreground="#444"
FontSize="12"
VerticalAlignment="Center"/>
<TextBlock Grid.Column="1"
Foreground="#666"
FontSize="12"
VerticalAlignment="Center"
Margin="8,0,0,0">
<Run Text="记录数:"/>
<Run Text="{Binding TotalCount}"/>
</TextBlock>
</Grid>
</Border>
</Grid>
</Border>
</UserControl> </UserControl>

View File

@@ -38,22 +38,35 @@ namespace Service.Implement
} }
/// <summary> /// <summary>
/// 根据 TestRoundId 分组,获取唯一记录并按分组统计最大与最小时间 /// 根据 TestRoundId 分组,获取唯一记录并按分组统计最大与最小时间
/// 支持按日期范围与台架名称筛选。
/// </summary> /// </summary>
public async Task<Result<IList<TestReportModel>>> GetFilterList() public async Task<Result<IList<TestReportModel>>> GetFilterList(DateTime? startTime = null, DateTime? endTime = null, string? scope = null)
{ {
try try
{ {
// 使用 SqlSugar 的 GroupBy 查询,聚合最大/最小时间 // 使用 SqlSugar 的 GroupBy 查询,聚合最大/最小时间
// 推荐采用这种按组投影的方式,将分组计算交由数据库处理,性能最佳 // 推荐采用这种按组投影的方式,将分组计算交由数据库处理,性能最佳
var list = await _repository.Context.Queryable<TestReportEntity>() var query = _repository.Context.Queryable<TestReportEntity>();
// 日期范围筛选
if (startTime.HasValue)
query = query.Where(x => x.CreateTime >= startTime.Value);
if (endTime.HasValue)
query = query.Where(x => x.CreateTime <= endTime.Value);
// 台架名称筛选
if (!string.IsNullOrEmpty(scope))
query = query.Where(x => x.Scope == scope);
var list = await query
.GroupBy(x => x.TestRoundId) .GroupBy(x => x.TestRoundId)
.Select(x => new TestReportModel .Select(x => new TestReportModel
{ {
TestRoundId = x.TestRoundId, TestRoundId = x.TestRoundId,
StartTime = SqlFunc.AggregateMin(x.CreateTime), StartTime = SqlFunc.AggregateMin(x.CreateTime),
EndTime = SqlFunc.AggregateMax(x.CreateTime), EndTime = SqlFunc.AggregateMax(x.CreateTime),
Scope=x.Scope, Scope = x.Scope,
FileName = x.FileName, FileName = x.FileName,
}) })
.ToListAsync(); .ToListAsync();
@@ -65,5 +78,32 @@ namespace Service.Implement
return Result<IList<TestReportModel>>.Error("获取分组过滤列表失败", ex); return Result<IList<TestReportModel>>.Error("获取分组过滤列表失败", ex);
} }
} }
/// <summary>
/// 根据日期和台架名称筛选获取完整的测试步骤实体列表(无分组,用于导出)
/// </summary>
public async Task<Result<List<TestReportEntity>>> GetEntitiesByFilter(DateTime? startTime = null, DateTime? endTime = null, string? scope = null)
{
try
{
var query = _repository.Context.Queryable<TestReportEntity>();
if (startTime.HasValue)
query = query.Where(x => x.CreateTime >= startTime.Value);
if (endTime.HasValue)
query = query.Where(x => x.CreateTime <= endTime.Value);
if (!string.IsNullOrEmpty(scope))
query = query.Where(x => x.Scope == scope);
var list = await query
.OrderBy(x => x.CreateTime)
.ToListAsync();
return Result<List<TestReportEntity>>.Success(list);
}
catch (Exception ex)
{
return Result<List<TestReportEntity>>.Error("获取导出数据失败", ex);
}
}
} }
} }

View File

@@ -21,6 +21,17 @@ namespace Service.Interface
/// <summary> /// <summary>
/// 根据 TestRoundId 分组,获取唯一记录并按分组统计最大与最小时间 /// 根据 TestRoundId 分组,获取唯一记录并按分组统计最大与最小时间
/// </summary> /// </summary>
Task<Result<IList<TestReportModel>>> GetFilterList(); /// <param name="startTime">开始日期筛选(可选)</param>
/// <param name="endTime">结束日期筛选(可选)</param>
/// <param name="scope">台架名称筛选(可选)</param>
Task<Result<IList<TestReportModel>>> GetFilterList(DateTime? startTime = null, DateTime? endTime = null, string? scope = null);
/// <summary>
/// 根据日期和台架名称筛选获取完整的测试步骤实体列表(无分组,用于导出)
/// </summary>
/// <param name="startTime">开始日期筛选(可选)</param>
/// <param name="endTime">结束日期筛选(可选)</param>
/// <param name="scope">台架名称筛选(可选)</param>
Task<Result<List<TestReportEntity>>> GetEntitiesByFilter(DateTime? startTime = null, DateTime? endTime = null, string? scope = null);
} }
} }