feat(docs): add new assignment projects and update existing content
This commit is contained in:
@@ -1433,7 +1433,7 @@ Sitemap: ${siteUrl}/sitemap.xml
|
||||
link: '/zh-cn/stage-2/frontend/2.1-figma-mastergo/'
|
||||
},
|
||||
{
|
||||
text: '参考 UI 设计规范与多产品 UI 设计',
|
||||
text: '参考 UI 设计规范设计页面和按钮',
|
||||
link: '/zh-cn/stage-2/frontend/2.3-multi-product-ui/'
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user