feat(docs): enhance interactive demos and improve documentation

- Add new interactive components for frontend routing, browser rendering pipeline, and database transactions
- Improve existing demos with better visuals, explanations, and examples
- Update documentation structure and content for better clarity
- Add new utility scripts and update package.json with new commands
- Fix formatting and alignment in documentation tables
This commit is contained in:
sanbuphy
2026-02-13 22:10:03 +08:00
parent 599052b2e0
commit d174ceea32
88 changed files with 26273 additions and 15539 deletions
@@ -1,126 +1,74 @@
<template>
<div class="route-matching-demo">
<div class="demo-header">
<h4>路由匹配机制</h4>
<p class="demo-desc">输入URL路径查看路由是如何匹配和解析参数的</p>
<span class="icon">🎯</span>
<span class="title">路由匹配</span>
<span class="subtitle">URL如何找到对应组件</span>
</div>
<div class="demo-container">
<div class="intro-text">
想象你在<span class="highlight">查字典</span>输入一个词字典会帮你找到对应的解释路由匹配也是这样浏览器根据URL路径在路由配置中找到最匹配的那一项然后渲染对应组件
</div>
<div class="demo-content">
<div class="input-section">
<h5>📍 测试路径</h5>
<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 }]"
<span class="input-prefix">/</span>
<input
v-model="testPath"
type="text"
placeholder="user/123"
class="path-input"
@input="testMatch"
>
<div class="route-path">
<span class="route-pattern">{{ route.path }}</span>
<span v-if="route.hasParams" class="param-badge">含参数</span>
</div>
<div class="hint-text">试试user/123 products/electronics/456</div>
</div>
<div class="result-section">
<h5>🎯 匹配结果</h5>
<div v-if="matchResult && matchResult.matched" class="match-success">
<div class="success-icon"></div>
<div class="result-details">
<div class="result-row">
<span class="label">匹配路由:</span>
<code class="value">{{ matchResult.route.path }}</code>
</div>
<div v-if="Object.keys(matchResult.params).length" class="params-box">
<span class="label">提取参数:</span>
<div class="params-list">
<span v-for="(value, key) in matchResult.params" :key="key" class="param-tag">
{{ key }} = {{ value }}
</span>
</div>
</div>
<div class="route-name">{{ route.name }}</div>
</div>
</div>
<div v-else class="match-fail">
<div class="fail-icon"></div>
<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 class="routes-list">
<h5>📋 已定义的路由</h5>
<div class="routes-grid">
<div
v-for="route in routes"
:key="route.path"
:class="['route-item', { matched: matchedRoute?.path === route.path }]"
>
<code class="route-path">{{ route.path }}</code>
<span class="route-name">{{ route.name }}</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 class="info-box">
<span class="icon">💡</span>
<strong>匹配规则</strong>路由按定义顺序匹配先定义的优先动态参数:id可以匹配任意值但精确匹配优先级更高
</div>
</div>
</template>
@@ -128,7 +76,7 @@
<script setup>
import { ref, computed } from 'vue'
const testPath = ref('user/123/posts')
const testPath = ref('user/123')
const matchResult = ref(null)
const matchedRoute = ref(null)
@@ -137,14 +85,11 @@ const routes = [
{ 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)
}
@@ -152,20 +97,17 @@ const parsePath = (path) => {
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('?')
@@ -180,13 +122,11 @@ const matchPath = (routePath, testPath) => {
}
}
// 精确匹配
if (routePart !== testPart) {
return { matched: false, params: {} }
}
}
// 检查是否有剩余的测试路径部分(除非是通配符路由)
if (testParts.length > routeParts.length) {
const lastRoutePart = routeParts[routeParts.length - 1]
if (!lastRoutePart || (!lastRoutePart.includes('*') && !lastRoutePart.endsWith('+'))) {
@@ -227,380 +167,218 @@ const testMatch = () => {
}
}
// 自动测试初始路径
testMatch()
</script>
<style scoped>
.route-matching-demo {
padding: 20px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.demo-header .icon { font-size: 1.25rem; }
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-desc {
margin: 0;
.intro-text {
font-size: 0.9rem;
color: var(--vp-c-text-2);
font-size: 14px;
line-height: 1.6;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.demo-container {
.intro-text .highlight {
color: var(--vp-c-brand-1);
font-weight: 500;
}
.demo-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
gap: 1rem;
margin-bottom: 1rem;
}
.input-section {
.input-section, .result-section {
background: var(--vp-c-bg);
padding: 20px;
border-radius: 8px;
padding: 0.75rem;
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;
h5 {
margin: 0 0 0.5rem 0;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.path-input-wrapper {
.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;
margin-bottom: 0.5rem;
}
.path-prefix {
padding: 10px 8px 10px 12px;
.input-prefix {
padding: 0.5rem;
color: var(--vp-c-text-3);
font-family: monospace;
font-size: 14px;
font-size: 0.85rem;
}
.path-input {
flex: 1;
border: none;
background: transparent;
padding: 10px 12px 10px 0;
font-size: 14px;
padding: 0.5rem;
font-size: 0.85rem;
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;
.hint-text {
font-size: 0.7rem;
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 {
.match-success {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--vp-c-divider);
gap: 0.75rem;
}
.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);
.success-icon {
font-size: 1.5rem;
flex-shrink: 0;
}
.detail-value {
font-size: 14px;
color: var(--vp-c-text-1);
.result-details {
flex: 1;
}
.detail-value.code {
font-family: 'Monaco', 'Menlo', monospace;
.result-row {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.label {
font-size: 0.75rem;
color: var(--vp-c-text-3);
min-width: 60px;
}
.value {
font-size: 0.8rem;
color: var(--vp-c-text-1);
font-family: monospace;
background: var(--vp-c-bg-soft);
padding: 4px 8px;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.params-section {
margin-top: 8px;
padding-top: 12px;
.params-box {
padding-top: 0.5rem;
border-top: 1px solid var(--vp-c-divider);
}
.params-list {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
gap: 0.5rem;
margin-top: 0.5rem;
}
.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;
.param-tag {
background: var(--vp-c-brand-soft);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-family: monospace;
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 {
.match-fail {
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;
padding: 1rem;
color: var(--vp-c-text-3);
font-size: 13px;
}
.suggestions li {
margin: 4px 0;
.fail-icon {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.tips-section {
margin-top: 20px;
padding: 20px;
.routes-list {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
margin-bottom: 1rem;
}
.tips-section h5 {
margin: 0 0 16px 0;
font-size: 14px;
.routes-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0.5rem;
}
.route-item {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.5rem 0.75rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.route-item.matched {
border-color: var(--vp-c-brand);
background: rgba(66, 184, 131, 0.1);
}
.route-path {
font-size: 0.75rem;
font-family: monospace;
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;
.route-name {
font-size: 0.7rem;
color: var(--vp-c-text-3);
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.info-box .icon { margin-right: 0.25rem; }
@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 {
.demo-content {
grid-template-columns: 1fr;
}
}