海康摄像头增加了云台控制等
This commit is contained in:
14
SHH.CameraSdk/Abstractions/IHikContext.cs
Normal file
14
SHH.CameraSdk/Abstractions/IHikContext.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 海康驱动上下文
|
||||
/// 作用:允许功能组件(如校时、云台)访问主驱动的核心数据,而无需公开给外部
|
||||
/// </summary>
|
||||
public interface IHikContext
|
||||
{
|
||||
/// <summary> 获取 SDK 登录句柄 (lUserId) </summary>
|
||||
int GetUserId();
|
||||
|
||||
/// <summary> 获取设备 IP (用于日志) </summary>
|
||||
string GetDeviceIp();
|
||||
}
|
||||
40
SHH.CameraSdk/Abstractions/ISyncFeature.cs
Normal file
40
SHH.CameraSdk/Abstractions/ISyncFeature.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using SHH.CameraSdk.HikFeatures;
|
||||
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 能力接口:时间同步
|
||||
/// 只有实现了此接口的设备,才支持 WebAPI 的时间查询与设置
|
||||
/// </summary>
|
||||
public interface ITimeSyncFeature
|
||||
{
|
||||
/// <summary> 获取设备当前时间 </summary>
|
||||
Task<DateTime> GetTimeAsync();
|
||||
|
||||
/// <summary> 设置设备时间 </summary>
|
||||
Task SetTimeAsync(DateTime time);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 能力接口:设备重启
|
||||
/// </summary>
|
||||
public interface IRebootFeature
|
||||
{
|
||||
/// <summary>
|
||||
/// 发送重启指令
|
||||
/// </summary>
|
||||
/// <returns>任务完成表示指令发送成功</returns>
|
||||
Task RebootAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 能力接口:云台控制
|
||||
/// </summary>
|
||||
public interface IPtzFeature
|
||||
{
|
||||
// 原有的手动控制 (按下/松开)
|
||||
Task PtzControlAsync(PtzAction action, bool stop, int speed = 4);
|
||||
|
||||
// [新增] 点动控制 (自动复位)
|
||||
Task PtzStepAsync(PtzAction action, int durationMs, int speed = 4);
|
||||
}
|
||||
@@ -399,4 +399,126 @@ public class CamerasController : ControllerBase
|
||||
// 3. 返回 JSON 给前端
|
||||
return Ok(options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取设备时间 (支持海康/大华等具备此能力的设备)
|
||||
/// </summary>
|
||||
[HttpGet("{id}/time")]
|
||||
public async Task<IActionResult> GetDeviceTime(long id)
|
||||
{
|
||||
var device = _manager.GetDevice(id);
|
||||
if (device == null) return NotFound(new { error = "Device not found" });
|
||||
|
||||
if (!device.IsPhysicalOnline) return BadRequest(new { error = "Device is offline" });
|
||||
|
||||
// 【核心】模式匹配:判断这个设备是否实现了“时间同步接口”
|
||||
if (device is ITimeSyncFeature timeFeature)
|
||||
{
|
||||
try
|
||||
{
|
||||
var time = await timeFeature.GetTimeAsync();
|
||||
return Ok(new { deviceId = id, currentTime = time });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是 RTSP/USB 等不支持的设备
|
||||
return BadRequest(new { error = "This device does not support time synchronization." });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置设备时间
|
||||
/// </summary>
|
||||
[HttpPost("{id}/time")]
|
||||
public async Task<IActionResult> SetDeviceTime(long id, [FromBody] DateTime time)
|
||||
{
|
||||
var device = _manager.GetDevice(id);
|
||||
if (device == null) return NotFound();
|
||||
|
||||
if (device is ITimeSyncFeature timeFeature)
|
||||
{
|
||||
try
|
||||
{
|
||||
await timeFeature.SetTimeAsync(time);
|
||||
return Ok(new { success = true, message = $"Time synced to {time}" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
return BadRequest(new { error = "This device does not support time synchronization." });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 远程重启设备
|
||||
/// </summary>
|
||||
[HttpPost("{id}/reboot")]
|
||||
public async Task<IActionResult> RebootDevice(long id)
|
||||
{
|
||||
var device = _manager.GetDevice(id);
|
||||
if (device == null) return NotFound(new { error = "Device not found" });
|
||||
|
||||
// 依然是两道防线:先检查在线,再检查能力
|
||||
if (!device.IsOnline) return BadRequest(new { error = "Device is offline" });
|
||||
|
||||
if (device is IRebootFeature rebootFeature)
|
||||
{
|
||||
try
|
||||
{
|
||||
await rebootFeature.RebootAsync();
|
||||
|
||||
// 记录审计日志 (建议加上)
|
||||
device.AddAuditLog("用户执行了远程重启");
|
||||
|
||||
return Ok(new { success = true, message = "重启指令已发送,设备将在几分钟后重新上线。" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
return BadRequest(new { error = "This device does not support remote reboot." });
|
||||
}
|
||||
|
||||
[HttpPost("{id}/ptz")]
|
||||
public async Task<IActionResult> PtzControl(long id, [FromBody] PtzControlDto dto)
|
||||
{
|
||||
var device = _manager.GetDevice(id);
|
||||
if (device == null) return NotFound();
|
||||
if (!device.IsOnline) return BadRequest("Device offline");
|
||||
|
||||
if (device is IPtzFeature ptz)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 逻辑分流
|
||||
if (dto.Duration > 0)
|
||||
{
|
||||
// 场景:点动模式 (一次调用,自动停止)
|
||||
// 建议限制一下最大时长,防止前端传个 10000秒 导致云台转疯了
|
||||
int safeDuration = Math.Clamp(dto.Duration, 50, 2000);
|
||||
await ptz.PtzStepAsync(dto.Action, safeDuration, dto.Speed);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 场景:手动模式 (按下/松开)
|
||||
await ptz.PtzControlAsync(dto.Action, dto.Stop, dto.Speed);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new { error = ex.Message });
|
||||
}//
|
||||
}
|
||||
|
||||
return BadRequest("Device does not support PTZ.");
|
||||
}
|
||||
}
|
||||
22
SHH.CameraSdk/Controllers/Dto/PtzControlDto.cs
Normal file
22
SHH.CameraSdk/Controllers/Dto/PtzControlDto.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using SHH.CameraSdk.HikFeatures;
|
||||
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
public class PtzControlDto
|
||||
{
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public PtzAction Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 仅用于手动模式:true=停止, false=开始
|
||||
/// </summary>
|
||||
public bool Stop { get; set; }
|
||||
|
||||
public int Speed { get; set; } = 4;
|
||||
|
||||
/// <summary>
|
||||
/// [新增] 点动耗时 (毫秒)
|
||||
/// 如果此值 > 0,则忽略 Stop 参数,执行 "开始 -> 等待 -> 停止" 的原子操作
|
||||
/// </summary>
|
||||
public int Duration { get; set; } = 0;
|
||||
}
|
||||
19
SHH.CameraSdk/Drivers/HikVision/Features/FeaturesEnums.cs
Normal file
19
SHH.CameraSdk/Drivers/HikVision/Features/FeaturesEnums.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace SHH.CameraSdk.HikFeatures;
|
||||
|
||||
/// <summary>
|
||||
/// 云台动作枚举
|
||||
/// </summary>
|
||||
public enum PtzAction
|
||||
{
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
ZoomIn, // 放大
|
||||
ZoomOut, // 缩小
|
||||
FocusNear, // 聚焦近
|
||||
FocusFar, // 聚焦远
|
||||
IrisOpen, // 光圈大
|
||||
IrisClose, // 光圈小
|
||||
Wiper, // 雨刷
|
||||
}
|
||||
71
SHH.CameraSdk/Drivers/HikVision/Features/HikPtzProvider.cs
Normal file
71
SHH.CameraSdk/Drivers/HikVision/Features/HikPtzProvider.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
namespace SHH.CameraSdk.HikFeatures;
|
||||
|
||||
public class HikPtzProvider : IPtzFeature
|
||||
{
|
||||
private readonly IHikContext _context;
|
||||
|
||||
public HikPtzProvider(IHikContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task PtzControlAsync(PtzAction action, bool stop, int speed)
|
||||
{
|
||||
int userId = _context.GetUserId();
|
||||
if (userId < 0) throw new InvalidOperationException("设备离线");
|
||||
|
||||
// 1. 映射指令
|
||||
uint hikCommand = action switch
|
||||
{
|
||||
PtzAction.Up => HikNativeMethods.TILT_UP,
|
||||
PtzAction.Down => HikNativeMethods.TILT_DOWN,
|
||||
PtzAction.Left => HikNativeMethods.PAN_LEFT,
|
||||
PtzAction.Right => HikNativeMethods.PAN_RIGHT,
|
||||
PtzAction.ZoomIn => HikNativeMethods.ZOOM_IN,
|
||||
PtzAction.ZoomOut => HikNativeMethods.ZOOM_OUT,
|
||||
PtzAction.FocusNear => HikNativeMethods.FOCUS_NEAR,
|
||||
PtzAction.FocusFar => HikNativeMethods.FOCUS_FAR,
|
||||
PtzAction.IrisOpen => HikNativeMethods.IRIS_OPEN,
|
||||
PtzAction.IrisClose => HikNativeMethods.IRIS_CLOSE,
|
||||
PtzAction.Wiper => HikNativeMethods.WIPER_PWRON,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
if (hikCommand == 0) return;
|
||||
|
||||
// 2. 转换停止标志 (海康: 0=开始, 1=停止)
|
||||
uint dwStop = stop ? 1u : 0u;
|
||||
|
||||
// 3. 限制速度范围 (1-7)
|
||||
uint dwSpeed = (uint)Math.Clamp(speed, 1, 7);
|
||||
|
||||
// 4. 调用 SDK
|
||||
await Task.Run(() =>
|
||||
{
|
||||
// Channel 默认为 1
|
||||
bool result = HikNativeMethods.NET_DVR_PTZControlWithSpeed_Other(userId, 1, hikCommand, dwStop, dwSpeed);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
// 这里通常不抛异常,因为云台繁忙或到头是常态,记录日志即可
|
||||
// Console.WriteLine($"PTZ Error: {HikNativeMethods.NET_DVR_GetLastError()}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task PtzStepAsync(PtzAction action, int durationMs, int speed)
|
||||
{
|
||||
// 1. 开始转动 (调用已有的逻辑,stop=false)
|
||||
await PtzControlAsync(action, false, speed);
|
||||
|
||||
// 2. 等待指定时间 (非阻塞等待)
|
||||
// 注意:这里使用 Task.Delay,精度对云台控制来说足够了
|
||||
if (durationMs > 0)
|
||||
{
|
||||
await Task.Delay(durationMs);
|
||||
}
|
||||
|
||||
// 3. 停止转动 (调用已有的逻辑,stop=true)
|
||||
await PtzControlAsync(action, true, speed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace SHH.CameraSdk.HikFeatures;
|
||||
|
||||
public class HikRebootProvider : IRebootFeature
|
||||
{
|
||||
private readonly IHikContext _context;
|
||||
|
||||
public HikRebootProvider(IHikContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task RebootAsync()
|
||||
{
|
||||
// 1. 检查登录状态
|
||||
int userId = _context.GetUserId();
|
||||
if (userId < 0) throw new InvalidOperationException("设备未登录或离线,无法发送重启指令");
|
||||
|
||||
// 2. 执行 SDK 调用
|
||||
await Task.Run(() =>
|
||||
{
|
||||
bool result = HikNativeMethods.NET_DVR_RebootDVR(userId);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
uint err = HikNativeMethods.NET_DVR_GetLastError();
|
||||
throw new Exception($"重启指令发送失败,错误码: {err}");
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 注意:
|
||||
// 重启指令发送成功后,设备会断开网络。
|
||||
// 宿主类(HikVideoSource)的保活机制(KeepAlive)会检测到断线,
|
||||
// 并自动开始尝试重连,直到设备重启完成上线。
|
||||
// 所以这里我们不需要手动断开连接,交给底层自愈机制即可。
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
namespace SHH.CameraSdk.HikFeatures;
|
||||
|
||||
/// <summary>
|
||||
/// 海康时间同步组件
|
||||
/// </summary>
|
||||
public class HikTimeSyncProvider : ITimeSyncFeature
|
||||
{
|
||||
private readonly IHikContext _context;
|
||||
|
||||
public HikTimeSyncProvider(IHikContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<DateTime> GetTimeAsync()
|
||||
{
|
||||
// 1. 获取句柄
|
||||
int userId = _context.GetUserId();
|
||||
if (userId < 0) throw new InvalidOperationException("设备未登录");
|
||||
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
uint returned = 0;
|
||||
uint size = (uint)Marshal.SizeOf(typeof(HikNativeMethods.NET_DVR_TIME));
|
||||
IntPtr ptr = Marshal.AllocHGlobal((int)size);
|
||||
|
||||
try
|
||||
{
|
||||
// 调用 SDK: NET_DVR_GET_TIME (命令号 118)
|
||||
bool result = HikNativeMethods.NET_DVR_GetDVRConfig(
|
||||
userId,
|
||||
HikNativeMethods.NET_DVR_GET_TIMECFG,
|
||||
0,
|
||||
ptr,
|
||||
size,
|
||||
ref returned);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
var err = HikNativeMethods.NET_DVR_GetLastError();
|
||||
throw new Exception($"获取时间失败, 错误码: {err}");
|
||||
}
|
||||
|
||||
var t = Marshal.PtrToStructure<HikNativeMethods.NET_DVR_TIME>(ptr);
|
||||
return new DateTime((int)t.dwYear, (int)t.dwMonth, (int)t.dwDay, (int)t.dwHour, (int)t.dwMinute, (int)t.dwSecond);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(ptr);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task SetTimeAsync(DateTime time)
|
||||
{
|
||||
int userId = _context.GetUserId();
|
||||
if (userId < 0) throw new InvalidOperationException("设备未登录");
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
var t = new HikNativeMethods.NET_DVR_TIME
|
||||
{
|
||||
dwYear = (uint)time.Year,
|
||||
dwMonth = (uint)time.Month,
|
||||
dwDay = (uint)time.Day,
|
||||
dwHour = (uint)time.Hour,
|
||||
dwMinute = (uint)time.Minute,
|
||||
dwSecond = (uint)time.Second
|
||||
};
|
||||
|
||||
uint size = (uint)Marshal.SizeOf(t);
|
||||
IntPtr ptr = Marshal.AllocHGlobal((int)size);
|
||||
|
||||
try
|
||||
{
|
||||
Marshal.StructureToPtr(t, ptr, false);
|
||||
|
||||
// 调用 SDK: NET_DVR_SET_TIME (命令号 119)
|
||||
bool result = HikNativeMethods.NET_DVR_SetDVRConfig(
|
||||
userId,
|
||||
HikNativeMethods.NET_DVR_SET_TIMECFG,
|
||||
0,
|
||||
ptr,
|
||||
size);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
var err = HikNativeMethods.NET_DVR_GetLastError();
|
||||
throw new Exception($"设置时间失败, 错误码: {err}");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(ptr);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -252,6 +252,9 @@ public static partial class HikNativeMethods
|
||||
/// <summary> 命令常量:获取时间配置 </summary>
|
||||
public const uint NET_DVR_GET_TIMECFG = 118;
|
||||
|
||||
/// <summary> 命令常量:设置时间 </summary>
|
||||
public const int NET_DVR_SET_TIMECFG = 119;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- PTZ 控制相关 (PTZ Control) ---
|
||||
@@ -492,4 +495,16 @@ public static partial class HikNativeMethods
|
||||
ref uint lpBytesReturned);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
[DllImport(DllName)]
|
||||
public static extern bool NET_DVR_SetDVRConfig(int lUserID, uint dwCommand, int lChannel, System.IntPtr lpInBuffer, uint dwInBufferSize);
|
||||
|
||||
/// <summary>
|
||||
/// 设备重启
|
||||
/// </summary>
|
||||
/// <param name="lUserID"></param>
|
||||
/// <returns></returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool NET_DVR_RebootDVR(int lUserID);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using OpenCvSharp;
|
||||
using SHH.CameraSdk.HikFeatures;
|
||||
using System;
|
||||
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
@@ -9,7 +11,8 @@ namespace SHH.CameraSdk;
|
||||
/// 2. [Feat A] 热更新支持:实现 OnApplyOptions,支持码流/句柄不亦断线热切换。
|
||||
/// 3. [Feat B] 审计集成:全面接入 AddAuditLog,对接 Web 运维仪表盘。
|
||||
/// </summary>
|
||||
public class HikVideoSource : BaseVideoSource
|
||||
public class HikVideoSource : BaseVideoSource,
|
||||
IHikContext, ITimeSyncFeature, IRebootFeature, IPtzFeature
|
||||
{
|
||||
#region --- 静态资源 (Global Resources) ---
|
||||
|
||||
@@ -22,6 +25,33 @@ public class HikVideoSource : BaseVideoSource
|
||||
|
||||
#endregion
|
||||
|
||||
// 声明组件
|
||||
private readonly HikTimeSyncProvider _timeProvider;
|
||||
private readonly HikRebootProvider _rebootProvider;
|
||||
private readonly HikPtzProvider _ptzProvider;
|
||||
|
||||
// ==========================================
|
||||
// 实现 IHikContext (核心数据暴露)
|
||||
// ==========================================
|
||||
public int GetUserId() => _userId; // 暴露父类或私有的 _userId
|
||||
public string GetDeviceIp() => Config.IpAddress;
|
||||
|
||||
// ==========================================
|
||||
// 实现 ITimeSyncFeature (路由转发)
|
||||
// ==========================================
|
||||
// 核心逻辑:全部委托给 _timeProvider 处理,自己不写一行逻辑
|
||||
public Task<DateTime> GetTimeAsync() => _timeProvider.GetTimeAsync();
|
||||
|
||||
public Task SetTimeAsync(DateTime time) => _timeProvider.SetTimeAsync(time);
|
||||
|
||||
public Task RebootAsync() => _rebootProvider.RebootAsync();
|
||||
|
||||
public Task PtzControlAsync(PtzAction action, bool stop, int speed = 4)
|
||||
=> _ptzProvider.PtzControlAsync(action, stop, speed);
|
||||
|
||||
public Task PtzStepAsync(PtzAction action, int durationMs, int speed = 4)
|
||||
=> _ptzProvider.PtzStepAsync(action, durationMs, speed);
|
||||
|
||||
#region --- 实例成员 (Instance Members) ---
|
||||
|
||||
private int _userId = -1; // SDK 登录句柄
|
||||
@@ -53,7 +83,10 @@ public class HikVideoSource : BaseVideoSource
|
||||
|
||||
public HikVideoSource(VideoSourceConfig config) : base(config)
|
||||
{
|
||||
// 构造函数保持简洁
|
||||
// 初始化组件,将 "this" 作为上下文传进去
|
||||
_timeProvider = new HikTimeSyncProvider(this);
|
||||
_rebootProvider = new HikRebootProvider(this);
|
||||
_ptzProvider = new HikPtzProvider(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -7,80 +7,197 @@
|
||||
<link href="https://cdn.staticfile.org/bootstrap-icons/1.10.0/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||
<style>
|
||||
[v-cloak] { display: none; }
|
||||
body { background: #fff; margin: 0; padding: 0; font-family: "Segoe UI", sans-serif; overflow: hidden; display: flex; flex-direction: column; height: 100vh; }
|
||||
|
||||
.modal-header-custom { padding: 12px 20px; background: #f8f9fa; border-bottom: 1px solid #dee2e6; display: flex; justify-content: space-between; align-items: center; }
|
||||
.content-body { flex: 1; padding: 20px; overflow-y: auto; display: flex; gap: 20px; }
|
||||
html, body {
|
||||
height: 100%; margin: 0; padding: 0;
|
||||
font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
overflow: hidden; background: #fff;
|
||||
}
|
||||
|
||||
/* 左侧:云台控制区 */
|
||||
.ptz-section { flex: 0 0 240px; display: flex; flex-direction: column; align-items: center; border-right: 1px solid #eee; padding-right: 20px; }
|
||||
#app {
|
||||
height: 100%; display: flex; flex-direction: column;
|
||||
}
|
||||
|
||||
.d-pad { position: relative; width: 180px; height: 180px; background: #f1f3f5; border-radius: 50%; margin-bottom: 20px; box-shadow: inset 0 2px 10px rgba(0,0,0,0.05); }
|
||||
.d-btn { position: absolute; width: 50px; height: 50px; border: none; background: #fff; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); color: #495057; transition: 0.1s; display: flex; align-items: center; justify-content: center; cursor: pointer; }
|
||||
.d-btn:active { background: #e9ecef; transform: scale(0.95); box-shadow: inset 0 2px 4px rgba(0,0,0,0.1); color: #0d6efd; }
|
||||
.d-btn i { font-size: 1.5rem; }
|
||||
/* 头部 */
|
||||
.page-header {
|
||||
padding: 15px 30px; border-bottom: 1px solid #e9ecef; background: #fff;
|
||||
display: flex; justify-content: space-between; align-items: center; flex-shrink: 0;
|
||||
}
|
||||
.page-title { font-size: 1.2rem; font-weight: 700; color: #343a40; display: flex; align-items: center; }
|
||||
|
||||
.btn-up { top: 10px; left: 65px; }
|
||||
.btn-down { bottom: 10px; left: 65px; }
|
||||
.btn-left { top: 65px; left: 10px; }
|
||||
.btn-right { top: 65px; right: 10px; }
|
||||
.btn-center { top: 65px; left: 65px; border-radius: 50%; background: #e7f1ff; color: #0d6efd; font-weight: bold; font-size: 0.8rem; }
|
||||
/* 内容区:左右分栏 */
|
||||
.content-body {
|
||||
flex: 1; overflow: hidden;
|
||||
display: flex;
|
||||
max-width: 1400px; margin: 0 auto; width: 100%;
|
||||
}
|
||||
|
||||
.zoom-ctrl { display: flex; width: 100%; gap: 10px; justify-content: center; }
|
||||
.zoom-btn { flex: 1; padding: 8px; border: 1px solid #dee2e6; background: #fff; border-radius: 6px; font-size: 0.9rem; font-weight: 600; color: #555; }
|
||||
.zoom-btn:active { background: #f8f9fa; border-color: #adb5bd; }
|
||||
/* 左侧:云台区 */
|
||||
.ptz-section {
|
||||
flex: 0 0 400px;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
border-right: 1px solid #f0f0f0; background: #fafafa;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 右侧:系统维护区 */
|
||||
.sys-section { flex: 1; display: flex; flex-direction: column; gap: 15px; }
|
||||
.func-card { border: 1px solid #e9ecef; border-radius: 8px; padding: 15px; background: #fff; transition: 0.2s; }
|
||||
.func-card:hover { border-color: #dee2e6; box-shadow: 0 2px 8px rgba(0,0,0,0.02); }
|
||||
.func-title { font-size: 0.9rem; font-weight: 700; margin-bottom: 10px; color: #343a40; display: flex; align-items: center; }
|
||||
.time-box { background: #f8f9fa; padding: 8px 12px; border-radius: 4px; font-family: monospace; font-size: 1.1rem; letter-spacing: 1px; color: #0d6efd; text-align: center; border: 1px solid #e9ecef; margin-bottom: 10px; }
|
||||
/* 右侧:系统功能区 */
|
||||
.sys-section {
|
||||
flex: 1; padding: 30px 40px; overflow-y: auto;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 超大方向盘 */
|
||||
.d-pad-container { position: relative; width: 260px; height: 260px; margin-bottom: 30px; }
|
||||
.d-pad-bg { width: 100%; height: 100%; background: #e9ecef; border-radius: 50%; box-shadow: inset 0 5px 15px rgba(0,0,0,0.05); position: absolute; z-index: 0; }
|
||||
|
||||
.ptz-btn {
|
||||
position: absolute; width: 65px; height: 65px; border: none; background: #fff;
|
||||
border-radius: 12px; box-shadow: 0 4px 10px rgba(0,0,0,0.08); color: #495057;
|
||||
display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 1;
|
||||
transition: all 0.1s;
|
||||
}
|
||||
.ptz-btn:active { background: #0d6efd; color: #fff; transform: scale(0.92); box-shadow: 0 2px 5px rgba(13,110,253,0.3); }
|
||||
.ptz-btn i { font-size: 1.8rem; }
|
||||
|
||||
.btn-up { top: 15px; left: 97.5px; }
|
||||
.btn-down { bottom: 15px; left: 97.5px; }
|
||||
.btn-left { top: 97.5px; left: 15px; }
|
||||
.btn-right { top: 97.5px; right: 15px; }
|
||||
.btn-center {
|
||||
top: 97.5px; left: 97.5px; border-radius: 50%;
|
||||
background: #fff; color: #dc3545;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
.btn-center:active { background: #dc3545; color: #fff; }
|
||||
|
||||
/* 速度滑块 */
|
||||
.speed-ctrl { width: 80%; padding: 15px; background: #fff; border: 1px solid #dee2e6; border-radius: 8px; }
|
||||
|
||||
/* 右侧分组 */
|
||||
.ctrl-group { border: 1px solid #e9ecef; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
|
||||
.group-title { font-size: 0.9rem; font-weight: 700; color: #0d6efd; margin-bottom: 15px; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 2px solid #f0f0f0; padding-bottom: 8px; }
|
||||
|
||||
.lens-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
||||
.lens-label { font-weight: 600; color: #555; width: 80px; }
|
||||
|
||||
.time-display {
|
||||
font-family: 'Consolas', monospace; font-size: 1.2rem; color: #0d6efd;
|
||||
background: #f8f9fa; padding: 10px; text-align: center; border-radius: 6px; margin-bottom: 15px; border: 1px dashed #dee2e6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<div class="modal-header-custom">
|
||||
<h6 class="m-0 fw-bold"><i class="bi bi-joystick me-2 text-primary"></i>设备控制台</h6>
|
||||
<div class="text-muted small">ID: {{ deviceId }}</div>
|
||||
<div class="page-header">
|
||||
<div class="page-title">
|
||||
<i class="bi bi-joystick me-2 text-primary"></i>
|
||||
云台与设备控制 <span class="text-muted ms-2 fw-normal fs-6">(ID: {{ deviceId }})</span>
|
||||
</div>
|
||||
<button class="btn btn-outline-secondary" @click="closeMode">
|
||||
<i class="bi bi-arrow-return-left me-1"></i> 返回
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="content-body">
|
||||
|
||||
<div class="ptz-section">
|
||||
<h6 class="text-muted small mb-3 fw-bold">云台控制 (PTZ)</h6>
|
||||
<div class="d-pad">
|
||||
<button class="d-btn btn-up" @mousedown="ptz('up')" @mouseup="ptzStop"><i class="bi bi-caret-up-fill"></i></button>
|
||||
<button class="d-btn btn-left" @mousedown="ptz('left')" @mouseup="ptzStop"><i class="bi bi-caret-left-fill"></i></button>
|
||||
<button class="d-btn btn-center" @click="ptz('home')" title="回原点">HOME</button>
|
||||
<button class="d-btn btn-right" @mousedown="ptz('right')" @mouseup="ptzStop"><i class="bi bi-caret-right-fill"></i></button>
|
||||
<button class="d-btn btn-down" @mousedown="ptz('down')" @mouseup="ptzStop"><i class="bi bi-caret-down-fill"></i></button>
|
||||
<h6 class="text-muted text-uppercase fw-bold mb-4" style="letter-spacing: 1px;">PTZ Direction</h6>
|
||||
|
||||
<div class="d-pad-container">
|
||||
<div class="d-pad-bg"></div>
|
||||
<button class="ptz-btn btn-up" @mousedown="ptzStart('Up')" @mouseup="ptzStop('Up')" @mouseleave="ptzStop('Up')"><i class="bi bi-caret-up-fill"></i></button>
|
||||
<button class="ptz-btn btn-left" @mousedown="ptzStart('Left')" @mouseup="ptzStop('Left')" @mouseleave="ptzStop('Left')"><i class="bi bi-caret-left-fill"></i></button>
|
||||
<button class="ptz-btn btn-right" @mousedown="ptzStart('Right')" @mouseup="ptzStop('Right')" @mouseleave="ptzStop('Right')"><i class="bi bi-caret-right-fill"></i></button>
|
||||
<button class="ptz-btn btn-down" @mousedown="ptzStart('Down')" @mouseup="ptzStop('Down')" @mouseleave="ptzStop('Down')"><i class="bi bi-caret-down-fill"></i></button>
|
||||
<button class="ptz-btn btn-center" title="停止"><i class="bi bi-stop-fill"></i></button>
|
||||
</div>
|
||||
|
||||
<div class="speed-ctrl">
|
||||
<label class="form-label d-flex justify-content-between mb-2">
|
||||
<span class="fw-bold text-muted"><i class="bi bi-speedometer2 me-2"></i>转动速度</span>
|
||||
<span class="badge bg-primary">{{ speed }}</span>
|
||||
</label>
|
||||
<input type="range" class="form-range" min="1" max="7" step="1" v-model.number="speed">
|
||||
<div class="d-flex justify-content-between small text-muted font-monospace">
|
||||
<span>1 (Slow)</span><span>7 (Fast)</span>
|
||||
</div>
|
||||
<div class="zoom-ctrl">
|
||||
<button class="zoom-btn" @mousedown="ptz('zoomIn')" @mouseup="ptzStop"><i class="bi bi-zoom-in me-1"></i>放大</button>
|
||||
<button class="zoom-btn" @mousedown="ptz('zoomOut')" @mouseup="ptzStop"><i class="bi bi-zoom-out me-1"></i>缩小</button>
|
||||
</div>
|
||||
<div class="mt-2 text-muted small" style="font-size: 0.75rem;"><i class="bi bi-info-circle me-1"></i>长按移动,松开停止</div>
|
||||
</div>
|
||||
|
||||
<div class="sys-section">
|
||||
<div class="func-card">
|
||||
<div class="func-title"><i class="bi bi-clock-history me-2 text-success"></i>时间同步</div>
|
||||
<div class="text-muted small mb-2">设备当前时间:</div>
|
||||
<div class="time-box">{{ deviceTime || '--:--:--' }}</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-primary flex-fill" @click="getDeviceTime"><i class="bi bi-arrow-clockwise me-1"></i>刷新</button>
|
||||
<button class="btn btn-sm btn-success flex-fill" @click="syncTime" :disabled="syncing">
|
||||
<span v-if="syncing" class="spinner-border spinner-border-sm me-1"></span>
|
||||
<i v-else class="bi bi-check2-circle me-1"></i>同步本机
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="ctrl-group h-100">
|
||||
<div class="group-title"><i class="bi bi-camera-fill me-2"></i>镜头参数 (Lens)</div>
|
||||
|
||||
<div class="lens-row">
|
||||
<span class="lens-label">变倍 Zoom</span>
|
||||
<div class="btn-group flex-grow-1">
|
||||
<button class="btn btn-outline-secondary" @mousedown="ptzStart('ZoomOut')" @mouseup="ptzStop('ZoomOut')" @mouseleave="ptzStop('ZoomOut')"><i class="bi bi-dash-lg"></i> 缩小</button>
|
||||
<button class="btn btn-outline-secondary" @mousedown="ptzStart('ZoomIn')" @mouseup="ptzStop('ZoomIn')" @mouseleave="ptzStop('ZoomIn')"><i class="bi bi-plus-lg"></i> 放大</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lens-row">
|
||||
<span class="lens-label">聚焦 Focus</span>
|
||||
<div class="btn-group flex-grow-1">
|
||||
<button class="btn btn-outline-secondary" @mousedown="ptzStart('FocusNear')" @mouseup="ptzStop('FocusNear')" @mouseleave="ptzStop('FocusNear')">近焦</button>
|
||||
<button class="btn btn-outline-secondary" @mousedown="ptzStart('FocusFar')" @mouseup="ptzStop('FocusFar')" @mouseleave="ptzStop('FocusFar')">远焦</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lens-row">
|
||||
<span class="lens-label">光圈 Iris</span>
|
||||
<div class="btn-group flex-grow-1">
|
||||
<button class="btn btn-outline-secondary" @mousedown="ptzStart('IrisClose')" @mouseup="ptzStop('IrisClose')" @mouseleave="ptzStop('IrisClose')">变小</button>
|
||||
<button class="btn btn-outline-secondary" @mousedown="ptzStart('IrisOpen')" @mouseup="ptzStop('IrisOpen')" @mouseleave="ptzStop('IrisOpen')">变大</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="ctrl-group h-100">
|
||||
<div class="group-title"><i class="bi bi-stars me-2"></i>辅助 (Aux)</div>
|
||||
<p class="text-muted small mb-3">雨刷功能支持开关模式,点击一次开启,再次点击关闭。</p>
|
||||
<button class="btn w-100 py-3 fw-bold transition-all"
|
||||
:class="wiperOn ? 'btn-primary' : 'btn-outline-secondary'"
|
||||
@click="toggleWiper">
|
||||
<i class="bi bi-wind me-2 fs-5"></i>雨刷 (Wiper) {{ wiperOn ? 'ON' : 'OFF' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="func-card">
|
||||
<div class="func-title"><i class="bi bi-tools me-2 text-warning"></i>系统维护</div>
|
||||
<button class="btn btn-sm btn-light border w-100 text-start" @click="reboot"><i class="bi bi-bootstrap-reboot me-2"></i>重启设备</button>
|
||||
<div class="col-12">
|
||||
<div class="ctrl-group">
|
||||
<div class="group-title"><i class="bi bi-hdd-rack me-2"></i>系统维护 (System Maintenance)</div>
|
||||
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6 border-end">
|
||||
<div class="d-flex justify-content-between small text-muted mb-2">
|
||||
<span>设备时间 (Device Time)</span>
|
||||
<a href="#" @click.prevent="getDeviceTime" class="text-decoration-none">刷新</a>
|
||||
</div>
|
||||
<div class="time-display">{{ deviceTime || '等待获取...' }}</div>
|
||||
<button class="btn btn-success w-100" @click="syncTime" :disabled="loading">
|
||||
<i class="bi bi-clock-history me-1"></i> 将本机时间同步至设备
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 ps-4">
|
||||
<div class="alert alert-light border mb-3 small text-muted">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
重启设备将导致视频流中断约 60 秒,期间无法录像。
|
||||
</div>
|
||||
<button class="btn btn-danger w-100 py-2" @click="reboot" :disabled="loading">
|
||||
<i class="bi bi-power me-1"></i> 执行远程重启 (Reboot)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -94,40 +211,117 @@
|
||||
createApp({
|
||||
setup() {
|
||||
const deviceId = ref(0);
|
||||
const speed = ref(4);
|
||||
const deviceTime = ref("");
|
||||
const syncing = ref(false);
|
||||
const loading = ref(false);
|
||||
const wiperOn = ref(false);
|
||||
|
||||
// PTZ控制
|
||||
const ptz = async (action) => {
|
||||
log(`PTZ: ${action}`);
|
||||
try { await axios.post(`${API_BASE}/api/Cameras/${deviceId.value}/ptz?action=${action}&speed=5`); } catch(e) {}
|
||||
};
|
||||
const ptzStop = async () => {
|
||||
try { await axios.post(`${API_BASE}/api/Cameras/${deviceId.value}/ptz?action=stop`); } catch(e) {}
|
||||
};
|
||||
|
||||
// 校时逻辑
|
||||
const getDeviceTime = async () => {
|
||||
const now = new Date(); now.setMinutes(now.getMinutes() - 5); // 模拟
|
||||
deviceTime.value = now.toLocaleTimeString();
|
||||
};
|
||||
const syncTime = async () => {
|
||||
syncing.value = true;
|
||||
try {
|
||||
await axios.post(`${API_BASE}/api/Cameras/${deviceId.value}/sync-time`, { time: new Date().toISOString() });
|
||||
alert("指令已下发");
|
||||
deviceTime.value = new Date().toLocaleTimeString();
|
||||
} catch(e) { alert("失败: " + e.message); }
|
||||
finally { syncing.value = false; }
|
||||
};
|
||||
|
||||
const reboot = async () => {
|
||||
if(confirm("确定要重启设备吗?")) {
|
||||
try { await axios.post(`${API_BASE}/api/Cameras/${deviceId.value}/reboot`); alert("重启中..."); } catch(e){}
|
||||
// 通用日志函数
|
||||
const logToParent = (method, url, status, msg) => {
|
||||
if(window.parent) {
|
||||
window.parent.postMessage({
|
||||
type: 'API_LOG',
|
||||
log: { method, url, status, msg }
|
||||
}, '*');
|
||||
}
|
||||
};
|
||||
|
||||
const log = (msg) => window.parent.postMessage({ type: 'API_LOG', log: { method: 'PTZ', url: msg, status: 200, msg: 'Sent' } }, '*');
|
||||
// 【核心修改】PTZ 控制函数
|
||||
const sendPtz = async (action, stop, duration = 0) => {
|
||||
const url = `${API_BASE}/api/cameras/${deviceId.value}/ptz`;
|
||||
try {
|
||||
// 1. 严格类型转换,防止发送字符串 "4" 导致后端 400
|
||||
const payload = {
|
||||
action: action,
|
||||
speed: parseInt(speed.value), // 强制转 Int
|
||||
duration: parseInt(duration), // 强制转 Int
|
||||
stop: Boolean(stop) // 强制转 Bool
|
||||
};
|
||||
|
||||
// 2. 打印即将发送的包,方便调试 (在诊断台看)
|
||||
const statusText = stop ? "STOP" : "START";
|
||||
const shortLog = `${action} [${statusText}] Spd:${payload.speed}`;
|
||||
|
||||
await axios.post(url, payload);
|
||||
|
||||
// 3. 成功日志
|
||||
logToParent('PTZ', url, 200, shortLog);
|
||||
} catch(e) {
|
||||
// 4. 【关键】捕获 400 错误并解析详细原因
|
||||
let msg = e.response?.data?.message || e.message;
|
||||
|
||||
// 尝试解析 ASP.NET Core 的详细验证错误 (ValidationProblems)
|
||||
if (e.response?.data?.errors) {
|
||||
const errors = e.response.data.errors;
|
||||
// 将错误对象转为字符串
|
||||
msg += " | " + JSON.stringify(errors);
|
||||
}
|
||||
|
||||
// 上报红色的错误日志
|
||||
logToParent('PTZ', url, e.response?.status || 'ERROR', msg);
|
||||
|
||||
// 弹窗提示,方便您第一时间看到原因
|
||||
// alert("控制失败: " + msg);
|
||||
}
|
||||
};
|
||||
|
||||
const ptzStart = (action) => sendPtz(action, false);
|
||||
const ptzStop = (action) => sendPtz(action, true);
|
||||
|
||||
const toggleWiper = async () => {
|
||||
wiperOn.value = !wiperOn.value;
|
||||
await sendPtz('Wiper', !wiperOn.value);
|
||||
};
|
||||
|
||||
const getDeviceTime = async () => {
|
||||
const url = `${API_BASE}/api/cameras/${deviceId.value}/time`;
|
||||
try {
|
||||
logToParent('GET', url, 'PENDING', 'Getting Time...');
|
||||
const res = await axios.get(url);
|
||||
if(res.data && res.data.currentTime) {
|
||||
const t = new Date(res.data.currentTime);
|
||||
deviceTime.value = t.toLocaleString();
|
||||
logToParent('GET', url, 200, 'OK');
|
||||
}
|
||||
} catch(e) {
|
||||
deviceTime.value = "获取失败";
|
||||
logToParent('GET', url, 'ERROR', e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const syncTime = async () => {
|
||||
loading.value = true;
|
||||
const url = `${API_BASE}/api/cameras/${deviceId.value}/time`;
|
||||
try {
|
||||
const nowStr = new Date().toISOString();
|
||||
await axios.post(url, JSON.stringify(nowStr), { headers: { 'Content-Type': 'application/json' } });
|
||||
|
||||
deviceTime.value = new Date().toLocaleString() + " (已同步)";
|
||||
alert("指令已下发");
|
||||
logToParent('POST', url, 200, 'Time Synced');
|
||||
} catch(e) {
|
||||
alert("失败: " + e.message);
|
||||
logToParent('POST', url, 'ERROR', e.message);
|
||||
}
|
||||
finally { loading.value = false; }
|
||||
};
|
||||
|
||||
const reboot = async () => {
|
||||
if(!confirm("确定要重启吗?")) return;
|
||||
const url = `${API_BASE}/api/cameras/${deviceId.value}/reboot`;
|
||||
try {
|
||||
await axios.post(url);
|
||||
alert("重启指令已发送");
|
||||
logToParent('POST', url, 200, 'Reboot Sent');
|
||||
} catch(e) {
|
||||
alert("失败: " + e.message);
|
||||
logToParent('POST', url, 'ERROR', e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const closeMode = () => {
|
||||
window.parent.postMessage({ type: 'CLOSE_CONTROL_MODE' }, '*');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', (e) => {
|
||||
@@ -139,7 +333,11 @@
|
||||
});
|
||||
});
|
||||
|
||||
return { deviceId, deviceTime, syncing, ptz, ptzStop, getDeviceTime, syncTime, reboot };
|
||||
return {
|
||||
deviceId, speed, deviceTime, loading, wiperOn,
|
||||
ptzStart, ptzStop, toggleWiper,
|
||||
getDeviceTime, syncTime, reboot, closeMode
|
||||
};
|
||||
}
|
||||
}).mount('#app');
|
||||
</script>
|
||||
|
||||
@@ -41,8 +41,6 @@
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="subModal" tabindex="-1"><div class="modal-dialog modal-lg modal-dialog-centered"><div class="modal-content shadow-lg"><div class="modal-body p-0"><iframe id="frame-sub" src="Subscription.html" style="height: 650px;" name="sub"></iframe></div></div></div></div>
|
||||
<div class="modal fade" id="preModal" tabindex="-1"><div class="modal-dialog modal-md modal-dialog-centered"><div class="modal-content shadow-lg"><div class="modal-body p-0"><iframe id="frame-pre" src="Preprocessing.html" style="height: 580px;" name="pre"></iframe></div></div></div></div>
|
||||
<div class="modal fade" id="ctrlModal" tabindex="-1"><div class="modal-dialog modal-dialog-centered"><div class="modal-content shadow-lg"><div class="modal-body p-0"><iframe id="frame-ctrl" src="CameraControl.html" style="height: 480px;" name="ctrl"></iframe></div></div></div></div>
|
||||
|
||||
<script src="https://cdn.staticfile.org/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
@@ -58,12 +56,11 @@
|
||||
top: document.getElementById('frame-top').contentWindow,
|
||||
editor: document.getElementById('frame-editor').contentWindow,
|
||||
diag: document.getElementById('frame-diag').contentWindow,
|
||||
sub: document.getElementById('frame-sub').contentWindow,
|
||||
pre: document.getElementById('frame-pre').contentWindow,
|
||||
ctrl: document.getElementById('frame-ctrl').contentWindow
|
||||
sub: document.getElementById('frame-sub').contentWindow
|
||||
};
|
||||
const editorIframe = document.getElementById('frame-editor');
|
||||
|
||||
// 辅助:切回详情页
|
||||
const switchToDetail = () => {
|
||||
editorIframe.src = "Editor.html";
|
||||
editorIframe.onload = () => {
|
||||
@@ -76,7 +73,7 @@
|
||||
switch(msg.type) {
|
||||
case 'DEVICE_SELECTED':
|
||||
currentDeviceId = msg.data.id;
|
||||
// 如果当前不是Editor页面,切回
|
||||
// 如果当前在深层页面(Edit/Add/Pre/Ctrl),强制切回详情页
|
||||
if (!editorIframe.src.includes('Editor.html')) {
|
||||
switchToDetail();
|
||||
} else {
|
||||
@@ -102,42 +99,44 @@
|
||||
if(diagEl) diagEl.style.height = msg.expanded ? '350px' : '45px';
|
||||
break;
|
||||
|
||||
// 弹窗逻辑
|
||||
case 'OPEN_SUBSCRIPTION':
|
||||
new bootstrap.Modal(document.getElementById('subModal')).show();
|
||||
setTimeout(() => frames.sub.postMessage({ type: 'LOAD_SUBS_DATA', deviceId: msg.id }, '*'), 400);
|
||||
break;
|
||||
|
||||
// --- 路由切换逻辑 ---
|
||||
|
||||
// 1. 编辑/新增
|
||||
case 'OPEN_CAMERA_EDIT':
|
||||
case 'OPEN_CAMERA_ADD':
|
||||
editorIframe.src = "CameraEdit.html";
|
||||
editorIframe.onload = () => {
|
||||
const initType = msg.type === 'OPEN_CAMERA_ADD' ? 'INIT_ADD_PAGE' : 'LOAD_EDIT_DATA';
|
||||
editorIframe.contentWindow.postMessage({ type: initType, deviceId: msg.id, apiBase: API_BASE }, '*');
|
||||
};
|
||||
break;
|
||||
|
||||
// 2. 预处理
|
||||
case 'OPEN_PREPROCESS':
|
||||
editorIframe.src = "Preprocessing.html";
|
||||
editorIframe.onload = () => {
|
||||
editorIframe.contentWindow.postMessage({ type: 'LOAD_PREPROCESS_DATA', deviceId: msg.id, apiBase: API_BASE }, '*');
|
||||
};
|
||||
break;
|
||||
|
||||
// 3. 【修改】云台控制 (不弹窗,全屏)
|
||||
case 'OPEN_CAMERA_CONTROL':
|
||||
new bootstrap.Modal(document.getElementById('ctrlModal')).show();
|
||||
setTimeout(() => frames.ctrl.postMessage({ type: 'LOAD_CTRL_DATA', deviceId: msg.id, apiBase: API_BASE }, '*'), 400);
|
||||
break;
|
||||
|
||||
// --- 统一使用 CameraEdit.html ---
|
||||
|
||||
case 'OPEN_CAMERA_EDIT':
|
||||
editorIframe.src = "CameraEdit.html";
|
||||
editorIframe.src = "CameraControl.html";
|
||||
editorIframe.onload = () => {
|
||||
editorIframe.contentWindow.postMessage({ type: 'LOAD_EDIT_DATA', deviceId: msg.id, apiBase: API_BASE }, '*');
|
||||
};
|
||||
break;
|
||||
|
||||
case 'OPEN_CAMERA_ADD': // 新增也指向同一个文件
|
||||
editorIframe.src = "CameraEdit.html";
|
||||
editorIframe.onload = () => {
|
||||
editorIframe.contentWindow.postMessage({ type: 'INIT_ADD_PAGE', apiBase: API_BASE }, '*');
|
||||
editorIframe.contentWindow.postMessage({ type: 'LOAD_CTRL_DATA', deviceId: msg.id, apiBase: API_BASE }, '*');
|
||||
};
|
||||
break;
|
||||
|
||||
// 统一关闭逻辑
|
||||
case 'CLOSE_EDIT_MODE':
|
||||
case 'CLOSE_PREPROCESS_MODE':
|
||||
case 'CLOSE_ADD_MODE':
|
||||
case 'CLOSE_PREPROCESS_MODE':
|
||||
case 'CLOSE_CONTROL_MODE': // 【新增】
|
||||
switchToDetail();
|
||||
if(msg.needRefresh) frames.list.postMessage({ type: 'REFRESH_LIST' }, '*');
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user