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; }
|
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>
|
/// <summary>
|
||||||
/// 初始化HTTP视频流控制器
|
/// 初始化HTTP视频流控制器
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -233,4 +253,198 @@ public class VideoStreamController : ControllerBase
|
|||||||
return TypedResults.Ok(false);
|
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 HttpListener? _httpListener;
|
||||||
private readonly int _serverPort = 8080;
|
private readonly int _serverPort = 8080;
|
||||||
private readonly int _frameRate = 30; // 30 FPS
|
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;
|
private Camera? _camera;
|
||||||
@@ -154,6 +157,7 @@ public class HttpVideoStreamService : BackgroundService
|
|||||||
}
|
}
|
||||||
_cameraEnable = isEnabled;
|
_cameraEnable = isEnabled;
|
||||||
await _camera.EnableCamera(_cameraEnable);
|
await _camera.EnableCamera(_cameraEnable);
|
||||||
|
await _camera.SleepCameraHardware(!_cameraEnable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -439,8 +443,16 @@ public class HttpVideoStreamService : BackgroundService
|
|||||||
// 获取当前帧
|
// 获取当前帧
|
||||||
var imageData = await GetFPGAImageData();
|
var imageData = await GetFPGAImageData();
|
||||||
|
|
||||||
|
// 获取当前分辨率
|
||||||
|
int currentWidth, currentHeight;
|
||||||
|
lock (_resolutionLock)
|
||||||
|
{
|
||||||
|
currentWidth = _frameWidth;
|
||||||
|
currentHeight = _frameHeight;
|
||||||
|
}
|
||||||
|
|
||||||
// 直接使用Common.Image.ConvertRGB24ToJpeg进行转换
|
// 直接使用Common.Image.ConvertRGB24ToJpeg进行转换
|
||||||
var jpegResult = Common.Image.ConvertRGB24ToJpeg(imageData, _frameWidth, _frameHeight, 80);
|
var jpegResult = Common.Image.ConvertRGB24ToJpeg(imageData, currentWidth, currentHeight, 80);
|
||||||
if (!jpegResult.IsSuccessful)
|
if (!jpegResult.IsSuccessful)
|
||||||
{
|
{
|
||||||
logger.Error("RGB24转JPEG失败: {Error}", jpegResult.Error);
|
logger.Error("RGB24转JPEG失败: {Error}", jpegResult.Error);
|
||||||
@@ -647,6 +659,14 @@ public class HttpVideoStreamService : BackgroundService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// 获取当前分辨率
|
||||||
|
int currentWidth, currentHeight;
|
||||||
|
lock (_resolutionLock)
|
||||||
|
{
|
||||||
|
currentWidth = _frameWidth;
|
||||||
|
currentHeight = _frameHeight;
|
||||||
|
}
|
||||||
|
|
||||||
// 从摄像头读取帧数据
|
// 从摄像头读取帧数据
|
||||||
var readStartTime = DateTime.UtcNow;
|
var readStartTime = DateTime.UtcNow;
|
||||||
var result = await currentCamera.ReadFrame();
|
var result = await currentCamera.ReadFrame();
|
||||||
@@ -662,15 +682,15 @@ public class HttpVideoStreamService : BackgroundService
|
|||||||
var rgb565Data = result.Value;
|
var rgb565Data = result.Value;
|
||||||
|
|
||||||
// 验证数据长度是否正确
|
// 验证数据长度是否正确
|
||||||
if (!Common.Image.ValidateImageDataLength(rgb565Data, _frameWidth, _frameHeight, 2))
|
if (!Common.Image.ValidateImageDataLength(rgb565Data, currentWidth, currentHeight, 2))
|
||||||
{
|
{
|
||||||
logger.Warn("摄像头数据长度不匹配,期望: {Expected}, 实际: {Actual}",
|
logger.Warn("摄像头数据长度不匹配,期望: {Expected}, 实际: {Actual}",
|
||||||
_frameWidth * _frameHeight * 2, rgb565Data.Length);
|
currentWidth * currentHeight * 2, rgb565Data.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将 RGB565 转换为 RGB24
|
// 将 RGB565 转换为 RGB24
|
||||||
var convertStartTime = DateTime.UtcNow;
|
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 convertEndTime = DateTime.UtcNow;
|
||||||
var convertTime = (convertEndTime - convertStartTime).TotalMilliseconds;
|
var convertTime = (convertEndTime - convertStartTime).TotalMilliseconds;
|
||||||
|
|
||||||
@@ -708,8 +728,16 @@ public class HttpVideoStreamService : BackgroundService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取当前分辨率
|
||||||
|
int currentWidth, currentHeight;
|
||||||
|
lock (_resolutionLock)
|
||||||
|
{
|
||||||
|
currentWidth = _frameWidth;
|
||||||
|
currentHeight = _frameHeight;
|
||||||
|
}
|
||||||
|
|
||||||
// 直接使用Common.Image.ConvertRGB24ToJpeg进行转换
|
// 直接使用Common.Image.ConvertRGB24ToJpeg进行转换
|
||||||
var jpegResult = Common.Image.ConvertRGB24ToJpeg(frameData, _frameWidth, _frameHeight, 80);
|
var jpegResult = Common.Image.ConvertRGB24ToJpeg(frameData, currentWidth, currentHeight, 80);
|
||||||
if (!jpegResult.IsSuccessful)
|
if (!jpegResult.IsSuccessful)
|
||||||
{
|
{
|
||||||
logger.Error("RGB24转JPEG失败: {Error}", jpegResult.Error);
|
logger.Error("RGB24转JPEG失败: {Error}", jpegResult.Error);
|
||||||
@@ -904,4 +932,196 @@ public class HttpVideoStreamService : BackgroundService
|
|||||||
|
|
||||||
base.Dispose();
|
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); });
|
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>
|
/// <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>
|
/// <summary>
|
||||||
/// 向设备地址写入32位数据
|
/// 向设备地址写入32位数据
|
||||||
/// </summary>
|
/// </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>
|
||||||
/// 获取还未被读取的数据列表
|
/// 获取还未被读取的数据列表
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="ipAddr">IP地址</param>
|
/// <param name="ipAddr">IP地址</param>
|
||||||
/// <param name="taskID">[TODO:parameter]</param>
|
/// <param name="taskID">任务ID</param>
|
||||||
/// <param name="timeout">超时时间</param>
|
/// <param name="timeout">超时时间</param>
|
||||||
/// <param name="cycle">延迟时间</param>
|
|
||||||
/// <returns>数据列表</returns>
|
/// <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;
|
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>
|
||||||
/// 异步等待写响应
|
/// 异步等待写响应
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -280,28 +367,32 @@ public class UDPServer
|
|||||||
|
|
||||||
return retPack.Value;
|
return retPack.Value;
|
||||||
}
|
}
|
||||||
|
static int ReceiveHandleCcount = 0;
|
||||||
|
|
||||||
private void ReceiveHandler(IAsyncResult res)
|
private void ReceiveHandler(IAsyncResult res)
|
||||||
{
|
{
|
||||||
logger.Trace("Enter handler");
|
|
||||||
var remoteEP = new IPEndPoint(IPAddress.Any, listenPort);
|
var remoteEP = new IPEndPoint(IPAddress.Any, listenPort);
|
||||||
byte[] bytes = listener.EndReceive(res, ref remoteEP);
|
byte[] bytes = listener.EndReceive(res, ref remoteEP);
|
||||||
|
|
||||||
|
// 提前开始接收下一个包
|
||||||
|
listener.BeginReceive(new AsyncCallback(ReceiveHandler), null);
|
||||||
|
logger.Debug($"Test ReceiveHandler Count = {ReceiveHandleCcount}");
|
||||||
|
ReceiveHandleCcount++;
|
||||||
|
|
||||||
// Handle RemoteEP
|
// Handle RemoteEP
|
||||||
if (remoteEP is null)
|
if (remoteEP is null)
|
||||||
{
|
{
|
||||||
// logger.Debug($"Receive Data from Unknown at {DateTime.Now.ToString()}:");
|
logger.Debug($"Receive Data from Unknown at {DateTime.Now.ToString()}:");
|
||||||
// logger.Debug($" Original Data : {BitConverter.ToString(bytes).Replace("-", " ")}");
|
logger.Debug($" Original Data : {BitConverter.ToString(bytes).Replace("-", " ")}");
|
||||||
goto BEGIN_RECEIVE;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 异步处理数据包
|
||||||
// Handle Package
|
Task.Run(() =>
|
||||||
|
{
|
||||||
var udpData = RecordUDPData(bytes, remoteEP, Convert.ToInt32(bytes[1]));
|
var udpData = RecordUDPData(bytes, remoteEP, Convert.ToInt32(bytes[1]));
|
||||||
PrintData(udpData);
|
PrintData(udpData);
|
||||||
|
});
|
||||||
BEGIN_RECEIVE:
|
|
||||||
listener.BeginReceive(new AsyncCallback(ReceiveHandler), null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool SendBytes(IPEndPoint endPoint, byte[] buf)
|
private bool SendBytes(IPEndPoint endPoint, byte[] buf)
|
||||||
@@ -342,7 +433,7 @@ public class UDPServer
|
|||||||
udpData.TryGetValue($"{remoteAddress}-{taskID}", out var dataQueue))
|
udpData.TryGetValue($"{remoteAddress}-{taskID}", out var dataQueue))
|
||||||
{
|
{
|
||||||
dataQueue.Enqueue(data);
|
dataQueue.Enqueue(data);
|
||||||
logger.Trace("Receive data from old client");
|
logger.Debug($"Test Lock dataQueue.Count = {dataQueue.Count}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
200
src/APIClient.ts
200
src/APIClient.ts
@@ -311,6 +311,160 @@ export class VideoStreamClient {
|
|||||||
}
|
}
|
||||||
return Promise.resolve<boolean>(null as any);
|
return Promise.resolve<boolean>(null as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置视频流分辨率
|
||||||
|
* @param request 分辨率配置请求
|
||||||
|
* @return 设置结果
|
||||||
|
*/
|
||||||
|
setResolution(request: ResolutionConfigRequest): Promise<any> {
|
||||||
|
let url_ = this.baseUrl + "/api/VideoStream/Resolution";
|
||||||
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
const content_ = JSON.stringify(request);
|
||||||
|
|
||||||
|
let options_: RequestInit = {
|
||||||
|
body: content_,
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||||
|
return this.processSetResolution(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processSetResolution(response: Response): Promise<any> {
|
||||||
|
const status = response.status;
|
||||||
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
|
if (status === 200) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
let result200: any = null;
|
||||||
|
let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
|
||||||
|
result200 = resultData200 !== undefined ? resultData200 : <any>null;
|
||||||
|
|
||||||
|
return result200;
|
||||||
|
});
|
||||||
|
} else if (status === 400) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
let result400: any = null;
|
||||||
|
let resultData400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
|
||||||
|
result400 = resultData400 !== undefined ? resultData400 : <any>null;
|
||||||
|
|
||||||
|
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
|
||||||
|
});
|
||||||
|
} else if (status === 500) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
let result500: any = null;
|
||||||
|
let resultData500 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
|
||||||
|
result500 = resultData500 !== undefined ? resultData500 : <any>null;
|
||||||
|
|
||||||
|
return throwException("A server side error occurred.", status, _responseText, _headers, result500);
|
||||||
|
});
|
||||||
|
} else if (status !== 200 && status !== 204) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve<any>(null as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前分辨率
|
||||||
|
* @return 当前分辨率信息
|
||||||
|
*/
|
||||||
|
getCurrentResolution(): Promise<any> {
|
||||||
|
let url_ = this.baseUrl + "/api/VideoStream/Resolution";
|
||||||
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
let options_: RequestInit = {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||||
|
return this.processGetCurrentResolution(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processGetCurrentResolution(response: Response): Promise<any> {
|
||||||
|
const status = response.status;
|
||||||
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
|
if (status === 200) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
let result200: any = null;
|
||||||
|
let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
|
||||||
|
result200 = resultData200 !== undefined ? resultData200 : <any>null;
|
||||||
|
|
||||||
|
return result200;
|
||||||
|
});
|
||||||
|
} else if (status === 500) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
let result500: any = null;
|
||||||
|
let resultData500 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
|
||||||
|
result500 = resultData500 !== undefined ? resultData500 : <any>null;
|
||||||
|
|
||||||
|
return throwException("A server side error occurred.", status, _responseText, _headers, result500);
|
||||||
|
});
|
||||||
|
} else if (status !== 200 && status !== 204) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve<any>(null as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支持的分辨率列表
|
||||||
|
* @return 支持的分辨率列表
|
||||||
|
*/
|
||||||
|
getSupportedResolutions(): Promise<any> {
|
||||||
|
let url_ = this.baseUrl + "/api/VideoStream/SupportedResolutions";
|
||||||
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
let options_: RequestInit = {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||||
|
return this.processGetSupportedResolutions(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processGetSupportedResolutions(response: Response): Promise<any> {
|
||||||
|
const status = response.status;
|
||||||
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
|
if (status === 200) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
let result200: any = null;
|
||||||
|
let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
|
||||||
|
result200 = resultData200 !== undefined ? resultData200 : <any>null;
|
||||||
|
|
||||||
|
return result200;
|
||||||
|
});
|
||||||
|
} else if (status === 500) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
let result500: any = null;
|
||||||
|
let resultData500 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
|
||||||
|
result500 = resultData500 !== undefined ? resultData500 : <any>null;
|
||||||
|
|
||||||
|
return throwException("A server side error occurred.", status, _responseText, _headers, result500);
|
||||||
|
});
|
||||||
|
} else if (status !== 200 && status !== 204) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve<any>(null as any);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BsdlParserClient {
|
export class BsdlParserClient {
|
||||||
@@ -2787,6 +2941,52 @@ export interface ICameraConfigRequest {
|
|||||||
port: number;
|
port: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 分辨率配置请求模型 */
|
||||||
|
export class ResolutionConfigRequest implements IResolutionConfigRequest {
|
||||||
|
/** 宽度 */
|
||||||
|
width!: number;
|
||||||
|
/** 高度 */
|
||||||
|
height!: number;
|
||||||
|
|
||||||
|
constructor(data?: IResolutionConfigRequest) {
|
||||||
|
if (data) {
|
||||||
|
for (var property in data) {
|
||||||
|
if (data.hasOwnProperty(property))
|
||||||
|
(<any>this)[property] = (<any>data)[property];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_data?: any) {
|
||||||
|
if (_data) {
|
||||||
|
this.width = _data["width"];
|
||||||
|
this.height = _data["height"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJS(data: any): ResolutionConfigRequest {
|
||||||
|
data = typeof data === 'object' ? data : {};
|
||||||
|
let result = new ResolutionConfigRequest();
|
||||||
|
result.init(data);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(data?: any) {
|
||||||
|
data = typeof data === 'object' ? data : {};
|
||||||
|
data["width"] = this.width;
|
||||||
|
data["height"] = this.height;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分辨率配置请求模型 */
|
||||||
|
export interface IResolutionConfigRequest {
|
||||||
|
/** 宽度 */
|
||||||
|
width: number;
|
||||||
|
/** 高度 */
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class ProblemDetails implements IProblemDetails {
|
export class ProblemDetails implements IProblemDetails {
|
||||||
type?: string | undefined;
|
type?: string | undefined;
|
||||||
title?: string | undefined;
|
title?: string | undefined;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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
|
<transition
|
||||||
name="alert"
|
name="alert"
|
||||||
enter-active-class="alert-enter-active"
|
enter-active-class="alert-enter-active"
|
||||||
|
|||||||
@@ -25,13 +25,12 @@
|
|||||||
<Plus :size="20" class="text-primary" />
|
<Plus :size="20" class="text-primary" />
|
||||||
添加元器件
|
添加元器件
|
||||||
</h3>
|
</h3>
|
||||||
<label
|
<button
|
||||||
for="component-drawer"
|
|
||||||
class="btn btn-ghost btn-sm btn-circle"
|
class="btn btn-ghost btn-sm btn-circle"
|
||||||
@click="closeMenu"
|
@click="closeMenu"
|
||||||
>
|
>
|
||||||
<X :size="20" />
|
<X :size="20" />
|
||||||
</label>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 导航栏 -->
|
<!-- 导航栏 -->
|
||||||
@@ -112,8 +111,8 @@
|
|||||||
import { ref, computed, shallowRef, onMounted } from "vue";
|
import { ref, computed, shallowRef, onMounted } from "vue";
|
||||||
import { Plus, X, Search } from "lucide-vue-next";
|
import { Plus, X, Search } from "lucide-vue-next";
|
||||||
import ItemList from "./ItemList.vue";
|
import ItemList from "./ItemList.vue";
|
||||||
|
import { useAlertStore } from "@/components/Alert";
|
||||||
import {
|
import {
|
||||||
useComponentManager,
|
|
||||||
availableComponents,
|
availableComponents,
|
||||||
availableVirtualDevices,
|
availableVirtualDevices,
|
||||||
availableTemplates,
|
availableTemplates,
|
||||||
@@ -121,6 +120,7 @@ import {
|
|||||||
type ComponentConfig,
|
type ComponentConfig,
|
||||||
type VirtualDeviceConfig,
|
type VirtualDeviceConfig,
|
||||||
type TemplateConfig,
|
type TemplateConfig,
|
||||||
|
useComponentManager, // 导入 componentManager
|
||||||
} from "./index.ts";
|
} from "./index.ts";
|
||||||
|
|
||||||
// Props 定义
|
// Props 定义
|
||||||
@@ -130,16 +130,18 @@ interface Props {
|
|||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
const componentManager = useComponentManager();
|
// 定义组件发出的事件(保留部分必要的事件)
|
||||||
|
|
||||||
// 定义组件发出的事件
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
"close",
|
"close",
|
||||||
"add-component",
|
|
||||||
"add-template",
|
|
||||||
"update:open",
|
"update:open",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 使用 componentManager
|
||||||
|
const componentManager = useComponentManager();
|
||||||
|
|
||||||
|
// 使用 Alert 系统
|
||||||
|
const alert = useAlertStore();
|
||||||
|
|
||||||
// 当前激活的选项卡
|
// 当前激活的选项卡
|
||||||
const activeTab = ref("components");
|
const activeTab = ref("components");
|
||||||
|
|
||||||
@@ -192,14 +194,19 @@ async function preloadComponentModules() {
|
|||||||
|
|
||||||
// 关闭菜单
|
// 关闭菜单
|
||||||
function closeMenu() {
|
function closeMenu() {
|
||||||
showComponentsMenu.value = false;
|
emit("update:open", false);
|
||||||
emit("close");
|
emit("close");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加新元器件
|
// 添加新元器件 - 使用 componentManager
|
||||||
async function addComponent(
|
async function addComponent(
|
||||||
componentTemplate: ComponentConfig | VirtualDeviceConfig,
|
componentTemplate: ComponentConfig | VirtualDeviceConfig,
|
||||||
) {
|
) {
|
||||||
|
if (!componentManager) {
|
||||||
|
console.error("ComponentManager not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 先加载组件模块
|
// 先加载组件模块
|
||||||
const moduleRef = await loadComponentModule(componentTemplate.type);
|
const moduleRef = await loadComponentModule(componentTemplate.type);
|
||||||
let defaultProps: Record<string, any> = {};
|
let defaultProps: Record<string, any> = {};
|
||||||
@@ -220,19 +227,32 @@ async function addComponent(
|
|||||||
console.log(`Failed to load module for ${componentTemplate.type}`);
|
console.log(`Failed to load module for ${componentTemplate.type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送添加组件事件给父组件
|
try {
|
||||||
emit("add-component", {
|
// 使用 componentManager 添加组件
|
||||||
|
await componentManager.addComponent({
|
||||||
type: componentTemplate.type,
|
type: componentTemplate.type,
|
||||||
name: componentTemplate.name,
|
name: componentTemplate.name,
|
||||||
props: defaultProps,
|
props: defaultProps,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 显示成功消息
|
||||||
|
alert?.success(`成功添加元器件: ${componentTemplate.name}`);
|
||||||
|
|
||||||
// 关闭菜单
|
// 关闭菜单
|
||||||
closeMenu();
|
closeMenu();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("添加元器件失败:", error);
|
||||||
|
alert?.error("添加元器件失败,请检查控制台错误信息");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加模板
|
// 添加模板 - 使用 componentManager
|
||||||
async function addTemplate(template: TemplateConfig) {
|
async function addTemplate(template: TemplateConfig) {
|
||||||
|
if (!componentManager) {
|
||||||
|
console.error("ComponentManager not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 加载模板JSON文件
|
// 加载模板JSON文件
|
||||||
const response = await fetch(template.path);
|
const response = await fetch(template.path);
|
||||||
@@ -243,19 +263,27 @@ async function addTemplate(template: TemplateConfig) {
|
|||||||
const templateData = await response.json();
|
const templateData = await response.json();
|
||||||
console.log("加载模板:", templateData);
|
console.log("加载模板:", templateData);
|
||||||
|
|
||||||
// 发出事件,将模板数据传递给父组件
|
// 使用 componentManager 添加模板
|
||||||
emit("add-template", {
|
const result = await componentManager.addTemplate({
|
||||||
id: template.id,
|
id: template.id,
|
||||||
name: template.name,
|
name: template.name,
|
||||||
template: templateData,
|
template: templateData,
|
||||||
capsPage: template.capsPage,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
// 使用 Alert 显示结果消息
|
||||||
|
if (result.success) {
|
||||||
|
alert?.success(result.message);
|
||||||
|
} else {
|
||||||
|
alert?.error(result.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 关闭菜单
|
// 关闭菜单
|
||||||
closeMenu();
|
closeMenu();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("加载模板出错:", error);
|
console.error("加载模板出错:", error);
|
||||||
alert("无法加载模板文件,请检查控制台错误信息");
|
alert?.error("无法加载模板文件,请检查控制台错误信息");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<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"
|
ref="canvasContainer"
|
||||||
@mousedown="handleCanvasMouseDown"
|
@mousedown="handleCanvasMouseDown"
|
||||||
@mousedown.middle.prevent="startMiddleDrag"
|
@mousedown.middle.prevent="startMiddleDrag"
|
||||||
@@ -38,13 +38,17 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
class="diagram-canvas"
|
class="diagram-canvas relative select-none"
|
||||||
:style="{
|
: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
|
<WireComponent
|
||||||
v-for="(wire, index) in wireItems"
|
v-for="(wire, index) in wireItems"
|
||||||
@@ -83,11 +87,11 @@
|
|||||||
<div
|
<div
|
||||||
v-for="component in diagramParts"
|
v-for="component in diagramParts"
|
||||||
:key="component.id"
|
:key="component.id"
|
||||||
class="component-wrapper"
|
class="component-wrapper absolute p-0 inline-block overflow-visible select-none"
|
||||||
:class="{
|
:class="{
|
||||||
'component-hover': hoveredComponent === component.id,
|
'component-hover': hoveredComponent === component.id,
|
||||||
'component-selected': selectedComponentId === component.id,
|
'component-selected': selectedComponentId === component.id,
|
||||||
'component-disabled': !component.isOn,
|
'cursor-not-allowed grayscale-70 opacity-60': !component.isOn,
|
||||||
'component-hidepins': component.hidepins,
|
'component-hidepins': component.hidepins,
|
||||||
}"
|
}"
|
||||||
:style="{
|
:style="{
|
||||||
@@ -101,16 +105,8 @@
|
|||||||
display: 'block',
|
display: 'block',
|
||||||
}"
|
}"
|
||||||
@mousedown.left.stop="startComponentDrag($event, component)"
|
@mousedown.left.stop="startComponentDrag($event, component)"
|
||||||
@mouseover="
|
@mouseover="hoveredComponent = component.id"
|
||||||
(event) => {
|
@mouseleave="hoveredComponent = null"
|
||||||
hoveredComponent = component.id;
|
|
||||||
}
|
|
||||||
"
|
|
||||||
@mouseleave="
|
|
||||||
(event) => {
|
|
||||||
hoveredComponent = null;
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<!-- 动态渲染组件 -->
|
<!-- 动态渲染组件 -->
|
||||||
<component
|
<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"
|
class="absolute bottom-4 right-4 bg-base-100 px-3 py-1.5 rounded-md shadow-md z-20"
|
||||||
style="opacity: 0.9"
|
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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -193,8 +191,6 @@ import {
|
|||||||
saveDiagramData,
|
saveDiagramData,
|
||||||
updatePartPosition,
|
updatePartPosition,
|
||||||
updatePartAttribute,
|
updatePartAttribute,
|
||||||
deletePart,
|
|
||||||
deleteConnection,
|
|
||||||
parseConnectionPin,
|
parseConnectionPin,
|
||||||
connectionArrayToWireItem,
|
connectionArrayToWireItem,
|
||||||
validateDiagramData,
|
validateDiagramData,
|
||||||
@@ -216,13 +212,7 @@ function handleContextMenu(e: MouseEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 定义组件发出的事件
|
// 定义组件发出的事件
|
||||||
const emit = defineEmits([
|
const emit = defineEmits(["toggle-doc-panel", "open-components"]);
|
||||||
"diagram-updated",
|
|
||||||
"toggle-doc-panel",
|
|
||||||
"wire-created",
|
|
||||||
"wire-deleted",
|
|
||||||
"open-components",
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 定义组件接受的属性
|
// 定义组件接受的属性
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -243,8 +233,6 @@ const alertStore = useAlertStore();
|
|||||||
// --- 画布状态 ---
|
// --- 画布状态 ---
|
||||||
const canvasContainer = ref<HTMLElement | null>(null);
|
const canvasContainer = ref<HTMLElement | null>(null);
|
||||||
const canvas = 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 isDragging = ref(false);
|
||||||
const isMiddleDragging = ref(false);
|
const isMiddleDragging = ref(false);
|
||||||
const dragStart = reactive({ x: 0, y: 0 });
|
const dragStart = reactive({ x: 0, y: 0 });
|
||||||
@@ -265,11 +253,6 @@ const diagramData = ref<DiagramData>({
|
|||||||
connections: [],
|
connections: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 组件引用跟踪(保留以便向后兼容)
|
|
||||||
const componentRefs = computed(
|
|
||||||
() => componentManager?.componentRefs.value || {},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 计算属性:从 diagramData 中提取组件列表,并按index属性排序
|
// 计算属性:从 diagramData 中提取组件列表,并按index属性排序
|
||||||
const diagramParts = computed<DiagramPart[]>(() => {
|
const diagramParts = computed<DiagramPart[]>(() => {
|
||||||
// 克隆原始数组以避免直接修改原始数据
|
// 克隆原始数组以避免直接修改原始数据
|
||||||
@@ -385,7 +368,7 @@ const isWireCreationEventActive = ref(false);
|
|||||||
// 画布拖拽事件
|
// 画布拖拽事件
|
||||||
useEventListener(document, "mousemove", (e: MouseEvent) => {
|
useEventListener(document, "mousemove", (e: MouseEvent) => {
|
||||||
if (isDragEventActive.value) {
|
if (isDragEventActive.value) {
|
||||||
onDrag(e);
|
onCanvasDrag(e);
|
||||||
}
|
}
|
||||||
if (isComponentDragEventActive.value) {
|
if (isComponentDragEventActive.value) {
|
||||||
onComponentDrag(e);
|
onComponentDrag(e);
|
||||||
@@ -423,25 +406,13 @@ function onZoom(e: WheelEvent) {
|
|||||||
const mouseX = e.clientX - containerRect.left;
|
const mouseX = e.clientX - containerRect.left;
|
||||||
const mouseY = e.clientY - containerRect.top;
|
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 zoomFactor = 1.1; // 每次放大/缩小10%
|
||||||
const direction = e.deltaY > 0 ? -1 : 1;
|
const direction = e.deltaY > 0 ? -1 : 1;
|
||||||
|
const finalZoomFactor = direction > 0 ? zoomFactor : 1 / zoomFactor;
|
||||||
|
|
||||||
// 计算新的缩放值
|
// 使用componentManager的缩放方法
|
||||||
let newScale =
|
componentManager?.zoomAtPosition(mouseX, mouseY, finalZoomFactor);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 画布交互逻辑 ---
|
// --- 画布交互逻辑 ---
|
||||||
@@ -472,8 +443,10 @@ function startDrag(e: MouseEvent) {
|
|||||||
|
|
||||||
isDragging.value = true;
|
isDragging.value = true;
|
||||||
isMiddleDragging.value = false;
|
isMiddleDragging.value = false;
|
||||||
dragStart.x = e.clientX - position.x;
|
const currentPosition = componentManager?.getCanvasPosition();
|
||||||
dragStart.y = e.clientY - position.y;
|
if (!currentPosition) return;
|
||||||
|
dragStart.x = e.clientX - currentPosition.x;
|
||||||
|
dragStart.y = e.clientY - currentPosition.y;
|
||||||
|
|
||||||
isDragEventActive.value = true;
|
isDragEventActive.value = true;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -487,19 +460,24 @@ function startMiddleDrag(e: MouseEvent) {
|
|||||||
isDragging.value = false;
|
isDragging.value = false;
|
||||||
draggingComponentId.value = null;
|
draggingComponentId.value = null;
|
||||||
|
|
||||||
dragStart.x = e.clientX - position.x;
|
const currentPosition = componentManager?.getCanvasPosition();
|
||||||
dragStart.y = e.clientY - position.y;
|
if (!currentPosition) return;
|
||||||
|
dragStart.x = e.clientX - currentPosition.x;
|
||||||
|
dragStart.y = e.clientY - currentPosition.y;
|
||||||
|
|
||||||
isDragEventActive.value = true;
|
isDragEventActive.value = true;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拖拽画布过程
|
// 拖拽画布过程
|
||||||
function onDrag(e: MouseEvent) {
|
function onCanvasDrag(e: MouseEvent) {
|
||||||
if (!isDragging.value && !isMiddleDragging.value) return;
|
if (!isDragging.value && !isMiddleDragging.value) return;
|
||||||
|
|
||||||
position.x = e.clientX - dragStart.x;
|
const newX = e.clientX - dragStart.x;
|
||||||
position.y = e.clientY - dragStart.y;
|
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;
|
if (!canvasContainer.value) return;
|
||||||
const containerRect = canvasContainer.value.getBoundingClientRect();
|
const containerRect = canvasContainer.value.getBoundingClientRect();
|
||||||
|
|
||||||
// 计算鼠标在画布坐标系中的位置
|
// 使用componentManager的屏幕坐标转画布坐标方法
|
||||||
const mouseX_canvas =
|
const mouseCanvasPos = componentManager?.screenToCanvas(
|
||||||
(e.clientX - containerRect.left - position.x) / scale.value;
|
e.clientX - containerRect.left,
|
||||||
const mouseY_canvas =
|
e.clientY - containerRect.top,
|
||||||
(e.clientY - containerRect.top - position.y) / scale.value;
|
);
|
||||||
|
if (!mouseCanvasPos) return;
|
||||||
|
|
||||||
// 计算鼠标相对于组件左上角的偏移量
|
// 计算鼠标相对于组件左上角的偏移量
|
||||||
componentDragOffset.x = mouseX_canvas - component.x;
|
componentDragOffset.x = mouseCanvasPos.x - component.x;
|
||||||
componentDragOffset.y = mouseY_canvas - component.y;
|
componentDragOffset.y = mouseCanvasPos.y - component.y;
|
||||||
|
|
||||||
// 激活组件拖拽事件监听
|
// 激活组件拖拽事件监听
|
||||||
isComponentDragEventActive.value = true;
|
isComponentDragEventActive.value = true;
|
||||||
@@ -575,15 +554,16 @@ function onComponentDrag(e: MouseEvent) {
|
|||||||
|
|
||||||
const containerRect = canvasContainer.value.getBoundingClientRect();
|
const containerRect = canvasContainer.value.getBoundingClientRect();
|
||||||
|
|
||||||
// 计算鼠标在画布坐标系中的位置
|
// 使用componentManager的屏幕坐标转画布坐标方法
|
||||||
const mouseX_canvas =
|
const mouseCanvasPos = componentManager?.screenToCanvas(
|
||||||
(e.clientX - containerRect.left - position.x) / scale.value;
|
e.clientX - containerRect.left,
|
||||||
const mouseY_canvas =
|
e.clientY - containerRect.top,
|
||||||
(e.clientY - containerRect.top - position.y) / scale.value;
|
);
|
||||||
|
if (!mouseCanvasPos) return;
|
||||||
|
|
||||||
// 计算组件新位置
|
// 计算组件新位置
|
||||||
const newX = mouseX_canvas - componentDragOffset.x;
|
const newX = mouseCanvasPos.x - componentDragOffset.x;
|
||||||
const newY = mouseY_canvas - componentDragOffset.y;
|
const newY = mouseCanvasPos.y - componentDragOffset.y;
|
||||||
|
|
||||||
// 获取当前拖动的组件
|
// 获取当前拖动的组件
|
||||||
const draggedComponent = diagramParts.value.find(
|
const draggedComponent = diagramParts.value.find(
|
||||||
@@ -600,8 +580,7 @@ function onComponentDrag(e: MouseEvent) {
|
|||||||
const groupComponents = diagramParts.value.filter(
|
const groupComponents = diagramParts.value.filter(
|
||||||
(p) =>
|
(p) =>
|
||||||
p.group === draggedComponent.group &&
|
p.group === draggedComponent.group &&
|
||||||
p.id !== draggingComponentId.value &&
|
p.id !== draggingComponentId.value,
|
||||||
!p.positionlock,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 更新这些组件的位置
|
// 更新这些组件的位置
|
||||||
@@ -623,25 +602,18 @@ function onComponentDrag(e: MouseEvent) {
|
|||||||
y: Math.round(newY),
|
y: Math.round(newY),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通知图表已更新
|
|
||||||
emit("diagram-updated", diagramData.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止拖拽组件
|
// 停止拖拽组件
|
||||||
function stopComponentDrag() {
|
function stopComponentDrag() {
|
||||||
// 如果有组件被拖拽,保存当前状态
|
// 如果有组件被拖拽,保存当前状态
|
||||||
if (draggingComponentId.value) {
|
if (draggingComponentId.value) {
|
||||||
// console.log(`组件拖拽结束: ${draggingComponentId.value}`);
|
|
||||||
|
|
||||||
// 保存图表数据
|
|
||||||
saveDiagramData(diagramData.value);
|
|
||||||
|
|
||||||
// 清除拖动状态
|
|
||||||
draggingComponentId.value = null;
|
draggingComponentId.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
isComponentDragEventActive.value = false;
|
isComponentDragEventActive.value = false;
|
||||||
|
|
||||||
|
saveDiagramData(diagramData.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新组件属性
|
// 更新组件属性
|
||||||
@@ -661,8 +633,6 @@ function updateComponentProp(
|
|||||||
propName,
|
propName,
|
||||||
value,
|
value,
|
||||||
);
|
);
|
||||||
emit("diagram-updated", diagramData.value);
|
|
||||||
saveDiagramData(diagramData.value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,9 +641,19 @@ function updateComponentProp(
|
|||||||
function updateMousePosition(e: MouseEvent) {
|
function updateMousePosition(e: MouseEvent) {
|
||||||
if (!canvasContainer.value) return;
|
if (!canvasContainer.value) return;
|
||||||
const containerRect = canvasContainer.value.getBoundingClientRect();
|
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) {
|
function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
|
||||||
if (!canvasContainer.value) return;
|
if (!canvasContainer.value) return;
|
||||||
updateMousePosition(event);
|
updateMousePosition(event);
|
||||||
@@ -830,13 +810,6 @@ function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
|
|||||||
connections: [...diagramData.value.connections, newConnection],
|
connections: [...diagramData.value.connections, newConnection],
|
||||||
};
|
};
|
||||||
|
|
||||||
// 通知连线创建
|
|
||||||
emit("wire-created", newConnection);
|
|
||||||
emit("diagram-updated", diagramData.value);
|
|
||||||
|
|
||||||
// 保存图表数据
|
|
||||||
saveDiagramData(diagramData.value);
|
|
||||||
|
|
||||||
// 重置连线创建状态
|
// 重置连线创建状态
|
||||||
resetWireCreation();
|
resetWireCreation();
|
||||||
isWireCreationEventActive.value = false;
|
isWireCreationEventActive.value = false;
|
||||||
@@ -880,23 +853,12 @@ function onCreatingWireMouseMove(e: MouseEvent) {
|
|||||||
updateMousePosition(e);
|
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) {
|
function deleteComponent(componentId: string) {
|
||||||
diagramData.value = deletePart(diagramData.value, componentId);
|
|
||||||
// 直接通过componentManager删除组件
|
// 直接通过componentManager删除组件
|
||||||
if (componentManager) {
|
if (componentManager) {
|
||||||
componentManager.deleteComponent(componentId);
|
componentManager.deleteComponent(componentId);
|
||||||
}
|
}
|
||||||
emit("diagram-updated", diagramData.value);
|
|
||||||
saveDiagramData(diagramData.value);
|
|
||||||
|
|
||||||
// 清除选中状态
|
// 清除选中状态
|
||||||
if (selectedComponentId.value === componentId) {
|
if (selectedComponentId.value === componentId) {
|
||||||
@@ -950,12 +912,6 @@ function handleFileSelected(event: Event) {
|
|||||||
// 更新画布数据
|
// 更新画布数据
|
||||||
diagramData.value = parsed as DiagramData;
|
diagramData.value = parsed as DiagramData;
|
||||||
|
|
||||||
// 保存到本地文件
|
|
||||||
saveDiagramData(diagramData.value);
|
|
||||||
|
|
||||||
// 发出更新事件
|
|
||||||
emit("diagram-updated", diagramData.value);
|
|
||||||
|
|
||||||
alertStore?.show(`成功导入diagram文件`, "success");
|
alertStore?.show(`成功导入diagram文件`, "success");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("解析JSON文件出错:", error);
|
console.error("解析JSON文件出错:", error);
|
||||||
@@ -1019,23 +975,6 @@ function exportDiagram() {
|
|||||||
|
|
||||||
// --- 生命周期钩子 ---
|
// --- 生命周期钩子 ---
|
||||||
onMounted(async () => {
|
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 {
|
try {
|
||||||
diagramData.value = await loadDiagramData();
|
diagramData.value = await loadDiagramData();
|
||||||
@@ -1060,11 +999,13 @@ onMounted(async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("加载图表数据失败:", error);
|
console.error("加载图表数据失败:", error);
|
||||||
}
|
}
|
||||||
// 初始化中心位置
|
|
||||||
if (canvasContainer.value) {
|
// 初始化中心位置 - 使用componentManager设置
|
||||||
|
if (canvasContainer.value && componentManager) {
|
||||||
// 修改为将画布中心点放在容器中心点
|
// 修改为将画布中心点放在容器中心点
|
||||||
position.x = canvasContainer.value.clientWidth / 2 - 5000; // 画布宽度的一半
|
const centerX = canvasContainer.value.clientWidth / 2 - 5000; // 画布宽度的一半
|
||||||
position.y = canvasContainer.value.clientHeight / 2 - 5000; // 画布高度的一半
|
const centerY = canvasContainer.value.clientHeight / 2 - 5000; // 画布高度的一半
|
||||||
|
componentManager.setCanvasPosition(centerX, centerY);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1091,10 +1032,6 @@ function updateDiagramDataDirectly(data: DiagramData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
diagramData.value = data;
|
diagramData.value = data;
|
||||||
saveDiagramData(data);
|
|
||||||
|
|
||||||
// 发出diagram-updated事件
|
|
||||||
emit("diagram-updated", data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 暴露方法给父组件
|
// 暴露方法给父组件
|
||||||
@@ -1102,95 +1039,31 @@ defineExpose({
|
|||||||
// 基本数据操作
|
// 基本数据操作
|
||||||
getDiagramData: () => diagramData.value,
|
getDiagramData: () => diagramData.value,
|
||||||
updateDiagramDataDirectly,
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 基础容器样式 - 使用 Tailwind 类替代 */
|
||||||
.diagram-container {
|
.diagram-container {
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
background-size:
|
background-size:
|
||||||
20px 20px,
|
20px 20px,
|
||||||
20px 20px,
|
20px 20px,
|
||||||
100px 100px,
|
100px 100px,
|
||||||
100px 100px;
|
100px 100px;
|
||||||
background-position: 0 0;
|
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 {
|
.diagram-canvas {
|
||||||
position: relative;
|
|
||||||
width: 10000px;
|
width: 10000px;
|
||||||
height: 10000px;
|
height: 10000px;
|
||||||
transform-origin: 0 0;
|
transform-origin: 0 0;
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 连线层样式 */
|
/* 连线层样式 */
|
||||||
.wires-layer {
|
.wires-layer {
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
/* 修复:允许线被点击 */
|
|
||||||
z-index: 50;
|
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
/* 确保超出SVG范围的内容也能显示 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wires-layer path {
|
.wires-layer path {
|
||||||
@@ -1198,66 +1071,28 @@ watch(
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 元器件容器样式 */
|
/* 组件容器样式 */
|
||||||
.component-wrapper {
|
.component-wrapper {
|
||||||
position: absolute;
|
|
||||||
padding: 0;
|
|
||||||
/* 移除内边距,确保元素大小与内容完全匹配 */
|
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
/* 使用content-box确保内容尺寸不受padding影响 */
|
|
||||||
display: inline-block;
|
|
||||||
overflow: visible;
|
|
||||||
/* 允许内容溢出(用于显示边框) */
|
|
||||||
cursor: move;
|
cursor: move;
|
||||||
/* 显示移动光标 */
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 悬停状态 - 使用outline而非伪元素 */
|
/* 悬停状态 */
|
||||||
.component-hover {
|
.component-hover {
|
||||||
outline: 2px dashed #3498db;
|
outline: 2px dashed #3498db;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
z-index: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 选中状态 - 使用outline而非伪元素 */
|
/* 选中状态 */
|
||||||
.component-selected {
|
.component-selected {
|
||||||
outline: 3px dashed;
|
outline: 3px dashed;
|
||||||
outline-color: #e74c3c #f39c12 #3498db #2ecc71;
|
outline-color: #e74c3c #f39c12 #3498db #2ecc71;
|
||||||
outline-offset: 3px;
|
outline-offset: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 禁用状态 */
|
/* SVG 交互样式 */
|
||||||
.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本身和特定交互元素 */
|
|
||||||
.component-wrapper :deep(svg) {
|
.component-wrapper :deep(svg) {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
/* 确保SVG本身可以接收鼠标事件用于拖拽 */
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-wrapper
|
.component-wrapper
|
||||||
@@ -1268,10 +1103,8 @@ watch(
|
|||||||
):not([fill-opacity])
|
):not([fill-opacity])
|
||||||
) {
|
) {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
/* 非交互元素不接收鼠标事件 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 允许特定SVG元素接收鼠标事件,用于交互 */
|
|
||||||
.component-wrapper :deep(svg circle[fill-opacity]),
|
.component-wrapper :deep(svg circle[fill-opacity]),
|
||||||
.component-wrapper :deep(svg rect[fill-opacity]),
|
.component-wrapper :deep(svg rect[fill-opacity]),
|
||||||
.component-wrapper :deep(svg rect[class*="glow"]),
|
.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 { 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 type { PropertyConfig } from "@/components/equipments/componentConfig";
|
||||||
import {
|
import {
|
||||||
generatePropertyConfigs,
|
generatePropertyConfigs,
|
||||||
@@ -24,10 +28,16 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
|||||||
// --- 状态管理 ---
|
// --- 状态管理 ---
|
||||||
const componentModules = ref<Record<string, ComponentModule>>({});
|
const componentModules = ref<Record<string, ComponentModule>>({});
|
||||||
const selectedComponentId = ref<string | null>(null);
|
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 diagramCanvas = ref<any>(null);
|
||||||
const componentRefs = ref<Record<string, any>>({});
|
const componentRefs = ref<Record<string, any>>({});
|
||||||
|
|
||||||
|
// 新增:直接管理canvas的位置和缩放
|
||||||
|
const canvasPosition = reactive({ x: 0, y: 0 });
|
||||||
|
const canvasScale = ref(1);
|
||||||
|
|
||||||
// 计算当前选中的组件数据
|
// 计算当前选中的组件数据
|
||||||
const selectedComponentData = computed(() => {
|
const selectedComponentData = computed(() => {
|
||||||
if (!diagramCanvas.value || !selectedComponentId.value) return null;
|
if (!diagramCanvas.value || !selectedComponentId.value) return null;
|
||||||
@@ -35,11 +45,130 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
|||||||
const canvasInstance = diagramCanvas.value as any;
|
const canvasInstance = diagramCanvas.value as any;
|
||||||
if (canvasInstance && canvasInstance.getDiagramData) {
|
if (canvasInstance && canvasInstance.getDiagramData) {
|
||||||
const data = 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;
|
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 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// --- 组件模块管理 ---
|
// --- 组件模块管理 ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,7 +207,7 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
|||||||
async function preloadComponentModules(componentTypes: string[]) {
|
async function preloadComponentModules(componentTypes: string[]) {
|
||||||
console.log("Preloading component modules:", componentTypes);
|
console.log("Preloading component modules:", componentTypes);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
componentTypes.map((type) => loadComponentModule(type))
|
componentTypes.map((type) => loadComponentModule(type)),
|
||||||
);
|
);
|
||||||
console.log("All component modules loaded");
|
console.log("All component modules loaded");
|
||||||
}
|
}
|
||||||
@@ -111,23 +240,19 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
|||||||
}
|
}
|
||||||
console.log(`组件模块加载成功: ${componentData.type}`, componentModule);
|
console.log(`组件模块加载成功: ${componentData.type}`, componentModule);
|
||||||
|
|
||||||
// 获取画布位置信息
|
// 使用内部管理的位置和缩放信息
|
||||||
let position = { x: 100, y: 100 };
|
let position = { x: 100, y: 100 };
|
||||||
let scale = 1;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (canvasInstance.getCanvasPosition && canvasInstance.getScale) {
|
|
||||||
position = canvasInstance.getCanvasPosition();
|
|
||||||
scale = canvasInstance.getScale();
|
|
||||||
|
|
||||||
const canvasContainer = canvasInstance.$el as HTMLElement;
|
const canvasContainer = canvasInstance.$el as HTMLElement;
|
||||||
if (canvasContainer) {
|
if (canvasContainer) {
|
||||||
const viewportWidth = canvasContainer.clientWidth;
|
const viewportWidth = canvasContainer.clientWidth;
|
||||||
const viewportHeight = canvasContainer.clientHeight;
|
const viewportHeight = canvasContainer.clientHeight;
|
||||||
|
|
||||||
position.x = (viewportWidth / 2 - position.x) / scale;
|
position.x =
|
||||||
position.y = (viewportHeight / 2 - position.y) / scale;
|
(viewportWidth / 2 - canvasPosition.x) / canvasScale.value;
|
||||||
}
|
position.y =
|
||||||
|
(viewportHeight / 2 - canvasPosition.y) / canvasScale.value;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取画布位置时出错:", 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();
|
const currentData = canvasInstance.getDiagramData();
|
||||||
currentData.parts.push(newComponent);
|
currentData.parts.push(newComponent);
|
||||||
|
|
||||||
// 使用 updateDiagramDataDirectly 避免触发加载状态
|
// 使用 updateDiagramDataDirectly 避免触发加载状态
|
||||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||||
|
saveDiagramData(currentData);
|
||||||
|
|
||||||
console.log("组件添加完成:", newComponent);
|
console.log("组件添加完成:", newComponent);
|
||||||
|
|
||||||
// 等待Vue的下一个tick,确保组件模块已经更新
|
// 等待Vue的下一个tick,确保组件模块已经更新
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +322,10 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
|||||||
console.log("添加模板:", templateData);
|
console.log("添加模板:", templateData);
|
||||||
|
|
||||||
const canvasInstance = diagramCanvas.value as any;
|
const canvasInstance = diagramCanvas.value as any;
|
||||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
if (
|
||||||
|
!canvasInstance?.getDiagramData ||
|
||||||
|
!canvasInstance?.updateDiagramDataDirectly
|
||||||
|
) {
|
||||||
console.error("没有可用的画布实例添加模板");
|
console.error("没有可用的画布实例添加模板");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -205,20 +337,18 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
|||||||
const idPrefix = `template-${Date.now()}-`;
|
const idPrefix = `template-${Date.now()}-`;
|
||||||
|
|
||||||
if (templateData.template?.parts) {
|
if (templateData.template?.parts) {
|
||||||
// 获取视口中心位置
|
// 使用内部管理的位置和缩放信息获取视口中心位置
|
||||||
let viewportCenter = { x: 300, y: 200 };
|
let viewportCenter = { x: 300, y: 200 };
|
||||||
try {
|
try {
|
||||||
if (canvasInstance.getCanvasPosition && canvasInstance.getScale) {
|
|
||||||
const position = canvasInstance.getCanvasPosition();
|
|
||||||
const scale = canvasInstance.getScale();
|
|
||||||
const canvasContainer = canvasInstance.$el as HTMLElement;
|
const canvasContainer = canvasInstance.$el as HTMLElement;
|
||||||
|
|
||||||
if (canvasContainer) {
|
if (canvasContainer) {
|
||||||
const viewportWidth = canvasContainer.clientWidth;
|
const viewportWidth = canvasContainer.clientWidth;
|
||||||
const viewportHeight = canvasContainer.clientHeight;
|
const viewportHeight = canvasContainer.clientHeight;
|
||||||
viewportCenter.x = (viewportWidth / 2 - position.x) / scale;
|
viewportCenter.x =
|
||||||
viewportCenter.y = (viewportHeight / 2 - position.y) / scale;
|
(viewportWidth / 2 - canvasPosition.x) / canvasScale.value;
|
||||||
}
|
viewportCenter.y =
|
||||||
|
(viewportHeight / 2 - canvasPosition.y) / canvasScale.value;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取视口中心位置时出错:", 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 relativeX = part.x - mainPart.x;
|
||||||
const relativeY = part.y - mainPart.y;
|
const relativeY = part.y - mainPart.y;
|
||||||
newPart.x = viewportCenter.x + relativeX;
|
newPart.x = viewportCenter.x + relativeX;
|
||||||
@@ -255,7 +388,7 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return newPart;
|
return newPart;
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
currentData.parts.push(...newParts);
|
currentData.parts.push(...newParts);
|
||||||
@@ -267,7 +400,8 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
|||||||
idMap[part.id] = `${idPrefix}${part.id}`;
|
idMap[part.id] = `${idPrefix}${part.id}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const newConnections = templateData.template.connections.map((conn: any) => {
|
const newConnections = templateData.template.connections.map(
|
||||||
|
(conn: any) => {
|
||||||
if (Array.isArray(conn)) {
|
if (Array.isArray(conn)) {
|
||||||
const [from, to, type, path] = conn;
|
const [from, to, type, path] = conn;
|
||||||
const fromParts = from.split(":");
|
const fromParts = from.split(":");
|
||||||
@@ -286,13 +420,18 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return conn;
|
return conn;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
currentData.connections.push(...newConnections);
|
currentData.connections.push(...newConnections);
|
||||||
}
|
}
|
||||||
|
|
||||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||||
console.log("=== 更新图表数据完成,新组件数量:", currentData.parts.length);
|
console.log(
|
||||||
|
"=== 更新图表数据完成,新组件数量:",
|
||||||
|
currentData.parts.length,
|
||||||
|
);
|
||||||
|
saveDiagramData(currentData);
|
||||||
|
|
||||||
return { success: true, message: `已添加 ${templateData.name} 模板` };
|
return { success: true, message: `已添加 ${templateData.name} 模板` };
|
||||||
} else {
|
} else {
|
||||||
@@ -306,12 +445,17 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
|||||||
*/
|
*/
|
||||||
function deleteComponent(componentId: string) {
|
function deleteComponent(componentId: string) {
|
||||||
const canvasInstance = diagramCanvas.value as any;
|
const canvasInstance = diagramCanvas.value as any;
|
||||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
if (
|
||||||
|
!canvasInstance?.getDiagramData ||
|
||||||
|
!canvasInstance?.updateDiagramDataDirectly
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentData = canvasInstance.getDiagramData();
|
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;
|
if (!component) return;
|
||||||
|
|
||||||
@@ -320,34 +464,47 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
|||||||
// 处理组件组
|
// 处理组件组
|
||||||
if (component.group && component.group !== "") {
|
if (component.group && component.group !== "") {
|
||||||
const groupMembers = currentData.parts.filter(
|
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));
|
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(
|
currentData.parts = currentData.parts.filter(
|
||||||
(p: DiagramPart) => !componentsToDelete.includes(p.id)
|
(p: DiagramPart) => !componentsToDelete.includes(p.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 删除相关连接
|
// 删除相关连接
|
||||||
currentData.connections = currentData.connections.filter((connection: any) => {
|
currentData.connections = currentData.connections.filter(
|
||||||
|
(connection: any) => {
|
||||||
for (const id of componentsToDelete) {
|
for (const id of componentsToDelete) {
|
||||||
if (connection[0].startsWith(`${id}:`) || connection[1].startsWith(`${id}:`)) {
|
if (
|
||||||
|
connection[0].startsWith(`${id}:`) ||
|
||||||
|
connection[1].startsWith(`${id}:`)
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// 清除选中状态
|
// 清除选中状态
|
||||||
if (selectedComponentId.value && componentsToDelete.includes(selectedComponentId.value)) {
|
if (
|
||||||
|
selectedComponentId.value &&
|
||||||
|
componentsToDelete.includes(selectedComponentId.value)
|
||||||
|
) {
|
||||||
selectedComponentId.value = null;
|
selectedComponentId.value = null;
|
||||||
selectedComponentConfig.value = null;
|
selectedComponentConfig.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||||
|
|
||||||
|
saveDiagramData(currentData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -378,16 +535,18 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
|||||||
// 添加组件直接属性
|
// 添加组件直接属性
|
||||||
const directPropConfigs = generatePropertyConfigs(componentData);
|
const directPropConfigs = generatePropertyConfigs(componentData);
|
||||||
const newDirectProps = directPropConfigs.filter(
|
const newDirectProps = directPropConfigs.filter(
|
||||||
(config) => !addedProps.has(config.name)
|
(config) => !addedProps.has(config.name),
|
||||||
);
|
);
|
||||||
propConfigs.push(...newDirectProps);
|
propConfigs.push(...newDirectProps);
|
||||||
|
|
||||||
// 添加 attrs 中的属性
|
// 添加 attrs 中的属性
|
||||||
if (componentData.attrs) {
|
if (componentData.attrs) {
|
||||||
const attrPropConfigs = generatePropsFromAttrs(componentData.attrs);
|
const attrPropConfigs = generatePropsFromAttrs(
|
||||||
|
componentData.attrs,
|
||||||
|
);
|
||||||
attrPropConfigs.forEach((attrConfig) => {
|
attrPropConfigs.forEach((attrConfig) => {
|
||||||
const existingIndex = propConfigs.findIndex(
|
const existingIndex = propConfigs.findIndex(
|
||||||
(p) => p.name === attrConfig.name
|
(p) => p.name === attrConfig.name,
|
||||||
);
|
);
|
||||||
if (existingIndex >= 0) {
|
if (existingIndex >= 0) {
|
||||||
propConfigs[existingIndex] = attrConfig;
|
propConfigs[existingIndex] = attrConfig;
|
||||||
@@ -398,9 +557,15 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
selectedComponentConfig.value = { props: propConfigs };
|
selectedComponentConfig.value = { props: propConfigs };
|
||||||
console.log(`Built config for ${componentData.type}:`, selectedComponentConfig.value);
|
console.log(
|
||||||
|
`Built config for ${componentData.type}:`,
|
||||||
|
selectedComponentConfig.value,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error building config for ${componentData.type}:`, error);
|
console.error(
|
||||||
|
`Error building config for ${componentData.type}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
selectedComponentConfig.value = { props: [] };
|
selectedComponentConfig.value = { props: [] };
|
||||||
}
|
}
|
||||||
} else {
|
} 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;
|
const canvasInstance = diagramCanvas.value as any;
|
||||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
if (
|
||||||
|
!canvasInstance?.getDiagramData ||
|
||||||
|
!canvasInstance?.updateDiagramDataDirectly
|
||||||
|
) {
|
||||||
console.error("没有可用的画布实例进行属性更新");
|
console.error("没有可用的画布实例进行属性更新");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -426,7 +598,9 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentData = canvasInstance.getDiagramData();
|
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 (part) {
|
||||||
if (propName in part) {
|
if (propName in part) {
|
||||||
@@ -439,27 +613,44 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
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;
|
const canvasInstance = diagramCanvas.value as any;
|
||||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
if (
|
||||||
|
!canvasInstance?.getDiagramData ||
|
||||||
|
!canvasInstance?.updateDiagramDataDirectly
|
||||||
|
) {
|
||||||
console.error("没有可用的画布实例进行属性更新");
|
console.error("没有可用的画布实例进行属性更新");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentData = canvasInstance.getDiagramData();
|
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 (part) {
|
||||||
(part as any)[propName] = value;
|
(part as any)[propName] = value;
|
||||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
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 }) {
|
function moveComponent(moveData: { id: string; x: number; y: number }) {
|
||||||
const canvasInstance = diagramCanvas.value as any;
|
const canvasInstance = diagramCanvas.value as any;
|
||||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
if (
|
||||||
|
!canvasInstance?.getDiagramData ||
|
||||||
|
!canvasInstance?.updateDiagramDataDirectly
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentData = canvasInstance.getDiagramData();
|
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) {
|
if (part) {
|
||||||
part.x = moveData.x;
|
part.x = moveData.x;
|
||||||
part.y = moveData.y;
|
part.y = moveData.y;
|
||||||
@@ -514,7 +710,13 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
|||||||
if (canvasInstance && canvasInstance.getDiagramData) {
|
if (canvasInstance && canvasInstance.getDiagramData) {
|
||||||
return canvasInstance.getDiagramData();
|
return canvasInstance.getDiagramData();
|
||||||
}
|
}
|
||||||
return { parts: [], connections: [], version: 1, author: "admin", editor: "me" };
|
return {
|
||||||
|
parts: [],
|
||||||
|
connections: [],
|
||||||
|
version: 1,
|
||||||
|
author: "admin",
|
||||||
|
editor: "me",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -527,29 +729,6 @@ 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取组件定义
|
* 获取组件定义
|
||||||
*/
|
*/
|
||||||
@@ -619,6 +798,10 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
|||||||
selectedComponentConfig,
|
selectedComponentConfig,
|
||||||
componentRefs,
|
componentRefs,
|
||||||
|
|
||||||
|
// Canvas控制状态
|
||||||
|
canvasPosition,
|
||||||
|
canvasScale,
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
loadComponentModule,
|
loadComponentModule,
|
||||||
preloadComponentModules,
|
preloadComponentModules,
|
||||||
@@ -634,13 +817,22 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
|||||||
getComponentRef,
|
getComponentRef,
|
||||||
getDiagramData,
|
getDiagramData,
|
||||||
updateDiagramData,
|
updateDiagramData,
|
||||||
getCanvasInfo,
|
|
||||||
showToast,
|
|
||||||
getComponentDefinition,
|
getComponentDefinition,
|
||||||
prepareComponentProps,
|
prepareComponentProps,
|
||||||
initialize,
|
initialize,
|
||||||
|
|
||||||
|
// Canvas控制方法
|
||||||
|
setCanvasPosition,
|
||||||
|
updateCanvasPosition,
|
||||||
|
setCanvasScale,
|
||||||
|
getCanvasPosition,
|
||||||
|
getCanvasScale,
|
||||||
|
zoomAtPosition,
|
||||||
|
screenToCanvas,
|
||||||
|
canvasToScreen,
|
||||||
|
centerView,
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export { useProvideComponentManager, useComponentManager };
|
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(
|
export function updatePartPosition(
|
||||||
data: DiagramData,
|
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(
|
export function addConnection(
|
||||||
data: DiagramData,
|
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文件的函数
|
// 添加验证diagram.json文件的函数
|
||||||
export function validateDiagramData(data: any): { isValid: boolean; errors: string[] } {
|
export function validateDiagramData(data: any): { isValid: boolean; errors: string[] } {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|||||||
@@ -98,7 +98,6 @@ const props = defineProps<{
|
|||||||
const propertySectionExpanded = ref(false); // 基本属性区域默认展开
|
const propertySectionExpanded = ref(false); // 基本属性区域默认展开
|
||||||
const pinsSectionExpanded = ref(false); // 引脚配置区域默认折叠
|
const pinsSectionExpanded = ref(false); // 引脚配置区域默认折叠
|
||||||
const componentCapsExpanded = ref(true); // 组件功能区域默认展开
|
const componentCapsExpanded = ref(true); // 组件功能区域默认展开
|
||||||
const wireSectionExpanded = ref(false); // 连线管理区域默认折叠
|
|
||||||
|
|
||||||
const componentCaps = useTemplateRef("ComponentCapabilities");
|
const componentCaps = useTemplateRef("ComponentCapabilities");
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,16 @@
|
|||||||
<SquareActivityIcon class="icon" />
|
<SquareActivityIcon class="icon" />
|
||||||
示波器
|
示波器
|
||||||
</label>
|
</label>
|
||||||
|
<label class="tab">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="function-bar"
|
||||||
|
id="4"
|
||||||
|
@change="handleTabChange"
|
||||||
|
/>
|
||||||
|
<Zap class="icon" />
|
||||||
|
逻辑分析仪
|
||||||
|
</label>
|
||||||
<!-- 全屏按钮 -->
|
<!-- 全屏按钮 -->
|
||||||
<button
|
<button
|
||||||
class="fullscreen-btn ml-auto btn btn-ghost btn-sm"
|
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">
|
<div v-else-if="checkID === 3" class="h-full overflow-y-auto">
|
||||||
<OscilloscopeView />
|
<OscilloscopeView />
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="checkID === 4" class="h-full overflow-y-auto">
|
||||||
|
<LogicAnalyzerView />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -62,13 +75,16 @@ import {
|
|||||||
TerminalIcon,
|
TerminalIcon,
|
||||||
MaximizeIcon,
|
MaximizeIcon,
|
||||||
MinimizeIcon,
|
MinimizeIcon,
|
||||||
|
Zap,
|
||||||
} from "lucide-vue-next";
|
} from "lucide-vue-next";
|
||||||
|
import { useLocalStorage } from "@vueuse/core";
|
||||||
import VideoStreamView from "@/views/Project/VideoStream.vue";
|
import VideoStreamView from "@/views/Project/VideoStream.vue";
|
||||||
import OscilloscopeView from "@/views/Project/Oscilloscope.vue";
|
import OscilloscopeView from "@/views/Project/Oscilloscope.vue";
|
||||||
|
import LogicAnalyzerView from "@/views/Project/LogicAnalyzer.vue";
|
||||||
import { isNull, toNumber } from "lodash";
|
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<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
<DiagramCanvas
|
<DiagramCanvas
|
||||||
ref="diagramCanvas"
|
ref="diagramCanvas"
|
||||||
:showDocPanel="showDocPanel"
|
:showDocPanel="showDocPanel"
|
||||||
@diagram-updated="handleDiagramUpdated"
|
|
||||||
@open-components="openComponentsMenu"
|
@open-components="openComponentsMenu"
|
||||||
@toggle-doc-panel="toggleDocPanel"
|
@toggle-doc-panel="toggleDocPanel"
|
||||||
/>
|
/>
|
||||||
@@ -92,8 +91,6 @@
|
|||||||
<ComponentSelector
|
<ComponentSelector
|
||||||
:open="showComponentsMenu"
|
:open="showComponentsMenu"
|
||||||
@update:open="showComponentsMenu = $event"
|
@update:open="showComponentsMenu = $event"
|
||||||
@add-component="handleAddComponent"
|
|
||||||
@add-template="handleAddTemplate"
|
|
||||||
@close="showComponentsMenu = false"
|
@close="showComponentsMenu = false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -159,9 +156,12 @@ function handleHorizontalSplitterResize(sizes: number[]) {
|
|||||||
|
|
||||||
function handleVerticalSplitterResize(sizes: number[]) {
|
function handleVerticalSplitterResize(sizes: number[]) {
|
||||||
if (sizes && sizes.length > 0) {
|
if (sizes && sizes.length > 0) {
|
||||||
|
// 只在非全屏状态下保存分栏大小,避免全屏时的100%被保存
|
||||||
|
if (!isBottomBarFullscreen.value) {
|
||||||
verticalSplitterSize.value = sizes[0];
|
verticalSplitterSize.value = sizes[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- 实验板申请对话框 ---
|
// --- 实验板申请对话框 ---
|
||||||
const showRequestBoardDialog = ref(false);
|
const showRequestBoardDialog = ref(false);
|
||||||
@@ -219,32 +219,6 @@ function openComponentsMenu() {
|
|||||||
showComponentsMenu.value = true;
|
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
|
// 更新组件属性的方法 - 委托给componentManager
|
||||||
function updateComponentProp(
|
function updateComponentProp(
|
||||||
componentId: string,
|
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>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="open"
|
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="bg-base-100 rounded-lg shadow-xl max-w-md w-full mx-4">
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
控制面板
|
控制面板
|
||||||
</h2>
|
</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="stats shadow">
|
||||||
<div class="stat bg-base-100">
|
<div class="stat bg-base-100">
|
||||||
@@ -42,6 +42,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</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="stats shadow">
|
||||||
<div class="stat bg-base-100 relative">
|
<div class="stat bg-base-100 relative">
|
||||||
@@ -308,7 +340,7 @@ import {
|
|||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
} from "lucide-vue-next";
|
} from "lucide-vue-next";
|
||||||
import { VideoStreamClient, CameraConfigRequest } from "@/APIClient";
|
import { VideoStreamClient, CameraConfigRequest, ResolutionConfigRequest } from "@/APIClient";
|
||||||
import { useEquipments } from "@/stores/equipments";
|
import { useEquipments } from "@/stores/equipments";
|
||||||
|
|
||||||
const eqps = useEquipments();
|
const eqps = useEquipments();
|
||||||
@@ -321,6 +353,15 @@ const isPlaying = ref(false);
|
|||||||
const hasVideoError = ref(false);
|
const hasVideoError = ref(false);
|
||||||
const videoStatus = ref('点击"播放视频流"按钮开始查看实时视频');
|
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({
|
const statusInfo = ref({
|
||||||
isRunning: false,
|
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 = () => {
|
const stopStream = () => {
|
||||||
try {
|
try {
|
||||||
@@ -574,6 +683,7 @@ const stopStream = () => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
addLog("info", "HTTP 视频流页面已加载");
|
addLog("info", "HTTP 视频流页面已加载");
|
||||||
await refreshStatus();
|
await refreshStatus();
|
||||||
|
await refreshResolutions(); // 初始化分辨率信息
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user