feat(docs): add interactive demo components for technical appendices
Add placeholder Vue components for visualizing technical concepts across multiple domains including frontend routing, browser rendering, cache design, queue design, database principles, API design, cloud services, and backend evolution. These components provide interactive educational content for the documentation. Update documentation structure to include new appendix sections and enhance existing content with visual components. Remove unused 'codex' dependency from package.json.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="demo-container">
|
||||
<div class="demo-header">
|
||||
<h4>{{ title }}</h4>
|
||||
<p class="hint">{{ description }}</p>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<el-alert type="info" :closable="false">
|
||||
访问分析演示组件占位符 - 待实现具体交互
|
||||
</el-alert>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const title = ref('访问分析演示')
|
||||
const description = ref('展示CDN和对象存储的访问统计分析,包括流量、带宽、访问热点等')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="demo-container">
|
||||
<div class="demo-header">
|
||||
<h4>{{ title }}</h4>
|
||||
<p class="hint">{{ description }}</p>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<el-alert type="info" :closable="false">
|
||||
缓存策略演示组件占位符 - 待实现具体交互
|
||||
</el-alert>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const title = ref('缓存策略演示')
|
||||
const description = ref('展示CDN和对象存储的缓存策略配置,包括缓存时间、刷新机制等')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,696 @@
|
||||
<!--
|
||||
CdnAccelerationDemo.vue
|
||||
CDN 加速原理演示 - 展示边缘节点、源站、回源等概念
|
||||
-->
|
||||
<template>
|
||||
<div class="cdn-acceleration-demo">
|
||||
<div class="header">
|
||||
<div class="title">CDN 加速原理</div>
|
||||
<div class="subtitle">边缘节点、源站与回源的协同工作</div>
|
||||
</div>
|
||||
|
||||
<div class="cdn-architecture">
|
||||
<!-- 用户层 -->
|
||||
<div class="layer users-layer">
|
||||
<div class="layer-title">
|
||||
<span class="icon">👥</span>
|
||||
<span>全球用户</span>
|
||||
</div>
|
||||
<div class="users-map">
|
||||
<div
|
||||
v-for="user in users"
|
||||
:key="user.id"
|
||||
class="user-marker"
|
||||
:class="{ active: activeUser === user.id, requesting: requestingUser === user.id }"
|
||||
:style="{ left: user.x + '%', top: user.y + '%' }"
|
||||
@click="selectUser(user)"
|
||||
>
|
||||
<div class="user-icon">{{ user.icon }}</div>
|
||||
<div class="user-label">{{ user.name }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 请求动画线 -->
|
||||
<div v-if="requestAnimation" class="request-line" :style="requestLineStyle"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 边缘节点层 -->
|
||||
<div class="layer edge-layer">
|
||||
<div class="layer-title">
|
||||
<span class="icon">🌐</span>
|
||||
<span>CDN 边缘节点 (Edge Nodes)</span>
|
||||
<span class="layer-status" :class="{ hit: cacheHit, miss: !cacheHit && showCacheStatus }">
|
||||
{{ cacheStatusText }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="edge-nodes">
|
||||
<div
|
||||
v-for="node in edgeNodes"
|
||||
:key="node.id"
|
||||
class="edge-node"
|
||||
:class="{ active: activeNode === node.id, serving: servingNode === node.id }"
|
||||
@click="selectNode(node)"
|
||||
>
|
||||
<div class="node-icon">{{ node.icon }}</div>
|
||||
<div class="node-info">
|
||||
<div class="node-name">{{ node.name }}</div>
|
||||
<div class="node-location">{{ node.location }}</div>
|
||||
</div>
|
||||
<div class="node-stats">
|
||||
<div class="stat">
|
||||
<span class="stat-label">缓存</span>
|
||||
<span class="stat-value">{{ node.cacheSize }}</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">命中</span>
|
||||
<span class="stat-value" :style="{ color: node.hitRate > 80 ? '#22c55e' : '#f59e0b' }">
|
||||
{{ node.hitRate }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 源站层 -->
|
||||
<div class="layer origin-layer">
|
||||
<div class="layer-title">
|
||||
<span class="icon">🏢</span>
|
||||
<span>源站 (Origin Server)</span>
|
||||
<span class="layer-status" :class="{ active: showBackToSource }">
|
||||
{{ backToSourceText }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="origin-servers">
|
||||
<div class="origin-server">
|
||||
<div class="server-icon">🗄️</div>
|
||||
<div class="server-info">
|
||||
<div class="server-name">对象存储源站</div>
|
||||
<div class="server-address">bucket.oss-cn-beijing.aliyuncs.com</div>
|
||||
</div>
|
||||
<div class="server-status">
|
||||
<span class="status-dot active"></span>
|
||||
<span class="status-text">健康</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showBackToSource" class="back-to-source-flow">
|
||||
<div class="flow-arrow">
|
||||
<span>⬆️ 回源请求</span>
|
||||
</div>
|
||||
<div class="flow-detail">
|
||||
<div class="flow-step">1. CDN 节点未命中缓存</div>
|
||||
<div class="flow-step">2. 向源站发起回源请求</div>
|
||||
<div class="flow-step">3. 源站返回文件内容</div>
|
||||
<div class="flow-step">4. CDN 缓存并响应用户</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 交互控制区 -->
|
||||
<div class="demo-controls">
|
||||
<div class="controls-title">🎮 模拟演示</div>
|
||||
<div class="controls-row">
|
||||
<button class="control-btn" @click="simulateCacheHit">
|
||||
<span>✅</span>
|
||||
<span>模拟缓存命中</span>
|
||||
</button>
|
||||
<button class="control-btn" @click="simulateCacheMiss">
|
||||
<span>❌</span>
|
||||
<span>模拟缓存未命中(回源)</span>
|
||||
</button>
|
||||
<button class="control-btn reset" @click="resetDemo">
|
||||
<span>🔄</span>
|
||||
<span>重置</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="stats-panel">
|
||||
<div class="stats-title">📊 访问统计</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" :style="{ color: '#22c55e' }">{{ stats.cacheHit }}</div>
|
||||
<div class="stat-label">缓存命中</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" :style="{ color: '#ef4444' }">{{ stats.cacheMiss }}</div>
|
||||
<div class="stat-label">缓存未命中</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" :style="{ color: stats.hitRate > 80 ? '#22c55e' : '#f59e0b' }">
|
||||
{{ stats.hitRate }}%
|
||||
</div>
|
||||
<div class="stat-label">命中率</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" :style="{ color: '#3b82f6' }">{{ stats.avgResponseTime }}ms</div>
|
||||
<div class="stat-label">平均响应</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, reactive } from 'vue'
|
||||
|
||||
// 用户数据
|
||||
const users = [
|
||||
{ id: 'user1', name: '北京用户', icon: '👤', x: 75, y: 35 },
|
||||
{ id: 'user2', name: '上海用户', icon: '👤', x: 80, y: 55 },
|
||||
{ id: 'user3', name: '广州用户', icon: '👤', x: 70, y: 75 },
|
||||
{ id: 'user4', name: '成都用户', icon: '👤', x: 50, y: 60 },
|
||||
{ id: 'user5', name: '海外用户', icon: '👤', x: 90, y: 25 }
|
||||
]
|
||||
|
||||
// 边缘节点数据
|
||||
const edgeNodes = [
|
||||
{ id: 'node1', name: '北京节点', icon: '🌐', location: '华北', cacheSize: '2.5 TB', hitRate: 92 },
|
||||
{ id: 'node2', name: '上海节点', icon: '🌐', location: '华东', cacheSize: '3.1 TB', hitRate: 89 },
|
||||
{ id: 'node3', name: '广州节点', icon: '🌐', location: '华南', cacheSize: '1.8 TB', hitRate: 87 },
|
||||
{ id: 'node4', name: '成都节点', icon: '🌐', location: '西南', cacheSize: '1.2 TB', hitRate: 85 }
|
||||
]
|
||||
|
||||
// 状态
|
||||
const activeUser = ref(null)
|
||||
const requestingUser = ref(null)
|
||||
const activeNode = ref(null)
|
||||
const servingNode = ref(null)
|
||||
const cacheHit = ref(false)
|
||||
const showCacheStatus = ref(false)
|
||||
const showBackToSource = ref(false)
|
||||
const requestAnimation = ref(false)
|
||||
|
||||
// 统计
|
||||
const stats = reactive({
|
||||
cacheHit: 0,
|
||||
cacheMiss: 0,
|
||||
hitRate: 0,
|
||||
avgResponseTime: 0
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const requestLineStyle = computed(() => {
|
||||
if (!activeUser.value || !activeNode.value) return {}
|
||||
// 这里简化处理,实际应该计算从用户到节点的线
|
||||
return {}
|
||||
})
|
||||
|
||||
const cacheStatusText = computed(() => {
|
||||
if (!showCacheStatus.value) return ''
|
||||
return cacheHit.value ? '✅ 缓存命中' : '❌ 未命中'
|
||||
})
|
||||
|
||||
const backToSourceText = computed(() => {
|
||||
if (!showBackToSource.value) return ''
|
||||
return '📥 回源中...'
|
||||
})
|
||||
|
||||
// 方法
|
||||
const selectUser = (user) => {
|
||||
activeUser.value = user.id
|
||||
}
|
||||
|
||||
const selectNode = (node) => {
|
||||
activeNode.value = node.id
|
||||
}
|
||||
|
||||
const simulateCacheHit = () => {
|
||||
resetDemo()
|
||||
stats.cacheHit++
|
||||
updateStats()
|
||||
|
||||
// 模拟缓存命中流程
|
||||
activeUser.value = 'user1'
|
||||
requestingUser.value = 'user1'
|
||||
activeNode.value = 'node1'
|
||||
servingNode.value = 'node1'
|
||||
|
||||
setTimeout(() => {
|
||||
showCacheStatus.value = true
|
||||
cacheHit.value = true
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const simulateCacheMiss = () => {
|
||||
resetDemo()
|
||||
stats.cacheMiss++
|
||||
updateStats()
|
||||
|
||||
// 模拟缓存未命中(回源)流程
|
||||
activeUser.value = 'user3'
|
||||
requestingUser.value = 'user3'
|
||||
activeNode.value = 'node3'
|
||||
servingNode.value = 'node3'
|
||||
|
||||
setTimeout(() => {
|
||||
showCacheStatus.value = true
|
||||
cacheHit.value = false
|
||||
showBackToSource.value = true
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const updateStats = () => {
|
||||
const total = stats.cacheHit + stats.cacheMiss
|
||||
stats.hitRate = total > 0 ? Math.round((stats.cacheHit / total) * 100) : 0
|
||||
// 模拟平均响应时间:命中约 20ms,未命中约 200ms
|
||||
stats.avgResponseTime = total > 0
|
||||
? Math.round((stats.cacheHit * 20 + stats.cacheMiss * 200) / total)
|
||||
: 0
|
||||
}
|
||||
|
||||
const resetDemo = () => {
|
||||
activeUser.value = null
|
||||
requestingUser.value = null
|
||||
activeNode.value = null
|
||||
servingNode.value = null
|
||||
cacheHit.value = false
|
||||
showCacheStatus.value = false
|
||||
showBackToSource.value = false
|
||||
requestAnimation.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cdn-acceleration-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.cdn-architecture {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.layer {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.layer-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.layer-title .icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.layer-status {
|
||||
margin-left: auto;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.layer-status.hit {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.layer-status.miss {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.layer-status.active {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
/* 用户层 */
|
||||
.users-map {
|
||||
position: relative;
|
||||
height: 120px;
|
||||
background: linear-gradient(135deg, #f0f9ff, #e0f2fe);
|
||||
border-radius: 8px;
|
||||
border: 1px solid #bae6fd;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-marker {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.user-marker:hover {
|
||||
transform: translate(-50%, -50%) scale(1.1);
|
||||
}
|
||||
|
||||
.user-marker.active {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.user-marker.requesting .user-icon {
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.2); }
|
||||
}
|
||||
|
||||
.user-icon {
|
||||
font-size: 1.5rem;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.user-label {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
color: #0369a1;
|
||||
margin-top: 0.25rem;
|
||||
white-space: nowrap;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 边缘节点层 */
|
||||
.edge-nodes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.edge-nodes {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.edge-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
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;
|
||||
}
|
||||
|
||||
.edge-node:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.edge-node.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.edge-node.serving {
|
||||
animation: servingPulse 1s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes servingPulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); }
|
||||
50% { box-shadow: 0 0 0 8px rgba(59, 130, 246, 0.3); }
|
||||
}
|
||||
|
||||
.node-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.node-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.node-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.node-location {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.node-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.7rem;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
/* 源站层 */
|
||||
.origin-servers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.origin-server {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: linear-gradient(135deg, #fef3c7, #fde68a);
|
||||
border: 2px solid #f59e0b;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.server-icon {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.server-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.server-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.server-address {
|
||||
font-size: 0.75rem;
|
||||
color: #b45309;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.server-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #22c55e;
|
||||
animation: statusPulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes statusPulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.back-to-source-flow {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.flow-arrow {
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: #dc2626;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.flow-detail {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.flow-detail {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
font-size: 0.75rem;
|
||||
color: #991b1b;
|
||||
background: white;
|
||||
padding: 0.4rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #dc2626;
|
||||
}
|
||||
|
||||
/* 控制区 */
|
||||
.demo-controls {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.controls-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.controls-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background: var(--vp-c-bg-mute);
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.control-btn.reset {
|
||||
background: #fef2f2;
|
||||
border-color: #fecaca;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.control-btn.reset:hover {
|
||||
background: #fee2e2;
|
||||
}
|
||||
|
||||
/* 统计面板 */
|
||||
.stats-panel {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
</style>
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="demo-container">
|
||||
<div class="demo-header">
|
||||
<h4>{{ title }}</h4>
|
||||
<p class="hint">{{ description }}</p>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<el-alert type="info" :closable="false">
|
||||
边缘节点分布演示组件占位符 - 待实现具体交互
|
||||
</el-alert>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const title = ref('边缘节点分布演示')
|
||||
const description = ref('展示CDN边缘节点在全球的分布情况和调度策略')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="demo-container">
|
||||
<div class="demo-header">
|
||||
<h4>{{ title }}</h4>
|
||||
<p class="hint">{{ description }}</p>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<el-alert type="info" :closable="false">
|
||||
HTTPS优化演示组件占位符 - 待实现具体交互
|
||||
</el-alert>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const title = ref('HTTPS优化演示')
|
||||
const description = ref('展示CDN的HTTPS优化技术,包括TLS握手优化、证书管理、HSTS等')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,590 @@
|
||||
<!--
|
||||
ObjectStorageDemo.vue
|
||||
对象存储架构演示 - 展示桶、对象、元数据的核心概念
|
||||
-->
|
||||
<template>
|
||||
<div class="object-storage-demo">
|
||||
<div class="header">
|
||||
<div class="title">对象存储架构</div>
|
||||
<div class="subtitle">理解 Bucket、Object 和 Metadata 的关系</div>
|
||||
</div>
|
||||
|
||||
<div class="storage-architecture">
|
||||
<!-- 账户层 -->
|
||||
<div class="account-layer">
|
||||
<div class="account-icon">👤</div>
|
||||
<div class="account-name">云账户 (Account)</div>
|
||||
<div class="account-desc">管理权限、计费、全局配置</div>
|
||||
</div>
|
||||
|
||||
<div class="connector">▼</div>
|
||||
|
||||
<!-- 桶层 -->
|
||||
<div class="buckets-container">
|
||||
<div class="section-title">
|
||||
<span>📦</span>
|
||||
<span>存储桶 (Buckets)</span>
|
||||
<span class="section-desc">命名空间隔离,权限控制</span>
|
||||
</div>
|
||||
|
||||
<div class="buckets-row">
|
||||
<div
|
||||
v-for="bucket in buckets"
|
||||
:key="bucket.name"
|
||||
class="bucket-card"
|
||||
:class="{ active: selectedBucket === bucket.name }"
|
||||
@click="selectBucket(bucket.name)"
|
||||
>
|
||||
<div class="bucket-icon">{{ bucket.icon }}</div>
|
||||
<div class="bucket-name">{{ bucket.name }}</div>
|
||||
<div class="bucket-meta">{{ bucket.objects }} 对象</div>
|
||||
<div class="bucket-size">{{ bucket.size }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="connector">▼</div>
|
||||
|
||||
<!-- 对象层 -->
|
||||
<div class="objects-container">
|
||||
<div class="section-title">
|
||||
<span>📄</span>
|
||||
<span>对象 (Objects)</span>
|
||||
<span class="section-desc">文件数据 + 元数据</span>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedBucket" class="objects-list">
|
||||
<div
|
||||
v-for="obj in currentObjects"
|
||||
:key="obj.key"
|
||||
class="object-item"
|
||||
:class="{ selected: selectedObject === obj.key }"
|
||||
@click="selectObject(obj)"
|
||||
>
|
||||
<div class="object-icon">{{ getFileIcon(obj.type) }}</div>
|
||||
<div class="object-info">
|
||||
<div class="object-key">{{ obj.key }}</div>
|
||||
<div class="object-meta">{{ obj.size }} · {{ obj.lastModified }}</div>
|
||||
</div>
|
||||
<div class="object-arrow">▶</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="objects-placeholder">
|
||||
点击上方存储桶查看对象列表
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="connector">▼</div>
|
||||
|
||||
<!-- 元数据层 -->
|
||||
<div class="metadata-container">
|
||||
<div class="section-title">
|
||||
<span>🏷️</span>
|
||||
<span>元数据 (Metadata)</span>
|
||||
<span class="section-desc">系统元数据 + 自定义元数据</span>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedObject && currentMetadata" class="metadata-content">
|
||||
<div class="metadata-section">
|
||||
<div class="metadata-section-title">系统元数据 (System)</div>
|
||||
<div class="metadata-list">
|
||||
<div v-for="(value, key) in currentMetadata.system" :key="key" class="metadata-item">
|
||||
<span class="metadata-key">{{ key }}:</span>
|
||||
<span class="metadata-value">{{ value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metadata-section">
|
||||
<div class="metadata-section-title">自定义元数据 (Custom)</div>
|
||||
<div class="metadata-list">
|
||||
<div v-for="(value, key) in currentMetadata.custom" :key="key" class="metadata-item">
|
||||
<span class="metadata-key">{{ key }}:</span>
|
||||
<span class="metadata-value">{{ value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="metadata-placeholder">
|
||||
点击左侧对象查看详细元数据
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="architecture-summary">
|
||||
<div class="summary-title">架构要点总结</div>
|
||||
<div class="summary-grid">
|
||||
<div class="summary-item">
|
||||
<div class="summary-icon">📦</div>
|
||||
<div class="summary-text">
|
||||
<strong>Bucket(桶)</strong>
|
||||
<span>全局命名空间,用于组织和隔离数据</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<div class="summary-icon">📄</div>
|
||||
<div class="summary-text">
|
||||
<strong>Object(对象)</strong>
|
||||
<span>键值对存储,包含数据、元数据和唯一 Key</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<div class="summary-icon">🏷️</div>
|
||||
<div class="summary-text">
|
||||
<strong>Metadata(元数据)</strong>
|
||||
<span>系统元数据 + 自定义标签,支持检索和管理</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<div class="summary-icon">🔐</div>
|
||||
<div class="summary-text">
|
||||
<strong>Access Control(访问控制)</strong>
|
||||
<span>Bucket Policy、ACL、STS 临时凭证多层权限</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// 存储桶数据
|
||||
const buckets = [
|
||||
{
|
||||
name: 'myapp-images-prod',
|
||||
icon: '🖼️',
|
||||
objects: 12543,
|
||||
size: '256 GB'
|
||||
},
|
||||
{
|
||||
name: 'myapp-videos-prod',
|
||||
icon: '🎬',
|
||||
objects: 892,
|
||||
size: '1.2 TB'
|
||||
},
|
||||
{
|
||||
name: 'myapp-backups',
|
||||
icon: '💾',
|
||||
objects: 3456,
|
||||
size: '500 GB'
|
||||
}
|
||||
]
|
||||
|
||||
// 对象数据
|
||||
const objectsData = {
|
||||
'myapp-images-prod': [
|
||||
{ key: 'avatars/user123.jpg', type: 'image/jpeg', size: '156 KB', lastModified: '2024-01-15' },
|
||||
{ key: 'products/shoes-01.png', type: 'image/png', size: '2.3 MB', lastModified: '2024-01-14' },
|
||||
{ key: 'banners/sale-2024.webp', type: 'image/webp', size: '456 KB', lastModified: '2024-01-13' }
|
||||
],
|
||||
'myapp-videos-prod': [
|
||||
{ key: 'tutorials/intro.mp4', type: 'video/mp4', size: '156 MB', lastModified: '2024-01-15' },
|
||||
{ key: 'ads/promo-2024.mp4', type: 'video/mp4', size: '234 MB', lastModified: '2024-01-14' }
|
||||
],
|
||||
'myapp-backups': [
|
||||
{ key: 'db/daily-20240115.sql.gz', type: 'application/gzip', size: '456 MB', lastModified: '2024-01-15' },
|
||||
{ key: 'logs/access-20240114.log.gz', type: 'application/gzip', size: '123 MB', lastModified: '2024-01-14' }
|
||||
]
|
||||
}
|
||||
|
||||
// 元数据
|
||||
const metadataData = {
|
||||
'avatars/user123.jpg': {
|
||||
system: {
|
||||
'Content-Type': 'image/jpeg',
|
||||
'Content-Length': '159745',
|
||||
'Last-Modified': '2024-01-15T08:30:00Z',
|
||||
'ETag': '"abc123def456"',
|
||||
'x-oss-storage-class': 'Standard'
|
||||
},
|
||||
custom: {
|
||||
'x-oss-meta-owner': 'user123',
|
||||
'x-oss-meta-usage': 'avatar',
|
||||
'x-oss-meta-uploaded-by': 'web-upload'
|
||||
}
|
||||
},
|
||||
'products/shoes-01.png': {
|
||||
system: {
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Length': '2412555',
|
||||
'Last-Modified': '2024-01-14T16:20:00Z',
|
||||
'ETag': '"xyz789ghi012"',
|
||||
'x-oss-storage-class': 'Standard'
|
||||
},
|
||||
custom: {
|
||||
'x-oss-meta-product-id': 'shoes-01',
|
||||
'x-oss-meta-category': 'footwear',
|
||||
'x-oss-meta-price': '199.99'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 状态
|
||||
const selectedBucket = ref(null)
|
||||
const selectedObject = ref(null)
|
||||
|
||||
// 计算属性
|
||||
const currentObjects = computed(() => {
|
||||
if (!selectedBucket.value) return []
|
||||
return objectsData[selectedBucket.value] || []
|
||||
})
|
||||
|
||||
const currentMetadata = computed(() => {
|
||||
if (!selectedObject.value) return null
|
||||
return metadataData[selectedObject.value] || null
|
||||
})
|
||||
|
||||
// 方法
|
||||
const selectBucket = (name) => {
|
||||
selectedBucket.value = name
|
||||
selectedObject.value = null
|
||||
}
|
||||
|
||||
const selectObject = (obj) => {
|
||||
selectedObject.value = obj.key
|
||||
}
|
||||
|
||||
const getFileIcon = (type) => {
|
||||
if (type.startsWith('image/')) return '🖼️'
|
||||
if (type.startsWith('video/')) return '🎬'
|
||||
if (type.startsWith('audio/')) return '🎵'
|
||||
if (type.includes('pdf')) return '📄'
|
||||
if (type.includes('zip') || type.includes('gzip')) return '📦'
|
||||
return '📄'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.object-storage-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.storage-architecture {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.account-layer {
|
||||
background: linear-gradient(135deg, #e0e7ff, #c7d2fe);
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
border: 2px solid #6366f1;
|
||||
}
|
||||
|
||||
.account-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.account-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: #4338ca;
|
||||
}
|
||||
|
||||
.account-desc {
|
||||
font-size: 0.75rem;
|
||||
color: #6366f1;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.connector {
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.buckets-container {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
font-size: 0.75rem;
|
||||
font-weight: normal;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.buckets-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.bucket-card {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.bucket-card:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.bucket-card.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
box-shadow: 0 0 0 3px var(--vp-c-brand-dimm);
|
||||
}
|
||||
|
||||
.bucket-icon {
|
||||
font-size: 1.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.bucket-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-1);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.bucket-meta {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.bucket-size {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.objects-container {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.objects-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.object-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.object-item:hover {
|
||||
background: var(--vp-c-bg-mute);
|
||||
}
|
||||
|
||||
.object-item.selected {
|
||||
background: var(--vp-c-brand-soft);
|
||||
border: 1px solid var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.object-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.object-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.object-key {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.object-meta {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.object-arrow {
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.objects-placeholder {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.metadata-container {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.metadata-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.metadata-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.metadata-section {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.metadata-section-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-brand);
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.metadata-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.metadata-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.metadata-key {
|
||||
color: var(--vp-c-text-2);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.metadata-value {
|
||||
color: var(--vp-c-text-1);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.metadata-placeholder {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.architecture-summary {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 10px;
|
||||
padding: 1.25rem;
|
||||
margin-top: 1.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.summary-title {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.summary-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.summary-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.summary-text strong {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.summary-text span {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="demo-container">
|
||||
<div class="demo-header">
|
||||
<h4>{{ title }}</h4>
|
||||
<p class="hint">{{ description }}</p>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<el-alert type="info" :closable="false">
|
||||
流量调度演示组件占位符 - 待实现具体交互
|
||||
</el-alert>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const title = ref('流量调度演示')
|
||||
const description = ref('展示CDN的智能流量调度机制,包括负载均衡、就近访问、故障切换等')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,612 @@
|
||||
<!--
|
||||
UploadProcessDemo.vue
|
||||
上传流程演示 - 展示直传、分片、断点续传等上传方式
|
||||
-->
|
||||
<template>
|
||||
<div class="upload-process-demo">
|
||||
<div class="header">
|
||||
<div class="title">文件上传流程</div>
|
||||
<div class="subtitle">直传 vs 分片上传 vs 断点续传</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传方式选择 -->
|
||||
<div class="upload-methods">
|
||||
<div
|
||||
v-for="method in uploadMethods"
|
||||
:key="method.id"
|
||||
class="method-card"
|
||||
:class="{ active: selectedMethod === method.id }"
|
||||
@click="selectMethod(method.id)"
|
||||
>
|
||||
<div class="method-icon">{{ method.icon }}</div>
|
||||
<div class="method-name">{{ method.name }}</div>
|
||||
<div class="method-desc">{{ method.description }}</div>
|
||||
<div class="method-size">适合: {{ method.suitable }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传流程可视化 -->
|
||||
<div class="upload-flow">
|
||||
<div class="flow-title">
|
||||
<span v-if="selectedMethod === 'direct'">🚀 直传流程</span>
|
||||
<span v-else-if="selectedMethod === 'multipart'">🔪 分片上传流程</span>
|
||||
<span v-else>💾 断点续传流程</span>
|
||||
</div>
|
||||
|
||||
<!-- 直传流程 -->
|
||||
<div v-if="selectedMethod === 'direct'" class="flow-steps">
|
||||
<div class="flow-step" :class="{ active: currentStep >= 1 }">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">用户选择文件</div>
|
||||
<div class="step-detail">浏览器选择 5MB 图片文件</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-arrow">⬇️</div>
|
||||
<div class="flow-step" :class="{ active: currentStep >= 2 }">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">申请上传凭证</div>
|
||||
<div class="step-detail">前端 → 后端 → STS 临时凭证</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-arrow">⬇️</div>
|
||||
<div class="flow-step" :class="{ active: currentStep >= 3 }">
|
||||
<div class="step-num">3</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">直传到对象存储</div>
|
||||
<div class="step-detail">浏览器 → OSS/COS(5MB 一次性上传)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-arrow">⬇️</div>
|
||||
<div class="flow-step" :class="{ active: currentStep >= 4 }">
|
||||
<div class="step-num">4</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">上传完成</div>
|
||||
<div class="step-detail">返回 URL,前端通知后端保存记录</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分片上传流程 -->
|
||||
<div v-else-if="selectedMethod === 'multipart'" class="flow-steps multipart-flow">
|
||||
<div class="flow-step" :class="{ active: currentStep >= 1 }">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">文件分片</div>
|
||||
<div class="step-detail">500MB 视频 → 50个 10MB 分片</div>
|
||||
<div class="chunks-preview">
|
||||
<div v-for="i in 10" :key="i" class="chunk" :class="{ uploaded: i <= 3 }">{{ i }}</div>
|
||||
<span class="chunks-more">...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-arrow">⬇️</div>
|
||||
<div class="flow-step" :class="{ active: currentStep >= 2 }">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">初始化分片上传</div>
|
||||
<div class="step-detail">获取 uploadId(上传会话 ID)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-arrow">⬇️</div>
|
||||
<div class="flow-step" :class="{ active: currentStep >= 3 }">
|
||||
<div class="step-num">3</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">并行上传分片</div>
|
||||
<div class="step-detail">3 个并发,每片 10MB</div>
|
||||
<div class="parallel-upload">
|
||||
<div class="upload-slot" :class="{ active: parallelActive >= 1 }">分片 1</div>
|
||||
<div class="upload-slot" :class="{ active: parallelActive >= 2 }">分片 2</div>
|
||||
<div class="upload-slot" :class="{ active: parallelActive >= 3 }">分片 3</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-arrow">⬇️</div>
|
||||
<div class="flow-step" :class="{ active: currentStep >= 4 }">
|
||||
<div class="step-num">4</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">合并分片</div>
|
||||
<div class="step-detail">服务端合并所有分片为完整文件</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 断点续传流程 -->
|
||||
<div v-else class="flow-steps resume-flow">
|
||||
<div class="flow-step" :class="{ active: currentStep >= 1 }">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">开始上传 1GB 视频</div>
|
||||
<div class="step-detail">已上传 6 个分片(60MB),正在上传第 7 个</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: 6%;"></div>
|
||||
<div class="progress-text">6% (60MB / 1GB)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-arrow">⬇️</div>
|
||||
<div class="flow-step error-step" :class="{ active: currentStep >= 2 }">
|
||||
<div class="step-num">⚠️</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">网络中断!</div>
|
||||
<div class="step-detail">WiFi 切换到 4G,上传中断,第 7 个分片上传失败</div>
|
||||
<div class="error-info">
|
||||
<span>❌ Error: ETIMEDOUT</span>
|
||||
<span>已上传分片: 6/100</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-arrow">⬇️</div>
|
||||
<div class="flow-step" :class="{ active: currentStep >= 3 }">
|
||||
<div class="step-num">3</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">查询已上传分片</div>
|
||||
<div class="step-detail">恢复网络后,查询服务端已保存的分片列表</div>
|
||||
<div class="resume-info">
|
||||
<div class="resume-item success">
|
||||
<span>✅ 分片 1-6</span>
|
||||
<span>已上传</span>
|
||||
</div>
|
||||
<div class="resume-item pending">
|
||||
<span>⏳ 分片 7-100</span>
|
||||
<span>待上传</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-arrow">⬇️</div>
|
||||
<div class="flow-step" :class="{ active: currentStep >= 4 }">
|
||||
<div class="step-num">4</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">断点续传成功!</div>
|
||||
<div class="step-detail">从第 7 个分片继续上传,无需重传前 6 个分片</div>
|
||||
<div class="success-info">
|
||||
<div class="success-item">
|
||||
<span>💾 节省流量</span>
|
||||
<span>60MB</span>
|
||||
</div>
|
||||
<div class="success-item">
|
||||
<span>⏱️ 节省时间</span>
|
||||
<span>~6s</span>
|
||||
</div>
|
||||
<div class="success-item">
|
||||
<span>🎯 续传进度</span>
|
||||
<span>6% → 100%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// 上传方式数据
|
||||
const uploadMethods = [
|
||||
{
|
||||
id: 'direct',
|
||||
name: '直传',
|
||||
icon: '🚀',
|
||||
description: '小文件一次性上传到对象存储',
|
||||
suitable: '< 100MB'
|
||||
},
|
||||
{
|
||||
id: 'multipart',
|
||||
name: '分片上传',
|
||||
icon: '🔪',
|
||||
description: '大文件切分多片并行上传',
|
||||
suitable: '> 100MB'
|
||||
},
|
||||
{
|
||||
id: 'resume',
|
||||
name: '断点续传',
|
||||
icon: '💾',
|
||||
description: '网络中断后从断点继续上传',
|
||||
suitable: '任何大小'
|
||||
}
|
||||
]
|
||||
|
||||
// 状态
|
||||
const selectedMethod = ref('direct')
|
||||
const currentStep = ref(0)
|
||||
const parallelActive = ref(0)
|
||||
const stats = ref({
|
||||
uploadedChunks: 3,
|
||||
totalChunks: 50,
|
||||
uploadedSize: '60MB',
|
||||
totalSize: '1GB',
|
||||
progress: 6
|
||||
})
|
||||
|
||||
// 方法
|
||||
const selectMethod = (id) => {
|
||||
selectedMethod.value = id
|
||||
resetDemo()
|
||||
}
|
||||
|
||||
const simulateCacheHit = () => {
|
||||
resetDemo()
|
||||
currentStep.value = 4
|
||||
}
|
||||
|
||||
const simulateCacheMiss = () => {
|
||||
resetDemo()
|
||||
currentStep.value = 4
|
||||
}
|
||||
|
||||
const resetDemo = () => {
|
||||
currentStep.value = 0
|
||||
parallelActive.value = 0
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const uploadProgress = computed(() => {
|
||||
return Math.round((stats.value.uploadedChunks / stats.value.totalChunks) * 100)
|
||||
})
|
||||
|
||||
// 方法
|
||||
const selectMethod = (id) => {
|
||||
selectedMethod.value = id
|
||||
resetDemo()
|
||||
}
|
||||
|
||||
const simulateCacheHit = () => {
|
||||
resetDemo()
|
||||
currentStep.value = 4
|
||||
}
|
||||
|
||||
const simulateCacheMiss = () => {
|
||||
resetDemo()
|
||||
currentStep.value = 4
|
||||
}
|
||||
|
||||
const resetDemo = () => {
|
||||
currentStep.value = 0
|
||||
parallelActive.value = 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.upload-process-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.upload-methods {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.upload-methods {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.method-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.method-card:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.method-card.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
box-shadow: 0 0 0 3px var(--vp-c-brand-dimm);
|
||||
}
|
||||
|
||||
.method-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.method-name {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.method-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.method-size {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
background: var(--vp-c-brand-soft);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.upload-flow {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.flow-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.flow-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid var(--vp-c-divider);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.flow-step.active {
|
||||
border-left-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.flow-step.error-step {
|
||||
background: #fef2f2;
|
||||
border-left-color: #dc2626;
|
||||
}
|
||||
|
||||
.step-num {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.flow-step.error-step .step-num {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.step-detail {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.flow-arrow {
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* 分片预览 */
|
||||
.chunks-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.chunk {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.chunk.uploaded {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chunks-more {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
/* 并行上传 */
|
||||
.parallel-upload {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.upload-slot {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.upload-slot.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 进度条 */
|
||||
.progress-bar {
|
||||
position: relative;
|
||||
height: 24px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--vp-c-brand), var(--vp-c-brand-light));
|
||||
border-radius: 12px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* 错误信息 */
|
||||
.error-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #dc2626;
|
||||
}
|
||||
|
||||
.error-info span {
|
||||
font-size: 0.75rem;
|
||||
color: #dc2626;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
/* 恢复信息 */
|
||||
.resume-info {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.resume-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.4rem 0.6rem;
|
||||
margin-bottom: 0.25rem;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.resume-item.success {
|
||||
border-left: 3px solid #22c55e;
|
||||
}
|
||||
|
||||
.resume-item.success span:first-child {
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.resume-item.pending {
|
||||
border-left: 3px solid #f59e0b;
|
||||
}
|
||||
|
||||
.resume-item.pending span:first-child {
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.resume-item span:last-child {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
/* 成功信息 */
|
||||
.success-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.success-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #bbf7d0;
|
||||
}
|
||||
|
||||
.success-item span:first-child {
|
||||
font-size: 0.7rem;
|
||||
color: #166534;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.success-item span:last-child {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: #16a34a;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user