feat: update docs and components, fix DLQ demo bug
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
<!--
|
||||
DataCollectionDemo.vue
|
||||
数据采集方案对比 - 客户端、服务端、CDN日志采集
|
||||
-->
|
||||
<template>
|
||||
<div class="data-collection-demo">
|
||||
<div class="header">
|
||||
<div class="title">数据采集方案</div>
|
||||
<div class="subtitle">客户端、服务端、CDN三种采集方式对比</div>
|
||||
</div>
|
||||
|
||||
<div class="collection-methods">
|
||||
<div
|
||||
v-for="method in methods"
|
||||
:key="method.id"
|
||||
class="method-card"
|
||||
@click="selectedMethod = method.id"
|
||||
:class="{ active: selectedMethod === method.id }"
|
||||
>
|
||||
<div class="method-icon">{{ method.icon }}</div>
|
||||
<div class="method-name">{{ method.name }}</div>
|
||||
<div class="method-desc">{{ method.desc }}</div>
|
||||
|
||||
<div class="method-details" v-if="selectedMethod === method.id">
|
||||
<div class="detail-section">
|
||||
<div class="section-title">✅ 优点</div>
|
||||
<ul class="detail-list">
|
||||
<li v-for="(pro, i) in method.pros" :key="i">{{ pro }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="section-title">❌ 缺点</div>
|
||||
<ul class="detail-list">
|
||||
<li v-for="(con, i) in method.cons" :key="i">{{ con }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="section-title">🎯 适用场景</div>
|
||||
<ul class="detail-list">
|
||||
<li v-for="(use, i) in method.useCases" :key="i">{{ use }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-table">
|
||||
<div class="table-title">方案对比</div>
|
||||
<table class="comparison">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>对比维度</th>
|
||||
<th v-for="method in methods" :key="method.id">
|
||||
{{ method.name }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>数据准确性</td>
|
||||
<td v-for="method in methods" :key="method.id">
|
||||
{{ method.accuracy }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>实时性</td>
|
||||
<td v-for="method in methods" :key="method.id">
|
||||
{{ method.realtime }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>开发成本</td>
|
||||
<td v-for="method in methods" :key="method.id">
|
||||
{{ method.cost }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>维护成本</td>
|
||||
<td v-for="method in methods" :key="method.id">
|
||||
{{ method.maintenance }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const selectedMethod = ref('client')
|
||||
|
||||
const methods = [
|
||||
{
|
||||
id: 'client',
|
||||
name: '客户端埋点',
|
||||
icon: '📱',
|
||||
desc: '在 Web、App 前端代码中集成埋点 SDK',
|
||||
pros: ['实时性好', '可采集设备信息', '离线缓存'],
|
||||
cons: ['数据可能被篡改', '耗电流量', 'App 崩溃可能丢失'],
|
||||
useCases: ['页面浏览', '按钮点击', '表单提交'],
|
||||
accuracy: '★★★☆☆',
|
||||
realtime: '★★★★★',
|
||||
cost: '★★★☆☆',
|
||||
maintenance: '★★★☆☆'
|
||||
},
|
||||
{
|
||||
id: 'server',
|
||||
name: '服务端埋点',
|
||||
icon: '⚙️',
|
||||
desc: '在服务器端业务逻辑中添加埋点代码',
|
||||
pros: ['数据准确', '不可篡改', '采集服务端特有数据'],
|
||||
cons: ['无法获取客户端信息', '需要业务代码侵入'],
|
||||
useCases: ['支付成功', '订单创建', 'API 调用'],
|
||||
accuracy: '★★★★★',
|
||||
realtime: '★★★★☆',
|
||||
cost: '★★★☆☆',
|
||||
maintenance: '★★★☆☆'
|
||||
},
|
||||
{
|
||||
id: 'cdn',
|
||||
name: 'CDN 日志采集',
|
||||
icon: '🌐',
|
||||
desc: '通过 CDN 访问日志分析用户行为',
|
||||
pros: ['零代码侵入', '覆盖所有用户', '成本低'],
|
||||
cons: ['数据维度有限', '无法获取业务数据'],
|
||||
useCases: ['PV/UV 统计', '资源加载性能', '错误监控'],
|
||||
accuracy: '★★★☆☆',
|
||||
realtime: '★★★☆☆',
|
||||
cost: '★★★★★',
|
||||
maintenance: '★★★★★'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.data-collection-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin: 2rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.collection-methods {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.method-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.method-card:hover,
|
||||
.method-card.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.method-icon {
|
||||
font-size: 3rem;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.method-name {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
text-align: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.method-desc {
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.method-details {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.detail-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.detail-list li {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25rem 0;
|
||||
padding-left: 1.25rem;
|
||||
position: relative;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.detail-list li::before {
|
||||
content: '•';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.comparison-table {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.table-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.comparison {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.comparison th {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.comparison td {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.comparison td:first-child {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.collection-methods {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,906 @@
|
||||
<!--
|
||||
DataModelDesignDemo.vue
|
||||
数据模型设计 - 事件、用户、会话模型
|
||||
-->
|
||||
<template>
|
||||
<div class="data-model-design-demo">
|
||||
<div class="header">
|
||||
<div class="title">数据模型设计</div>
|
||||
<div class="subtitle">埋点数据的核心三要素:事件、用户、会话</div>
|
||||
</div>
|
||||
|
||||
<div class="model-tabs">
|
||||
<button
|
||||
v-for="model in models"
|
||||
:key="model.id"
|
||||
class="model-tab"
|
||||
:class="{ active: selectedModel === model.id }"
|
||||
@click="selectModel(model.id)"
|
||||
>
|
||||
{{ model.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="model-content">
|
||||
<!-- 事件模型 -->
|
||||
<div v-if="selectedModel === 'event'" class="model-detail">
|
||||
<div class="model-intro">
|
||||
<div class="intro-icon">📊</div>
|
||||
<div class="intro-text">
|
||||
<div class="intro-title">事件模型 (Event Model)</div>
|
||||
<div class="intro-desc">
|
||||
一个事件 = 用户的一次行为动作,是埋点系统中最基本的数据单元
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="event-naming">
|
||||
<div class="section-title">命名规范</div>
|
||||
<div class="naming-rules">
|
||||
<div class="rule-item good">
|
||||
<div class="rule-label">✅ 好的命名</div>
|
||||
<div class="rule-examples">
|
||||
<code>click_button</code>
|
||||
<code>view_page</code>
|
||||
<code>add_to_cart</code>
|
||||
<code>submit_form</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rule-item bad">
|
||||
<div class="rule-label">❌ 不好的命名</div>
|
||||
<div class="rule-examples">
|
||||
<code>button_click</code>
|
||||
<code>page_view</code>
|
||||
<code>cart_add</code>
|
||||
<code>form_submit</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="naming-tip">💡 原则:动词在前,名词在后,简洁明确</div>
|
||||
</div>
|
||||
|
||||
<div class="event-structure">
|
||||
<div class="section-title">事件数据结构</div>
|
||||
<div class="code-example">
|
||||
<pre><code>{
|
||||
<span class="key">"event"</span>: <span class="string">"click_button"</span>,
|
||||
<span class="key">"timestamp"</span>: <span class="number">1704067200000</span>,
|
||||
|
||||
<span class="comment">// 公共属性 (SDK 自动采集)</span>
|
||||
<span class="key">"common"</span>: {
|
||||
<span class="key">"platform"</span>: <span class="string">"iOS"</span>,
|
||||
<span class="key">"app_version"</span>: <span class="string">"1.2.3"</span>,
|
||||
<span class="key">"device_id"</span>: <span class="string">"device_123"</span>,
|
||||
<span class="key">"network"</span>: <span class="string">"WiFi"</span>
|
||||
},
|
||||
|
||||
<span class="comment">// 自定义属性 (业务数据)</span>
|
||||
<span class="key">"properties"</span>: {
|
||||
<span class="key">"button_name"</span>: <span class="string">"立即购买"</span>,
|
||||
<span class="key">"page"</span>: <span class="string">"商品详情页"</span>,
|
||||
<span class="key">"product_id"</span>: <span class="string">"prod_98765"</span>,
|
||||
<span class="key">"price"</span>: <span class="number">299.00</span>
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="event-best-practices">
|
||||
<div class="section-title">最佳实践</div>
|
||||
<div class="practices-grid">
|
||||
<div class="practice-item">
|
||||
<div class="practice-icon">🎯</div>
|
||||
<div class="practice-content">
|
||||
<div class="practice-title">明确事件目标</div>
|
||||
<div class="practice-desc">
|
||||
每个事件都应有明确的业务分析目标
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="practice-item">
|
||||
<div class="practice-icon">📝</div>
|
||||
<div class="practice-content">
|
||||
<div class="practice-title">属性完整丰富</div>
|
||||
<div class="practice-desc">包含所有可能影响业务决策的维度</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="practice-item">
|
||||
<div class="practice-icon">🔄</div>
|
||||
<div class="practice-content">
|
||||
<div class="practice-title">保持命名一致</div>
|
||||
<div class="practice-desc">同一类型事件使用统一的命名规范</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="practice-item">
|
||||
<div class="practice-icon">🚫</div>
|
||||
<div class="practice-content">
|
||||
<div class="practice-title">避免过度采集</div>
|
||||
<div class="practice-desc">只采集必要数据,减少隐私风险</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户模型 -->
|
||||
<div v-if="selectedModel === 'user'" class="model-detail">
|
||||
<div class="model-intro">
|
||||
<div class="intro-icon">👤</div>
|
||||
<div class="intro-text">
|
||||
<div class="intro-title">用户模型 (User Model)</div>
|
||||
<div class="intro-desc">
|
||||
跨设备关联用户身份,实现用户全生命周期管理
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-identity">
|
||||
<div class="section-title">身份识别体系</div>
|
||||
<div class="identity-types">
|
||||
<div class="identity-card primary">
|
||||
<div class="identity-header">
|
||||
<div class="identity-icon">🆔</div>
|
||||
<div class="identity-name">user_id</div>
|
||||
</div>
|
||||
<div class="identity-info">
|
||||
<div class="info-row">
|
||||
<span class="label">稳定性:</span>
|
||||
<span class="value high">极高</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">来源:</span>
|
||||
<span class="value">后端分配</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">用途:</span>
|
||||
<span class="value">跨设备关联</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="identity-card secondary">
|
||||
<div class="identity-header">
|
||||
<div class="identity-icon">📱</div>
|
||||
<div class="identity-name">device_id</div>
|
||||
</div>
|
||||
<div class="identity-info">
|
||||
<div class="info-row">
|
||||
<span class="label">稳定性:</span>
|
||||
<span class="value medium">高</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">来源:</span>
|
||||
<span class="value">设备指纹</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">用途:</span>
|
||||
<span class="value">匿名用户分析</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="identity-card tertiary">
|
||||
<div class="identity-header">
|
||||
<div class="identity-icon">🌐</div>
|
||||
<div class="identity-name">session_id</div>
|
||||
</div>
|
||||
<div class="identity-info">
|
||||
<div class="info-row">
|
||||
<span class="label">稳定性:</span>
|
||||
<span class="value low">低</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">来源:</span>
|
||||
<span class="value">会话生成</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">用途:</span>
|
||||
<span class="value">单次会话分析</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="id-mapping">
|
||||
<div class="section-title">ID Mapping(身份打通)</div>
|
||||
<div class="mapping-flow">
|
||||
<div class="mapping-step">
|
||||
<div class="step-title">注册前(匿名)</div>
|
||||
<div class="step-code">
|
||||
<code>device_id: "device_123"</code><br />
|
||||
<code>user_id: null</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mapping-arrow">→</div>
|
||||
<div class="mapping-step">
|
||||
<div class="step-title">注册后(登录)</div>
|
||||
<div class="step-code">
|
||||
<code>device_id: "device_123"</code><br />
|
||||
<code>user_id: "user_456"</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mapping-arrow">→</div>
|
||||
<div class="mapping-step">
|
||||
<div class="step-title">数据分析</div>
|
||||
<div class="step-desc">
|
||||
通过 device_id 关联<br />
|
||||
用户注册前后行为
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-profile">
|
||||
<div class="section-title">用户画像维度</div>
|
||||
<div class="profile-dimensions">
|
||||
<div class="dimension-group">
|
||||
<div class="group-title">基础属性</div>
|
||||
<div class="dimension-list">
|
||||
<span class="dimension-tag">性别</span>
|
||||
<span class="dimension-tag">年龄</span>
|
||||
<span class="dimension-tag">地域</span>
|
||||
<span class="dimension-tag">语言</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dimension-group">
|
||||
<div class="group-title">行为特征</div>
|
||||
<div class="dimension-list">
|
||||
<span class="dimension-tag">活跃度</span>
|
||||
<span class="dimension-tag">偏好</span>
|
||||
<span class="dimension-tag">购买力</span>
|
||||
<span class="dimension-tag">生命周期</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dimension-group">
|
||||
<div class="group-title">设备信息</div>
|
||||
<div class="dimension-list">
|
||||
<span class="dimension-tag">平台</span>
|
||||
<span class="dimension-tag">操作系统</span>
|
||||
<span class="dimension-tag">分辨率</span>
|
||||
<span class="dimension-tag">运营商</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 会话模型 -->
|
||||
<div v-if="selectedModel === 'session'" class="model-detail">
|
||||
<div class="model-intro">
|
||||
<div class="intro-icon">⏱️</div>
|
||||
<div class="intro-text">
|
||||
<div class="intro-title">会话模型 (Session Model)</div>
|
||||
<div class="intro-desc">
|
||||
用户一次连续的使用过程,用于分析用户粘性和转化
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="session-definition">
|
||||
<div class="section-title">会话定义</div>
|
||||
<div class="session-rules">
|
||||
<div class="rule-card web">
|
||||
<div class="rule-icon">🌐</div>
|
||||
<div class="rule-content">
|
||||
<div class="rule-title">Web 会话</div>
|
||||
<div class="rule-desc">连续浏览,无操作超过 30 分钟则结束</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rule-card app">
|
||||
<div class="rule-icon">📱</div>
|
||||
<div class="rule-content">
|
||||
<div class="rule-title">App 会话</div>
|
||||
<div class="rule-desc">
|
||||
App 从后台回到前台,超过 5 分钟则新会话
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="session-metrics">
|
||||
<div class="section-title">会话关键指标</div>
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon">📊</div>
|
||||
<div class="metric-name">会话时长</div>
|
||||
<div class="metric-value">8m 32s</div>
|
||||
<div class="metric-desc">用户平均使用时长</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon">📄</div>
|
||||
<div class="metric-name">会话深度</div>
|
||||
<div class="metric-value">12.5</div>
|
||||
<div class="metric-desc">平均浏览页面数</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon">🔄</div>
|
||||
<div class="metric-name">会话频率</div>
|
||||
<div class="metric-value">3.2/天</div>
|
||||
<div class="metric-desc">日均打开次数</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon">💼</div>
|
||||
<div class="metric-name">跳出率</div>
|
||||
<div class="metric-value">35.2%</div>
|
||||
<div class="metric-desc">单页面跳出比例</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="session-use-cases">
|
||||
<div class="section-title">典型应用场景</div>
|
||||
<div class="use-case-list">
|
||||
<div class="use-case-item">
|
||||
<div class="use-case-number">1</div>
|
||||
<div class="use-case-content">
|
||||
<div class="use-case-title">转化漏斗分析</div>
|
||||
<div class="use-case-desc">
|
||||
分析用户从进入到完成目标的转化路径,识别流失环节
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="use-case-item">
|
||||
<div class="use-case-number">2</div>
|
||||
<div class="use-case-content">
|
||||
<div class="use-case-title">用户粘性分析</div>
|
||||
<div class="use-case-desc">
|
||||
通过会话时长和频次,评估产品吸引力和用户忠诚度
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="use-case-item">
|
||||
<div class="use-case-number">3</div>
|
||||
<div class="use-case-content">
|
||||
<div class="use-case-title">用户分群</div>
|
||||
<div class="use-case-desc">
|
||||
基于会话行为特征,将用户分为高价值、流失风险等群体
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const selectedModel = ref('event')
|
||||
|
||||
const models = [
|
||||
{ id: 'event', name: '事件模型' },
|
||||
{ id: 'user', name: '用户模型' },
|
||||
{ id: 'session', name: '会话模型' }
|
||||
]
|
||||
|
||||
const selectModel = (modelId) => {
|
||||
selectedModel.value = modelId
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.data-model-design-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin: 2rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.model-tabs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.model-tab {
|
||||
padding: 0.75rem 2rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.model-tab:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.model-tab.active {
|
||||
background: var(--vp-c-brand);
|
||||
border-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.model-content {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.model-intro {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.intro-icon {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.intro-title {
|
||||
font-weight: 700;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.intro-desc {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
/* 事件模型样式 */
|
||||
.event-naming {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.naming-rules {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.rule-item {
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.rule-item.good {
|
||||
background: #dcfce7;
|
||||
border: 2px solid #22c55e;
|
||||
}
|
||||
|
||||
.rule-item.bad {
|
||||
background: #fee2e2;
|
||||
border: 2px solid #ef4444;
|
||||
}
|
||||
|
||||
.rule-label {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.rule-examples {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.rule-examples code {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.naming-tip {
|
||||
text-align: center;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.event-structure {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.code-example {
|
||||
background: #1e1e1e;
|
||||
border-radius: 10px;
|
||||
padding: 1.5rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-example pre {
|
||||
margin: 0;
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.key {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
.string {
|
||||
color: #ce9178;
|
||||
}
|
||||
|
||||
.number {
|
||||
color: #b5cea8;
|
||||
}
|
||||
|
||||
.comment {
|
||||
color: #6a9955;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.event-best-practices {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.practices-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.practice-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.practice-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.practice-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.practice-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 用户模型样式 */
|
||||
.user-identity {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.identity-types {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.identity-card {
|
||||
padding: 1.25rem;
|
||||
border-radius: 12px;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
.identity-card.primary {
|
||||
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.identity-card.secondary {
|
||||
background: linear-gradient(135deg, #fef3c7, #fde68a);
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
.identity-card.tertiary {
|
||||
background: linear-gradient(135deg, #dcfce7, #bbf7d0);
|
||||
border-color: #22c55e;
|
||||
}
|
||||
|
||||
.identity-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.identity-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.identity-name {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.identity-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.info-row .label {
|
||||
color: var(--vp-c-text-2);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.value.high {
|
||||
color: #22c55e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.value.medium {
|
||||
color: #f59e0b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.value.low {
|
||||
color: #ef4444;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.id-mapping {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.mapping-flow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.mapping-step {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.step-code {
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.6;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.mapping-arrow {
|
||||
font-size: 1.5rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.user-profile {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.profile-dimensions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.dimension-group {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.dimension-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.dimension-tag {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: white;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 会话模型样式 */
|
||||
.session-definition {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.session-rules {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.rule-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1.25rem;
|
||||
border-radius: 10px;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
.rule-card.web {
|
||||
background: linear-gradient(135deg, #e0e7ff, #c7d2fe);
|
||||
border-color: #6366f1;
|
||||
}
|
||||
|
||||
.rule-card.app {
|
||||
background: linear-gradient(135deg, #fce7f3, #fbcfe8);
|
||||
border-color: #ec4899;
|
||||
}
|
||||
|
||||
.rule-icon {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.rule-title {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.rule-desc {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.session-metrics {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
text-align: center;
|
||||
padding: 1.25rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.metric-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.metric-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.metric-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.session-use-cases {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.use-case-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.use-case-item {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.use-case-number {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.use-case-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.use-case-desc {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.naming-rules,
|
||||
.practices-grid,
|
||||
.identity-types,
|
||||
.mapping-flow,
|
||||
.session-rules,
|
||||
.metrics-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.mapping-flow {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,579 @@
|
||||
<!--
|
||||
DataPipelineDemo.vue
|
||||
数据处理管道 - 展示数据从采集到分析的完整流程
|
||||
-->
|
||||
<template>
|
||||
<div class="data-pipeline-demo">
|
||||
<div class="header">
|
||||
<div class="title">数据处理管道</div>
|
||||
<div class="subtitle">从用户行为到数据洞察的完整链路</div>
|
||||
</div>
|
||||
|
||||
<div class="pipeline-container">
|
||||
<div class="pipeline-flow">
|
||||
<div
|
||||
v-for="(step, index) in pipelineSteps"
|
||||
:key="step.id"
|
||||
class="pipeline-step"
|
||||
:class="{
|
||||
active: currentStep === index,
|
||||
completed: currentStep > index
|
||||
}"
|
||||
>
|
||||
<div class="step-header">
|
||||
<div class="step-number">{{ index + 1 }}</div>
|
||||
<div class="step-info">
|
||||
<div class="step-name">{{ step.name }}</div>
|
||||
<div class="step-icon">{{ step.icon }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-content">
|
||||
<div class="step-description">{{ step.description }}</div>
|
||||
|
||||
<div class="step-details">
|
||||
<div v-if="step.technologies" class="technologies">
|
||||
<div class="tech-label">技术栈:</div>
|
||||
<div class="tech-list">
|
||||
<span
|
||||
v-for="(tech, i) in step.technologies"
|
||||
:key="i"
|
||||
class="tech-tag"
|
||||
>
|
||||
{{ tech }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="step.metrics" class="metrics">
|
||||
<div
|
||||
class="metric"
|
||||
v-for="(metric, i) in step.metrics"
|
||||
:key="i"
|
||||
>
|
||||
<div class="metric-value">{{ metric.value }}</div>
|
||||
<div class="metric-label">{{ metric.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="index < pipelineSteps.length - 1" class="step-connector">
|
||||
<div class="connector-line"></div>
|
||||
<div class="connector-arrow">↓</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="play-controls">
|
||||
<button class="control-btn" @click="startAnimation" :disabled="isPlaying">
|
||||
<span v-if="!isPlaying">▶️ 演示数据流</span>
|
||||
<span v-else>⏸️ 演示中...</span>
|
||||
</button>
|
||||
<button class="control-btn secondary" @click="resetAnimation">
|
||||
🔄 重置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="data-flow-visualization">
|
||||
<div class="flow-title">实时数据流</div>
|
||||
<div class="flow-cards">
|
||||
<div class="flow-card" v-for="(item, index) in dataFlow" :key="index">
|
||||
<div class="flow-icon">{{ item.icon }}</div>
|
||||
<div class="flow-content">
|
||||
<div class="flow-name">{{ item.name }}</div>
|
||||
<div class="flow-count">
|
||||
{{ formatNumber(item.count) }} {{ item.unit }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="best-practices">
|
||||
<div class="practices-title">💡 数据管道最佳实践</div>
|
||||
<div class="practices-grid">
|
||||
<div class="practice-card">
|
||||
<div class="practice-icon">🔄</div>
|
||||
<div class="practice-content">
|
||||
<div class="practice-name">批量处理</div>
|
||||
<div class="practice-desc">
|
||||
将小数据包合并成大数据块处理,减少 I/O 开销,提升吞吐量
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="practice-card">
|
||||
<div class="practice-icon">⚡</div>
|
||||
<div class="practice-content">
|
||||
<div class="practice-name">异步非阻塞</div>
|
||||
<div class="practice-desc">
|
||||
使用消息队列和异步任务,避免阻塞主业务流程
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="practice-card">
|
||||
<div class="practice-icon">🛡️</div>
|
||||
<div class="practice-content">
|
||||
<div class="practice-name">容错机制</div>
|
||||
<div class="practice-desc">
|
||||
失败重试、死信队列、降级策略,确保数据不丢失
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="practice-card">
|
||||
<div class="practice-icon">📊</div>
|
||||
<div class="practice-content">
|
||||
<div class="practice-name">监控告警</div>
|
||||
<div class="practice-desc">
|
||||
实时监控数据量、延迟、错误率,异常及时告警
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const currentStep = ref(-1)
|
||||
const isPlaying = ref(false)
|
||||
|
||||
const pipelineSteps = [
|
||||
{
|
||||
id: 'collection',
|
||||
name: '数据采集',
|
||||
icon: '📡',
|
||||
description: '客户端 SDK、后端埋点代码、CDN 日志采集用户行为数据',
|
||||
technologies: ['JavaScript SDK', 'Python SDK', 'CDN Logs', 'Webhook'],
|
||||
metrics: [
|
||||
{ label: '采集量', value: '10M+/天' },
|
||||
{ label: '成功率', value: '99.9%' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'transmission',
|
||||
name: '数据传输',
|
||||
icon: '🚚',
|
||||
description: '加密上报、批量传输、断点续传,确保数据安全送达',
|
||||
technologies: ['HTTPS', 'Batch Upload', 'Retry Logic'],
|
||||
metrics: [
|
||||
{ label: '传输量', value: '5GB/天' },
|
||||
{ label: '延迟', value: '<100ms' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'cleaning',
|
||||
name: '数据清洗',
|
||||
icon: '🧹',
|
||||
description: '去重、校验、格式化、补全,确保数据质量',
|
||||
technologies: ['ETL', 'Data Validation', 'Deduplication'],
|
||||
metrics: [
|
||||
{ label: '清洗率', value: '95%' },
|
||||
{ label: '准确率', value: '99.99%' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'storage',
|
||||
name: '数据存储',
|
||||
icon: '🗄️',
|
||||
description: '分层存储:热数据、温数据、冷数据,优化成本',
|
||||
technologies: ['ClickHouse', 'S3', 'Redis', 'Hive'],
|
||||
metrics: [
|
||||
{ label: '存储量', value: '100TB' },
|
||||
{ label: '查询', value: '<1s' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'analysis',
|
||||
name: '数据分析',
|
||||
icon: '📊',
|
||||
description: '可视化报表、用户分群、漏斗分析、归因分析',
|
||||
technologies: ['SQL', 'Python', 'Tableau', 'Metabase'],
|
||||
metrics: [
|
||||
{ label: '报表数', value: '500+' },
|
||||
{ label: '用户', value: '10K+' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const dataFlow = ref([
|
||||
{ icon: '📱', name: '客户端事件', count: 158420, unit: '次/分' },
|
||||
{ icon: '📤', name: '上报请求', count: 15842, unit: '次/分' },
|
||||
{ icon: '✅', name: '成功入库', count: 15840, unit: '条/分' },
|
||||
{ icon: '❌', name: '处理失败', count: 2, unit: '条/分' }
|
||||
])
|
||||
|
||||
let animationInterval = null
|
||||
|
||||
const startAnimation = () => {
|
||||
if (isPlaying.value) return
|
||||
|
||||
isPlaying.value = true
|
||||
currentStep.value = -1
|
||||
|
||||
animationInterval = setInterval(() => {
|
||||
if (currentStep.value < pipelineSteps.length - 1) {
|
||||
currentStep.value++
|
||||
} else {
|
||||
clearInterval(animationInterval)
|
||||
isPlaying.value = false
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const resetAnimation = () => {
|
||||
if (animationInterval) {
|
||||
clearInterval(animationInterval)
|
||||
}
|
||||
currentStep.value = -1
|
||||
isPlaying.value = false
|
||||
}
|
||||
|
||||
const formatNumber = (num) => {
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 模拟实时数据流
|
||||
setInterval(() => {
|
||||
dataFlow.value = dataFlow.value.map((item) => ({
|
||||
...item,
|
||||
count: item.count + Math.floor(Math.random() * 100) - 50
|
||||
}))
|
||||
}, 2000)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.data-pipeline-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin: 2rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.pipeline-container {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.pipeline-flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.pipeline-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
padding: 1.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.pipeline-step.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
box-shadow: 0 0 0 3px rgba(60, 130, 246, 0.1);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.pipeline-step.completed {
|
||||
border-color: #22c55e;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.step-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pipeline-step.completed .step-number {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.step-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.step-name {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.step-description {
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.step-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.technologies {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tech-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.tech-list {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tech-tag {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.metrics {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.metric {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.step-connector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: -0.5rem 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.connector-line {
|
||||
width: 2px;
|
||||
height: 20px;
|
||||
background: var(--vp-c-divider);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.pipeline-step.active ~ .pipeline-step .connector-line,
|
||||
.pipeline-step.completed + .pipeline-step .connector-line {
|
||||
background: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.connector-arrow {
|
||||
font-size: 1.5rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
.play-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
padding: 0.75rem 2rem;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.control-btn:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(60, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.control-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.control-btn.secondary {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.data-flow-visualization {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.flow-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flow-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.flow-card {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.flow-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.flow-name {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.flow-count {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.best-practices {
|
||||
background: linear-gradient(135deg, #fef3c7, #fde68a);
|
||||
border: 2px solid #f59e0b;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.practices-title {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.practices-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.practice-card {
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.practice-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.practice-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.practice-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.flow-cards,
|
||||
.practices-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.metrics {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,833 @@
|
||||
<!--
|
||||
PrivacyComplianceDemo.vue
|
||||
隐私合规演示 - 展示如何实现隐私合规的埋点系统
|
||||
-->
|
||||
<template>
|
||||
<div class="privacy-compliance-demo">
|
||||
<div class="header">
|
||||
<div class="title">隐私合规最佳实践</div>
|
||||
<div class="subtitle">GDPR、PIPL 等法规要求下的埋点系统设计</div>
|
||||
</div>
|
||||
|
||||
<div class="compliance-cards">
|
||||
<div class="compliance-card gdpr">
|
||||
<div class="card-header">
|
||||
<div class="card-icon">🇪🇺</div>
|
||||
<div class="card-title">GDPR</div>
|
||||
<div class="card-subtitle">欧盟数据保护法规</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="requirement-list">
|
||||
<div class="requirement-item">
|
||||
<span class="requirement-icon">✓</span>
|
||||
<span>用户明确同意</span>
|
||||
</div>
|
||||
<div class="requirement-item">
|
||||
<span class="requirement-icon">✓</span>
|
||||
<span>数据可删除</span>
|
||||
</div>
|
||||
<div class="requirement-item">
|
||||
<span class="requirement-icon">✓</span>
|
||||
<span>数据可导出</span>
|
||||
</div>
|
||||
<div class="requirement-item">
|
||||
<span class="requirement-icon">✓</span>
|
||||
<span>数据处理透明化</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="compliance-card pipl">
|
||||
<div class="card-header">
|
||||
<div class="card-icon">🇨🇳</div>
|
||||
<div class="card-title">PIPL</div>
|
||||
<div class="card-subtitle">中国个人信息保护法</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="requirement-list">
|
||||
<div class="requirement-item">
|
||||
<span class="requirement-icon">✓</span>
|
||||
<span>明确告知目的</span>
|
||||
</div>
|
||||
<div class="requirement-item">
|
||||
<span class="requirement-icon">✓</span>
|
||||
<span>最小必要原则</span>
|
||||
</div>
|
||||
<div class="requirement-item">
|
||||
<span class="requirement-icon">✓</span>
|
||||
<span>用户同意</span>
|
||||
</div>
|
||||
<div class="requirement-item">
|
||||
<span class="requirement-icon">✓</span>
|
||||
<span>数据本地化</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="implementation-steps">
|
||||
<div class="steps-title">实施步骤</div>
|
||||
<div class="steps-container">
|
||||
<div class="step-item" v-for="(step, index) in steps" :key="index">
|
||||
<div class="step-number">{{ index + 1 }}</div>
|
||||
<div class="step-content">
|
||||
<div class="step-name">{{ step.name }}</div>
|
||||
<div class="step-desc">{{ step.desc }}</div>
|
||||
<div class="step-code" v-if="step.code">
|
||||
<pre><code>{{ step.code }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="consent-flow-demo">
|
||||
<div class="flow-title">隐私同意流程演示</div>
|
||||
<div class="consent-simulation">
|
||||
<div class="simulation-screen">
|
||||
<div v-if="!userConsented" class="consent-dialog">
|
||||
<div class="dialog-header">
|
||||
<div class="dialog-title">🔐 隐私设置</div>
|
||||
<div class="dialog-subtitle">我们需要您的同意来收集数据</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-body">
|
||||
<div class="consent-item">
|
||||
<div class="consent-info">
|
||||
<div class="consent-name">必要数据</div>
|
||||
<div class="consent-desc">
|
||||
应用程序运行所必需的数据(崩溃日志、性能指标)
|
||||
</div>
|
||||
</div>
|
||||
<div class="consent-status required">必需</div>
|
||||
</div>
|
||||
|
||||
<div class="consent-item">
|
||||
<div class="consent-info">
|
||||
<div class="consent-name">行为分析</div>
|
||||
<div class="consent-desc">
|
||||
收集用户行为数据用于产品优化(页面浏览、按钮点击)
|
||||
</div>
|
||||
</div>
|
||||
<label class="consent-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="consents.analytics"
|
||||
:disabled="!userConsented"
|
||||
/>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="consent-item">
|
||||
<div class="consent-info">
|
||||
<div class="consent-name">个性化推荐</div>
|
||||
<div class="consent-desc">基于您的兴趣提供个性化内容推荐</div>
|
||||
</div>
|
||||
<label class="consent-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="consents.personalization"
|
||||
:disabled="!userConsented"
|
||||
/>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-footer">
|
||||
<button class="dialog-btn secondary" @click="rejectAll">
|
||||
拒绝全部
|
||||
</button>
|
||||
<button class="dialog-btn primary" @click="acceptSelected">
|
||||
接受选中
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="consented-view">
|
||||
<div class="consented-icon">✅</div>
|
||||
<div class="consented-title">感谢您的同意</div>
|
||||
<div class="consented-desc">您已经同意收集以下类型的数据:</div>
|
||||
|
||||
<div class="consented-list">
|
||||
<div class="consented-item" v-if="consents.analytics">
|
||||
<span class="item-icon">📊</span>
|
||||
<span>行为分析数据</span>
|
||||
</div>
|
||||
<div class="consented-item" v-if="consents.personalization">
|
||||
<span class="item-icon">🎯</span>
|
||||
<span>个性化推荐数据</span>
|
||||
</div>
|
||||
<div class="consented-item">
|
||||
<span class="item-icon">🔧</span>
|
||||
<span>必要运行数据</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="consented-actions">
|
||||
<button class="action-btn" @click="changeSettings">
|
||||
修改设置
|
||||
</button>
|
||||
<button class="action-btn danger" @click="deleteData">
|
||||
删除我的数据
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-protection">
|
||||
<div class="protection-title">🛡️ 数据保护措施</div>
|
||||
<div class="protection-grid">
|
||||
<div class="protection-item">
|
||||
<div class="protection-icon">🔒</div>
|
||||
<div class="protection-content">
|
||||
<div class="protection-name">数据加密</div>
|
||||
<div class="protection-desc">
|
||||
传输层 HTTPS 加密,存储层 AES-256 加密
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="protection-item">
|
||||
<div class="protection-icon">🎭</div>
|
||||
<div class="protection-content">
|
||||
<div class="protection-name">数据脱敏</div>
|
||||
<div class="protection-desc">
|
||||
手机号、邮箱等敏感信息自动脱敏处理
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="protection-item">
|
||||
<div class="protection-icon">⏰</div>
|
||||
<div class="protection-content">
|
||||
<div class="protection-name">数据保留期限</div>
|
||||
<div class="protection-desc">
|
||||
不同类型数据设置不同保留期限,自动清理过期数据
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="protection-item">
|
||||
<div class="protection-icon">👤</div>
|
||||
<div class="protection-content">
|
||||
<div class="protection-name">用户控制权</div>
|
||||
<div class="protection-desc">用户可查看、导出、删除自己的数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checklist">
|
||||
<div class="checklist-title">✅ 合规检查清单</div>
|
||||
<div class="checklist-items">
|
||||
<div
|
||||
v-for="(item, index) in checklistItems"
|
||||
:key="index"
|
||||
class="checklist-item"
|
||||
:class="{ checked: item.checked }"
|
||||
@click="toggleCheck(index)"
|
||||
>
|
||||
<span class="checklist-icon">{{ item.checked ? '✅' : '⬜' }}</span>
|
||||
<span class="checklist-text">{{ item.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const userConsented = ref(false)
|
||||
const consents = ref({
|
||||
analytics: false,
|
||||
personalization: false
|
||||
})
|
||||
|
||||
const steps = [
|
||||
{
|
||||
name: '隐私弹窗获取同意',
|
||||
desc: '在首次启动时展示隐私弹窗,明确告知数据收集目的,获取用户明确同意',
|
||||
code: `if (!hasUserConsent()) {
|
||||
showPrivacyDialog({
|
||||
onAccept: () => {
|
||||
grantTrackingConsent()
|
||||
tracker.start()
|
||||
},
|
||||
onReject: () => {
|
||||
denyTrackingConsent()
|
||||
tracker.stop()
|
||||
}
|
||||
})
|
||||
}`
|
||||
},
|
||||
{
|
||||
name: '数据脱敏处理',
|
||||
desc: '对敏感信息进行加密或脱敏处理,确保用户隐私安全',
|
||||
code: `track('user_register', {
|
||||
user_id: hash('user_123'), // 用户 ID 加密
|
||||
phone: mask_phone('138****1234'), // 手机号脱敏
|
||||
email: mask_email('u***@example.com') // 邮箱脱敏
|
||||
})`
|
||||
},
|
||||
{
|
||||
name: '提供数据删除接口',
|
||||
desc: '响应用户的"被遗忘权",提供数据删除功能',
|
||||
code: `function deleteUserData(userId) {
|
||||
// 1. 删除所有事件数据
|
||||
database.delete_all_events(userId)
|
||||
|
||||
// 2. 删除用户画像
|
||||
database.delete_user_profile(userId)
|
||||
|
||||
// 3. 确认删除完成
|
||||
sendDeletionConfirmation(userId)
|
||||
}`
|
||||
},
|
||||
{
|
||||
name: '数据导出功能',
|
||||
desc: '允许用户导出自己的所有数据,满足数据可携带权',
|
||||
code: `function exportUserData(userId) {
|
||||
const userData = {
|
||||
events: database.get_all_events(userId),
|
||||
profile: database.get_user_profile(userId),
|
||||
preferences: database.get_user_preferences(userId)
|
||||
}
|
||||
|
||||
// 生成 JSON 文件供用户下载
|
||||
return downloadJSON(userData, 'my-data.json')
|
||||
}`
|
||||
}
|
||||
]
|
||||
|
||||
const checklistItems = ref([
|
||||
{ text: '展示隐私政策,明确告知数据收集目的', checked: true },
|
||||
{ text: '提供清晰的同意/拒绝选项', checked: true },
|
||||
{ text: '用户可随时撤回同意', checked: false },
|
||||
{ text: '敏感数据加密存储', checked: true },
|
||||
{ text: '提供数据删除功能', checked: false },
|
||||
{ text: '提供数据导出功能', checked: false },
|
||||
{ text: '设置数据保留期限', checked: true },
|
||||
{ text: '定期进行隐私合规审计', checked: false }
|
||||
])
|
||||
|
||||
const acceptSelected = () => {
|
||||
userConsented.value = true
|
||||
}
|
||||
|
||||
const rejectAll = () => {
|
||||
consents.value.analytics = false
|
||||
consents.value.personalization = false
|
||||
userConsented.value = true
|
||||
}
|
||||
|
||||
const changeSettings = () => {
|
||||
userConsented.value = false
|
||||
}
|
||||
|
||||
const deleteData = () => {
|
||||
alert('数据删除请求已提交,我们将在 30 天内完成删除')
|
||||
}
|
||||
|
||||
const toggleCheck = (index) => {
|
||||
checklistItems.value[index].checked = !checklistItems.value[index].checked
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.privacy-compliance-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin: 2rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.compliance-cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.compliance-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
.compliance-card.gdpr {
|
||||
border-color: #003399;
|
||||
}
|
||||
|
||||
.compliance-card.pipl {
|
||||
border-color: #de2910;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.requirement-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.requirement-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.requirement-icon {
|
||||
color: #22c55e;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.implementation-steps {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.steps-title {
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.steps-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 1.25rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step-name {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.step-code {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.step-code pre {
|
||||
margin: 0;
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.consent-flow-demo {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.flow-title {
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.consent-simulation {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.simulation-screen {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.consent-dialog {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.dialog-subtitle {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.consent-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.consent-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.consent-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.consent-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.consent-status {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.consent-status.required {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.consent-toggle {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.consent-toggle input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: 0.3s;
|
||||
border-radius: 28px;
|
||||
}
|
||||
|
||||
.toggle-slider:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
transition: 0.3s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider {
|
||||
background-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider:before {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.dialog-btn {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dialog-btn.primary {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dialog-btn.primary:hover {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.dialog-btn.secondary {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.consented-view {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.consented-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.consented-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.consented-desc {
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.consented-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.consented-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.consented-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.75rem;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
color: #ef4444;
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.action-btn.danger:hover {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.data-protection {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.protection-title {
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.protection-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.protection-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.protection-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.protection-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.protection-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.checklist {
|
||||
background: linear-gradient(135deg, #dcfce7, #bbf7d0);
|
||||
border: 2px solid #22c55e;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.checklist-title {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.checklist-items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.checklist-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.checklist-item:hover {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.checklist-item.checked {
|
||||
background: #dcfce7;
|
||||
}
|
||||
|
||||
.checklist-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.checklist-text {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.compliance-cards,
|
||||
.protection-grid,
|
||||
.checklist-items {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,687 @@
|
||||
<!--
|
||||
RealWorldCaseDemo.vue
|
||||
实战案例 - 电商、推荐、用户行为分析埋点设计
|
||||
-->
|
||||
<template>
|
||||
<div class="real-world-case-demo">
|
||||
<div class="header">
|
||||
<div class="title">实战案例</div>
|
||||
<div class="subtitle">真实场景下的埋点设计最佳实践</div>
|
||||
</div>
|
||||
|
||||
<div class="case-tabs">
|
||||
<button
|
||||
v-for="caseItem in cases"
|
||||
:key="caseItem.id"
|
||||
class="case-tab"
|
||||
:class="{ active: selectedCase === caseItem.id }"
|
||||
@click="selectedCase = caseItem.id"
|
||||
>
|
||||
{{ caseItem.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="case-content">
|
||||
<!-- 电商系统 -->
|
||||
<div v-if="selectedCase === 'ecommerce'" class="case-detail">
|
||||
<div class="case-intro">
|
||||
<div class="intro-icon">🛒</div>
|
||||
<div class="intro-text">
|
||||
<div class="intro-title">电商系统埋点设计</div>
|
||||
<div class="intro-desc">分析购买转化漏斗,优化用户体验</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="funnel-visualization">
|
||||
<div class="funnel-title">购买转化漏斗</div>
|
||||
<div class="funnel-steps">
|
||||
<div
|
||||
v-for="(step, index) in ecommerceFunnel"
|
||||
:key="index"
|
||||
class="funnel-step"
|
||||
:style="{ width: step.width }"
|
||||
>
|
||||
<div class="step-name">{{ step.name }}</div>
|
||||
<div class="step-count">{{ formatNumber(step.count) }}</div>
|
||||
<div class="step-rate">{{ step.rate }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tracking-events">
|
||||
<div class="events-title">关键埋点</div>
|
||||
<div class="events-list">
|
||||
<div
|
||||
v-for="(event, index) in ecommerceEvents"
|
||||
:key="index"
|
||||
class="event-item"
|
||||
>
|
||||
<div class="event-code">
|
||||
<code>{{ event.name }}</code>
|
||||
</div>
|
||||
<div class="event-details">
|
||||
<div class="event-trigger">{{ event.trigger }}</div>
|
||||
<div class="event-props">
|
||||
<span
|
||||
v-for="(prop, i) in event.props"
|
||||
:key="i"
|
||||
class="prop-tag"
|
||||
>
|
||||
{{ prop }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 推荐系统 -->
|
||||
<div v-if="selectedCase === 'recommendation'" class="case-detail">
|
||||
<div class="case-intro">
|
||||
<div class="intro-icon">🎯</div>
|
||||
<div class="intro-text">
|
||||
<div class="intro-title">内容推荐埋点设计</div>
|
||||
<div class="intro-desc">优化推荐算法,提高点击率</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ab-test-demo">
|
||||
<div class="ab-title">A/B 测试效果对比</div>
|
||||
<div class="ab-metrics">
|
||||
<div class="metric-group">
|
||||
<div class="metric-label">算法 A</div>
|
||||
<div class="metric-value">{{ abTest.algorithmA }}%</div>
|
||||
<div class="metric-bar">
|
||||
<div
|
||||
class="bar-fill"
|
||||
:style="{ width: abTest.algorithmA + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-group">
|
||||
<div class="metric-label">算法 B</div>
|
||||
<div class="metric-value">{{ abTest.algorithmB }}%</div>
|
||||
<div class="metric-bar">
|
||||
<div
|
||||
class="bar-fill better"
|
||||
:style="{ width: abTest.algorithmB + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ab-conclusion">
|
||||
✨ 算法 B 点击率提升
|
||||
<span class="highlight"
|
||||
>{{
|
||||
(
|
||||
((abTest.algorithmB - abTest.algorithmA) /
|
||||
abTest.algorithmA) *
|
||||
100
|
||||
).toFixed(1)
|
||||
}}%</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tracking-events">
|
||||
<div class="events-title">关键埋点</div>
|
||||
<div class="events-list">
|
||||
<div
|
||||
v-for="(event, index) in recommendationEvents"
|
||||
:key="index"
|
||||
class="event-item"
|
||||
>
|
||||
<div class="event-code">
|
||||
<code>{{ event.name }}</code>
|
||||
</div>
|
||||
<div class="event-details">
|
||||
<div class="event-trigger">{{ event.trigger }}</div>
|
||||
<div class="event-props">
|
||||
<span
|
||||
v-for="(prop, i) in event.props"
|
||||
:key="i"
|
||||
class="prop-tag"
|
||||
>
|
||||
{{ prop }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户行为分析 -->
|
||||
<div v-if="selectedCase === 'userbehavior'" class="case-detail">
|
||||
<div class="case-intro">
|
||||
<div class="intro-icon">👤</div>
|
||||
<div class="intro-text">
|
||||
<div class="intro-title">用户行为分析埋点</div>
|
||||
<div class="intro-desc">分析用户粘性,识别流失风险</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rfm-segments">
|
||||
<div class="segments-title">RFM 用户分群</div>
|
||||
<div class="segments-grid">
|
||||
<div
|
||||
v-for="(segment, index) in rfmSegments"
|
||||
:key="index"
|
||||
class="segment-card"
|
||||
:class="segment.type"
|
||||
>
|
||||
<div class="segment-name">{{ segment.name }}</div>
|
||||
<div class="segment-users">
|
||||
{{ formatNumber(segment.users) }} 用户
|
||||
</div>
|
||||
<div class="segment-desc">{{ segment.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="retention-chart">
|
||||
<div class="chart-title">用户留存率</div>
|
||||
<div class="chart-bars">
|
||||
<div
|
||||
v-for="(data, index) in retentionData"
|
||||
:key="index"
|
||||
class="chart-bar"
|
||||
>
|
||||
<div class="bar-label">{{ data.label }}</div>
|
||||
<div class="bar-container">
|
||||
<div
|
||||
class="bar-fill"
|
||||
:style="{ height: data.rate + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="bar-value">{{ data.rate }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tracking-events">
|
||||
<div class="events-title">关键埋点</div>
|
||||
<div class="events-list">
|
||||
<div
|
||||
v-for="(event, index) in userBehaviorEvents"
|
||||
:key="index"
|
||||
class="event-item"
|
||||
>
|
||||
<div class="event-code">
|
||||
<code>{{ event.name }}</code>
|
||||
</div>
|
||||
<div class="event-details">
|
||||
<div class="event-trigger">{{ event.trigger }}</div>
|
||||
<div class="event-props">
|
||||
<span
|
||||
v-for="(prop, i) in event.props"
|
||||
:key="i"
|
||||
class="prop-tag"
|
||||
>
|
||||
{{ prop }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const selectedCase = ref('ecommerce')
|
||||
|
||||
const cases = [
|
||||
{ id: 'ecommerce', name: '电商系统' },
|
||||
{ id: 'recommendation', name: '内容推荐' },
|
||||
{ id: 'userbehavior', name: '用户行为' }
|
||||
]
|
||||
|
||||
const ecommerceFunnel = [
|
||||
{ name: '浏览商品', count: 100000, rate: 100, width: '100%' },
|
||||
{ name: '加入购物车', count: 25000, rate: 25, width: '80%' },
|
||||
{ name: '查看购物车', count: 18000, rate: 18, width: '60%' },
|
||||
{ name: '开始结算', count: 12000, rate: 12, width: '45%' },
|
||||
{ name: '支付成功', count: 8500, rate: 8.5, width: '30%' }
|
||||
]
|
||||
|
||||
const ecommerceEvents = [
|
||||
{
|
||||
name: 'view_product',
|
||||
trigger: '商品详情页浏览',
|
||||
props: ['product_id', 'category', 'source', 'position']
|
||||
},
|
||||
{
|
||||
name: 'add_to_cart',
|
||||
trigger: '加入购物车',
|
||||
props: ['product_id', 'quantity', 'price', 'source']
|
||||
},
|
||||
{
|
||||
name: 'begin_checkout',
|
||||
trigger: '开始结算',
|
||||
props: ['cart_total', 'item_count', 'payment_method']
|
||||
},
|
||||
{
|
||||
name: 'purchase',
|
||||
trigger: '支付成功',
|
||||
props: ['order_id', 'total_amount', 'coupon', 'payment_method']
|
||||
}
|
||||
]
|
||||
|
||||
const abTest = {
|
||||
algorithmA: 3.2,
|
||||
algorithmB: 4.1
|
||||
}
|
||||
|
||||
const recommendationEvents = [
|
||||
{
|
||||
name: 'recommend_exposure',
|
||||
trigger: '推荐内容曝光',
|
||||
props: ['item_id', 'position', 'algorithm', 'rank_score']
|
||||
},
|
||||
{
|
||||
name: 'recommend_click',
|
||||
trigger: '点击推荐内容',
|
||||
props: ['item_id', 'position', 'algorithm']
|
||||
},
|
||||
{
|
||||
name: 'content_view_duration',
|
||||
trigger: '内容观看时长',
|
||||
props: ['item_id', 'duration', 'completion_rate']
|
||||
}
|
||||
]
|
||||
|
||||
const rfmSegments = [
|
||||
{
|
||||
name: '高价值用户',
|
||||
users: 15842,
|
||||
desc: '最近购买+高频+高金额',
|
||||
type: 'high'
|
||||
},
|
||||
{
|
||||
name: '重要保持客户',
|
||||
users: 32158,
|
||||
desc: '最近购买+高频+中金额',
|
||||
type: 'medium'
|
||||
},
|
||||
{ name: '流失风险用户', users: 28456, desc: '很久未购买+低频', type: 'risk' },
|
||||
{ name: '已流失用户', users: 45123, desc: '超过90天未购买', type: 'lost' }
|
||||
]
|
||||
|
||||
const retentionData = [
|
||||
{ label: '次日', rate: 45 },
|
||||
{ label: '7日', rate: 32 },
|
||||
{ label: '30日', rate: 18 },
|
||||
{ label: '90日', rate: 8 }
|
||||
]
|
||||
|
||||
const userBehaviorEvents = [
|
||||
{
|
||||
name: 'app_start',
|
||||
trigger: 'App 启动',
|
||||
props: ['source', 'is_first_launch', 'last_visit_days']
|
||||
},
|
||||
{
|
||||
name: 'daily_active',
|
||||
trigger: '每日活跃',
|
||||
props: ['session_count', 'total_duration', 'feature_usage']
|
||||
},
|
||||
{
|
||||
name: 'feature_usage',
|
||||
trigger: '功能使用',
|
||||
props: ['feature_name', 'usage_duration', 'action_count']
|
||||
}
|
||||
]
|
||||
|
||||
const formatNumber = (num) => {
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.real-world-case-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin: 2rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.case-tabs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.case-tab {
|
||||
padding: 0.75rem 2rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.case-tab:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.case-tab.active {
|
||||
background: var(--vp-c-brand);
|
||||
border-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.case-content {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.case-intro {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.intro-icon {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.intro-title {
|
||||
font-weight: 700;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.intro-desc {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.funnel-visualization,
|
||||
.ab-test-demo,
|
||||
.rfm-segments,
|
||||
.retention-chart {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.funnel-title,
|
||||
.ab-title,
|
||||
.segments-title,
|
||||
.chart-title,
|
||||
.events-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.funnel-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.funnel-step {
|
||||
background: linear-gradient(90deg, var(--vp-c-brand), #3b82f6);
|
||||
color: white;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: width 0.5s;
|
||||
}
|
||||
|
||||
.step-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.step-count {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ab-metrics {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.metric-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.metric-bar {
|
||||
height: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar-fill {
|
||||
height: 100%;
|
||||
background: var(--vp-c-brand);
|
||||
border-radius: 4px;
|
||||
transition: width 0.5s;
|
||||
}
|
||||
|
||||
.bar-fill.better {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.ab-conclusion {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
background: #dcfce7;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: #22c55e;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.segments-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.segment-card {
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.segment-card.high {
|
||||
background: linear-gradient(135deg, #dcfce7, #bbf7d0);
|
||||
border: 2px solid #22c55e;
|
||||
}
|
||||
|
||||
.segment-card.medium {
|
||||
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
|
||||
border: 2px solid #3b82f6;
|
||||
}
|
||||
|
||||
.segment-card.risk {
|
||||
background: linear-gradient(135deg, #fef3c7, #fde68a);
|
||||
border: 2px solid #f59e0b;
|
||||
}
|
||||
|
||||
.segment-card.lost {
|
||||
background: linear-gradient(135deg, #fee2e2, #fecaca);
|
||||
border: 2px solid #ef4444;
|
||||
}
|
||||
|
||||
.segment-name {
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.segment-users {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.segment-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.chart-bars {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: flex-end;
|
||||
height: 200px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.chart-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.bar-container {
|
||||
width: 40px;
|
||||
height: 150px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.chart-bar .bar-fill {
|
||||
width: 100%;
|
||||
background: linear-gradient(180deg, var(--vp-c-brand), #3b82f6);
|
||||
border-radius: 4px;
|
||||
transition: height 0.5s;
|
||||
}
|
||||
|
||||
.bar-value {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tracking-events {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.events-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.event-item {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.event-code code {
|
||||
background: #1e1e1e;
|
||||
color: #ce9178;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.event-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.event-trigger {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.event-props {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.prop-tag {
|
||||
padding: 0.15rem 0.5rem;
|
||||
background: white;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.segments-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.chart-bars {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.event-item {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,492 @@
|
||||
<!--
|
||||
ToolSelectionDemo.vue
|
||||
工具选型建议 - 帮助选择合适的埋点工具
|
||||
-->
|
||||
<template>
|
||||
<div class="tool-selection-demo">
|
||||
<div class="header">
|
||||
<div class="title">埋点工具选型</div>
|
||||
<div class="subtitle">根据团队规模和需求选择合适的方案</div>
|
||||
</div>
|
||||
|
||||
<div class="selection-criteria">
|
||||
<div class="criteria-title">请选择您的场景</div>
|
||||
<div class="criteria-options">
|
||||
<div
|
||||
v-for="(option, key) in criteria"
|
||||
:key="key"
|
||||
class="criteria-option"
|
||||
>
|
||||
<div class="option-label">{{ option.label }}</div>
|
||||
<div class="option-buttons">
|
||||
<button
|
||||
v-for="(value, index) in option.values"
|
||||
:key="index"
|
||||
class="value-btn"
|
||||
:class="{ active: selectedCriteria[key] === value }"
|
||||
@click="selectedCriteria[key] = value"
|
||||
>
|
||||
{{ value }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="recommend-btn" @click="getRecommendation">
|
||||
获取推荐方案
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="recommendation" class="recommendation-result">
|
||||
<div class="result-header">
|
||||
<div class="result-icon">🎯</div>
|
||||
<div class="result-title">推荐方案</div>
|
||||
</div>
|
||||
|
||||
<div class="result-card">
|
||||
<div class="result-name">{{ recommendation.name }}</div>
|
||||
<div class="result-desc">{{ recommendation.desc }}</div>
|
||||
|
||||
<div class="result-details">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">适用阶段:</span>
|
||||
<span class="detail-value">{{ recommendation.stage }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">预估成本:</span>
|
||||
<span class="detail-value">{{ recommendation.cost }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">实施难度:</span>
|
||||
<span class="detail-value">{{ recommendation.difficulty }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-pros">
|
||||
<div class="pros-title">✅ 优势</div>
|
||||
<ul class="pros-list">
|
||||
<li v-for="(pro, i) in recommendation.pros" :key="i">{{ pro }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="result-cons">
|
||||
<div class="cons-title">⚠️ 注意事项</div>
|
||||
<ul class="cons-list">
|
||||
<li v-for="(con, i) in recommendation.cons" :key="i">{{ con }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tools-comparison">
|
||||
<div class="comparison-title">工具对比表</div>
|
||||
<table class="comparison-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>工具</th>
|
||||
<th>类型</th>
|
||||
<th>价格</th>
|
||||
<th>适用场景</th>
|
||||
<th>推荐指数</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(tool, index) in tools" :key="index">
|
||||
<td class="tool-name">{{ tool.name }}</td>
|
||||
<td>{{ tool.type }}</td>
|
||||
<td>{{ tool.price }}</td>
|
||||
<td>{{ tool.scenario }}</td>
|
||||
<td>
|
||||
<span class="rating">
|
||||
{{ '⭐'.repeat(tool.rating) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const selectedCriteria = ref({
|
||||
teamSize: '',
|
||||
budget: '',
|
||||
technical: '',
|
||||
dataSecurity: ''
|
||||
})
|
||||
|
||||
const criteria = {
|
||||
teamSize: {
|
||||
label: '团队规模',
|
||||
values: ['1-5人', '5-20人', '20-100人', '100+人']
|
||||
},
|
||||
budget: {
|
||||
label: '预算',
|
||||
values: ['免费优先', '低预算', '中等预算', '预算充足']
|
||||
},
|
||||
technical: {
|
||||
label: '技术能力',
|
||||
values: ['无技术团队', '有开发人员', '技术团队完善']
|
||||
},
|
||||
dataSecurity: {
|
||||
label: '数据安全要求',
|
||||
values: ['一般', '较高', '极高(需私有化)']
|
||||
}
|
||||
}
|
||||
|
||||
const recommendation = ref(null)
|
||||
|
||||
const recommendations = {
|
||||
small: {
|
||||
name: 'Google Analytics',
|
||||
desc: '全球最流行的免费网站分析工具,功能强大,易于上手',
|
||||
stage: '0-1 阶段(初创期)',
|
||||
cost: '免费',
|
||||
difficulty: '低',
|
||||
pros: ['完全免费', '功能全面', '社区资源丰富', '上手简单'],
|
||||
cons: ['数据在海外服务器', '国内访问可能不稳定', '高级功能需要翻墙']
|
||||
},
|
||||
medium: {
|
||||
name: '神策数据 / GrowingIO',
|
||||
desc: '国内领先的用户行为分析平台,支持私有化部署',
|
||||
stage: '1-10 阶段(成长期)',
|
||||
cost: '$5,000 - $20,000 /年',
|
||||
difficulty: '中',
|
||||
pros: ['专业的事件分析', '支持私有化部署', '国内技术支持', '符合国内法规'],
|
||||
cons: ['价格较高', '需要技术团队维护', '定制化需求成本高']
|
||||
},
|
||||
large: {
|
||||
name: '自建埋点系统',
|
||||
desc: '基于开源技术栈(Kafka + ClickHouse)搭建私有化埋点平台',
|
||||
stage: '10-100 阶段(成熟期)',
|
||||
cost: '$50,000+ /年(人力+服务器)',
|
||||
difficulty: '高',
|
||||
pros: ['数据完全自主可控', '灵活定制化', '长期成本更低', '数据安全性最高'],
|
||||
cons: ['初期投入大', '需要专业团队', '维护成本高', '实施周期长']
|
||||
}
|
||||
}
|
||||
|
||||
const tools = [
|
||||
{
|
||||
name: 'Google Analytics',
|
||||
type: 'SaaS',
|
||||
price: '免费',
|
||||
scenario: '小型项目、个人网站',
|
||||
rating: 5
|
||||
},
|
||||
{
|
||||
name: 'Umami',
|
||||
type: '开源',
|
||||
price: '服务器成本',
|
||||
scenario: '注重隐私、需要私有化',
|
||||
rating: 4
|
||||
},
|
||||
{
|
||||
name: '神策数据',
|
||||
type: '商业+私有化',
|
||||
price: '$10,000+/年',
|
||||
scenario: '中大型企业',
|
||||
rating: 5
|
||||
},
|
||||
{
|
||||
name: 'GrowingIO',
|
||||
type: '商业+SaaS',
|
||||
price: '$5,000+/年',
|
||||
scenario: '增长团队、产品优化',
|
||||
rating: 4
|
||||
},
|
||||
{
|
||||
name: 'Mixpanel',
|
||||
type: 'SaaS',
|
||||
price: '$25,000+/年',
|
||||
scenario: '产品数据分析',
|
||||
rating: 4
|
||||
}
|
||||
]
|
||||
|
||||
const getRecommendation = () => {
|
||||
const { teamSize, budget, technical, dataSecurity } = selectedCriteria.value
|
||||
|
||||
if (dataSecurity === '极高(需私有化)') {
|
||||
recommendation.value = recommendations.large
|
||||
} else if (teamSize === '1-5人' || budget === '免费优先') {
|
||||
recommendation.value = recommendations.small
|
||||
} else if (teamSize === '5-20人' || teamSize === '20-100人') {
|
||||
recommendation.value = recommendations.medium
|
||||
} else {
|
||||
recommendation.value = recommendations.large
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tool-selection-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin: 2rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.selection-criteria {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.criteria-title {
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.criteria-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.criteria-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.option-buttons {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.value-btn {
|
||||
padding: 0.5rem 1.25rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.value-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.value-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
border-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.recommend-btn {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.recommend-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(60, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.recommendation-result {
|
||||
background: linear-gradient(135deg, #fef3c7, #fde68a);
|
||||
border: 2px solid #f59e0b;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.result-icon {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.result-name {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.result-desc {
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.result-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.result-pros,
|
||||
.result-cons {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.pros-title,
|
||||
.cons-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.pros-list,
|
||||
.cons-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pros-list li,
|
||||
.cons-list li {
|
||||
padding: 0.25rem 0;
|
||||
padding-left: 1.5rem;
|
||||
position: relative;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.pros-list li::before {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #22c55e;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cons-list li::before {
|
||||
content: '⚠️';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.tools-comparison {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.comparison-title {
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.comparison-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.comparison-table th {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.comparison-table td {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.tool-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rating {
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.result-details {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.option-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.value-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+553
@@ -0,0 +1,553 @@
|
||||
<!--
|
||||
TrackingMethodsComparisonDemo.vue
|
||||
埋点方法对比 - 代码埋点、可视化埋点、全埋点
|
||||
-->
|
||||
<template>
|
||||
<div class="tracking-methods-comparison-demo">
|
||||
<div class="header">
|
||||
<div class="title">埋点方法对比</div>
|
||||
<div class="subtitle">三种主流埋点实现方式的深度对比</div>
|
||||
</div>
|
||||
|
||||
<div class="methods-grid">
|
||||
<div
|
||||
v-for="method in methods"
|
||||
:key="method.id"
|
||||
class="method-card"
|
||||
:class="{ selected: selectedMethod === method.id }"
|
||||
@click="selectMethod(method.id)"
|
||||
>
|
||||
<div class="method-header">
|
||||
<div class="method-icon">{{ method.icon }}</div>
|
||||
<div class="method-info">
|
||||
<div class="method-name">{{ method.name }}</div>
|
||||
<div class="method-english">{{ method.english }}</div>
|
||||
</div>
|
||||
<div v-if="selectedMethod === method.id" class="selected-badge">
|
||||
已选择
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-body">
|
||||
<div class="method-description">{{ method.description }}</div>
|
||||
|
||||
<div class="method-features">
|
||||
<div class="feature-category">
|
||||
<div class="category-title">✅ 优点</div>
|
||||
<ul class="feature-list pros">
|
||||
<li v-for="(pro, index) in method.pros" :key="index">
|
||||
{{ pro }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="feature-category">
|
||||
<div class="category-title">❌ 缺点</div>
|
||||
<ul class="feature-list cons">
|
||||
<li v-for="(con, index) in method.cons" :key="index">
|
||||
{{ con }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-code">
|
||||
<div class="code-title">代码示例</div>
|
||||
<pre class="code-block"><code>{{ method.code }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-matrix">
|
||||
<div class="matrix-title">综合对比矩阵</div>
|
||||
<table class="matrix">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>评估维度</th>
|
||||
<th v-for="method in methods" :key="method.id">
|
||||
{{ method.name }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index) in matrixData" :key="index">
|
||||
<td class="dimension">{{ row.dimension }}</td>
|
||||
<td
|
||||
v-for="method in methods"
|
||||
:key="method.id"
|
||||
class="score"
|
||||
:class="{ best: row.best === method.id }"
|
||||
>
|
||||
<div class="score-bar">
|
||||
<div
|
||||
class="score-fill"
|
||||
:style="{ width: row.scores[method.id] + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="score-value">{{ row.scores[method.id] }}%</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="recommendation">
|
||||
<div class="recommendation-title">💡 选型建议</div>
|
||||
<div class="recommendation-content">
|
||||
<div class="recommendation-item">
|
||||
<div class="rec-scenario">核心业务指标</div>
|
||||
<div class="rec-method">推荐:代码埋点</div>
|
||||
<div class="rec-reason">
|
||||
原因:数据准确性最高,可自定义属性,适合支付、注册等关键业务
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recommendation-item">
|
||||
<div class="rec-scenario">运营活动埋点</div>
|
||||
<div class="rec-method">推荐:可视化埋点</div>
|
||||
<div class="rec-reason">
|
||||
原因:快速部署,产品经理可操作,适合快速验证活动效果
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recommendation-item">
|
||||
<div class="rec-scenario">页面浏览数据</div>
|
||||
<div class="rec-method">推荐:全埋点</div>
|
||||
<div class="rec-reason">
|
||||
原因:零开发成本,一次性采集,适合 PV/UV 等基础指标
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recommendation-item">
|
||||
<div class="rec-scenario">大型企业级应用</div>
|
||||
<div class="rec-method">推荐:混合方案</div>
|
||||
<div class="rec-reason">
|
||||
原因:核心业务用代码埋点,运营活动用可视化埋点,基础数据用全埋点
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const selectedMethod = ref('code')
|
||||
|
||||
const methods = [
|
||||
{
|
||||
id: 'code',
|
||||
name: '代码埋点',
|
||||
english: 'Code-based Tracking',
|
||||
icon: '💻',
|
||||
description: '在代码中显式调用埋点 SDK,由开发人员手动添加采集代码',
|
||||
pros: [
|
||||
'数据准确,时机可控',
|
||||
'灵活度高,可自定义属性',
|
||||
'可采集复杂业务逻辑',
|
||||
'适用于各种场景'
|
||||
],
|
||||
cons: ['需要开发资源', '新增埋点需要发版', '维护成本较高', '依赖开发团队'],
|
||||
code: `// 点击"购买"按钮埋点
|
||||
function onBuyButtonClick() {
|
||||
// 业务逻辑
|
||||
addToCart(product)
|
||||
|
||||
// 埋点
|
||||
track('click_buy_button', {
|
||||
product_id: product.id,
|
||||
product_name: product.name,
|
||||
price: product.price,
|
||||
page: 'product_detail'
|
||||
})
|
||||
}`
|
||||
},
|
||||
{
|
||||
id: 'visual',
|
||||
name: '可视化埋点',
|
||||
english: 'Visual Tracking',
|
||||
icon: '🎨',
|
||||
description: '通过可视化工具圈选页面元素,自动生成埋点代码',
|
||||
pros: ['无需编码', '产品经理可操作', '快速部署', '所见即所得'],
|
||||
cons: [
|
||||
'只能采集标准事件',
|
||||
'自定义属性能力弱',
|
||||
'页面改版后易失效',
|
||||
'功能相对单一'
|
||||
],
|
||||
code: `// 可视化埋点管理后台
|
||||
// 1. 打开可视化埋点工具
|
||||
// 2. 在页面上圈选"立即购买"按钮
|
||||
// 3. 配置事件名称:click_buy_button
|
||||
// 4. 配置属性:product_id, price
|
||||
// 5. 一键发布
|
||||
|
||||
// SDK 自动生成埋点代码
|
||||
// 无需手动编写代码`
|
||||
},
|
||||
{
|
||||
id: 'auto',
|
||||
name: '全埋点',
|
||||
english: 'Auto Tracking',
|
||||
icon: '🤖',
|
||||
description: 'SDK 自动采集所有用户行为,无需手动添加代码',
|
||||
pros: ['零开发成本', '一次性采集所有数据', '支持回溯分析', '部署简单'],
|
||||
cons: [
|
||||
'数据量大,噪声多',
|
||||
'无法自定义属性',
|
||||
'隐私合规风险',
|
||||
'数据质量相对较低'
|
||||
],
|
||||
code: `// SDK 初始化(只需一行代码)
|
||||
const tracker = new AutoTracker({
|
||||
serverUrl: 'https://analytics.example.com',
|
||||
autoTrack: true // 开启全埋点
|
||||
})
|
||||
|
||||
// SDK 自动采集:
|
||||
// - 所有页面浏览
|
||||
// - 所有元素点击
|
||||
// - 所有表单提交
|
||||
// - 所有页面滚动`
|
||||
}
|
||||
]
|
||||
|
||||
const matrixData = [
|
||||
{
|
||||
dimension: '灵活性',
|
||||
scores: { code: 95, visual: 70, auto: 30 },
|
||||
best: 'code'
|
||||
},
|
||||
{
|
||||
dimension: '开发成本',
|
||||
scores: { code: 30, visual: 80, auto: 100 },
|
||||
best: 'auto'
|
||||
},
|
||||
{
|
||||
dimension: '维护成本',
|
||||
scores: { code: 40, visual: 60, auto: 90 },
|
||||
best: 'auto'
|
||||
},
|
||||
{
|
||||
dimension: '数据质量',
|
||||
scores: { code: 100, visual: 75, auto: 60 },
|
||||
best: 'code'
|
||||
},
|
||||
{
|
||||
dimension: '部署速度',
|
||||
scores: { code: 40, visual: 85, auto: 100 },
|
||||
best: 'auto'
|
||||
},
|
||||
{
|
||||
dimension: '自定义能力',
|
||||
scores: { code: 100, visual: 50, auto: 20 },
|
||||
best: 'code'
|
||||
}
|
||||
]
|
||||
|
||||
const selectMethod = (methodId) => {
|
||||
selectedMethod.value = methodId
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tracking-methods-comparison-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin: 2rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.methods-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.method-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.method-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.method-card.selected {
|
||||
border-color: var(--vp-c-brand);
|
||||
box-shadow: 0 0 0 3px rgba(60, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.method-header {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.method-icon {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.method-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.method-name {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.method-english {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.selected-badge {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.method-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.method-description {
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.method-features {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.feature-category {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.feature-list li {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25rem 0;
|
||||
padding-left: 1.25rem;
|
||||
position: relative;
|
||||
color: var(--vp-c-text-1);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.feature-list.pros li::before {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #22c55e;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.feature-list.cons li::before {
|
||||
content: '✗';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #ef4444;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.method-code {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.code-title {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.comparison-matrix {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.matrix-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.matrix {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.matrix th {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
border-bottom: 2px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.matrix td {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.matrix .dimension {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.score {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.score.best {
|
||||
background: #dcfce7;
|
||||
}
|
||||
|
||||
.score-bar {
|
||||
height: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.score-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--vp-c-brand), #3b82f6);
|
||||
border-radius: 4px;
|
||||
transition: width 0.5s;
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.recommendation {
|
||||
background: linear-gradient(135deg, #fef3c7, #fde68a);
|
||||
border: 2px solid #f59e0b;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.recommendation-title {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.recommendation-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.recommendation-item {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
border: 1px solid #f59e0b;
|
||||
}
|
||||
|
||||
.rec-scenario {
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.rec-method {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-brand);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.rec-reason {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.methods-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.recommendation-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,479 @@
|
||||
<!--
|
||||
TrackingOverviewDemo.vue
|
||||
埋点系统概览 - 展示埋点在系统中的位置和作用
|
||||
-->
|
||||
<template>
|
||||
<div class="tracking-overview-demo">
|
||||
<div class="header">
|
||||
<div class="title">埋点系统概览</div>
|
||||
<div class="subtitle">从用户行为到数据洞察的完整链路</div>
|
||||
</div>
|
||||
|
||||
<div class="system-flow">
|
||||
<div class="flow-section user-actions">
|
||||
<div class="section-title">用户行为层</div>
|
||||
<div class="action-grid">
|
||||
<div
|
||||
v-for="action in userActions"
|
||||
:key="action.id"
|
||||
class="action-item"
|
||||
:class="{ active: selectedAction === action.id }"
|
||||
@click="selectAction(action)"
|
||||
>
|
||||
<div class="action-icon">{{ action.icon }}</div>
|
||||
<div class="action-name">{{ action.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow">↓</div>
|
||||
|
||||
<div class="flow-section tracking-layer">
|
||||
<div class="section-title">埋点采集层</div>
|
||||
<div class="tracking-box">
|
||||
<div class="tracking-icon">📊</div>
|
||||
<div class="tracking-info">
|
||||
<div class="event-name">{{ selectedEventData.event }}</div>
|
||||
<div class="event-data">
|
||||
{{ formatEventData(selectedEventData) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow">↓</div>
|
||||
|
||||
<div class="flow-section data-pipeline">
|
||||
<div class="section-title">数据处理层</div>
|
||||
<div class="pipeline-steps">
|
||||
<div
|
||||
v-for="(step, index) in pipelineSteps"
|
||||
:key="step.name"
|
||||
class="pipeline-step"
|
||||
:class="{ active: currentStep === index }"
|
||||
>
|
||||
<div class="step-number">{{ index + 1 }}</div>
|
||||
<div class="step-info">
|
||||
<div class="step-name">{{ step.name }}</div>
|
||||
<div class="step-desc">{{ step.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow">↓</div>
|
||||
|
||||
<div class="flow-section insights">
|
||||
<div class="section-title">数据洞察层</div>
|
||||
<div class="insight-cards">
|
||||
<div class="insight-card">
|
||||
<div class="insight-value">
|
||||
{{ formatNumber(metrics.totalUsers) }}
|
||||
</div>
|
||||
<div class="insight-label">总用户数</div>
|
||||
</div>
|
||||
<div class="insight-card">
|
||||
<div class="insight-value">
|
||||
{{ formatNumber(metrics.totalEvents) }}
|
||||
</div>
|
||||
<div class="insight-label">总事件数</div>
|
||||
</div>
|
||||
<div class="insight-card">
|
||||
<div class="insight-value">{{ metrics.conversionRate }}%</div>
|
||||
<div class="insight-label">转化率</div>
|
||||
</div>
|
||||
<div class="insight-card">
|
||||
<div class="insight-value">{{ metrics.retentionRate }}%</div>
|
||||
<div class="insight-label">留存率</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="benefits">
|
||||
<div class="benefit-title">埋点的核心价值</div>
|
||||
<div class="benefit-grid">
|
||||
<div class="benefit-item">
|
||||
<div class="benefit-icon">🎯</div>
|
||||
<div class="benefit-text">
|
||||
<div class="benefit-name">精准决策</div>
|
||||
<div class="benefit-desc">基于数据而非直觉做决策</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="benefit-item">
|
||||
<div class="benefit-icon">🔍</div>
|
||||
<div class="benefit-text">
|
||||
<div class="benefit-name">用户洞察</div>
|
||||
<div class="benefit-desc">理解用户行为和需求</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="benefit-item">
|
||||
<div class="benefit-icon">📈</div>
|
||||
<div class="benefit-text">
|
||||
<div class="benefit-name">增长优化</div>
|
||||
<div class="benefit-desc">发现增长机会和瓶颈</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="benefit-item">
|
||||
<div class="benefit-icon">⚡</div>
|
||||
<div class="benefit-text">
|
||||
<div class="benefit-name">快速迭代</div>
|
||||
<div class="benefit-desc">验证假设,快速调整</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const selectedAction = ref('click')
|
||||
const currentStep = ref(0)
|
||||
|
||||
const userActions = [
|
||||
{ id: 'click', name: '点击按钮', icon: '👆' },
|
||||
{ id: 'view', name: '浏览页面', icon: '👀' },
|
||||
{ id: 'search', name: '搜索内容', icon: '🔍' },
|
||||
{ id: 'purchase', name: '购买商品', icon: '🛒' }
|
||||
]
|
||||
|
||||
const selectedEventData = computed(() => {
|
||||
const eventMap = {
|
||||
click: {
|
||||
event: 'click_button',
|
||||
properties: {
|
||||
button_name: '立即购买',
|
||||
page: '商品详情页',
|
||||
position: '顶部'
|
||||
}
|
||||
},
|
||||
view: {
|
||||
event: 'page_view',
|
||||
properties: {
|
||||
page_title: '商品详情页',
|
||||
page_url: '/product/123',
|
||||
referrer: '首页'
|
||||
}
|
||||
},
|
||||
search: {
|
||||
event: 'search',
|
||||
properties: {
|
||||
query: 'iPhone 15',
|
||||
results_count: 42,
|
||||
filter: '价格升序'
|
||||
}
|
||||
},
|
||||
purchase: {
|
||||
event: 'purchase',
|
||||
properties: {
|
||||
order_id: 'ORD123456',
|
||||
total_amount: 7999.0,
|
||||
payment_method: '支付宝'
|
||||
}
|
||||
}
|
||||
}
|
||||
return eventMap[selectedAction.value]
|
||||
})
|
||||
|
||||
const pipelineSteps = [
|
||||
{ name: '数据采集', desc: '客户端 SDK 收集用户行为' },
|
||||
{ name: '数据传输', desc: '加密上报到服务器' },
|
||||
{ name: '数据清洗', desc: '去重、校验、格式化' },
|
||||
{ name: '数据存储', desc: '存入数据仓库' },
|
||||
{ name: '数据分析', desc: '生成报表和洞察' }
|
||||
]
|
||||
|
||||
const metrics = ref({
|
||||
totalUsers: 158420,
|
||||
totalEvents: 8921450,
|
||||
conversionRate: 3.2,
|
||||
retentionRate: 45.8
|
||||
})
|
||||
|
||||
let stepInterval = null
|
||||
|
||||
const selectAction = (action) => {
|
||||
selectedAction.value = action.id
|
||||
currentStep.value = 0
|
||||
|
||||
if (stepInterval) clearInterval(stepInterval)
|
||||
|
||||
stepInterval = setInterval(() => {
|
||||
if (currentStep.value < pipelineSteps.length - 1) {
|
||||
currentStep.value++
|
||||
} else {
|
||||
clearInterval(stepInterval)
|
||||
}
|
||||
}, 800)
|
||||
}
|
||||
|
||||
const formatEventData = (data) => {
|
||||
return JSON.stringify(data.properties, null, 2)
|
||||
}
|
||||
|
||||
const formatNumber = (num) => {
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M'
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'K'
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tracking-overview-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin: 2rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.system-flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.flow-section {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.action-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem 1rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.action-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.action-item.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.action-name {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.tracking-box {
|
||||
background: linear-gradient(135deg, #fef3c7, #fde68a);
|
||||
border: 2px solid #f59e0b;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.tracking-icon {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.event-name {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.event-data {
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.8rem;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
color: var(--vp-c-text-2);
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.pipeline-steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.pipeline-step {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 1rem 0.75rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.pipeline-step.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg-soft);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
margin: 0 auto 0.5rem;
|
||||
}
|
||||
|
||||
.step-name {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.insight-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.insight-card {
|
||||
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
|
||||
border: 2px solid #3b82f6;
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.insight-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.insight-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 2rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.benefits {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.benefit-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.benefit-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.benefit-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.benefit-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.benefit-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.benefit-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.action-grid,
|
||||
.insight-cards,
|
||||
.benefit-grid,
|
||||
.pipeline-steps {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.pipeline-steps {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,622 @@
|
||||
<!--
|
||||
TrackingTypesDemo.vue
|
||||
埋点类型对比 - 展示前端、后端、全链路埋点的区别
|
||||
-->
|
||||
<template>
|
||||
<div class="tracking-types-demo">
|
||||
<div class="header">
|
||||
<div class="title">埋点类型对比</div>
|
||||
<div class="subtitle">三种埋点方式的优缺点与适用场景</div>
|
||||
</div>
|
||||
|
||||
<div class="type-tabs">
|
||||
<button
|
||||
v-for="type in trackingTypes"
|
||||
:key="type.id"
|
||||
class="type-tab"
|
||||
:class="{ active: selectedType === type.id }"
|
||||
@click="selectType(type.id)"
|
||||
>
|
||||
{{ type.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="type-content">
|
||||
<div class="type-info">
|
||||
<div class="type-header">
|
||||
<div class="type-icon">{{ currentType.icon }}</div>
|
||||
<div class="type-title">
|
||||
<div class="name">{{ currentType.name }}</div>
|
||||
<div class="subtitle">{{ currentType.subtitle }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="type-description">
|
||||
{{ currentType.description }}
|
||||
</div>
|
||||
|
||||
<div class="characteristics">
|
||||
<div class="characteristics-title">主要特征</div>
|
||||
<div class="characteristics-list">
|
||||
<div
|
||||
v-for="(char, index) in currentType.characteristics"
|
||||
:key="index"
|
||||
class="characteristic-item"
|
||||
>
|
||||
<span class="check">✓</span>
|
||||
<span>{{ char }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="use-cases">
|
||||
<div class="use-cases-title">典型场景</div>
|
||||
<div class="use-cases-list">
|
||||
<div
|
||||
v-for="(useCase, index) in currentType.useCases"
|
||||
:key="index"
|
||||
class="use-case-item"
|
||||
>
|
||||
<div class="use-case-icon">{{ useCase.icon }}</div>
|
||||
<div class="use-case-info">
|
||||
<div class="use-case-name">{{ useCase.name }}</div>
|
||||
<div class="use-case-desc">{{ useCase.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="type-architecture">
|
||||
<div class="architecture-title">架构示意</div>
|
||||
<div class="architecture-diagram">
|
||||
<div class="diagram-layer">
|
||||
<div class="layer-label">用户</div>
|
||||
<div class="layer-icon">👤</div>
|
||||
</div>
|
||||
<div class="diagram-arrow">↓</div>
|
||||
<div class="diagram-layer client">
|
||||
<div class="layer-label">客户端</div>
|
||||
<div class="layer-content">
|
||||
<div
|
||||
v-if="selectedType === 'frontend'"
|
||||
class="layer-box frontend"
|
||||
>
|
||||
<div>前端埋点 SDK</div>
|
||||
<div class="layer-detail">采集用户交互</div>
|
||||
</div>
|
||||
<div v-if="selectedType === 'backend'" class="layer-box backend">
|
||||
<div>业务代码</div>
|
||||
<div class="layer-detail">调用后端埋点</div>
|
||||
</div>
|
||||
<div v-if="selectedType === 'full'" class="layer-box full">
|
||||
<div>前端埋点 SDK</div>
|
||||
<div>后端埋点</div>
|
||||
<div class="layer-detail">全链路追踪</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="diagram-arrow">↓</div>
|
||||
<div
|
||||
v-if="selectedType === 'backend' || selectedType === 'full'"
|
||||
class="diagram-layer server"
|
||||
>
|
||||
<div class="layer-label">服务端</div>
|
||||
<div class="layer-content">
|
||||
<div class="layer-box server">
|
||||
<div>埋点服务</div>
|
||||
<div class="layer-detail">处理埋点请求</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="diagram-arrow">↓</div>
|
||||
<div class="diagram-layer data">
|
||||
<div class="layer-label">数据平台</div>
|
||||
<div class="layer-content">
|
||||
<div class="layer-box data">
|
||||
<div>数据仓库</div>
|
||||
<div class="layer-detail">存储与分析</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-table">
|
||||
<div class="comparison-title">详细对比</div>
|
||||
<table class="comparison">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>对比维度</th>
|
||||
<th v-for="type in trackingTypes" :key="type.id">
|
||||
{{ type.name }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index) in comparisonData" :key="index">
|
||||
<td class="dimension">{{ row.dimension }}</td>
|
||||
<td
|
||||
v-for="type in trackingTypes"
|
||||
:key="type.id"
|
||||
class="value"
|
||||
:class="{ best: row.best === type.id }"
|
||||
>
|
||||
{{ row.values[type.id] }}
|
||||
<span v-if="row.best === type.id" class="best-badge">最优</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const selectedType = ref('frontend')
|
||||
|
||||
const trackingTypes = [
|
||||
{
|
||||
id: 'frontend',
|
||||
name: '前端埋点',
|
||||
subtitle: 'Client-side Tracking',
|
||||
icon: '💻',
|
||||
description:
|
||||
'在 Web、App、小程序的前端代码中集成埋点 SDK,直接采集用户与界面的交互行为。数据实时性好,可采集设备信息,但可能被篡改。',
|
||||
characteristics: [
|
||||
'实时采集用户行为',
|
||||
'可获取设备信息、网络状态',
|
||||
'可视化数据收集',
|
||||
'离线缓存,联网补传',
|
||||
'支持 A/B 测试和热力图'
|
||||
],
|
||||
useCases: [
|
||||
{ icon: '📱', name: '页面浏览', desc: '记录用户访问了哪些页面' },
|
||||
{ icon: '👆', name: '按钮点击', desc: '统计用户点击了哪些按钮' },
|
||||
{ icon: '📝', name: '表单提交', desc: '追踪表单填写和提交' },
|
||||
{ icon: '🎯', name: '转化漏斗', desc: '分析用户转化路径' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'backend',
|
||||
name: '后端埋点',
|
||||
subtitle: 'Server-side Tracking',
|
||||
icon: '⚙️',
|
||||
description:
|
||||
'在服务器端业务逻辑中添加埋点代码,采集服务端事件。数据准确可靠,不可篡改,但无法获取客户端信息。',
|
||||
characteristics: [
|
||||
'数据准确,不可篡改',
|
||||
'采集业务核心事件',
|
||||
'不受客户端网络影响',
|
||||
'可采集服务端特有数据',
|
||||
'隐私合规性更好'
|
||||
],
|
||||
useCases: [
|
||||
{ icon: '💰', name: '支付成功', desc: '记录订单支付完成' },
|
||||
{ icon: '📦', name: '订单创建', desc: '追踪订单生成' },
|
||||
{ icon: '🔐', name: '用户注册', desc: '记录账号注册' },
|
||||
{ icon: '📊', name: 'API 调用', desc: '统计接口调用次数' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'full',
|
||||
name: '全链路埋点',
|
||||
subtitle: 'Full-funnel Tracking',
|
||||
icon: '🔗',
|
||||
description:
|
||||
'前端埋点 + 后端埋点组合,实现从用户行为到业务完成的端到端追踪。数据最完整,但实现成本最高。',
|
||||
characteristics: [
|
||||
'端到端完整追踪',
|
||||
'数据交叉验证',
|
||||
'前后端数据打通',
|
||||
'漏斗分析更准确',
|
||||
'异常定位更快速'
|
||||
],
|
||||
useCases: [
|
||||
{ icon: '🛒', name: '购物流程', desc: '从浏览到购买的完整链路' },
|
||||
{ icon: '📈', name: '用户旅程', desc: '分析用户全生命周期行为' },
|
||||
{ icon: '🔍', name: '问题排查', desc: '定位前后端异常' },
|
||||
{ icon: '💎', name: '数据治理', desc: '提升数据质量和准确性' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const comparisonData = [
|
||||
{
|
||||
dimension: '数据准确性',
|
||||
values: {
|
||||
frontend: '★★★☆☆',
|
||||
backend: '★★★★★',
|
||||
full: '★★★★★'
|
||||
},
|
||||
best: 'backend'
|
||||
},
|
||||
{
|
||||
dimension: '实时性',
|
||||
values: {
|
||||
frontend: '★★★★★',
|
||||
backend: '★★★★☆',
|
||||
full: '★★★★★'
|
||||
},
|
||||
best: 'frontend'
|
||||
},
|
||||
{
|
||||
dimension: '开发成本',
|
||||
values: {
|
||||
frontend: '★★★☆☆',
|
||||
backend: '★★★☆☆',
|
||||
full: '★☆☆☆☆'
|
||||
},
|
||||
best: 'frontend'
|
||||
},
|
||||
{
|
||||
dimension: '维护成本',
|
||||
values: {
|
||||
frontend: '★★★☆☆',
|
||||
backend: '★★★☆☆',
|
||||
full: '★★☆☆☆'
|
||||
},
|
||||
best: 'frontend'
|
||||
},
|
||||
{
|
||||
dimension: '数据完整性',
|
||||
values: {
|
||||
frontend: '★★★☆☆',
|
||||
backend: '★★★☆☆',
|
||||
full: '★★★★★'
|
||||
},
|
||||
best: 'full'
|
||||
},
|
||||
{
|
||||
dimension: '隐私合规',
|
||||
values: {
|
||||
frontend: '★★☆☆☆',
|
||||
backend: '★★★★★',
|
||||
full: '★★★★☆'
|
||||
},
|
||||
best: 'backend'
|
||||
}
|
||||
]
|
||||
|
||||
const currentType = computed(() => {
|
||||
return trackingTypes.find((t) => t.id === selectedType.value)
|
||||
})
|
||||
|
||||
const selectType = (typeId) => {
|
||||
selectedType.value = typeId
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tracking-types-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin: 2rem 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.type-tabs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.type-tab {
|
||||
padding: 0.75rem 2rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.type-tab:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.type-tab.active {
|
||||
background: var(--vp-c-brand);
|
||||
border-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.type-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.type-info {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.type-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.type-icon {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.type-title .name {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.type-title .subtitle {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.type-description {
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.characteristics,
|
||||
.use-cases {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.characteristics-title,
|
||||
.use-cases-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.characteristics-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.characteristic-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.check {
|
||||
color: #22c55e;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.use-cases-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.use-case-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.use-case-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.use-case-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
|
||||
.use-case-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.type-architecture {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.architecture-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.architecture-diagram {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.diagram-layer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 10px;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.diagram-layer.client,
|
||||
.diagram-layer.server,
|
||||
.diagram-layer.data {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.layer-label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.layer-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.layer-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.layer-box {
|
||||
background: white;
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.layer-box.frontend {
|
||||
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.layer-box.backend {
|
||||
background: linear-gradient(135deg, #fef3c7, #fde68a);
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
.layer-box.full {
|
||||
background: linear-gradient(135deg, #dcfce7, #bbf7d0);
|
||||
border-color: #22c55e;
|
||||
}
|
||||
|
||||
.layer-box.server {
|
||||
background: linear-gradient(135deg, #fce7f3, #fbcfe8);
|
||||
border-color: #ec4899;
|
||||
}
|
||||
|
||||
.layer-box.data {
|
||||
background: linear-gradient(135deg, #e0e7ff, #c7d2fe);
|
||||
border-color: #6366f1;
|
||||
}
|
||||
|
||||
.layer-detail {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.diagram-arrow {
|
||||
font-size: 1.5rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.comparison-table {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.comparison-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.comparison {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.comparison th {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
border-bottom: 2px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.comparison td {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.comparison .dimension {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.comparison .value {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.comparison .value.best {
|
||||
background: #dcfce7;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.best-badge {
|
||||
display: inline-block;
|
||||
margin-left: 0.5rem;
|
||||
padding: 0.15rem 0.5rem;
|
||||
background: #22c55e;
|
||||
color: white;
|
||||
font-size: 0.7rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.type-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.type-tabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user