style(docs): improve typography and layout consistency
- Standardize font sizes and line heights across docs - Add ChapterIntroduction component for consistent chapter headers - Fix markdown formatting and whitespace issues - Improve code block and table styling - Add font size and line height controls to layout
This commit is contained in:
@@ -47,7 +47,7 @@ export default defineConfig({
|
||||
{
|
||||
text: '1. 认识 AI IDE 工具',
|
||||
link: '/stage-1/1.1-introduction-to-ai-ide/'
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '2. 动手做出原型',
|
||||
link: '/stage-1/1.2-building-prototype/'
|
||||
@@ -71,8 +71,7 @@ export default defineConfig({
|
||||
{
|
||||
text: '附录 B:常见报错及解决方案',
|
||||
link: '/stage-1/appendix-b-common-errors/'
|
||||
}
|
||||
,
|
||||
},
|
||||
{
|
||||
text: '附录示例:贪吃蛇游戏教程',
|
||||
link: '/stage-1/appendix-articles/example0-1/vibe-coding-tools-snake-game-tutorial'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import { useData } from 'vitepress'
|
||||
import TextType from './components/TextType.vue'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
|
||||
const { frontmatter } = useData()
|
||||
|
||||
@@ -12,16 +13,251 @@ const homeTaglineTyping = {
|
||||
postDeletingDelay: 500,
|
||||
deletingSpeed: 18
|
||||
}
|
||||
|
||||
const FONT_SIZE_STORAGE_KEY = 'ev-doc-font-size'
|
||||
const LINE_HEIGHT_STORAGE_KEY = 'ev-doc-line-height'
|
||||
const MIN_FONT_SIZE = 12
|
||||
const MAX_FONT_SIZE = 18
|
||||
const DEFAULT_FONT_SIZE = 13
|
||||
const MIN_LINE_HEIGHT = 1.25
|
||||
const MAX_LINE_HEIGHT = 1.8
|
||||
const DEFAULT_LINE_HEIGHT = 1.5
|
||||
|
||||
const fontSize = ref(DEFAULT_FONT_SIZE)
|
||||
const lineHeight = ref(DEFAULT_LINE_HEIGHT)
|
||||
const isHydrated = ref(false)
|
||||
|
||||
const clampFontSize = (value) => {
|
||||
const numeric = Number(value)
|
||||
if (!Number.isFinite(numeric)) return DEFAULT_FONT_SIZE
|
||||
return Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, numeric))
|
||||
}
|
||||
|
||||
const clampLineHeight = (value) => {
|
||||
const numeric = Number(value)
|
||||
if (!Number.isFinite(numeric)) return DEFAULT_LINE_HEIGHT
|
||||
return Math.min(MAX_LINE_HEIGHT, Math.max(MIN_LINE_HEIGHT, numeric))
|
||||
}
|
||||
|
||||
const applyFontSize = (size) => {
|
||||
if (typeof document === 'undefined') return
|
||||
document.documentElement.style.setProperty('--ev-doc-font-size', `${size}px`)
|
||||
}
|
||||
|
||||
const applyLineHeight = (value) => {
|
||||
if (typeof document === 'undefined') return
|
||||
document.documentElement.style.setProperty('--ev-doc-line-height', String(value))
|
||||
}
|
||||
|
||||
const decreaseFontSize = () => {
|
||||
fontSize.value = clampFontSize(fontSize.value - 1)
|
||||
}
|
||||
|
||||
const increaseFontSize = () => {
|
||||
fontSize.value = clampFontSize(fontSize.value + 1)
|
||||
}
|
||||
|
||||
const resetFontSize = () => {
|
||||
fontSize.value = DEFAULT_FONT_SIZE
|
||||
}
|
||||
|
||||
const resetLineHeight = () => {
|
||||
lineHeight.value = DEFAULT_LINE_HEIGHT
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const saved = clampFontSize(localStorage.getItem(FONT_SIZE_STORAGE_KEY))
|
||||
const savedLineHeight = clampLineHeight(localStorage.getItem(LINE_HEIGHT_STORAGE_KEY))
|
||||
fontSize.value = saved
|
||||
lineHeight.value = savedLineHeight
|
||||
applyFontSize(saved)
|
||||
applyLineHeight(savedLineHeight)
|
||||
isHydrated.value = true
|
||||
})
|
||||
|
||||
watch(fontSize, (next) => {
|
||||
if (!isHydrated.value) return
|
||||
const normalized = clampFontSize(next)
|
||||
applyFontSize(normalized)
|
||||
localStorage.setItem(FONT_SIZE_STORAGE_KEY, String(normalized))
|
||||
})
|
||||
|
||||
watch(lineHeight, (next) => {
|
||||
if (!isHydrated.value) return
|
||||
const normalized = clampLineHeight(next)
|
||||
applyLineHeight(normalized)
|
||||
localStorage.setItem(LINE_HEIGHT_STORAGE_KEY, String(normalized))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DefaultTheme.Layout>
|
||||
<template #nav-bar-content-after>
|
||||
<ClientOnly>
|
||||
<el-popover placement="bottom-end" trigger="click" :width="260">
|
||||
<template #reference>
|
||||
<button
|
||||
class="ev-fontsize-button"
|
||||
type="button"
|
||||
aria-label="阅读设置"
|
||||
style="margin-left: 16px; padding: 0; width: 32px;"
|
||||
>
|
||||
<el-icon :size="16"><Setting /></el-icon>
|
||||
</button>
|
||||
</template>
|
||||
<div class="ev-fontsize-panel">
|
||||
<div class="ev-setting-group">
|
||||
<div class="ev-setting-header">
|
||||
<div class="ev-setting-title">字号</div>
|
||||
<div class="ev-setting-value">{{ fontSize }}px</div>
|
||||
</div>
|
||||
<div class="ev-fontsize-actions">
|
||||
<button
|
||||
class="ev-fontsize-action"
|
||||
type="button"
|
||||
@click="decreaseFontSize"
|
||||
>
|
||||
A-
|
||||
</button>
|
||||
<button
|
||||
class="ev-fontsize-action"
|
||||
type="button"
|
||||
@click="resetFontSize"
|
||||
>
|
||||
默认
|
||||
</button>
|
||||
<button
|
||||
class="ev-fontsize-action"
|
||||
type="button"
|
||||
@click="increaseFontSize"
|
||||
>
|
||||
A+
|
||||
</button>
|
||||
</div>
|
||||
<el-slider v-model="fontSize" :min="MIN_FONT_SIZE" :max="MAX_FONT_SIZE" :step="1" />
|
||||
</div>
|
||||
|
||||
<div class="ev-setting-group">
|
||||
<div class="ev-setting-header">
|
||||
<div class="ev-setting-title">行距</div>
|
||||
<div class="ev-setting-value">{{ lineHeight.toFixed(2) }}</div>
|
||||
</div>
|
||||
<div class="ev-fontsize-actions">
|
||||
<button class="ev-fontsize-action" type="button" @click="resetLineHeight">
|
||||
默认
|
||||
</button>
|
||||
<button
|
||||
class="ev-fontsize-action"
|
||||
type="button"
|
||||
@click="lineHeight = clampLineHeight(lineHeight - 0.05)"
|
||||
>
|
||||
更紧
|
||||
</button>
|
||||
<button
|
||||
class="ev-fontsize-action"
|
||||
type="button"
|
||||
@click="lineHeight = clampLineHeight(lineHeight + 0.05)"
|
||||
>
|
||||
更松
|
||||
</button>
|
||||
</div>
|
||||
<el-slider
|
||||
v-model="lineHeight"
|
||||
:min="MIN_LINE_HEIGHT"
|
||||
:max="MAX_LINE_HEIGHT"
|
||||
:step="0.05"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
<template #home-hero-info-after>
|
||||
<div v-if="frontmatter.layout === 'home' && frontmatter.hero?.tagline" class="vp-typed-tagline">
|
||||
<div
|
||||
v-if="frontmatter.layout === 'home' && frontmatter.hero?.tagline"
|
||||
class="vp-typed-tagline"
|
||||
>
|
||||
<ClientOnly>
|
||||
<TextType :text="frontmatter.hero.tagline" v-bind="homeTaglineTyping" :loop="true" />
|
||||
<TextType
|
||||
:text="frontmatter.hero.tagline"
|
||||
v-bind="homeTaglineTyping"
|
||||
:loop="true"
|
||||
/>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
</DefaultTheme.Layout>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.ev-fontsize-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 32px;
|
||||
min-width: 32px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 999px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ev-fontsize-button:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.ev-fontsize-panel {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ev-setting-group {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ev-setting-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ev-setting-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.ev-setting-value {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.ev-fontsize-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ev-fontsize-action {
|
||||
height: 32px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ev-fontsize-action:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
duration: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
expectedOutput: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
coreOutput: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
assignment: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const hasMeta = computed(() => props.duration || props.expectedOutput || props.coreOutput || props.assignment)
|
||||
const hasTags = computed(() => props.tags && props.tags.length > 0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chapter-introduction">
|
||||
<!-- Learning Objective -->
|
||||
<div class="objective-section">
|
||||
<div class="objective-label">
|
||||
<span class="icon">🎯</span>
|
||||
<span class="title">本章学习目标</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<!-- If tags are provided, show tags list -->
|
||||
<div v-if="hasTags" class="tags-container">
|
||||
<span v-for="(tag, index) in tags" :key="index" class="objective-tag">
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Slot content (full description) always rendered below tags if tags exist, or alone if not -->
|
||||
<div class="description-text" :class="{ 'has-tags': hasTags }">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metrics Grid -->
|
||||
<div v-if="hasMeta" class="metrics-grid">
|
||||
<!-- Duration Card -->
|
||||
<div v-if="duration" class="metric-card time-card">
|
||||
<div class="card-icon">⏱️</div>
|
||||
<div class="card-content">
|
||||
<div class="card-label">预计耗时</div>
|
||||
<div class="card-value" v-html="duration"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Output Card -->
|
||||
<div v-if="expectedOutput || coreOutput" class="metric-card output-card">
|
||||
<div class="card-icon">📦</div>
|
||||
<div class="card-content">
|
||||
<div class="card-label">预期产出</div>
|
||||
<div class="output-container">
|
||||
<div v-if="coreOutput" class="core-output">{{ coreOutput }}</div>
|
||||
<div v-if="expectedOutput" class="output-desc" v-html="expectedOutput"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assignment Card -->
|
||||
<div v-if="assignment" class="metric-card task-card">
|
||||
<div class="card-icon">📝</div>
|
||||
<div class="card-content">
|
||||
<div class="card-label">课后任务</div>
|
||||
<div class="card-value" v-html="assignment"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chapter-introduction {
|
||||
margin: 24px 0;
|
||||
border-radius: 16px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.objective-section {
|
||||
padding: 24px 28px;
|
||||
background: linear-gradient(to right, rgba(var(--vp-c-brand-rgb), 0.05), transparent);
|
||||
border-bottom: 1px dashed var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.objective-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 20px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Tags Styling */
|
||||
.tags-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.description-text {
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.description-text.has-tags {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.objective-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 14px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 99px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.objective-tag:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand);
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Metrics Grid */
|
||||
.metrics-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1px;
|
||||
background-color: var(--vp-c-divider);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
flex: 1 1 240px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 20px 24px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.metric-card:hover {
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.card-value :deep(strong) {
|
||||
display: inline-block;
|
||||
color: var(--vp-c-brand-dark);
|
||||
font-weight: 800;
|
||||
font-size: 16px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Output Container Styling */
|
||||
.output-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.core-output {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: var(--vp-c-brand);
|
||||
line-height: 1.4;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.output-desc {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.output-desc :deep(strong) {
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media (max-width: 640px) {
|
||||
.metric-card {
|
||||
padding: 16px 20px;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.objective-section {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -18,10 +18,10 @@ defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{ title: "困境与机会", description: "普通人的编程新可能" },
|
||||
{ title: "能力初探", description: "60秒极速开发体验" },
|
||||
{ title: "原生实战", description: "打造AI原生贪吃蛇" },
|
||||
{ title: "拓展创造", description: "举一反三做游戏" }
|
||||
{ title: '困境与机会', description: '普通人的编程新可能' },
|
||||
{ title: '能力初探', description: '60秒极速开发体验' },
|
||||
{ title: '原生实战', description: '打造AI原生贪吃蛇' },
|
||||
{ title: '拓展创造', description: '举一反三做游戏' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref, useAttrs, watchEffect } from 'vue'
|
||||
import {
|
||||
computed,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
useAttrs,
|
||||
watchEffect
|
||||
} from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
text: {
|
||||
@@ -91,7 +98,9 @@ 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 textArray = computed(() =>
|
||||
Array.isArray(props.text) ? props.text : [props.text]
|
||||
)
|
||||
|
||||
const cursorStyle = computed(() => ({
|
||||
animationDuration: `${props.cursorBlinkDuration}s`
|
||||
@@ -104,8 +113,14 @@ const currentColor = computed(() => {
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
@@ -114,7 +129,7 @@ let observer
|
||||
onMounted(() => {
|
||||
if (!props.startOnVisible || !containerRef.value) return
|
||||
observer = new IntersectionObserver(
|
||||
entries => {
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
isVisible.value = true
|
||||
@@ -131,7 +146,7 @@ onUnmounted(() => {
|
||||
if (observer) observer.disconnect()
|
||||
})
|
||||
|
||||
watchEffect(onCleanup => {
|
||||
watchEffect((onCleanup) => {
|
||||
if (!isVisible.value) return
|
||||
|
||||
if (!textArray.value.length) {
|
||||
@@ -140,13 +155,16 @@ watchEffect(onCleanup => {
|
||||
}
|
||||
|
||||
const currentText = textArray.value[currentTextIndex.value] ?? ''
|
||||
const processedText = props.reverseMode ? String(currentText).split('').reverse().join('') : String(currentText)
|
||||
const processedText = props.reverseMode
|
||||
? String(currentText).split('').reverse().join('')
|
||||
: String(currentText)
|
||||
|
||||
if (!isClient) {
|
||||
return
|
||||
}
|
||||
|
||||
const shouldStopAtEnd = !props.loop && currentTextIndex.value === textArray.value.length - 1
|
||||
const shouldStopAtEnd =
|
||||
!props.loop && currentTextIndex.value === textArray.value.length - 1
|
||||
|
||||
let timeoutId
|
||||
|
||||
@@ -155,11 +173,15 @@ watchEffect(onCleanup => {
|
||||
if (!displayedText.value) {
|
||||
isDeleting.value = false
|
||||
if (props.onSentenceComplete) {
|
||||
props.onSentenceComplete(textArray.value[currentTextIndex.value], currentTextIndex.value)
|
||||
props.onSentenceComplete(
|
||||
textArray.value[currentTextIndex.value],
|
||||
currentTextIndex.value
|
||||
)
|
||||
}
|
||||
if (shouldStopAtEnd) return
|
||||
timeoutId = setTimeout(() => {
|
||||
currentTextIndex.value = (currentTextIndex.value + 1) % textArray.value.length
|
||||
currentTextIndex.value =
|
||||
(currentTextIndex.value + 1) % textArray.value.length
|
||||
currentCharIndex.value = 0
|
||||
}, props.postDeletingDelay)
|
||||
return
|
||||
@@ -172,10 +194,13 @@ watchEffect(onCleanup => {
|
||||
}
|
||||
|
||||
if (currentCharIndex.value < processedText.length) {
|
||||
timeoutId = setTimeout(() => {
|
||||
displayedText.value += processedText[currentCharIndex.value]
|
||||
currentCharIndex.value += 1
|
||||
}, props.variableSpeed ? getRandomSpeed() : props.typingSpeed)
|
||||
timeoutId = setTimeout(
|
||||
() => {
|
||||
displayedText.value += processedText[currentCharIndex.value]
|
||||
currentCharIndex.value += 1
|
||||
},
|
||||
props.variableSpeed ? getRandomSpeed() : props.typingSpeed
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -185,7 +210,11 @@ watchEffect(onCleanup => {
|
||||
}, props.pauseDuration)
|
||||
}
|
||||
|
||||
if (currentCharIndex.value === 0 && !isDeleting.value && !displayedText.value) {
|
||||
if (
|
||||
currentCharIndex.value === 0 &&
|
||||
!isDeleting.value &&
|
||||
!displayedText.value
|
||||
) {
|
||||
timeoutId = setTimeout(schedule, props.initialDelay)
|
||||
} else {
|
||||
schedule()
|
||||
@@ -197,7 +226,9 @@ watchEffect(onCleanup => {
|
||||
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)
|
||||
const processedText = props.reverseMode
|
||||
? String(currentText).split('').reverse().join('')
|
||||
: String(currentText)
|
||||
return currentCharIndex.value < processedText.length || isDeleting.value
|
||||
})
|
||||
</script>
|
||||
@@ -209,13 +240,19 @@ const shouldHideCursor = computed(() => {
|
||||
:class="['text-type', className]"
|
||||
v-bind="attrs"
|
||||
>
|
||||
<span class="text-type__content" :style="{ color: currentColor || 'inherit' }">
|
||||
<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' : '']"
|
||||
:class="[
|
||||
cursorClassName,
|
||||
shouldHideCursor ? 'text-type__cursor--hidden' : ''
|
||||
]"
|
||||
:style="cursorStyle"
|
||||
>
|
||||
{{ cursorCharacter }}
|
||||
|
||||
@@ -7,13 +7,17 @@ import TypeIt from 'typeit'
|
||||
import { onMounted, watch, nextTick } from 'vue'
|
||||
import { useRoute, useData } from 'vitepress'
|
||||
import './style.css'
|
||||
import Layout from './Layout.vue'
|
||||
import StepBar from './components/StepBar.vue'
|
||||
import ChapterIntroduction from './components/ChapterIntroduction.vue'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
Layout,
|
||||
enhanceApp({ app }) {
|
||||
app.use(ElementPlus)
|
||||
app.component('StepBar', StepBar)
|
||||
app.component('ChapterIntroduction', ChapterIntroduction)
|
||||
},
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
@@ -67,7 +71,7 @@ export default {
|
||||
const taglineEl = document.querySelector('.VPHomeHero .tagline')
|
||||
if (taglineEl) {
|
||||
taglineEl.innerHTML = ''
|
||||
|
||||
|
||||
const typeIt = new TypeIt(taglineEl, {
|
||||
speed: 50,
|
||||
startDelay: 500,
|
||||
@@ -75,9 +79,9 @@ export default {
|
||||
})
|
||||
|
||||
taglineData.forEach((text) => {
|
||||
typeIt.type(text).pause(2000).delete().pause(500)
|
||||
typeIt.type(text).pause(2000).delete().pause(500)
|
||||
})
|
||||
|
||||
|
||||
typeIt.go()
|
||||
}
|
||||
}
|
||||
@@ -85,7 +89,7 @@ export default {
|
||||
|
||||
const optimizeImages = () => {
|
||||
const images = document.querySelectorAll('.vp-doc img')
|
||||
images.forEach(img => {
|
||||
images.forEach((img) => {
|
||||
if (img.complete) {
|
||||
applyImageStyle(img)
|
||||
} else {
|
||||
@@ -133,11 +137,12 @@ export default {
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
() => nextTick(() => {
|
||||
initViewer()
|
||||
initTypewriter()
|
||||
optimizeImages()
|
||||
})
|
||||
() =>
|
||||
nextTick(() => {
|
||||
initViewer()
|
||||
initTypewriter()
|
||||
optimizeImages()
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,70 @@
|
||||
/* Easy-Vibe Theme Fix v2025-01-12 */
|
||||
/* 通过变量控制分组底部留白(默认 24px) */
|
||||
--vp-sidebar-nav-section-gap: 8px;
|
||||
--ev-doc-font-size: 13px;
|
||||
--ev-doc-line-height: 1.5;
|
||||
}
|
||||
|
||||
.vp-doc {
|
||||
font-size: 15px;
|
||||
font-size: var(--ev-doc-font-size);
|
||||
line-height: var(--ev-doc-line-height);
|
||||
--el-font-size-extra-large: calc(var(--ev-doc-font-size) + 6px);
|
||||
--el-font-size-large: calc(var(--ev-doc-font-size) + 4px);
|
||||
--el-font-size-medium: calc(var(--ev-doc-font-size) + 2px);
|
||||
--el-font-size-base: var(--ev-doc-font-size);
|
||||
--el-font-size-small: calc(var(--ev-doc-font-size) - 1px);
|
||||
--el-font-size-extra-small: calc(var(--ev-doc-font-size) - 2px);
|
||||
--el-font-line-height-primary: var(--ev-doc-line-height);
|
||||
}
|
||||
|
||||
.vp-doc :where(p, ul, ol, table, blockquote, pre, details, figure) {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.vp-doc :where(li) {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.vp-doc :where(ul, ol) {
|
||||
padding-left: 1.15em;
|
||||
}
|
||||
|
||||
.vp-doc :where(h1, h2, h3, h4, h5, h6) {
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.vp-doc :where(h1) {
|
||||
margin: 22px 0 12px;
|
||||
}
|
||||
|
||||
.vp-doc :where(h2) {
|
||||
margin: 20px 0 10px;
|
||||
}
|
||||
|
||||
.vp-doc h2 {
|
||||
margin: 16px 0 8px !important;
|
||||
padding-top: 10px !important;
|
||||
border-top: 0 !important;
|
||||
}
|
||||
|
||||
.vp-doc :where(h3) {
|
||||
margin: 18px 0 8px;
|
||||
}
|
||||
|
||||
.vp-doc :where(h4, h5, h6) {
|
||||
margin: 16px 0 8px;
|
||||
}
|
||||
|
||||
.vp-doc :where(hr) {
|
||||
margin: 14px 0;
|
||||
}
|
||||
|
||||
.vp-doc :where(th, td) {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.vp-doc :where(:not(pre) > code) {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
/* 生产环境(带 data-v-* 的 scoped 样式)会比 class 选择器更高优先级。
|
||||
@@ -44,7 +104,9 @@
|
||||
|
||||
/* 进一步压缩分组标题与第一项之间的间距 */
|
||||
:where(html) .VPSidebarItem.level-0 + .VPSidebarItem.level-1,
|
||||
:where(html) .VPSidebarItem.level-0[data-v-d81de50c] + .VPSidebarItem.level-1[data-v-d81de50c] {
|
||||
:where(html)
|
||||
.VPSidebarItem.level-0[data-v-d81de50c]
|
||||
+ .VPSidebarItem.level-1[data-v-d81de50c] {
|
||||
margin-top: -2px !important;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user