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