add: 添加实验列表界面,实验增删完全依赖数据库实现

This commit is contained in:
alivender
2025-08-01 12:57:30 +08:00
parent d27b5d7737
commit c564844673
19 changed files with 3511 additions and 1688 deletions

View File

@@ -1,5 +1,5 @@
<template>
<div class="fixed left-1/2 top-30 z-999 -translate-x-1/2">
<div class="fixed left-1/2 top-30 z-[9999] -translate-x-1/2">
<transition
name="alert"
enter-active-class="alert-enter-active"

View File

@@ -119,6 +119,7 @@
componentManager.prepareComponentProps(
component.attrs || {},
component.id,
props.examId,
)
"
@update:bindKey="
@@ -175,9 +176,7 @@ import {
ref,
reactive,
onMounted,
onUnmounted,
computed,
watch,
provide,
} from "vue";
import { useEventListener } from "@vueuse/core";
@@ -217,6 +216,7 @@ const emit = defineEmits(["toggle-doc-panel", "open-components"]);
// 定义组件接受的属性
const props = defineProps<{
showDocPanel?: boolean; // 添加属性接收文档面板的显示状态
examId?: string; // 新增examId属性
}>();
// 获取componentManager实例
@@ -977,7 +977,8 @@ function exportDiagram() {
onMounted(async () => {
// 加载图表数据
try {
diagramData.value = await loadDiagramData();
// 传入examId参数让diagramManager处理动态加载
diagramData.value = await loadDiagramData(props.examId);
// 预加载所有组件模块
const componentTypes = new Set<string>();

View File

@@ -763,11 +763,15 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
function prepareComponentProps(
attrs: Record<string, any>,
componentId?: string,
examId?: string,
): Record<string, any> {
const result: Record<string, any> = { ...attrs };
if (componentId) {
result.componentId = componentId;
}
if (examId) {
result.examId = examId;
}
return result;
}

View File

@@ -26,6 +26,8 @@ export interface DiagramPart {
// 连接类型定义 - 使用元组类型表示四元素数组
export type ConnectionArray = [string, string, number, string[]];
import { AuthManager } from '@/utils/AuthManager';
// 解析连接字符串为组件ID和引脚ID
export function parseConnectionPin(connectionPin: string): { componentId: string; pinId: string } {
const [componentId, pinId] = connectionPin.split(':');
@@ -80,22 +82,72 @@ export interface WireItem {
showLabel: boolean;
}
// 从本地存储加载图表数据
export async function loadDiagramData(): Promise<DiagramData> {
// 从本地存储或动态API加载图表数据
export async function loadDiagramData(examId?: string): Promise<DiagramData> {
try {
// 先尝试从本地存储加载
const savedData = localStorage.getItem('diagramData');
if (savedData) {
return JSON.parse(savedData);
// 如果提供了examId优先从API加载实验的diagram
if (examId) {
try {
const examClient = AuthManager.createAuthenticatedExamClient();
// 获取diagram类型的资源列表
const resources = await examClient.getExamResourceList(examId, 'canvas');
if (resources && resources.length > 0) {
// 获取第一个diagram资源
const diagramResource = resources[0];
// 使用动态API获取资源文件内容
const response = await examClient.getExamResourceById(diagramResource.id);
if (response && response.data) {
const text = await response.data.text();
const data = JSON.parse(text);
// 验证数据格式
const validation = validateDiagramData(data);
if (validation.isValid) {
console.log('成功从API加载实验diagram:', examId);
return data;
} else {
console.warn('API返回的diagram数据格式无效:', validation.errors);
}
}
} else {
console.log('未找到实验diagram资源使用默认加载方式');
}
} catch (error) {
console.warn('从API加载实验diagram失败使用默认加载方式:', error);
}
}
// 如果本地存储没有,从文件加载
// 如果没有examId或API加载失败尝试从本地存储加载
const savedData = localStorage.getItem('diagramData');
if (savedData) {
const data = JSON.parse(savedData);
const validation = validateDiagramData(data);
if (validation.isValid) {
return data;
} else {
console.warn('本地存储的diagram数据格式无效:', validation.errors);
}
}
// 如果本地存储也没有,从静态文件加载(作为最后的备选)
const response = await fetch('/src/components/diagram.json');
if (!response.ok) {
throw new Error(`Failed to load diagram.json: ${response.statusText}`);
}
const data = await response.json();
return data;
// 验证静态文件数据
const validation = validateDiagramData(data);
if (validation.isValid) {
return data;
} else {
console.warn('静态diagram文件数据格式无效:', validation.errors);
throw new Error('所有diagram数据源都无效');
}
} catch (error) {
console.error('Error loading diagram data:', error);
// 返回空的默认数据结构

View File

@@ -588,9 +588,9 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
};
const forceCapture = async () => {
// 检查是否有其他操作正在进行
if (operationMutex.isLocked()) {
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
// 检查是否正在捕获
if (!isCapturing.value) {
alert.warn("当前没有正在进行的捕获操作", 2000);
return;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,305 +1,324 @@
<template>
<div
class="tutorial-carousel relative"
@wheel.prevent="handleWheel"
@mouseenter="pauseAutoRotation"
@mouseleave="resumeAutoRotation"
> <!-- 例程卡片堆叠 -->
<div class="card-stack relative mx-auto">
<div
v-for="(tutorial, index) in tutorials"
:key="index"
class="tutorial-card absolute transition-all duration-500 ease-in-out rounded-2xl shadow-2xl border-4 border-base-300 overflow-hidden"
:class="getCardClass(index)"
:style="getCardStyle(index)"
@click="handleCardClick(index, tutorial.id)"
>
<!-- 卡片内容 -->
<div class="relative">
<!-- 图片 --> <img
:src="tutorial.thumbnail || `https://placehold.co/600x400?text=${tutorial.title}`"
class="w-full object-contain"
:alt="tutorial.title"
style="width: 600px; height: 400px;"
/>
<!-- 卡片蒙层 -->
<div
class="absolute inset-0 bg-primary opacity-20 transition-opacity duration-300"
:class="{'opacity-10': index === currentIndex}"
></div>
<!-- 标题覆盖层 -->
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-base-300 to-transparent">
<h3 class="text-lg font-bold text-base-content">{{ tutorial.title }}</h3>
<p class="text-sm opacity-80 truncate">{{ tutorial.description }}</p>
</div>
</div>
</div>
</div>
<!-- 导航指示器 -->
<div class="indicators flex justify-center gap-2 mt-4">
<button
v-for="(_, index) in tutorials"
:key="index"
@click="setActiveCard(index)"
class="w-3 h-3 rounded-full transition-all duration-300"
:class="index === currentIndex ? 'bg-primary scale-125' : 'bg-base-300'"
></button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
// 接口定义
interface Tutorial {
id: string;
title: string;
description: string;
thumbnail?: string;
docPath: string;
}
// Props
const props = defineProps<{
autoRotationInterval?: number;
}>();
// 配置默认值
const autoRotationInterval = props.autoRotationInterval || 5000; // 默认5秒
// 状态管理
const tutorials = ref<Tutorial[]>([]);
const currentIndex = ref(0);
const router = useRouter();
let autoRotationTimer: number | null = null;
// 处理卡片点击
const handleCardClick = (index: number, tutorialId: string) => {
if (index === currentIndex.value) {
goToTutorial(tutorialId);
} else {
setActiveCard(index);
}
};
// 从 public/doc 目录加载例程信息
onMounted(async () => {
try {
// 尝试从API获取教程目录
let tutorialIds: string[] = [];
try {
const response = await fetch('/api/tutorial');
if (response.ok) {
const data = await response.json();
tutorialIds = data.tutorials || [];
}
} catch (error) {
console.warn('无法从API获取教程目录使用默认值:', error);
}
// 如果API调用失败或返回空列表使用默认值
if (tutorialIds.length === 0) {
console.log('使用默认教程列表');
tutorialIds = ['01', '02', '03', '04', '05', '06', '11', '12', '13']; // 默认例程
} else {
console.log('使用API获取的教程列表:', tutorialIds);
}
// 为每个例程创建对象并尝试获取文档标题
const tutorialPromises = tutorialIds.map(async (id) => {
// 尝试读取doc.md获取标题
let title = `例程 ${id}`;
let description = "点击加载此例程";
let thumbnail = `/doc/${id}/cover.png`; // 默认使用第一张图片作为缩略图
try {
// 尝试读取文档内容获取标题
const response = await fetch(`/doc/${id}/doc.md`);
if (response.ok) {
const text = await response.text();
// 从Markdown提取标题
const titleMatch = text.match(/^#\s+(.+)$/m);
if (titleMatch && titleMatch[1]) {
title = titleMatch[1].trim();
}
// 提取第一段作为描述
const descMatch = text.match(/\n\n([^#\n][^\n]+)/);
if (descMatch && descMatch[1]) {
description = descMatch[1].substring(0, 100).trim();
if (description.length === 100) description += '...';
}
}
} catch (error) {
console.warn(`无法读取例程${id}的文档内容:`, error);
}
return {
id,
title,
description,
thumbnail,
docPath: `/doc/${id}/doc.md`
};
});
tutorials.value = await Promise.all(tutorialPromises);
// 启动自动旋转
startAutoRotation();
} catch (error) {
console.error('加载例程失败:', error);
}
});
// 在组件销毁时清除计时器
onUnmounted(() => {
if (autoRotationTimer) {
clearInterval(autoRotationTimer);
}
});
// 鼠标滚轮处理
const handleWheel = (event: WheelEvent) => {
if (event.deltaY > 0) {
nextCard();
} else {
prevCard();
}
};
// 下一张卡片
const nextCard = () => {
currentIndex.value = (currentIndex.value + 1) % tutorials.value.length;
};
// 上一张卡片
const prevCard = () => {
currentIndex.value = (currentIndex.value - 1 + tutorials.value.length) % tutorials.value.length;
};
// 设置活动卡片
const setActiveCard = (index: number) => {
currentIndex.value = index;
};
// 自动旋转
const startAutoRotation = () => {
autoRotationTimer = window.setInterval(() => {
nextCard();
}, autoRotationInterval);
};
// 暂停自动旋转
const pauseAutoRotation = () => {
if (autoRotationTimer) {
clearInterval(autoRotationTimer);
autoRotationTimer = null;
}
};
// 恢复自动旋转
const resumeAutoRotation = () => {
if (!autoRotationTimer) {
startAutoRotation();
}
};
// 前往例程
const goToTutorial = (tutorialId: string) => {
// 跳转到工程页面,并通过 query 参数传递文档路径
router.push({
path: '/project',
query: { tutorial: tutorialId }
});
};
// 计算卡片类和样式
const getCardClass = (index: number) => {
const isActive = index === currentIndex.value;
const isPrev = (index === currentIndex.value - 1) || (currentIndex.value === 0 && index === tutorials.value.length - 1);
const isNext = (index === currentIndex.value + 1) || (currentIndex.value === tutorials.value.length - 1 && index === 0);
return {
'z-30': isActive,
'z-20': isPrev || isNext,
'z-10': !isActive && !isPrev && !isNext,
'hover:scale-105': isActive,
'cursor-pointer': true
};
};
const getCardStyle = (index: number) => {
const isActive = index === currentIndex.value;
const isPrev = (index === currentIndex.value - 1) || (currentIndex.value === 0 && index === tutorials.value.length - 1);
const isNext = (index === currentIndex.value + 1) || (currentIndex.value === tutorials.value.length - 1 && index === 0);
// 基本样式
let style = {
transform: 'scale(1) translateY(0) rotate(0deg)',
opacity: '1',
filter: 'blur(0)'
};
// 活动卡片
if (isActive) {
return style;
}
// 上一张卡片
if (isPrev) {
style.transform = 'scale(0.85) translateY(-10%) rotate(-5deg)';
style.opacity = '0.7';
style.filter = 'blur(1px)';
return style;
}
// 下一张卡片
if (isNext) {
style.transform = 'scale(0.85) translateY(10%) rotate(5deg)';
style.opacity = '0.7';
style.filter = 'blur(1px)';
return style;
}
// 其他卡片
style.transform = 'scale(0.7) translateY(0) rotate(0deg)';
style.opacity = '0.4';
style.filter = 'blur(2px)';
return style;
}
</script>
<style scoped>
.tutorial-carousel {
width: 100%;
height: 500px;
perspective: 1000px;
display: flex;
flex-direction: column;
align-items: center;
}
.card-stack {
width: 600px;
height: 440px;
position: relative;
transform-style: preserve-3d;
}
.tutorial-card {
width: 600px;
height: 400px;
background-color: hsl(var(--b2));
will-change: transform, opacity;
}
.tutorial-card:hover {
box-shadow: 0 0 15px rgba(var(--p), 0.5);
}
</style>
<template>
<div
class="tutorial-carousel relative"
@wheel.prevent="handleWheel"
@mouseenter="pauseAutoRotation"
@mouseleave="resumeAutoRotation"
> <!-- 例程卡片堆叠 -->
<div class="card-stack relative mx-auto">
<div
v-for="(tutorial, index) in tutorials"
:key="index"
class="tutorial-card absolute transition-all duration-500 ease-in-out rounded-2xl shadow-2xl border-4 border-base-300 overflow-hidden"
:class="getCardClass(index)"
:style="getCardStyle(index)"
@click="handleCardClick(index, tutorial.id)"
>
<!-- 卡片内容 -->
<div class="relative">
<!-- 图片 --> <img
:src="tutorial.thumbnail || `https://placehold.co/600x400?text=${tutorial.title}`"
class="w-full object-contain"
:alt="tutorial.title"
style="width: 600px; height: 400px;"
/>
<!-- 卡片蒙层 -->
<div
class="absolute inset-0 bg-primary opacity-20 transition-opacity duration-300"
:class="{'opacity-10': index === currentIndex}"
></div>
<!-- 标题覆盖层 -->
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-base-300 to-transparent">
<div class="flex flex-col gap-2">
<h3 class="text-lg font-bold text-base-content">{{ tutorial.title }}</h3>
<p class="text-sm opacity-80 truncate">{{ tutorial.description }}</p>
<!-- 标签显示 -->
<div v-if="tutorial.tags && tutorial.tags.length > 0" class="flex flex-wrap gap-1">
<span
v-for="tag in tutorial.tags.slice(0, 3)"
:key="tag"
class="badge badge-outline badge-xs text-xs"
>
{{ tag }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 导航指示器 -->
<div class="indicators flex justify-center gap-2 mt-4">
<button
v-for="(_, index) in tutorials"
:key="index"
@click="setActiveCard(index)"
class="w-3 h-3 rounded-full transition-all duration-300"
:class="index === currentIndex ? 'bg-primary scale-125' : 'bg-base-300'"
></button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { AuthManager } from '@/utils/AuthManager';
import type { ExamSummary } from '@/APIClient';
// 接口定义
interface Tutorial {
id: string;
title: string;
description: string;
thumbnail?: string;
tags: string[];
}
// Props
const props = defineProps<{
autoRotationInterval?: number;
}>();
// 配置默认值
const autoRotationInterval = props.autoRotationInterval || 5000; // 默认5秒
// 状态管理
const tutorials = ref<Tutorial[]>([]);
const currentIndex = ref(0);
const router = useRouter();
let autoRotationTimer: number | null = null;
// 处理卡片点击
const handleCardClick = (index: number, tutorialId: string) => {
if (index === currentIndex.value) {
goToExam(tutorialId);
} else {
setActiveCard(index);
}
};
// 从数据库加载实验数据
onMounted(async () => {
try {
console.log('正在从数据库加载实验数据...');
// 创建认证客户端
const client = AuthManager.createAuthenticatedExamClient();
// 获取实验列表
const examList: ExamSummary[] = await client.getExamList();
// 筛选可见的实验并转换为Tutorial格式
const visibleExams = examList
.filter(exam => exam.isVisibleToUsers)
.slice(0, 6); // 限制轮播显示最多6个实验
if (visibleExams.length === 0) {
console.warn('没有找到可见的实验');
return;
}
// 转换数据格式并获取封面图片
const tutorialPromises = visibleExams.map(async (exam) => {
let thumbnail: string | undefined;
try {
// 获取实验的封面资源
const resourceList = await client.getExamResourceList(exam.id, 'cover');
if (resourceList && resourceList.length > 0) {
// 使用第一个封面资源
const coverResource = resourceList[0];
const fileResponse = await client.getExamResourceById(coverResource.id);
// 创建Blob URL作为缩略图
thumbnail = URL.createObjectURL(fileResponse.data);
}
} catch (error) {
console.warn(`无法获取实验${exam.id}的封面图片:`, error);
}
return {
id: exam.id,
title: exam.name,
description: '点击查看实验详情',
thumbnail,
tags: exam.tags || []
};
});
tutorials.value = await Promise.all(tutorialPromises);
console.log('成功加载实验数据:', tutorials.value.length, '个实验');
// 启动自动旋转
startAutoRotation();
} catch (error) {
console.error('加载实验数据失败:', error);
// 如果加载失败,显示默认的占位内容
tutorials.value = [{
id: 'placeholder',
title: '实验数据加载中...',
description: '请稍后或刷新页面重试',
thumbnail: undefined,
tags: []
}];
}
});
// 在组件销毁时清除计时器和Blob URLs
onUnmounted(() => {
if (autoRotationTimer) {
clearInterval(autoRotationTimer);
}
// 清理创建的Blob URLs
tutorials.value.forEach(tutorial => {
if (tutorial.thumbnail && tutorial.thumbnail.startsWith('blob:')) {
URL.revokeObjectURL(tutorial.thumbnail);
}
});
});
// 鼠标滚轮处理
const handleWheel = (event: WheelEvent) => {
if (event.deltaY > 0) {
nextCard();
} else {
prevCard();
}
};
// 下一张卡片
const nextCard = () => {
currentIndex.value = (currentIndex.value + 1) % tutorials.value.length;
};
// 上一张卡片
const prevCard = () => {
currentIndex.value = (currentIndex.value - 1 + tutorials.value.length) % tutorials.value.length;
};
// 设置活动卡片
const setActiveCard = (index: number) => {
currentIndex.value = index;
};
// 自动旋转
const startAutoRotation = () => {
autoRotationTimer = window.setInterval(() => {
nextCard();
}, autoRotationInterval);
};
// 暂停自动旋转
const pauseAutoRotation = () => {
if (autoRotationTimer) {
clearInterval(autoRotationTimer);
autoRotationTimer = null;
}
};
// 恢复自动旋转
const resumeAutoRotation = () => {
if (!autoRotationTimer) {
startAutoRotation();
}
};
// 前往实验
const goToExam = (examId: string) => {
// 跳转到实验列表页面并传递examId参数页面将自动打开对应的实验详情模态框
router.push({
path: '/exam',
query: { examId: examId }
});
};
// 计算卡片类和样式
const getCardClass = (index: number) => {
const isActive = index === currentIndex.value;
const isPrev = (index === currentIndex.value - 1) || (currentIndex.value === 0 && index === tutorials.value.length - 1);
const isNext = (index === currentIndex.value + 1) || (currentIndex.value === tutorials.value.length - 1 && index === 0);
return {
'z-30': isActive,
'z-20': isPrev || isNext,
'z-10': !isActive && !isPrev && !isNext,
'hover:scale-105': isActive,
'cursor-pointer': true
};
};
const getCardStyle = (index: number) => {
const isActive = index === currentIndex.value;
const isPrev = (index === currentIndex.value - 1) || (currentIndex.value === 0 && index === tutorials.value.length - 1);
const isNext = (index === currentIndex.value + 1) || (currentIndex.value === tutorials.value.length - 1 && index === 0);
// 基本样式
let style = {
transform: 'scale(1) translateY(0) rotate(0deg)',
opacity: '1',
filter: 'blur(0)'
};
// 活动卡片
if (isActive) {
return style;
}
// 上一张卡片
if (isPrev) {
style.transform = 'scale(0.85) translateY(-10%) rotate(-5deg)';
style.opacity = '0.7';
style.filter = 'blur(1px)';
return style;
}
// 下一张卡片
if (isNext) {
style.transform = 'scale(0.85) translateY(10%) rotate(5deg)';
style.opacity = '0.7';
style.filter = 'blur(1px)';
return style;
}
// 其他卡片
style.transform = 'scale(0.7) translateY(0) rotate(0deg)';
style.opacity = '0.4';
style.filter = 'blur(2px)';
return style;
}
</script>
<style scoped>
.tutorial-carousel {
width: 100%;
height: 500px;
perspective: 1000px;
display: flex;
flex-direction: column;
align-items: center;
}
.card-stack {
width: 600px;
height: 440px;
position: relative;
transform-style: preserve-3d;
}
.tutorial-card {
width: 600px;
height: 400px;
background-color: hsl(var(--b2));
will-change: transform, opacity;
}
.tutorial-card:hover {
box-shadow: 0 0 15px rgba(var(--p), 0.5);
}
</style>

View File

@@ -1,21 +1,64 @@
<template>
<div class="flex flex-col bg-base-100 justify-center items-center">
<div class="flex flex-col bg-base-100 justify-center items-center gap-4">
<!-- Title -->
<h1 class="font-bold text-2xl">上传比特流文件</h1>
<h1 class="font-bold text-2xl">比特流文件</h1>
<!-- 示例比特流下载区域 (仅在有examId时显示) -->
<div v-if="examId && availableBitstreams.length > 0" class="w-full">
<fieldset class="fieldset w-full">
<legend class="fieldset-legend text-sm">示例比特流文件</legend>
<div class="space-y-2">
<div v-for="bitstream in availableBitstreams" :key="bitstream.id" class="flex items-center justify-between p-2 bg-base-200 rounded">
<span class="text-sm">{{ bitstream.name }}</span>
<div class="flex gap-2">
<button
@click="downloadExampleBitstream(bitstream)"
class="btn btn-sm btn-secondary"
:disabled="isDownloading || isProgramming"
>
<div v-if="isDownloading">
<span class="loading loading-spinner loading-xs"></span>
下载中...
</div>
<div v-else>
下载示例
</div>
</button>
<button
@click="programExampleBitstream(bitstream)"
class="btn btn-sm btn-primary"
:disabled="isDownloading || isProgramming || !uploadEvent"
>
<div v-if="isProgramming">
<span class="loading loading-spinner loading-xs"></span>
烧录中...
</div>
<div v-else>
直接烧录
</div>
</button>
</div>
</div>
</div>
</fieldset>
</div>
<!-- 分割线 -->
<div v-if="examId && availableBitstreams.length > 0" class="divider"></div>
<!-- Input File -->
<fieldset class="fieldset w-full">
<legend class="fieldset-legend text-sm">选择或拖拽上传文件</legend>
<legend class="fieldset-legend text-sm">上传自定义比特流文件</legend>
<input type="file" ref="fileInput" class="file-input w-full" @change="handleFileChange" />
<label class="fieldset-label">文件最大容量: {{ maxMemory }}MB</label>
</fieldset>
<!-- Upload Button -->
<div class="card-actions w-full">
<button @click="handleClick" class="btn btn-primary grow" :disabled="isUploading">
<button @click="handleClick" class="btn btn-primary grow" :disabled="isUploading || isProgramming">
<div v-if="isUploading">
<span class="loading loading-spinner"></span>
下载...
上传...
</div>
<div v-else>
{{ buttonText }}
@@ -27,6 +70,7 @@
<script lang="ts" setup>
import { computed, ref, useTemplateRef, onMounted } from "vue";
import { AuthManager } from "@/utils/AuthManager"; // Adjust the path based on your project structure
import { useDialogStore } from "@/stores/dialog";
import { isNull, isUndefined } from "lodash";
@@ -34,10 +78,12 @@ interface Props {
uploadEvent?: (file: File) => Promise<boolean>;
downloadEvent?: () => Promise<boolean>;
maxMemory?: number;
examId?: string; // 新增examId属性
}
const props = withDefaults(defineProps<Props>(), {
maxMemory: 4,
examId: '',
});
const emits = defineEmits<{
@@ -47,6 +93,10 @@ const emits = defineEmits<{
const dialog = useDialogStore();
const isUploading = ref(false);
const isDownloading = ref(false);
const isProgramming = ref(false);
const availableBitstreams = ref<{id: number, name: string}[]>([]);
const buttonText = computed(() => {
return isUndefined(props.downloadEvent) ? "上传" : "上传并下载";
});
@@ -56,14 +106,113 @@ const bitstream = defineModel("bitstreamFile", {
type: File,
default: undefined,
});
onMounted(() => {
// 初始化时加载示例比特流
onMounted(async () => {
if (!isUndefined(bitstream.value) && !isNull(fileInput.value)) {
let fileList = new DataTransfer();
fileList.items.add(bitstream.value);
fileInput.value.files = fileList.files;
}
await loadAvailableBitstreams();
});
// 加载可用的比特流文件列表
async function loadAvailableBitstreams() {
console.log('加载可用比特流文件examId:', props.examId);
if (!props.examId) {
availableBitstreams.value = [];
return;
}
try {
const examClient = AuthManager.createAuthenticatedExamClient();
// 使用新的API获取比特流资源列表
const resources = await examClient.getExamResourceList(props.examId, 'bitstream');
availableBitstreams.value = resources.map(r => ({ id: r.id, name: r.name })) || [];
} catch (error) {
console.error('加载比特流列表失败:', error);
availableBitstreams.value = [];
}
}
// 下载示例比特流
async function downloadExampleBitstream(bitstream: {id: number, name: string}) {
if (isDownloading.value) return;
isDownloading.value = true;
try {
const examClient = AuthManager.createAuthenticatedExamClient();
// 使用动态API获取资源文件
const response = await examClient.getExamResourceById(bitstream.id);
if (response && response.data) {
// 创建下载链接
const url = URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = url;
link.download = response.fileName || bitstream.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
dialog.info("示例比特流下载成功");
} else {
dialog.error("下载失败:响应数据为空");
}
} catch (error) {
console.error('下载示例比特流失败:', error);
dialog.error("下载示例比特流失败");
} finally {
isDownloading.value = false;
}
}
// 直接烧录示例比特流
async function programExampleBitstream(bitstream: {id: number, name: string}) {
if (isProgramming.value || !props.uploadEvent) return;
isProgramming.value = true;
try {
const examClient = AuthManager.createAuthenticatedExamClient();
// 使用动态API获取比特流文件数据
const response = await examClient.getExamResourceById(bitstream.id);
if (!response || !response.data) {
throw new Error('获取比特流文件失败');
}
const file = new File([response.data], response.fileName || bitstream.name, { type: response.data.type });
// 调用上传事件
const uploadSuccess = await props.uploadEvent(file);
if (uploadSuccess) {
// 如果有下载事件(烧录),则执行
if (props.downloadEvent) {
const downloadSuccess = await props.downloadEvent();
if (downloadSuccess) {
dialog.info("示例比特流烧录成功");
} else {
dialog.error("烧录失败");
}
} else {
dialog.info("示例比特流上传成功");
}
} else {
dialog.error("上传失败");
}
} catch (error) {
console.error('烧录示例比特流失败:', error);
dialog.error("烧录示例比特流失败");
} finally {
isProgramming.value = false;
}
}
function handleFileChange(event: Event): void {
const target = event.target as HTMLInputElement;
const file = target.files?.[0]; // 获取选中的第一个文件

View File

@@ -27,7 +27,7 @@
to="#ComponentCapabilities"
v-if="selectecComponentID === props.componentId"
>
<MotherBoardCaps :jtagFreq="jtagFreq" @change-jtag-freq="changeJtagFreq" />
<MotherBoardCaps :jtagFreq="jtagFreq" :exam-id="examId" @change-jtag-freq="changeJtagFreq" />
</Teleport>
</template>
@@ -41,6 +41,7 @@ import { toNumber } from "lodash";
export interface MotherBoardProps {
size: number;
componentId?: string;
examId?: string; // 新增examId属性
}
const emit = defineEmits<{

View File

@@ -16,6 +16,7 @@
<div class="divider"></div>
<UploadCard class="bg-base-200" :upload-event="eqps.jtagUploadBitstream"
:download-event="eqps.jtagDownloadBitstream" :bitstream-file="eqps.jtagBitstream"
:exam-id="examId"
@update:bitstream-file="handleBitstreamChange">
</UploadCard>
<div class="divider"></div>
@@ -61,6 +62,7 @@ import { RefreshCcwIcon } from "lucide-vue-next";
interface CapsProps {
jtagFreq?: string;
examId?: string; // 新增examId属性
}
const emits = defineEmits<{