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:
@@ -1,12 +1,14 @@
|
||||
<!--
|
||||
CacheArchitectureDemo.vue
|
||||
缓存架构概览 - 展示缓存在系统中的位置和作用
|
||||
-->
|
||||
<template>
|
||||
<div class="cache-architecture-demo">
|
||||
<div class="header">
|
||||
<div class="title">缓存架构概览</div>
|
||||
<div class="subtitle">数据访问的"高速公路系统"</div>
|
||||
<div class="demo-header">
|
||||
<span class="icon">🏗️</span>
|
||||
<span class="title">缓存架构</span>
|
||||
<span class="subtitle">数据访问的"高速公路系统"</span>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
想象你在<span class="highlight">图书馆</span>找书:如果书桌上已经有你要的书(缓存),直接拿就行;
|
||||
如果没有,就得去书架(数据库)找。缓存就是这样一张"书桌",让常用数据触手可及。
|
||||
</div>
|
||||
|
||||
<div class="architecture-flow">
|
||||
@@ -17,10 +19,7 @@
|
||||
|
||||
<div class="arrow">↓</div>
|
||||
|
||||
<div
|
||||
class="flow-layer cache"
|
||||
:class="{ active: currentLayer === 'cache' }"
|
||||
>
|
||||
<div class="flow-layer cache" :class="{ active: currentLayer === 'cache' }">
|
||||
<div class="layer-label">缓存层 (Cache)</div>
|
||||
<div class="cache-box">
|
||||
<div class="cache-icon">⚡</div>
|
||||
@@ -82,6 +81,11 @@
|
||||
<span class="response-time">{{ lastResult.time }}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>缓存就像内存和数据库之间的"高速缓冲区",用空间换时间,把热点数据放在更快的地方。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -99,9 +103,7 @@ const simulateRequest = () => {
|
||||
const hit = Math.random() * 100 < hitRate.value
|
||||
lastResult.value = {
|
||||
hit,
|
||||
time: hit
|
||||
? Math.floor(Math.random() * 3) + 1
|
||||
: Math.floor(Math.random() * 20) + 40
|
||||
time: hit ? Math.floor(Math.random() * 3) + 1 : Math.floor(Math.random() * 20) + 40
|
||||
}
|
||||
|
||||
currentLayer.value = 'cache'
|
||||
@@ -119,26 +121,36 @@ const simulateRequest = () => {
|
||||
<style scoped>
|
||||
.cache-architecture-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 1.5rem;
|
||||
.demo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.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; }
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
.intro-text {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.intro-text .highlight {
|
||||
color: var(--vp-c-brand-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.architecture-flow {
|
||||
@@ -336,4 +348,15 @@ const simulateRequest = () => {
|
||||
.response-time {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.info-box .icon { margin-right: 0.25rem; }
|
||||
</style>
|
||||
|
||||
+284
-21
@@ -1,13 +1,126 @@
|
||||
<template>
|
||||
<div class="demo-container">
|
||||
<div class="cache-architecture-demo">
|
||||
<div class="demo-header">
|
||||
<h4>{{ title }}</h4>
|
||||
<p class="hint">{{ description }}</p>
|
||||
<span class="icon">🏗️</span>
|
||||
<span class="title">多级缓存架构</span>
|
||||
<span class="subtitle">像图书分馆一样层层拦截请求</span>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<el-alert type="info" :closable="false">
|
||||
缓存架构概览演示组件占位符 - 待实现具体交互
|
||||
</el-alert>
|
||||
|
||||
<div class="intro-text">
|
||||
想象你在<span class="highlight">连锁图书馆</span>找书:先在桌面上找(CDN),没有就去房间书架(本地缓存),
|
||||
再没有就去楼层的公共阅览室(Redis),最后才去总馆(数据库)。每一层都能拦截大量请求。
|
||||
</div>
|
||||
|
||||
<div class="architecture-diagram">
|
||||
<div class="layer user-layer">
|
||||
<div class="layer-icon">👤</div>
|
||||
<div class="layer-label">用户请求</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow-down">⬇</div>
|
||||
|
||||
<div class="layer cdn-layer" :class="{ active: activeLayer === 'cdn' }">
|
||||
<div class="layer-header">
|
||||
<span class="icon">🌐</span>
|
||||
<span class="layer-name">CDN 缓存</span>
|
||||
</div>
|
||||
<div class="layer-details">
|
||||
<div class="detail-item">
|
||||
<span class="label">位置</span>
|
||||
<span class="value">全球边缘节点</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">内容</span>
|
||||
<span class="value">静态资源</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">命中率</span>
|
||||
<span class="value highlight">{{ cdnHitRate }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow-down">⬇</div>
|
||||
|
||||
<div class="layer local-layer" :class="{ active: activeLayer === 'local' }">
|
||||
<div class="layer-header">
|
||||
<span class="icon">💻</span>
|
||||
<span class="layer-name">本地缓存</span>
|
||||
</div>
|
||||
<div class="layer-details">
|
||||
<div class="detail-item">
|
||||
<span class="label">位置</span>
|
||||
<span class="value">应用服务器内存</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">内容</span>
|
||||
<span class="value">热点数据</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">速度</span>
|
||||
<span class="value highlight">极快 (~1ms)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow-down">⬇</div>
|
||||
|
||||
<div class="layer distributed-layer" :class="{ active: activeLayer === 'distributed' }">
|
||||
<div class="layer-header">
|
||||
<span class="icon">🗄️</span>
|
||||
<span class="layer-name">分布式缓存</span>
|
||||
</div>
|
||||
<div class="layer-details">
|
||||
<div class="detail-item">
|
||||
<span class="label">位置</span>
|
||||
<span class="value">Redis 集群</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">内容</span>
|
||||
<span class="value">共享缓存数据</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">容量</span>
|
||||
<span class="value highlight">可扩展</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow-down">⬇</div>
|
||||
|
||||
<div class="layer database-layer">
|
||||
<div class="layer-header">
|
||||
<span class="icon">🗃️</span>
|
||||
<span class="layer-name">数据库</span>
|
||||
</div>
|
||||
<div class="layer-details">
|
||||
<div class="detail-item">
|
||||
<span class="label">位置</span>
|
||||
<span class="value">MySQL / PostgreSQL</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">速度</span>
|
||||
<span class="value warning">较慢 (~100ms)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-panel">
|
||||
<button
|
||||
v-for="layer in layers"
|
||||
:key="layer.id"
|
||||
class="layer-btn"
|
||||
:class="{ active: activeLayer === layer.id }"
|
||||
@click="activeLayer = layer.id"
|
||||
>
|
||||
{{ layer.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>多级缓存通过在不同层次拦截请求,逐层过滤,最终只有极少数请求会打到数据库。就像漏斗一样,越往下流量越小。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -15,36 +128,186 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const title = ref('缓存架构概览')
|
||||
const description = ref('展示缓存系统的整体架构,包括本地缓存、分布式缓存、CDN缓存等多级缓存架构')
|
||||
const activeLayer = ref('local')
|
||||
const cdnHitRate = ref(95)
|
||||
|
||||
const layers = [
|
||||
{ id: 'cdn', name: 'CDN 缓存' },
|
||||
{ id: 'local', name: '本地缓存' },
|
||||
{ id: 'distributed', name: '分布式缓存' },
|
||||
{ id: 'database', name: '数据库' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
.cache-architecture-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 8px 0;
|
||||
.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; }
|
||||
|
||||
.intro-text {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.intro-text .highlight {
|
||||
color: var(--vp-c-brand-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.architecture-diagram {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.layer {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.user-layer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.layer-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.layer-label {
|
||||
font-weight: 600;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.cdn-layer,
|
||||
.local-layer,
|
||||
.distributed-layer,
|
||||
.database-layer {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
padding: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.layer.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.layer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.layer-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.detail-item .label {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.detail-item .value {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-item .value.highlight {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.detail-item .value.warning {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.arrow-down {
|
||||
font-size: 1.5rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.layer-btn {
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 4px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 0.85rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
.layer-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.layer-btn.active {
|
||||
background-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.info-box .icon {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,95 @@
|
||||
<template>
|
||||
<div class="demo-container">
|
||||
<div class="cache-hierarchy-demo">
|
||||
<div class="demo-header">
|
||||
<h4>{{ title }}</h4>
|
||||
<p class="hint">{{ description }}</p>
|
||||
<span class="icon">🏗️</span>
|
||||
<span class="title">缓存层级结构</span>
|
||||
<span class="subtitle">数据是如何在不同缓存层级间流动的</span>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<el-alert type="info" :closable="false">
|
||||
缓存层级演示组件占位符 - 待实现具体交互
|
||||
</el-alert>
|
||||
|
||||
<div class="intro-text">
|
||||
想象你在<span class="highlight">超市</span>买东西:先在购物车找(L1缓存),没有就去货架上找(L2缓存),
|
||||
再没有就去仓库找(L3缓存)。越往上层,速度越快但容量越小;越往下层,速度越慢但容量越大。
|
||||
</div>
|
||||
|
||||
<div class="hierarchy-layers">
|
||||
<div
|
||||
v-for="(layer, index) in layers"
|
||||
:key="layer.id"
|
||||
class="layer"
|
||||
:class="{ active: activeLayer === layer.id }"
|
||||
@click="activeLayer = layer.id"
|
||||
>
|
||||
<div class="layer-header">
|
||||
<span class="layer-icon">{{ layer.icon }}</span>
|
||||
<span class="layer-name">{{ layer.name }}</span>
|
||||
</div>
|
||||
<div class="layer-stats">
|
||||
<div class="stat">
|
||||
<span class="stat-label">速度</span>
|
||||
<span class="stat-value" :class="layer.speedClass">{{ layer.speed }}</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">容量</span>
|
||||
<span class="stat-value">{{ layer.capacity }}</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">成本</span>
|
||||
<span class="stat-value">{{ layer.cost }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="index < layers.length - 1" class="arrow">↓</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-flow">
|
||||
<div class="flow-title">数据流动演示</div>
|
||||
<div class="flow-steps">
|
||||
<div class="flow-step" :class="{ active: flowStep >= 1 }">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-text">查询 L1 缓存</div>
|
||||
<div class="step-time">~1ns</div>
|
||||
</div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step" :class="{ active: flowStep >= 2 }">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-text">未命中,查 L2</div>
|
||||
<div class="step-time">~10ns</div>
|
||||
</div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step" :class="{ active: flowStep >= 3 }">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-text">未命中,查 L3</div>
|
||||
<div class="step-time">~100ns</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="simulate-btn" @click="simulateFlow">模拟数据查找</button>
|
||||
</div>
|
||||
|
||||
<div class="comparison-table">
|
||||
<div class="table-title">各层级对比</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>层级</th>
|
||||
<th>速度</th>
|
||||
<th>容量</th>
|
||||
<th>成本</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="layer in layers" :key="layer.id" :class="{ active: activeLayer === layer.id }">
|
||||
<td>{{ layer.name }}</td>
|
||||
<td>{{ layer.speed }}</td>
|
||||
<td>{{ layer.capacity }}</td>
|
||||
<td>{{ layer.cost }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>多级缓存利用<span class="highlight">局部性原理</span>——程序倾向于访问最近访问过的数据位置。通过把热点数据放在最快的层级,大幅提升访问速度。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -15,36 +97,306 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const title = ref('缓存层级演示')
|
||||
const description = ref('展示多级缓存的层级结构和数据流动过程')
|
||||
const activeLayer = ref('l1')
|
||||
const flowStep = ref(0)
|
||||
|
||||
const layers = [
|
||||
{
|
||||
id: 'l1',
|
||||
name: 'L1 缓存',
|
||||
icon: '⚡',
|
||||
speed: '~1ns',
|
||||
capacity: '~64KB',
|
||||
cost: '极高',
|
||||
speedClass: 'fast'
|
||||
},
|
||||
{
|
||||
id: 'l2',
|
||||
name: 'L2 缓存',
|
||||
icon: '🚀',
|
||||
speed: '~10ns',
|
||||
capacity: '~256KB',
|
||||
cost: '高',
|
||||
speedClass: 'medium'
|
||||
},
|
||||
{
|
||||
id: 'l3',
|
||||
name: 'L3 缓存',
|
||||
icon: '📦',
|
||||
speed: '~100ns',
|
||||
capacity: '~8MB',
|
||||
cost: '中',
|
||||
speedClass: 'slow'
|
||||
}
|
||||
]
|
||||
|
||||
const simulateFlow = () => {
|
||||
flowStep.value = 0
|
||||
setTimeout(() => { flowStep.value = 1 }, 300)
|
||||
setTimeout(() => { flowStep.value = 2 }, 800)
|
||||
setTimeout(() => { flowStep.value = 3 }, 1300)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
.cache-hierarchy-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 20px;
|
||||
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; }
|
||||
|
||||
.intro-text {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
.intro-text .highlight {
|
||||
color: var(--vp-c-brand-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hierarchy-layers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.layer {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.layer:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.layer.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.layer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.layer-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.layer-name {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.layer-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.stat {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-value.fast {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.stat-value.medium {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.stat-value.slow {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.data-flow {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.flow-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.flow-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
transition: all 0.3s;
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.flow-step.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.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-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.step-time {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.flow-arrow {
|
||||
font-size: 1.2rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.simulate-btn {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.simulate-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.comparison-table {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.table-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--vp-c-bg-soft);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
tr.active {
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.info-box .icon {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.info-box .highlight {
|
||||
color: var(--vp-c-brand-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
+505
-23
@@ -1,51 +1,533 @@
|
||||
|
||||
<template>
|
||||
<div class="demo-container">
|
||||
<div class="cache-monitoring-demo">
|
||||
<div class="demo-header">
|
||||
<h4>{{ title }}</h4>
|
||||
<p class="hint">{{ description }}</p>
|
||||
<span class="icon">📊</span>
|
||||
<span class="title">缓存监控面板</span>
|
||||
<span class="subtitle">实时追踪缓存的健康状况</span>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<el-alert type="info" :closable="false">
|
||||
缓存监控面板演示组件占位符 - 待实现具体交互
|
||||
</el-alert>
|
||||
|
||||
<div class="intro-text">
|
||||
想象你在<span class="highlight">开车</span>:仪表盘显示速度、油量、引擎温度。缓存监控就像仪表盘,
|
||||
让你实时看到命中率、响应时间、内存使用等关键指标,及时发现问题。
|
||||
</div>
|
||||
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card hit-rate">
|
||||
<div class="metric-icon">🎯</div>
|
||||
<div class="metric-content">
|
||||
<div class="metric-label">命中率</div>
|
||||
<div class="metric-value" :class="getHitRateClass">
|
||||
{{ hitRate }}%
|
||||
</div>
|
||||
<div class="metric-trend" :class="trendClass">
|
||||
{{ trendIcon }} {{ trendValue }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card response-time">
|
||||
<div class="metric-icon">⚡</div>
|
||||
<div class="metric-content">
|
||||
<div class="metric-label">平均响应时间</div>
|
||||
<div class="metric-value">
|
||||
{{ avgResponseTime }}ms
|
||||
</div>
|
||||
<div class="metric-sub">
|
||||
命中: {{ hitTime }}ms | 未命中: {{ missTime }}ms
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card cache-size">
|
||||
<div class="metric-icon">📦</div>
|
||||
<div class="metric-content">
|
||||
<div class="metric-label">缓存使用量</div>
|
||||
<div class="metric-value">
|
||||
{{ usedSize }}MB
|
||||
</div>
|
||||
<div class="metric-bar">
|
||||
<div
|
||||
class="metric-bar-fill"
|
||||
:style="{
|
||||
width: `${sizeUsagePercent}%`,
|
||||
backgroundColor: getSizeBarColor
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="metric-sub">
|
||||
{{ usedSize }}MB / {{ maxSize }}MB
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card requests">
|
||||
<div class="metric-icon">📊</div>
|
||||
<div class="metric-content">
|
||||
<div class="metric-label">总请求数</div>
|
||||
<div class="metric-value">
|
||||
{{ totalRequests.toLocaleString() }}
|
||||
</div>
|
||||
<div class="metric-sub">
|
||||
命中: {{ totalHits.toLocaleString() }} | 未命中: {{ totalMisses.toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="request-log">
|
||||
<div class="log-header">
|
||||
<span>📋 请求日志</span>
|
||||
<button class="clear-btn" @click="clearLog">清空</button>
|
||||
</div>
|
||||
<div class="log-list">
|
||||
<transition-group name="log-item">
|
||||
<div
|
||||
v-for="log in requestLogs"
|
||||
:key="log.id"
|
||||
class="log-entry"
|
||||
:class="log.type"
|
||||
>
|
||||
<span class="log-icon">{{ log.type === 'hit' ? '✅' : '❌' }}</span>
|
||||
<span class="log-time">{{ log.time }}</span>
|
||||
<span class="log-key">{{ log.key }}</span>
|
||||
<span class="log-result">{{ log.type === 'hit' ? '命中' : '未命中' }}</span>
|
||||
<span class="log-latency">{{ log.latency }}ms</span>
|
||||
</div>
|
||||
</transition-group>
|
||||
<div v-if="requestLogs.length === 0" class="empty-log">
|
||||
暂无请求记录,点击下方按钮发送请求
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-panel">
|
||||
<button class="action-btn" @click="simulateRequest">
|
||||
🎲 模拟请求
|
||||
</button>
|
||||
<button class="action-btn" @click="simulateBurst">
|
||||
🚀 连续请求 (10次)
|
||||
</button>
|
||||
<button class="action-btn outline" @click="resetMetrics">
|
||||
↺ 重置指标
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心指标:</strong>命中率应该 > 80%,响应时间 < 10ms,内存使用 < 80%。如果命中率突然下降,可能是缓存穿透或雪崩;如果响应时间变长,可能是缓存满了。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const title = ref('缓存监控面板')
|
||||
const description = ref('可视化展示缓存系统的监控指标,包括命中率、响应时间、缓存大小等')
|
||||
const hitRate = ref(85)
|
||||
const avgResponseTime = ref(15)
|
||||
const hitTime = ref(2)
|
||||
const missTime = ref(50)
|
||||
const usedSize = ref(450)
|
||||
const maxSize = ref(512)
|
||||
const totalRequests = ref(1234)
|
||||
const totalHits = ref(1049)
|
||||
const totalMisses = ref(185)
|
||||
|
||||
const requestLogs = ref([])
|
||||
let logId = 0
|
||||
let autoSimulate = null
|
||||
|
||||
const sizeUsagePercent = computed(() => {
|
||||
return (usedSize.value / maxSize.value) * 100
|
||||
})
|
||||
|
||||
const getHitRateClass = computed(() => {
|
||||
if (hitRate.value >= 80) return 'excellent'
|
||||
if (hitRate.value >= 60) return 'good'
|
||||
return 'poor'
|
||||
})
|
||||
|
||||
const trendValue = ref(2.5)
|
||||
const trendClass = computed(() => {
|
||||
return trendValue.value >= 0 ? 'up' : 'down'
|
||||
})
|
||||
|
||||
const trendIcon = computed(() => {
|
||||
return trendValue.value >= 0 ? '📈' : '📉'
|
||||
})
|
||||
|
||||
const getSizeBarColor = computed(() => {
|
||||
const percent = sizeUsagePercent.value
|
||||
if (percent >= 90) return 'var(--vp-c-danger-1)'
|
||||
if (percent >= 75) return 'var(--vp-c-warning-1)'
|
||||
return 'var(--vp-c-success-1)'
|
||||
})
|
||||
|
||||
const simulateRequest = () => {
|
||||
const isHit = Math.random() < (hitRate.value / 100)
|
||||
const latency = isHit
|
||||
? Math.round(hitTime.value + Math.random() * 3)
|
||||
: Math.round(missTime.value + Math.random() * 10)
|
||||
|
||||
const keys = ['user:123', 'product:456', 'config:app', 'session:abc', 'cache:xyz']
|
||||
const key = keys[Math.floor(Math.random() * keys.length)]
|
||||
|
||||
const now = new Date()
|
||||
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
|
||||
|
||||
requestLogs.value.unshift({
|
||||
id: logId++,
|
||||
type: isHit ? 'hit' : 'miss',
|
||||
time,
|
||||
key,
|
||||
latency
|
||||
})
|
||||
|
||||
// Keep only last 10 logs
|
||||
if (requestLogs.value.length > 10) {
|
||||
requestLogs.value = requestLogs.value.slice(0, 10)
|
||||
}
|
||||
|
||||
// Update metrics
|
||||
totalRequests.value++
|
||||
if (isHit) {
|
||||
totalHits.value++
|
||||
} else {
|
||||
totalMisses.value++
|
||||
}
|
||||
|
||||
// Recalculate hit rate
|
||||
hitRate.value = Math.round((totalHits.value / totalRequests.value) * 100)
|
||||
|
||||
// Update response time (moving average)
|
||||
avgResponseTime.value = Math.round(
|
||||
(avgResponseTime.value * 0.9) + (latency * 0.1)
|
||||
)
|
||||
|
||||
// Update cache size (random fluctuation)
|
||||
const sizeChange = Math.round((Math.random() - 0.5) * 10)
|
||||
usedSize.value = Math.max(0, Math.min(maxSize.value, usedSize.value + sizeChange))
|
||||
}
|
||||
|
||||
const simulateBurst = () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
setTimeout(() => simulateRequest(), i * 100)
|
||||
}
|
||||
}
|
||||
|
||||
const resetMetrics = () => {
|
||||
hitRate.value = 85
|
||||
avgResponseTime.value = 15
|
||||
usedSize.value = 450
|
||||
totalRequests.value = 0
|
||||
totalHits.value = 0
|
||||
totalMisses.value = 0
|
||||
requestLogs.value = []
|
||||
logId = 0
|
||||
}
|
||||
|
||||
const clearLog = () => {
|
||||
requestLogs.value = []
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Auto-simulate a request every 3 seconds
|
||||
autoSimulate = setInterval(() => {
|
||||
simulateRequest()
|
||||
}, 3000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (autoSimulate) {
|
||||
clearInterval(autoSimulate)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
.cache-monitoring-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 8px 0;
|
||||
.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; }
|
||||
|
||||
.intro-text {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.intro-text .highlight {
|
||||
color: var(--vp-c-brand-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.metric-card:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.metric-icon {
|
||||
font-size: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metric-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.metric-value.excellent {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.metric-value.good {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.metric-value.poor {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.metric-trend {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.metric-trend.up {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.metric-trend.down {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.metric-sub {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.metric-bar {
|
||||
height: 4px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.metric-bar-fill {
|
||||
height: 100%;
|
||||
transition: width 0.3s ease, background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.request-log {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.log-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
color: var(--vp-c-text-1);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.clear-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.log-list {
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.log-entry.hit {
|
||||
background: rgba(34, 197, 94, 0.05);
|
||||
}
|
||||
|
||||
.log-entry.miss {
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
}
|
||||
|
||||
.log-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: var(--vp-c-text-3);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.log-key {
|
||||
flex: 1;
|
||||
font-family: monospace;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
.log-result {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.log-entry.hit .log-result {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.log-entry.miss .log-result {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.log-latency {
|
||||
color: var(--vp-c-text-2);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.empty-log {
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
color: var(--vp-c-text-3);
|
||||
font-style: italic;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 4px;
|
||||
background-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
font-size: 0.85rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background-color: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
.action-btn.outline {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.action-btn.outline:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.info-box .icon {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.log-item-enter-active,
|
||||
.log-item-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.log-item-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
|
||||
.log-item-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.metrics-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+378
-22
@@ -1,14 +1,177 @@
|
||||
|
||||
<template>
|
||||
<div class="demo-container">
|
||||
<div class="cache-pattern-comparison-demo">
|
||||
<div class="demo-header">
|
||||
<h4>{{ title }}</h4>
|
||||
<p class="hint">{{ description }}</p>
|
||||
<span class="icon">🔄</span>
|
||||
<span class="title">缓存读写模式</span>
|
||||
<span class="subtitle">Cache-Aside vs Read-Through vs Write-Behind</span>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<el-alert type="info" :closable="false">
|
||||
缓存模式对比演示组件占位符 - 待实现具体交互
|
||||
</el-alert>
|
||||
|
||||
<div class="intro-text">
|
||||
想象你在<span class="highlight">厨房</span>做菜:Cache-aside 就像自己决定什么时候从冰箱拿菜;
|
||||
Read-Through 像有个助手,你说要什么他就帮你拿;Write-Behind 像先记在购物清单上,之后再去买。
|
||||
</div>
|
||||
|
||||
<div class="pattern-tabs">
|
||||
<button
|
||||
v-for="pattern in patterns"
|
||||
:key="pattern.id"
|
||||
class="tab-btn"
|
||||
:class="{ active: activePattern === pattern.id }"
|
||||
@click="activePattern = pattern.id"
|
||||
>
|
||||
<span class="tab-icon">{{ pattern.icon }}</span>
|
||||
<span class="tab-name">{{ pattern.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pattern-content">
|
||||
<div v-if="activePattern === 'cache-aside'" class="pattern-detail">
|
||||
<div class="pattern-header">
|
||||
<h3>Cache-Aside (旁路缓存)</h3>
|
||||
<p class="pattern-desc">最常用的模式,应用代码直接控制缓存</p>
|
||||
</div>
|
||||
|
||||
<div class="flow-diagram">
|
||||
<div class="flow-step read">
|
||||
<div class="step-icon">📖</div>
|
||||
<div class="step-content">
|
||||
<strong>读取:</strong>先查缓存 → 没有就查数据库 → 写入缓存
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-step write">
|
||||
<div class="step-icon">✏️</div>
|
||||
<div class="step-content">
|
||||
<strong>更新:</strong>先更新数据库 → <span class="highlight">删除</span>缓存(不是更新!)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pros-cons">
|
||||
<div class="pros">
|
||||
<div class="list-title">✅ 优点</div>
|
||||
<div class="list-item">灵活,可精细控制</div>
|
||||
<div class="list-item">适合大多数场景</div>
|
||||
</div>
|
||||
<div class="cons">
|
||||
<div class="list-title">❌ 缺点</div>
|
||||
<div class="list-item">代码复杂度较高</div>
|
||||
<div class="list-item">需要手动维护一致性</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activePattern === 'read-through'" class="pattern-detail">
|
||||
<div class="pattern-header">
|
||||
<h3>Read-Through (读穿透)</h3>
|
||||
<p class="pattern-desc">缓存库负责从数据库加载数据</p>
|
||||
</div>
|
||||
|
||||
<div class="flow-diagram">
|
||||
<div class="flow-step">
|
||||
<div class="step-icon">📖</div>
|
||||
<div class="step-content">
|
||||
<strong>读取:</strong>应用只调 cache.get(),缓存库负责查数据库
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-step">
|
||||
<div class="step-icon">✏️</div>
|
||||
<div class="step-content">
|
||||
<strong>写入:</strong>通常与 Write-Through 配合,同步写缓存和数据库
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pros-cons">
|
||||
<div class="pros">
|
||||
<div class="list-title">✅ 优点</div>
|
||||
<div class="list-item">代码简洁</div>
|
||||
<div class="list-item">一致性更好</div>
|
||||
</div>
|
||||
<div class="cons">
|
||||
<div class="list-title">❌ 缺点</div>
|
||||
<div class="list-item">需要专门的缓存库</div>
|
||||
<div class="list-item">灵活性较低</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activePattern === 'write-behind'" class="pattern-detail">
|
||||
<div class="pattern-header">
|
||||
<h3>Write-Behind (异步写回)</h3>
|
||||
<p class="pattern-desc">写入时只写缓存,异步批量写数据库</p>
|
||||
</div>
|
||||
|
||||
<div class="flow-diagram">
|
||||
<div class="flow-step">
|
||||
<div class="step-icon">⚡</div>
|
||||
<div class="step-content">
|
||||
<strong>写入:</strong>立即写缓存 → 异步批量写数据库
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-step">
|
||||
<div class="step-icon">⚠️</div>
|
||||
<div class="step-content">
|
||||
<strong>风险:</strong>缓存崩溃会导致数据丢失
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pros-cons">
|
||||
<div class="pros">
|
||||
<div class="list-title">✅ 优点</div>
|
||||
<div class="list-item">写入极快</div>
|
||||
<div class="list-item">适合写多场景</div>
|
||||
</div>
|
||||
<div class="cons">
|
||||
<div class="list-title">❌ 缺点</div>
|
||||
<div class="list-item">可能丢失数据</div>
|
||||
<div class="list-item">一致性差</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-table">
|
||||
<div class="table-title">三种模式对比</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>模式</th>
|
||||
<th>复杂度</th>
|
||||
<th>性能</th>
|
||||
<th>一致性</th>
|
||||
<th>适用场景</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr :class="{ active: activePattern === 'cache-aside' }">
|
||||
<td>Cache-Aside</td>
|
||||
<td>中</td>
|
||||
<td>高</td>
|
||||
<td>中</td>
|
||||
<td>大多数场景</td>
|
||||
</tr>
|
||||
<tr :class="{ active: activePattern === 'read-through' }">
|
||||
<td>Read-Through</td>
|
||||
<td>低</td>
|
||||
<td>中</td>
|
||||
<td>高</td>
|
||||
<td>读多写少</td>
|
||||
</tr>
|
||||
<tr :class="{ active: activePattern === 'write-behind' }">
|
||||
<td>Write-Behind</td>
|
||||
<td>高</td>
|
||||
<td>极高</td>
|
||||
<td>低</td>
|
||||
<td>写多、可丢失</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>选择建议:</strong>90% 的场景用 Cache-Aside;如果追求代码简洁用 Read-Through;如果是秒杀、点赞这种"能丢数据"的场景才用 Write-Behind。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -16,36 +179,229 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const title = ref('缓存模式对比')
|
||||
const description = ref('对比展示Cache-Aside、Read-Through、Write-Through、Write-Behind等不同缓存模式')
|
||||
const activePattern = ref('cache-aside')
|
||||
|
||||
const patterns = [
|
||||
{ id: 'cache-aside', name: 'Cache-Aside', icon: '🔧' },
|
||||
{ id: 'read-through', name: 'Read-Through', icon: '📖' },
|
||||
{ id: 'write-behind', name: 'Write-Behind', icon: '⚡' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
.cache-pattern-comparison-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 20px;
|
||||
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; }
|
||||
|
||||
.intro-text {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
.intro-text .highlight {
|
||||
color: var(--vp-c-brand-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pattern-tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.tab-name {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.pattern-content {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.pattern-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.pattern-header {
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.pattern-header h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.pattern-desc {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.flow-diagram {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
font-size: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.step-content .highlight {
|
||||
color: var(--vp-c-brand-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pros-cons {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.pros, .cons {
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.pros {
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
}
|
||||
|
||||
.cons {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.35rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.comparison-table {
|
||||
background: var(--vp-c-bg);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.table-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--vp-c-bg-soft);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
tr.active {
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.info-box .icon {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user