1131 lines
42 KiB
C#
1131 lines
42 KiB
C#
using FATrace.Com;
|
||
using FATrace.HKNetLib.Hardware;
|
||
using FATrace.HKNetLib.Wrapper;
|
||
using FATrace.Model;
|
||
using FATrace.OEMApp.Services;
|
||
using FATrace.OEMApp.Model;
|
||
using System.Threading.Tasks;
|
||
using LibVLCSharp.Shared;
|
||
using NLog;
|
||
using ReaLTaiizor.Forms;
|
||
using System.Collections.Concurrent;
|
||
using System.ComponentModel;
|
||
using TaskStatus = FATrace.Model.TaskStatus;
|
||
|
||
namespace FATrace.OEMApp
|
||
{
|
||
public partial class MainApp : MaterialForm
|
||
{
|
||
/// <summary>
|
||
/// 日志
|
||
/// </summary>
|
||
private Logger Logger { get; set; } = LogManager.GetCurrentClassLogger();
|
||
|
||
/// <summary>
|
||
/// 海康Camera 客户端
|
||
/// </summary>
|
||
public HkCamera HkCameraClient { get; set; }
|
||
|
||
// 历史表列头中文映射
|
||
private readonly Dictionary<string, string> _historyHeaderMap = new Dictionary<string, string>
|
||
{
|
||
{ nameof(VideoAction.Id), "编号" },
|
||
{ nameof(VideoAction.Code), "条码" },
|
||
{ nameof(VideoAction.User), "用户" },
|
||
{ nameof(VideoAction.VideoFilePath), "视频路径" },
|
||
{ nameof(VideoAction.VideoName), "视频名称" },
|
||
{ nameof(VideoAction.StartTime), "开始时间" },
|
||
{ nameof(VideoAction.EndTime), "结束时间" },
|
||
{ nameof(VideoAction.CreateTime), "创建时间" }
|
||
};
|
||
|
||
private int _lvLogMaxItems = 1000;
|
||
private int _lastProgressLogged = -1;
|
||
|
||
private void InitLvLog()
|
||
{
|
||
try
|
||
{
|
||
LvLog.BeginUpdate();
|
||
LvLog.Clear();
|
||
LvLog.View = View.Details;
|
||
LvLog.FullRowSelect = true;
|
||
LvLog.GridLines = true;
|
||
LvLog.HeaderStyle = ColumnHeaderStyle.Nonclickable;
|
||
LvLog.Columns.Add("时间", 150);
|
||
LvLog.Columns.Add("级别", 100);
|
||
LvLog.Columns.Add("消息", 800);
|
||
}
|
||
finally
|
||
{
|
||
LvLog.EndUpdate();
|
||
}
|
||
}
|
||
|
||
private void AppendLog(string level, string message, Color? color = null)
|
||
{
|
||
if (LvLog == null || LvLog.IsDisposed) return;
|
||
if (InvokeRequired)
|
||
{
|
||
try { BeginInvoke(new Action(() => AppendLog(level, message, color))); } catch { }
|
||
return;
|
||
}
|
||
try
|
||
{
|
||
var item = new ListViewItem(DateTime.Now.ToString("HH:mm:ss.fff"));
|
||
item.SubItems.Add(level);
|
||
item.SubItems.Add(message ?? string.Empty);
|
||
if (color.HasValue) item.ForeColor = color.Value;
|
||
LvLog.Items.Add(item);
|
||
try { LvLog.EnsureVisible(LvLog.Items.Count - 1); } catch { }
|
||
if (LvLog.Items.Count > _lvLogMaxItems)
|
||
{
|
||
for (int i = 0; i < 200; i++) { if (LvLog.Items.Count == 0) break; LvLog.Items.RemoveAt(0); }
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
private void LogInfo(string msg) => AppendLog("INFO", msg, Color.FromArgb(33, 33, 33));
|
||
private void LogWarn(string msg) => AppendLog("WARN", msg, Color.DarkOrange);
|
||
private void LogError(string msg) => AppendLog("ERROR", msg, Color.DarkRed);
|
||
|
||
public MainApp()
|
||
{
|
||
InitializeComponent();
|
||
}
|
||
|
||
/// <summary>
|
||
/// PLC数据服务
|
||
/// </summary>
|
||
private PLCDataService PLCDataService { get; set; }
|
||
private TimeClearDataService TimeClearService { get; set; }
|
||
private System.Windows.Forms.Timer _statusTimer;
|
||
|
||
/// <summary>
|
||
/// 主窗体加载:
|
||
/// - 初始化海康客户端与基础配置(日志目录与保存路径)
|
||
/// - 初始化播放器与历史记录网格绑定
|
||
/// - 启动后台任务服务(顺序下载队列 + Jellyfin 批量监听)
|
||
/// - 初始化并启动 gridRULog(DataGridView)定时刷新,实时展示下载/监听任务状态
|
||
/// </summary>
|
||
private void MainApp_Load(object sender, EventArgs e)
|
||
{
|
||
InitLvLog();
|
||
LogInfo("主界面初始化");
|
||
HkCameraClient = new HkCamera();
|
||
//保存SDK日志
|
||
CHCNetSDK.NET_DVR_SetLogToFile(3, "C:\\SdkLog\\", true);
|
||
LogInfo("已启用海康SDK日志输出 C:\\SdkLog\\");
|
||
|
||
try
|
||
{
|
||
HkCameraClient.NVRLoadVideoProcessEventHandler += HkCameraClient_NVRLoadVideoProcessEventHandler;
|
||
}
|
||
catch { }
|
||
|
||
//读取配置
|
||
//HkCameraClient.NVR_IP = ConfigHelper.GetValue("NVRIP");
|
||
//HkCameraClient.NVR_Port = ConfigHelper.GetValue("NVRPort");
|
||
//HkCameraClient.NVR_UserName = ConfigHelper.GetValue("NVRUserName");
|
||
|
||
PLCDataService = new PLCDataService();
|
||
PLCDataService.PlcConnectedEventHandler += PLCDataService_PlcConnectedEventHandler;
|
||
PLCDataService.ScanCodeEventHandler += PLCDataService_ScanCodeEventHandler;
|
||
|
||
HkCameraClient.NVRVideoSavePath = ConfigHelper.GetValue("NVRVideoSavePath");
|
||
|
||
//NVR登录
|
||
NVRLogin();
|
||
|
||
InitMediaPlayer();
|
||
InitHistoryGridBinding();
|
||
|
||
// 启动后台任务服务:下载队列(单线程顺序)与 Jellyfin 批量监控(并行匹配)
|
||
try
|
||
{
|
||
DownloadTaskWorker.Instance.Start(HkCameraClient);
|
||
LogInfo("下载队列服务已启动");
|
||
try { DownloadTaskWorker.Instance.DownloadFileNameChanged += OnDownloadFileNameChanged; } catch { }
|
||
try { DownloadTaskWorker.Instance.TaskStarted += OnTaskStatusChanged; } catch { }
|
||
try { DownloadTaskWorker.Instance.TaskCompleted += OnTaskStatusChanged; } catch { }
|
||
try { DownloadTaskWorker.Instance.TaskFailed += OnTaskFailed; } catch { }
|
||
// JellyfinMonitorQueueService.Instance.Start();
|
||
// LogInfo("Jellyfin 监控服务已启动");
|
||
LogInfo("Jellyfin 监控服务已停用");
|
||
TimeClearService = new TimeClearDataService();
|
||
TimeClearService.Info += (m) => LogInfo($"[清理]{m}");
|
||
TimeClearService.Start();
|
||
LogInfo("定时清理服务已启动");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine($"[TaskServices] 启动失败: {ex.Message}");
|
||
LogError($"后台服务启动失败: {ex.Message}");
|
||
}
|
||
|
||
// 初始化 gridRULog 并启动 UI 定时刷新
|
||
InitRuLogGrid();
|
||
// 仅初始化一次,当天数据
|
||
RefreshRuLogGrid();
|
||
|
||
// 初始化底部连接状态(立即检测一次 + 定时轻量检测)
|
||
SafeSetStatus(tslPlcConnection, PLCDataService?.PlcConnected == true, "Plc连接状态");
|
||
_ = UpdateDbStatusAsync();
|
||
_ = UpdateNasStatusAsync();
|
||
_ = UpdateNvrStatusAsync();
|
||
StartStatusTimer();
|
||
|
||
//materialListView1.DataBindings
|
||
try
|
||
{
|
||
var systemName = Program.SystemName;
|
||
var authText = Program.IsActive ? "(已授权)" : "(未授权)";
|
||
this.Text = string.IsNullOrWhiteSpace(systemName)
|
||
? $"添加剂追溯系统 {authText}"
|
||
: $"{systemName} {authText}";
|
||
}
|
||
catch (Exception)
|
||
{
|
||
// 安静失败,不影响主界面显示
|
||
}
|
||
|
||
try
|
||
{
|
||
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show($"初始化界面失败: {ex.Message}");
|
||
LogError($"初始化界面失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// PLC数据服务:扫描条码
|
||
/// </summary>
|
||
/// <param name="sender"></param>
|
||
/// <param name="Code"></param>
|
||
private void PLCDataService_ScanCodeEventHandler(object? sender, string Code)
|
||
{
|
||
//解析Code条码数据,内包条码数据
|
||
CurParsedCodeInfo = NVRCom.ParseCodeFull(Code);
|
||
|
||
var taskId = DownloadTaskWorker.Instance.Enqueue(
|
||
CurParsedCodeInfo,
|
||
user: CurUserName,
|
||
start: DateTime.Now,
|
||
end: DateTime.Now.AddSeconds(DownloadTaskWorker.VideoTime+2)
|
||
);
|
||
//MessageBox.Show($"[Test] 已入队下载任务,Id={taskId}");
|
||
|
||
LogInfo($"扫码: {Code}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// PLC数据服务:PLC连接
|
||
/// </summary>
|
||
/// <param name="sender"></param>
|
||
/// <param name="e"></param>
|
||
private void PLCDataService_PlcConnectedEventHandler(object? sender, string e)
|
||
{
|
||
LogInfo($"PLC连接: {e}");
|
||
var ok = string.Equals(e, "OK", StringComparison.OrdinalIgnoreCase);
|
||
SafeSetStatus(tslPlcConnection, ok, "Plc连接状态");
|
||
}
|
||
|
||
private void NVRLogin()
|
||
{
|
||
HkCameraClient.NVR_IP = ConfigHelper.GetValue("NVRIP");
|
||
HkCameraClient.NVR_Port = ushort.Parse(ConfigHelper.GetValue("NVRPort"));
|
||
HkCameraClient.NVR_UserName = ConfigHelper.GetValue("NVRUserName");
|
||
HkCameraClient.NVR_Pw = ConfigHelper.GetValue("NVRPw");
|
||
|
||
LogInfo($"尝试登录NVR {HkCameraClient.NVR_IP}:{HkCameraClient.NVR_Port} 用户 {HkCameraClient.NVR_UserName}");
|
||
var result = HkCameraClient.Sdk_NET_DVR_Login_V30(HkCameraClient.NVR_IP, HkCameraClient.NVR_Port, HkCameraClient.NVR_UserName, HkCameraClient.NVR_Pw);
|
||
if (result)
|
||
{
|
||
MessageBox.Show("登录成功");
|
||
LogInfo("NVR 登录成功");
|
||
_ = UpdateNvrStatusAsync();
|
||
return;
|
||
}
|
||
MessageBox.Show($"登录失败:{HkCameraClient.LastMsgErr}");
|
||
LogError($"NVR 登录失败: {HkCameraClient.LastMsgErr}");
|
||
SafeSetStatus(tslNVRConnection, false, "NVR连接状态");
|
||
return;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 当前登录用户
|
||
/// </summary>
|
||
public string CurUserName { get; set; } = "Admin";
|
||
|
||
#region 视频处理变量
|
||
|
||
/// <summary>
|
||
/// 当前播放的视频
|
||
/// </summary>
|
||
public string CurrentVideoPath { get; set; }
|
||
|
||
/// <summary>
|
||
/// 按文件名/路径去重,避免同一下载完成事件被重复处理
|
||
/// key: 文件名或路径(小写) value: 对应插入的 OEMRawUse.Id(0 表示尚未完成插入)
|
||
/// </summary>
|
||
private readonly ConcurrentDictionary<string, long> _downloadProcessingKeys = new();
|
||
|
||
#endregion
|
||
|
||
|
||
#region 内包信息
|
||
|
||
///// <summary>
|
||
///// 内包条码
|
||
///// </summary>
|
||
//public string CurInBagCode { get; set; } = "AAAAAA";
|
||
|
||
///// <summary>
|
||
///// 内包条码 原料名称
|
||
///// </summary>
|
||
//public string CurRawName { get; set; } = "添加剂Test";
|
||
///// <summary>
|
||
///// 内包条码 原料代码
|
||
///// </summary>
|
||
//public string CurRawCode { get; set; } = "ASASASS";
|
||
|
||
/// <summary>
|
||
/// 内包条码 解析结果
|
||
/// </summary>
|
||
public ParsedCodeInfo CurParsedCodeInfo { get; set; }
|
||
|
||
#endregion
|
||
|
||
private void btnNVRLogin_Click(object sender, EventArgs e)
|
||
{
|
||
HkCameraClient.NVR_IP = ConfigHelper.GetValue("NVRIP");
|
||
HkCameraClient.NVR_Port = ushort.Parse(ConfigHelper.GetValue("NVRPort"));
|
||
HkCameraClient.NVR_UserName = ConfigHelper.GetValue("NVRUserName");
|
||
HkCameraClient.NVR_Pw = ConfigHelper.GetValue("NVRPw");
|
||
|
||
LogInfo($"尝试登录NVR {HkCameraClient.NVR_IP}:{HkCameraClient.NVR_Port} 用户 {HkCameraClient.NVR_UserName}");
|
||
var result = HkCameraClient.Sdk_NET_DVR_Login_V30(HkCameraClient.NVR_IP, HkCameraClient.NVR_Port, HkCameraClient.NVR_UserName, HkCameraClient.NVR_Pw);
|
||
if (result)
|
||
{
|
||
MessageBox.Show("登录成功");
|
||
LogInfo("NVR 登录成功");
|
||
return;
|
||
}
|
||
MessageBox.Show($"登录失败:{HkCameraClient.LastMsgErr}");
|
||
LogError($"NVR 登录失败: {HkCameraClient.LastMsgErr}");
|
||
return;
|
||
}
|
||
|
||
private void HkCameraClient_NVRLoadVideoProcessEventHandler(object? sender, short value)
|
||
{
|
||
this.BeginInvoke(new Action(() =>
|
||
{
|
||
DownloadProgressBarMain.Value = value;
|
||
}));
|
||
if (value >= 100 && _lastProgressLogged != 100)
|
||
{
|
||
_lastProgressLogged = 100;
|
||
LogInfo($"{DownloadTaskWorker.Instance.CurDownloadTask.Code} - 下载进度 100%");
|
||
}
|
||
}
|
||
|
||
private void btnDownloadName_Click(object sender, EventArgs e)
|
||
{
|
||
//if (m_lDownHandle >= 0)
|
||
//{
|
||
// MessageBox.Show("Downloading, please stop firstly!");//正在下载,请先停止下载
|
||
// return;
|
||
//}
|
||
|
||
//string sVideoFileName; //录像文件保存路径和文件名 the path and file name to save
|
||
//sVideoFileName = "D:\\Downtest1111_" + sPlayBackFileName + ".mp4";
|
||
|
||
////按文件名下载 Download by file name
|
||
//m_lDownHandle = CHCNetSDK.NET_DVR_GetFileByName(m_lUserID, sPlayBackFileName, sVideoFileName);
|
||
//if (m_lDownHandle < 0)
|
||
//{
|
||
// iLastErr = CHCNetSDK.NET_DVR_GetLastError();
|
||
// str = "NET_DVR_GetFileByName failed, error code= " + iLastErr;
|
||
// MessageBox.Show(str);
|
||
// return;
|
||
//}
|
||
|
||
//uint iOutValue = 0;
|
||
|
||
////设置转封装格式
|
||
////UInt32 iInValue = 5;
|
||
////IntPtr lpInValue = Marshal.AllocHGlobal(4);
|
||
////Marshal.StructureToPtr(iInValue, lpInValue, false);
|
||
|
||
////if (!CHCNetSDK.NET_DVR_PlayBackControl_V40(m_lDownHandle, CHCNetSDK.NET_DVR_SET_TRANS_TYPE, lpInValue, 4, IntPtr.Zero, ref iOutValue))
|
||
////{
|
||
//// iLastErr = CHCNetSDK.NET_DVR_GetLastError();
|
||
//// str = "NET_DVR_PLAYSTART failed, error code= " + iLastErr; //下载控制失败,输出错误号
|
||
//// MessageBox.Show(str);
|
||
//// return;
|
||
////}
|
||
|
||
//if (!CHCNetSDK.NET_DVR_PlayBackControl_V40(m_lDownHandle, CHCNetSDK.NET_DVR_PLAYSTART, IntPtr.Zero, 0, IntPtr.Zero, ref iOutValue))
|
||
//{
|
||
// iLastErr = CHCNetSDK.NET_DVR_GetLastError();
|
||
// str = "NET_DVR_PLAYSTART failed, error code= " + iLastErr; //下载控制失败,输出错误号
|
||
// MessageBox.Show(str);
|
||
// return;
|
||
//}
|
||
//timerDownload.Interval = 1000;
|
||
//timerDownload.Enabled = true;
|
||
//btnStopDownload.Enabled = true;
|
||
}
|
||
|
||
private void btnStopLoadVideo_Click(object sender, EventArgs e)
|
||
{
|
||
var Result = HkCameraClient.Sdk_NET_DVR_StopGetFile();
|
||
if (Result.Result)
|
||
{
|
||
MessageBox.Show($"[暂停成功] :{Result.Msg}");
|
||
LogInfo($"下载暂停: {Result.Msg}");
|
||
}
|
||
else
|
||
{
|
||
MessageBox.Show($"[暂停失败] :{Result.Msg}");
|
||
LogWarn($"下载暂停失败: {Result.Msg}");
|
||
}
|
||
}
|
||
|
||
#region 视频播放
|
||
|
||
private LibVLC _libVLC;
|
||
private MediaPlayer _mediaPlayer;
|
||
|
||
/// <summary>
|
||
/// 初始化MediaPlay
|
||
/// </summary>
|
||
private void InitMediaPlayer()
|
||
{
|
||
_libVLC = new LibVLC();
|
||
_mediaPlayer = new MediaPlayer(_libVLC);
|
||
videoView1.MediaPlayer = _mediaPlayer;
|
||
|
||
}
|
||
|
||
|
||
#endregion
|
||
|
||
/// <summary>
|
||
/// 测试手动播放
|
||
/// </summary>
|
||
/// <param name="sender"></param>
|
||
/// <param name="e"></param>
|
||
private void btnPlayHistoryVideo_Click(object sender, EventArgs e)
|
||
{
|
||
string path = "D:\\Downtest_Channel2025-09-12 102454.mp4";
|
||
_mediaPlayer.Play(new Media(_libVLC, path));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止播放当前的视频文件
|
||
/// </summary>
|
||
/// <param name="sender"></param>
|
||
/// <param name="e"></param>
|
||
private void btnStopHistoryPlay_Click(object sender, EventArgs e)
|
||
{
|
||
_mediaPlayer.Stop();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理播放资源
|
||
/// </summary>
|
||
private void CleanMediaPlay()
|
||
{
|
||
_mediaPlayer.Stop();
|
||
_mediaPlayer.Dispose();
|
||
_libVLC.Dispose();
|
||
}
|
||
|
||
|
||
#region 历史数据查询
|
||
|
||
|
||
|
||
/// <summary>
|
||
/// 采用 BindingList + BindingSource,使数据变化自动触发 UI 刷新
|
||
/// </summary>
|
||
private BindingSource historyBindingSource { get; set; }
|
||
private BindingList<VideoAction> historyVideoBindingList { get; set; }
|
||
|
||
|
||
/// <summary>
|
||
/// 初始化历史记录表格的数据绑定
|
||
/// </summary>
|
||
private void InitHistoryGridBinding()
|
||
{
|
||
historyBindingSource = new BindingSource();
|
||
historyVideoBindingList = new BindingList<VideoAction>();
|
||
historyBindingSource.DataSource = historyVideoBindingList;
|
||
// 绑定到 WinForms 原生 DataGridView
|
||
dataGridView1.AutoGenerateColumns = true;
|
||
dataGridView1.DataSource = historyBindingSource;
|
||
// 绑定完成后统一调整列头与可见性
|
||
dataGridView1.DataBindingComplete -= DataGridView1_DataBindingComplete;
|
||
dataGridView1.DataBindingComplete += DataGridView1_DataBindingComplete;
|
||
// 双击行播放对应视频
|
||
dataGridView1.CellDoubleClick -= DataGridView1_CellDoubleClick;
|
||
dataGridView1.CellDoubleClick += DataGridView1_CellDoubleClick;
|
||
// 若此时已存在列,立即调整一次
|
||
ConfigureHistoryGridColumns();
|
||
}
|
||
|
||
|
||
private void btnHistoryVideoSearch_Click(object sender, EventArgs e)
|
||
{
|
||
var query = FSqlContext.FDb.Select<VideoAction>();
|
||
if (!string.IsNullOrEmpty(txtSearchCode.Text.Trim()))
|
||
{
|
||
query = query.Where(a => a.Code!.Contains(txtSearchCode.Text.Trim()));
|
||
}
|
||
query.Where(a => a.CreateTime >= PdtHistorySearchStart.Value && a.CreateTime <= PdtHistorySearchEnd.Value);
|
||
|
||
// 拉取结果并刷新绑定列表
|
||
var resultList = query.ToList();
|
||
// 暂停变更通知,批量更新提高效率
|
||
historyVideoBindingList.RaiseListChangedEvents = false;
|
||
historyVideoBindingList.Clear();
|
||
foreach (var item in resultList)
|
||
{
|
||
historyVideoBindingList.Add(item);
|
||
}
|
||
historyVideoBindingList.RaiseListChangedEvents = true;
|
||
// 通知 UI 刷新
|
||
historyVideoBindingList.ResetBindings();
|
||
}
|
||
|
||
// 与历史记录表格列配置相关的成员(WinForms DataGridView)
|
||
private void DataGridView1_DataBindingComplete(object? sender, DataGridViewBindingCompleteEventArgs e)
|
||
{
|
||
try
|
||
{
|
||
ConfigureHistoryGridColumns();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 仅记录,不中断界面
|
||
System.Diagnostics.Debug.WriteLine($"[DataBindingComplete] 列配置失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 双击行,播放该行对应的视频文件
|
||
/// </summary>
|
||
private void DataGridView1_CellDoubleClick(object? sender, DataGridViewCellEventArgs e)
|
||
{
|
||
if (e.RowIndex < 0) return; // 双击列头等
|
||
try
|
||
{
|
||
var row = dataGridView1.Rows[e.RowIndex];
|
||
if (row?.DataBoundItem is VideoAction va)
|
||
{
|
||
var path = va.VideoFilePath;
|
||
if (string.IsNullOrWhiteSpace(path))
|
||
{
|
||
MessageBox.Show("该记录没有可播放的视频路径。", "播放提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
if (!File.Exists(path))
|
||
{
|
||
MessageBox.Show($"未找到视频文件:\n{path}", "播放失败", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
return;
|
||
}
|
||
|
||
_mediaPlayer?.Stop();
|
||
_mediaPlayer?.Play(new LibVLCSharp.Shared.Media(_libVLC, path));
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show($"播放失败: {ex.Message}", "播放失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按照 _historyHeaderMap 配置列头、隐藏不需要的列,并做常用格式化
|
||
/// </summary>
|
||
private void ConfigureHistoryGridColumns()
|
||
{
|
||
if (dataGridView1 == null || dataGridView1.Columns.Count == 0) return;
|
||
|
||
foreach (DataGridViewColumn col in dataGridView1.Columns)
|
||
{
|
||
var propName = col.DataPropertyName;
|
||
|
||
// 设置列头中文
|
||
if (!string.IsNullOrWhiteSpace(propName) && _historyHeaderMap.TryGetValue(propName, out var headerText))
|
||
{
|
||
col.HeaderText = headerText;
|
||
}
|
||
|
||
// 隐藏 Id 列
|
||
if (propName == nameof(VideoAction.Id))
|
||
{
|
||
col.Visible = false;
|
||
continue;
|
||
}
|
||
|
||
//// 常用列宽与格式
|
||
//if (propName is nameof(VideoAction.Code))
|
||
//{
|
||
// col.AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells;
|
||
//}
|
||
//else if (propName is nameof(VideoAction.VideoFilePath))
|
||
//{
|
||
// col.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
|
||
//}
|
||
|
||
// 时间列格式化
|
||
if (propName is nameof(VideoAction.StartTime) or nameof(VideoAction.EndTime) or nameof(VideoAction.CreateTime))
|
||
{
|
||
col.DefaultCellStyle.Format = "yyyy-MM-dd HH:mm:ss";
|
||
col.AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells;
|
||
}
|
||
}
|
||
|
||
// 选中整行,易于操作
|
||
dataGridView1.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
|
||
dataGridView1.MultiSelect = false;
|
||
dataGridView1.ReadOnly = true;
|
||
dataGridView1.AllowUserToAddRows = false;
|
||
}
|
||
|
||
|
||
#endregion
|
||
|
||
|
||
/// <summary>
|
||
/// 测试/结束操作按钮:同样仅入队一条下载任务,方便快速验证下载->监听全流程。
|
||
/// </summary>
|
||
/// <param name="sender"></param>
|
||
/// <param name="e"></param>
|
||
private void btnTestAction_Click(object sender, EventArgs e)
|
||
{
|
||
var taskId = DownloadTaskWorker.Instance.Enqueue(
|
||
CurParsedCodeInfo,
|
||
user: CurUserName,
|
||
start: DateTime.Now,
|
||
end: DateTime.Now.AddSeconds(DownloadTaskWorker.VideoTime)
|
||
);
|
||
//MessageBox.Show($"[Test] 已入队下载任务,Id={taskId}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 下载完成事件:触发后入库并启动 Jellyfin 轮询匹配
|
||
/// </summary>
|
||
/// <param name="sender"></param>
|
||
/// <param name="e"></param>
|
||
private void HkCameraClient_NVRLoadVideoCompleteEventHandler(object? sender, string e)
|
||
{
|
||
// 计算用于去重的 key(文件路径或文件名,小写)
|
||
if (string.IsNullOrWhiteSpace(e) && string.IsNullOrWhiteSpace(this.CurrentVideoPath))
|
||
{
|
||
LogWarn("下载完成事件未提供文件名,忽略");
|
||
return;
|
||
}
|
||
var localNameOrPath = (!string.IsNullOrWhiteSpace(e) && (e.Contains("\\") || e.Contains("/") || e.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase)))
|
||
? e
|
||
: this.CurrentVideoPath;
|
||
var safePath = localNameOrPath ?? string.Empty;
|
||
var key = (System.IO.Path.GetFileName(safePath) ?? safePath).ToLowerInvariant();
|
||
|
||
// 若同一 key 已在处理中,则忽略本次回调
|
||
if (!_downloadProcessingKeys.TryAdd(key, 0))
|
||
{
|
||
System.Diagnostics.Debug.WriteLine($"[NVRLoadVideoComplete] 正在处理相同文件,忽略: {key}");
|
||
return;
|
||
}
|
||
|
||
LogInfo($"下载完成: {localNameOrPath}");
|
||
// Jellyfin 监控已停用:仅移除去重 key
|
||
_downloadProcessingKeys.TryRemove(key, out _);
|
||
}
|
||
|
||
#region 任务监控(gridRULog / DataGridView)
|
||
|
||
private BindingSource ruLogBindingSource;
|
||
private BindingList<RuLogRow> ruLogBindingList;
|
||
|
||
/// <summary>
|
||
/// DataGridView 行模型
|
||
/// </summary>
|
||
private sealed class RuLogRow
|
||
{
|
||
public string TimeText { get; set; } = string.Empty;
|
||
public string Type { get; set; } = string.Empty; // 下载/监听
|
||
public string Code { get; set; } = string.Empty;
|
||
public long TaskId { get; set; }
|
||
public string StatusText { get; set; } = string.Empty;
|
||
public string? ProgressText { get; set; }
|
||
public string Remark { get; set; } = string.Empty; // 错误优先,否则文件路径/ItemId
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化 gridRULog:列定义/数据绑定/低闪烁
|
||
/// </summary>
|
||
private void InitRuLogGrid()
|
||
{
|
||
ruLogBindingSource = new BindingSource();
|
||
ruLogBindingList = new BindingList<RuLogRow>();
|
||
ruLogBindingSource.DataSource = ruLogBindingList;
|
||
|
||
gridRULog.AutoGenerateColumns = false;
|
||
gridRULog.Columns.Clear();
|
||
// 列:时间、类型、任务Id、状态、进度、备注(填充)
|
||
gridRULog.Columns.Add(new DataGridViewTextBoxColumn
|
||
{
|
||
Name = "colTime",
|
||
HeaderText = "时间",
|
||
DataPropertyName = nameof(RuLogRow.TimeText),
|
||
Width = 100,
|
||
AutoSizeMode = DataGridViewAutoSizeColumnMode.None
|
||
});
|
||
gridRULog.Columns.Add(new DataGridViewTextBoxColumn
|
||
{
|
||
Name = "colType",
|
||
HeaderText = "类型",
|
||
DataPropertyName = nameof(RuLogRow.Type),
|
||
Width = 100,
|
||
AutoSizeMode = DataGridViewAutoSizeColumnMode.None
|
||
});
|
||
gridRULog.Columns.Add(new DataGridViewTextBoxColumn
|
||
{
|
||
Name = "colCode",
|
||
HeaderText = "条码",
|
||
DataPropertyName = nameof(RuLogRow.Code),
|
||
Width = 400,
|
||
AutoSizeMode = DataGridViewAutoSizeColumnMode.None
|
||
});
|
||
gridRULog.Columns.Add(new DataGridViewTextBoxColumn
|
||
{
|
||
Name = "colTaskId",
|
||
HeaderText = "任务Id",
|
||
DataPropertyName = nameof(RuLogRow.TaskId),
|
||
Width = 120,
|
||
AutoSizeMode = DataGridViewAutoSizeColumnMode.None
|
||
});
|
||
gridRULog.Columns.Add(new DataGridViewTextBoxColumn
|
||
{
|
||
Name = "colStatus",
|
||
HeaderText = "状态",
|
||
DataPropertyName = nameof(RuLogRow.StatusText),
|
||
Width = 120,
|
||
AutoSizeMode = DataGridViewAutoSizeColumnMode.None
|
||
});
|
||
gridRULog.Columns.Add(new DataGridViewTextBoxColumn
|
||
{
|
||
Name = "colProgress",
|
||
HeaderText = "进度",
|
||
DataPropertyName = nameof(RuLogRow.ProgressText),
|
||
Width = 80,
|
||
AutoSizeMode = DataGridViewAutoSizeColumnMode.None
|
||
});
|
||
gridRULog.Columns.Add(new DataGridViewTextBoxColumn
|
||
{
|
||
Name = "colRemark",
|
||
HeaderText = "备注",
|
||
DataPropertyName = nameof(RuLogRow.Remark),
|
||
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill
|
||
});
|
||
|
||
// 双缓存减少闪烁
|
||
SetDataGridViewDoubleBuffered(gridRULog);
|
||
|
||
gridRULog.DataSource = ruLogBindingSource;
|
||
gridRULog.ColumnHeadersVisible = true;
|
||
gridRULog.EnableHeadersVisualStyles = false;
|
||
gridRULog.AllowUserToResizeRows = false;
|
||
gridRULog.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.DisplayedCells;
|
||
gridRULog.BackgroundColor = Color.White;
|
||
gridRULog.BorderStyle = BorderStyle.FixedSingle;
|
||
gridRULog.ColumnHeadersBorderStyle = DataGridViewHeaderBorderStyle.Single;
|
||
gridRULog.ColumnHeadersDefaultCellStyle.BackColor = Color.Gainsboro;
|
||
gridRULog.ColumnHeadersDefaultCellStyle.ForeColor = Color.Black;
|
||
gridRULog.RowHeadersVisible = false;
|
||
// 使用 Designer 中的 Location/Size 与 Anchor,不额外 Dock
|
||
|
||
// 跟随父容器尺寸变化进行定位与大小调整
|
||
materialCard2.Resize -= MaterialCard2_Resize;
|
||
materialCard2.Resize += MaterialCard2_Resize;
|
||
LayoutRuLogGrid();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据左侧控件宽度动态计算 gridRULog 的 Left/Size,确保位置与尺寸正确
|
||
/// </summary>
|
||
private void LayoutRuLogGrid()
|
||
{
|
||
if (gridRULog == null || materialCard2 == null) return;
|
||
try
|
||
{
|
||
int leftReserved = 540; // 默认左侧操作区宽度
|
||
try
|
||
{
|
||
var candidates = new Control[] { txtRUInBagCode, txtRURawName, btnTestAction, btnRawStopLoadVideo, DownloadProgressBarMain };
|
||
foreach (var c in candidates)
|
||
{
|
||
if (c?.Parent == materialCard2)
|
||
{
|
||
leftReserved = Math.Max(leftReserved, c.Right);
|
||
}
|
||
}
|
||
leftReserved += 20; // 预留间距
|
||
}
|
||
catch { }
|
||
|
||
var client = materialCard2.ClientSize;
|
||
var left = leftReserved;
|
||
var top = 9;
|
||
var width = Math.Max(200, client.Width - left - 14);
|
||
var height = Math.Max(120, client.Height - top - 9);
|
||
gridRULog.Location = new Point(left, top);
|
||
gridRULog.Size = new Size(width, height);
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
private void MaterialCard2_Resize(object? sender, EventArgs e)
|
||
{
|
||
LayoutRuLogGrid();
|
||
}
|
||
|
||
// 定时刷新已移除:仅在下载开始/完成/失败时按需更新
|
||
|
||
/// <summary>
|
||
/// 刷新 gridRULog(仅当天下载)
|
||
/// </summary>
|
||
private void RefreshRuLogGrid()
|
||
{
|
||
try
|
||
{
|
||
var db = FSqlContext.FDb;
|
||
var today = DateTime.Today;
|
||
var downloads = db.Select<DownloadTask>()
|
||
.Where(a => a.UpdateTime >= today)
|
||
.OrderByDescending(a => a.UpdateTime)
|
||
.ToList();
|
||
var rows = new List<RuLogRow>(downloads.Count);
|
||
|
||
// 运行中优先
|
||
foreach (var t in downloads.Where(x => x.Status == TaskStatus.Running))
|
||
{
|
||
rows.Add(new RuLogRow
|
||
{
|
||
TimeText = t.UpdateTime.ToString("HH:mm:ss"),
|
||
Type = "下载",
|
||
Code = t.Code ?? string.Empty,
|
||
TaskId = t.Id,
|
||
StatusText = t.Status.ToString(),
|
||
ProgressText = t.Progress.ToString(),
|
||
Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.VideoFilePath ?? string.Empty) : t.Error!
|
||
});
|
||
}
|
||
// 监听任务展示已移除
|
||
|
||
// 其余
|
||
foreach (var t in downloads.Where(x => x.Status != TaskStatus.Running))
|
||
{
|
||
rows.Add(new RuLogRow
|
||
{
|
||
TimeText = t.UpdateTime.ToString("HH:mm:ss"),
|
||
Type = "下载",
|
||
Code = t.Code ?? string.Empty,
|
||
TaskId = t.Id,
|
||
StatusText = t.Status.ToString(),
|
||
ProgressText = t.Progress.ToString(),
|
||
Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.VideoFilePath ?? string.Empty) : t.Error!
|
||
});
|
||
}
|
||
// 监听任务展示已移除
|
||
|
||
// 批量更新绑定列表
|
||
ruLogBindingList.RaiseListChangedEvents = false;
|
||
ruLogBindingList.Clear();
|
||
foreach (var r in rows)
|
||
{
|
||
ruLogBindingList.Add(r);
|
||
}
|
||
ruLogBindingList.RaiseListChangedEvents = true;
|
||
ruLogBindingList.ResetBindings();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine($"[gridRULog] 刷新失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private void OnTaskStatusChanged(DownloadTask t)
|
||
{
|
||
try
|
||
{
|
||
if (t.UpdateTime.Date != DateTime.Today) return;
|
||
if (InvokeRequired)
|
||
{
|
||
BeginInvoke(new Action(() => UpsertRuLogRowFromTask(t)));
|
||
}
|
||
else
|
||
{
|
||
UpsertRuLogRowFromTask(t);
|
||
}
|
||
|
||
if (t.Status == TaskStatus.Completed || t.Status == TaskStatus.Failed)
|
||
{
|
||
ResetProgressBar();
|
||
}
|
||
if (t.Status == TaskStatus.Completed)
|
||
{
|
||
_ = SaveCsvForTaskAsync(t);
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
private void OnTaskFailed(DownloadTask t, string? error)
|
||
{
|
||
try
|
||
{
|
||
if (t.UpdateTime.Date != DateTime.Today) return; // 仅当天
|
||
if (InvokeRequired)
|
||
{
|
||
BeginInvoke(new Action(() => UpsertRuLogRowFromTask(t)));
|
||
}
|
||
else
|
||
{
|
||
UpsertRuLogRowFromTask(t);
|
||
}
|
||
// 失败同样清零进度
|
||
ResetProgressBar();
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
private void UpsertRuLogRowFromTask(DownloadTask t)
|
||
{
|
||
if (ruLogBindingList == null) return;
|
||
var existing = ruLogBindingList.FirstOrDefault(x => x.TaskId == t.Id);
|
||
if (existing == null)
|
||
{
|
||
ruLogBindingList.Insert(0, new RuLogRow
|
||
{
|
||
TimeText = t.UpdateTime.ToString("HH:mm:ss"),
|
||
Type = "下载",
|
||
Code = t.Code ?? string.Empty,
|
||
TaskId = t.Id,
|
||
StatusText = t.Status.ToString(),
|
||
ProgressText = t.Progress.ToString(),
|
||
Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.VideoFilePath ?? string.Empty) : t.Error!
|
||
});
|
||
}
|
||
else
|
||
{
|
||
existing.TimeText = t.UpdateTime.ToString("HH:mm:ss");
|
||
existing.StatusText = t.Status.ToString();
|
||
existing.ProgressText = t.Progress.ToString();
|
||
existing.Code = t.Code ?? string.Empty;
|
||
existing.Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.VideoFilePath ?? string.Empty) : t.Error!;
|
||
}
|
||
ruLogBindingList.ResetBindings();
|
||
}
|
||
|
||
private void ResetProgressBar()
|
||
{
|
||
try
|
||
{
|
||
if (DownloadProgressBarMain == null || DownloadProgressBarMain.IsDisposed) return;
|
||
if (InvokeRequired)
|
||
{
|
||
BeginInvoke(new Action(() => DownloadProgressBarMain.Value = 0));
|
||
}
|
||
else
|
||
{
|
||
DownloadProgressBarMain.Value = 0;
|
||
}
|
||
_lastProgressLogged = -1;
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
private async Task SaveCsvForTaskAsync(DownloadTask t)
|
||
{
|
||
try
|
||
{
|
||
var dto = new RawUseCsvDto
|
||
{
|
||
RawCode = t.RawCode,
|
||
RawName = t.RawName,
|
||
InBagCode = t.Code,
|
||
OpUser = t.User,
|
||
VideoSavePath = t.VideoFilePath,
|
||
UseTime = t.UpdateTime
|
||
};
|
||
var svc = new CsvService();
|
||
var path = await Task.Run(() => svc.ExportSingle(dto));
|
||
SafeSetCsvState($"CSV保存成功: {path}", true);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
SafeSetCsvState($"CSV保存失败: {ex.Message}", false);
|
||
}
|
||
}
|
||
|
||
private void SafeSetCsvState(string text, bool ok)
|
||
{
|
||
try
|
||
{
|
||
if (txtCsvSaveState == null || txtCsvSaveState.IsDisposed) return;
|
||
if (InvokeRequired)
|
||
{
|
||
BeginInvoke(new Action(() => { txtCsvSaveState.Text = text ?? string.Empty; txtCsvSaveState.ForeColor = ok ? Color.ForestGreen : Color.DarkRed; }));
|
||
}
|
||
else
|
||
{
|
||
txtCsvSaveState.Text = text ?? string.Empty;
|
||
txtCsvSaveState.ForeColor = ok ? Color.ForestGreen : Color.DarkRed;
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
private static void SetDataGridViewDoubleBuffered(DataGridView dgv)
|
||
{
|
||
try
|
||
{
|
||
typeof(DataGridView).InvokeMember(
|
||
"DoubleBuffered",
|
||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.SetProperty,
|
||
null,
|
||
dgv,
|
||
new object[] { true }
|
||
);
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
#endregion
|
||
|
||
private void btnRawStopLoadVideo_Click(object sender, EventArgs e)
|
||
{
|
||
HkCameraClient.Sdk_NET_DVR_StopGetFile();
|
||
}
|
||
|
||
protected override void OnFormClosing(FormClosingEventArgs e)
|
||
{
|
||
try { DownloadTaskWorker.Instance.DownloadFileNameChanged -= OnDownloadFileNameChanged; } catch { }
|
||
try { DownloadTaskWorker.Instance.TaskStarted -= OnTaskStatusChanged; } catch { }
|
||
try { DownloadTaskWorker.Instance.TaskCompleted -= OnTaskStatusChanged; } catch { }
|
||
try { DownloadTaskWorker.Instance.TaskFailed -= OnTaskFailed; } catch { }
|
||
try { _statusTimer?.Stop(); _statusTimer?.Dispose(); _statusTimer = null; } catch { }
|
||
try { TimeClearService?.Stop(); } catch { }
|
||
base.OnFormClosing(e);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 当前下载文件的名称改变
|
||
/// </summary>
|
||
/// <param name="name"></param>
|
||
private void OnDownloadFileNameChanged(string name)
|
||
{
|
||
try
|
||
{
|
||
if (DownloadFileName == null || DownloadFileName.IsDisposed) return;
|
||
if (InvokeRequired)
|
||
{
|
||
try { BeginInvoke(new Action(() => DownloadFileName.Text = name ?? string.Empty)); } catch { }
|
||
}
|
||
else
|
||
{
|
||
try { BeginInvoke(new Action(() => DownloadFileName.Text = name ?? string.Empty)); } catch { }
|
||
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
#region 底部状态栏(PLC/DB/NAS/NVR)
|
||
private void StartStatusTimer()
|
||
{
|
||
try
|
||
{
|
||
_statusTimer = new System.Windows.Forms.Timer();
|
||
_statusTimer.Interval = 30000; // 30s 轻量检测
|
||
_statusTimer.Tick += async (s, e) =>
|
||
{
|
||
await UpdateDbStatusAsync();
|
||
await UpdateNasStatusAsync();
|
||
await UpdateNvrStatusAsync();
|
||
};
|
||
_statusTimer.Start();
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
private void SafeSetStatus(ToolStripStatusLabel lbl, bool ok, string title)
|
||
{
|
||
try
|
||
{
|
||
string text = ok ? $"{title}: 正常" : $"{title}: 异常";
|
||
var fore = ok ? Color.ForestGreen : Color.DarkRed;
|
||
if (InvokeRequired)
|
||
{
|
||
BeginInvoke(new Action(() => { lbl.Text = text; lbl.ForeColor = fore; }));
|
||
}
|
||
else
|
||
{
|
||
lbl.Text = text; lbl.ForeColor = fore;
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
private async Task UpdateDbStatusAsync()
|
||
{
|
||
bool ok = false;
|
||
try
|
||
{
|
||
var db = FSqlContext.FDb;
|
||
var ret = await db.Ado.ExecuteScalarAsync("SELECT 1");
|
||
ok = Convert.ToString(ret) == "1";
|
||
}
|
||
catch { ok = false; }
|
||
SafeSetStatus(tslSqlConnection, ok, "服务连接状态");
|
||
}
|
||
|
||
private async Task UpdateNasStatusAsync()
|
||
{
|
||
bool ok = false;
|
||
try
|
||
{
|
||
var path = HkCameraClient?.NVRVideoSavePath;
|
||
if (!string.IsNullOrWhiteSpace(path))
|
||
{
|
||
ok = await Task.Run(() => System.IO.Directory.Exists(path));
|
||
}
|
||
}
|
||
catch { ok = false; }
|
||
SafeSetStatus(tslNasConnection, ok, "NAS连接状态");
|
||
}
|
||
|
||
private async Task UpdateNvrStatusAsync()
|
||
{
|
||
bool ok = false;
|
||
try
|
||
{
|
||
ok = HkCameraClient != null && HkCameraClient.NVRLoginState && HkCameraClient.IsOnline();
|
||
}
|
||
catch { ok = false; }
|
||
await Task.Yield();
|
||
SafeSetStatus(tslNVRConnection, ok, "NVR连接状态");
|
||
}
|
||
#endregion
|
||
}
|
||
}
|