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:
sanbuphy
2026-01-13 14:42:34 +08:00
parent 7c546e62f8
commit 1d25eb9b9b
20 changed files with 1655 additions and 945 deletions
@@ -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>
+4 -4
View File
@@ -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: '举一反三做游戏' }
]
}
})
+55 -18
View File
@@ -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 }}