Files
test-repo/docs/.vitepress/theme/components/appendix/data-encoding/PhotoUploadJourneyDemo.vue
T
sanbuphy 6b1a9cf056 feat(docs): add Netlify deployment guide and data encoding demos
- Add Netlify deployment section with form handling and functions examples
- Replace old Git demos with new interactive components
- Add comprehensive data encoding visualization demos
- Update comparison table with Netlify information
2026-02-22 01:21:39 +08:00

366 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="journey-demo">
<!-- Step tabs -->
<div class="step-tabs">
<div
v-for="(step, i) in steps"
:key="i"
:class="['step-tab', { active: currentStep >= i, current: currentStep === i }]"
@click="goToStep(i)"
>
<span class="tab-num">{{ i + 1 }}</span>
<span class="tab-label">{{ step.label }}</span>
</div>
</div>
<!-- Main canvas -->
<div class="journey-canvas" :style="{ borderColor: currentStepData.color + '88' }">
<!-- Scene -->
<div class="scene">
<div class="scene-actors">
<div
v-for="(actor, i) in currentStepData.actors"
:key="i"
class="actor"
:class="{ highlighted: actor.highlight, animated: actor.animated }"
>
<div class="actor-icon">{{ actor.icon }}</div>
<div class="actor-name">{{ actor.name }}</div>
<div v-if="actor.value" class="actor-value">{{ actor.value }}</div>
</div>
<!-- Arrows between actors -->
<div
v-for="(arrow, i) in currentStepData.arrows"
:key="'arrow' + i"
class="flow-arrow"
:class="{ animated: isRunning }"
>
<span class="arrow-label">{{ arrow.label }}</span>
<span class="arrow-sym"></span>
</div>
</div>
</div>
<!-- Explanation panel -->
<div class="explanation-panel" :style="{ borderLeftColor: currentStepData.color }">
<div class="exp-header">
<span class="exp-icon">{{ currentStepData.icon }}</span>
<span class="exp-title">{{ currentStepData.title }}</span>
</div>
<ul class="exp-points">
<li v-for="(pt, i) in currentStepData.points" :key="i" class="exp-point" :class="{ visible: visiblePoints.includes(i) }">
{{ pt }}
</li>
</ul>
<div class="exp-insight">💡 {{ currentStepData.insight }}</div>
</div>
</div>
<!-- Controls -->
<div class="controls">
<button class="ctrl-btn secondary" :disabled="currentStep === 0" @click="goToStep(currentStep - 1)"> 上一步</button>
<button class="ctrl-btn primary" @click="runCurrentStep" :disabled="isRunning">
{{ isRunning ? '进行中...' : currentStep === steps.length - 1 ? '🔄 重新演示' : '▶ 执行这一步' }}
</button>
<button class="ctrl-btn secondary" :disabled="currentStep >= steps.length - 1" @click="goToStep(currentStep + 1)">下一步 </button>
</div>
<!-- Overall insight -->
<div class="final-insight">
🎯 <strong>三步三役</strong><strong>编码</strong>负责"翻译成机器语言"<strong>存储</strong>负责"记住它"<strong>传输</strong>负责"送到目的地"缺了任何一环这张照片就不会出现在云端
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentStep = ref(0)
const isRunning = ref(false)
const visiblePoints = ref([])
const steps = [
{
label: '编码',
icon: '🔢',
title: '第一步:编码 — 把光变成数字',
color: '#7c3aed',
actors: [
{ icon: '☀️', name: '光线', highlight: false },
{ icon: '📷', name: '传感器', highlight: true, animated: true },
{ icon: '📊', name: 'RAW 数据', value: '24MB / 4860万像素' },
{ icon: '🗜️', name: 'JPEG 压缩', highlight: true },
{ icon: '📄', name: 'JPEG 文件', value: '3.2MB(压缩后)' }
],
arrows: [
{ label: 'ADC 采样' },
{ label: '像素编码' },
{ label: '有损压缩' }
],
points: [
'📸 相机传感器把光信号转换成 RGB 数值(每个像素 3 × 8 bit = 24 bit',
'🔢 整张照片 4860 万像素 × 24 bit ≈ 140 MB 的原始数据',
'🗜️ JPEG 算法分析像素之间的相似性,去掉人眼不敏感的信息,压缩到 3 MB'
],
insight: '压缩 ≠ 降质,好的压缩算法让你几乎看不出差别,但文件小了 97%。'
},
{
label: '存储',
icon: '💾',
title: '第二步:存储 — 先闪存后闪存',
color: '#059669',
actors: [
{ icon: '📄', name: 'JPEG(已编码)', value: '3.2 MB' },
{ icon: '🧠', name: 'RAM(内存)', value: '写入耗时:~1 ms', highlight: true, animated: true },
{ icon: '💾', name: '闪存(Flash', value: '写入耗时:~10 ms', highlight: true }
],
arrows: [
{ label: '临时缓存' },
{ label: '持久写入' }
],
points: [
'⚡ 图像先写进内存(RAM)——速度极快,但断电消失',
'💾 内存中的数据再异步写入闪存(手机存储)——速度慢一些,但永久保存',
'🔒 写完后操作系统标记文件"安全",你才能看到相册里的新照片'
],
insight: '为什么拍完不能马上拔电池?因为数据可能还在内存里,还没写进闪存!'
},
{
label: '传输',
icon: '📡',
title: '第三步:传输 — 数据"旅行"到云端',
color: '#d97706',
actors: [
{ icon: '💾', name: '闪存(JPEG', value: '3.2 MB' },
{ icon: '📶', name: 'Wi-Fi / 4G', value: 'TCP 分包传输', highlight: true, animated: true },
{ icon: '☁️', name: '云端服务器', value: '写入云存储', highlight: true }
],
arrows: [
{ label: '分包 + 加密' },
{ label: '校验 + 重组' }
],
points: [
'📦 3.2 MB 的 JPEG 文件被 TCP 协议切成数千个小"数据包"',
'🔐 每个包都有序号和校验码,丢了会自动重传——所以传输是可靠的',
'☁️ 云端收齐所有包,重新拼成完整 JPEG,写入对象存储(如 OSS/S3'
],
insight: '上传时你以为数据是"整个发过去"的,其实是"切碎了一片片送过去"。'
}
]
const currentStepData = computed(() => steps[currentStep.value])
function goToStep(i) {
currentStep.value = i
visiblePoints.value = []
isRunning.value = false
}
async function runCurrentStep() {
if (currentStep.value === steps.length - 1 && !isRunning.value && visiblePoints.value.length === steps[currentStep.value].points.length) {
goToStep(0)
return
}
isRunning.value = true
visiblePoints.value = []
const pts = steps[currentStep.value].points
for (let i = 0; i < pts.length; i++) {
await new Promise(r => setTimeout(r, 600))
visiblePoints.value.push(i)
}
isRunning.value = false
// Auto advance after last point, unless last step
if (currentStep.value < steps.length - 1) {
await new Promise(r => setTimeout(r, 1000))
currentStep.value++
visiblePoints.value = []
}
}
</script>
<style scoped>
.journey-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.25rem;
margin: 1rem 0;
display: flex;
flex-direction: column;
gap: 1rem;
}
/* Step tabs */
.step-tabs {
display: flex;
gap: 0.5rem;
}
.step-tab {
flex: 1;
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.45rem 0.6rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.82rem;
transition: all 0.2s;
opacity: 0.6;
}
.step-tab.active { opacity: 1; border-color: var(--vp-c-brand); }
.step-tab.current { background: var(--vp-c-brand-soft); }
.tab-num {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.72rem;
font-weight: bold;
flex-shrink: 0;
}
.step-tab.active .tab-num { background: var(--vp-c-brand); color: white; }
.tab-label { font-weight: bold; }
/* Canvas */
.journey-canvas {
display: flex;
flex-direction: column;
gap: 0.75rem;
background: var(--vp-c-bg);
border: 2px solid;
border-radius: 8px;
padding: 1rem;
transition: border-color 0.4s;
}
/* Scene */
.scene { padding: 0.5rem 0; }
.scene-actors {
display: flex;
align-items: center;
gap: 0.3rem;
flex-wrap: wrap;
}
.actor {
display: flex;
flex-direction: column;
align-items: center;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.5rem 0.75rem;
min-width: 80px;
transition: all 0.3s;
text-align: center;
}
.actor.highlighted { border-color: var(--vp-c-brand); background: var(--vp-c-brand-soft); }
.actor.animated { animation: pulse-gentle 1.5s ease-in-out infinite; }
@keyframes pulse-gentle {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.04); }
}
.actor-icon { font-size: 1.6rem; }
.actor-name { font-size: 0.72rem; font-weight: bold; margin-top: 2px; }
.actor-value { font-size: 0.65rem; color: var(--vp-c-text-2); margin-top: 2px; white-space: nowrap; }
.flow-arrow {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
flex-shrink: 0;
}
.arrow-label { font-size: 0.65rem; color: var(--vp-c-text-3); white-space: nowrap; }
.arrow-sym { font-size: 1.2rem; color: var(--vp-c-brand); }
/* Explanation */
.explanation-panel {
border-left: 4px solid;
padding: 0.75rem 1rem;
background: var(--vp-c-bg-soft);
border-radius: 0 6px 6px 0;
transition: border-left-color 0.4s;
}
.exp-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.exp-icon { font-size: 1.2rem; }
.exp-title { font-weight: bold; font-size: 0.95rem; }
.exp-points { list-style: none; padding: 0; margin: 0 0 0.6rem 0; display: flex; flex-direction: column; gap: 0.4rem; }
.exp-point {
font-size: 0.83rem;
color: var(--vp-c-text-1);
line-height: 1.5;
opacity: 0;
transform: translateX(-8px);
transition: all 0.4s ease;
}
.exp-point.visible { opacity: 1; transform: translateX(0); }
.exp-insight {
font-size: 0.82rem;
color: var(--vp-c-text-2);
font-style: italic;
}
/* Controls */
.controls {
display: flex;
gap: 0.5rem;
align-items: center;
}
.ctrl-btn {
padding: 0.45rem 1rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
cursor: pointer;
font-size: 0.88rem;
transition: all 0.2s;
}
.ctrl-btn.primary {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
flex: 1;
font-weight: bold;
}
.ctrl-btn.secondary { background: var(--vp-c-bg); }
.ctrl-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.final-insight {
background: var(--vp-c-bg-alt);
border-left: 4px solid var(--vp-c-brand);
padding: 0.75rem 1rem;
border-radius: 0 6px 6px 0;
font-size: 0.85rem;
line-height: 1.6;
}
</style>