feat: complete English translation of AI IDE introduction including Appendix 2
This commit is contained in:
@@ -20,7 +20,7 @@ const localeMap = {
|
||||
lang: 'zh-CN',
|
||||
hreflang: 'zh-CN'
|
||||
},
|
||||
'en': {
|
||||
en: {
|
||||
ogLocale: 'en_US',
|
||||
twitterSite: '@datawhale',
|
||||
lang: 'en-US',
|
||||
@@ -1025,7 +1025,7 @@ export default defineConfig({
|
||||
locales: {
|
||||
// 根路径 — 仅用于 404 页面兜底,实际首页由 docs/index.md 自动重定向
|
||||
root: {
|
||||
label: '简体中文',
|
||||
label: '',
|
||||
lang: 'zh-CN',
|
||||
link: '/zh-cn/',
|
||||
themeConfig: {
|
||||
@@ -1778,7 +1778,7 @@ export default defineConfig({
|
||||
},
|
||||
|
||||
// 英文
|
||||
'en': {
|
||||
en: {
|
||||
label: 'English',
|
||||
lang: 'en-US',
|
||||
link: '/en/',
|
||||
@@ -1794,7 +1794,8 @@ export default defineConfig({
|
||||
...commonThemeConfig,
|
||||
notFound: {
|
||||
title: 'Page Not Found',
|
||||
quote: 'The page you are looking for does not exist or has been moved.',
|
||||
quote:
|
||||
'The page you are looking for does not exist or has been moved.',
|
||||
linkText: 'Take me home',
|
||||
linkUrl: '/en/'
|
||||
},
|
||||
@@ -2015,8 +2016,8 @@ export default defineConfig({
|
||||
...commonThemeConfig,
|
||||
notFound: {
|
||||
title: 'Page non trouvée',
|
||||
quote: 'La page que vous recherchez n\'existe pas ou a été déplacée.',
|
||||
linkText: 'Retour à l\'accueil',
|
||||
quote: "La page que vous recherchez n'existe pas ou a été déplacée.",
|
||||
linkText: "Retour à l'accueil",
|
||||
linkUrl: '/fr-fr/'
|
||||
},
|
||||
outline: {
|
||||
@@ -2138,7 +2139,8 @@ export default defineConfig({
|
||||
...commonThemeConfig,
|
||||
notFound: {
|
||||
title: 'Không tìm thấy trang',
|
||||
quote: 'Trang bạn đang tìm kiếm không tồn tại hoặc đã được di chuyển.',
|
||||
quote:
|
||||
'Trang bạn đang tìm kiếm không tồn tại hoặc đã được di chuyển.',
|
||||
linkText: 'Về trang chủ',
|
||||
linkUrl: '/vi-vn/'
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { withBase } from 'vitepress'
|
||||
|
||||
const categories = [
|
||||
@@ -9,14 +9,17 @@ const categories = [
|
||||
icon: '💻',
|
||||
color: '#10b981',
|
||||
bgGradient: 'linear-gradient(135deg, #10b98115, #10b98108)',
|
||||
description: '理解计算机最底层的工作原理',
|
||||
whyLearn: '这是所有软件工程的基础。掌握计算机如何执行代码、管理内存、处理请求,能帮助你写出更高效的代码。',
|
||||
learningGoals: ['CPU 与内存原理', '操作系统核心', '网络通信基础', '数据结构与算法'],
|
||||
articles: [
|
||||
{ title: 'Vibe Coding 全栈开发', path: '/zh-cn/appendix/1-computer-fundamentals/vibe-coding-fullstack' },
|
||||
{ title: '从晶体管到 CPU', path: '/zh-cn/appendix/1-computer-fundamentals/transistor-to-cpu' },
|
||||
{ title: '操作系统', path: '/zh-cn/appendix/1-computer-fundamentals/operating-systems' },
|
||||
{ title: '数据结构', path: '/zh-cn/appendix/1-computer-fundamentals/data-structures' },
|
||||
{ title: '算法思维入门', path: '/zh-cn/appendix/1-computer-fundamentals/algorithm-thinking' },
|
||||
{ title: '编程语言图谱', path: '/zh-cn/appendix/1-computer-fundamentals/programming-languages' },
|
||||
{ title: '网络基础', path: '/zh-cn/appendix/1-computer-fundamentals/computer-networks' }
|
||||
{ title: 'Vibe Coding 全栈开发', path: '/zh-cn/appendix/1-computer-fundamentals/vibe-coding-fullstack', description: 'AI 辅助时代下的全栈开发全景图' },
|
||||
{ title: '从晶体管到 CPU', path: '/zh-cn/appendix/1-computer-fundamentals/transistor-to-cpu', description: '理解计算机最底层的硬件逻辑' },
|
||||
{ title: '操作系统', path: '/zh-cn/appendix/1-computer-fundamentals/operating-systems', description: '进程管理、内存管理、文件系统' },
|
||||
{ title: '数据结构', path: '/zh-cn/appendix/1-computer-fundamentals/data-structures', description: '数组、链表、树、图的组织方式' },
|
||||
{ title: '算法思维入门', path: '/zh-cn/appendix/1-computer-fundamentals/algorithm-thinking', description: '排序、搜索、递归的思维框架' },
|
||||
{ title: '编程语言图谱', path: '/zh-cn/appendix/1-computer-fundamentals/programming-languages', description: '从汇编到高级语言的演进' },
|
||||
{ title: '网络基础', path: '/zh-cn/appendix/1-computer-fundamentals/computer-networks', description: '从网线到互联网的通信原理' }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -25,13 +28,16 @@ const categories = [
|
||||
icon: '🔧',
|
||||
color: '#3b82f6',
|
||||
bgGradient: 'linear-gradient(135deg, #3b82f615, #3b82f608)',
|
||||
description: '熟练使用命令行、Git、IDE 等工具',
|
||||
whyLearn: '工具是开发者的武器。掌握高效的工具使用能让你事半功倍,减少重复劳动。',
|
||||
learningGoals: ['IDE 高效使用', 'Git 版本控制', '命令行操作', '调试与排查'],
|
||||
articles: [
|
||||
{ title: 'IDE 基础', path: '/zh-cn/appendix/2-development-tools/ide-basics' },
|
||||
{ title: '命令行与 Shell', path: '/zh-cn/appendix/2-development-tools/command-line-shell' },
|
||||
{ title: 'Git 版本控制', path: '/zh-cn/appendix/2-development-tools/git-version-control' },
|
||||
{ title: '环境变量与 PATH', path: '/zh-cn/appendix/2-development-tools/environment-path' },
|
||||
{ title: '包管理器', path: '/zh-cn/appendix/2-development-tools/package-managers' },
|
||||
{ title: '调试的艺术', path: '/zh-cn/appendix/2-development-tools/debugging-art/' }
|
||||
{ title: 'IDE 基础', path: '/zh-cn/appendix/2-development-tools/ide-basics', description: 'VS Code、Cursor、Trae 的使用技巧' },
|
||||
{ title: '命令行与 Shell', path: '/zh-cn/appendix/2-development-tools/command-line-shell', description: '终端操作与脚本自动化' },
|
||||
{ title: 'Git 版本控制', path: '/zh-cn/appendix/2-development-tools/git-version-control', description: '版本控制与团队协作' },
|
||||
{ title: '环境变量与 PATH', path: '/zh-cn/appendix/2-development-tools/environment-path', description: '系统环境配置与问题排查' },
|
||||
{ title: '包管理器', path: '/zh-cn/appendix/2-development-tools/package-managers', description: 'npm、pip、cargo 依赖管理' },
|
||||
{ title: '调试的艺术', path: '/zh-cn/appendix/2-development-tools/debugging-art/', description: '断点调试与问题定位' }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -40,13 +46,16 @@ const categories = [
|
||||
icon: '🌍',
|
||||
color: '#f59e0b',
|
||||
bgGradient: 'linear-gradient(135deg, #f59e0b15, #f59e0b08)',
|
||||
description: '掌握浏览器原理和前端开发技术',
|
||||
whyLearn: '浏览器是用户接触软件的入口。理解浏览器如何渲染页面,能帮助你构建更流畅的 Web 应用。',
|
||||
learningGoals: ['浏览器渲染原理', 'JavaScript 核心', '前端框架对比', '前端工程化'],
|
||||
articles: [
|
||||
{ title: 'JavaScript 深入', path: '/zh-cn/appendix/3-browser-and-frontend/javascript-deep-dive' },
|
||||
{ title: 'TypeScript', path: '/zh-cn/appendix/3-browser-and-frontend/typescript' },
|
||||
{ title: '浏览器是一个操作系统', path: '/zh-cn/appendix/3-browser-and-frontend/browser-as-os' },
|
||||
{ title: '浏览器渲染管道', path: '/zh-cn/appendix/3-browser-and-frontend/browser-as-os-rendering' },
|
||||
{ title: '前端框架对比', path: '/zh-cn/appendix/3-browser-and-frontend/frontend-frameworks' },
|
||||
{ title: '前端工程化', path: '/zh-cn/appendix/3-browser-and-frontend/frontend-engineering' }
|
||||
{ title: 'JavaScript 深入', path: '/zh-cn/appendix/3-browser-and-frontend/javascript-deep-dive', description: '闭包、原型链、异步核心概念' },
|
||||
{ title: 'TypeScript', path: '/zh-cn/appendix/3-browser-and-frontend/typescript', description: '类型安全与接口定义' },
|
||||
{ title: '浏览器是一个操作系统', path: '/zh-cn/appendix/3-browser-and-frontend/browser-as-os', description: '进程模型与资源管理' },
|
||||
{ title: '浏览器渲染管道', path: '/zh-cn/appendix/3-browser-and-frontend/browser-as-os-rendering', description: 'DOM、CSSOM、布局与绘制' },
|
||||
{ title: '前端框架对比', path: '/zh-cn/appendix/3-browser-and-frontend/frontend-frameworks', description: 'React、Vue、Svelte、Angular' },
|
||||
{ title: '前端工程化', path: '/zh-cn/appendix/3-browser-and-frontend/frontend-engineering', description: '构建工具与模块化' }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -55,14 +64,17 @@ const categories = [
|
||||
icon: '⚙️',
|
||||
color: '#8b5cf6',
|
||||
bgGradient: 'linear-gradient(135deg, #8b5cf615, #8b5cf608)',
|
||||
description: '构建可靠的后端服务和 API',
|
||||
whyLearn: '后端是应用的神经中枢。学会设计 API、处理数据,能让你独立完成全栈开发。',
|
||||
learningGoals: ['HTTP 协议', 'API 设计原则', '认证与授权', '缓存与消息队列'],
|
||||
articles: [
|
||||
{ title: '后端语言对比', path: '/zh-cn/appendix/4-server-and-backend/backend-languages' },
|
||||
{ title: 'HTTP 协议', path: '/zh-cn/appendix/4-server-and-backend/http-protocol' },
|
||||
{ title: 'API 设计哲学', path: '/zh-cn/appendix/4-server-and-backend/api-design' },
|
||||
{ title: 'Web 框架的本质', path: '/zh-cn/appendix/4-server-and-backend/web-frameworks' },
|
||||
{ title: '认证与授权', path: '/zh-cn/appendix/4-server-and-backend/auth-authorization' },
|
||||
{ title: '缓存策略', path: '/zh-cn/appendix/4-server-and-backend/caching' },
|
||||
{ title: '消息队列', path: '/zh-cn/appendix/4-server-and-backend/message-queues' }
|
||||
{ title: '后端语言对比', path: '/zh-cn/appendix/4-server-and-backend/backend-languages', description: 'Go、Node.js、Python 后端选型' },
|
||||
{ title: 'HTTP 协议', path: '/zh-cn/appendix/4-server-and-backend/http-protocol', description: '请求响应与状态码' },
|
||||
{ title: 'API 设计哲学', path: '/zh-cn/appendix/4-server-and-backend/api-design', description: 'RESTful 与 GraphQL 设计' },
|
||||
{ title: 'Web 框架的本质', path: '/zh-cn/appendix/4-server-and-backend/web-frameworks', description: '路由、中间件、模板引擎' },
|
||||
{ title: '认证与授权', path: '/zh-cn/appendix/4-server-and-backend/auth-authorization', description: 'JWT、OAuth 与权限控制' },
|
||||
{ title: '缓存策略', path: '/zh-cn/appendix/4-server-and-backend/caching', description: 'Redis 与 CDN 缓存' },
|
||||
{ title: '消息队列', path: '/zh-cn/appendix/4-server-and-backend/message-queues', description: 'RabbitMQ、Kafka 应用' }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -71,11 +83,14 @@ const categories = [
|
||||
icon: '📊',
|
||||
color: '#ec4899',
|
||||
bgGradient: 'linear-gradient(135deg, #ec489915, #ec489908)',
|
||||
description: '掌握数据库和数据分析技能',
|
||||
whyLearn: '数据是现代应用的核心资产。学会存储、查询、分析数据,能帮助你做出数据驱动的决策。',
|
||||
learningGoals: ['SQL 查询', '数据库原理', '数据模型设计', '数据分析基础'],
|
||||
articles: [
|
||||
{ title: 'SQL', path: '/zh-cn/appendix/5-data/sql' },
|
||||
{ title: '数据库原理', path: '/zh-cn/appendix/5-data/database-fundamentals' },
|
||||
{ title: '数据模型全景', path: '/zh-cn/appendix/5-data/data-models' },
|
||||
{ title: '数据分析基础', path: '/zh-cn/appendix/5-data/data-analysis' }
|
||||
{ title: 'SQL', path: '/zh-cn/appendix/5-data/sql', description: '查询、聚合与事务' },
|
||||
{ title: '数据库原理', path: '/zh-cn/appendix/5-data/database-fundamentals', description: '索引、事务与隔离级别' },
|
||||
{ title: '数据模型全景', path: '/zh-cn/appendix/5-data/data-models', description: '关系型 vs NoSQL vs NewSQL' },
|
||||
{ title: '数据分析基础', path: '/zh-cn/appendix/5-data/data-analysis', description: 'Excel、SQL 与 BI 可视化' }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -84,11 +99,14 @@ const categories = [
|
||||
icon: '🏗️',
|
||||
color: '#14b8a6',
|
||||
bgGradient: 'linear-gradient(135deg, #14b8a615, #14b8a608)',
|
||||
description: '学习系统设计和架构模式',
|
||||
whyLearn: '架构决定系统的未来。学会从宏观角度设计系统,能让你构建可扩展的大型应用。',
|
||||
learningGoals: ['微服务架构', '分布式系统', '高可用设计', '系统设计方法论'],
|
||||
articles: [
|
||||
{ title: '从单体到微服务', path: '/zh-cn/appendix/6-architecture-and-system-design/monolith-to-microservices' },
|
||||
{ title: '分布式系统', path: '/zh-cn/appendix/6-architecture-and-system-design/distributed-systems' },
|
||||
{ title: '高可用与容灾', path: '/zh-cn/appendix/6-architecture-and-system-design/high-availability' },
|
||||
{ title: '系统设计方法论', path: '/zh-cn/appendix/6-architecture-and-system-design/system-design-methodology' }
|
||||
{ title: '从单体到微服务', path: '/zh-cn/appendix/6-architecture-and-system-design/monolith-to-microservices', description: '服务拆分与架构演进' },
|
||||
{ title: '分布式系统', path: '/zh-cn/appendix/6-architecture-and-system-design/distributed-systems', description: 'CAP 定理与一致性' },
|
||||
{ title: '高可用与容灾', path: '/zh-cn/appendix/6-architecture-and-system-design/high-availability', description: '负载均衡与故障转移' },
|
||||
{ title: '系统设计方法论', path: '/zh-cn/appendix/6-architecture-and-system-design/system-design-methodology', description: '从需求到方案的思路' }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -97,11 +115,14 @@ const categories = [
|
||||
icon: '☁️',
|
||||
color: '#06b6d4',
|
||||
bgGradient: 'linear-gradient(135deg, #06b6d415, #06b6d408)',
|
||||
description: '掌握云原生和运维技能',
|
||||
whyLearn: '基础设施是应用的底座。学会容器化、自动化部署,能让你高效地运维应用。',
|
||||
learningGoals: ['Linux 基础', 'Docker 容器化', 'Kubernetes', 'CI/CD 自动化'],
|
||||
articles: [
|
||||
{ title: 'Linux 基础', path: '/zh-cn/appendix/7-infrastructure-and-operations/linux-basics' },
|
||||
{ title: 'Docker 容器化', path: '/zh-cn/appendix/7-infrastructure-and-operations/docker-containers' },
|
||||
{ title: 'Kubernetes', path: '/zh-cn/appendix/7-infrastructure-and-operations/kubernetes' },
|
||||
{ title: 'CI/CD 自动化', path: '/zh-cn/appendix/7-infrastructure-and-operations/ci-cd' }
|
||||
{ title: 'Linux 基础', path: '/zh-cn/appendix/7-infrastructure-and-operations/linux-basics', description: '文件系统与进程管理' },
|
||||
{ title: 'Docker 容器化', path: '/zh-cn/appendix/7-infrastructure-and-operations/docker-containers', description: '镜像、容器与网络' },
|
||||
{ title: 'Kubernetes', path: '/zh-cn/appendix/7-infrastructure-and-operations/kubernetes', description: 'Pod、Deployment 与 Service' },
|
||||
{ title: 'CI/CD 自动化', path: '/zh-cn/appendix/7-infrastructure-and-operations/ci-cd', description: 'GitHub Actions 与流水线' }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -110,13 +131,16 @@ const categories = [
|
||||
icon: '🤖',
|
||||
color: '#f97316',
|
||||
bgGradient: 'linear-gradient(135deg, #f9731615, #f9731608)',
|
||||
description: '了解 AI 原理和 LLM 应用开发',
|
||||
whyLearn: 'AI 正在改变软件开发的方式。理解大语言模型,能帮助你更好地利用 AI 提升效率。',
|
||||
learningGoals: ['神经网络基础', 'Transformer 架构', 'LLM 原理', 'RAG 与 Agent'],
|
||||
articles: [
|
||||
{ title: 'AI 简史', path: '/zh-cn/appendix/8-artificial-intelligence/ai-history' },
|
||||
{ title: '神经网络', path: '/zh-cn/appendix/8-artificial-intelligence/neural-networks' },
|
||||
{ title: 'Transformer', path: '/zh-cn/appendix/8-artificial-intelligence/transformer-attention' },
|
||||
{ title: '大语言模型原理', path: '/zh-cn/appendix/8-artificial-intelligence/llm-principles' },
|
||||
{ title: 'RAG 架构', path: '/zh-cn/appendix/8-artificial-intelligence/rag' },
|
||||
{ title: 'AI Agent', path: '/zh-cn/appendix/8-artificial-intelligence/ai-agents' }
|
||||
{ title: 'AI 简史', path: '/zh-cn/appendix/8-artificial-intelligence/ai-history', description: '从专家系统到深度学习' },
|
||||
{ title: '神经网络', path: '/zh-cn/appendix/8-artificial-intelligence/neural-networks', description: '感知机与反向传播' },
|
||||
{ title: 'Transformer', path: '/zh-cn/appendix/8-artificial-intelligence/transformer-attention', description: '注意力机制与自注意力' },
|
||||
{ title: '大语言模型原理', path: '/zh-cn/appendix/8-artificial-intelligence/llm-principles', description: '预训练与指令微调' },
|
||||
{ title: 'RAG 架构', path: '/zh-cn/appendix/8-artificial-intelligence/rag', description: '检索增强生成实战' },
|
||||
{ title: 'AI Agent', path: '/zh-cn/appendix/8-artificial-intelligence/ai-agents', description: 'Agent 架构与工具调用' }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -125,24 +149,39 @@ const categories = [
|
||||
icon: '✨',
|
||||
color: '#a855f7',
|
||||
bgGradient: 'linear-gradient(135deg, #a855f715, #a855f708)',
|
||||
description: '提升代码质量和工程实践能力',
|
||||
whyLearn: '代码是写给人看的。掌握设计模式、测试策略,能让你写出更优雅、更易维护的代码。',
|
||||
learningGoals: ['设计模式', '代码重构', '测试策略', '技术写作'],
|
||||
articles: [
|
||||
{ title: '设计模式', path: '/zh-cn/appendix/9-engineering-excellence/design-patterns' },
|
||||
{ title: '代码质量与重构', path: '/zh-cn/appendix/9-engineering-excellence/code-quality-refactoring' },
|
||||
{ title: '测试策略', path: '/zh-cn/appendix/9-engineering-excellence/testing-strategies' },
|
||||
{ title: '技术写作', path: '/zh-cn/appendix/9-engineering-excellence/technical-writing' },
|
||||
{ title: '开源协作', path: '/zh-cn/appendix/9-engineering-excellence/open-source-collaboration' }
|
||||
{ title: '设计模式', path: '/zh-cn/appendix/9-engineering-excellence/design-patterns', description: 'SOLID 原则与 23 种模式' },
|
||||
{ title: '代码质量与重构', path: '/zh-cn/appendix/9-engineering-excellence/code-quality-refactoring', description: '坏味道与重构手法' },
|
||||
{ title: '测试策略', path: '/zh-cn/appendix/9-engineering-excellence/testing-strategies', description: '单元测试、集成测试、E2E' },
|
||||
{ title: '技术写作', path: '/zh-cn/appendix/9-engineering-excellence/technical-writing', description: '文档与 API 编写规范' },
|
||||
{ title: '开源协作', path: '/zh-cn/appendix/9-engineering-excellence/open-source-collaboration', description: 'Issue、PR 与社区参与' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const activeCategory = ref(null)
|
||||
const hoveredArticle = ref(null)
|
||||
const showDetail = ref(true)
|
||||
|
||||
const toggleCategory = (id) => {
|
||||
activeCategory.value = activeCategory.value === id ? null : id
|
||||
showDetail.value = true
|
||||
}
|
||||
|
||||
const articleCount = categories.reduce((sum, cat) => sum + cat.articles.length, 0)
|
||||
|
||||
const activeCategoryData = computed(() => {
|
||||
if (!activeCategory.value) return null
|
||||
return categories.find(cat => cat.id === activeCategory.value)
|
||||
})
|
||||
|
||||
const hoveredArticleData = computed(() => {
|
||||
if (!hoveredArticle.value || !activeCategoryData.value) return null
|
||||
return activeCategoryData.value.articles.find(article => article.path === hoveredArticle.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -152,45 +191,89 @@ const articleCount = categories.reduce((sum, cat) => sum + cat.articles.length,
|
||||
<p class="bento-subtitle">9 个主题方向 · {{ articleCount }} 篇文章</p>
|
||||
</div>
|
||||
|
||||
<div class="bento-grid">
|
||||
<div
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
class="bento-card"
|
||||
:class="{ active: activeCategory === category.id }"
|
||||
:style="{
|
||||
'--card-color': category.color,
|
||||
'--card-bg': category.bgGradient
|
||||
}"
|
||||
@click="toggleCategory(category.id)"
|
||||
>
|
||||
<div class="card-icon">{{ category.icon }}</div>
|
||||
<div class="card-content">
|
||||
<h4 class="card-title">{{ category.name }}</h4>
|
||||
<p class="card-count">{{ category.articles.length }} 篇</p>
|
||||
</div>
|
||||
|
||||
<Transition name="pop">
|
||||
<div v-if="activeCategory === category.id" class="card-articles">
|
||||
<a
|
||||
v-for="article in category.articles"
|
||||
:key="article.path"
|
||||
:href="withBase(article.path)"
|
||||
class="article-item"
|
||||
@mouseenter="hoveredArticle = article.path"
|
||||
@mouseleave="hoveredArticle = null"
|
||||
>
|
||||
<span class="article-dot"></span>
|
||||
<span class="article-title">{{ article.title }}</span>
|
||||
<span class="article-arrow">→</span>
|
||||
</a>
|
||||
<div class="bento-main">
|
||||
<div class="bento-grid">
|
||||
<div
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
class="bento-card"
|
||||
:class="{ active: activeCategory === category.id }"
|
||||
:style="{
|
||||
'--card-color': category.color,
|
||||
'--card-bg': category.bgGradient
|
||||
}"
|
||||
@click="toggleCategory(category.id)"
|
||||
>
|
||||
<div class="card-icon">{{ category.icon }}</div>
|
||||
<div class="card-content">
|
||||
<h4 class="card-title">{{ category.name }}</h4>
|
||||
<p class="card-count">{{ category.articles.length }} 篇</p>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<div class="card-indicator" v-if="activeCategory !== category.id">
|
||||
<span>{{ category.articles.length }} 篇 →</span>
|
||||
<div class="card-indicator" v-if="activeCategory !== category.id">
|
||||
<span>{{ category.articles.length }} 篇 →</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Transition name="slide">
|
||||
<div
|
||||
v-if="activeCategoryData"
|
||||
class="detail-panel"
|
||||
:style="{ '--panel-color': activeCategoryData.color }"
|
||||
>
|
||||
<!-- 头部信息 -->
|
||||
<div class="panel-header">
|
||||
<div class="panel-title-row">
|
||||
<span class="panel-icon">{{ hoveredArticleData ? '📄' : activeCategoryData.icon }}</span>
|
||||
<div class="panel-title-group">
|
||||
<h4 class="panel-title">{{ hoveredArticleData?.title || activeCategoryData.name }}</h4>
|
||||
<p class="panel-desc">{{ hoveredArticleData?.description || activeCategoryData.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类介绍 -->
|
||||
<div v-if="!hoveredArticleData" class="panel-intro">
|
||||
<p class="intro-text">{{ activeCategoryData.whyLearn }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 学习目标 -->
|
||||
<div v-if="!hoveredArticleData" class="panel-goals">
|
||||
<h5 class="goals-title">能学到什么?</h5>
|
||||
<div class="goals-list">
|
||||
<span v-for="(goal, index) in activeCategoryData.learningGoals" :key="index" class="goal-tag">
|
||||
{{ goal }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文章列表区 -->
|
||||
<div class="panel-articles">
|
||||
<div class="articles-header">
|
||||
<span class="articles-icon">{{ activeCategoryData.icon }}</span>
|
||||
<span class="articles-title">文章列表 ({{ activeCategoryData.articles.length }}篇)</span>
|
||||
</div>
|
||||
<div class="articles-list-scroll">
|
||||
<a
|
||||
v-for="article in activeCategoryData.articles"
|
||||
:key="article.path"
|
||||
:href="withBase(article.path)"
|
||||
class="article-item"
|
||||
:class="{ hover: hoveredArticle === article.path }"
|
||||
@mouseenter="hoveredArticle = article.path"
|
||||
@mouseleave="hoveredArticle = null"
|
||||
>
|
||||
<span class="article-bullet"></span>
|
||||
<div class="article-info">
|
||||
<span class="article-name">{{ article.title }}</span>
|
||||
<span class="article-desc">{{ article.description }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -219,24 +302,19 @@ const articleCount = categories.reduce((sum, cat) => sum + cat.articles.length,
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bento-main {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 360px;
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.bento-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.bento-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.bento-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.bento-card {
|
||||
position: relative;
|
||||
background: var(--vp-c-bg);
|
||||
@@ -269,7 +347,6 @@ const articleCount = categories.reduce((sum, cat) => sum + cat.articles.length,
|
||||
|
||||
.bento-card.active {
|
||||
border-color: var(--card-color);
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.bento-card.active::before {
|
||||
@@ -312,79 +389,210 @@ const articleCount = categories.reduce((sum, cat) => sum + cat.articles.length,
|
||||
color: var(--card-color);
|
||||
}
|
||||
|
||||
.card-articles {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
/* 右侧面板 */
|
||||
.detail-panel {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 15px;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
z-index: 2;
|
||||
border: 1px solid var(--panel-color);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
max-height: 520px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.panel-title-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.panel-icon {
|
||||
font-size: 1.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel-title-group {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin: 0 0 0.25rem;
|
||||
}
|
||||
|
||||
.panel-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.panel-intro {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 学习目标 */
|
||||
.panel-goals {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.goals-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--panel-color);
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.goals-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.goal-tag {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.3rem 0.6rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
/* 文章列表区 */
|
||||
.panel-articles {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.articles-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.articles-icon {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.articles-title {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--panel-color);
|
||||
}
|
||||
|
||||
.articles-list-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.article-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem 0.75rem;
|
||||
align-items: flex-start;
|
||||
gap: 0.6rem;
|
||||
padding: 0.6rem;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: all 0.15s ease;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.article-item:hover {
|
||||
.article-item:hover,
|
||||
.article-item.hover {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.article-dot {
|
||||
.article-bullet {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--card-color);
|
||||
background: var(--panel-color);
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.article-title {
|
||||
.article-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.article-name {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.article-arrow {
|
||||
opacity: 0;
|
||||
color: var(--card-color);
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.9rem;
|
||||
.article-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.article-item:hover .article-arrow {
|
||||
opacity: 1;
|
||||
transform: translateX(3px);
|
||||
/* 动画 */
|
||||
.slide-enter-active {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.pop-enter-active {
|
||||
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.pop-leave-active {
|
||||
.slide-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pop-enter-from {
|
||||
.slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.pop-leave-to {
|
||||
.slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 1100px) {
|
||||
.bento-main {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.detail-panel {
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.bento-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.bento-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,275 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeOp = ref('groupBy')
|
||||
|
||||
const rawOrders = [
|
||||
{ userId: 'U001', orderId: 'ORD001', amount: 100, date: '2024-01-01' },
|
||||
{ userId: 'U001', orderId: 'ORD002', amount: 200, date: '2024-01-02' },
|
||||
{ userId: 'U002', orderId: 'ORD003', amount: 150, date: '2024-01-01' },
|
||||
{ userId: 'U002', orderId: 'ORD004', amount: 300, date: '2024-01-03' },
|
||||
{ userId: 'U003', orderId: 'ORD005', amount: 250, date: '2024-01-02' },
|
||||
{ userId: 'U001', orderId: 'ORD006', amount: 180, date: '2024-01-04' }
|
||||
]
|
||||
|
||||
const ops = {
|
||||
groupBy: {
|
||||
name: '按用户分组',
|
||||
sql: `SELECT user_id, COUNT(*) as order_count, SUM(amount) as total
|
||||
FROM orders GROUP BY user_id;`,
|
||||
columns: ['用户 ID', '订单数', '总金额'],
|
||||
data: [
|
||||
{ '用户 ID': 'U001', 订单数: 3, 总金额: 480 },
|
||||
{ '用户 ID': 'U002', 订单数: 2, 总金额: 450 },
|
||||
{ '用户 ID': 'U003', 订单数: 1, 总金额: 250 }
|
||||
]
|
||||
},
|
||||
sum: {
|
||||
name: '总销售额',
|
||||
sql: `SELECT SUM(amount) as total_sales FROM orders;`,
|
||||
columns: ['总销售额'],
|
||||
data: [{ 总销售额: 1180 }]
|
||||
},
|
||||
avg: {
|
||||
name: '平均订单额',
|
||||
sql: `SELECT AVG(amount) as avg_amount FROM orders;`,
|
||||
columns: ['平均订单额'],
|
||||
data: [{ 平均订单额: 196.67 }]
|
||||
},
|
||||
max: {
|
||||
name: '最大订单额',
|
||||
sql: `SELECT MAX(amount) as max_amount FROM orders;`,
|
||||
columns: ['最大订单额'],
|
||||
data: [{ 最大订单额: 300 }]
|
||||
}
|
||||
}
|
||||
|
||||
const opKeys = Object.keys(ops)
|
||||
const currentOp = computed(() => ops[activeOp.value])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="agg-demo">
|
||||
<div class="demo-header">
|
||||
<span class="icon">🧮</span>
|
||||
<span class="title">数据聚合演示</span>
|
||||
<span class="subtitle">拆分-计算-组合</span>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
"所有用户平均转化率 5%" 往往毫无意义。通过
|
||||
<span class="hl">分组聚合</span>
|
||||
把数据"切开",才能发现不同用户之间的真实差异。点击下方操作,观察同一份原始数据如何产生不同的
|
||||
<span class="hl">聚合视角</span>。
|
||||
</div>
|
||||
|
||||
<!-- 原始数据表 -->
|
||||
<div class="section">
|
||||
<div class="section-label">原始订单数据</div>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用户 ID</th>
|
||||
<th>订单号</th>
|
||||
<th>金额(元)</th>
|
||||
<th>日期</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="r in rawOrders" :key="r.orderId">
|
||||
<td>{{ r.userId }}</td>
|
||||
<td>{{ r.orderId }}</td>
|
||||
<td>{{ r.amount }}</td>
|
||||
<td>{{ r.date }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="ops-row">
|
||||
<button
|
||||
v-for="k in opKeys"
|
||||
:key="k"
|
||||
:class="['op-btn', { active: activeOp === k }]"
|
||||
@click="activeOp = k"
|
||||
>
|
||||
{{ ops[k].name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 聚合结果 -->
|
||||
<div class="section result-section">
|
||||
<div class="section-label">{{ currentOp.name }} 结果</div>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="col in currentOp.columns" :key="col">{{ col }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, i) in currentOp.data" :key="i">
|
||||
<td v-for="col in currentOp.columns" :key="col">
|
||||
{{ row[col] }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="sql-block">
|
||||
<div class="sql-label">SQL 示例</div>
|
||||
<pre class="sql-code">{{ currentOp.sql }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.agg-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
margin: 24px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
padding: 14px 20px;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
padding: 16px 20px;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.7;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.hl {
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: var(--vp-c-bg-alt);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.ops-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 0 20px 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.op-btn {
|
||||
padding: 8px 14px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.op-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.op-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
border-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.sql-block {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.sql-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.sql-code {
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
font-family: 'Menlo', 'Monaco', monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ops-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,124 +1,95 @@
|
||||
<template>
|
||||
<div class="demo data-tracking-demo">
|
||||
<div class="header">
|
||||
<span class="title">数据埋点与采集演示</span>
|
||||
</div>
|
||||
|
||||
<!-- Overview Diagram -->
|
||||
<div v-if="activeTab === 'overview'" class="content">
|
||||
<div class="overview-container">
|
||||
<div class="app-screen">
|
||||
<div class="app-header">电商 App</div>
|
||||
<div class="app-body">
|
||||
<div class="product-card">
|
||||
<div class="product-img"></div>
|
||||
<div class="product-info">新款手机</div>
|
||||
<div class="product-btn">点击购买</div>
|
||||
</div>
|
||||
<!-- Animated click cursor and ripple -->
|
||||
<div class="animation-cursor"></div>
|
||||
<div class="animation-ripple"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-flow">
|
||||
<div class="flow-line"></div>
|
||||
<div class="data-packet">
|
||||
<span class="bracket">{</span>
|
||||
<div class="packet-lines">
|
||||
<div class="pline">e: "click_buy"</div>
|
||||
<div class="pline">u: "user123"</div>
|
||||
</div>
|
||||
<span class="bracket">}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="server-db">
|
||||
<div class="server-header">后端分析系统</div>
|
||||
<div class="server-body">
|
||||
<div class="db-row">user123 | click_buy | 10:05</div>
|
||||
<div class="db-row skeleton"></div>
|
||||
<div class="db-row skeleton"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="desc">用户每一次关键操作都在底层触发了一个埋点事件,飞掠网络被永远记录在案。</p>
|
||||
</div>
|
||||
|
||||
<!-- Methods Compare -->
|
||||
<!-- Methods: 同一场景,三种方式各自捕获到什么 -->
|
||||
<div v-if="activeTab === 'methods'" class="content">
|
||||
<div class="methods-compare">
|
||||
<div class="method-card">
|
||||
<div class="method-title">代码埋点 (Code)</div>
|
||||
<div class="method-body">
|
||||
<div class="code-block">tracker.track('buy', { price: 299 })</div>
|
||||
<div class="method-pro">极度精准、深入业务字段</div>
|
||||
<div class="method-con">需要开发排期,成本高</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="method-card">
|
||||
<div class="method-title">可视化埋点 (Visual)</div>
|
||||
<div class="method-body">
|
||||
<div class="visual-tool">
|
||||
<div class="v-box selected"></div>
|
||||
<div class="v-box"></div>
|
||||
</div>
|
||||
<div class="method-pro">产品经理可自行圈选</div>
|
||||
<div class="method-con">只能抓取表层点击,无法获取深层属性</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="method-card">
|
||||
<div class="method-title">全埋点 (Auto)</div>
|
||||
<div class="method-body">
|
||||
<div class="auto-tool">
|
||||
<div class="noise-line"></div>
|
||||
<div class="noise-line"></div>
|
||||
<div class="noise-line"></div>
|
||||
</div>
|
||||
<div class="method-pro">无死角全量捕捉</div>
|
||||
<div class="method-con">数据如同雪花般庞大,无用噪音极多</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scenario-bar">场景:用户在电商 App 点击了「加入购物车」按钮</div>
|
||||
<table class="capture-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-dim">捕获到的信息</th>
|
||||
<th>代码埋点</th>
|
||||
<th>可视化埋点</th>
|
||||
<th>全埋点</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in captureRows" :key="row.label">
|
||||
<td class="col-dim">{{ row.label }}</td>
|
||||
<td><span :class="row.code ? 'yes' : 'no'">{{ row.code ? '✔' : '✘' }}</span></td>
|
||||
<td><span :class="row.visual ? 'yes' : 'no'">{{ row.visual ? '✔' : '✘' }}</span></td>
|
||||
<td><span :class="row.auto ? 'yes' : 'no'">{{ row.auto ? '✔' : '✘' }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="capture-footer">
|
||||
<span class="cf-item"><span class="yes">✔</span> 能捕获</span>
|
||||
<span class="cf-item"><span class="no">✘</span> 无法捕获</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Model -->
|
||||
<!-- Model: 点击模拟,看 JSON 逐行组装 -->
|
||||
<div v-if="activeTab === 'model'" class="content">
|
||||
<div class="model-container">
|
||||
<div class="json-code">
|
||||
{
|
||||
<span class="key">"event_name"</span>: <span class="string">"add_to_cart"</span>, <span class="comment">// 发生了什么 (What)</span>
|
||||
<span class="key">"timestamp"</span>: <span class="number">1723456789000</span>, <span class="comment">// 什么时候 (When)</span>
|
||||
<span class="key">"user_id"</span>: <span class="string">"u_98765"</span>, <span class="comment">// 是谁 (Who)</span>
|
||||
|
||||
<span class="key">"common_props"</span>: { <span class="comment">// 在哪里/环境 (Where & How)</span>
|
||||
<span class="key">"device"</span>: <span class="string">"iPhone 15Pro"</span>,
|
||||
<span class="key">"network"</span>: <span class="string">"5G"</span>,
|
||||
<span class="key">"os"</span>: <span class="string">"iOS 17"</span>
|
||||
},
|
||||
|
||||
<span class="key">"custom_props"</span>: { <span class="comment">// 业务详情 (Details)</span>
|
||||
<span class="key">"product_id"</span>: <span class="string">"p_001"</span>,
|
||||
<span class="key">"price"</span>: <span class="number">7999.00</span>
|
||||
}
|
||||
}
|
||||
<div class="sim-header">
|
||||
<button class="sim-btn" @click="runSimulation" :disabled="simRunning">
|
||||
{{ simRunning ? '记录生成中...' : '模拟:用户点击「加入购物车」' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="json-build">
|
||||
<div class="json-line" v-for="(line, i) in jsonLines" :key="i"
|
||||
:class="{ visible: simStep > i, highlight: simStep === i + 1 }">
|
||||
<span class="line-tag" :style="{ background: line.color }">{{ line.tag }}</span>
|
||||
<code>{{ line.code }}</code>
|
||||
</div>
|
||||
</div>
|
||||
<p class="desc">每一个标准事件都必须回答 4W1H:Who, What, When, Where, How。</p>
|
||||
<div class="sim-hint" v-if="simStep === 0">点击上方按钮,观察一条埋点记录是如何被组装出来的</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Pipeline -->
|
||||
<!-- Pipeline: 动画数据流 -->
|
||||
<div v-if="activeTab === 'pipeline'" class="content">
|
||||
<div class="pipeline-flow">
|
||||
<div class="pipe-node">App 客户端</div>
|
||||
<div class="pipe-arrow">本地缓存<br>批量上报</div>
|
||||
<div class="pipe-node server">接入网关</div>
|
||||
<div class="pipe-arrow">消息队列</div>
|
||||
<div class="pipe-node etl">清洗 (ETL)</div>
|
||||
<div class="pipe-arrow">入库</div>
|
||||
<div class="pipe-node db">数据仓库</div>
|
||||
</div>
|
||||
<p class="desc">数据并非立刻入库,为了抵御高并发和弱网环境,它必须经历缓存、打包、列队和清洗的漫长流水线。</p>
|
||||
<div class="pipe-visual">
|
||||
<div class="pipe-stage" v-for="(s, i) in pipeStages" :key="i">
|
||||
<div class="stage-icon" :style="{ background: s.bg }">{{ s.icon }}</div>
|
||||
<div class="stage-name">{{ s.name }}</div>
|
||||
</div>
|
||||
<div class="pipe-track">
|
||||
<div class="packet" :class="{ flying: pipeFlying }"
|
||||
v-for="n in 3" :key="n"
|
||||
:style="{ animationDelay: (n - 1) * 0.6 + 's' }">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sim-btn pipe-btn" @click="startPipeAnim">
|
||||
{{ pipeFlying ? '传输中...' : '模拟:发送一批数据' }}
|
||||
</button>
|
||||
<div class="pipe-legend">
|
||||
<span v-for="(s, i) in pipeStages" :key="i" class="legend-item">
|
||||
<span class="legend-dot" :style="{ background: s.bg }"></span>{{ s.label }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ETL: before / after 数据对比 -->
|
||||
<div v-if="activeTab === 'overview'" class="content">
|
||||
<div class="etl-compare">
|
||||
<div class="etl-side etl-before">
|
||||
<div class="etl-side-title">原始数据(服务器收到的)</div>
|
||||
<div class="etl-row-data" v-for="(r, i) in rawData" :key="i" :class="r.issue">
|
||||
<code>{{ r.text }}</code>
|
||||
<span class="issue-tag" v-if="r.tag">{{ r.tag }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="etl-arrow-col">
|
||||
<div class="etl-arrow-label">ETL 清洗</div>
|
||||
<div class="etl-arrow-icon">→</div>
|
||||
</div>
|
||||
<div class="etl-side etl-after">
|
||||
<div class="etl-side-title">清洗后(写入数据仓库的)</div>
|
||||
<div class="etl-row-data clean" v-for="(r, i) in cleanData" :key="i">
|
||||
<code>{{ r }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -128,13 +99,77 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
tab: {
|
||||
type: String,
|
||||
default: 'overview'
|
||||
}
|
||||
tab: { type: String, default: 'overview' }
|
||||
})
|
||||
|
||||
const activeTab = ref(props.tab)
|
||||
|
||||
// === Methods tab: 同一场景,三种方式各自能捕获什么 ===
|
||||
const captureRows = [
|
||||
{ label: '点击了哪个按钮', code: true, visual: true, auto: true },
|
||||
{ label: '点击发生的时间', code: true, visual: true, auto: true },
|
||||
{ label: '用户停留了多久', code: false, visual: false, auto: true },
|
||||
{ label: '商品名称 / 价格', code: true, visual: false, auto: false },
|
||||
{ label: '用了哪张优惠券', code: true, visual: false, auto: false },
|
||||
{ label: '账户余额', code: true, visual: false, auto: false },
|
||||
{ label: '页面滑动轨迹', code: false, visual: false, auto: true }
|
||||
]
|
||||
|
||||
// === Model tab: 模拟 JSON 逐行组装 ===
|
||||
const simStep = ref(0)
|
||||
const simRunning = ref(false)
|
||||
const jsonLines = [
|
||||
{ tag: 'What', color: '#10b981', code: '"event": "add_to_cart"' },
|
||||
{ tag: 'Who', color: '#3b82f6', code: '"user_id": "u_98765"' },
|
||||
{ tag: 'When', color: '#8b5cf6', code: '"time": "2025-08-12T10:33:09Z"' },
|
||||
{ tag: 'Where', color: '#f59e0b', code: '"device": "iPhone 15", "network": "5G"' },
|
||||
{ tag: 'What', color: '#10b981', code: '"product": "新款手机", "price": 2999' }
|
||||
]
|
||||
|
||||
function runSimulation() {
|
||||
if (simRunning.value) return
|
||||
simRunning.value = true
|
||||
simStep.value = 0
|
||||
let i = 0
|
||||
const timer = setInterval(() => {
|
||||
i++
|
||||
simStep.value = i
|
||||
if (i >= jsonLines.length) {
|
||||
clearInterval(timer)
|
||||
simRunning.value = false
|
||||
}
|
||||
}, 600)
|
||||
}
|
||||
|
||||
// === Pipeline tab: 动画数据流 ===
|
||||
const pipeFlying = ref(false)
|
||||
const pipeStages = [
|
||||
{ icon: '📱', name: '手机', label: '产生数据', bg: '#e0f2fe' },
|
||||
{ icon: '📦', name: '打包', label: '攒一批', bg: '#fef08a' },
|
||||
{ icon: '🌐', name: '发送', label: '网络传输', bg: '#fed7aa' },
|
||||
{ icon: '🚦', name: '排队', label: '消息队列', bg: '#fecaca' },
|
||||
{ icon: '🗄️', name: '入库', label: '数据仓库', bg: '#bbf7d0' }
|
||||
]
|
||||
|
||||
function startPipeAnim() {
|
||||
if (pipeFlying.value) return
|
||||
pipeFlying.value = true
|
||||
setTimeout(() => { pipeFlying.value = false }, 3000)
|
||||
}
|
||||
|
||||
// === ETL tab: before / after 对比 ===
|
||||
const rawData = [
|
||||
{ text: 'id-001 userId: "zhang" add_to_cart ¥2999', issue: '', tag: '' },
|
||||
{ text: 'id-001 userId: "zhang" add_to_cart ¥2999', issue: 'dup', tag: '重复' },
|
||||
{ text: 'id-002 user_id: "li" click_buy ¥0', issue: '', tag: '' },
|
||||
{ text: 'id-003 userId: "wang" pay 1970-01-01', issue: 'bad', tag: '时间异常' },
|
||||
{ text: 'id-004 user_id: "zhao" click_buy ¥599', issue: '', tag: '' }
|
||||
]
|
||||
|
||||
const cleanData = [
|
||||
'id-001 user_id: "zhang" add_to_cart ¥2999',
|
||||
'id-002 user_id: "li" click_buy ¥0',
|
||||
'id-004 user_id: "zhao" click_buy ¥599'
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -144,20 +179,6 @@ const activeTab = ref(props.tab)
|
||||
background: var(--vp-c-bg-soft);
|
||||
margin: 24px 0;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 14px 20px;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -165,326 +186,348 @@ const activeTab = ref(props.tab)
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
.dark .content {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
/* Overview Styles & Animations */
|
||||
.overview-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.app-screen {
|
||||
width: 140px;
|
||||
height: 220px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
border: 4px solid #333;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
.sim-btn {
|
||||
display: block;
|
||||
margin: 0 auto 20px;
|
||||
padding: 10px 24px;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.sim-btn:hover:not(:disabled) { background: #2563eb; }
|
||||
.sim-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
|
||||
/* === Methods: Capture Table === */
|
||||
.scenario-bar {
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.app-body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
border: 1px solid #eee;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.product-img {
|
||||
height: 60px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
font-size: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.product-btn {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@keyframes cursor-move {
|
||||
0% { transform: translate(60px, 180px); opacity: 0; }
|
||||
20% { opacity: 1; }
|
||||
40% { transform: translate(60px, 120px); }
|
||||
50% { transform: translate(60px, 120px) scale(0.9); }
|
||||
60% { transform: translate(60px, 120px); }
|
||||
80% { opacity: 1; }
|
||||
100% { transform: translate(60px, 180px); opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes ripple-effect {
|
||||
0% { transform: scale(0.5); opacity: 1; }
|
||||
100% { transform: scale(2); opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes packet-fly {
|
||||
0% { left: 0; opacity: 0; }
|
||||
10% { opacity: 1; left: 0;}
|
||||
90% { left: 100%; opacity: 1; }
|
||||
100% { left: 100%; opacity: 0; }
|
||||
}
|
||||
|
||||
.animation-cursor {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 12px; height: 12px;
|
||||
background: #1e293b;
|
||||
border-radius: 50%;
|
||||
animation: cursor-move 3s infinite;
|
||||
}
|
||||
|
||||
.animation-ripple {
|
||||
position: absolute;
|
||||
top: 120px; left: 60px;
|
||||
width: 20px; height: 20px;
|
||||
border: 2px solid #ef4444;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
animation: ripple-effect 3s infinite;
|
||||
animation-delay: 1.5s;
|
||||
}
|
||||
|
||||
.data-flow {
|
||||
flex: 1;
|
||||
height: 60px;
|
||||
position: relative;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.flow-line {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0; right: 0;
|
||||
height: 2px;
|
||||
background: dashed 2px #cbd5e1;
|
||||
}
|
||||
|
||||
.data-packet {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transform: translateY(-5px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
background: #e0f2fe;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
font-size: 10px;
|
||||
color: #0369a1;
|
||||
animation: packet-fly 3s infinite;
|
||||
animation-delay: 1.5s;
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.server-db {
|
||||
width: 160px;
|
||||
background: #1e293b;
|
||||
.capture-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.server-header {
|
||||
background: #334155;
|
||||
color: white;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.server-body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.db-row {
|
||||
background: #475569;
|
||||
color: #94a3b8;
|
||||
padding: 4px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.db-row.skeleton {
|
||||
height: 14px;
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
/* Methods Compare */
|
||||
.methods-compare {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.method-card {
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.method-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
color: #1e293b;
|
||||
.capture-table th,
|
||||
.capture-table td {
|
||||
padding: 10px 14px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #1e293b;
|
||||
color: #cbd5e1;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 10px;
|
||||
margin-bottom: 12px;
|
||||
.capture-table th {
|
||||
background: #f8fafc;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.visual-tool {
|
||||
background: #f1f5f9;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 12px;
|
||||
.col-dim {
|
||||
text-align: left !important;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.yes { color: #16a34a; font-weight: 700; }
|
||||
.no { color: #dc2626; opacity: 0.4; }
|
||||
|
||||
.capture-footer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.v-box {
|
||||
width: 20px; height: 20px;
|
||||
background: #cbd5e1;
|
||||
border-radius: 2px;
|
||||
.cf-item { display: flex; align-items: center; gap: 4px; }
|
||||
|
||||
/* === Model: JSON 逐行组装 === */
|
||||
.sim-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.v-box.selected {
|
||||
border: 2px dashed #ef4444;
|
||||
background: #fee2e2;
|
||||
}
|
||||
|
||||
.auto-tool {
|
||||
background: #f1f5f9;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 12px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.noise-line {
|
||||
height: 4px;
|
||||
background: #cbd5e1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.method-pro, .method-con {
|
||||
font-size: 11px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.method-pro {
|
||||
color: #16a34a;
|
||||
}
|
||||
.method-pro::before { content: "优势:"; font-weight: bold; }
|
||||
.method-con {
|
||||
color: #dc2626;
|
||||
}
|
||||
.method-con::before { content: "劣势:"; font-weight: bold; }
|
||||
|
||||
/* JSON Model */
|
||||
.model-container {
|
||||
.json-build {
|
||||
background: #1e293b;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
overflow-x: auto;
|
||||
padding: 20px 24px;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.json-code {
|
||||
.json-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 0;
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
|
||||
.json-line.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.json-line.highlight {
|
||||
background: rgba(56, 189, 248, 0.08);
|
||||
border-radius: 4px;
|
||||
margin: 0 -8px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.line-tag {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
min-width: 44px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.json-line code {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 13px;
|
||||
color: #cbd5e1;
|
||||
line-height: 1.6;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.key { color: #38bdf8; }
|
||||
.string { color: #a3e635; }
|
||||
.number { color: #f472b6; }
|
||||
.comment { color: #64748b; font-style: italic; }
|
||||
.sim-hint {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* Pipeline */
|
||||
.pipeline-flow {
|
||||
/* === Pipeline: 动画数据流 === */
|
||||
.pipe-visual {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
padding: 28px 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.pipe-stage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.stage-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
overflow-x: auto;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.pipe-node {
|
||||
padding: 12px 16px;
|
||||
background: #e0f2fe;
|
||||
color: #0369a1;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border: 1px solid #bae6fd;
|
||||
text-align: center;
|
||||
.stage-name {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.pipe-node.server { background: #fef08a; color: #854d0e; border-color: #fde047; }
|
||||
.pipe-node.etl { background: #fed7aa; color: #9a3412; border-color: #fdba74; }
|
||||
.pipe-node.db { background: #bbf7d0; color: #166534; border-color: #86efac; }
|
||||
.pipe-track {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 60px;
|
||||
right: 60px;
|
||||
height: 3px;
|
||||
background: #e2e8f0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
.pipe-arrow {
|
||||
position: relative;
|
||||
font-size: 10px;
|
||||
.packet {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #3b82f6;
|
||||
border-radius: 50%;
|
||||
top: -3.5px;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.packet.flying {
|
||||
animation: fly-across 2.4s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fly-across {
|
||||
0% { left: 0; opacity: 0; }
|
||||
5% { opacity: 1; }
|
||||
90% { opacity: 1; }
|
||||
100% { left: 100%; opacity: 0; }
|
||||
}
|
||||
|
||||
.pipe-btn {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pipe-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pipe-arrow::after {
|
||||
content: "→";
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* === ETL: Before / After 对比 === */
|
||||
.etl-compare {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
align-items: stretch;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.etl-side {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.etl-before {
|
||||
background: #fefce8;
|
||||
}
|
||||
|
||||
.etl-after {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.etl-side-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
.etl-before .etl-side-title { color: #854d0e; }
|
||||
.etl-after .etl-side-title { color: #166534; }
|
||||
|
||||
.etl-arrow-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 8px;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.etl-arrow-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.etl-arrow-icon {
|
||||
font-size: 22px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.etl-row-data {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 11px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.etl-row-data:last-child { margin-bottom: 0; }
|
||||
|
||||
.etl-row-data.dup {
|
||||
background: #fef2f2;
|
||||
text-decoration: line-through;
|
||||
color: #991b1b;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.etl-row-data.bad {
|
||||
background: #fff7ed;
|
||||
color: #9a3412;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.etl-row-data.clean {
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.issue-tag {
|
||||
font-family: sans-serif;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
background: #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 640px) {
|
||||
.capture-table { font-size: 12px; }
|
||||
.capture-table th,
|
||||
.capture-table td { padding: 8px 8px; }
|
||||
|
||||
.etl-compare { flex-direction: column; }
|
||||
|
||||
.etl-arrow-col {
|
||||
flex-direction: row;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.pipe-visual { padding: 20px 12px; }
|
||||
.stage-name { font-size: 10px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const dataInput = ref('23, 45, 67, 89, 12, 34, 56, 78, 90, 21')
|
||||
|
||||
const rawData = computed(() =>
|
||||
dataInput.value
|
||||
.split(',')
|
||||
.map((s) => parseFloat(s.trim()))
|
||||
.filter((n) => !isNaN(n))
|
||||
)
|
||||
|
||||
const sortedData = computed(() => [...rawData.value].sort((a, b) => a - b))
|
||||
const count = computed(() => rawData.value.length)
|
||||
|
||||
const mean = computed(() => {
|
||||
if (!count.value) return 0
|
||||
return (rawData.value.reduce((a, b) => a + b, 0) / count.value).toFixed(2)
|
||||
})
|
||||
|
||||
const median = computed(() => {
|
||||
const s = sortedData.value
|
||||
const n = s.length
|
||||
if (!n) return 0
|
||||
return n % 2 === 0
|
||||
? ((s[n / 2 - 1] + s[n / 2]) / 2).toFixed(2)
|
||||
: s[Math.floor(n / 2)].toFixed(2)
|
||||
})
|
||||
|
||||
const mode = computed(() => {
|
||||
const freq = {}
|
||||
let maxFreq = 0
|
||||
rawData.value.forEach((n) => {
|
||||
freq[n] = (freq[n] || 0) + 1
|
||||
if (freq[n] > maxFreq) maxFreq = freq[n]
|
||||
})
|
||||
if (maxFreq === 1) return '无'
|
||||
return Object.keys(freq)
|
||||
.filter((k) => freq[k] === maxFreq)
|
||||
.join(', ')
|
||||
})
|
||||
|
||||
const stdDev = computed(() => {
|
||||
if (!count.value) return 0
|
||||
const m = parseFloat(mean.value)
|
||||
const variance =
|
||||
rawData.value.reduce((sum, n) => sum + Math.pow(n - m, 2), 0) /
|
||||
count.value
|
||||
return Math.sqrt(variance).toFixed(2)
|
||||
})
|
||||
|
||||
const stats = computed(() => [
|
||||
{ label: '样本数', value: count.value, desc: '数据点总数', color: '#3b82f6' },
|
||||
{
|
||||
label: '均值',
|
||||
value: mean.value,
|
||||
desc: '所有数值的平均值',
|
||||
color: '#22c55e'
|
||||
},
|
||||
{
|
||||
label: '中位数',
|
||||
value: median.value,
|
||||
desc: '排序后中间位置的值',
|
||||
color: '#f59e0b'
|
||||
},
|
||||
{
|
||||
label: '众数',
|
||||
value: mode.value,
|
||||
desc: '出现次数最多的值',
|
||||
color: '#8b5cf6'
|
||||
},
|
||||
{
|
||||
label: '标准差',
|
||||
value: stdDev.value,
|
||||
desc: '数据离散程度',
|
||||
color: '#06b6d4'
|
||||
}
|
||||
])
|
||||
|
||||
function generateRandom() {
|
||||
dataInput.value = Array.from(
|
||||
{ length: 10 },
|
||||
() => Math.floor(Math.random() * 100) + 1
|
||||
).join(', ')
|
||||
}
|
||||
|
||||
function getBarHeight(val) {
|
||||
const max = Math.max(...sortedData.value)
|
||||
const min = Math.min(...sortedData.value)
|
||||
const range = max - min || 1
|
||||
return ((val - min) / range) * 80 + 20 + '%'
|
||||
}
|
||||
|
||||
const barColors = ['#3b82f6', '#22c55e', '#f59e0b', '#8b5cf6', '#ec4899']
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="stats-demo">
|
||||
<div class="demo-header">
|
||||
<span class="icon">📊</span>
|
||||
<span class="title">描述性统计演示</span>
|
||||
<span class="subtitle">输入数据,实时计算统计指标</span>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
面对大量数据时,我们需要用少数
|
||||
<span class="hl">代表性指标</span>
|
||||
来概括全貌。输入一组数字,观察均值、中位数、标准差等指标如何描述数据的
|
||||
<span class="hl">集中趋势</span> 和
|
||||
<span class="hl">离散程度</span>。
|
||||
</div>
|
||||
|
||||
<div class="input-area">
|
||||
<div class="input-row">
|
||||
<input
|
||||
v-model="dataInput"
|
||||
class="data-input"
|
||||
placeholder="用逗号分隔,例如:1, 2, 3, 4, 5"
|
||||
/>
|
||||
<button class="btn-random" @click="generateRandom">随机生成</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div v-for="s in stats" :key="s.label" class="stat-card">
|
||||
<div class="stat-label">{{ s.label }}</div>
|
||||
<div class="stat-value" :style="{ color: s.color }">{{ s.value }}</div>
|
||||
<div class="stat-desc">{{ s.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-area">
|
||||
<div class="chart-title">数据分布(升序排列)</div>
|
||||
<div class="bar-chart">
|
||||
<div
|
||||
v-for="(val, i) in sortedData"
|
||||
:key="i"
|
||||
class="bar"
|
||||
:style="{
|
||||
height: getBarHeight(val),
|
||||
background: barColors[i % barColors.length]
|
||||
}"
|
||||
>
|
||||
<span class="bar-label">{{ val }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.stats-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
margin: 24px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
padding: 14px 20px;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
padding: 16px 20px;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.7;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.hl {
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.data-input {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
font-size: 13px;
|
||||
font-family: 'Menlo', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
.btn-random {
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-random:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
||||
gap: 10px;
|
||||
padding: 0 20px 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-desc {
|
||||
font-size: 11px;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.chart-area {
|
||||
padding: 16px 20px 20px;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.bar-chart {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-around;
|
||||
height: 160px;
|
||||
gap: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 20px 12px 8px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
flex: 1;
|
||||
max-width: 50px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
position: relative;
|
||||
transition: height 0.3s;
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
position: absolute;
|
||||
top: -18px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.bar-chart {
|
||||
gap: 3px;
|
||||
}
|
||||
.bar-label {
|
||||
font-size: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,238 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const steps = [
|
||||
{ name: '访问商品页', count: 10000 },
|
||||
{ name: '加入购物车', count: 6000 },
|
||||
{ name: '进入结算页', count: 4000 },
|
||||
{ name: '完成支付', count: 2500 }
|
||||
]
|
||||
|
||||
const total = steps[0].count
|
||||
|
||||
function stepRate(i) {
|
||||
if (i === 0) return '100%'
|
||||
return ((steps[i].count / steps[i - 1].count) * 100).toFixed(1) + '%'
|
||||
}
|
||||
|
||||
function overallRate(i) {
|
||||
return ((steps[i].count / total) * 100).toFixed(1) + '%'
|
||||
}
|
||||
|
||||
function barWidth(i) {
|
||||
return Math.max(30, (steps[i].count / total) * 100) + '%'
|
||||
}
|
||||
|
||||
const worstIdx = computed(() => {
|
||||
let min = 100
|
||||
let idx = 1
|
||||
for (let i = 1; i < steps.length; i++) {
|
||||
const r = (steps[i].count / steps[i - 1].count) * 100
|
||||
if (r < min) {
|
||||
min = r
|
||||
idx = i
|
||||
}
|
||||
}
|
||||
return idx
|
||||
})
|
||||
|
||||
const overallConversion = computed(() =>
|
||||
((steps[steps.length - 1].count / total) * 100).toFixed(1)
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="funnel-demo">
|
||||
<div class="demo-header">
|
||||
<span class="icon">🔻</span>
|
||||
<span class="title">漏斗分析演示</span>
|
||||
<span class="subtitle">定位转化链的"出血点"</span>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
用户从进入到完成目标是一个层层筛选的过程。漏斗模型不只看最终转化率,更要找到
|
||||
<span class="hl">在哪里丢了人</span>
|
||||
——在最窄的地方投入优化,收益通常最大。
|
||||
</div>
|
||||
|
||||
<div class="funnel-body">
|
||||
<div
|
||||
v-for="(step, i) in steps"
|
||||
:key="step.name"
|
||||
:class="['funnel-step', { worst: i === worstIdx }]"
|
||||
:style="{ width: barWidth(i) }"
|
||||
>
|
||||
<div class="step-top">
|
||||
<span class="step-name">{{ step.name }}</span>
|
||||
<span class="step-count">{{ step.count.toLocaleString() }} 人</span>
|
||||
</div>
|
||||
<div class="step-bar"></div>
|
||||
<div class="step-rates">
|
||||
<span>总转化 {{ overallRate(i) }}</span>
|
||||
<span v-if="i > 0" class="step-conv">
|
||||
步骤转化 {{ stepRate(i) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="insights">
|
||||
<div class="insight-title">洞察</div>
|
||||
<div class="insight-items">
|
||||
<div class="insight-item">
|
||||
最低转化步骤:
|
||||
<strong>{{ steps[worstIdx].name }}</strong>
|
||||
({{ stepRate(worstIdx) }})
|
||||
</div>
|
||||
<div class="insight-item">
|
||||
整体转化率:<strong>{{ overallConversion }}%</strong>
|
||||
</div>
|
||||
<div class="insight-item">
|
||||
建议:优先优化
|
||||
<strong>{{ steps[worstIdx].name }}</strong>
|
||||
环节,减少体验摩擦
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.funnel-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
margin: 24px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
padding: 14px 20px;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
padding: 16px 20px;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.7;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.hl {
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.funnel-body {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.funnel-step {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.funnel-step.worst {
|
||||
border-color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
}
|
||||
|
||||
.step-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.step-name {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.step-count {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.step-bar {
|
||||
height: 20px;
|
||||
background: linear-gradient(90deg, var(--vp-c-brand), #60a5fa);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.worst .step-bar {
|
||||
background: linear-gradient(90deg, #ef4444, #f87171);
|
||||
}
|
||||
|
||||
.step-rates {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.step-conv {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.worst .step-conv {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.insights {
|
||||
padding: 16px 20px 20px;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.insight-title {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.insight-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.insight-item {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.funnel-step {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,232 @@
|
||||
<script setup>
|
||||
const retentionData = [
|
||||
{ date: '2024-01-01', users: 1000, day1: 45, day7: 32, day30: 18 },
|
||||
{ date: '2024-01-02', users: 1200, day1: 42, day7: 28, day30: 15 },
|
||||
{ date: '2024-01-03', users: 950, day1: 40, day7: 25, day30: 12 },
|
||||
{ date: '2024-01-04', users: 1100, day1: 38, day7: 30, day30: 14 },
|
||||
{ date: '2024-01-05', users: 1050, day1: 41, day7: 33, day30: 16 },
|
||||
{ date: '2024-01-06', users: 1300, day1: 43, day7: 29, day30: 13 },
|
||||
{ date: '2024-01-07', users: 1150, day1: 40, day7: 31, day30: 15 }
|
||||
]
|
||||
|
||||
const curves = [
|
||||
{
|
||||
label: '次日留存',
|
||||
color: '#3b82f6',
|
||||
data: retentionData.map((r) => r.day1)
|
||||
},
|
||||
{
|
||||
label: '7日留存',
|
||||
color: '#22c55e',
|
||||
data: retentionData.map((r) => r.day7)
|
||||
},
|
||||
{
|
||||
label: '30日留存',
|
||||
color: '#f59e0b',
|
||||
data: retentionData.map((r) => r.day30)
|
||||
}
|
||||
]
|
||||
|
||||
function points(data) {
|
||||
return data.map((v, i) => `${60 + i * 50},${180 - v * 1.6}`).join(' ')
|
||||
}
|
||||
|
||||
function rateClass(rate) {
|
||||
if (rate >= 40) return 'high'
|
||||
if (rate >= 25) return 'mid'
|
||||
return 'low'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="retention-demo">
|
||||
<div class="demo-header">
|
||||
<span class="icon">📈</span>
|
||||
<span class="title">留存分析演示</span>
|
||||
<span class="subtitle">产品的"硬核"体检</span>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
拉新是给桶加水,留存是看桶漏不漏。留存曲线若
|
||||
<span class="hl">趋于平稳</span>,说明产品已获得 PMF;若
|
||||
<span class="hl">持续跌落至零</span>,说明核心价值未被验证。
|
||||
</div>
|
||||
|
||||
<!-- 留存数据表 -->
|
||||
<div class="section">
|
||||
<div class="section-label">留存数据</div>
|
||||
<div class="table-wrap">
|
||||
<table class="r-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>注册日期</th>
|
||||
<th>注册人数</th>
|
||||
<th>次日留存</th>
|
||||
<th>7日留存</th>
|
||||
<th>30日留存</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="r in retentionData" :key="r.date">
|
||||
<td>{{ r.date }}</td>
|
||||
<td>{{ r.users }}</td>
|
||||
<td :class="rateClass(r.day1)">{{ r.day1 }}%</td>
|
||||
<td :class="rateClass(r.day7)">{{ r.day7 }}%</td>
|
||||
<td :class="rateClass(r.day30)">{{ r.day30 }}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 留存曲线 -->
|
||||
<div class="section">
|
||||
<div class="section-label">留存曲线</div>
|
||||
<div class="chart-wrap">
|
||||
<svg viewBox="0 0 400 210" class="curve-svg">
|
||||
<!-- 坐标轴 -->
|
||||
<line x1="40" y1="180" x2="380" y2="180" stroke="#666" stroke-width="1" />
|
||||
<line x1="40" y1="20" x2="40" y2="180" stroke="#666" stroke-width="1" />
|
||||
|
||||
<!-- Y轴标签 -->
|
||||
<text x="12" y="30" font-size="10" fill="#999">100%</text>
|
||||
<text x="17" y="100" font-size="10" fill="#999">50%</text>
|
||||
<text x="25" y="183" font-size="10" fill="#999">0</text>
|
||||
|
||||
<!-- 曲线 -->
|
||||
<template v-for="c in curves" :key="c.label">
|
||||
<polyline
|
||||
:points="points(c.data)"
|
||||
fill="none"
|
||||
:stroke="c.color"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<circle
|
||||
v-for="(v, i) in c.data"
|
||||
:key="i"
|
||||
:cx="60 + i * 50"
|
||||
:cy="180 - v * 1.6"
|
||||
r="3.5"
|
||||
:fill="c.color"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- X轴标签 -->
|
||||
<text
|
||||
v-for="(d, i) in ['D1','D2','D3','D4','D5','D6','D7']"
|
||||
:key="d"
|
||||
:x="60 + i * 50"
|
||||
y="196"
|
||||
font-size="10"
|
||||
fill="#999"
|
||||
text-anchor="middle"
|
||||
>{{ d }}</text>
|
||||
</svg>
|
||||
|
||||
<div class="legend">
|
||||
<div v-for="c in curves" :key="c.label" class="legend-item">
|
||||
<span class="legend-dot" :style="{ background: c.color }"></span>
|
||||
<span class="legend-text">{{ c.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.retention-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
margin: 24px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
padding: 14px 20px;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon { font-size: 18px; }
|
||||
.title { font-weight: 600; font-size: 15px; }
|
||||
.subtitle { font-size: 12px; color: var(--vp-c-text-3); margin-left: auto; }
|
||||
|
||||
.intro-text {
|
||||
padding: 16px 20px;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.7;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.hl { color: var(--vp-c-brand); font-weight: 600; }
|
||||
|
||||
.section { padding: 16px 20px; }
|
||||
.section-label { font-weight: 600; font-size: 13px; margin-bottom: 10px; }
|
||||
|
||||
.table-wrap {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.r-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.r-table th,
|
||||
.r-table td {
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.r-table th {
|
||||
background: var(--vp-c-bg-alt);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.r-table tbody tr:hover { background: var(--vp-c-bg-soft); }
|
||||
|
||||
.high { color: #22c55e; font-weight: 600; }
|
||||
.mid { color: #f59e0b; font-weight: 600; }
|
||||
.low { color: #ef4444; font-weight: 600; }
|
||||
|
||||
.chart-wrap {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.curve-svg { width: 100%; height: auto; }
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.legend-text { color: var(--vp-c-text-2); }
|
||||
</style>
|
||||
@@ -710,7 +710,10 @@ import DecisionMatrixDemo from './components/appendix/engineering-excellence/Dec
|
||||
import SqlDemo from './components/appendix/data/SqlDemo.vue'
|
||||
import DataModelsDemo from './components/appendix/data/DataModelsDemo.vue'
|
||||
import ABTestingDemo from './components/appendix/data/ABTestingDemo.vue'
|
||||
import DataAnalysisDemo from './components/appendix/data/DataAnalysisDemo.vue'
|
||||
import DescriptiveStatsDemo from './components/appendix/data/DescriptiveStatsDemo.vue'
|
||||
import DataAggregationDemo from './components/appendix/data/DataAggregationDemo.vue'
|
||||
import FunnelAnalysisDemo from './components/appendix/data/FunnelAnalysisDemo.vue'
|
||||
import RetentionAnalysisDemo from './components/appendix/data/RetentionAnalysisDemo.vue'
|
||||
import DataTrackingDemo from './components/appendix/data/DataTrackingDemo.vue'
|
||||
|
||||
// RAG Components
|
||||
@@ -1542,7 +1545,10 @@ export default {
|
||||
app.component('SqlDemo', SqlDemo)
|
||||
app.component('DataModelsDemo', DataModelsDemo)
|
||||
app.component('ABTestingDemo', ABTestingDemo)
|
||||
app.component('DataAnalysisDemo', DataAnalysisDemo)
|
||||
app.component('DescriptiveStatsDemo', DescriptiveStatsDemo)
|
||||
app.component('DataAggregationDemo', DataAggregationDemo)
|
||||
app.component('FunnelAnalysisDemo', FunnelAnalysisDemo)
|
||||
app.component('RetentionAnalysisDemo', RetentionAnalysisDemo)
|
||||
app.component('DataTrackingDemo', DataTrackingDemo)
|
||||
|
||||
// Engineering Excellence Components Registration
|
||||
@@ -1673,7 +1679,10 @@ export default {
|
||||
app.component('NetworkArchitectureDemo', NetworkArchitectureDemo)
|
||||
|
||||
// Project Architecture Components Registration
|
||||
app.component('ProjectArchitectureComparisonDemo', ProjectArchitectureComparisonDemo)
|
||||
app.component(
|
||||
'ProjectArchitectureComparisonDemo',
|
||||
ProjectArchitectureComparisonDemo
|
||||
)
|
||||
|
||||
// Appendix Navigation Component Registration
|
||||
app.component('AppendixFlowMap', AppendixFlowMap)
|
||||
|
||||
@@ -48,13 +48,45 @@ export default {
|
||||
foundation: {
|
||||
label: 'Core idea of Symbolism — encoding knowledge as rules',
|
||||
lines: [
|
||||
{ parts: [{ kw: 'IF' }, { text: ' temperature > 38.5°C ' }, { kw: 'AND' }, { text: ' WBC count > 11000' }] },
|
||||
{ indent: true, parts: [{ kw: 'THEN' }, { text: ' diagnosis = ' }, { str: '"bacterial infection"' }] },
|
||||
{ parts: [{ kw: 'IF' }, { text: ' diagnosis = ' }, { str: '"bacterial infection"' }, { text: ' ' }, { kw: 'AND' }, { text: ' no penicillin allergy' }] },
|
||||
{ indent: true, parts: [{ kw: 'THEN' }, { text: ' treatment = ' }, { str: '"penicillin 400mg / twice daily"' }] }
|
||||
{
|
||||
parts: [
|
||||
{ kw: 'IF' },
|
||||
{ text: ' temperature > 38.5°C ' },
|
||||
{ kw: 'AND' },
|
||||
{ text: ' WBC count > 11000' }
|
||||
]
|
||||
},
|
||||
{
|
||||
indent: true,
|
||||
parts: [
|
||||
{ kw: 'THEN' },
|
||||
{ text: ' diagnosis = ' },
|
||||
{ str: '"bacterial infection"' }
|
||||
]
|
||||
},
|
||||
{
|
||||
parts: [
|
||||
{ kw: 'IF' },
|
||||
{ text: ' diagnosis = ' },
|
||||
{ str: '"bacterial infection"' },
|
||||
{ text: ' ' },
|
||||
{ kw: 'AND' },
|
||||
{ text: ' no penicillin allergy' }
|
||||
]
|
||||
},
|
||||
{
|
||||
indent: true,
|
||||
parts: [
|
||||
{ kw: 'THEN' },
|
||||
{ text: ' treatment = ' },
|
||||
{ str: '"penicillin 400mg / twice daily"' }
|
||||
]
|
||||
}
|
||||
],
|
||||
comment: '// The early medical expert system MYCIN (1977) consisted of 450+ rules like these',
|
||||
caption: 'Human experts translate experience into IF-THEN rules; the machine matches and executes them one by one'
|
||||
comment:
|
||||
'// The early medical expert system MYCIN (1977) consisted of 450+ rules like these',
|
||||
caption:
|
||||
'Human experts translate experience into IF-THEN rules; the machine matches and executes them one by one'
|
||||
},
|
||||
|
||||
// PerceptronDemo
|
||||
@@ -63,16 +95,33 @@ export default {
|
||||
biasLabel: 'Bias',
|
||||
activated: 'Fire',
|
||||
silent: 'Silent',
|
||||
caption: '① Input features\u2003② Multiply by weights (importance)\u2003③ Sum + bias\u2003④ Fires output 1 if above threshold, otherwise 0'
|
||||
caption:
|
||||
'① Input features\u2003② Multiply by weights (importance)\u2003③ Sum + bias\u2003④ Fires output 1 if above threshold, otherwise 0'
|
||||
},
|
||||
|
||||
// BackpropagationDemo
|
||||
backprop: {
|
||||
steps: [
|
||||
{ icon: '➡️', name: 'Forward Pass', desc: 'Data flows through the network to produce a prediction' },
|
||||
{ icon: '📐', name: 'Compute Loss', desc: 'Prediction vs. ground truth → calculate loss' },
|
||||
{ icon: '⬅️', name: 'Backpropagation', desc: 'Trace back each weight\'s "responsibility" layer by layer' },
|
||||
{ icon: '⚙️', name: 'Update Weights', desc: 'Adjust proportionally to reduce future error' }
|
||||
{
|
||||
icon: '➡️',
|
||||
name: 'Forward Pass',
|
||||
desc: 'Data flows through the network to produce a prediction'
|
||||
},
|
||||
{
|
||||
icon: '📐',
|
||||
name: 'Compute Loss',
|
||||
desc: 'Prediction vs. ground truth → calculate loss'
|
||||
},
|
||||
{
|
||||
icon: '⬅️',
|
||||
name: 'Backpropagation',
|
||||
desc: 'Trace back each weight\'s "responsibility" layer by layer'
|
||||
},
|
||||
{
|
||||
icon: '⚙️',
|
||||
name: 'Update Weights',
|
||||
desc: 'Adjust proportionally to reduce future error'
|
||||
}
|
||||
],
|
||||
lossLabel: 'Loss decreases over training epochs:',
|
||||
axisHigh: 'High',
|
||||
@@ -84,7 +133,10 @@ export default {
|
||||
neuralNet: {
|
||||
layers: [
|
||||
{ name: 'Input Layer', desc: 'Raw pixels / numerical signals' },
|
||||
{ name: 'Hidden Layers (stackable)', desc: 'Low → edges; Mid → shapes; High → semantic concepts' },
|
||||
{
|
||||
name: 'Hidden Layers (stackable)',
|
||||
desc: 'Low → edges; Mid → shapes; High → semantic concepts'
|
||||
},
|
||||
{ name: 'Output Layer', desc: 'Final classification or prediction' }
|
||||
]
|
||||
},
|
||||
@@ -94,16 +146,41 @@ export default {
|
||||
colLabel: 'Attention distribution when processing "{word}":',
|
||||
sentence: ['John', 'gave', 'the', 'apple', 'to', 'his', 'mother'],
|
||||
focusIdx: 5,
|
||||
weights: [0.62, 0.08, 0.03, 0.10, 0.05, 0.07, 0.05],
|
||||
caption: '"his" sits mid-sentence, yet the model directs 62% attention to "John" at the start — resolving the pronoun across distance'
|
||||
weights: [0.62, 0.08, 0.03, 0.1, 0.05, 0.07, 0.05],
|
||||
caption:
|
||||
'"his" sits mid-sentence, yet the model directs 62% attention to "John" at the start — resolving the pronoun across distance'
|
||||
},
|
||||
|
||||
// GPTEvolutionDemo
|
||||
gptEvolution: [
|
||||
{ name: 'GPT-1', year: '2018', params: '117 M', barWidth: '2%', key: 'Pre-train + fine-tune paradigm' },
|
||||
{ name: 'GPT-2', year: '2019', params: '1.5 B', barWidth: '6%', key: 'Zero-shot generalization' },
|
||||
{ name: 'GPT-3', year: '2020', params: '175 B', barWidth: '45%', key: '⚡ Emergence! In-context learning' },
|
||||
{ name: 'GPT-4', year: '2023', params: '~1.8 T', barWidth: '100%', key: 'Multimodal + complex reasoning' }
|
||||
{
|
||||
name: 'GPT-1',
|
||||
year: '2018',
|
||||
params: '117 M',
|
||||
barWidth: '2%',
|
||||
key: 'Pre-train + fine-tune paradigm'
|
||||
},
|
||||
{
|
||||
name: 'GPT-2',
|
||||
year: '2019',
|
||||
params: '1.5 B',
|
||||
barWidth: '6%',
|
||||
key: 'Zero-shot generalization'
|
||||
},
|
||||
{
|
||||
name: 'GPT-3',
|
||||
year: '2020',
|
||||
params: '175 B',
|
||||
barWidth: '45%',
|
||||
key: '⚡ Emergence! In-context learning'
|
||||
},
|
||||
{
|
||||
name: 'GPT-4',
|
||||
year: '2023',
|
||||
params: '~1.8 T',
|
||||
barWidth: '100%',
|
||||
key: 'Multimodal + complex reasoning'
|
||||
}
|
||||
],
|
||||
|
||||
// AIErasComparisonDemo
|
||||
@@ -113,11 +190,41 @@ export default {
|
||||
mechanismLabel: 'Core Mechanism',
|
||||
examplesLabel: 'Key Examples',
|
||||
eras: [
|
||||
{ name: 'Rule-Based Era', time: '1960s - 1980s', driver: 'Human-coded knowledge', mechanism: 'If-Then logical deduction', examples: ['Dendral', 'Deep Blue'] },
|
||||
{ name: 'Classical ML', time: '1990s - 2000s', driver: 'Manual feature engineering + statistics', mechanism: 'Finding mathematical decision boundaries', examples: ['SVM', 'Random Forest'] },
|
||||
{ name: 'Deep Learning Revolution', time: '2010s', driver: 'Big data + GPU compute', mechanism: 'Neural nets auto-extract features', examples: ['AlexNet (CNN)', 'AlphaGo (RL)'] },
|
||||
{ name: 'Large Language Models', time: '2018 - present', driver: 'Massive unlabeled data + brute-force compute', mechanism: 'Next-token prediction + emergent knowledge', examples: ['GPT-4', 'Claude 3'] },
|
||||
{ name: 'Agentic AI', time: 'Now - future', driver: 'LLM brain + environment perception', mechanism: 'Autonomous planning + tool use', examples: ['AI Programmer', 'Embodied AI'] }
|
||||
{
|
||||
name: 'Rule-Based Era',
|
||||
time: '1960s - 1980s',
|
||||
driver: 'Human-coded knowledge',
|
||||
mechanism: 'If-Then logical deduction',
|
||||
examples: ['Dendral', 'Deep Blue']
|
||||
},
|
||||
{
|
||||
name: 'Classical ML',
|
||||
time: '1990s - 2000s',
|
||||
driver: 'Manual feature engineering + statistics',
|
||||
mechanism: 'Finding mathematical decision boundaries',
|
||||
examples: ['SVM', 'Random Forest']
|
||||
},
|
||||
{
|
||||
name: 'Deep Learning Revolution',
|
||||
time: '2010s',
|
||||
driver: 'Big data + GPU compute',
|
||||
mechanism: 'Neural nets auto-extract features',
|
||||
examples: ['AlexNet (CNN)', 'AlphaGo (RL)']
|
||||
},
|
||||
{
|
||||
name: 'Large Language Models',
|
||||
time: '2018 - present',
|
||||
driver: 'Massive unlabeled data + brute-force compute',
|
||||
mechanism: 'Next-token prediction + emergent knowledge',
|
||||
examples: ['GPT-4', 'Claude 3']
|
||||
},
|
||||
{
|
||||
name: 'Agentic AI',
|
||||
time: 'Now - future',
|
||||
driver: 'LLM brain + environment perception',
|
||||
mechanism: 'Autonomous planning + tool use',
|
||||
examples: ['AI Programmer', 'Embodied AI']
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,10 +48,36 @@ export default {
|
||||
foundation: {
|
||||
label: '符号主义的核心思路 ── 把知识写成规则',
|
||||
lines: [
|
||||
{ parts: [{ kw: 'IF' }, { text: ' 体温 > 38.5°C ' }, { kw: 'AND' }, { text: ' 白细胞计数 > 11000' }] },
|
||||
{ indent: true, parts: [{ kw: 'THEN' }, { text: ' 诊断 = ' }, { str: '"细菌感染"' }] },
|
||||
{ parts: [{ kw: 'IF' }, { text: ' 诊断 = ' }, { str: '"细菌感染"' }, { text: ' ' }, { kw: 'AND' }, { text: ' 对青霉素不过敏' }] },
|
||||
{ indent: true, parts: [{ kw: 'THEN' }, { text: ' 治疗方案 = ' }, { str: '"青霉素 400mg / 每日两次"' }] }
|
||||
{
|
||||
parts: [
|
||||
{ kw: 'IF' },
|
||||
{ text: ' 体温 > 38.5°C ' },
|
||||
{ kw: 'AND' },
|
||||
{ text: ' 白细胞计数 > 11000' }
|
||||
]
|
||||
},
|
||||
{
|
||||
indent: true,
|
||||
parts: [{ kw: 'THEN' }, { text: ' 诊断 = ' }, { str: '"细菌感染"' }]
|
||||
},
|
||||
{
|
||||
parts: [
|
||||
{ kw: 'IF' },
|
||||
{ text: ' 诊断 = ' },
|
||||
{ str: '"细菌感染"' },
|
||||
{ text: ' ' },
|
||||
{ kw: 'AND' },
|
||||
{ text: ' 对青霉素不过敏' }
|
||||
]
|
||||
},
|
||||
{
|
||||
indent: true,
|
||||
parts: [
|
||||
{ kw: 'THEN' },
|
||||
{ text: ' 治疗方案 = ' },
|
||||
{ str: '"青霉素 400mg / 每日两次"' }
|
||||
]
|
||||
}
|
||||
],
|
||||
comment: '// 早期医疗专家系统(MYCIN,1977)就是由 450+ 条这样的规则组成的',
|
||||
caption: '人类专家把经验翻译成一条条 IF-THEN 规则,机器逐条匹配执行'
|
||||
@@ -63,7 +89,8 @@ export default {
|
||||
biasLabel: '偏置',
|
||||
activated: '激活',
|
||||
silent: '静默',
|
||||
caption: '① 输入特征\u2003② 乘以权重(重要性)\u2003③ 求和 + 偏置\u2003④ 超过阈值就激活输出 1,否则输出 0'
|
||||
caption:
|
||||
'① 输入特征\u2003② 乘以权重(重要性)\u2003③ 求和 + 偏置\u2003④ 超过阈值就激活输出 1,否则输出 0'
|
||||
},
|
||||
|
||||
// BackpropagationDemo
|
||||
@@ -84,7 +111,10 @@ export default {
|
||||
neuralNet: {
|
||||
layers: [
|
||||
{ name: '输入层', desc: '原始像素 / 数值信号' },
|
||||
{ name: '隐藏层(可叠加多层)', desc: '底层识别边缘 → 中层识别形状 → 高层识别语义概念' },
|
||||
{
|
||||
name: '隐藏层(可叠加多层)',
|
||||
desc: '底层识别边缘 → 中层识别形状 → 高层识别语义概念'
|
||||
},
|
||||
{ name: '输出层', desc: '最终分类或预测结果' }
|
||||
]
|
||||
},
|
||||
@@ -94,16 +124,41 @@ export default {
|
||||
colLabel: '处理「{word}」时的注意力分配:',
|
||||
sentence: ['小明', '把', '苹果', '给了', '他', '的', '母亲'],
|
||||
focusIdx: 4,
|
||||
weights: [0.65, 0.05, 0.10, 0.10, 0.05, 0.03, 0.02],
|
||||
caption: '「他」虽在句中间,模型却把 65% 注意力精准投向句首的「小明」,跨越距离识别代词指代'
|
||||
weights: [0.65, 0.05, 0.1, 0.1, 0.05, 0.03, 0.02],
|
||||
caption:
|
||||
'「他」虽在句中间,模型却把 65% 注意力精准投向句首的「小明」,跨越距离识别代词指代'
|
||||
},
|
||||
|
||||
// GPTEvolutionDemo
|
||||
gptEvolution: [
|
||||
{ name: 'GPT-1', year: '2018', params: '1.17 亿', barWidth: '2%', key: '预训练+微调范式确立' },
|
||||
{ name: 'GPT-2', year: '2019', params: '15 亿', barWidth: '6%', key: 'Zero-shot 零样本泛化' },
|
||||
{ name: 'GPT-3', year: '2020', params: '1750 亿', barWidth: '45%', key: '⚡ 涌现!上下文学习' },
|
||||
{ name: 'GPT-4', year: '2023', params: '~1.8 万亿', barWidth: '100%', key: '多模态 + 复杂推理' }
|
||||
{
|
||||
name: 'GPT-1',
|
||||
year: '2018',
|
||||
params: '1.17 亿',
|
||||
barWidth: '2%',
|
||||
key: '预训练+微调范式确立'
|
||||
},
|
||||
{
|
||||
name: 'GPT-2',
|
||||
year: '2019',
|
||||
params: '15 亿',
|
||||
barWidth: '6%',
|
||||
key: 'Zero-shot 零样本泛化'
|
||||
},
|
||||
{
|
||||
name: 'GPT-3',
|
||||
year: '2020',
|
||||
params: '1750 亿',
|
||||
barWidth: '45%',
|
||||
key: '⚡ 涌现!上下文学习'
|
||||
},
|
||||
{
|
||||
name: 'GPT-4',
|
||||
year: '2023',
|
||||
params: '~1.8 万亿',
|
||||
barWidth: '100%',
|
||||
key: '多模态 + 复杂推理'
|
||||
}
|
||||
],
|
||||
|
||||
// AIErasComparisonDemo
|
||||
@@ -113,11 +168,41 @@ export default {
|
||||
mechanismLabel: '核心机制',
|
||||
examplesLabel: '典型代表',
|
||||
eras: [
|
||||
{ name: '规则系统时代', time: '1960s - 1980s', driver: '人类硬编码知识', mechanism: 'If-Then 逻辑推演', examples: ['Dendral', '深蓝 (Deep Blue)'] },
|
||||
{ name: '传统机器学习', time: '1990s - 2000s', driver: '人工特征工程 + 统计学', mechanism: '寻找数学决策边界', examples: ['支持向量机 (SVM)', '随机森林'] },
|
||||
{ name: '深度学习革命', time: '2010s', driver: '大数据 + 算力爬升', mechanism: '神经网络自动提取特征', examples: ['AlexNet (CNN)', 'AlphaGo (RL)'] },
|
||||
{ name: '大语言模型 (LLM)', time: '2018 - 至今', driver: '海量无标注数据 + 暴力计算', mechanism: '预测下一个词 + 涌现常识', examples: ['GPT-4', 'Claude 3'] },
|
||||
{ name: '智能体 (Agentic AI)', time: '现在 - 未来', driver: '大模型大脑 + 环境感知', mechanism: '自主规划 + 工具调用', examples: ['AI 程序员', '具身智能'] }
|
||||
{
|
||||
name: '规则系统时代',
|
||||
time: '1960s - 1980s',
|
||||
driver: '人类硬编码知识',
|
||||
mechanism: 'If-Then 逻辑推演',
|
||||
examples: ['Dendral', '深蓝 (Deep Blue)']
|
||||
},
|
||||
{
|
||||
name: '传统机器学习',
|
||||
time: '1990s - 2000s',
|
||||
driver: '人工特征工程 + 统计学',
|
||||
mechanism: '寻找数学决策边界',
|
||||
examples: ['支持向量机 (SVM)', '随机森林']
|
||||
},
|
||||
{
|
||||
name: '深度学习革命',
|
||||
time: '2010s',
|
||||
driver: '大数据 + 算力爬升',
|
||||
mechanism: '神经网络自动提取特征',
|
||||
examples: ['AlexNet (CNN)', 'AlphaGo (RL)']
|
||||
},
|
||||
{
|
||||
name: '大语言模型 (LLM)',
|
||||
time: '2018 - 至今',
|
||||
driver: '海量无标注数据 + 暴力计算',
|
||||
mechanism: '预测下一个词 + 涌现常识',
|
||||
examples: ['GPT-4', 'Claude 3']
|
||||
},
|
||||
{
|
||||
name: '智能体 (Agentic AI)',
|
||||
time: '现在 - 未来',
|
||||
driver: '大模型大脑 + 环境感知',
|
||||
mechanism: '自主规划 + 工具调用',
|
||||
examples: ['AI 程序员', '具身智能']
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 383 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 278 KiB |
@@ -0,0 +1,312 @@
|
||||
# Integrated Development Environment (IDE) Basics
|
||||
|
||||
::: tip 💡 Learning Guide
|
||||
This chapter will take you deep into the core productivity tool for programmers—the **Integrated Development Environment (IDE)**. We'll start from the design philosophy of IDEs, analyze their core components one by one, and demonstrate their working principles through a virtual IDE.
|
||||
:::
|
||||
|
||||
## What to Do When You Don't Understand Something? (How to solve problems)
|
||||
|
||||
In the process of learning and using an IDE, you may encounter various buttons, menus, or code errors that you don't understand. At this time, **don't panic—using an AI assistant is the most efficient solution**.
|
||||
|
||||
**Recommended Approach: Screenshot and Ask AI**
|
||||
|
||||
Modern AIs (such as ChatGPT, Claude, DeepSeek, etc.) have powerful image recognition capabilities. When you encounter unfamiliar interface elements or complex code snippets:
|
||||
|
||||
1. **Screenshot**: Capture the part you don't understand (such as a strange icon or a complex configuration code).
|
||||
2. **Ask**: Send the image to AI and ask: "What is this? What's it for?" or "What does xxx do in this code?"
|
||||
3. **Follow up**: If AI's answer is too technical to understand, continue asking: "Please explain it in plain language, preferably with a real-life example."
|
||||
|
||||
<AiHelpDemo />
|
||||
|
||||
---
|
||||
|
||||
## 0. Introduction: Why Do We Need an IDE?
|
||||
|
||||
In the software development process, programmers need to frequently write code, manage files, compile and run programs, debug errors, and so on. If all these operations needed to be completed in different independent software (for example, using Notepad to write code, command line to compile, and file folders to manage files), efficiency would be extremely low and error-prone.
|
||||
|
||||
The core value of an **IDE (Integrated Development Environment)** lies in **integration**. It integrates various tools needed for software development (editor, compiler, debugger, file manager, etc.) into a unified graphical interface, providing a one-stop working experience.
|
||||
|
||||
**VS Code is one of the most popular IDEs.** Although it is essentially a lightweight code editor, through its powerful plugin system, it has all the core functions of an IDE (code editing, debugging, version control, etc.), and is therefore widely regarded as the preferred IDE for modern frontend and full-stack development.
|
||||
|
||||
In short, IDEs aim to maximize developer productivity and reduce the time cost of switching between different tools.
|
||||
|
||||
> 🔗 **Resource Downloads**:
|
||||
>
|
||||
> - [VS Code Official Download](https://code.visualstudio.com/Download)
|
||||
> - [VS Code Web Version Experience](https://vscode.dev/)
|
||||
>
|
||||
> **VS Code (Visual Studio Code)** is a free, open-source, cross-platform code editor developed by Microsoft. With its **lightweight nature, rich plugins, and fast startup speed**, it has become one of the most popular development tools worldwide. Whether you're writing Python, JavaScript, or C++, VS Code can become the most suitable "tool" for you through plugin installation.
|
||||
|
||||
---
|
||||
|
||||
## 1. Core Interface Analysis
|
||||
|
||||
The interface layout of modern IDEs (taking VS Code as an example) has been carefully designed and usually contains the following four core areas:
|
||||
|
||||
1. **Sidebar: Resource Management**
|
||||
Displays the project's file tree, supports creating, renaming, moving, and deleting files, providing a global view and quick access to the project structure.
|
||||
|
||||
2. **Editor Area: Code Creation**
|
||||
The core area for writing and modifying code. Supports syntax highlighting, intelligent code completion, syntax checking, and other functions, providing an efficient and intelligent code writing environment.
|
||||
|
||||
3. **Bottom Panel: Execution and Feedback**
|
||||
Interacts with the underlying system and views running results. Includes Terminal, Output, etc., used for executing commands, viewing logs, and debugging.
|
||||
|
||||
4. **Activity Bar: Function Navigation**
|
||||
Located on the far left of the interface, containing icons for file explorer, search, Git management, etc., used to quickly switch between different work contexts (such as "writing code" and "submitting code").
|
||||
|
||||
---
|
||||
|
||||
## 2. Interactive Demo: Functional Experience
|
||||
|
||||
Seeing is believing. To let you truly feel the convenience of an IDE, we have prepared a **virtual VS Code environment** for you.
|
||||
|
||||
**Please try the following operations**:
|
||||
|
||||
1. Click the **"▶ Start Auto Tour"** button in the upper right corner to follow the cursor and learn about each area.
|
||||
2. **Free Exploration**: Click the icons on the left to switch views, or click file names to open code.
|
||||
3. **Experience Integration**: You'll find that file management, code editing, and terminal running are all seamlessly connected within the same window.
|
||||
4. **Install Plugins**: Select **"Extensions Installation"** mode from the dropdown menu to experience how to install Python plugins in a virtual store.
|
||||
|
||||
<ClientOnly>
|
||||
<VirtualVSCodeDemo />
|
||||
</ClientOnly>
|
||||
|
||||
---
|
||||
|
||||
## 3. Core Mechanism: Why Can VS Code Do Everything?
|
||||
|
||||
You might be curious: Why can the same software write Python, C++, and do web development? How does it do it?
|
||||
Actually, VS Code's design philosophy can be summarized in one sentence: **"Minimalist core, pluggable capabilities."**
|
||||
|
||||
### 3.1 Minimalist Core: Just a "Canvas"
|
||||
|
||||
Imagine, the VS Code you just downloaded, if no plugins are installed, actually **doesn't understand programming**.
|
||||
At this point, it is essentially just a **powerful text editor**.
|
||||
|
||||
- It is responsible for displaying text (rendering).
|
||||
- It is responsible for managing files (IO).
|
||||
- But it doesn't know that `print("Hello")` is Python code, nor does it know that `int main()` is a C++ entry point.
|
||||
|
||||
### 3.2 Plugin System: Injecting "Soul"
|
||||
|
||||
To make VS Code able to "understand" code, we need to install **Extensions**.
|
||||
Plugins are like specialized **translators**:
|
||||
|
||||
- **Python Plugin**: Tells VS Code what variables are, what functions are, and how to run `.py` files.
|
||||
- **C++ Plugin**: Tells VS Code how to call the compiler and how to debug memory.
|
||||
|
||||
This design makes VS Code very lightweight—if you don't write Java, you don't have to carry Java's runtime environment.
|
||||
|
||||
### 3.3 Behind the Scenes: From Code to Execution
|
||||
|
||||
<ClientOnly>
|
||||
<IdeArchitectureDemo />
|
||||
</ClientOnly>
|
||||
|
||||
Let's look at how VS Code, plugins, and the underlying environment collaborate through a specific scenario.
|
||||
Suppose you write a line of Python code and click **Run** or **Debug**:
|
||||
|
||||
#### 1. Language Recognition (Activation)
|
||||
|
||||
VS Code detects the `.py` suffix and automatically wakes up the **Python Plugin**. The plugin immediately takes over the editor, begins syntax analysis, colors the code differently (syntax highlighting), and provides intelligent suggestions.
|
||||
|
||||
#### 2. Task Delegation (Delegation)
|
||||
|
||||
When you issue a command, the plugin itself does not directly execute the code, but **delegates** the task to underlying professional tools:
|
||||
|
||||
- **Run Mode**: The plugin generates a command (such as `python main.py`) and sends it to the system's **terminal** for execution.
|
||||
- **Debug Mode**: The plugin starts a **Debug Adapter**. It's like a "monitoring probe," connecting to the internals of the Python interpreter, allowing you to control code execution line by line.
|
||||
|
||||
#### 3. Result Feedback (Feedback)
|
||||
|
||||
The Python interpreter (or compiler) executes the code and returns the results (or error messages) to the plugin. The plugin then "carries" this information back and displays it in VS Code's **bottom terminal panel**.
|
||||
|
||||
### 3.4 Summary: Using a "Restaurant" as an Analogy
|
||||
|
||||
If the above formula is a bit abstract, we can imagine the process of writing code as **dining at a restaurant**:
|
||||
|
||||
1. **VS Code is the "Restaurant Lobby"**:
|
||||
- The decoration is luxurious and the environment is comfortable (code highlighting, beautiful themes).
|
||||
- **But the lobby itself doesn't produce food**. You sit here just to more comfortably "order" (write code).
|
||||
|
||||
2. **Environment (Python/Node) is the "Kitchen"**:
|
||||
- This is where the real **cooking (running code)** happens.
|
||||
- If the restaurant has no kitchen (Python not installed), you can sit in the lobby until dark and still won't get food.
|
||||
|
||||
3. **Plugins are the "Waiters"**:
|
||||
- They connect the lobby and the kitchen.
|
||||
- They understand your menu, run to tell the kitchen: "Table 3 wants a 'run main.py'!"
|
||||
- When it's done, they bring the results (steaming hot food) back to you.
|
||||
|
||||
**Conclusion**:
|
||||
|
||||
- Only installing VS Code = **Only lobby, no kitchen** (can only look, can't eat).
|
||||
- Only installing Python = **Only kitchen, no lobby** (can eat, but have to squat on the kitchen floor, poor experience).
|
||||
- **Installing VS Code + Plugins + Python = Perfect dining experience.**
|
||||
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
onMounted(() => {
|
||||
const openTarget = () => {
|
||||
const hash = window.location.hash
|
||||
if (hash) {
|
||||
try {
|
||||
// Handle encoded characters in hash
|
||||
const target = document.querySelector(decodeURIComponent(hash))
|
||||
// If the target is a details element, open it
|
||||
if (target && target.tagName === 'DETAILS') {
|
||||
target.setAttribute('open', '')
|
||||
}
|
||||
// If the target is inside a details element, open the parent details
|
||||
const parentDetails = target?.closest('details')
|
||||
if (parentDetails) {
|
||||
parentDetails.setAttribute('open', '')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
openTarget()
|
||||
window.addEventListener('hashchange', openTarget)
|
||||
})
|
||||
</script>
|
||||
|
||||
# Appendix: Visual Studio Code Menu Bar Analysis
|
||||
|
||||
To help everyone understand the meaning of each option, here we provide an in-depth analysis of the menu bar:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
<details class="custom-block details" id="vscode-file-menu">
|
||||
<summary>File: Project and File Open/Save/Workspace Management</summary>
|
||||
|
||||
This menu is mainly responsible for: **Creating/Opening Files**, **Opening Project Folders**, **Managing Workspaces**, **Saving and Closing**.
|
||||
|
||||
> The most commonly used are: Open Folder to open a project; Open… to open a single file; then use Save / Save All to save changes, and finally use Close Editor / Close Folder to end the current work. Workspace-related content can be slowly learned as you get more projects, no need to understand everything at once.
|
||||
|
||||
- **New Text File**: Create a new unnamed text buffer for temporary notes or quick pasting.
|
||||
- **New File…**: Create a new file in the project (usually asks you to choose path/name).
|
||||
- **New Window**: Open a new VS Code window instance.
|
||||
- **New Window with Profile**: Open a new window with a specified Profile (extension/settings combination), suitable for isolating environments for different courses/projects.
|
||||
- **Open…**: Open a single file for editing.
|
||||
- **Open Folder…**: Open a folder as the project root directory (the most commonly used "open project" method).
|
||||
- **Open Workspace from File…**: Open a `.code-workspace` file to load a workspace with multiple folders/specific settings.
|
||||
- **Open Recent**: Quickly access recently opened files/folders/workspaces.
|
||||
- **Add Folder to Workspace…**: Add another folder to the current workspace (forming a multi-root workspace).
|
||||
- **Save Workspace As…**: Save the current workspace structure as a `.code-workspace` file for easy sharing/reuse.
|
||||
- **Duplicate Workspace**: Duplicate the current workspace configuration (commonly used to create similar project environments).
|
||||
- **Save**: Save changes to the current file.
|
||||
- **Save As…**: Save the current file with a new name/path.
|
||||
- **Save All**: Save all opened files that have modifications.
|
||||
- **Share**: Entry related to sharing/collaboration (specific content depends on version and extensions).
|
||||
- **Auto Save**: Toggle auto-save strategy (e.g., delayed save/focus change save).
|
||||
- **Revert File**: Discard unsaved changes to the current file and revert to the disk version.
|
||||
- **Close Editor**: Close the current tab.
|
||||
- **Close Folder**: Close the current project folder (workspace becomes empty).
|
||||
- **Close Window**: Close the current VS Code window.
|
||||
|
||||
</details>
|
||||
|
||||
<details class="custom-block details" id="vscode-edit-menu">
|
||||
<summary>Edit: Basic Editing, Find/Replace, Comments and Quick Edit Actions</summary>
|
||||
|
||||
This menu is mainly responsible for: **Undo/Redo**, **Cut/Copy/Paste**, **Find/Replace**, **Comments and Editor Actions** (improving editing efficiency).
|
||||
|
||||
- **Undo / Redo**: The most basic operations for when you write code wrong.
|
||||
- **Cut / Copy / Paste**: Text transportation.
|
||||
- **Find / Replace**: Search or batch modify in the current file.
|
||||
- **Find in Files / Replace in Files**: Global (whole project) search and replace, very powerful but use with caution.
|
||||
- **Toggle Line Comment**: `Ctrl + /`, quickly comment/uncomment the current line.
|
||||
- **Toggle Block Comment**: `Shift + Alt + A`, quickly comment/uncomment the selected area.
|
||||
- **Emmet: Expand Abbreviation**: A powerful tool for HTML/CSS development, type shorthand and press Tab to expand code.
|
||||
|
||||
</details>
|
||||
|
||||
<details class="custom-block details" id="vscode-selection-menu">
|
||||
<summary>Selection: Multi-cursor and Smart Selection</summary>
|
||||
|
||||
This menu is mainly responsible for: **Cursor Control**, **Multi-line Editing**, **Expand/Shrink Selection**. This is VS Code's killer feature for improving efficiency.
|
||||
|
||||
- **Select All**: Select all content in the current file.
|
||||
- **Expand Selection / Shrink Selection**: Intelligently perceive syntax structure, gradually expand or shrink the selection range (e.g., word -> string -> inside parentheses -> whole line -> function body).
|
||||
- **Copy Line Up / Down**: Quickly clone the current line.
|
||||
- **Move Line Up / Down**: `Alt + ↑ / ↓`, adjust code line order directly without cut and paste.
|
||||
- **Add Cursor Above / Below**: `Ctrl + Alt + ↑ / ↓`, enable multi-cursor mode to edit multiple lines simultaneously.
|
||||
- **Add Cursor to Line Ends**: After selecting multiple lines of text, add a cursor at the end of each line.
|
||||
|
||||
</details>
|
||||
|
||||
<details class="custom-block details" id="vscode-view-menu">
|
||||
<summary>View: Interface Layout and Panel Control</summary>
|
||||
|
||||
This menu is mainly responsible for: **Toggle Sidebar/Panel**, **Adjust Layout**, **Command Palette**, **Output and Debug Console**.
|
||||
|
||||
- **Command Palette…**: `Ctrl + Shift + P` / `F1`, VS Code's central command center, can search and execute all commands.
|
||||
- **Open View…**: Quickly open specific sidebar views (such as Explorer, Source Control).
|
||||
- **Appearance**: Control fullscreen, menu bar visibility, sidebar position, zoom level (Zoom In/Out).
|
||||
- **Editor Layout**: Split editor (Split Up/Down/Left/Right) for side-by-side code comparison.
|
||||
- **Explorer / Search / Source Control / Run / Extensions**: Directly switch views in the Activity Bar.
|
||||
- **Problems / Output / Debug Console / Terminal**: Directly control the display content of the bottom panel.
|
||||
- **Word Wrap**: `Alt + Z`, control whether long lines of code automatically wrap (does not affect actual file content).
|
||||
|
||||
</details>
|
||||
|
||||
<details class="custom-block details" id="vscode-go-menu">
|
||||
<summary>Go: Code Navigation and Jumping</summary>
|
||||
|
||||
This menu is mainly responsible for: **Jumping Between Files**, **Jumping Between Symbols (Functions/Variables)**.
|
||||
|
||||
- **Back / Forward**: Like a browser, jump between your cursor history positions.
|
||||
- **Switch Editor…**: Quickly switch between opened tabs.
|
||||
- **Go to File…**: `Ctrl + P`, type filename to quickly open files.
|
||||
- **Go to Symbol in Editor…**: `Ctrl + Shift + O`, list functions/classes/variables in the current file for quick jumping.
|
||||
- **Go to Definition**: `F12`, jump to the definition of the variable or function at the cursor.
|
||||
- **Go to References**: `Shift + F12`, see where this variable or function is used.
|
||||
- **Go to Line/Column…**: `Ctrl + G`, jump to a specified line number.
|
||||
|
||||
</details>
|
||||
|
||||
<details class="custom-block details" id="vscode-run-menu">
|
||||
<summary>Run: Debugging and Execution</summary>
|
||||
|
||||
This menu is mainly responsible for: **Start Debugging**, **Breakpoint Management**.
|
||||
|
||||
- **Start Debugging**: `F5`, run the program in debug mode (supports breakpoints, variable watching).
|
||||
- **Run Without Debugging**: `Ctrl + F5`, run the program directly without attaching a debugger (slightly faster).
|
||||
- **Stop Debugging**: Forcefully end the current debugging session.
|
||||
- **Restart Debugging**: Run again.
|
||||
- **Toggle Breakpoint**: `F9`, add or remove a red dot (breakpoint) on the current line.
|
||||
- **New Breakpoint**: Supports conditional breakpoints, log breakpoints, and other advanced features.
|
||||
|
||||
</details>
|
||||
|
||||
<details class="custom-block details" id="vscode-terminal-menu">
|
||||
<summary>Terminal: Integrated Command Line</summary>
|
||||
|
||||
This menu is mainly responsible for: **New Terminal**, **Manage Terminal Windows**.
|
||||
|
||||
- **New Terminal**: Open a new Shell (PowerShell/Bash/Zsh) in the bottom panel.
|
||||
- **Split Terminal**: Split left/right/up/down in the same terminal panel to run multiple commands simultaneously.
|
||||
- **Run Task…**: Run build/test tasks defined in `tasks.json`.
|
||||
|
||||
</details>
|
||||
|
||||
<details class="custom-block details" id="vscode-help-menu">
|
||||
<summary>Help: Documentation and Feedback</summary>
|
||||
|
||||
- **Welcome**: Open the welcome page (contains getting started guide, recent projects).
|
||||
- **Show All Commands**: Same as Command Palette.
|
||||
- **Documentation**: Jump to official documentation.
|
||||
- **Editor Playground**: Interactive tutorial for learning editing techniques.
|
||||
- **Check for Updates…**: Manually check for updates.
|
||||
- **About**: View version number, build time, Electron/Node version information.
|
||||
|
||||
</details>
|
||||
@@ -66,17 +66,17 @@ Master the Vibe Coding workflow, learn to deconstruct requirements, and independ
|
||||
|
||||
<NavGrid>
|
||||
<NavCard
|
||||
href="/zh-cn/stage-1/appendix-a-product-thinking/"
|
||||
href="/en/stage-1/appendix-a-product-thinking/"
|
||||
title="Product Thinking and Solution Design"
|
||||
description="Supplement necessary thinking models for product managers to improve demand analysis and product design capabilities."
|
||||
/>
|
||||
<NavCard
|
||||
href="/zh-cn/stage-1/appendix-industry-scenarios/"
|
||||
href="/en/stage-1/appendix-industry-scenarios/"
|
||||
title="AI Industry Application Scenarios (B-end)"
|
||||
description="Understand AI application scenarios in different industries to find product inspiration and direction."
|
||||
/>
|
||||
<NavCard
|
||||
href="/zh-cn/stage-1/appendix-c-consumer-scenarios/"
|
||||
href="/en/stage-1/appendix-c-consumer-scenarios/"
|
||||
title="AI Consumer Scenarios Inspiration (C-end)"
|
||||
description="Explore AI application scenarios in consumer-grade products and inspire creative ideas."
|
||||
/>
|
||||
@@ -93,17 +93,17 @@ Master the Vibe Coding workflow, learn to deconstruct requirements, and independ
|
||||
|
||||
<NavGrid>
|
||||
<NavCard
|
||||
href="/zh-cn/stage-1/appendix-b-common-errors/"
|
||||
href="/en/stage-1/appendix-b-common-errors/"
|
||||
title="What to do if you encounter errors when coding"
|
||||
description="Summarize common error messages and solutions during development to help you troubleshoot problems quickly."
|
||||
/>
|
||||
<NavCard
|
||||
href="/zh-cn/stage-1/appendix-articles/example0-1/vibe-coding-tools-snake-game-tutorial"
|
||||
href="/en/stage-1/appendix-articles/example0-1/vibe-coding-tools-snake-game-tutorial"
|
||||
title="Comparison of Seven AI Programming Tools"
|
||||
description="Compare and test mainstream AI programming platforms to help you choose the most suitable tool."
|
||||
/>
|
||||
<NavCard
|
||||
href="/zh-cn/stage-1/appendix-articles/example0-2/vibe-coding-tools-build-website-with-ai-coding-and-design-agents"
|
||||
href="/en/stage-1/appendix-articles/example0-2/vibe-coding-tools-build-website-with-ai-coding-and-design-agents"
|
||||
title="Design Websites using Design and Programming Agents"
|
||||
description="Learn how to use AI agents to work together and improve development efficiency."
|
||||
/>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,118 +1,117 @@
|
||||
# 数据分析:拨开数字的迷雾
|
||||
# 数据分析:核心概念、逻辑与深度洞察
|
||||
|
||||
::: tip 🎯 核心问题
|
||||
**如果把网站比作人,数据到底是在检查什么?**
|
||||
当我们面对每天产生的惊人数据:几十亿行的日志、数以千万计的点击量,我们该怎么下手?是依靠 DAU (日活) 自满,还是对次日惊人下跌的留存率战战兢兢?本章以侦探探案的视角告诉你,怎样让这些死气沉沉的数字,自己开口说话。
|
||||
**如何从散乱的数据中提取出能够指导业务的“确定性”?**
|
||||
在互联网产品中,每秒都在产生海量的用户行为记录。仅看总量(如总访问量)往往会掩盖真相。本章将由浅入深,从基础统计学指标到高级业务分析模型,带你掌握数据分析的底层逻辑。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 0. 全景图:从数字垃圾到金矿洞察
|
||||
## 0. 概述:数据分析的本质
|
||||
|
||||
数据本身毫无生命。它们不过是安静地躺在磁盘扇区里,堆砌着毫无温度的 0 和 1。每一天有 100 万人在你的 APP 里注册,但其中可能有 80% 的人在头三分钟内就再也不回来了。
|
||||
> 很多人认为看一眼报表就是数据分析。如果你不理解“数据、信息、洞察”之间的转化逻辑,你就会被困在数字的海量细节中。学习本节是为了让你建立全局观,明白数据分析的最终目的不是为了“汇报”,而是为了“决策”。
|
||||
|
||||
这些就是赤裸裸的“数据”。
|
||||
而我们该如何像一名法医或是名侦探一样,从一滩纷乱如麻的现场脚印中,剥茧抽丝,揪出潜藏背后的罪魁祸首呢?“发现问题、分析本质、指引改进”,这套流程的学名叫 **数据分析 (Data Analysis)**。
|
||||
数据分析并非简单的“报表汇总”,而是一个**信息降维**与**特征提取**的过程。
|
||||
|
||||
如果把企业的运营比喻成一个人去医院做了体检:
|
||||
1. **收集体检单**:把肝功能、心电图全查出来(数据收集与埋点)。
|
||||
2. **血常规初诊**:看看身体各个指标的大概均值是否异常(描述性统计)。
|
||||
3. **CT扫出病原**:深入病灶查看到底大面上哪里出问题了(群体过滤与数据聚合)。
|
||||
4. **追溯病情**:观察健康下降的全过程,看看这病到底是哪一天落下的(留存和漏斗排查)。
|
||||
- **原始数据 (Raw Data)**:是零散、无序的记录(如:用户A在10:01点击了按钮B)。
|
||||
- **信息 (Information)**:是加工后的数据(如:今天有30%的用户点击了按钮B)。
|
||||
- **洞察 (Insight)**:是发现数据背后的规律(如:按钮B的点击率在移动端远高于PC端,说明移动端用户更依赖该功能)。
|
||||
|
||||
我们的目标是建立一套系统的分析框架,通过“观测 -> 拆解 -> 定位 -> 决策”的闭环来驱动业务增长。
|
||||
|
||||
---
|
||||
|
||||
## 1. 描述性统计:体温计与血常规
|
||||
## 1. 描述性统计:如何一句话概括全貌
|
||||
|
||||
“最近我们班的成绩怎么这么差?”
|
||||
面对这样的疑问,如果你一条条念名字:“张三 52,李四 95...”,那简直是要发疯的事情。
|
||||
> 当面对 10 万行数据时,你不可能逐行查阅。你需要一种“信息压缩”的能力,用极少数的指标精准抓住数据的脉络。如果你不懂均值与中位数的统计陷阱,你就会在分析业务表现(如用户人均消费)时被极端数值误导,得出荒谬的结论。
|
||||
|
||||
面对庞杂无垠的海量细节,我们发明了一种极为凝练的力量:用几个极致抽象的**指标**,来一把掐住整体数据的脉络。
|
||||
当数据集有数万条记录时,我们需要用极少数的“代表性指标”来描述其整体面貌。
|
||||
|
||||
<DataAnalysisDemo tab="stats" />
|
||||
<DescriptiveStatsDemo />
|
||||
|
||||
### 1.1 均值 (Mean)
|
||||
|
||||
它就如同温度计,一把揽住了班级里的所有成员并简单平摊。但这把标尺有时候会撒谎。如果你正在和比尔盖茨一起吃饭,那你们这桌就拥有两个亿万富翁,但这并不能说明你同样身怀巨款。这就是均值在面对**极端值 (Outliers)** 时的脆弱。
|
||||
### 1.1 均值 (Mean):整体水平的基准
|
||||
均值(算术平均数)是最直观的指标。
|
||||
- **计算逻辑**:所有数值的总和除以数据总量。
|
||||
- **局限性**:它极易受到**极端离群值 (Outliers)** 的干扰。
|
||||
- **示例**:如果 9 名员工月薪 5k,老板月薪 100k,则平均工资高达 1.45w。此时均值并不能真实代表大多数员工的收入水平。
|
||||
|
||||
### 1.2 中位数 (Median) 与 众数 (Mode)
|
||||
- **中位数**:将数据由小到大排序,取最中间位置的数值。它能有效抵御离群值的干扰,真实反映典型的“中间层”水平。
|
||||
- **众数**:数据集中出现频次最高的数值。在分析“用户最喜欢的商品”、“最常发生的错误代码”时,众数能最直接地指明群体倾向。
|
||||
|
||||
此时我们引入了“把大家按名次站成一排”的理念,精确掐住站在正中央(第 50 分位数)的那个人的水平。不管比尔盖茨身价多高,只要他排在队尾,他就完全拉扯不了位于中央的那位普通人。
|
||||
而众数则是告诉你货架上卖的最火的那件爆款是什么(因为它是出现次数最多的常客)。
|
||||
|
||||
### 1.3 标准差 (Standard Deviation)
|
||||
|
||||
如果你和隔壁班的均值都是 80 分。隔壁班所有人考的都是 80 左右,而我们班是一半人满分 100,一半人刚刚及格 60。这怎么区分?
|
||||
方差和标准差就是一把尺,专门衡量这种“数据像散沙一样分散”或者“像砖块一样抱团”的聚合程度。
|
||||
### 1.3 标准差 (Standard Deviation):分布的“宽窄”
|
||||
它描述了数据点距离均值的波动力度。
|
||||
- **低标准差**:数据非常集中,均值的代表性强(如:工厂流水线的零件尺寸)。
|
||||
- **高标准差**:数据分布散乱,个体差异极大。
|
||||
- **意义**:在性能监控中,高标准差往往意味着系统的稳定性不足,存在大量响应极慢的“长尾请求”。
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据聚合:上帝视角鸟瞰众生
|
||||
## 2. 数据聚合:挖掘群体的微观规律
|
||||
|
||||
当我们凝视个人时,就像管中窥豹。一个人偶尔退货并不能说明这身衣服的质量有问题。但如果我们打开上帝视角,把零散的一件件碎片拼在一起呢?
|
||||
> “所有用户平均转化率 5%” 往往是一句毫无意义的真话。你必须学会如何把数据“切开”,才能发现不同地域、不同渠道、不同设备用户之间的巨大差异。聚合分析能带你穿透“大锅饭”般的平均值,直达那些被掩盖的真实业务痛点。
|
||||
|
||||
这就来到了 **数据聚合 (Aggregation)** 的领域。
|
||||
我们经常会把海量订单,按着地区、年龄、或者是购买时间,像推倒多米诺骨牌一样给拢成一打一打的。
|
||||
个体行为往往具有偶然性,但群体行为具有统计规律。**数据聚合 (Aggregation)** 的核心在于通过特定的维度对人群进行“切片”。
|
||||
|
||||
<DataAnalysisDemo tab="aggregation" />
|
||||
<DataAggregationDemo />
|
||||
|
||||
### 2.1 常见的上帝之手
|
||||
### 2.1 聚合的核心逻辑:拆分-计算-组合
|
||||
1. **拆分 (Split)**:根据某个属性(如:城市、注册渠道、新老用户)进行分组。
|
||||
2. **计算 (Apply)**:在每个组内执行聚合函数,如 `COUNT()` 计数、`SUM()` 求和、`AVG()` 求均值。
|
||||
3. **组合 (Combine)**:对比不同组的结果,发现差异点。
|
||||
|
||||
比如我们常用 SQL(结构化查询语言)的:
|
||||
- **COUNT ()**:迅速数出这堆究竟有多少人。
|
||||
- **SUM ()**:把他们兜里的钱全部卷在一起加总。
|
||||
- **GROUP BY**:按照某种鲜明的属性(比如城市、或者是新老用户),把他们硬生生拆开分别摆好,这就叫做分组比对。
|
||||
|
||||
这是一种摧枯拉朽的化繁为简:通过聚合,几百 G 巨大的行为轨迹表格被瞬间碾压成了短短的两三行汇总。
|
||||
### 2.2 为什么必须进行分组 (Group By)?
|
||||
汇总数据往往会掩盖问题。例如,整体转化率在涨,但拆分后发现其实是“上海地区”暴增拉高了整体,而其他地区都在跌。通过聚合分析,我们可以从“大锅饭”中精准定位到表现最优秀或最糟糕的分支。
|
||||
|
||||
---
|
||||
|
||||
## 3. 漏斗模型:捕捉消失的幸存者
|
||||
## 3. 漏斗模型:定位价值链的“出血点”
|
||||
|
||||
当你满心欢喜地投入千万广告费,拉来了 10 万名兴冲冲想要下单的用户。但第二天打开报表却如同坠入冰窟:实际付款到账的只有区区 1,200 人!
|
||||
> 你投入了大量资源拉来用户,结果成交寥寥,钱都白花了吗?漏斗模型能告诉你用户到底在哪个门槛被绊倒了。学会这一节,你能把“业务优化”从盲目猜测变成精准研发,将资源投入到转化率产出最高的环节。
|
||||
|
||||
这几万人都死在这条路上的哪里了?
|
||||
这个时候,我们需要一把异常锋利的手术刀——**转化漏斗 (Funnel)**。
|
||||
用户从进入到完成最终目标(如付费)是一个层层筛选的过程。漏斗模型(Funnel)不仅是看最终转化率,更是为了看**在哪里丢了人**。
|
||||
|
||||
<DataAnalysisDemo tab="funnel" />
|
||||
<FunnelAnalysisDemo />
|
||||
|
||||
### 3.1 横切业务链的断层
|
||||
### 3.1 核心转化指标
|
||||
- **总体转化率**:完成终点的总人数 / 进入起点的总人数。
|
||||
- **步骤转化率**:当前步骤人数 / 上一步骤人数(反映了该步的通过效率)。
|
||||
- **流失率**:1 - 步骤转化率。
|
||||
|
||||
用户从进入店铺大门到最终结账,中间会经历一系列的惊险跨栏:
|
||||
|
||||
- **第一关**:看到商品详情页(10万人)。
|
||||
- **第二关**:愿意拉到底点击购买(跌至6万人)。
|
||||
- **第三关**:填写那让人烦躁的收货地址(跌至3万人)。
|
||||
- **第四关**:打开支付系统输入复杂的验证码验证(终于只剩下最后生还的 1,200 人)。
|
||||
|
||||
通过这张鲜血淋漓的“阵亡率清单”,你一眼就能看出:绝大多数人倒在了填写地址和支付跳转的过程上!比起盲目地去修改什么按钮颜色,**直接砍掉两项非必要的地址表单,才是救这帮快要在路上死绝的用户的致命灵丹**。
|
||||
### 3.2 深度分析思路
|
||||
如果某一环节的流失率异常偏高,说明在该处存在**体验摩擦**。例如:
|
||||
- 在注册页流失严重:说明表单太复杂或验证码收不到。
|
||||
- 在选择支付方式处流失:说明支付方式太少或跳转加载过慢。
|
||||
在漏斗最窄的地方投入力量进行优化,其收益通常是最大的。
|
||||
|
||||
---
|
||||
|
||||
## 4. 留存分析:忠诚度的生死考验
|
||||
## 4. 留存分析:产品的“硬核”体检
|
||||
|
||||
如果一款产品天天能像抽水机一样抽来大量用户,但池子底部却被挖开了一个大黑洞,漏水漏得精光。这能算是成功的商业模式吗?
|
||||
> 留存是产品价值的第一金标准。如果拉新是给桶加水,留存就是看这桶漏不漏。如果你只会看总访问量(流量)而不会分析留存(留客),你就无法判断产品是在健康成长,还是在玩一场注定崩盘的数字游戏。
|
||||
|
||||
在这个领域,日活跃用户数(DAU)往往是一个自欺欺人的“虚荣指标”,真正照魔镜的是**留存率 (Retention Rate)**。
|
||||
用户增长不代表成功,能留住用户才是核心价值。留存率(Retention)衡量了用户在特定时间后回访的比例。
|
||||
|
||||
<DataAnalysisDemo tab="retention" />
|
||||
<RetentionAnalysisDemo />
|
||||
|
||||
### 4.1 留存的衰减断崖
|
||||
### 4.1 核心时间窗口
|
||||
- **次日留存 (Day 1)**:关注“第一印象”。用户首次进入后的 24 小时内是否感受到了核心价值?
|
||||
- **7日留存 (Day 7)**:关注“习惯养成”。用户是否在第一周内形成了周期性使用的习惯?
|
||||
- **30日留存 (Day 30)**:关注“长期粘性”。它决定了产品的生存上限。
|
||||
|
||||
留存告诉我们一个真相:在一群人经历狂欢般地第一天疯狂下载使用后,第二天还有多少人记得登录(次日留存),一周后有多少人形成了习惯(7日留存),一个月以后还有没有哪怕是一小撮顽固信徒在这个平台扎根(30日留存)?
|
||||
|
||||
这不仅代表着你的产品是不是在玩一场昂贵的拉新骗局。甚至,如果留存跌破 15%,你拉多少人进来都是一场注定向深渊下坠的徒劳。寻找产品初期的破局点(Aha Moment,啊哈时刻),就等于是如何拯救那条惨不忍睹的跌落生命线。
|
||||
### 4.2 留存曲线的形态:判定 PMF
|
||||
- **持续跌落至零**:说明产品没有解决用户痛点,或者获取了错误的用户群体。
|
||||
- **趋于平稳(长尾)**:说明产品已经获得了 **PMF (Product-Market Fit)**,拥有了一群忠实粘性用户,具备了规模化扩张的基础。
|
||||
|
||||
---
|
||||
|
||||
## 5. 总结:培养向事实低头的直觉
|
||||
## 5. 结语:建立科学的数据直觉
|
||||
|
||||
**数据从不撒谎,但人经常说谎。**
|
||||
优秀的分析师应当具备批判性思维,不被表象误导:
|
||||
1. **看分布而不仅看均值**:思考数据背后的差异性和离群值。
|
||||
2. **看局部而不仅看总量**:通过多维聚合(Group By)还原真实场景。
|
||||
3. **看趋势而不仅看时点**:通过留存曲线观察产品的长期健康度。
|
||||
4. **寻找断层而非盲目优化**:通过漏斗定位真正的业务瓶颈。
|
||||
|
||||
一个优秀的数据大脑不是为了用漂亮的术语去忽悠领导或投资人,而是像医生看化验单一样寻找最深刻的根源:
|
||||
1. **寻找异常**:当极差和标准差大幅波动的那些黑天鹅夜晚,到底发生了什么?
|
||||
2. **切分颗粒**:用 GROUP BY 不停地顺藤摸瓜,用上帝视角将整体拆成碎片寻找突破。
|
||||
3. **定位断崖**:盯着那流血满地的转化漏斗,寻找最薄弱的环节去对症下药。
|
||||
4. **守住生命线**:不再为每天短暂飙升的流量叫好,只紧盯留存在一个月后的真实忠诚。
|
||||
|
||||
这,就是一个真正的破冰者手起刀落的数据修行。
|
||||
数据分析的目标不是生成漂亮的报告,而是将“不确定性”降至最低,做出基于事实的明智决策。
|
||||
test
|
||||
|
||||
@@ -1,136 +1,221 @@
|
||||
# 数据埋点:数字化帝国的全视之眼
|
||||
# 数据埋点:记录用户在应用中做了什么
|
||||
|
||||
::: tip 🎯 核心问题
|
||||
**如果看不见用户,我们要怎么做业务?**
|
||||
::: tip 🎯 本章要解决的问题
|
||||
**我们怎么知道用户在应用里做了什么?**
|
||||
|
||||
每天有 100 万人登录你的应用,但如果没有埋点,你面对的就像是 100 万个飘忽不定的黑箱幽灵。你不知道他们点击了什么,不知道他们在哪里离开,更不知道他们为什么不买单。
|
||||
想象你开了一家线下奶茶店。你可以站在柜台后面,亲眼观察每位顾客:他们走进来先看了菜单多久?点了哪款饮品?有没有犹豫后放弃离开?
|
||||
|
||||
这个看似极其抽象的词汇——"埋点"(Event Tracking),其实就是在用户必经之路上安插"隐形探头"。本章,我们将不讲枯燥的大数据黑话,而是顺着**"画出监控图纸 -> 规范笔录格式 -> 装车寄送包裹 -> 海关质检入库"**这条主线,带你零基础看清海量用户行为是如何一步步被抓取、清洗,并最终变成报表上的黄金的。
|
||||
但如果你的"店铺"是一个手机 App 或网站,你无法亲眼看到用户的操作。这时候就需要一种技术手段,在应用的关键位置"埋"下记录点,自动帮你记录用户的每一步操作。这就是**数据埋点(Event Tracking)**。
|
||||
|
||||
"埋点"这个词听起来很专业,但它的核心思路很简单:**在用户可能操作的地方,放一个"记录器",把用户做了什么记下来。**
|
||||
|
||||
本章将分四步讲解这个过程:
|
||||
|
||||
1. **选择采集方案** — 决定在哪里放记录器、怎么放
|
||||
2. **设计数据格式** — 决定每条记录应该包含哪些信息
|
||||
3. **传输与缓存** — 把记录从用户手机安全送到服务器
|
||||
4. **清洗与入库** — 整理数据,去掉重复和错误,存入数据库
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 第一步:画出监控图纸 (采集方案选择)
|
||||
## 第一步:选择采集方案 — 在哪里放记录器?
|
||||
|
||||
**目标**:决定在产品的哪些角落,使用什么样的手段安放"监控探头"。
|
||||
**目标**:决定用什么方式来记录用户的操作。
|
||||
|
||||
当产品经理想要知道"到底有多少人点击了购买按钮"时,程序员第一步要做的,就是去代码里埋下采集器。但这就像在商场里装监控,你是紧紧盯着收银台,还是无死角地布满整个商场?
|
||||
举个例子:产品经理想知道"有多少用户点击了购买按钮"。要回答这个问题,开发者需要在"购买按钮"的代码里加上一段记录逻辑 — 每当用户点击这个按钮,就自动记一笔。
|
||||
|
||||
但这里有一个选择题:我们是**只在重要的地方放记录器**(比如只记录"购买"和"注册"),还是**在所有地方都放记录器**(记录用户的每一次点击、滑动、停留)?
|
||||
|
||||
不同的选择,对应不同的埋点方案。
|
||||
|
||||
<DataTrackingDemo tab="methods" />
|
||||
|
||||
**💡 核心原理解析:三大埋点流派**
|
||||
**💡 三种主流的埋点方式**
|
||||
|
||||
目前业界在经历了不断的摩擦和演进后,确定了三种最主流的探头安放方式:
|
||||
行业中常用的埋点方案有三种,各有优劣:
|
||||
|
||||
- **代码埋点 (Code Tracking):最精准的狙击枪**
|
||||
程序员深入到最核心的业务代码内部,当用户真正点击"购买"按下确定的那一刹那,手动写下一行拦截代码。
|
||||
*优势*:它可以顺藤摸瓜,把你兜里的余额、优惠券编号一并死死抓走发给服务器。这是所有核心业务(如支付、注册)的唯一依靠。
|
||||
*代价*:每加一个埋点都要等下一次 App 发版,极其缓慢笨重。
|
||||
- **可视化埋点 (Visual Tracking):产品经理的魔法棒**
|
||||
通过在手机屏幕上蒙上一层可视化的透明层,产品经理直接在屏幕上画个框:"凡是点这个框的人,都给我记下来。"
|
||||
*优势*:极度快捷,完全不需要程序员写代码,所见即所得。
|
||||
*代价*:它只能抓到"表面点击",无法获取内存深处的订单号等深度信息。
|
||||
- **全埋点 (Auto Tracking):无死角的超级雷达**
|
||||
直接在 App 里塞入一个"核弹级"的 SDK,它会像吸尘器一样把你点击的长宽高等所有屏幕动作统统暴力记录。
|
||||
*优势*:绝对不会漏掉任何一个角落的行为。
|
||||
*代价*:数据量如同雪花般庞大,无用的噪音极多,极其考验后期的算力和清洗能力。
|
||||
**方式一:代码埋点(Code Tracking)— 手动精确记录**
|
||||
|
||||
**这一步完成了什么?** 我们成功在用户的手机里埋下了探头,只要用户有动作,探头就会被触发。
|
||||
开发者在代码中手动指定:当用户做了某个操作时,记录一条数据。
|
||||
|
||||
**但问题来了**:探头虽然抓到了动作,如果每个探头都按自己的心情随便乱写信息(有的写中文,有的写英文,有的不写时间),服务器拿到后根本无法阅读。下一步,我们需要规定一套极其严格的书写规范。
|
||||
打个比方:这就像在奶茶店的收银台专门安排一个人,只记录"谁买了什么、花了多少钱"。记录的信息非常详细和准确。
|
||||
|
||||
- *优势*:可以记录非常详细的业务信息,比如用户用了哪张优惠券、账户余额是多少
|
||||
- *代价*:每增加一个新的记录点,都需要开发者写代码、测试、发布新版本,流程较长
|
||||
|
||||
**方式二:可视化埋点(Visual Tracking)— 点击圈选记录**
|
||||
|
||||
不需要写代码。系统提供一个可视化工具,运营人员可以直接在应用界面上"圈选"想要监测的按钮或区域,系统自动开始记录。
|
||||
|
||||
打个比方:这就像在奶茶店的监控画面上,用鼠标框选"收银台区域",系统就自动开始统计这个区域的人流量。
|
||||
|
||||
- *优势*:不需要开发者参与,运营人员自己就能配置,效率很高
|
||||
- *代价*:只能记录"用户点了什么"这类界面操作,无法记录"订单金额"等深层业务数据
|
||||
|
||||
**方式三:全埋点(Auto Tracking)— 自动记录一切**
|
||||
|
||||
在应用中集成一个 SDK(可以理解为一个"工具包"),它会自动记录用户的所有操作:每一次点击、每一次滑动、在每个页面停留了多久。
|
||||
|
||||
打个比方:这就像在奶茶店的每个角落都装上摄像头,记录顾客的一举一动。
|
||||
|
||||
- *优势*:不会遗漏任何操作,覆盖最全面
|
||||
- *代价*:数据量非常大,其中很多是无用信息(比如用户无意识的滑动),后续需要花大量精力筛选和清理
|
||||
|
||||
**本步小结**:选好了埋点方式后,我们的应用就具备了"记录用户操作"的能力。
|
||||
|
||||
**但这里有一个新问题**:记录器虽然能捕获到用户的操作,但如果每个记录器记下来的格式都不一样(比如有的写"用户ID",有的写"userID",有的干脆没记),后续就没法统一分析。所以下一步,我们需要规定一个统一的记录格式。
|
||||
|
||||
---
|
||||
|
||||
## 第二步:规范笔录格式 (事件与数据模型)
|
||||
## 第二步:设计数据格式 — 每条记录应该包含什么?
|
||||
|
||||
**上一步完成了**:我们已经在客户端选择了合适的探头(如代码埋点),成功拦截到了用户的点击动作。
|
||||
**前置条件**:我们已经选好了埋点方式(比如代码埋点),应用已经能够捕获用户的操作了。
|
||||
|
||||
**这一步要实现**:让所有探头都必须使用一种统一结构化的格式,把数据汇报给服务器。
|
||||
**本步目标**:规定一个统一的"记录模板",让所有埋点记录的格式保持一致。
|
||||
|
||||
**目的**:把全世界最复杂的、因人而异的操作,全部降维、拍扁成一张清清爽爽的数据明细表。
|
||||
**为什么需要统一格式?** 想象一下:如果奶茶店有三个店员同时记录销售情况,一个写"小明买了珍珠奶茶 15 元",另一个写"15,奶茶,珍珠",第三个写"珍珠奶茶一杯"。到了月底汇总的时候,这些记录格式完全不同,整理起来会非常痛苦。所以我们需要一张统一的"记录表",规定每条记录必须填写哪些栏位。
|
||||
|
||||
<DataTrackingDemo tab="model" />
|
||||
|
||||
**💡 核心原理解析:4W1H 数据模型**
|
||||
**💡 核心原理:4W1H 记录模板**
|
||||
|
||||
不管你用什么语言开发,这团数据到了服务器门口,就必须回答出极其关键的 `4W1H` 灵魂拷问。你可以把它看作一份给警察局的审讯笔录:
|
||||
无论记录什么操作,每条数据都需要回答以下五个问题(简称 4W1H):
|
||||
|
||||
- **Who (是谁 - user_id/device_id)**:这简直是最核心的部分。如果用户没登录,我们就抓他手机底层的 MAC 地址或 UUID(设备指纹);如果登录了,就死死绑定他的 `user_id`。
|
||||
- **When (何时 - timestamp)**:精确到毫秒的时间戳。特别注意,针对跨国业务,必须强制换算成格林威治标准时间 (UTC),否则你会看到昨天的人穿越到了明天。
|
||||
- **Where & How (何地与如何 - 公共属性)**:被统称为**公共属性 (Common Properties)**。它交代了作案环境:不管你在干什么,你的手机型号(iPhone 15)、网络环境(5G)、App 版本号(v1.2.3)都会被系统自动提取,像一个标签一样死死贴在这条数据上。
|
||||
- **What (业务详情 - 自定义属性)**:被统称为**自定义属性 (Custom Properties)**。如果你的动作是 `add_to_cart` (加入购物车),那我们就必须自定义几个专属的细作:比如商品型号是 iPhone,价格是 7999 元。
|
||||
**Who — 谁做的?**
|
||||
|
||||
**这一步完成了什么?** 这是一场混乱向秩序的妥协,我们终于拿到了一份极致规范、机器可读的 JSON 代码(就像你在上面组件模型中看到的那样)。
|
||||
我们需要知道这条记录是哪个用户产生的。
|
||||
|
||||
**但问题来了**:一份标准的 JSON 准备好了。但如果是双十一,一秒钟有一万个人点击了加入购物车。如果我们让手机直接一秒钟发一万次请求给远方的数据库,数据库瞬间就会被这股洪水打穿融化,手机电量也会瞬间耗尽。下一步,我们要解决运输问题。
|
||||
- 如果用户已经登录,就用他的账号 ID(比如 `user_id: "zhangsan123"`)
|
||||
- 如果用户没有登录,就用设备的唯一标识(比如手机的设备编号),这样至少能区分"这是同一台手机上的操作"
|
||||
|
||||
**When — 什么时候做的?**
|
||||
|
||||
记录操作发生的精确时间,精确到毫秒。
|
||||
|
||||
这里有一个细节:如果你的应用有海外用户,北京时间下午 3 点和纽约时间下午 3 点其实差了 13 个小时。为了避免混乱,所有时间统一转换为 UTC 标准时间(可以理解为"世界统一时间")。
|
||||
|
||||
**Where & How — 在什么环境下做的?**
|
||||
|
||||
这部分记录用户操作时的设备和网络环境,称为**公共属性**。之所以叫"公共",是因为无论用户做了什么操作,这些信息都会自动附带上去。例如:
|
||||
|
||||
- 设备型号:iPhone 15 / 小米 14
|
||||
- 网络类型:WiFi / 5G / 4G
|
||||
- App 版本号:v1.2.3
|
||||
- 操作系统:iOS 18 / Android 15
|
||||
|
||||
这些信息的价值在于:如果发现某个 Bug 只在特定机型上出现,公共属性可以帮助快速定位问题。
|
||||
|
||||
**What — 具体做了什么?**
|
||||
|
||||
这部分记录操作的具体业务细节,称为**自定义属性**。不同的操作需要记录不同的信息。例如:
|
||||
|
||||
- 用户点击"加入购物车":需要记录商品名称、商品价格、商品数量
|
||||
- 用户完成支付:需要记录订单金额、支付方式、优惠券编号
|
||||
|
||||
**本步小结**:通过 4W1H 模板,我们把用户的每一个操作都转化成了一条格式统一的数据记录。在技术实现中,这条记录通常以 JSON 格式存储(JSON 是一种通用的数据格式,上方的交互组件展示了它的样子)。
|
||||
|
||||
**但这里又有一个新问题**:数据格式统一了,但如果应用的用户量很大(比如促销活动期间,每秒钟可能产生上万条记录),用户手机不可能每产生一条记录就立刻发送一次 — 这样既费电又费流量,服务器也扛不住。所以下一步,我们需要设计一个更聪明的传输方式。
|
||||
|
||||
---
|
||||
|
||||
## 第三步:装车与寄送包裏 (本地缓存与管道传输)
|
||||
## 第三步:传输与缓存 — 怎么把数据安全送到服务器?
|
||||
|
||||
**上一步完成了**:我们将用户行为封装成了标准的、带有时间戳和属性的格式化数据块。
|
||||
**前置条件**:用户的每个操作已经被记录成了格式统一的 JSON 数据。
|
||||
|
||||
**这一步要实现**:确保数据在极端的弱网、高并发等恶劣环境下,能够安全、不翻车地送达公司的服务器。
|
||||
**本步目标**:把这些数据从用户的手机(或浏览器)可靠地传输到我们的服务器,即使在网络不好的情况下也不丢数据。
|
||||
|
||||
**目的**:通过缓存和攒批机制,保护用户的手机电量,同时拯救公司脆弱的数据库。
|
||||
**为什么不能直接发送?** 如果每产生一条记录就立刻发一次网络请求,就像每写一封信就跑一趟邮局一样 — 效率太低了。更合理的做法是:攒一批信,一次性送过去。
|
||||
|
||||
<DataTrackingDemo tab="pipeline" />
|
||||
|
||||
**💡 核心原理解析:漫长而致命的数据长征**
|
||||
**💡 核心原理:数据传输的三道保障**
|
||||
|
||||
数据绝不是“点一下按钮,就嗖地一声飞进数据库”的。在它真正被数据分析师查到之前,它在黑暗的管道中经历了一场你无法想象的跋涉:
|
||||
数据从用户手机到服务器,需要经过三道保障机制,确保既高效又不丢数据:
|
||||
|
||||
1. **装车攒批 (Batching)**:SDK 本质上是个老司机,它绝不会拿到一个包裹就发车。它会把用户的点击行为死死扣留在手机内存里。直到积攒了 30 条数据,或者熬过了 5 秒钟的倒计时,它才会把它们压缩成一个大包裹一次性掷出。这样不仅省流量,更是省下了几十倍的 HTTP 网络握手开销。
|
||||
2. **断网地堡 (本地存储)**:如果你刚好走进电梯或者高铁进入隧道,网络断了怎么办?这口数据如果在内存里,App 一关就灰飞烟灭了。所以埋点 SDK 在发车前,必须把数据悄悄写进手机的 SQLite 或者 IndexedDB 硬盘缓存中。即使手机没电关机了,一个月后你重新连上 WiFi,它也会像诈尸一般疯狂补发。
|
||||
3. **削峰填谷 (消息队列)**:当大包裹终于跨越太平洋抵达公司网关时,服务器仍旧不敢直接把它写进硬盘。所有的包裹都会被全数接入卡夫卡(Kafka)等**消息队列**的汪洋大海中。无论流量多么残暴,消息队列都会温柔地将其吸收,然后像涓涓细流一样,慢慢排队喂给后端的数据库。
|
||||
**第一道:攒一批再发(批量聚合)**
|
||||
|
||||
**这一步完成了什么?** 历经千难万险,无数个封装好的 JSON 包裹,终于安全平稳地度过了网络高峰,抵达了后端机房。
|
||||
SDK(埋点工具包)不会每产生一条记录就发送一次,而是先把记录暂存在手机内存里。当攒够一定数量(比如 30 条),或者等待超过一定时间(比如 5 秒),再把这一批数据打包,一次性发送出去。
|
||||
|
||||
**但问题来了**:包裹虽然到了,但在客户端疯狂的断点续传甚至重试机制下,我们极大概率会收到两份完全一样的双胞胎数据。直接存起来,财务看到报表会当场崩溃(销售额翻倍的假象)。下一步,必须进行海关重重清洗。
|
||||
这就像寄快递:你不会买一件东西就跑一趟快递站,而是攒几件一起寄,省时省力。对手机来说,这样做能减少网络请求次数,省电省流量。
|
||||
|
||||
**第二道:断网也不丢(本地存储)**
|
||||
|
||||
用户在电梯里、地铁隧道中,手机经常没有网络信号。如果数据只存在内存里,用户一关闭 App,数据就没了。
|
||||
|
||||
所以 SDK 会把还没发送的数据存到手机的本地存储中(类似于把信先放进抽屉)。等网络恢复后,再自动把这些数据补发出去。这样即使用户短暂断网,数据也不会丢失。
|
||||
|
||||
**第三道:服务器不被压垮(消息队列)**
|
||||
|
||||
数据到达服务器后,并不会直接写入数据库。为什么?因为在促销活动等高峰期,可能每秒有几万条数据同时涌入,数据库如果直接处理这么大的量,可能会崩溃。
|
||||
|
||||
解决方案是在中间加一个"缓冲区",技术上叫**消息队列**(常用的工具叫 Kafka)。它的作用就像餐厅的取号排队系统:高峰期顾客(数据)先排队等候,厨房(数据库)按自己的节奏一个一个处理,不会被同时涌入的订单压垮。
|
||||
|
||||
**本步小结**:通过"攒一批再发 → 断网本地存储 → 消息队列缓冲"这三道保障,数据已经安全抵达了服务器。
|
||||
|
||||
**但还有一个问题**:因为断网重连后会自动补发数据,同一条记录有可能被发送了两次。如果不处理就直接存入数据库,数据就会重复(比如一笔 100 元的订单被记成了两笔,销售额就虚高了)。所以下一步,我们需要对数据进行"清洗"。
|
||||
|
||||
---
|
||||
|
||||
## 第四步:海关质检与入库 (ETL 清洗)
|
||||
## 第四步:清洗与入库 — 整理数据,去掉"脏数据"
|
||||
|
||||
**上一步完成了**:数据安全度过了网络拥堵,平稳地来到了后端服务器的内存里。
|
||||
**前置条件**:数据已经通过传输管道安全抵达服务器。
|
||||
|
||||
**这一步要实现**:剔除水分和杂质,把原本泥沙俱下的原石,提炼成绝对纯净的数据金条。
|
||||
**本步目标**:在数据正式存入数据库之前,先做一次"体检"— 去掉重复的、修复格式有问题的,确保最终存储的数据干净、准确。
|
||||
|
||||
**目的**:确保入库的数据无重复、无错漏,供分析师直接查询使用。
|
||||
**为什么需要清洗?** 就像收到一箱快递后,你需要检查一下:有没有重复发货的?有没有发错的?有没有包装破损的?数据也是一样,直接存入数据库之前,需要先检查和整理。
|
||||
|
||||
这个过程在技术上叫做 **ETL**,是三个英文单词的缩写:
|
||||
- **E**xtract(提取):从消息队列中取出数据
|
||||
- **T**ransform(转换):检查和修复数据格式
|
||||
- **L**oad(加载):把清洗好的数据写入数据库
|
||||
|
||||
<DataTrackingDemo tab="overview" />
|
||||
|
||||
**💡 核心原理解析:ETL (提取、转换、加载) 提纯车间**
|
||||
**💡 核心原理:清洗数据的两个关键动作**
|
||||
|
||||
数据在最终落入冰冷的 ClickHouse 或 Hive 这样的庞大**数据仓库 (Data Warehouse)** 之前,需要经历最后一道关卡——数据清洗室。
|
||||
**动作一:去重 — 去掉重复的记录**
|
||||
|
||||
- **无情的去重杀手 (De-duplication)**:
|
||||
因为手机网络很差,可能第一次发出了包裹,但没收到服务器的回复,手机以为没发成功,就又发了一遍。服务器怎么辨别?
|
||||
答案是在第一步打包时,手机就给每一条数据生成了一个极其唯一的 `dedup_id` (比如全球唯一的 UUID)。服务器清洗站有一个滤网,看到同样的 ID,直接把第二条丢进垃圾桶,保证数据绝对唯一。
|
||||
- **修剪畸形儿 (Validation & Transformation)**:
|
||||
早期版本 App 发来的数据可能是 `userId`,而最新版叫 `user_id`。如果带着这种混乱入库会导致灾难。此时在这里必须写脚本强制把它们全部对齐。发现有的时间戳是未来时间?抛弃!发现有的国家代码写了外星文字?直接打上 `unknown`。
|
||||
前面提到,断网重连后 SDK 会自动补发数据,这可能导致同一条记录被发送了多次。怎么识别哪些是重复的?
|
||||
|
||||
**这一步完成了什么?** 沙子被洗成了真金。这些被抚平了所有褶皱、格式统一、完美无缝的数据,终于静静地躺在了数据仓库里。只需分析师一句 `SELECT * FROM events` 的 SQL 召唤,它们就会在 0.1 秒内呈现出人类群体最深的奥秘。
|
||||
方法很简单:在客户端打包数据时,给每条记录分配一个全球唯一的编号(叫做 `dedup_id`,类似于快递单号)。服务器在存储数据前,先检查这个编号是否已经存在 — 如果已经存在,说明是重复数据,直接丢弃。
|
||||
|
||||
**动作二:校验与格式统一 — 修复不规范的记录**
|
||||
|
||||
应用会不断更新版本,不同版本的埋点代码可能存在细微差异。比如:
|
||||
|
||||
- 旧版本把用户 ID 字段命名为 `userId`,新版本改成了 `user_id`
|
||||
- 某些记录的时间戳明显不合理(比如显示为 1970 年)
|
||||
- 某些字段的值无法识别
|
||||
|
||||
在这一步,系统会编写转换规则来统一处理这些问题:字段名不一致的统一对齐,时间戳异常的记录予以丢弃,无法识别的值标记为 `unknown`。
|
||||
|
||||
**本步小结**:经过去重和格式校验后,数据以干净、统一的形式写入**数据仓库**(一种专门用于存储和分析大量数据的数据库,常见的有 ClickHouse、Hive 等)。数据分析师可以直接用 SQL 语句查询这些数据,获得可靠的分析结果。
|
||||
|
||||
---
|
||||
|
||||
## 完整流程回顾
|
||||
|
||||
让我们把整个万里长征串联起来看一看:
|
||||
以下是数据埋点从采集到入库的四步流程总结:
|
||||
|
||||
| 步骤 | 行动 | 完成了什么 | 下一步我们需要解决 |
|
||||
|------|-----------|---------------|----------------|
|
||||
| **1. 采集方案选择** | 决定用代码还是全埋点 | 在客户端深处安插了吸取操作的探头 | 探头抓到的数据像乱码一样,需要规范 |
|
||||
| **2. 事件与数据模型** | 建立 4W1H 模板 | 将乱码收敛为格式完美的极简 JSON | 手机网络极端脆弱,没法立刻发这么标准的数据 |
|
||||
| **3. 数据管道与缓存** | 缓冲攒批,重试补传 | 保证数据扛住了电梯断网和双十一洪峰 | 安全抵达但可能存在重复发包的脏数据 |
|
||||
| **4. ETL 清洗入库** | 去除重复、修复乱码 | ✅ 落入数据仓库,变成 BI 报表上的增长金条 | (完美结束,开始分析数据反哺商业) |
|
||||
| 步骤 | 做了什么 | 得到了什么 | 还剩什么问题 |
|
||||
|------|----------|-----------|-------------|
|
||||
| **1. 选择采集方案** | 决定用哪种方式记录用户操作 | 应用具备了记录能力 | 各记录器的数据格式不统一 |
|
||||
| **2. 设计数据格式** | 用 4W1H 模板统一记录格式 | 每条记录都是标准的 JSON | 用户量大时逐条发送扛不住 |
|
||||
| **3. 传输与缓存** | 攒批发送、断网存储、队列缓冲 | 数据安全抵达服务器 | 重试可能导致数据重复 |
|
||||
| **4. 清洗与入库** | 去重、校验、格式统一 | ✅ 干净的数据存入数据仓库 | — |
|
||||
|
||||
---
|
||||
|
||||
## 结语:不可见的地下长城
|
||||
## 结语
|
||||
|
||||
当我们点一下外卖的付款界面的那 1 毫秒内,我们几乎感觉不到任何的波动甚至是迟滞。
|
||||
当用户在应用中点击一个按钮时,表面上只是一个瞬间的动作。但在这背后,一条完整的数据链路已经开始运转:
|
||||
|
||||
但此时此刻,这段包含了你买的黄焖鸡、花了 20 元、用着一台 iPhone 的微小包裹,正在你的手机后台默默组装。它也许潜伏在内存里等了 3 秒钟,随后化作一段无线电波飞向基站,在光纤中跨越几百个路由器,抵达了网关机房,随后被吸入消息队列排队、被清洗掉一切污垢,最终永久固化在一块闪烁着冷光的磁盘扇区深处。
|
||||
1. 埋点代码捕获到这次点击,按照 4W1H 模板生成一条标准记录
|
||||
2. 记录被暂存在手机本地,攒够一批后统一发送到服务器
|
||||
3. 服务器通过消息队列平稳接收,再经过去重和格式校验
|
||||
4. 最终,一条干净、准确的数据被写入数据仓库
|
||||
|
||||
正是这种从“采集 -> 建模 -> 传输 -> 清洗”长达千万里的坚固体系,用一根根最枯燥的数据流水线,反向拼凑出了商业世界上极度生动、甚至比你自己还要了解你自己的用户画像图腾。这,就是代码之外最伟大的工程之一。
|
||||
这就是数据埋点的完整过程。它把用户分散的、看不见的操作行为,转化成了可以查询、可以分析的结构化数据。产品经理可以据此了解用户喜欢什么功能、在哪里流失;运营人员可以评估活动效果;开发者可以定位问题出现在哪个版本。
|
||||
|
||||
这套"采集 → 建模 → 传输 → 清洗"的体系,是数据驱动决策的基础设施。
|
||||
|
||||
+7
-3
@@ -2,10 +2,12 @@ import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
function walkSync(dir, filelist = []) {
|
||||
fs.readdirSync(dir).forEach(file => {
|
||||
fs.readdirSync(dir).forEach((file) => {
|
||||
const dirFile = path.join(dir, file)
|
||||
try {
|
||||
filelist = fs.statSync(dirFile).isDirectory() ? walkSync(dirFile, filelist) : filelist.concat(dirFile)
|
||||
filelist = fs.statSync(dirFile).isDirectory()
|
||||
? walkSync(dirFile, filelist)
|
||||
: filelist.concat(dirFile)
|
||||
} catch (err) {
|
||||
if (err.code === 'OOM' || err.code === 'EMFILE') throw err
|
||||
}
|
||||
@@ -13,7 +15,9 @@ function walkSync(dir, filelist = []) {
|
||||
return filelist
|
||||
}
|
||||
|
||||
const vueFiles = walkSync('docs/.vitepress/theme/components').filter(f => f.endsWith('.vue'))
|
||||
const vueFiles = walkSync('docs/.vitepress/theme/components').filter((f) =>
|
||||
f.endsWith('.vue')
|
||||
)
|
||||
|
||||
for (const file of vueFiles) {
|
||||
const content = fs.readFileSync(file, 'utf8')
|
||||
|
||||
+46
-44
@@ -1,52 +1,54 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
function walk(dir) {
|
||||
let results = [];
|
||||
const list = fs.readdirSync(dir);
|
||||
list.forEach(function (file) {
|
||||
file = dir + '/' + file;
|
||||
const stat = fs.statSync(file);
|
||||
if (stat && stat.isDirectory()) {
|
||||
results = results.concat(walk(file));
|
||||
} else {
|
||||
if (file.endsWith('.vue')) results.push(file);
|
||||
}
|
||||
});
|
||||
return results;
|
||||
let results = []
|
||||
const list = fs.readdirSync(dir)
|
||||
list.forEach(function (file) {
|
||||
file = dir + '/' + file
|
||||
const stat = fs.statSync(file)
|
||||
if (stat && stat.isDirectory()) {
|
||||
results = results.concat(walk(file))
|
||||
} else {
|
||||
if (file.endsWith('.vue')) results.push(file)
|
||||
}
|
||||
})
|
||||
return results
|
||||
}
|
||||
|
||||
const vueFiles = walk('docs/.vitepress/theme/components');
|
||||
const vueFiles = walk('docs/.vitepress/theme/components')
|
||||
|
||||
vueFiles.forEach(file => {
|
||||
const lines = fs.readFileSync(file, 'utf8').split('\n');
|
||||
let bracketDepth = 0;
|
||||
let insideScript = false;
|
||||
vueFiles.forEach((file) => {
|
||||
const lines = fs.readFileSync(file, 'utf8').split('\n')
|
||||
let bracketDepth = 0
|
||||
let insideScript = false
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
if (line.includes('<script setup')) {
|
||||
insideScript = true;
|
||||
bracketDepth = 0;
|
||||
return;
|
||||
}
|
||||
if (line.includes('</script>')) {
|
||||
insideScript = false;
|
||||
}
|
||||
lines.forEach((line, index) => {
|
||||
if (line.includes('<script setup')) {
|
||||
insideScript = true
|
||||
bracketDepth = 0
|
||||
return
|
||||
}
|
||||
if (line.includes('</script>')) {
|
||||
insideScript = false
|
||||
}
|
||||
|
||||
if (insideScript) {
|
||||
// Check for setInterval BEFORE updating bracket depth for the current line
|
||||
// because `setInterval(() => {` will increase depth but the call ITSELF is at depth 0
|
||||
if (line.includes('setInterval') && bracketDepth === 0) {
|
||||
console.log(`Top-level setInterval: ${file}:${index + 1} - ${line.trim()}`);
|
||||
}
|
||||
if (insideScript) {
|
||||
// Check for setInterval BEFORE updating bracket depth for the current line
|
||||
// because `setInterval(() => {` will increase depth but the call ITSELF is at depth 0
|
||||
if (line.includes('setInterval') && bracketDepth === 0) {
|
||||
console.log(
|
||||
`Top-level setInterval: ${file}:${index + 1} - ${line.trim()}`
|
||||
)
|
||||
}
|
||||
|
||||
// Simple Bracket Depth Counting
|
||||
// Handle one-liners like `if (x) { ... }` natively by adding/subtracting on the same line
|
||||
// Wait, we only care about trailing brackets basically. Ignore string contents for simplicity.
|
||||
// This heuristic is usually fine for formatting standard vue codes
|
||||
const openCount = (line.match(/\{/g) || []).length;
|
||||
const closeCount = (line.match(/\}/g) || []).length;
|
||||
bracketDepth += openCount - closeCount;
|
||||
}
|
||||
});
|
||||
});
|
||||
// Simple Bracket Depth Counting
|
||||
// Handle one-liners like `if (x) { ... }` natively by adding/subtracting on the same line
|
||||
// Wait, we only care about trailing brackets basically. Ignore string contents for simplicity.
|
||||
// This heuristic is usually fine for formatting standard vue codes
|
||||
const openCount = (line.match(/\{/g) || []).length
|
||||
const closeCount = (line.match(/\}/g) || []).length
|
||||
bracketDepth += openCount - closeCount
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,59 +1,61 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const compiler = require('vue/compiler-sfc');
|
||||
const babel = require('@babel/parser');
|
||||
const traverse = require('@babel/traverse').default;
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const compiler = require('vue/compiler-sfc')
|
||||
const babel = require('@babel/parser')
|
||||
const traverse = require('@babel/traverse').default
|
||||
|
||||
function walk(dir) {
|
||||
let results = [];
|
||||
const list = fs.readdirSync(dir);
|
||||
list.forEach(function(file) {
|
||||
file = dir + '/' + file;
|
||||
const stat = fs.statSync(file);
|
||||
if (stat && stat.isDirectory()) {
|
||||
results = results.concat(walk(file));
|
||||
} else {
|
||||
if (file.endsWith('.vue')) results.push(file);
|
||||
}
|
||||
});
|
||||
return results;
|
||||
let results = []
|
||||
const list = fs.readdirSync(dir)
|
||||
list.forEach(function (file) {
|
||||
file = dir + '/' + file
|
||||
const stat = fs.statSync(file)
|
||||
if (stat && stat.isDirectory()) {
|
||||
results = results.concat(walk(file))
|
||||
} else {
|
||||
if (file.endsWith('.vue')) results.push(file)
|
||||
}
|
||||
})
|
||||
return results
|
||||
}
|
||||
|
||||
const vueFiles = walk('docs/.vitepress/theme/components');
|
||||
const vueFiles = walk('docs/.vitepress/theme/components')
|
||||
|
||||
vueFiles.forEach(file => {
|
||||
const content = fs.readFileSync(file, 'utf8');
|
||||
if (!content.includes('setInterval')) return;
|
||||
const { descriptor } = compiler.parse(content);
|
||||
if (!descriptor.scriptSetup) return;
|
||||
vueFiles.forEach((file) => {
|
||||
const content = fs.readFileSync(file, 'utf8')
|
||||
if (!content.includes('setInterval')) return
|
||||
const { descriptor } = compiler.parse(content)
|
||||
if (!descriptor.scriptSetup) return
|
||||
|
||||
const ast = babel.parse(descriptor.scriptSetup.content, {
|
||||
sourceType: 'module',
|
||||
plugins: ['typescript']
|
||||
});
|
||||
const ast = babel.parse(descriptor.scriptSetup.content, {
|
||||
sourceType: 'module',
|
||||
plugins: ['typescript']
|
||||
})
|
||||
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
if (path.node.callee.name === 'setInterval') {
|
||||
// check if we are inside a function declaration or arrow function or onMounted check
|
||||
let parent = path.parentPath;
|
||||
let insideFunction = false;
|
||||
while (parent) {
|
||||
if (
|
||||
parent.isFunctionDeclaration() ||
|
||||
parent.isArrowFunctionExpression() ||
|
||||
parent.isFunctionExpression() ||
|
||||
parent.isObjectMethod()
|
||||
) {
|
||||
insideFunction = true;
|
||||
break;
|
||||
}
|
||||
parent = parent.parentPath;
|
||||
}
|
||||
if (!insideFunction) {
|
||||
console.log(`Top-level setInterval found in ${file} at line ${path.node.loc.start.line}`);
|
||||
}
|
||||
}
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
if (path.node.callee.name === 'setInterval') {
|
||||
// check if we are inside a function declaration or arrow function or onMounted check
|
||||
let parent = path.parentPath
|
||||
let insideFunction = false
|
||||
while (parent) {
|
||||
if (
|
||||
parent.isFunctionDeclaration() ||
|
||||
parent.isArrowFunctionExpression() ||
|
||||
parent.isFunctionExpression() ||
|
||||
parent.isObjectMethod()
|
||||
) {
|
||||
insideFunction = true
|
||||
break
|
||||
}
|
||||
parent = parent.parentPath
|
||||
}
|
||||
});
|
||||
});
|
||||
if (!insideFunction) {
|
||||
console.log(
|
||||
`Top-level setInterval found in ${file} at line ${path.node.loc.start.line}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user