添加项目文件。
This commit is contained in:
330
MonitorModule/ViewModels/MonitorViewModel.cs
Normal file
330
MonitorModule/ViewModels/MonitorViewModel.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
using OxyPlot;
|
||||
using OxyPlot.Axes;
|
||||
using OxyPlot.Legends;
|
||||
using OxyPlot.Series;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
using UIShare.GlobalVariable;
|
||||
using UIShare.PubEvent;
|
||||
using UIShare.ViewModelBase;
|
||||
|
||||
namespace MonitorModule.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 监控界面 VM —— 基于 OxyPlot 的曲线监控壳。
|
||||
/// 当前阶段不接入真实数据源,仅搭好框架:
|
||||
/// - 添加信号 / 删除信号
|
||||
/// - 重置视图(按数据范围复原)
|
||||
/// - 刷新(重绘 PlotView)
|
||||
/// 仍延续 ScopedContext 隔离 + 双击展开 的范式,每个工位独立一份图表与信号列表。
|
||||
/// </summary>
|
||||
public class MonitorViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
|
||||
{
|
||||
#region 私有字段
|
||||
private readonly IScopedProvider _scope;
|
||||
// 颜色轮转池,给新加的信号自动分配区分色
|
||||
private static readonly OxyColor[] _palette =
|
||||
{
|
||||
OxyColors.SteelBlue, OxyColors.IndianRed, OxyColors.SeaGreen,
|
||||
OxyColors.DarkOrange, OxyColors.MediumPurple, OxyColors.Goldenrod,
|
||||
OxyColors.Teal, OxyColors.Crimson, OxyColors.OliveDrab
|
||||
};
|
||||
private int _signalCounter;
|
||||
|
||||
// ===== 模拟数据相关 =====
|
||||
// 模拟定时器:周期性给每条信号追加新点,形成滚动效果
|
||||
private readonly DispatcherTimer _simTimer;
|
||||
// 全局采样时间(秒),每 tick 自增 _simInterval
|
||||
private double _simT;
|
||||
// 采样间隔(秒),与 DispatcherTimer.Interval 保持一致
|
||||
private const double _simInterval = 0.1;
|
||||
// 滚动窗口大小:每条曲线最多保留 200 个点(约 20 秒)
|
||||
private const int _maxPoints = 200;
|
||||
// 用于 random walk 的随机源
|
||||
private static readonly Random _rng = new();
|
||||
#endregion
|
||||
|
||||
#region 隔离 / 标题
|
||||
public bool KeepAlive => true;
|
||||
public ScopedContext _scopedContext { get; }
|
||||
public GlobalInfo GlobalInfoRef { get; }
|
||||
|
||||
private string _testStatus = string.Empty;
|
||||
public string TestStatus
|
||||
{
|
||||
get => _testStatus;
|
||||
set => SetProperty(ref _testStatus, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region OxyPlot
|
||||
/// <summary>PlotView 直接绑这个 PlotModel</summary>
|
||||
public PlotModel Plot { get; }
|
||||
|
||||
/// <summary>当前已添加的信号集合(左侧列表展示)</summary>
|
||||
public ObservableCollection<SignalItem> Signals { get; } = new();
|
||||
|
||||
private SignalItem? _selectedSignal;
|
||||
public SignalItem? SelectedSignal
|
||||
{
|
||||
get => _selectedSignal;
|
||||
set => SetProperty(ref _selectedSignal, value);
|
||||
}
|
||||
|
||||
private string _statusMessage = "图表已就绪,暂无信号";
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 命令
|
||||
public ICommand AddSignalCommand { get; }
|
||||
public ICommand DeleteSignalCommand { get; }
|
||||
public ICommand ResetViewCommand { get; }
|
||||
public ICommand RefreshDataCommand { get; }
|
||||
// 双击展开/折叠:与 RecordView/AutomatedTestingView 共用同一套 ExpandViewEvent
|
||||
public ICommand RefreshCommand { get; }
|
||||
#endregion
|
||||
|
||||
public MonitorViewModel(IContainerExtension container) : base(container)
|
||||
{
|
||||
_scope = container.CreateScope();
|
||||
GlobalInfoRef = container.Resolve<GlobalInfo>();
|
||||
_scopedContext = _scope.Resolve<ScopedContext>();
|
||||
|
||||
Plot = BuildEmptyPlot();
|
||||
|
||||
AddSignalCommand = new DelegateCommand(OnAddSignal);
|
||||
DeleteSignalCommand = new DelegateCommand(OnDeleteSignal);
|
||||
ResetViewCommand = new DelegateCommand(OnResetView);
|
||||
RefreshDataCommand = new DelegateCommand(OnRefreshData);
|
||||
RefreshCommand = new DelegateCommand(OnExpand);
|
||||
|
||||
// 启动模拟数据定时器:100ms 一帧,给每条信号喂一个新点
|
||||
_simTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(_simInterval) };
|
||||
_simTimer.Tick += OnSimTick;
|
||||
_simTimer.Start();
|
||||
|
||||
// 默认预置 3 条模拟信号,便于直接看到滚动效果
|
||||
OnAddSignal(); // sin
|
||||
OnAddSignal(); // cos
|
||||
OnAddSignal(); // random walk
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_simTimer?.Stop();
|
||||
_scope?.Dispose();
|
||||
}
|
||||
|
||||
#region PlotModel 构建
|
||||
private static PlotModel BuildEmptyPlot()
|
||||
{
|
||||
var pm = new PlotModel
|
||||
{
|
||||
Title = string.Empty,
|
||||
PlotAreaBorderColor = OxyColors.LightGray,
|
||||
Background = OxyColors.White
|
||||
};
|
||||
pm.Axes.Add(new LinearAxis
|
||||
{
|
||||
Position = AxisPosition.Bottom,
|
||||
Title = "X",
|
||||
MajorGridlineStyle = LineStyle.Dot,
|
||||
MinorGridlineStyle = LineStyle.None
|
||||
});
|
||||
pm.Axes.Add(new LinearAxis
|
||||
{
|
||||
Position = AxisPosition.Left,
|
||||
Title = "Y",
|
||||
MajorGridlineStyle = LineStyle.Dot,
|
||||
MinorGridlineStyle = LineStyle.None
|
||||
});
|
||||
pm.Legends.Add(new Legend
|
||||
{
|
||||
LegendPosition = LegendPosition.RightTop,
|
||||
LegendBackground = OxyColor.FromAColor(200, OxyColors.White),
|
||||
LegendBorder = OxyColors.LightGray
|
||||
});
|
||||
return pm;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 命令处理
|
||||
/// <summary>
|
||||
/// 添加一条信号:按 _signalCounter 轮转选择不同波形 generator,
|
||||
/// 后续 _simTimer 每帧会调用 generator(t) 给曲线追加新点形成滚动效果。
|
||||
/// </summary>
|
||||
private void OnAddSignal()
|
||||
{
|
||||
_signalCounter++;
|
||||
var color = _palette[(_signalCounter - 1) % _palette.Length];
|
||||
|
||||
// 6 种波形轮转:sin / cos / 锯齿 / 方波 / 衰减正弦 / random walk
|
||||
var (waveName, generator) = BuildGenerator(_signalCounter);
|
||||
var name = $"{waveName}_{_signalCounter}";
|
||||
|
||||
var series = new LineSeries
|
||||
{
|
||||
Title = name,
|
||||
Color = color,
|
||||
StrokeThickness = 1.5
|
||||
};
|
||||
|
||||
var item = new SignalItem(name, series, generator);
|
||||
Signals.Add(item);
|
||||
SelectedSignal = item;
|
||||
|
||||
Plot.Series.Add(series);
|
||||
Plot.InvalidatePlot(true);
|
||||
|
||||
StatusMessage = $"已添加信号 [{name}],当前共 {Signals.Count} 条";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按索引返回一种模拟波形发生器。
|
||||
/// 振幅 / 频率 / 相位都做了区分,让多条曲线视觉上分开。
|
||||
/// </summary>
|
||||
private static (string waveName, Func<double, double> generator) BuildGenerator(int index)
|
||||
{
|
||||
// 给同种波形不同实例一些随机偏移,避免完全重叠
|
||||
double phase = (index * 0.7) % (2 * Math.PI);
|
||||
double amp = 1.0 + (index % 3) * 0.3;
|
||||
double freq = 0.5 + (index % 4) * 0.2;
|
||||
|
||||
return (index % 6) switch
|
||||
{
|
||||
0 => ("Sin", t => amp * Math.Sin(2 * Math.PI * freq * t + phase)),
|
||||
1 => ("Cos", t => amp * Math.Cos(2 * Math.PI * freq * t + phase)),
|
||||
2 => ("Saw", t => amp * (2 * ((t * freq) - Math.Floor(t * freq + 0.5)))),
|
||||
3 => ("Square", t => amp * Math.Sign(Math.Sin(2 * Math.PI * freq * t + phase))),
|
||||
4 => ("Decay", t => amp * Math.Exp(-0.05 * t) * Math.Sin(2 * Math.PI * freq * t + phase)),
|
||||
_ => RandomWalkGenerator(amp),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>构造一个随机游走 generator:在前一次值基础上叠加高斯噪声。</summary>
|
||||
private static (string, Func<double, double>) RandomWalkGenerator(double amp)
|
||||
{
|
||||
double last = 0;
|
||||
return ("Walk", _ =>
|
||||
{
|
||||
last += (_rng.NextDouble() - 0.5) * 0.2 * amp;
|
||||
// 软约束在 [-amp*3, amp*3],避免一直跑偏
|
||||
if (last > amp * 3) last = amp * 3;
|
||||
if (last < -amp * 3) last = -amp * 3;
|
||||
return last;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>删除当前选中信号;若未选中则删除最后一条。</summary>
|
||||
private void OnDeleteSignal()
|
||||
{
|
||||
var target = SelectedSignal ?? Signals.LastOrDefault();
|
||||
if (target == null)
|
||||
{
|
||||
StatusMessage = "无可删除的信号";
|
||||
return;
|
||||
}
|
||||
|
||||
Plot.Series.Remove(target.Series);
|
||||
Signals.Remove(target);
|
||||
SelectedSignal = Signals.LastOrDefault();
|
||||
|
||||
Plot.InvalidatePlot(true);
|
||||
StatusMessage = $"已删除信号 [{target.Name}],剩余 {Signals.Count} 条";
|
||||
}
|
||||
|
||||
/// <summary>按数据范围复原视图(重置所有坐标轴的缩放/平移)。</summary>
|
||||
private void OnResetView()
|
||||
{
|
||||
Plot.ResetAllAxes();
|
||||
Plot.InvalidatePlot(false);
|
||||
StatusMessage = "视图已按数据范围复原";
|
||||
}
|
||||
|
||||
/// <summary>刷新:触发 PlotView 重绘;后续接数据源时可在此重拉数据。</summary>
|
||||
private void OnRefreshData()
|
||||
{
|
||||
Plot.InvalidatePlot(true);
|
||||
StatusMessage = $"已刷新({DateTime.Now:HH:mm:ss})";
|
||||
}
|
||||
|
||||
/// <summary>双击展开 / 折叠九宫格。</summary>
|
||||
private void OnExpand()
|
||||
{
|
||||
if (string.IsNullOrEmpty(TestStatus)) return;
|
||||
_eventAggregator.GetEvent<ExpandViewEvent>().Publish(TestStatus);
|
||||
GlobalInfoRef.CurrentScope = TestStatus;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟数据 tick:每条信号按当前 _simT 算出新点并追加。
|
||||
/// 超过 _maxPoints 时丢弃最旧的点形成滚动窗口。
|
||||
/// </summary>
|
||||
private void OnSimTick(object? sender, EventArgs e)
|
||||
{
|
||||
_simT += _simInterval;
|
||||
|
||||
if (Signals.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var item in Signals)
|
||||
{
|
||||
double y = item.Generator(_simT);
|
||||
item.Series.Points.Add(new DataPoint(_simT, y));
|
||||
if (item.Series.Points.Count > _maxPoints)
|
||||
{
|
||||
item.Series.Points.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 让 X 轴跟着最新数据滚动
|
||||
var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom);
|
||||
if (xAxis != null)
|
||||
{
|
||||
double window = _maxPoints * _simInterval;
|
||||
xAxis.Minimum = Math.Max(0, _simT - window);
|
||||
xAxis.Maximum = _simT + 0.5;
|
||||
}
|
||||
|
||||
Plot.InvalidatePlot(true);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 导航
|
||||
public override void OnNavigatedTo(NavigationContext navigationContext)
|
||||
{
|
||||
base.OnNavigatedTo(navigationContext);
|
||||
if (navigationContext.Parameters.ContainsKey("Name"))
|
||||
{
|
||||
TestStatus = navigationContext.Parameters.GetValue<string>("Name");
|
||||
Plot.Title = $"监控 - {TestStatus}";
|
||||
Plot.InvalidatePlot(false);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 信号列表项:UI 显示名 + 对应的 OxyPlot LineSeries 引用 + 模拟数据 generator。
|
||||
/// generator(t) 接收当前模拟时间,返回该时刻 y 值。
|
||||
/// </summary>
|
||||
public class SignalItem
|
||||
{
|
||||
public string Name { get; }
|
||||
public LineSeries Series { get; }
|
||||
public Func<double, double> Generator { get; }
|
||||
public SignalItem(string name, LineSeries series, Func<double, double> generator)
|
||||
{
|
||||
Name = name;
|
||||
Series = series;
|
||||
Generator = generator;
|
||||
}
|
||||
}
|
||||
}
|
||||
362
MonitorModule/ViewModels/RecordViewModel.cs
Normal file
362
MonitorModule/ViewModels/RecordViewModel.cs
Normal file
@@ -0,0 +1,362 @@
|
||||
using Logger;
|
||||
using SqlSugar;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Windows.Input;
|
||||
using UIShare.GlobalVariable;
|
||||
using UIShare.PubEvent;
|
||||
using UIShare.ViewModelBase;
|
||||
|
||||
namespace MonitorModule.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 记录界面:用于查看运行目录下 SQL/ADP.db 中的数据。
|
||||
/// 功能:表选择 / 关键字 WHERE 查询 / 分页 / 导出 CSV。
|
||||
/// 仍延续 ScopedContext 隔离 + 双击展开 的范式,每个工位独立一份查询状态。
|
||||
/// </summary>
|
||||
public class RecordViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
|
||||
{
|
||||
#region 私有字段
|
||||
// 数据库相对路径:运行目录\SQL\ADP.db(数据库未就绪时容错处理,不抛异常)
|
||||
private static readonly string DbFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SQL");
|
||||
private static readonly string DbPath = Path.Combine(DbFolder, "ADP.db");
|
||||
private static readonly string ConnStr = $"Data Source={DbPath};Version=3;";
|
||||
|
||||
private readonly IScopedProvider _scope;
|
||||
#endregion
|
||||
|
||||
#region 隔离演示属性(保留 ScopedContext 验证用)
|
||||
public bool KeepAlive => true;
|
||||
public ScopedContext _scopedContext { get; }
|
||||
// 公开一份 GlobalInfo 供双击展开逻辑使用(基类的 _globalInfo 是 private)
|
||||
public GlobalInfo GlobalInfoRef { get; }
|
||||
#endregion
|
||||
|
||||
#region 顶部工位标题
|
||||
private string _testStatus = string.Empty;
|
||||
public string TestStatus
|
||||
{
|
||||
get => _testStatus;
|
||||
set => SetProperty(ref _testStatus, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 数据库 / 表 / 查询条件
|
||||
private ObservableCollection<string> _tableNames = new();
|
||||
public ObservableCollection<string> TableNames
|
||||
{
|
||||
get => _tableNames;
|
||||
set => SetProperty(ref _tableNames, value);
|
||||
}
|
||||
|
||||
private string? _selectedTable;
|
||||
public string? SelectedTable
|
||||
{
|
||||
get => _selectedTable;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedTable, value))
|
||||
{
|
||||
PageIndex = 1;
|
||||
Query();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 用户填写的 WHERE 子句(不带 WHERE 关键字),例如:Status='OK' AND Id>10
|
||||
private string _whereClause = string.Empty;
|
||||
public string WhereClause
|
||||
{
|
||||
get => _whereClause;
|
||||
set => SetProperty(ref _whereClause, value);
|
||||
}
|
||||
|
||||
private DataTable _resultTable = new();
|
||||
public DataTable ResultTable
|
||||
{
|
||||
get => _resultTable;
|
||||
set => SetProperty(ref _resultTable, value);
|
||||
}
|
||||
|
||||
private string _statusMessage = "未连接";
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 分页
|
||||
private int _pageIndex = 1;
|
||||
public int PageIndex
|
||||
{
|
||||
get => _pageIndex;
|
||||
set => SetProperty(ref _pageIndex, value);
|
||||
}
|
||||
|
||||
private int _pageSize = 50;
|
||||
public int PageSize
|
||||
{
|
||||
get => _pageSize;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _pageSize, value <= 0 ? 50 : value))
|
||||
{
|
||||
RaisePropertyChanged(nameof(TotalPages));
|
||||
PageIndex = 1;
|
||||
Query();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long _totalCount;
|
||||
public long TotalCount
|
||||
{
|
||||
get => _totalCount;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _totalCount, value);
|
||||
RaisePropertyChanged(nameof(TotalPages));
|
||||
}
|
||||
}
|
||||
|
||||
public int TotalPages
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PageSize <= 0) return 1;
|
||||
var pages = (int)((TotalCount + PageSize - 1) / PageSize);
|
||||
return pages <= 0 ? 1 : pages;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 命令
|
||||
public ICommand LoadedCommand { get; }
|
||||
public ICommand RefreshTablesCommand { get; }
|
||||
public ICommand QueryCommand { get; }
|
||||
public ICommand FirstPageCommand { get; }
|
||||
public ICommand PrevPageCommand { get; }
|
||||
public ICommand NextPageCommand { get; }
|
||||
public ICommand LastPageCommand { get; }
|
||||
public ICommand ExportCsvCommand { get; }
|
||||
// 双击展开/折叠:与 MonitorView/AutomatedTestingView 共用
|
||||
public ICommand RefreshCommand { get; }
|
||||
#endregion
|
||||
|
||||
public RecordViewModel(IContainerExtension container) : base(container)
|
||||
{
|
||||
_scope = container.CreateScope();
|
||||
GlobalInfoRef = container.Resolve<GlobalInfo>();
|
||||
_scopedContext = _scope.Resolve<ScopedContext>();
|
||||
|
||||
LoadedCommand = new DelegateCommand(LoadTables);
|
||||
RefreshTablesCommand = new DelegateCommand(LoadTables);
|
||||
QueryCommand = new DelegateCommand(() => { PageIndex = 1; Query(); });
|
||||
FirstPageCommand = new DelegateCommand(() => { if (PageIndex > 1) { PageIndex = 1; Query(); } });
|
||||
PrevPageCommand = new DelegateCommand(() => { if (PageIndex > 1) { PageIndex--; Query(); } });
|
||||
NextPageCommand = new DelegateCommand(() => { if (PageIndex < TotalPages) { PageIndex++; Query(); } });
|
||||
LastPageCommand = new DelegateCommand(() => { if (PageIndex < TotalPages) { PageIndex = TotalPages; Query(); } });
|
||||
ExportCsvCommand = new DelegateCommand(ExportCsv);
|
||||
RefreshCommand = new DelegateCommand(OnExpand);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_scope?.Dispose();
|
||||
}
|
||||
|
||||
#region 数据库操作
|
||||
private SqlSugarClient CreateClient()
|
||||
{
|
||||
return new SqlSugarClient(new ConnectionConfig
|
||||
{
|
||||
DbType = SqlSugar.DbType.Sqlite,
|
||||
ConnectionString = ConnStr,
|
||||
IsAutoCloseConnection = true,
|
||||
InitKeyType = InitKeyType.Attribute
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取数据库中的所有用户表。数据库不存在时不报错,仅在状态栏提示。
|
||||
/// </summary>
|
||||
private void LoadTables()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(DbFolder)) Directory.CreateDirectory(DbFolder);
|
||||
if (!File.Exists(DbPath))
|
||||
{
|
||||
TableNames = new ObservableCollection<string>();
|
||||
SelectedTable = null;
|
||||
ResultTable = new DataTable();
|
||||
TotalCount = 0;
|
||||
StatusMessage = $"数据库尚未创建:{DbPath}";
|
||||
return;
|
||||
}
|
||||
|
||||
using var db = CreateClient();
|
||||
var tables = db.DbMaintenance.GetTableInfoList(false)
|
||||
.Select(t => t.Name)
|
||||
.OrderBy(n => n)
|
||||
.ToList();
|
||||
|
||||
TableNames = new ObservableCollection<string>(tables);
|
||||
if (tables.Count == 0)
|
||||
{
|
||||
SelectedTable = null;
|
||||
ResultTable = new DataTable();
|
||||
TotalCount = 0;
|
||||
StatusMessage = "数据库已连接,但暂无数据表";
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusMessage = $"已连接:{DbPath}({tables.Count} 张表)";
|
||||
if (string.IsNullOrEmpty(SelectedTable) || !tables.Contains(SelectedTable))
|
||||
{
|
||||
SelectedTable = tables[0]; // setter 会触发 Query
|
||||
}
|
||||
else
|
||||
{
|
||||
Query();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify($"加载数据表失败:{ex.Message}");
|
||||
StatusMessage = $"加载失败:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行分页查询。WHERE 为空则查询全部。
|
||||
/// </summary>
|
||||
private void Query()
|
||||
{
|
||||
if (string.IsNullOrEmpty(SelectedTable) || !File.Exists(DbPath))
|
||||
{
|
||||
ResultTable = new DataTable();
|
||||
TotalCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var db = CreateClient();
|
||||
string table = $"[{SelectedTable}]";
|
||||
string where = string.IsNullOrWhiteSpace(WhereClause) ? "1=1" : WhereClause;
|
||||
|
||||
// COUNT 总数
|
||||
var countSql = $"SELECT COUNT(*) FROM {table} WHERE {where}";
|
||||
TotalCount = db.Ado.GetLong(countSql);
|
||||
|
||||
// 修正越界
|
||||
if (PageIndex > TotalPages) PageIndex = TotalPages;
|
||||
if (PageIndex < 1) PageIndex = 1;
|
||||
|
||||
int offset = (PageIndex - 1) * PageSize;
|
||||
var dataSql = $"SELECT * FROM {table} WHERE {where} LIMIT {PageSize} OFFSET {offset}";
|
||||
ResultTable = db.Ado.GetDataTable(dataSql);
|
||||
|
||||
StatusMessage = $"表 [{SelectedTable}] 共 {TotalCount} 行 第 {PageIndex}/{TotalPages} 页";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify($"查询失败:{ex.Message}");
|
||||
StatusMessage = $"查询失败:{ex.Message}";
|
||||
ResultTable = new DataTable();
|
||||
TotalCount = 0;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 导出 CSV
|
||||
/// <summary>
|
||||
/// 按当前 WHERE 条件导出全部结果到 CSV(不分页)。
|
||||
/// </summary>
|
||||
private void ExportCsv()
|
||||
{
|
||||
if (string.IsNullOrEmpty(SelectedTable) || !File.Exists(DbPath))
|
||||
{
|
||||
StatusMessage = "无可导出数据";
|
||||
return;
|
||||
}
|
||||
|
||||
var dlg = new Microsoft.Win32.SaveFileDialog
|
||||
{
|
||||
Filter = "CSV 文件 (*.csv)|*.csv|所有文件|*.*",
|
||||
FileName = $"{SelectedTable}_{DateTime.Now:yyyyMMdd_HHmmss}.csv",
|
||||
Title = $"导出 [{SelectedTable}] 为 CSV"
|
||||
};
|
||||
if (dlg.ShowDialog() != true) return;
|
||||
|
||||
try
|
||||
{
|
||||
using var db = CreateClient();
|
||||
string table = $"[{SelectedTable}]";
|
||||
string where = string.IsNullOrWhiteSpace(WhereClause) ? "1=1" : WhereClause;
|
||||
var sql = $"SELECT * FROM {table} WHERE {where}";
|
||||
var dt = db.Ado.GetDataTable(sql);
|
||||
|
||||
WriteCsv(dlg.FileName, dt);
|
||||
|
||||
StatusMessage = $"导出完成:{dlg.FileName}({dt.Rows.Count} 行)";
|
||||
LoggerHelper.InfoWithNotify($"工位 [{TestStatus}] 导出 [{SelectedTable}] 至 {dlg.FileName},共 {dt.Rows.Count} 行");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify($"导出失败:{ex.Message}");
|
||||
StatusMessage = $"导出失败:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteCsv(string path, DataTable dt)
|
||||
{
|
||||
// 写 UTF-8 BOM 让 Excel 直接识别中文
|
||||
using var sw = new StreamWriter(path, false, new UTF8Encoding(true));
|
||||
// 表头
|
||||
sw.WriteLine(string.Join(",", dt.Columns.Cast<DataColumn>().Select(c => Escape(c.ColumnName))));
|
||||
// 行
|
||||
foreach (DataRow row in dt.Rows)
|
||||
{
|
||||
sw.WriteLine(string.Join(",", row.ItemArray.Select(v => Escape(v?.ToString() ?? string.Empty))));
|
||||
}
|
||||
}
|
||||
|
||||
private static string Escape(string field)
|
||||
{
|
||||
if (field.Contains('"') || field.Contains(',') || field.Contains('\r') || field.Contains('\n'))
|
||||
{
|
||||
return "\"" + field.Replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
return field;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 双击展开
|
||||
private void OnExpand()
|
||||
{
|
||||
if (string.IsNullOrEmpty(TestStatus)) return;
|
||||
_eventAggregator.GetEvent<ExpandViewEvent>().Publish(TestStatus);
|
||||
GlobalInfoRef.CurrentScope = TestStatus;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 导航
|
||||
public override void OnNavigatedTo(NavigationContext navigationContext)
|
||||
{
|
||||
base.OnNavigatedTo(navigationContext);
|
||||
if (navigationContext.Parameters.ContainsKey("Name"))
|
||||
{
|
||||
TestStatus = navigationContext.Parameters.GetValue<string>("Name");
|
||||
}
|
||||
// 进入界面时自动加载表
|
||||
LoadTables();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user