feat: 前端七段数码管添加数字孪生功能
This commit is contained in:
parent
3644c75304
commit
0a1e0982c2
|
@ -29,7 +29,7 @@ DebuggerCmd.md
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sw?
|
*.sw?
|
||||||
|
prompt.md
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
# Generated Files
|
# Generated Files
|
||||||
|
|
13
TODO.md
13
TODO.md
|
@ -1,13 +0,0 @@
|
||||||
# TODO
|
|
||||||
|
|
||||||
1. 后端HTTP视频流
|
|
||||||
|
|
||||||
640*480, RGB565
|
|
||||||
0x0000_0000 + 25800
|
|
||||||
|
|
||||||
|
|
||||||
2. 信号发生器界面导入.dat文件
|
|
||||||
3. 示波器后端交互、前端界面
|
|
||||||
4. 逻辑分析仪后端交互、前端界面
|
|
||||||
5. 前端重构
|
|
||||||
6. 数据库 —— 用户登录、板卡资源分配、板卡IP地址分配
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { ResourcePurpose } from "@/APIClient";
|
||||||
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
|
|
||||||
// 定义 diagram.json 的类型结构
|
// 定义 diagram.json 的类型结构
|
||||||
export interface DiagramData {
|
export interface DiagramData {
|
||||||
version: number;
|
version: number;
|
||||||
|
@ -26,11 +29,12 @@ export interface DiagramPart {
|
||||||
// 连接类型定义 - 使用元组类型表示四元素数组
|
// 连接类型定义 - 使用元组类型表示四元素数组
|
||||||
export type ConnectionArray = [string, string, number, string[]];
|
export type ConnectionArray = [string, string, number, string[]];
|
||||||
|
|
||||||
import { AuthManager } from '@/utils/AuthManager';
|
|
||||||
|
|
||||||
// 解析连接字符串为组件ID和引脚ID
|
// 解析连接字符串为组件ID和引脚ID
|
||||||
export function parseConnectionPin(connectionPin: string): { componentId: string; pinId: string } {
|
export function parseConnectionPin(connectionPin: string): {
|
||||||
const [componentId, pinId] = connectionPin.split(':');
|
componentId: string;
|
||||||
|
pinId: string;
|
||||||
|
} {
|
||||||
|
const [componentId, pinId] = connectionPin.split(":");
|
||||||
return { componentId, pinId };
|
return { componentId, pinId };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,11 +43,13 @@ export function connectionArrayToWireItem(
|
||||||
connection: ConnectionArray,
|
connection: ConnectionArray,
|
||||||
index: number,
|
index: number,
|
||||||
startPos = { x: 0, y: 0 },
|
startPos = { x: 0, y: 0 },
|
||||||
endPos = { x: 0, y: 0 }
|
endPos = { x: 0, y: 0 },
|
||||||
): WireItem {
|
): WireItem {
|
||||||
const [startPinStr, endPinStr, width, path] = connection;
|
const [startPinStr, endPinStr, width, path] = connection;
|
||||||
const { componentId: startComponentId, pinId: startPinId } = parseConnectionPin(startPinStr);
|
const { componentId: startComponentId, pinId: startPinId } =
|
||||||
const { componentId: endComponentId, pinId: endPinId } = parseConnectionPin(endPinStr);
|
parseConnectionPin(startPinStr);
|
||||||
|
const { componentId: endComponentId, pinId: endPinId } =
|
||||||
|
parseConnectionPin(endPinStr);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `wire-${index}`,
|
id: `wire-${index}`,
|
||||||
|
@ -56,10 +62,10 @@ export function connectionArrayToWireItem(
|
||||||
endComponentId,
|
endComponentId,
|
||||||
endPinId,
|
endPinId,
|
||||||
strokeWidth: width,
|
strokeWidth: width,
|
||||||
color: '#4a5568', // 默认颜色
|
color: "#4a5568", // 默认颜色
|
||||||
routingMode: 'path',
|
routingMode: "path",
|
||||||
pathCommands: path,
|
pathCommands: path,
|
||||||
showLabel: false
|
showLabel: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +82,7 @@ export interface WireItem {
|
||||||
endPinId?: string;
|
endPinId?: string;
|
||||||
strokeWidth: number;
|
strokeWidth: number;
|
||||||
color: string;
|
color: string;
|
||||||
routingMode: 'orthogonal' | 'path';
|
routingMode: "orthogonal" | "path";
|
||||||
constraint?: string;
|
constraint?: string;
|
||||||
pathCommands?: string[];
|
pathCommands?: string[];
|
||||||
showLabel: boolean;
|
showLabel: boolean;
|
||||||
|
@ -91,14 +97,20 @@ export async function loadDiagramData(examId?: string): Promise<DiagramData> {
|
||||||
const resourceClient = AuthManager.createAuthenticatedResourceClient();
|
const resourceClient = AuthManager.createAuthenticatedResourceClient();
|
||||||
|
|
||||||
// 获取diagram类型的资源列表
|
// 获取diagram类型的资源列表
|
||||||
const resources = await resourceClient.getResourceList(examId, 'canvas', 'template');
|
const resources = await resourceClient.getResourceList(
|
||||||
|
examId,
|
||||||
|
"canvas",
|
||||||
|
ResourcePurpose.Template,
|
||||||
|
);
|
||||||
|
|
||||||
if (resources && resources.length > 0) {
|
if (resources && resources.length > 0) {
|
||||||
// 获取第一个diagram资源
|
// 获取第一个diagram资源
|
||||||
const diagramResource = resources[0];
|
const diagramResource = resources[0];
|
||||||
|
|
||||||
// 使用动态API获取资源文件内容
|
// 使用动态API获取资源文件内容
|
||||||
const response = await resourceClient.getResourceById(diagramResource.id);
|
const response = await resourceClient.getResourceById(
|
||||||
|
diagramResource.id,
|
||||||
|
);
|
||||||
|
|
||||||
if (response && response.data) {
|
if (response && response.data) {
|
||||||
const text = await response.data.text();
|
const text = await response.data.text();
|
||||||
|
@ -107,24 +119,24 @@ export async function loadDiagramData(examId?: string): Promise<DiagramData> {
|
||||||
// 验证数据格式
|
// 验证数据格式
|
||||||
const validation = validateDiagramData(data);
|
const validation = validateDiagramData(data);
|
||||||
if (validation.isValid) {
|
if (validation.isValid) {
|
||||||
console.log('成功从API加载实验diagram:', examId);
|
console.log("成功从API加载实验diagram:", examId);
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
console.warn('API返回的diagram数据格式无效:', validation.errors);
|
console.warn("API返回的diagram数据格式无效:", validation.errors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('未找到实验diagram资源,使用默认加载方式');
|
console.log("未找到实验diagram资源,使用默认加载方式");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('从API加载实验diagram失败,使用默认加载方式:', error);
|
console.warn("从API加载实验diagram失败,使用默认加载方式:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有examId或API加载失败,尝试从静态文件加载(不再使用本地存储)
|
// 如果没有examId或API加载失败,尝试从静态文件加载(不再使用本地存储)
|
||||||
|
|
||||||
// 从静态文件加载(作为备选方案)
|
// 从静态文件加载(作为备选方案)
|
||||||
const response = await fetch('/src/components/diagram.json');
|
const response = await fetch("/src/components/diagram.json");
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to load diagram.json: ${response.statusText}`);
|
throw new Error(`Failed to load diagram.json: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
@ -135,11 +147,11 @@ export async function loadDiagramData(examId?: string): Promise<DiagramData> {
|
||||||
if (validation.isValid) {
|
if (validation.isValid) {
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
console.warn('静态diagram文件数据格式无效:', validation.errors);
|
console.warn("静态diagram文件数据格式无效:", validation.errors);
|
||||||
throw new Error('所有diagram数据源都无效');
|
throw new Error("所有diagram数据源都无效");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading diagram data:', error);
|
console.error("Error loading diagram data:", error);
|
||||||
// 返回空的默认数据结构
|
// 返回空的默认数据结构
|
||||||
return createEmptyDiagram();
|
return createEmptyDiagram();
|
||||||
}
|
}
|
||||||
|
@ -149,17 +161,17 @@ export async function loadDiagramData(examId?: string): Promise<DiagramData> {
|
||||||
export function createEmptyDiagram(): DiagramData {
|
export function createEmptyDiagram(): DiagramData {
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
author: 'user',
|
author: "user",
|
||||||
editor: 'user',
|
editor: "user",
|
||||||
parts: [],
|
parts: [],
|
||||||
connections: []
|
connections: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存图表数据(已禁用本地存储)
|
// 保存图表数据(已禁用本地存储)
|
||||||
export function saveDiagramData(data: DiagramData): void {
|
export function saveDiagramData(data: DiagramData): void {
|
||||||
// 本地存储功能已禁用 - 不再保存到localStorage
|
// 本地存储功能已禁用 - 不再保存到localStorage
|
||||||
console.debug('saveDiagramData called but localStorage saving is disabled');
|
console.debug("saveDiagramData called but localStorage saving is disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新组件位置
|
// 更新组件位置
|
||||||
|
@ -167,15 +179,13 @@ export function updatePartPosition(
|
||||||
data: DiagramData,
|
data: DiagramData,
|
||||||
partId: string,
|
partId: string,
|
||||||
x: number,
|
x: number,
|
||||||
y: number
|
y: number,
|
||||||
): DiagramData {
|
): DiagramData {
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
parts: data.parts.map(part =>
|
parts: data.parts.map((part) =>
|
||||||
part.id === partId
|
part.id === partId ? { ...part, x, y } : part,
|
||||||
? { ...part, x, y }
|
),
|
||||||
: part
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,21 +194,21 @@ export function updatePartAttribute(
|
||||||
data: DiagramData,
|
data: DiagramData,
|
||||||
partId: string,
|
partId: string,
|
||||||
attrName: string,
|
attrName: string,
|
||||||
value: any
|
value: any,
|
||||||
): DiagramData {
|
): DiagramData {
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
parts: data.parts.map(part =>
|
parts: data.parts.map((part) =>
|
||||||
part.id === partId
|
part.id === partId
|
||||||
? {
|
? {
|
||||||
...part,
|
...part,
|
||||||
attrs: {
|
attrs: {
|
||||||
...part.attrs,
|
...part.attrs,
|
||||||
[attrName]: value
|
[attrName]: value,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
: part
|
: part,
|
||||||
)
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,72 +220,79 @@ export function addConnection(
|
||||||
endComponentId: string,
|
endComponentId: string,
|
||||||
endPinId: string,
|
endPinId: string,
|
||||||
width: number = 2,
|
width: number = 2,
|
||||||
path: string[] = []
|
path: string[] = [],
|
||||||
): DiagramData {
|
): DiagramData {
|
||||||
const newConnection: ConnectionArray = [
|
const newConnection: ConnectionArray = [
|
||||||
`${startComponentId}:${startPinId}`,
|
`${startComponentId}:${startPinId}`,
|
||||||
`${endComponentId}:${endPinId}`,
|
`${endComponentId}:${endPinId}`,
|
||||||
width,
|
width,
|
||||||
path
|
path,
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
connections: [...data.connections, newConnection]
|
connections: [...data.connections, newConnection],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除连接
|
// 删除连接
|
||||||
export function deleteConnection(
|
export function deleteConnection(
|
||||||
data: DiagramData,
|
data: DiagramData,
|
||||||
connectionIndex: number
|
connectionIndex: number,
|
||||||
): DiagramData {
|
): DiagramData {
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
connections: data.connections.filter((_, index) => index !== connectionIndex)
|
connections: data.connections.filter(
|
||||||
|
(_, index) => index !== connectionIndex,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找与组件关联的所有连接
|
// 查找与组件关联的所有连接
|
||||||
export function findConnectionsByPart(
|
export function findConnectionsByPart(
|
||||||
data: DiagramData,
|
data: DiagramData,
|
||||||
partId: string
|
partId: string,
|
||||||
): { connection: ConnectionArray; index: number }[] {
|
): { connection: ConnectionArray; index: number }[] {
|
||||||
return data.connections
|
return data.connections
|
||||||
.map((connection, index) => ({ connection, index }))
|
.map((connection, index) => ({ connection, index }))
|
||||||
.filter(({ connection }) => {
|
.filter(({ connection }) => {
|
||||||
const [startPin, endPin] = connection;
|
const [startPin, endPin] = connection;
|
||||||
const startCompId = startPin.split(':')[0];
|
const startCompId = startPin.split(":")[0];
|
||||||
const endCompId = endPin.split(':')[0];
|
const endCompId = endPin.split(":")[0];
|
||||||
return startCompId === partId || endCompId === partId;
|
return startCompId === partId || endCompId === partId;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加验证diagram.json文件的函数
|
// 添加验证diagram.json文件的函数
|
||||||
export function validateDiagramData(data: any): { isValid: boolean; errors: string[] } {
|
export function validateDiagramData(data: any): {
|
||||||
|
isValid: boolean;
|
||||||
|
errors: string[];
|
||||||
|
} {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
// 检查版本号
|
// 检查版本号
|
||||||
if (!data.version) {
|
if (!data.version) {
|
||||||
errors.push('缺少version字段');
|
errors.push("缺少version字段");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查parts数组
|
// 检查parts数组
|
||||||
if (!Array.isArray(data.parts)) {
|
if (!Array.isArray(data.parts)) {
|
||||||
errors.push('parts字段不是数组');
|
errors.push("parts字段不是数组");
|
||||||
} else {
|
} else {
|
||||||
// 验证parts中的每个对象
|
// 验证parts中的每个对象
|
||||||
data.parts.forEach((part: any, index: number) => {
|
data.parts.forEach((part: any, index: number) => {
|
||||||
if (!part.id) errors.push(`parts[${index}]缺少id`);
|
if (!part.id) errors.push(`parts[${index}]缺少id`);
|
||||||
if (!part.type) errors.push(`parts[${index}]缺少type`);
|
if (!part.type) errors.push(`parts[${index}]缺少type`);
|
||||||
if (typeof part.x !== 'number') errors.push(`parts[${index}]缺少有效的x坐标`);
|
if (typeof part.x !== "number")
|
||||||
if (typeof part.y !== 'number') errors.push(`parts[${index}]缺少有效的y坐标`);
|
errors.push(`parts[${index}]缺少有效的x坐标`);
|
||||||
|
if (typeof part.y !== "number")
|
||||||
|
errors.push(`parts[${index}]缺少有效的y坐标`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查connections数组
|
// 检查connections数组
|
||||||
if (!Array.isArray(data.connections)) {
|
if (!Array.isArray(data.connections)) {
|
||||||
errors.push('connections字段不是数组');
|
errors.push("connections字段不是数组");
|
||||||
} else {
|
} else {
|
||||||
// 验证connections中的每个数组
|
// 验证connections中的每个数组
|
||||||
data.connections.forEach((conn: any, index: number) => {
|
data.connections.forEach((conn: any, index: number) => {
|
||||||
|
@ -286,15 +303,15 @@ export function validateDiagramData(data: any): { isValid: boolean; errors: stri
|
||||||
|
|
||||||
const [startPin, endPin, width] = conn;
|
const [startPin, endPin, width] = conn;
|
||||||
|
|
||||||
if (typeof startPin !== 'string' || !startPin.includes(':')) {
|
if (typeof startPin !== "string" || !startPin.includes(":")) {
|
||||||
errors.push(`connections[${index}]的起始针脚格式无效`);
|
errors.push(`connections[${index}]的起始针脚格式无效`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof endPin !== 'string' || !endPin.includes(':')) {
|
if (typeof endPin !== "string" || !endPin.includes(":")) {
|
||||||
errors.push(`connections[${index}]的结束针脚格式无效`);
|
errors.push(`connections[${index}]的结束针脚格式无效`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof width !== 'number') {
|
if (typeof width !== "number") {
|
||||||
errors.push(`connections[${index}]的宽度不是有效的数字`);
|
errors.push(`connections[${index}]的宽度不是有效的数字`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -302,6 +319,6 @@ export function validateDiagramData(data: any): { isValid: boolean; errors: stri
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isValid: errors.length === 0,
|
isValid: errors.length === 0,
|
||||||
errors
|
errors,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ export const previewSizes: Record<string, number> = {
|
||||||
Switch: 0.35,
|
Switch: 0.35,
|
||||||
Pin: 0.8,
|
Pin: 0.8,
|
||||||
SMT_LED: 0.7,
|
SMT_LED: 0.7,
|
||||||
SevenSegmentDisplay: 0.4,
|
SevenSegmentDisplayUltimate: 0.4,
|
||||||
HDMI: 0.5,
|
HDMI: 0.5,
|
||||||
DDR: 0.5,
|
DDR: 0.5,
|
||||||
ETH: 0.5,
|
ETH: 0.5,
|
||||||
|
@ -50,7 +50,7 @@ export const availableComponents: ComponentConfig[] = [
|
||||||
{ type: "Switch", name: "开关" },
|
{ type: "Switch", name: "开关" },
|
||||||
{ type: "Pin", name: "引脚" },
|
{ type: "Pin", name: "引脚" },
|
||||||
{ type: "SMT_LED", name: "贴片LED" },
|
{ type: "SMT_LED", name: "贴片LED" },
|
||||||
{ type: "SevenSegmentDisplay", name: "数码管" },
|
{ type: "SevenSegmentDisplayUltimate", name: "数码管" },
|
||||||
{ type: "HDMI", name: "HDMI接口" },
|
{ type: "HDMI", name: "HDMI接口" },
|
||||||
{ type: "DDR", name: "DDR内存" },
|
{ type: "DDR", name: "DDR内存" },
|
||||||
{ type: "ETH", name: "以太网接口" },
|
{ type: "ETH", name: "以太网接口" },
|
||||||
|
|
|
@ -1,65 +1,114 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="seven-segment-display" :style="{
|
<div
|
||||||
width: width + 'px',
|
class="seven-segment-display"
|
||||||
height: height + 'px',
|
:style="{
|
||||||
position: 'relative',
|
width: width + 'px',
|
||||||
}">
|
height: height + 'px',
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" viewBox="0 0 120 220" class="display">
|
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="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" />
|
<rect width="110" height="170" x="5" y="5" fill="#333" rx="5" ry="5" />
|
||||||
<!-- 7段 + 小数点,每个段由多边形表示,重新设计点位置使其更接近实际数码管 -->
|
<!-- 7段 + 小数点,每个段由多边形表示,重新设计点位置使其更接近实际数码管 -->
|
||||||
<!-- a段 (顶部横线) -->
|
<!-- 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"
|
:fill="isSegmentActive('a') ? segmentColor : inactiveColor"
|
||||||
:style="{ opacity: isSegmentActive('a') ? 1 : 0.15 }" class="segment" />
|
:style="{ opacity: isSegmentActive('a') ? 1 : 0.15 }"
|
||||||
|
class="segment"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- b段 (右上竖线) -->
|
<!-- 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"
|
:fill="isSegmentActive('b') ? segmentColor : inactiveColor"
|
||||||
:style="{ opacity: isSegmentActive('b') ? 1 : 0.15 }" class="segment" />
|
:style="{ opacity: isSegmentActive('b') ? 1 : 0.15 }"
|
||||||
|
class="segment"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- c段 (右下竖线) -->
|
<!-- 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"
|
:fill="isSegmentActive('c') ? segmentColor : inactiveColor"
|
||||||
:style="{ opacity: isSegmentActive('c') ? 1 : 0.15 }" class="segment" />
|
:style="{ opacity: isSegmentActive('c') ? 1 : 0.15 }"
|
||||||
|
class="segment"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- d段 (底部横线) -->
|
<!-- 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"
|
:fill="isSegmentActive('d') ? segmentColor : inactiveColor"
|
||||||
:style="{ opacity: isSegmentActive('d') ? 1 : 0.15 }" class="segment" />
|
:style="{ opacity: isSegmentActive('d') ? 1 : 0.15 }"
|
||||||
|
class="segment"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- e段 (左下竖线) -->
|
<!-- 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"
|
:fill="isSegmentActive('e') ? segmentColor : inactiveColor"
|
||||||
:style="{ opacity: isSegmentActive('e') ? 1 : 0.15 }" class="segment" />
|
:style="{ opacity: isSegmentActive('e') ? 1 : 0.15 }"
|
||||||
|
class="segment"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- f段 (左上竖线) -->
|
<!-- 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"
|
:fill="isSegmentActive('f') ? segmentColor : inactiveColor"
|
||||||
:style="{ opacity: isSegmentActive('f') ? 1 : 0.15 }" class="segment" />
|
:style="{ opacity: isSegmentActive('f') ? 1 : 0.15 }"
|
||||||
|
class="segment"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- g段 (中间横线) -->
|
<!-- 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"
|
:fill="isSegmentActive('g') ? segmentColor : inactiveColor"
|
||||||
:style="{ opacity: isSegmentActive('g') ? 1 : 0.15 }" class="segment" />
|
:style="{ opacity: isSegmentActive('g') ? 1 : 0.15 }"
|
||||||
|
class="segment"
|
||||||
|
/>
|
||||||
<!-- dp段 (小数点) -->
|
<!-- dp段 (小数点) -->
|
||||||
<circle cx="108" cy="154" r="6" :fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
|
<circle
|
||||||
:style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }" class="segment" />
|
cx="108"
|
||||||
|
cy="154"
|
||||||
|
r="6"
|
||||||
|
:fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
|
||||||
|
:style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }"
|
||||||
|
class="segment"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<!-- 引脚 -->
|
<!-- 引脚 -->
|
||||||
<div v-for="pin in pins" :key="pin.pinId" :style="{
|
<div
|
||||||
position: 'absolute',
|
v-for="pin in pins"
|
||||||
left: `${pin.x * props.size}px`,
|
:key="pin.pinId"
|
||||||
top: `${pin.y * props.size}px`,
|
:style="{
|
||||||
transform: 'translate(-50%, -50%)',
|
position: 'absolute',
|
||||||
}" :data-pin-wrapper="`${pin.pinId}`" :data-pin-x="`${pin.x * props.size}`"
|
left: `${pin.x * props.size}px`,
|
||||||
:data-pin-y="`${pin.y * props.size}`">
|
top: `${pin.y * props.size}px`,
|
||||||
<Pin :ref="(el) => {
|
transform: 'translate(-50%, -50%)',
|
||||||
if (el) pinRefs[pin.pinId] = el;
|
}"
|
||||||
}
|
:data-pin-wrapper="`${pin.pinId}`"
|
||||||
" :label="pin.pinId" :constraint="pin.constraint" :pinId="pin.pinId" @pin-click="$emit('pin-click', $event)" />
|
: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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -274,7 +323,8 @@ function updateSegmentStates() {
|
||||||
for (const pin of props.pins) {
|
for (const pin of props.pins) {
|
||||||
if (["a", "b", "c", "d", "e", "f", "g", "dp"].includes(pin.pinId)) {
|
if (["a", "b", "c", "d", "e", "f", "g", "dp"].includes(pin.pinId)) {
|
||||||
if (!pin.constraint) {
|
if (!pin.constraint) {
|
||||||
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = false;
|
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] =
|
||||||
|
false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const pinState = getConstraintState(pin.constraint);
|
const pinState = getConstraintState(pin.constraint);
|
||||||
|
@ -285,7 +335,8 @@ function updateSegmentStates() {
|
||||||
newState = pinState === "low";
|
newState = pinState === "low";
|
||||||
}
|
}
|
||||||
// 段状态只有在COM激活时才有效
|
// 段状态只有在COM激活时才有效
|
||||||
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = newState;
|
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] =
|
||||||
|
newState;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -332,7 +383,8 @@ function enterAfterglowMode() {
|
||||||
// 保存当前稳定状态作为余晖状态
|
// 保存当前稳定状态作为余晖状态
|
||||||
for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
|
for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
|
||||||
const typedSegmentId = segmentId as keyof typeof stableSegmentStates.value;
|
const typedSegmentId = segmentId as keyof typeof stableSegmentStates.value;
|
||||||
afterglowStates.value[typedSegmentId] = stableSegmentStates.value[typedSegmentId];
|
afterglowStates.value[typedSegmentId] =
|
||||||
|
stableSegmentStates.value[typedSegmentId];
|
||||||
|
|
||||||
// 设置定时器,在余晖持续时间后退出余晖模式
|
// 设置定时器,在余晖持续时间后退出余晖模式
|
||||||
if (afterglowTimers.value[segmentId]) {
|
if (afterglowTimers.value[segmentId]) {
|
||||||
|
@ -343,7 +395,9 @@ function enterAfterglowMode() {
|
||||||
afterglowStates.value[typedSegmentId] = false;
|
afterglowStates.value[typedSegmentId] = false;
|
||||||
|
|
||||||
// 检查是否所有段都已经关闭
|
// 检查是否所有段都已经关闭
|
||||||
const allSegmentsOff = Object.values(afterglowStates.value).every(state => !state);
|
const allSegmentsOff = Object.values(afterglowStates.value).every(
|
||||||
|
(state) => !state,
|
||||||
|
);
|
||||||
if (allSegmentsOff) {
|
if (allSegmentsOff) {
|
||||||
exitAfterglowMode();
|
exitAfterglowMode();
|
||||||
}
|
}
|
||||||
|
@ -397,11 +451,6 @@ onUnmounted(() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 暴露属性和方法
|
|
||||||
defineExpose({
|
|
||||||
updateSegmentStates,
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -418,7 +467,8 @@ defineExpose({
|
||||||
|
|
||||||
/* 数码管发光效果 */
|
/* 数码管发光效果 */
|
||||||
.segment[style*="opacity: 1"] {
|
.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>
|
</style>
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
sevenSegmentDisplaySetOnOff,
|
||||||
|
sevenSegmentDisplayData,
|
||||||
|
sevenSegmentDisplaySetFrequency,
|
||||||
|
} = useEquipments();
|
||||||
|
|
||||||
|
async function initDigitalTwin() {
|
||||||
|
if (!props.enableDigitalTwin || !props.digitalTwinNum) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
sevenSegmentDisplaySetOnOff(props.enableDigitalTwin);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Digital twin initialized for address: ${props.digitalTwinNum}`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to initialize digital twin:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [sevenSegmentDisplayData],
|
||||||
|
() => {
|
||||||
|
if (
|
||||||
|
!sevenSegmentDisplayData ||
|
||||||
|
props.digitalTwinNum < 0 ||
|
||||||
|
props.digitalTwinNum > 31
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
handleDigitalTwinData(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() {
|
||||||
|
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, props.digitalTwinNum],
|
||||||
|
async () => {
|
||||||
|
// 清理旧模式
|
||||||
|
cleanupDigitalTwin();
|
||||||
|
if (constraintUnsubscribe) {
|
||||||
|
constraintUnsubscribe();
|
||||||
|
constraintUnsubscribe = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化新模式
|
||||||
|
if (props.enableDigitalTwin) {
|
||||||
|
await initDigitalTwin();
|
||||||
|
} else {
|
||||||
|
constraintUnsubscribe = onConstraintStateChange(updateConstraintStates);
|
||||||
|
updateConstraintStates();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: false },
|
||||||
|
);
|
||||||
|
</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>
|
|
@ -7,15 +7,21 @@ import { isNumber } from "mathjs";
|
||||||
import { Mutex, withTimeout } from "async-mutex";
|
import { Mutex, withTimeout } from "async-mutex";
|
||||||
import { useConstraintsStore } from "@/stores/constraints";
|
import { useConstraintsStore } from "@/stores/constraints";
|
||||||
import { useDialogStore } from "./dialog";
|
import { useDialogStore } from "./dialog";
|
||||||
import { toFileParameterOrUndefined } from "@/utils/Common";
|
import {
|
||||||
|
base64ToArrayBuffer,
|
||||||
|
toFileParameterOrUndefined,
|
||||||
|
} from "@/utils/Common";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
import { HubConnection, HubConnectionBuilder } from "@microsoft/signalr";
|
import { HubConnection } from "@microsoft/signalr";
|
||||||
import {
|
import {
|
||||||
getHubProxyFactory,
|
getHubProxyFactory,
|
||||||
getReceiverRegister,
|
getReceiverRegister,
|
||||||
} from "@/utils/signalR/TypedSignalR.Client";
|
} from "@/utils/signalR/TypedSignalR.Client";
|
||||||
import { ResourcePurpose, type ResourceInfo } from "@/APIClient";
|
import { ResourcePurpose, type ResourceInfo } from "@/APIClient";
|
||||||
import type { IJtagHub } from "@/utils/signalR/TypedSignalR.Client/server.Hubs";
|
import type {
|
||||||
|
IDigitalTubesHub,
|
||||||
|
IJtagHub,
|
||||||
|
} from "@/utils/signalR/TypedSignalR.Client/server.Hubs";
|
||||||
|
|
||||||
export const useEquipments = defineStore("equipments", () => {
|
export const useEquipments = defineStore("equipments", () => {
|
||||||
// Global Stores
|
// Global Stores
|
||||||
|
@ -26,6 +32,7 @@ export const useEquipments = defineStore("equipments", () => {
|
||||||
const boardPort = useLocalStorage("fpga-board-port", 1234);
|
const boardPort = useLocalStorage("fpga-board-port", 1234);
|
||||||
|
|
||||||
// Jtag
|
// Jtag
|
||||||
|
const enableJtagBoundaryScan = ref(false);
|
||||||
const jtagBitstream = ref<File>();
|
const jtagBitstream = ref<File>();
|
||||||
const jtagBoundaryScanFreq = ref(100);
|
const jtagBoundaryScanFreq = ref(100);
|
||||||
const jtagUserBitstreams = ref<ResourceInfo[]>([]);
|
const jtagUserBitstreams = ref<ResourceInfo[]>([]);
|
||||||
|
@ -62,46 +69,6 @@ export const useEquipments = defineStore("equipments", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Matrix Key
|
|
||||||
const matrixKeyStates = reactive(new Array<boolean>(16).fill(false));
|
|
||||||
const matrixKeypadClientMutex = withTimeout(
|
|
||||||
new Mutex(),
|
|
||||||
1000,
|
|
||||||
new Error("Matrixkeyclient Mutex Timeout!"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Power
|
|
||||||
const powerClientMutex = withTimeout(
|
|
||||||
new Mutex(),
|
|
||||||
1000,
|
|
||||||
new Error("Matrixkeyclient Mutex Timeout!"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Enable Setting
|
|
||||||
const enableJtagBoundaryScan = ref(false);
|
|
||||||
const enableMatrixKey = ref(false);
|
|
||||||
const enablePower = ref(false);
|
|
||||||
|
|
||||||
function setMatrixKey(
|
|
||||||
keyNum: number | string | undefined,
|
|
||||||
keyValue: boolean,
|
|
||||||
): boolean {
|
|
||||||
let _keyNum: number;
|
|
||||||
if (isString(keyNum)) {
|
|
||||||
_keyNum = toNumber(keyNum);
|
|
||||||
} else if (isNumber(keyNum)) {
|
|
||||||
_keyNum = keyNum;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (z.number().nonnegative().max(16).safeParse(_keyNum).success) {
|
|
||||||
matrixKeyStates[_keyNum] = keyValue;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function jtagBoundaryScanSetOnOff(enable: boolean) {
|
async function jtagBoundaryScanSetOnOff(enable: boolean) {
|
||||||
if (isUndefined(jtagHubProxy.value)) {
|
if (isUndefined(jtagHubProxy.value)) {
|
||||||
console.error("JtagHub Not Initialize...");
|
console.error("JtagHub Not Initialize...");
|
||||||
|
@ -223,6 +190,34 @@ export const useEquipments = defineStore("equipments", () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Matrix Key
|
||||||
|
const enableMatrixKey = ref(false);
|
||||||
|
const matrixKeyStates = reactive(new Array<boolean>(16).fill(false));
|
||||||
|
const matrixKeypadClientMutex = withTimeout(
|
||||||
|
new Mutex(),
|
||||||
|
1000,
|
||||||
|
new Error("Matrixkeyclient Mutex Timeout!"),
|
||||||
|
);
|
||||||
|
function setMatrixKey(
|
||||||
|
keyNum: number | string | undefined,
|
||||||
|
keyValue: boolean,
|
||||||
|
): boolean {
|
||||||
|
let _keyNum: number;
|
||||||
|
if (isString(keyNum)) {
|
||||||
|
_keyNum = toNumber(keyNum);
|
||||||
|
} else if (isNumber(keyNum)) {
|
||||||
|
_keyNum = keyNum;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (z.number().nonnegative().max(16).safeParse(_keyNum).success) {
|
||||||
|
matrixKeyStates[_keyNum] = keyValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async function matrixKeypadSetKeyStates(keyStates: boolean[]) {
|
async function matrixKeypadSetKeyStates(keyStates: boolean[]) {
|
||||||
const release = await matrixKeypadClientMutex.acquire();
|
const release = await matrixKeypadClientMutex.acquire();
|
||||||
console.log("set Key !!!!!!!!!!!!");
|
console.log("set Key !!!!!!!!!!!!");
|
||||||
|
@ -274,6 +269,13 @@ export const useEquipments = defineStore("equipments", () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Power
|
||||||
|
const powerClientMutex = withTimeout(
|
||||||
|
new Mutex(),
|
||||||
|
1000,
|
||||||
|
new Error("Matrixkeyclient Mutex Timeout!"),
|
||||||
|
);
|
||||||
|
const enablePower = ref(false);
|
||||||
async function powerSetOnOff(enable: boolean) {
|
async function powerSetOnOff(enable: boolean) {
|
||||||
const release = await powerClientMutex.acquire();
|
const release = await powerClientMutex.acquire();
|
||||||
try {
|
try {
|
||||||
|
@ -293,6 +295,71 @@ export const useEquipments = defineStore("equipments", () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seven Segment Display
|
||||||
|
const enableSevenSegmentDisplay = ref(false);
|
||||||
|
const sevenSegmentDisplayFrequency = ref(100);
|
||||||
|
const sevenSegmentDisplayData = ref<Uint8Array>();
|
||||||
|
const sevenSegmentDisplayHub = ref<HubConnection>();
|
||||||
|
const sevenSegmentDisplayHubProxy = ref<IDigitalTubesHub>();
|
||||||
|
|
||||||
|
async function sevenSegmentDisplaySetOnOff(enable: boolean) {
|
||||||
|
if (!sevenSegmentDisplayHub.value || !sevenSegmentDisplayHubProxy.value)
|
||||||
|
return;
|
||||||
|
await sevenSegmentDisplayHub.value.start();
|
||||||
|
|
||||||
|
if (enable) {
|
||||||
|
await sevenSegmentDisplayHubProxy.value.startScan();
|
||||||
|
} else {
|
||||||
|
await sevenSegmentDisplayHubProxy.value.stopScan();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sevenSegmentDisplaySetFrequency(frequency: number) {
|
||||||
|
if (!sevenSegmentDisplayHub.value || !sevenSegmentDisplayHubProxy.value)
|
||||||
|
return;
|
||||||
|
await sevenSegmentDisplayHub.value.start();
|
||||||
|
|
||||||
|
await sevenSegmentDisplayHubProxy.value.setFrequency(frequency);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sevenSegmentDisplayGetStatus() {
|
||||||
|
if (!sevenSegmentDisplayHub.value || !sevenSegmentDisplayHubProxy.value)
|
||||||
|
return;
|
||||||
|
await sevenSegmentDisplayHub.value.start();
|
||||||
|
|
||||||
|
return await sevenSegmentDisplayHubProxy.value.getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSevenSegmentDisplayOnReceive(msg: string) {
|
||||||
|
const bytes = base64ToArrayBuffer(msg);
|
||||||
|
sevenSegmentDisplayData.value = new Uint8Array(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// 每次挂载都重新创建连接
|
||||||
|
sevenSegmentDisplayHub.value =
|
||||||
|
AuthManager.createAuthenticatedJtagHubConnection();
|
||||||
|
sevenSegmentDisplayHubProxy.value = getHubProxyFactory(
|
||||||
|
"IDigitalTubesHub",
|
||||||
|
).createHubProxy(sevenSegmentDisplayHub.value);
|
||||||
|
|
||||||
|
getReceiverRegister("IDigitalTubesReceiver").register(
|
||||||
|
sevenSegmentDisplayHub.value,
|
||||||
|
{
|
||||||
|
onReceive: handleSevenSegmentDisplayOnReceive,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 断开连接,清理资源
|
||||||
|
if (sevenSegmentDisplayHub.value) {
|
||||||
|
sevenSegmentDisplayHub.value.stop();
|
||||||
|
sevenSegmentDisplayHub.value = undefined;
|
||||||
|
sevenSegmentDisplayHubProxy.value = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
boardAddr,
|
boardAddr,
|
||||||
boardPort,
|
boardPort,
|
||||||
|
@ -320,5 +387,13 @@ export const useEquipments = defineStore("equipments", () => {
|
||||||
enablePower,
|
enablePower,
|
||||||
powerClientMutex,
|
powerClientMutex,
|
||||||
powerSetOnOff,
|
powerSetOnOff,
|
||||||
|
|
||||||
|
// Seven Segment Display
|
||||||
|
enableSevenSegmentDisplay,
|
||||||
|
sevenSegmentDisplayData,
|
||||||
|
sevenSegmentDisplayFrequency,
|
||||||
|
sevenSegmentDisplaySetOnOff,
|
||||||
|
sevenSegmentDisplaySetFrequency,
|
||||||
|
sevenSegmentDisplayGetStatus,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -229,6 +229,15 @@ export class AuthManager {
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static createAuthenticatedDigitalTubesHubConnection() {
|
||||||
|
return new HubConnectionBuilder()
|
||||||
|
.withUrl("http://127.0.0.1:5000/hubs/DigitalTubesHub", {
|
||||||
|
accessTokenFactory: () => this.getToken() ?? "",
|
||||||
|
})
|
||||||
|
.withAutomaticReconnect()
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
// 登录函数
|
// 登录函数
|
||||||
public static async login(
|
public static async login(
|
||||||
username: string,
|
username: string,
|
||||||
|
|
|
@ -59,3 +59,12 @@ export function formatDate(date: Date | string) {
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function base64ToArrayBuffer(base64: string) {
|
||||||
|
var binaryString = atob(base64);
|
||||||
|
var bytes = new Uint8Array(binaryString.length);
|
||||||
|
for (var i = 0; i < binaryString.length; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes.buffer;
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import type { HubConnection, IStreamResult, Subject } from '@microsoft/signalr';
|
import type { HubConnection, IStreamResult, Subject } from '@microsoft/signalr';
|
||||||
import type { IDigitalTubesHub, IJtagHub, IProgressHub, IDigitalTubesReceiver, IJtagReceiver, IProgressReceiver } from './server.Hubs';
|
import type { IDigitalTubesHub, IJtagHub, IProgressHub, IDigitalTubesReceiver, IJtagReceiver, IProgressReceiver } from './server.Hubs';
|
||||||
import type { ProgressInfo } from '../server.Hubs';
|
import type { DigitalTubeTaskStatus, ProgressInfo } from '../server.Hubs';
|
||||||
|
|
||||||
|
|
||||||
// components
|
// components
|
||||||
|
@ -107,6 +107,10 @@ class IDigitalTubesHub_HubProxy implements IDigitalTubesHub {
|
||||||
public readonly setFrequency = async (frequency: number): Promise<boolean> => {
|
public readonly setFrequency = async (frequency: number): Promise<boolean> => {
|
||||||
return await this.connection.invoke("SetFrequency", frequency);
|
return await this.connection.invoke("SetFrequency", frequency);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public readonly getStatus = async (): Promise<DigitalTubeTaskStatus> => {
|
||||||
|
return await this.connection.invoke("GetStatus");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class IJtagHub_HubProxyFactory implements HubProxyFactory<IJtagHub> {
|
class IJtagHub_HubProxyFactory implements HubProxyFactory<IJtagHub> {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import type { IStreamResult, Subject } from '@microsoft/signalr';
|
import type { IStreamResult, Subject } from '@microsoft/signalr';
|
||||||
import type { ProgressInfo } from '../server.Hubs';
|
import type { DigitalTubeTaskStatus, ProgressInfo } from '../server.Hubs';
|
||||||
|
|
||||||
export type IDigitalTubesHub = {
|
export type IDigitalTubesHub = {
|
||||||
/**
|
/**
|
||||||
|
@ -19,6 +19,10 @@ export type IDigitalTubesHub = {
|
||||||
* @returns Transpiled from System.Threading.Tasks.Task<bool>
|
* @returns Transpiled from System.Threading.Tasks.Task<bool>
|
||||||
*/
|
*/
|
||||||
setFrequency(frequency: number): Promise<boolean>;
|
setFrequency(frequency: number): Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* @returns Transpiled from System.Threading.Tasks.Task<server.Hubs.DigitalTubeTaskStatus>
|
||||||
|
*/
|
||||||
|
getStatus(): Promise<DigitalTubeTaskStatus>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IJtagHub = {
|
export type IJtagHub = {
|
||||||
|
|
|
@ -2,6 +2,14 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
|
|
||||||
|
/** Transpiled from server.Hubs.DigitalTubeTaskStatus */
|
||||||
|
export type DigitalTubeTaskStatus = {
|
||||||
|
/** Transpiled from int */
|
||||||
|
frequency: number;
|
||||||
|
/** Transpiled from bool */
|
||||||
|
isRunning: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/** Transpiled from server.Hubs.ProgressStatus */
|
/** Transpiled from server.Hubs.ProgressStatus */
|
||||||
export enum ProgressStatus {
|
export enum ProgressStatus {
|
||||||
Pending = 0,
|
Pending = 0,
|
||||||
|
|
Loading…
Reference in New Issue