feat: 使首页的教程placehold支持中文,同时使markdown编辑器同app主题变化
This commit is contained in:
parent
c4b3a09198
commit
24622d30cf
|
@ -1,325 +1,356 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="tutorial-carousel relative"
|
class="tutorial-carousel relative"
|
||||||
@wheel.prevent="handleWheel"
|
@wheel.prevent="handleWheel"
|
||||||
@mouseenter="pauseAutoRotation"
|
@mouseenter="pauseAutoRotation"
|
||||||
@mouseleave="resumeAutoRotation"
|
@mouseleave="resumeAutoRotation"
|
||||||
> <!-- 例程卡片堆叠 -->
|
>
|
||||||
<div class="card-stack relative mx-auto">
|
<!-- 例程卡片堆叠 -->
|
||||||
<div
|
<div class="card-stack relative mx-auto">
|
||||||
v-for="(tutorial, index) in tutorials"
|
<div
|
||||||
:key="index"
|
v-for="(tutorial, index) in tutorials"
|
||||||
class="tutorial-card absolute transition-all duration-500 ease-in-out rounded-2xl shadow-2xl border-4 border-base-300 overflow-hidden"
|
:key="index"
|
||||||
:class="getCardClass(index)"
|
class="tutorial-card absolute transition-all duration-500 ease-in-out rounded-2xl shadow-2xl border-4 border-base-300 overflow-hidden"
|
||||||
:style="getCardStyle(index)"
|
:class="getCardClass(index)"
|
||||||
@click="handleCardClick(index, tutorial.id)"
|
:style="getCardStyle(index)"
|
||||||
>
|
@click="handleCardClick(index, tutorial.id)"
|
||||||
<!-- 卡片内容 -->
|
>
|
||||||
<div class="relative">
|
<!-- 卡片内容 -->
|
||||||
<!-- 图片 --> <img
|
<div class="relative">
|
||||||
:src="tutorial.thumbnail || `https://placehold.co/600x400?text=${tutorial.title}`"
|
<!-- 图片 -->
|
||||||
class="w-full object-contain"
|
<img
|
||||||
:alt="tutorial.title"
|
:src="
|
||||||
style="width: 600px; height: 400px;"
|
tutorial.thumbnail ||
|
||||||
/>
|
`https://kaifage.com/api/placeholder/600/400?text=${tutorial.title}&color=000000&bgColor=ffffff&fontSize=72`
|
||||||
|
"
|
||||||
<!-- 卡片蒙层 -->
|
class="w-full object-contain"
|
||||||
<div
|
:alt="tutorial.title"
|
||||||
class="absolute inset-0 bg-primary opacity-20 transition-opacity duration-300"
|
style="width: 600px; height: 400px"
|
||||||
:class="{'opacity-10': index === currentIndex}"
|
/>
|
||||||
></div>
|
|
||||||
|
<!-- 卡片蒙层 -->
|
||||||
<!-- 标题覆盖层 -->
|
<div
|
||||||
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-base-300 to-transparent">
|
class="absolute inset-0 bg-primary opacity-20 transition-opacity duration-300"
|
||||||
<div class="flex flex-col gap-2">
|
:class="{ 'opacity-10': index === currentIndex }"
|
||||||
<h3 class="text-lg font-bold text-base-content">{{ tutorial.title }}</h3>
|
></div>
|
||||||
<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">
|
<div
|
||||||
<span
|
class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-base-300 to-transparent"
|
||||||
v-for="tag in tutorial.tags.slice(0, 3)"
|
>
|
||||||
:key="tag"
|
<div class="flex flex-col gap-2">
|
||||||
class="badge badge-outline badge-xs text-xs"
|
<h3 class="text-lg font-bold text-base-content">
|
||||||
>
|
{{ tutorial.title }}
|
||||||
{{ tag }}
|
</h3>
|
||||||
</span>
|
<p class="text-sm opacity-80 truncate">
|
||||||
</div>
|
{{ tutorial.description }}
|
||||||
</div>
|
</p>
|
||||||
</div>
|
<!-- 标签显示 -->
|
||||||
</div>
|
<div
|
||||||
</div>
|
v-if="tutorial.tags && tutorial.tags.length > 0"
|
||||||
</div>
|
class="flex flex-wrap gap-1"
|
||||||
|
>
|
||||||
<!-- 导航指示器 -->
|
<span
|
||||||
<div class="indicators flex justify-center gap-2 mt-4">
|
v-for="tag in tutorial.tags.slice(0, 3)"
|
||||||
<button
|
:key="tag"
|
||||||
v-for="(_, index) in tutorials"
|
class="badge badge-outline badge-xs text-xs"
|
||||||
:key="index"
|
>
|
||||||
@click="setActiveCard(index)"
|
{{ tag }}
|
||||||
class="w-3 h-3 rounded-full transition-all duration-300"
|
</span>
|
||||||
:class="index === currentIndex ? 'bg-primary scale-125' : 'bg-base-300'"
|
</div>
|
||||||
></button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
</div>
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted, onUnmounted } from 'vue';
|
<!-- 导航指示器 -->
|
||||||
import { useRouter } from 'vue-router';
|
<div class="indicators flex justify-center gap-2 mt-4">
|
||||||
import { AuthManager } from '@/utils/AuthManager';
|
<button
|
||||||
import type { ExamSummary } from '@/APIClient';
|
v-for="(_, index) in tutorials"
|
||||||
|
:key="index"
|
||||||
// 接口定义
|
@click="setActiveCard(index)"
|
||||||
interface Tutorial {
|
class="w-3 h-3 rounded-full transition-all duration-300"
|
||||||
id: string;
|
:class="index === currentIndex ? 'bg-primary scale-125' : 'bg-base-300'"
|
||||||
title: string;
|
></button>
|
||||||
description: string;
|
</div>
|
||||||
thumbnail?: string;
|
</div>
|
||||||
tags: string[];
|
</template>
|
||||||
}
|
|
||||||
|
<script setup lang="ts">
|
||||||
// Props
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
const props = defineProps<{
|
import { useRouter } from "vue-router";
|
||||||
autoRotationInterval?: number;
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
}>();
|
import type { ExamInfo } from "@/APIClient";
|
||||||
|
|
||||||
// 配置默认值
|
// 接口定义
|
||||||
const autoRotationInterval = props.autoRotationInterval || 5000; // 默认5秒
|
interface Tutorial {
|
||||||
|
id: string;
|
||||||
// 状态管理
|
title: string;
|
||||||
const tutorials = ref<Tutorial[]>([]);
|
description: string;
|
||||||
const currentIndex = ref(0);
|
thumbnail?: string;
|
||||||
const router = useRouter();
|
tags: string[];
|
||||||
let autoRotationTimer: number | null = null;
|
}
|
||||||
|
|
||||||
// 处理卡片点击
|
// Props
|
||||||
const handleCardClick = (index: number, tutorialId: string) => {
|
const props = defineProps<{
|
||||||
if (index === currentIndex.value) {
|
autoRotationInterval?: number;
|
||||||
goToExam(tutorialId);
|
}>();
|
||||||
} else {
|
|
||||||
setActiveCard(index);
|
// 配置默认值
|
||||||
}
|
const autoRotationInterval = props.autoRotationInterval || 5000; // 默认5秒
|
||||||
};
|
|
||||||
|
// 状态管理
|
||||||
// 从数据库加载实验数据
|
const tutorials = ref<Tutorial[]>([]);
|
||||||
onMounted(async () => {
|
const currentIndex = ref(0);
|
||||||
try {
|
const router = useRouter();
|
||||||
console.log('正在从数据库加载实验数据...');
|
let autoRotationTimer: number | null = null;
|
||||||
|
|
||||||
// 创建认证客户端
|
// 处理卡片点击
|
||||||
const client = AuthManager.createAuthenticatedExamClient();
|
const handleCardClick = (index: number, tutorialId: string) => {
|
||||||
|
if (index === currentIndex.value) {
|
||||||
// 获取实验列表
|
goToExam(tutorialId);
|
||||||
const examList: ExamSummary[] = await client.getExamList();
|
} else {
|
||||||
|
setActiveCard(index);
|
||||||
// 筛选可见的实验并转换为Tutorial格式
|
}
|
||||||
const visibleExams = examList
|
};
|
||||||
.filter(exam => exam.isVisibleToUsers)
|
|
||||||
.slice(0, 6); // 限制轮播显示最多6个实验
|
// 从数据库加载实验数据
|
||||||
|
onMounted(async () => {
|
||||||
if (visibleExams.length === 0) {
|
try {
|
||||||
console.warn('没有找到可见的实验');
|
console.log("正在从数据库加载实验数据...");
|
||||||
return;
|
|
||||||
}
|
// 创建认证客户端
|
||||||
|
const client = AuthManager.createAuthenticatedExamClient();
|
||||||
// 转换数据格式并获取封面图片
|
|
||||||
const tutorialPromises = visibleExams.map(async (exam) => {
|
// 获取实验列表
|
||||||
let thumbnail: string | undefined;
|
const examList: ExamInfo[] = await client.getExamList();
|
||||||
|
|
||||||
try {
|
// 筛选可见的实验并转换为Tutorial格式
|
||||||
// 获取实验的封面资源(模板资源)
|
const visibleExams = examList
|
||||||
const resourceClient = AuthManager.createAuthenticatedResourceClient();
|
.filter((exam) => exam.isVisibleToUsers)
|
||||||
const resourceList = await resourceClient.getResourceList(exam.id, 'cover', 'template');
|
.slice(0, 6); // 限制轮播显示最多6个实验
|
||||||
if (resourceList && resourceList.length > 0) {
|
|
||||||
// 使用第一个封面资源
|
if (visibleExams.length === 0) {
|
||||||
const coverResource = resourceList[0];
|
console.warn("没有找到可见的实验");
|
||||||
const fileResponse = await resourceClient.getResourceById(coverResource.id);
|
return;
|
||||||
// 创建Blob URL作为缩略图
|
}
|
||||||
thumbnail = URL.createObjectURL(fileResponse.data);
|
|
||||||
}
|
// 转换数据格式并获取封面图片
|
||||||
} catch (error) {
|
const tutorialPromises = visibleExams.map(async (exam) => {
|
||||||
console.warn(`无法获取实验${exam.id}的封面图片:`, error);
|
let thumbnail: string | undefined;
|
||||||
}
|
|
||||||
|
try {
|
||||||
return {
|
// 获取实验的封面资源(模板资源)
|
||||||
id: exam.id,
|
const resourceClient = AuthManager.createAuthenticatedResourceClient();
|
||||||
title: exam.name,
|
const resourceList = await resourceClient.getResourceList(
|
||||||
description: '点击查看实验详情',
|
exam.id,
|
||||||
thumbnail,
|
"cover",
|
||||||
tags: exam.tags || []
|
"template",
|
||||||
};
|
);
|
||||||
});
|
if (resourceList && resourceList.length > 0) {
|
||||||
|
// 使用第一个封面资源
|
||||||
tutorials.value = await Promise.all(tutorialPromises);
|
const coverResource = resourceList[0];
|
||||||
|
const fileResponse = await resourceClient.getResourceById(
|
||||||
console.log('成功加载实验数据:', tutorials.value.length, '个实验');
|
coverResource.id,
|
||||||
|
);
|
||||||
// 启动自动旋转
|
// 创建Blob URL作为缩略图
|
||||||
startAutoRotation();
|
thumbnail = URL.createObjectURL(fileResponse.data);
|
||||||
} catch (error) {
|
}
|
||||||
console.error('加载实验数据失败:', error);
|
} catch (error) {
|
||||||
|
console.warn(`无法获取实验${exam.id}的封面图片:`, error);
|
||||||
// 如果加载失败,显示默认的占位内容
|
}
|
||||||
tutorials.value = [{
|
|
||||||
id: 'placeholder',
|
return {
|
||||||
title: '实验数据加载中...',
|
id: exam.id,
|
||||||
description: '请稍后或刷新页面重试',
|
title: exam.name,
|
||||||
thumbnail: undefined,
|
description: "点击查看实验详情",
|
||||||
tags: []
|
thumbnail,
|
||||||
}];
|
tags: exam.tags || [],
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 在组件销毁时清除计时器和Blob URLs
|
tutorials.value = await Promise.all(tutorialPromises);
|
||||||
onUnmounted(() => {
|
|
||||||
if (autoRotationTimer) {
|
console.log("成功加载实验数据:", tutorials.value.length, "个实验");
|
||||||
clearInterval(autoRotationTimer);
|
|
||||||
}
|
// 启动自动旋转
|
||||||
|
startAutoRotation();
|
||||||
// 清理创建的Blob URLs
|
} catch (error) {
|
||||||
tutorials.value.forEach(tutorial => {
|
console.error("加载实验数据失败:", error);
|
||||||
if (tutorial.thumbnail && tutorial.thumbnail.startsWith('blob:')) {
|
|
||||||
URL.revokeObjectURL(tutorial.thumbnail);
|
// 如果加载失败,显示默认的占位内容
|
||||||
}
|
tutorials.value = [
|
||||||
});
|
{
|
||||||
});
|
id: "placeholder",
|
||||||
|
title: "实验数据加载中...",
|
||||||
// 鼠标滚轮处理
|
description: "请稍后或刷新页面重试",
|
||||||
const handleWheel = (event: WheelEvent) => {
|
thumbnail: undefined,
|
||||||
if (event.deltaY > 0) {
|
tags: [],
|
||||||
nextCard();
|
},
|
||||||
} else {
|
];
|
||||||
prevCard();
|
}
|
||||||
}
|
});
|
||||||
};
|
|
||||||
|
// 在组件销毁时清除计时器和Blob URLs
|
||||||
// 下一张卡片
|
onUnmounted(() => {
|
||||||
const nextCard = () => {
|
if (autoRotationTimer) {
|
||||||
currentIndex.value = (currentIndex.value + 1) % tutorials.value.length;
|
clearInterval(autoRotationTimer);
|
||||||
};
|
}
|
||||||
|
|
||||||
// 上一张卡片
|
// 清理创建的Blob URLs
|
||||||
const prevCard = () => {
|
tutorials.value.forEach((tutorial) => {
|
||||||
currentIndex.value = (currentIndex.value - 1 + tutorials.value.length) % tutorials.value.length;
|
if (tutorial.thumbnail && tutorial.thumbnail.startsWith("blob:")) {
|
||||||
};
|
URL.revokeObjectURL(tutorial.thumbnail);
|
||||||
|
}
|
||||||
// 设置活动卡片
|
});
|
||||||
const setActiveCard = (index: number) => {
|
});
|
||||||
currentIndex.value = index;
|
|
||||||
};
|
// 鼠标滚轮处理
|
||||||
|
const handleWheel = (event: WheelEvent) => {
|
||||||
// 自动旋转
|
if (event.deltaY > 0) {
|
||||||
const startAutoRotation = () => {
|
nextCard();
|
||||||
autoRotationTimer = window.setInterval(() => {
|
} else {
|
||||||
nextCard();
|
prevCard();
|
||||||
}, autoRotationInterval);
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 暂停自动旋转
|
// 下一张卡片
|
||||||
const pauseAutoRotation = () => {
|
const nextCard = () => {
|
||||||
if (autoRotationTimer) {
|
currentIndex.value = (currentIndex.value + 1) % tutorials.value.length;
|
||||||
clearInterval(autoRotationTimer);
|
};
|
||||||
autoRotationTimer = null;
|
|
||||||
}
|
// 上一张卡片
|
||||||
};
|
const prevCard = () => {
|
||||||
|
currentIndex.value =
|
||||||
// 恢复自动旋转
|
(currentIndex.value - 1 + tutorials.value.length) % tutorials.value.length;
|
||||||
const resumeAutoRotation = () => {
|
};
|
||||||
if (!autoRotationTimer) {
|
|
||||||
startAutoRotation();
|
// 设置活动卡片
|
||||||
}
|
const setActiveCard = (index: number) => {
|
||||||
};
|
currentIndex.value = index;
|
||||||
|
};
|
||||||
// 前往实验
|
|
||||||
const goToExam = (examId: string) => {
|
// 自动旋转
|
||||||
// 跳转到实验列表页面并传递examId参数,页面将自动打开对应的实验详情模态框
|
const startAutoRotation = () => {
|
||||||
router.push({
|
autoRotationTimer = window.setInterval(() => {
|
||||||
path: '/exam',
|
nextCard();
|
||||||
query: { examId: examId }
|
}, autoRotationInterval);
|
||||||
});
|
};
|
||||||
};
|
|
||||||
|
// 暂停自动旋转
|
||||||
// 计算卡片类和样式
|
const pauseAutoRotation = () => {
|
||||||
const getCardClass = (index: number) => {
|
if (autoRotationTimer) {
|
||||||
const isActive = index === currentIndex.value;
|
clearInterval(autoRotationTimer);
|
||||||
const isPrev = (index === currentIndex.value - 1) || (currentIndex.value === 0 && index === tutorials.value.length - 1);
|
autoRotationTimer = null;
|
||||||
const isNext = (index === currentIndex.value + 1) || (currentIndex.value === tutorials.value.length - 1 && index === 0);
|
}
|
||||||
|
};
|
||||||
return {
|
|
||||||
'z-30': isActive,
|
// 恢复自动旋转
|
||||||
'z-20': isPrev || isNext,
|
const resumeAutoRotation = () => {
|
||||||
'z-10': !isActive && !isPrev && !isNext,
|
if (!autoRotationTimer) {
|
||||||
'hover:scale-105': isActive,
|
startAutoRotation();
|
||||||
'cursor-pointer': true
|
}
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
// 前往实验
|
||||||
const getCardStyle = (index: number) => {
|
const goToExam = (examId: string) => {
|
||||||
const isActive = index === currentIndex.value;
|
// 跳转到实验列表页面并传递examId参数,页面将自动打开对应的实验详情模态框
|
||||||
const isPrev = (index === currentIndex.value - 1) || (currentIndex.value === 0 && index === tutorials.value.length - 1);
|
router.push({
|
||||||
const isNext = (index === currentIndex.value + 1) || (currentIndex.value === tutorials.value.length - 1 && index === 0);
|
path: "/exam",
|
||||||
|
query: { examId: examId },
|
||||||
// 基本样式
|
});
|
||||||
let style = {
|
};
|
||||||
transform: 'scale(1) translateY(0) rotate(0deg)',
|
|
||||||
opacity: '1',
|
// 计算卡片类和样式
|
||||||
filter: 'blur(0)'
|
const getCardClass = (index: number) => {
|
||||||
};
|
const isActive = index === currentIndex.value;
|
||||||
|
const isPrev =
|
||||||
// 活动卡片
|
index === currentIndex.value - 1 ||
|
||||||
if (isActive) {
|
(currentIndex.value === 0 && index === tutorials.value.length - 1);
|
||||||
return style;
|
const isNext =
|
||||||
}
|
index === currentIndex.value + 1 ||
|
||||||
|
(currentIndex.value === tutorials.value.length - 1 && index === 0);
|
||||||
// 上一张卡片
|
|
||||||
if (isPrev) {
|
return {
|
||||||
style.transform = 'scale(0.85) translateY(-10%) rotate(-5deg)';
|
"z-30": isActive,
|
||||||
style.opacity = '0.7';
|
"z-20": isPrev || isNext,
|
||||||
style.filter = 'blur(1px)';
|
"z-10": !isActive && !isPrev && !isNext,
|
||||||
return style;
|
"hover:scale-105": isActive,
|
||||||
}
|
"cursor-pointer": true,
|
||||||
|
};
|
||||||
// 下一张卡片
|
};
|
||||||
if (isNext) {
|
|
||||||
style.transform = 'scale(0.85) translateY(10%) rotate(5deg)';
|
const getCardStyle = (index: number) => {
|
||||||
style.opacity = '0.7';
|
const isActive = index === currentIndex.value;
|
||||||
style.filter = 'blur(1px)';
|
const isPrev =
|
||||||
return style;
|
index === currentIndex.value - 1 ||
|
||||||
}
|
(currentIndex.value === 0 && index === tutorials.value.length - 1);
|
||||||
|
const isNext =
|
||||||
// 其他卡片
|
index === currentIndex.value + 1 ||
|
||||||
style.transform = 'scale(0.7) translateY(0) rotate(0deg)';
|
(currentIndex.value === tutorials.value.length - 1 && index === 0);
|
||||||
style.opacity = '0.4';
|
|
||||||
style.filter = 'blur(2px)';
|
// 基本样式
|
||||||
return style;
|
let style = {
|
||||||
}
|
transform: "scale(1) translateY(0) rotate(0deg)",
|
||||||
</script>
|
opacity: "1",
|
||||||
|
filter: "blur(0)",
|
||||||
<style scoped>
|
};
|
||||||
.tutorial-carousel {
|
|
||||||
width: 100%;
|
// 活动卡片
|
||||||
height: 500px;
|
if (isActive) {
|
||||||
perspective: 1000px;
|
return style;
|
||||||
display: flex;
|
}
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
// 上一张卡片
|
||||||
}
|
if (isPrev) {
|
||||||
|
style.transform = "scale(0.85) translateY(-10%) rotate(-5deg)";
|
||||||
.card-stack {
|
style.opacity = "0.7";
|
||||||
width: 600px;
|
style.filter = "blur(1px)";
|
||||||
height: 440px;
|
return style;
|
||||||
position: relative;
|
}
|
||||||
transform-style: preserve-3d;
|
|
||||||
}
|
// 下一张卡片
|
||||||
|
if (isNext) {
|
||||||
.tutorial-card {
|
style.transform = "scale(0.85) translateY(10%) rotate(5deg)";
|
||||||
width: 600px;
|
style.opacity = "0.7";
|
||||||
height: 400px;
|
style.filter = "blur(1px)";
|
||||||
background-color: hsl(var(--b2));
|
return style;
|
||||||
will-change: transform, opacity;
|
}
|
||||||
}
|
|
||||||
|
// 其他卡片
|
||||||
.tutorial-card:hover {
|
style.transform = "scale(0.7) translateY(0) rotate(0deg)";
|
||||||
box-shadow: 0 0 15px rgba(var(--p), 0.5);
|
style.opacity = "0.4";
|
||||||
}
|
style.filter = "blur(2px)";
|
||||||
</style>
|
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>
|
||||||
|
|
|
@ -129,7 +129,7 @@ export const useEquipments = defineStore("equipments", () => {
|
||||||
async function jtagUploadBitstream(
|
async function jtagUploadBitstream(
|
||||||
bitstream: File,
|
bitstream: File,
|
||||||
examId?: string,
|
examId?: string,
|
||||||
): Promise<number | null> {
|
): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
// 自动开启电源
|
// 自动开启电源
|
||||||
await powerSetOnOff(true);
|
await powerSetOnOff(true);
|
||||||
|
@ -155,7 +155,7 @@ export const useEquipments = defineStore("equipments", () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function jtagDownloadBitstream(bitstreamId?: number): Promise<string> {
|
async function jtagDownloadBitstream(bitstreamId?: string): Promise<string> {
|
||||||
if (bitstreamId === null || bitstreamId === undefined) {
|
if (bitstreamId === null || bitstreamId === undefined) {
|
||||||
dialog.error("请先选择要下载的比特流");
|
dialog.error("请先选择要下载的比特流");
|
||||||
return "";
|
return "";
|
||||||
|
|
|
@ -1,67 +1,73 @@
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from "vue";
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
// 本地存储主题的键名
|
// 本地存储主题的键名
|
||||||
const THEME_STORAGE_KEY = 'fpga-weblab-theme'
|
const THEME_STORAGE_KEY = "fpga-weblab-theme";
|
||||||
|
|
||||||
export const useThemeStore = defineStore('theme', () => {
|
export const useThemeStore = defineStore("theme", () => {
|
||||||
const allTheme = ["winter", "night"]
|
const allTheme = ["winter", "night"];
|
||||||
const darkTheme = "night";
|
const darkTheme = "night";
|
||||||
const lightTheme = "winter";
|
const lightTheme = "winter";
|
||||||
|
|
||||||
// 尝试从本地存储中获取保存的主题
|
// 尝试从本地存储中获取保存的主题
|
||||||
const getSavedTheme = (): string | null => {
|
const getSavedTheme = (): string | null => {
|
||||||
return localStorage.getItem(THEME_STORAGE_KEY)
|
return localStorage.getItem(THEME_STORAGE_KEY);
|
||||||
}
|
};
|
||||||
|
|
||||||
// 检测系统主题偏好
|
// 检测系统主题偏好
|
||||||
const getPreferredTheme = (): string => {
|
const getPreferredTheme = (): string => {
|
||||||
const savedTheme = getSavedTheme()
|
const savedTheme = getSavedTheme();
|
||||||
// 如果有保存的主题设置,优先使用
|
// 如果有保存的主题设置,优先使用
|
||||||
if (savedTheme && allTheme.includes(savedTheme)) {
|
if (savedTheme && allTheme.includes(savedTheme)) {
|
||||||
return savedTheme
|
return savedTheme;
|
||||||
}
|
}
|
||||||
// 否则检测系统主题模式
|
// 否则检测系统主题模式
|
||||||
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
return window.matchMedia &&
|
||||||
? darkTheme : lightTheme
|
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
}
|
? darkTheme
|
||||||
|
: lightTheme;
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化主题为首选主题
|
// 初始化主题为首选主题
|
||||||
const currentTheme = ref(getPreferredTheme())
|
const currentTheme = ref(getPreferredTheme());
|
||||||
|
const currentMode = computed(() =>
|
||||||
|
currentTheme.value === darkTheme ? "dark" : "light",
|
||||||
|
);
|
||||||
|
|
||||||
// 保存主题到本地存储
|
// 保存主题到本地存储
|
||||||
const saveTheme = (theme: string) => {
|
const saveTheme = (theme: string) => {
|
||||||
localStorage.setItem(THEME_STORAGE_KEY, theme)
|
localStorage.setItem(THEME_STORAGE_KEY, theme);
|
||||||
}
|
};
|
||||||
|
|
||||||
// 当主题变化时,保存到本地存储
|
// 当主题变化时,保存到本地存储
|
||||||
watch(currentTheme, (newTheme) => {
|
watch(currentTheme, (newTheme) => {
|
||||||
saveTheme(newTheme)
|
saveTheme(newTheme);
|
||||||
})
|
});
|
||||||
|
|
||||||
// 添加系统主题变化的监听
|
// 添加系统主题变化的监听
|
||||||
const setupThemeListener = () => {
|
const setupThemeListener = () => {
|
||||||
if (window.matchMedia) {
|
if (window.matchMedia) {
|
||||||
const colorSchemeQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
const colorSchemeQuery = window.matchMedia(
|
||||||
|
"(prefers-color-scheme: dark)",
|
||||||
|
);
|
||||||
const handler = (e: MediaQueryListEvent) => {
|
const handler = (e: MediaQueryListEvent) => {
|
||||||
// 只有当用户没有手动设置过主题时,才跟随系统变化
|
// 只有当用户没有手动设置过主题时,才跟随系统变化
|
||||||
if (!getSavedTheme()) {
|
if (!getSavedTheme()) {
|
||||||
currentTheme.value = e.matches ? darkTheme : lightTheme
|
currentTheme.value = e.matches ? darkTheme : lightTheme;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// 添加主题变化监听器
|
// 添加主题变化监听器
|
||||||
colorSchemeQuery.addEventListener('change', handler)
|
colorSchemeQuery.addEventListener("change", handler);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
function setTheme(theme: string) {
|
function setTheme(theme: string) {
|
||||||
const isContained: boolean = allTheme.includes(theme)
|
const isContained: boolean = allTheme.includes(theme);
|
||||||
if (isContained) {
|
if (isContained) {
|
||||||
currentTheme.value = theme
|
currentTheme.value = theme;
|
||||||
saveTheme(theme) // 保存主题到本地存储
|
saveTheme(theme); // 保存主题到本地存储
|
||||||
}
|
} else {
|
||||||
else {
|
console.error(`Not have such theme: ${theme}`);
|
||||||
console.error(`Not have such theme: ${theme}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,26 +83,26 @@ export const useThemeStore = defineStore('theme', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDarkTheme(): boolean {
|
function isDarkTheme(): boolean {
|
||||||
return currentTheme.value == darkTheme
|
return currentTheme.value == darkTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLightTheme(): boolean {
|
function isLightTheme(): boolean {
|
||||||
return currentTheme.value == lightTheme
|
return currentTheme.value == lightTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化时设置系统主题变化监听器
|
// 初始化时设置系统主题变化监听器
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
setupThemeListener()
|
setupThemeListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
allTheme,
|
allTheme,
|
||||||
currentTheme,
|
currentTheme,
|
||||||
|
currentMode,
|
||||||
setTheme,
|
setTheme,
|
||||||
toggleTheme,
|
toggleTheme,
|
||||||
isDarkTheme,
|
isDarkTheme,
|
||||||
isLightTheme,
|
isLightTheme,
|
||||||
setupThemeListener
|
setupThemeListener,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue