refactor: 使用更简洁的方式进行认证
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { ResourcePurpose } from "@/APIClient";
|
import { ResourceClient, ResourcePurpose } from "@/APIClient";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
|
|
||||||
// 定义 diagram.json 的类型结构
|
// 定义 diagram.json 的类型结构
|
||||||
@@ -94,7 +94,7 @@ export async function loadDiagramData(examId?: string): Promise<DiagramData> {
|
|||||||
// 如果提供了examId,优先从API加载实验的diagram
|
// 如果提供了examId,优先从API加载实验的diagram
|
||||||
if (examId) {
|
if (examId) {
|
||||||
try {
|
try {
|
||||||
const resourceClient = AuthManager.createAuthenticatedResourceClient();
|
const resourceClient = AuthManager.createClient(ResourceClient);
|
||||||
|
|
||||||
// 获取diagram类型的资源列表
|
// 获取diagram类型的资源列表
|
||||||
const resources = await resourceClient.getResourceList(
|
const resources = await resourceClient.getResourceList(
|
||||||
|
|||||||
@@ -31,8 +31,16 @@ export type Channel = {
|
|||||||
|
|
||||||
// 全局模式选项
|
// 全局模式选项
|
||||||
const globalModes = [
|
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.NAND, label: "NAND", description: "AND的非" },
|
||||||
{ value: GlobalCaptureMode.NOR, label: "NOR", description: "OR的非" },
|
{ value: GlobalCaptureMode.NOR, label: "NOR", description: "OR的非" },
|
||||||
];
|
];
|
||||||
@@ -70,14 +78,46 @@ const channelDivOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const ClockDivOptions = [
|
const ClockDivOptions = [
|
||||||
{ value: AnalyzerClockDiv.DIV1, label: "120MHz", description: "采样频率120MHz" },
|
{
|
||||||
{ value: AnalyzerClockDiv.DIV2, label: "60MHz", description: "采样频率60MHz" },
|
value: AnalyzerClockDiv.DIV1,
|
||||||
{ value: AnalyzerClockDiv.DIV4, label: "30MHz", description: "采样频率30MHz" },
|
label: "120MHz",
|
||||||
{ value: AnalyzerClockDiv.DIV8, label: "15MHz", description: "采样频率15MHz" },
|
description: "采样频率120MHz",
|
||||||
{ 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.DIV2,
|
||||||
{ value: AnalyzerClockDiv.DIV128, label: "937.5KHz", description: "采样频率937.5KHz" },
|
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",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 捕获深度限制常量
|
// 捕获深度限制常量
|
||||||
@@ -170,40 +210,64 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
|||||||
// 转换通道数字到枚举值
|
// 转换通道数字到枚举值
|
||||||
const getChannelDivEnum = (channelCount: number): AnalyzerChannelDiv => {
|
const getChannelDivEnum = (channelCount: number): AnalyzerChannelDiv => {
|
||||||
switch (channelCount) {
|
switch (channelCount) {
|
||||||
case 1: return AnalyzerChannelDiv.ONE;
|
case 1:
|
||||||
case 2: return AnalyzerChannelDiv.TWO;
|
return AnalyzerChannelDiv.ONE;
|
||||||
case 4: return AnalyzerChannelDiv.FOUR;
|
case 2:
|
||||||
case 8: return AnalyzerChannelDiv.EIGHT;
|
return AnalyzerChannelDiv.TWO;
|
||||||
case 16: return AnalyzerChannelDiv.XVI;
|
case 4:
|
||||||
case 32: return AnalyzerChannelDiv.XXXII;
|
return AnalyzerChannelDiv.FOUR;
|
||||||
default: return AnalyzerChannelDiv.EIGHT;
|
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)) {
|
if (!Number.isInteger(value)) {
|
||||||
return { valid: false, message: "捕获深度必须是整数" };
|
return { valid: false, message: "捕获深度必须是整数" };
|
||||||
}
|
}
|
||||||
if (value < CAPTURE_LENGTH_MIN) {
|
if (value < CAPTURE_LENGTH_MIN) {
|
||||||
return { valid: false, message: `捕获深度不能小于 ${CAPTURE_LENGTH_MIN}` };
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `捕获深度不能小于 ${CAPTURE_LENGTH_MIN}`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (value > CAPTURE_LENGTH_MAX) {
|
if (value > CAPTURE_LENGTH_MAX) {
|
||||||
return { valid: false, message: `捕获深度不能大于 ${CAPTURE_LENGTH_MAX.toLocaleString()}` };
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `捕获深度不能大于 ${CAPTURE_LENGTH_MAX.toLocaleString()}`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return { valid: true };
|
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)) {
|
if (!Number.isInteger(value)) {
|
||||||
return { valid: false, message: "预捕获深度必须是整数" };
|
return { valid: false, message: "预捕获深度必须是整数" };
|
||||||
}
|
}
|
||||||
if (value < PRE_CAPTURE_LENGTH_MIN) {
|
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) {
|
if (value >= currentCaptureLength) {
|
||||||
return { valid: false, message: `预捕获深度不能大于等于捕获深度 (${currentCaptureLength})` };
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `预捕获深度不能大于等于捕获深度 (${currentCaptureLength})`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return { valid: true };
|
return { valid: true };
|
||||||
};
|
};
|
||||||
@@ -241,7 +305,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
|||||||
// 设置通道组
|
// 设置通道组
|
||||||
const setChannelDiv = (channelCount: number) => {
|
const setChannelDiv = (channelCount: number) => {
|
||||||
// 验证通道数量是否有效
|
// 验证通道数量是否有效
|
||||||
if (!channelDivOptions.find(option => option.value === channelCount)) {
|
if (!channelDivOptions.find((option) => option.value === channelCount)) {
|
||||||
console.error(`无效的通道组设置: ${channelCount}`);
|
console.error(`无效的通道组设置: ${channelCount}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -257,7 +321,9 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
|||||||
channels[i].enabled = true;
|
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);
|
alert?.success(`已设置为${option?.label}`, 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -294,7 +360,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
|||||||
|
|
||||||
const getCaptureData = async () => {
|
const getCaptureData = async () => {
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
|
const client = AuthManager.createClient(LogicAnalyzerClient);
|
||||||
// 获取捕获数据,使用当前设置的捕获长度
|
// 获取捕获数据,使用当前设置的捕获长度
|
||||||
const base64Data = await client.getCaptureData(captureLength.value);
|
const base64Data = await client.getCaptureData(captureLength.value);
|
||||||
|
|
||||||
@@ -324,10 +390,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
|||||||
); // 转换为微秒
|
); // 转换为微秒
|
||||||
|
|
||||||
// 创建通道数据数组
|
// 创建通道数据数组
|
||||||
y = Array.from(
|
y = Array.from({ length: 1 }, () => new Array(sampleCount));
|
||||||
{ length: 1 },
|
|
||||||
() => new Array(sampleCount),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 解析数据:每个字节的8个位对应8个时间单位
|
// 解析数据:每个字节的8个位对应8个时间单位
|
||||||
for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
|
for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
|
||||||
@@ -348,10 +411,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
|||||||
); // 转换为微秒
|
); // 转换为微秒
|
||||||
|
|
||||||
// 创建通道数据数组
|
// 创建通道数据数组
|
||||||
y = Array.from(
|
y = Array.from({ length: 2 }, () => new Array(sampleCount));
|
||||||
{ length: 2 },
|
|
||||||
() => new Array(sampleCount),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 解析数据:每个字节的8个位对应4个时间单位的2通道数据
|
// 解析数据:每个字节的8个位对应4个时间单位的2通道数据
|
||||||
// 位分布:[T3_CH1, T3_CH0, T2_CH1, T2_CH0, T1_CH1, T1_CH0, T0_CH1, T0_CH0]
|
// 位分布:[T3_CH1, T3_CH0, T2_CH1, T2_CH0, T1_CH1, T1_CH0, T0_CH1, T0_CH0]
|
||||||
@@ -360,7 +420,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
|||||||
for (let timeUnit = 0; timeUnit < 4; timeUnit++) {
|
for (let timeUnit = 0; timeUnit < 4; timeUnit++) {
|
||||||
const timeIndex = byteIndex * 4 + timeUnit;
|
const timeIndex = byteIndex * 4 + timeUnit;
|
||||||
const bitOffset = timeUnit * 2;
|
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
|
y[1][timeIndex] = (byte >> (bitOffset + 1)) & 1; // CH1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -375,10 +435,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
|||||||
); // 转换为微秒
|
); // 转换为微秒
|
||||||
|
|
||||||
// 创建通道数据数组
|
// 创建通道数据数组
|
||||||
y = Array.from(
|
y = Array.from({ length: 4 }, () => new Array(sampleCount));
|
||||||
{ length: 4 },
|
|
||||||
() => new Array(sampleCount),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 解析数据:每个字节的8个位对应2个时间单位的4通道数据
|
// 解析数据:每个字节的8个位对应2个时间单位的4通道数据
|
||||||
// 位分布:[T1_CH3, T1_CH2, T1_CH1, T1_CH0, T0_CH3, T0_CH2, T0_CH1, T0_CH0]
|
// 位分布:[T1_CH3, T1_CH2, T1_CH1, T1_CH0, T0_CH3, T0_CH2, T0_CH1, T0_CH0]
|
||||||
@@ -408,10 +465,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
|||||||
); // 转换为微秒
|
); // 转换为微秒
|
||||||
|
|
||||||
// 创建8个通道的数据
|
// 创建8个通道的数据
|
||||||
y = Array.from(
|
y = Array.from({ length: 8 }, () => new Array(sampleCount));
|
||||||
{ length: 8 },
|
|
||||||
() => new Array(sampleCount),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 解析每个字节的8个位到对应通道
|
// 解析每个字节的8个位到对应通道
|
||||||
for (let i = 0; i < sampleCount; i++) {
|
for (let i = 0; i < sampleCount; i++) {
|
||||||
@@ -432,15 +486,12 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
|||||||
); // 转换为微秒
|
); // 转换为微秒
|
||||||
|
|
||||||
// 创建16个通道的数据
|
// 创建16个通道的数据
|
||||||
y = Array.from(
|
y = Array.from({ length: 16 }, () => new Array(sampleCount));
|
||||||
{ length: 16 },
|
|
||||||
() => new Array(sampleCount),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 解析数据:每2个字节为一个时间单位
|
// 解析数据:每2个字节为一个时间单位
|
||||||
for (let timeIndex = 0; timeIndex < sampleCount; timeIndex++) {
|
for (let timeIndex = 0; timeIndex < sampleCount; timeIndex++) {
|
||||||
const byteIndex = timeIndex * 2;
|
const byteIndex = timeIndex * 2;
|
||||||
const byte1 = bytes[byteIndex]; // [7:0]
|
const byte1 = bytes[byteIndex]; // [7:0]
|
||||||
const byte2 = bytes[byteIndex + 1]; // [15:8]
|
const byte2 = bytes[byteIndex + 1]; // [15:8]
|
||||||
|
|
||||||
// 处理低8位通道 [7:0]
|
// 处理低8位通道 [7:0]
|
||||||
@@ -464,15 +515,12 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
|||||||
); // 转换为微秒
|
); // 转换为微秒
|
||||||
|
|
||||||
// 创建32个通道的数据
|
// 创建32个通道的数据
|
||||||
y = Array.from(
|
y = Array.from({ length: 32 }, () => new Array(sampleCount));
|
||||||
{ length: 32 },
|
|
||||||
() => new Array(sampleCount),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 解析数据:每4个字节为一个时间单位
|
// 解析数据:每4个字节为一个时间单位
|
||||||
for (let timeIndex = 0; timeIndex < sampleCount; timeIndex++) {
|
for (let timeIndex = 0; timeIndex < sampleCount; timeIndex++) {
|
||||||
const byteIndex = timeIndex * 4;
|
const byteIndex = timeIndex * 4;
|
||||||
const byte1 = bytes[byteIndex]; // [7:0]
|
const byte1 = bytes[byteIndex]; // [7:0]
|
||||||
const byte2 = bytes[byteIndex + 1]; // [15:8]
|
const byte2 = bytes[byteIndex + 1]; // [15:8]
|
||||||
const byte3 = bytes[byteIndex + 2]; // [23:16]
|
const byte3 = bytes[byteIndex + 2]; // [23:16]
|
||||||
const byte4 = bytes[byteIndex + 3]; // [31:24]
|
const byte4 = bytes[byteIndex + 3]; // [31:24]
|
||||||
@@ -525,7 +573,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
|||||||
isCapturing.value = true;
|
isCapturing.value = true;
|
||||||
const release = await operationMutex.acquire();
|
const release = await operationMutex.acquire();
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
|
const client = AuthManager.createClient(LogicAnalyzerClient);
|
||||||
|
|
||||||
// 1. 先应用配置
|
// 1. 先应用配置
|
||||||
alert?.info("正在应用配置...", 2000);
|
alert?.info("正在应用配置...", 2000);
|
||||||
@@ -632,7 +680,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
|||||||
|
|
||||||
const release = await operationMutex.acquire();
|
const release = await operationMutex.acquire();
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
|
const client = AuthManager.createClient(LogicAnalyzerClient);
|
||||||
|
|
||||||
// 执行强制捕获来停止当前捕获
|
// 执行强制捕获来停止当前捕获
|
||||||
const forceSuccess = await client.setCaptureMode(false, false);
|
const forceSuccess = await client.setCaptureMode(false, false);
|
||||||
@@ -661,7 +709,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
|||||||
|
|
||||||
const release = await operationMutex.acquire();
|
const release = await operationMutex.acquire();
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
|
const client = AuthManager.createClient(LogicAnalyzerClient);
|
||||||
|
|
||||||
// 执行强制捕获来停止当前捕获
|
// 执行强制捕获来停止当前捕获
|
||||||
const forceSuccess = await client.setCaptureMode(true, true);
|
const forceSuccess = await client.setCaptureMode(true, true);
|
||||||
@@ -677,7 +725,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
|||||||
`强制捕获失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
`强制捕获失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||||
3000,
|
3000,
|
||||||
);
|
);
|
||||||
} finally{
|
} finally {
|
||||||
release();
|
release();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from "vue";
|
||||||
import { marked } from 'marked';
|
import { marked } from "marked";
|
||||||
import hljs from 'highlight.js';
|
import hljs from "highlight.js";
|
||||||
// 导入亮色主题样式
|
// 导入亮色主题样式
|
||||||
import 'highlight.js/styles/github.css'; // 亮色主题
|
import "highlight.js/styles/github.css"; // 亮色主题
|
||||||
// 导入主题存储
|
// 导入主题存储
|
||||||
import { useThemeStore } from '@/stores/theme';
|
import { useThemeStore } from "@/stores/theme";
|
||||||
import { AuthManager } from '@/utils/AuthManager';
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
|
import { ResourceClient, ResourcePurpose } from "@/APIClient";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
content: {
|
content: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
removeFirstH1: {
|
removeFirstH1: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
examId: {
|
examId: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: "",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 使用主题存储
|
// 使用主题存储
|
||||||
@@ -32,17 +33,26 @@ const isDarkMode = computed(() => themeStore.isDarkTheme());
|
|||||||
const imageResourceCache = ref<Map<string, string>>(new Map());
|
const imageResourceCache = ref<Map<string, string>>(new Map());
|
||||||
|
|
||||||
// 获取图片资源ID的函数
|
// 获取图片资源ID的函数
|
||||||
async function getImageResourceId(examId: string, imagePath: string): Promise<string | null> {
|
async function getImageResourceId(
|
||||||
|
examId: string,
|
||||||
|
imagePath: string,
|
||||||
|
): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createAuthenticatedResourceClient();
|
const client = AuthManager.createClient(ResourceClient);
|
||||||
const resources = await client.getResourceList(examId, 'images', 'template');
|
const resources = await client.getResourceList(
|
||||||
|
examId,
|
||||||
|
"images",
|
||||||
|
ResourcePurpose.Template,
|
||||||
|
);
|
||||||
|
|
||||||
// 查找匹配的图片资源
|
// 查找匹配的图片资源
|
||||||
const imageResource = resources.find(r => r.name === imagePath || r.name.endsWith(imagePath));
|
const imageResource = resources.find(
|
||||||
|
(r) => r.name === imagePath || r.name.endsWith(imagePath),
|
||||||
|
);
|
||||||
|
|
||||||
return imageResource ? imageResource.id.toString() : null;
|
return imageResource ? imageResource.id.toString() : null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取图片资源ID失败:', error);
|
console.error("获取图片资源ID失败:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,7 +60,7 @@ async function getImageResourceId(examId: string, imagePath: string): Promise<st
|
|||||||
// 通过资源ID获取图片数据URL
|
// 通过资源ID获取图片数据URL
|
||||||
async function getImageDataUrl(resourceId: string): Promise<string | null> {
|
async function getImageDataUrl(resourceId: string): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createAuthenticatedResourceClient();
|
const client = AuthManager.createClient(ResourceClient);
|
||||||
const response = await client.getResourceById(parseInt(resourceId));
|
const response = await client.getResourceById(parseInt(resourceId));
|
||||||
|
|
||||||
if (response && response.data) {
|
if (response && response.data) {
|
||||||
@@ -59,140 +69,164 @@ async function getImageDataUrl(resourceId: string): Promise<string | null> {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取图片数据失败:', error);
|
console.error("获取图片数据失败:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听主题变化
|
// 监听主题变化
|
||||||
watch(() => themeStore.currentTheme, () => {
|
watch(
|
||||||
|
() => themeStore.currentTheme,
|
||||||
|
() => {
|
||||||
// 主题变化时更新代码高亮样式
|
// 主题变化时更新代码高亮样式
|
||||||
updateCodeBlocksTheme();
|
updateCodeBlocksTheme();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// 更新代码块主题样式
|
// 更新代码块主题样式
|
||||||
const updateCodeBlocksTheme = () => {
|
const updateCodeBlocksTheme = () => {
|
||||||
// 这个函数可以在需要时手动更新代码块的样式
|
// 这个函数可以在需要时手动更新代码块的样式
|
||||||
// 由于我们使用CSS变量控制样式,可能不需要特定实现
|
// 由于我们使用CSS变量控制样式,可能不需要特定实现
|
||||||
// 但如果需要,可以在这里添加额外逻辑
|
// 但如果需要,可以在这里添加额外逻辑
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderedContent = computed(() => {
|
const renderedContent = computed(() => {
|
||||||
if (!props.content) return '<p>没有内容</p>';
|
if (!props.content) return "<p>没有内容</p>";
|
||||||
|
|
||||||
let processedContent = props.content;
|
let processedContent = props.content;
|
||||||
|
|
||||||
// 如果需要,移除第一个一级标题
|
// 如果需要,移除第一个一级标题
|
||||||
if (props.removeFirstH1) {
|
if (props.removeFirstH1) {
|
||||||
const lines = processedContent.split('\n');
|
const lines = processedContent.split("\n");
|
||||||
const firstH1Index = lines.findIndex(line => line.startsWith('# '));
|
const firstH1Index = lines.findIndex((line) => line.startsWith("# "));
|
||||||
|
|
||||||
if (firstH1Index !== -1) {
|
if (firstH1Index !== -1) {
|
||||||
processedContent = lines.slice(firstH1Index + 1).join('\n');
|
processedContent = lines.slice(firstH1Index + 1).join("\n");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建自定义渲染器
|
||||||
|
const renderer = new marked.Renderer();
|
||||||
|
|
||||||
|
// 重写图片渲染方法,处理相对路径
|
||||||
|
renderer.image = (href, title, text) => {
|
||||||
|
let src = href;
|
||||||
|
|
||||||
|
console.log(`原始图片路径: ${href}, examId: ${props.examId}`);
|
||||||
|
|
||||||
|
// 如果是相对路径且有实验ID,需要通过动态API获取
|
||||||
|
if (props.examId && href && href.startsWith("./")) {
|
||||||
|
// 对于相对路径的图片,我们需要先获取图片资源ID,然后通过动态API获取
|
||||||
|
// 暂时保留原始路径,在后处理中进行替换
|
||||||
|
src = href;
|
||||||
|
console.log(`保留原始路径用于后处理: ${src}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建自定义渲染器
|
const titleAttr = title ? ` title="${title}"` : "";
|
||||||
const renderer = new marked.Renderer();
|
const altAttr = text ? ` alt="${text}"` : "";
|
||||||
|
const dataOriginal =
|
||||||
|
href && href.startsWith("./") ? ` data-original-src="${href}"` : "";
|
||||||
|
console.log(
|
||||||
|
`最终渲染的HTML: <img src="${src}"${titleAttr}${altAttr}${dataOriginal} />`,
|
||||||
|
);
|
||||||
|
return `<img src="${src}"${titleAttr}${altAttr}${dataOriginal} />`;
|
||||||
|
};
|
||||||
|
|
||||||
// 重写图片渲染方法,处理相对路径
|
// 重写代码块渲染方法,添加语言信息
|
||||||
renderer.image = (href, title, text) => {
|
renderer.code = (code, incomingLanguage) => {
|
||||||
let src = href;
|
// 确保语言参数是字符串
|
||||||
|
const language = incomingLanguage || "plaintext";
|
||||||
|
// 验证语言
|
||||||
|
const validLanguage = hljs.getLanguage(language) ? language : "plaintext";
|
||||||
|
// 高亮代码
|
||||||
|
const highlightedCode = hljs.highlight(code, {
|
||||||
|
language: validLanguage,
|
||||||
|
}).value;
|
||||||
|
|
||||||
console.log(`原始图片路径: ${href}, examId: ${props.examId}`);
|
// 添加语言标签到代码块
|
||||||
|
return `<pre class="hljs" data-language="${validLanguage}"><code class="language-${validLanguage}">${highlightedCode}</code></pre>`;
|
||||||
|
};
|
||||||
|
|
||||||
// 如果是相对路径且有实验ID,需要通过动态API获取
|
// 设置 marked 选项并解析内容
|
||||||
if (props.examId && href && href.startsWith('./')) {
|
let html = marked.parse(processedContent, {
|
||||||
// 对于相对路径的图片,我们需要先获取图片资源ID,然后通过动态API获取
|
renderer: renderer,
|
||||||
// 暂时保留原始路径,在后处理中进行替换
|
gfm: true,
|
||||||
src = href;
|
breaks: true,
|
||||||
console.log(`保留原始路径用于后处理: ${src}`);
|
}) as string;
|
||||||
|
|
||||||
|
// 后处理HTML,异步处理图片
|
||||||
|
if (props.examId) {
|
||||||
|
// 查找所有需要处理的图片
|
||||||
|
const imgMatches = Array.from(
|
||||||
|
html.matchAll(
|
||||||
|
/(<img[^>]+data-original-src=["'])\.\/([^"']+)(["'][^>]*>)/g,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 异步处理每个图片
|
||||||
|
imgMatches.forEach(async (match) => {
|
||||||
|
const [fullMatch, prefix, path, suffix] = match;
|
||||||
|
const imagePath = path.replace("images/", "");
|
||||||
|
|
||||||
|
// 检查缓存
|
||||||
|
if (imageResourceCache.value.has(imagePath)) {
|
||||||
|
const cachedUrl = imageResourceCache.value.get(imagePath)!;
|
||||||
|
html = html.replace(
|
||||||
|
fullMatch,
|
||||||
|
`${prefix}${cachedUrl}${suffix.replace(' data-original-src="./' + path + '"', "")}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取图片资源ID
|
||||||
|
const resourceId = await getImageResourceId(props.examId, imagePath);
|
||||||
|
if (resourceId) {
|
||||||
|
// 获取图片数据URL
|
||||||
|
const dataUrl = await getImageDataUrl(resourceId);
|
||||||
|
if (dataUrl) {
|
||||||
|
// 缓存URL
|
||||||
|
imageResourceCache.value.set(imagePath, dataUrl);
|
||||||
|
|
||||||
|
// 更新HTML中的图片src
|
||||||
|
const updatedHtml = html.replace(
|
||||||
|
fullMatch,
|
||||||
|
`${prefix}${dataUrl}${suffix.replace(' data-original-src="./' + path + '"', "")}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 触发重新渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
const imgElements = document.querySelectorAll(
|
||||||
|
`img[data-original-src="./${path}"]`,
|
||||||
|
);
|
||||||
|
imgElements.forEach((img) => {
|
||||||
|
(img as HTMLImageElement).src = dataUrl;
|
||||||
|
img.removeAttribute("data-original-src");
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`处理图片 ${imagePath} 失败:`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const titleAttr = title ? ` title="${title}"` : '';
|
return html;
|
||||||
const altAttr = text ? ` alt="${text}"` : '';
|
|
||||||
const dataOriginal = href && href.startsWith('./') ? ` data-original-src="${href}"` : '';
|
|
||||||
console.log(`最终渲染的HTML: <img src="${src}"${titleAttr}${altAttr}${dataOriginal} />`);
|
|
||||||
return `<img src="${src}"${titleAttr}${altAttr}${dataOriginal} />`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 重写代码块渲染方法,添加语言信息
|
|
||||||
renderer.code = (code, incomingLanguage) => {
|
|
||||||
// 确保语言参数是字符串
|
|
||||||
const language = incomingLanguage || 'plaintext';
|
|
||||||
// 验证语言
|
|
||||||
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext';
|
|
||||||
// 高亮代码
|
|
||||||
const highlightedCode = hljs.highlight(code, { language: validLanguage }).value;
|
|
||||||
|
|
||||||
// 添加语言标签到代码块
|
|
||||||
return `<pre class="hljs" data-language="${validLanguage}"><code class="language-${validLanguage}">${highlightedCode}</code></pre>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 设置 marked 选项并解析内容
|
|
||||||
let html = marked.parse(processedContent, {
|
|
||||||
renderer: renderer,
|
|
||||||
gfm: true,
|
|
||||||
breaks: true
|
|
||||||
}) as string;
|
|
||||||
|
|
||||||
// 后处理HTML,异步处理图片
|
|
||||||
if (props.examId) {
|
|
||||||
// 查找所有需要处理的图片
|
|
||||||
const imgMatches = Array.from(html.matchAll(/(<img[^>]+data-original-src=["'])\.\/([^"']+)(["'][^>]*>)/g));
|
|
||||||
|
|
||||||
// 异步处理每个图片
|
|
||||||
imgMatches.forEach(async (match) => {
|
|
||||||
const [fullMatch, prefix, path, suffix] = match;
|
|
||||||
const imagePath = path.replace('images/', '');
|
|
||||||
|
|
||||||
// 检查缓存
|
|
||||||
if (imageResourceCache.value.has(imagePath)) {
|
|
||||||
const cachedUrl = imageResourceCache.value.get(imagePath)!;
|
|
||||||
html = html.replace(fullMatch, `${prefix}${cachedUrl}${suffix.replace(' data-original-src="./'+path+'"', '')}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 获取图片资源ID
|
|
||||||
const resourceId = await getImageResourceId(props.examId, imagePath);
|
|
||||||
if (resourceId) {
|
|
||||||
// 获取图片数据URL
|
|
||||||
const dataUrl = await getImageDataUrl(resourceId);
|
|
||||||
if (dataUrl) {
|
|
||||||
// 缓存URL
|
|
||||||
imageResourceCache.value.set(imagePath, dataUrl);
|
|
||||||
|
|
||||||
// 更新HTML中的图片src
|
|
||||||
const updatedHtml = html.replace(fullMatch, `${prefix}${dataUrl}${suffix.replace(' data-original-src="./'+path+'"', '')}`);
|
|
||||||
|
|
||||||
// 触发重新渲染
|
|
||||||
setTimeout(() => {
|
|
||||||
const imgElements = document.querySelectorAll(`img[data-original-src="./${path}"]`);
|
|
||||||
imgElements.forEach(img => {
|
|
||||||
(img as HTMLImageElement).src = dataUrl;
|
|
||||||
img.removeAttribute('data-original-src');
|
|
||||||
});
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`处理图片 ${imagePath} 失败:`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 页面挂载后,确保应用正确的主题样式
|
// 页面挂载后,确保应用正确的主题样式
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
updateCodeBlocksTheme();
|
updateCodeBlocksTheme();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="markdown-content" :data-theme="themeStore.currentTheme" v-html="renderedContent"></div>
|
<div
|
||||||
|
class="markdown-content"
|
||||||
|
:data-theme="themeStore.currentTheme"
|
||||||
|
v-html="renderedContent"
|
||||||
|
></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -211,7 +245,7 @@ onMounted(() => {
|
|||||||
display: block;
|
display: block;
|
||||||
margin: 1rem auto;
|
margin: 1rem auto;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content :deep(h1) {
|
.markdown-content :deep(h1) {
|
||||||
@@ -223,7 +257,7 @@ onMounted(() => {
|
|||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
padding-bottom: 0.7rem;
|
padding-bottom: 0.7rem;
|
||||||
border-bottom: 2px solid hsl(var(--p) / 0.7);
|
border-bottom: 2px solid hsl(var(--p) / 0.7);
|
||||||
text-shadow: 1px 1px 2px rgba(0,0,0,0.05);
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content :deep(h2) {
|
.markdown-content :deep(h2) {
|
||||||
@@ -268,7 +302,7 @@ onMounted(() => {
|
|||||||
.markdown-content :deep(h4::before),
|
.markdown-content :deep(h4::before),
|
||||||
.markdown-content :deep(h5::before),
|
.markdown-content :deep(h5::before),
|
||||||
.markdown-content :deep(h6::before) {
|
.markdown-content :deep(h6::before) {
|
||||||
content: '▶';
|
content: "▶";
|
||||||
color: hsl(var(--p) / 0.7);
|
color: hsl(var(--p) / 0.7);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0.2rem;
|
left: 0.2rem;
|
||||||
@@ -343,13 +377,13 @@ onMounted(() => {
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
border: 1px solid hsl(var(--b2));
|
border: 1px solid hsl(var(--b2));
|
||||||
margin: 1.5rem 0;
|
margin: 1.5rem 0;
|
||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
position: relative;
|
position: relative;
|
||||||
color: var(--code-color, hsl(var(--bc)));
|
color: var(--code-color, hsl(var(--bc)));
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content :deep(pre::before) {
|
.markdown-content :deep(pre::before) {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -375,7 +409,9 @@ onMounted(() => {
|
|||||||
|
|
||||||
/* 内联代码样式 */
|
/* 内联代码样式 */
|
||||||
.markdown-content :deep(code) {
|
.markdown-content :deep(code) {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
font-family:
|
||||||
|
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||||
|
"Courier New", monospace;
|
||||||
background-color: var(--inline-code-bg, hsl(var(--b3) / 0.7));
|
background-color: var(--inline-code-bg, hsl(var(--b3) / 0.7));
|
||||||
padding: 0.2rem 0.5rem;
|
padding: 0.2rem 0.5rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
@@ -392,7 +428,9 @@ onMounted(() => {
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
font-family:
|
||||||
|
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||||
|
"Courier New", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 为常见语言添加一些特殊的高亮效果 */
|
/* 为常见语言添加一些特殊的高亮效果 */
|
||||||
@@ -443,7 +481,7 @@ onMounted(() => {
|
|||||||
background-color: hsl(var(--b1));
|
background-color: hsl(var(--b1));
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
border: 1px solid hsl(var(--b2));
|
border: 1px solid hsl(var(--b2));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,7 +519,7 @@ onMounted(() => {
|
|||||||
color: hsl(var(--bc) / 0.9);
|
color: hsl(var(--bc) / 0.9);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
border-radius: 0 0.5rem 0.5rem 0;
|
border-radius: 0 0.5rem 0.5rem 0;
|
||||||
box-shadow: 0 2px 5px rgba(0,0,0,0.03);
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.03);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,7 +710,7 @@ onMounted(() => {
|
|||||||
background-color: hsl(var(--b1));
|
background-color: hsl(var(--b1));
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
border: 1px solid hsl(var(--b2));
|
border: 1px solid hsl(var(--b2));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,7 +748,7 @@ onMounted(() => {
|
|||||||
color: hsl(var(--bc) / 0.9);
|
color: hsl(var(--bc) / 0.9);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
border-radius: 0 0.5rem 0.5rem 0;
|
border-radius: 0 0.5rem 0.5rem 0;
|
||||||
box-shadow: 0 2px 5px rgba(0,0,0,0.03);
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.03);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ import {
|
|||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
} from "lucide-vue-next";
|
} from "lucide-vue-next";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
|
import { DataClient } from "@/APIClient";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -158,7 +159,7 @@ const loadUserInfo = async () => {
|
|||||||
try {
|
try {
|
||||||
const authenticated = await AuthManager.isAuthenticated();
|
const authenticated = await AuthManager.isAuthenticated();
|
||||||
if (authenticated) {
|
if (authenticated) {
|
||||||
const client = AuthManager.createAuthenticatedDataClient();
|
const client = AuthManager.createClient(DataClient);
|
||||||
const userInfo = await client.getUserInfo();
|
const userInfo = await client.getUserInfo();
|
||||||
userName.value = userInfo.name;
|
userName.value = userInfo.name;
|
||||||
isLoggedIn.value = true;
|
isLoggedIn.value = true;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Mutex } from "async-mutex";
|
|||||||
import {
|
import {
|
||||||
OscilloscopeFullConfig,
|
OscilloscopeFullConfig,
|
||||||
OscilloscopeDataResponse,
|
OscilloscopeDataResponse,
|
||||||
|
OscilloscopeApiClient,
|
||||||
} from "@/APIClient";
|
} from "@/APIClient";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
import { useAlertStore } from "@/components/Alert";
|
import { useAlertStore } from "@/components/Alert";
|
||||||
@@ -31,257 +32,269 @@ const DEFAULT_CONFIG: OscilloscopeFullConfig = new OscilloscopeFullConfig({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 采样频率常量(后端返回)
|
// 采样频率常量(后端返回)
|
||||||
const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(() => {
|
const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
|
||||||
const oscData = shallowRef<OscilloscopeDataType>();
|
() => {
|
||||||
const alert = useRequiredInjection(useAlertStore);
|
const oscData = shallowRef<OscilloscopeDataType>();
|
||||||
|
const alert = useRequiredInjection(useAlertStore);
|
||||||
|
|
||||||
// 互斥锁
|
// 互斥锁
|
||||||
const operationMutex = new Mutex();
|
const operationMutex = new Mutex();
|
||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
const isApplying = ref(false);
|
const isApplying = ref(false);
|
||||||
const isCapturing = 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计算
|
// 采样周期(ns),由adFrequency计算
|
||||||
const samplePeriodNs = computed(() =>
|
const samplePeriodNs = computed(() =>
|
||||||
oscData.value?.adFrequency ? 1_000_000_000 / oscData.value.adFrequency : 200
|
oscData.value?.adFrequency
|
||||||
);
|
? 1_000_000_000 / oscData.value.adFrequency
|
||||||
|
: 200,
|
||||||
|
);
|
||||||
|
|
||||||
// 应用配置
|
// 应用配置
|
||||||
const applyConfiguration = async () => {
|
const applyConfiguration = async () => {
|
||||||
if (operationMutex.isLocked()) {
|
if (operationMutex.isLocked()) {
|
||||||
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
|
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
|
||||||
return;
|
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("应用失败");
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
const release = await operationMutex.acquire();
|
||||||
alert.error("应用配置失败", 3000);
|
isApplying.value = true;
|
||||||
} finally {
|
try {
|
||||||
isApplying.value = false;
|
const client = AuthManager.createClient(OscilloscopeApiClient);
|
||||||
release();
|
const success = await client.initialize({ ...config });
|
||||||
}
|
if (success) {
|
||||||
};
|
alert.success("示波器配置已应用", 2000);
|
||||||
|
} else {
|
||||||
// 重置配置
|
throw new Error("应用失败");
|
||||||
const resetConfiguration = () => {
|
}
|
||||||
Object.assign(config, { ...DEFAULT_CONFIG });
|
} catch (error) {
|
||||||
alert.info("配置已重置", 2000);
|
alert.error("应用配置失败", 3000);
|
||||||
};
|
} finally {
|
||||||
|
isApplying.value = false;
|
||||||
const clearOscilloscopeData = () => {
|
release();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
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(
|
const x = Array.from(
|
||||||
{ length: bytes.length },
|
{ length: points },
|
||||||
(_, i) => (i * samplePeriodNs.value) / 1000 // us
|
(_, 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 = {
|
oscData.value = {
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
xUnit: "us",
|
xUnit: "us",
|
||||||
yUnit: "V",
|
yUnit: "V",
|
||||||
adFrequency: resp.adFrequency,
|
adFrequency: freq,
|
||||||
adVpp: resp.adVpp,
|
adVpp: 2.0,
|
||||||
adMax: resp.adMax,
|
adMax: 255,
|
||||||
adMin: resp.adMin,
|
adMin: 0,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
alert.success("测试数据生成成功", 2000);
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
oscData,
|
oscData,
|
||||||
config,
|
config,
|
||||||
isApplying,
|
isApplying,
|
||||||
isCapturing,
|
isCapturing,
|
||||||
sampleCount,
|
sampleCount,
|
||||||
samplePeriodNs,
|
samplePeriodNs,
|
||||||
refreshIntervalMs,
|
refreshIntervalMs,
|
||||||
|
|
||||||
applyConfiguration,
|
applyConfiguration,
|
||||||
resetConfiguration,
|
resetConfiguration,
|
||||||
clearOscilloscopeData,
|
clearOscilloscopeData,
|
||||||
getOscilloscopeData,
|
getOscilloscopeData,
|
||||||
startCapture,
|
startCapture,
|
||||||
stopCapture,
|
stopCapture,
|
||||||
updateTrigger,
|
updateTrigger,
|
||||||
updateSampling,
|
updateSampling,
|
||||||
refreshRAM,
|
refreshRAM,
|
||||||
generateTestData,
|
generateTestData,
|
||||||
};
|
};
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export { useProvideOscilloscope, useOscilloscopeState, DEFAULT_CONFIG };
|
export { useProvideOscilloscope, useOscilloscopeState, DEFAULT_CONFIG };
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
import type { ExamInfo } from "@/APIClient";
|
import { ExamClient, ResourceClient, type ExamInfo } from "@/APIClient";
|
||||||
|
|
||||||
// 接口定义
|
// 接口定义
|
||||||
interface Tutorial {
|
interface Tutorial {
|
||||||
@@ -121,7 +121,7 @@ onMounted(async () => {
|
|||||||
console.log("正在从数据库加载实验数据...");
|
console.log("正在从数据库加载实验数据...");
|
||||||
|
|
||||||
// 创建认证客户端
|
// 创建认证客户端
|
||||||
const client = AuthManager.createAuthenticatedExamClient();
|
const client = AuthManager.createClient(ExamClient);
|
||||||
|
|
||||||
// 获取实验列表
|
// 获取实验列表
|
||||||
const examList: ExamInfo[] = await client.getExamList();
|
const examList: ExamInfo[] = await client.getExamList();
|
||||||
@@ -142,7 +142,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取实验的封面资源(模板资源)
|
// 获取实验的封面资源(模板资源)
|
||||||
const resourceClient = AuthManager.createAuthenticatedResourceClient();
|
const resourceClient = AuthManager.createClient(ResourceClient);
|
||||||
const resourceList = await resourceClient.getResourceList(
|
const resourceList = await resourceClient.getResourceList(
|
||||||
exam.id,
|
exam.id,
|
||||||
"cover",
|
"cover",
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ import {
|
|||||||
import { ProgressStatus } from "@/utils/signalR/server.Hubs";
|
import { ProgressStatus } from "@/utils/signalR/server.Hubs";
|
||||||
import { useRequiredInjection } from "@/utils/Common";
|
import { useRequiredInjection } from "@/utils/Common";
|
||||||
import { useAlertStore } from "./Alert";
|
import { useAlertStore } from "./Alert";
|
||||||
|
import { ResourceClient } from "@/APIClient";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
maxMemory?: number;
|
maxMemory?: number;
|
||||||
@@ -138,8 +139,7 @@ const progressHubReceiver: IProgressReceiver = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
progressHubConnection.value =
|
progressHubConnection.value = AuthManager.createHubConnection("ProgressHub");
|
||||||
AuthManager.createAuthenticatedProgressHubConnection();
|
|
||||||
progressHubProxy.value = getHubProxyFactory("IProgressHub").createHubProxy(
|
progressHubProxy.value = getHubProxyFactory("IProgressHub").createHubProxy(
|
||||||
progressHubConnection.value,
|
progressHubConnection.value,
|
||||||
);
|
);
|
||||||
@@ -175,7 +175,7 @@ async function loadAvailableBitstreams() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resourceClient = AuthManager.createAuthenticatedResourceClient();
|
const resourceClient = AuthManager.createClient(ResourceClient);
|
||||||
// 使用新的ResourceClient API获取比特流模板资源列表
|
// 使用新的ResourceClient API获取比特流模板资源列表
|
||||||
const resources = await resourceClient.getResourceList(
|
const resources = await resourceClient.getResourceList(
|
||||||
props.examId,
|
props.examId,
|
||||||
@@ -199,7 +199,7 @@ async function downloadExampleBitstream(bitstream: {
|
|||||||
|
|
||||||
isDownloading.value = true;
|
isDownloading.value = true;
|
||||||
try {
|
try {
|
||||||
const resourceClient = AuthManager.createAuthenticatedResourceClient();
|
const resourceClient = AuthManager.createClient(ResourceClient);
|
||||||
|
|
||||||
// 使用新的ResourceClient API获取资源文件
|
// 使用新的ResourceClient API获取资源文件
|
||||||
const response = await resourceClient.getResourceById(bitstream.id);
|
const response = await resourceClient.getResourceById(bitstream.id);
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ import { useEquipments } from "@/stores/equipments";
|
|||||||
import { useDialogStore } from "@/stores/dialog";
|
import { useDialogStore } from "@/stores/dialog";
|
||||||
import { toInteger } from "lodash";
|
import { toInteger } from "lodash";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
|
import { DDSClient } from "@/APIClient";
|
||||||
|
|
||||||
// Component Attributes
|
// Component Attributes
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -221,7 +222,7 @@ const props = defineProps<{
|
|||||||
const emit = defineEmits(["update:modelValue"]);
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
|
|
||||||
// Global varibles
|
// Global varibles
|
||||||
const dds = AuthManager.createAuthenticatedDDSClient();
|
const dds = AuthManager.createClient(DDSClient);
|
||||||
const eqps = useEquipments();
|
const eqps = useEquipments();
|
||||||
const dialog = useDialogStore();
|
const dialog = useDialogStore();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// filepath: c:\_Project\FPGA_WebLab\FPGA_WebLab\src\components\equipments\Switch.vue
|
|
||||||
<template>
|
<template>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -9,9 +8,23 @@
|
|||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
<feFlood result="flood" flood-color="#f08a5d" flood-opacity="1"></feFlood>
|
<feFlood
|
||||||
<feComposite in="flood" result="mask" in2="SourceGraphic" operator="in"></feComposite>
|
result="flood"
|
||||||
<feMorphology in="mask" result="dilated" operator="dilate" radius="0.02"></feMorphology>
|
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.05" result="blur1" />
|
||||||
<feGaussianBlur in="dilated" stdDeviation="0.1" result="blur2" />
|
<feGaussianBlur in="dilated" stdDeviation="0.1" result="blur2" />
|
||||||
<feGaussianBlur in="dilated" stdDeviation="0.2" result="blur3" />
|
<feGaussianBlur in="dilated" stdDeviation="0.2" result="blur3" />
|
||||||
@@ -23,12 +36,24 @@
|
|||||||
</feMerge>
|
</feMerge>
|
||||||
</filter>
|
</filter>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
<g>
|
<g>
|
||||||
<!-- 红色背景随开关数量变化宽度 -->
|
<rect
|
||||||
<rect :width="props.switchCount + 2" height="4" x="4" y="6" fill="#c01401" rx="0.1" />
|
:width="props.switchCount + 2"
|
||||||
<text v-if="props.showLabels" fill="white" font-size="0.7" x="4.25" y="6.75">ON</text>
|
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>
|
<g>
|
||||||
<template v-for="(_, index) in Array(props.switchCount)" :key="index">
|
<template v-for="(_, index) in Array(props.switchCount)" :key="index">
|
||||||
<rect
|
<rect
|
||||||
@@ -53,9 +78,11 @@
|
|||||||
</text>
|
</text>
|
||||||
</template>
|
</template>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<g>
|
<g>
|
||||||
<template v-for="(location, index) in btnLocation" :key="`btn-${index}`">
|
<template
|
||||||
|
v-for="(location, index) in btnLocation"
|
||||||
|
:key="`btn-${index}`"
|
||||||
|
>
|
||||||
<rect
|
<rect
|
||||||
class="interactive"
|
class="interactive"
|
||||||
@click="toggleBtnStatus(index)"
|
@click="toggleBtnStatus(index)"
|
||||||
@@ -74,118 +101,99 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, watch } from "vue";
|
import { ref, computed, watch, onMounted } from "vue";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
size?: number;
|
size?: number;
|
||||||
switchCount?: number;
|
switchCount?: number;
|
||||||
// 新增属性
|
initialValues?: boolean[] | string;
|
||||||
initialValues?: boolean[] | string; // 开关的初始状态,可以是布尔数组或逗号分隔的字符串
|
showLabels?: boolean;
|
||||||
showLabels?: boolean; // 是否显示标签
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
size: 1,
|
size: 1,
|
||||||
switchCount: 6,
|
switchCount: 6,
|
||||||
initialValues: () => [],
|
initialValues: () => [],
|
||||||
showLabels: true
|
showLabels: true,
|
||||||
});
|
});
|
||||||
|
const emit = defineEmits(["change"]);
|
||||||
|
|
||||||
// 计算实际宽高
|
// 解析初始值
|
||||||
const width = computed(() => {
|
function parseInitialValues(): boolean[] {
|
||||||
// 每个开关占用25px宽度,再加上两侧边距(20px)
|
|
||||||
return (props.switchCount * 25 + 20) * props.size;
|
|
||||||
});
|
|
||||||
const height = computed(() => 85 * props.size); // 高度保持固定比例
|
|
||||||
|
|
||||||
// 定义发出的事件
|
|
||||||
const emit = defineEmits(['change', 'switch-toggle']);
|
|
||||||
|
|
||||||
// 解析初始值,支持字符串和数组两种格式
|
|
||||||
const parseInitialValues = () => {
|
|
||||||
if (Array.isArray(props.initialValues)) {
|
if (Array.isArray(props.initialValues)) {
|
||||||
return [...props.initialValues].slice(0, props.switchCount);
|
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;
|
|
||||||
}
|
}
|
||||||
// 默认返回全部为 false 的数组
|
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);
|
||||||
|
}
|
||||||
return Array(props.switchCount).fill(false);
|
return Array(props.switchCount).fill(false);
|
||||||
};
|
}
|
||||||
|
|
||||||
// 初始化按钮状态
|
// 状态唯一真相
|
||||||
const btnStatus = ref(parseInitialValues());
|
const btnStatus = ref<boolean[]>(parseInitialValues());
|
||||||
|
|
||||||
// 监听 switchCount 变化,调整开关状态数组
|
// 计算宽高
|
||||||
watch(() => props.switchCount, (newCount) => {
|
const width = computed(() => (props.switchCount * 25 + 20) * props.size);
|
||||||
if (newCount !== btnStatus.value.length) {
|
const height = computed(() => 85 * props.size);
|
||||||
// 如果新数量大于当前数量,则扩展数组
|
|
||||||
if (newCount > btnStatus.value.length) {
|
|
||||||
btnStatus.value = [
|
|
||||||
...btnStatus.value,
|
|
||||||
...Array(newCount - btnStatus.value.length).fill(false)
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
// 如果新数量小于当前数量,则截断数组
|
|
||||||
btnStatus.value = btnStatus.value.slice(0, newCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
// 监听 initialValues 变化,更新开关状态
|
// 按钮位置
|
||||||
watch(() => props.initialValues, () => {
|
const btnLocation = computed(() =>
|
||||||
btnStatus.value = parseInitialValues();
|
btnStatus.value.map((status) => (status ? 7.025 : 8.325)),
|
||||||
});
|
);
|
||||||
|
|
||||||
const btnLocation = computed(() => {
|
// 状态变更统一处理
|
||||||
return btnStatus.value.map((status) => {
|
function updateStatus(newStates: boolean[], index?: number) {
|
||||||
return status ? 7.025 : 8.325;
|
btnStatus.value = newStates.slice(0, props.switchCount);
|
||||||
|
SwitchClient.setStates(btnStatus.value); // 同步后端
|
||||||
|
emit("change", {
|
||||||
|
index,
|
||||||
|
value: index !== undefined ? btnStatus.value[index] : undefined,
|
||||||
|
states: [...btnStatus.value],
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
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] });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleBtnStatus(btnNum: number): void {
|
// 切换单个
|
||||||
if (btnNum >= 0 && btnNum < btnStatus.value.length) {
|
function toggleBtnStatus(idx: number) {
|
||||||
btnStatus.value[btnNum] = !btnStatus.value[btnNum];
|
if (idx < 0 || idx >= btnStatus.value.length) return;
|
||||||
emit('switch-toggle', {
|
const newStates = [...btnStatus.value];
|
||||||
index: btnNum,
|
newStates[idx] = !newStates[idx];
|
||||||
value: btnStatus.value[btnNum],
|
updateStatus(newStates, idx);
|
||||||
states: [...btnStatus.value]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 一次性设置所有开关状态
|
// 一次性设置全部
|
||||||
function setAllStates(states: boolean[]): void {
|
function setAllStates(states: boolean[]) {
|
||||||
const newStates = states.slice(0, props.switchCount);
|
updateStatus(states);
|
||||||
while (newStates.length < props.switchCount) {
|
|
||||||
newStates.push(false);
|
|
||||||
}
|
|
||||||
btnStatus.value = newStates;
|
|
||||||
emit('change', { states: [...btnStatus.value] });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 暴露组件方法和状态
|
// 单个设置
|
||||||
defineExpose({
|
function setBtnStatus(idx: number, isOn: boolean) {
|
||||||
setBtnStatus,
|
if (idx < 0 || idx >= btnStatus.value.length) return;
|
||||||
toggleBtnStatus,
|
const newStates = [...btnStatus.value];
|
||||||
setAllStates,
|
newStates[idx] = isOn;
|
||||||
getBtnStatus: () => [...btnStatus.value]
|
updateStatus(newStates, idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 props 变化只同步一次
|
||||||
|
watch(
|
||||||
|
() => [props.switchCount, props.initialValues],
|
||||||
|
() => {
|
||||||
|
btnStatus.value = parseInitialValues();
|
||||||
|
SwitchClient.setStates(btnStatus.value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听后端推送
|
||||||
|
onMounted(() => {
|
||||||
|
SwitchClient.onStateChange((states: boolean[]) => {
|
||||||
|
btnStatus.value = states.slice(0, props.switchCount);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -194,16 +202,14 @@ defineExpose({
|
|||||||
display: block;
|
display: block;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 0; /* 移除行高导致的额外间距 */
|
line-height: 0;
|
||||||
font-size: 0; /* 防止文本节点造成的间距 */
|
font-size: 0;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
rect {
|
rect {
|
||||||
transition: all 100ms ease-in-out;
|
transition: all 100ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.interactive {
|
.interactive {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,14 @@ import {
|
|||||||
getHubProxyFactory,
|
getHubProxyFactory,
|
||||||
getReceiverRegister,
|
getReceiverRegister,
|
||||||
} from "@/utils/signalR/TypedSignalR.Client";
|
} from "@/utils/signalR/TypedSignalR.Client";
|
||||||
import { ResourcePurpose, type ResourceInfo } from "@/APIClient";
|
import {
|
||||||
|
JtagClient,
|
||||||
|
MatrixKeyClient,
|
||||||
|
PowerClient,
|
||||||
|
ResourceClient,
|
||||||
|
ResourcePurpose,
|
||||||
|
type ResourceInfo,
|
||||||
|
} from "@/APIClient";
|
||||||
import type {
|
import type {
|
||||||
IDigitalTubesHub,
|
IDigitalTubesHub,
|
||||||
IJtagHub,
|
IJtagHub,
|
||||||
@@ -46,8 +53,7 @@ export const useEquipments = defineStore("equipments", () => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 每次挂载都重新创建连接
|
// 每次挂载都重新创建连接
|
||||||
jtagHubConnection.value =
|
jtagHubConnection.value = AuthManager.createHubConnection("JtagHub");
|
||||||
AuthManager.createAuthenticatedJtagHubConnection();
|
|
||||||
jtagHubProxy.value = getHubProxyFactory("IJtagHub").createHubProxy(
|
jtagHubProxy.value = getHubProxyFactory("IJtagHub").createHubProxy(
|
||||||
jtagHubConnection.value,
|
jtagHubConnection.value,
|
||||||
);
|
);
|
||||||
@@ -101,7 +107,7 @@ export const useEquipments = defineStore("equipments", () => {
|
|||||||
// 自动开启电源
|
// 自动开启电源
|
||||||
await powerSetOnOff(true);
|
await powerSetOnOff(true);
|
||||||
|
|
||||||
const resourceClient = AuthManager.createAuthenticatedResourceClient();
|
const resourceClient = AuthManager.createClient(ResourceClient);
|
||||||
const resp = await resourceClient.addResource(
|
const resp = await resourceClient.addResource(
|
||||||
"bitstream",
|
"bitstream",
|
||||||
ResourcePurpose.User,
|
ResourcePurpose.User,
|
||||||
@@ -133,7 +139,7 @@ export const useEquipments = defineStore("equipments", () => {
|
|||||||
// 自动开启电源
|
// 自动开启电源
|
||||||
await powerSetOnOff(true);
|
await powerSetOnOff(true);
|
||||||
|
|
||||||
const jtagClient = AuthManager.createAuthenticatedJtagClient();
|
const jtagClient = AuthManager.createClient(JtagClient);
|
||||||
const resp = await jtagClient.downloadBitstream(
|
const resp = await jtagClient.downloadBitstream(
|
||||||
boardAddr.value,
|
boardAddr.value,
|
||||||
boardPort.value,
|
boardPort.value,
|
||||||
@@ -155,7 +161,7 @@ export const useEquipments = defineStore("equipments", () => {
|
|||||||
// 自动开启电源
|
// 自动开启电源
|
||||||
await powerSetOnOff(true);
|
await powerSetOnOff(true);
|
||||||
|
|
||||||
const jtagClient = AuthManager.createAuthenticatedJtagClient();
|
const jtagClient = AuthManager.createClient(JtagClient);
|
||||||
const resp = await jtagClient.getDeviceIDCode(
|
const resp = await jtagClient.getDeviceIDCode(
|
||||||
boardAddr.value,
|
boardAddr.value,
|
||||||
boardPort.value,
|
boardPort.value,
|
||||||
@@ -175,7 +181,7 @@ export const useEquipments = defineStore("equipments", () => {
|
|||||||
// 自动开启电源
|
// 自动开启电源
|
||||||
await powerSetOnOff(true);
|
await powerSetOnOff(true);
|
||||||
|
|
||||||
const jtagClient = AuthManager.createAuthenticatedJtagClient();
|
const jtagClient = AuthManager.createClient(JtagClient);
|
||||||
const resp = await jtagClient.setSpeed(
|
const resp = await jtagClient.setSpeed(
|
||||||
boardAddr.value,
|
boardAddr.value,
|
||||||
boardPort.value,
|
boardPort.value,
|
||||||
@@ -221,8 +227,7 @@ export const useEquipments = defineStore("equipments", () => {
|
|||||||
async function matrixKeypadSetKeyStates(keyStates: boolean[]) {
|
async function matrixKeypadSetKeyStates(keyStates: boolean[]) {
|
||||||
const release = await matrixKeypadClientMutex.acquire();
|
const release = await matrixKeypadClientMutex.acquire();
|
||||||
try {
|
try {
|
||||||
const matrixKeypadClient =
|
const matrixKeypadClient = AuthManager.createClient(MatrixKeyClient);
|
||||||
AuthManager.createAuthenticatedMatrixKeyClient();
|
|
||||||
const resp = await matrixKeypadClient.setMatrixKeyStatus(
|
const resp = await matrixKeypadClient.setMatrixKeyStatus(
|
||||||
boardAddr.value,
|
boardAddr.value,
|
||||||
boardPort.value,
|
boardPort.value,
|
||||||
@@ -240,8 +245,7 @@ export const useEquipments = defineStore("equipments", () => {
|
|||||||
async function matrixKeypadEnable(enable: boolean) {
|
async function matrixKeypadEnable(enable: boolean) {
|
||||||
const release = await matrixKeypadClientMutex.acquire();
|
const release = await matrixKeypadClientMutex.acquire();
|
||||||
try {
|
try {
|
||||||
const matrixKeypadClient =
|
const matrixKeypadClient = AuthManager.createClient(MatrixKeyClient);
|
||||||
AuthManager.createAuthenticatedMatrixKeyClient();
|
|
||||||
if (enable) {
|
if (enable) {
|
||||||
const resp = await matrixKeypadClient.enabelMatrixKey(
|
const resp = await matrixKeypadClient.enabelMatrixKey(
|
||||||
boardAddr.value,
|
boardAddr.value,
|
||||||
@@ -276,7 +280,7 @@ export const useEquipments = defineStore("equipments", () => {
|
|||||||
async function powerSetOnOff(enable: boolean) {
|
async function powerSetOnOff(enable: boolean) {
|
||||||
const release = await powerClientMutex.acquire();
|
const release = await powerClientMutex.acquire();
|
||||||
try {
|
try {
|
||||||
const powerClient = AuthManager.createAuthenticatedPowerClient();
|
const powerClient = AuthManager.createClient(PowerClient);
|
||||||
const resp = await powerClient.setPowerOnOff(
|
const resp = await powerClient.setPowerOnOff(
|
||||||
boardAddr.value,
|
boardAddr.value,
|
||||||
boardPort.value,
|
boardPort.value,
|
||||||
@@ -338,7 +342,7 @@ export const useEquipments = defineStore("equipments", () => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 每次挂载都重新创建连接
|
// 每次挂载都重新创建连接
|
||||||
sevenSegmentDisplayHub.value =
|
sevenSegmentDisplayHub.value =
|
||||||
AuthManager.createAuthenticatedDigitalTubesHubConnection();
|
AuthManager.createHubConnection("DigitalTubesHub");
|
||||||
sevenSegmentDisplayHubProxy.value = getHubProxyFactory(
|
sevenSegmentDisplayHubProxy.value = getHubProxyFactory(
|
||||||
"IDigitalTubesHub",
|
"IDigitalTubesHub",
|
||||||
).createHubProxy(sevenSegmentDisplayHub.value);
|
).createHubProxy(sevenSegmentDisplayHub.value);
|
||||||
|
|||||||
@@ -1,322 +1,105 @@
|
|||||||
import {
|
import { DataClient } from "@/APIClient";
|
||||||
DataClient,
|
|
||||||
VideoStreamClient,
|
|
||||||
BsdlParserClient,
|
|
||||||
DDSClient,
|
|
||||||
JtagClient,
|
|
||||||
MatrixKeyClient,
|
|
||||||
PowerClient,
|
|
||||||
RemoteUpdateClient,
|
|
||||||
TutorialClient,
|
|
||||||
UDPClient,
|
|
||||||
LogicAnalyzerClient,
|
|
||||||
NetConfigClient,
|
|
||||||
OscilloscopeApiClient,
|
|
||||||
DebuggerClient,
|
|
||||||
ExamClient,
|
|
||||||
ResourceClient,
|
|
||||||
HdmiVideoStreamClient,
|
|
||||||
} from "@/APIClient";
|
|
||||||
import router from "@/router";
|
|
||||||
import { HubConnectionBuilder } from "@microsoft/signalr";
|
import { HubConnectionBuilder } from "@microsoft/signalr";
|
||||||
import axios, { type AxiosInstance } from "axios";
|
import axios, { type AxiosInstance } from "axios";
|
||||||
import { isNull } from "lodash";
|
|
||||||
|
|
||||||
// 支持的客户端类型联合类型
|
|
||||||
type SupportedClient =
|
|
||||||
| DataClient
|
|
||||||
| VideoStreamClient
|
|
||||||
| BsdlParserClient
|
|
||||||
| DDSClient
|
|
||||||
| JtagClient
|
|
||||||
| MatrixKeyClient
|
|
||||||
| PowerClient
|
|
||||||
| RemoteUpdateClient
|
|
||||||
| TutorialClient
|
|
||||||
| LogicAnalyzerClient
|
|
||||||
| UDPClient
|
|
||||||
| NetConfigClient
|
|
||||||
| OscilloscopeApiClient
|
|
||||||
| DebuggerClient
|
|
||||||
| ExamClient
|
|
||||||
| ResourceClient
|
|
||||||
| HdmiVideoStreamClient;
|
|
||||||
|
|
||||||
|
// 简单到让人想哭的认证管理器
|
||||||
export class AuthManager {
|
export class AuthManager {
|
||||||
// 存储token到localStorage
|
private static readonly TOKEN_KEY = "authToken";
|
||||||
public static setToken(token: string): void {
|
|
||||||
localStorage.setItem("authToken", token);
|
// 核心数据:就是个字符串
|
||||||
|
static getToken(): string | null {
|
||||||
|
return localStorage.getItem(this.TOKEN_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从localStorage获取token
|
static setToken(token: string): void {
|
||||||
public static getToken(): string | null {
|
localStorage.setItem(this.TOKEN_KEY, token);
|
||||||
return localStorage.getItem("authToken");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除token
|
static clearToken(): void {
|
||||||
public static clearToken(): void {
|
localStorage.removeItem(this.TOKEN_KEY);
|
||||||
localStorage.removeItem("authToken");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否已认证
|
// 核心功能:创建带认证的HTTP配置
|
||||||
public static async isAuthenticated(): Promise<boolean> {
|
static getAuthHeaders(): Record<string, string> {
|
||||||
return await AuthManager.verifyToken();
|
const token = this.getToken();
|
||||||
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通用的为HTTP请求添加Authorization header的方法
|
// 一个方法搞定所有客户端,不要17个垃圾方法
|
||||||
public static addAuthHeader(client: SupportedClient): void {
|
static createClient<T>(
|
||||||
const token = AuthManager.getToken();
|
ClientClass: new (baseUrl?: string, config?: any) => T,
|
||||||
if (token) {
|
baseUrl?: string,
|
||||||
// 创建一个自定义的 http 对象,包装原有的 fetch 方法
|
|
||||||
const customHttp = {
|
|
||||||
fetch: (url: RequestInfo, init?: RequestInit) => {
|
|
||||||
if (!init) init = {};
|
|
||||||
if (!init.headers) init.headers = {};
|
|
||||||
|
|
||||||
// 添加Authorization header
|
|
||||||
if (typeof init.headers === "object" && init.headers !== null) {
|
|
||||||
(init.headers as any)["Authorization"] = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用全局 fetch 或 window.fetch
|
|
||||||
return (window as any).fetch(url, init);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 重新构造客户端,传入自定义的 http 对象
|
|
||||||
const ClientClass = client.constructor as new (
|
|
||||||
baseUrl?: string,
|
|
||||||
http?: any,
|
|
||||||
) => SupportedClient;
|
|
||||||
const newClient = new ClientClass(undefined, customHttp);
|
|
||||||
|
|
||||||
// 将新客户端的属性复制到原客户端(这是一个 workaround)
|
|
||||||
// 更好的做法是返回新的客户端实例
|
|
||||||
Object.setPrototypeOf(client, Object.getPrototypeOf(newClient));
|
|
||||||
Object.assign(client, newClient);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 私有方法:创建带认证的HTTP客户端
|
|
||||||
private static createAuthenticatedHttp() {
|
|
||||||
const token = AuthManager.getToken();
|
|
||||||
if (!token) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
fetch: (url: RequestInfo, init?: RequestInit) => {
|
|
||||||
if (!init) init = {};
|
|
||||||
if (!init.headers) init.headers = {};
|
|
||||||
|
|
||||||
if (typeof init.headers === "object" && init.headers !== null) {
|
|
||||||
(init.headers as any)["Authorization"] = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (window as any).fetch(url, init);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 私有方法:创建带认证的Axios实例
|
|
||||||
private static createAuthenticatedAxiosInstance(): AxiosInstance | null {
|
|
||||||
const token = AuthManager.getToken();
|
|
||||||
if (!token) return null;
|
|
||||||
|
|
||||||
const instance = axios.create();
|
|
||||||
instance.interceptors.request.use((config) => {
|
|
||||||
config.headers = config.headers || {};
|
|
||||||
(config.headers as any)["Authorization"] = `Bearer ${token}`;
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通用的创建已认证客户端的方法(使用泛型)
|
|
||||||
public static createAuthenticatedClient<T extends SupportedClient>(
|
|
||||||
ClientClass: new (baseUrl?: string, instance?: AxiosInstance) => T,
|
|
||||||
): T {
|
): T {
|
||||||
const axiosInstance = AuthManager.createAuthenticatedAxiosInstance();
|
const token = this.getToken();
|
||||||
return axiosInstance
|
if (!token) {
|
||||||
? new ClientClass(undefined, axiosInstance)
|
return new ClientClass(baseUrl);
|
||||||
: new ClientClass();
|
}
|
||||||
|
|
||||||
|
// 对于axios客户端
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
headers: this.getAuthHeaders(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return new ClientClass(baseUrl, axiosInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 便捷方法:创建已配置认证的各种客户端
|
// SignalR连接 - 简单明了
|
||||||
public static createAuthenticatedDataClient(): DataClient {
|
static createHubConnection(
|
||||||
return AuthManager.createAuthenticatedClient(DataClient);
|
hubPath: "ProgressHub" | "JtagHub" | "DigitalTubesHub",
|
||||||
}
|
) {
|
||||||
|
|
||||||
public static createAuthenticatedVideoStreamClient(): VideoStreamClient {
|
|
||||||
return AuthManager.createAuthenticatedClient(VideoStreamClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createAuthenticatedBsdlParserClient(): BsdlParserClient {
|
|
||||||
return AuthManager.createAuthenticatedClient(BsdlParserClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createAuthenticatedDDSClient(): DDSClient {
|
|
||||||
return AuthManager.createAuthenticatedClient(DDSClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createAuthenticatedJtagClient(): JtagClient {
|
|
||||||
return AuthManager.createAuthenticatedClient(JtagClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createAuthenticatedMatrixKeyClient(): MatrixKeyClient {
|
|
||||||
return AuthManager.createAuthenticatedClient(MatrixKeyClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createAuthenticatedPowerClient(): PowerClient {
|
|
||||||
return AuthManager.createAuthenticatedClient(PowerClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createAuthenticatedRemoteUpdateClient(): RemoteUpdateClient {
|
|
||||||
return AuthManager.createAuthenticatedClient(RemoteUpdateClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createAuthenticatedTutorialClient(): TutorialClient {
|
|
||||||
return AuthManager.createAuthenticatedClient(TutorialClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createAuthenticatedUDPClient(): UDPClient {
|
|
||||||
return AuthManager.createAuthenticatedClient(UDPClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createAuthenticatedLogicAnalyzerClient(): LogicAnalyzerClient {
|
|
||||||
return AuthManager.createAuthenticatedClient(LogicAnalyzerClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createAuthenticatedNetConfigClient(): NetConfigClient {
|
|
||||||
return AuthManager.createAuthenticatedClient(NetConfigClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createAuthenticatedOscilloscopeApiClient(): OscilloscopeApiClient {
|
|
||||||
return AuthManager.createAuthenticatedClient(OscilloscopeApiClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createAuthenticatedDebuggerClient(): DebuggerClient {
|
|
||||||
return AuthManager.createAuthenticatedClient(DebuggerClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createAuthenticatedExamClient(): ExamClient {
|
|
||||||
return AuthManager.createAuthenticatedClient(ExamClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createAuthenticatedResourceClient(): ResourceClient {
|
|
||||||
return AuthManager.createAuthenticatedClient(ResourceClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createAuthenticatedHdmiVideoStreamClient(): HdmiVideoStreamClient {
|
|
||||||
return AuthManager.createAuthenticatedClient(HdmiVideoStreamClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createAuthenticatedJtagHubConnection() {
|
|
||||||
return new HubConnectionBuilder()
|
return new HubConnectionBuilder()
|
||||||
.withUrl("http://127.0.0.1:5000/hubs/JtagHub", {
|
.withUrl(`http://127.0.0.1:5000/hubs/${hubPath}`, {
|
||||||
accessTokenFactory: () => this.getToken() ?? "",
|
accessTokenFactory: () => this.getToken() ?? "",
|
||||||
})
|
})
|
||||||
.withAutomaticReconnect()
|
.withAutomaticReconnect()
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createAuthenticatedProgressHubConnection() {
|
// 认证逻辑 - 去除所有废话
|
||||||
return new HubConnectionBuilder()
|
static async login(username: string, password: string): Promise<boolean> {
|
||||||
.withUrl("http://127.0.0.1:5000/hubs/ProgressHub", {
|
|
||||||
accessTokenFactory: () => this.getToken() ?? "",
|
|
||||||
})
|
|
||||||
.withAutomaticReconnect()
|
|
||||||
.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(
|
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
): Promise<boolean> {
|
|
||||||
try {
|
try {
|
||||||
const client = new DataClient();
|
const client = new DataClient();
|
||||||
const token = await client.login(username, password);
|
const token = await client.login(username, password);
|
||||||
|
|
||||||
if (token) {
|
if (!token) return false;
|
||||||
AuthManager.setToken(token);
|
|
||||||
|
|
||||||
// 验证token
|
this.setToken(token);
|
||||||
const authClient = AuthManager.createAuthenticatedDataClient();
|
|
||||||
await authClient.testAuth();
|
|
||||||
|
|
||||||
return true;
|
// 验证token - 如果失败直接抛异常
|
||||||
}
|
await this.createClient(DataClient).testAuth();
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
AuthManager.clearToken();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 登出函数
|
|
||||||
public static logout(): void {
|
|
||||||
AuthManager.clearToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证当前token是否有效
|
|
||||||
public static async verifyToken(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const token = AuthManager.getToken();
|
|
||||||
if (!token) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = AuthManager.createAuthenticatedDataClient();
|
|
||||||
await client.testAuth();
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch {
|
||||||
AuthManager.clearToken();
|
this.clearToken();
|
||||||
return false;
|
throw new Error("Login failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证管理员权限
|
static logout(): void {
|
||||||
public static async verifyAdminAuth(): Promise<boolean> {
|
this.clearToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单的验证 - 不要搞复杂
|
||||||
|
static async isAuthenticated(): Promise<boolean> {
|
||||||
|
if (!this.getToken()) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = AuthManager.getToken();
|
await this.createClient(DataClient).testAuth();
|
||||||
if (!token) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = AuthManager.createAuthenticatedDataClient();
|
|
||||||
await client.testAdminAuth();
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch {
|
||||||
// 只有在token完全无效的情况下才清除token
|
this.clearToken();
|
||||||
// 401错误表示token有效但权限不足,不应清除token
|
|
||||||
if (error && typeof error === "object" && "status" in error) {
|
|
||||||
// 如果是403 (Forbidden) 或401 (Unauthorized),说明token有效但权限不足
|
|
||||||
if (error.status === 401 || error.status === 403) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 其他状态码可能表示token无效,清除token
|
|
||||||
AuthManager.clearToken();
|
|
||||||
} else {
|
|
||||||
// 网络错误等,不清除token
|
|
||||||
console.error("管理员权限验证失败:", error);
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查客户端是否已配置认证
|
static async isAdminAuthenticated(): Promise<boolean> {
|
||||||
public static isClientAuthenticated(client: SupportedClient): boolean {
|
if (!this.getToken()) return false;
|
||||||
const token = AuthManager.getToken();
|
|
||||||
return !!token;
|
try {
|
||||||
|
await this.createClient(DataClient).testAdminAuth();
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
this.clearToken();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export interface BoardData extends Board {
|
|||||||
const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
|
const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
|
||||||
// 远程升级相关参数
|
// 远程升级相关参数
|
||||||
const devPort = 1234;
|
const devPort = 1234;
|
||||||
const remoteUpdater = AuthManager.createAuthenticatedRemoteUpdateClient();
|
const remoteUpdater = AuthManager.createClient(RemoteUpdateClient);
|
||||||
|
|
||||||
// 统一的板卡数据
|
// 统一的板卡数据
|
||||||
const boards = ref<BoardData[]>([]);
|
const boards = ref<BoardData[]>([]);
|
||||||
@@ -35,13 +35,13 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
|
|||||||
async function getAllBoards(): Promise<{ success: boolean; error?: string }> {
|
async function getAllBoards(): Promise<{ success: boolean; error?: string }> {
|
||||||
try {
|
try {
|
||||||
// 验证管理员权限
|
// 验证管理员权限
|
||||||
const hasAdminAuth = await AuthManager.verifyAdminAuth();
|
const hasAdminAuth = await AuthManager.isAdminAuthenticated();
|
||||||
if (!hasAdminAuth) {
|
if (!hasAdminAuth) {
|
||||||
console.error("权限验证失败");
|
console.error("权限验证失败");
|
||||||
return { success: false, error: "权限不足" };
|
return { success: false, error: "权限不足" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = AuthManager.createAuthenticatedDataClient();
|
const client = AuthManager.createClient(DataClient);
|
||||||
const result = await client.getAllBoards();
|
const result = await client.getAllBoards();
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
@@ -77,7 +77,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
|
|||||||
): Promise<{ success: boolean; error?: string; boardId?: string }> {
|
): Promise<{ success: boolean; error?: string; boardId?: string }> {
|
||||||
try {
|
try {
|
||||||
// 验证管理员权限
|
// 验证管理员权限
|
||||||
const hasAdminAuth = await AuthManager.verifyAdminAuth();
|
const hasAdminAuth = await AuthManager.isAdminAuthenticated();
|
||||||
if (!hasAdminAuth) {
|
if (!hasAdminAuth) {
|
||||||
console.error("权限验证失败");
|
console.error("权限验证失败");
|
||||||
return { success: false, error: "权限不足" };
|
return { success: false, error: "权限不足" };
|
||||||
@@ -89,11 +89,11 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
|
|||||||
return { success: false, error: "参数不完整" };
|
return { success: false, error: "参数不完整" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = AuthManager.createAuthenticatedDataClient();
|
const client = AuthManager.createClient(DataClient);
|
||||||
const boardId = await client.addBoard(name);
|
const boardId = await client.addBoard(name);
|
||||||
|
|
||||||
if (boardId) {
|
if (boardId) {
|
||||||
console.log("新增板卡成功", { boardId, name});
|
console.log("新增板卡成功", { boardId, name });
|
||||||
// 刷新板卡列表
|
// 刷新板卡列表
|
||||||
await getAllBoards();
|
await getAllBoards();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -119,7 +119,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
|
|||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
try {
|
try {
|
||||||
// 验证管理员权限
|
// 验证管理员权限
|
||||||
const hasAdminAuth = await AuthManager.verifyAdminAuth();
|
const hasAdminAuth = await AuthManager.isAdminAuthenticated();
|
||||||
if (!hasAdminAuth) {
|
if (!hasAdminAuth) {
|
||||||
console.error("权限验证失败");
|
console.error("权限验证失败");
|
||||||
return { success: false, error: "权限不足" };
|
return { success: false, error: "权限不足" };
|
||||||
@@ -130,7 +130,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
|
|||||||
return { success: false, error: "板卡ID不能为空" };
|
return { success: false, error: "板卡ID不能为空" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = AuthManager.createAuthenticatedDataClient();
|
const client = AuthManager.createClient(DataClient);
|
||||||
const result = await client.deleteBoard(boardId);
|
const result = await client.deleteBoard(boardId);
|
||||||
|
|
||||||
if (result > 0) {
|
if (result > 0) {
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ const handleSignUp = async () => {
|
|||||||
// 页面初始化时检查是否已有有效token
|
// 页面初始化时检查是否已有有效token
|
||||||
const checkExistingToken = async () => {
|
const checkExistingToken = async () => {
|
||||||
try {
|
try {
|
||||||
const isValid = await AuthManager.verifyToken();
|
const isValid = await AuthManager.isAuthenticated();
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
// 如果token仍然有效,直接跳转到project页面
|
// 如果token仍然有效,直接跳转到project页面
|
||||||
router.go(-1);
|
router.go(-1);
|
||||||
|
|||||||
@@ -418,7 +418,12 @@ import {
|
|||||||
FileArchiveIcon,
|
FileArchiveIcon,
|
||||||
FileJsonIcon,
|
FileJsonIcon,
|
||||||
} from "lucide-vue-next";
|
} from "lucide-vue-next";
|
||||||
import { ExamDto, type FileParameter } from "@/APIClient";
|
import {
|
||||||
|
ExamClient,
|
||||||
|
ExamDto,
|
||||||
|
ResourceClient,
|
||||||
|
type FileParameter,
|
||||||
|
} from "@/APIClient";
|
||||||
import { useAlertStore } from "@/components/Alert";
|
import { useAlertStore } from "@/components/Alert";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
import { useRequiredInjection } from "@/utils/Common";
|
import { useRequiredInjection } from "@/utils/Common";
|
||||||
@@ -618,7 +623,7 @@ const submitCreateExam = async () => {
|
|||||||
isUpdating.value = true;
|
isUpdating.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createAuthenticatedExamClient();
|
const client = AuthManager.createClient(ExamClient);
|
||||||
|
|
||||||
let exam: ExamInfo;
|
let exam: ExamInfo;
|
||||||
if (mode.value === "create") {
|
if (mode.value === "create") {
|
||||||
@@ -671,7 +676,7 @@ const submitCreateExam = async () => {
|
|||||||
|
|
||||||
// 上传实验资源
|
// 上传实验资源
|
||||||
async function uploadExamResources(examId: string) {
|
async function uploadExamResources(examId: string) {
|
||||||
const client = AuthManager.createAuthenticatedResourceClient();
|
const client = AuthManager.createClient(ResourceClient);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 上传MD文档
|
// 上传MD文档
|
||||||
@@ -750,7 +755,7 @@ function close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function editExam(examId: string) {
|
async function editExam(examId: string) {
|
||||||
const client = AuthManager.createAuthenticatedExamClient();
|
const client = AuthManager.createClient(ExamClient);
|
||||||
const examInfo = await client.getExam(examId);
|
const examInfo = await client.getExam(examId);
|
||||||
|
|
||||||
editExamInfo.value = {
|
editExamInfo.value = {
|
||||||
|
|||||||
@@ -250,7 +250,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ResourcePurpose, type ExamInfo, type ResourceInfo } from "@/APIClient";
|
import {
|
||||||
|
ExamClient,
|
||||||
|
ResourceClient,
|
||||||
|
ResourcePurpose,
|
||||||
|
type ExamInfo,
|
||||||
|
type ResourceInfo,
|
||||||
|
} from "@/APIClient";
|
||||||
import { useAlertStore } from "@/components/Alert";
|
import { useAlertStore } from "@/components/Alert";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
import { useRequiredInjection } from "@/utils/Common";
|
import { useRequiredInjection } from "@/utils/Common";
|
||||||
@@ -274,7 +280,7 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const commitsList = ref<ResourceInfo[]>();
|
const commitsList = ref<ResourceInfo[]>();
|
||||||
async function updateCommits() {
|
async function updateCommits() {
|
||||||
const client = AuthManager.createAuthenticatedExamClient();
|
const client = AuthManager.createClient(ExamClient);
|
||||||
const list = await client.getCommitsByExamId(props.selectedExam.id);
|
const list = await client.getCommitsByExamId(props.selectedExam.id);
|
||||||
commitsList.value = list;
|
commitsList.value = list;
|
||||||
}
|
}
|
||||||
@@ -288,7 +294,7 @@ const downloadResources = async () => {
|
|||||||
downloadingResources.value = true;
|
downloadingResources.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resourceClient = AuthManager.createAuthenticatedResourceClient();
|
const resourceClient = AuthManager.createClient(ResourceClient);
|
||||||
|
|
||||||
// 获取资源包列表(模板资源)
|
// 获取资源包列表(模板资源)
|
||||||
const resourceList = await resourceClient.getResourceList(
|
const resourceList = await resourceClient.getResourceList(
|
||||||
|
|||||||
@@ -181,7 +181,7 @@
|
|||||||
import { ref, onMounted, computed } from "vue";
|
import { ref, onMounted, computed } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
import { type ExamInfo } from "@/APIClient";
|
import { ExamClient, type ExamInfo } from "@/APIClient";
|
||||||
import { formatDate } from "@/utils/Common";
|
import { formatDate } from "@/utils/Common";
|
||||||
import ExamInfoModal from "./ExamInfoModal.vue";
|
import ExamInfoModal from "./ExamInfoModal.vue";
|
||||||
import ExamEditModal from "./ExamEditModal.vue";
|
import ExamEditModal from "./ExamEditModal.vue";
|
||||||
@@ -206,7 +206,7 @@ async function refreshExams() {
|
|||||||
error.value = "";
|
error.value = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createAuthenticatedExamClient();
|
const client = AuthManager.createClient(ExamClient);
|
||||||
exams.value = await client.getExamList();
|
exams.value = await client.getExamList();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error.value = err.message || "获取实验列表失败";
|
error.value = err.message || "获取实验列表失败";
|
||||||
@@ -218,7 +218,7 @@ async function refreshExams() {
|
|||||||
|
|
||||||
async function viewExam(examId: string) {
|
async function viewExam(examId: string) {
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createAuthenticatedExamClient();
|
const client = AuthManager.createClient(ExamClient);
|
||||||
selectedExam.value = await client.getExam(examId);
|
selectedExam.value = await client.getExam(examId);
|
||||||
showInfoModal.value = true;
|
showInfoModal.value = true;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -248,7 +248,7 @@ onMounted(async () => {
|
|||||||
router.push("/login");
|
router.push("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
isAdmin.value = await AuthManager.verifyAdminAuth();
|
isAdmin.value = await AuthManager.isAdminAuthenticated();
|
||||||
|
|
||||||
await refreshExams();
|
await refreshExams();
|
||||||
|
|
||||||
|
|||||||
@@ -266,7 +266,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CaptureMode, ChannelConfig, DebuggerConfig } from "@/APIClient";
|
import {
|
||||||
|
CaptureMode,
|
||||||
|
ChannelConfig,
|
||||||
|
DebuggerClient,
|
||||||
|
DebuggerConfig,
|
||||||
|
} from "@/APIClient";
|
||||||
import { useAlertStore } from "@/components/Alert";
|
import { useAlertStore } from "@/components/Alert";
|
||||||
import BaseInputField from "@/components/InputField/BaseInputField.vue";
|
import BaseInputField from "@/components/InputField/BaseInputField.vue";
|
||||||
import type { LogicDataType } from "@/components/WaveformDisplay";
|
import type { LogicDataType } from "@/components/WaveformDisplay";
|
||||||
@@ -421,7 +426,7 @@ async function startCapture() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isCapturing.value = true;
|
isCapturing.value = true;
|
||||||
const client = AuthManager.createAuthenticatedDebuggerClient();
|
const client = AuthManager.createClient(DebuggerClient);
|
||||||
|
|
||||||
// 构造API配置
|
// 构造API配置
|
||||||
const channelConfigs = channels.value
|
const channelConfigs = channels.value
|
||||||
|
|||||||
@@ -13,13 +13,22 @@
|
|||||||
<div class="stats shadow">
|
<div class="stats shadow">
|
||||||
<div class="stat bg-base-100">
|
<div class="stat bg-base-100">
|
||||||
<div class="stat-figure text-primary">
|
<div class="stat-figure text-primary">
|
||||||
<div class="badge" :class="endpoint ? 'badge-success' : 'badge-warning'">
|
<div
|
||||||
|
class="badge"
|
||||||
|
:class="endpoint ? 'badge-success' : 'badge-warning'"
|
||||||
|
>
|
||||||
{{ endpoint ? "已连接" : "未配置" }}
|
{{ endpoint ? "已连接" : "未配置" }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-title">板卡状态</div>
|
<div class="stat-title">板卡状态</div>
|
||||||
<div class="stat-value text-primary">HDMI</div>
|
<div class="stat-value text-primary">HDMI</div>
|
||||||
<div class="stat-desc">{{ endpoint ? `板卡: ${endpoint.boardId.substring(0, 8)}...` : "请先连接板卡" }}</div>
|
<div class="stat-desc">
|
||||||
|
{{
|
||||||
|
endpoint
|
||||||
|
? `板卡: ${endpoint.boardId.substring(0, 8)}...`
|
||||||
|
: "请先连接板卡"
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -40,12 +49,20 @@
|
|||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<div class="card-actions justify-end mt-4">
|
<div class="card-actions justify-end mt-4">
|
||||||
<button class="btn btn-outline btn-primary" @click="refreshEndpoint" :disabled="loading">
|
<button
|
||||||
|
class="btn btn-outline btn-primary"
|
||||||
|
@click="refreshEndpoint"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
<RefreshCw v-if="loading" class="animate-spin h-4 w-4 mr-2" />
|
<RefreshCw v-if="loading" class="animate-spin h-4 w-4 mr-2" />
|
||||||
<RefreshCw v-else class="h-4 w-4 mr-2" />
|
<RefreshCw v-else class="h-4 w-4 mr-2" />
|
||||||
{{ loading ? "刷新中..." : "刷新连接" }}
|
{{ loading ? "刷新中..." : "刷新连接" }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary" @click="testConnection" :disabled="testing || !endpoint">
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
@click="testConnection"
|
||||||
|
:disabled="testing || !endpoint"
|
||||||
|
>
|
||||||
<RefreshCw v-if="testing" class="animate-spin h-4 w-4 mr-2" />
|
<RefreshCw v-if="testing" class="animate-spin h-4 w-4 mr-2" />
|
||||||
<TestTube v-else class="h-4 w-4 mr-2" />
|
<TestTube v-else class="h-4 w-4 mr-2" />
|
||||||
{{ testing ? "测试中..." : "测试连接" }}
|
{{ testing ? "测试中..." : "测试连接" }}
|
||||||
@@ -62,17 +79,33 @@
|
|||||||
HDMI视频预览
|
HDMI视频预览
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="relative bg-black rounded-lg overflow-hidden cursor-pointer" :class="[
|
<div
|
||||||
{ 'cursor-not-allowed': !isPlaying || hasVideoError || !endpoint }
|
class="relative bg-black rounded-lg overflow-hidden cursor-pointer"
|
||||||
]" style="aspect-ratio: 16/9" @click="handleVideoClick">
|
:class="[
|
||||||
|
{ 'cursor-not-allowed': !isPlaying || hasVideoError || !endpoint },
|
||||||
|
]"
|
||||||
|
style="aspect-ratio: 16/9"
|
||||||
|
@click="handleVideoClick"
|
||||||
|
>
|
||||||
<!-- 视频播放器 - 使用img标签直接显示MJPEG流 -->
|
<!-- 视频播放器 - 使用img标签直接显示MJPEG流 -->
|
||||||
<div v-show="isPlaying && endpoint" class="w-full h-full flex items-center justify-center">
|
<div
|
||||||
<img :src="currentVideoSource" alt="HDMI视频流" class="max-w-full max-h-full object-contain"
|
v-show="isPlaying && endpoint"
|
||||||
@error="handleVideoError" @load="handleVideoLoad" />
|
class="w-full h-full flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="currentVideoSource"
|
||||||
|
alt="HDMI视频流"
|
||||||
|
class="max-w-full max-h-full object-contain"
|
||||||
|
@error="handleVideoError"
|
||||||
|
@load="handleVideoLoad"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 错误信息显示 -->
|
<!-- 错误信息显示 -->
|
||||||
<div v-if="hasVideoError" class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-70">
|
<div
|
||||||
|
v-if="hasVideoError"
|
||||||
|
class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-70"
|
||||||
|
>
|
||||||
<div class="card bg-error text-white shadow-lg w-full max-w-lg">
|
<div class="card bg-error text-white shadow-lg w-full max-w-lg">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3 class="card-title flex items-center gap-2">
|
<h3 class="card-title flex items-center gap-2">
|
||||||
@@ -87,7 +120,10 @@
|
|||||||
<li>HDMI视频流服务是否已启动</li>
|
<li>HDMI视频流服务是否已启动</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="card-actions justify-end mt-2">
|
<div class="card-actions justify-end mt-2">
|
||||||
<button class="btn btn-sm btn-outline btn-primary" @click="tryReconnect">
|
<button
|
||||||
|
class="btn btn-sm btn-outline btn-primary"
|
||||||
|
@click="tryReconnect"
|
||||||
|
>
|
||||||
重试连接
|
重试连接
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,13 +132,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 占位符 -->
|
<!-- 占位符 -->
|
||||||
<div v-show="(!isPlaying && !hasVideoError) || !endpoint"
|
<div
|
||||||
class="absolute inset-0 flex items-center justify-center text-white">
|
v-show="(!isPlaying && !hasVideoError) || !endpoint"
|
||||||
|
class="absolute inset-0 flex items-center justify-center text-white"
|
||||||
|
>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<Video class="w-16 h-16 mx-auto mb-4 opacity-50" />
|
<Video class="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||||||
<p class="text-lg opacity-75">{{ videoStatus }}</p>
|
<p class="text-lg opacity-75">{{ videoStatus }}</p>
|
||||||
<p class="text-sm opacity-60 mt-2">
|
<p class="text-sm opacity-60 mt-2">
|
||||||
{{ endpoint ? '点击"播放HDMI视频流"按钮开始查看实时视频' : '请先刷新连接以获取板卡信息' }}
|
{{
|
||||||
|
endpoint
|
||||||
|
? '点击"播放HDMI视频流"按钮开始查看实时视频'
|
||||||
|
: "请先刷新连接以获取板卡信息"
|
||||||
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,11 +160,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="space-x-2">
|
<div class="space-x-2">
|
||||||
<div class="dropdown dropdown-hover dropdown-top dropdown-end">
|
<div class="dropdown dropdown-hover dropdown-top dropdown-end">
|
||||||
<div tabindex="0" role="button" class="btn btn-sm btn-outline btn-accent">
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
role="button"
|
||||||
|
class="btn btn-sm btn-outline btn-accent"
|
||||||
|
>
|
||||||
<MoreHorizontal class="w-4 h-4 mr-1" />
|
<MoreHorizontal class="w-4 h-4 mr-1" />
|
||||||
更多功能
|
更多功能
|
||||||
</div>
|
</div>
|
||||||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52">
|
<ul
|
||||||
|
tabindex="0"
|
||||||
|
class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52"
|
||||||
|
>
|
||||||
<li>
|
<li>
|
||||||
<a @click="openInNewTab(endpoint.videoUrl)">
|
<a @click="openInNewTab(endpoint.videoUrl)">
|
||||||
<ExternalLink class="w-4 h-4" />
|
<ExternalLink class="w-4 h-4" />
|
||||||
@@ -143,11 +192,19 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-success btn-sm" @click="startStream" :disabled="isPlaying || !endpoint">
|
<button
|
||||||
|
class="btn btn-success btn-sm"
|
||||||
|
@click="startStream"
|
||||||
|
:disabled="isPlaying || !endpoint"
|
||||||
|
>
|
||||||
<Play class="w-4 h-4 mr-1" />
|
<Play class="w-4 h-4 mr-1" />
|
||||||
播放HDMI视频流
|
播放HDMI视频流
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-error btn-sm" @click="stopStream" :disabled="!isPlaying">
|
<button
|
||||||
|
class="btn btn-error btn-sm"
|
||||||
|
@click="stopStream"
|
||||||
|
:disabled="!isPlaying"
|
||||||
|
>
|
||||||
<Square class="w-4 h-4 mr-1" />
|
<Square class="w-4 h-4 mr-1" />
|
||||||
停止视频流
|
停止视频流
|
||||||
</button>
|
</button>
|
||||||
@@ -165,11 +222,20 @@
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="bg-base-300 rounded-lg p-4 h-48 overflow-y-auto">
|
<div class="bg-base-300 rounded-lg p-4 h-48 overflow-y-auto">
|
||||||
<div v-for="(log, index) in logs" :key="index" class="text-sm font-mono mb-1">
|
<div
|
||||||
<span class="text-base-content/50">[{{ formatTime(log.time) }}]</span>
|
v-for="(log, index) in logs"
|
||||||
|
:key="index"
|
||||||
|
class="text-sm font-mono mb-1"
|
||||||
|
>
|
||||||
|
<span class="text-base-content/50"
|
||||||
|
>[{{ formatTime(log.time) }}]</span
|
||||||
|
>
|
||||||
<span :class="getLogClass(log.level)">{{ log.message }}</span>
|
<span :class="getLogClass(log.level)">{{ log.message }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="logs.length === 0" class="text-base-content/50 text-center py-8">
|
<div
|
||||||
|
v-if="logs.length === 0"
|
||||||
|
class="text-base-content/50 text-center py-8"
|
||||||
|
>
|
||||||
暂无日志记录
|
暂无日志记录
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -200,7 +266,10 @@ import {
|
|||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
} from "lucide-vue-next";
|
} from "lucide-vue-next";
|
||||||
import { HdmiVideoStreamClient, type HdmiVideoStreamEndpoint } from "@/APIClient";
|
import {
|
||||||
|
HdmiVideoStreamClient,
|
||||||
|
type HdmiVideoStreamEndpoint,
|
||||||
|
} from "@/APIClient";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
import { useAlertStore } from "@/components/Alert";
|
import { useAlertStore } from "@/components/Alert";
|
||||||
|
|
||||||
@@ -212,27 +281,27 @@ const loading = ref(false);
|
|||||||
const testing = ref(false);
|
const testing = ref(false);
|
||||||
const isPlaying = ref(false);
|
const isPlaying = ref(false);
|
||||||
const hasVideoError = ref(false);
|
const hasVideoError = ref(false);
|
||||||
const videoStatus = ref('未连接');
|
const videoStatus = ref("未连接");
|
||||||
|
|
||||||
// HDMI视频流数据
|
// HDMI视频流数据
|
||||||
const endpoint = ref<HdmiVideoStreamEndpoint | null>(null);
|
const endpoint = ref<HdmiVideoStreamEndpoint | null>(null);
|
||||||
const currentVideoSource = ref('');
|
const currentVideoSource = ref("");
|
||||||
|
|
||||||
// 日志系统
|
// 日志系统
|
||||||
interface LogEntry {
|
interface LogEntry {
|
||||||
time: Date;
|
time: Date;
|
||||||
level: 'info' | 'success' | 'warning' | 'error';
|
level: "info" | "success" | "warning" | "error";
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const logs = ref<LogEntry[]>([]);
|
const logs = ref<LogEntry[]>([]);
|
||||||
|
|
||||||
// 添加日志
|
// 添加日志
|
||||||
function addLog(level: LogEntry['level'], message: string) {
|
function addLog(level: LogEntry["level"], message: string) {
|
||||||
logs.value.unshift({
|
logs.value.unshift({
|
||||||
time: new Date(),
|
time: new Date(),
|
||||||
level,
|
level,
|
||||||
message
|
message,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 保持最近100条日志
|
// 保持最近100条日志
|
||||||
@@ -247,51 +316,54 @@ function formatTime(date: Date): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取日志样式类
|
// 获取日志样式类
|
||||||
function getLogClass(level: LogEntry['level']): string {
|
function getLogClass(level: LogEntry["level"]): string {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case 'success':
|
case "success":
|
||||||
return 'text-success';
|
return "text-success";
|
||||||
case 'warning':
|
case "warning":
|
||||||
return 'text-warning';
|
return "text-warning";
|
||||||
case 'error':
|
case "error":
|
||||||
return 'text-error';
|
return "text-error";
|
||||||
default:
|
default:
|
||||||
return 'text-base-content';
|
return "text-base-content";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清空日志
|
// 清空日志
|
||||||
function clearLogs() {
|
function clearLogs() {
|
||||||
logs.value = [];
|
logs.value = [];
|
||||||
addLog('info', '日志已清空');
|
addLog("info", "日志已清空");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新HDMI视频流端点
|
// 刷新HDMI视频流端点
|
||||||
async function refreshEndpoint() {
|
async function refreshEndpoint() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
addLog('info', '正在获取HDMI视频流端点...');
|
addLog("info", "正在获取HDMI视频流端点...");
|
||||||
|
|
||||||
const client = AuthManager.createAuthenticatedHdmiVideoStreamClient();
|
const client = AuthManager.createClient(HdmiVideoStreamClient);
|
||||||
const result = await client.getMyEndpoint();
|
const result = await client.getMyEndpoint();
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
endpoint.value = result;
|
endpoint.value = result;
|
||||||
videoStatus.value = '已连接板卡,可以播放视频流';
|
videoStatus.value = "已连接板卡,可以播放视频流";
|
||||||
addLog('success', `成功获取HDMI视频流端点,板卡ID: ${result.boardId.substring(0, 8)}...`);
|
addLog(
|
||||||
alert?.success('HDMI视频流连接成功');
|
"success",
|
||||||
|
`成功获取HDMI视频流端点,板卡ID: ${result.boardId.substring(0, 8)}...`,
|
||||||
|
);
|
||||||
|
alert?.success("HDMI视频流连接成功");
|
||||||
} else {
|
} else {
|
||||||
endpoint.value = null;
|
endpoint.value = null;
|
||||||
videoStatus.value = '无法获取板卡信息';
|
videoStatus.value = "无法获取板卡信息";
|
||||||
addLog('error', '未找到绑定的板卡或板卡未配置HDMI输入');
|
addLog("error", "未找到绑定的板卡或板卡未配置HDMI输入");
|
||||||
alert?.error('未找到绑定的板卡');
|
alert?.error("未找到绑定的板卡");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取HDMI视频流端点失败:', error);
|
console.error("获取HDMI视频流端点失败:", error);
|
||||||
endpoint.value = null;
|
endpoint.value = null;
|
||||||
videoStatus.value = '连接失败';
|
videoStatus.value = "连接失败";
|
||||||
addLog('error', `获取HDMI视频流端点失败: ${error}`);
|
addLog("error", `获取HDMI视频流端点失败: ${error}`);
|
||||||
alert?.error('获取HDMI视频流信息失败');
|
alert?.error("获取HDMI视频流信息失败");
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -300,34 +372,34 @@ async function refreshEndpoint() {
|
|||||||
// 测试连接
|
// 测试连接
|
||||||
async function testConnection() {
|
async function testConnection() {
|
||||||
if (!endpoint.value) {
|
if (!endpoint.value) {
|
||||||
alert?.warn('请先刷新连接获取板卡信息');
|
alert?.warn("请先刷新连接获取板卡信息");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
testing.value = true;
|
testing.value = true;
|
||||||
try {
|
try {
|
||||||
addLog('info', '正在测试HDMI视频流连接...');
|
addLog("info", "正在测试HDMI视频流连接...");
|
||||||
|
|
||||||
// 尝试获取快照来测试连接
|
// 尝试获取快照来测试连接
|
||||||
const response = await fetch(endpoint.value.snapshotUrl, {
|
const response = await fetch(endpoint.value.snapshotUrl, {
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
'Cache-Control': 'no-cache'
|
"Cache-Control": "no-cache",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
addLog('success', 'HDMI视频流连接测试成功');
|
addLog("success", "HDMI视频流连接测试成功");
|
||||||
alert?.success('HDMI连接测试成功');
|
alert?.success("HDMI连接测试成功");
|
||||||
videoStatus.value = '连接正常,可以播放视频流';
|
videoStatus.value = "连接正常,可以播放视频流";
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('HDMI视频流连接测试失败:', error);
|
console.error("HDMI视频流连接测试失败:", error);
|
||||||
addLog('error', `连接测试失败: ${error}`);
|
addLog("error", `连接测试失败: ${error}`);
|
||||||
alert?.error('HDMI连接测试失败');
|
alert?.error("HDMI连接测试失败");
|
||||||
videoStatus.value = '连接测试失败';
|
videoStatus.value = "连接测试失败";
|
||||||
} finally {
|
} finally {
|
||||||
testing.value = false;
|
testing.value = false;
|
||||||
}
|
}
|
||||||
@@ -336,7 +408,7 @@ async function testConnection() {
|
|||||||
// 开始播放视频流
|
// 开始播放视频流
|
||||||
function startStream() {
|
function startStream() {
|
||||||
if (!endpoint.value) {
|
if (!endpoint.value) {
|
||||||
alert?.warn('请先刷新连接获取板卡信息');
|
alert?.warn("请先刷新连接获取板卡信息");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,42 +418,42 @@ function startStream() {
|
|||||||
currentVideoSource.value = `${endpoint.value.mjpegUrl}&t=${timestamp}`;
|
currentVideoSource.value = `${endpoint.value.mjpegUrl}&t=${timestamp}`;
|
||||||
isPlaying.value = true;
|
isPlaying.value = true;
|
||||||
hasVideoError.value = false;
|
hasVideoError.value = false;
|
||||||
videoStatus.value = '正在加载视频流...';
|
videoStatus.value = "正在加载视频流...";
|
||||||
|
|
||||||
addLog('info', '开始播放HDMI视频流');
|
addLog("info", "开始播放HDMI视频流");
|
||||||
alert?.success('开始播放HDMI视频流');
|
alert?.success("开始播放HDMI视频流");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('启动HDMI视频流失败:', error);
|
console.error("启动HDMI视频流失败:", error);
|
||||||
addLog('error', `启动视频流失败: ${error}`);
|
addLog("error", `启动视频流失败: ${error}`);
|
||||||
alert?.error('启动HDMI视频流失败');
|
alert?.error("启动HDMI视频流失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止播放视频流
|
// 停止播放视频流
|
||||||
function stopStream() {
|
function stopStream() {
|
||||||
isPlaying.value = false;
|
isPlaying.value = false;
|
||||||
currentVideoSource.value = '';
|
currentVideoSource.value = "";
|
||||||
videoStatus.value = '已停止播放';
|
videoStatus.value = "已停止播放";
|
||||||
|
|
||||||
const client = AuthManager.createAuthenticatedHdmiVideoStreamClient();
|
const client = AuthManager.createClient(HdmiVideoStreamClient);
|
||||||
client.disableHdmiTransmission();
|
client.disableHdmiTransmission();
|
||||||
|
|
||||||
addLog('info', '停止播放HDMI视频流');
|
addLog("info", "停止播放HDMI视频流");
|
||||||
alert?.info('已停止播放HDMI视频流');
|
alert?.info("已停止播放HDMI视频流");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理视频加载错误
|
// 处理视频加载错误
|
||||||
function handleVideoError() {
|
function handleVideoError() {
|
||||||
hasVideoError.value = true;
|
hasVideoError.value = true;
|
||||||
videoStatus.value = '视频流加载失败';
|
videoStatus.value = "视频流加载失败";
|
||||||
addLog('error', 'HDMI视频流加载失败');
|
addLog("error", "HDMI视频流加载失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理视频加载成功
|
// 处理视频加载成功
|
||||||
function handleVideoLoad() {
|
function handleVideoLoad() {
|
||||||
hasVideoError.value = false;
|
hasVideoError.value = false;
|
||||||
videoStatus.value = '视频流播放中';
|
videoStatus.value = "视频流播放中";
|
||||||
addLog('success', 'HDMI视频流加载成功');
|
addLog("success", "HDMI视频流加载成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理视频点击
|
// 处理视频点击
|
||||||
@@ -391,7 +463,7 @@ function handleVideoClick() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 可以在这里添加点击视频的交互逻辑
|
// 可以在这里添加点击视频的交互逻辑
|
||||||
addLog('info', '视频画面被点击');
|
addLog("info", "视频画面被点击");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重试连接
|
// 重试连接
|
||||||
@@ -404,47 +476,47 @@ function tryReconnect() {
|
|||||||
|
|
||||||
// 在新标签页打开视频
|
// 在新标签页打开视频
|
||||||
function openInNewTab(url: string) {
|
function openInNewTab(url: string) {
|
||||||
window.open(url, '_blank');
|
window.open(url, "_blank");
|
||||||
addLog('info', '在新标签页打开HDMI视频页面');
|
addLog("info", "在新标签页打开HDMI视频页面");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取快照
|
// 获取快照
|
||||||
async function takeSnapshot() {
|
async function takeSnapshot() {
|
||||||
if (!endpoint.value) {
|
if (!endpoint.value) {
|
||||||
alert?.warn('请先刷新连接获取板卡信息');
|
alert?.warn("请先刷新连接获取板卡信息");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
addLog('info', '正在获取HDMI视频快照...');
|
addLog("info", "正在获取HDMI视频快照...");
|
||||||
|
|
||||||
const response = await fetch(endpoint.value.snapshotUrl, {
|
const response = await fetch(endpoint.value.snapshotUrl, {
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
'Cache-Control': 'no-cache'
|
"Cache-Control": "no-cache",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = `hdmi_snapshot_${new Date().toISOString().replace(/:/g, '-')}.jpg`;
|
a.download = `hdmi_snapshot_${new Date().toISOString().replace(/:/g, "-")}.jpg`;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
addLog('success', '快照下载成功');
|
addLog("success", "快照下载成功");
|
||||||
alert?.success('HDMI快照下载成功');
|
alert?.success("HDMI快照下载成功");
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取HDMI快照失败:', error);
|
console.error("获取HDMI快照失败:", error);
|
||||||
addLog('error', `获取快照失败: ${error}`);
|
addLog("error", `获取快照失败: ${error}`);
|
||||||
alert?.error('获取HDMI快照失败');
|
alert?.error("获取HDMI快照失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,18 +524,18 @@ async function takeSnapshot() {
|
|||||||
async function copyToClipboard(text: string) {
|
async function copyToClipboard(text: string) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
addLog('success', '地址已复制到剪贴板');
|
addLog("success", "地址已复制到剪贴板");
|
||||||
alert?.success('地址已复制到剪贴板');
|
alert?.success("地址已复制到剪贴板");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('复制到剪贴板失败:', error);
|
console.error("复制到剪贴板失败:", error);
|
||||||
addLog('error', '复制到剪贴板失败');
|
addLog("error", "复制到剪贴板失败");
|
||||||
alert?.error('复制到剪贴板失败');
|
alert?.error("复制到剪贴板失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件挂载时初始化
|
// 组件挂载时初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
addLog('info', 'HDMI视频流界面已初始化');
|
addLog("info", "HDMI视频流界面已初始化");
|
||||||
refreshEndpoint();
|
refreshEndpoint();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -476,7 +548,8 @@ onUnmounted(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
/* 对焦动画效果 */
|
/* 对焦动画效果 */
|
||||||
@keyframes focus-pulse {
|
@keyframes focus-pulse {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
|
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
|
|||||||
@@ -80,7 +80,9 @@
|
|||||||
<!-- 功能底栏 -->
|
<!-- 功能底栏 -->
|
||||||
<SplitterPanel
|
<SplitterPanel
|
||||||
id="splitter-group-v-panel-bar"
|
id="splitter-group-v-panel-bar"
|
||||||
:default-size="isBottomBarFullscreen ? 100 : (100 - verticalSplitterSize)"
|
:default-size="
|
||||||
|
isBottomBarFullscreen ? 100 : 100 - verticalSplitterSize
|
||||||
|
"
|
||||||
:min-size="isBottomBarFullscreen ? 100 : 15"
|
:min-size="isBottomBarFullscreen ? 100 : 15"
|
||||||
class="w-full overflow-hidden pt-3"
|
class="w-full overflow-hidden pt-3"
|
||||||
>
|
>
|
||||||
@@ -114,14 +116,40 @@
|
|||||||
@click="navbarControl.toggleNavbar"
|
@click="navbarControl.toggleNavbar"
|
||||||
class="btn btn-circle btn-primary shadow-lg hover:shadow-xl transition-all duration-300"
|
class="btn btn-circle btn-primary shadow-lg hover:shadow-xl transition-all duration-300"
|
||||||
:class="{ 'btn-outline': navbarControl.showNavbar.value }"
|
:class="{ 'btn-outline': navbarControl.showNavbar.value }"
|
||||||
:title="navbarControl.showNavbar.value ? '隐藏顶部导航栏' : '显示顶部导航栏'"
|
:title="
|
||||||
|
navbarControl.showNavbar.value ? '隐藏顶部导航栏' : '显示顶部导航栏'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<!-- 使用SVG图标表示菜单/关闭状态 -->
|
<!-- 使用SVG图标表示菜单/关闭状态 -->
|
||||||
<svg v-if="navbarControl.showNavbar.value" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
v-if="navbarControl.showNavbar.value"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
v-else
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,7 +159,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, watch, inject, type Ref } from "vue";
|
import { ref, onMounted, watch, inject, type Ref } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { useLocalStorage } from '@vueuse/core'; // 添加VueUse导入
|
import { useLocalStorage } from "@vueuse/core"; // 添加VueUse导入
|
||||||
import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from "reka-ui";
|
import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from "reka-ui";
|
||||||
import DiagramCanvas from "@/components/LabCanvas/DiagramCanvas.vue";
|
import DiagramCanvas from "@/components/LabCanvas/DiagramCanvas.vue";
|
||||||
import ComponentSelector from "@/components/LabCanvas/ComponentSelector.vue";
|
import ComponentSelector from "@/components/LabCanvas/ComponentSelector.vue";
|
||||||
@@ -143,7 +171,7 @@ import { useProvideComponentManager } from "@/components/LabCanvas";
|
|||||||
import { useAlertStore } from "@/components/Alert";
|
import { useAlertStore } from "@/components/Alert";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
import { useEquipments } from "@/stores/equipments";
|
import { useEquipments } from "@/stores/equipments";
|
||||||
import type { Board } from "@/APIClient";
|
import { DataClient, ResourceClient, type Board } from "@/APIClient";
|
||||||
|
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -158,20 +186,29 @@ const equipments = useEquipments();
|
|||||||
const alert = useAlertStore();
|
const alert = useAlertStore();
|
||||||
|
|
||||||
// --- Navbar控制 ---
|
// --- Navbar控制 ---
|
||||||
const navbarControl = inject('navbar') as {
|
const navbarControl = inject("navbar") as {
|
||||||
showNavbar: Ref<boolean>;
|
showNavbar: Ref<boolean>;
|
||||||
toggleNavbar: () => void;
|
toggleNavbar: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 使用VueUse保存分栏状态 ---
|
// --- 使用VueUse保存分栏状态 ---
|
||||||
// 左右分栏比例(默认60%)
|
// 左右分栏比例(默认60%)
|
||||||
const horizontalSplitterSize = useLocalStorage('project-horizontal-splitter-size', 60);
|
const horizontalSplitterSize = useLocalStorage(
|
||||||
|
"project-horizontal-splitter-size",
|
||||||
|
60,
|
||||||
|
);
|
||||||
// 上下分栏比例(默认80%)
|
// 上下分栏比例(默认80%)
|
||||||
const verticalSplitterSize = useLocalStorage('project-vertical-splitter-size', 80);
|
const verticalSplitterSize = useLocalStorage(
|
||||||
|
"project-vertical-splitter-size",
|
||||||
|
80,
|
||||||
|
);
|
||||||
// 底栏全屏状态
|
// 底栏全屏状态
|
||||||
const isBottomBarFullscreen = useLocalStorage('project-bottom-bar-fullscreen', false);
|
const isBottomBarFullscreen = useLocalStorage(
|
||||||
|
"project-bottom-bar-fullscreen",
|
||||||
|
false,
|
||||||
|
);
|
||||||
// 文档面板显示状态
|
// 文档面板显示状态
|
||||||
const showDocPanel = useLocalStorage('project-show-doc-panel', false);
|
const showDocPanel = useLocalStorage("project-show-doc-panel", false);
|
||||||
|
|
||||||
function handleToggleBottomBarFullscreen() {
|
function handleToggleBottomBarFullscreen() {
|
||||||
isBottomBarFullscreen.value = !isBottomBarFullscreen.value;
|
isBottomBarFullscreen.value = !isBottomBarFullscreen.value;
|
||||||
@@ -216,11 +253,11 @@ async function loadDocumentContent() {
|
|||||||
const examId = route.query.examId as string;
|
const examId = route.query.examId as string;
|
||||||
if (examId) {
|
if (examId) {
|
||||||
// 如果有实验ID,从API加载实验文档
|
// 如果有实验ID,从API加载实验文档
|
||||||
console.log('加载实验文档:', examId);
|
console.log("加载实验文档:", examId);
|
||||||
const client = AuthManager.createAuthenticatedResourceClient();
|
const client = AuthManager.createClient(ResourceClient);
|
||||||
|
|
||||||
// 获取markdown类型的模板资源列表
|
// 获取markdown类型的模板资源列表
|
||||||
const resources = await client.getResourceList(examId, 'doc', 'template');
|
const resources = await client.getResourceList(examId, "doc", "template");
|
||||||
|
|
||||||
if (resources && resources.length > 0) {
|
if (resources && resources.length > 0) {
|
||||||
// 获取第一个markdown资源
|
// 获取第一个markdown资源
|
||||||
@@ -230,7 +267,7 @@ async function loadDocumentContent() {
|
|||||||
const response = await client.getResourceById(markdownResource.id);
|
const response = await client.getResourceById(markdownResource.id);
|
||||||
|
|
||||||
if (!response || !response.data) {
|
if (!response || !response.data) {
|
||||||
throw new Error('获取markdown文件失败');
|
throw new Error("获取markdown文件失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = await response.data.text();
|
const content = await response.data.text();
|
||||||
@@ -279,17 +316,17 @@ function updateComponentDirectProp(
|
|||||||
// 检查并初始化用户实验板
|
// 检查并初始化用户实验板
|
||||||
async function checkAndInitializeBoard() {
|
async function checkAndInitializeBoard() {
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createAuthenticatedDataClient();
|
const client = AuthManager.createClient(DataClient);
|
||||||
const userInfo = await client.getUserInfo();
|
const userInfo = await client.getUserInfo();
|
||||||
|
|
||||||
if (userInfo.boardID && userInfo.boardID.trim() !== '') {
|
if (userInfo.boardID && userInfo.boardID.trim() !== "") {
|
||||||
// 用户已绑定实验板,获取实验板信息并更新到equipment
|
// 用户已绑定实验板,获取实验板信息并更新到equipment
|
||||||
try {
|
try {
|
||||||
const board = await client.getBoardByID(userInfo.boardID);
|
const board = await client.getBoardByID(userInfo.boardID);
|
||||||
updateEquipmentFromBoard(board);
|
updateEquipmentFromBoard(board);
|
||||||
alert?.show(`实验板 ${board.boardName} 已连接`, "success");
|
alert?.show(`实验板 ${board.boardName} 已连接`, "success");
|
||||||
} catch (boardError) {
|
} catch (boardError) {
|
||||||
console.error('获取实验板信息失败:', boardError);
|
console.error("获取实验板信息失败:", boardError);
|
||||||
alert?.show("获取实验板信息失败", "error");
|
alert?.show("获取实验板信息失败", "error");
|
||||||
showRequestBoardDialog.value = true;
|
showRequestBoardDialog.value = true;
|
||||||
}
|
}
|
||||||
@@ -298,7 +335,7 @@ async function checkAndInitializeBoard() {
|
|||||||
showRequestBoardDialog.value = true;
|
showRequestBoardDialog.value = true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('检查用户实验板失败:', error);
|
console.error("检查用户实验板失败:", error);
|
||||||
alert?.show("检查用户信息失败", "error");
|
alert?.show("检查用户信息失败", "error");
|
||||||
showRequestBoardDialog.value = true;
|
showRequestBoardDialog.value = true;
|
||||||
}
|
}
|
||||||
@@ -313,7 +350,7 @@ function updateEquipmentFromBoard(board: Board) {
|
|||||||
address: board.ipAddr,
|
address: board.ipAddr,
|
||||||
port: board.port,
|
port: board.port,
|
||||||
boardName: board.boardName,
|
boardName: board.boardName,
|
||||||
boardId: board.id
|
boardId: board.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +358,7 @@ function updateEquipmentFromBoard(board: Board) {
|
|||||||
function handleRequestBoardClose() {
|
function handleRequestBoardClose() {
|
||||||
showRequestBoardDialog.value = false;
|
showRequestBoardDialog.value = false;
|
||||||
// 如果用户取消申请,可以选择返回上一页或显示警告
|
// 如果用户取消申请,可以选择返回上一页或显示警告
|
||||||
router.push('/');
|
router.push("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理申请实验板成功
|
// 处理申请实验板成功
|
||||||
@@ -338,12 +375,12 @@ onMounted(async () => {
|
|||||||
const isAuthenticated = await AuthManager.isAuthenticated();
|
const isAuthenticated = await AuthManager.isAuthenticated();
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
// 验证失败,跳转到登录页面
|
// 验证失败,跳转到登录页面
|
||||||
router.push('/login');
|
router.push("/login");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('身份验证失败:', error);
|
console.error("身份验证失败:", error);
|
||||||
router.push('/login');
|
router.push("/login");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ import { ref, watch } from "vue";
|
|||||||
import { CheckCircle } from "lucide-vue-next";
|
import { CheckCircle } from "lucide-vue-next";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
import { useAlertStore } from "@/components/Alert";
|
import { useAlertStore } from "@/components/Alert";
|
||||||
import type { Board } from "@/APIClient";
|
import { DataClient, type Board } from "@/APIClient";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -113,7 +113,7 @@ async function checkUserBoard() {
|
|||||||
boardInfo.value = null;
|
boardInfo.value = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createAuthenticatedDataClient();
|
const client = AuthManager.createClient(DataClient);
|
||||||
const userInfo = await client.getUserInfo();
|
const userInfo = await client.getUserInfo();
|
||||||
|
|
||||||
if (userInfo.boardID && userInfo.boardID.trim() !== "") {
|
if (userInfo.boardID && userInfo.boardID.trim() !== "") {
|
||||||
@@ -140,7 +140,7 @@ async function requestBoard() {
|
|||||||
requesting.value = true;
|
requesting.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createAuthenticatedDataClient();
|
const client = AuthManager.createClient(DataClient);
|
||||||
const board = await client.getAvailableBoard(undefined);
|
const board = await client.getAvailableBoard(undefined);
|
||||||
|
|
||||||
if (board) {
|
if (board) {
|
||||||
|
|||||||
@@ -433,7 +433,7 @@ const currentVideoSource = ref("");
|
|||||||
const logs = ref<Array<{ time: Date; level: string; message: string }>>([]);
|
const logs = ref<Array<{ time: Date; level: string; message: string }>>([]);
|
||||||
|
|
||||||
// API 客户端
|
// API 客户端
|
||||||
const videoClient = AuthManager.createAuthenticatedVideoStreamClient();
|
const videoClient = AuthManager.createClient(VideoStreamClient);
|
||||||
|
|
||||||
// 添加日志
|
// 添加日志
|
||||||
const addLog = (level: string, message: string) => {
|
const addLog = (level: string, message: string) => {
|
||||||
|
|||||||
@@ -174,7 +174,12 @@
|
|||||||
import { ref, reactive, watch } from "vue";
|
import { ref, reactive, watch } from "vue";
|
||||||
import { AuthManager } from "../../utils/AuthManager";
|
import { AuthManager } from "../../utils/AuthManager";
|
||||||
import { useAlertStore } from "../../components/Alert";
|
import { useAlertStore } from "../../components/Alert";
|
||||||
import { BoardStatus, type NetworkConfigDto } from "../../APIClient";
|
import {
|
||||||
|
BoardStatus,
|
||||||
|
DataClient,
|
||||||
|
NetConfigClient,
|
||||||
|
type NetworkConfigDto,
|
||||||
|
} from "../../APIClient";
|
||||||
import { useRequiredInjection } from "@/utils/Common";
|
import { useRequiredInjection } from "@/utils/Common";
|
||||||
import { useBoardManager } from "@/utils/BoardManager";
|
import { useBoardManager } from "@/utils/BoardManager";
|
||||||
|
|
||||||
@@ -267,8 +272,7 @@ async function handleSubmit() {
|
|||||||
isSubmitting.value = true;
|
isSubmitting.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 通过 AuthManager 获取认证的 DataClient
|
const dataClient = AuthManager.createClient(DataClient);
|
||||||
const dataClient = AuthManager.createAuthenticatedDataClient();
|
|
||||||
|
|
||||||
// 添加板卡到数据库
|
// 添加板卡到数据库
|
||||||
const boardId = await dataClient.addBoard(form.name.trim());
|
const boardId = await dataClient.addBoard(form.name.trim());
|
||||||
@@ -293,8 +297,7 @@ async function handleCancelPairing() {
|
|||||||
if (!addedBoardId.value) return;
|
if (!addedBoardId.value) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 通过 AuthManager 获取认证的 DataClient
|
const dataClient = AuthManager.createClient(DataClient);
|
||||||
const dataClient = AuthManager.createAuthenticatedDataClient();
|
|
||||||
|
|
||||||
// 删除添加的板卡
|
// 删除添加的板卡
|
||||||
await dataClient.deleteBoard(addedBoardId.value);
|
await dataClient.deleteBoard(addedBoardId.value);
|
||||||
@@ -317,8 +320,8 @@ async function handlePairingConfirm() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 通过 AuthManager 获取认证的客户端
|
// 通过 AuthManager 获取认证的客户端
|
||||||
const dataClient = AuthManager.createAuthenticatedDataClient();
|
const dataClient = AuthManager.createClient(DataClient);
|
||||||
const netConfigClient = AuthManager.createAuthenticatedNetConfigClient();
|
const netConfigClient = AuthManager.createClient(NetConfigClient);
|
||||||
|
|
||||||
// 获取数据库中对应分配的板卡信息
|
// 获取数据库中对应分配的板卡信息
|
||||||
const boardInfo = await dataClient.getBoardByID(addedBoardId.value);
|
const boardInfo = await dataClient.getBoardByID(addedBoardId.value);
|
||||||
@@ -365,7 +368,7 @@ async function handlePairingConfirm() {
|
|||||||
|
|
||||||
// 配置失败,删除数据库中的板卡信息
|
// 配置失败,删除数据库中的板卡信息
|
||||||
try {
|
try {
|
||||||
const dataClient = AuthManager.createAuthenticatedDataClient();
|
const dataClient = AuthManager.createClient(DataClient);
|
||||||
await dataClient.deleteBoard(addedBoardId.value);
|
await dataClient.deleteBoard(addedBoardId.value);
|
||||||
} catch (deleteError) {
|
} catch (deleteError) {
|
||||||
console.error("删除板卡失败:", deleteError);
|
console.error("删除板卡失败:", deleteError);
|
||||||
|
|||||||
@@ -62,14 +62,14 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证管理员权限
|
// 验证管理员权限
|
||||||
isAdmin.value = await AuthManager.verifyAdminAuth();
|
isAdmin.value = await AuthManager.isAdminAuthenticated();
|
||||||
|
|
||||||
// 如果当前页面是管理员页面但用户不是管理员,切换到用户信息页面
|
// 如果当前页面是管理员页面但用户不是管理员,切换到用户信息页面
|
||||||
if (activePage.value === 100 && !isAdmin.value) {
|
if (activePage.value === 100 && !isAdmin.value) {
|
||||||
activePage.value = 1;
|
activePage.value = 1;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('用户认证检查失败:', error);
|
console.error("用户认证检查失败:", error);
|
||||||
// 可以在这里处理错误,比如显示错误信息或重定向到登录页面
|
// 可以在这里处理错误,比如显示错误信息或重定向到登录页面
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -273,7 +273,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
import { UserInfo, Board, BoardStatus } from "@/APIClient";
|
import {
|
||||||
|
UserInfo,
|
||||||
|
Board,
|
||||||
|
BoardStatus,
|
||||||
|
DataClient,
|
||||||
|
JtagClient,
|
||||||
|
} from "@/APIClient";
|
||||||
import { Alert, useAlertStore } from "@/components/Alert";
|
import { Alert, useAlertStore } from "@/components/Alert";
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
@@ -319,7 +325,7 @@ const loadBoardInfo = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createAuthenticatedDataClient();
|
const client = AuthManager.createClient(DataClient);
|
||||||
boardInfo.value = await client.getBoardByID(userInfo.value.boardID);
|
boardInfo.value = await client.getBoardByID(userInfo.value.boardID);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("加载实验板信息失败:", err);
|
console.error("加载实验板信息失败:", err);
|
||||||
@@ -335,7 +341,7 @@ const loadUserInfo = async (showSuccessMessage = false) => {
|
|||||||
try {
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
|
||||||
const client = AuthManager.createAuthenticatedDataClient();
|
const client = AuthManager.createClient(DataClient);
|
||||||
userInfo.value = await client.getUserInfo();
|
userInfo.value = await client.getUserInfo();
|
||||||
|
|
||||||
// 如果有绑定的实验板ID,加载实验板信息
|
// 如果有绑定的实验板ID,加载实验板信息
|
||||||
@@ -370,7 +376,7 @@ const applyBoard = async () => {
|
|||||||
alertStore?.info("正在申请实验板...");
|
alertStore?.info("正在申请实验板...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createAuthenticatedDataClient();
|
const client = AuthManager.createClient(DataClient);
|
||||||
|
|
||||||
// 获取可用的实验板
|
// 获取可用的实验板
|
||||||
const availableBoard = await client.getAvailableBoard(undefined);
|
const availableBoard = await client.getAvailableBoard(undefined);
|
||||||
@@ -407,7 +413,7 @@ const testBoardConnection = async () => {
|
|||||||
alertStore?.info("正在测试连接...");
|
alertStore?.info("正在测试连接...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const jtagClient = AuthManager.createAuthenticatedJtagClient();
|
const jtagClient = AuthManager.createClient(JtagClient);
|
||||||
|
|
||||||
// 使用JTAG客户端读取设备ID Code
|
// 使用JTAG客户端读取设备ID Code
|
||||||
const idCode = await jtagClient.getDeviceIDCode(
|
const idCode = await jtagClient.getDeviceIDCode(
|
||||||
@@ -444,7 +450,7 @@ const unbindBoard = async () => {
|
|||||||
alertStore?.info("正在解绑实验板...");
|
alertStore?.info("正在解绑实验板...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createAuthenticatedDataClient();
|
const client = AuthManager.createClient(DataClient);
|
||||||
const success = await client.unbindBoard();
|
const success = await client.unbindBoard();
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
|||||||
Reference in New Issue
Block a user