refactor(docs): simplify deployment demo components
This commit is contained in:
@@ -1,279 +0,0 @@
|
||||
<!--
|
||||
CdnCacheDemo.vue
|
||||
CDN 加速原理:快递柜隐喻
|
||||
-->
|
||||
<template>
|
||||
<div class="cdn">
|
||||
<div class="header">
|
||||
<div class="title">CDN 加速演示</div>
|
||||
<div class="subtitle">就像在小区楼下装了个“丰巢快递柜”</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control">
|
||||
<label>你要取什么东西?(资源类型)</label>
|
||||
<div class="chips">
|
||||
<button
|
||||
v-for="r in resourceTypes"
|
||||
:key="r.id"
|
||||
:class="['chip', { active: r.id === resourceType }]"
|
||||
@click="resourceType = r.id"
|
||||
>
|
||||
{{ r.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<label>快递柜里有吗?(命中率)</label>
|
||||
<input type="range" min="0" max="100" v-model.number="hit" />
|
||||
<div class="hint">
|
||||
当前概率:{{ hit }}% ({{ hit > 80 ? '大部分都有' : '经常要跑远路' }})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary">
|
||||
<div class="card">
|
||||
<div class="label">跑总仓库的次数 (回源)</div>
|
||||
<div class="value">{{ miss }}%</div>
|
||||
<div class="note">次数越少,总仓库越轻松</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">省下的路费 (带宽节省)</div>
|
||||
<div class="value">{{ saved }}%</div>
|
||||
<div class="note">省到就是赚到</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">老司机的建议</div>
|
||||
<div class="value">{{ cacheAdvice }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow">
|
||||
<div class="step" v-for="(s, idx) in flow" :key="idx">
|
||||
<div class="head">
|
||||
<span class="dot" :style="{ background: s.color }"></span>
|
||||
<span class="name">{{ s.name }}</span>
|
||||
<span class="time">{{ s.time }}</span>
|
||||
</div>
|
||||
<div class="desc">{{ s.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const resourceTypes = [
|
||||
{ id: 'static', label: '标准件 (图片/CSS/JS)' },
|
||||
{ id: 'html', label: '信件 (HTML)' },
|
||||
{ id: 'api', label: '生鲜 (API数据)' }
|
||||
]
|
||||
|
||||
const resourceType = ref('static')
|
||||
const hit = ref(85)
|
||||
|
||||
const miss = computed(() => 100 - hit.value)
|
||||
const saved = computed(() => hit.value)
|
||||
|
||||
const cacheAdvice = computed(() => {
|
||||
if (resourceType.value === 'static')
|
||||
return '标准件保质期长,建议放柜子里一年 (max-age=1年)'
|
||||
if (resourceType.value === 'html')
|
||||
return '信件可能随时更新,每次取之前问一下 (no-cache)'
|
||||
return '生鲜容易坏,不要放柜子,直接去产地拿 (no-store)'
|
||||
})
|
||||
|
||||
const flow = computed(() => {
|
||||
const base = [
|
||||
{
|
||||
name: '用户 🙋♂️',
|
||||
time: '0ms',
|
||||
desc: '我想取个包裹',
|
||||
color: '#6366f1'
|
||||
},
|
||||
{
|
||||
name: '家门口快递柜 📦',
|
||||
time: '15ms',
|
||||
desc: '看看柜子里有没有...',
|
||||
color: '#6366f1'
|
||||
}
|
||||
]
|
||||
if (hit.value >= 70 && resourceType.value === 'static') {
|
||||
base.push({
|
||||
name: '有货!✅',
|
||||
time: '+5ms',
|
||||
desc: '直接拿走,不用跑远路',
|
||||
color: '#22c55e'
|
||||
})
|
||||
} else {
|
||||
base.push({
|
||||
name: '没货... ❌',
|
||||
time: '+10ms',
|
||||
desc: '柜子是空的,得去总仓库',
|
||||
color: '#f59e0b'
|
||||
})
|
||||
base.push({
|
||||
name: '总仓库 (源站) 🏭',
|
||||
time: resourceType.value === 'api' ? '+60ms' : '+40ms',
|
||||
desc: '翻山越岭把货取回来',
|
||||
color: '#e11d48'
|
||||
})
|
||||
if (resourceType.value !== 'api') {
|
||||
base.push({
|
||||
name: '顺手存柜子',
|
||||
time: '+8ms',
|
||||
desc: '下次邻居来拿就不用跑了',
|
||||
color: '#22c55e'
|
||||
})
|
||||
}
|
||||
}
|
||||
base.push({
|
||||
name: '拿到手 🎁',
|
||||
time: 'Total',
|
||||
desc: '交易完成',
|
||||
color: '#0ea5e9'
|
||||
})
|
||||
return base
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cdn {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header .title {
|
||||
font-weight: 800;
|
||||
font-size: 18px;
|
||||
}
|
||||
.header .subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chip {
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.chip.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
background: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-weight: 800;
|
||||
margin-top: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.note {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.flow {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.step {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.time {
|
||||
margin-left: auto;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,296 +0,0 @@
|
||||
<!--
|
||||
CicdPipelineDemo.vue
|
||||
CI/CD 流水线:自动炒菜机隐喻
|
||||
-->
|
||||
<template>
|
||||
<div class="cicd">
|
||||
<div class="header">
|
||||
<div>
|
||||
<div class="title">自动化流水线 (CI/CD)</div>
|
||||
<div class="subtitle">就像一台“全自动炒菜机”</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<label class="fail-toggle"
|
||||
><input type="checkbox" v-model="failTest" /> 混入一颗烂菜
|
||||
(模拟报错)</label
|
||||
>
|
||||
<button :disabled="running" @click="run" class="run-btn">
|
||||
{{ running ? '机器运转中...' : '开始做菜 (触发构建)' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="steps">
|
||||
<div class="step" v-for="step in steps" :key="step.id">
|
||||
<div class="step-head">
|
||||
<span class="badge" :class="step.status">{{
|
||||
statusIcon(step.status)
|
||||
}}</span>
|
||||
<span class="name">{{ step.name }}</span>
|
||||
</div>
|
||||
<div class="analogy">{{ step.analogy }}</div>
|
||||
<div class="desc">{{ step.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="log" v-if="log">
|
||||
<div class="log-title">🖥️ 机器日志</div>
|
||||
<pre><code>{{ log }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const steps = ref([
|
||||
{
|
||||
id: 'install',
|
||||
name: '安装依赖 (Install)',
|
||||
analogy: '🥬 准备食材',
|
||||
desc: 'npm install',
|
||||
status: 'idle'
|
||||
},
|
||||
{
|
||||
id: 'test',
|
||||
name: '自动测试 (Test)',
|
||||
analogy: '🔍 食品安检',
|
||||
desc: 'npm test',
|
||||
status: 'idle'
|
||||
},
|
||||
{
|
||||
id: 'build',
|
||||
name: '打包构建 (Build)',
|
||||
analogy: '🍳 下锅烹饪',
|
||||
desc: 'npm run build',
|
||||
status: 'idle'
|
||||
},
|
||||
{
|
||||
id: 'deploy',
|
||||
name: '自动部署 (Deploy)',
|
||||
analogy: '🍽️ 端上桌',
|
||||
desc: 'pm2 restart',
|
||||
status: 'idle'
|
||||
}
|
||||
])
|
||||
|
||||
const running = ref(false)
|
||||
const failTest = ref(false)
|
||||
const log = ref('')
|
||||
|
||||
const wait = (ms) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
const statusIcon = (status) => {
|
||||
if (status === 'done') return '✔'
|
||||
if (status === 'running') return '⏳'
|
||||
if (status === 'fail') return '✖'
|
||||
return '•'
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
steps.value = steps.value.map((s) => ({ ...s, status: 'idle' }))
|
||||
log.value = ''
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
if (running.value) return
|
||||
running.value = true
|
||||
reset()
|
||||
|
||||
const timeline = [
|
||||
{
|
||||
id: 'install',
|
||||
ms: 1000,
|
||||
log: '> 正在去菜市场买菜...\n> 成功买到 842 个包裹 (node_modules)'
|
||||
},
|
||||
{
|
||||
id: 'test',
|
||||
ms: 800,
|
||||
log: '> 正在检查食材新鲜度...\n> 单元测试运行中...'
|
||||
},
|
||||
{
|
||||
id: 'build',
|
||||
ms: 1200,
|
||||
log: '> 开始烹饪...\n> 正在压缩混淆代码...\n> 产出 dist/ 目录 (一盘好菜)'
|
||||
},
|
||||
{
|
||||
id: 'deploy',
|
||||
ms: 1000,
|
||||
log: '> 正在把菜端给顾客...\n> 重启服务器...\n> 上线成功!'
|
||||
}
|
||||
]
|
||||
|
||||
for (const item of timeline) {
|
||||
const step = steps.value.find((s) => s.id === item.id)
|
||||
step.status = 'running'
|
||||
log.value = item.log
|
||||
await wait(item.ms)
|
||||
|
||||
if (item.id === 'test' && failTest.value) {
|
||||
step.status = 'fail'
|
||||
log.value =
|
||||
'❌ 警告:发现一颗烂白菜!(测试失败)\n❌ 立即停机,防止端给顾客。'
|
||||
steps.value
|
||||
.filter((s) => s.id !== 'test')
|
||||
.forEach((s) => (s.status = 'idle'))
|
||||
running.value = false
|
||||
return
|
||||
}
|
||||
|
||||
step.status = 'done'
|
||||
}
|
||||
|
||||
log.value = '✅ 流程结束:大家吃得很开心 (服务正常运行)'
|
||||
running.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cicd {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 800;
|
||||
font-size: 18px;
|
||||
}
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.fail-toggle {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.run-btn {
|
||||
background: var(--vp-c-brand);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.run-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.step {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.step-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.badge.running {
|
||||
border-color: #f59e0b;
|
||||
color: #f59e0b;
|
||||
}
|
||||
.badge.done {
|
||||
border-color: #22c55e;
|
||||
color: #22c55e;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
.badge.fail {
|
||||
border-color: #ef4444;
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.analogy {
|
||||
color: var(--vp-c-brand);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 12px;
|
||||
margin-left: 32px;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.log {
|
||||
background: #1e1e20;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
color: #eee;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.log-title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -1,360 +0,0 @@
|
||||
<!--
|
||||
DeploymentArchitecture.vue
|
||||
全景图:快递配送隐喻
|
||||
-->
|
||||
<template>
|
||||
<div class="arch">
|
||||
<div class="header">
|
||||
<div>
|
||||
<div class="title">全景演示:一个请求的“奇幻漂流”</div>
|
||||
<div class="subtitle">
|
||||
点击下方按钮,看看三种模式的“配送路线”有什么不同
|
||||
</div>
|
||||
</div>
|
||||
<div class="modes">
|
||||
<button
|
||||
v-for="mode in modes"
|
||||
:key="mode.id"
|
||||
:class="['mode', { active: mode.id === currentMode }]"
|
||||
@click="currentMode = mode.id"
|
||||
>
|
||||
<span class="icon">{{ mode.icon }}</span>
|
||||
{{ mode.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow">
|
||||
<div
|
||||
v-for="(node, idx) in nodes"
|
||||
:key="node.name"
|
||||
class="node"
|
||||
:style="{ borderColor: node.color }"
|
||||
>
|
||||
<div class="node-head">
|
||||
<div class="dot" :style="{ background: node.color }">
|
||||
{{ node.icon }}
|
||||
</div>
|
||||
<div class="name-box">
|
||||
<div class="role">{{ node.role }}</div>
|
||||
<div class="tech-name">{{ node.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desc">{{ node.desc }}</div>
|
||||
<div v-if="idx < nodes.length - 1" class="arrow">→</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary">
|
||||
<div class="metric">
|
||||
<div class="label">当前场景</div>
|
||||
<div class="value">{{ currentModeLabel }}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">瓶颈环节</div>
|
||||
<div class="value">{{ bottleneck }}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">通俗解释</div>
|
||||
<div class="value">{{ advice }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const modes = [
|
||||
{ id: 'static', label: '看海报 (静态)', icon: '🖼️' },
|
||||
{ id: 'spa', label: '玩 App (SPA)', icon: '📱' },
|
||||
{ id: 'ssr', label: '刷动态 (SSR)', icon: '🔄' }
|
||||
]
|
||||
|
||||
const currentMode = ref('spa')
|
||||
|
||||
const currentModeLabel = computed(
|
||||
() => modes.find((m) => m.id === currentMode.value)?.label
|
||||
)
|
||||
|
||||
// 角色:User(寄件人), DNS(查号台), CDN(快递柜), WAF(保安), LB(大堂经理), Server(办事员), DB(档案室)
|
||||
const commonNodes = {
|
||||
user: {
|
||||
role: '寄件人',
|
||||
name: 'User',
|
||||
icon: '🧑',
|
||||
color: '#64748b',
|
||||
desc: '发出请求'
|
||||
},
|
||||
dns: {
|
||||
role: '查号台',
|
||||
name: 'DNS',
|
||||
icon: '📒',
|
||||
color: '#0ea5e9',
|
||||
desc: '查询 IP 地址'
|
||||
},
|
||||
cdn: {
|
||||
role: '快递柜',
|
||||
name: 'CDN',
|
||||
icon: '📦',
|
||||
color: '#22c55e',
|
||||
desc: '就近取货'
|
||||
},
|
||||
waf: {
|
||||
role: '保安',
|
||||
name: 'WAF',
|
||||
icon: '🛡️',
|
||||
color: '#ef4444',
|
||||
desc: '拦截黑客'
|
||||
},
|
||||
lb: {
|
||||
role: '大堂经理',
|
||||
name: 'LB',
|
||||
icon: '💁',
|
||||
color: '#f59e0b',
|
||||
desc: '分配窗口'
|
||||
},
|
||||
server: {
|
||||
role: '办事员',
|
||||
name: 'Server',
|
||||
icon: '👨💼',
|
||||
color: '#8b5cf6',
|
||||
desc: '处理业务'
|
||||
},
|
||||
db: {
|
||||
role: '档案室',
|
||||
name: 'Database',
|
||||
icon: '🗄️',
|
||||
color: '#d946ef',
|
||||
desc: '存取数据'
|
||||
},
|
||||
obj: {
|
||||
role: '仓库',
|
||||
name: 'OSS',
|
||||
icon: '🏭',
|
||||
color: '#f97316',
|
||||
desc: '拿静态文件'
|
||||
}
|
||||
}
|
||||
|
||||
const flowMap = {
|
||||
static: [
|
||||
{ ...commonNodes.user, desc: '想看一张图片' },
|
||||
{ ...commonNodes.dns, desc: '找到图片仓库地址' },
|
||||
{ ...commonNodes.cdn, desc: '家门口就有?直接拿走!' },
|
||||
{ ...commonNodes.obj, desc: '没有?去总仓库拿' }
|
||||
],
|
||||
spa: [
|
||||
{ ...commonNodes.user, desc: '打开网页 App' },
|
||||
{ ...commonNodes.dns, desc: '找到服务器地址' },
|
||||
{ ...commonNodes.cdn, desc: '先拿网页外壳 (HTML/JS)' },
|
||||
{ ...commonNodes.server, desc: '再拿动态数据 (API)' },
|
||||
{ ...commonNodes.db, desc: '查用户数据' }
|
||||
],
|
||||
ssr: [
|
||||
{ ...commonNodes.user, desc: '打开复杂网页' },
|
||||
{ ...commonNodes.dns, desc: '找到服务器地址' },
|
||||
{ ...commonNodes.lb, desc: '人多排队,你以此去 2 号窗口' },
|
||||
{ ...commonNodes.server, desc: '现场拼装好整个页面' },
|
||||
{ ...commonNodes.db, desc: '查所有需要的数据' }
|
||||
]
|
||||
}
|
||||
|
||||
const nodes = computed(() => flowMap[currentMode.value])
|
||||
|
||||
const bottleneck = computed(() => {
|
||||
switch (currentMode.value) {
|
||||
case 'static':
|
||||
return '几乎没有瓶颈,起飞!'
|
||||
case 'spa':
|
||||
return 'API 接口响应速度'
|
||||
case 'ssr':
|
||||
return '办事员 (Server) 拼装页面的速度'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const advice = computed(() => {
|
||||
switch (currentMode.value) {
|
||||
case 'static':
|
||||
return '这是最简单的模式。就像去看公告栏的海报(或者发传单),内容印死在上面了,所有人看到的都一样。速度最快!'
|
||||
case 'spa':
|
||||
return '就像送你一套乐高积木。先给你个空盒子和图纸(网页壳子),你的浏览器自己在本地把页面拼出来。拼好后怎么玩都快。'
|
||||
case 'ssr':
|
||||
return '就像点了一份热披萨。厨师(服务器)必须现场烤好,再热乎乎地送给你。虽然慢点,但保证新鲜、不仅能吃(能看)还能闻到香味(SEO友好)。'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.arch {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.header {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 800;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.modes {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.mode {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mode:hover {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.mode.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.flow {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.node {
|
||||
position: relative;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.node:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.node-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.name-box {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.role {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.tech-name {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
right: -14px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--vp-c-divider);
|
||||
font-size: 18px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.arrow {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
</style>
|
||||
@@ -1,524 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const backupType = ref('full')
|
||||
const isBackingUp = ref(false)
|
||||
const backupProgress = ref(0)
|
||||
const lastBackup = ref('2024-01-15 14:30')
|
||||
|
||||
const backups = ref([
|
||||
{ id: 1, type: 'full', date: '2024-01-15 14:30', size: '2.3 GB', status: 'completed' },
|
||||
{ id: 2, type: 'incremental', date: '2024-01-15 10:00', size: '156 MB', status: 'completed' },
|
||||
{ id: 3, type: 'full', date: '2024-01-14 14:30', size: '2.2 GB', status: 'completed' }
|
||||
])
|
||||
|
||||
const backupTypes = [
|
||||
{ id: 'full', name: '全量备份', desc: '备份所有数据,像拍整套照片', icon: '📸' },
|
||||
{ id: 'incremental', name: '增量备份', desc: '只备份新增/修改的部分', icon: '📝' },
|
||||
{ id: 'differential', name: '差异备份', desc: '备份自上次全量后的变化', icon: '🔄' }
|
||||
]
|
||||
|
||||
const startBackup = () => {
|
||||
isBackingUp.value = true
|
||||
backupProgress.value = 0
|
||||
|
||||
const interval = setInterval(() => {
|
||||
backupProgress.value += 5
|
||||
if (backupProgress.value >= 100) {
|
||||
clearInterval(interval)
|
||||
isBackingUp.value = false
|
||||
lastBackup.value = new Date().toLocaleString()
|
||||
|
||||
backups.value.unshift({
|
||||
id: Date.now(),
|
||||
type: backupType.value,
|
||||
date: lastBackup.value,
|
||||
size: backupType.value === 'full' ? '2.4 GB' : '180 MB',
|
||||
status: 'completed'
|
||||
})
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="deployment-backup">
|
||||
<div class="demo-header">
|
||||
<h3>备份演示</h3>
|
||||
<p class="subtitle">数据安全就像保险柜</p>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
<p>
|
||||
就像小明每天<strong>记账</strong>、<strong>保留发票</strong>,定期<strong>盘点库存</strong>,
|
||||
数据备份是防止数据丢失的最后一道防线。服务器故障、人为误操作都可能丢失数据!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<!-- 备份类型选择 -->
|
||||
<div class="backup-type-section">
|
||||
<div class="section-title">💾 选择备份类型</div>
|
||||
<div class="type-cards">
|
||||
<div
|
||||
v-for="type in backupTypes"
|
||||
:key="type.id"
|
||||
class="type-card"
|
||||
:class="{ active: backupType === type.id }"
|
||||
@click="backupType = type.id"
|
||||
>
|
||||
<span class="type-icon">{{ type.icon }}</span>
|
||||
<span class="type-name">{{ type.name }}</span>
|
||||
<span class="type-desc">{{ type.desc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 备份操作 -->
|
||||
<div class="backup-action">
|
||||
<div class="action-info">
|
||||
<div class="info-row">
|
||||
<span class="info-label">上次备份</span>
|
||||
<span class="info-value">{{ lastBackup }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">备份总数</span>
|
||||
<span class="info-value">{{ backups.length }} 份</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="!isBackingUp"
|
||||
@click="startBackup"
|
||||
class="btn primary"
|
||||
>
|
||||
🚀 开始备份
|
||||
</button>
|
||||
<button v-else class="btn" disabled>
|
||||
⏳ 备份中...
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 备份进度 -->
|
||||
<div v-if="isBackingUp" class="backup-progress">
|
||||
<div class="progress-header">
|
||||
<span class="progress-label">正在备份...</span>
|
||||
<span class="progress-percent">{{ backupProgress }}%</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress-fill"
|
||||
:style="{ width: `${backupProgress}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 备份历史 -->
|
||||
<div class="backup-history">
|
||||
<div class="section-title">📜 备份历史</div>
|
||||
<div class="history-list">
|
||||
<div
|
||||
v-for="backup in backups"
|
||||
:key="backup.id"
|
||||
class="history-item"
|
||||
>
|
||||
<div class="backup-icon">
|
||||
{{ backup.type === 'full' ? '📦' : '📄' }}
|
||||
</div>
|
||||
<div class="backup-info">
|
||||
<div class="backup-type">
|
||||
{{ backup.type === 'full' ? '全量备份' : backup.type === 'incremental' ? '增量备份' : '差异备份' }}
|
||||
</div>
|
||||
<div class="backup-date">{{ backup.date }}</div>
|
||||
</div>
|
||||
<div class="backup-meta">
|
||||
<div class="backup-size">{{ backup.size }}</div>
|
||||
<div class="backup-status">
|
||||
<span class="status-dot completed"></span>
|
||||
<span class="status-text">成功</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 备份策略 -->
|
||||
<div class="backup-strategy">
|
||||
<div class="strategy-title">🎯 推荐备份策略 (3-2-1 原则)</div>
|
||||
<div class="strategy-content">
|
||||
<div class="strategy-item">
|
||||
<div class="strategy-number">3</div>
|
||||
<div class="strategy-desc">至少保留 <strong>3 份</strong>备份</div>
|
||||
</div>
|
||||
<div class="strategy-item">
|
||||
<div class="strategy-number">2</div>
|
||||
<div class="strategy-desc">使用 <strong>2 种</strong>不同存储介质</div>
|
||||
</div>
|
||||
<div class="strategy-item">
|
||||
<div class="strategy-number">1</div>
|
||||
<div class="strategy-desc"><strong>1 份</strong>异地备份</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
💡 <strong>小明教训</strong>:曾经因系统崩溃丢了所有销售数据,现在每天自动备份!
|
||||
<strong>备份不是是否需要的问题,而是何时需要的问题。</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.deployment-backup {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
padding: 1.25rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.2rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.backup-type-section {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.type-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.type-card {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.type-card:hover {
|
||||
border-color: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.type-card.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.type-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.type-name {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.type-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.backup-action {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.action-info {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.6rem 1.25rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn.primary:hover {
|
||||
background: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.backup-progress {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.progress-percent {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 10px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--vp-c-brand), var(--vp-c-brand-1));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.backup-history {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.history-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.backup-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.backup-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.backup-type {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.backup-date {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.backup-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.backup-size {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.backup-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-dot.completed {
|
||||
background: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.backup-strategy {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-left: 3px solid var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.strategy-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.strategy-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.strategy-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.strategy-number {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.strategy-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 1rem 1.25rem;
|
||||
margin: 0;
|
||||
background: var(--vp-c-bg);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.backup-action {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.type-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,519 +1,192 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const files = ref([
|
||||
{ name: 'App.vue', size: '5KB', status: 'pending' },
|
||||
{ name: 'main.js', size: '2KB', status: 'pending' },
|
||||
{ name: 'utils.js', size: '8KB', status: 'pending' },
|
||||
{ name: 'style.css', size: '15KB', status: 'pending' }
|
||||
])
|
||||
|
||||
const buildProgress = ref(0)
|
||||
const buildStatus = ref('idle') // idle, building, completed
|
||||
const optimizedSize = ref('0KB')
|
||||
const originalSize = ref('30KB')
|
||||
|
||||
const startBuild = () => {
|
||||
buildStatus.value = 'building'
|
||||
buildProgress.value = 0
|
||||
files.value.forEach(f => f.status = 'pending')
|
||||
|
||||
// 模拟构建过程
|
||||
const steps = [
|
||||
{ progress: 20, file: 0 },
|
||||
{ progress: 40, file: 1 },
|
||||
{ progress: 60, file: 2 },
|
||||
{ progress: 80, file: 3 },
|
||||
{ progress: 100, file: -1 }
|
||||
]
|
||||
|
||||
steps.forEach((step, idx) => {
|
||||
setTimeout(() => {
|
||||
buildProgress.value = step.progress
|
||||
if (step.file >= 0) {
|
||||
files.value[step.file].status = 'completed'
|
||||
}
|
||||
if (idx === steps.length - 1) {
|
||||
buildStatus.value = 'completed'
|
||||
optimizedSize.value = '12KB'
|
||||
}
|
||||
}, (idx + 1) * 600)
|
||||
})
|
||||
}
|
||||
|
||||
const resetBuild = () => {
|
||||
buildStatus.value = 'idle'
|
||||
buildProgress.value = 0
|
||||
files.value.forEach(f => f.status = 'pending')
|
||||
optimizedSize.value = '0KB'
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--
|
||||
DeploymentBuildDemo.vue
|
||||
构建过程演示:原材料变成品(简化版)
|
||||
-->
|
||||
<template>
|
||||
<div class="deployment-build">
|
||||
<div class="demo-header">
|
||||
<h3>构建过程演示</h3>
|
||||
<p class="subtitle">把原材料变成成品的过程</p>
|
||||
<div class="header">
|
||||
<span class="icon">📦</span>
|
||||
<span class="title">代码构建</span>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
<p>
|
||||
就像小明制作咖啡前要<strong>研磨咖啡豆</strong>、<strong>调配比例</strong>、<strong>加热融合</strong>,
|
||||
代码构建也需要<strong>编译</strong>、<strong>压缩</strong>、<strong>优化</strong>才能变成可以部署的成品。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<!-- 源文件列表 -->
|
||||
<div class="source-files">
|
||||
<div class="section-title">📁 源代码文件</div>
|
||||
<div class="file-list">
|
||||
<div
|
||||
v-for="(file, idx) in files"
|
||||
:key="file.name"
|
||||
class="file-item"
|
||||
:class="{ completed: file.status === 'completed' }"
|
||||
>
|
||||
<div class="file-icon">📄</div>
|
||||
<div class="file-info">
|
||||
<div class="file-name">{{ file.name }}</div>
|
||||
<div class="file-size">{{ file.size }}</div>
|
||||
</div>
|
||||
<div class="file-status">
|
||||
<span v-if="file.status === 'completed'" class="status-badge success">✓ 已处理</span>
|
||||
<span v-else class="status-badge pending">待处理</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="flow">
|
||||
<div class="step" :class="{ done: buildProgress >= 25 }">
|
||||
<span class="num">1</span>
|
||||
<span class="text">解析依赖</span>
|
||||
</div>
|
||||
<span class="arrow">→</span>
|
||||
<div class="step" :class="{ done: buildProgress >= 50 }">
|
||||
<span class="num">2</span>
|
||||
<span class="text">编译转换</span>
|
||||
</div>
|
||||
<span class="arrow">→</span>
|
||||
<div class="step" :class="{ done: buildProgress >= 75 }">
|
||||
<span class="num">3</span>
|
||||
<span class="text">打包压缩</span>
|
||||
</div>
|
||||
<span class="arrow">→</span>
|
||||
<div class="step" :class="{ done: buildProgress >= 100 }">
|
||||
<span class="num">4</span>
|
||||
<span class="text">完成</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 构建流程 -->
|
||||
<div class="build-process">
|
||||
<div class="section-title">⚙️ 构建流程</div>
|
||||
<div class="pipeline">
|
||||
<div class="pipeline-step">
|
||||
<div class="step-icon">1️⃣</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">解析依赖</div>
|
||||
<div class="step-desc">分析文件导入关系</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pipeline-arrow">↓</div>
|
||||
<div class="pipeline-step">
|
||||
<div class="step-icon">2️⃣</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">编译转换</div>
|
||||
<div class="step-desc">Vue → JavaScript</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pipeline-arrow">↓</div>
|
||||
<div class="pipeline-step">
|
||||
<div class="step-icon">3️⃣</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">打包压缩</div>
|
||||
<div class="step-desc">合并文件,去除空格</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="bar">
|
||||
<div class="fill" :style="{ width: `${buildProgress}%` }"></div>
|
||||
</div>
|
||||
<div class="percent">{{ buildProgress }}%</div>
|
||||
</div>
|
||||
|
||||
<!-- 构建进度 -->
|
||||
<div class="build-status">
|
||||
<div class="status-header">
|
||||
<span class="status-label">构建进度</span>
|
||||
<span class="status-percent">{{ buildProgress }}%</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress-fill"
|
||||
:style="{ width: `${buildProgress}%` }"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- 对比展示 -->
|
||||
<div v-if="buildStatus === 'completed'" class="size-comparison">
|
||||
<div class="size-item original">
|
||||
<div class="size-label">原始大小</div>
|
||||
<div class="size-value">{{ originalSize }}</div>
|
||||
</div>
|
||||
<div class="size-arrow">→</div>
|
||||
<div class="size-item optimized">
|
||||
<div class="size-label">优化后</div>
|
||||
<div class="size-value success">{{ optimizedSize }}</div>
|
||||
</div>
|
||||
<div class="compression-badge">
|
||||
压缩 60%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
v-if="buildStatus === 'idle' || buildStatus === 'completed'"
|
||||
@click="startBuild"
|
||||
class="btn primary"
|
||||
>
|
||||
{{ buildStatus === 'idle' ? '开始构建' : '重新构建' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="buildStatus === 'completed'"
|
||||
@click="resetBuild"
|
||||
class="btn secondary"
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p v-if="buildStatus === 'idle'">
|
||||
💡 <strong>待机中</strong>:点击"开始构建"按钮,看看代码是如何一步步变成可部署的文件的。
|
||||
</p>
|
||||
<p v-else-if="buildStatus === 'building'">
|
||||
⏳ <strong>构建中</strong>:正在处理源文件,就像小明在准备咖啡材料...
|
||||
</p>
|
||||
<p v-else class="success-text">
|
||||
✅ <strong>构建完成</strong>:所有文件已打包压缩,体积减少了60%!就像把咖啡豆研磨成粉,更易冲泡。
|
||||
</p>
|
||||
<button @click="startBuild" class="build-btn" :disabled="building">
|
||||
{{ building ? '构建中...' : '▶ 开始构建' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const building = ref(false)
|
||||
const buildProgress = ref(0)
|
||||
|
||||
const startBuild = () => {
|
||||
if (building.value) return
|
||||
building.value = true
|
||||
buildProgress.value = 0
|
||||
|
||||
const interval = setInterval(() => {
|
||||
buildProgress.value += 5
|
||||
if (buildProgress.value >= 100) {
|
||||
clearInterval(interval)
|
||||
building.value = false
|
||||
setTimeout(() => {
|
||||
buildProgress.value = 0
|
||||
}, 2000)
|
||||
}
|
||||
}, 150)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.deployment-build {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.75rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
padding: 1.25rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.2rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: 1.25rem;
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.source-files {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
.header .icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.file-item.completed {
|
||||
border-color: var(--vp-c-brand-delta);
|
||||
background: var(--vp-c-brand-dimm);
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.file-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-badge.pending {
|
||||
background: var(--vp-c-bg-alt);
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.status-badge.success {
|
||||
background: var(--vp-c-brand-delta);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.build-process {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.pipeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.pipeline-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.pipeline-arrow {
|
||||
font-size: 1.5rem;
|
||||
color: var(--vp-c-brand);
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.build-status {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.status-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.status-percent {
|
||||
font-size: 1.1rem;
|
||||
.header .title {
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 10px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1rem;
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--vp-c-brand), var(--vp-c-brand-1));
|
||||
transition: width 0.6s ease;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.size-comparison {
|
||||
.flow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.size-item {
|
||||
.step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.size-label {
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.size-value {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.size-value.success {
|
||||
.step.done {
|
||||
background: var(--vp-c-brand-dimm);
|
||||
color: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.size-arrow {
|
||||
font-size: 1.2rem;
|
||||
.step .num {
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.bar {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fill {
|
||||
height: 100%;
|
||||
background: var(--vp-c-brand);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.percent {
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.compression-badge {
|
||||
position: absolute;
|
||||
top: -0.5rem;
|
||||
right: 1rem;
|
||||
.build-btn {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.95rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn.primary:hover {
|
||||
.build-btn:hover:not(:disabled) {
|
||||
background: var(--vp-c-brand-1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn.secondary {
|
||||
background: var(--vp-c-bg-alt);
|
||||
color: var(--vp-c-text-1);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.btn.secondary:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 1rem 1.25rem;
|
||||
margin: 0;
|
||||
background: var(--vp-c-bg);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.success-text {
|
||||
color: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.file-item {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.size-comparison {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.compression-badge {
|
||||
position: static;
|
||||
}
|
||||
.build-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,414 +0,0 @@
|
||||
<template>
|
||||
<div class="deployment-cdn-demo">
|
||||
<div class="demo-header">
|
||||
<span class="icon">🌍</span>
|
||||
<span class="title">CDN 加速原理</span>
|
||||
<span class="subtitle">把资源送到用户家门口</span>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
<strong>CDN</strong>(Content Delivery Network)就像在全世界开了连锁仓库,用户访问时从<strong>最近的仓库</strong>取货,速度超快!
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<div class="world-map">
|
||||
<div class="server-origin">
|
||||
<span class="server-icon">🏠</span>
|
||||
<span class="server-label">源站服务器<br/>(北京)</span>
|
||||
</div>
|
||||
|
||||
<div class="cdn-nodes">
|
||||
<div
|
||||
v-for="node in cdnNodes"
|
||||
:key="node.id"
|
||||
class="cdn-node"
|
||||
:class="{ active: activeNode === node.id }"
|
||||
@click="selectNode(node)"
|
||||
>
|
||||
<span class="node-icon">{{ node.icon }}</span>
|
||||
<span class="node-label">{{ node.city }}</span>
|
||||
<span class="node-time">{{ node.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-requests">
|
||||
<div
|
||||
v-for="user in users"
|
||||
:key="user.id"
|
||||
class="user-request"
|
||||
:class="{ active: activeUser === user.id }"
|
||||
@click="selectUser(user)"
|
||||
>
|
||||
<span class="user-icon">{{ user.icon }}</span>
|
||||
<span class="user-label">{{ user.location }}</span>
|
||||
<span class="user-arrow">→</span>
|
||||
<span class="user-target">{{ user.cdnNode }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-table">
|
||||
<div class="table-title">⚡ 性能对比</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用户位置</th>
|
||||
<th>不使用 CDN</th>
|
||||
<th>使用 CDN</th>
|
||||
<th>提速</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in speedData" :key="row.location">
|
||||
<td>{{ row.location }}</td>
|
||||
<td class="slow">{{ row.withoutCdn }}</td>
|
||||
<td class="fast">{{ row.withCdn }}</td>
|
||||
<td class="speedup">{{ row.speedup }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="benefits">
|
||||
<div class="benefit-title">✨ CDN 的好处</div>
|
||||
<div class="benefit-list">
|
||||
<div class="benefit-item">
|
||||
<span class="benefit-icon">🚀</span>
|
||||
<span class="benefit-text">访问速度提升 50-80%</span>
|
||||
</div>
|
||||
<div class="benefit-item">
|
||||
<span class="benefit-icon">💰</span>
|
||||
<span class="benefit-text">节省源站带宽成本</span>
|
||||
</div>
|
||||
<div class="benefit-item">
|
||||
<span class="benefit-icon">🛡️</span>
|
||||
<span class="benefit-text">DDoS 防护能力</span>
|
||||
</div>
|
||||
<div class="benefit-item">
|
||||
<span class="benefit-icon">📱</span>
|
||||
<span class="benefit-text">全球覆盖无死角</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>适用场景:</strong>静态资源(图片、CSS、JS)最适合上 CDN,动态数据还是走源站。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const activeNode = ref(null)
|
||||
const activeUser = ref(null)
|
||||
|
||||
const cdnNodes = [
|
||||
{ id: 'beijing', city: '北京', icon: '🏙️', time: '10ms' },
|
||||
{ id: 'shanghai', city: '上海', icon: '🏙️', time: '15ms' },
|
||||
{ id: 'tokyo', city: '东京', icon: '🗼', time: '20ms' },
|
||||
{ id: 'london', city: '伦敦', icon: '🎡', time: '25ms' },
|
||||
{ id: 'newyork', city: '纽约', icon: '🗽', time: '18ms' }
|
||||
]
|
||||
|
||||
const users = [
|
||||
{ id: 'user1', location: '北京用户', cdnNode: '北京节点', icon: '👤' },
|
||||
{ id: 'user2', location: '纽约用户', cdnNode: '纽约节点', icon: '👤' },
|
||||
{ id: 'user3', location: '伦敦用户', cdnNode: '伦敦节点', icon: '👤' }
|
||||
]
|
||||
|
||||
const speedData = [
|
||||
{ location: '北京', withoutCdn: '10ms', withCdn: '10ms', speedup: '-' },
|
||||
{ location: '上海', withoutCdn: '30ms', withCdn: '15ms', speedup: '50%' },
|
||||
{ location: '纽约', withoutCdn: '200ms', withCdn: '18ms', speedup: '91%' },
|
||||
{ location: '伦敦', withoutCdn: '180ms', withCdn: '25ms', speedup: '86%' }
|
||||
]
|
||||
|
||||
const selectNode = (node) => {
|
||||
activeNode.value = node.id
|
||||
activeUser.value = null
|
||||
}
|
||||
|
||||
const selectUser = (user) => {
|
||||
activeUser.value = user.id
|
||||
activeNode.value = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.deployment-cdn-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.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 strong {
|
||||
color: var(--vp-c-brand-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.world-map {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.server-origin {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
background: linear-gradient(135deg, var(--vp-c-brand-soft), var(--vp-c-bg));
|
||||
border-radius: 8px;
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.server-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.server-label {
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cdn-nodes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.cdn-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.cdn-node:hover,
|
||||
.cdn-node.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.node-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.node-label {
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.node-time {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-requests {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.user-request {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.user-request:hover,
|
||||
.user-request.active {
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.user-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.user-label {
|
||||
color: var(--vp-c-text-1);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-arrow {
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.user-target {
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.comparison-table {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.table-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
thead {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
border-bottom: 2px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
td.slow {
|
||||
color: #dc3545;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
td.fast {
|
||||
color: #28a745;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
td.speedup {
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.benefits {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.benefit-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.benefit-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.benefit-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.benefit-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.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) {
|
||||
.benefit-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.cdn-nodes {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,474 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const checklist = ref([
|
||||
{ id: 1, category: '代码', task: '代码已通过测试', checked: true, critical: true },
|
||||
{ id: 2, category: '代码', task: '移除调试代码和 console.log', checked: false, critical: false },
|
||||
{ id: 3, category: '环境', task: '生产环境配置已设置', checked: true, critical: true },
|
||||
{ id: 4, category: '环境', task: '数据库迁移脚本已准备', checked: false, critical: true },
|
||||
{ id: 5, category: '环境', task: '环境变量已配置', checked: true, critical: true },
|
||||
{ id: 6, category: '安全', task: '敏感信息已从代码中移除', checked: true, critical: true },
|
||||
{ id: 7, category: '安全', task: 'HTTPS 证书已配置', checked: false, critical: true },
|
||||
{ id: 8, category: '安全', task: '防火墙规则已设置', checked: true, critical: false },
|
||||
{ id: 9, category: '性能', task: '静态资源已压缩', checked: true, critical: false },
|
||||
{ id: 10, category: '性能', task: 'CDN 已配置', checked: false, critical: false },
|
||||
{ id: 11, category: '监控', task: '日志收集已启用', checked: true, critical: false },
|
||||
{ id: 12, category: '监控', task: '错误监控已配置', checked: false, critical: true },
|
||||
{ id: 13, category: '备份', task: '数据备份策略已确认', checked: true, critical: true },
|
||||
{ id: 14, category: '回滚', task: '回滚方案已准备', checked: false, critical: true },
|
||||
{ id: 15, category: '文档', task: '部署文档已更新', checked: false, critical: false }
|
||||
])
|
||||
|
||||
const categories = computed(() => {
|
||||
const cats = [...new Set(checklist.value.map(item => item.category))]
|
||||
return cats
|
||||
})
|
||||
|
||||
const itemsByCategory = (category) => {
|
||||
return checklist.value.filter(item => item.category === category)
|
||||
}
|
||||
|
||||
const totalTasks = computed(() => checklist.value.length)
|
||||
const completedTasks = computed(() => checklist.value.filter(item => item.checked).length)
|
||||
const progress = computed(() => Math.round((completedTasks.value / totalTasks.value) * 100))
|
||||
|
||||
const criticalCompleted = computed(() => {
|
||||
const criticalItems = checklist.value.filter(item => item.critical)
|
||||
return criticalItems.filter(item => item.checked).length
|
||||
})
|
||||
|
||||
const criticalTotal = computed(() => checklist.value.filter(item => item.critical).length)
|
||||
const allCriticalDone = computed(() => criticalCompleted.value === criticalTotal.value)
|
||||
|
||||
const readyToDeploy = computed(() => {
|
||||
return progress.value === 100 && allCriticalDone.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="deployment-checklist">
|
||||
<div class="demo-header">
|
||||
<h3>上线前检查清单</h3>
|
||||
<p class="subtitle">确保万无一失的起飞前检查</p>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
<p>
|
||||
就像飞机起飞前飞行员要逐项检查<strong>仪表盘</strong>、<strong>油量</strong>、<strong>跑道</strong>,
|
||||
软件上线前的检查清单能避免很多低级错误导致的线上事故。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<!-- 进度概览 -->
|
||||
<div class="progress-overview">
|
||||
<div class="progress-circle">
|
||||
<div class="progress-value" :class="{ success: progress === 100 }">
|
||||
{{ progress }}%
|
||||
</div>
|
||||
<svg class="progress-ring" viewBox="0 0 100 100">
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="45"
|
||||
fill="none"
|
||||
:stroke="progress === 100 ? 'var(--vp-c-brand-delta)' : 'var(--vp-c-bg-alt)'"
|
||||
stroke-width="8"
|
||||
/>
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="45"
|
||||
fill="none"
|
||||
:stroke="readyToDeploy ? 'var(--vp-c-brand-delta)' : 'var(--vp-c-brand)'"
|
||||
stroke-width="8"
|
||||
stroke-dasharray="283"
|
||||
:stroke-dashoffset="283 - (283 * progress) / 100"
|
||||
transform="rotate(-90 50 50)"
|
||||
class="progress-ring-circle"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="progress-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ completedTasks }}/{{ totalTasks }}</span>
|
||||
<span class="stat-label">总任务</span>
|
||||
</div>
|
||||
<div class="stat-item critical">
|
||||
<span class="stat-value">{{ criticalCompleted }}/{{ criticalTotal }}</span>
|
||||
<span class="stat-label">关键任务</span>
|
||||
</div>
|
||||
<div class="deploy-status" :class="{ ready: readyToDeploy }">
|
||||
{{ readyToDeploy ? '✅ 准备就绪' : '⚠️ 还有待办项' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类检查清单 -->
|
||||
<div class="checklist-categories">
|
||||
<div
|
||||
v-for="category in categories"
|
||||
:key="category"
|
||||
class="category-section"
|
||||
>
|
||||
<div class="category-title">{{ category }}</div>
|
||||
<div class="checklist-items">
|
||||
<div
|
||||
v-for="item in itemsByCategory(category)"
|
||||
:key="item.id"
|
||||
class="checklist-item"
|
||||
:class="{ checked: item.checked, critical: item.critical }"
|
||||
>
|
||||
<label class="checkbox-wrapper">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="item.checked"
|
||||
class="checkbox"
|
||||
/>
|
||||
<span class="checkbox-custom"></span>
|
||||
<span class="task-text">{{ item.task }}</span>
|
||||
<span v-if="item.critical" class="critical-badge">关键</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div v-if="!allCriticalDone" class="warning-box">
|
||||
<span class="warning-icon">⚠️</span>
|
||||
<span class="warning-text">
|
||||
还有 <strong>{{ criticalTotal - criticalCompleted }}</strong> 项关键任务未完成,
|
||||
建议优先处理这些关键项后再上线。
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="progress < 100" class="info-box-inline">
|
||||
<span class="info-icon">ℹ️</span>
|
||||
<span class="info-text">
|
||||
关键任务已完成!还有 <strong>{{ totalTasks - completedTasks }}</strong> 项可选任务,
|
||||
建议尽快完成以提升系统稳定性。
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="success-box">
|
||||
<span class="success-icon">🎉</span>
|
||||
<span class="success-text">
|
||||
太棒了!所有检查项都已完成,可以放心上线了!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
💡 <strong>最佳实践</strong>:将此检查清单集成到 CI/CD 流程中,
|
||||
关键项不通过则禁止自动部署,避免人为疏忽导致线上故障。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.deployment-checklist {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
padding: 1.25rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.2rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.progress-overview {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.progress-circle {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.progress-ring {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.progress-ring-circle {
|
||||
transition: stroke-dashoffset 0.5s ease;
|
||||
}
|
||||
|
||||
.progress-value {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.progress-value.success {
|
||||
color: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.progress-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.stat-item.critical .stat-value {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.deploy-status {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
padding: 0.5rem 1rem;
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.deploy-status.ready {
|
||||
background: var(--vp-c-brand-dimm);
|
||||
color: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.checklist-categories {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.category-section {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.checklist-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.checklist-item {
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.checklist-item.checked {
|
||||
background: var(--vp-c-brand-dimm);
|
||||
border-color: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.checklist-item.critical {
|
||||
border-left: 3px solid #ef4444;
|
||||
}
|
||||
|
||||
.checklist-item.critical.checked {
|
||||
border-left-color: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.checkbox-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.checkbox-custom {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.checkbox:checked + .checkbox-custom {
|
||||
background: var(--vp-c-brand);
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.checkbox:checked + .checkbox-custom::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.task-text {
|
||||
flex: 1;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.checklist-item.checked .task-text {
|
||||
text-decoration: line-through;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.critical-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.15rem 0.4rem;
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.warning-box,
|
||||
.info-box-inline,
|
||||
.success-box {
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.warning-box {
|
||||
background: #fef3c7;
|
||||
border-left: 3px solid #f59e0b;
|
||||
}
|
||||
|
||||
.info-box-inline {
|
||||
background: #dbeafe;
|
||||
border-left: 3px solid #3b82f6;
|
||||
}
|
||||
|
||||
.success-box {
|
||||
background: var(--vp-c-brand-dimm);
|
||||
border-left: 3px solid var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.warning-icon,
|
||||
.info-icon,
|
||||
.success-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.warning-text,
|
||||
.info-text,
|
||||
.success-text {
|
||||
flex: 1;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 1rem 1.25rem;
|
||||
margin: 0;
|
||||
background: var(--vp-c-bg);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.progress-overview {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.checkbox-wrapper {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.task-text {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,358 +1,151 @@
|
||||
<!--
|
||||
DeploymentCicdDemo.vue
|
||||
CI/CD 自动化(精简版)
|
||||
-->
|
||||
<template>
|
||||
<div class="deployment-cicd-demo">
|
||||
<div class="demo-header">
|
||||
<div class="deployment-cicd">
|
||||
<div class="header">
|
||||
<span class="icon">🔄</span>
|
||||
<span class="title">CI/CD 自动化流程</span>
|
||||
<span class="title">CI/CD 自动化</span>
|
||||
<span class="subtitle">从代码到上线,一键搞定</span>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
<strong>CI/CD</strong> 就像一条<strong>自动化流水线</strong>:你只管写代码,剩下的测试、构建、部署,流水线自动帮你完成!
|
||||
<div class="pipeline">
|
||||
<div class="step">
|
||||
<span class="num">1</span>
|
||||
<span class="text">代码推送</span>
|
||||
</div>
|
||||
<span class="arrow">→</span>
|
||||
<div class="step">
|
||||
<span class="num">2</span>
|
||||
<span class="text">自动测试</span>
|
||||
</div>
|
||||
<span class="arrow">→</span>
|
||||
<div class="step">
|
||||
<span class="num">3</span>
|
||||
<span class="text">自动构建</span>
|
||||
</div>
|
||||
<span class="arrow">→</span>
|
||||
<div class="step">
|
||||
<span class="num">4</span>
|
||||
<span class="text">自动部署</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<div class="pipeline">
|
||||
<div
|
||||
v-for="(step, index) in pipelineSteps"
|
||||
:key="index"
|
||||
class="pipeline-step"
|
||||
:class="{ active: currentStep === index, completed: currentStep > index }"
|
||||
>
|
||||
<div class="step-connector" v-if="index > 0"></div>
|
||||
<div class="step-icon">{{ step.icon }}</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">{{ step.title }}</div>
|
||||
<div class="step-desc">{{ step.desc }}</div>
|
||||
</div>
|
||||
<div class="step-status" v-if="currentStep === index">
|
||||
<span class="spinner">⏳</span>
|
||||
</div>
|
||||
<div class="step-status" v-if="currentStep > index">
|
||||
<span class="check">✅</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="compare">
|
||||
<div class="col">
|
||||
<div class="title">手动部署</div>
|
||||
<div class="item">❌ 容易出错</div>
|
||||
</div>
|
||||
|
||||
<div class="manual-vs-auto">
|
||||
<div class="compare-column manual">
|
||||
<div class="column-header">
|
||||
<span class="column-icon">😰</span>
|
||||
<span class="column-title">手动部署</span>
|
||||
</div>
|
||||
<div class="column-body">
|
||||
<div class="step-list">
|
||||
<div class="step-item">❌ 手动改代码</div>
|
||||
<div class="step-item">❌ 手动上传 FTP</div>
|
||||
<div class="step-item">❌ 手动 SSH 连接</div>
|
||||
<div class="step-item">❌ 手动重启服务</div>
|
||||
<div class="step-item">❌ 容易出错,慢</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="compare-column auto">
|
||||
<div class="column-header">
|
||||
<span class="column-icon">🎉</span>
|
||||
<span class="column-title">CI/CD 自动化</span>
|
||||
</div>
|
||||
<div class="column-body">
|
||||
<div class="step-list">
|
||||
<div class="step-item">✅ Git 推送代码</div>
|
||||
<div class="step-item">✅ 自动运行测试</div>
|
||||
<div class="step-item">✅ 自动构建打包</div>
|
||||
<div class="step-item">✅ 自动部署上线</div>
|
||||
<div class="step-item">✅ 快速可靠</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col highlight">
|
||||
<div class="title">CI/CD</div>
|
||||
<div class="item">✅ 快速可靠</div>
|
||||
</div>
|
||||
|
||||
<button @click="startPipeline" class="start-btn" :disabled="isRunning">
|
||||
{{ isRunning ? '⏳ 流水线运行中...' : '▶ 启动流水线' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>推荐工具:</strong>GitHub Actions(免费)、GitLab CI、Jenkins。几分钟就能配置好!
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const currentStep = ref(-1)
|
||||
const isRunning = ref(false)
|
||||
|
||||
const pipelineSteps = [
|
||||
{ icon: '💻', title: '代码提交', desc: 'git push 到 GitHub' },
|
||||
{ icon: '🧪', title: '自动测试', desc: '运行单元测试' },
|
||||
{ icon: '📦', title: '自动构建', desc: 'npm run build' },
|
||||
{ icon: '🚀', title: '自动部署', desc: '部署到服务器' },
|
||||
{ icon: '✨', title: '完成上线', desc: '新版本可用' }
|
||||
]
|
||||
|
||||
const startPipeline = () => {
|
||||
if (isRunning.value) return
|
||||
|
||||
isRunning.value = true
|
||||
currentStep.value = -1
|
||||
|
||||
const steps = [0, 1, 2, 3, 4]
|
||||
let delay = 0
|
||||
|
||||
steps.forEach((step, index) => {
|
||||
delay += 1500
|
||||
setTimeout(() => {
|
||||
currentStep.value = step
|
||||
if (index === steps.length - 1) {
|
||||
setTimeout(() => {
|
||||
isRunning.value = false
|
||||
setTimeout(() => {
|
||||
currentStep.value = -1
|
||||
}, 2000)
|
||||
}, 1000)
|
||||
}
|
||||
}, delay)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.deployment-cicd-demo {
|
||||
.deployment-cicd {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
padding: 0.75rem;
|
||||
margin: 1rem 0;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header .icon {
|
||||
.header .icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.demo-header .title {
|
||||
font-weight: bold;
|
||||
.header .title {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.demo-header .subtitle {
|
||||
.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 strong {
|
||||
color: var(--vp-c-brand-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.pipeline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
overflow-x: auto;
|
||||
padding: 0.5rem 0;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.pipeline-step {
|
||||
.step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
min-width: 100px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-connector {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: -50%;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--vp-c-divider);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.pipeline-step.completed .step-connector {
|
||||
background: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
z-index: 1;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.step-info {
|
||||
gap: 0.25rem;
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
.step .num {
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.step .text {
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.compare {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.col.highlight {
|
||||
background: var(--vp-c-brand-dimm);
|
||||
}
|
||||
|
||||
.col .title {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.step-status {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.pipeline-step.active .step-icon {
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
.pipeline-step.completed .step-icon {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.manual-vs-auto {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.compare-column {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.compare-column.manual {
|
||||
background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
|
||||
}
|
||||
|
||||
.compare-column.auto {
|
||||
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
|
||||
}
|
||||
|
||||
.column-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.column-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.column-title {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.column-body {
|
||||
background: white;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.step-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
.col .item {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.4rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
width: 100%;
|
||||
padding: 0.875rem;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.start-btn:hover:not(:disabled) {
|
||||
background: var(--vp-c-brand-1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.start-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.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) {
|
||||
.manual-vs-auto {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.pipeline {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pipeline-step {
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,315 +1,135 @@
|
||||
<!--
|
||||
DeploymentDnsDemo.vue
|
||||
DNS 解析(精简版)
|
||||
-->
|
||||
<template>
|
||||
<div class="deployment-dns-demo">
|
||||
<div class="demo-header">
|
||||
<div class="deployment-dns">
|
||||
<div class="header">
|
||||
<span class="icon">🔍</span>
|
||||
<span class="title">DNS 解析流程</span>
|
||||
<span class="subtitle">域名是怎么变成 IP 地址的</span>
|
||||
<span class="title">DNS 解析</span>
|
||||
<span class="subtitle">把"好记的名字"变成"机器能懂的IP"</span>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
想象你要给朋友打电话,但你只记得他的名字,不记得电话号。<strong>DNS</strong> 就像一个<strong>电话本</strong>,帮你把名字翻译成号码。
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<div class="dns-flow">
|
||||
<div class="flow-step" :class="{ active: currentStep >= 1 }">
|
||||
<div class="step-icon">💻</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">用户输入域名</div>
|
||||
<div class="step-desc">在浏览器输入 coffee.example.com</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-arrow">→</div>
|
||||
|
||||
<div class="flow-step" :class="{ active: currentStep >= 2 }">
|
||||
<div class="step-icon">📋</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">查询本地 DNS</div>
|
||||
<div class="step-desc">先查电脑的"电话本"(缓存)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-arrow">→</div>
|
||||
|
||||
<div class="flow-step" :class="{ active: currentStep >= 3 }">
|
||||
<div class="step-icon">🌐</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">向上级 DNS 查询</div>
|
||||
<div class="step-desc">本地没有,问"上级电话局"</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-arrow">→</div>
|
||||
|
||||
<div class="flow-step" :class="{ active: currentStep >= 4 }">
|
||||
<div class="step-icon">🏠</div>
|
||||
<div class="step-info">
|
||||
<div class="step-title">返回 IP 地址</div>
|
||||
<div class="step-desc">找到了!123.45.67.89</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow">
|
||||
<div class="step">
|
||||
<span class="emoji">💻</span>
|
||||
<span class="text">用户输入域名</span>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<div class="example-title">DNS 记录示例</div>
|
||||
<div class="record-list">
|
||||
<div class="record-item">
|
||||
<span class="record-type">A 记录</span>
|
||||
<span class="record-name">coffee.example.com</span>
|
||||
<span class="record-arrow">→</span>
|
||||
<span class="record-value">123.45.67.89</span>
|
||||
</div>
|
||||
<div class="record-item">
|
||||
<span class="record-type">CNAME</span>
|
||||
<span class="record-name">www.coffee.example.com</span>
|
||||
<span class="record-arrow">→</span>
|
||||
<span class="record-value">coffee.example.com</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="arrow">→</span>
|
||||
<div class="step">
|
||||
<span class="emoji">📋</span>
|
||||
<span class="text">查询 DNS</span>
|
||||
</div>
|
||||
<span class="arrow">→</span>
|
||||
<div class="step success">
|
||||
<span class="emoji">✅</span>
|
||||
<span class="text">返回 IP</span>
|
||||
</div>
|
||||
|
||||
<button @click="playAnimation" class="play-btn">
|
||||
{{ isPlaying ? '▶ 重新播放' : '▶ 播放动画' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>关键点:</strong>DNS 修改不是立即生效的,全球同步需要几分钟到几小时(受 TTL 影响)。
|
||||
<div class="example">
|
||||
<span class="label">示例:</span>
|
||||
<code>example.com → 192.168.1.1</code>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const currentStep = ref(0)
|
||||
const isPlaying = ref(false)
|
||||
|
||||
const playAnimation = () => {
|
||||
isPlaying.value = true
|
||||
currentStep.value = 0
|
||||
|
||||
const steps = [1, 2, 3, 4]
|
||||
let delay = 0
|
||||
|
||||
steps.forEach((step, index) => {
|
||||
delay += 800
|
||||
setTimeout(() => {
|
||||
currentStep.value = step
|
||||
if (index === steps.length - 1) {
|
||||
setTimeout(() => {
|
||||
isPlaying.value = false
|
||||
}, 1000)
|
||||
}
|
||||
}, delay)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.deployment-dns-demo {
|
||||
.deployment-dns {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
padding: 0.75rem;
|
||||
margin: 1rem 0;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header .icon {
|
||||
.header .icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.demo-header .title {
|
||||
font-weight: bold;
|
||||
.header .title {
|
||||
font-weight: 700;
|
||||
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 strong {
|
||||
color: var(--vp-c-brand-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.dns-flow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.5rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.flow-step.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.step-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 0.75rem;
|
||||
.header .subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.4;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.flow-arrow {
|
||||
font-size: 1.25rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.example-box {
|
||||
.flow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.example-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.record-list {
|
||||
.step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 0.25rem;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.record-type {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
font-size: 0.7rem;
|
||||
border-radius: 6px;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.record-name {
|
||||
color: var(--vp-c-text-2);
|
||||
flex: 1;
|
||||
font-family: monospace;
|
||||
.step.success {
|
||||
background: var(--vp-c-brand-dimm);
|
||||
color: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.record-arrow {
|
||||
.step .emoji {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.step .text {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.record-value {
|
||||
color: var(--vp-c-brand);
|
||||
font-family: monospace;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.play-btn:hover {
|
||||
background: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
.example {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.example .label {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.info-box .icon {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dns-flow {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.flow-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
min-width: 45%;
|
||||
}
|
||||
.example code {
|
||||
font-family: var(--vp-font-family-mono);
|
||||
color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,395 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const environments = ref(['dev', 'test', 'prod'])
|
||||
const currentEnv = ref('dev')
|
||||
|
||||
const envConfigs = {
|
||||
dev: {
|
||||
name: '开发环境',
|
||||
icon: '🔧',
|
||||
color: '#3b82f6',
|
||||
apiUrl: 'http://dev.api.example.com',
|
||||
dbUrl: 'dev-db.example.com',
|
||||
features: ['热重载', '详细日志', '调试工具'],
|
||||
analogy: '小明的测试厨房,不断尝试新配方'
|
||||
},
|
||||
test: {
|
||||
name: '测试环境',
|
||||
icon: '🧪',
|
||||
color: '#f59e0b',
|
||||
apiUrl: 'http://test.api.example.com',
|
||||
dbUrl: 'test-db.example.com',
|
||||
features: ['模拟数据', '自动化测试', 'Bug 追踪'],
|
||||
analogy: '内部试吃环节,确保品质稳定'
|
||||
},
|
||||
prod: {
|
||||
name: '生产环境',
|
||||
icon: '🚀',
|
||||
color: '#10b981',
|
||||
apiUrl: 'https://api.example.com',
|
||||
dbUrl: 'prod-db.example.com',
|
||||
features: ['性能优化', '安全加固', '监控告警'],
|
||||
analogy: '正式营业的咖啡店,服务真实顾客'
|
||||
}
|
||||
}
|
||||
|
||||
const currentConfig = computed(() => envConfigs[currentEnv.value])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="deployment-environment">
|
||||
<div class="demo-header">
|
||||
<h3>环境配置演示</h3>
|
||||
<p class="subtitle">开发、测试、生产三分离</p>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
<p>
|
||||
就像小明有<strong>研发厨房</strong>、<strong>试吃区域</strong>、<strong>正式门店</strong>三个独立空间,
|
||||
软件也需要隔离的环境避免开发测试影响真实用户。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<!-- 环境切换 -->
|
||||
<div class="env-tabs">
|
||||
<div
|
||||
v-for="env in environments"
|
||||
:key="env"
|
||||
class="env-tab"
|
||||
:class="{ active: currentEnv === env }"
|
||||
@click="currentEnv = env"
|
||||
:style="{ '--env-color': envConfigs[env].color }"
|
||||
>
|
||||
<span class="tab-icon">{{ envConfigs[env].icon }}</span>
|
||||
<span class="tab-name">{{ envConfigs[env].name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 环境详情 -->
|
||||
<div class="env-detail">
|
||||
<div class="detail-header" :style="{ '--env-color': currentConfig.color }">
|
||||
<span class="detail-icon">{{ currentConfig.icon }}</span>
|
||||
<div class="detail-info">
|
||||
<h4>{{ currentConfig.name }}</h4>
|
||||
<p class="detail-analogy">{{ currentConfig.analogy }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-list">
|
||||
<div class="config-item">
|
||||
<span class="config-label">API 地址</span>
|
||||
<span class="config-value">{{ currentConfig.apiUrl }}</span>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="config-label">数据库</span>
|
||||
<span class="config-value">{{ currentConfig.dbUrl }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="features">
|
||||
<div class="features-title">✨ 特性</div>
|
||||
<div class="features-list">
|
||||
<span
|
||||
v-for="feature in currentConfig.features"
|
||||
:key="feature"
|
||||
class="feature-tag"
|
||||
>
|
||||
{{ feature }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 流程说明 -->
|
||||
<div class="flow-diagram">
|
||||
<div class="flow-title">🔄 环境流转</div>
|
||||
<div class="flow-steps">
|
||||
<div class="flow-step" :class="{ active: currentEnv === 'dev' }">
|
||||
<div class="step-badge">1</div>
|
||||
<div class="step-text">开发</div>
|
||||
</div>
|
||||
<div class="flow-arrow">→</div>
|
||||
<div class="flow-step" :class="{ active: currentEnv === 'test' }">
|
||||
<div class="step-badge">2</div>
|
||||
<div class="step-text">测试</div>
|
||||
</div>
|
||||
<div class="flow-arrow">→</div>
|
||||
<div class="flow-step" :class="{ active: currentEnv === 'prod' }">
|
||||
<div class="step-badge">3</div>
|
||||
<div class="step-text">生产</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
💡 <strong>最佳实践</strong>:永远不要在开发环境直接修改生产配置!就像小明不会在正式营业时突然换咖啡配方。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.deployment-environment {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
padding: 1.25rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.2rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.env-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.env-tab {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.env-tab:hover {
|
||||
border-color: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.env-tab.active {
|
||||
border-color: var(--env-color);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.tab-name {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.env-detail {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid var(--env-color);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.detail-icon {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.detail-info h4 {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-size: 1.1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.detail-analogy {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.config-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.config-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.config-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.config-value {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-brand-1);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.features {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.features-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.features-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.feature-tag {
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand-1);
|
||||
padding: 0.3rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.flow-diagram {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.flow-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.flow-steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 6px;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
transition: all 0.3s ease;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.flow-step.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.step-badge {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.flow-arrow {
|
||||
font-size: 1.2rem;
|
||||
color: var(--vp-c-brand);
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 1rem 1.25rem;
|
||||
margin: 0;
|
||||
background: var(--vp-c-bg);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.env-tabs {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.flow-steps {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flow-arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,98 +1,28 @@
|
||||
<!--
|
||||
DeploymentHttpsDemo.vue
|
||||
HTTPS 安全(精简版)
|
||||
-->
|
||||
<template>
|
||||
<div class="deployment-https-demo">
|
||||
<div class="demo-header">
|
||||
<div class="deployment-https">
|
||||
<div class="header">
|
||||
<span class="icon">🔒</span>
|
||||
<span class="title">HTTPS 安全传输</span>
|
||||
<span class="title">HTTPS 安全</span>
|
||||
<span class="subtitle">给数据传输加把锁</span>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
<strong>HTTP</strong> 就像寄明信片,邮递员能看见内容。<strong>HTTPS</strong> 就像寄保险箱,只有收件人能打开。
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<div class="comparison">
|
||||
<div class="compare-card http">
|
||||
<div class="card-header">
|
||||
<span class="lock-icon">🔓</span>
|
||||
<span class="card-title">HTTP(不安全)</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="data-flow">
|
||||
<div class="sender">👤 用户</div>
|
||||
<div class="envelope open">
|
||||
<div class="content-visible">密码: 123456</div>
|
||||
</div>
|
||||
<div class="thief">😈 黑客</div>
|
||||
<div class="receiver">🌐 服务器</div>
|
||||
</div>
|
||||
<div class="warning-text">⚠️ 数据"裸奔",黑客能看见密码</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="compare-card https">
|
||||
<div class="card-header">
|
||||
<span class="lock-icon">🔒</span>
|
||||
<span class="card-title">HTTPS(安全)</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="data-flow">
|
||||
<div class="sender">👤 用户</div>
|
||||
<div class="envelope locked">
|
||||
<div class="content-hidden">??加密??</div>
|
||||
</div>
|
||||
<div class="thief-confused">😕 黑客看不懂</div>
|
||||
<div class="receiver">🌐 服务器</div>
|
||||
</div>
|
||||
<div class="success-text">✅ 数据加密,黑客看不懂</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="compare">
|
||||
<div class="col">
|
||||
<div class="title">HTTP</div>
|
||||
<div class="item">❌ 明文传输</div>
|
||||
</div>
|
||||
|
||||
<div class="certificate-box">
|
||||
<div class="cert-title">📜 SSL/TLS 证书的作用</div>
|
||||
<div class="cert-features">
|
||||
<div class="cert-feature">
|
||||
<span class="feature-icon">🔐</span>
|
||||
<span class="feature-text">加密传输数据</span>
|
||||
</div>
|
||||
<div class="cert-feature">
|
||||
<span class="feature-icon">✅</span>
|
||||
<span class="feature-text">验证网站身份</span>
|
||||
</div>
|
||||
<div class="cert-feature">
|
||||
<span class="feature-icon">🛡️</span>
|
||||
<span class="feature-text">防止数据篡改</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setup-steps">
|
||||
<div class="steps-title">🚀 快速上手(Let's Encrypt 免费)</div>
|
||||
<div class="step-list">
|
||||
<div class="step-item">
|
||||
<span class="step-num">1</span>
|
||||
<span class="step-text">安装 Certbot 工具</span>
|
||||
</div>
|
||||
<div class="step-item">
|
||||
<span class="step-num">2</span>
|
||||
<span class="step-text">运行命令申请证书</span>
|
||||
</div>
|
||||
<div class="step-item">
|
||||
<span class="step-num">3</span>
|
||||
<span class="step-text">自动配置 Nginx</span>
|
||||
</div>
|
||||
<div class="step-item">
|
||||
<span class="step-num">4</span>
|
||||
<span class="step-text">完成!网站显示🔒小锁</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col highlight">
|
||||
<div class="title">HTTPS</div>
|
||||
<div class="item">✅ 加密传输</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>推荐工具:</strong>Let's Encrypt 免费证书 + Certbot 自动工具,几分钟就能搞定 HTTPS!
|
||||
<div class="info">
|
||||
<span class="text">💡 推荐:Let's Encrypt 免费证书</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -101,244 +31,78 @@
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.deployment-https-demo {
|
||||
.deployment-https {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
padding: 0.75rem;
|
||||
margin: 1rem 0;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header .icon {
|
||||
.header .icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.demo-header .title {
|
||||
font-weight: bold;
|
||||
.header .title {
|
||||
font-weight: 700;
|
||||
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 strong {
|
||||
color: var(--vp-c-brand-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.comparison {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.compare-card {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.compare-card.http {
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ff8787 100%);
|
||||
}
|
||||
|
||||
.compare-card.https {
|
||||
background: linear-gradient(135deg, #51cf66 0%, #69db7c 100%);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.lock-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
background: white;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.data-flow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.sender,
|
||||
.receiver {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.envelope {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
border: 2px dashed var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.envelope.open {
|
||||
background: #fff3cd;
|
||||
}
|
||||
|
||||
.envelope.locked {
|
||||
background: #d4edda;
|
||||
}
|
||||
|
||||
.thief {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.thief-confused {
|
||||
font-size: 1.25rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
color: #dc3545;
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.success-text {
|
||||
color: #28a745;
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.certificate-box {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.cert-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.cert-features {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.cert-feature {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
.header .subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.setup-steps {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.steps-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.step-list {
|
||||
.compare {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.step-num {
|
||||
.col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
.col.highlight {
|
||||
background: var(--vp-c-brand-dimm);
|
||||
}
|
||||
|
||||
.col .title {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.col .item {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.info-box .icon {
|
||||
margin-right: 0.25rem;
|
||||
.info {
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.comparison {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.data-flow {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.info .text {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,465 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const algorithm = ref('round-robin')
|
||||
const totalRequests = ref(0)
|
||||
const servers = ref([
|
||||
{ id: 1, name: '服务器 1', requests: 0, status: 'healthy' },
|
||||
{ id: 2, name: '服务器 2', requests: 0, status: 'healthy' },
|
||||
{ id: 3, name: '服务器 3', requests: 0, status: 'healthy' }
|
||||
])
|
||||
|
||||
const algorithms = [
|
||||
{ id: 'round-robin', name: '轮询 (Round Robin)', desc: '依次分配,像排队发号' },
|
||||
{ id: 'least-connections', name: '最少连接 (Least Connections)', desc: '谁最空闲分配给谁' },
|
||||
{ id: 'ip-hash', name: 'IP 哈希 (IP Hash)', desc: '同一IP总是分配给同一服务器' }
|
||||
]
|
||||
|
||||
const currentAlgorithm = computed(() => {
|
||||
return algorithms.find(a => a.id === algorithm.value)
|
||||
})
|
||||
|
||||
let requestIndex = 0
|
||||
const sendRequest = () => {
|
||||
totalRequests.value++
|
||||
requestIndex++
|
||||
|
||||
let serverIndex = 0
|
||||
|
||||
if (algorithm.value === 'round-robin') {
|
||||
serverIndex = (totalRequests.value - 1) % servers.value.length
|
||||
} else if (algorithm.value === 'least-connections') {
|
||||
const minRequests = Math.min(...servers.value.map(s => s.requests))
|
||||
serverIndex = servers.value.findIndex(s => s.requests === minRequests)
|
||||
} else if (algorithm.value === 'ip-hash') {
|
||||
const mockIp = `192.168.1.${(requestIndex % 10) + 1}`
|
||||
serverIndex = parseInt(mockIp.split('.')[3]) % servers.value.length
|
||||
}
|
||||
|
||||
servers.value[serverIndex].requests++
|
||||
}
|
||||
|
||||
// Auto simulate
|
||||
setInterval(() => {
|
||||
sendRequest()
|
||||
}, 1500)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="deployment-lb">
|
||||
<div class="demo-header">
|
||||
<h3>负载均衡演示</h3>
|
||||
<p class="subtitle">多店协同,分散客流</p>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
<p>
|
||||
就像小明开了三家咖啡店,<strong>引导员</strong>根据不同策略把顾客分流到不同门店,
|
||||
避免单店过载,提高整体服务能力。负载均衡器就是那个"引导员"。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<!-- 算法选择 -->
|
||||
<div class="algorithm-section">
|
||||
<div class="section-title">🎯 负载均衡算法</div>
|
||||
<div class="algorithm-list">
|
||||
<div
|
||||
v-for="algo in algorithms"
|
||||
:key="algo.id"
|
||||
class="algorithm-item"
|
||||
:class="{ active: algorithm === algo.id }"
|
||||
@click="algorithm = algo.id"
|
||||
>
|
||||
<div class="algo-header">
|
||||
<span class="algo-icon">{{ algorithm === algo.id ? '✓' : '○' }}</span>
|
||||
<span class="algo-name">{{ algo.name }}</span>
|
||||
</div>
|
||||
<div class="algo-desc">{{ algo.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 负载均衡器可视化 -->
|
||||
<div class="lb-visualization">
|
||||
<div class="lb-node">
|
||||
<div class="lb-icon">⚖️</div>
|
||||
<div class="lb-title">负载均衡器</div>
|
||||
<div class="lb-algorithm">{{ currentAlgorithm.name }}</div>
|
||||
<div class="lb-stats">{{ totalRequests }} 次请求</div>
|
||||
</div>
|
||||
|
||||
<div class="lb-arrows">
|
||||
<div
|
||||
v-for="i in 3"
|
||||
:key="i"
|
||||
class="arrow-line"
|
||||
:style="{ animationDelay: `${i * 0.2}s` }"
|
||||
>
|
||||
→
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="servers-grid">
|
||||
<div
|
||||
v-for="(server, idx) in servers"
|
||||
:key="server.id"
|
||||
class="server-card"
|
||||
:class="{ highlighted: server.requests > 0 }"
|
||||
>
|
||||
<div class="server-icon">🖥️</div>
|
||||
<div class="server-name">{{ server.name }}</div>
|
||||
<div class="server-status">
|
||||
<span class="status-dot healthy"></span>
|
||||
<span class="status-text">健康</span>
|
||||
</div>
|
||||
<div class="server-metrics">
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">请求数</span>
|
||||
<span class="metric-value">{{ server.requests }}</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">负载</span>
|
||||
<div class="load-bar">
|
||||
<div
|
||||
class="load-fill"
|
||||
:style="{ width: `${Math.min(server.requests * 5, 100)}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 生活类比 -->
|
||||
<div class="analogy-box">
|
||||
<div class="analogy-title">💡 生活类比</div>
|
||||
<div class="analogy-content">
|
||||
<div v-if="algorithm === 'round-robin'" class="analogy-item">
|
||||
<strong>轮询</strong>:就像三家咖啡店轮流接待,A店、B店、C店、A店、B店、C店...公平分配。
|
||||
</div>
|
||||
<div v-if="algorithm === 'least-connections'" class="analogy-item">
|
||||
<strong>最少连接</strong>:就像引导员看哪家店人少就往哪家导,确保每家都不会太忙。
|
||||
</div>
|
||||
<div v-if="algorithm === 'ip-hash'" class="analogy-item">
|
||||
<strong>IP哈希</strong>:就像记住老顾客的习惯,张三总是去A店,李四总是去B店,保证体验一致。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
💡 <strong>关键价值</strong>:负载均衡不仅能<strong>提高吞吐量</strong>,还能提供<strong>高可用性</strong>。
|
||||
某台服务器挂了,负载均衡器会自动把流量导向其他健康的服务器!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.deployment-lb {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
padding: 1.25rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.2rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.algorithm-section {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.algorithm-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.algorithm-item {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.algorithm-item:hover {
|
||||
border-color: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.algorithm-item.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.algo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.algo-icon {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.algo-name {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.algo-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
padding-left: 1.4rem;
|
||||
}
|
||||
|
||||
.lb-visualization {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.lb-node {
|
||||
background: var(--vp-c-brand-soft);
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.lb-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.lb-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.lb-algorithm {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-brand);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.lb-stats {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.lb-arrows {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.arrow-line {
|
||||
font-size: 1.5rem;
|
||||
color: var(--vp-c-brand);
|
||||
animation: flow 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes flow {
|
||||
0%, 100% { opacity: 0.3; transform: translateX(-5px); }
|
||||
50% { opacity: 1; transform: translateX(5px); }
|
||||
}
|
||||
|
||||
.servers-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.server-card {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.server-card.highlighted {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-dimm);
|
||||
}
|
||||
|
||||
.server-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.server-name {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.server-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.status-dot.healthy {
|
||||
background: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.server-metrics {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.load-bar {
|
||||
width: 50px;
|
||||
height: 6px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.load-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--vp-c-brand), var(--vp-c-brand-1));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.analogy-box {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-left: 3px solid var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.analogy-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.analogy-content {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 1rem 1.25rem;
|
||||
margin: 0;
|
||||
background: var(--vp-c-bg);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.servers-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,525 +1,138 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
<!--
|
||||
DeploymentMonitorDemo.vue
|
||||
监控备份(精简版)
|
||||
-->
|
||||
<template>
|
||||
<div class="deployment-monitor">
|
||||
<div class="header">
|
||||
<span class="icon">📊</span>
|
||||
<span class="title">监控 & 备份</span>
|
||||
<span class="subtitle">守住网站底线的最后一道防线</span>
|
||||
</div>
|
||||
|
||||
<div class="metrics">
|
||||
<div class="metric">
|
||||
<span class="label">CPU 使用率</span>
|
||||
<span class="value">{{ cpuUsage }}%</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="label">内存使用率</span>
|
||||
<span class="value">{{ memoryUsage }}%</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="label">在线用户</span>
|
||||
<span class="value">{{ activeUsers }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="backup">
|
||||
<div class="label">上次备份:</div>
|
||||
<span class="value">{{ lastBackup }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const isMonitoring = ref(false)
|
||||
const cpuUsage = ref(45)
|
||||
const memoryUsage = ref(62)
|
||||
const activeUsers = ref(23)
|
||||
const requestRate = ref(145)
|
||||
const errorRate = ref(0.8)
|
||||
|
||||
const alerts = ref([
|
||||
{ id: 1, type: 'warning', message: 'CPU 使用率超过 70%', time: '2分钟前' },
|
||||
{ id: 2, type: 'info', message: '新用户注册激增', time: '5分钟前' }
|
||||
])
|
||||
|
||||
const metrics = ref([
|
||||
{ name: '响应时间', value: '120ms', status: 'good', threshold: '200ms' },
|
||||
{ name: '可用性', value: '99.9%', status: 'good', threshold: '99.5%' },
|
||||
{ name: '错误率', value: '0.8%', status: 'warning', threshold: '1%' },
|
||||
{ name: '吞吐量', value: '145 req/s', status: 'good', threshold: '100 req/s' }
|
||||
])
|
||||
const lastBackup = ref('2024-01-15 14:30')
|
||||
|
||||
let interval = null
|
||||
|
||||
const toggleMonitoring = () => {
|
||||
isMonitoring.value = !isMonitoring.value
|
||||
|
||||
if (isMonitoring.value) {
|
||||
startSimulation()
|
||||
} else {
|
||||
stopSimulation()
|
||||
}
|
||||
}
|
||||
|
||||
const startSimulation = () => {
|
||||
onMounted(() => {
|
||||
interval = setInterval(() => {
|
||||
cpuUsage.value = Math.max(20, Math.min(95, cpuUsage.value + (Math.random() - 0.5) * 10))
|
||||
memoryUsage.value = Math.max(30, Math.min(90, memoryUsage.value + (Math.random() - 0.5) * 5))
|
||||
activeUsers.value = Math.max(10, Math.min(100, activeUsers.value + Math.floor((Math.random() - 0.5) * 5)))
|
||||
requestRate.value = Math.max(50, Math.min(300, requestRate.value + Math.floor((Math.random() - 0.5) * 20)))
|
||||
errorRate.value = Math.max(0, Math.min(5, errorRate.value + (Math.random() - 0.5) * 0.3))
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const stopSimulation = () => {
|
||||
if (interval) {
|
||||
clearInterval(interval)
|
||||
interval = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopSimulation()
|
||||
if (interval) {
|
||||
clearInterval(interval)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="deployment-monitor">
|
||||
<div class="demo-header">
|
||||
<h3>监控演示</h3>
|
||||
<p class="subtitle">实时掌握咖啡店运营状况</p>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
<p>
|
||||
就像小明通过<strong>监控摄像头</strong>、<strong>收银系统</strong>实时了解客流量、销售额、库存情况,
|
||||
服务监控能帮助我们发现性能瓶颈、提前预警故障。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<!-- 控制栏 -->
|
||||
<div class="control-bar">
|
||||
<div class="monitor-status">
|
||||
<span class="status-indicator" :class="{ active: isMonitoring }"></span>
|
||||
<span class="status-text">{{ isMonitoring ? '监控运行中' : '监控已停止' }}</span>
|
||||
</div>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: isMonitoring }"
|
||||
@click="toggleMonitoring"
|
||||
>
|
||||
{{ isMonitoring ? '⏸ 暂停' : '▶ 启动监控' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 核心指标 -->
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card">
|
||||
<div class="metric-header">
|
||||
<span class="metric-icon">💻</span>
|
||||
<span class="metric-name">CPU 使用率</span>
|
||||
</div>
|
||||
<div class="metric-value">
|
||||
{{ cpuUsage.toFixed(1) }}%
|
||||
</div>
|
||||
<div class="metric-bar">
|
||||
<div
|
||||
class="metric-fill"
|
||||
:class="cpuUsage > 80 ? 'danger' : cpuUsage > 60 ? 'warning' : 'good'"
|
||||
:style="{ width: `${cpuUsage}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-header">
|
||||
<span class="metric-icon">🧠</span>
|
||||
<span class="metric-name">内存使用率</span>
|
||||
</div>
|
||||
<div class="metric-value">
|
||||
{{ memoryUsage.toFixed(1) }}%
|
||||
</div>
|
||||
<div class="metric-bar">
|
||||
<div
|
||||
class="metric-fill"
|
||||
:class="memoryUsage > 80 ? 'danger' : memoryUsage > 60 ? 'warning' : 'good'"
|
||||
:style="{ width: `${memoryUsage}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-header">
|
||||
<span class="metric-icon">👥</span>
|
||||
<span class="metric-name">在线用户</span>
|
||||
</div>
|
||||
<div class="metric-value large">
|
||||
{{ activeUsers }}
|
||||
</div>
|
||||
<div class="metric-sub">实时</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-header">
|
||||
<span class="metric-icon">⚡</span>
|
||||
<span class="metric-name">请求速率</span>
|
||||
</div>
|
||||
<div class="metric-value large">
|
||||
{{ requestRate }}
|
||||
</div>
|
||||
<div class="metric-sub">req/s</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 业务指标 -->
|
||||
<div class="business-metrics">
|
||||
<div class="section-title">📊 业务指标</div>
|
||||
<div class="metrics-list">
|
||||
<div
|
||||
v-for="(metric, idx) in metrics"
|
||||
:key="idx"
|
||||
class="business-metric-item"
|
||||
:class="metric.status"
|
||||
>
|
||||
<div class="metric-info">
|
||||
<span class="metric-label">{{ metric.name }}</span>
|
||||
<span class="metric-threshold">目标: {{ metric.threshold }}</span>
|
||||
</div>
|
||||
<div class="metric-val">{{ metric.value }}</div>
|
||||
<div class="metric-status-badge">
|
||||
<span v-if="metric.status === 'good'" class="badge good">✓ 正常</span>
|
||||
<span v-else-if="metric.status === 'warning'" class="badge warning">⚠️ 警告</span>
|
||||
<span v-else class="badge danger">❌ 异常</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 告警列表 -->
|
||||
<div class="alerts-section">
|
||||
<div class="section-title">🔔 最近告警</div>
|
||||
<div class="alerts-list">
|
||||
<div
|
||||
v-for="alert in alerts"
|
||||
:key="alert.id"
|
||||
class="alert-item"
|
||||
:class="alert.type"
|
||||
>
|
||||
<span class="alert-icon">{{ alert.type === 'warning' ? '⚠️' : 'ℹ️' }}</span>
|
||||
<span class="alert-message">{{ alert.message }}</span>
|
||||
<span class="alert-time">{{ alert.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p v-if="!isMonitoring">
|
||||
💡 <strong>准备就绪</strong>:点击"启动监控"按钮,开始实时查看服务器各项指标。就像打开咖啡店的监控系统!
|
||||
</p>
|
||||
<p v-else>
|
||||
✅ <strong>监控中</strong>:各项指标实时更新。设置合理的阈值和告警,才能在问题发生时第一时间响应!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.deployment-monitor {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.75rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
padding: 1.25rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.2rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.control-bar {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monitor-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.status-indicator.active {
|
||||
background: var(--vp-c-brand-delta);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
padding: 0.6rem 1.25rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.toggle-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
border-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.metric-header {
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.metric-icon {
|
||||
font-size: 1.2rem;
|
||||
.header .icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.metric-name {
|
||||
.header .title {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.header .subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric .label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 1.8rem;
|
||||
.metric .value {
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.metric-value.large {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.metric-bar {
|
||||
height: 8px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.metric-fill {
|
||||
height: 100%;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.metric-fill.good {
|
||||
background: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.metric-fill.warning {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.metric-fill.danger {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.metric-sub {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.business-metrics {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.metrics-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.business-metric-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.business-metric-item.warning {
|
||||
border-left-color: #f59e0b;
|
||||
}
|
||||
|
||||
.business-metric-item.danger {
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.metric-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.metric-threshold {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.metric-val {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.metric-status-badge {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge.good {
|
||||
background: var(--vp-c-brand-dimm);
|
||||
color: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.badge.warning {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.badge.danger {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.alerts-section {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.alerts-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
.backup {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.alert-item.warning {
|
||||
background: #fef3c7;
|
||||
border-left: 3px solid #f59e0b;
|
||||
}
|
||||
|
||||
.alert-item.info {
|
||||
background: #dbeafe;
|
||||
border-left: 3px solid #3b82f6;
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.alert-message {
|
||||
flex: 1;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.alert-time {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 1rem 1.25rem;
|
||||
margin: 0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.9rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.backup .label {
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.metrics-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.backup .value {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,460 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const enableProxy = ref(false)
|
||||
const incomingRequests = ref(0)
|
||||
const proxiedRequests = ref(0)
|
||||
|
||||
const serverPort = ref(3000)
|
||||
const nginxPort = ref(80)
|
||||
|
||||
const servers = ref([
|
||||
{ id: 1, name: 'Node.js 应用', port: 3000, status: 'running', load: 45 },
|
||||
{ id: 2, name: 'Python API', port: 4000, status: 'running', load: 30 }
|
||||
])
|
||||
|
||||
const selectedServer = ref(0)
|
||||
|
||||
const toggleProxy = () => {
|
||||
enableProxy.value = !enableProxy.value
|
||||
if (!enableProxy.value) {
|
||||
proxiedRequests.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
const simulateRequest = () => {
|
||||
if (!enableProxy.value) return
|
||||
incomingRequests.value++
|
||||
proxiedRequests.value++
|
||||
servers.value[selectedServer.value].load = Math.min(100, servers.value[selectedServer.value].load + 5)
|
||||
setTimeout(() => {
|
||||
servers.value[selectedServer.value].load = Math.max(10, servers.value[selectedServer.value].load - 5)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// Auto simulate
|
||||
setInterval(() => {
|
||||
if (enableProxy.value) {
|
||||
simulateRequest()
|
||||
}
|
||||
}, 2000)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="deployment-nginx">
|
||||
<div class="demo-header">
|
||||
<h3>Nginx反向代理演示</h3>
|
||||
<p class="subtitle">门店服务员引导顾客</p>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
<p>
|
||||
就像咖啡店的<strong>服务员</strong>引导顾客到相应的吧台(咖啡、蛋糕、收银),
|
||||
Nginx 作为反向代理,把外部请求转发给后端的不同服务。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<!-- 开关控制 -->
|
||||
<div class="control-panel">
|
||||
<div class="switch-section">
|
||||
<span class="switch-label">启用反向代理</span>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: enableProxy }"
|
||||
@click="toggleProxy"
|
||||
>
|
||||
<span class="toggle-slider"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ incomingRequests }}</span>
|
||||
<span class="stat-label">总请求</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ proxiedRequests }}</span>
|
||||
<span class="stat-label">已转发</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 架构图 -->
|
||||
<div class="architecture-diagram">
|
||||
<div class="diagram-layer client">
|
||||
<div class="layer-icon">👥</div>
|
||||
<div class="layer-title">用户浏览器</div>
|
||||
<div class="layer-detail">访问 :80</div>
|
||||
</div>
|
||||
|
||||
<div class="diagram-arrow">↓</div>
|
||||
|
||||
<div class="diagram-layer nginx" :class="{ active: enableProxy }">
|
||||
<div class="layer-icon">🛎️</div>
|
||||
<div class="layer-title">Nginx (反向代理)</div>
|
||||
<div class="layer-detail">监听 {{ nginxPort }} 端口</div>
|
||||
</div>
|
||||
|
||||
<div class="diagram-arrow" :class="{ active: enableProxy }">↓ 转发</div>
|
||||
|
||||
<div class="diagram-layer backend">
|
||||
<div class="layer-title">后端服务</div>
|
||||
<div class="server-list">
|
||||
<div
|
||||
v-for="(server, idx) in servers"
|
||||
:key="server.id"
|
||||
class="server-item"
|
||||
:class="{ active: selectedServer === idx }"
|
||||
>
|
||||
<div class="server-icon">🖥️</div>
|
||||
<div class="server-info">
|
||||
<div class="server-name">{{ server.name }}</div>
|
||||
<div class="server-port">:{{ server.port }}</div>
|
||||
</div>
|
||||
<div class="server-load">
|
||||
<div class="load-bar">
|
||||
<div
|
||||
class="load-fill"
|
||||
:style="{ width: `${server.load}%` }"
|
||||
></div>
|
||||
</div>
|
||||
<span class="load-text">{{ server.load }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置示例 -->
|
||||
<div class="config-example">
|
||||
<div class="config-title">📝 Nginx 配置示例</div>
|
||||
<pre class="code-block"><code>server {
|
||||
listen {{ nginxPort }};
|
||||
server_name example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:{{ serverPort }};
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p v-if="!enableProxy">
|
||||
💡 <strong>等待启用</strong>:点击"启用反向代理"开关,看看 Nginx 如何将请求转发给后端服务。
|
||||
</p>
|
||||
<p v-else>
|
||||
✅ <strong>运行中</strong>:Nginx 正在监听 {{ nginxPort }} 端口,将请求转发到后端的 {{ serverPort }} 端口!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.deployment-nginx {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
padding: 1.25rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.2rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.switch-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
width: 50px;
|
||||
height: 26px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 13px;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.toggle-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 2px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.toggle-btn.active .toggle-slider {
|
||||
left: 25px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.architecture-diagram {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.diagram-layer {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.diagram-layer.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.layer-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.layer-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.layer-detail {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.diagram-arrow {
|
||||
font-size: 1.5rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.diagram-arrow.active {
|
||||
color: var(--vp-c-brand);
|
||||
animation: bounce 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-5px); }
|
||||
}
|
||||
|
||||
.server-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.server-item {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.server-item.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.server-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.server-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.server-name {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.server-port {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-brand);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.server-load {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.load-bar {
|
||||
width: 60px;
|
||||
height: 6px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.load-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--vp-c-brand), var(--vp-c-brand-1));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.load-text {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
min-width: 2.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.config-example {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.config-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #1e1e1e;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-block code {
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 0.85rem;
|
||||
color: #d4d4d4;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 1rem 1.25rem;
|
||||
margin: 0;
|
||||
background: var(--vp-c-bg);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.control-panel {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stats {
|
||||
width: 100%;
|
||||
justify-content: space-around;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,300 +1,200 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const currentStep = ref(0)
|
||||
const steps = [
|
||||
{ id: 'code', title: '写代码', desc: '小明在厨房开发新咖啡配方', icon: '☕' },
|
||||
{ id: 'build', title: '构建打包', desc: '准备原材料,清洗咖啡豆', icon: '📦' },
|
||||
{ id: 'test', title: '测试验证', desc: '小明自己先品尝确认没问题', icon: '🧪' },
|
||||
{ id: 'deploy', title: '部署上线', desc: '把咖啡上架到门店售卖', icon: '🚀' },
|
||||
{ id: 'monitor', title: '监控维护', desc: '观察顾客反馈,持续优化', icon: '📊' }
|
||||
]
|
||||
|
||||
const stepProgress = computed(() => ((currentStep.value + 1) / steps.length) * 100)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="deployment-overview">
|
||||
<div class="demo-header">
|
||||
<h3>服务上线全流程</h3>
|
||||
<p class="subtitle">从小明咖啡店看部署流程</p>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
<p>
|
||||
就像小明要推出一款新咖啡,需要经过<strong>配方研发</strong>→<strong>材料准备</strong>→<strong>试喝确认</strong>→<strong>上架售卖</strong>→<strong>收集反馈</strong>,
|
||||
软件上线也需要完整的流程保障质量。
|
||||
</p>
|
||||
<span class="icon">🚀</span>
|
||||
<span class="title">服务上线全流程</span>
|
||||
<span class="subtitle">从代码到用户眼中的网页</span>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<!-- 进度条 -->
|
||||
<div class="progress-section">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: `${stepProgress}%` }"></div>
|
||||
<div class="service-flow">
|
||||
<div class="flow-step" :class="{ active: currentStep >= 1 }">
|
||||
<div class="step-icon">📦</div>
|
||||
<div class="step-title">Build</div>
|
||||
<div class="tech-term">源码→可执行文件</div>
|
||||
</div>
|
||||
<div class="progress-label">{{ currentStep + 1 }} / {{ steps.length }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤卡片 -->
|
||||
<div class="steps-container">
|
||||
<div
|
||||
v-for="(step, index) in steps"
|
||||
:key="step.id"
|
||||
class="step-card"
|
||||
:class="{ active: currentStep === index, completed: index < currentStep }"
|
||||
@click="currentStep = index"
|
||||
>
|
||||
<div class="step-icon">{{ step.icon }}</div>
|
||||
<div class="step-title">{{ step.title }}</div>
|
||||
<div class="step-desc">{{ step.desc }}</div>
|
||||
|
||||
<!-- 状态指示 -->
|
||||
<div class="step-status">
|
||||
<span v-if="index < currentStep" class="status-icon completed">✓</span>
|
||||
<span v-else-if="index === currentStep" class="status-icon active">●</span>
|
||||
<span v-else class="status-icon pending">○</span>
|
||||
</div>
|
||||
<span class="flow-arrow">→</span>
|
||||
<div class="flow-step" :class="{ active: currentStep >= 2 }">
|
||||
<div class="step-icon">🖥️</div>
|
||||
<div class="step-title">Server</div>
|
||||
<div class="tech-term">24h运行的电脑</div>
|
||||
</div>
|
||||
<span class="flow-arrow">→</span>
|
||||
<div class="flow-step" :class="{ active: currentStep >= 3 }">
|
||||
<div class="step-icon">�</div>
|
||||
<div class="step-title">Deploy</div>
|
||||
<div class="tech-term">代码放到服务器</div>
|
||||
</div>
|
||||
<span class="flow-arrow">→</span>
|
||||
<div class="flow-step" :class="{ active: currentStep >= 4 }">
|
||||
<div class="step-icon">🔀</div>
|
||||
<div class="step-title">Nginx</div>
|
||||
<div class="tech-term">接收请求分发响应</div>
|
||||
</div>
|
||||
<span class="flow-arrow">→</span>
|
||||
<div class="flow-step" :class="{ active: currentStep >= 5 }">
|
||||
<div class="step-icon">�</div>
|
||||
<div class="step-title">DNS</div>
|
||||
<div class="tech-term">域名→IP地址</div>
|
||||
</div>
|
||||
<span class="flow-arrow">→</span>
|
||||
<div class="flow-step" :class="{ active: currentStep >= 6 }">
|
||||
<div class="step-icon">�</div>
|
||||
<div class="step-title">HTTPS</div>
|
||||
<div class="tech-term">加密数据传输</div>
|
||||
</div>
|
||||
<span class="flow-arrow">→</span>
|
||||
<div class="flow-step" :class="{ active: currentStep >= 7 }">
|
||||
<div class="step-icon">⚡</div>
|
||||
<div class="step-title">CDN</div>
|
||||
<div class="tech-term">就近访问加速</div>
|
||||
</div>
|
||||
<span class="flow-arrow">→</span>
|
||||
<div class="flow-step" :class="{ active: currentStep >= 8 }">
|
||||
<div class="step-icon">🔄</div>
|
||||
<div class="step-title">CI/CD</div>
|
||||
<div class="tech-term">自动化部署流程</div>
|
||||
</div>
|
||||
<span class="flow-arrow">→</span>
|
||||
<div class="flow-step" :class="{ active: currentStep >= 9 }">
|
||||
<div class="step-icon">�</div>
|
||||
<div class="step-title">Monitor</div>
|
||||
<div class="tech-term">监控运行状态</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前步骤详情 -->
|
||||
<div class="step-detail">
|
||||
<h4>{{ steps[currentStep].title }}</h4>
|
||||
<p>{{ steps[currentStep].desc }}</p>
|
||||
<div class="detail-analogy">
|
||||
<div class="analogy-label">💡 技术对应</div>
|
||||
<div class="analogy-content">
|
||||
<span v-if="currentStep === 0">编写代码:开发新功能</span>
|
||||
<span v-if="currentStep === 1">构建打包:Webpack/Vite 编译资源</span>
|
||||
<span v-if="currentStep === 2">测试验证:单元测试、集成测试</span>
|
||||
<span v-if="currentStep === 3">部署上线:推送到服务器/云平台</span>
|
||||
<span v-if="currentStep === 4">监控维护:日志、性能监控、告警</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心原则</strong>:小步快跑 → 先上线MVP → 逐步完善
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>💡 <strong>关键要点</strong>:每个环节都不可或缺。跳过测试就上线,就像没试喝就卖咖啡,可能让顾客喝到难喝的咖啡(Bug)!</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const currentStep = ref(0)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.deployment-overview {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.75rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
padding: 1.25rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.2rem;
|
||||
color: var(--vp-c-text-1);
|
||||
.demo-header .icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
.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 {
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.intro-text strong {
|
||||
color: var(--vp-c-brand-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
.service-flow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
gap: 0.25rem;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--vp-c-brand), var(--vp-c-brand-1));
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand);
|
||||
min-width: 3rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.steps-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.step-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
min-height: 120px;
|
||||
.flow-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
min-width: 85px;
|
||||
}
|
||||
|
||||
.step-card:hover {
|
||||
border-color: var(--vp-c-brand-soft);
|
||||
transform: translateY(-2px);
|
||||
.flow-step:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.step-card.active {
|
||||
.flow-step.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.step-card.completed {
|
||||
border-color: var(--vp-c-brand-delta);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 0.8rem;
|
||||
.tech-term {
|
||||
font-size: 0.6rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.step-status {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.status-icon.completed {
|
||||
color: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.status-icon.active {
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.status-icon.pending {
|
||||
.flow-arrow {
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.step-detail {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1.25rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.step-detail h4 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 1.1rem;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.step-detail p {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 0.95rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.detail-analogy {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
border-left: 3px solid var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.analogy-label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.analogy-content {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-brand-1);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 1rem 1.25rem;
|
||||
margin: 0;
|
||||
background: var(--vp-c-bg);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.9rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.5rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.steps-container {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.step-card {
|
||||
min-height: 100px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.info-box .icon {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,561 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const connected = ref(false)
|
||||
const connecting = ref(false)
|
||||
const currentStep = ref(0)
|
||||
const commandHistory = ref([])
|
||||
const currentCommand = ref('')
|
||||
|
||||
const steps = [
|
||||
{ text: '正在连接服务器...', icon: '🔌' },
|
||||
{ text: '身份验证中...', icon: '🔑' },
|
||||
{ text: '建立安全通道...', icon: '🛡️' },
|
||||
{ text: '连接成功!', icon: '✅' }
|
||||
]
|
||||
|
||||
const connect = () => {
|
||||
if (connected.value || connecting.value) return
|
||||
|
||||
connecting.value = true
|
||||
currentStep.value = 0
|
||||
commandHistory.value = []
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (currentStep.value < steps.length - 1) {
|
||||
currentStep.value++
|
||||
} else {
|
||||
clearInterval(interval)
|
||||
connecting.value = false
|
||||
connected.value = true
|
||||
commandHistory.value.push({
|
||||
type: 'success',
|
||||
text: 'Welcome to Ubuntu 22.04 LTS'
|
||||
})
|
||||
commandHistory.value.push({
|
||||
type: 'info',
|
||||
text: 'Last login: ' + new Date().toLocaleString()
|
||||
})
|
||||
}
|
||||
}, 800)
|
||||
}
|
||||
|
||||
const disconnect = () => {
|
||||
connected.value = false
|
||||
currentStep.value = 0
|
||||
commandHistory.value = []
|
||||
}
|
||||
|
||||
const executeCommand = () => {
|
||||
if (!currentCommand.value.trim()) return
|
||||
|
||||
commandHistory.value.push({
|
||||
type: 'command',
|
||||
text: `$ ${currentCommand.value}`
|
||||
})
|
||||
|
||||
// 模拟命令响应
|
||||
setTimeout(() => {
|
||||
if (currentCommand.value === 'ls') {
|
||||
commandHistory.value.push({
|
||||
type: 'output',
|
||||
text: 'app.js package.json node_modules/ README.md'
|
||||
})
|
||||
} else if (currentCommand.value === 'pwd') {
|
||||
commandHistory.value.push({
|
||||
type: 'output',
|
||||
text: '/home/user/my-app'
|
||||
})
|
||||
} else if (currentCommand.value === 'whoami') {
|
||||
commandHistory.value.push({
|
||||
type: 'output',
|
||||
text: 'user'
|
||||
})
|
||||
} else {
|
||||
commandHistory.value.push({
|
||||
type: 'output',
|
||||
text: `Command '${currentCommand.value}' executed`
|
||||
})
|
||||
}
|
||||
}, 300)
|
||||
|
||||
currentCommand.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="deployment-ssh">
|
||||
<div class="demo-header">
|
||||
<h3>SSH远程连接演示</h3>
|
||||
<p class="subtitle">像小明远程指挥咖啡店</p>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
<p>
|
||||
SSH就像小明通过<strong>电话远程指挥</strong>咖啡店员工工作,
|
||||
不需要亲自到店里,就能执行命令、查看状态、部署应用。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<!-- 连接控制 -->
|
||||
<div class="connection-panel">
|
||||
<div class="connection-info">
|
||||
<div class="info-item">
|
||||
<span class="label">服务器地址</span>
|
||||
<span class="value">192.168.1.100</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">用户名</span>
|
||||
<span class="value">xiaoming</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">状态</span>
|
||||
<span class="status" :class="{ connected, connecting }">
|
||||
{{ connecting ? '连接中...' : connected ? '已连接' : '未连接' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="!connected && !connecting"
|
||||
@click="connect"
|
||||
class="btn primary"
|
||||
>
|
||||
🔗 连接服务器
|
||||
</button>
|
||||
<button
|
||||
v-else-if="connected"
|
||||
@click="disconnect"
|
||||
class="btn danger"
|
||||
>
|
||||
❌ 断开连接
|
||||
</button>
|
||||
<button v-else class="btn" disabled>
|
||||
⏳ 连接中...
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 连接进度 -->
|
||||
<div v-if="connecting || (connected && currentStep === steps.length - 1)" class="connection-progress">
|
||||
<div class="progress-steps">
|
||||
<div
|
||||
v-for="(step, idx) in steps"
|
||||
:key="idx"
|
||||
class="progress-step"
|
||||
:class="{ active: idx === currentStep, completed: idx < currentStep }"
|
||||
>
|
||||
<span class="step-icon">{{ step.icon }}</span>
|
||||
<span class="step-text">{{ step.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 终端模拟 -->
|
||||
<div v-if="connected" class="terminal">
|
||||
<div class="terminal-header">
|
||||
<span class="terminal-title">xiaoming@server ~</span>
|
||||
<div class="terminal-buttons">
|
||||
<span class="btn-dot red"></span>
|
||||
<span class="btn-dot yellow"></span>
|
||||
<span class="btn-dot green"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-body">
|
||||
<div
|
||||
v-for="(cmd, idx) in commandHistory"
|
||||
:key="idx"
|
||||
class="terminal-line"
|
||||
:class="cmd.type"
|
||||
>
|
||||
{{ cmd.text }}
|
||||
</div>
|
||||
<div class="terminal-input-line">
|
||||
<span class="prompt">$</span>
|
||||
<input
|
||||
v-model="currentCommand"
|
||||
@keyup.enter="executeCommand"
|
||||
type="text"
|
||||
class="terminal-input"
|
||||
placeholder="输入命令 (try: ls, pwd, whoami)"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 说明 -->
|
||||
<div v-if="!connected && !connecting" class="ssh-features">
|
||||
<div class="feature-grid">
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">🔐</div>
|
||||
<div class="feature-title">加密通信</div>
|
||||
<div class="feature-desc">所有数据加密传输,防止被窃听</div>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">🎫</div>
|
||||
<div class="feature-title">身份验证</div>
|
||||
<div class="feature-desc">密码或密钥验证,确保只有授权用户访问</div>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">⚡</div>
|
||||
<div class="feature-title">远程执行</div>
|
||||
<div class="feature-desc">像在本地一样操作远程服务器</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p v-if="!connected">
|
||||
💡 <strong>生活类比</strong>:SSH就像小明用专用电话打给咖啡店,只有知道号码(IP)和密码(密钥)的人才能指挥店里工作。
|
||||
</p>
|
||||
<p v-else>
|
||||
✅ <strong>已连接</strong>:现在你可以像在本地一样操作远程服务器了!试试输入 <code>ls</code>、<code>pwd</code> 或 <code>whoami</code>。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.deployment-ssh {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
padding: 1.25rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.2rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.connection-panel {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.connection-info {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-item .label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.info-item .value {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.status.connected {
|
||||
background: var(--vp-c-brand-delta);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status.connecting {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.6rem 1.25rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn.primary:hover {
|
||||
background: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.btn.danger {
|
||||
background: var(--vp-c-red);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn.danger:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.connection-progress {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.progress-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.progress-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 6px;
|
||||
opacity: 0.4;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-step.active {
|
||||
opacity: 1;
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.progress-step.completed {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.terminal {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
background: #2d2d2d;
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #404040;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
font-size: 0.8rem;
|
||||
color: #b4b4b4;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.terminal-buttons {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.btn-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.btn-dot.red { background: #ff5f56; }
|
||||
.btn-dot.yellow { background: #ffbd2e; }
|
||||
.btn-dot.green { background: #27c93f; }
|
||||
|
||||
.terminal-body {
|
||||
padding: 1rem;
|
||||
min-height: 200px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.terminal-line {
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.terminal-line.command {
|
||||
color: #4ec9b0;
|
||||
}
|
||||
|
||||
.terminal-line.output {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.terminal-line.success {
|
||||
color: #4ec9b0;
|
||||
}
|
||||
|
||||
.terminal-line.info {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
.terminal-input-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: #4ec9b0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.terminal-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: #d4d4d4;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.terminal-input::placeholder {
|
||||
color: #606060;
|
||||
}
|
||||
|
||||
.ssh-features {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 1rem 1.25rem;
|
||||
margin: 0;
|
||||
background: var(--vp-c-bg);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info-box code {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.connection-panel {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.connection-info {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,426 +1,113 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const scenarios = ref([
|
||||
{
|
||||
id: 'personal',
|
||||
name: '个人博客',
|
||||
icon: '📝',
|
||||
users: '100/天',
|
||||
cpu: '1核',
|
||||
memory: '1GB',
|
||||
cost: '¥50/月',
|
||||
suitable: '适合',
|
||||
color: '#10b981'
|
||||
},
|
||||
{
|
||||
id: 'small',
|
||||
name: '小型电商',
|
||||
icon: '🛒',
|
||||
users: '1000/天',
|
||||
cpu: '2核',
|
||||
memory: '4GB',
|
||||
cost: '¥200/月',
|
||||
suitable: '适合',
|
||||
color: '#3b82f6'
|
||||
},
|
||||
{
|
||||
id: 'medium',
|
||||
name: '中型应用',
|
||||
icon: '🏢',
|
||||
users: '10000/天',
|
||||
cpu: '4核',
|
||||
memory: '8GB',
|
||||
cost: '¥800/月',
|
||||
suitable: '适合',
|
||||
color: '#f59e0b'
|
||||
},
|
||||
{
|
||||
id: 'large',
|
||||
name: '大型平台',
|
||||
icon: '🏛️',
|
||||
users: '100000+/天',
|
||||
cpu: '8核+',
|
||||
memory: '16GB+',
|
||||
cost: '¥3000+/月',
|
||||
suitable: '集群',
|
||||
color: '#ef4444'
|
||||
}
|
||||
])
|
||||
|
||||
const selectedScenario = ref('small')
|
||||
const serverTypes = ['云服务器', '物理服务器', '容器化部署']
|
||||
const selectedServerType = ref('云服务器')
|
||||
|
||||
const currentScenario = computed(() => {
|
||||
return scenarios.value.find(s => s.id === selectedScenario.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--
|
||||
DeploymentServerDemo.vue
|
||||
服务器选择(精简版)
|
||||
-->
|
||||
<template>
|
||||
<div class="deployment-server">
|
||||
<div class="demo-header">
|
||||
<h3>服务器选择演示</h3>
|
||||
<p class="subtitle">根据客流量选择合适的店面</p>
|
||||
<div class="header">
|
||||
<span class="icon">🖥️</span>
|
||||
<span class="title">服务器选择</span>
|
||||
<span class="subtitle">根据客流量选择合适的店面</span>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
<p>
|
||||
就像小明开咖啡店,<strong>街边小摊</strong>、<strong>社区店</strong>、<strong>商场店</strong>、<strong>旗舰店</strong>需要的场地面积和设备完全不同,
|
||||
选择服务器也要根据用户量来匹配,避免<strong>资源浪费</strong>或<strong>性能不足</strong>。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<!-- 场景选择 -->
|
||||
<div class="scenario-section">
|
||||
<div class="section-title">🎯 选择你的场景</div>
|
||||
<div class="scenario-cards">
|
||||
<div
|
||||
v-for="scenario in scenarios"
|
||||
:key="scenario.id"
|
||||
class="scenario-card"
|
||||
:class="{ active: selectedScenario === scenario.id }"
|
||||
@click="selectedScenario = scenario.id"
|
||||
:style="{ '--scenario-color': scenario.color }"
|
||||
>
|
||||
<div class="scenario-icon">{{ scenario.icon }}</div>
|
||||
<div class="scenario-name">{{ scenario.name }}</div>
|
||||
<div class="scenario-users">{{ scenario.users }}</div>
|
||||
<div class="scenario-badge" :class="scenario.suitable === '适合' ? 'good' : 'cluster'">
|
||||
{{ scenario.suitable === '适合' ? '✓ 单机' : '需要集群' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scenarios">
|
||||
<div class="scenario" :class="{ active: scenario === 'personal' }" @click="scenario = 'personal'">
|
||||
<span class="name">个人博客</span>
|
||||
<span class="spec">1核 1G</span>
|
||||
<span class="cost">¥50/月</span>
|
||||
</div>
|
||||
|
||||
<!-- 服务器配置 -->
|
||||
<div class="config-section">
|
||||
<div class="section-title">⚙️ 推荐配置</div>
|
||||
<div class="config-cards">
|
||||
<div class="config-card">
|
||||
<div class="config-icon">🖥️</div>
|
||||
<div class="config-label">CPU</div>
|
||||
<div class="config-value">{{ currentScenario.cpu }}</div>
|
||||
<div class="config-desc">处理订单的厨师数量</div>
|
||||
</div>
|
||||
|
||||
<div class="config-card">
|
||||
<div class="config-icon">💾</div>
|
||||
<div class="config-label">内存</div>
|
||||
<div class="config-value">{{ currentScenario.memory }}</div>
|
||||
<div class="config-desc">同时处理订单的能力</div>
|
||||
</div>
|
||||
|
||||
<div class="config-card">
|
||||
<div class="config-icon">💰</div>
|
||||
<div class="config-label">成本</div>
|
||||
<div class="config-value">{{ currentScenario.cost }}</div>
|
||||
<div class="config-desc">相当于租金+水电费</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scenario" :class="{ active: scenario === 'small' }" @click="scenario = 'small'">
|
||||
<span class="name">小型电商</span>
|
||||
<span class="spec">2核 4G</span>
|
||||
<span class="cost">¥300/月</span>
|
||||
</div>
|
||||
|
||||
<!-- 服务器类型选择 -->
|
||||
<div class="server-type-section">
|
||||
<div class="section-title">🏗️ 部署方式</div>
|
||||
<div class="server-types">
|
||||
<div
|
||||
v-for="type in serverTypes"
|
||||
:key="type"
|
||||
class="type-item"
|
||||
:class="{ active: selectedServerType === type }"
|
||||
@click="selectedServerType = type"
|
||||
>
|
||||
<span class="type-icon">
|
||||
{{ type === '云服务器' ? '☁️' : type === '物理服务器' ? '🏢' : '📦' }}
|
||||
</span>
|
||||
<span class="type-name">{{ type }}</span>
|
||||
<span v-if="selectedServerType === type" class="type-check">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 类型说明 -->
|
||||
<div class="type-description">
|
||||
<div v-if="selectedServerType === '云服务器'" class="desc-content">
|
||||
<strong>☁️ 云服务器(推荐)</strong>
|
||||
<p>像租用共享厨房,灵活扩展,按需付费。适合大多数场景。</p>
|
||||
</div>
|
||||
<div v-if="selectedServerType === '物理服务器'" class="desc-content">
|
||||
<strong>🏢 物理服务器</strong>
|
||||
<p>像买下整个店面,性能稳定但成本高,适合大规模应用。</p>
|
||||
</div>
|
||||
<div v-if="selectedServerType === '容器化部署'" class="desc-content">
|
||||
<strong>📦 容器化部署(Docker/K8s)</strong>
|
||||
<p>像用预制盒做饭,标准化、可复制,适合快速扩容和微服务。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scenario" :class="{ active: scenario === 'medium' }" @click="scenario = 'medium'">
|
||||
<span class="name">中型应用</span>
|
||||
<span class="spec">4核 8G</span>
|
||||
<span class="cost">¥1000/月</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
💡 <strong>小明建议</strong>:刚开始用云服务器最合适,就像开咖啡店先租个小店面测试生意,
|
||||
客流量大了再升级或开分店(集群部署)。<strong>不要一开始就租豪华店面!</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const scenario = ref('small')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.deployment-server {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.75rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
padding: 1.25rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.2rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: 1.25rem;
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.scenario-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.scenario-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.scenario-card:hover {
|
||||
border-color: var(--vp-c-brand-soft);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.scenario-card.active {
|
||||
border-color: var(--scenario-color);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.scenario-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.scenario-name {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.scenario-users {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.scenario-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.scenario-badge.good {
|
||||
background: var(--vp-c-brand-delta);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.scenario-badge.cluster {
|
||||
background: var(--vp-c-red);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.config-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.config-card {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.config-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.config-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.config-value {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.config-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.server-type-section {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.server-types {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.type-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.type-item:hover {
|
||||
border-color: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.type-item.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.type-icon {
|
||||
.header .icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.type-name {
|
||||
flex: 1;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.type-check {
|
||||
color: var(--vp-c-brand);
|
||||
.header .title {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.type-description {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
border-left: 3px solid var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.desc-content {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.desc-content strong {
|
||||
display: block;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.desc-content p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 1rem 1.25rem;
|
||||
margin: 0;
|
||||
background: var(--vp-c-bg);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.9rem;
|
||||
.header .subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
margin: 0;
|
||||
.scenarios {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.scenario-cards {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.scenario {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.config-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.scenario:hover {
|
||||
border-color: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.scenario.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-dimm);
|
||||
}
|
||||
|
||||
.scenario .name {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.scenario .spec {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.scenario .cost {
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,456 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const symptoms = ref([
|
||||
{ id: 'slow', text: '网站访问很慢', icon: '🐌' },
|
||||
{ id: 'error', text: '显示500错误', icon: '❌' },
|
||||
{ id: 'timeout', text: '请求超时', icon: '⏰' },
|
||||
{ id: 'blank', text: '页面空白', icon: '📄' }
|
||||
])
|
||||
|
||||
const selectedSymptom = ref('')
|
||||
const diagnosis = ref('')
|
||||
const solution = ref('')
|
||||
const step = ref(1)
|
||||
|
||||
const diagnose = (symptom) => {
|
||||
selectedSymptom.value = symptom
|
||||
step.value = 2
|
||||
|
||||
if (symptom === 'slow') {
|
||||
diagnosis.value = '可能原因:服务器负载过高、数据库查询慢、带宽不足'
|
||||
solution.value = '检查CPU/内存使用率,优化数据库查询,考虑使用CDN加速'
|
||||
} else if (symptom === 'error') {
|
||||
diagnosis.value = '可能原因:代码Bug、配置错误、依赖缺失'
|
||||
solution.value = '查看服务器日志,检查环境变量,确认依赖包是否安装'
|
||||
} else if (symptom === 'timeout') {
|
||||
diagnosis.value = '可能原因:网络问题、防火墙阻拦、服务未启动'
|
||||
solution.value = '检查网络连通性,确认防火墙规则,验证服务状态'
|
||||
} else if (symptom === 'blank') {
|
||||
diagnosis.value = '可能原因:前端资源加载失败、JS报错、路径配置错误'
|
||||
solution.value = '检查浏览器控制台错误,验证静态资源路径,查看构建日志'
|
||||
}
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
selectedSymptom.value = ''
|
||||
diagnosis.value = ''
|
||||
solution.value = ''
|
||||
step.value = 1
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="deployment-troubleshoot">
|
||||
<div class="demo-header">
|
||||
<h3>故障排查演示</h3>
|
||||
<p class="subtitle">像医生诊断病人一样排查问题</p>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
<p>
|
||||
就像小明发现咖啡店<strong>出餐慢</strong>、<strong>机器故障</strong>时,
|
||||
需要系统性地排查问题(咖啡豆→磨豆机→咖啡机→操作员),
|
||||
服务器故障也需要科学的排查流程。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<!-- 步骤1: 选择症状 -->
|
||||
<div v-if="step === 1" class="symptom-selection">
|
||||
<div class="section-title">🔍 第一步:选择你遇到的问题</div>
|
||||
<div class="symptom-grid">
|
||||
<div
|
||||
v-for="symptom in symptoms"
|
||||
:key="symptom.id"
|
||||
class="symptom-card"
|
||||
@click="diagnose(symptom.id)"
|
||||
>
|
||||
<div class="symptom-icon">{{ symptom.icon }}</div>
|
||||
<div class="symptom-text">{{ symptom.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤2: 诊断结果 -->
|
||||
<div v-if="step === 2" class="diagnosis-result">
|
||||
<div class="section-title">🩺 诊断结果</div>
|
||||
|
||||
<div class="result-card">
|
||||
<div class="result-header">
|
||||
<span class="result-icon">😷</span>
|
||||
<span class="result-title">问题症状</span>
|
||||
</div>
|
||||
<div class="result-content">
|
||||
{{ symptoms.find(s => s.id === selectedSymptom).text }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-card">
|
||||
<div class="result-header">
|
||||
<span class="result-icon">🔬</span>
|
||||
<span class="result-title">可能原因</span>
|
||||
</div>
|
||||
<div class="result-content">{{ diagnosis }}</div>
|
||||
</div>
|
||||
|
||||
<div class="result-card success">
|
||||
<div class="result-header">
|
||||
<span class="result-icon">💊</span>
|
||||
<span class="result-title">解决方案</span>
|
||||
</div>
|
||||
<div class="result-content">{{ solution }}</div>
|
||||
</div>
|
||||
|
||||
<button class="btn secondary" @click="reset">
|
||||
← 重新诊断
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 通用排查流程 -->
|
||||
<div class="troubleshoot-flow">
|
||||
<div class="flow-title">📋 通用排查流程</div>
|
||||
<div class="flow-steps">
|
||||
<div class="flow-step">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">查看日志</div>
|
||||
<div class="step-desc">服务器日志、应用日志、错误日志</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-step">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">检查状态</div>
|
||||
<div class="step-desc">服务是否运行、端口是否监听、进程是否存在</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-step">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">资源监控</div>
|
||||
<div class="step-desc">CPU、内存、磁盘、网络使用情况</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-step">
|
||||
<div class="step-number">4</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">网络测试</div>
|
||||
<div class="step-desc">ping、telnet、curl 测试连通性</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 常用命令 -->
|
||||
<div class="commands-cheatsheet">
|
||||
<div class="cheatsheet-title">⚡ 常用排查命令</div>
|
||||
<div class="command-list">
|
||||
<div class="command-item">
|
||||
<code class="command-code">tail -f /var/log/nginx/error.log</code>
|
||||
<span class="command-desc">实时查看 Nginx 错误日志</span>
|
||||
</div>
|
||||
<div class="command-item">
|
||||
<code class="command-code">systemctl status nginx</code>
|
||||
<span class="command-desc">检查 Nginx 服务状态</span>
|
||||
</div>
|
||||
<div class="command-item">
|
||||
<code class="command-code">netstat -tlnp | grep :80</code>
|
||||
<span class="command-desc">检查 80 端口是否被监听</span>
|
||||
</div>
|
||||
<div class="command-item">
|
||||
<code class="command-code">ps aux | grep node</code>
|
||||
<span class="command-desc">查看 Node.js 进程</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
💡 <strong>小明经验</strong>:遇到问题不要慌!按照"查看症状→分析原因→尝试解决→验证效果"的流程,
|
||||
90%的问题都能快速定位。记得记录问题,避免重复踩坑!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.deployment-troubleshoot {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
padding: 1.25rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.2rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.symptom-selection {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.symptom-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.symptom-card {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.symptom-card:hover {
|
||||
border-color: var(--vp-c-brand-soft);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.symptom-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.symptom-text {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.diagnosis-result {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.result-card {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.result-card.success {
|
||||
border-color: var(--vp-c-brand-delta);
|
||||
background: var(--vp-c-brand-dimm);
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.result-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.result-content {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
padding-left: 1.7rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.6rem 1.25rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn.secondary {
|
||||
background: var(--vp-c-bg-alt);
|
||||
color: var(--vp-c-text-1);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.btn.secondary:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.troubleshoot-flow {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.flow-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.flow-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.commands-cheatsheet {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.cheatsheet-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.command-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.command-item {
|
||||
background: #1e1e1e;
|
||||
border-radius: 4px;
|
||||
padding: 0.6rem 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.command-code {
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 0.8rem;
|
||||
color: #4ec9b0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.command-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 1rem 1.25rem;
|
||||
margin: 0;
|
||||
background: var(--vp-c-bg);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.symptom-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.command-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,217 +0,0 @@
|
||||
<!--
|
||||
DnsFlowDemo.vue
|
||||
DNS 记录操练台:地址簿隐喻
|
||||
-->
|
||||
<template>
|
||||
<div class="dns">
|
||||
<div class="header">
|
||||
<div class="title">DNS 查号台</div>
|
||||
<div class="subtitle">把“好记的名字”变成“机器的 IP”</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="field">
|
||||
<label>你要配哪个域名?</label>
|
||||
<input v-model="domain" placeholder="例如:baidu.com" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>你要做什么?(记录类型)</label>
|
||||
<div class="chips">
|
||||
<button
|
||||
v-for="r in recordTypes"
|
||||
:key="r.type"
|
||||
:class="['chip', { active: recordType === r.type }]"
|
||||
@click="recordType = r.type"
|
||||
>
|
||||
{{ r.desc }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<span>类型 (Type)</span>
|
||||
<code class="highlight">{{ recordType }}</code>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span>前缀 (Host)</span>
|
||||
<code>{{ hostLabel }}</code>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span>目标 (Value)</span>
|
||||
<code>{{ recordValue }}</code>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span>记忆时间 (TTL)</span>
|
||||
<code>{{ ttlSuggestion }}</code>
|
||||
</div>
|
||||
|
||||
<div class="human-speak">
|
||||
<span class="emoji">💡</span>
|
||||
<div class="text">
|
||||
<strong>人话解释:</strong>
|
||||
{{ humanExplanation }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const domain = ref('my-site.com')
|
||||
const recordType = ref('A')
|
||||
|
||||
const recordTypes = [
|
||||
{
|
||||
type: 'A',
|
||||
desc: '直接指向 IP',
|
||||
value: '1.2.3.4',
|
||||
explanation: '告诉查号台:我家住在“1.2.3.4”这个门牌号。最常用!',
|
||||
ttl: '600s (10分钟)'
|
||||
},
|
||||
{
|
||||
type: 'CNAME',
|
||||
desc: '指向别名',
|
||||
value: 'shops.myshopify.com',
|
||||
explanation: '告诉查号台:我搬家了,你去问问“shops.myshopify.com”我在哪。',
|
||||
ttl: '600s (10分钟)'
|
||||
},
|
||||
{
|
||||
type: 'MX',
|
||||
desc: '配置邮箱',
|
||||
value: 'mxbiz1.qq.com',
|
||||
explanation: '告诉邮递员:寄给我的信,请送到“mxbiz1.qq.com”这个邮局去。',
|
||||
ttl: '3600s (1小时)'
|
||||
}
|
||||
]
|
||||
|
||||
const currentRecord = computed(() =>
|
||||
recordTypes.find((r) => r.type === recordType.value)
|
||||
)
|
||||
|
||||
const hostLabel = computed(() => (recordType.value === 'CNAME' ? 'www' : '@'))
|
||||
const recordValue = computed(() => currentRecord.value?.value || '')
|
||||
const ttlSuggestion = computed(() => currentRecord.value?.ttl || '600s')
|
||||
const humanExplanation = computed(() => currentRecord.value?.explanation || '')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dns {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header .title {
|
||||
font-weight: 800;
|
||||
font-size: 18px;
|
||||
}
|
||||
.header .subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
background: var(--vp-c-bg);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chip {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 999px;
|
||||
padding: 6px 14px;
|
||||
background: var(--vp-c-bg);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.chip.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.row span {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
code {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.human-speak {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px dashed var(--vp-c-divider);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
</style>
|
||||
@@ -1,212 +0,0 @@
|
||||
<!--
|
||||
HttpsNginxDemo.vue
|
||||
Nginx 配置生成器:极简版
|
||||
-->
|
||||
<template>
|
||||
<div class="https">
|
||||
<div class="header">
|
||||
<div class="title">Nginx 配置生成器</div>
|
||||
<div class="subtitle">复制粘贴就能用,自动配置 HTTPS</div>
|
||||
</div>
|
||||
|
||||
<div class="options">
|
||||
<div
|
||||
class="option"
|
||||
:class="{ active: mode === 'static' }"
|
||||
@click="mode = 'static'"
|
||||
>
|
||||
<span class="icon">📄</span>
|
||||
<span>静态网站</span>
|
||||
</div>
|
||||
<div
|
||||
class="option"
|
||||
:class="{ active: mode === 'proxy' }"
|
||||
@click="mode = 'proxy'"
|
||||
>
|
||||
<span class="icon">🔄</span>
|
||||
<span>反向代理 (Node/Python)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="code-box">
|
||||
<div class="code-header">
|
||||
<span>/etc/nginx/sites-available/default</span>
|
||||
<button class="copy-btn" @click="copy">
|
||||
{{ copied ? '已复制' : '复制' }}
|
||||
</button>
|
||||
</div>
|
||||
<pre><code>{{ snippet }}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="tips">
|
||||
<div class="tip-item">
|
||||
<span class="emoji">🔑</span>
|
||||
<div>
|
||||
<strong>开启 HTTPS 神器:</strong>
|
||||
<div class="cmd">sudo certbot --nginx</div>
|
||||
<div class="desc">
|
||||
运行这行命令,它会自动修改上面的配置,帮你加上 SSL 证书。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const mode = ref('proxy')
|
||||
const copied = ref(false)
|
||||
|
||||
const snippet = computed(() => {
|
||||
if (mode.value === 'static') {
|
||||
return `server {
|
||||
listen 80;
|
||||
server_name example.com; # 改成你的域名
|
||||
|
||||
# 静态文件在哪里?
|
||||
root /var/www/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
}`
|
||||
} else {
|
||||
return `server {
|
||||
listen 80;
|
||||
server_name example.com; # 改成你的域名
|
||||
|
||||
location / {
|
||||
# 把请求转发给 3000 端口的程序
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
|
||||
# 告诉后端真实的客户 IP 是多少
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}`
|
||||
}
|
||||
})
|
||||
|
||||
function copy() {
|
||||
navigator.clipboard.writeText(snippet.value)
|
||||
copied.value = true
|
||||
setTimeout(() => (copied.value = false), 2000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.https {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.header .title {
|
||||
font-weight: 800;
|
||||
font-size: 18px;
|
||||
}
|
||||
.header .subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.option {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.option.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.code-box {
|
||||
background: #1e1e20;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.code-header {
|
||||
background: #2d2d30;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tips {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.cmd {
|
||||
background: #1e1e20;
|
||||
color: #22c55e;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
margin: 4px 0;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
@@ -1,267 +0,0 @@
|
||||
<!--
|
||||
ObservabilityBackupDemo.vue
|
||||
监控与备份:买保险隐喻
|
||||
-->
|
||||
<template>
|
||||
<div class="obs">
|
||||
<div class="header">
|
||||
<div class="title">监控与备份</div>
|
||||
<div class="subtitle">给你的网站买份“保险”</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control">
|
||||
<label>安保级别 (监控)</label>
|
||||
<select v-model="monitorLevel">
|
||||
<option value="lite">入门:只装个摄像头 (日志)</option>
|
||||
<option value="std">标准:雇个保安 (指标+告警)</option>
|
||||
<option value="pro">专业:24h 安保中心 (全链路追踪)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="control">
|
||||
<label>通知谁?(告警渠道)</label>
|
||||
<div class="chips">
|
||||
<button
|
||||
v-for="c in channels"
|
||||
:key="c.id"
|
||||
:class="['chip', { active: channel === c.id }]"
|
||||
@click="channel = c.id"
|
||||
>
|
||||
{{ c.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<label>备份频率 (小时)</label>
|
||||
<input
|
||||
type="range"
|
||||
min="6"
|
||||
max="48"
|
||||
step="6"
|
||||
v-model.number="backupHours"
|
||||
/>
|
||||
<div class="hint">每 {{ backupHours }} 小时存一次盘</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="label">安全评分</div>
|
||||
<div
|
||||
class="value big"
|
||||
:class="{
|
||||
green: riskScore <= 40,
|
||||
orange: riskScore > 40 && riskScore <= 70,
|
||||
red: riskScore > 70
|
||||
}"
|
||||
>
|
||||
{{ 100 - riskScore }} 分
|
||||
</div>
|
||||
<div class="note">
|
||||
{{
|
||||
riskScore > 70
|
||||
? '极其危险!'
|
||||
: riskScore > 40
|
||||
? '勉强及格'
|
||||
: '非常稳!'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">最坏情况 (丢数据)</div>
|
||||
<div class="value">{{ rpo }}</div>
|
||||
<div class="note">最多丢失多少小时的数据</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">恢复速度</div>
|
||||
<div class="value">{{ rto }}</div>
|
||||
<div class="note">出事后多久能修好</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checklist-box">
|
||||
<div class="box-title">保命清单 (Checklist)</div>
|
||||
<div class="checks">
|
||||
<div class="check" v-for="item in checklist" :key="item.label">
|
||||
<input type="checkbox" v-model="item.done" />
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
const monitorLevel = ref('std')
|
||||
const channels = [
|
||||
{ id: 'email', label: '发邮件 (慢)' },
|
||||
{ id: 'chat', label: '企业微信/飞书 (快)' },
|
||||
{ id: 'pager', label: '电话轰炸 (急)' }
|
||||
]
|
||||
const channel = ref('chat')
|
||||
const backupHours = ref(24)
|
||||
|
||||
const checklist = reactive([
|
||||
{ label: '日志能不能查到?', done: true },
|
||||
{ label: 'CPU 飙高了会不会报警?', done: false },
|
||||
{ label: '关键接口慢了知不知道?', done: false },
|
||||
{ label: '数据库有没有自动备份?', done: true },
|
||||
{ label: '备份文件真的能恢复吗?(演练过)', done: false }
|
||||
])
|
||||
|
||||
const riskScore = computed(() => {
|
||||
let score = 70
|
||||
if (monitorLevel.value === 'pro') score -= 20
|
||||
else if (monitorLevel.value === 'std') score -= 10
|
||||
|
||||
if (channel.value === 'pager') score -= 10
|
||||
else if (channel.value === 'chat') score -= 5
|
||||
|
||||
score -= Math.min(20, (48 - backupHours.value) * 0.8)
|
||||
|
||||
const doneCount = checklist.filter((i) => i.done).length
|
||||
score -= doneCount * 4
|
||||
|
||||
return Math.max(0, Math.min(100, Math.round(score)))
|
||||
})
|
||||
|
||||
const rpo = computed(() => `${backupHours.value} 小时`)
|
||||
const rto = computed(() => {
|
||||
if (monitorLevel.value === 'pro') return '15-30 分钟'
|
||||
if (monitorLevel.value === 'std') return '30-60 分钟'
|
||||
return '1-2 小时 (甚至更久)'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.obs {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header .title {
|
||||
font-weight: 800;
|
||||
font-size: 18px;
|
||||
}
|
||||
.header .subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.control {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
label {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
select,
|
||||
input[type='range'] {
|
||||
width: 100%;
|
||||
}
|
||||
.hint {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.chips {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.chip {
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.chip.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
background: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
}
|
||||
.label {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 13px;
|
||||
}
|
||||
.value {
|
||||
font-weight: 800;
|
||||
margin-top: 4px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.value.big {
|
||||
font-size: 24px;
|
||||
}
|
||||
.value.green {
|
||||
color: #22c55e;
|
||||
}
|
||||
.value.orange {
|
||||
color: #f59e0b;
|
||||
}
|
||||
.value.red {
|
||||
color: #ef4444;
|
||||
}
|
||||
.note {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.checklist-box {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
}
|
||||
.box-title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.checks {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.check {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,194 +0,0 @@
|
||||
<!--
|
||||
RollbackSwitchDemo.vue
|
||||
发布策略:如何不关门装修
|
||||
-->
|
||||
<template>
|
||||
<div class="roll">
|
||||
<div class="header">
|
||||
<div class="title">发布策略对比</div>
|
||||
<div class="subtitle">网站升级就像店铺装修,怎么才能不影响做生意?</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button
|
||||
v-for="s in strategies"
|
||||
:key="s.id"
|
||||
:class="['tab', { active: current === s.id }]"
|
||||
@click="current = s.id"
|
||||
>
|
||||
<span class="emoji">{{ s.emoji }}</span>
|
||||
{{ s.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="card">
|
||||
<div class="label">操作方式</div>
|
||||
<div class="value">{{ flow }}</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">后悔药 (回滚时间)</div>
|
||||
<div class="value">{{ rollbackTime }}</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">成本代价</div>
|
||||
<div class="value">{{ cost }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="analogy-section">
|
||||
<div class="col">
|
||||
<div class="section-title">🧐 通俗理解</div>
|
||||
<div class="analogy-text">{{ analogy }}</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="section-title">⚠️ 风险点</div>
|
||||
<div class="risk-text">{{ risk }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const strategies = [
|
||||
{ id: 'rolling', label: '滚动更新 (Rolling)', emoji: '🔄' },
|
||||
{ id: 'blue', label: '蓝绿发布 (Blue/Green)', emoji: '🔵🟢' },
|
||||
{ id: 'canary', label: '金丝雀发布 (Canary)', emoji: '🐦' }
|
||||
]
|
||||
|
||||
const current = ref('rolling')
|
||||
|
||||
const flow = computed(() => {
|
||||
if (current.value === 'rolling') return '分批替换'
|
||||
if (current.value === 'blue') return '全量切换'
|
||||
return '按比例慢慢切'
|
||||
})
|
||||
|
||||
const rollbackTime = computed(() => {
|
||||
if (current.value === 'rolling') return '慢 (3-10 分钟)'
|
||||
if (current.value === 'blue') return '极快 (秒级)'
|
||||
return '快 (秒级)'
|
||||
})
|
||||
|
||||
const cost = computed(() => {
|
||||
if (current.value === 'rolling') return '低 (资源利用率高)'
|
||||
if (current.value === 'blue') return '高 (需要双倍机器)'
|
||||
return '中 (需要复杂网关)'
|
||||
})
|
||||
|
||||
const analogy = computed(() => {
|
||||
switch (current.value) {
|
||||
case 'rolling':
|
||||
return '就像餐厅换桌布。不关门,一桌一桌换。客人来了坐新桌子,还没换好的桌子先空着。'
|
||||
case 'blue':
|
||||
return '有钱任性。在隔壁新开一家一模一样的店。装修好了,直接把大门指路牌改到新店。'
|
||||
case 'canary':
|
||||
return '先让 VIP 客户去新包间体验一下。如果 VIP 没投诉,再把所有客人都请进去。'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const risk = computed(() => {
|
||||
if (current.value === 'rolling')
|
||||
return '中间状态比较乱,有的客人看到新装修,有的看到旧装修。'
|
||||
if (current.value === 'blue') return '太贵了!'
|
||||
return '技术要求高,得有能识别 VIP 的“门童” (流量网关)。'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.roll {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header .title {
|
||||
font-weight: 800;
|
||||
font-size: 18px;
|
||||
}
|
||||
.header .subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.tab {
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.tab.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg-soft);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
.label {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 12px;
|
||||
}
|
||||
.value {
|
||||
font-weight: 800;
|
||||
margin-top: 4px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.analogy-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.col {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
}
|
||||
.section-title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.analogy-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.risk-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #ef4444;
|
||||
}
|
||||
</style>
|
||||
@@ -1,246 +0,0 @@
|
||||
<!--
|
||||
ServerSizerDemo.vue
|
||||
服务器选购指南:租房隐喻
|
||||
-->
|
||||
<template>
|
||||
<div class="sizer">
|
||||
<div class="header">
|
||||
<div class="title">服务器选购指南</div>
|
||||
<div class="subtitle">不花冤枉钱,够用就好</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control">
|
||||
<label>你的业务规模?</label>
|
||||
<div class="range-box">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="3"
|
||||
step="1"
|
||||
v-model.number="scaleIndex"
|
||||
/>
|
||||
<div class="scale-labels">
|
||||
<span>个人博客</span>
|
||||
<span>初创官网</span>
|
||||
<span>小型 App</span>
|
||||
<span>中型平台</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-card">
|
||||
<div class="main-rec">
|
||||
<div class="icon">{{ recommendation.icon }}</div>
|
||||
<div class="details">
|
||||
<div class="rec-title">{{ recommendation.title }}</div>
|
||||
<div class="rec-spec">{{ recommendation.spec }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="analogy-box">
|
||||
<p><strong>🤔 通俗理解:</strong></p>
|
||||
<p>{{ recommendation.analogy }}</p>
|
||||
</div>
|
||||
|
||||
<div class="specs-grid">
|
||||
<div class="spec-item">
|
||||
<span class="label">带宽 (水管)</span>
|
||||
<span class="val">{{ recommendation.bandwidth }}</span>
|
||||
</div>
|
||||
<div class="spec-item">
|
||||
<span class="label">预算估算</span>
|
||||
<span class="val">{{ recommendation.cost }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const scaleIndex = ref(1)
|
||||
|
||||
const levels = [
|
||||
{
|
||||
title: '入门级 (Entry)',
|
||||
spec: '1核 1G ~ 2G',
|
||||
icon: '🚲',
|
||||
bandwidth: '1 Mbps',
|
||||
cost: '¥50~99 / 年 (活动价)',
|
||||
analogy: '就像租了个单间。放个博客、跑个脚本完全够用。人多了会挤不动。'
|
||||
},
|
||||
{
|
||||
title: '标准级 (Standard)',
|
||||
spec: '2核 4G',
|
||||
icon: '🚗',
|
||||
bandwidth: '3~5 Mbps',
|
||||
cost: '¥300 / 年',
|
||||
analogy:
|
||||
'就像租了个两居室。正经跑个公司官网、小程序后端没问题。大多数人的首选。'
|
||||
},
|
||||
{
|
||||
title: '专业级 (Pro)',
|
||||
spec: '4核 8G',
|
||||
icon: '🏎️',
|
||||
bandwidth: '5~10 Mbps',
|
||||
cost: '¥1000+ / 年',
|
||||
analogy:
|
||||
'就像租了个大平层办公室。能抗住几千人同时在线,跑复杂的计算任务也不虚。'
|
||||
},
|
||||
{
|
||||
title: '企业级 (Enterprise)',
|
||||
spec: '8核 16G + 负载均衡',
|
||||
icon: '✈️',
|
||||
bandwidth: '按量付费',
|
||||
cost: '¥5000+ / 年',
|
||||
analogy:
|
||||
'这已经不是租房了,是包下了一整层楼。通常需要多台机器配合,专人维护。'
|
||||
}
|
||||
]
|
||||
|
||||
const recommendation = computed(() => levels[scaleIndex.value])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sizer {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.header .title {
|
||||
font-weight: 800;
|
||||
font-size: 18px;
|
||||
}
|
||||
.header .subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.control label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.range-box {
|
||||
background: var(--vp-c-bg);
|
||||
padding: 16px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
input[type='range'] {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.scale-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.result-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-brand);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.result-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.main-rec {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 40px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 12px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.rec-title {
|
||||
font-weight: 800;
|
||||
font-size: 18px;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.rec-spec {
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.analogy-box {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.analogy-box p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.analogy-box strong {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.specs-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
border-top: 1px dashed var(--vp-c-divider);
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.spec-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.spec-item .label {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.spec-item .val {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user