add: home select exp

This commit is contained in:
alivender
2025-05-20 09:35:29 +08:00
parent 8eefed92a8
commit 6d640e8049
27 changed files with 1595 additions and 77 deletions

View File

@@ -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>

View File

@@ -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">

View 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>

View File

@@ -1,23 +1,57 @@
import { createWebHistory, createRouter } from "vue-router";
import LoginView from "../views/LoginView.vue";
import UserView from "../views/UserView.vue";
import TestView from "../views/TestView.vue";
import ProjectView from "../views/ProjectView.vue";
import HomeView from "@/views/HomeView.vue";
import AdminView from "@/views/AdminView.vue";
const routes = [
{ path: "/", name: "Home", component: HomeView },
{ path: "/login", name: "Login", component: LoginView },
{ path: "/user", name: "User", component: UserView },
{ path: "/test", name: "Test", component: TestView },
{ path: "/project", name: "Project", component: ProjectView },
{ path: "/admin", name: "Admin", component: AdminView },
];
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue'
import LabView from '../views/LabView.vue'
import ProjectView from '../views/ProjectView.vue'
import TestView from '../views/TestView.vue'
import UserView from '../views/UserView.vue'
import AdminView from '../views/AdminView.vue'
import MarkdownTestView from '../views/MarkdownTestView.vue'
const router = createRouter({
history: createWebHistory(),
routes,
});
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/login',
name: 'login',
component: LoginView
},
{
path: '/lab/:id',
name: 'lab',
component: LabView
},
{
path: '/project',
name: 'project',
component: ProjectView
},
{
path: '/test',
name: 'test',
component: TestView
},
{
path: '/markdown-test',
name: 'markdown-test',
component: MarkdownTestView
},
{
path: '/user',
name: 'user',
component: UserView
},
{
path: '/admin',
name: 'admin',
component: AdminView
}
]
})
export default router;
export default router

View File

@@ -1,14 +1,9 @@
<template>
<div class="bg-base-200 min-h-screen">
<main class="hero min-h-screen bg-base-200">
<div class="hero-content flex-col lg:flex-row-reverse gap-8 lg:gap-12 py-10 px-4">
<!-- 图片容器 -->
<div
class="image-container relative w-full max-w-sm hover:scale-105 hover:-rotate-1 transition-transform duration-500 ease-in-out">
<img src="https://placehold.co/600x400"
class="w-full rounded-2xl shadow-2xl border-4 border-base-300 transition-shadow duration-300 hover:shadow-primary" />
<!-- 这里使用relative定位限制覆盖层只在图片容器内 -->
<div class="absolute inset-0 bg-primary opacity-10 rounded-2xl pointer-events-none"></div>
<div class="hero-content flex-col xl:flex-row-reverse gap-8 xl:gap-12 py-10 px-4"> <!-- 例程轮播容器 -->
<div class="w-full flex justify-center" style="min-width: 650px;">
<TutorialCarousel :autoRotationInterval="3000" />
</div>
<!-- 内容容器 -->
<div class="content-container max-w-md lg:max-w-2xl transform transition-all duration-500 ease-in-out">
@@ -93,6 +88,7 @@
<script lang="ts" setup>
import "@/router";
import TutorialCarousel from "@/components/TutorialCarousel.vue";
</script>
<style scoped lang="postcss">

View File

@@ -0,0 +1,445 @@
<template>
<div class="p-8 max-w-5xl mx-auto">
<h1 class="text-2xl font-bold mb-6">Markdown 渲染器语法高亮测试</h1>
<div class="grid grid-cols-1 gap-8">
<!-- 测试控制面板 -->
<div class="bg-base-200 p-4 rounded-lg">
<div class="flex gap-4 mb-4">
<button @click="currentTheme = 'light'" class="btn btn-primary" :class="{'btn-outline': currentTheme !== 'light'}">
亮色主题
</button>
<button @click="currentTheme = 'dark'" class="btn btn-primary" :class="{'btn-outline': currentTheme !== 'dark'}">
暗色主题
</button>
</div>
</div>
<!-- 示例展示 -->
<div class="bg-base-100 rounded-lg shadow-lg overflow-hidden" :data-theme="currentTheme">
<div class="p-4 bg-base-200 font-semibold">Markdown 渲染结果</div>
<div class="px-1">
<MarkdownRenderer :content="sampleContent" :remove-first-h1="false" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MarkdownRenderer from '@/components/MarkdownRenderer.vue';
const currentTheme = ref('light');
// 包含各种代码示例的 Markdown 示例内容
const sampleContent = ref(`
# Markdown 语法高亮测试
这是一个用于测试 Markdown 渲染器语法高亮功能的页面。下面是一些代码示例。
## JavaScript 代码示例
\`\`\`javascript
// 一个简单的 JavaScript 函数
function calculateSum(a, b) {
// 这是一个注释
const sum = a + b;
console.log(\`计算结果: \${sum}\`);
return sum;
}
// 类示例
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return \`你好,我是 \${this.name},我 \${this.age} 岁了。\`;
}
}
// 使用 async/await
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
} catch (error) {
console.error('获取数据出错:', error);
}
}
\`\`\`
## TypeScript 代码示例
\`\`\`typescript
// TypeScript 接口
interface User {
id: number;
name: string;
email: string;
age?: number;
}
// 泛型函数
function getFirst<T>(array: T[]): T | undefined {
return array.length > 0 ? array[0] : undefined;
}
// 类型别名和联合类型
type Result<T> =
| { success: true; value: T }
| { success: false; error: Error };
// 装饰器
function log(target: any, key: string) {
const originalMethod = target[key];
target[key] = function(...args: any[]) {
console.log(\`调用方法 \${key} 参数:, args);
return originalMethod.apply(this, args);
};
return target;
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
\`\`\`
## HTML & CSS 示例
\`\`\`html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>示例页面</title>
<style>
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.card {
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.card-header {
background-color: #4a6cf7;
color: white;
padding: 15px;
}
.card-body {
padding: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<div class="card-header">
<h2>欢迎使用我们的应用</h2>
</div>
<div class="card-body">
<p>这是卡片内容区域。</p>
<button onclick="alert('你点击了按钮!')">点击我</button>
</div>
</div>
</div>
</body>
</html>
\`\`\`
## C# 代码示例
\`\`\`csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Demo
{
// 简单的用户类
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public override string ToString()
{
return $"User(Id={Id}, Name={Name}, Email={Email})";
}
}
public class Program
{
public static async Task<List<User>> GetUsersAsync()
{
// 模拟异步操作
await Task.Delay(1000);
return new List<User>
{
new User { Id = 1, Name = "张三", Email = "zhangsan@example.com" },
new User { Id = 2, Name = "李四", Email = "lisi@example.com" },
new User { Id = 3, Name = "王五", Email = "wangwu@example.com" }
};
}
public static void Main(string[] args)
{
Console.WriteLine("获取用户列表中...");
var users = GetUsersAsync().GetAwaiter().GetResult();
foreach (var user in users)
{
Console.WriteLine(user);
}
Console.WriteLine("完成!");
}
}
}
\`\`\`
## Python 代码示例
\`\`\`python
import os
import json
from typing import List, Dict, Any, Optional
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
email: Optional[str] = None
def greet(self) -> str:
return f"你好,我是 {self.name},我 {self.age} 岁了。"
def to_dict(self) -> Dict[str, Any]:
return {
"name": self.name,
"age": self.age,
"email": self.email
}
# 一些常用的 Python 函数
def read_json_file(file_path: str) -> Dict[str, Any]:
"""从JSON文件读取数据
Args:
file_path: JSON文件路径
Returns:
解析后的JSON数据
Raises:
FileNotFoundError: 如果文件不存在
json.JSONDecodeError: 如果JSON格式不正确
"""
if not os.path.exists(file_path):
raise FileNotFoundError(f"文件不存在: {file_path}")
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
# 列表推导式与生成器
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_numbers = [n for n in numbers if n % 2 == 0]
squared = (n**2 for n in even_numbers) # 生成器表达式
# 使用装饰器
def log_function_call(func):
def wrapper(*args, **kwargs):
print(f"调用函数: {func.__name__}")
result = func(*args, **kwargs)
print(f"函数返回: {result}")
return result
return wrapper
@log_function_call
def add(a: int, b: int) -> int:
return a + b
if __name__ == "__main__":
person = Person("张三", 30, "zhangsan@example.com")
print(person.greet())
print(add(5, 3))
\`\`\`
## VHDL 代码示例
\`\`\`vhdl
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity Counter is
Port (
clk : in STD_LOGIC;
reset : in STD_LOGIC;
enable : in STD_LOGIC;
count : out STD_LOGIC_VECTOR(7 downto 0)
);
end Counter;
architecture Behavioral of Counter is
signal count_reg : unsigned(7 downto 0) := (others => '0');
begin
process(clk, reset)
begin
if reset = '1' then
count_reg <= (others => '0');
elsif rising_edge(clk) then
if enable = '1' then
count_reg <= count_reg + 1;
end if;
end if;
end process;
count <= STD_LOGIC_VECTOR(count_reg);
end Behavioral;
\`\`\`
## Verilog 代码示例
\`\`\`verilog
module counter(
input wire clk,
input wire reset,
input wire enable,
output reg [7:0] count
);
// 初始化计数器
initial begin
count = 8'b0;
end
// 在时钟上升沿处理
always @(posedge clk or posedge reset) begin
if (reset) begin
count <= 8'b0;
end else if (enable) begin
count <= count + 1'b1;
end
end
// 显示当前计数值
always @(count) begin
$display("当前计数值: %d", count);
end
endmodule
\`\`\`
## JSON 示例
\`\`\`json
{
"name": "fpga-weblab",
"version": "1.0.0",
"description": "FPGA WebLab 项目配置",
"settings": {
"theme": {
"light": {
"primary": "#4a6cf7",
"secondary": "#f79e1b",
"background": "#ffffff"
},
"dark": {
"primary": "#6d8aff",
"secondary": "#ffb74d",
"background": "#121212"
}
},
"features": [
"代码高亮",
"实时预览",
"项目管理",
"远程硬件访问"
]
},
"dependencies": {
"vue": "^3.5.0",
"marked": "^12.0.0",
"highlight.js": "^11.9.0"
}
}
\`\`\`
## Shell 脚本示例
\`\`\`bash
#!/bin/bash
# 定义变量
PROJECT_DIR=$(pwd)
OUTPUT_DIR="$PROJECT_DIR/build"
LOG_FILE="$PROJECT_DIR/build.log"
# 创建输出目录
mkdir -p "$OUTPUT_DIR"
# 定义函数
function log_message() {
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
echo "[$timestamp] $1" | tee -a "$LOG_FILE"
}
# 清理旧构建
log_message "清理旧构建文件..."
rm -rf "$OUTPUT_DIR/*"
# 执行构建
log_message "开始构建项目..."
npm run build
# 检查构建结果
if [ $? -eq 0 ]; then
log_message "构建成功!输出文件位于: $OUTPUT_DIR"
else
log_message "构建失败,请检查错误信息"
exit 1
fi
# 统计文件数量
file_count=$(find "$OUTPUT_DIR" -type f | wc -l)
log_message "共构建了 $file_count 个文件"
# 显示环境信息
echo "系统信息:"
echo "----------------------"
echo "操作系统: $(uname -s)"
echo "Node 版本: $(node -v)"
echo "NPM 版本: $(npm -v)"
echo "磁盘空间: $(df -h | grep -E '^/dev')"
\`\`\`
## 其他示例
这里是一个内联代码示例:\`const value = calculate(x, y);\`
`);
</script>
<style scoped>
</style>

View File

@@ -26,9 +26,7 @@
<div
class="resizer bg-base-100 hover:bg-primary hover:opacity-70 active:bg-primary active:opacity-90 transition-colors"
@mousedown="startResize"
></div>
<!-- 右侧编辑区域 -->
></div> <!-- 右侧编辑区域 -->
<div
class="bg-base-200 h-full overflow-hidden flex flex-col"
:style="{ width: 100 - leftPanelWidth + '%' }"
@@ -41,10 +39,9 @@
:componentConfig="selectedComponentConfig"
@updateProp="updateComponentProp"
@updateDirectProp="updateComponentDirectProp"
/>
<div
/> <div
v-else
class="doc-panel overflow-y-auto bg-base-100 rounded-md h-full"
class="doc-panel overflow-y-auto h-full"
>
<MarkdownRenderer :content="documentContent" />
</div>
@@ -82,20 +79,57 @@ import {
const showDocPanel = ref(false);
const documentContent = ref("");
// 获取路由参数
import { useRoute } from 'vue-router';
const route = useRoute();
// 切换文档面板和属性面板
async function toggleDocPanel() {
showDocPanel.value = !showDocPanel.value;
// 如果切换到文档面板,则获取文档内容
if (showDocPanel.value) {
const response = await fetch("/doc/01_water_led/water_led.md");
documentContent.value = (await response.text()).replace(
/.\/images/gi,
"/doc/01_water_led/images",
);
await loadDocumentContent();
}
}
// 加载文档内容
async function loadDocumentContent() {
try {
// 从路由参数中获取教程ID
const tutorialId = route.query.tutorial as string || '02'; // 默认加载02例程
// 构建文档路径
let docPath = `/doc/${tutorialId}/doc.md`;
// 检查当前路径是否包含下划线(例如 02_key 格式)
// 如果不包含,那么使用更新的命名格式
if (!tutorialId.includes('_')) {
docPath = `/doc/${tutorialId}/doc.md`;
}
// 获取文档内容
const response = await fetch(docPath);
if (!response.ok) {
throw new Error(`Failed to load document: ${response.status}`);
}
// 更新文档内容,并替换图片路径
documentContent.value = (await response.text())
.replace(/.\/images/gi, `/doc/${tutorialId}/images`);
} catch (error) {
console.error('加载文档失败:', error);
documentContent.value = '# 文档加载失败\n\n无法加载请求的文档。'; }
}
// 检查是否有例程参数,如果有则自动打开文档面板
onMounted(async () => {
if (route.query.tutorial) {
showDocPanel.value = true;
await loadDocumentContent();
}
});
// --- 元器件管理 ---
const showComponentsMenu = ref(false);
const diagramData = ref<DiagramData>({
@@ -807,8 +841,10 @@ body {
/* 文档面板样式 */
.doc-panel {
padding: 1.5rem;
max-width: 800px;
margin: 0 auto;
max-width: 100%;
margin: 0;
background-color: transparent; /* 使用透明背景 */
border: none; /* 确保没有边框 */
}
/* 文档切换按钮样式 */