Compare commits
14 Commits
0350ce8829
...
c5f0e706a4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5f0e706a4 | ||
|
|
9b580be5e9 | ||
| 1273be7dee | |||
| 78737f6839 | |||
| e38770a496 | |||
| a76ee74656 | |||
|
|
f710a66c69 | ||
|
|
4e5dc91f10 | ||
| 8221f8e133 | |||
| bad64bdfbd | |||
|
|
c29c3652bc | ||
|
|
352ee1f4f2 | ||
| 32b126b93f | |||
| b913f58f13 |
358
server/src/Controllers/LogicAnalyzerController.cs
Normal file
358
server/src/Controllers/LogicAnalyzerController.cs
Normal file
@@ -0,0 +1,358 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Peripherals.LogicAnalyzerClient;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 逻辑分析仪控制器
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
public class LogicAnalyzerController : ControllerBase
|
||||
{
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 信号触发配置
|
||||
/// </summary>
|
||||
public class SignalTriggerConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 信号索引 (0-7)
|
||||
/// </summary>
|
||||
public int SignalIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作符
|
||||
/// </summary>
|
||||
public SignalOperator Operator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 信号值
|
||||
/// </summary>
|
||||
public SignalValue Value { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 捕获配置
|
||||
/// </summary>
|
||||
public class CaptureConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局触发模式
|
||||
/// </summary>
|
||||
public GlobalCaptureMode GlobalMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 信号触发配置列表
|
||||
/// </summary>
|
||||
public SignalTriggerConfig[] SignalConfigs { get; set; } = Array.Empty<SignalTriggerConfig>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取逻辑分析仪实例
|
||||
/// </summary>
|
||||
private Analyzer? GetAnalyzer()
|
||||
{
|
||||
try
|
||||
{
|
||||
var userName = User.Identity?.Name;
|
||||
if (string.IsNullOrEmpty(userName))
|
||||
return null;
|
||||
|
||||
using var db = new Database.AppDataConnection();
|
||||
var userRet = db.GetUserByName(userName);
|
||||
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
|
||||
return null;
|
||||
|
||||
var user = userRet.Value.Value;
|
||||
if (user.BoardID == Guid.Empty)
|
||||
return null;
|
||||
|
||||
var boardRet = db.GetBoardByID(user.BoardID);
|
||||
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
|
||||
return null;
|
||||
|
||||
var board = boardRet.Value.Value;
|
||||
return new Analyzer(board.IpAddr, board.Port, 2);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取逻辑分析仪实例时发生异常");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置捕获模式
|
||||
/// </summary>
|
||||
/// <param name="captureOn">是否开始捕获</param>
|
||||
/// <param name="force">是否强制捕获</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("SetCaptureMode")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> SetCaptureMode(bool captureOn, bool force = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var analyzer = GetAnalyzer();
|
||||
if (analyzer == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var result = await analyzer.SetCaptureMode(captureOn, force);
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置捕获模式失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置捕获模式失败");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "设置捕获模式时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取捕获状态
|
||||
/// </summary>
|
||||
/// <returns>捕获状态</returns>
|
||||
[HttpGet("GetCaptureStatus")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(CaptureStatus), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> GetCaptureStatus()
|
||||
{
|
||||
try
|
||||
{
|
||||
var analyzer = GetAnalyzer();
|
||||
if (analyzer == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var result = await analyzer.ReadCaptureStatus();
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"读取捕获状态失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "读取捕获状态失败");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "读取捕获状态时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置全局触发模式
|
||||
/// </summary>
|
||||
/// <param name="mode">全局触发模式</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("SetGlobalTrigMode")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> SetGlobalTrigMode(GlobalCaptureMode mode)
|
||||
{
|
||||
try
|
||||
{
|
||||
var analyzer = GetAnalyzer();
|
||||
if (analyzer == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var result = await analyzer.SetGlobalTrigMode(mode);
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置全局触发模式失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置全局触发模式失败");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "设置全局触发模式时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置信号触发模式
|
||||
/// </summary>
|
||||
/// <param name="signalIndex">信号索引 (0-7)</param>
|
||||
/// <param name="op">操作符</param>
|
||||
/// <param name="val">信号值</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("SetSignalTrigMode")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> SetSignalTrigMode(int signalIndex, SignalOperator op, SignalValue val)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (signalIndex < 0 || signalIndex > 7)
|
||||
return BadRequest("信号索引必须在0-7之间");
|
||||
|
||||
var analyzer = GetAnalyzer();
|
||||
if (analyzer == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var result = await analyzer.SetSignalTrigMode(signalIndex, op, val);
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置信号触发模式失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置信号触发模式失败");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "设置信号触发模式时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量配置捕获参数
|
||||
/// </summary>
|
||||
/// <param name="config">捕获配置</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("ConfigureCapture")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> ConfigureCapture([FromBody] CaptureConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (config == null)
|
||||
return BadRequest("配置参数不能为空");
|
||||
|
||||
var analyzer = GetAnalyzer();
|
||||
if (analyzer == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
// 设置全局触发模式
|
||||
var globalResult = await analyzer.SetGlobalTrigMode(config.GlobalMode);
|
||||
if (!globalResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置全局触发模式失败: {globalResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置全局触发模式失败");
|
||||
}
|
||||
|
||||
// 设置信号触发模式
|
||||
foreach (var signalConfig in config.SignalConfigs)
|
||||
{
|
||||
if (signalConfig.SignalIndex < 0 || signalConfig.SignalIndex > 7)
|
||||
return BadRequest($"信号索引{signalConfig.SignalIndex}超出范围0-7");
|
||||
|
||||
var signalResult = await analyzer.SetSignalTrigMode(
|
||||
signalConfig.SignalIndex, signalConfig.Operator, signalConfig.Value);
|
||||
if (!signalResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置信号{signalConfig.SignalIndex}触发模式失败: {signalResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError,
|
||||
$"设置信号{signalConfig.SignalIndex}触发模式失败");
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "配置捕获参数时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制捕获
|
||||
/// </summary>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("ForceCapture")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> ForceCapture()
|
||||
{
|
||||
try
|
||||
{
|
||||
var analyzer = GetAnalyzer();
|
||||
if (analyzer == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var result = await analyzer.SetCaptureMode(true, true);
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"强制捕获失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "强制捕获失败");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "强制捕获时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取捕获数据
|
||||
/// </summary>
|
||||
/// <returns>捕获的波形数据(Base64编码)</returns>
|
||||
[HttpGet("GetCaptureData")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> GetCaptureData()
|
||||
{
|
||||
try
|
||||
{
|
||||
var analyzer = GetAnalyzer();
|
||||
if (analyzer == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var result = await analyzer.ReadCaptureData();
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"读取捕获数据失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "读取捕获数据失败");
|
||||
}
|
||||
|
||||
// 将二进制数据编码为Base64字符串返回
|
||||
var base64Data = Convert.ToBase64String(result.Value);
|
||||
return Ok(base64Data);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "读取捕获数据时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,26 @@ public class VideoStreamController : ControllerBase
|
||||
public int Port { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分辨率配置请求模型
|
||||
/// </summary>
|
||||
public class ResolutionConfigRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 宽度
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Range(640, 1920, ErrorMessage = "宽度必须在640-1920范围内")]
|
||||
public int Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 高度
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Range(480, 1080, ErrorMessage = "高度必须在480-1080范围内")]
|
||||
public int Height { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化HTTP视频流控制器
|
||||
/// </summary>
|
||||
@@ -233,4 +253,198 @@ public class VideoStreamController : ControllerBase
|
||||
return TypedResults.Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置视频流分辨率
|
||||
/// </summary>
|
||||
/// <param name="request">分辨率配置请求</param>
|
||||
/// <returns>设置结果</returns>
|
||||
[HttpPost("Resolution")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IResult> SetResolution([FromBody] ResolutionConfigRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info($"设置视频流分辨率为 {request.Width}x{request.Height}");
|
||||
|
||||
var (isSuccess, message) = await _videoStreamService.SetResolutionAsync(request.Width, request.Height);
|
||||
|
||||
if (isSuccess)
|
||||
{
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
success = true,
|
||||
message = message,
|
||||
width = request.Width,
|
||||
height = request.Height,
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
return TypedResults.BadRequest(new
|
||||
{
|
||||
success = false,
|
||||
message = message,
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, $"设置分辨率为 {request.Width}x{request.Height} 失败");
|
||||
return TypedResults.InternalServerError($"设置分辨率失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前分辨率
|
||||
/// </summary>
|
||||
/// <returns>当前分辨率信息</returns>
|
||||
[HttpGet("Resolution")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
|
||||
public IResult GetCurrentResolution()
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("获取当前视频流分辨率");
|
||||
|
||||
var (width, height) = _videoStreamService.GetCurrentResolution();
|
||||
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
width = width,
|
||||
height = height,
|
||||
resolution = $"{width}x{height}",
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取当前分辨率失败");
|
||||
return TypedResults.InternalServerError($"获取当前分辨率失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取支持的分辨率列表
|
||||
/// </summary>
|
||||
/// <returns>支持的分辨率列表</returns>
|
||||
[HttpGet("SupportedResolutions")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
|
||||
public IResult GetSupportedResolutions()
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("获取支持的分辨率列表");
|
||||
|
||||
var resolutions = _videoStreamService.GetSupportedResolutions();
|
||||
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
resolutions = resolutions.Select(r => new
|
||||
{
|
||||
width = r.Width,
|
||||
height = r.Height,
|
||||
name = r.Name,
|
||||
value = $"{r.Width}x{r.Height}"
|
||||
}),
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取支持的分辨率列表失败");
|
||||
return TypedResults.InternalServerError($"获取支持的分辨率列表失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化摄像头自动对焦功能
|
||||
/// </summary>
|
||||
/// <returns>初始化结果</returns>
|
||||
[HttpPost("InitAutoFocus")]
|
||||
public async Task<IResult> InitAutoFocus()
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("收到初始化自动对焦请求");
|
||||
|
||||
var result = await _videoStreamService.InitAutoFocusAsync();
|
||||
|
||||
if (result)
|
||||
{
|
||||
logger.Info("自动对焦初始化成功");
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
success = true,
|
||||
message = "自动对焦初始化成功",
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn("自动对焦初始化失败");
|
||||
return TypedResults.BadRequest(new
|
||||
{
|
||||
success = false,
|
||||
message = "自动对焦初始化失败",
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "初始化自动对焦时发生异常");
|
||||
return TypedResults.InternalServerError($"初始化自动对焦失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行自动对焦
|
||||
/// </summary>
|
||||
/// <returns>对焦结果</returns>
|
||||
[HttpPost("AutoFocus")]
|
||||
public async Task<IResult> AutoFocus()
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("收到执行自动对焦请求");
|
||||
|
||||
var result = await _videoStreamService.PerformAutoFocusAsync();
|
||||
|
||||
if (result)
|
||||
{
|
||||
logger.Info("自动对焦执行成功");
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
success = true,
|
||||
message = "自动对焦执行成功",
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn("自动对焦执行失败");
|
||||
return TypedResults.BadRequest(new
|
||||
{
|
||||
success = false,
|
||||
message = "自动对焦执行失败",
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "执行自动对焦时发生异常");
|
||||
return TypedResults.InternalServerError($"执行自动对焦失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
352
server/src/Peripherals/LogicAnalyzerClient.cs
Normal file
352
server/src/Peripherals/LogicAnalyzerClient.cs
Normal file
@@ -0,0 +1,352 @@
|
||||
using System.Collections;
|
||||
using System.Net;
|
||||
using DotNext;
|
||||
|
||||
namespace Peripherals.LogicAnalyzerClient;
|
||||
|
||||
static class AnalyzerAddr
|
||||
{
|
||||
const UInt32 BASE = 0x9000_0000;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0000 R/W [ 0] capture on: 置1开始等待捕获,0停止捕获。捕获到信号后该位自动清零。 <br/>
|
||||
/// [ 8] capture force: 置1则强制捕获信号,自动置0。 <br/>
|
||||
/// [16] capture busy: 1为逻辑分析仪正在捕获信号。 <br/>
|
||||
/// [24] capture done: 1为逻辑分析仪内存完整存储了此次捕获的信号。 <br/>
|
||||
/// 配置顺序:若[0]为0,则将其置1,随后不断获取[0],若其变为0则表示触发成功。随后不断获取[24],若其为1则表示捕获完成。 <br/>
|
||||
/// </summary>
|
||||
public const UInt32 CAPTURE_MODE = BASE + 0x0000_0000;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0001 R/W [1:0] global trig mode: 00: 全局与 (&) <br/>
|
||||
/// 01: 全局或 (|) <br/>
|
||||
/// 10: 全局非与(~&) <br/>
|
||||
/// 11: 全局非或(~|) <br/>
|
||||
/// </summary>
|
||||
public const UInt32 GLOBAL_TRIG_MODE = BASE + 0x0000_0000;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0010 - 0x0000_0017 R/W [5:0] 信号M的触发操作符,共8路 <br/>
|
||||
/// [2:0] M's Operator: 000 == <br/>
|
||||
/// 001 != <br/>
|
||||
/// 010 < <br/>
|
||||
/// 011 <= <br/>
|
||||
/// 100 > <br/>
|
||||
/// 101 >= <br/>
|
||||
/// [5:3] M's Value: 000 LOGIC 0 <br/>
|
||||
/// 001 LOGIC 1 <br/>
|
||||
/// 010 X(not care) <br/>
|
||||
/// 011 RISE <br/>
|
||||
/// 100 FALL <br/>
|
||||
/// 101 RISE OR FALL <br/>
|
||||
/// 110 NOCHANGE <br/>
|
||||
/// 111 SOME NUMBER <br/>
|
||||
/// </summary>
|
||||
public static readonly UInt32[] SIGNAL_TRIG_MODE = {
|
||||
BASE + 0x0000_0010,
|
||||
BASE + 0x0000_0011,
|
||||
BASE + 0x0000_0012,
|
||||
BASE + 0x0000_0013,
|
||||
BASE + 0x0000_0014,
|
||||
BASE + 0x0000_0015,
|
||||
BASE + 0x0000_0016,
|
||||
BASE + 0x0000_0017,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 0x0100_0000 - 0x0100_03FF 只读 32位波形存储,得到的32位数据中低八位最先捕获,高八位最后捕获。<br/>
|
||||
/// 共1024个地址,每个地址存储4组,深度为4096。<br/>
|
||||
/// </summary>
|
||||
public const UInt32 CAPTURE_DATA_ADDR = BASE + 0x0100_0000;
|
||||
public const Int32 CAPTURE_DATA_LENGTH = 1024;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 逻辑分析仪运行状态枚举
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum CaptureStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 无状态标志
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 捕获使能位,置1开始等待捕获,0停止捕获。捕获到信号后该位自动清零
|
||||
/// </summary>
|
||||
CaptureOn = 1 << 0, // [0] 捕获使能
|
||||
|
||||
/// <summary>
|
||||
/// 强制捕获位,置1则强制捕获信号,自动置0
|
||||
/// </summary>
|
||||
CaptureForce = 1 << 8, // [8] 强制捕获
|
||||
|
||||
/// <summary>
|
||||
/// 捕获忙碌位,1为逻辑分析仪正在捕获信号
|
||||
/// </summary>
|
||||
CaptureBusy = 1 << 16, // [16] 捕获进行中
|
||||
|
||||
/// <summary>
|
||||
/// 捕获完成位,1为逻辑分析仪内存完整存储了此次捕获的信号
|
||||
/// </summary>
|
||||
CaptureDone = 1 << 24 // [24] 捕获完成
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全局触发模式枚举,定义多路信号触发条件的逻辑组合方式
|
||||
/// </summary>
|
||||
public enum GlobalCaptureMode
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局与模式,所有触发条件都必须满足
|
||||
/// </summary>
|
||||
AND = 0b00,
|
||||
|
||||
/// <summary>
|
||||
/// 全局或模式,任一触发条件满足即可
|
||||
/// </summary>
|
||||
OR = 0b01,
|
||||
|
||||
/// <summary>
|
||||
/// 全局非与模式,不是所有触发条件都满足
|
||||
/// </summary>
|
||||
NAND = 0b10,
|
||||
|
||||
/// <summary>
|
||||
/// 全局非或模式,所有触发条件都不满足
|
||||
/// </summary>
|
||||
NOR = 0b11
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 信号M的操作符枚举
|
||||
/// </summary>
|
||||
public enum SignalOperator : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// 等于操作符
|
||||
/// </summary>
|
||||
Equal = 0b000, // ==
|
||||
/// <summary>
|
||||
/// 不等于操作符
|
||||
/// </summary>
|
||||
NotEqual = 0b001, // !=
|
||||
/// <summary>
|
||||
/// 小于操作符
|
||||
/// </summary>
|
||||
LessThan = 0b010, // <
|
||||
/// <summary>
|
||||
/// 小于等于操作符
|
||||
/// </summary>
|
||||
LessThanOrEqual = 0b011, // <=
|
||||
/// <summary>
|
||||
/// 大于操作符
|
||||
/// </summary>
|
||||
GreaterThan = 0b100, // >
|
||||
/// <summary>
|
||||
/// 大于等于操作符
|
||||
/// </summary>
|
||||
GreaterThanOrEqual = 0b101 // >=
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 信号M的值枚举
|
||||
/// </summary>
|
||||
public enum SignalValue : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// 逻辑0电平
|
||||
/// </summary>
|
||||
Logic0 = 0b000, // LOGIC 0
|
||||
/// <summary>
|
||||
/// 逻辑1电平
|
||||
/// </summary>
|
||||
Logic1 = 0b001, // LOGIC 1
|
||||
/// <summary>
|
||||
/// 不关心该信号状态
|
||||
/// </summary>
|
||||
NotCare = 0b010, // X(not care)
|
||||
/// <summary>
|
||||
/// 上升沿触发
|
||||
/// </summary>
|
||||
Rise = 0b011, // RISE
|
||||
/// <summary>
|
||||
/// 下降沿触发
|
||||
/// </summary>
|
||||
Fall = 0b100, // FALL
|
||||
/// <summary>
|
||||
/// 上升沿或下降沿触发
|
||||
/// </summary>
|
||||
RiseOrFall = 0b101, // RISE OR FALL
|
||||
/// <summary>
|
||||
/// 信号无变化
|
||||
/// </summary>
|
||||
NoChange = 0b110, // NOCHANGE
|
||||
/// <summary>
|
||||
/// 特定数值
|
||||
/// </summary>
|
||||
SomeNumber = 0b111 // SOME NUMBER
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FPGA逻辑分析仪客户端,用于控制FPGA上的逻辑分析仪模块进行信号捕获和分析
|
||||
/// </summary>
|
||||
public class Analyzer
|
||||
{
|
||||
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
readonly int timeout = 2000;
|
||||
readonly int taskID;
|
||||
readonly int port;
|
||||
readonly string address;
|
||||
private IPEndPoint ep;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化逻辑分析仪客户端
|
||||
/// </summary>
|
||||
/// <param name="address">FPGA设备的IP地址</param>
|
||||
/// <param name="port">通信端口号</param>
|
||||
/// <param name="taskID">任务标识符</param>
|
||||
/// <param name="timeout">通信超时时间(毫秒),默认2000ms</param>
|
||||
/// <exception cref="ArgumentException">当timeout为负数时抛出</exception>
|
||||
public Analyzer(string address, int port, int taskID, int timeout = 2000)
|
||||
{
|
||||
if (timeout < 0)
|
||||
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
|
||||
this.address = address;
|
||||
this.taskID = taskID;
|
||||
this.port = port;
|
||||
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 控制逻辑分析仪的捕获模式
|
||||
/// </summary>
|
||||
/// <param name="captureOn">是否开始捕获</param>
|
||||
/// <param name="force">是否强制捕获</param>
|
||||
/// <returns>操作结果,成功返回true,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<bool>> SetCaptureMode(bool captureOn, bool force)
|
||||
{
|
||||
// 构造寄存器值
|
||||
UInt32 value = 0;
|
||||
if (captureOn) value |= 1 << 0;
|
||||
if (force) value |= 1 << 8;
|
||||
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.CAPTURE_MODE, value, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to set capture mode: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to CAPTURE_MODE returned false");
|
||||
return new(new Exception("Failed to set capture mode"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取逻辑分析仪捕获运行状态
|
||||
/// </summary>
|
||||
/// <returns>操作结果,成功返回寄存器值,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<CaptureStatus>> ReadCaptureStatus()
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, AnalyzerAddr.CAPTURE_MODE, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to read capture status: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (ret.Value.Options.Data == null || ret.Value.Options.Data.Length < 4)
|
||||
{
|
||||
logger.Error("ReadAddr returned invalid data for capture status");
|
||||
return new(new Exception("Failed to read capture status"));
|
||||
}
|
||||
UInt32 status = BitConverter.ToUInt32(ret.Value.Options.Data, 0);
|
||||
return (CaptureStatus)status;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置全局触发模式
|
||||
/// </summary>
|
||||
/// <param name="mode">全局触发模式(0:与, 1:或, 2:非与, 3:非或)</param>
|
||||
/// <returns>操作结果,成功返回true,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<bool>> SetGlobalTrigMode(GlobalCaptureMode mode)
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(
|
||||
this.ep, this.taskID, AnalyzerAddr.GLOBAL_TRIG_MODE, (byte)mode, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to set global trigger mode: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to GLOBAL_TRIG_MODE returned false");
|
||||
return new(new Exception("Failed to set global trigger mode"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置指定信号通道的触发模式
|
||||
/// </summary>
|
||||
/// <param name="signalIndex">信号通道索引(0-7)</param>
|
||||
/// <param name="op">触发操作符</param>
|
||||
/// <param name="val">触发信号值</param>
|
||||
/// <returns>操作结果,成功返回true,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<bool>> SetSignalTrigMode(int signalIndex, SignalOperator op, SignalValue val)
|
||||
{
|
||||
if (signalIndex < 0 || signalIndex >= AnalyzerAddr.SIGNAL_TRIG_MODE.Length)
|
||||
return new(new ArgumentException($"Signal index must be 0~{AnalyzerAddr.SIGNAL_TRIG_MODE.Length}"));
|
||||
|
||||
// 计算模式值: [2:0] 操作符, [5:3] 信号值
|
||||
UInt32 mode = ((UInt32)val << 3) | (UInt32)op;
|
||||
|
||||
var addr = AnalyzerAddr.SIGNAL_TRIG_MODE[signalIndex];
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, addr, mode, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to set signal trigger mode: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to SIGNAL_TRIG_MODE returned false");
|
||||
return new(new Exception("Failed to set signal trigger mode"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取捕获的波形数据
|
||||
/// </summary>
|
||||
/// <returns>操作结果,成功返回byte[],否则返回异常信息</returns>
|
||||
public async ValueTask<Result<byte[]>> ReadCaptureData()
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddr4BytesAsync(
|
||||
this.ep,
|
||||
this.taskID,
|
||||
AnalyzerAddr.CAPTURE_DATA_ADDR,
|
||||
AnalyzerAddr.CAPTURE_DATA_LENGTH,
|
||||
this.timeout
|
||||
);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to read capture data: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
var data = ret.Value;
|
||||
if (data == null || data.Length != AnalyzerAddr.CAPTURE_DATA_LENGTH * 4)
|
||||
{
|
||||
logger.Error($"Capture data length mismatch: {data?.Length}");
|
||||
return new(new Exception("Capture data length mismatch"));
|
||||
}
|
||||
var reversed = Common.Number.ReverseBytes(data, 4).Value;
|
||||
return reversed;
|
||||
}
|
||||
}
|
||||
@@ -82,8 +82,11 @@ public class HttpVideoStreamService : BackgroundService
|
||||
private HttpListener? _httpListener;
|
||||
private readonly int _serverPort = 8080;
|
||||
private readonly int _frameRate = 30; // 30 FPS
|
||||
private readonly int _frameWidth = 1280;
|
||||
private readonly int _frameHeight = 720;
|
||||
|
||||
// 动态分辨率配置
|
||||
private int _frameWidth = 640; // 默认640x480
|
||||
private int _frameHeight = 480;
|
||||
private readonly object _resolutionLock = new object();
|
||||
|
||||
// 摄像头客户端
|
||||
private Camera? _camera;
|
||||
@@ -154,6 +157,7 @@ public class HttpVideoStreamService : BackgroundService
|
||||
}
|
||||
_cameraEnable = isEnabled;
|
||||
await _camera.EnableCamera(_cameraEnable);
|
||||
await _camera.SleepCameraHardware(!_cameraEnable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -439,8 +443,16 @@ public class HttpVideoStreamService : BackgroundService
|
||||
// 获取当前帧
|
||||
var imageData = await GetFPGAImageData();
|
||||
|
||||
// 获取当前分辨率
|
||||
int currentWidth, currentHeight;
|
||||
lock (_resolutionLock)
|
||||
{
|
||||
currentWidth = _frameWidth;
|
||||
currentHeight = _frameHeight;
|
||||
}
|
||||
|
||||
// 直接使用Common.Image.ConvertRGB24ToJpeg进行转换
|
||||
var jpegResult = Common.Image.ConvertRGB24ToJpeg(imageData, _frameWidth, _frameHeight, 80);
|
||||
var jpegResult = Common.Image.ConvertRGB24ToJpeg(imageData, currentWidth, currentHeight, 80);
|
||||
if (!jpegResult.IsSuccessful)
|
||||
{
|
||||
logger.Error("RGB24转JPEG失败: {Error}", jpegResult.Error);
|
||||
@@ -647,6 +659,14 @@ public class HttpVideoStreamService : BackgroundService
|
||||
|
||||
try
|
||||
{
|
||||
// 获取当前分辨率
|
||||
int currentWidth, currentHeight;
|
||||
lock (_resolutionLock)
|
||||
{
|
||||
currentWidth = _frameWidth;
|
||||
currentHeight = _frameHeight;
|
||||
}
|
||||
|
||||
// 从摄像头读取帧数据
|
||||
var readStartTime = DateTime.UtcNow;
|
||||
var result = await currentCamera.ReadFrame();
|
||||
@@ -662,15 +682,15 @@ public class HttpVideoStreamService : BackgroundService
|
||||
var rgb565Data = result.Value;
|
||||
|
||||
// 验证数据长度是否正确
|
||||
if (!Common.Image.ValidateImageDataLength(rgb565Data, _frameWidth, _frameHeight, 2))
|
||||
if (!Common.Image.ValidateImageDataLength(rgb565Data, currentWidth, currentHeight, 2))
|
||||
{
|
||||
logger.Warn("摄像头数据长度不匹配,期望: {Expected}, 实际: {Actual}",
|
||||
_frameWidth * _frameHeight * 2, rgb565Data.Length);
|
||||
currentWidth * currentHeight * 2, rgb565Data.Length);
|
||||
}
|
||||
|
||||
// 将 RGB565 转换为 RGB24
|
||||
var convertStartTime = DateTime.UtcNow;
|
||||
var rgb24Result = Common.Image.ConvertRGB565ToRGB24(rgb565Data, _frameWidth, _frameHeight, isLittleEndian: false);
|
||||
var rgb24Result = Common.Image.ConvertRGB565ToRGB24(rgb565Data, currentWidth, currentHeight, isLittleEndian: false);
|
||||
var convertEndTime = DateTime.UtcNow;
|
||||
var convertTime = (convertEndTime - convertStartTime).TotalMilliseconds;
|
||||
|
||||
@@ -708,8 +728,16 @@ public class HttpVideoStreamService : BackgroundService
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前分辨率
|
||||
int currentWidth, currentHeight;
|
||||
lock (_resolutionLock)
|
||||
{
|
||||
currentWidth = _frameWidth;
|
||||
currentHeight = _frameHeight;
|
||||
}
|
||||
|
||||
// 直接使用Common.Image.ConvertRGB24ToJpeg进行转换
|
||||
var jpegResult = Common.Image.ConvertRGB24ToJpeg(frameData, _frameWidth, _frameHeight, 80);
|
||||
var jpegResult = Common.Image.ConvertRGB24ToJpeg(frameData, currentWidth, currentHeight, 80);
|
||||
if (!jpegResult.IsSuccessful)
|
||||
{
|
||||
logger.Error("RGB24转JPEG失败: {Error}", jpegResult.Error);
|
||||
@@ -904,4 +932,196 @@ public class HttpVideoStreamService : BackgroundService
|
||||
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置视频流分辨率
|
||||
/// </summary>
|
||||
/// <param name="width">宽度</param>
|
||||
/// <param name="height">高度</param>
|
||||
/// <returns>设置结果</returns>
|
||||
public async Task<(bool IsSuccess, string Message)> SetResolutionAsync(int width, int height)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info($"正在设置视频流分辨率为 {width}x{height}");
|
||||
|
||||
// 验证分辨率
|
||||
if (!IsSupportedResolution(width, height))
|
||||
{
|
||||
var message = $"不支持的分辨率: {width}x{height},支持的分辨率: 640x480, 1280x720";
|
||||
logger.Error(message);
|
||||
return (false, message);
|
||||
}
|
||||
|
||||
Camera? currentCamera = null;
|
||||
lock (_cameraLock)
|
||||
{
|
||||
currentCamera = _camera;
|
||||
}
|
||||
|
||||
if (currentCamera == null)
|
||||
{
|
||||
var message = "摄像头未配置,无法设置分辨率";
|
||||
logger.Error(message);
|
||||
return (false, message);
|
||||
}
|
||||
|
||||
// 设置摄像头分辨率
|
||||
var cameraResult = await currentCamera.ChangeResolution(width, height);
|
||||
if (!cameraResult.IsSuccessful)
|
||||
{
|
||||
var message = $"设置摄像头分辨率失败: {cameraResult.Error}";
|
||||
logger.Error(message);
|
||||
return (false, message);
|
||||
}
|
||||
|
||||
// 更新HTTP服务的分辨率配置
|
||||
lock (_resolutionLock)
|
||||
{
|
||||
_frameWidth = width;
|
||||
_frameHeight = height;
|
||||
}
|
||||
|
||||
var successMessage = $"视频流分辨率已成功设置为 {width}x{height}";
|
||||
logger.Info(successMessage);
|
||||
return (true, successMessage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var message = $"设置分辨率时发生错误: {ex.Message}";
|
||||
logger.Error(ex, message);
|
||||
return (false, message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前分辨率
|
||||
/// </summary>
|
||||
/// <returns>当前分辨率(宽度, 高度)</returns>
|
||||
public (int Width, int Height) GetCurrentResolution()
|
||||
{
|
||||
lock (_resolutionLock)
|
||||
{
|
||||
return (_frameWidth, _frameHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否支持该分辨率
|
||||
/// </summary>
|
||||
/// <param name="width">宽度</param>
|
||||
/// <param name="height">高度</param>
|
||||
/// <returns>是否支持</returns>
|
||||
private bool IsSupportedResolution(int width, int height)
|
||||
{
|
||||
var resolution = $"{width}x{height}";
|
||||
return resolution == "640x480" || resolution == "1280x720";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取支持的分辨率列表
|
||||
/// </summary>
|
||||
/// <returns>支持的分辨率列表</returns>
|
||||
public List<(int Width, int Height, string Name)> GetSupportedResolutions()
|
||||
{
|
||||
return new List<(int, int, string)>
|
||||
{
|
||||
(640, 480, "640x480 (VGA)"),
|
||||
(1280, 720, "1280x720 (HD)")
|
||||
};
|
||||
}
|
||||
|
||||
#region 自动对焦功能
|
||||
|
||||
/// <summary>
|
||||
/// 检查摄像头是否已配置
|
||||
/// </summary>
|
||||
/// <returns>是否已配置</returns>
|
||||
public bool IsCameraConfigured()
|
||||
{
|
||||
lock (_cameraLock)
|
||||
{
|
||||
return _camera != null && !string.IsNullOrEmpty(_cameraAddress);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化摄像头自动对焦功能
|
||||
/// </summary>
|
||||
/// <returns>初始化结果</returns>
|
||||
public async Task<bool> InitAutoFocusAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_cameraLock)
|
||||
{
|
||||
if (_camera == null)
|
||||
{
|
||||
logger.Error("摄像头未配置,无法初始化自动对焦");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("开始初始化摄像头自动对焦功能");
|
||||
|
||||
var result = await _camera!.InitAutoFocus();
|
||||
|
||||
if (result.IsSuccessful && result.Value)
|
||||
{
|
||||
logger.Info("摄像头自动对焦功能初始化成功");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"摄像头自动对焦功能初始化失败: {result.Error?.Message ?? "未知错误"}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "初始化摄像头自动对焦功能时发生异常");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行摄像头自动对焦
|
||||
/// </summary>
|
||||
/// <returns>对焦结果</returns>
|
||||
public async Task<bool> PerformAutoFocusAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_cameraLock)
|
||||
{
|
||||
if (_camera == null)
|
||||
{
|
||||
logger.Error("摄像头未配置,无法执行自动对焦");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("开始执行摄像头自动对焦");
|
||||
|
||||
var result = await _camera!.PerformAutoFocus();
|
||||
|
||||
if (result.IsSuccessful && result.Value)
|
||||
{
|
||||
logger.Info("摄像头自动对焦执行成功");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"摄像头自动对焦执行失败: {result.Error?.Message ?? "未知错误"}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "执行摄像头自动对焦时发生异常");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -110,6 +110,46 @@ public class UDPClientPool
|
||||
return await Task.Run(() => { return SendAddrPack(endPoint, pkg); });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送多个地址包
|
||||
/// </summary>
|
||||
/// <param name="endPoint">IP端点(IP地址与端口)</param>
|
||||
/// <param name="pkgs">地址包集合(最多512 / 8)</param>
|
||||
/// <returns>是否全部成功</returns>
|
||||
public static bool SendMultiAddrPack(IPEndPoint endPoint, IEnumerable<WebProtocol.SendAddrPackage> pkgs)
|
||||
{
|
||||
const int maxPkgs = 512 / 8;
|
||||
var pkgList = pkgs.Take(maxPkgs).ToList();
|
||||
if (pkgList.Count == 0) return false;
|
||||
|
||||
// 合并所有包为一个buffer
|
||||
int totalLen = pkgList.Sum(pkg => pkg.ToBytes().Length);
|
||||
byte[] buffer = new byte[totalLen];
|
||||
int offset = 0;
|
||||
foreach (var pkg in pkgList)
|
||||
{
|
||||
var bytes = pkg.ToBytes();
|
||||
Buffer.BlockCopy(bytes, 0, buffer, offset, bytes.Length);
|
||||
offset += bytes.Length;
|
||||
}
|
||||
|
||||
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
var sendLen = socket.SendTo(buffer.ToArray(), endPoint);
|
||||
socket.Close();
|
||||
|
||||
return sendLen == buffer.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步发送多个地址包
|
||||
/// </summary>
|
||||
/// <param name="endPoint">IP端点(IP地址与端口)</param>
|
||||
/// <param name="pkgs">地址包集合(最多512 / 8)</param>
|
||||
/// <returns>是否全部成功</returns>
|
||||
public async static ValueTask<bool> SendMultiAddrPackAsync(IPEndPoint endPoint, IEnumerable<WebProtocol.SendAddrPackage> pkgs)
|
||||
{
|
||||
return await Task.Run(() => SendMultiAddrPack(endPoint, pkgs));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送数据包
|
||||
@@ -380,6 +420,117 @@ public class UDPClientPool
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 从设备地址读取字节数组数据(支持大数据量分段传输,先发送所有地址包再接收所有数据包)
|
||||
/// </summary>
|
||||
/// <param name="endPoint">IP端点(IP地址与端口)</param>
|
||||
/// <param name="taskID">任务ID</param>
|
||||
/// <param name="devAddr">设备地址</param>
|
||||
/// <param name="dataLength">要读取的数据长度(4字节)</param>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
/// <returns>读取结果,包含接收到的字节数组</returns>
|
||||
public static async ValueTask<Result<byte[]>> ReadAddr4BytesAsync(
|
||||
IPEndPoint endPoint, int taskID, UInt32 devAddr, int dataLength, int timeout = 1000)
|
||||
{
|
||||
var pkgList = new List<SendAddrPackage>();
|
||||
var resultData = new List<byte>();
|
||||
|
||||
// Check Msg Bus
|
||||
if (!MsgBus.IsRunning)
|
||||
return new(new Exception("Message bus not working!"));
|
||||
|
||||
// Prepare packages for each segment
|
||||
var max4BytesPerRead = 0x80; // 512 bytes per read
|
||||
var rest4Bytes = dataLength % max4BytesPerRead;
|
||||
var readTimes = (rest4Bytes != 0) ?
|
||||
(dataLength / max4BytesPerRead + 1) :
|
||||
(dataLength / max4BytesPerRead);
|
||||
|
||||
for (var i = 0; i < readTimes; i++)
|
||||
{
|
||||
var isLastSegment = i == readTimes - 1;
|
||||
var currentSegmentSize = (isLastSegment && rest4Bytes != 0) ? rest4Bytes : max4BytesPerRead;
|
||||
|
||||
var opts = new SendAddrPackOptions
|
||||
{
|
||||
BurstType = BurstType.FixedBurst,
|
||||
CommandID = Convert.ToByte(taskID),
|
||||
IsWrite = false,
|
||||
BurstLength = (byte)(currentSegmentSize - 1),
|
||||
Address = devAddr + (uint)(i * max4BytesPerRead)
|
||||
};
|
||||
pkgList.Add(new SendAddrPackage(opts));
|
||||
}
|
||||
|
||||
// Send address packages in batches of 128, control outstanding
|
||||
int sentCount = 0;
|
||||
var startTime = DateTime.Now;
|
||||
const int batchSize = 64;
|
||||
while (sentCount < pkgList.Count)
|
||||
{
|
||||
var elapsed = DateTime.Now - startTime;
|
||||
if (elapsed >= TimeSpan.FromMilliseconds(timeout))
|
||||
break;
|
||||
var timeleft = timeout - (int)elapsed.TotalMilliseconds;
|
||||
|
||||
var found = await MsgBus.UDPServer.GetDataCountAsync(endPoint.Address.ToString(), taskID, timeleft);
|
||||
int outstanding = sentCount - (found.HasValue ? found.Value : 0);
|
||||
|
||||
// If outstanding >= 512 - batchSize, wait for some data to be received
|
||||
if (outstanding >= 512 - batchSize)
|
||||
continue;
|
||||
|
||||
|
||||
// Send next batch of address packages (up to 128)
|
||||
int batchSend = Math.Min(batchSize, pkgList.Count - sentCount);
|
||||
var batchPkgs = pkgList.Skip(sentCount).Take(batchSend);
|
||||
var ret = await UDPClientPool.SendMultiAddrPackAsync(endPoint, batchPkgs);
|
||||
if (!ret) return new(new Exception($"Send address package batch failed at segment {sentCount}!"));
|
||||
sentCount += batchSend;
|
||||
// Task.Delay(1).Wait();
|
||||
}
|
||||
|
||||
// Wait until enough data is received or timeout
|
||||
startTime = DateTime.Now;
|
||||
var udpDatas = new List<UDPData>();
|
||||
while (true)
|
||||
{
|
||||
var elapsed = DateTime.Now - startTime;
|
||||
if (elapsed >= TimeSpan.FromMilliseconds(timeout)) break;
|
||||
var timeleft = timeout - (int)elapsed.TotalMilliseconds;
|
||||
|
||||
var found = await MsgBus.UDPServer.GetDataCountAsync(endPoint.Address.ToString(), taskID, timeleft);
|
||||
if (found.HasValue && found.Value >= readTimes)
|
||||
{
|
||||
var dataArr = await MsgBus.UDPServer.FindDataArrayAsync(endPoint.Address.ToString(), taskID, timeleft);
|
||||
if (dataArr.HasValue)
|
||||
{
|
||||
udpDatas.AddRange(dataArr.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (udpDatas.Count < readTimes)
|
||||
return new(new Exception($"Expected {readTimes} UDP data packets but received {udpDatas.Count}"));
|
||||
|
||||
// Collect and validate all received data
|
||||
for (var i = 0; i < udpDatas.Count; i++)
|
||||
{
|
||||
var bytes = udpDatas[i].Data;
|
||||
var expectedLen = ((pkgList[i].Options.BurstLength + 1) * 4);
|
||||
if ((bytes.Length - 4) != expectedLen)
|
||||
return new(new Exception($"Expected {expectedLen} bytes but received {bytes.Length - 4} bytes at segment {i}"));
|
||||
resultData.AddRange(bytes[4..]);
|
||||
}
|
||||
|
||||
// Validate total data length
|
||||
if (resultData.Count != dataLength * 4)
|
||||
return new(new Exception($"Expected total {dataLength * 4} bytes but received {resultData.Count} bytes"));
|
||||
|
||||
return resultData.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 向设备地址写入32位数据
|
||||
/// </summary>
|
||||
|
||||
@@ -183,15 +183,59 @@ public class UDPServer
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步寻找目标发送的所有内容,并清空队列
|
||||
/// </summary>
|
||||
/// <param name="ipAddr">目标IP地址</param>
|
||||
/// <param name="taskID">任务ID</param>
|
||||
/// <param name="timeout">超时时间</param>
|
||||
/// <returns>异步Optional 数据包列表</returns>
|
||||
public async ValueTask<Optional<List<UDPData>>> FindDataArrayAsync(string ipAddr, int taskID, int timeout = 1000)
|
||||
{
|
||||
List<UDPData>? data = null;
|
||||
|
||||
var startTime = DateTime.Now;
|
||||
var isTimeout = false;
|
||||
var timeleft = TimeSpan.FromMilliseconds(timeout);
|
||||
while (!isTimeout)
|
||||
{
|
||||
var elapsed = DateTime.Now - startTime;
|
||||
isTimeout = elapsed >= TimeSpan.FromMilliseconds(timeout);
|
||||
if (isTimeout) break;
|
||||
|
||||
timeleft = TimeSpan.FromMilliseconds(timeout) - elapsed;
|
||||
using (await udpData.AcquireWriteLockAsync(timeleft))
|
||||
{
|
||||
if (udpData.ContainsKey($"{ipAddr}-{taskID}") &&
|
||||
udpData.TryGetValue($"{ipAddr}-{taskID}", out var dataQueue) &&
|
||||
dataQueue.Count > 0)
|
||||
{
|
||||
data = dataQueue.ToList();
|
||||
dataQueue.Clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data is null)
|
||||
{
|
||||
logger.Trace("Get nothing even after time out");
|
||||
return new(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(data);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取还未被读取的数据列表
|
||||
/// </summary>
|
||||
/// <param name="ipAddr">IP地址</param>
|
||||
/// <param name="taskID">[TODO:parameter]</param>
|
||||
/// <param name="taskID">任务ID</param>
|
||||
/// <param name="timeout">超时时间</param>
|
||||
/// <param name="cycle">延迟时间</param>
|
||||
/// <returns>数据列表</returns>
|
||||
public async ValueTask<Optional<List<UDPData>>> GetDataArrayAsync(string ipAddr, int taskID, int timeout = 1000, int cycle = 0)
|
||||
public async ValueTask<Optional<List<UDPData>>> GetDataArrayAsync(string ipAddr, int taskID, int timeout = 1000)
|
||||
{
|
||||
List<UDPData>? data = null;
|
||||
|
||||
@@ -229,6 +273,49 @@ public class UDPServer
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取指定IP和任务ID的数据队列长度
|
||||
/// </summary>
|
||||
/// <param name="ipAddr">IP地址</param>
|
||||
/// <param name="taskID">任务ID</param>
|
||||
/// <param name="timeout">超时时间</param>
|
||||
/// <returns>数据队列长度</returns>
|
||||
public async ValueTask<Optional<int>> GetDataCountAsync(string ipAddr, int taskID, int timeout = 1000)
|
||||
{
|
||||
int? count = null;
|
||||
|
||||
var startTime = DateTime.Now;
|
||||
var isTimeout = false;
|
||||
var timeleft = TimeSpan.FromMilliseconds(timeout);
|
||||
while (!isTimeout)
|
||||
{
|
||||
var elapsed = DateTime.Now - startTime;
|
||||
isTimeout = elapsed >= TimeSpan.FromMilliseconds(timeout);
|
||||
if (isTimeout) break;
|
||||
|
||||
timeleft = TimeSpan.FromMilliseconds(timeout) - elapsed;
|
||||
using (await udpData.AcquireReadLockAsync(timeleft))
|
||||
{
|
||||
if (udpData.ContainsKey($"{ipAddr}-{taskID}") &&
|
||||
udpData.TryGetValue($"{ipAddr}-{taskID}", out var dataQueue))
|
||||
{
|
||||
count = dataQueue.Count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count is null)
|
||||
{
|
||||
logger.Trace("Get nothing even after time out");
|
||||
return Optional<int>.None;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(count.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步等待写响应
|
||||
/// </summary>
|
||||
@@ -280,28 +367,32 @@ public class UDPServer
|
||||
|
||||
return retPack.Value;
|
||||
}
|
||||
static int ReceiveHandleCcount = 0;
|
||||
|
||||
private void ReceiveHandler(IAsyncResult res)
|
||||
{
|
||||
logger.Trace("Enter handler");
|
||||
var remoteEP = new IPEndPoint(IPAddress.Any, listenPort);
|
||||
byte[] bytes = listener.EndReceive(res, ref remoteEP);
|
||||
|
||||
// 提前开始接收下一个包
|
||||
listener.BeginReceive(new AsyncCallback(ReceiveHandler), null);
|
||||
logger.Debug($"Test ReceiveHandler Count = {ReceiveHandleCcount}");
|
||||
ReceiveHandleCcount++;
|
||||
|
||||
// Handle RemoteEP
|
||||
if (remoteEP is null)
|
||||
{
|
||||
// logger.Debug($"Receive Data from Unknown at {DateTime.Now.ToString()}:");
|
||||
// logger.Debug($" Original Data : {BitConverter.ToString(bytes).Replace("-", " ")}");
|
||||
goto BEGIN_RECEIVE;
|
||||
logger.Debug($"Receive Data from Unknown at {DateTime.Now.ToString()}:");
|
||||
logger.Debug($" Original Data : {BitConverter.ToString(bytes).Replace("-", " ")}");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Handle Package
|
||||
var udpData = RecordUDPData(bytes, remoteEP, Convert.ToInt32(bytes[1]));
|
||||
PrintData(udpData);
|
||||
|
||||
BEGIN_RECEIVE:
|
||||
listener.BeginReceive(new AsyncCallback(ReceiveHandler), null);
|
||||
// 异步处理数据包
|
||||
Task.Run(() =>
|
||||
{
|
||||
var udpData = RecordUDPData(bytes, remoteEP, Convert.ToInt32(bytes[1]));
|
||||
PrintData(udpData);
|
||||
});
|
||||
}
|
||||
|
||||
private bool SendBytes(IPEndPoint endPoint, byte[] buf)
|
||||
@@ -342,7 +433,7 @@ public class UDPServer
|
||||
udpData.TryGetValue($"{remoteAddress}-{taskID}", out var dataQueue))
|
||||
{
|
||||
dataQueue.Enqueue(data);
|
||||
logger.Trace("Receive data from old client");
|
||||
logger.Debug($"Test Lock dataQueue.Count = {dataQueue.Count}");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
6738
src/APIClient.ts
6738
src/APIClient.ts
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="fixed left-1/2 top-30 z-50 -translate-x-1/2">
|
||||
<div class="fixed left-1/2 top-30 z-999 -translate-x-1/2">
|
||||
<transition
|
||||
name="alert"
|
||||
enter-active-class="alert-enter-active"
|
||||
|
||||
@@ -25,13 +25,12 @@
|
||||
<Plus :size="20" class="text-primary" />
|
||||
添加元器件
|
||||
</h3>
|
||||
<label
|
||||
for="component-drawer"
|
||||
<button
|
||||
class="btn btn-ghost btn-sm btn-circle"
|
||||
@click="closeMenu"
|
||||
>
|
||||
<X :size="20" />
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 导航栏 -->
|
||||
@@ -112,8 +111,8 @@
|
||||
import { ref, computed, shallowRef, onMounted } from "vue";
|
||||
import { Plus, X, Search } from "lucide-vue-next";
|
||||
import ItemList from "./ItemList.vue";
|
||||
import { useAlertStore } from "@/components/Alert";
|
||||
import {
|
||||
useComponentManager,
|
||||
availableComponents,
|
||||
availableVirtualDevices,
|
||||
availableTemplates,
|
||||
@@ -121,6 +120,7 @@ import {
|
||||
type ComponentConfig,
|
||||
type VirtualDeviceConfig,
|
||||
type TemplateConfig,
|
||||
useComponentManager, // 导入 componentManager
|
||||
} from "./index.ts";
|
||||
|
||||
// Props 定义
|
||||
@@ -130,16 +130,18 @@ interface Props {
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const componentManager = useComponentManager();
|
||||
|
||||
// 定义组件发出的事件
|
||||
// 定义组件发出的事件(保留部分必要的事件)
|
||||
const emit = defineEmits([
|
||||
"close",
|
||||
"add-component",
|
||||
"add-template",
|
||||
"update:open",
|
||||
]);
|
||||
|
||||
// 使用 componentManager
|
||||
const componentManager = useComponentManager();
|
||||
|
||||
// 使用 Alert 系统
|
||||
const alert = useAlertStore();
|
||||
|
||||
// 当前激活的选项卡
|
||||
const activeTab = ref("components");
|
||||
|
||||
@@ -192,14 +194,19 @@ async function preloadComponentModules() {
|
||||
|
||||
// 关闭菜单
|
||||
function closeMenu() {
|
||||
showComponentsMenu.value = false;
|
||||
emit("update:open", false);
|
||||
emit("close");
|
||||
}
|
||||
|
||||
// 添加新元器件
|
||||
// 添加新元器件 - 使用 componentManager
|
||||
async function addComponent(
|
||||
componentTemplate: ComponentConfig | VirtualDeviceConfig,
|
||||
) {
|
||||
if (!componentManager) {
|
||||
console.error("ComponentManager not available");
|
||||
return;
|
||||
}
|
||||
|
||||
// 先加载组件模块
|
||||
const moduleRef = await loadComponentModule(componentTemplate.type);
|
||||
let defaultProps: Record<string, any> = {};
|
||||
@@ -220,19 +227,32 @@ async function addComponent(
|
||||
console.log(`Failed to load module for ${componentTemplate.type}`);
|
||||
}
|
||||
|
||||
// 发送添加组件事件给父组件
|
||||
emit("add-component", {
|
||||
type: componentTemplate.type,
|
||||
name: componentTemplate.name,
|
||||
props: defaultProps,
|
||||
});
|
||||
try {
|
||||
// 使用 componentManager 添加组件
|
||||
await componentManager.addComponent({
|
||||
type: componentTemplate.type,
|
||||
name: componentTemplate.name,
|
||||
props: defaultProps,
|
||||
});
|
||||
|
||||
// 关闭菜单
|
||||
closeMenu();
|
||||
// 显示成功消息
|
||||
alert?.success(`成功添加元器件: ${componentTemplate.name}`);
|
||||
|
||||
// 关闭菜单
|
||||
closeMenu();
|
||||
} catch (error) {
|
||||
console.error("添加元器件失败:", error);
|
||||
alert?.error("添加元器件失败,请检查控制台错误信息");
|
||||
}
|
||||
}
|
||||
|
||||
// 添加模板
|
||||
// 添加模板 - 使用 componentManager
|
||||
async function addTemplate(template: TemplateConfig) {
|
||||
if (!componentManager) {
|
||||
console.error("ComponentManager not available");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 加载模板JSON文件
|
||||
const response = await fetch(template.path);
|
||||
@@ -243,19 +263,27 @@ async function addTemplate(template: TemplateConfig) {
|
||||
const templateData = await response.json();
|
||||
console.log("加载模板:", templateData);
|
||||
|
||||
// 发出事件,将模板数据传递给父组件
|
||||
emit("add-template", {
|
||||
// 使用 componentManager 添加模板
|
||||
const result = await componentManager.addTemplate({
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
template: templateData,
|
||||
capsPage: template.capsPage,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
// 使用 Alert 显示结果消息
|
||||
if (result.success) {
|
||||
alert?.success(result.message);
|
||||
} else {
|
||||
alert?.error(result.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭菜单
|
||||
closeMenu();
|
||||
} catch (error) {
|
||||
console.error("加载模板出错:", error);
|
||||
alert("无法加载模板文件,请检查控制台错误信息");
|
||||
alert?.error("无法加载模板文件,请检查控制台错误信息");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex-1 h-full w-full bg-base-200 relative overflow-hidden diagram-container"
|
||||
class="flex-1 h-full w-full bg-base-200 relative overflow-hidden diagram-container flex flex-col select-none"
|
||||
ref="canvasContainer"
|
||||
@mousedown="handleCanvasMouseDown"
|
||||
@mousedown.middle.prevent="startMiddleDrag"
|
||||
@@ -38,13 +38,17 @@
|
||||
|
||||
<div
|
||||
ref="canvas"
|
||||
class="diagram-canvas"
|
||||
class="diagram-canvas relative select-none"
|
||||
:style="{
|
||||
transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`,
|
||||
transform: `translate(${componentManager.canvasPosition.x}px, ${componentManager.canvasPosition.y}px) scale(${componentManager.canvasScale.value})`,
|
||||
}"
|
||||
>
|
||||
<!-- 渲染连线 -->
|
||||
<svg class="wires-layer" width="10000" height="10000">
|
||||
<svg
|
||||
class="wires-layer absolute top-0 left-0 w-full h-full z-50"
|
||||
width="10000"
|
||||
height="10000"
|
||||
>
|
||||
<!-- 已完成的连线 -->
|
||||
<WireComponent
|
||||
v-for="(wire, index) in wireItems"
|
||||
@@ -83,11 +87,11 @@
|
||||
<div
|
||||
v-for="component in diagramParts"
|
||||
:key="component.id"
|
||||
class="component-wrapper"
|
||||
class="component-wrapper absolute p-0 inline-block overflow-visible select-none"
|
||||
:class="{
|
||||
'component-hover': hoveredComponent === component.id,
|
||||
'component-selected': selectedComponentId === component.id,
|
||||
'component-disabled': !component.isOn,
|
||||
'cursor-not-allowed grayscale-70 opacity-60': !component.isOn,
|
||||
'component-hidepins': component.hidepins,
|
||||
}"
|
||||
:style="{
|
||||
@@ -101,16 +105,8 @@
|
||||
display: 'block',
|
||||
}"
|
||||
@mousedown.left.stop="startComponentDrag($event, component)"
|
||||
@mouseover="
|
||||
(event) => {
|
||||
hoveredComponent = component.id;
|
||||
}
|
||||
"
|
||||
@mouseleave="
|
||||
(event) => {
|
||||
hoveredComponent = null;
|
||||
}
|
||||
"
|
||||
@mouseover="hoveredComponent = component.id"
|
||||
@mouseleave="hoveredComponent = null"
|
||||
>
|
||||
<!-- 动态渲染组件 -->
|
||||
<component
|
||||
@@ -167,7 +163,9 @@
|
||||
class="absolute bottom-4 right-4 bg-base-100 px-3 py-1.5 rounded-md shadow-md z-20"
|
||||
style="opacity: 0.9"
|
||||
>
|
||||
<span class="text-sm font-medium">{{ Math.round(scale * 100) }}%</span>
|
||||
<span class="text-sm font-medium"
|
||||
>{{ Math.round(componentManager?.canvasScale.value * 100) }}%</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -193,8 +191,6 @@ import {
|
||||
saveDiagramData,
|
||||
updatePartPosition,
|
||||
updatePartAttribute,
|
||||
deletePart,
|
||||
deleteConnection,
|
||||
parseConnectionPin,
|
||||
connectionArrayToWireItem,
|
||||
validateDiagramData,
|
||||
@@ -216,13 +212,7 @@ function handleContextMenu(e: MouseEvent) {
|
||||
}
|
||||
|
||||
// 定义组件发出的事件
|
||||
const emit = defineEmits([
|
||||
"diagram-updated",
|
||||
"toggle-doc-panel",
|
||||
"wire-created",
|
||||
"wire-deleted",
|
||||
"open-components",
|
||||
]);
|
||||
const emit = defineEmits(["toggle-doc-panel", "open-components"]);
|
||||
|
||||
// 定义组件接受的属性
|
||||
const props = defineProps<{
|
||||
@@ -243,8 +233,6 @@ const alertStore = useAlertStore();
|
||||
// --- 画布状态 ---
|
||||
const canvasContainer = ref<HTMLElement | null>(null);
|
||||
const canvas = ref<HTMLElement | null>(null);
|
||||
const position = reactive({ x: 0, y: 0 });
|
||||
const scale = ref(1);
|
||||
const isDragging = ref(false);
|
||||
const isMiddleDragging = ref(false);
|
||||
const dragStart = reactive({ x: 0, y: 0 });
|
||||
@@ -265,11 +253,6 @@ const diagramData = ref<DiagramData>({
|
||||
connections: [],
|
||||
});
|
||||
|
||||
// 组件引用跟踪(保留以便向后兼容)
|
||||
const componentRefs = computed(
|
||||
() => componentManager?.componentRefs.value || {},
|
||||
);
|
||||
|
||||
// 计算属性:从 diagramData 中提取组件列表,并按index属性排序
|
||||
const diagramParts = computed<DiagramPart[]>(() => {
|
||||
// 克隆原始数组以避免直接修改原始数据
|
||||
@@ -385,7 +368,7 @@ const isWireCreationEventActive = ref(false);
|
||||
// 画布拖拽事件
|
||||
useEventListener(document, "mousemove", (e: MouseEvent) => {
|
||||
if (isDragEventActive.value) {
|
||||
onDrag(e);
|
||||
onCanvasDrag(e);
|
||||
}
|
||||
if (isComponentDragEventActive.value) {
|
||||
onComponentDrag(e);
|
||||
@@ -423,25 +406,13 @@ function onZoom(e: WheelEvent) {
|
||||
const mouseX = e.clientX - containerRect.left;
|
||||
const mouseY = e.clientY - containerRect.top;
|
||||
|
||||
// 计算鼠标在画布坐标系中的位置
|
||||
const mouseXCanvas = (mouseX - position.x) / scale.value;
|
||||
const mouseYCanvas = (mouseY - position.y) / scale.value;
|
||||
|
||||
// 计算缩放值
|
||||
const zoomFactor = 1.1; // 每次放大/缩小10%
|
||||
const direction = e.deltaY > 0 ? -1 : 1;
|
||||
const finalZoomFactor = direction > 0 ? zoomFactor : 1 / zoomFactor;
|
||||
|
||||
// 计算新的缩放值
|
||||
let newScale =
|
||||
direction > 0 ? scale.value * zoomFactor : scale.value / zoomFactor;
|
||||
newScale = Math.max(MIN_SCALE, Math.min(newScale, MAX_SCALE));
|
||||
|
||||
// 计算新的位置,使鼠标指针位置在缩放前后保持不变
|
||||
position.x = mouseX - mouseXCanvas * newScale;
|
||||
position.y = mouseY - mouseYCanvas * newScale;
|
||||
|
||||
// 更新缩放值
|
||||
scale.value = newScale;
|
||||
// 使用componentManager的缩放方法
|
||||
componentManager?.zoomAtPosition(mouseX, mouseY, finalZoomFactor);
|
||||
}
|
||||
|
||||
// --- 画布交互逻辑 ---
|
||||
@@ -472,8 +443,10 @@ function startDrag(e: MouseEvent) {
|
||||
|
||||
isDragging.value = true;
|
||||
isMiddleDragging.value = false;
|
||||
dragStart.x = e.clientX - position.x;
|
||||
dragStart.y = e.clientY - position.y;
|
||||
const currentPosition = componentManager?.getCanvasPosition();
|
||||
if (!currentPosition) return;
|
||||
dragStart.x = e.clientX - currentPosition.x;
|
||||
dragStart.y = e.clientY - currentPosition.y;
|
||||
|
||||
isDragEventActive.value = true;
|
||||
e.preventDefault();
|
||||
@@ -487,19 +460,24 @@ function startMiddleDrag(e: MouseEvent) {
|
||||
isDragging.value = false;
|
||||
draggingComponentId.value = null;
|
||||
|
||||
dragStart.x = e.clientX - position.x;
|
||||
dragStart.y = e.clientY - position.y;
|
||||
const currentPosition = componentManager?.getCanvasPosition();
|
||||
if (!currentPosition) return;
|
||||
dragStart.x = e.clientX - currentPosition.x;
|
||||
dragStart.y = e.clientY - currentPosition.y;
|
||||
|
||||
isDragEventActive.value = true;
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// 拖拽画布过程
|
||||
function onDrag(e: MouseEvent) {
|
||||
function onCanvasDrag(e: MouseEvent) {
|
||||
if (!isDragging.value && !isMiddleDragging.value) return;
|
||||
|
||||
position.x = e.clientX - dragStart.x;
|
||||
position.y = e.clientY - dragStart.y;
|
||||
const newX = e.clientX - dragStart.x;
|
||||
const newY = e.clientY - dragStart.y;
|
||||
|
||||
// 使用componentManager设置画布位置
|
||||
componentManager?.setCanvasPosition(newX, newY);
|
||||
}
|
||||
|
||||
// 停止拖拽画布
|
||||
@@ -551,15 +529,16 @@ function startComponentDrag(e: MouseEvent, component: DiagramPart) {
|
||||
if (!canvasContainer.value) return;
|
||||
const containerRect = canvasContainer.value.getBoundingClientRect();
|
||||
|
||||
// 计算鼠标在画布坐标系中的位置
|
||||
const mouseX_canvas =
|
||||
(e.clientX - containerRect.left - position.x) / scale.value;
|
||||
const mouseY_canvas =
|
||||
(e.clientY - containerRect.top - position.y) / scale.value;
|
||||
// 使用componentManager的屏幕坐标转画布坐标方法
|
||||
const mouseCanvasPos = componentManager?.screenToCanvas(
|
||||
e.clientX - containerRect.left,
|
||||
e.clientY - containerRect.top,
|
||||
);
|
||||
if (!mouseCanvasPos) return;
|
||||
|
||||
// 计算鼠标相对于组件左上角的偏移量
|
||||
componentDragOffset.x = mouseX_canvas - component.x;
|
||||
componentDragOffset.y = mouseY_canvas - component.y;
|
||||
componentDragOffset.x = mouseCanvasPos.x - component.x;
|
||||
componentDragOffset.y = mouseCanvasPos.y - component.y;
|
||||
|
||||
// 激活组件拖拽事件监听
|
||||
isComponentDragEventActive.value = true;
|
||||
@@ -575,15 +554,16 @@ function onComponentDrag(e: MouseEvent) {
|
||||
|
||||
const containerRect = canvasContainer.value.getBoundingClientRect();
|
||||
|
||||
// 计算鼠标在画布坐标系中的位置
|
||||
const mouseX_canvas =
|
||||
(e.clientX - containerRect.left - position.x) / scale.value;
|
||||
const mouseY_canvas =
|
||||
(e.clientY - containerRect.top - position.y) / scale.value;
|
||||
// 使用componentManager的屏幕坐标转画布坐标方法
|
||||
const mouseCanvasPos = componentManager?.screenToCanvas(
|
||||
e.clientX - containerRect.left,
|
||||
e.clientY - containerRect.top,
|
||||
);
|
||||
if (!mouseCanvasPos) return;
|
||||
|
||||
// 计算组件新位置
|
||||
const newX = mouseX_canvas - componentDragOffset.x;
|
||||
const newY = mouseY_canvas - componentDragOffset.y;
|
||||
const newX = mouseCanvasPos.x - componentDragOffset.x;
|
||||
const newY = mouseCanvasPos.y - componentDragOffset.y;
|
||||
|
||||
// 获取当前拖动的组件
|
||||
const draggedComponent = diagramParts.value.find(
|
||||
@@ -600,8 +580,7 @@ function onComponentDrag(e: MouseEvent) {
|
||||
const groupComponents = diagramParts.value.filter(
|
||||
(p) =>
|
||||
p.group === draggedComponent.group &&
|
||||
p.id !== draggingComponentId.value &&
|
||||
!p.positionlock,
|
||||
p.id !== draggingComponentId.value,
|
||||
);
|
||||
|
||||
// 更新这些组件的位置
|
||||
@@ -623,25 +602,18 @@ function onComponentDrag(e: MouseEvent) {
|
||||
y: Math.round(newY),
|
||||
});
|
||||
}
|
||||
|
||||
// 通知图表已更新
|
||||
emit("diagram-updated", diagramData.value);
|
||||
}
|
||||
|
||||
// 停止拖拽组件
|
||||
function stopComponentDrag() {
|
||||
// 如果有组件被拖拽,保存当前状态
|
||||
if (draggingComponentId.value) {
|
||||
// console.log(`组件拖拽结束: ${draggingComponentId.value}`);
|
||||
|
||||
// 保存图表数据
|
||||
saveDiagramData(diagramData.value);
|
||||
|
||||
// 清除拖动状态
|
||||
draggingComponentId.value = null;
|
||||
}
|
||||
|
||||
isComponentDragEventActive.value = false;
|
||||
|
||||
saveDiagramData(diagramData.value);
|
||||
}
|
||||
|
||||
// 更新组件属性
|
||||
@@ -661,8 +633,6 @@ function updateComponentProp(
|
||||
propName,
|
||||
value,
|
||||
);
|
||||
emit("diagram-updated", diagramData.value);
|
||||
saveDiagramData(diagramData.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,9 +641,19 @@ function updateComponentProp(
|
||||
function updateMousePosition(e: MouseEvent) {
|
||||
if (!canvasContainer.value) return;
|
||||
const containerRect = canvasContainer.value.getBoundingClientRect();
|
||||
mousePosition.x = (e.clientX - containerRect.left - position.x) / scale.value;
|
||||
mousePosition.y = (e.clientY - containerRect.top - position.y) / scale.value;
|
||||
} // 处理引脚点击
|
||||
|
||||
// 使用componentManager的屏幕坐标转画布坐标方法
|
||||
const canvasPos = componentManager?.screenToCanvas(
|
||||
e.clientX - containerRect.left,
|
||||
e.clientY - containerRect.top,
|
||||
);
|
||||
if (!canvasPos) return;
|
||||
|
||||
mousePosition.x = canvasPos.x;
|
||||
mousePosition.y = canvasPos.y;
|
||||
}
|
||||
|
||||
// 处理引脚点击
|
||||
function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
|
||||
if (!canvasContainer.value) return;
|
||||
updateMousePosition(event);
|
||||
@@ -830,13 +810,6 @@ function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
|
||||
connections: [...diagramData.value.connections, newConnection],
|
||||
};
|
||||
|
||||
// 通知连线创建
|
||||
emit("wire-created", newConnection);
|
||||
emit("diagram-updated", diagramData.value);
|
||||
|
||||
// 保存图表数据
|
||||
saveDiagramData(diagramData.value);
|
||||
|
||||
// 重置连线创建状态
|
||||
resetWireCreation();
|
||||
isWireCreationEventActive.value = false;
|
||||
@@ -880,23 +853,12 @@ function onCreatingWireMouseMove(e: MouseEvent) {
|
||||
updateMousePosition(e);
|
||||
}
|
||||
|
||||
// 删除连线
|
||||
function deleteWire(wireIndex: number) {
|
||||
diagramData.value = deleteConnection(diagramData.value, wireIndex);
|
||||
emit("wire-deleted", wireIndex);
|
||||
emit("diagram-updated", diagramData.value);
|
||||
saveDiagramData(diagramData.value);
|
||||
}
|
||||
|
||||
// 删除组件
|
||||
function deleteComponent(componentId: string) {
|
||||
diagramData.value = deletePart(diagramData.value, componentId);
|
||||
// 直接通过componentManager删除组件
|
||||
if (componentManager) {
|
||||
componentManager.deleteComponent(componentId);
|
||||
}
|
||||
emit("diagram-updated", diagramData.value);
|
||||
saveDiagramData(diagramData.value);
|
||||
|
||||
// 清除选中状态
|
||||
if (selectedComponentId.value === componentId) {
|
||||
@@ -950,12 +912,6 @@ function handleFileSelected(event: Event) {
|
||||
// 更新画布数据
|
||||
diagramData.value = parsed as DiagramData;
|
||||
|
||||
// 保存到本地文件
|
||||
saveDiagramData(diagramData.value);
|
||||
|
||||
// 发出更新事件
|
||||
emit("diagram-updated", diagramData.value);
|
||||
|
||||
alertStore?.show(`成功导入diagram文件`, "success");
|
||||
} catch (error) {
|
||||
console.error("解析JSON文件出错:", error);
|
||||
@@ -1019,23 +975,6 @@ function exportDiagram() {
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
onMounted(async () => {
|
||||
// 设置componentManager的画布引用
|
||||
if (componentManager) {
|
||||
// 创建一个包含必要方法的画布API对象
|
||||
const canvasAPI = {
|
||||
getDiagramData: () => diagramData.value,
|
||||
updateDiagramDataDirectly: (data: DiagramData) => {
|
||||
diagramData.value = data;
|
||||
saveDiagramData(data);
|
||||
emit("diagram-updated", data);
|
||||
},
|
||||
getCanvasPosition: () => ({ x: position.x, y: position.y }),
|
||||
getScale: () => scale.value,
|
||||
$el: canvasContainer.value,
|
||||
};
|
||||
componentManager.setCanvasRef(canvasAPI);
|
||||
}
|
||||
|
||||
// 加载图表数据
|
||||
try {
|
||||
diagramData.value = await loadDiagramData();
|
||||
@@ -1060,11 +999,13 @@ onMounted(async () => {
|
||||
} catch (error) {
|
||||
console.error("加载图表数据失败:", error);
|
||||
}
|
||||
// 初始化中心位置
|
||||
if (canvasContainer.value) {
|
||||
|
||||
// 初始化中心位置 - 使用componentManager设置
|
||||
if (canvasContainer.value && componentManager) {
|
||||
// 修改为将画布中心点放在容器中心点
|
||||
position.x = canvasContainer.value.clientWidth / 2 - 5000; // 画布宽度的一半
|
||||
position.y = canvasContainer.value.clientHeight / 2 - 5000; // 画布高度的一半
|
||||
const centerX = canvasContainer.value.clientWidth / 2 - 5000; // 画布宽度的一半
|
||||
const centerY = canvasContainer.value.clientHeight / 2 - 5000; // 画布高度的一半
|
||||
componentManager.setCanvasPosition(centerX, centerY);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1091,10 +1032,6 @@ function updateDiagramDataDirectly(data: DiagramData) {
|
||||
}
|
||||
|
||||
diagramData.value = data;
|
||||
saveDiagramData(data);
|
||||
|
||||
// 发出diagram-updated事件
|
||||
emit("diagram-updated", data);
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
@@ -1102,95 +1039,31 @@ defineExpose({
|
||||
// 基本数据操作
|
||||
getDiagramData: () => diagramData.value,
|
||||
updateDiagramDataDirectly,
|
||||
setDiagramData: (data: DiagramData) => {
|
||||
// 检查组件是否仍然挂载
|
||||
if (!document.body.contains(canvasContainer.value)) {
|
||||
return; // 如果组件已经卸载,不执行后续操作
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
// 使用requestAnimationFrame确保UI更新
|
||||
window.requestAnimationFrame(() => {
|
||||
// 再次检查组件是否仍然挂载
|
||||
if (!document.body.contains(canvasContainer.value)) {
|
||||
return; // 如果组件已经卸载,不执行后续操作
|
||||
}
|
||||
|
||||
diagramData.value = data;
|
||||
saveDiagramData(data);
|
||||
|
||||
// 发出diagram-updated事件
|
||||
emit("diagram-updated", data);
|
||||
|
||||
// 短暂延迟后结束加载状态,以便UI能更新
|
||||
setTimeout(() => {
|
||||
// 检查组件是否仍然挂载
|
||||
if (document.body.contains(canvasContainer.value)) {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
},
|
||||
|
||||
// 画布状态
|
||||
getCanvasPosition: () => ({ x: position.x, y: position.y }),
|
||||
getScale: () => scale.value,
|
||||
});
|
||||
|
||||
// 监视器 - 当图表数据更改时保存
|
||||
watch(
|
||||
diagramData,
|
||||
(newData) => {
|
||||
saveDiagramData(newData);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 基础容器样式 - 使用 Tailwind 类替代 */
|
||||
.diagram-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-size:
|
||||
20px 20px,
|
||||
20px 20px,
|
||||
100px 100px,
|
||||
100px 100px;
|
||||
background-position: 0 0;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 画布样式 - 部分保留自定义属性 */
|
||||
.diagram-canvas {
|
||||
position: relative;
|
||||
width: 10000px;
|
||||
height: 10000px;
|
||||
transform-origin: 0 0;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
/* 连线层样式 */
|
||||
.wires-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: auto;
|
||||
/* 修复:允许线被点击 */
|
||||
z-index: 50;
|
||||
overflow: visible;
|
||||
/* 确保超出SVG范围的内容也能显示 */
|
||||
}
|
||||
|
||||
.wires-layer path {
|
||||
@@ -1198,66 +1071,28 @@ watch(
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 元器件容器样式 */
|
||||
/* 组件容器样式 */
|
||||
.component-wrapper {
|
||||
position: absolute;
|
||||
padding: 0;
|
||||
/* 移除内边距,确保元素大小与内容完全匹配 */
|
||||
box-sizing: content-box;
|
||||
/* 使用content-box确保内容尺寸不受padding影响 */
|
||||
display: inline-block;
|
||||
overflow: visible;
|
||||
/* 允许内容溢出(用于显示边框) */
|
||||
cursor: move;
|
||||
/* 显示移动光标 */
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
/* 悬停状态 - 使用outline而非伪元素 */
|
||||
/* 悬停状态 */
|
||||
.component-hover {
|
||||
outline: 2px dashed #3498db;
|
||||
outline-offset: 2px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 选中状态 - 使用outline而非伪元素 */
|
||||
/* 选中状态 */
|
||||
.component-selected {
|
||||
outline: 3px dashed;
|
||||
outline-color: #e74c3c #f39c12 #3498db #2ecc71;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
/* 禁用状态 */
|
||||
.component-disabled {
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(70%);
|
||||
}
|
||||
|
||||
/* 隐藏引脚状态 */
|
||||
.component-hidepins :deep([data-pin-wrapper]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 为黑暗模式设置不同的网格线颜色 */
|
||||
/* :root[data-theme="dark"] .diagram-container {
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(to right, rgba(180, 180, 180, 0.15) 100px, transparent 100px),
|
||||
linear-gradient(to bottom, rgba(180, 180, 180, 0.15) 100px, transparent 100px);
|
||||
} */
|
||||
|
||||
/* 深度选择器 - 默认阻止SVG内部元素的鼠标事件,但允许SVG本身和特定交互元素 */
|
||||
/* SVG 交互样式 */
|
||||
.component-wrapper :deep(svg) {
|
||||
pointer-events: auto;
|
||||
/* 确保SVG本身可以接收鼠标事件用于拖拽 */
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
.component-wrapper
|
||||
@@ -1268,10 +1103,8 @@ watch(
|
||||
):not([fill-opacity])
|
||||
) {
|
||||
pointer-events: none;
|
||||
/* 非交互元素不接收鼠标事件 */
|
||||
}
|
||||
|
||||
/* 允许特定SVG元素接收鼠标事件,用于交互 */
|
||||
.component-wrapper :deep(svg circle[fill-opacity]),
|
||||
.component-wrapper :deep(svg rect[fill-opacity]),
|
||||
.component-wrapper :deep(svg rect[class*="glow"]),
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { ref, shallowRef, computed } from "vue";
|
||||
import { ref, shallowRef, computed, reactive } from "vue";
|
||||
import { createInjectionState } from "@vueuse/core";
|
||||
import type { DiagramData, DiagramPart } from "./diagramManager";
|
||||
import {
|
||||
saveDiagramData,
|
||||
type DiagramData,
|
||||
type DiagramPart,
|
||||
} from "./diagramManager";
|
||||
import type { PropertyConfig } from "@/components/equipments/componentConfig";
|
||||
import {
|
||||
generatePropertyConfigs,
|
||||
@@ -24,37 +28,162 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
// --- 状态管理 ---
|
||||
const componentModules = ref<Record<string, ComponentModule>>({});
|
||||
const selectedComponentId = ref<string | null>(null);
|
||||
const selectedComponentConfig = shallowRef<{ props: PropertyConfig[] } | null>(null);
|
||||
const selectedComponentConfig = shallowRef<{
|
||||
props: PropertyConfig[];
|
||||
} | null>(null);
|
||||
const diagramCanvas = ref<any>(null);
|
||||
const componentRefs = ref<Record<string, any>>({});
|
||||
|
||||
// 新增:直接管理canvas的位置和缩放
|
||||
const canvasPosition = reactive({ x: 0, y: 0 });
|
||||
const canvasScale = ref(1);
|
||||
|
||||
// 计算当前选中的组件数据
|
||||
const selectedComponentData = computed(() => {
|
||||
if (!diagramCanvas.value || !selectedComponentId.value) return null;
|
||||
|
||||
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (canvasInstance && canvasInstance.getDiagramData) {
|
||||
const data = canvasInstance.getDiagramData();
|
||||
return data.parts.find((p: DiagramPart) => p.id === selectedComponentId.value) || null;
|
||||
return (
|
||||
data.parts.find(
|
||||
(p: DiagramPart) => p.id === selectedComponentId.value,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// --- Canvas 控制方法 ---
|
||||
|
||||
/**
|
||||
* 设置canvas位置
|
||||
*/
|
||||
function setCanvasPosition(x: number, y: number) {
|
||||
canvasPosition.x = x;
|
||||
canvasPosition.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新canvas位置(相对偏移)
|
||||
*/
|
||||
function updateCanvasPosition(deltaX: number, deltaY: number) {
|
||||
canvasPosition.x += deltaX;
|
||||
canvasPosition.y += deltaY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置canvas缩放
|
||||
*/
|
||||
function setCanvasScale(scale: number) {
|
||||
canvasScale.value = Math.max(0.2, Math.min(scale, 10.0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取canvas位置
|
||||
*/
|
||||
function getCanvasPosition() {
|
||||
return { x: canvasPosition.x, y: canvasPosition.y };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取canvas缩放
|
||||
*/
|
||||
function getCanvasScale() {
|
||||
return canvasScale.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缩放到指定位置(以鼠标位置为中心)
|
||||
*/
|
||||
function zoomAtPosition(
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
zoomFactor: number,
|
||||
) {
|
||||
// 计算鼠标在画布坐标系中的位置
|
||||
const mouseXCanvas = (mouseX - canvasPosition.x) / canvasScale.value;
|
||||
const mouseYCanvas = (mouseY - canvasPosition.y) / canvasScale.value;
|
||||
|
||||
// 计算新的缩放值
|
||||
const newScale = Math.max(
|
||||
0.2,
|
||||
Math.min(canvasScale.value * zoomFactor, 10.0),
|
||||
);
|
||||
|
||||
// 计算新的位置,使鼠标指针位置在缩放前后保持不变
|
||||
canvasPosition.x = mouseX - mouseXCanvas * newScale;
|
||||
canvasPosition.y = mouseY - mouseYCanvas * newScale;
|
||||
canvasScale.value = newScale;
|
||||
|
||||
return {
|
||||
scale: newScale,
|
||||
position: { x: canvasPosition.x, y: canvasPosition.y },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将屏幕坐标转换为画布坐标
|
||||
*/
|
||||
function screenToCanvas(screenX: number, screenY: number) {
|
||||
return {
|
||||
x: (screenX - canvasPosition.x) / canvasScale.value,
|
||||
y: (screenY - canvasPosition.y) / canvasScale.value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将画布坐标转换为屏幕坐标
|
||||
*/
|
||||
function canvasToScreen(canvasX: number, canvasY: number) {
|
||||
return {
|
||||
x: canvasX * canvasScale.value + canvasPosition.x,
|
||||
y: canvasY * canvasScale.value + canvasPosition.y,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 居中显示指定区域
|
||||
*/
|
||||
function centerView(
|
||||
bounds: { x: number; y: number; width: number; height: number },
|
||||
containerWidth: number,
|
||||
containerHeight: number,
|
||||
) {
|
||||
// 计算合适的缩放比例
|
||||
const scaleX = containerWidth / bounds.width;
|
||||
const scaleY = containerHeight / bounds.height;
|
||||
const newScale = Math.min(scaleX, scaleY, 1) * 0.8; // 留一些边距
|
||||
|
||||
// 计算居中位置
|
||||
const centerX = bounds.x + bounds.width / 2;
|
||||
const centerY = bounds.y + bounds.height / 2;
|
||||
|
||||
canvasScale.value = newScale;
|
||||
canvasPosition.x = containerWidth / 2 - centerX * newScale;
|
||||
canvasPosition.y = containerHeight / 2 - centerY * newScale;
|
||||
|
||||
return {
|
||||
scale: newScale,
|
||||
position: { x: canvasPosition.x, y: canvasPosition.y },
|
||||
};
|
||||
}
|
||||
|
||||
// --- 组件模块管理 ---
|
||||
|
||||
|
||||
/**
|
||||
* 动态加载组件模块
|
||||
*/
|
||||
async function loadComponentModule(type: string) {
|
||||
console.log(`尝试加载组件模块: ${type}`);
|
||||
console.log(`当前已加载的模块:`, Object.keys(componentModules.value));
|
||||
|
||||
|
||||
if (!componentModules.value[type]) {
|
||||
try {
|
||||
console.log(`正在动态导入模块: @/components/equipments/${type}.vue`);
|
||||
const module = await import(`@/components/equipments/${type}.vue`);
|
||||
console.log(`成功导入模块 ${type}:`, module);
|
||||
|
||||
|
||||
// 直接设置新的对象引用以触发响应性
|
||||
componentModules.value = {
|
||||
...componentModules.value,
|
||||
@@ -78,7 +207,7 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
async function preloadComponentModules(componentTypes: string[]) {
|
||||
console.log("Preloading component modules:", componentTypes);
|
||||
await Promise.all(
|
||||
componentTypes.map((type) => loadComponentModule(type))
|
||||
componentTypes.map((type) => loadComponentModule(type)),
|
||||
);
|
||||
console.log("All component modules loaded");
|
||||
}
|
||||
@@ -95,7 +224,7 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
}) {
|
||||
console.log("=== 开始添加组件 ===");
|
||||
console.log("组件数据:", componentData);
|
||||
|
||||
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (!canvasInstance) {
|
||||
console.error("没有可用的画布实例");
|
||||
@@ -111,23 +240,19 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
}
|
||||
console.log(`组件模块加载成功: ${componentData.type}`, componentModule);
|
||||
|
||||
// 获取画布位置信息
|
||||
// 使用内部管理的位置和缩放信息
|
||||
let position = { x: 100, y: 100 };
|
||||
let scale = 1;
|
||||
|
||||
try {
|
||||
if (canvasInstance.getCanvasPosition && canvasInstance.getScale) {
|
||||
position = canvasInstance.getCanvasPosition();
|
||||
scale = canvasInstance.getScale();
|
||||
const canvasContainer = canvasInstance.$el as HTMLElement;
|
||||
if (canvasContainer) {
|
||||
const viewportWidth = canvasContainer.clientWidth;
|
||||
const viewportHeight = canvasContainer.clientHeight;
|
||||
|
||||
const canvasContainer = canvasInstance.$el as HTMLElement;
|
||||
if (canvasContainer) {
|
||||
const viewportWidth = canvasContainer.clientWidth;
|
||||
const viewportHeight = canvasContainer.clientHeight;
|
||||
|
||||
position.x = (viewportWidth / 2 - position.x) / scale;
|
||||
position.y = (viewportHeight / 2 - position.y) / scale;
|
||||
}
|
||||
position.x =
|
||||
(viewportWidth / 2 - canvasPosition.x) / canvasScale.value;
|
||||
position.y =
|
||||
(viewportHeight / 2 - canvasPosition.y) / canvasScale.value;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取画布位置时出错:", error);
|
||||
@@ -168,17 +293,21 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
};
|
||||
|
||||
// 通过画布实例添加组件
|
||||
if (canvasInstance.getDiagramData && canvasInstance.updateDiagramDataDirectly) {
|
||||
if (
|
||||
canvasInstance.getDiagramData &&
|
||||
canvasInstance.updateDiagramDataDirectly
|
||||
) {
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
currentData.parts.push(newComponent);
|
||||
|
||||
|
||||
// 使用 updateDiagramDataDirectly 避免触发加载状态
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
|
||||
saveDiagramData(currentData);
|
||||
|
||||
console.log("组件添加完成:", newComponent);
|
||||
|
||||
|
||||
// 等待Vue的下一个tick,确保组件模块已经更新
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,9 +320,12 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
template: any;
|
||||
}) {
|
||||
console.log("添加模板:", templateData);
|
||||
|
||||
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||
if (
|
||||
!canvasInstance?.getDiagramData ||
|
||||
!canvasInstance?.updateDiagramDataDirectly
|
||||
) {
|
||||
console.error("没有可用的画布实例添加模板");
|
||||
return;
|
||||
}
|
||||
@@ -205,20 +337,18 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
const idPrefix = `template-${Date.now()}-`;
|
||||
|
||||
if (templateData.template?.parts) {
|
||||
// 获取视口中心位置
|
||||
// 使用内部管理的位置和缩放信息获取视口中心位置
|
||||
let viewportCenter = { x: 300, y: 200 };
|
||||
try {
|
||||
if (canvasInstance.getCanvasPosition && canvasInstance.getScale) {
|
||||
const position = canvasInstance.getCanvasPosition();
|
||||
const scale = canvasInstance.getScale();
|
||||
const canvasContainer = canvasInstance.$el as HTMLElement;
|
||||
|
||||
if (canvasContainer) {
|
||||
const viewportWidth = canvasContainer.clientWidth;
|
||||
const viewportHeight = canvasContainer.clientHeight;
|
||||
viewportCenter.x = (viewportWidth / 2 - position.x) / scale;
|
||||
viewportCenter.y = (viewportHeight / 2 - position.y) / scale;
|
||||
}
|
||||
const canvasContainer = canvasInstance.$el as HTMLElement;
|
||||
|
||||
if (canvasContainer) {
|
||||
const viewportWidth = canvasContainer.clientWidth;
|
||||
const viewportHeight = canvasContainer.clientHeight;
|
||||
viewportCenter.x =
|
||||
(viewportWidth / 2 - canvasPosition.x) / canvasScale.value;
|
||||
viewportCenter.y =
|
||||
(viewportHeight / 2 - canvasPosition.y) / canvasScale.value;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取视口中心位置时出错:", error);
|
||||
@@ -247,7 +377,10 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
}
|
||||
|
||||
// 计算新位置
|
||||
if (typeof newPart.x === "number" && typeof newPart.y === "number") {
|
||||
if (
|
||||
typeof newPart.x === "number" &&
|
||||
typeof newPart.y === "number"
|
||||
) {
|
||||
const relativeX = part.x - mainPart.x;
|
||||
const relativeY = part.y - mainPart.y;
|
||||
newPart.x = viewportCenter.x + relativeX;
|
||||
@@ -255,7 +388,7 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
}
|
||||
|
||||
return newPart;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
currentData.parts.push(...newParts);
|
||||
@@ -267,32 +400,38 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
idMap[part.id] = `${idPrefix}${part.id}`;
|
||||
});
|
||||
|
||||
const newConnections = templateData.template.connections.map((conn: any) => {
|
||||
if (Array.isArray(conn)) {
|
||||
const [from, to, type, path] = conn;
|
||||
const fromParts = from.split(":");
|
||||
const toParts = to.split(":");
|
||||
const newConnections = templateData.template.connections.map(
|
||||
(conn: any) => {
|
||||
if (Array.isArray(conn)) {
|
||||
const [from, to, type, path] = conn;
|
||||
const fromParts = from.split(":");
|
||||
const toParts = to.split(":");
|
||||
|
||||
if (fromParts.length === 2 && toParts.length === 2) {
|
||||
const fromComponentId = fromParts[0];
|
||||
const fromPinId = fromParts[1];
|
||||
const toComponentId = toParts[0];
|
||||
const toPinId = toParts[1];
|
||||
if (fromParts.length === 2 && toParts.length === 2) {
|
||||
const fromComponentId = fromParts[0];
|
||||
const fromPinId = fromParts[1];
|
||||
const toComponentId = toParts[0];
|
||||
const toPinId = toParts[1];
|
||||
|
||||
const newFrom = `${idMap[fromComponentId] || fromComponentId}:${fromPinId}`;
|
||||
const newTo = `${idMap[toComponentId] || toComponentId}:${toPinId}`;
|
||||
const newFrom = `${idMap[fromComponentId] || fromComponentId}:${fromPinId}`;
|
||||
const newTo = `${idMap[toComponentId] || toComponentId}:${toPinId}`;
|
||||
|
||||
return [newFrom, newTo, type, path];
|
||||
return [newFrom, newTo, type, path];
|
||||
}
|
||||
}
|
||||
}
|
||||
return conn;
|
||||
});
|
||||
return conn;
|
||||
},
|
||||
);
|
||||
|
||||
currentData.connections.push(...newConnections);
|
||||
}
|
||||
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
console.log("=== 更新图表数据完成,新组件数量:", currentData.parts.length);
|
||||
console.log(
|
||||
"=== 更新图表数据完成,新组件数量:",
|
||||
currentData.parts.length,
|
||||
);
|
||||
saveDiagramData(currentData);
|
||||
|
||||
return { success: true, message: `已添加 ${templateData.name} 模板` };
|
||||
} else {
|
||||
@@ -306,13 +445,18 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
*/
|
||||
function deleteComponent(componentId: string) {
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||
if (
|
||||
!canvasInstance?.getDiagramData ||
|
||||
!canvasInstance?.updateDiagramDataDirectly
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
const component = currentData.parts.find((p: DiagramPart) => p.id === componentId);
|
||||
|
||||
const component = currentData.parts.find(
|
||||
(p: DiagramPart) => p.id === componentId,
|
||||
);
|
||||
|
||||
if (!component) return;
|
||||
|
||||
const componentsToDelete: string[] = [componentId];
|
||||
@@ -320,34 +464,47 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
// 处理组件组
|
||||
if (component.group && component.group !== "") {
|
||||
const groupMembers = currentData.parts.filter(
|
||||
(p: DiagramPart) => p.group === component.group && p.id !== componentId
|
||||
(p: DiagramPart) =>
|
||||
p.group === component.group && p.id !== componentId,
|
||||
);
|
||||
componentsToDelete.push(...groupMembers.map((p: DiagramPart) => p.id));
|
||||
console.log(`删除组件 ${componentId} 及其组 ${component.group} 中的 ${groupMembers.length} 个组件`);
|
||||
console.log(
|
||||
`删除组件 ${componentId} 及其组 ${component.group} 中的 ${groupMembers.length} 个组件`,
|
||||
);
|
||||
}
|
||||
|
||||
// 删除组件
|
||||
currentData.parts = currentData.parts.filter(
|
||||
(p: DiagramPart) => !componentsToDelete.includes(p.id)
|
||||
(p: DiagramPart) => !componentsToDelete.includes(p.id),
|
||||
);
|
||||
|
||||
// 删除相关连接
|
||||
currentData.connections = currentData.connections.filter((connection: any) => {
|
||||
for (const id of componentsToDelete) {
|
||||
if (connection[0].startsWith(`${id}:`) || connection[1].startsWith(`${id}:`)) {
|
||||
return false;
|
||||
currentData.connections = currentData.connections.filter(
|
||||
(connection: any) => {
|
||||
for (const id of componentsToDelete) {
|
||||
if (
|
||||
connection[0].startsWith(`${id}:`) ||
|
||||
connection[1].startsWith(`${id}:`)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
// 清除选中状态
|
||||
if (selectedComponentId.value && componentsToDelete.includes(selectedComponentId.value)) {
|
||||
if (
|
||||
selectedComponentId.value &&
|
||||
componentsToDelete.includes(selectedComponentId.value)
|
||||
) {
|
||||
selectedComponentId.value = null;
|
||||
selectedComponentConfig.value = null;
|
||||
}
|
||||
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
|
||||
saveDiagramData(currentData);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -378,16 +535,18 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
// 添加组件直接属性
|
||||
const directPropConfigs = generatePropertyConfigs(componentData);
|
||||
const newDirectProps = directPropConfigs.filter(
|
||||
(config) => !addedProps.has(config.name)
|
||||
(config) => !addedProps.has(config.name),
|
||||
);
|
||||
propConfigs.push(...newDirectProps);
|
||||
|
||||
// 添加 attrs 中的属性
|
||||
if (componentData.attrs) {
|
||||
const attrPropConfigs = generatePropsFromAttrs(componentData.attrs);
|
||||
const attrPropConfigs = generatePropsFromAttrs(
|
||||
componentData.attrs,
|
||||
);
|
||||
attrPropConfigs.forEach((attrConfig) => {
|
||||
const existingIndex = propConfigs.findIndex(
|
||||
(p) => p.name === attrConfig.name
|
||||
(p) => p.name === attrConfig.name,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
propConfigs[existingIndex] = attrConfig;
|
||||
@@ -398,9 +557,15 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
}
|
||||
|
||||
selectedComponentConfig.value = { props: propConfigs };
|
||||
console.log(`Built config for ${componentData.type}:`, selectedComponentConfig.value);
|
||||
console.log(
|
||||
`Built config for ${componentData.type}:`,
|
||||
selectedComponentConfig.value,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`Error building config for ${componentData.type}:`, error);
|
||||
console.error(
|
||||
`Error building config for ${componentData.type}:`,
|
||||
error,
|
||||
);
|
||||
selectedComponentConfig.value = { props: [] };
|
||||
}
|
||||
} else {
|
||||
@@ -413,9 +578,16 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
/**
|
||||
* 更新组件属性
|
||||
*/
|
||||
function updateComponentProp(componentId: string, propName: string, value: any) {
|
||||
function updateComponentProp(
|
||||
componentId: string,
|
||||
propName: string,
|
||||
value: any,
|
||||
) {
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||
if (
|
||||
!canvasInstance?.getDiagramData ||
|
||||
!canvasInstance?.updateDiagramDataDirectly
|
||||
) {
|
||||
console.error("没有可用的画布实例进行属性更新");
|
||||
return;
|
||||
}
|
||||
@@ -426,7 +598,9 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
}
|
||||
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
const part = currentData.parts.find((p: DiagramPart) => p.id === componentId);
|
||||
const part = currentData.parts.find(
|
||||
(p: DiagramPart) => p.id === componentId,
|
||||
);
|
||||
|
||||
if (part) {
|
||||
if (propName in part) {
|
||||
@@ -439,27 +613,44 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
}
|
||||
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
console.log(`更新组件${componentId}的属性${propName}为:`, value, typeof value);
|
||||
console.log(
|
||||
`更新组件${componentId}的属性${propName}为:`,
|
||||
value,
|
||||
typeof value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新组件直接属性
|
||||
*/
|
||||
function updateComponentDirectProp(componentId: string, propName: string, value: any) {
|
||||
function updateComponentDirectProp(
|
||||
componentId: string,
|
||||
propName: string,
|
||||
value: any,
|
||||
) {
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||
if (
|
||||
!canvasInstance?.getDiagramData ||
|
||||
!canvasInstance?.updateDiagramDataDirectly
|
||||
) {
|
||||
console.error("没有可用的画布实例进行属性更新");
|
||||
return;
|
||||
}
|
||||
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
const part = currentData.parts.find((p: DiagramPart) => p.id === componentId);
|
||||
const part = currentData.parts.find(
|
||||
(p: DiagramPart) => p.id === componentId,
|
||||
);
|
||||
|
||||
if (part) {
|
||||
(part as any)[propName] = value;
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
console.log(`更新组件${componentId}的直接属性${propName}为:`, value, typeof value);
|
||||
console.log(
|
||||
`更新组件${componentId}的直接属性${propName}为:`,
|
||||
value,
|
||||
typeof value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,12 +659,17 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
*/
|
||||
function moveComponent(moveData: { id: string; x: number; y: number }) {
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||
if (
|
||||
!canvasInstance?.getDiagramData ||
|
||||
!canvasInstance?.updateDiagramDataDirectly
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
const part = currentData.parts.find((p: DiagramPart) => p.id === moveData.id);
|
||||
const part = currentData.parts.find(
|
||||
(p: DiagramPart) => p.id === moveData.id,
|
||||
);
|
||||
if (part) {
|
||||
part.x = moveData.x;
|
||||
part.y = moveData.y;
|
||||
@@ -514,7 +710,13 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
if (canvasInstance && canvasInstance.getDiagramData) {
|
||||
return canvasInstance.getDiagramData();
|
||||
}
|
||||
return { parts: [], connections: [], version: 1, author: "admin", editor: "me" };
|
||||
return {
|
||||
parts: [],
|
||||
connections: [],
|
||||
version: 1,
|
||||
author: "admin",
|
||||
editor: "me",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -527,35 +729,12 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取画布位置和缩放信息
|
||||
*/
|
||||
function getCanvasInfo() {
|
||||
const canvasInstance = diagramCanvas.value;
|
||||
if (!canvasInstance) return { position: { x: 0, y: 0 }, scale: 1 };
|
||||
|
||||
const position = canvasInstance.getCanvasPosition ? canvasInstance.getCanvasPosition() : { x: 0, y: 0 };
|
||||
const scale = canvasInstance.getScale ? canvasInstance.getScale() : 1;
|
||||
|
||||
return { position, scale };
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示通知
|
||||
*/
|
||||
function showToast(message: string, type: "success" | "error" | "info" = "info") {
|
||||
const canvasInstance = diagramCanvas.value;
|
||||
if (canvasInstance && canvasInstance.showToast) {
|
||||
canvasInstance.showToast(message, type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件定义
|
||||
*/
|
||||
function getComponentDefinition(type: string) {
|
||||
const module = componentModules.value[type];
|
||||
|
||||
|
||||
if (!module) {
|
||||
console.warn(`No module found for component type: ${type}`);
|
||||
// 尝试异步加载组件模块
|
||||
@@ -599,7 +778,7 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (canvasInstance?.getDiagramData) {
|
||||
const diagramData = canvasInstance.getDiagramData();
|
||||
|
||||
|
||||
// 收集所有组件类型
|
||||
const componentTypes = new Set<string>();
|
||||
diagramData.parts.forEach((part: DiagramPart) => {
|
||||
@@ -618,7 +797,11 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
selectedComponentData,
|
||||
selectedComponentConfig,
|
||||
componentRefs,
|
||||
|
||||
|
||||
// Canvas控制状态
|
||||
canvasPosition,
|
||||
canvasScale,
|
||||
|
||||
// 方法
|
||||
loadComponentModule,
|
||||
preloadComponentModules,
|
||||
@@ -634,13 +817,22 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
getComponentRef,
|
||||
getDiagramData,
|
||||
updateDiagramData,
|
||||
getCanvasInfo,
|
||||
showToast,
|
||||
getComponentDefinition,
|
||||
prepareComponentProps,
|
||||
initialize,
|
||||
|
||||
// Canvas控制方法
|
||||
setCanvasPosition,
|
||||
updateCanvasPosition,
|
||||
setCanvasScale,
|
||||
getCanvasPosition,
|
||||
getCanvasScale,
|
||||
zoomAtPosition,
|
||||
screenToCanvas,
|
||||
canvasToScreen,
|
||||
centerView,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export { useProvideComponentManager, useComponentManager };
|
||||
|
||||
@@ -123,14 +123,6 @@ export function saveDiagramData(data: DiagramData): void {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加新组件到图表数据
|
||||
export function addPart(data: DiagramData, part: DiagramPart): DiagramData {
|
||||
return {
|
||||
...data,
|
||||
parts: [...data.parts, part]
|
||||
};
|
||||
}
|
||||
|
||||
// 更新组件位置
|
||||
export function updatePartPosition(
|
||||
data: DiagramData,
|
||||
@@ -171,42 +163,6 @@ export function updatePartAttribute(
|
||||
};
|
||||
}
|
||||
|
||||
// 删除组件及同组组件
|
||||
export function deletePart(data: DiagramData, partId: string): DiagramData {
|
||||
// 首先找到要删除的组件
|
||||
const component = data.parts.find(part => part.id === partId);
|
||||
if (!component) return data;
|
||||
|
||||
// 收集需要删除的组件ID列表
|
||||
const componentsToDelete: string[] = [partId];
|
||||
|
||||
// 如果组件属于一个组,则找出所有同组的组件
|
||||
if (component.group && component.group !== '') {
|
||||
const groupMembers = data.parts.filter(
|
||||
p => p.group === component.group && p.id !== partId
|
||||
);
|
||||
|
||||
// 将同组组件ID添加到删除列表
|
||||
componentsToDelete.push(...groupMembers.map(p => p.id));
|
||||
console.log(`删除组件 ${partId} 及其组 ${component.group} 中的 ${groupMembers.length} 个组件`);
|
||||
}
|
||||
|
||||
return {
|
||||
...data,
|
||||
// 删除所有标记的组件
|
||||
parts: data.parts.filter(part => !componentsToDelete.includes(part.id)),
|
||||
// 删除与这些组件相关的所有连接
|
||||
connections: data.connections.filter(conn => {
|
||||
const [startPin, endPin] = conn;
|
||||
const startCompId = startPin.split(':')[0];
|
||||
const endCompId = endPin.split(':')[0];
|
||||
|
||||
// 检查连接两端的组件是否在删除列表中
|
||||
return !componentsToDelete.includes(startCompId) && !componentsToDelete.includes(endCompId);
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
// 添加连接
|
||||
export function addConnection(
|
||||
data: DiagramData,
|
||||
@@ -256,25 +212,6 @@ export function findConnectionsByPart(
|
||||
});
|
||||
}
|
||||
|
||||
// 基于组的移动相关组件
|
||||
export function moveGroupComponents(
|
||||
data: DiagramData,
|
||||
groupId: string,
|
||||
deltaX: number,
|
||||
deltaY: number
|
||||
): DiagramData {
|
||||
if (!groupId) return data;
|
||||
|
||||
return {
|
||||
...data,
|
||||
parts: data.parts.map(part =>
|
||||
part.group === groupId
|
||||
? { ...part, x: part.x + deltaX, y: part.y + deltaY }
|
||||
: part
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
// 添加验证diagram.json文件的函数
|
||||
export function validateDiagramData(data: any): { isValid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
@@ -98,7 +98,6 @@ const props = defineProps<{
|
||||
const propertySectionExpanded = ref(false); // 基本属性区域默认展开
|
||||
const pinsSectionExpanded = ref(false); // 引脚配置区域默认折叠
|
||||
const componentCapsExpanded = ref(true); // 组件功能区域默认展开
|
||||
const wireSectionExpanded = ref(false); // 连线管理区域默认折叠
|
||||
|
||||
const componentCaps = useTemplateRef("ComponentCapabilities");
|
||||
|
||||
|
||||
@@ -32,6 +32,16 @@
|
||||
<SquareActivityIcon class="icon" />
|
||||
示波器
|
||||
</label>
|
||||
<label class="tab">
|
||||
<input
|
||||
type="radio"
|
||||
name="function-bar"
|
||||
id="4"
|
||||
@change="handleTabChange"
|
||||
/>
|
||||
<Zap class="icon" />
|
||||
逻辑分析仪
|
||||
</label>
|
||||
<!-- 全屏按钮 -->
|
||||
<button
|
||||
class="fullscreen-btn ml-auto btn btn-ghost btn-sm"
|
||||
@@ -51,6 +61,9 @@
|
||||
<div v-else-if="checkID === 3" class="h-full overflow-y-auto">
|
||||
<OscilloscopeView />
|
||||
</div>
|
||||
<div v-else-if="checkID === 4" class="h-full overflow-y-auto">
|
||||
<LogicAnalyzerView />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -62,13 +75,16 @@ import {
|
||||
TerminalIcon,
|
||||
MaximizeIcon,
|
||||
MinimizeIcon,
|
||||
Zap,
|
||||
} from "lucide-vue-next";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import VideoStreamView from "@/views/Project/VideoStream.vue";
|
||||
import OscilloscopeView from "@/views/Project/Oscilloscope.vue";
|
||||
import LogicAnalyzerView from "@/views/Project/LogicAnalyzer.vue";
|
||||
import { isNull, toNumber } from "lodash";
|
||||
import { ref, watch } from "vue";
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
|
||||
const checkID = ref(1);
|
||||
const checkID = useLocalStorage("checkID", 1);
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
<DiagramCanvas
|
||||
ref="diagramCanvas"
|
||||
:showDocPanel="showDocPanel"
|
||||
@diagram-updated="handleDiagramUpdated"
|
||||
@open-components="openComponentsMenu"
|
||||
@toggle-doc-panel="toggleDocPanel"
|
||||
/>
|
||||
@@ -92,8 +91,6 @@
|
||||
<ComponentSelector
|
||||
:open="showComponentsMenu"
|
||||
@update:open="showComponentsMenu = $event"
|
||||
@add-component="handleAddComponent"
|
||||
@add-template="handleAddTemplate"
|
||||
@close="showComponentsMenu = false"
|
||||
/>
|
||||
|
||||
@@ -159,7 +156,10 @@ function handleHorizontalSplitterResize(sizes: number[]) {
|
||||
|
||||
function handleVerticalSplitterResize(sizes: number[]) {
|
||||
if (sizes && sizes.length > 0) {
|
||||
verticalSplitterSize.value = sizes[0];
|
||||
// 只在非全屏状态下保存分栏大小,避免全屏时的100%被保存
|
||||
if (!isBottomBarFullscreen.value) {
|
||||
verticalSplitterSize.value = sizes[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,32 +219,6 @@ function openComponentsMenu() {
|
||||
showComponentsMenu.value = true;
|
||||
}
|
||||
|
||||
// 处理 ComponentSelector 组件添加元器件事件
|
||||
async function handleAddComponent(componentData: {
|
||||
type: string;
|
||||
name: string;
|
||||
props: Record<string, any>;
|
||||
}) {
|
||||
await componentManager.addComponent(componentData);
|
||||
}
|
||||
|
||||
// 处理模板添加事件
|
||||
async function handleAddTemplate(templateData: {
|
||||
id: string;
|
||||
name: string;
|
||||
template: any;
|
||||
}) {
|
||||
const result = await componentManager.addTemplate(templateData);
|
||||
if (result) {
|
||||
alert?.show(result.message, result.success ? "success" : "error");
|
||||
}
|
||||
}
|
||||
|
||||
// 处理图表数据更新事件
|
||||
function handleDiagramUpdated(data: DiagramData) {
|
||||
console.log("Diagram data updated:", data);
|
||||
}
|
||||
|
||||
// 更新组件属性的方法 - 委托给componentManager
|
||||
function updateComponentProp(
|
||||
componentId: string,
|
||||
|
||||
43
src/views/Project/LogicAnalyzer.vue
Normal file
43
src/views/Project/LogicAnalyzer.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="bg-base-100 flex flex-col">
|
||||
<!-- 逻辑信号展示 -->
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">
|
||||
<Zap class="w-5 h-5" />
|
||||
逻辑信号分析
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 触发设置 -->
|
||||
<div class="card bg-base-200 shadow-xl mt-4">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">
|
||||
<Settings class="w-5 h-5" />
|
||||
触发设置
|
||||
</h2>
|
||||
<TriggerSettings />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 通道配置 -->
|
||||
<div class="card bg-base-200 shadow-xl mt-4">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">
|
||||
<Layers class="w-5 h-5" />
|
||||
通道配置
|
||||
</h2>
|
||||
<ChannelConfig />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Zap, Settings, Layers } from "lucide-vue-next";
|
||||
import { useEquipments } from "@/stores/equipments";
|
||||
|
||||
// 使用全局设备配置
|
||||
const equipments = useEquipments();
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="open"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-opacity-30 backdrop-blur-sm shadow-2xl"
|
||||
>
|
||||
<div class="bg-base-100 rounded-lg shadow-xl max-w-md w-full mx-4">
|
||||
<div class="p-6">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
控制面板
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<!-- 服务状态 -->
|
||||
<div class="stats shadow">
|
||||
<div class="stat bg-base-100">
|
||||
@@ -42,6 +42,38 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分辨率控制 -->
|
||||
<div class="stats shadow">
|
||||
<div class="stat bg-base-100">
|
||||
<div class="stat-figure text-info">
|
||||
<Settings class="w-8 h-8" />
|
||||
</div>
|
||||
<div class="stat-title">分辨率设置</div>
|
||||
<div class="stat-value text-sm">
|
||||
<select
|
||||
class="select select-sm select-bordered max-w-xs"
|
||||
v-model="selectedResolution"
|
||||
@change="changeResolution"
|
||||
:disabled="changingResolution"
|
||||
>
|
||||
<option v-for="res in supportedResolutions" :key="`${res.width}x${res.height}`" :value="res">
|
||||
{{ res.width }}×{{ res.height }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="stat-desc">
|
||||
<button
|
||||
class="btn btn-xs btn-outline btn-info mt-1"
|
||||
@click="refreshResolutions"
|
||||
:disabled="loadingResolutions"
|
||||
>
|
||||
<RefreshCw v-if="loadingResolutions" class="animate-spin h-3 w-3" />
|
||||
{{ loadingResolutions ? "刷新中..." : "刷新" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 连接数 -->
|
||||
<div class="stats shadow">
|
||||
<div class="stat bg-base-100 relative">
|
||||
@@ -308,7 +340,7 @@ import {
|
||||
AlertTriangle,
|
||||
MoreHorizontal,
|
||||
} from "lucide-vue-next";
|
||||
import { VideoStreamClient, CameraConfigRequest } from "@/APIClient";
|
||||
import { VideoStreamClient, CameraConfigRequest, ResolutionConfigRequest } from "@/APIClient";
|
||||
import { useEquipments } from "@/stores/equipments";
|
||||
|
||||
const eqps = useEquipments();
|
||||
@@ -321,6 +353,15 @@ const isPlaying = ref(false);
|
||||
const hasVideoError = ref(false);
|
||||
const videoStatus = ref('点击"播放视频流"按钮开始查看实时视频');
|
||||
|
||||
// 分辨率相关状态
|
||||
const changingResolution = ref(false);
|
||||
const loadingResolutions = ref(false);
|
||||
const selectedResolution = ref({ width: 640, height: 480 });
|
||||
const supportedResolutions = ref([
|
||||
{ width: 640, height: 480 },
|
||||
{ width: 1280, height: 720 }
|
||||
]);
|
||||
|
||||
// 数据
|
||||
const statusInfo = ref({
|
||||
isRunning: false,
|
||||
@@ -549,6 +590,74 @@ const startStream = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 分辨率相关方法
|
||||
// 获取支持的分辨率列表
|
||||
const refreshResolutions = async () => {
|
||||
loadingResolutions.value = true;
|
||||
try {
|
||||
addLog("info", "正在获取支持的分辨率列表...");
|
||||
const resolutions = await videoClient.getSupportedResolutions();
|
||||
supportedResolutions.value = resolutions.resolutions;
|
||||
console.log("支持的分辨率列表:", supportedResolutions.value);
|
||||
|
||||
// 获取当前分辨率
|
||||
const currentRes = await videoClient.getCurrentResolution();
|
||||
selectedResolution.value = currentRes;
|
||||
|
||||
addLog("success", "分辨率列表获取成功");
|
||||
} catch (error) {
|
||||
addLog("error", `获取分辨率列表失败: ${error}`);
|
||||
console.error("获取分辨率列表失败:", error);
|
||||
} finally {
|
||||
loadingResolutions.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 切换分辨率
|
||||
const changeResolution = async () => {
|
||||
if (!selectedResolution.value) return;
|
||||
|
||||
changingResolution.value = true;
|
||||
const wasPlaying = isPlaying.value;
|
||||
|
||||
try {
|
||||
addLog("info", `正在切换分辨率到 ${selectedResolution.value.width}×${selectedResolution.value.height}...`);
|
||||
|
||||
// 如果正在播放,先停止视频流
|
||||
if (wasPlaying) {
|
||||
stopStream();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
|
||||
}
|
||||
|
||||
// 设置新分辨率
|
||||
const resolutionRequest = new ResolutionConfigRequest({
|
||||
width: selectedResolution.value.width,
|
||||
height: selectedResolution.value.height
|
||||
});
|
||||
const success = await videoClient.setResolution(resolutionRequest);
|
||||
|
||||
if (success) {
|
||||
// 刷新流信息
|
||||
await refreshStatus();
|
||||
|
||||
// 如果之前在播放,重新启动视频流
|
||||
if (wasPlaying) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500)); // 短暂延迟
|
||||
await startStream();
|
||||
}
|
||||
|
||||
addLog("success", `分辨率已切换到 ${selectedResolution.value.width}×${selectedResolution.value.height}`);
|
||||
} else {
|
||||
addLog("error", "分辨率切换失败");
|
||||
}
|
||||
} catch (error) {
|
||||
addLog("error", `分辨率切换失败: ${error}`);
|
||||
console.error("分辨率切换失败:", error);
|
||||
} finally {
|
||||
changingResolution.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 停止视频流
|
||||
const stopStream = () => {
|
||||
try {
|
||||
@@ -574,6 +683,7 @@ const stopStream = () => {
|
||||
onMounted(async () => {
|
||||
addLog("info", "HTTP 视频流页面已加载");
|
||||
await refreshStatus();
|
||||
await refreshResolutions(); // 初始化分辨率信息
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
Reference in New Issue
Block a user