add: home select exp
This commit is contained in:
@@ -1,26 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { marked } from 'marked';
|
||||
import hljs from 'highlight.js';
|
||||
// 导入默认样式 - 选择一个适合你的主题
|
||||
import 'highlight.js/styles/github-dark.css';
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
removeFirstH1: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const renderedContent = computed(() => {
|
||||
if (!props.content) return '<p>没有内容</p>';
|
||||
|
||||
let processedContent = props.content;
|
||||
// 设置 marked 选项
|
||||
|
||||
// 如果需要,移除第一个一级标题
|
||||
if (props.removeFirstH1) {
|
||||
const lines = processedContent.split('\n');
|
||||
const firstH1Index = lines.findIndex(line => line.startsWith('# '));
|
||||
|
||||
if (firstH1Index !== -1) {
|
||||
processedContent = lines.slice(firstH1Index + 1).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// 创建自定义渲染器
|
||||
const renderer = new marked.Renderer();
|
||||
marked.setOptions({
|
||||
|
||||
// 重写代码块渲染方法,添加语言信息
|
||||
renderer.code = (code, incomingLanguage) => {
|
||||
// 确保语言参数是字符串
|
||||
const language = incomingLanguage || 'plaintext';
|
||||
// 验证语言
|
||||
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext';
|
||||
// 高亮代码
|
||||
const highlightedCode = hljs.highlight(code, { language: validLanguage }).value;
|
||||
|
||||
// 添加语言标签到代码块
|
||||
return `<pre class="hljs" data-language="${validLanguage}"><code class="language-${validLanguage}">${highlightedCode}</code></pre>`;
|
||||
};
|
||||
|
||||
// 设置 marked 选项
|
||||
marked.use({
|
||||
renderer: renderer,
|
||||
gfm: true,
|
||||
breaks: true
|
||||
});
|
||||
|
||||
return marked(processedContent);
|
||||
});
|
||||
|
||||
// 页面挂载后,对已渲染的代码块应用高亮效果
|
||||
onMounted(() => {
|
||||
// 如果需要在客户端重新高亮(通常不需要,因为我们已经在服务端高亮)
|
||||
// hljs.highlightAll();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -33,6 +74,8 @@ const renderedContent = computed(() => {
|
||||
line-height: 1.6;
|
||||
padding: 1rem 1.5rem;
|
||||
max-width: 100%;
|
||||
background-color: inherit; /* 继承父元素的背景色 */
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.markdown-content :deep(img) {
|
||||
@@ -45,47 +88,64 @@ const renderedContent = computed(() => {
|
||||
}
|
||||
|
||||
.markdown-content :deep(h1) {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 2.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: hsl(var(--bc));
|
||||
font-weight: 700;
|
||||
font-size: 2rem;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1.3;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid hsl(var(--b2));
|
||||
padding-bottom: 0.7rem;
|
||||
border-bottom: 2px solid hsl(var(--p) / 0.7);
|
||||
text-shadow: 1px 1px 2px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.markdown-content :deep(h2) {
|
||||
margin-top: 1.8rem;
|
||||
margin-bottom: 0.8rem;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: hsl(var(--bc));
|
||||
font-weight: 600;
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.7rem;
|
||||
line-height: 1.4;
|
||||
padding-left: 0.5rem;
|
||||
border-left: 4px solid hsl(var(--p));
|
||||
padding: 0.5rem 1rem;
|
||||
border-left: 5px solid hsl(var(--p));
|
||||
background: linear-gradient(to right, hsl(var(--b2) / 0.5), transparent);
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
|
||||
.markdown-content :deep(h3) {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-top: 1.8rem;
|
||||
margin-bottom: 0.9rem;
|
||||
color: hsl(var(--bc));
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.4;
|
||||
padding-left: 1rem;
|
||||
border-left: 3px solid hsl(var(--s));
|
||||
padding-top: 0.3rem;
|
||||
padding-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.markdown-content :deep(h4),
|
||||
.markdown-content :deep(h5),
|
||||
.markdown-content :deep(h6) {
|
||||
margin-top: 1.2rem;
|
||||
margin-bottom: 0.6rem;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.7rem;
|
||||
color: hsl(var(--bc));
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.5;
|
||||
padding-left: 1.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.markdown-content :deep(h4::before),
|
||||
.markdown-content :deep(h5::before),
|
||||
.markdown-content :deep(h6::before) {
|
||||
content: '▶';
|
||||
color: hsl(var(--p) / 0.7);
|
||||
position: absolute;
|
||||
left: 0.2rem;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.markdown-content :deep(p) {
|
||||
@@ -97,21 +157,31 @@ const renderedContent = computed(() => {
|
||||
|
||||
.markdown-content :deep(ul),
|
||||
.markdown-content :deep(ol) {
|
||||
padding-left: 2em;
|
||||
margin: 0.75rem 0;
|
||||
padding-left: 2.5em;
|
||||
margin: 1.25rem 0;
|
||||
color: hsl(var(--bc) / 0.8);
|
||||
background-color: hsl(var(--b1) / 0.3);
|
||||
border-radius: 0.5rem;
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
padding-right: 1rem;
|
||||
border-left: 3px solid hsl(var(--p) / 0.7);
|
||||
}
|
||||
|
||||
.markdown-content :deep(li) {
|
||||
margin: 0.4rem 0;
|
||||
margin: 0.5rem 0;
|
||||
position: relative;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.markdown-content :deep(ul ul),
|
||||
.markdown-content :deep(ul ol),
|
||||
.markdown-content :deep(ol ul),
|
||||
.markdown-content :deep(ol ol) {
|
||||
margin: 0.4rem 0 0.4rem 1rem;
|
||||
margin: 0.5rem 0 0.5rem 0.5rem;
|
||||
padding-left: 1.5rem;
|
||||
border-left: 2px solid hsl(var(--s) / 0.5);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.markdown-content :deep(ul) {
|
||||
@@ -134,45 +204,142 @@ const renderedContent = computed(() => {
|
||||
color: hsl(var(--p));
|
||||
}
|
||||
|
||||
.markdown-content :deep(ol li::marker) {
|
||||
color: hsl(var(--s));
|
||||
}
|
||||
|
||||
/* 代码块样式增强 */
|
||||
.markdown-content :deep(pre) {
|
||||
background-color: hsl(var(--b3));
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
border: 1px solid hsl(var(--b2));
|
||||
margin: 1rem 0;
|
||||
margin: 1.5rem 0;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.markdown-content :deep(pre::before) {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, hsl(var(--p)), hsl(var(--s)));
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
}
|
||||
|
||||
/* 代码语言标签 */
|
||||
.markdown-content :deep(pre.hljs::after) {
|
||||
content: attr(class);
|
||||
content: attr(data-language);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: hsl(var(--bc) / 0.7);
|
||||
font-size: 0.75rem;
|
||||
background-color: hsl(var(--b2));
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 0 0.3rem 0 0.3rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 内联代码样式 */
|
||||
.markdown-content :deep(code) {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
background-color: hsl(var(--b3));
|
||||
padding: 2px 0.5rem;
|
||||
background-color: hsl(var(--b3) / 0.7);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.9em;
|
||||
color: hsl(var(--p));
|
||||
border: 1px solid hsl(var(--b2) / 0.5);
|
||||
}
|
||||
|
||||
/* 确保代码块内的代码不受内联代码样式影响 */
|
||||
.markdown-content :deep(pre code) {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border: none;
|
||||
color: inherit;
|
||||
font-size: 0.95em;
|
||||
line-height: 1.5;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
/* 为常见语言添加一些特殊的高亮效果 */
|
||||
.markdown-content :deep(.hljs-keyword),
|
||||
.markdown-content :deep(.hljs-tag),
|
||||
.markdown-content :deep(.hljs-selector-tag) {
|
||||
color: #cc99cd; /* 紫色,用于关键字 */
|
||||
}
|
||||
|
||||
.markdown-content :deep(.hljs-string),
|
||||
.markdown-content :deep(.hljs-regexp),
|
||||
.markdown-content :deep(.hljs-template-tag) {
|
||||
color: #7ec699; /* 绿色,用于字符串 */
|
||||
}
|
||||
|
||||
.markdown-content :deep(.hljs-number),
|
||||
.markdown-content :deep(.hljs-literal) {
|
||||
color: #f08d49; /* 橙色,用于数字 */
|
||||
}
|
||||
|
||||
.markdown-content :deep(.hljs-comment) {
|
||||
color: #999999; /* 灰色,用于注释 */
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.markdown-content :deep(.hljs-name),
|
||||
.markdown-content :deep(.hljs-attribute),
|
||||
.markdown-content :deep(.hljs-selector-id),
|
||||
.markdown-content :deep(.hljs-selector-class) {
|
||||
color: #e2777a; /* 红色,用于HTML标签名和属性 */
|
||||
}
|
||||
|
||||
.markdown-content :deep(.hljs-built_in),
|
||||
.markdown-content :deep(.hljs-builtin-name) {
|
||||
color: #6196cc; /* 蓝色,用于内置函数 */
|
||||
}
|
||||
|
||||
.markdown-content :deep(.hljs-title),
|
||||
.markdown-content :deep(.hljs-function) {
|
||||
color: #f8c555; /* 金色,用于函数名和类名 */
|
||||
}
|
||||
|
||||
.markdown-content :deep(table) {
|
||||
border-collapse: collapse;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
margin: 1rem 0;
|
||||
margin: 1.5rem 0;
|
||||
background-color: hsl(var(--b1));
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
border: 1px solid hsl(var(--b2));
|
||||
}
|
||||
|
||||
.markdown-content :deep(th),
|
||||
.markdown-content :deep(td) {
|
||||
border: 1px solid hsl(var(--b2));
|
||||
padding: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-content :deep(th) {
|
||||
background-color: hsl(var(--b2));
|
||||
font-weight: 500;
|
||||
background-color: hsl(var(--p) / 0.15);
|
||||
font-weight: 600;
|
||||
color: hsl(var(--bc));
|
||||
border-bottom: 2px solid hsl(var(--p) / 0.5);
|
||||
}
|
||||
|
||||
.markdown-content :deep(tr:nth-child(even)) {
|
||||
background-color: hsl(var(--b2) / 0.3);
|
||||
}
|
||||
|
||||
.markdown-content :deep(tr:hover) {
|
||||
background-color: hsl(var(--b2) / 0.5);
|
||||
}
|
||||
|
||||
.markdown-content :deep(td) {
|
||||
@@ -180,12 +347,29 @@ const renderedContent = computed(() => {
|
||||
}
|
||||
|
||||
.markdown-content :deep(blockquote) {
|
||||
margin: 1rem 0;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 1.5rem 0;
|
||||
padding: 1rem 1.5rem;
|
||||
border-left: 4px solid hsl(var(--p));
|
||||
background-color: hsl(var(--b2));
|
||||
color: hsl(var(--bc) / 0.8);
|
||||
background-color: hsl(var(--b2) / 0.3);
|
||||
color: hsl(var(--bc) / 0.9);
|
||||
font-style: italic;
|
||||
border-radius: 0 0.5rem 0.5rem 0;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.03);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.markdown-content :deep(blockquote::before) {
|
||||
content: '"';
|
||||
font-size: 2rem;
|
||||
color: hsl(var(--p) / 0.3);
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
font-family: serif;
|
||||
}
|
||||
|
||||
.markdown-content :deep(blockquote p) {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.markdown-content :deep(hr) {
|
||||
@@ -204,4 +388,16 @@ const renderedContent = computed(() => {
|
||||
color: hsl(var(--pf));
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 暗黑模式下的代码高亮调整 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.markdown-content :deep(pre) {
|
||||
background-color: hsl(var(--b3));
|
||||
border-color: hsl(var(--b1) / 0.7);
|
||||
}
|
||||
|
||||
.markdown-content :deep(code) {
|
||||
background-color: hsl(var(--b2) / 0.7);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -49,6 +49,19 @@
|
||||
测试功能
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="my-1 hover:translate-x-1 transition-all duration-300">
|
||||
<router-link to="/markdown-test" class="text-base font-medium">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-70" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||||
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||||
<polyline points="10 9 9 9 8 9"></polyline>
|
||||
</svg>
|
||||
Markdown测试
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="my-1 hover:translate-x-1 transition-all duration-300">
|
||||
<a href="http://localhost:5000/swagger" target="_self" rel="noopener noreferrer"
|
||||
class="text-base font-medium">
|
||||
|
||||
302
src/components/TutorialCarousel.vue
Normal file
302
src/components/TutorialCarousel.vue
Normal file
@@ -0,0 +1,302 @@
|
||||
<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) {
|
||||
tutorialIds = ['01', '02', '11']; // 默认例程
|
||||
}
|
||||
|
||||
// 为每个例程创建对象并尝试获取文档标题
|
||||
const tutorialPromises = tutorialIds.map(async (id) => {
|
||||
// 尝试读取doc.md获取标题
|
||||
let title = `例程 ${id}`;
|
||||
let description = "点击加载此例程";
|
||||
let thumbnail = `/doc/${id}/images/1.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>
|
||||
Reference in New Issue
Block a user