Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import { ResourceClient, ResourcePurpose } from "@/APIClient";
|
||||
import { AuthManager } from "@/utils/AuthManager";
|
||||
|
||||
// 定义 diagram.json 的类型结构
|
||||
export interface DiagramData {
|
||||
version: number;
|
||||
@@ -26,40 +29,43 @@ export interface DiagramPart {
|
||||
// 连接类型定义 - 使用元组类型表示四元素数组
|
||||
export type ConnectionArray = [string, string, number, string[]];
|
||||
|
||||
import { AuthManager } from '@/utils/AuthManager';
|
||||
|
||||
// 解析连接字符串为组件ID和引脚ID
|
||||
export function parseConnectionPin(connectionPin: string): { componentId: string; pinId: string } {
|
||||
const [componentId, pinId] = connectionPin.split(':');
|
||||
export function parseConnectionPin(connectionPin: string): {
|
||||
componentId: string;
|
||||
pinId: string;
|
||||
} {
|
||||
const [componentId, pinId] = connectionPin.split(":");
|
||||
return { componentId, pinId };
|
||||
}
|
||||
|
||||
// 将连接数组转换为适用于渲染的格式
|
||||
export function connectionArrayToWireItem(
|
||||
connection: ConnectionArray,
|
||||
index: number,
|
||||
startPos = { x: 0, y: 0 },
|
||||
endPos = { x: 0, y: 0 }
|
||||
connection: ConnectionArray,
|
||||
index: number,
|
||||
startPos = { x: 0, y: 0 },
|
||||
endPos = { x: 0, y: 0 },
|
||||
): WireItem {
|
||||
const [startPinStr, endPinStr, width, path] = connection;
|
||||
const { componentId: startComponentId, pinId: startPinId } = parseConnectionPin(startPinStr);
|
||||
const { componentId: endComponentId, pinId: endPinId } = parseConnectionPin(endPinStr);
|
||||
|
||||
const { componentId: startComponentId, pinId: startPinId } =
|
||||
parseConnectionPin(startPinStr);
|
||||
const { componentId: endComponentId, pinId: endPinId } =
|
||||
parseConnectionPin(endPinStr);
|
||||
|
||||
return {
|
||||
id: `wire-${index}`,
|
||||
startX: startPos.x,
|
||||
startY: startPos.y,
|
||||
endX: endPos.x,
|
||||
endX: endPos.x,
|
||||
endY: endPos.y,
|
||||
startComponentId,
|
||||
startPinId,
|
||||
endComponentId,
|
||||
endPinId,
|
||||
strokeWidth: width,
|
||||
color: '#4a5568', // 默认颜色
|
||||
routingMode: 'path',
|
||||
color: "#4a5568", // 默认颜色
|
||||
routingMode: "path",
|
||||
pathCommands: path,
|
||||
showLabel: false
|
||||
showLabel: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,7 +82,7 @@ export interface WireItem {
|
||||
endPinId?: string;
|
||||
strokeWidth: number;
|
||||
color: string;
|
||||
routingMode: 'orthogonal' | 'path';
|
||||
routingMode: "orthogonal" | "path";
|
||||
constraint?: string;
|
||||
pathCommands?: string[];
|
||||
showLabel: boolean;
|
||||
@@ -88,58 +94,64 @@ export async function loadDiagramData(examId?: string): Promise<DiagramData> {
|
||||
// 如果提供了examId,优先从API加载实验的diagram
|
||||
if (examId) {
|
||||
try {
|
||||
const resourceClient = AuthManager.createAuthenticatedResourceClient();
|
||||
|
||||
const resourceClient = AuthManager.createClient(ResourceClient);
|
||||
|
||||
// 获取diagram类型的资源列表
|
||||
const resources = await resourceClient.getResourceList(examId, 'canvas', 'template');
|
||||
|
||||
const resources = await resourceClient.getResourceList(
|
||||
examId,
|
||||
"canvas",
|
||||
ResourcePurpose.Template,
|
||||
);
|
||||
|
||||
if (resources && resources.length > 0) {
|
||||
// 获取第一个diagram资源
|
||||
const diagramResource = resources[0];
|
||||
|
||||
|
||||
// 使用动态API获取资源文件内容
|
||||
const response = await resourceClient.getResourceById(diagramResource.id);
|
||||
|
||||
const response = await resourceClient.getResourceById(
|
||||
diagramResource.id,
|
||||
);
|
||||
|
||||
if (response && response.data) {
|
||||
const text = await response.data.text();
|
||||
const data = JSON.parse(text);
|
||||
|
||||
|
||||
// 验证数据格式
|
||||
const validation = validateDiagramData(data);
|
||||
if (validation.isValid) {
|
||||
console.log('成功从API加载实验diagram:', examId);
|
||||
console.log("成功从API加载实验diagram:", examId);
|
||||
return data;
|
||||
} else {
|
||||
console.warn('API返回的diagram数据格式无效:', validation.errors);
|
||||
console.warn("API返回的diagram数据格式无效:", validation.errors);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('未找到实验diagram资源,使用默认加载方式');
|
||||
console.log("未找到实验diagram资源,使用默认加载方式");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('从API加载实验diagram失败,使用默认加载方式:', error);
|
||||
console.warn("从API加载实验diagram失败,使用默认加载方式:", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果没有examId或API加载失败,尝试从静态文件加载(不再使用本地存储)
|
||||
|
||||
|
||||
// 从静态文件加载(作为备选方案)
|
||||
const response = await fetch('/src/components/diagram.json');
|
||||
const response = await fetch("/src/components/diagram.json");
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load diagram.json: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
// 验证静态文件数据
|
||||
const validation = validateDiagramData(data);
|
||||
if (validation.isValid) {
|
||||
return data;
|
||||
} else {
|
||||
console.warn('静态diagram文件数据格式无效:', validation.errors);
|
||||
throw new Error('所有diagram数据源都无效');
|
||||
console.warn("静态diagram文件数据格式无效:", validation.errors);
|
||||
throw new Error("所有diagram数据源都无效");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading diagram data:', error);
|
||||
console.error("Error loading diagram data:", error);
|
||||
// 返回空的默认数据结构
|
||||
return createEmptyDiagram();
|
||||
}
|
||||
@@ -149,33 +161,31 @@ export async function loadDiagramData(examId?: string): Promise<DiagramData> {
|
||||
export function createEmptyDiagram(): DiagramData {
|
||||
return {
|
||||
version: 1,
|
||||
author: 'user',
|
||||
editor: 'user',
|
||||
author: "user",
|
||||
editor: "user",
|
||||
parts: [],
|
||||
connections: []
|
||||
connections: [],
|
||||
};
|
||||
}
|
||||
|
||||
// 保存图表数据(已禁用本地存储)
|
||||
export function saveDiagramData(data: DiagramData): void {
|
||||
// 本地存储功能已禁用 - 不再保存到localStorage
|
||||
console.debug('saveDiagramData called but localStorage saving is disabled');
|
||||
console.debug("saveDiagramData called but localStorage saving is disabled");
|
||||
}
|
||||
|
||||
// 更新组件位置
|
||||
export function updatePartPosition(
|
||||
data: DiagramData,
|
||||
partId: string,
|
||||
x: number,
|
||||
y: number
|
||||
data: DiagramData,
|
||||
partId: string,
|
||||
x: number,
|
||||
y: number,
|
||||
): DiagramData {
|
||||
return {
|
||||
...data,
|
||||
parts: data.parts.map(part =>
|
||||
part.id === partId
|
||||
? { ...part, x, y }
|
||||
: part
|
||||
)
|
||||
parts: data.parts.map((part) =>
|
||||
part.id === partId ? { ...part, x, y } : part,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -184,21 +194,21 @@ export function updatePartAttribute(
|
||||
data: DiagramData,
|
||||
partId: string,
|
||||
attrName: string,
|
||||
value: any
|
||||
value: any,
|
||||
): DiagramData {
|
||||
return {
|
||||
...data,
|
||||
parts: data.parts.map(part =>
|
||||
part.id === partId
|
||||
? {
|
||||
...part,
|
||||
attrs: {
|
||||
...part.attrs,
|
||||
[attrName]: value
|
||||
}
|
||||
}
|
||||
: part
|
||||
)
|
||||
parts: data.parts.map((part) =>
|
||||
part.id === partId
|
||||
? {
|
||||
...part,
|
||||
attrs: {
|
||||
...part.attrs,
|
||||
[attrName]: value,
|
||||
},
|
||||
}
|
||||
: part,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -210,72 +220,79 @@ export function addConnection(
|
||||
endComponentId: string,
|
||||
endPinId: string,
|
||||
width: number = 2,
|
||||
path: string[] = []
|
||||
path: string[] = [],
|
||||
): DiagramData {
|
||||
const newConnection: ConnectionArray = [
|
||||
`${startComponentId}:${startPinId}`,
|
||||
`${endComponentId}:${endPinId}`,
|
||||
width,
|
||||
path
|
||||
path,
|
||||
];
|
||||
|
||||
|
||||
return {
|
||||
...data,
|
||||
connections: [...data.connections, newConnection]
|
||||
connections: [...data.connections, newConnection],
|
||||
};
|
||||
}
|
||||
|
||||
// 删除连接
|
||||
export function deleteConnection(
|
||||
data: DiagramData,
|
||||
connectionIndex: number
|
||||
connectionIndex: number,
|
||||
): DiagramData {
|
||||
return {
|
||||
...data,
|
||||
connections: data.connections.filter((_, index) => index !== connectionIndex)
|
||||
connections: data.connections.filter(
|
||||
(_, index) => index !== connectionIndex,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// 查找与组件关联的所有连接
|
||||
export function findConnectionsByPart(
|
||||
data: DiagramData,
|
||||
partId: string
|
||||
partId: string,
|
||||
): { connection: ConnectionArray; index: number }[] {
|
||||
return data.connections
|
||||
.map((connection, index) => ({ connection, index }))
|
||||
.filter(({ connection }) => {
|
||||
const [startPin, endPin] = connection;
|
||||
const startCompId = startPin.split(':')[0];
|
||||
const endCompId = endPin.split(':')[0];
|
||||
const startCompId = startPin.split(":")[0];
|
||||
const endCompId = endPin.split(":")[0];
|
||||
return startCompId === partId || endCompId === partId;
|
||||
});
|
||||
}
|
||||
|
||||
// 添加验证diagram.json文件的函数
|
||||
export function validateDiagramData(data: any): { isValid: boolean; errors: string[] } {
|
||||
export function validateDiagramData(data: any): {
|
||||
isValid: boolean;
|
||||
errors: string[];
|
||||
} {
|
||||
const errors: string[] = [];
|
||||
|
||||
|
||||
// 检查版本号
|
||||
if (!data.version) {
|
||||
errors.push('缺少version字段');
|
||||
errors.push("缺少version字段");
|
||||
}
|
||||
|
||||
|
||||
// 检查parts数组
|
||||
if (!Array.isArray(data.parts)) {
|
||||
errors.push('parts字段不是数组');
|
||||
errors.push("parts字段不是数组");
|
||||
} else {
|
||||
// 验证parts中的每个对象
|
||||
data.parts.forEach((part: any, index: number) => {
|
||||
if (!part.id) errors.push(`parts[${index}]缺少id`);
|
||||
if (!part.type) errors.push(`parts[${index}]缺少type`);
|
||||
if (typeof part.x !== 'number') errors.push(`parts[${index}]缺少有效的x坐标`);
|
||||
if (typeof part.y !== 'number') errors.push(`parts[${index}]缺少有效的y坐标`);
|
||||
if (typeof part.x !== "number")
|
||||
errors.push(`parts[${index}]缺少有效的x坐标`);
|
||||
if (typeof part.y !== "number")
|
||||
errors.push(`parts[${index}]缺少有效的y坐标`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 检查connections数组
|
||||
if (!Array.isArray(data.connections)) {
|
||||
errors.push('connections字段不是数组');
|
||||
errors.push("connections字段不是数组");
|
||||
} else {
|
||||
// 验证connections中的每个数组
|
||||
data.connections.forEach((conn: any, index: number) => {
|
||||
@@ -283,25 +300,25 @@ export function validateDiagramData(data: any): { isValid: boolean; errors: stri
|
||||
errors.push(`connections[${index}]不是有效的连接数组`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const [startPin, endPin, width] = conn;
|
||||
|
||||
if (typeof startPin !== 'string' || !startPin.includes(':')) {
|
||||
|
||||
if (typeof startPin !== "string" || !startPin.includes(":")) {
|
||||
errors.push(`connections[${index}]的起始针脚格式无效`);
|
||||
}
|
||||
|
||||
if (typeof endPin !== 'string' || !endPin.includes(':')) {
|
||||
|
||||
if (typeof endPin !== "string" || !endPin.includes(":")) {
|
||||
errors.push(`connections[${index}]的结束针脚格式无效`);
|
||||
}
|
||||
|
||||
if (typeof width !== 'number') {
|
||||
|
||||
if (typeof width !== "number") {
|
||||
errors.push(`connections[${index}]的宽度不是有效的数字`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export const previewSizes: Record<string, number> = {
|
||||
EC11RotaryEncoder: 0.4,
|
||||
Pin: 0.8,
|
||||
SMT_LED: 0.7,
|
||||
SevenSegmentDisplay: 0.4,
|
||||
SevenSegmentDisplayUltimate: 0.4,
|
||||
HDMI: 0.5,
|
||||
DDR: 0.5,
|
||||
ETH: 0.5,
|
||||
@@ -52,7 +52,7 @@ export const availableComponents: ComponentConfig[] = [
|
||||
{ type: "EC11RotaryEncoder", name: "EC11旋转编码器" },
|
||||
{ type: "Pin", name: "引脚" },
|
||||
{ type: "SMT_LED", name: "贴片LED" },
|
||||
{ type: "SevenSegmentDisplay", name: "数码管" },
|
||||
{ type: "SevenSegmentDisplayUltimate", name: "数码管" },
|
||||
{ type: "HDMI", name: "HDMI接口" },
|
||||
{ type: "DDR", name: "DDR内存" },
|
||||
{ type: "ETH", name: "以太网接口" },
|
||||
|
||||
@@ -31,8 +31,16 @@ export type Channel = {
|
||||
|
||||
// 全局模式选项
|
||||
const globalModes = [
|
||||
{value: GlobalCaptureMode.AND,label: "AND",description: "所有条件都满足时触发",},
|
||||
{value: GlobalCaptureMode.OR,label: "OR",description: "任一条件满足时触发",},
|
||||
{
|
||||
value: GlobalCaptureMode.AND,
|
||||
label: "AND",
|
||||
description: "所有条件都满足时触发",
|
||||
},
|
||||
{
|
||||
value: GlobalCaptureMode.OR,
|
||||
label: "OR",
|
||||
description: "任一条件满足时触发",
|
||||
},
|
||||
{ value: GlobalCaptureMode.NAND, label: "NAND", description: "AND的非" },
|
||||
{ value: GlobalCaptureMode.NOR, label: "NOR", description: "OR的非" },
|
||||
];
|
||||
@@ -70,21 +78,53 @@ const channelDivOptions = [
|
||||
];
|
||||
|
||||
const ClockDivOptions = [
|
||||
{ value: AnalyzerClockDiv.DIV1, label: "120MHz", description: "采样频率120MHz" },
|
||||
{ value: AnalyzerClockDiv.DIV2, label: "60MHz", description: "采样频率60MHz" },
|
||||
{ value: AnalyzerClockDiv.DIV4, label: "30MHz", description: "采样频率30MHz" },
|
||||
{ value: AnalyzerClockDiv.DIV8, label: "15MHz", description: "采样频率15MHz" },
|
||||
{ value: AnalyzerClockDiv.DIV16, label: "7.5MHz", description: "采样频率7.5MHz" },
|
||||
{ value: AnalyzerClockDiv.DIV32, label: "3.75MHz", description: "采样频率3.75MHz" },
|
||||
{ value: AnalyzerClockDiv.DIV64, label: "1.875MHz", description: "采样频率1.875MHz" },
|
||||
{ value: AnalyzerClockDiv.DIV128, label: "937.5KHz", description: "采样频率937.5KHz" },
|
||||
{
|
||||
value: AnalyzerClockDiv.DIV1,
|
||||
label: "120MHz",
|
||||
description: "采样频率120MHz",
|
||||
},
|
||||
{
|
||||
value: AnalyzerClockDiv.DIV2,
|
||||
label: "60MHz",
|
||||
description: "采样频率60MHz",
|
||||
},
|
||||
{
|
||||
value: AnalyzerClockDiv.DIV4,
|
||||
label: "30MHz",
|
||||
description: "采样频率30MHz",
|
||||
},
|
||||
{
|
||||
value: AnalyzerClockDiv.DIV8,
|
||||
label: "15MHz",
|
||||
description: "采样频率15MHz",
|
||||
},
|
||||
{
|
||||
value: AnalyzerClockDiv.DIV16,
|
||||
label: "7.5MHz",
|
||||
description: "采样频率7.5MHz",
|
||||
},
|
||||
{
|
||||
value: AnalyzerClockDiv.DIV32,
|
||||
label: "3.75MHz",
|
||||
description: "采样频率3.75MHz",
|
||||
},
|
||||
{
|
||||
value: AnalyzerClockDiv.DIV64,
|
||||
label: "1.875MHz",
|
||||
description: "采样频率1.875MHz",
|
||||
},
|
||||
{
|
||||
value: AnalyzerClockDiv.DIV128,
|
||||
label: "937.5KHz",
|
||||
description: "采样频率937.5KHz",
|
||||
},
|
||||
];
|
||||
|
||||
// 捕获深度限制常量
|
||||
const CAPTURE_LENGTH_MIN = 1024; // 最小捕获深度 1024
|
||||
const CAPTURE_LENGTH_MAX = 0x10000000 - 0x01000000; // 最大捕获深度
|
||||
|
||||
// 预捕获深度限制常量
|
||||
// 预捕获深度限制常量
|
||||
const PRE_CAPTURE_LENGTH_MIN = 2; // 最小预捕获深度 2
|
||||
|
||||
// 默认颜色数组
|
||||
@@ -170,40 +210,64 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
||||
// 转换通道数字到枚举值
|
||||
const getChannelDivEnum = (channelCount: number): AnalyzerChannelDiv => {
|
||||
switch (channelCount) {
|
||||
case 1: return AnalyzerChannelDiv.ONE;
|
||||
case 2: return AnalyzerChannelDiv.TWO;
|
||||
case 4: return AnalyzerChannelDiv.FOUR;
|
||||
case 8: return AnalyzerChannelDiv.EIGHT;
|
||||
case 16: return AnalyzerChannelDiv.XVI;
|
||||
case 32: return AnalyzerChannelDiv.XXXII;
|
||||
default: return AnalyzerChannelDiv.EIGHT;
|
||||
case 1:
|
||||
return AnalyzerChannelDiv.ONE;
|
||||
case 2:
|
||||
return AnalyzerChannelDiv.TWO;
|
||||
case 4:
|
||||
return AnalyzerChannelDiv.FOUR;
|
||||
case 8:
|
||||
return AnalyzerChannelDiv.EIGHT;
|
||||
case 16:
|
||||
return AnalyzerChannelDiv.XVI;
|
||||
case 32:
|
||||
return AnalyzerChannelDiv.XXXII;
|
||||
default:
|
||||
return AnalyzerChannelDiv.EIGHT;
|
||||
}
|
||||
};
|
||||
|
||||
// 验证捕获深度
|
||||
const validateCaptureLength = (value: number): { valid: boolean; message?: string } => {
|
||||
const validateCaptureLength = (
|
||||
value: number,
|
||||
): { valid: boolean; message?: string } => {
|
||||
if (!Number.isInteger(value)) {
|
||||
return { valid: false, message: "捕获深度必须是整数" };
|
||||
}
|
||||
if (value < CAPTURE_LENGTH_MIN) {
|
||||
return { valid: false, message: `捕获深度不能小于 ${CAPTURE_LENGTH_MIN}` };
|
||||
return {
|
||||
valid: false,
|
||||
message: `捕获深度不能小于 ${CAPTURE_LENGTH_MIN}`,
|
||||
};
|
||||
}
|
||||
if (value > CAPTURE_LENGTH_MAX) {
|
||||
return { valid: false, message: `捕获深度不能大于 ${CAPTURE_LENGTH_MAX.toLocaleString()}` };
|
||||
return {
|
||||
valid: false,
|
||||
message: `捕获深度不能大于 ${CAPTURE_LENGTH_MAX.toLocaleString()}`,
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
};
|
||||
|
||||
// 验证预捕获深度
|
||||
const validatePreCaptureLength = (value: number, currentCaptureLength: number): { valid: boolean; message?: string } => {
|
||||
const validatePreCaptureLength = (
|
||||
value: number,
|
||||
currentCaptureLength: number,
|
||||
): { valid: boolean; message?: string } => {
|
||||
if (!Number.isInteger(value)) {
|
||||
return { valid: false, message: "预捕获深度必须是整数" };
|
||||
}
|
||||
if (value < PRE_CAPTURE_LENGTH_MIN) {
|
||||
return { valid: false, message: `预捕获深度不能小于 ${PRE_CAPTURE_LENGTH_MIN}` };
|
||||
return {
|
||||
valid: false,
|
||||
message: `预捕获深度不能小于 ${PRE_CAPTURE_LENGTH_MIN}`,
|
||||
};
|
||||
}
|
||||
if (value >= currentCaptureLength) {
|
||||
return { valid: false, message: `预捕获深度不能大于等于捕获深度 (${currentCaptureLength})` };
|
||||
return {
|
||||
valid: false,
|
||||
message: `预捕获深度不能大于等于捕获深度 (${currentCaptureLength})`,
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
};
|
||||
@@ -215,13 +279,13 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
||||
alert?.error(validation.message!, 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 检查预捕获深度是否仍然有效
|
||||
if (preCaptureLength.value >= value) {
|
||||
preCaptureLength.value = Math.max(0, value - 1);
|
||||
alert?.warn(`预捕获深度已自动调整为 ${preCaptureLength.value}`, 3000);
|
||||
}
|
||||
|
||||
|
||||
captureLength.value = value;
|
||||
return true;
|
||||
};
|
||||
@@ -233,7 +297,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
||||
alert?.error(validation.message!, 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
preCaptureLength.value = value;
|
||||
return true;
|
||||
};
|
||||
@@ -241,12 +305,12 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
||||
// 设置通道组
|
||||
const setChannelDiv = (channelCount: number) => {
|
||||
// 验证通道数量是否有效
|
||||
if (!channelDivOptions.find(option => option.value === channelCount)) {
|
||||
if (!channelDivOptions.find((option) => option.value === channelCount)) {
|
||||
console.error(`无效的通道组设置: ${channelCount}`);
|
||||
return;
|
||||
}
|
||||
currentChannelDiv.value = channelCount;
|
||||
|
||||
|
||||
// 禁用所有通道
|
||||
channels.forEach((channel) => {
|
||||
channel.enabled = false;
|
||||
@@ -257,7 +321,9 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
||||
channels[i].enabled = true;
|
||||
}
|
||||
|
||||
const option = channelDivOptions.find(opt => opt.value === channelCount);
|
||||
const option = channelDivOptions.find(
|
||||
(opt) => opt.value === channelCount,
|
||||
);
|
||||
alert?.success(`已设置为${option?.label}`, 2000);
|
||||
};
|
||||
|
||||
@@ -294,7 +360,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
||||
|
||||
const getCaptureData = async () => {
|
||||
try {
|
||||
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
|
||||
const client = AuthManager.createClient(LogicAnalyzerClient);
|
||||
// 获取捕获数据,使用当前设置的捕获长度
|
||||
const base64Data = await client.getCaptureData(captureLength.value);
|
||||
|
||||
@@ -308,7 +374,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
||||
// 根据当前通道数量解析数据
|
||||
const channelCount = currentChannelDiv.value;
|
||||
const timeStepNs = currentSamplePeriodNs.value;
|
||||
|
||||
|
||||
let sampleCount: number;
|
||||
let x: number[];
|
||||
let y: number[][];
|
||||
@@ -316,19 +382,16 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
||||
if (channelCount === 1) {
|
||||
// 1通道:每个字节包含8个时间单位的数据
|
||||
sampleCount = bytes.length * 8;
|
||||
|
||||
|
||||
// 创建时间轴
|
||||
x = Array.from(
|
||||
{ length: sampleCount },
|
||||
(_, i) => (i * timeStepNs) / 1000,
|
||||
); // 转换为微秒
|
||||
|
||||
|
||||
// 创建通道数据数组
|
||||
y = Array.from(
|
||||
{ length: 1 },
|
||||
() => new Array(sampleCount),
|
||||
);
|
||||
|
||||
y = Array.from({ length: 1 }, () => new Array(sampleCount));
|
||||
|
||||
// 解析数据:每个字节的8个位对应8个时间单位
|
||||
for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
|
||||
const byte = bytes[byteIndex];
|
||||
@@ -340,19 +403,16 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
||||
} else if (channelCount === 2) {
|
||||
// 2通道:每个字节包含4个时间单位的数据
|
||||
sampleCount = bytes.length * 4;
|
||||
|
||||
|
||||
// 创建时间轴
|
||||
x = Array.from(
|
||||
{ length: sampleCount },
|
||||
(_, i) => (i * timeStepNs) / 1000,
|
||||
); // 转换为微秒
|
||||
|
||||
|
||||
// 创建通道数据数组
|
||||
y = Array.from(
|
||||
{ length: 2 },
|
||||
() => new Array(sampleCount),
|
||||
);
|
||||
|
||||
y = Array.from({ length: 2 }, () => new Array(sampleCount));
|
||||
|
||||
// 解析数据:每个字节的8个位对应4个时间单位的2通道数据
|
||||
// 位分布:[T3_CH1, T3_CH0, T2_CH1, T2_CH0, T1_CH1, T1_CH0, T0_CH1, T0_CH0]
|
||||
for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
|
||||
@@ -360,37 +420,34 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
||||
for (let timeUnit = 0; timeUnit < 4; timeUnit++) {
|
||||
const timeIndex = byteIndex * 4 + timeUnit;
|
||||
const bitOffset = timeUnit * 2;
|
||||
y[0][timeIndex] = (byte >> bitOffset) & 1; // CH0
|
||||
y[0][timeIndex] = (byte >> bitOffset) & 1; // CH0
|
||||
y[1][timeIndex] = (byte >> (bitOffset + 1)) & 1; // CH1
|
||||
}
|
||||
}
|
||||
} else if (channelCount === 4) {
|
||||
// 4通道:每个字节包含2个时间单位的数据
|
||||
sampleCount = bytes.length * 2;
|
||||
|
||||
|
||||
// 创建时间轴
|
||||
x = Array.from(
|
||||
{ length: sampleCount },
|
||||
(_, i) => (i * timeStepNs) / 1000,
|
||||
); // 转换为微秒
|
||||
|
||||
|
||||
// 创建通道数据数组
|
||||
y = Array.from(
|
||||
{ length: 4 },
|
||||
() => new Array(sampleCount),
|
||||
);
|
||||
|
||||
y = Array.from({ length: 4 }, () => new Array(sampleCount));
|
||||
|
||||
// 解析数据:每个字节的8个位对应2个时间单位的4通道数据
|
||||
// 位分布:[T1_CH3, T1_CH2, T1_CH1, T1_CH0, T0_CH3, T0_CH2, T0_CH1, T0_CH0]
|
||||
for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
|
||||
const byte = bytes[byteIndex];
|
||||
|
||||
|
||||
// 处理第一个时间单位(低4位)
|
||||
const timeIndex1 = byteIndex * 2;
|
||||
for (let channel = 0; channel < 4; channel++) {
|
||||
y[channel][timeIndex1] = (byte >> channel) & 1;
|
||||
}
|
||||
|
||||
|
||||
// 处理第二个时间单位(高4位)
|
||||
const timeIndex2 = byteIndex * 2 + 1;
|
||||
for (let channel = 0; channel < 4; channel++) {
|
||||
@@ -400,19 +457,16 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
||||
} else if (channelCount === 8) {
|
||||
// 8通道:每个字节包含1个时间单位的8个通道数据
|
||||
sampleCount = bytes.length;
|
||||
|
||||
|
||||
// 创建时间轴
|
||||
x = Array.from(
|
||||
{ length: sampleCount },
|
||||
(_, i) => (i * timeStepNs) / 1000,
|
||||
); // 转换为微秒
|
||||
|
||||
|
||||
// 创建8个通道的数据
|
||||
y = Array.from(
|
||||
{ length: 8 },
|
||||
() => new Array(sampleCount),
|
||||
);
|
||||
|
||||
y = Array.from({ length: 8 }, () => new Array(sampleCount));
|
||||
|
||||
// 解析每个字节的8个位到对应通道
|
||||
for (let i = 0; i < sampleCount; i++) {
|
||||
const byte = bytes[i];
|
||||
@@ -424,30 +478,27 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
||||
} else if (channelCount === 16) {
|
||||
// 16通道:每2个字节包含1个时间单位的16个通道数据
|
||||
sampleCount = bytes.length / 2;
|
||||
|
||||
|
||||
// 创建时间轴
|
||||
x = Array.from(
|
||||
{ length: sampleCount },
|
||||
(_, i) => (i * timeStepNs) / 1000,
|
||||
); // 转换为微秒
|
||||
|
||||
|
||||
// 创建16个通道的数据
|
||||
y = Array.from(
|
||||
{ length: 16 },
|
||||
() => new Array(sampleCount),
|
||||
);
|
||||
|
||||
y = Array.from({ length: 16 }, () => new Array(sampleCount));
|
||||
|
||||
// 解析数据:每2个字节为一个时间单位
|
||||
for (let timeIndex = 0; timeIndex < sampleCount; timeIndex++) {
|
||||
const byteIndex = timeIndex * 2;
|
||||
const byte1 = bytes[byteIndex]; // [7:0]
|
||||
const byte1 = bytes[byteIndex]; // [7:0]
|
||||
const byte2 = bytes[byteIndex + 1]; // [15:8]
|
||||
|
||||
|
||||
// 处理低8位通道 [7:0]
|
||||
for (let channel = 0; channel < 8; channel++) {
|
||||
y[channel][timeIndex] = (byte1 >> channel) & 1;
|
||||
}
|
||||
|
||||
|
||||
// 处理高8位通道 [15:8]
|
||||
for (let channel = 0; channel < 8; channel++) {
|
||||
y[channel + 8][timeIndex] = (byte2 >> channel) & 1;
|
||||
@@ -456,42 +507,39 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
||||
} else if (channelCount === 32) {
|
||||
// 32通道:每4个字节包含1个时间单位的32个通道数据
|
||||
sampleCount = bytes.length / 4;
|
||||
|
||||
|
||||
// 创建时间轴
|
||||
x = Array.from(
|
||||
{ length: sampleCount },
|
||||
(_, i) => (i * timeStepNs) / 1000,
|
||||
); // 转换为微秒
|
||||
|
||||
|
||||
// 创建32个通道的数据
|
||||
y = Array.from(
|
||||
{ length: 32 },
|
||||
() => new Array(sampleCount),
|
||||
);
|
||||
|
||||
y = Array.from({ length: 32 }, () => new Array(sampleCount));
|
||||
|
||||
// 解析数据:每4个字节为一个时间单位
|
||||
for (let timeIndex = 0; timeIndex < sampleCount; timeIndex++) {
|
||||
const byteIndex = timeIndex * 4;
|
||||
const byte1 = bytes[byteIndex]; // [7:0]
|
||||
const byte1 = bytes[byteIndex]; // [7:0]
|
||||
const byte2 = bytes[byteIndex + 1]; // [15:8]
|
||||
const byte3 = bytes[byteIndex + 2]; // [23:16]
|
||||
const byte4 = bytes[byteIndex + 3]; // [31:24]
|
||||
|
||||
|
||||
// 处理 [7:0]
|
||||
for (let channel = 0; channel < 8; channel++) {
|
||||
y[channel][timeIndex] = (byte1 >> channel) & 1;
|
||||
}
|
||||
|
||||
|
||||
// 处理 [15:8]
|
||||
for (let channel = 0; channel < 8; channel++) {
|
||||
y[channel + 8][timeIndex] = (byte2 >> channel) & 1;
|
||||
}
|
||||
|
||||
|
||||
// 处理 [23:16]
|
||||
for (let channel = 0; channel < 8; channel++) {
|
||||
y[channel + 16][timeIndex] = (byte3 >> channel) & 1;
|
||||
}
|
||||
|
||||
|
||||
// 处理 [31:24]
|
||||
for (let channel = 0; channel < 8; channel++) {
|
||||
y[channel + 24][timeIndex] = (byte4 >> channel) & 1;
|
||||
@@ -525,11 +573,11 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
||||
isCapturing.value = true;
|
||||
const release = await operationMutex.acquire();
|
||||
try {
|
||||
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
|
||||
const client = AuthManager.createClient(LogicAnalyzerClient);
|
||||
|
||||
// 1. 先应用配置
|
||||
alert?.info("正在应用配置...", 2000);
|
||||
|
||||
|
||||
// 准备配置数据 - 包含所有32个通道,未启用的通道设置为默认值
|
||||
const allSignals = signalConfigs.map((signal, index) => {
|
||||
if (channels[index].enabled) {
|
||||
@@ -632,7 +680,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
||||
|
||||
const release = await operationMutex.acquire();
|
||||
try {
|
||||
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
|
||||
const client = AuthManager.createClient(LogicAnalyzerClient);
|
||||
|
||||
// 执行强制捕获来停止当前捕获
|
||||
const forceSuccess = await client.setCaptureMode(false, false);
|
||||
@@ -661,7 +709,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
||||
|
||||
const release = await operationMutex.acquire();
|
||||
try {
|
||||
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
|
||||
const client = AuthManager.createClient(LogicAnalyzerClient);
|
||||
|
||||
// 执行强制捕获来停止当前捕获
|
||||
const forceSuccess = await client.setCaptureMode(true, true);
|
||||
@@ -677,7 +725,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
||||
`强制捕获失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||
3000,
|
||||
);
|
||||
} finally{
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -145,6 +145,7 @@ import {
|
||||
ChevronDownIcon,
|
||||
} from "lucide-vue-next";
|
||||
import { AuthManager } from "@/utils/AuthManager";
|
||||
import { DataClient } from "@/APIClient";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -158,7 +159,7 @@ const loadUserInfo = async () => {
|
||||
try {
|
||||
const authenticated = await AuthManager.isAuthenticated();
|
||||
if (authenticated) {
|
||||
const client = AuthManager.createAuthenticatedDataClient();
|
||||
const client = AuthManager.createClient(DataClient);
|
||||
const userInfo = await client.getUserInfo();
|
||||
userName.value = userInfo.name;
|
||||
isLoggedIn.value = true;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Mutex } from "async-mutex";
|
||||
import {
|
||||
OscilloscopeFullConfig,
|
||||
OscilloscopeDataResponse,
|
||||
OscilloscopeApiClient,
|
||||
} from "@/APIClient";
|
||||
import { AuthManager } from "@/utils/AuthManager";
|
||||
import { useAlertStore } from "@/components/Alert";
|
||||
@@ -31,257 +32,269 @@ const DEFAULT_CONFIG: OscilloscopeFullConfig = new OscilloscopeFullConfig({
|
||||
});
|
||||
|
||||
// 采样频率常量(后端返回)
|
||||
const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(() => {
|
||||
const oscData = shallowRef<OscilloscopeDataType>();
|
||||
const alert = useRequiredInjection(useAlertStore);
|
||||
const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
|
||||
() => {
|
||||
const oscData = shallowRef<OscilloscopeDataType>();
|
||||
const alert = useRequiredInjection(useAlertStore);
|
||||
|
||||
// 互斥锁
|
||||
const operationMutex = new Mutex();
|
||||
// 互斥锁
|
||||
const operationMutex = new Mutex();
|
||||
|
||||
// 状态
|
||||
const isApplying = ref(false);
|
||||
const isCapturing = ref(false);
|
||||
// 状态
|
||||
const isApplying = ref(false);
|
||||
const isCapturing = ref(false);
|
||||
|
||||
// 配置
|
||||
const config = reactive<OscilloscopeFullConfig>(new OscilloscopeFullConfig({ ...DEFAULT_CONFIG }));
|
||||
// 配置
|
||||
const config = reactive<OscilloscopeFullConfig>(
|
||||
new OscilloscopeFullConfig({ ...DEFAULT_CONFIG }),
|
||||
);
|
||||
|
||||
// 采样点数(由后端数据决定)
|
||||
const sampleCount = ref(0);
|
||||
// 采样点数(由后端数据决定)
|
||||
const sampleCount = ref(0);
|
||||
|
||||
// 采样周期(ns),由adFrequency计算
|
||||
const samplePeriodNs = computed(() =>
|
||||
oscData.value?.adFrequency ? 1_000_000_000 / oscData.value.adFrequency : 200
|
||||
);
|
||||
// 采样周期(ns),由adFrequency计算
|
||||
const samplePeriodNs = computed(() =>
|
||||
oscData.value?.adFrequency
|
||||
? 1_000_000_000 / oscData.value.adFrequency
|
||||
: 200,
|
||||
);
|
||||
|
||||
// 应用配置
|
||||
const applyConfiguration = async () => {
|
||||
if (operationMutex.isLocked()) {
|
||||
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
|
||||
return;
|
||||
}
|
||||
const release = await operationMutex.acquire();
|
||||
isApplying.value = true;
|
||||
try {
|
||||
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
|
||||
const success = await client.initialize({ ...config });
|
||||
if (success) {
|
||||
alert.success("示波器配置已应用", 2000);
|
||||
} else {
|
||||
throw new Error("应用失败");
|
||||
// 应用配置
|
||||
const applyConfiguration = async () => {
|
||||
if (operationMutex.isLocked()) {
|
||||
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
alert.error("应用配置失败", 3000);
|
||||
} finally {
|
||||
isApplying.value = false;
|
||||
release();
|
||||
}
|
||||
};
|
||||
|
||||
// 重置配置
|
||||
const resetConfiguration = () => {
|
||||
Object.assign(config, { ...DEFAULT_CONFIG });
|
||||
alert.info("配置已重置", 2000);
|
||||
};
|
||||
|
||||
const clearOscilloscopeData = () => {
|
||||
oscData.value = undefined;
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const getOscilloscopeData = async () => {
|
||||
try {
|
||||
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
|
||||
const resp: OscilloscopeDataResponse = await client.getData();
|
||||
|
||||
// 解析波形数据
|
||||
const binaryString = atob(resp.waveformData);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
const release = await operationMutex.acquire();
|
||||
isApplying.value = true;
|
||||
try {
|
||||
const client = AuthManager.createClient(OscilloscopeApiClient);
|
||||
const success = await client.initialize({ ...config });
|
||||
if (success) {
|
||||
alert.success("示波器配置已应用", 2000);
|
||||
} else {
|
||||
throw new Error("应用失败");
|
||||
}
|
||||
} catch (error) {
|
||||
alert.error("应用配置失败", 3000);
|
||||
} finally {
|
||||
isApplying.value = false;
|
||||
release();
|
||||
}
|
||||
sampleCount.value = bytes.length;
|
||||
};
|
||||
|
||||
// 构建时间轴
|
||||
// 重置配置
|
||||
const resetConfiguration = () => {
|
||||
Object.assign(config, { ...DEFAULT_CONFIG });
|
||||
alert.info("配置已重置", 2000);
|
||||
};
|
||||
|
||||
const clearOscilloscopeData = () => {
|
||||
oscData.value = undefined;
|
||||
};
|
||||
|
||||
// 获取数据
|
||||
const getOscilloscopeData = async () => {
|
||||
try {
|
||||
const client = AuthManager.createClient(OscilloscopeApiClient);
|
||||
const resp: OscilloscopeDataResponse = await client.getData();
|
||||
|
||||
// 解析波形数据
|
||||
const binaryString = atob(resp.waveformData);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
sampleCount.value = bytes.length;
|
||||
|
||||
// 构建时间轴
|
||||
const x = Array.from(
|
||||
{ length: bytes.length },
|
||||
(_, i) => (i * samplePeriodNs.value) / 1000, // us
|
||||
);
|
||||
const y = Array.from(bytes);
|
||||
|
||||
oscData.value = {
|
||||
x,
|
||||
y,
|
||||
xUnit: "us",
|
||||
yUnit: "V",
|
||||
adFrequency: resp.adFrequency,
|
||||
adVpp: resp.adVpp,
|
||||
adMax: resp.adMax,
|
||||
adMin: resp.adMin,
|
||||
};
|
||||
} catch (error) {
|
||||
alert.error("获取示波器数据失败", 3000);
|
||||
}
|
||||
};
|
||||
|
||||
// 定时器引用
|
||||
let refreshIntervalId: number | undefined;
|
||||
// 刷新间隔(毫秒),可根据需要调整
|
||||
const refreshIntervalMs = ref(1000);
|
||||
|
||||
// 定时刷新函数
|
||||
const startAutoRefresh = () => {
|
||||
if (refreshIntervalId !== undefined) return;
|
||||
refreshIntervalId = window.setInterval(async () => {
|
||||
await refreshRAM();
|
||||
await getOscilloscopeData();
|
||||
}, refreshIntervalMs.value);
|
||||
};
|
||||
|
||||
const stopAutoRefresh = () => {
|
||||
if (refreshIntervalId !== undefined) {
|
||||
clearInterval(refreshIntervalId);
|
||||
refreshIntervalId = undefined;
|
||||
isCapturing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 启动捕获
|
||||
const startCapture = async () => {
|
||||
if (operationMutex.isLocked()) {
|
||||
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
|
||||
return;
|
||||
}
|
||||
isCapturing.value = true;
|
||||
const release = await operationMutex.acquire();
|
||||
try {
|
||||
const client = AuthManager.createClient(OscilloscopeApiClient);
|
||||
const started = await client.startCapture();
|
||||
if (!started) throw new Error("无法启动捕获");
|
||||
alert.info("开始捕获...", 2000);
|
||||
|
||||
// 启动定时刷新
|
||||
startAutoRefresh();
|
||||
} catch (error) {
|
||||
alert.error("捕获失败", 3000);
|
||||
isCapturing.value = false;
|
||||
stopAutoRefresh();
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
};
|
||||
|
||||
// 停止捕获
|
||||
const stopCapture = async () => {
|
||||
if (!isCapturing.value) {
|
||||
alert.warn("当前没有正在进行的捕获操作", 2000);
|
||||
return;
|
||||
}
|
||||
isCapturing.value = false;
|
||||
stopAutoRefresh();
|
||||
const release = await operationMutex.acquire();
|
||||
try {
|
||||
const client = AuthManager.createClient(OscilloscopeApiClient);
|
||||
const stopped = await client.stopCapture();
|
||||
if (!stopped) throw new Error("无法停止捕获");
|
||||
alert.info("捕获已停止", 2000);
|
||||
} catch (error) {
|
||||
alert.error("停止捕获失败", 3000);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
};
|
||||
|
||||
// 更新触发参数
|
||||
const updateTrigger = async (level: number, risingEdge: boolean) => {
|
||||
const client = AuthManager.createClient(OscilloscopeApiClient);
|
||||
try {
|
||||
const ok = await client.updateTrigger(level, risingEdge);
|
||||
if (ok) {
|
||||
config.triggerLevel = level;
|
||||
config.triggerRisingEdge = risingEdge;
|
||||
alert.success("触发参数已更新", 2000);
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
} catch {
|
||||
alert.error("更新触发参数失败", 2000);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新采样参数
|
||||
const updateSampling = async (
|
||||
horizontalShift: number,
|
||||
decimationRate: number,
|
||||
) => {
|
||||
const client = AuthManager.createClient(OscilloscopeApiClient);
|
||||
try {
|
||||
const ok = await client.updateSampling(horizontalShift, decimationRate);
|
||||
if (ok) {
|
||||
config.horizontalShift = horizontalShift;
|
||||
config.decimationRate = decimationRate;
|
||||
alert.success("采样参数已更新", 2000);
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
} catch {
|
||||
alert.error("更新采样参数失败", 2000);
|
||||
}
|
||||
};
|
||||
|
||||
// 手动刷新RAM
|
||||
const refreshRAM = async () => {
|
||||
const client = AuthManager.createClient(OscilloscopeApiClient);
|
||||
try {
|
||||
const ok = await client.refreshRAM();
|
||||
if (ok) {
|
||||
// alert.success("RAM已刷新", 2000);
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
} catch {
|
||||
alert.error("刷新RAM失败", 2000);
|
||||
}
|
||||
};
|
||||
|
||||
// 生成测试数据
|
||||
const generateTestData = () => {
|
||||
const freq = 5_000_000;
|
||||
const duration = 0.001; // 1ms
|
||||
const points = Math.floor(freq * duration);
|
||||
const x = Array.from(
|
||||
{ length: bytes.length },
|
||||
(_, i) => (i * samplePeriodNs.value) / 1000 // us
|
||||
{ length: points },
|
||||
(_, i) => (i * 1_000_000_000) / freq / 1000,
|
||||
);
|
||||
const y = Array.from({ length: points }, (_, i) =>
|
||||
Math.floor(Math.sin(i * 0.01) * 127 + 128),
|
||||
);
|
||||
const y = Array.from(bytes);
|
||||
|
||||
oscData.value = {
|
||||
x,
|
||||
y,
|
||||
xUnit: "us",
|
||||
yUnit: "V",
|
||||
adFrequency: resp.adFrequency,
|
||||
adVpp: resp.adVpp,
|
||||
adMax: resp.adMax,
|
||||
adMin: resp.adMin,
|
||||
adFrequency: freq,
|
||||
adVpp: 2.0,
|
||||
adMax: 255,
|
||||
adMin: 0,
|
||||
};
|
||||
} catch (error) {
|
||||
alert.error("获取示波器数据失败", 3000);
|
||||
}
|
||||
};
|
||||
|
||||
// 定时器引用
|
||||
let refreshIntervalId: number | undefined;
|
||||
// 刷新间隔(毫秒),可根据需要调整
|
||||
const refreshIntervalMs = ref(1000);
|
||||
|
||||
// 定时刷新函数
|
||||
const startAutoRefresh = () => {
|
||||
if (refreshIntervalId !== undefined) return;
|
||||
refreshIntervalId = window.setInterval(async () => {
|
||||
await refreshRAM();
|
||||
await getOscilloscopeData();
|
||||
}, refreshIntervalMs.value);
|
||||
};
|
||||
|
||||
const stopAutoRefresh = () => {
|
||||
if (refreshIntervalId !== undefined) {
|
||||
clearInterval(refreshIntervalId);
|
||||
refreshIntervalId = undefined;
|
||||
isCapturing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 启动捕获
|
||||
const startCapture = async () => {
|
||||
if (operationMutex.isLocked()) {
|
||||
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
|
||||
return;
|
||||
}
|
||||
isCapturing.value = true;
|
||||
const release = await operationMutex.acquire();
|
||||
try {
|
||||
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
|
||||
const started = await client.startCapture();
|
||||
if (!started) throw new Error("无法启动捕获");
|
||||
alert.info("开始捕获...", 2000);
|
||||
|
||||
// 启动定时刷新
|
||||
startAutoRefresh();
|
||||
} catch (error) {
|
||||
alert.error("捕获失败", 3000);
|
||||
isCapturing.value = false;
|
||||
stopAutoRefresh();
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
};
|
||||
|
||||
// 停止捕获
|
||||
const stopCapture = async () => {
|
||||
if (!isCapturing.value) {
|
||||
alert.warn("当前没有正在进行的捕获操作", 2000);
|
||||
return;
|
||||
}
|
||||
isCapturing.value = false;
|
||||
stopAutoRefresh();
|
||||
const release = await operationMutex.acquire();
|
||||
try {
|
||||
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
|
||||
const stopped = await client.stopCapture();
|
||||
if (!stopped) throw new Error("无法停止捕获");
|
||||
alert.info("捕获已停止", 2000);
|
||||
} catch (error) {
|
||||
alert.error("停止捕获失败", 3000);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
};
|
||||
|
||||
// 更新触发参数
|
||||
const updateTrigger = async (level: number, risingEdge: boolean) => {
|
||||
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
|
||||
try {
|
||||
const ok = await client.updateTrigger(level, risingEdge);
|
||||
if (ok) {
|
||||
config.triggerLevel = level;
|
||||
config.triggerRisingEdge = risingEdge;
|
||||
alert.success("触发参数已更新", 2000);
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
} catch {
|
||||
alert.error("更新触发参数失败", 2000);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新采样参数
|
||||
const updateSampling = async (horizontalShift: number, decimationRate: number) => {
|
||||
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
|
||||
try {
|
||||
const ok = await client.updateSampling(horizontalShift, decimationRate);
|
||||
if (ok) {
|
||||
config.horizontalShift = horizontalShift;
|
||||
config.decimationRate = decimationRate;
|
||||
alert.success("采样参数已更新", 2000);
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
} catch {
|
||||
alert.error("更新采样参数失败", 2000);
|
||||
}
|
||||
};
|
||||
|
||||
// 手动刷新RAM
|
||||
const refreshRAM = async () => {
|
||||
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
|
||||
try {
|
||||
const ok = await client.refreshRAM();
|
||||
if (ok) {
|
||||
// alert.success("RAM已刷新", 2000);
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
} catch {
|
||||
alert.error("刷新RAM失败", 2000);
|
||||
}
|
||||
};
|
||||
|
||||
// 生成测试数据
|
||||
const generateTestData = () => {
|
||||
const freq = 5_000_000;
|
||||
const duration = 0.001; // 1ms
|
||||
const points = Math.floor(freq * duration);
|
||||
const x = Array.from({ length: points }, (_, i) => (i * 1_000_000_000 / freq) / 1000);
|
||||
const y = Array.from({ length: points }, (_, i) =>
|
||||
Math.floor(Math.sin(i * 0.01) * 127 + 128)
|
||||
);
|
||||
oscData.value = {
|
||||
x,
|
||||
y,
|
||||
xUnit: "us",
|
||||
yUnit: "V",
|
||||
adFrequency: freq,
|
||||
adVpp: 2.0,
|
||||
adMax: 255,
|
||||
adMin: 0,
|
||||
alert.success("测试数据生成成功", 2000);
|
||||
};
|
||||
alert.success("测试数据生成成功", 2000);
|
||||
};
|
||||
|
||||
return {
|
||||
oscData,
|
||||
config,
|
||||
isApplying,
|
||||
isCapturing,
|
||||
sampleCount,
|
||||
samplePeriodNs,
|
||||
refreshIntervalMs,
|
||||
return {
|
||||
oscData,
|
||||
config,
|
||||
isApplying,
|
||||
isCapturing,
|
||||
sampleCount,
|
||||
samplePeriodNs,
|
||||
refreshIntervalMs,
|
||||
|
||||
applyConfiguration,
|
||||
resetConfiguration,
|
||||
clearOscilloscopeData,
|
||||
getOscilloscopeData,
|
||||
startCapture,
|
||||
stopCapture,
|
||||
updateTrigger,
|
||||
updateSampling,
|
||||
refreshRAM,
|
||||
generateTestData,
|
||||
};
|
||||
});
|
||||
applyConfiguration,
|
||||
resetConfiguration,
|
||||
clearOscilloscopeData,
|
||||
getOscilloscopeData,
|
||||
startCapture,
|
||||
stopCapture,
|
||||
updateTrigger,
|
||||
updateSampling,
|
||||
refreshRAM,
|
||||
generateTestData,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export { useProvideOscilloscope, useOscilloscopeState, DEFAULT_CONFIG };
|
||||
export { useProvideOscilloscope, useOscilloscopeState, DEFAULT_CONFIG };
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { AuthManager } from "@/utils/AuthManager";
|
||||
import type { ExamInfo } from "@/APIClient";
|
||||
import { ExamClient, ResourceClient, type ExamInfo } from "@/APIClient";
|
||||
|
||||
// 接口定义
|
||||
interface Tutorial {
|
||||
@@ -121,7 +121,7 @@ onMounted(async () => {
|
||||
console.log("正在从数据库加载实验数据...");
|
||||
|
||||
// 创建认证客户端
|
||||
const client = AuthManager.createAuthenticatedExamClient();
|
||||
const client = AuthManager.createClient(ExamClient);
|
||||
|
||||
// 获取实验列表
|
||||
const examList: ExamInfo[] = await client.getExamList();
|
||||
@@ -142,7 +142,7 @@ onMounted(async () => {
|
||||
|
||||
try {
|
||||
// 获取实验的封面资源(模板资源)
|
||||
const resourceClient = AuthManager.createAuthenticatedResourceClient();
|
||||
const resourceClient = AuthManager.createClient(ResourceClient);
|
||||
const resourceList = await resourceClient.getResourceList(
|
||||
exam.id,
|
||||
"cover",
|
||||
|
||||
@@ -16,22 +16,32 @@
|
||||
<span class="text-sm">{{ bitstream.name }}</span>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="downloadExampleBitstream(bitstream)"
|
||||
@click="handleExampleBitstream('download', bitstream)"
|
||||
class="btn btn-sm btn-secondary"
|
||||
:disabled="isDownloading || isProgramming"
|
||||
:disabled="currentTask !== 'none'"
|
||||
>
|
||||
<div v-if="isDownloading">
|
||||
<div
|
||||
v-if="
|
||||
currentTask === 'downloading' &&
|
||||
currentBitstreamId === bitstream.id
|
||||
"
|
||||
>
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{{ downloadProgress }}%
|
||||
下载中...
|
||||
</div>
|
||||
<div v-else>下载示例</div>
|
||||
</button>
|
||||
<button
|
||||
@click="programExampleBitstream(bitstream)"
|
||||
@click="handleExampleBitstream('program', bitstream)"
|
||||
class="btn btn-sm btn-primary"
|
||||
:disabled="isDownloading || isProgramming"
|
||||
:disabled="currentTask !== 'none'"
|
||||
>
|
||||
<div v-if="isProgramming">
|
||||
<div
|
||||
v-if="
|
||||
currentTask === 'programming' &&
|
||||
currentBitstreamId === bitstream.id
|
||||
"
|
||||
>
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
烧录中...
|
||||
</div>
|
||||
@@ -63,14 +73,18 @@
|
||||
<!-- Upload Button -->
|
||||
<div class="card-actions w-full">
|
||||
<button
|
||||
@click="handleClick"
|
||||
@click="handleUploadAndDownload"
|
||||
class="btn btn-primary grow"
|
||||
:disabled="isUploading || isProgramming"
|
||||
:disabled="currentTask !== 'none'"
|
||||
>
|
||||
<div v-if="isUploading">
|
||||
<div v-if="currentTask === 'uploading'">
|
||||
<span class="loading loading-spinner"></span>
|
||||
上传中...
|
||||
</div>
|
||||
<div v-else-if="currentTask === 'programming'">
|
||||
<span class="loading loading-spinner"></span>
|
||||
{{ currentProgressPercent }}% ...
|
||||
</div>
|
||||
<div v-else>上传并下载</div>
|
||||
</button>
|
||||
</div>
|
||||
@@ -78,27 +92,19 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, useTemplateRef, onMounted } from "vue";
|
||||
import { AuthManager } from "@/utils/AuthManager"; // Adjust the path based on your project structure
|
||||
import { ref, useTemplateRef, onMounted } from "vue";
|
||||
import { AuthManager } from "@/utils/AuthManager";
|
||||
import { useDialogStore } from "@/stores/dialog";
|
||||
import { isNull, isUndefined } from "lodash";
|
||||
import { useEquipments } from "@/stores/equipments";
|
||||
import type { HubConnection } from "@microsoft/signalr";
|
||||
import type {
|
||||
IProgressHub,
|
||||
IProgressReceiver,
|
||||
} from "@/utils/signalR/TypedSignalR.Client/server.Hubs";
|
||||
import {
|
||||
getHubProxyFactory,
|
||||
getReceiverRegister,
|
||||
} from "@/utils/signalR/TypedSignalR.Client";
|
||||
import { ProgressStatus } from "@/utils/signalR/server.Hubs";
|
||||
import { useRequiredInjection } from "@/utils/Common";
|
||||
import { useAlertStore } from "./Alert";
|
||||
import { ResourceClient, ResourcePurpose } from "@/APIClient";
|
||||
import { useProgressStore } from "@/stores/progress";
|
||||
import { ProgressStatus, type ProgressInfo } from "@/utils/signalR/server.Hubs";
|
||||
|
||||
interface Props {
|
||||
maxMemory?: number;
|
||||
examId?: string; // 新增examId属性
|
||||
examId?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -111,203 +117,166 @@ const emits = defineEmits<{
|
||||
}>();
|
||||
|
||||
const alert = useRequiredInjection(useAlertStore);
|
||||
const progressTracker = useProgressStore();
|
||||
const dialog = useDialogStore();
|
||||
const eqps = useEquipments();
|
||||
|
||||
const isUploading = ref(false);
|
||||
const isDownloading = ref(false);
|
||||
const isProgramming = ref(false);
|
||||
const availableBitstreams = ref<{ id: number; name: string }[]>([]);
|
||||
|
||||
// Progress
|
||||
const downloadTaskId = ref("");
|
||||
const downloadProgress = ref(0);
|
||||
const progressHubConnection = ref<HubConnection>();
|
||||
const progressHubProxy = ref<IProgressHub>();
|
||||
const progressHubReceiver: IProgressReceiver = {
|
||||
onReceiveProgress: async (msg) => {
|
||||
if (msg.taskId == downloadTaskId.value) {
|
||||
if (msg.status == ProgressStatus.InProgress) {
|
||||
downloadProgress.value = msg.progressPercent;
|
||||
} else if (msg.status == ProgressStatus.Failed) {
|
||||
dialog.error(msg.errorMessage);
|
||||
} else if (msg.status == ProgressStatus.Completed) {
|
||||
alert.info("比特流下载成功");
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
onMounted(async () => {
|
||||
progressHubConnection.value =
|
||||
AuthManager.createAuthenticatedProgressHubConnection();
|
||||
progressHubProxy.value = getHubProxyFactory("IProgressHub").createHubProxy(
|
||||
progressHubConnection.value,
|
||||
);
|
||||
getReceiverRegister("IProgressReceiver").register(
|
||||
progressHubConnection.value,
|
||||
progressHubReceiver,
|
||||
);
|
||||
});
|
||||
|
||||
const availableBitstreams = ref<{ id: string; name: string }[]>([]);
|
||||
const fileInput = useTemplateRef("fileInput");
|
||||
const bitstream = defineModel("bitstreamFile", {
|
||||
type: File,
|
||||
default: undefined,
|
||||
});
|
||||
const bitstream = ref<File | undefined>(undefined);
|
||||
|
||||
// 用一个状态变量替代多个
|
||||
const currentTask = ref<"none" | "uploading" | "downloading" | "programming">(
|
||||
"none",
|
||||
);
|
||||
const currentBitstreamId = ref<string>("");
|
||||
const currentProgressId = ref<string>("");
|
||||
const currentProgressPercent = ref<number>(0);
|
||||
|
||||
// 初始化时加载示例比特流
|
||||
onMounted(async () => {
|
||||
if (!isUndefined(bitstream.value) && !isNull(fileInput.value)) {
|
||||
if (bitstream.value && fileInput.value) {
|
||||
let fileList = new DataTransfer();
|
||||
fileList.items.add(bitstream.value);
|
||||
fileInput.value.files = fileList.files;
|
||||
}
|
||||
|
||||
await loadAvailableBitstreams();
|
||||
});
|
||||
|
||||
// 加载可用的比特流文件列表
|
||||
async function loadAvailableBitstreams() {
|
||||
console.log("加载可用比特流文件,examId:", props.examId);
|
||||
if (!props.examId) {
|
||||
availableBitstreams.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resourceClient = AuthManager.createAuthenticatedResourceClient();
|
||||
// 使用新的ResourceClient API获取比特流模板资源列表
|
||||
const resources = await resourceClient.getResourceList(
|
||||
props.examId,
|
||||
"bitstream",
|
||||
"template",
|
||||
);
|
||||
availableBitstreams.value =
|
||||
resources.map((r) => ({ id: r.id, name: r.name })) || [];
|
||||
} catch (error) {
|
||||
console.error("加载比特流列表失败:", error);
|
||||
availableBitstreams.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 下载示例比特流
|
||||
async function downloadExampleBitstream(bitstream: {
|
||||
id: number;
|
||||
name: string;
|
||||
}) {
|
||||
if (isDownloading.value) return;
|
||||
|
||||
isDownloading.value = true;
|
||||
try {
|
||||
const resourceClient = AuthManager.createAuthenticatedResourceClient();
|
||||
|
||||
// 使用新的ResourceClient API获取资源文件
|
||||
const response = await resourceClient.getResourceById(bitstream.id);
|
||||
|
||||
if (response && response.data) {
|
||||
// 创建下载链接
|
||||
const url = URL.createObjectURL(response.data);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = response.fileName || bitstream.name;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
dialog.info("示例比特流下载成功");
|
||||
} else {
|
||||
dialog.error("下载失败:响应数据为空");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("下载示例比特流失败:", error);
|
||||
dialog.error("下载示例比特流失败");
|
||||
} finally {
|
||||
isDownloading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 直接烧录示例比特流
|
||||
async function programExampleBitstream(bitstream: {
|
||||
id: number;
|
||||
name: string;
|
||||
}) {
|
||||
if (isProgramming.value) return;
|
||||
|
||||
isProgramming.value = true;
|
||||
try {
|
||||
const downloadTaskId = await eqps.jtagDownloadBitstream(bitstream.id);
|
||||
} catch (error) {
|
||||
console.error("烧录示例比特流失败:", error);
|
||||
dialog.error("烧录示例比特流失败");
|
||||
} finally {
|
||||
isProgramming.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileChange(event: Event): void {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0]; // 获取选中的第一个文件
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
bitstream.value = file;
|
||||
const file = target.files?.[0];
|
||||
bitstream.value = file || undefined;
|
||||
}
|
||||
|
||||
function checkFile(file: File): boolean {
|
||||
const maxBytes = props.maxMemory! * 1024 * 1024; // 将最大容量从 MB 转换为字节
|
||||
if (file.size > maxBytes) {
|
||||
function checkFileInput(): boolean {
|
||||
if (!bitstream.value) {
|
||||
dialog.error(`未选择文件`);
|
||||
return false;
|
||||
}
|
||||
const maxBytes = props.maxMemory! * 1024 * 1024;
|
||||
if (bitstream.value.size > maxBytes) {
|
||||
dialog.error(`文件大小超过最大限制: ${props.maxMemory}MB`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleClick(event: Event): Promise<void> {
|
||||
console.log("上传按钮被点击");
|
||||
if (isNull(bitstream.value) || isUndefined(bitstream.value)) {
|
||||
dialog.error(`未选择文件`);
|
||||
async function downloadBitstream() {
|
||||
currentTask.value = "programming";
|
||||
try {
|
||||
currentProgressId.value = await eqps.jtagDownloadBitstream(
|
||||
currentBitstreamId.value,
|
||||
);
|
||||
progressTracker.register(
|
||||
currentProgressId.value,
|
||||
"programBitstream",
|
||||
handleProgressUpdate,
|
||||
);
|
||||
} catch {
|
||||
dialog.error("比特流烧录失败");
|
||||
cleanProgressTracker();
|
||||
}
|
||||
}
|
||||
|
||||
function cleanProgressTracker() {
|
||||
currentTask.value = "none";
|
||||
currentProgressId.value = "";
|
||||
currentBitstreamId.value = "";
|
||||
currentProgressPercent.value = 0;
|
||||
progressTracker.unregister(currentProgressId.value, "programBitstream");
|
||||
}
|
||||
|
||||
async function loadAvailableBitstreams() {
|
||||
if (!props.examId) {
|
||||
availableBitstreams.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkFile(bitstream.value)) return;
|
||||
|
||||
isUploading.value = true;
|
||||
let uploadedBitstreamId: number | null = null;
|
||||
try {
|
||||
console.log("开始上传比特流文件:", bitstream.value.name);
|
||||
const bitstreamId = await eqps.jtagUploadBitstream(
|
||||
bitstream.value,
|
||||
const resourceClient = AuthManager.createClient(ResourceClient);
|
||||
const resources = await resourceClient.getResourceList(
|
||||
props.examId,
|
||||
"bitstream",
|
||||
ResourcePurpose.Template,
|
||||
);
|
||||
availableBitstreams.value =
|
||||
resources.map((r) => ({ id: r.id, name: r.name })) || [];
|
||||
} catch (error) {
|
||||
availableBitstreams.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 统一处理示例比特流的下载/烧录
|
||||
async function handleExampleBitstream(
|
||||
action: "download" | "program",
|
||||
bitstreamObj: { id: string; name: string },
|
||||
) {
|
||||
if (currentTask.value !== "none") return;
|
||||
currentBitstreamId.value = bitstreamObj.id;
|
||||
if (action === "download") {
|
||||
currentTask.value = "downloading";
|
||||
try {
|
||||
const resourceClient = AuthManager.createClient(ResourceClient);
|
||||
const response = await resourceClient.getResourceById(bitstreamObj.id);
|
||||
if (response && response.data) {
|
||||
const url = URL.createObjectURL(response.data);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = response.fileName || bitstreamObj.name;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
alert.info("示例比特流下载成功");
|
||||
} else {
|
||||
alert.error("下载失败:响应数据为空");
|
||||
}
|
||||
} catch {
|
||||
alert.error("下载示例比特流失败");
|
||||
} finally {
|
||||
currentTask.value = "none";
|
||||
currentBitstreamId.value = "";
|
||||
}
|
||||
} else if (action === "program") {
|
||||
currentBitstreamId.value = bitstreamObj.id;
|
||||
await downloadBitstream();
|
||||
}
|
||||
}
|
||||
|
||||
// 上传并下载
|
||||
async function handleUploadAndDownload() {
|
||||
if (currentTask.value !== "none") return;
|
||||
if (!checkFileInput()) return;
|
||||
|
||||
currentTask.value = "uploading";
|
||||
let uploadedBitstreamId: string | null = null;
|
||||
try {
|
||||
uploadedBitstreamId = await eqps.jtagUploadBitstream(
|
||||
bitstream.value!,
|
||||
props.examId || "",
|
||||
);
|
||||
console.log("上传结果,ID:", bitstreamId);
|
||||
if (bitstreamId === null || bitstreamId === undefined) {
|
||||
isUploading.value = false;
|
||||
return;
|
||||
}
|
||||
uploadedBitstreamId = bitstreamId;
|
||||
} catch (e) {
|
||||
if (!uploadedBitstreamId) throw new Error("上传失败");
|
||||
emits("finishedUpload", bitstream.value!);
|
||||
} catch {
|
||||
dialog.error("上传失败");
|
||||
console.error(e);
|
||||
currentTask.value = "none";
|
||||
return;
|
||||
}
|
||||
isUploading.value = false;
|
||||
|
||||
// Download
|
||||
try {
|
||||
console.log("开始下载比特流,ID:", uploadedBitstreamId);
|
||||
if (uploadedBitstreamId === null || uploadedBitstreamId === undefined) {
|
||||
dialog.error("uploadedBitstreamId is null or undefined");
|
||||
} else {
|
||||
isDownloading.value = true;
|
||||
downloadTaskId.value =
|
||||
await eqps.jtagDownloadBitstream(uploadedBitstreamId);
|
||||
}
|
||||
} catch (e) {
|
||||
dialog.error("下载失败");
|
||||
console.error(e);
|
||||
currentBitstreamId.value = uploadedBitstreamId;
|
||||
|
||||
await downloadBitstream();
|
||||
}
|
||||
|
||||
function handleProgressUpdate(msg: ProgressInfo) {
|
||||
// console.log(msg);
|
||||
if (msg.status === ProgressStatus.Running)
|
||||
currentProgressPercent.value = msg.progressPercent;
|
||||
else if (msg.status === ProgressStatus.Failed) {
|
||||
dialog.error(`比特流烧录失败: ${msg.errorMessage}`);
|
||||
cleanProgressTracker();
|
||||
} else if (msg.status === ProgressStatus.Completed) {
|
||||
dialog.info("比特流烧录成功");
|
||||
cleanProgressTracker();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -212,6 +212,7 @@ import { useEquipments } from "@/stores/equipments";
|
||||
import { useDialogStore } from "@/stores/dialog";
|
||||
import { toInteger } from "lodash";
|
||||
import { AuthManager } from "@/utils/AuthManager";
|
||||
import { DDSClient } from "@/APIClient";
|
||||
|
||||
// Component Attributes
|
||||
const props = defineProps<{
|
||||
@@ -221,7 +222,7 @@ const props = defineProps<{
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
// Global varibles
|
||||
const dds = AuthManager.createAuthenticatedDDSClient();
|
||||
const dds = AuthManager.createClient(DDSClient);
|
||||
const eqps = useEquipments();
|
||||
const dialog = useDialogStore();
|
||||
|
||||
|
||||
@@ -1,65 +1,114 @@
|
||||
<template>
|
||||
<div class="seven-segment-display" :style="{
|
||||
width: width + 'px',
|
||||
height: height + 'px',
|
||||
position: 'relative',
|
||||
}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" viewBox="0 0 120 220" class="display">
|
||||
<div
|
||||
class="seven-segment-display"
|
||||
:style="{
|
||||
width: width + 'px',
|
||||
height: height + 'px',
|
||||
position: 'relative',
|
||||
}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="width"
|
||||
:height="height"
|
||||
viewBox="0 0 120 220"
|
||||
class="display"
|
||||
>
|
||||
<!-- 数码管基座 -->
|
||||
<rect width="120" height="180" x="0" y="0" fill="#222" rx="10" ry="10" />
|
||||
<rect width="110" height="170" x="5" y="5" fill="#333" rx="5" ry="5" />
|
||||
<!-- 7段 + 小数点,每个段由多边形表示,重新设计点位置使其更接近实际数码管 -->
|
||||
<!-- a段 (顶部横线) -->
|
||||
<polygon :points="'30,20 90,20 98,28 82,36 38,36 22,28'"
|
||||
<polygon
|
||||
:points="'30,20 90,20 98,28 82,36 38,36 22,28'"
|
||||
:fill="isSegmentActive('a') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('a') ? 1 : 0.15 }" class="segment" />
|
||||
:style="{ opacity: isSegmentActive('a') ? 1 : 0.15 }"
|
||||
class="segment"
|
||||
/>
|
||||
|
||||
<!-- b段 (右上竖线) -->
|
||||
<polygon :points="'100,30 108,38 108,82 100,90 92,82 92,38'"
|
||||
<polygon
|
||||
:points="'100,30 108,38 108,82 100,90 92,82 92,38'"
|
||||
:fill="isSegmentActive('b') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('b') ? 1 : 0.15 }" class="segment" />
|
||||
:style="{ opacity: isSegmentActive('b') ? 1 : 0.15 }"
|
||||
class="segment"
|
||||
/>
|
||||
|
||||
<!-- c段 (右下竖线) -->
|
||||
<polygon :points="'100,90 108,98 108,142 100,150 92,142 92,98'"
|
||||
<polygon
|
||||
:points="'100,90 108,98 108,142 100,150 92,142 92,98'"
|
||||
:fill="isSegmentActive('c') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('c') ? 1 : 0.15 }" class="segment" />
|
||||
:style="{ opacity: isSegmentActive('c') ? 1 : 0.15 }"
|
||||
class="segment"
|
||||
/>
|
||||
|
||||
<!-- d段 (底部横线) -->
|
||||
<polygon :points="'30,160 90,160 98,152 82,144 38,144 22,152'"
|
||||
<polygon
|
||||
:points="'30,160 90,160 98,152 82,144 38,144 22,152'"
|
||||
:fill="isSegmentActive('d') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('d') ? 1 : 0.15 }" class="segment" />
|
||||
:style="{ opacity: isSegmentActive('d') ? 1 : 0.15 }"
|
||||
class="segment"
|
||||
/>
|
||||
|
||||
<!-- e段 (左下竖线) -->
|
||||
<polygon :points="'20,90 28,98 28,142 20,150 12,142 12,98'"
|
||||
<polygon
|
||||
:points="'20,90 28,98 28,142 20,150 12,142 12,98'"
|
||||
:fill="isSegmentActive('e') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('e') ? 1 : 0.15 }" class="segment" />
|
||||
:style="{ opacity: isSegmentActive('e') ? 1 : 0.15 }"
|
||||
class="segment"
|
||||
/>
|
||||
|
||||
<!-- f段 (左上竖线) -->
|
||||
<polygon :points="'20,30 28,38 28,82 20,90 12,82 12,38'"
|
||||
<polygon
|
||||
:points="'20,30 28,38 28,82 20,90 12,82 12,38'"
|
||||
:fill="isSegmentActive('f') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('f') ? 1 : 0.15 }" class="segment" />
|
||||
:style="{ opacity: isSegmentActive('f') ? 1 : 0.15 }"
|
||||
class="segment"
|
||||
/>
|
||||
|
||||
<!-- g段 (中间横线) -->
|
||||
<polygon :points="'30,90 38,82 82,82 90,90 82,98 38,98'"
|
||||
<polygon
|
||||
:points="'30,90 38,82 82,82 90,90 82,98 38,98'"
|
||||
:fill="isSegmentActive('g') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('g') ? 1 : 0.15 }" class="segment" />
|
||||
:style="{ opacity: isSegmentActive('g') ? 1 : 0.15 }"
|
||||
class="segment"
|
||||
/>
|
||||
<!-- dp段 (小数点) -->
|
||||
<circle cx="108" cy="154" r="6" :fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }" class="segment" />
|
||||
<circle
|
||||
cx="108"
|
||||
cy="154"
|
||||
r="6"
|
||||
:fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }"
|
||||
class="segment"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- 引脚 -->
|
||||
<div v-for="pin in pins" :key="pin.pinId" :style="{
|
||||
position: 'absolute',
|
||||
left: `${pin.x * props.size}px`,
|
||||
top: `${pin.y * props.size}px`,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}" :data-pin-wrapper="`${pin.pinId}`" :data-pin-x="`${pin.x * props.size}`"
|
||||
:data-pin-y="`${pin.y * props.size}`">
|
||||
<Pin :ref="(el) => {
|
||||
if (el) pinRefs[pin.pinId] = el;
|
||||
}
|
||||
" :label="pin.pinId" :constraint="pin.constraint" :pinId="pin.pinId" @pin-click="$emit('pin-click', $event)" />
|
||||
<div
|
||||
v-for="pin in pins"
|
||||
:key="pin.pinId"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: `${pin.x * props.size}px`,
|
||||
top: `${pin.y * props.size}px`,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}"
|
||||
:data-pin-wrapper="`${pin.pinId}`"
|
||||
:data-pin-x="`${pin.x * props.size}`"
|
||||
:data-pin-y="`${pin.y * props.size}`"
|
||||
>
|
||||
<Pin
|
||||
:ref="
|
||||
(el) => {
|
||||
if (el) pinRefs[pin.pinId] = el;
|
||||
}
|
||||
"
|
||||
:label="pin.pinId"
|
||||
:constraint="pin.constraint"
|
||||
:pinId="pin.pinId"
|
||||
@pin-click="$emit('pin-click', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -217,12 +266,12 @@ function isSegmentActive(
|
||||
if (isInAfterglowMode.value) {
|
||||
return afterglowStates.value[segment];
|
||||
}
|
||||
|
||||
|
||||
// 如果COM口未激活,所有段都不显示
|
||||
if (!currentComActive.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 否则使用稳定状态
|
||||
return stableSegmentStates.value[segment];
|
||||
}
|
||||
@@ -232,7 +281,7 @@ function updateSegmentStates() {
|
||||
// 先获取COM口状态
|
||||
const comPin = props.pins.find((p) => p.pinId === "COM");
|
||||
let comActive = false; // 默认未激活
|
||||
|
||||
|
||||
if (comPin && comPin.constraint) {
|
||||
const comState = getConstraintState(comPin.constraint);
|
||||
if (props.cathodeType === "anode") {
|
||||
@@ -274,7 +323,8 @@ function updateSegmentStates() {
|
||||
for (const pin of props.pins) {
|
||||
if (["a", "b", "c", "d", "e", "f", "g", "dp"].includes(pin.pinId)) {
|
||||
if (!pin.constraint) {
|
||||
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = false;
|
||||
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] =
|
||||
false;
|
||||
continue;
|
||||
}
|
||||
const pinState = getConstraintState(pin.constraint);
|
||||
@@ -285,7 +335,8 @@ function updateSegmentStates() {
|
||||
newState = pinState === "low";
|
||||
}
|
||||
// 段状态只有在COM激活时才有效
|
||||
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = newState;
|
||||
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] =
|
||||
newState;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,22 +379,25 @@ function updateAfterglowBuffers() {
|
||||
// 进入余晖模式
|
||||
function enterAfterglowMode() {
|
||||
isInAfterglowMode.value = true;
|
||||
|
||||
|
||||
// 保存当前稳定状态作为余晖状态
|
||||
for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
|
||||
const typedSegmentId = segmentId as keyof typeof stableSegmentStates.value;
|
||||
afterglowStates.value[typedSegmentId] = stableSegmentStates.value[typedSegmentId];
|
||||
|
||||
afterglowStates.value[typedSegmentId] =
|
||||
stableSegmentStates.value[typedSegmentId];
|
||||
|
||||
// 设置定时器,在余晖持续时间后退出余晖模式
|
||||
if (afterglowTimers.value[segmentId]) {
|
||||
clearTimeout(afterglowTimers.value[segmentId]!);
|
||||
}
|
||||
|
||||
|
||||
afterglowTimers.value[segmentId] = setTimeout(() => {
|
||||
afterglowStates.value[typedSegmentId] = false;
|
||||
|
||||
|
||||
// 检查是否所有段都已经关闭
|
||||
const allSegmentsOff = Object.values(afterglowStates.value).every(state => !state);
|
||||
const allSegmentsOff = Object.values(afterglowStates.value).every(
|
||||
(state) => !state,
|
||||
);
|
||||
if (allSegmentsOff) {
|
||||
exitAfterglowMode();
|
||||
}
|
||||
@@ -354,14 +408,14 @@ function enterAfterglowMode() {
|
||||
// 退出余晖模式
|
||||
function exitAfterglowMode() {
|
||||
isInAfterglowMode.value = false;
|
||||
|
||||
|
||||
// 清除所有定时器
|
||||
for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
|
||||
if (afterglowTimers.value[segmentId]) {
|
||||
clearTimeout(afterglowTimers.value[segmentId]!);
|
||||
afterglowTimers.value[segmentId] = null;
|
||||
}
|
||||
|
||||
|
||||
// 重置余晖状态
|
||||
const typedSegmentId = segmentId as keyof typeof afterglowStates.value;
|
||||
afterglowStates.value[typedSegmentId] = false;
|
||||
@@ -397,11 +451,6 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 暴露属性和方法
|
||||
defineExpose({
|
||||
updateSegmentStates,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -418,7 +467,8 @@ defineExpose({
|
||||
|
||||
/* 数码管发光效果 */
|
||||
.segment[style*="opacity: 1"] {
|
||||
filter: drop-shadow(0 0 4px v-bind(segmentColor)) drop-shadow(0 0 2px v-bind(segmentColor));
|
||||
filter: drop-shadow(0 0 4px v-bind(segmentColor))
|
||||
drop-shadow(0 0 2px v-bind(segmentColor));
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
413
src/components/equipments/SevenSegmentDisplayUltimate.vue
Normal file
413
src/components/equipments/SevenSegmentDisplayUltimate.vue
Normal file
@@ -0,0 +1,413 @@
|
||||
<template>
|
||||
<div
|
||||
class="seven-segment-display"
|
||||
:style="{
|
||||
width: width + 'px',
|
||||
height: height + 'px',
|
||||
position: 'relative',
|
||||
}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="width"
|
||||
:height="height"
|
||||
viewBox="0 0 120 220"
|
||||
class="display"
|
||||
>
|
||||
<!-- 数码管基座 -->
|
||||
<rect width="120" height="180" x="0" y="0" fill="#222" rx="10" ry="10" />
|
||||
<rect width="110" height="170" x="5" y="5" fill="#333" rx="5" ry="5" />
|
||||
|
||||
<!-- 7段显示 -->
|
||||
<polygon
|
||||
v-for="(segment, id) in segmentPaths"
|
||||
:key="id"
|
||||
:points="segment.points"
|
||||
:fill="isSegmentActive(id) ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive(id) ? 1 : 0.15 }"
|
||||
:class="{ segment: true, active: isSegmentActive(id) }"
|
||||
/>
|
||||
|
||||
<!-- 小数点 -->
|
||||
<circle
|
||||
cx="108"
|
||||
cy="154"
|
||||
r="6"
|
||||
:fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }"
|
||||
:class="{ segment: true, active: isSegmentActive('dp') }"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- 引脚(仅在非数字孪生模式下显示) -->
|
||||
<div
|
||||
v-if="!props.enableDigitalTwin"
|
||||
v-for="pin in props.pins"
|
||||
:key="pin.pinId"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: `${pin.x * props.size}px`,
|
||||
top: `${pin.y * props.size}px`,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}"
|
||||
:data-pin-wrapper="`${pin.pinId}`"
|
||||
:data-pin-x="`${pin.x * props.size}`"
|
||||
:data-pin-y="`${pin.y * props.size}`"
|
||||
>
|
||||
<Pin
|
||||
:ref="
|
||||
(el) => {
|
||||
if (el) pinRefs[pin.pinId] = el;
|
||||
}
|
||||
"
|
||||
:label="pin.pinId"
|
||||
:constraint="pin.constraint"
|
||||
:pinId="pin.pinId"
|
||||
@pin-click="$emit('pin-click', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
|
||||
import { useConstraintsStore } from "../../stores/constraints";
|
||||
import Pin from "./Pin.vue";
|
||||
import { useEquipments } from "@/stores/equipments";
|
||||
|
||||
// ============================================================================
|
||||
// Linus式极简数据结构:一个byte解决一切
|
||||
// ============================================================================
|
||||
|
||||
interface Props {
|
||||
size?: number;
|
||||
color?: string;
|
||||
enableDigitalTwin?: boolean;
|
||||
digitalTwinNum?: number;
|
||||
afterglowDuration?: number;
|
||||
cathodeType?: "common" | "anode";
|
||||
pins?: Array<{
|
||||
pinId: string;
|
||||
constraint: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 1,
|
||||
color: "red",
|
||||
enableDigitalTwin: false,
|
||||
digitalTwinNum: 0,
|
||||
afterglowDuration: 500,
|
||||
cathodeType: "common",
|
||||
pins: () => [
|
||||
{ pinId: "a", constraint: "", x: 10, y: 170 },
|
||||
{ pinId: "b", constraint: "", x: 24, y: 170 },
|
||||
{ pinId: "c", constraint: "", x: 38, y: 170 },
|
||||
{ pinId: "d", constraint: "", x: 52, y: 170 },
|
||||
{ pinId: "e", constraint: "", x: 66, y: 170 },
|
||||
{ pinId: "f", constraint: "", x: 80, y: 170 },
|
||||
{ pinId: "g", constraint: "", x: 94, y: 170 },
|
||||
{ pinId: "dp", constraint: "", x: 108, y: 170 },
|
||||
{ pinId: "COM", constraint: "", x: 60, y: 10 },
|
||||
],
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// 核心状态:简单到极致
|
||||
// ============================================================================
|
||||
|
||||
// 当前显示状态 - 8bit对应8个段
|
||||
const displayByte = ref<number>(0);
|
||||
|
||||
// 余晖状态
|
||||
const afterglowByte = ref<number>(0);
|
||||
const afterglowTimer = ref<number | null>(null);
|
||||
|
||||
// 约束系统状态(兼容模式)
|
||||
const constraintStates = ref<Record<string, boolean>>({
|
||||
a: false,
|
||||
b: false,
|
||||
c: false,
|
||||
d: false,
|
||||
e: false,
|
||||
f: false,
|
||||
g: false,
|
||||
dp: false,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Bit操作:硬件工程师的好品味
|
||||
// ============================================================================
|
||||
|
||||
// 段到bit位的映射 (标准7段数码管编码)
|
||||
const SEGMENT_BITS = {
|
||||
a: 0, // bit 0
|
||||
b: 1, // bit 1
|
||||
c: 2, // bit 2
|
||||
d: 3, // bit 3
|
||||
e: 4, // bit 4
|
||||
f: 5, // bit 5
|
||||
g: 6, // bit 6
|
||||
dp: 7, // bit 7
|
||||
} as const;
|
||||
|
||||
function isBitSet(byte: number, bit: number): boolean {
|
||||
return (byte & (1 << bit)) !== 0;
|
||||
}
|
||||
|
||||
function isSegmentActive(segmentId: keyof typeof SEGMENT_BITS): boolean {
|
||||
if (props.enableDigitalTwin) {
|
||||
// 数字孪生模式:余晖优先,然后是当前byte
|
||||
const bit = SEGMENT_BITS[segmentId];
|
||||
return (
|
||||
isBitSet(afterglowByte.value, bit) || isBitSet(displayByte.value, bit)
|
||||
);
|
||||
} else {
|
||||
// 约束模式:使用传统逻辑
|
||||
return constraintStates.value[segmentId] || false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SignalR数字孪生集成
|
||||
// ============================================================================
|
||||
|
||||
const eqps = useEquipments();
|
||||
|
||||
async function initDigitalTwin() {
|
||||
if (
|
||||
!props.enableDigitalTwin ||
|
||||
props.digitalTwinNum < 0 ||
|
||||
props.digitalTwinNum > 31
|
||||
)
|
||||
return;
|
||||
|
||||
try {
|
||||
eqps.sevenSegmentDisplaySetOnOff(props.enableDigitalTwin);
|
||||
|
||||
console.log(
|
||||
`Digital twin initialized for address: ${props.digitalTwinNum}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn("Failed to initialize digital twin:", error);
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [eqps.sevenSegmentDisplayData],
|
||||
() => {
|
||||
if (
|
||||
!eqps.sevenSegmentDisplayData ||
|
||||
props.digitalTwinNum < 0 ||
|
||||
props.digitalTwinNum > 31
|
||||
)
|
||||
return;
|
||||
|
||||
handleDigitalTwinData(eqps.sevenSegmentDisplayData[props.digitalTwinNum]);
|
||||
},
|
||||
);
|
||||
|
||||
function handleDigitalTwinData(data: any) {
|
||||
let newByte = 0;
|
||||
|
||||
if (typeof data === "number") {
|
||||
// 直接是byte数据
|
||||
newByte = data & 0xff; // 确保只取低8位
|
||||
} else if (data && typeof data.value === "number") {
|
||||
// 包装在对象中的byte数据
|
||||
newByte = data.value & 0xff;
|
||||
} else if (data && data.segments) {
|
||||
// 段状态对象格式
|
||||
Object.keys(SEGMENT_BITS).forEach((segment) => {
|
||||
if (data.segments[segment]) {
|
||||
newByte |= 1 << SEGMENT_BITS[segment as keyof typeof SEGMENT_BITS];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateDisplayByte(newByte);
|
||||
}
|
||||
|
||||
function updateDisplayByte(newByte: number) {
|
||||
const oldByte = displayByte.value;
|
||||
displayByte.value = newByte;
|
||||
|
||||
// 启动余晖效果
|
||||
if (oldByte !== 0 && newByte !== oldByte) {
|
||||
startAfterglow(oldByte);
|
||||
}
|
||||
}
|
||||
|
||||
function startAfterglow(byte: number) {
|
||||
afterglowByte.value = byte;
|
||||
|
||||
if (afterglowTimer.value) {
|
||||
clearTimeout(afterglowTimer.value);
|
||||
}
|
||||
|
||||
afterglowTimer.value = setTimeout(() => {
|
||||
afterglowByte.value = 0;
|
||||
afterglowTimer.value = null;
|
||||
}, props.afterglowDuration);
|
||||
}
|
||||
|
||||
function cleanupDigitalTwin() {
|
||||
eqps.sevenSegmentDisplaySetOnOff(false);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 约束系统兼容(传统模式)
|
||||
// ============================================================================
|
||||
|
||||
const { getConstraintState, onConstraintStateChange } = useConstraintsStore();
|
||||
let constraintUnsubscribe: (() => void) | null = null;
|
||||
|
||||
function updateConstraintStates() {
|
||||
if (props.enableDigitalTwin) return; // 数字孪生模式下忽略约束
|
||||
|
||||
// 获取COM状态
|
||||
const comPin = props.pins.find((p) => p.pinId === "COM");
|
||||
const comActive = isComActive(comPin);
|
||||
|
||||
if (!comActive) {
|
||||
// COM不活跃,所有段关闭
|
||||
Object.keys(constraintStates.value).forEach((key) => {
|
||||
constraintStates.value[key] = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新各段状态
|
||||
props.pins.forEach((pin) => {
|
||||
if (Object.hasOwnProperty.call(SEGMENT_BITS, pin.pinId)) {
|
||||
constraintStates.value[pin.pinId] = isPinActive(pin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isComActive(comPin: any): boolean {
|
||||
if (!comPin?.constraint) return true;
|
||||
const state = getConstraintState(comPin.constraint);
|
||||
return props.cathodeType === "common" ? state === "low" : state === "low";
|
||||
}
|
||||
|
||||
function isPinActive(pin: any): boolean {
|
||||
if (!pin.constraint) return false;
|
||||
const state = getConstraintState(pin.constraint);
|
||||
return props.cathodeType === "common" ? state === "high" : state === "low";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 渲染数据
|
||||
// ============================================================================
|
||||
|
||||
const segmentPaths = {
|
||||
a: { points: "30,20 90,20 98,28 82,36 38,36 22,28" },
|
||||
b: { points: "100,30 108,38 108,82 100,90 92,82 92,38" },
|
||||
c: { points: "100,90 108,98 108,142 100,150 92,142 92,98" },
|
||||
d: { points: "30,160 90,160 98,152 82,144 38,144 22,152" },
|
||||
e: { points: "20,90 28,98 28,142 20,150 12,142 12,98" },
|
||||
f: { points: "20,30 28,38 28,82 20,90 12,82 12,38" },
|
||||
g: { points: "30,90 38,82 82,82 90,90 82,98 38,98" },
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
// 计算属性
|
||||
// ============================================================================
|
||||
|
||||
const width = computed(() => 120 * props.size);
|
||||
const height = computed(() => 220 * props.size);
|
||||
const segmentColor = computed(() => props.color);
|
||||
const inactiveColor = computed(() => "#FFFFFF");
|
||||
const pinRefs = ref<Record<string, any>>({});
|
||||
|
||||
// ============================================================================
|
||||
// 生命周期
|
||||
// ============================================================================
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.enableDigitalTwin) {
|
||||
await initDigitalTwin();
|
||||
} else {
|
||||
constraintUnsubscribe = onConstraintStateChange(updateConstraintStates);
|
||||
updateConstraintStates();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanupDigitalTwin();
|
||||
|
||||
if (constraintUnsubscribe) {
|
||||
constraintUnsubscribe();
|
||||
}
|
||||
|
||||
if (afterglowTimer.value) {
|
||||
clearTimeout(afterglowTimer.value);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听模式切换
|
||||
watch(
|
||||
() => [props.enableDigitalTwin],
|
||||
async () => {
|
||||
// 清理旧模式
|
||||
cleanupDigitalTwin();
|
||||
if (constraintUnsubscribe) {
|
||||
constraintUnsubscribe();
|
||||
constraintUnsubscribe = null;
|
||||
}
|
||||
|
||||
// 初始化新模式
|
||||
if (props.enableDigitalTwin) {
|
||||
await initDigitalTwin();
|
||||
} else {
|
||||
constraintUnsubscribe = onConstraintStateChange(updateConstraintStates);
|
||||
updateConstraintStates();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.seven-segment-display {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.segment {
|
||||
transition:
|
||||
opacity 0.2s,
|
||||
fill 0.2s;
|
||||
}
|
||||
|
||||
.segment.active {
|
||||
filter: drop-shadow(0 0 4px v-bind(segmentColor))
|
||||
drop-shadow(0 0 2px v-bind(segmentColor));
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
export function getDefaultProps() {
|
||||
return {
|
||||
size: 1,
|
||||
color: "red",
|
||||
enableDigitalTwin: false,
|
||||
digitalTwinNum: 0,
|
||||
afterglowDuration: 500,
|
||||
cathodeType: "common",
|
||||
pins: [
|
||||
{ pinId: "a", constraint: "", x: 10, y: 170 },
|
||||
{ pinId: "b", constraint: "", x: 24, y: 170 },
|
||||
{ pinId: "c", constraint: "", x: 38, y: 170 },
|
||||
{ pinId: "d", constraint: "", x: 52, y: 170 },
|
||||
{ pinId: "e", constraint: "", x: 66, y: 170 },
|
||||
{ pinId: "f", constraint: "", x: 80, y: 170 },
|
||||
{ pinId: "g", constraint: "", x: 94, y: 170 },
|
||||
{ pinId: "dp", constraint: "", x: 108, y: 170 },
|
||||
{ pinId: "COM", constraint: "", x: 60, y: 10 },
|
||||
],
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@@ -1,17 +1,30 @@
|
||||
// filepath: c:\_Project\FPGA_WebLab\FPGA_WebLab\src\components\equipments\Switch.vue
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:viewBox="`4 6 ${props.switchCount + 2} 4`"
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:viewBox="`4 6 ${switchCount + 2} 4`"
|
||||
class="dip-switch"
|
||||
>
|
||||
<defs>
|
||||
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feFlood result="flood" flood-color="#f08a5d" flood-opacity="1"></feFlood>
|
||||
<feComposite in="flood" result="mask" in2="SourceGraphic" operator="in"></feComposite>
|
||||
<feMorphology in="mask" result="dilated" operator="dilate" radius="0.02"></feMorphology>
|
||||
<feFlood
|
||||
result="flood"
|
||||
flood-color="#f08a5d"
|
||||
flood-opacity="1"
|
||||
></feFlood>
|
||||
<feComposite
|
||||
in="flood"
|
||||
result="mask"
|
||||
in2="SourceGraphic"
|
||||
operator="in"
|
||||
></feComposite>
|
||||
<feMorphology
|
||||
in="mask"
|
||||
result="dilated"
|
||||
operator="dilate"
|
||||
radius="0.02"
|
||||
></feMorphology>
|
||||
<feGaussianBlur in="dilated" stdDeviation="0.05" result="blur1" />
|
||||
<feGaussianBlur in="dilated" stdDeviation="0.1" result="blur2" />
|
||||
<feGaussianBlur in="dilated" stdDeviation="0.2" result="blur3" />
|
||||
@@ -23,29 +36,41 @@
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<g>
|
||||
<!-- 红色背景随开关数量变化宽度 -->
|
||||
<rect :width="props.switchCount + 2" height="4" x="4" y="6" fill="#c01401" rx="0.1" />
|
||||
<text v-if="props.showLabels" fill="white" font-size="0.7" x="4.25" y="6.75">ON</text>
|
||||
|
||||
<rect
|
||||
:width="switchCount + 2"
|
||||
height="4"
|
||||
x="4"
|
||||
y="6"
|
||||
fill="#c01401"
|
||||
rx="0.1"
|
||||
/>
|
||||
<text
|
||||
v-if="props.showLabels"
|
||||
fill="white"
|
||||
font-size="0.7"
|
||||
x="4.25"
|
||||
y="6.75"
|
||||
>
|
||||
ON
|
||||
</text>
|
||||
<g>
|
||||
<template v-for="(_, index) in Array(props.switchCount)" :key="index">
|
||||
<rect
|
||||
class="glow interactive"
|
||||
@click="toggleBtnStatus(index)"
|
||||
width="0.7"
|
||||
height="2"
|
||||
fill="#68716f"
|
||||
:x="5.15 + index"
|
||||
y="7"
|
||||
rx="0.1"
|
||||
<template v-for="(_, index) in Array(switchCount)" :key="index">
|
||||
<rect
|
||||
class="glow interactive"
|
||||
@click="toggleBtnStatus(index)"
|
||||
width="0.7"
|
||||
height="2"
|
||||
fill="#68716f"
|
||||
:x="5.15 + index"
|
||||
y="7"
|
||||
rx="0.1"
|
||||
/>
|
||||
<text
|
||||
<text
|
||||
v-if="props.showLabels"
|
||||
:x="5.5 + index"
|
||||
y="9.5"
|
||||
font-size="0.4"
|
||||
:x="5.5 + index"
|
||||
y="9.5"
|
||||
font-size="0.4"
|
||||
text-anchor="middle"
|
||||
fill="#444"
|
||||
>
|
||||
@@ -53,19 +78,21 @@
|
||||
</text>
|
||||
</template>
|
||||
</g>
|
||||
|
||||
<g>
|
||||
<template v-for="(location, index) in btnLocation" :key="`btn-${index}`">
|
||||
<rect
|
||||
<template
|
||||
v-for="(location, index) in btnLocation"
|
||||
:key="`btn-${index}`"
|
||||
>
|
||||
<rect
|
||||
class="interactive"
|
||||
@click="toggleBtnStatus(index)"
|
||||
width="0.65"
|
||||
height="0.65"
|
||||
fill="white"
|
||||
:x="5.175 + index"
|
||||
:y="location"
|
||||
@click="toggleBtnStatus(index)"
|
||||
width="0.65"
|
||||
height="0.65"
|
||||
fill="white"
|
||||
:x="5.175 + index"
|
||||
:y="location"
|
||||
rx="0.1"
|
||||
opacity="1"
|
||||
opacity="1"
|
||||
/>
|
||||
</template>
|
||||
</g>
|
||||
@@ -74,119 +101,112 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { SwitchClient } from "@/APIClient";
|
||||
import { AuthManager } from "@/utils/AuthManager";
|
||||
import { isUndefined } from "lodash";
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
|
||||
interface Props {
|
||||
size?: number;
|
||||
enableDigitalTwin?: boolean;
|
||||
switchCount?: number;
|
||||
// 新增属性
|
||||
initialValues?: boolean[] | string; // 开关的初始状态,可以是布尔数组或逗号分隔的字符串
|
||||
showLabels?: boolean; // 是否显示标签
|
||||
initialValues?: string;
|
||||
showLabels?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 1,
|
||||
enableDigitalTwin: false,
|
||||
switchCount: 6,
|
||||
initialValues: () => [],
|
||||
showLabels: true
|
||||
initialValues: "",
|
||||
showLabels: true,
|
||||
});
|
||||
|
||||
// 计算实际宽高
|
||||
const width = computed(() => {
|
||||
// 每个开关占用25px宽度,再加上两侧边距(20px)
|
||||
return (props.switchCount * 25 + 20) * props.size;
|
||||
const switchCount = computed(() => {
|
||||
if (props.enableDigitalTwin) return 5;
|
||||
else return props.switchCount;
|
||||
});
|
||||
const height = computed(() => 85 * props.size); // 高度保持固定比例
|
||||
|
||||
// 定义发出的事件
|
||||
const emit = defineEmits(['change', 'switch-toggle']);
|
||||
function getClient() {
|
||||
return AuthManager.createClient(SwitchClient);
|
||||
}
|
||||
|
||||
// 解析初始值,支持字符串和数组两种格式
|
||||
const parseInitialValues = () => {
|
||||
// 解析初始值
|
||||
function parseInitialValues(): boolean[] {
|
||||
if (Array.isArray(props.initialValues)) {
|
||||
return [...props.initialValues].slice(0, props.switchCount);
|
||||
} else if (typeof props.initialValues === 'string' && props.initialValues.trim() !== '') {
|
||||
// 将逗号分隔的字符串转换为布尔数组
|
||||
const values = props.initialValues.split(',')
|
||||
.map(val => val.trim() === '1' || val.trim().toLowerCase() === 'true')
|
||||
.slice(0, props.switchCount);
|
||||
|
||||
// 如果数组长度小于开关数量,用 false 填充
|
||||
while (values.length < props.switchCount) {
|
||||
values.push(false);
|
||||
}
|
||||
|
||||
return values;
|
||||
return [...props.initialValues].slice(0, switchCount.value);
|
||||
}
|
||||
// 默认返回全部为 false 的数组
|
||||
return Array(props.switchCount).fill(false);
|
||||
};
|
||||
|
||||
// 初始化按钮状态
|
||||
const btnStatus = ref(parseInitialValues());
|
||||
|
||||
// 监听 switchCount 变化,调整开关状态数组
|
||||
watch(() => props.switchCount, (newCount) => {
|
||||
if (newCount !== btnStatus.value.length) {
|
||||
// 如果新数量大于当前数量,则扩展数组
|
||||
if (newCount > btnStatus.value.length) {
|
||||
btnStatus.value = [
|
||||
...btnStatus.value,
|
||||
...Array(newCount - btnStatus.value.length).fill(false)
|
||||
];
|
||||
} else {
|
||||
// 如果新数量小于当前数量,则截断数组
|
||||
btnStatus.value = btnStatus.value.slice(0, newCount);
|
||||
}
|
||||
if (
|
||||
typeof props.initialValues === "string" &&
|
||||
props.initialValues.trim() !== ""
|
||||
) {
|
||||
const arr = props.initialValues
|
||||
.split(",")
|
||||
.map((val) => val.trim() === "1" || val.trim().toLowerCase() === "true");
|
||||
while (arr.length < props.switchCount) arr.push(false);
|
||||
return arr.slice(0, props.switchCount);
|
||||
}
|
||||
}, { immediate: true });
|
||||
return Array(switchCount.value).fill(false);
|
||||
}
|
||||
|
||||
// 监听 initialValues 变化,更新开关状态
|
||||
watch(() => props.initialValues, () => {
|
||||
btnStatus.value = parseInitialValues();
|
||||
});
|
||||
// 状态唯一真相
|
||||
const btnStatus = ref<boolean[]>(parseInitialValues());
|
||||
|
||||
const btnLocation = computed(() => {
|
||||
return btnStatus.value.map((status) => {
|
||||
return status ? 7.025 : 8.325;
|
||||
});
|
||||
});
|
||||
// 计算宽高
|
||||
const width = computed(() => (switchCount.value * 25 + 20) * props.size);
|
||||
const height = computed(() => 85 * props.size);
|
||||
|
||||
function setBtnStatus(btnNum: number, isOn: boolean): void {
|
||||
if (btnNum >= 0 && btnNum < btnStatus.value.length) {
|
||||
btnStatus.value[btnNum] = isOn;
|
||||
emit('change', { index: btnNum, value: isOn, states: [...btnStatus.value] });
|
||||
// 按钮位置
|
||||
const btnLocation = computed(() =>
|
||||
btnStatus.value.map((status) => (status ? 7.025 : 8.325)),
|
||||
);
|
||||
|
||||
// 状态变更统一处理
|
||||
function updateStatus(newStates: boolean[], index?: number) {
|
||||
btnStatus.value = newStates.slice(0, switchCount.value);
|
||||
if (props.enableDigitalTwin) {
|
||||
try {
|
||||
const client = getClient();
|
||||
if (!isUndefined(index))
|
||||
client.setSwitchOnOff(index + 1, newStates[index]);
|
||||
else client.setMultiSwitchsOnOff(btnStatus.value);
|
||||
} catch (error: any) {}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleBtnStatus(btnNum: number): void {
|
||||
if (btnNum >= 0 && btnNum < btnStatus.value.length) {
|
||||
btnStatus.value[btnNum] = !btnStatus.value[btnNum];
|
||||
emit('switch-toggle', {
|
||||
index: btnNum,
|
||||
value: btnStatus.value[btnNum],
|
||||
states: [...btnStatus.value]
|
||||
});
|
||||
}
|
||||
// 切换单个
|
||||
function toggleBtnStatus(idx: number) {
|
||||
if (idx < 0 || idx >= btnStatus.value.length) return;
|
||||
const newStates = [...btnStatus.value];
|
||||
newStates[idx] = !newStates[idx];
|
||||
updateStatus(newStates, idx);
|
||||
}
|
||||
|
||||
// 一次性设置所有开关状态
|
||||
function setAllStates(states: boolean[]): void {
|
||||
const newStates = states.slice(0, props.switchCount);
|
||||
while (newStates.length < props.switchCount) {
|
||||
newStates.push(false);
|
||||
}
|
||||
btnStatus.value = newStates;
|
||||
emit('change', { states: [...btnStatus.value] });
|
||||
// 单个设置
|
||||
function setBtnStatus(idx: number, isOn: boolean) {
|
||||
if (idx < 0 || idx >= btnStatus.value.length) return;
|
||||
const newStates = [...btnStatus.value];
|
||||
newStates[idx] = isOn;
|
||||
updateStatus(newStates, idx);
|
||||
}
|
||||
|
||||
// 暴露组件方法和状态
|
||||
defineExpose({
|
||||
setBtnStatus,
|
||||
toggleBtnStatus,
|
||||
setAllStates,
|
||||
getBtnStatus: () => [...btnStatus.value]
|
||||
});
|
||||
// 监听 props 变化只同步一次
|
||||
watch(
|
||||
() => props.enableDigitalTwin,
|
||||
(newVal) => {
|
||||
const client = getClient();
|
||||
client.setEnable(newVal);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [switchCount.value, props.initialValues],
|
||||
() => {
|
||||
btnStatus.value = parseInitialValues();
|
||||
updateStatus(btnStatus.value);
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
@@ -194,17 +214,27 @@ defineExpose({
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: 0; /* 移除行高导致的额外间距 */
|
||||
font-size: 0; /* 防止文本节点造成的间距 */
|
||||
line-height: 0;
|
||||
font-size: 0;
|
||||
box-sizing: content-box;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
rect {
|
||||
transition: all 100ms ease-in-out;
|
||||
}
|
||||
|
||||
.interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
export function getDefaultProps() {
|
||||
return {
|
||||
size: 1,
|
||||
enableDigitalTwin: false,
|
||||
switchCount: 6,
|
||||
initialValues: "",
|
||||
showLabels: true,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user