feat(docs): integrate version2 curriculum and stage-3 updates
概要
- 将 version2 分支的课程结构重构、第三阶段章节新增、示例资源迁移、高级 RAG 文档与 Vercel 部署配置等整合为 main 上的一次汇总提交
内容导航与 README 调整
- 更新 README 的总体介绍文案,引入“第零阶段 + 第一到第三阶段”的完整学习路径描述
- 将原先的“三阶段实战路径”说明替换为新版分阶段描述,突出从小游戏到跨平台复杂应用的学习节奏
- 删除已过时的“第二次更新将在分支 version2 合并到主分支”的提示,改为直接以 main 为主线
- 统一 README 顶部标题和排版风格,保证中英文导航、徽章展示等视觉结构一致
课程结构与章节导航更新
- 调整 docs 目录下的学习阶段导航结构,使 README 中的导航表与各 stage 实际目录对齐
- 补全并创建 stage-3 相关章节入口文件,用于承载高级阶段的课程内容
- 新增或更新以下章节入口:
- 高级核心技能:
- docs/stage-3/core-skills/3.1-mcp-claudecode-skills/index.md
- docs/stage-3/core-skills/3.2-long-running-tasks/index.md
- 多平台开发:
- docs/stage-3/cross-platform/3.3-wechat-miniprogram/index.md
- docs/stage-3/cross-platform/3.4-wechat-miniprogram-backend/index.md
- docs/stage-3/cross-platform/3.5-android-app/index.md
- docs/stage-3/cross-platform/3.6-ios-app/index.md
- 个人品牌:
- docs/stage-3/personal-brand/3.7-personal-website-blog/index.md
- 保持 stage-0、stage-1、stage-2 既有章节结构不变的前提下,对导航表格进行排版和链接校正,使整体课程地图清晰、可点击
示例与图片资源重组
- 将原先位于 docs/examples/example1/images/ 下的微信小程序示例图片,整体迁移到 stage-3 的正式课程路径中:
- 目标路径:docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/images/
- 通过 rename 方式保留 git 历史关系,避免图片资源被视为完全新增,从而方便后续追踪
- 为微信小程序示例新增 index 页面:
- docs/stage-3/3.3-how-to-build-a-wechat-miniprogram/example1/index.md
- 使该示例在“高级三:多平台开发:如何构建微信小程序”章节中有清晰的入口,对应实际实战内容
高级 RAG 与 AI 进阶文档
- 新增一篇系统介绍 RAG 的高级文档:
- docs/stage-3/ai-advanced/3.a1-rag-introduction/extra5-what-is-rag-and-how-does-it-work-and-future.md
- 覆盖内容包括:RAG 的基本概念、典型架构、工作流程以及未来演进方向,为第三阶段的复杂应用提供知识检索基础
- 配套引入多张插图,帮助读者从架构图和流程视角理解 RAG:
- docs/stage-3/ai-advanced/3.a1-rag-introduction/images/image1.png ~ image15.png
部署与工程配置
- 新增 vercel.json 配置文件,为项目在 Vercel 上的部署提供基础配置
- 明确文档构建产物的输出路径和静态站点托管方式
- 为之后的一键部署和自动化预览打下基础
依赖与锁文件更新
- 调整 package.json 中与新版文档结构和部署相关的配置,保持脚本和依赖与当前课程形态同步
- 更新 package-lock.json,以反映最新的依赖树和版本锁定状态
- 保证在执行 npm install / npm run build 时,依赖环境与 version2 中的实际使用情况一致
兼容性与行为说明
- 该提交通过 npm run build 验证,确保在整合 version2 内容后,VitePress 构建过程正常完成
- main 分支上的历史被压缩为一条有语义的“第二次大更新”提交,详细的开发过程仍保留在 version2 分支,用于后续需要时回溯
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
import { defineConfig } from 'vitepress'
|
||||
|
||||
export default defineConfig({
|
||||
base: '/',
|
||||
title: 'Easy-Vibe Tutorial',
|
||||
description: 'Easy-Vibe 中文实战课 - 零基础学会用 AI 干实际工作',
|
||||
head: [['link', { rel: 'icon', href: '/logo.png' }]],
|
||||
themeConfig: {
|
||||
logo: '/logo.png',
|
||||
search: {
|
||||
provider: 'local'
|
||||
},
|
||||
outline: {
|
||||
level: 'deep',
|
||||
label: '页面导航'
|
||||
},
|
||||
nav: [
|
||||
{ text: '首页', link: '/' },
|
||||
{ text: '纯新手', link: '/stage-0/0.1-learning-map/' },
|
||||
{ text: '产品经理', link: '/stage-1/1.1-introduction-to-ai-ide/' },
|
||||
{
|
||||
text: '初中级开发',
|
||||
link: '/stage-2/frontend/2.0-lovart-assets/'
|
||||
},
|
||||
{
|
||||
text: '高级开发',
|
||||
link: '/stage-3/core-skills/3.1-mcp-claudecode-skills/'
|
||||
},
|
||||
{ text: '附录', link: '/appendix/ai-capability-dictionary' }
|
||||
],
|
||||
sidebar: {
|
||||
'/stage-0/': [
|
||||
{ text: '0.1 学习地图', link: '/stage-0/0.1-learning-map/' },
|
||||
{
|
||||
text: '0.2 AI 时代,会说话就会编程',
|
||||
link: '/stage-0/0.2-ai-capabilities-through-games/'
|
||||
}
|
||||
],
|
||||
'/stage-1/': [
|
||||
{
|
||||
text: '1.1 认识 AI IDE 工具',
|
||||
link: '/stage-1/1.1-introduction-to-ai-ide/'
|
||||
},
|
||||
{
|
||||
text: '1.2 动手做出原型',
|
||||
link: '/stage-1/1.2-building-prototype/'
|
||||
},
|
||||
{
|
||||
text: '1.3 给原型加上 AI 能力',
|
||||
link: '/stage-1/1.3-integrating-ai-capabilities/'
|
||||
},
|
||||
{
|
||||
text: '1.4 完整项目实战',
|
||||
link: '/stage-1/1.4-complete-project-practice/'
|
||||
},
|
||||
{
|
||||
text: '1.5 大作业:完成一个 Web 应用原型',
|
||||
link: '/stage-1/1.5-final-project/'
|
||||
},
|
||||
{
|
||||
text: '附录 A:产品思维补充',
|
||||
link: '/stage-1/appendix-a-product-thinking/'
|
||||
},
|
||||
{
|
||||
text: '附录 B:常见报错及解决方案',
|
||||
link: '/stage-1/appendix-b-common-errors/'
|
||||
}
|
||||
],
|
||||
'/stage-2/': [
|
||||
{
|
||||
text: '前端开发',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{
|
||||
text: '前端零:使用 Lovart 生产素材',
|
||||
link: '/stage-2/frontend/2.0-lovart-assets/'
|
||||
},
|
||||
{
|
||||
text: '前端一:Figma 与 MasterGo 入门',
|
||||
link: '/stage-2/frontend/2.1-figma-mastergo/'
|
||||
},
|
||||
{
|
||||
text: '前端二:构建第一个现代应用程序 - UI 设计',
|
||||
link: '/stage-2/frontend/2.2-ui-design/'
|
||||
},
|
||||
{
|
||||
text: '前端三:参考 UI 设计规范与多产品 UI 设计',
|
||||
link: '/stage-2/frontend/2.3-multi-product-ui/'
|
||||
},
|
||||
{
|
||||
text: '前端四:一起做霍格沃茨画像',
|
||||
link: '/stage-2/frontend/2.4-hogwarts-portraits/chapter4-lets-build-hogwarts-portraits'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '后端与全栈',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{
|
||||
text: '后端一:什么是 API',
|
||||
link: '/stage-2/backend/2.1-what-is-api/extra2/extra2-what-is-api'
|
||||
},
|
||||
{
|
||||
text: '后端二:从数据库到 Supabase',
|
||||
link: '/stage-2/backend/2.2-database-supabase/chapter5/chapter5-from-database-to-supabase'
|
||||
},
|
||||
{
|
||||
text: '后端三:大模型辅助编写接口代码与接口文档',
|
||||
link: '/stage-2/backend/2.3-ai-interface-code/'
|
||||
},
|
||||
{
|
||||
text: '后端四:Git 工作流',
|
||||
link: '/stage-2/backend/2.4-git-workflow/extra1/extra1-what-is-git-and-what-is-github'
|
||||
},
|
||||
{
|
||||
text: '后端五:Zeabur 部署',
|
||||
link: '/stage-2/backend/2.5-zeabur-deployment/extra6/extra6-zeabur-what-is-it-and-how-to-deploy-web-applications'
|
||||
},
|
||||
{
|
||||
text: '后端六:现代 CLI 开发工具',
|
||||
link: '/stage-2/backend/2.6-modern-cli/extra7/extra7-cli-ai-coding-tools-and-the-principles-of-test-driven-development'
|
||||
},
|
||||
{
|
||||
text: '后端七:如何集成 Stripe 等收费系统',
|
||||
link: '/stage-2/backend/2.7-stripe-payment/'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '大作业',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{
|
||||
text: '大作业 1:构建第一个现代应用程序 - 全栈应用',
|
||||
link: '/stage-2/assignments/2.1-fullstack-app/'
|
||||
},
|
||||
{
|
||||
text: '大作业 2:现代前端组件库 + Trae 实战',
|
||||
link: '/stage-2/assignments/2.2-modern-frontend-trae/'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'AI 能力附录',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{
|
||||
text: 'AI 一:Dify 入门与知识库集成',
|
||||
link: '/stage-2/ai-capabilities/2.1-dify-knowledge-base/chapter3/chapter3-getting-started-with-dify-and-its-knowledge-base-integration'
|
||||
},
|
||||
{
|
||||
text: 'AI 二:学会查询 AI 词典与集成多模态 API',
|
||||
link: '/stage-2/ai-capabilities/2.2-multimodal-api/extra3/extra3-ai-capability-starter-handbook'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
'/stage-3/': [
|
||||
{
|
||||
text: '核心技能',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{
|
||||
text: '高级一:MCP 与 ClaudeCode Skills',
|
||||
link: '/stage-3/core-skills/3.1-mcp-claudecode-skills/'
|
||||
},
|
||||
{
|
||||
text: '高级二:如何让 Coding Tools 长时间工作',
|
||||
link: '/stage-3/core-skills/3.2-long-running-tasks/'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '多平台开发',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{
|
||||
text: '高级三:如何构建微信小程序',
|
||||
link: '/stage-3/cross-platform/3.3-wechat-miniprogram/example1/index'
|
||||
},
|
||||
{
|
||||
text: '高级四:如何构建微信小程序(包含后端)',
|
||||
link: '/stage-3/cross-platform/3.4-wechat-miniprogram-backend/'
|
||||
},
|
||||
{
|
||||
text: '高级五:如何构建安卓程序',
|
||||
link: '/stage-3/cross-platform/3.5-android-app/'
|
||||
},
|
||||
{
|
||||
text: '高级六:如何构建 iOS 程序',
|
||||
link: '/stage-3/cross-platform/3.6-ios-app/'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '个人品牌',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{
|
||||
text: '高级七:如何构建属于自己的个人网页与学术博客',
|
||||
link: '/stage-3/personal-brand/3.7-personal-website-blog/'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'AI 能力附录',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{
|
||||
text: '高级 AI 一:什么是 RAG 以及它如何工作',
|
||||
link: '/stage-3/ai-advanced/3.a1-rag-introduction/extra5/extra5-what-is-rag-and-how-does-it-work-and-future'
|
||||
},
|
||||
{
|
||||
text: '高级 AI 二:中高级 RAG 与工作流编排 - 以 LangGraph 为例',
|
||||
link: '/stage-3/ai-advanced/3.a2-langgraph-advanced-rag/'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
'/guide/': [
|
||||
{
|
||||
text: '课程指南',
|
||||
items: [{ text: '课程介绍', link: '/guide/introduction' }]
|
||||
}
|
||||
],
|
||||
'/extra/': [
|
||||
{
|
||||
text: 'Extra 扩展知识(旧版,已迁移到 Stage 2/3)',
|
||||
items: [
|
||||
{
|
||||
text: 'Extra 1: Git & GitHub',
|
||||
link: '/extra/extra1/extra1-what-is-git-and-what-is-github'
|
||||
},
|
||||
{
|
||||
text: 'Extra 2: What is API',
|
||||
link: '/extra/extra2/extra2-what-is-api'
|
||||
},
|
||||
{
|
||||
text: 'Extra 5: What is RAG',
|
||||
link: '/extra/extra5/extra5-what-is-rag-and-how-does-it-work-and-future'
|
||||
},
|
||||
{
|
||||
text: 'Extra 6: Zeabur Deployment',
|
||||
link: '/extra/extra6/extra6-zeabur-what-is-it-and-how-to-deploy-web-applications'
|
||||
},
|
||||
{
|
||||
text: 'Extra 7: CLI AI Tools & TDD',
|
||||
link: '/extra/extra7/extra7-cli-ai-coding-tools-and-the-principles-of-test-driven-development'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
'/examples/': [
|
||||
{
|
||||
text: 'Examples 实战案例(旧版,已迁移到 Stage 0/3)',
|
||||
items: [
|
||||
{
|
||||
text: 'Ex 0.1: Snake Game',
|
||||
link: '/examples/example0/example0-1/vibe-coding-tools-snake-game-tutorial'
|
||||
},
|
||||
{
|
||||
text: 'Ex 0.2: Build Website with AI',
|
||||
link: '/examples/example0/example0-2/vibe-coding-tools-build-website-with-ai-coding-and-design-agents'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
'/project/': [
|
||||
{
|
||||
text: 'Project 文档(旧版,已迁移到 Stage 2)',
|
||||
items: [
|
||||
{
|
||||
text: '前端四:霍格沃茨画像',
|
||||
link: '/project/chapter4/chapter4-lets-build-hogwarts-portraits'
|
||||
},
|
||||
{
|
||||
text: '后端二:Supabase 数据库',
|
||||
link: '/project/chapter5/chapter5-from-database-to-supabase'
|
||||
},
|
||||
{
|
||||
text: 'AI 一:Dify & Knowledge Base',
|
||||
link: '/project/chapter3/chapter3-getting-started-with-dify-and-its-knowledge-base-integration'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
'/appendix/': [
|
||||
{
|
||||
text: '附录',
|
||||
items: [
|
||||
{ text: 'AI 能力词典', link: '/appendix/ai-capability-dictionary' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/datawhalechina/easy-vibe' }
|
||||
]
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import { useData } from 'vitepress'
|
||||
import TextType from './components/TextType.vue'
|
||||
|
||||
const { frontmatter } = useData()
|
||||
|
||||
const homeTaglineTyping = {
|
||||
typingSpeed: 45,
|
||||
initialDelay: 0,
|
||||
pauseDuration: 2500,
|
||||
postDeletingDelay: 500,
|
||||
deletingSpeed: 18
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DefaultTheme.Layout>
|
||||
<template #home-hero-info-after>
|
||||
<div v-if="frontmatter.layout === 'home' && frontmatter.hero?.tagline" class="vp-typed-tagline">
|
||||
<ClientOnly>
|
||||
<TextType :text="frontmatter.hero.tagline" v-bind="homeTaglineTyping" :loop="true" />
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
</DefaultTheme.Layout>
|
||||
</template>
|
||||
@@ -0,0 +1,260 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref, useAttrs, watchEffect } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: [String, Array],
|
||||
required: true
|
||||
},
|
||||
as: {
|
||||
type: [String, Object],
|
||||
default: 'div'
|
||||
},
|
||||
typingSpeed: {
|
||||
type: Number,
|
||||
default: 50
|
||||
},
|
||||
initialDelay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
pauseDuration: {
|
||||
type: Number,
|
||||
default: 2000
|
||||
},
|
||||
postDeletingDelay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
deletingSpeed: {
|
||||
type: Number,
|
||||
default: 30
|
||||
},
|
||||
loop: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
showCursor: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
hideCursorWhileTyping: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
cursorCharacter: {
|
||||
type: String,
|
||||
default: '|'
|
||||
},
|
||||
cursorClassName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
cursorBlinkDuration: {
|
||||
type: Number,
|
||||
default: 0.5
|
||||
},
|
||||
textColors: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
variableSpeed: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
onSentenceComplete: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
startOnVisible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
reverseMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const isClient = typeof window !== 'undefined'
|
||||
|
||||
const attrs = useAttrs()
|
||||
|
||||
const displayedText = ref('')
|
||||
const currentCharIndex = ref(0)
|
||||
const isDeleting = ref(false)
|
||||
const currentTextIndex = ref(0)
|
||||
const isVisible = ref(!props.startOnVisible)
|
||||
const containerRef = ref(null)
|
||||
|
||||
const textArray = computed(() => (Array.isArray(props.text) ? props.text : [props.text]))
|
||||
|
||||
const cursorStyle = computed(() => ({
|
||||
animationDuration: `${props.cursorBlinkDuration}s`
|
||||
}))
|
||||
|
||||
const currentColor = computed(() => {
|
||||
if (!props.textColors.length) return undefined
|
||||
return props.textColors[currentTextIndex.value % props.textColors.length]
|
||||
})
|
||||
|
||||
const getRandomSpeed = () => {
|
||||
if (!props.variableSpeed) return props.typingSpeed
|
||||
const min = typeof props.variableSpeed.min === 'number' ? props.variableSpeed.min : props.typingSpeed
|
||||
const max = typeof props.variableSpeed.max === 'number' ? props.variableSpeed.max : props.typingSpeed
|
||||
if (max <= min) return min
|
||||
return Math.random() * (max - min) + min
|
||||
}
|
||||
|
||||
let observer
|
||||
onMounted(() => {
|
||||
if (!props.startOnVisible || !containerRef.value) return
|
||||
observer = new IntersectionObserver(
|
||||
entries => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
isVisible.value = true
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
)
|
||||
observer.observe(containerRef.value)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (observer) observer.disconnect()
|
||||
})
|
||||
|
||||
watchEffect(onCleanup => {
|
||||
if (!isVisible.value) return
|
||||
|
||||
if (!textArray.value.length) {
|
||||
displayedText.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
const currentText = textArray.value[currentTextIndex.value] ?? ''
|
||||
const processedText = props.reverseMode ? String(currentText).split('').reverse().join('') : String(currentText)
|
||||
|
||||
if (!isClient) {
|
||||
return
|
||||
}
|
||||
|
||||
const shouldStopAtEnd = !props.loop && currentTextIndex.value === textArray.value.length - 1
|
||||
|
||||
let timeoutId
|
||||
|
||||
const schedule = () => {
|
||||
if (isDeleting.value) {
|
||||
if (!displayedText.value) {
|
||||
isDeleting.value = false
|
||||
if (props.onSentenceComplete) {
|
||||
props.onSentenceComplete(textArray.value[currentTextIndex.value], currentTextIndex.value)
|
||||
}
|
||||
if (shouldStopAtEnd) return
|
||||
timeoutId = setTimeout(() => {
|
||||
currentTextIndex.value = (currentTextIndex.value + 1) % textArray.value.length
|
||||
currentCharIndex.value = 0
|
||||
}, props.postDeletingDelay)
|
||||
return
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
displayedText.value = displayedText.value.slice(0, -1)
|
||||
}, props.deletingSpeed)
|
||||
return
|
||||
}
|
||||
|
||||
if (currentCharIndex.value < processedText.length) {
|
||||
timeoutId = setTimeout(() => {
|
||||
displayedText.value += processedText[currentCharIndex.value]
|
||||
currentCharIndex.value += 1
|
||||
}, props.variableSpeed ? getRandomSpeed() : props.typingSpeed)
|
||||
return
|
||||
}
|
||||
|
||||
if (shouldStopAtEnd) return
|
||||
timeoutId = setTimeout(() => {
|
||||
isDeleting.value = true
|
||||
}, props.pauseDuration)
|
||||
}
|
||||
|
||||
if (currentCharIndex.value === 0 && !isDeleting.value && !displayedText.value) {
|
||||
timeoutId = setTimeout(schedule, props.initialDelay)
|
||||
} else {
|
||||
schedule()
|
||||
}
|
||||
|
||||
onCleanup(() => clearTimeout(timeoutId))
|
||||
})
|
||||
|
||||
const shouldHideCursor = computed(() => {
|
||||
if (!props.hideCursorWhileTyping) return false
|
||||
const currentText = textArray.value[currentTextIndex.value] ?? ''
|
||||
const processedText = props.reverseMode ? String(currentText).split('').reverse().join('') : String(currentText)
|
||||
return currentCharIndex.value < processedText.length || isDeleting.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="as"
|
||||
ref="containerRef"
|
||||
:class="['text-type', className]"
|
||||
v-bind="attrs"
|
||||
>
|
||||
<span class="text-type__content" :style="{ color: currentColor || 'inherit' }">
|
||||
{{ displayedText }}
|
||||
</span>
|
||||
<span
|
||||
v-if="showCursor"
|
||||
class="text-type__cursor"
|
||||
:class="[cursorClassName, shouldHideCursor ? 'text-type__cursor--hidden' : '']"
|
||||
:style="cursorStyle"
|
||||
>
|
||||
{{ cursorCharacter }}
|
||||
</span>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.text-type {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.text-type__content {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.text-type__cursor {
|
||||
display: inline-block;
|
||||
margin-left: 2px;
|
||||
animation-name: text-type-blink;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
|
||||
.text-type__cursor--hidden {
|
||||
opacity: 0;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes text-type-blink {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,119 @@
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import Viewer from 'viewerjs'
|
||||
import 'viewerjs/dist/viewer.css'
|
||||
import TypeIt from 'typeit'
|
||||
import { onMounted, watch, nextTick } from 'vue'
|
||||
import { useRoute, useData } from 'vitepress'
|
||||
import './style.css'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
const { frontmatter } = useData()
|
||||
let viewer = null
|
||||
|
||||
const initViewer = () => {
|
||||
// 销毁旧实例
|
||||
if (viewer) {
|
||||
viewer.destroy()
|
||||
viewer = null
|
||||
}
|
||||
|
||||
// 找到文章内容容器
|
||||
const doc = document.querySelector('.vp-doc')
|
||||
if (doc) {
|
||||
// 初始化 Viewer,配置一些常用选项
|
||||
viewer = new Viewer(doc, {
|
||||
button: true, // 显示右上角关闭按钮
|
||||
navbar: true, // 显示底部缩略图导航
|
||||
title: true, // 显示图片标题(alt 属性)
|
||||
toolbar: true, // 显示工具栏(缩放、旋转等)
|
||||
tooltip: true, // 显示缩放百分比
|
||||
movable: true, // 允许拖拽
|
||||
zoomable: true, // 允许缩放
|
||||
rotatable: true, // 允许旋转
|
||||
scalable: true, // 允许翻转
|
||||
transition: false, // 禁用自带动画,确保打开瞬间无飞入
|
||||
fullscreen: true, // 允许全屏播放
|
||||
shown() {
|
||||
// 打开完成后,标记为 ready,CSS 此时才会介入 transition
|
||||
document.body.classList.add('viewer-ready')
|
||||
},
|
||||
hide() {
|
||||
// 关闭前移除标记,确保关闭瞬间无动画
|
||||
document.body.classList.remove('viewer-ready')
|
||||
},
|
||||
keyboard: true, // 允许键盘控制
|
||||
url: 'src', // 图片源
|
||||
// 过滤掉不想查看的图片(比如表情包等小图标,如果需要的话)
|
||||
filter(image) {
|
||||
return !image.classList.contains('no-viewer')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const initTypewriter = () => {
|
||||
const taglineData = frontmatter.value.hero?.tagline
|
||||
if (Array.isArray(taglineData) && taglineData.length > 0) {
|
||||
const taglineEl = document.querySelector('.VPHomeHero .tagline')
|
||||
if (taglineEl) {
|
||||
taglineEl.innerHTML = ''
|
||||
|
||||
const typeIt = new TypeIt(taglineEl, {
|
||||
speed: 50,
|
||||
startDelay: 500,
|
||||
loop: true
|
||||
})
|
||||
|
||||
taglineData.forEach((text) => {
|
||||
typeIt.type(text).pause(2000).delete().pause(500)
|
||||
})
|
||||
|
||||
typeIt.go()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const optimizeImages = () => {
|
||||
const images = document.querySelectorAll('.vp-doc img')
|
||||
images.forEach(img => {
|
||||
if (img.complete) {
|
||||
applyImageStyle(img)
|
||||
} else {
|
||||
img.onload = () => applyImageStyle(img)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const applyImageStyle = (img) => {
|
||||
const { naturalWidth, naturalHeight } = img
|
||||
if (!naturalWidth || !naturalHeight) return
|
||||
|
||||
const ratio = naturalHeight / naturalWidth
|
||||
img.classList.remove('img-tall', 'img-very-tall')
|
||||
|
||||
if (ratio > 2) {
|
||||
img.classList.add('img-very-tall')
|
||||
} else if (ratio > 1.2) {
|
||||
img.classList.add('img-tall')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initViewer()
|
||||
initTypewriter()
|
||||
optimizeImages()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
() => nextTick(() => {
|
||||
initViewer()
|
||||
initTypewriter()
|
||||
optimizeImages()
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/* Custom styles for viewerjs */
|
||||
.viewer-container {
|
||||
z-index: 9999 !important; /* Ensure it's above everything including navbar */
|
||||
}
|
||||
|
||||
/* Optional: Adjust backdrop opacity if needed */
|
||||
.viewer-backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.9); /* Darker backdrop for better focus */
|
||||
}
|
||||
|
||||
/*
|
||||
Core Logic:
|
||||
1. Default (opening/closing): No transition -> Instant.
|
||||
2. body.viewer-ready (viewing): Force transition -> Smooth Zoom.
|
||||
*/
|
||||
body.viewer-ready .viewer-canvas > img {
|
||||
transition: transform 0.2s ease-out !important;
|
||||
}
|
||||
|
||||
/* Limit image max height in document content */
|
||||
.vp-doc img {
|
||||
max-height: 500px;
|
||||
width: auto;
|
||||
margin: 16px auto; /* Center with spacing */
|
||||
display: block;
|
||||
border-radius: 8px; /* Optional: Rounded corners */
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); /* Optional: Shadow */
|
||||
}
|
||||
|
||||
/* Tall images (e.g. mobile screenshots) - ratio > 1.2 */
|
||||
.vp-doc img.img-tall {
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
/* Very tall images (e.g. long screenshots) - ratio > 2 */
|
||||
.vp-doc img.img-very-tall {
|
||||
max-height: 300px;
|
||||
}
|
||||
Reference in New Issue
Block a user