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:
sanbuphy
2026-01-12 12:21:35 +08:00
parent 307a37cdb9
commit a4b583b13f
632 changed files with 18082 additions and 8092 deletions
@@ -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>