feat(docs): add new assignment projects and update existing content

This commit is contained in:
sanbuphy
2026-03-26 11:20:31 +08:00
parent f6454b0342
commit 7d6c2cbf9c
24 changed files with 4536 additions and 37 deletions
+276 -3
View File
@@ -4,7 +4,7 @@ import 'element-plus/dist/index.css'
import Viewer from 'viewerjs'
import 'viewerjs/dist/viewer.css'
import TypeIt from 'typeit'
import { onMounted, watch, nextTick } from 'vue'
import { onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
import { useRoute, useData } from 'vitepress'
import './style.css'
import Layout from './Layout.vue'
@@ -1712,12 +1712,259 @@ export default {
const route = useRoute()
const { frontmatter } = useData()
let viewer = null
let mermaidViewer = null
let mermaidViewerWrapper = null
let mermaidViewerObjectUrl = null
let mermaidApi = null
let themeObserver = null
let currentMermaidTheme = null
const COLLAPSIBLE_CODE_MIN_LINES = 14
// Skip browser-only initialization during SSR
if (import.meta.env.SSR) {
return
}
const getMermaidTheme = () =>
document.documentElement.classList.contains('dark') ? 'dark' : 'default'
const loadMermaid = async () => {
if (mermaidApi) return mermaidApi
const mermaidModule = await import('mermaid')
mermaidApi = mermaidModule.default ?? mermaidModule
return mermaidApi
}
const renderMermaidDiagrams = async (force = false) => {
const mermaidBlocks = document.querySelectorAll(
'.vp-doc div.language-mermaid, .vp-doc .mermaid-diagram[data-source]'
)
if (!mermaidBlocks.length) return
const mermaid = await loadMermaid()
const nextTheme = getMermaidTheme()
if (force || currentMermaidTheme !== nextTheme) {
mermaid.initialize({
startOnLoad: false,
securityLevel: 'loose',
theme: nextTheme
})
currentMermaidTheme = nextTheme
}
let index = 0
for (const block of mermaidBlocks) {
let source = ''
let container = block
if (block.classList.contains('language-mermaid')) {
source = block.querySelector('code')?.textContent?.trim() ?? ''
if (!source) continue
container = document.createElement('div')
container.className = 'mermaid-diagram'
container.dataset.source = source
block.replaceWith(container)
} else {
source = block.dataset.source ?? ''
if (!source) continue
}
try {
const diagramId = `mermaid-${route.path.replace(/\W+/g, '-')}-${Date.now()}-${index}`
const { svg, bindFunctions } = await mermaid.render(diagramId, source)
container.innerHTML = svg
container.classList.remove('mermaid-diagram-error')
container.setAttribute('role', 'button')
container.setAttribute('tabindex', '0')
container.setAttribute('aria-label', 'Open Mermaid diagram in fullscreen viewer')
container.onclick = (event) => {
if (event.target.closest?.('a')) return
openMermaidViewer(container)
}
container.onkeydown = (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
openMermaidViewer(container)
}
}
bindFunctions?.(container)
} catch (error) {
console.error('Mermaid render failed:', error)
container.innerHTML = ''
container.classList.add('mermaid-diagram-error')
}
index += 1
}
}
const cleanupMermaidViewer = () => {
if (mermaidViewer) {
mermaidViewer.destroy()
mermaidViewer = null
}
if (mermaidViewerWrapper) {
mermaidViewerWrapper.remove()
mermaidViewerWrapper = null
}
if (mermaidViewerObjectUrl) {
URL.revokeObjectURL(mermaidViewerObjectUrl)
mermaidViewerObjectUrl = null
}
document.body.classList.remove('mermaid-viewer-open')
document.body.classList.remove('viewer-ready')
}
const openMermaidViewer = (container) => {
const svg = container.querySelector('svg')
if (!svg) return
cleanupMermaidViewer()
const serializer = new XMLSerializer()
let svgMarkup = serializer.serializeToString(svg)
if (!svgMarkup.includes('xmlns="http://www.w3.org/2000/svg"')) {
svgMarkup = svgMarkup.replace(
'<svg',
'<svg xmlns="http://www.w3.org/2000/svg"'
)
}
const blob = new Blob([svgMarkup], {
type: 'image/svg+xml;charset=utf-8'
})
mermaidViewerObjectUrl = URL.createObjectURL(blob)
mermaidViewerWrapper = document.createElement('div')
mermaidViewerWrapper.className = 'mermaid-viewer-source'
const previewImage = document.createElement('img')
previewImage.src = mermaidViewerObjectUrl
previewImage.alt = 'Mermaid diagram preview'
mermaidViewerWrapper.append(previewImage)
document.body.append(mermaidViewerWrapper)
mermaidViewer = new Viewer(mermaidViewerWrapper, {
button: true,
navbar: false,
title: false,
toolbar: true,
tooltip: true,
movable: true,
zoomable: true,
rotatable: false,
scalable: false,
transition: false,
fullscreen: true,
keyboard: true,
url: 'src',
shown() {
document.body.classList.add('mermaid-viewer-open')
document.body.classList.add('viewer-ready')
},
viewed() {
requestAnimationFrame(() => {
const imageData = mermaidViewer?.imageData
const viewerData = mermaidViewer?.viewerData
if (!imageData || !viewerData) return
const widthScale = (viewerData.width * 0.94) / imageData.width
const heightScale = (viewerData.height * 0.94) / imageData.height
const targetScale = Math.min(widthScale, heightScale)
if (targetScale > 1.02) {
mermaidViewer.zoomTo(imageData.ratio * targetScale, false)
}
})
},
hidden() {
cleanupMermaidViewer()
}
})
mermaidViewer.view(0)
}
const initRenderedMermaidFeatures = async (force = false) => {
await renderMermaidDiagrams(force)
}
const getCodeToggleLabels = () => {
const isChineseRoute =
route.path.startsWith('/zh-cn/') || route.path.startsWith('/zh-tw/')
return isChineseRoute
? {
expand: '展开代码',
collapse: '收起代码'
}
: {
expand: 'Expand code',
collapse: 'Collapse code'
}
}
const getCodeLineCount = (source) => {
const normalized = source.replace(/\s+$/, '')
if (!normalized) return 0
return normalized.split('\n').length
}
const updateCodeToggleButton = (block, button, lineCount) => {
const labels = getCodeToggleLabels()
const isCollapsed = block.classList.contains('is-code-collapsed')
const nextLabel = isCollapsed ? labels.expand : labels.collapse
button.textContent = `${nextLabel} (${lineCount} 行)`
button.setAttribute('aria-expanded', String(!isCollapsed))
button.setAttribute('title', nextLabel)
}
const initCollapsibleCodeBlocks = () => {
const codeBlocks = document.querySelectorAll(
'.vp-doc div[class*="language-"]:not(.language-mermaid)'
)
codeBlocks.forEach((block) => {
const pre = block.querySelector('pre')
const code = pre?.querySelector('code')
if (!pre || !code) return
const lineCount = getCodeLineCount(code.textContent ?? '')
const existingToggle = block.querySelector('.code-collapse-toggle')
if (lineCount < COLLAPSIBLE_CODE_MIN_LINES) {
block.classList.remove('is-collapsible-code', 'is-code-collapsed')
existingToggle?.remove()
return
}
block.classList.add('is-collapsible-code')
let toggle = existingToggle
if (!toggle) {
toggle = document.createElement('button')
toggle.type = 'button'
toggle.className = 'code-collapse-toggle'
toggle.addEventListener('click', () => {
block.classList.toggle('is-code-collapsed')
updateCodeToggleButton(block, toggle, lineCount)
})
block.append(toggle)
}
block.classList.add('is-code-collapsed')
updateCodeToggleButton(block, toggle, lineCount)
})
}
const initViewer = () => {
// 销毁旧实例
if (viewer) {
@@ -1827,20 +2074,46 @@ export default {
}
}
onMounted(() => {
onMounted(async () => {
initViewer()
initTypewriter()
optimizeImages()
await initRenderedMermaidFeatures(true)
initCollapsibleCodeBlocks()
themeObserver = new MutationObserver(() => {
const nextTheme = getMermaidTheme()
if (nextTheme === currentMermaidTheme) return
nextTick(async () => {
await initRenderedMermaidFeatures(true)
})
})
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
})
})
watch(
() => route.path,
() =>
nextTick(() => {
nextTick(async () => {
cleanupMermaidViewer()
initViewer()
initTypewriter()
optimizeImages()
await initRenderedMermaidFeatures(true)
initCollapsibleCodeBlocks()
})
)
onBeforeUnmount(() => {
cleanupMermaidViewer()
if (themeObserver) {
themeObserver.disconnect()
themeObserver = null
}
})
}
}
+132
View File
@@ -460,3 +460,135 @@
white-space: nowrap;
flex-shrink: 0;
}
/* Mermaid diagrams */
.vp-doc .mermaid-diagram {
margin: 18px 0 22px;
padding: 0;
overflow-x: auto;
position: relative;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
cursor: zoom-in;
}
.vp-doc .mermaid-diagram svg {
display: block;
max-width: 100%;
height: auto;
margin: 0 auto;
}
.vp-doc .mermaid-diagram-error {
min-height: 80px;
}
.dark .vp-doc .mermaid-diagram {
border: 0;
background: transparent;
}
.vp-doc .mermaid-diagram:focus-visible {
outline: 2px solid var(--vp-c-brand-1);
outline-offset: 3px;
}
.mermaid-viewer-source {
position: fixed;
width: 0;
height: 0;
overflow: hidden;
opacity: 0;
pointer-events: none;
}
body.mermaid-viewer-open .viewer-backdrop {
background: rgba(245, 247, 250, 0.96);
}
body.mermaid-viewer-open .viewer-canvas {
background: rgba(248, 250, 252, 0.98) !important;
}
body.mermaid-viewer-open .viewer-canvas img {
background: transparent;
border-radius: 0;
box-shadow: none;
}
/* Long code blocks */
.vp-doc div[class*='language-'].is-collapsible-code {
position: relative;
}
.vp-doc div[class*='language-'].is-collapsible-code pre {
position: relative;
transition: max-height 0.24s ease;
}
.vp-doc div[class*='language-'].is-collapsible-code.is-code-collapsed pre {
max-height: 320px;
overflow-x: auto;
overflow-y: hidden;
}
.vp-doc
div[class*='language-'].is-collapsible-code.is-code-collapsed
pre::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 88px;
pointer-events: none;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0),
var(--vp-code-block-bg) 72%
);
}
.vp-doc .code-collapse-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 132px;
margin: 12px auto 2px;
padding: 7px 14px;
border: 1px solid rgba(0, 113, 227, 0.2);
border-radius: 999px;
background: rgba(0, 113, 227, 0.06);
color: var(--vp-c-brand-1);
font-size: 13px;
font-weight: 600;
line-height: 1;
cursor: pointer;
transition:
background-color 0.2s ease,
border-color 0.2s ease,
transform 0.2s ease;
}
.vp-doc .code-collapse-toggle:hover {
border-color: rgba(0, 113, 227, 0.32);
background: rgba(0, 113, 227, 0.1);
transform: translateY(-1px);
}
.vp-doc .code-collapse-toggle:focus-visible {
outline: 2px solid var(--vp-c-brand-1);
outline-offset: 2px;
}
.dark .vp-doc .code-collapse-toggle {
border-color: rgba(96, 165, 250, 0.24);
background: rgba(96, 165, 250, 0.12);
}
.dark .vp-doc .code-collapse-toggle:hover {
border-color: rgba(96, 165, 250, 0.38);
background: rgba(96, 165, 250, 0.18);
}