feat(docs): add interactive demo components for technical appendices

Add placeholder Vue components for visualizing technical concepts across multiple domains including frontend routing, browser rendering, cache design, queue design, database principles, API design, cloud services, and backend evolution. These components provide interactive educational content for the documentation.

Update documentation structure to include new appendix sections and enhance existing content with visual components. Remove unused 'codex' dependency from package.json.
This commit is contained in:
sanbuphy
2026-02-06 03:34:50 +08:00
parent e8bba6f7c0
commit 7c70c37072
171 changed files with 69830 additions and 6689 deletions
@@ -0,0 +1,366 @@
<template>
<div class="dynamic-routes-demo">
<div class="demo-header">
<h4>动态路由与参数</h4>
<p class="demo-desc">探索动态参数正则匹配和可选参数的使用方式</p>
</div>
<div class="demo-content">
<!-- 路由参数类型说明 -->
<div class="param-types">
<div
v-for="type in paramTypes"
:key="type.name"
:class="['param-card', { active: selectedType === type.name }]"
@click="selectedType = type.name"
>
<div class="param-pattern">{{ type.pattern }}</div>
<div class="param-name">{{ type.name }}</div>
<div class="param-desc">{{ type.description }}</div>
</div>
</div>
<!-- 参数解析演示 -->
<div class="parsing-demo">
<div class="demo-section">
<h5>测试路径</h5>
<div class="input-group">
<span class="input-prefix">/</span>
<input
v-model="testPath"
type="text"
placeholder="user/123/profile"
class="demo-input"
@input="parsePath"
>
</div>
</div>
<div class="demo-section">
<h5>匹配结果</h5>
<div v-if="parseResult" class="result-box">
<div class="result-row">
<span class="result-label">匹配路由:</span>
<code class="result-value">{{ parseResult.route }}</code>
</div>
<div v-if="Object.keys(parseResult.params).length" class="result-params">
<span class="result-label">提取参数:</span>
<div class="params-grid">
<div
v-for="(value, key) in parseResult.params"
:key="key"
class="param-tag"
>
<span class="param-key">{{ key }}</span>
<span class="param-eq">=</span>
<span class="param-val">{{ value }}</span>
</div>
</div>
</div>
</div>
<div v-else class="no-result">
输入路径查看解析结果
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selectedType = ref('required')
const testPath = ref('user/123/profile')
const paramTypes = [
{
name: 'required',
pattern: ':id',
description: '必填参数,URL中必须有对应的值',
example: '/user/123'
},
{
name: 'optional',
pattern: ':id?',
description: '可选参数,可以省略',
example: '/user 或 /user/123'
},
{
name: 'multiple',
pattern: ':id+',
description: '一个或多个,至少有一个值',
example: '/files/a 或 /files/a/b/c'
},
{
name: 'zeroOrMore',
pattern: ':id*',
description: '零个或多个,可以没有',
example: '/tags 或 /tags/vue/router'
}
]
// 模拟的路由配置
const routePatterns = [
{ pattern: '/user/:id', name: 'UserDetail' },
{ pattern: '/user/:id/profile', name: 'UserProfile' },
{ pattern: '/user/:id/:tab', name: 'UserTab' },
{ pattern: '/products/:category/:id', name: 'ProductDetail' },
{ pattern: '/search/:keyword?', name: 'Search' },
{ pattern: '/files/:path*', name: 'FileBrowser' }
]
const parsePath = () => {
const path = testPath.value.trim()
if (!path) return null
// 简化的匹配逻辑
for (const route of routePatterns) {
const match = matchRoute(route.pattern, path)
if (match) {
return {
route: route.pattern,
params: match
}
}
}
return null
}
const matchRoute = (pattern, path) => {
// 将 :param 转换为正则
const regexPattern = pattern
.replace(/:([^/]+)\*/g, '(.*)') // :path* → (.*)
.replace(/:([^/]+)\?/g, '([^/]*)') // :keyword? → ([^/]*)
.replace(/:([^/]+)/g, '([^/]+)') // :id → ([^/]+)
const regex = new RegExp(`^${regexPattern}$`)
const match = path.match(regex)
if (!match) return null
// 提取参数名
const paramNames = []
const paramRegex = /:([^/]+)/g
let paramMatch
while ((paramMatch = paramRegex.exec(pattern)) !== null) {
paramNames.push(paramMatch[1].replace(/[?*+]$/, ''))
}
// 构建参数对象
const params = {}
paramNames.forEach((name, index) => {
params[name] = match[index + 1]
})
return params
}
const parseResult = computed(() => parsePath())
// 初始化
parsePath()
</script>
<style scoped>
.dynamic-routes-demo {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
}
.demo-header {
text-align: center;
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.demo-desc {
margin: 0;
color: var(--vp-c-text-2);
font-size: 14px;
}
.demo-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.param-types {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.param-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
cursor: pointer;
transition: all 0.2s;
}
.param-card:hover {
border-color: var(--vp-c-brand);
}
.param-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.param-pattern {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 16px;
font-weight: 600;
color: var(--vp-c-brand);
margin-bottom: 8px;
}
.param-name {
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.param-desc {
font-size: 12px;
color: var(--vp-c-text-2);
line-height: 1.4;
}
.parsing-demo {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.demo-section {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
}
.demo-section h5 {
margin: 0 0 12px 0;
font-size: 13px;
color: var(--vp-c-text-2);
font-weight: 500;
}
.input-group {
display: flex;
align-items: center;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.input-prefix {
padding: 10px 8px 10px 12px;
color: var(--vp-c-text-3);
font-family: monospace;
font-size: 14px;
}
.demo-input {
flex: 1;
border: none;
background: transparent;
padding: 10px 12px 10px 0;
font-size: 14px;
color: var(--vp-c-text-1);
outline: none;
font-family: monospace;
}
.result-box {
display: flex;
flex-direction: column;
gap: 12px;
}
.result-row {
display: flex;
align-items: center;
gap: 8px;
}
.result-label {
font-size: 12px;
color: var(--vp-c-text-3);
min-width: 60px;
}
.result-value {
font-size: 13px;
color: var(--vp-c-text-1);
font-family: monospace;
background: var(--vp-c-bg-soft);
padding: 4px 8px;
border-radius: 4px;
}
.result-params {
padding-top: 12px;
border-top: 1px solid var(--vp-c-divider);
}
.params-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.param-tag {
display: flex;
align-items: center;
gap: 4px;
background: var(--vp-c-brand-soft);
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
}
.param-key {
color: var(--vp-c-brand);
font-weight: 500;
}
.param-eq {
color: var(--vp-c-text-3);
}
.param-val {
color: var(--vp-c-text-1);
}
.no-result {
text-align: center;
padding: 32px;
color: var(--vp-c-text-3);
font-size: 13px;
}
@media (max-width: 768px) {
.param-types {
grid-template-columns: 1fr;
}
.parsing-demo {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,445 @@
<template>
<div class="hash-vs-history-demo">
<div class="demo-header">
<h4>Hash vs History 模式对比</h4>
<p class="demo-desc">直观对比两种主流路由模式的URL变化浏览器行为和兼容性</p>
</div>
<div class="comparison-container">
<!-- Hash Mode -->
<div class="mode-column">
<div class="mode-header hash">
<span class="mode-icon">#</span>
<span class="mode-title">Hash 模式</span>
</div>
<div class="browser-mockup">
<div class="browser-toolbar">
<div class="window-controls">
<span class="dot red"></span>
<span class="dot yellow"></span>
<span class="dot green"></span>
</div>
<div class="address-bar">
<span class="protocol">https://</span>
<span class="host">example.com</span>
<span class="hash-path">/#/{{ hashPath }}</span>
</div>
</div>
<div class="browser-viewport">
<nav class="nav-bar">
<a
v-for="item in navItems"
:key="item.path"
:class="['nav-item', { active: hashPath === item.path }]"
@click="hashPath = item.path"
>
{{ item.name }}
</a>
</nav>
<div class="page-content">
<h3>{{ getPageTitle(hashPath) }}</h3>
<p>{{ getPageContent(hashPath) }}</p>
</div>
</div>
</div>
<div class="characteristics">
<div class="char-item">
<span class="char-label">URL格式</span>
<code>/#/path</code>
</div>
<div class="char-item">
<span class="char-label">浏览器兼容</span>
<span class="badge good">IE8+</span>
</div>
<div class="char-item">
<span class="char-label">服务端配置</span>
<span class="badge good">无需配置</span>
</div>
<div class="char-item">
<span class="char-label">SEO友好度</span>
<span class="badge bad">较差</span>
</div>
</div>
</div>
<!-- History Mode -->
<div class="mode-column">
<div class="mode-header history">
<span class="mode-icon">/</span>
<span class="mode-title">History 模式</span>
</div>
<div class="browser-mockup">
<div class="browser-toolbar">
<div class="window-controls">
<span class="dot red"></span>
<span class="dot yellow"></span>
<span class="dot green"></span>
</div>
<div class="address-bar">
<span class="protocol">https://</span>
<span class="host">example.com</span>
<span class="history-path">/{{ historyPath }}</span>
</div>
</div>
<div class="browser-viewport">
<nav class="nav-bar">
<a
v-for="item in navItems"
:key="item.path"
:class="['nav-item', { active: historyPath === item.path }]"
@click="historyPath = item.path"
>
{{ item.name }}
</a>
</nav>
<div class="page-content">
<h3>{{ getPageTitle(historyPath) }}</h3>
<p>{{ getPageContent(historyPath) }}</p>
</div>
</div>
</div>
<div class="characteristics">
<div class="char-item">
<span class="char-label">URL格式</span>
<code>/path</code>
</div>
<div class="char-item">
<span class="char-label">浏览器兼容</span>
<span class="badge medium">IE10+</span>
</div>
<div class="char-item">
<span class="char-label">服务端配置</span>
<span class="badge warn">需要配置</span>
</div>
<div class="char-item">
<span class="char-label">SEO友好度</span>
<span class="badge good">良好</span>
</div>
</div>
</div>
</div>
<div class="summary-section">
<h4>如何选择</h4>
<div class="decision-tree">
<div class="decision-item">
<span class="decision-q">需要支持IE8/9</span>
<span class="decision-a"> Hash 模式</span>
</div>
<div class="decision-item">
<span class="decision-q">重视SEO</span>
<span class="decision-a"> History 模式</span>
</div>
<div class="decision-item">
<span class="decision-q">无法修改服务端配置</span>
<span class="decision-a"> Hash 模式</span>
</div>
<div class="decision-item">
<span class="decision-q">追求URL美观</span>
<span class="decision-a"> History 模式</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const hashPath = ref('home')
const historyPath = ref('home')
const navItems = [
{ name: '首页', path: 'home' },
{ name: '产品', path: 'products' },
{ name: '关于', path: 'about' }
]
const getPageTitle = (path) => {
const titles = {
home: '首页',
products: '产品中心',
about: '关于我们'
}
return titles[path] || '首页'
}
const getPageContent = (path) => {
const contents = {
home: '欢迎来到我们的网站!这是SPA的首页,所有页面切换都在前端完成,无需刷新。',
products: '这里展示了我们的核心产品系列。SPA让浏览体验更流畅,切换更快。',
about: '了解更多关于我们的故事。SPA模式下,页面间跳转几乎没有延迟。'
}
return contents[path] || contents.home
}
</script>
<style scoped>
.hash-vs-history-demo {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.demo-desc {
margin: 0;
color: var(--vp-c-text-2);
font-size: 14px;
}
.comparison-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-bottom: 24px;
}
.mode-column {
background: var(--vp-c-bg);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.mode-header {
padding: 16px;
display: flex;
align-items: center;
gap: 10px;
}
.mode-header.hash {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.mode-header.history {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.mode-icon {
font-size: 20px;
font-weight: bold;
}
.mode-title {
font-size: 16px;
font-weight: 600;
}
.browser-mockup {
border-bottom: 1px solid var(--vp-c-divider);
}
.browser-toolbar {
background: var(--vp-c-bg-soft);
padding: 10px 12px;
display: flex;
align-items: center;
gap: 10px;
}
.window-controls {
display: flex;
gap: 6px;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.dot.red { background: #ff5f56; }
.dot.yellow { background: #ffbd2e; }
.dot.green { background: #27c93f; }
.address-bar {
flex: 1;
background: var(--vp-c-bg);
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
font-family: 'Monaco', 'Menlo', monospace;
}
.protocol, .host { color: var(--vp-c-text-3); }
.hash-path { color: #e06c75; font-weight: 500; }
.history-path { color: #61afef; font-weight: 500; }
.browser-viewport {
display: flex;
min-height: 160px;
}
.nav-bar {
width: 80px;
background: var(--vp-c-bg-soft);
padding: 12px 0;
border-right: 1px solid var(--vp-c-divider);
}
.nav-item {
display: block;
padding: 8px 12px;
color: var(--vp-c-text-2);
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.nav-item:hover {
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
}
.nav-item.active {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand);
border-right: 2px solid var(--vp-c-brand);
}
.page-content {
flex: 1;
padding: 16px;
}
.page-content h3 {
margin: 0 0 8px 0;
font-size: 16px;
color: var(--vp-c-text-1);
}
.page-content p {
margin: 0;
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.characteristics {
padding: 16px;
}
.char-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid var(--vp-c-divider);
}
.char-item:last-child {
border-bottom: none;
}
.char-label {
font-size: 13px;
color: var(--vp-c-text-2);
}
.char-item code {
background: var(--vp-c-bg-soft);
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
}
.badge {
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
}
.badge.good {
background: rgba(39, 201, 63, 0.15);
color: #27c93f;
}
.badge.medium {
background: rgba(255, 189, 46, 0.15);
color: #ffbd2e;
}
.badge.warn {
background: rgba(255, 149, 0, 0.15);
color: #ff9500;
}
.badge.bad {
background: rgba(255, 95, 86, 0.15);
color: #ff5f56;
}
.summary-section {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 20px;
margin-top: 24px;
}
.summary-section h4 {
margin: 0 0 16px 0;
color: var(--vp-c-text-1);
font-size: 16px;
}
.decision-tree {
display: grid;
gap: 12px;
}
.decision-item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 16px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border-left: 3px solid var(--vp-c-brand);
}
.decision-q {
font-size: 14px;
color: var(--vp-c-text-2);
flex: 1;
}
.decision-a {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-brand);
white-space: nowrap;
}
@media (max-width: 900px) {
.comparison-container {
grid-template-columns: 1fr;
}
.decision-item {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}
</style>
@@ -0,0 +1,176 @@
<template>
<div class="mpa-routing-demo">
<div class="demo-header">
<h4>MPA vs SPA 路由对比</h4>
<p class="demo-desc">对比多页面应用和单页面应用的路由机制差异</p>
</div>
<div class="comparison-table">
<div class="table-header">
<div class="col feature">特性</div>
<div class="col mpa">MPA (多页面)</div>
<div class="col spa">SPA (单页面)</div>
</div>
<div class="table-row" v-for="(row, index) in comparisonData" :key="index">
<div class="col feature">{{ row.feature }}</div>
<div class="col mpa">{{ row.mpa }}</div>
<div class="col spa">{{ row.spa }}</div>
</div>
</div>
<div class="flow-comparison">
<div class="flow-box mpa-flow">
<h5>MPA 导航流程</h5>
<div class="flow-steps">
<div class="step">1. 用户点击链接</div>
<div class="step">2. 浏览器发送 HTTP 请求</div>
<div class="step">3. 服务器返回完整 HTML</div>
<div class="step">4. 浏览器解析并渲染新页面</div>
<div class="step">5. 页面资源重新加载 (JS/CSS)</div>
</div>
</div>
<div class="flow-box spa-flow">
<h5>SPA 导航流程</h5>
<div class="flow-steps">
<div class="step">1. 用户点击链接</div>
<div class="step">2. 拦截默认行为 (preventDefault)</div>
<div class="step">3. 更新 URL (History API)</div>
<div class="step">4. 匹配路由配置</div>
<div class="step">5. 动态渲染新组件</div>
<div class="step">6. 页面无刷新更新</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
const comparisonData = [
{ feature: '页面加载', mpa: '每次跳转加载完整页面', spa: '首次加载后只更新内容' },
{ feature: 'URL 变化', mpa: '浏览器地址栏正常变化', spa: 'History API 控制 URL' },
{ feature: '用户体验', mpa: '页面有白屏闪烁', spa: '过渡流畅无刷新' },
{ feature: 'SEO 友好', mpa: '天生对搜索引擎友好', spa: '需要 SSR/预渲染优化' },
{ feature: '首屏时间', mpa: '较快(只加载当前页)', spa: '较慢(需加载完整应用)' },
{ feature: '服务端压力', mpa: '较高(每次请求都渲染)', spa: '较低(大部分逻辑在客户端)' },
{ feature: '开发复杂度', mpa: '简单,传统开发模式', spa: '较复杂,需理解前端路由' }
]
</script>
<style scoped>
.mpa-routing-demo {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
}
.demo-header {
text-align: center;
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.demo-desc {
margin: 0;
color: var(--vp-c-text-2);
font-size: 14px;
}
.comparison-table {
background: var(--vp-c-bg);
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--vp-c-divider);
margin-bottom: 20px;
}
.table-header {
display: grid;
grid-template-columns: 1fr 1.2fr 1.2fr;
background: var(--vp-c-bg-soft);
font-weight: 600;
font-size: 13px;
}
.table-row {
display: grid;
grid-template-columns: 1fr 1.2fr 1.2fr;
border-top: 1px solid var(--vp-c-divider);
}
.col {
padding: 12px 16px;
font-size: 13px;
}
.col.feature {
font-weight: 500;
color: var(--vp-c-text-1);
}
.col.mpa {
color: var(--vp-c-text-2);
}
.col.spa {
color: var(--vp-c-brand);
}
.flow-comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.flow-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border: 1px solid var(--vp-c-divider);
}
.flow-box h5 {
margin: 0 0 16px 0;
font-size: 14px;
color: var(--vp-c-text-1);
}
.flow-steps {
display: flex;
flex-direction: column;
gap: 8px;
}
.step {
padding: 10px 12px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
font-size: 12px;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand);
}
.mpa-flow .step {
border-left-color: #888;
}
.spa-flow .step {
border-left-color: var(--vp-c-brand);
}
@media (max-width: 768px) {
.table-header,
.table-row {
grid-template-columns: 1fr;
}
.flow-comparison {
grid-template-columns: 1fr;
}
}
</style>
</content>
@@ -0,0 +1,492 @@
<template>
<div class="nested-routes-demo">
<div class="demo-header">
<h4>嵌套路由可视化</h4>
<p class="demo-desc">点击不同层级观察嵌套路由的渲染位置和层级关系</p>
</div>
<div class="demo-container">
<!-- 路由层级可视化 -->
<div class="routes-hierarchy">
<div class="tree-view">
<div
v-for="node in treeData"
:key="node.path"
class="tree-node"
:style="{ paddingLeft: `${node.level * 24}px` }"
@click="selectNode(node)"
>
<div
:class="[
'node-content',
{ active: currentPath === node.path },
{ 'has-children': node.children?.length }
]"
>
<span class="node-icon">{{ node.children?.length ? '📁' : '📄' }}</span>
<span class="node-path">{{ node.name }}</span>
<code class="node-route">{{ node.path || '/' }}</code>
</div>
</div>
</div>
</div>
<!-- 渲染区域预览 -->
<div class="render-preview">
<div class="preview-header">
<h5>渲染视图</h5>
<span class="current-path">{{ currentPath || '/' }}</span>
</div>
<div class="router-view-hierarchy">
<div
v-for="(route, index) in activeRouteChain"
:key="route.path"
class="router-view-level"
:style="{ marginLeft: `${index * 20}px` }"
>
<div class="router-view-box">
<div class="view-label">
<span class="view-icon">🔲</span>
<span class="view-name">{{ route.name }}</span>
</div>
<div class="view-path">{{ route.path }}</div>
</div>
</div>
</div>
<div class="breadcrumb">
<span
v-for="(crumb, index) in breadcrumbs"
:key="index"
class="breadcrumb-item"
@click="navigateTo(crumb.path)"
>
{{ crumb.name }}
<span v-if="index < breadcrumbs.length - 1" class="separator">/</span>
</span>
</div>
</div>
</div>
<!-- 代码示例 -->
<div class="code-section">
<h5>路由配置示例</h5>
<pre class="code-block"><code>{{ routeConfigCode }}</code></pre>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentPath = ref('/dashboard')
const routeConfig = [
{
path: '/',
name: 'Layout',
component: 'Layout',
children: [
{
path: '',
name: 'Home',
component: 'Home'
},
{
path: 'dashboard',
name: 'Dashboard',
component: 'Dashboard'
},
{
path: 'users',
name: 'Users',
component: 'UserLayout',
children: [
{
path: '',
name: 'UserList',
component: 'UserList'
},
{
path: ':id',
name: 'UserDetail',
component: 'UserDetail'
},
{
path: ':id/edit',
name: 'UserEdit',
component: 'UserEdit'
}
]
},
{
path: 'products',
name: 'Products',
component: 'ProductLayout',
children: [
{
path: '',
name: 'ProductList',
component: 'ProductList'
},
{
path: 'category/:categoryId',
name: 'ProductCategory',
component: 'ProductCategory'
}
]
},
{
path: 'settings',
name: 'Settings',
component: 'Settings',
children: [
{
path: 'profile',
name: 'ProfileSettings',
component: 'ProfileSettings'
},
{
path: 'security',
name: 'SecuritySettings',
component: 'SecuritySettings'
}
]
}
]
}
]
// 扁平化路由,添加层级信息
const flattenRoutes = (routes, level = 0, parentPath = '') => {
const result = []
routes.forEach(route => {
const fullPath = route.path
? `${parentPath}/${route.path}`.replace(/\/+/g, '/')
: parentPath || '/'
const node = {
...route,
fullPath,
level,
children: []
}
if (route.children?.length) {
node.children = flattenRoutes(route.children, level + 1, fullPath)
}
result.push(node)
})
return result
}
const treeData = computed(() => {
const flatten = (routes, level = 0) => {
const result = []
routes.forEach(route => {
const node = {
name: route.name,
path: route.path || '/',
fullPath: route.fullPath,
level,
component: route.component,
children: route.children?.length ? flatten(route.children, level + 1) : null
}
result.push(node)
})
return result
}
return flatten(flattenRoutes(routeConfig))
})
const activeRouteChain = computed(() => {
const findChain = (routes, target, chain = []) => {
for (const route of routes) {
const currentChain = [...chain, route]
if (route.path === target || route.fullPath === target) {
return currentChain
}
if (route.children?.length) {
const found = findChain(route.children, target, currentChain)
if (found) return found
}
}
return null
}
return findChain(flattenRoutes(routeConfig), currentPath.value) || []
})
const breadcrumbs = computed(() => {
return activeRouteChain.value.map(route => ({
name: route.name,
path: route.fullPath || route.path
}))
})
const routeConfigCode = computed(() => `const routes = [
{
path: '/',
component: Layout,
children: [
{ path: 'dashboard', component: Dashboard },
{
path: 'users',
component: UserLayout,
children: [
{ path: '', component: UserList },
{ path: ':id', component: UserDetail },
{ path: ':id/edit', component: UserEdit }
]
},
{
path: 'settings',
component: Settings,
children: [
{ path: 'profile', component: ProfileSettings },
{ path: 'security', component: SecuritySettings }
]
}
]
}
]`)
const selectNode = (node) => {
currentPath.value = node.fullPath || node.path
}
const navigateTo = (path) => {
currentPath.value = path
}
</script>
<style scoped>
.nested-routes-demo {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
}
.demo-header {
text-align: center;
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.demo-desc {
margin: 0;
color: var(--vp-c-text-2);
font-size: 14px;
}
.demo-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.routes-hierarchy {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
border: 1px solid var(--vp-c-divider);
}
.tree-view {
max-height: 350px;
overflow-y: auto;
}
.tree-node {
margin: 2px 0;
}
.node-content {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.node-content:hover {
background: var(--vp-c-bg-soft);
}
.node-content.active {
background: var(--vp-c-brand-soft);
border: 1px solid var(--vp-c-brand);
}
.node-icon {
font-size: 14px;
}
.node-path {
font-size: 13px;
font-weight: 500;
color: var(--vp-c-text-1);
}
.node-route {
margin-left: auto;
font-size: 11px;
color: var(--vp-c-text-3);
font-family: monospace;
background: var(--vp-c-bg-soft);
padding: 2px 6px;
border-radius: 4px;
}
.render-preview {
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
overflow: hidden;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.preview-header h5 {
margin: 0;
font-size: 13px;
color: var(--vp-c-text-1);
}
.current-path {
font-size: 12px;
color: var(--vp-c-text-3);
font-family: monospace;
background: var(--vp-c-bg);
padding: 2px 8px;
border-radius: 4px;
}
.router-view-hierarchy {
padding: 16px;
min-height: 200px;
}
.router-view-level {
margin-bottom: 8px;
}
.router-view-box {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 12px;
}
.view-label {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
}
.view-icon {
font-size: 12px;
}
.view-name {
font-size: 13px;
font-weight: 500;
color: var(--vp-c-text-1);
}
.view-path {
font-size: 11px;
color: var(--vp-c-text-3);
font-family: monospace;
}
.breadcrumb {
display: flex;
align-items: center;
gap: 4px;
padding: 12px 16px;
background: var(--vp-c-bg-soft);
border-top: 1px solid var(--vp-c-divider);
overflow-x: auto;
}
.breadcrumb-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--vp-c-text-2);
cursor: pointer;
white-space: nowrap;
}
.breadcrumb-item:hover {
color: var(--vp-c-brand);
}
.separator {
color: var(--vp-c-text-3);
margin: 0 2px;
}
.code-section {
margin-top: 20px;
padding: 20px;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.code-section h5 {
margin: 0 0 16px 0;
font-size: 14px;
color: var(--vp-c-text-1);
}
.code-block {
background: #1e1e1e;
color: #d4d4d4;
padding: 16px;
border-radius: 6px;
overflow-x: auto;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 13px;
line-height: 1.5;
margin: 0;
}
@media (max-width: 768px) {
.demo-container {
grid-template-columns: 1fr;
}
.breadcrumb {
flex-wrap: wrap;
}
}
</style>
@@ -0,0 +1,333 @@
<template>
<div class="route-guards-demo">
<div class="demo-header">
<h4>路由守卫机制</h4>
<p class="demo-desc">了解全局守卫路由独享守卫和组件内守卫的执行顺序和用途</p>
</div>
<div class="guards-container">
<div class="guard-type" v-for="guard in guardTypes" :key="guard.name">
<div class="guard-header" :class="guard.type">
<span class="guard-icon">{{ guard.icon }}</span>
<span class="guard-title">{{ guard.name }}</span>
</div>
<div class="guard-content">
<p class="guard-desc">{{ guard.description }}</p>
<div class="guard-example">
<code>{{ guard.example }}</code>
</div>
</div>
</div>
</div>
<div class="execution-flow">
<h5>守卫执行顺序</h5>
<div class="flow-chart">
<div class="flow-step" v-for="(step, index) in executionSteps" :key="index">
<div class="step-number">{{ index + 1 }}</div>
<div class="step-content">
<div class="step-name">{{ step.name }}</div>
<div class="step-desc">{{ step.description }}</div>
</div>
<div v-if="index < executionSteps.length - 1" class="flow-arrow"></div>
</div>
</div>
</div>
</div>
</template>
<script setup>
const guardTypes = [
{
name: '全局前置守卫',
type: 'global',
icon: '🌍',
description: '在路由跳转前执行,常用于权限验证、登录检查等',
example: `router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !isLoggedIn()) {
next('/login')
} else {
next()
}
})`
},
{
name: '全局解析守卫',
type: 'global',
icon: '🔍',
description: '在导航被确认之前、组件内守卫和异步路由组件被解析之后调用',
example: `router.beforeResolve((to, from, next) => {
// 可以在这里做数据预加载
next()
})`
},
{
name: '全局后置钩子',
type: 'global',
icon: '✅',
description: '在导航完成后执行,不接受 next 函数,不能改变导航',
example: `router.afterEach((to, from) => {
// 设置页面标题
document.title = to.meta.title || '默认标题'
// 发送页面浏览统计
analytics.track(to.path)
})`
},
{
name: '路由独享守卫',
type: 'route',
icon: '🛣️',
description: '在单个路由配置中定义,只在进入该路由时触发',
example: `{
path: '/admin',
component: Admin,
beforeEnter: (to, from, next) => {
if (!isAdmin()) {
next('/unauthorized')
} else {
next()
}
}
}`
},
{
name: '组件内守卫',
type: 'component',
icon: '🔧',
description: '在组件内部定义,可以访问组件实例 this',
example: `export default {
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 this
next(vm => {
// 通过 vm 访问组件实例
})
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但该组件被复用时调用
// 可以访问组件实例 this
this.name = to.params.name
next()
},
beforeRouteLeave(to, from, next) {
// 在导航离开渲染该组件的对应路由时调用
// 可以访问组件实例 this
const answer = window.confirm('确定要离开吗?未保存的更改将丢失。')
if (answer) {
next()
} else {
next(false)
}
}
}`
}
]
const executionSteps = [
{
name: '导航触发',
description: '用户点击链接或调用 router.push()'
},
{
name: '组件内 beforeRouteLeave',
description: '在离开的组件中调用,可以取消导航'
},
{
name: '全局 beforeEach',
description: '全局前置守卫,常用于权限检查'
},
{
name: '路由独享 beforeEnter',
description: '在重用的组件中调用'
},
{
name: '组件内 beforeRouteEnter',
description: '在进入新组件前调用,此时组件实例还未创建'
},
{
name: '全局 beforeResolve',
description: '在导航被确认前调用,所有组件内守卫和异步组件已解析'
},
{
name: '全局 afterEach',
description: '导航完成后调用,常用于页面统计、标题设置'
},
{
name: 'DOM 更新',
description: '触发组件更新,渲染新页面'
}
]
const selectedType = ref('global')
const currentPath = ref('/dashboard')
</script>
<style scoped>
.route-guards-demo {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
}
.demo-header {
text-align: center;
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.demo-desc {
margin: 0;
color: var(--vp-c-text-2);
font-size: 14px;
}
.guards-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.guard-type {
background: var(--vp-c-bg);
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--vp-c-divider);
}
.guard-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
font-weight: 600;
color: white;
}
.guard-header.global {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.guard-header.route {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.guard-header.component {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.guard-icon {
font-size: 18px;
}
.guard-title {
font-size: 14px;
}
.guard-content {
padding: 16px;
}
.guard-desc {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.5;
margin-bottom: 12px;
}
.guard-example {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 12px;
overflow-x: auto;
}
.guard-example code {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 12px;
color: var(--vp-c-text-1);
line-height: 1.6;
white-space: pre;
}
.execution-flow {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border: 1px solid var(--vp-c-divider);
}
.execution-flow h5 {
margin: 0 0 16px 0;
font-size: 14px;
color: var(--vp-c-text-1);
}
.flow-chart {
display: flex;
flex-direction: column;
gap: 8px;
}
.flow-step {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border-left: 3px solid var(--vp-c-brand);
}
.step-number {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-brand);
color: white;
border-radius: 50%;
font-size: 12px;
font-weight: 600;
}
.step-content {
flex: 1;
}
.step-name {
font-size: 13px;
font-weight: 500;
color: var(--vp-c-text-1);
}
.step-desc {
font-size: 12px;
color: var(--vp-c-text-3);
margin-top: 2px;
}
.flow-arrow {
text-align: center;
color: var(--vp-c-text-3);
font-size: 14px;
}
@media (max-width: 768px) {
.guards-container {
grid-template-columns: 1fr;
}
.flow-step {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}
</style>
@@ -0,0 +1,607 @@
<template>
<div class="route-matching-demo">
<div class="demo-header">
<h4>路由匹配机制</h4>
<p class="demo-desc">输入URL路径查看路由是如何匹配和解析参数的</p>
</div>
<div class="demo-container">
<div class="input-section">
<div class="input-group">
<label>测试路径</label>
<div class="path-input-wrapper">
<span class="path-prefix">/</span>
<input
v-model="testPath"
type="text"
placeholder="user/123/posts"
class="path-input"
@keyup.enter="testMatch"
>
</div>
</div>
<button class="test-btn" @click="testMatch">
<span class="btn-icon"></span>
测试匹配
</button>
</div>
<div class="routes-section">
<div class="section-header">
<h5>已定义的路由</h5>
<span class="route-count">{{ routes.length }} </span>
</div>
<div class="routes-list">
<div
v-for="route in routes"
:key="route.path"
:class="['route-item', { matched: matchedRoute?.path === route.path }]"
>
<div class="route-path">
<span class="route-pattern">{{ route.path }}</span>
<span v-if="route.hasParams" class="param-badge">含参数</span>
</div>
<div class="route-name">{{ route.name }}</div>
</div>
</div>
</div>
</div>
<div v-if="matchResult" class="result-section">
<div class="result-header">
<h5>匹配结果</h5>
<span :class="['match-status', matchResult.matched ? 'success' : 'fail']">
{{ matchResult.matched ? '匹配成功' : '无匹配路由' }}
</span>
</div>
<div v-if="matchResult.matched" class="match-details">
<div class="detail-item">
<span class="detail-label">匹配路由</span>
<span class="detail-value code">{{ matchResult.route.path }}</span>
</div>
<div class="detail-item">
<span class="detail-label">路由名称</span>
<span class="detail-value">{{ matchResult.route.name }}</span>
</div>
<div v-if="Object.keys(matchResult.params).length > 0" class="params-section">
<div class="detail-label">路径参数</div>
<div class="params-list">
<div
v-for="(value, key) in matchResult.params"
:key="key"
class="param-item"
>
<span class="param-key">{{ key }}</span>
<span class="param-arrow"></span>
<span class="param-value">{{ value }}</span>
</div>
</div>
</div>
</div>
<div v-else class="no-match">
<div class="no-match-icon"></div>
<p>路径 "{{ testPath }}" 未匹配到任何路由</p>
<ul class="suggestions">
<li>检查路径拼写是否正确</li>
<li>确认路径是否以斜杠开头</li>
<li>查看是否缺少必要的参数</li>
</ul>
</div>
</div>
<div class="tips-section">
<h5>路由匹配规则速查</h5>
<div class="tips-grid">
<div class="tip-item">
<code>/user</code>
<span>精确匹配 /user</span>
</div>
<div class="tip-item">
<code>/user/:id</code>
<span>匹配 /user/123id=123</span>
</div>
<div class="tip-item">
<code>/user/:id?</code>
<span>id可选匹配 /user /user/123</span>
</div>
<div class="tip-item">
<code>/user/:id+</code>
<span>匹配一个或多个 /user/1/2</span>
</div>
<div class="tip-item">
<code>/user/:id*</code>
<span>匹配零个或多个</span>
</div>
<div class="tip-item">
<code>/user(.*)*</code>
<span>通配符匹配任意路径</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const testPath = ref('user/123/posts')
const matchResult = ref(null)
const matchedRoute = ref(null)
const routes = [
{ path: '/', name: '首页', hasParams: false },
{ path: '/user', name: '用户列表', hasParams: false },
{ path: '/user/:id', name: '用户详情', hasParams: true },
{ path: '/user/:id/posts', name: '用户文章', hasParams: true },
{ path: '/products', name: '产品列表', hasParams: false },
{ path: '/products/:category/:id', name: '产品详情', hasParams: true },
{ path: '/search', name: '搜索结果', hasParams: false },
{ path: '/:path(.*)*', name: '404页面', hasParams: true }
]
const parsePath = (path) => {
// 移除开头的斜杠
const cleanPath = path.replace(/^\//, '')
return cleanPath.split('/').filter(Boolean)
}
const matchPath = (routePath, testPath) => {
const routeParts = parsePath(routePath)
const testParts = parsePath(testPath)
const params = {}
for (let i = 0; i < routeParts.length; i++) {
const routePart = routeParts[i]
const testPart = testParts[i]
// 通配符匹配
if (routePart === '(.*)*' || routePart === ':path(.*)*') {
params['pathMatch'] = testParts.slice(i).join('/')
return { matched: true, params }
}
// 动态参数匹配
if (routePart.startsWith(':')) {
const paramName = routePart.replace(/^:/, '').replace(/\?$/, '')
const isOptional = routePart.endsWith('?')
if (testPart !== undefined) {
params[paramName] = testPart
continue
} else if (isOptional) {
continue
} else {
return { matched: false, params: {} }
}
}
// 精确匹配
if (routePart !== testPart) {
return { matched: false, params: {} }
}
}
// 检查是否有剩余的测试路径部分(除非是通配符路由)
if (testParts.length > routeParts.length) {
const lastRoutePart = routeParts[routeParts.length - 1]
if (!lastRoutePart || (!lastRoutePart.includes('*') && !lastRoutePart.endsWith('+'))) {
return { matched: false, params: {} }
}
}
return { matched: true, params }
}
const testMatch = () => {
if (!testPath.value.trim()) {
matchResult.value = { matched: false }
matchedRoute.value = null
return
}
let foundMatch = false
for (const route of routes) {
const { matched, params } = matchPath(route.path, testPath.value)
if (matched) {
matchResult.value = {
matched: true,
route,
params
}
matchedRoute.value = route
foundMatch = true
break
}
}
if (!foundMatch) {
matchResult.value = { matched: false }
matchedRoute.value = null
}
}
// 自动测试初始路径
testMatch()
</script>
<style scoped>
.route-matching-demo {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.demo-desc {
margin: 0;
color: var(--vp-c-text-2);
font-size: 14px;
}
.demo-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.input-section {
background: var(--vp-c-bg);
padding: 20px;
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.input-group {
margin-bottom: 16px;
}
.input-group label {
display: block;
margin-bottom: 8px;
font-size: 13px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.path-input-wrapper {
display: flex;
align-items: center;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.path-prefix {
padding: 10px 8px 10px 12px;
color: var(--vp-c-text-3);
font-family: monospace;
font-size: 14px;
}
.path-input {
flex: 1;
border: none;
background: transparent;
padding: 10px 12px 10px 0;
font-size: 14px;
color: var(--vp-c-text-1);
outline: none;
}
.test-btn {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 16px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.test-btn:hover {
background: var(--vp-c-brand-dark);
}
.btn-icon {
font-size: 10px;
}
.routes-section {
background: var(--vp-c-bg);
padding: 20px;
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.section-header h5 {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-1);
}
.route-count {
font-size: 12px;
color: var(--vp-c-text-3);
background: var(--vp-c-bg-soft);
padding: 2px 8px;
border-radius: 10px;
}
.routes-list {
max-height: 280px;
overflow-y: auto;
}
.route-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
border-radius: 6px;
margin-bottom: 4px;
transition: all 0.2s;
}
.route-item:hover {
background: var(--vp-c-bg-soft);
}
.route-item.matched {
background: rgba(66, 184, 131, 0.1);
border: 1px solid rgba(66, 184, 131, 0.3);
}
.route-path {
display: flex;
align-items: center;
gap: 8px;
}
.route-pattern {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 13px;
color: var(--vp-c-text-1);
}
.param-badge {
font-size: 10px;
color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
padding: 2px 6px;
border-radius: 4px;
}
.route-name {
font-size: 12px;
color: var(--vp-c-text-3);
}
.result-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border: 1px solid var(--vp-c-divider);
margin-top: 20px;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--vp-c-divider);
}
.result-header h5 {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-1);
}
.match-status {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.match-status.success {
background: rgba(39, 201, 63, 0.15);
color: #27c93f;
}
.match-status.fail {
background: rgba(255, 95, 86, 0.15);
color: #ff5f56;
}
.match-details {
display: grid;
gap: 12px;
}
.detail-item {
display: flex;
align-items: center;
gap: 16px;
}
.detail-label {
width: 80px;
font-size: 13px;
color: var(--vp-c-text-3);
flex-shrink: 0;
}
.detail-value {
font-size: 14px;
color: var(--vp-c-text-1);
}
.detail-value.code {
font-family: 'Monaco', 'Menlo', monospace;
background: var(--vp-c-bg-soft);
padding: 4px 8px;
border-radius: 4px;
}
.params-section {
margin-top: 8px;
padding-top: 12px;
border-top: 1px solid var(--vp-c-divider);
}
.params-list {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.param-item {
display: flex;
align-items: center;
gap: 6px;
background: var(--vp-c-bg-soft);
padding: 6px 12px;
border-radius: 6px;
}
.param-key {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 13px;
color: var(--vp-c-brand);
}
.param-arrow {
font-size: 12px;
color: var(--vp-c-text-3);
}
.param-value {
font-size: 13px;
color: var(--vp-c-text-1);
font-weight: 500;
}
.no-match {
text-align: center;
padding: 32px;
}
.no-match-icon {
font-size: 48px;
margin-bottom: 16px;
}
.no-match p {
color: var(--vp-c-text-2);
margin-bottom: 16px;
}
.suggestions {
text-align: left;
display: inline-block;
color: var(--vp-c-text-3);
font-size: 13px;
}
.suggestions li {
margin: 4px 0;
}
.tips-section {
margin-top: 20px;
padding: 20px;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.tips-section h5 {
margin: 0 0 16px 0;
font-size: 14px;
color: var(--vp-c-text-1);
}
.tips-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.tip-item {
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.tip-item code {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 13px;
color: var(--vp-c-brand);
background: transparent;
padding: 0;
}
.tip-item span:last-child {
font-size: 12px;
color: var(--vp-c-text-3);
}
@media (max-width: 768px) {
.demo-container {
grid-template-columns: 1fr;
}
.detail-item {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.detail-label {
width: auto;
}
.tips-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,139 @@
<template>
<div class="router-architecture-demo">
<div class="demo-header">
<h4>前端路由架构</h4>
<p class="demo-desc">展示前端路由系统的核心组件和数据流向</p>
</div>
<div class="architecture-diagram">
<div class="layer browser-layer">
<div class="layer-title">浏览器层</div>
<div class="layer-content">
<div class="component">URL Bar</div>
<div class="component">History API</div>
<div class="component">Hash Change</div>
</div>
</div>
<div class="arrow down"></div>
<div class="layer router-core">
<div class="layer-title">路由核心层</div>
<div class="layer-content">
<div class="component main">Router Instance</div>
<div class="sub-components">
<div class="component">Route Matcher</div>
<div class="component">History Manager</div>
<div class="component">Guard Pipeline</div>
</div>
</div>
</div>
<div class="arrow down"></div>
<div class="layer component-layer">
<div class="layer-title">组件层</div>
<div class="layer-content">
<div class="component">Router View</div>
<div class="component">Router Link</div>
<div class="component">Page Components</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.router-architecture-demo {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
}
.demo-header {
text-align: center;
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.demo-desc {
margin: 0;
color: var(--vp-c-text-2);
font-size: 14px;
}
.architecture-diagram {
display: flex;
flex-direction: column;
gap: 8px;
}
.layer {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
}
.layer-title {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--vp-c-divider);
}
.layer-content {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.component {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 8px 12px;
font-size: 12px;
color: var(--vp-c-text-1);
}
.component.main {
background: var(--vp-c-brand-soft);
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
font-weight: 500;
width: 100%;
text-align: center;
}
.sub-components {
display: flex;
flex-wrap: wrap;
gap: 8px;
width: 100%;
margin-top: 8px;
}
.arrow {
text-align: center;
color: var(--vp-c-text-3);
font-size: 16px;
}
@media (max-width: 768px) {
.layer-content {
flex-direction: column;
}
.sub-components {
flex-direction: column;
}
}
</style>
@@ -0,0 +1,426 @@
<template>
<div class="routing-modes-demo">
<div class="demo-header">
<h4>路由模式对比</h4>
<p class="demo-desc">点击切换不同路由模式观察URL变化和浏览器行为</p>
</div>
<div class="mode-selector">
<button
v-for="mode in modes"
:key="mode.key"
:class="['mode-btn', { active: currentMode === mode.key }]"
@click="switchMode(mode.key)"
>
<span class="mode-icon">{{ mode.icon }}</span>
<span class="mode-name">{{ mode.name }}</span>
</button>
</div>
<div class="demo-content">
<div class="browser-mockup">
<div class="browser-header">
<div class="browser-controls">
<span class="dot red"></span>
<span class="dot yellow"></span>
<span class="dot green"></span>
</div>
<div class="address-bar">
<span class="url-protocol">{{ getProtocol() }}</span>
<span class="url-host">example.com</span>
<span class="url-path">{{ currentPath }}</span>
<span v-if="currentMode === 'hash'" class="url-hash">#/home</span>
</div>
</div>
<div class="browser-content">
<nav class="nav-menu">
<a
v-for="item in navItems"
:key="item.path"
:class="['nav-link', { active: currentPath === item.path }]"
@click="navigate(item.path)"
>
{{ item.name }}
</a>
</nav>
<div class="page-content">
<div class="page-header">
<h2>{{ getPageTitle() }}</h2>
</div>
<div class="page-body">
<p>{{ getPageContent() }}</p>
</div>
</div>
</div>
</div>
<div class="mode-info">
<div class="info-card">
<h5>{{ getCurrentMode().name }}</h5>
<p class="info-desc">{{ getCurrentMode().description }}</p>
<div class="pros-cons">
<div class="pros">
<h6>优点</h6>
<ul>
<li v-for="pro in getCurrentMode().pros" :key="pro">{{ pro }}</li>
</ul>
</div>
<div class="cons">
<h6>缺点</h6>
<ul>
<li v-for="con in getCurrentMode().cons" :key="con">{{ con }}</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentMode = ref('history')
const currentPath = ref('/home')
const modes = [
{
key: 'hash',
name: 'Hash 模式',
icon: '#',
description: '使用URL的hash部分(#)来模拟完整URL,当hash改变时不会触发页面刷新。',
pros: ['兼容性好,支持IE8+', '无需服务端配置', '部署简单'],
cons: ['URL带有#号,不够美观', '部分浏览器分享链接可能丢失hash', 'SEO不友好']
},
{
key: 'history',
name: 'History 模式',
icon: '/',
description: '使用HTML5 History APIpushState/replaceState)来实现无刷新导航。',
pros: ['URL美观,没有#号', '更符合用户习惯', 'SEO友好'],
cons: ['需要服务端支持', '兼容性要求IE10+', '配置相对复杂']
},
{
key: 'memory',
name: 'Memory 模式',
icon: 'M',
description: '将路由信息保存在内存中,不修改浏览器URL,适用于非浏览器环境。',
pros: ['无需浏览器环境', '适用于测试环境', '移动端App内嵌'],
cons: ['不支持浏览器刷新', 'URL不会同步', '仅适用于特定场景']
}
]
const navItems = [
{ name: '首页', path: '/home' },
{ name: '产品', path: '/products' },
{ name: '关于', path: '/about' },
{ name: '用户', path: '/user/profile' }
]
const switchMode = (mode) => {
currentMode.value = mode
// 重置路径
currentPath.value = '/home'
}
const navigate = (path) => {
currentPath.value = path
// 模拟路由切换效果
if (currentMode.value === 'history') {
// History模式下使用pushState(这里仅模拟)
console.log(`pushState: ${path}`)
} else if (currentMode.value === 'hash') {
// Hash模式下修改hash
console.log(`hashchange: #${path}`)
}
}
const getProtocol = () => {
return 'https://'
}
const getCurrentMode = () => {
return modes.find(m => m.key === currentMode.value) || modes[0]
}
const getPageTitle = () => {
const item = navItems.find(i => i.path === currentPath.value)
return item ? `${item.name}页面` : '首页'
}
const getPageContent = () => {
const contents = {
'/home': '欢迎来到首页!这是SPA应用的入口页面,所有导航都在前端完成,无需刷新整个页面。',
'/products': '产品列表页面展示了我们的核心产品。你可以在这里浏览、筛选和查看详情。',
'/about': '关于我们页面介绍了公司的历史、愿景和团队信息。',
'/user/profile': '个人中心页面,显示用户的基本信息、订单历史和设置选项。'
}
return contents[currentPath.value] || contents['/home']
}
</script>
<style scoped>
.routing-modes-demo {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
}
.demo-header {
text-align: center;
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.demo-desc {
margin: 0;
color: var(--vp-c-text-2);
font-size: 14px;
}
.mode-selector {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 24px;
flex-wrap: wrap;
}
.mode-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s;
}
.mode-btn:hover {
border-color: var(--vp-c-brand);
}
.mode-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.mode-icon {
font-weight: bold;
font-size: 16px;
}
.mode-name {
font-size: 14px;
font-weight: 500;
}
.demo-content {
display: grid;
gap: 20px;
}
.browser-mockup {
background: var(--vp-c-bg);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.browser-header {
background: var(--vp-c-bg-soft);
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
}
.browser-controls {
display: flex;
gap: 6px;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.dot.red { background: #ff5f56; }
.dot.yellow { background: #ffbd2e; }
.dot.green { background: #27c93f; }
.address-bar {
flex: 1;
background: var(--vp-c-bg);
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
font-family: monospace;
display: flex;
align-items: center;
gap: 2px;
}
.url-protocol { color: var(--vp-c-text-3); }
.url-host { color: var(--vp-c-text-2); }
.url-path { color: var(--vp-c-brand); font-weight: 500; }
.url-hash { color: #e06c75; font-weight: 500; }
.browser-content {
display: flex;
min-height: 200px;
}
.nav-menu {
width: 120px;
background: var(--vp-c-bg-soft);
padding: 16px 0;
border-right: 1px solid var(--vp-c-divider);
}
.nav-link {
display: block;
padding: 10px 16px;
color: var(--vp-c-text-2);
text-decoration: none;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.nav-link:hover {
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
}
.nav-link.active {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand);
border-right: 2px solid var(--vp-c-brand);
}
.page-content {
flex: 1;
padding: 20px;
}
.page-header h2 {
margin: 0 0 12px 0;
color: var(--vp-c-text-1);
font-size: 20px;
}
.page-body {
color: var(--vp-c-text-2);
line-height: 1.6;
}
.mode-info {
margin-top: 16px;
}
.info-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
}
.info-card h5 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
font-size: 16px;
}
.info-desc {
margin: 0 0 16px 0;
color: var(--vp-c-text-2);
font-size: 14px;
line-height: 1.5;
}
.pros-cons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.pros, .cons {
padding: 12px;
border-radius: 6px;
}
.pros {
background: rgba(39, 201, 63, 0.1);
}
.cons {
background: rgba(255, 95, 86, 0.1);
}
.pros h6, .cons h6 {
margin: 0 0 8px 0;
font-size: 13px;
font-weight: 600;
}
.pros h6 {
color: #27c93f;
}
.cons h6 {
color: #ff5f56;
}
.pros ul, .cons ul {
margin: 0;
padding-left: 16px;
font-size: 12px;
color: var(--vp-c-text-2);
}
.pros li, .cons li {
margin: 4px 0;
}
@media (max-width: 768px) {
.mode-selector {
flex-direction: column;
align-items: stretch;
}
.mode-btn {
justify-content: center;
}
.browser-content {
flex-direction: column;
}
.nav-menu {
width: 100%;
display: flex;
padding: 8px;
border-right: none;
border-bottom: 1px solid var(--vp-c-divider);
}
.nav-link {
padding: 8px 12px;
white-space: nowrap;
}
.pros-cons {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,108 @@
<template>
<div class="spa-navigation-demo">
<div class="demo-header">
<h4>SPA 导航流程</h4>
<p class="demo-desc">完整展示从点击链接到页面更新的全流程</p>
</div>
<div class="flow-container">
<div class="step" v-for="(step, index) in steps" :key="index">
<div class="step-number">{{ index + 1 }}</div>
<div class="step-content">
<div class="step-title">{{ step.title }}</div>
<div class="step-desc">{{ step.desc }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
const steps = [
{ title: '触发导航', desc: '用户点击链接或调用 router.push()' },
{ title: '全局前置守卫', desc: '执行 router.beforeEach 钩子' },
{ title: '解析路由', desc: '匹配路由配置,解析动态参数' },
{ title: '组件内守卫', desc: '执行 beforeRouteEnter 钩子' },
{ title: '全局解析守卫', desc: '执行 beforeResolve 钩子' },
{ title: '更新视图', desc: '渲染匹配的组件,更新 DOM' },
{ title: '全局后置钩子', desc: '执行 afterEach 钩子' }
]
</script>
<style scoped>
.spa-navigation-demo {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
}
.demo-header {
text-align: center;
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.demo-desc {
margin: 0;
color: var(--vp-c-text-2);
font-size: 14px;
}
.flow-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.step {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 16px;
background: var(--vp-c-bg);
border-radius: 8px;
border-left: 3px solid var(--vp-c-brand);
}
.step-number {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-brand);
color: white;
border-radius: 50%;
font-size: 13px;
font-weight: 600;
flex-shrink: 0;
}
.step-content {
flex: 1;
}
.step-title {
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-1);
margin-bottom: 2px;
}
.step-desc {
font-size: 12px;
color: var(--vp-c-text-3);
}
@media (max-width: 768px) {
.step {
flex-direction: column;
align-items: flex-start;
}
}
</style>
@@ -0,0 +1,10 @@
// Frontend Routing Components
export { default as RoutingModesDemo } from './RoutingModesDemo.vue'
export { default as HashVsHistoryDemo } from './HashVsHistoryDemo.vue'
export { default as RouteMatchingDemo } from './RouteMatchingDemo.vue'
export { default as NestedRoutesDemo } from './NestedRoutesDemo.vue'
export { default as DynamicRoutesDemo } from './DynamicRoutesDemo.vue'
export { default as RouteGuardsDemo } from './RouteGuardsDemo.vue'
export { default as SpaNavigationDemo } from './SpaNavigationDemo.vue'
export { default as MpaRoutingDemo } from './MpaRoutingDemo.vue'
export { default as RouterArchitectureDemo } from './RouterArchitectureDemo.vue'