feat(ai-protocols): add MCP and A2A protocol demos and documentation
docs(ai-protocols): update AI protocols page with visual demos and detailed explanations style(git-demos): improve responsive design and layout for git visualization components refactor(ai-history): simplify and clean up demo components chore: update config to register new AI protocol components
This commit is contained in:
@@ -702,7 +702,7 @@ export default defineConfig({
|
|||||||
{ text: 'Embedding 与向量检索', link: '/zh-cn/appendix/8-artificial-intelligence/embedding-vector-retrieval' },
|
{ text: 'Embedding 与向量检索', link: '/zh-cn/appendix/8-artificial-intelligence/embedding-vector-retrieval' },
|
||||||
{ text: 'RAG 架构', link: '/zh-cn/appendix/8-artificial-intelligence/rag' },
|
{ text: 'RAG 架构', link: '/zh-cn/appendix/8-artificial-intelligence/rag' },
|
||||||
{ text: 'AI Agent 与工具调用', link: '/zh-cn/appendix/8-artificial-intelligence/ai-agents' },
|
{ text: 'AI Agent 与工具调用', link: '/zh-cn/appendix/8-artificial-intelligence/ai-agents' },
|
||||||
{ text: 'AI 协议(MCP 等)', link: '/zh-cn/appendix/8-artificial-intelligence/ai-protocols' },
|
{ text: 'AI 协议(MCP & A2A)', link: '/zh-cn/appendix/8-artificial-intelligence/ai-protocols' },
|
||||||
{ text: '模型微调与部署', link: '/zh-cn/appendix/8-artificial-intelligence/model-finetuning-deployment' },
|
{ text: '模型微调与部署', link: '/zh-cn/appendix/8-artificial-intelligence/model-finetuning-deployment' },
|
||||||
{ text: 'AI 原生应用设计', link: '/zh-cn/appendix/8-artificial-intelligence/ai-native-app-design' },
|
{ text: 'AI 原生应用设计', link: '/zh-cn/appendix/8-artificial-intelligence/ai-native-app-design' },
|
||||||
{ text: 'AI 能力词典', link: '/zh-cn/appendix/8-artificial-intelligence/ai-capability-dictionary' }
|
{ text: 'AI 能力词典', link: '/zh-cn/appendix/8-artificial-intelligence/ai-capability-dictionary' }
|
||||||
|
|||||||
@@ -1,239 +1,3 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="ai-evolution-timeline-demo">
|
<div></div>
|
||||||
<el-card
|
|
||||||
shadow="hover"
|
|
||||||
class="main-card"
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<h3>AI 进化时间轴</h3>
|
|
||||||
<p class="subtitle">
|
|
||||||
点击不同时期,查看 AI 是如何一步步进化的
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="demo-content">
|
|
||||||
<el-tabs
|
|
||||||
v-model="activeEraName"
|
|
||||||
type="border-card"
|
|
||||||
class="timeline-tabs"
|
|
||||||
>
|
|
||||||
<el-tab-pane
|
|
||||||
v-for="(era, index) in eras"
|
|
||||||
:key="index"
|
|
||||||
:label="era.title"
|
|
||||||
:name="era.title"
|
|
||||||
>
|
|
||||||
<div class="era-content">
|
|
||||||
<div class="era-header">
|
|
||||||
<el-tag
|
|
||||||
effect="dark"
|
|
||||||
size="large"
|
|
||||||
class="year-tag"
|
|
||||||
>
|
|
||||||
{{ era.year }}
|
|
||||||
</el-tag>
|
|
||||||
<span class="era-desc-short">{{ era.desc }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="era-body">
|
|
||||||
<p class="full-desc">
|
|
||||||
{{ era.fullDesc }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="info-grid">
|
|
||||||
<div class="info-column">
|
|
||||||
<span class="column-title">💡 核心特点</span>
|
|
||||||
<ul class="key-points-list">
|
|
||||||
<li
|
|
||||||
v-for="(point, i) in era.keyPoints"
|
|
||||||
:key="i"
|
|
||||||
>
|
|
||||||
<el-icon class="point-icon">
|
|
||||||
<CaretRight />
|
|
||||||
</el-icon>
|
|
||||||
{{ point }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-column">
|
|
||||||
<span class="column-title">🌟 代表成就</span>
|
|
||||||
<div class="examples-container">
|
|
||||||
<el-tag
|
|
||||||
v-for="(example, i) in era.examples"
|
|
||||||
:key="i"
|
|
||||||
class="example-tag"
|
|
||||||
effect="plain"
|
|
||||||
>
|
|
||||||
{{ example }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { CaretRight } from '@element-plus/icons-vue'
|
|
||||||
|
|
||||||
const activeEraName = ref('符号主义时代')
|
|
||||||
|
|
||||||
const eras = [
|
|
||||||
{
|
|
||||||
year: '20世纪50-80年代',
|
|
||||||
title: '符号主义时代',
|
|
||||||
desc: '规则与逻辑推理',
|
|
||||||
fullDesc:
|
|
||||||
'早期人工智能研究认为,智能可以通过符号和逻辑规则来表达。科学家们尝试编写大量规则来让机器模拟人类专家的决策过程。',
|
|
||||||
examples: ['专家系统', '深蓝 (Deep Blue)', 'MYCIN'],
|
|
||||||
keyPoints: [
|
|
||||||
'人工编写 If-Then 规则',
|
|
||||||
'逻辑推理能力强大',
|
|
||||||
'可解释性强',
|
|
||||||
'难以处理模糊/复杂问题'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
year: '21世纪10年代',
|
|
||||||
title: '连接主义时代',
|
|
||||||
desc: '神经网络与深度学习',
|
|
||||||
fullDesc:
|
|
||||||
'随着大数据和 GPU 算力的突破,深度学习迎来了春天。神经网络通过多层结构自动学习特征,在图像识别、语音识别等领域取得巨大成功。',
|
|
||||||
examples: ['AlexNet', 'AlphaGo', '人脸识别'],
|
|
||||||
keyPoints: [
|
|
||||||
'模仿人脑神经元结构',
|
|
||||||
'从数据中自动学习特征',
|
|
||||||
'强大的模式识别能力',
|
|
||||||
'模型是"黑盒",缺乏可解释性'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
year: '21世纪20年代至今',
|
|
||||||
title: '生成式 AI 时代',
|
|
||||||
desc: '大模型与创造力',
|
|
||||||
fullDesc:
|
|
||||||
'Transformer 架构的诞生让机器理解了上下文关系。GPT 等大语言模型不仅能生成文本、图像,还展现出了惊人的推理和创造能力。',
|
|
||||||
examples: ['ChatGPT', 'Midjourney', 'Sora'],
|
|
||||||
keyPoints: [
|
|
||||||
'基于 Transformer 架构',
|
|
||||||
'通用的理解与生成能力',
|
|
||||||
'涌现出推理、规划等高级智能',
|
|
||||||
'通过提示词 (Prompt) 交互'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.ai-evolution-timeline-demo {
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-card {
|
|
||||||
/* Compact card style */
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
margin: 5px 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-tabs {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.era-content {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.era-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.era-desc-short {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #606266;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.full-desc {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #303133;
|
|
||||||
line-height: 1.6;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
background: #f5f7fa;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-grid {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-column {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column-title {
|
|
||||||
display: block;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #909399;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-bottom: 1px solid #ebeef5;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.key-points-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.key-points-list li {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #606266;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.point-icon {
|
|
||||||
margin-right: 5px;
|
|
||||||
color: #409eff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.examples-container {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.info-grid {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,697 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="evolution-demo">
|
<div class="demo-card">
|
||||||
<el-card
|
<div class="timeline-visual">
|
||||||
class="main-card"
|
<div class="era" v-for="era in eras" :key="era.label" :style="{ flex: era.flex, background: era.bg }">
|
||||||
shadow="hover"
|
<div class="era-label">{{ era.label }}</div>
|
||||||
>
|
<div class="era-years">{{ era.years }}</div>
|
||||||
<template #header>
|
|
||||||
<div class="header-container">
|
|
||||||
<div class="title-area">
|
|
||||||
<span class="main-title">AI 进化模拟器</span>
|
|
||||||
</div>
|
|
||||||
<el-steps
|
|
||||||
:active="currentStage"
|
|
||||||
finish-status="success"
|
|
||||||
align-center
|
|
||||||
class="compact-steps"
|
|
||||||
simple
|
|
||||||
>
|
|
||||||
<el-step
|
|
||||||
v-for="stage in stages"
|
|
||||||
:key="stage.id"
|
|
||||||
:title="stage.label"
|
|
||||||
/>
|
|
||||||
</el-steps>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Stage 1: Rule Based (Traffic Light Example) -->
|
|
||||||
<div
|
|
||||||
v-if="currentStage === 0"
|
|
||||||
class="stage-pane"
|
|
||||||
>
|
|
||||||
<el-alert
|
|
||||||
type="info"
|
|
||||||
:closable="false"
|
|
||||||
show-icon
|
|
||||||
class="compact-alert mb-2"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<span class="alert-title">阶段一:规则时代 (Rule-Based)</span>
|
|
||||||
</template>
|
|
||||||
<template #default>
|
|
||||||
<span class="alert-desc">就像教小孩:如果看到红灯,就停下。</span>
|
|
||||||
</template>
|
|
||||||
</el-alert>
|
|
||||||
|
|
||||||
<div class="game-area-grid">
|
|
||||||
<div class="panel left-panel">
|
|
||||||
<div class="panel-header">
|
|
||||||
规则库 (Code)
|
|
||||||
</div>
|
|
||||||
<div class="code-block">
|
|
||||||
<div class="code-line">
|
|
||||||
<span class="keyword">function</span> <span class="function">decideTrafficLight</span>(color) {
|
|
||||||
</div>
|
|
||||||
<div class="code-line indent">
|
|
||||||
<span class="keyword">if</span> (color === <span class="string">'red'</span>) <span class="keyword">return</span> <span class="string">'stop'</span>
|
|
||||||
</div>
|
|
||||||
<div class="code-line indent">
|
|
||||||
<span class="keyword">else if</span> (color === <span class="string">'yellow'</span>) <span class="keyword">return</span> <span class="string">'caution'</span>
|
|
||||||
</div>
|
|
||||||
<div class="code-line indent">
|
|
||||||
<span class="keyword">else if</span> (color === <span class="string">'green'</span>) <span class="keyword">return</span> <span class="string">'go'</span>
|
|
||||||
</div>
|
|
||||||
<div class="code-line">
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="panel right-panel">
|
|
||||||
<div class="panel-header">
|
|
||||||
测试输入
|
|
||||||
</div>
|
|
||||||
<div class="input-controls">
|
|
||||||
<el-select
|
|
||||||
v-model="ruleColor"
|
|
||||||
size="small"
|
|
||||||
style="width: 120px;"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
value="red"
|
|
||||||
label="🔴 红灯"
|
|
||||||
/>
|
|
||||||
<el-option
|
|
||||||
value="yellow"
|
|
||||||
label="🟡 黄灯"
|
|
||||||
/>
|
|
||||||
<el-option
|
|
||||||
value="green"
|
|
||||||
label="🟢 绿灯"
|
|
||||||
/>
|
|
||||||
<el-option
|
|
||||||
value="blue"
|
|
||||||
label="🔵 蓝灯"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
<div class="arrow">
|
|
||||||
→
|
|
||||||
</div>
|
|
||||||
<el-tag :type="ruleResult === 'stop' ? 'danger' : ruleResult === 'caution' ? 'warning' : ruleResult === 'go' ? 'success' : 'info'">
|
|
||||||
{{ ruleResult }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="ruleResult === 'Unknown'"
|
|
||||||
class="hint-text"
|
|
||||||
>
|
|
||||||
规则库中没有定义"蓝灯",所以系统不知道该做什么。这就是规则系统的局限性:无法处理未定义的规则。
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="hint-text"
|
|
||||||
>
|
|
||||||
系统严格按照预定义的规则执行指令。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Stage 2: Machine Learning (Interactive 2D Plot) -->
|
<div class="legend">
|
||||||
<div
|
<span class="legend-item"><span class="dot" style="background:#059669"></span>技术浪潮</span>
|
||||||
v-else-if="currentStage === 1"
|
<span class="legend-item"><span class="dot" style="background:#94a3b8"></span>❄️ AI 寒冬</span>
|
||||||
class="stage-pane"
|
<span class="legend-item"><span class="dot" style="background:#7c3aed"></span>大模型时代</span>
|
||||||
>
|
</div>
|
||||||
<el-alert
|
|
||||||
type="info"
|
|
||||||
:closable="false"
|
|
||||||
show-icon
|
|
||||||
class="compact-alert mb-2"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<span class="alert-title">阶段二:机器学习 (Machine Learning)</span>
|
|
||||||
</template>
|
|
||||||
<template #default>
|
|
||||||
<span class="alert-desc">点击画布添加数据点,训练模型自动寻找分类边界 (Decision Boundary)。</span>
|
|
||||||
</template>
|
|
||||||
</el-alert>
|
|
||||||
|
|
||||||
<div class="game-area-grid">
|
|
||||||
<div
|
|
||||||
class="panel left-panel canvas-container"
|
|
||||||
@click="addPoint"
|
|
||||||
>
|
|
||||||
<!-- Simple SVG Plot -->
|
|
||||||
<svg
|
|
||||||
width="100%"
|
|
||||||
height="200"
|
|
||||||
class="ml-plot"
|
|
||||||
>
|
|
||||||
<!-- Background Regions (Visible after training) -->
|
|
||||||
<rect
|
|
||||||
v-if="modelTrained"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
:fill="boundaryColor"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Decision Line -->
|
|
||||||
<line
|
|
||||||
v-if="modelTrained"
|
|
||||||
:x1="line.x1"
|
|
||||||
:y1="line.y1"
|
|
||||||
:x2="line.x2"
|
|
||||||
:y2="line.y2"
|
|
||||||
stroke="#333"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-dasharray="4"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Points -->
|
|
||||||
<circle
|
|
||||||
v-for="(p, i) in points"
|
|
||||||
:key="i"
|
|
||||||
:cx="p.x"
|
|
||||||
:cy="p.y"
|
|
||||||
r="6"
|
|
||||||
:fill="p.type === 'A' ? '#409eff' : '#e6a23c'"
|
|
||||||
stroke="white"
|
|
||||||
stroke-width="2"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<div
|
|
||||||
v-if="points.length === 0"
|
|
||||||
class="canvas-hint"
|
|
||||||
>
|
|
||||||
👆 点击此处添加数据点
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel right-panel">
|
|
||||||
<div class="panel-header">
|
|
||||||
控制面板
|
|
||||||
</div>
|
|
||||||
<div class="control-group">
|
|
||||||
<span class="label">当前类别:</span>
|
|
||||||
<el-radio-group
|
|
||||||
v-model="currentClass"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<el-radio-button label="A">
|
|
||||||
<span style="color: #409eff">● 蓝类</span>
|
|
||||||
</el-radio-button>
|
|
||||||
<el-radio-button label="B">
|
|
||||||
<span style="color: #e6a23c">● 橙类</span>
|
|
||||||
</el-radio-button>
|
|
||||||
</el-radio-group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group mt-2">
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
:disabled="points.length < 2"
|
|
||||||
@click="trainLinearModel"
|
|
||||||
>
|
|
||||||
⚡ 开始训练 (Fit)
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
:icon="Delete"
|
|
||||||
circle
|
|
||||||
@click="clearPoints"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stats-info mt-2">
|
|
||||||
<p
|
|
||||||
v-if="!modelTrained"
|
|
||||||
class="text-desc"
|
|
||||||
>
|
|
||||||
机器学习不再依赖硬编码规则,而是通过统计学方法(如寻找中心点或线性回归)在数据之间划出一条"界线"。试试在不同位置添加点,看看界线如何变化。
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
v-else
|
|
||||||
class="text-desc"
|
|
||||||
>
|
|
||||||
模型已训练!它找到了一条最佳分割线。新进来的数据将根据它在红区还是蓝区被自动分类。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stage 3: Deep Learning (3x3 Grid Feature Extraction) -->
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="stage-pane"
|
|
||||||
>
|
|
||||||
<el-alert
|
|
||||||
type="info"
|
|
||||||
:closable="false"
|
|
||||||
show-icon
|
|
||||||
class="compact-alert mb-2"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<span class="alert-title">阶段三:深度学习 (Deep Learning)</span>
|
|
||||||
</template>
|
|
||||||
<template #default>
|
|
||||||
<span class="alert-desc">神经网络通过多层结构自动提取特征(Feature Extraction)。点击格子绘制图案。</span>
|
|
||||||
</template>
|
|
||||||
</el-alert>
|
|
||||||
|
|
||||||
<div class="game-area-grid">
|
|
||||||
<div class="panel left-panel grid-container">
|
|
||||||
<div class="pixel-grid">
|
|
||||||
<div
|
|
||||||
v-for="(pixel, i) in pixels"
|
|
||||||
:key="i"
|
|
||||||
class="pixel"
|
|
||||||
:class="{ active: pixel }"
|
|
||||||
@click="togglePixel(i)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid-actions">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
link
|
|
||||||
@click="preset('x')"
|
|
||||||
>
|
|
||||||
❌ X型
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
link
|
|
||||||
@click="preset('plus')"
|
|
||||||
>
|
|
||||||
➕ 十字
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
link
|
|
||||||
@click="clearPixels"
|
|
||||||
>
|
|
||||||
清空
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel right-panel">
|
|
||||||
<div class="panel-header">
|
|
||||||
神经网络层级透视
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Visualization of Layers -->
|
|
||||||
<div class="network-viz">
|
|
||||||
<div class="layer input-layer">
|
|
||||||
<div class="layer-label">
|
|
||||||
输入层 (Pixels)
|
|
||||||
</div>
|
|
||||||
<div class="nodes">
|
|
||||||
<span
|
|
||||||
v-for="n in 9"
|
|
||||||
:key="n"
|
|
||||||
class="node mini"
|
|
||||||
:class="{active: pixels[n-1]}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="arrow-down">
|
|
||||||
⬇️ 卷积/提取特征
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="layer hidden-layer">
|
|
||||||
<div class="layer-label">
|
|
||||||
隐藏层 (Features)
|
|
||||||
</div>
|
|
||||||
<div class="feature-detectors">
|
|
||||||
<div
|
|
||||||
class="feature"
|
|
||||||
:class="{detected: features.center}"
|
|
||||||
>
|
|
||||||
<span class="f-icon">⏺</span> 中心点
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="feature"
|
|
||||||
:class="{detected: features.corners}"
|
|
||||||
>
|
|
||||||
<span class="f-icon">Corners</span> 四角
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="feature"
|
|
||||||
:class="{detected: features.cross}"
|
|
||||||
>
|
|
||||||
<span class="f-icon">➕</span> 交叉
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="arrow-down">
|
|
||||||
⬇️ 输出层
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="layer output-layer">
|
|
||||||
<div class="prediction-box">
|
|
||||||
识别结果: <span class="result-text">{{ prediction }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Footer Navigation -->
|
|
||||||
<div class="footer-nav mt-2 flex justify-end">
|
|
||||||
<el-button-group>
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
:disabled="currentStage === 0"
|
|
||||||
@click="currentStage--"
|
|
||||||
>
|
|
||||||
上一步
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
:disabled="currentStage === 2"
|
|
||||||
@click="currentStage++"
|
|
||||||
>
|
|
||||||
下一步
|
|
||||||
</el-button>
|
|
||||||
</el-button-group>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed } from 'vue'
|
const eras = [
|
||||||
import { Delete } from '@element-plus/icons-vue'
|
{ label: '理论奠基', years: '1940s-50s', flex: 1.5, bg: 'linear-gradient(135deg, #dbeafe, #bfdbfe)' },
|
||||||
|
{ label: '第一次浪潮', years: '1960s-70s', flex: 1.5, bg: 'linear-gradient(135deg, #d1fae5, #a7f3d0)' },
|
||||||
const currentStage = ref(0)
|
{ label: '❄️ 寒冬 I', years: '1974-80', flex: 0.7, bg: 'linear-gradient(135deg, #e2e8f0, #cbd5e1)' },
|
||||||
const stages = [
|
{ label: '第二次浪潮', years: '1980s', flex: 1, bg: 'linear-gradient(135deg, #d1fae5, #a7f3d0)' },
|
||||||
{ id: 0, label: '规则', desc: '人工规则' },
|
{ label: '❄️ 寒冬 II', years: '1987-93', flex: 0.7, bg: 'linear-gradient(135deg, #e2e8f0, #cbd5e1)' },
|
||||||
{ id: 1, label: '机器学习', desc: '统计特征' },
|
{ label: 'ML 崛起', years: '1990s-2000s', flex: 1.5, bg: 'linear-gradient(135deg, #d1fae5, #6ee7b7)' },
|
||||||
{ id: 2, label: '深度学习', desc: '自动特征' }
|
{ label: '深度学习', years: '2010s', flex: 1.2, bg: 'linear-gradient(135deg, #a7f3d0, #34d399)' },
|
||||||
|
{ label: '大模型时代', years: '2018+', flex: 1.2, bg: 'linear-gradient(135deg, #c4b5fd, #a78bfa)' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// --- Stage 1: Rule Based ---
|
|
||||||
const ruleColor = ref('red')
|
|
||||||
const ruleResult = computed(() => {
|
|
||||||
if (ruleColor.value === 'red') return 'stop'
|
|
||||||
if (ruleColor.value === 'yellow') return 'caution'
|
|
||||||
if (ruleColor.value === 'green') return 'go'
|
|
||||||
return 'Unknown'
|
|
||||||
})
|
|
||||||
|
|
||||||
// --- Stage 2: Machine Learning ---
|
|
||||||
const points = ref([])
|
|
||||||
const currentClass = ref('A')
|
|
||||||
const modelTrained = ref(false)
|
|
||||||
const line = reactive({ x1: 0, y1: 0, x2: 0, y2: 0 })
|
|
||||||
// SVG click coordinates are relative to the SVG element
|
|
||||||
// We'll use a simple approximation for the demo
|
|
||||||
// x, y are percentages (0-100)
|
|
||||||
const addPoint = (e) => {
|
|
||||||
const rect = e.target.getBoundingClientRect()
|
|
||||||
// Ensure we are clicking on the SVG or its children
|
|
||||||
// Best to put event on wrapper
|
|
||||||
// But event target might be circle.
|
|
||||||
// Use currentTarget
|
|
||||||
const x = e.offsetX
|
|
||||||
const y = e.offsetY
|
|
||||||
// Convert to % for responsiveness if needed, but pixel is easier for calc
|
|
||||||
// Let's stick to pixel for this simple demo, assuming fixed height 200
|
|
||||||
// width varies.
|
|
||||||
points.value.push({
|
|
||||||
x, y,
|
|
||||||
type: currentClass.value
|
|
||||||
})
|
|
||||||
modelTrained.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearPoints = () => {
|
|
||||||
points.value = []
|
|
||||||
modelTrained.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const trainLinearModel = () => {
|
|
||||||
// Simple Nearest Centroid Classifier
|
|
||||||
const groupA = points.value.filter(p => p.type === 'A')
|
|
||||||
const groupB = points.value.filter(p => p.type === 'B')
|
|
||||||
|
|
||||||
if (groupA.length === 0 || groupB.length === 0) return
|
|
||||||
|
|
||||||
const avgA = {
|
|
||||||
x: groupA.reduce((sum, p) => sum + p.x, 0) / groupA.length,
|
|
||||||
y: groupA.reduce((sum, p) => sum + p.y, 0) / groupA.length
|
|
||||||
}
|
|
||||||
const avgB = {
|
|
||||||
x: groupB.reduce((sum, p) => sum + p.x, 0) / groupB.length,
|
|
||||||
y: groupB.reduce((sum, p) => sum + p.y, 0) / groupB.length
|
|
||||||
}
|
|
||||||
|
|
||||||
// Midpoint
|
|
||||||
const midX = (avgA.x + avgB.x) / 2
|
|
||||||
const midY = (avgA.y + avgB.y) / 2
|
|
||||||
|
|
||||||
// Normal vector (from A to B)
|
|
||||||
const dx = avgB.x - avgA.x
|
|
||||||
const dy = avgB.y - avgA.y
|
|
||||||
|
|
||||||
// Perpendicular line: dx*x + dy*y = C
|
|
||||||
// Slope of normal is dy/dx. Slope of perp line is -dx/dy
|
|
||||||
|
|
||||||
// Let's just draw a line perpendicular to the segment AB passing through Midpoint
|
|
||||||
// Slope m = -dx/dy
|
|
||||||
|
|
||||||
// Calculate line coordinates for visualization
|
|
||||||
// y - midY = m * (x - midX)
|
|
||||||
// if dy is close to 0, vertical line x = midX
|
|
||||||
|
|
||||||
const width = 1000 // ample width
|
|
||||||
|
|
||||||
if (Math.abs(dy) < 0.001) {
|
|
||||||
// Vertical line
|
|
||||||
line.x1 = midX
|
|
||||||
line.x2 = midX
|
|
||||||
line.y1 = 0
|
|
||||||
line.y2 = 200
|
|
||||||
} else {
|
|
||||||
const m = -dx / dy
|
|
||||||
// At x=0
|
|
||||||
const y0 = midY + m * (0 - midX)
|
|
||||||
// At x=width
|
|
||||||
const y1 = midY + m * (width - midX)
|
|
||||||
|
|
||||||
line.x1 = 0
|
|
||||||
line.y1 = y0
|
|
||||||
line.x2 = width
|
|
||||||
line.y2 = y1
|
|
||||||
}
|
|
||||||
|
|
||||||
modelTrained.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple visual background
|
|
||||||
// If A is left/top, background is blue-ish
|
|
||||||
// SVG doesn't support "half plane fill" easily without path math
|
|
||||||
// For this demo, we won't fill the background perfectly, just draw the line.
|
|
||||||
const boundaryColor = computed(() => 'transparent')
|
|
||||||
|
|
||||||
|
|
||||||
// --- Stage 3: Deep Learning ---
|
|
||||||
const pixels = ref(Array(9).fill(false))
|
|
||||||
|
|
||||||
const togglePixel = (index) => {
|
|
||||||
pixels.value[index] = !pixels.value[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearPixels = () => {
|
|
||||||
pixels.value = pixels.value.map(() => false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const preset = (type) => {
|
|
||||||
clearPixels()
|
|
||||||
if (type === 'x') {
|
|
||||||
[0, 2, 4, 6, 8].forEach(i => pixels.value[i] = true)
|
|
||||||
} else if (type === 'plus') {
|
|
||||||
[1, 3, 4, 5, 7].forEach(i => pixels.value[i] = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const features = computed(() => {
|
|
||||||
// Simple heuristics to simulate feature detection
|
|
||||||
const p = pixels.value
|
|
||||||
const center = p[4]
|
|
||||||
const corners = p[0] && p[2] && p[6] && p[8]
|
|
||||||
const cross = p[1] && p[3] && p[5] && p[7]
|
|
||||||
|
|
||||||
return { center, corners, cross }
|
|
||||||
})
|
|
||||||
|
|
||||||
const prediction = computed(() => {
|
|
||||||
const f = features.value
|
|
||||||
if (f.corners && f.center) return 'X 型图案 (X-Shape)'
|
|
||||||
if (f.cross && f.center) return '十字型 (Plus-Shape)'
|
|
||||||
if (f.corners && !f.center) return '四角 (Corners)'
|
|
||||||
if (pixels.value.filter(Boolean).length === 0) return '无输入'
|
|
||||||
return '未知图案'
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.evolution-demo { margin: 10px 0; }
|
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1rem; margin: 1rem 0; }
|
||||||
.header-container { margin-bottom: 5px; }
|
.timeline-visual { display: flex; border-radius: 6px; overflow: hidden; border: 1px solid var(--vp-c-divider); min-height: 60px; }
|
||||||
.main-title { font-weight: bold; font-size: 16px; }
|
.era { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 0.4rem 0.2rem; text-align: center; border-right: 1px solid rgba(255,255,255,0.4); }
|
||||||
.compact-steps { padding: 5px 0; margin-bottom: 10px; }
|
.era:last-child { border-right: none; }
|
||||||
.compact-alert { padding: 5px 10px; }
|
.era-label { font-size: 0.65rem; font-weight: bold; color: #1e293b; line-height: 1.2; }
|
||||||
.alert-title { font-weight: bold; font-size: 13px; }
|
.era-years { font-size: 0.55rem; color: #475569; margin-top: 0.15rem; }
|
||||||
.alert-desc { font-size: 12px; }
|
.legend { display: flex; gap: 1rem; margin-top: 0.6rem; flex-wrap: wrap; }
|
||||||
|
.legend-item { display: flex; align-items: center; gap: 0.3rem; font-size: 0.72rem; color: var(--vp-c-text-2); }
|
||||||
.game-area-grid {
|
.dot { width: 8px; height: 8px; border-radius: 2px; }
|
||||||
display: flex;
|
@media (max-width: 640px) { .era-label { font-size: 0.58rem; } .era-years { display: none; } }
|
||||||
gap: 15px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
.panel {
|
|
||||||
border: 1px solid #ebeef5;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
.left-panel { flex: 1; }
|
|
||||||
.right-panel { flex: 1; background-color: #fcfcfc; }
|
|
||||||
.panel-header {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #606266;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-bottom: 1px solid #ebeef5;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stage 1 */
|
|
||||||
.code-block {
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
background: #282c34;
|
|
||||||
color: #abb2bf;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.keyword { color: #c678dd; }
|
|
||||||
.string { color: #98c379; }
|
|
||||||
.function { color: #61afef; }
|
|
||||||
.indent { padding-left: 15px; }
|
|
||||||
.input-controls {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
.hint-text {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stage 2 */
|
|
||||||
.canvas-container {
|
|
||||||
height: 220px;
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
position: relative;
|
|
||||||
cursor: crosshair;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.ml-plot {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.canvas-hint {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
color: #909399;
|
|
||||||
font-size: 12px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.text-desc {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #606266;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stage 3 */
|
|
||||||
.grid-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.pixel-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 40px);
|
|
||||||
gap: 4px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.pixel {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
background-color: #eee;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
.pixel:hover { background-color: #d9d9d9; }
|
|
||||||
.pixel.active { background-color: #333; }
|
|
||||||
|
|
||||||
.network-viz {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.layer {
|
|
||||||
width: 100%;
|
|
||||||
padding: 5px;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #ebeef5;
|
|
||||||
border-radius: 4px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.layer-label { font-size: 11px; color: #909399; margin-bottom: 4px; }
|
|
||||||
.nodes { display: flex; gap: 2px; justify-content: center; flex-wrap: wrap; width: 60px; margin: 0 auto; }
|
|
||||||
.node.mini { width: 6px; height: 6px; border-radius: 50%; background: #ddd; }
|
|
||||||
.node.mini.active { background: #333; }
|
|
||||||
.arrow-down { font-size: 10px; color: #ccc; }
|
|
||||||
|
|
||||||
.feature-detectors {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
.feature {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
opacity: 0.3;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
}
|
|
||||||
.feature.detected { opacity: 1; color: #409eff; font-weight: bold; }
|
|
||||||
.f-icon { font-size: 14px; margin-bottom: 2px; }
|
|
||||||
|
|
||||||
.prediction-box { font-weight: bold; font-size: 13px; color: #303133; }
|
|
||||||
.result-text { color: #67c23a; }
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.game-area-grid { flex-direction: column; }
|
|
||||||
}
|
|
||||||
.flex { display: flex; }
|
|
||||||
.justify-end { justify-content: flex-end; }
|
|
||||||
.mt-2 { margin-top: 8px; }
|
|
||||||
.mb-2 { margin-bottom: 8px; }
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,199 +1,50 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="attention-mechanism-demo">
|
<div class="demo-card">
|
||||||
<el-card shadow="hover">
|
<div class="attention-layout">
|
||||||
<template #header>
|
<div class="sentence-col">
|
||||||
<div class="card-header">
|
<div class="col-label">处理「<strong>他</strong>」时的注意力分配:</div>
|
||||||
<h4>👁️ 注意力机制演示</h4>
|
<div class="sentence-box">
|
||||||
<p class="subtitle">
|
<span v-for="(word, i) in sentence" :key="i" class="word-token" :class="{ focus: i === focusIdx }">{{ word }}</span>
|
||||||
点击词语,观察它如何"关注"句子中的其他词
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="sentence-container">
|
|
||||||
<div class="sentence">
|
|
||||||
<el-tag
|
|
||||||
v-for="(word, index) in sentence"
|
|
||||||
:key="index"
|
|
||||||
:type="activeIndex === index ? 'primary' : 'info'"
|
|
||||||
:effect="activeIndex === index ? 'dark' : 'plain'"
|
|
||||||
class="word-token"
|
|
||||||
@click="selectWord(index)"
|
|
||||||
>
|
|
||||||
{{ word }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="activeIndex !== null"
|
|
||||||
class="attention-bars"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="(attention, index) in attentionWeights"
|
|
||||||
:key="index"
|
|
||||||
class="attention-item"
|
|
||||||
>
|
|
||||||
<div class="word-label">
|
|
||||||
{{ attention.word }}
|
|
||||||
</div>
|
|
||||||
<el-progress
|
|
||||||
:percentage="Math.round(attention.weight * 100)"
|
|
||||||
:status="attention.weight > 0.5 ? 'exception' : ''"
|
|
||||||
:color="customColors"
|
|
||||||
class="attention-progress"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<el-empty
|
|
||||||
v-else
|
|
||||||
description="👆 点击句子中的任意词语开始"
|
|
||||||
:image-size="60"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="bars-col">
|
||||||
<el-collapse-transition>
|
<div class="attention-item" v-for="(item, i) in weights" :key="i">
|
||||||
<div
|
<span class="bar-word" :class="{ focus: i === focusIdx }">{{ item.word }}</span>
|
||||||
v-if="activeIndex !== null"
|
<div class="bar-bg">
|
||||||
class="explanation-panel"
|
<div class="bar-fill" :style="{ width: item.w * 100 + '%', background: barColor(item.w) }"></div>
|
||||||
>
|
</div>
|
||||||
<el-alert
|
<span class="bar-pct">{{ Math.round(item.w * 100) }}%</span>
|
||||||
type="success"
|
|
||||||
:closable="false"
|
|
||||||
show-icon
|
|
||||||
class="insight-alert"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<span class="insight-title">关键洞察</span>
|
|
||||||
</template>
|
|
||||||
<p>{{ getInsight(activeIndex) }}</p>
|
|
||||||
</el-alert>
|
|
||||||
</div>
|
</div>
|
||||||
</el-collapse-transition>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
<div class="caption">
|
||||||
|
「他」虽在句中间,模型却把 65% 注意力精准投向句首的「小明」,跨越距离识别代词指代
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
const sentence = ['小明', '把', '苹果', '给了', '他', '的', '母亲']
|
||||||
|
const focusIdx = 4
|
||||||
const sentence = ref(['小明', '把', '苹果', '给了', '他', '的', '母亲'])
|
const attn = [0.65, 0.05, 0.10, 0.10, 0.05, 0.03, 0.02]
|
||||||
const activeIndex = ref(null)
|
const weights = sentence.map((word, i) => ({ word, w: attn[i] }))
|
||||||
|
const barColor = (v) => v > 0.5 ? '#dc2626' : v > 0.15 ? '#d97706' : v > 0.06 ? '#059669' : 'var(--vp-c-divider)'
|
||||||
const customColors = [
|
|
||||||
{ color: '#909399', percentage: 20 },
|
|
||||||
{ color: '#e6a23c', percentage: 40 },
|
|
||||||
{ color: '#f56c6c', percentage: 80 },
|
|
||||||
{ color: '#67c23a', percentage: 100 }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 注意力权重矩阵(模拟)
|
|
||||||
const attentionMatrix = {
|
|
||||||
0: [0.15, 0.05, 0.6, 0.05, 0.05, 0.05, 0.05], // 小明 主要关注 苹果、他
|
|
||||||
1: [0.1, 0.1, 0.4, 0.3, 0.05, 0.03, 0.02], // 把 主要关注 苹果、给了
|
|
||||||
2: [0.5, 0.1, 0.15, 0.15, 0.05, 0.03, 0.02], // 苹果 主要关注 小明
|
|
||||||
3: [0.1, 0.1, 0.35, 0.15, 0.2, 0.05, 0.05], // 给了 主要关注 苹果、他
|
|
||||||
4: [0.65, 0.05, 0.1, 0.1, 0.05, 0.03, 0.02], // 他 主要关注 小明
|
|
||||||
5: [0.08, 0.05, 0.07, 0.08, 0.62, 0.05, 0.05], // 的 主要关注 他
|
|
||||||
6: [0.25, 0.1, 0.15, 0.15, 0.2, 0.1, 0.05] // 母亲 关注多个词
|
|
||||||
}
|
|
||||||
|
|
||||||
const insights = {
|
|
||||||
0: '当模型处理"小明"时,它最关注"苹果"(60%),因为这表明是"谁"拥有苹果。',
|
|
||||||
1: '"把"是介词,模型关注"苹果"和"给了",理解动作的对象和方向。',
|
|
||||||
2: '"苹果"作为宾语,主要关注主语"小明",确定归属关系。',
|
|
||||||
3: '"给了"关注"苹果"和"他",理解传递动作的对象。',
|
|
||||||
4: '"他"最关注"小明"(65%),因为"他"指代的就是"小明"!',
|
|
||||||
5: '"的"连接"他"和"母亲",主要关注"他"(62%)。',
|
|
||||||
6: '"母亲"作为句末宾语,关注前面的多个词语来理解完整语境。'
|
|
||||||
}
|
|
||||||
|
|
||||||
const attentionWeights = computed(() => {
|
|
||||||
if (activeIndex.value === null) return []
|
|
||||||
|
|
||||||
return sentence.value.map((word, index) => ({
|
|
||||||
word,
|
|
||||||
weight: attentionMatrix[activeIndex.value][index]
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectWord = (index) => {
|
|
||||||
activeIndex.value = index
|
|
||||||
}
|
|
||||||
|
|
||||||
const getInsight = (index) => {
|
|
||||||
return insights[index]
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.attention-mechanism-demo {
|
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1.25rem; margin: 1rem 0; }
|
||||||
margin: 20px 0;
|
.attention-layout { display: grid; grid-template-columns: 1fr 1.3fr; gap: 1rem; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-radius: 6px; padding: 0.9rem; margin-bottom: 0.5rem; }
|
||||||
}
|
@media (max-width: 560px) { .attention-layout { grid-template-columns: 1fr; } }
|
||||||
|
.col-label { font-size: 0.76rem; color: var(--vp-c-text-2); margin-bottom: 0.5rem; font-weight: bold; }
|
||||||
.card-header h4 {
|
.sentence-box { display: flex; flex-wrap: wrap; gap: 0.35rem; background: var(--vp-c-bg-alt); padding: 0.6rem; border-radius: 5px; border: 1px dashed var(--vp-c-divider); }
|
||||||
margin: 0;
|
.word-token { font-size: 0.88rem; font-weight: bold; padding: 0.2rem 0.5rem; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-radius: 4px; }
|
||||||
font-size: 16px;
|
.word-token.focus { background: var(--vp-c-brand); color: white; border-color: var(--vp-c-brand); }
|
||||||
font-weight: 600;
|
.bars-col { display: flex; flex-direction: column; gap: 0.3rem; justify-content: center; }
|
||||||
}
|
.attention-item { display: flex; align-items: center; gap: 0.4rem; }
|
||||||
|
.bar-word { width: 30px; text-align: right; font-size: 0.8rem; font-weight: bold; color: var(--vp-c-text-2); flex-shrink: 0; }
|
||||||
.subtitle {
|
.bar-word.focus { color: var(--vp-c-brand); }
|
||||||
font-size: 13px;
|
.bar-bg { flex: 1; height: 12px; background: var(--vp-c-bg-alt); border-radius: 6px; overflow: hidden; border: 1px solid var(--vp-c-divider); }
|
||||||
color: var(--vp-c-text-2);
|
.bar-fill { height: 100%; border-radius: 6px; }
|
||||||
margin: 4px 0 0;
|
.bar-pct { font-size: 0.7rem; font-weight: bold; color: var(--vp-c-text-2); width: 30px; flex-shrink: 0; }
|
||||||
}
|
.caption { font-size: 0.75rem; color: var(--vp-c-text-3); text-align: center; }
|
||||||
|
|
||||||
.sentence {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
padding: 16px;
|
|
||||||
background-color: var(--vp-c-bg-alt);
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.word-token {
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.word-token:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.attention-bars {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attention-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.word-label {
|
|
||||||
width: 40px;
|
|
||||||
text-align: right;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attention-progress {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.explanation-panel {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.insight-title {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,332 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="backpropagation-demo">
|
<div class="demo-card">
|
||||||
<el-card shadow="hover">
|
<div class="bp-flow">
|
||||||
<template #header>
|
<div class="step-block" v-for="(step, i) in steps" :key="i" :style="{ borderTopColor: step.color }">
|
||||||
<div class="card-header">
|
<div class="step-num" :style="{ background: step.color }">{{ i + 1 }}</div>
|
||||||
<h4>🔄 反向传播演示</h4>
|
<div class="step-icon">{{ step.icon }}</div>
|
||||||
<p class="subtitle">
|
<div class="step-name">{{ step.name }}</div>
|
||||||
观察神经网络如何通过误差反向调整权重
|
<div class="step-desc">{{ step.desc }}</div>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="demo-content">
|
|
||||||
<div class="network-view">
|
|
||||||
<svg
|
|
||||||
class="network-svg"
|
|
||||||
viewBox="0 0 600 300"
|
|
||||||
>
|
|
||||||
<!-- Layers visualization -->
|
|
||||||
<g
|
|
||||||
v-for="(layer, lIndex) in 3"
|
|
||||||
:key="lIndex"
|
|
||||||
>
|
|
||||||
<text
|
|
||||||
:x="100 + lIndex * 200"
|
|
||||||
y="20"
|
|
||||||
text-anchor="middle"
|
|
||||||
class="layer-label"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
lIndex === 0 ? '输入层' : lIndex === 1 ? '隐藏层' : '输出层'
|
|
||||||
}}
|
|
||||||
</text>
|
|
||||||
|
|
||||||
<circle
|
|
||||||
v-for="n in 3"
|
|
||||||
:key="`${lIndex}-${n}`"
|
|
||||||
:cx="100 + lIndex * 200"
|
|
||||||
:cy="60 + n * 70"
|
|
||||||
:r="25"
|
|
||||||
:class="['neuron', getNeuronClass(lIndex, n)]"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- Connections with error flow -->
|
|
||||||
<line
|
|
||||||
v-for="conn in connections"
|
|
||||||
:key="conn.id"
|
|
||||||
:x1="conn.x1"
|
|
||||||
:y1="conn.y1"
|
|
||||||
:x2="conn.x2"
|
|
||||||
:y2="conn.y2"
|
|
||||||
:stroke="conn.color"
|
|
||||||
:stroke-width="conn.width"
|
|
||||||
:opacity="conn.opacity"
|
|
||||||
class="connection"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-divider />
|
|
||||||
|
|
||||||
<div class="controls-panel">
|
|
||||||
<el-steps
|
|
||||||
:active="currentStep"
|
|
||||||
align-center
|
|
||||||
finish-status="success"
|
|
||||||
>
|
|
||||||
<el-step
|
|
||||||
v-for="(step, index) in steps"
|
|
||||||
:key="index"
|
|
||||||
:title="step"
|
|
||||||
/>
|
|
||||||
</el-steps>
|
|
||||||
|
|
||||||
<div class="error-display mt-4">
|
|
||||||
<div class="flex justify-between mb-2">
|
|
||||||
<span class="text-sm">误差 (Loss)</span>
|
|
||||||
<span class="text-sm font-bold">{{ errorValue.toFixed(4) }}</span>
|
|
||||||
</div>
|
|
||||||
<el-progress
|
|
||||||
:percentage="Math.round(errorValue * 100)"
|
|
||||||
:color="customColors"
|
|
||||||
:striped="currentStep === 2"
|
|
||||||
:striped-flow="currentStep === 2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-alert
|
|
||||||
:title="explanations[currentStep]"
|
|
||||||
type="info"
|
|
||||||
show-icon
|
|
||||||
:closable="false"
|
|
||||||
class="mt-4"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="action-buttons mt-4 flex justify-center gap-4">
|
|
||||||
<el-button
|
|
||||||
:disabled="currentStep === 0"
|
|
||||||
@click="resetDemo"
|
|
||||||
>
|
|
||||||
重置
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
:disabled="currentStep >= 4"
|
|
||||||
@click="nextStep"
|
|
||||||
>
|
|
||||||
{{ currentStep < 4 ? '下一步' : '完成' }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
|
<div class="loss-visual">
|
||||||
|
<div class="loss-label">Loss(误差)随训练轮次下降:</div>
|
||||||
|
<svg viewBox="0 0 300 60" class="loss-svg">
|
||||||
|
<polyline :points="lossPoints" fill="none" stroke="var(--vp-c-brand)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<text x="5" y="10" class="ax">高</text>
|
||||||
|
<text x="5" y="56" class="ax">低</text>
|
||||||
|
<text x="220" y="56" class="ax">训练轮次 →</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
const steps = [
|
||||||
|
{ icon: '➡️', name: '前向传播', desc: '数据流过网络,得出预测', color: '#3b82f6' },
|
||||||
const currentStep = ref(0)
|
{ icon: '📐', name: '计算误差', desc: '预测值 vs 正确答案,算 Loss', color: '#d97706' },
|
||||||
const errorValue = ref(0.95)
|
{ icon: '⬅️', name: '反向传播', desc: '逐层追溯每个权重的"责任"', color: '#dc2626' },
|
||||||
const steps = ['前向传播', '计算误差', '反向传播', '更新权重']
|
{ icon: '⚙️', name: '更新权重', desc: '按责任微调,减少下次误差', color: '#059669' },
|
||||||
const explanations = [
|
|
||||||
'输入数据通过各层传递,得到预测输出。就像学生做完了一套试卷。',
|
|
||||||
'对比预测值和真实值,计算误差。就像老师批改试卷,算出得了多少分(错得有多离谱)。',
|
|
||||||
'将误差从输出层反向传递到各层。就像老师把错题反馈给学生,告诉他是哪一步思路错了。',
|
|
||||||
'根据误差梯度调整每个神经元的权重。学生根据反馈修正自己的理解(权重),下次就能做对了。',
|
|
||||||
'演示完成!通过不断重复这个过程,网络就学会了任务。'
|
|
||||||
]
|
]
|
||||||
|
const lossPoints = (() => {
|
||||||
const customColors = [
|
const pts = []
|
||||||
{ color: '#67c23a', percentage: 20 },
|
for (let i = 0; i <= 50; i++) {
|
||||||
{ color: '#e6a23c', percentage: 50 },
|
const x = 20 + i * 5.2
|
||||||
{ color: '#f56c6c', percentage: 100 }
|
const y = 52 - 44 * Math.exp(-i * 0.09) + Math.sin(i * 0.7) * 1
|
||||||
]
|
pts.push(`${x},${y}`)
|
||||||
|
|
||||||
const connections = ref([])
|
|
||||||
|
|
||||||
// Generate initial connections
|
|
||||||
const initConnections = () => {
|
|
||||||
const conns = []
|
|
||||||
// Input -> Hidden
|
|
||||||
for (let i = 1; i <= 3; i++) {
|
|
||||||
for (let j = 1; j <= 3; j++) {
|
|
||||||
conns.push({
|
|
||||||
id: `i${i}-h${j}`,
|
|
||||||
x1: 100,
|
|
||||||
y1: 60 + i * 70,
|
|
||||||
x2: 300,
|
|
||||||
y2: 60 + j * 70,
|
|
||||||
width: 2,
|
|
||||||
color: '#dcdfe6',
|
|
||||||
opacity: 0.5
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Hidden -> Output
|
return pts.join(' ')
|
||||||
for (let i = 1; i <= 3; i++) {
|
})()
|
||||||
for (let j = 1; j <= 3; j++) {
|
|
||||||
conns.push({
|
|
||||||
id: `h${i}-o${j}`,
|
|
||||||
x1: 300,
|
|
||||||
y1: 60 + i * 70,
|
|
||||||
x2: 500,
|
|
||||||
y2: 60 + j * 70,
|
|
||||||
width: 2,
|
|
||||||
color: '#dcdfe6',
|
|
||||||
opacity: 0.5
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
connections.value = conns
|
|
||||||
}
|
|
||||||
|
|
||||||
const getNeuronClass = (layerIndex, neuronIndex) => {
|
|
||||||
if (currentStep.value === 0) return 'active' // Forward
|
|
||||||
if (currentStep.value === 2) {
|
|
||||||
// Backward
|
|
||||||
if (layerIndex === 2) return 'error-source'
|
|
||||||
if (layerIndex === 1) return 'error-passing'
|
|
||||||
}
|
|
||||||
if (currentStep.value === 3) return 'updating' // Update
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextStep = () => {
|
|
||||||
if (currentStep.value >= 4) return
|
|
||||||
currentStep.value++
|
|
||||||
|
|
||||||
if (currentStep.value === 1) {
|
|
||||||
// Calculate Error
|
|
||||||
// Visual effect only
|
|
||||||
} else if (currentStep.value === 2) {
|
|
||||||
// Backprop: highlight connections red
|
|
||||||
connections.value.forEach((c) => {
|
|
||||||
c.color = '#f56c6c'
|
|
||||||
c.width = 4
|
|
||||||
c.opacity = 1
|
|
||||||
})
|
|
||||||
} else if (currentStep.value === 3) {
|
|
||||||
// Update weights: error drops
|
|
||||||
const reduceError = setInterval(() => {
|
|
||||||
if (errorValue.value > 0.1) {
|
|
||||||
errorValue.value -= 0.05
|
|
||||||
} else {
|
|
||||||
clearInterval(reduceError)
|
|
||||||
}
|
|
||||||
}, 50)
|
|
||||||
|
|
||||||
connections.value.forEach((c) => {
|
|
||||||
c.color = '#67c23a'
|
|
||||||
c.width = 2
|
|
||||||
c.opacity = 0.8
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetDemo = () => {
|
|
||||||
currentStep.value = 0
|
|
||||||
errorValue.value = 0.95
|
|
||||||
initConnections()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
initConnections()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.backpropagation-demo {
|
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1.25rem; margin: 1rem 0; }
|
||||||
margin: 20px 0;
|
.bp-flow { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.5rem; margin-bottom: 0.8rem; }
|
||||||
}
|
@media (max-width: 600px) { .bp-flow { grid-template-columns: repeat(2, 1fr); } }
|
||||||
|
.step-block { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-top: 3px solid; border-radius: 6px; padding: 0.7rem 0.5rem; display: flex; flex-direction: column; align-items: center; gap: 0.25rem; text-align: center; }
|
||||||
.card-header h4 {
|
.step-num { width: 16px; height: 16px; border-radius: 50%; color: white; font-size: 0.6rem; font-weight: bold; display: flex; align-items: center; justify-content: center; }
|
||||||
margin: 0;
|
.step-icon { font-size: 1.2rem; }
|
||||||
font-size: 16px;
|
.step-name { font-weight: bold; font-size: 0.78rem; }
|
||||||
font-weight: 600;
|
.step-desc { font-size: 0.68rem; color: var(--vp-c-text-2); line-height: 1.3; }
|
||||||
}
|
.loss-visual { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-radius: 6px; padding: 0.7rem; }
|
||||||
|
.loss-label { font-size: 0.75rem; color: var(--vp-c-text-2); margin-bottom: 0.3rem; }
|
||||||
.subtitle {
|
.loss-svg { width: 100%; max-width: 380px; height: auto; display: block; margin: 0 auto; }
|
||||||
font-size: 13px;
|
.ax { font-size: 6px; fill: var(--vp-c-text-3); }
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
margin: 4px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-view {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: var(--vp-c-bg-alt);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-svg {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 600px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer-label {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
fill: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.neuron {
|
|
||||||
fill: var(--vp-c-bg);
|
|
||||||
stroke: var(--vp-c-text-2);
|
|
||||||
stroke-width: 2;
|
|
||||||
transition: all 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.neuron.active {
|
|
||||||
fill: var(--el-color-primary-light-9);
|
|
||||||
stroke: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.neuron.error-source {
|
|
||||||
fill: var(--el-color-danger-light-9);
|
|
||||||
stroke: var(--el-color-danger);
|
|
||||||
filter: drop-shadow(0 0 5px var(--el-color-danger));
|
|
||||||
}
|
|
||||||
|
|
||||||
.neuron.error-passing {
|
|
||||||
fill: var(--el-color-warning-light-9);
|
|
||||||
stroke: var(--el-color-warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
.neuron.updating {
|
|
||||||
fill: var(--el-color-success-light-9);
|
|
||||||
stroke: var(--el-color-success);
|
|
||||||
r: 28; /* Pulse effect */
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection {
|
|
||||||
transition: all 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-4 {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-2 {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.justify-between {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.justify-center {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap-4 {
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-sm {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-bold {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+1
-276
@@ -1,278 +1,3 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="combinatorial-explosion-demo">
|
<div></div>
|
||||||
<el-card shadow="hover">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<h4>🎯 组合爆炸模拟器</h4>
|
|
||||||
<p class="subtitle">
|
|
||||||
亲手体验"规则指数增长"的恐怖
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="demo-content">
|
|
||||||
<div class="controls-grid">
|
|
||||||
<div class="control-item">
|
|
||||||
<div class="label-row">
|
|
||||||
<span class="label-icon">🎨</span>
|
|
||||||
<span class="label-text">物体特征数量: {{ featureCount }}</span>
|
|
||||||
</div>
|
|
||||||
<el-slider
|
|
||||||
v-model="featureCount"
|
|
||||||
:min="2"
|
|
||||||
:max="6"
|
|
||||||
show-stops
|
|
||||||
:marks="{ 2: '2', 4: '4', 6: '6' }"
|
|
||||||
/>
|
|
||||||
<div class="preview-tags">
|
|
||||||
<el-tag
|
|
||||||
v-for="i in featureCount"
|
|
||||||
:key="i"
|
|
||||||
size="small"
|
|
||||||
:type="getFeatureTagType(i)"
|
|
||||||
effect="plain"
|
|
||||||
>
|
|
||||||
特征{{ i }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-item">
|
|
||||||
<div class="label-row">
|
|
||||||
<span class="label-icon">🔢</span>
|
|
||||||
<span class="label-text">每个特征的可能值: {{ valuesPerFeature }}</span>
|
|
||||||
</div>
|
|
||||||
<el-slider
|
|
||||||
v-model="valuesPerFeature"
|
|
||||||
:min="2"
|
|
||||||
:max="4"
|
|
||||||
show-stops
|
|
||||||
:marks="{ 2: '2', 3: '3', 4: '4' }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-divider />
|
|
||||||
|
|
||||||
<div class="visualization-panel">
|
|
||||||
<div class="counter-display">
|
|
||||||
<el-statistic
|
|
||||||
title="需要的规则总数"
|
|
||||||
:value="totalRules"
|
|
||||||
value-style="font-weight: bold; color: var(--el-color-primary)"
|
|
||||||
>
|
|
||||||
<template #suffix>
|
|
||||||
<span class="formula-suffix">= {{ valuesPerFeature }}<sup>{{ featureCount }}</sup></span>
|
|
||||||
</template>
|
|
||||||
</el-statistic>
|
|
||||||
<el-tag
|
|
||||||
:type="complexityInfo.type"
|
|
||||||
effect="dark"
|
|
||||||
class="mt-2"
|
|
||||||
>
|
|
||||||
{{ complexityInfo.label }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="action-buttons mt-4">
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
:disabled="ruleCount >= maxRules"
|
|
||||||
@click="addRule"
|
|
||||||
>
|
|
||||||
✨ 添加规则 ({{ ruleCount }}/{{ maxRules }})
|
|
||||||
</el-button>
|
|
||||||
<el-button @click="resetRules">
|
|
||||||
🔄 重置
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rules-container mt-4">
|
|
||||||
<transition-group
|
|
||||||
name="el-zoom-in-center"
|
|
||||||
tag="div"
|
|
||||||
class="rules-grid"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="(rule, index) in displayedRules"
|
|
||||||
:key="rule.id"
|
|
||||||
class="rule-card-mini"
|
|
||||||
:style="{ borderColor: rule.color }"
|
|
||||||
>
|
|
||||||
<div class="rule-idx">
|
|
||||||
#{{ index + 1 }}
|
|
||||||
</div>
|
|
||||||
<div class="rule-dots">
|
|
||||||
<span
|
|
||||||
v-for="d in 3"
|
|
||||||
:key="d"
|
|
||||||
class="dot"
|
|
||||||
:style="{ backgroundColor: rule.color }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition-group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-alert
|
|
||||||
v-if="showWarning"
|
|
||||||
title="规则太多了!"
|
|
||||||
description="这就是'组合爆炸'。仅仅增加一点点复杂度,规则数量就会爆炸式增长,人类根本写不完。"
|
|
||||||
type="error"
|
|
||||||
show-icon
|
|
||||||
class="mt-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, watch } from 'vue'
|
|
||||||
|
|
||||||
const featureCount = ref(3)
|
|
||||||
const valuesPerFeature = ref(2)
|
|
||||||
const displayedRules = ref([])
|
|
||||||
const maxRules = 20 // Visual limit
|
|
||||||
|
|
||||||
const totalRules = computed(() =>
|
|
||||||
Math.pow(valuesPerFeature.value, featureCount.value)
|
|
||||||
)
|
|
||||||
const ruleCount = computed(() => displayedRules.value.length)
|
|
||||||
const showWarning = computed(() => totalRules.value > 50)
|
|
||||||
|
|
||||||
const complexityInfo = computed(() => {
|
|
||||||
if (totalRules.value <= 10)
|
|
||||||
return { label: '简单 (可人工处理)', type: 'success' }
|
|
||||||
if (totalRules.value <= 50)
|
|
||||||
return { label: '中等 (有点累了)', type: 'warning' }
|
|
||||||
return { label: '极难 (组合爆炸!)', type: 'danger' }
|
|
||||||
})
|
|
||||||
|
|
||||||
const getFeatureTagType = (i) => {
|
|
||||||
const types = ['', 'success', 'warning', 'danger', 'info']
|
|
||||||
return types[i % types.length]
|
|
||||||
}
|
|
||||||
|
|
||||||
const addRule = () => {
|
|
||||||
if (ruleCount.value >= maxRules) return
|
|
||||||
|
|
||||||
const colors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399']
|
|
||||||
displayedRules.value.push({
|
|
||||||
id: Date.now(),
|
|
||||||
color: colors[Math.floor(Math.random() * colors.length)]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetRules = () => {
|
|
||||||
displayedRules.value = []
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset rules when parameters change
|
|
||||||
watch([featureCount, valuesPerFeature], () => {
|
|
||||||
resetRules()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.combinatorial-explosion-demo {
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header h4 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
margin: 4px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.controls-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-tags {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.counter-display {
|
|
||||||
text-align: center;
|
|
||||||
background-color: var(--vp-c-bg-alt);
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formula-suffix {
|
|
||||||
font-size: 0.6em;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rules-grid {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule-card-mini {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
border: 2px solid;
|
|
||||||
border-radius: 6px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: var(--vp-c-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule-idx {
|
|
||||||
font-size: 10px;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule-dots {
|
|
||||||
display: flex;
|
|
||||||
gap: 2px;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-2 {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
.mt-4 {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
+25
-316
@@ -1,327 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="discriminative-vs-generative-demo">
|
<div class="demo-card">
|
||||||
<el-card shadow="hover">
|
<div class="schools-grid">
|
||||||
<template #header>
|
<div class="school-card" v-for="s in schools" :key="s.name" :style="{ borderTopColor: s.color }">
|
||||||
<div class="card-header">
|
<div class="card-head">
|
||||||
<h4>🎯 判别式 vs 生成式 AI</h4>
|
<span class="school-icon">{{ s.icon }}</span>
|
||||||
<p class="subtitle">
|
<span class="school-name" :style="{ color: s.color }">{{ s.name }}</span>
|
||||||
理解两种不同的 AI 范式
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div class="school-idea">{{ s.idea }}</div>
|
||||||
|
<div class="school-rep">代表:{{ s.rep }}</div>
|
||||||
<div class="comparison-container">
|
<div class="school-status">{{ s.status }}</div>
|
||||||
<el-row :gutter="20">
|
|
||||||
<!-- Discriminative AI -->
|
|
||||||
<el-col
|
|
||||||
:xs="24"
|
|
||||||
:sm="12"
|
|
||||||
>
|
|
||||||
<el-card
|
|
||||||
shadow="always"
|
|
||||||
class="ai-panel discriminative"
|
|
||||||
:class="{ active: mode === 'discriminative' }"
|
|
||||||
@click="mode = 'discriminative'"
|
|
||||||
>
|
|
||||||
<div class="panel-header">
|
|
||||||
<div class="icon">
|
|
||||||
🔍
|
|
||||||
</div>
|
|
||||||
<h5>判别式 AI</h5>
|
|
||||||
<el-tag
|
|
||||||
size="small"
|
|
||||||
type="success"
|
|
||||||
>
|
|
||||||
分类/识别
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel-content">
|
|
||||||
<div class="input-output">
|
|
||||||
<div class="io-box input">
|
|
||||||
<div class="io-label">
|
|
||||||
输入
|
|
||||||
</div>
|
|
||||||
<div class="io-content">
|
|
||||||
<div class="svg-placeholder green">
|
|
||||||
<span class="svg-text">猫图</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="arrow">
|
|
||||||
<el-icon><Bottom /></el-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="io-box output">
|
|
||||||
<div class="io-label">
|
|
||||||
输出
|
|
||||||
</div>
|
|
||||||
<div class="io-content">
|
|
||||||
<el-tag
|
|
||||||
effect="dark"
|
|
||||||
type="success"
|
|
||||||
>
|
|
||||||
这是猫
|
|
||||||
</el-tag>
|
|
||||||
<div class="probability">
|
|
||||||
置信度: 98%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="examples">
|
|
||||||
<h6>典型应用:</h6>
|
|
||||||
<div class="example-tags">
|
|
||||||
<el-tag
|
|
||||||
v-for="tag in [
|
|
||||||
'图像分类',
|
|
||||||
'垃圾邮件过滤',
|
|
||||||
'疾病诊断',
|
|
||||||
'人脸识别'
|
|
||||||
]"
|
|
||||||
:key="tag"
|
|
||||||
size="small"
|
|
||||||
effect="plain"
|
|
||||||
>
|
|
||||||
{{ tag }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
|
|
||||||
<!-- Generative AI -->
|
|
||||||
<el-col
|
|
||||||
:xs="24"
|
|
||||||
:sm="12"
|
|
||||||
>
|
|
||||||
<el-card
|
|
||||||
shadow="always"
|
|
||||||
class="ai-panel generative"
|
|
||||||
:class="{ active: mode === 'generative' }"
|
|
||||||
@click="mode = 'generative'"
|
|
||||||
>
|
|
||||||
<div class="panel-header">
|
|
||||||
<div class="icon">
|
|
||||||
✨
|
|
||||||
</div>
|
|
||||||
<h5>生成式 AI</h5>
|
|
||||||
<el-tag
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
>
|
|
||||||
创造/生成
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel-content">
|
|
||||||
<div class="input-output">
|
|
||||||
<div class="io-box input">
|
|
||||||
<div class="io-label">
|
|
||||||
输入
|
|
||||||
</div>
|
|
||||||
<div class="io-content">
|
|
||||||
<div class="prompt-text">
|
|
||||||
"一只戴墨镜的猫"
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="arrow">
|
|
||||||
<el-icon><Bottom /></el-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="io-box output">
|
|
||||||
<div class="io-label">
|
|
||||||
输出
|
|
||||||
</div>
|
|
||||||
<div class="io-content">
|
|
||||||
<div class="svg-placeholder blue">
|
|
||||||
<span class="svg-text">生成图像 ✓</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="examples">
|
|
||||||
<h6>典型应用:</h6>
|
|
||||||
<div class="example-tags">
|
|
||||||
<el-tag
|
|
||||||
v-for="tag in [
|
|
||||||
'ChatGPT',
|
|
||||||
'Midjourney',
|
|
||||||
'代码生成',
|
|
||||||
'音乐创作'
|
|
||||||
]"
|
|
||||||
:key="tag"
|
|
||||||
size="small"
|
|
||||||
effect="plain"
|
|
||||||
type="primary"
|
|
||||||
>
|
|
||||||
{{ tag }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
const schools = [
|
||||||
import { Bottom } from '@element-plus/icons-vue'
|
{ name: '符号主义', icon: '📜', color: '#059669', idea: '智能 = 符号推理 / If-Then 规则', rep: '专家系统、深蓝', status: '→ 与连接主义融合(神经符号 AI)' },
|
||||||
|
{ name: '连接主义', icon: '🧠', color: '#7c3aed', idea: '智能 = 神经元网络 + 海量数据', rep: 'AlphaGo、GPT 系列', status: '→ 主导大模型时代,当前主流' },
|
||||||
const mode = ref('discriminative')
|
{ name: '行为主义', icon: '🎮', color: '#d97706', idea: '智能 = 与环境互动 / 强化学习', rep: 'AlphaGo(RL 部分)', status: '→ 与连接主义融合(深度强化学习)' },
|
||||||
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.discriminative-vs-generative-demo {
|
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1.25rem; margin: 1rem 0; }
|
||||||
margin: 20px 0;
|
.schools-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.7rem; }
|
||||||
}
|
@media (max-width: 640px) { .schools-grid { grid-template-columns: 1fr; } }
|
||||||
|
.school-card { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-top: 3px solid; border-radius: 6px; padding: 0.9rem; display: flex; flex-direction: column; gap: 0.4rem; }
|
||||||
.card-header h4 {
|
.card-head { display: flex; align-items: center; gap: 0.5rem; }
|
||||||
margin: 0;
|
.school-icon { font-size: 1.3rem; }
|
||||||
font-size: 16px;
|
.school-name { font-weight: bold; font-size: 0.9rem; }
|
||||||
font-weight: 600;
|
.school-idea { font-size: 0.78rem; color: var(--vp-c-text-1); background: var(--vp-c-bg-alt); padding: 0.35rem 0.5rem; border-radius: 4px; }
|
||||||
}
|
.school-rep { font-size: 0.72rem; color: var(--vp-c-text-3); }
|
||||||
|
.school-status { font-size: 0.72rem; color: var(--vp-c-text-2); border-top: 1px dashed var(--vp-c-divider); padding-top: 0.35rem; }
|
||||||
.subtitle {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
margin: 4px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comparison-container {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-panel {
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-panel:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-panel.active {
|
|
||||||
border-color: var(--el-color-primary);
|
|
||||||
background-color: var(--el-color-primary-light-9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-header h5 {
|
|
||||||
margin: 0;
|
|
||||||
flex: 1;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-output {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
background-color: var(--vp-c-bg);
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.io-box {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.io-label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.io-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svg-placeholder {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svg-placeholder.green {
|
|
||||||
background-color: #48bb78;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svg-placeholder.blue {
|
|
||||||
background-color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svg-text {
|
|
||||||
color: white;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-text {
|
|
||||||
background-color: var(--vp-c-bg-alt);
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow {
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.probability {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.examples h6 {
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.example-tags {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<div class="demo-card">
|
||||||
|
<div class="events">
|
||||||
|
<div class="event" v-for="e in events" :key="e.year">
|
||||||
|
<div class="year-col">
|
||||||
|
<span class="year-badge">{{ e.year }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="dot-col">
|
||||||
|
<div class="dot" :style="{ background: e.color }"></div>
|
||||||
|
<div class="line" v-if="e !== events[events.length - 1]"></div>
|
||||||
|
</div>
|
||||||
|
<div class="content-col">
|
||||||
|
<div class="event-title">{{ e.title }}</div>
|
||||||
|
<div class="event-note">{{ e.note }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const events = [
|
||||||
|
{ year: '1943', title: 'MP 神经元模型', note: '麦卡洛克 & 皮茨:首次用数学公式模拟神经元,证明"神经元可被计算"。', color: '#3b82f6' },
|
||||||
|
{ year: '1950', title: '图灵测试', note: '图灵:如果机器的回答让人无法分辨是人还是机器,则认为它具备智能。', color: '#7c3aed' },
|
||||||
|
{ year: '1956', title: '达特茅斯会议 — AI 学科诞生', note: '麦卡锡等人首次提出"人工智能"概念,AI 正式成为一门学科。', color: '#059669' },
|
||||||
|
{ year: '1956', title: '逻辑理论家(Logic Theorist)', note: '纽厄尔 & 西蒙:第一个用规则自动证明数学定理的 AI 程序。', color: '#059669' },
|
||||||
|
{ year: '1958', title: 'LISP 语言诞生', note: '麦卡锡发明,成为此后数十年 AI 研究的核心编程语言。', color: '#d97706' },
|
||||||
|
{ year: '1959', title: '首台工业机器人', note: '德沃尔 & 恩格尔伯格:AI 从实验室走向工厂,开始改变工业生产。', color: '#dc2626' },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1.25rem; margin: 1rem 0; }
|
||||||
|
.events { display: flex; flex-direction: column; }
|
||||||
|
.event { display: grid; grid-template-columns: 52px 24px 1fr; gap: 0 0.6rem; }
|
||||||
|
.year-col { display: flex; align-items: flex-start; padding-top: 0.15rem; justify-content: flex-end; }
|
||||||
|
.year-badge { font-size: 0.7rem; font-weight: bold; color: var(--vp-c-text-3); white-space: nowrap; }
|
||||||
|
.dot-col { display: flex; flex-direction: column; align-items: center; }
|
||||||
|
.dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; margin-top: 0.2rem; }
|
||||||
|
.line { width: 2px; flex: 1; background: var(--vp-c-divider); margin: 3px 0; min-height: 16px; }
|
||||||
|
.content-col { padding-bottom: 0.9rem; }
|
||||||
|
.event-title { font-weight: bold; font-size: 0.85rem; color: var(--vp-c-text-1); margin-bottom: 0.15rem; }
|
||||||
|
.event-note { font-size: 0.78rem; color: var(--vp-c-text-2); line-height: 1.5; }
|
||||||
|
</style>
|
||||||
@@ -1,293 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="gpt-evolution-demo">
|
<div class="demo-card">
|
||||||
<el-card
|
<div class="gpt-grid">
|
||||||
shadow="hover"
|
<div class="gpt-card" v-for="m in models" :key="m.name" :style="{ borderTopColor: m.color }">
|
||||||
class="main-card"
|
<div class="card-top">
|
||||||
>
|
<span class="gpt-name" :style="{ color: m.color }">{{ m.name }}</span>
|
||||||
<template #header>
|
<span class="gpt-year">{{ m.year }}</span>
|
||||||
<div class="card-header">
|
|
||||||
<span class="title">🚀 GPT 进化历程</span>
|
|
||||||
<span class="subtitle">从 GPT-1 到 GPT-4 的演进之路</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div class="param-val">{{ m.params }}</div>
|
||||||
|
<div class="param-bar-bg">
|
||||||
<div class="demo-content">
|
<div class="param-bar" :style="{ width: m.barWidth, background: m.color }"></div>
|
||||||
<!-- Replace Vertical Timeline with Horizontal Tabs -->
|
|
||||||
<el-tabs
|
|
||||||
v-model="activeModelName"
|
|
||||||
type="card"
|
|
||||||
class="evolution-tabs"
|
|
||||||
@tab-click="handleTabClick"
|
|
||||||
>
|
|
||||||
<el-tab-pane
|
|
||||||
v-for="(model, index) in gptModels"
|
|
||||||
:key="index"
|
|
||||||
:label="model.name"
|
|
||||||
:name="model.name"
|
|
||||||
>
|
|
||||||
<div class="model-view">
|
|
||||||
<div class="model-info-header">
|
|
||||||
<el-tag
|
|
||||||
effect="dark"
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
{{ model.year }}
|
|
||||||
</el-tag>
|
|
||||||
<div class="model-stats">
|
|
||||||
<div class="stat-item">
|
|
||||||
<span class="label">参数量</span>
|
|
||||||
<span class="value">{{ model.parameters }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<span class="label">上下文</span>
|
|
||||||
<span class="value">{{ model.context }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="model-description">
|
|
||||||
<p>{{ model.description }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="model-milestones">
|
|
||||||
<span class="section-label">🎯 关键能力:</span>
|
|
||||||
<div class="tags-container">
|
|
||||||
<el-tag
|
|
||||||
v-for="(milestone, i) in model.milestones"
|
|
||||||
:key="i"
|
|
||||||
size="small"
|
|
||||||
class="milestone-tag"
|
|
||||||
>
|
|
||||||
{{ milestone }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
|
|
||||||
<el-divider class="compact-divider">
|
|
||||||
进化趋势
|
|
||||||
</el-divider>
|
|
||||||
|
|
||||||
<div class="evolution-insight">
|
|
||||||
<div class="insight-row">
|
|
||||||
<div class="insight-item">
|
|
||||||
<el-icon><TrendCharts /></el-icon>
|
|
||||||
<div class="insight-text">
|
|
||||||
<span class="label">参数量增长</span>
|
|
||||||
<span class="value">10000倍+</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="insight-item">
|
|
||||||
<el-icon><ChatDotSquare /></el-icon>
|
|
||||||
<div class="insight-text">
|
|
||||||
<span class="label">对话能力</span>
|
|
||||||
<span class="value">单轮 → 多轮</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="insight-item">
|
|
||||||
<el-icon><Cpu /></el-icon>
|
|
||||||
<div class="insight-text">
|
|
||||||
<span class="label">逻辑推理</span>
|
|
||||||
<span class="value">弱 → 强</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="gpt-key">{{ m.key }}</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
const models = [
|
||||||
import { TrendCharts, ChatDotSquare, Cpu } from '@element-plus/icons-vue'
|
{ name: 'GPT-1', year: '2018', params: '1.17 亿', barWidth: '2%', color: '#94a3b8', key: '预训练+微调范式确立' },
|
||||||
|
{ name: 'GPT-2', year: '2019', params: '15 亿', barWidth: '6%', color: '#3b82f6', key: 'Zero-shot 零样本泛化' },
|
||||||
const activeModelName = ref('GPT-1')
|
{ name: 'GPT-3', year: '2020', params: '1750 亿', barWidth: '45%', color: '#7c3aed', key: '⚡ 涌现!上下文学习' },
|
||||||
|
{ name: 'GPT-4', year: '2023', params: '~1.8 万亿', barWidth: '100%', color: '#dc2626', key: '多模态 + 复杂推理' },
|
||||||
const gptModels = [
|
|
||||||
{
|
|
||||||
name: 'GPT-1',
|
|
||||||
year: '2018',
|
|
||||||
parameters: '1.17 亿',
|
|
||||||
paramDetail: '117 Million',
|
|
||||||
context: '512 tokens',
|
|
||||||
contextDetail: '约 1 页文本',
|
|
||||||
capability: '无监督预训练',
|
|
||||||
description: '开创性地使用了 Transformer 解码器进行预训练,证明了无监督学习在 NLP 中的潜力。',
|
|
||||||
milestones: ['预训练+微调范式', 'BookCorpus 数据集', '单向 Transformer']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'GPT-2',
|
|
||||||
year: '2019',
|
|
||||||
parameters: '15 亿',
|
|
||||||
paramDetail: '1.5 Billion',
|
|
||||||
context: '1024 tokens',
|
|
||||||
contextDetail: '约 2 页文本',
|
|
||||||
capability: '零样本任务',
|
|
||||||
description: '参数量扩大 10 倍,展示了惊人的零样本(Zero-shot)能力,能生成连贯的文本。',
|
|
||||||
milestones: ['WebText 数据集', 'Zero-shot Learning', '生成长文本']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'GPT-3',
|
|
||||||
year: '2020',
|
|
||||||
parameters: '1750 亿',
|
|
||||||
paramDetail: '175 Billion',
|
|
||||||
context: '2048 tokens',
|
|
||||||
contextDetail: '约 4 页文本',
|
|
||||||
capability: '上下文学习 (ICL)',
|
|
||||||
description: '参数量爆炸式增长,涌现出上下文学习能力(In-Context Learning),无需微调即可完成任务。',
|
|
||||||
milestones: ['Few-shot Learning', 'Common Crawl', '能力涌现']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'GPT-4',
|
|
||||||
year: '2023',
|
|
||||||
parameters: '1.8 万亿 (推测)',
|
|
||||||
paramDetail: '1.8 Trillion (Est.)',
|
|
||||||
context: '128k tokens',
|
|
||||||
contextDetail: '约 300 页书',
|
|
||||||
capability: '多模态 & 推理',
|
|
||||||
description: '引入多模态能力(识图),逻辑推理和代码能力大幅提升,支持超长上下文。',
|
|
||||||
milestones: ['多模态输入', 'MoE 架构', 'RLHF 对齐', '考试高手']
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const handleTabClick = (tab) => {
|
|
||||||
// activeModelName updated automatically
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.gpt-evolution-demo {
|
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1.25rem; margin: 1rem 0; }
|
||||||
margin: 10px 0;
|
.gpt-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.5rem; }
|
||||||
}
|
@media (max-width: 640px) { .gpt-grid { grid-template-columns: repeat(2, 1fr); } }
|
||||||
|
.gpt-card { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-top: 3px solid; border-radius: 6px; padding: 0.7rem; display: flex; flex-direction: column; gap: 0.35rem; }
|
||||||
.main-card {
|
.card-top { display: flex; justify-content: space-between; }
|
||||||
border-radius: 6px;
|
.gpt-name { font-weight: bold; font-size: 0.88rem; }
|
||||||
}
|
.gpt-year { font-size: 0.68rem; color: var(--vp-c-text-3); }
|
||||||
|
.param-val { font-size: 0.78rem; font-weight: bold; font-family: monospace; color: var(--vp-c-text-1); }
|
||||||
.card-header {
|
.param-bar-bg { height: 6px; background: var(--vp-c-bg-alt); border-radius: 3px; overflow: hidden; }
|
||||||
display: flex;
|
.param-bar { height: 100%; border-radius: 3px; min-width: 3px; }
|
||||||
justify-content: space-between;
|
.gpt-key { font-size: 0.7rem; color: var(--vp-c-brand-1); background: var(--vp-c-brand-soft); padding: 0.15rem 0.4rem; border-radius: 3px; }
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Compact Tabs */
|
|
||||||
.evolution-tabs :deep(.el-tabs__header) {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-view {
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-info-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-stats {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item .label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item .value {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #409eff;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-description {
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #606266;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-milestones {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-label {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
|
|
||||||
.milestone-tag {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compact-divider {
|
|
||||||
margin: 15px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.evolution-insight {
|
|
||||||
background-color: #f0f9eb;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.insight-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
}
|
|
||||||
|
|
||||||
.insight-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.insight-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.insight-text .label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #67c23a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.insight-text .value {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.insight-row {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
+43
-409
@@ -1,424 +1,58 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="nn-viz-demo">
|
<div class="demo-card">
|
||||||
<el-card shadow="hover">
|
<div class="net-layout">
|
||||||
<template #header>
|
<div class="svg-wrap">
|
||||||
<div class="card-header">
|
<svg viewBox="0 0 380 200" class="net-svg">
|
||||||
<h4>神经网络:手动前向传播(可控演示)</h4>
|
<line v-for="c in connections" :key="c.id" :x1="c.x1" :y1="c.y1" :x2="c.x2" :y2="c.y2" stroke="#94a3b8" stroke-width="1.2" opacity="0.35" />
|
||||||
<p class="subtitle">
|
<g v-for="layer in layers" :key="layer.idx">
|
||||||
用"开始 / 上一步 / 下一步"逐层推进,避免误把动画当成真实训练过程。
|
<circle v-for="n in layer.nodes" :key="n.id" :cx="n.x" :cy="n.y" r="15" :fill="layer.fill" :stroke="layer.stroke" stroke-width="2" />
|
||||||
</p>
|
</g>
|
||||||
</div>
|
<text v-for="layer in layers" :key="'l-'+layer.idx" :x="layer.x" y="194" text-anchor="middle" :fill="layer.stroke" class="lbl">{{ layer.name }}</text>
|
||||||
</template>
|
</svg>
|
||||||
|
|
||||||
<div class="controls mb-4 flex gap-2">
|
|
||||||
<el-button-group>
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
:disabled="step !== 0"
|
|
||||||
@click="start"
|
|
||||||
>
|
|
||||||
开始
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
:disabled="step <= 1"
|
|
||||||
@click="prev"
|
|
||||||
>
|
|
||||||
上一步
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
:disabled="step === 0 || step >= maxStep"
|
|
||||||
@click="next"
|
|
||||||
>
|
|
||||||
下一步
|
|
||||||
</el-button>
|
|
||||||
<el-button @click="reset">
|
|
||||||
重置
|
|
||||||
</el-button>
|
|
||||||
</el-button-group>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="layer-cards">
|
||||||
<div
|
<div class="layer-card" v-for="info in layerInfo" :key="info.name" :style="{ borderLeftColor: info.color }">
|
||||||
v-if="step > 0"
|
<div class="lc-title" :style="{ color: info.color }">{{ info.name }}</div>
|
||||||
class="progress mb-4"
|
<div class="lc-desc">{{ info.desc }}</div>
|
||||||
>
|
|
||||||
<el-steps
|
|
||||||
:active="step"
|
|
||||||
align-center
|
|
||||||
finish-status="success"
|
|
||||||
>
|
|
||||||
<el-step title="输入层" />
|
|
||||||
<el-step title="隐藏层" />
|
|
||||||
<el-step title="输出层" />
|
|
||||||
</el-steps>
|
|
||||||
<div class="step-info text-center mt-2 text-sm text-gray-500">
|
|
||||||
Step {{ step }} / {{ maxStep }} · {{ stepTitle }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="grid-layout">
|
|
||||||
<el-card
|
|
||||||
shadow="never"
|
|
||||||
class="viz-card"
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
<div class="card-title">
|
|
||||||
网络结构
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="network-container">
|
|
||||||
<svg
|
|
||||||
class="network-svg"
|
|
||||||
:viewBox="`0 0 ${svgWidth} ${svgHeight}`"
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<linearGradient
|
|
||||||
id="conn"
|
|
||||||
x1="0%"
|
|
||||||
y1="0%"
|
|
||||||
x2="100%"
|
|
||||||
y2="0%"
|
|
||||||
>
|
|
||||||
<stop
|
|
||||||
offset="0%"
|
|
||||||
:style="{
|
|
||||||
stopColor: 'var(--el-color-primary)',
|
|
||||||
stopOpacity: 0.18
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="100%"
|
|
||||||
:style="{
|
|
||||||
stopColor: 'var(--el-color-primary)',
|
|
||||||
stopOpacity: 0.45
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<g class="connections">
|
|
||||||
<line
|
|
||||||
v-for="c in connections"
|
|
||||||
:key="c.id"
|
|
||||||
:x1="c.x1"
|
|
||||||
:y1="c.y1"
|
|
||||||
:x2="c.x2"
|
|
||||||
:y2="c.y2"
|
|
||||||
:class="{
|
|
||||||
active: isConnectionActive(c),
|
|
||||||
focus: isConnectionFocus(c)
|
|
||||||
}"
|
|
||||||
class="connection-line"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g class="neurons">
|
|
||||||
<g
|
|
||||||
v-for="n in neurons"
|
|
||||||
:key="n.id"
|
|
||||||
:transform="`translate(${n.x}, ${n.y})`"
|
|
||||||
:class="{
|
|
||||||
neuron: true,
|
|
||||||
active: isNeuronActive(n),
|
|
||||||
focus: focusLayer === n.layer
|
|
||||||
}"
|
|
||||||
@click="focusLayer = n.layer"
|
|
||||||
>
|
|
||||||
<circle :r="n.r" />
|
|
||||||
<text
|
|
||||||
v-if="n.label"
|
|
||||||
y="32"
|
|
||||||
text-anchor="middle"
|
|
||||||
class="neuron-label"
|
|
||||||
>
|
|
||||||
{{ n.label }}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-alert
|
|
||||||
title="提示:点击某一层的神经元可以“聚焦”该层(仅用于查看,不会触发自动流程)。"
|
|
||||||
type="info"
|
|
||||||
show-icon
|
|
||||||
:closable="false"
|
|
||||||
class="mt-2"
|
|
||||||
/>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card
|
|
||||||
shadow="never"
|
|
||||||
class="info-card"
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
<div class="card-title">
|
|
||||||
每一层在做什么
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="layers-info">
|
|
||||||
<el-collapse v-model="activeCollapse">
|
|
||||||
<el-collapse-item
|
|
||||||
v-for="(l, idx) in layerConfigs"
|
|
||||||
:key="l.name"
|
|
||||||
:title="l.name"
|
|
||||||
:name="idx"
|
|
||||||
>
|
|
||||||
<div class="layer-detail">
|
|
||||||
<p>{{ l.desc }}</p>
|
|
||||||
<div class="math-box">
|
|
||||||
<code>{{ l.math }}</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-collapse-item>
|
|
||||||
</el-collapse>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
const W = 380, H = 185
|
||||||
|
const layerDef = [
|
||||||
const svgWidth = 600
|
{ name: '输入层', count: 3, xFrac: 0.13, color: '#3b82f6', fill: '#dbeafe' },
|
||||||
const svgHeight = 300
|
{ name: '隐藏层', count: 4, xFrac: 0.5, color: '#7c3aed', fill: '#ede9fe' },
|
||||||
const step = ref(0)
|
{ name: '输出层', count: 2, xFrac: 0.87, color: '#059669', fill: '#d1fae5' },
|
||||||
const maxStep = 3
|
|
||||||
const focusLayer = ref(null)
|
|
||||||
const activeCollapse = ref([0])
|
|
||||||
|
|
||||||
// Mock logic for demo
|
|
||||||
const layerConfigs = [
|
|
||||||
{
|
|
||||||
name: '输入层 (Input)',
|
|
||||||
desc: '接收原始数据(如图片的像素值)。',
|
|
||||||
math: 'x = [x1, x2, x3]'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '隐藏层 (Hidden)',
|
|
||||||
desc: '提取特征(如边缘、形状)。每个神经元计算加权和并激活。',
|
|
||||||
math: 'h = ReLU(W1·x + b1)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '输出层 (Output)',
|
|
||||||
desc: '给出最终结果(如分类概率)。',
|
|
||||||
math: 'y = Softmax(W2·h + b2)'
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
const layers = layerDef.map((l, idx) => {
|
||||||
const neurons = ref([])
|
const x = l.xFrac * W
|
||||||
const connections = ref([])
|
const gap = Math.min(46, (H - 36) / (l.count - 1 || 1))
|
||||||
|
const startY = (H - gap * (l.count - 1)) / 2
|
||||||
const stepTitle = computed(() => {
|
return { idx, x, name: l.name, fill: l.fill, stroke: l.color, nodes: Array.from({ length: l.count }, (_, i) => ({ id: `${idx}-${i}`, x, y: startY + i * gap })) }
|
||||||
if (step.value === 0) return '准备就绪'
|
|
||||||
if (step.value === 1) return '输入数据进入网络'
|
|
||||||
if (step.value === 2) return '隐藏层提取特征'
|
|
||||||
if (step.value === 3) return '输出层得出结果'
|
|
||||||
return ''
|
|
||||||
})
|
})
|
||||||
|
const connections = []
|
||||||
const initNetwork = () => {
|
for (let li = 0; li < layers.length - 1; li++) {
|
||||||
// Simple layout logic
|
layers[li].nodes.forEach(a => { layers[li + 1].nodes.forEach(b => { connections.push({ id: `${a.id}-${b.id}`, x1: a.x, y1: a.y, x2: b.x, y2: b.y }) }) })
|
||||||
const layers = [3, 4, 2] // Neuron counts per layer
|
|
||||||
const layerX = [100, 300, 500]
|
|
||||||
const ns = []
|
|
||||||
const cs = []
|
|
||||||
|
|
||||||
layers.forEach((count, layerIdx) => {
|
|
||||||
const startY = (svgHeight - (count - 1) * 60) / 2
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
ns.push({
|
|
||||||
id: `n-${layerIdx}-${i}`,
|
|
||||||
layer: layerIdx,
|
|
||||||
x: layerX[layerIdx],
|
|
||||||
y: startY + i * 60,
|
|
||||||
r: 20,
|
|
||||||
label: layerIdx === 0 ? `x${i + 1}` : layerIdx === 2 ? `y${i + 1}` : ''
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Create connections
|
|
||||||
ns.forEach((src) => {
|
|
||||||
ns.forEach((tgt) => {
|
|
||||||
if (tgt.layer === src.layer + 1) {
|
|
||||||
cs.push({
|
|
||||||
id: `c-${src.id}-${tgt.id}`,
|
|
||||||
srcId: src.id,
|
|
||||||
tgtId: tgt.id,
|
|
||||||
srcLayer: src.layer,
|
|
||||||
x1: src.x,
|
|
||||||
y1: src.y,
|
|
||||||
x2: tgt.x,
|
|
||||||
y2: tgt.y
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
neurons.value = ns
|
|
||||||
connections.value = cs
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
initNetwork()
|
|
||||||
})
|
|
||||||
|
|
||||||
const start = () => {
|
|
||||||
step.value = 1
|
|
||||||
focusLayer.value = 0
|
|
||||||
activeCollapse.value = [0]
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = () => {
|
|
||||||
if (step.value < maxStep) {
|
|
||||||
step.value++
|
|
||||||
focusLayer.value = step.value - 1
|
|
||||||
activeCollapse.value = [step.value - 1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const prev = () => {
|
|
||||||
if (step.value > 1) {
|
|
||||||
step.value--
|
|
||||||
focusLayer.value = step.value - 1
|
|
||||||
activeCollapse.value = [step.value - 1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
step.value = 0
|
|
||||||
focusLayer.value = null
|
|
||||||
activeCollapse.value = [0]
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNeuronActive = (n) => {
|
|
||||||
if (step.value === 0) return false
|
|
||||||
return n.layer < step.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const isConnectionActive = (c) => {
|
|
||||||
if (step.value === 0) return false
|
|
||||||
return c.srcLayer < step.value - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const isConnectionFocus = (c) => {
|
|
||||||
// Optional: highlight connections related to focused layer
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
const layerInfo = [
|
||||||
|
{ name: '输入层', desc: '原始像素 / 数值信号', color: '#3b82f6' },
|
||||||
|
{ name: '隐藏层(可叠加多层)', desc: '底层识别边缘 → 中层识别形状 → 高层识别语义概念', color: '#7c3aed' },
|
||||||
|
{ name: '输出层', desc: '最终分类或预测结果', color: '#059669' },
|
||||||
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.nn-viz-demo {
|
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1.25rem; margin: 1rem 0; }
|
||||||
margin: 20px 0;
|
.net-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-radius: 6px; padding: 0.9rem; }
|
||||||
}
|
@media (max-width: 600px) { .net-layout { grid-template-columns: 1fr; } }
|
||||||
|
.svg-wrap { display: flex; align-items: center; justify-content: center; background: var(--vp-c-bg-alt); border-radius: 6px; }
|
||||||
.card-header h4 {
|
.net-svg { width: 100%; height: auto; }
|
||||||
margin: 0;
|
.lbl { font-size: 9px; font-weight: bold; }
|
||||||
font-size: 16px;
|
.layer-cards { display: flex; flex-direction: column; gap: 0.4rem; justify-content: center; }
|
||||||
font-weight: 600;
|
.layer-card { border-left: 3px solid; padding: 0.5rem 0.7rem; background: var(--vp-c-bg-alt); border-radius: 0 5px 5px 0; }
|
||||||
}
|
.lc-title { font-weight: bold; font-size: 0.78rem; margin-bottom: 0.15rem; }
|
||||||
|
.lc-desc { font-size: 0.73rem; color: var(--vp-c-text-2); line-height: 1.4; }
|
||||||
.subtitle {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
margin: 4px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-4 {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-2 {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap-2 {
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-sm {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-gray-500 {
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-layout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 2fr 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.grid-layout {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: var(--vp-c-bg-alt);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-svg {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-line {
|
|
||||||
stroke: var(--vp-c-divider);
|
|
||||||
stroke-width: 2;
|
|
||||||
transition: all 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-line.active {
|
|
||||||
stroke: var(--el-color-primary);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.neuron circle {
|
|
||||||
fill: var(--vp-c-bg);
|
|
||||||
stroke: var(--vp-c-text-2);
|
|
||||||
stroke-width: 2;
|
|
||||||
transition: all 0.5s;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.neuron.active circle {
|
|
||||||
fill: var(--el-color-primary-light-9);
|
|
||||||
stroke: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.neuron.focus circle {
|
|
||||||
stroke-width: 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.neuron-label {
|
|
||||||
font-size: 12px;
|
|
||||||
fill: var(--vp-c-text-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.math-box {
|
|
||||||
background-color: var(--vp-c-bg-alt);
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-top: 8px;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,288 +1,71 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="perceptron-demo">
|
<div class="demo-card">
|
||||||
<el-card shadow="hover">
|
<div class="perceptron-layout">
|
||||||
<template #header>
|
<div class="inputs-col">
|
||||||
<div class="card-header">
|
<div class="input-node" v-for="inp in inputs" :key="inp.label">
|
||||||
<h4>感知机 (Perceptron) 演示</h4>
|
<span class="node-circle">{{ inp.val }}</span>
|
||||||
<p class="subtitle">
|
<span class="node-label">{{ inp.label }}</span>
|
||||||
最简单的神经元:输入 x 权重 + 偏置 = 输出
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="neuron-viz-container">
|
|
||||||
<!-- Inputs -->
|
|
||||||
<div class="col inputs-col">
|
|
||||||
<div class="node-wrapper">
|
|
||||||
<el-tag effect="dark">
|
|
||||||
输入 A
|
|
||||||
</el-tag>
|
|
||||||
<el-input-number
|
|
||||||
v-model="x1"
|
|
||||||
size="small"
|
|
||||||
:step="1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="node-wrapper">
|
|
||||||
<el-tag effect="dark">
|
|
||||||
输入 B
|
|
||||||
</el-tag>
|
|
||||||
<el-input-number
|
|
||||||
v-model="x2"
|
|
||||||
size="small"
|
|
||||||
:step="1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Weights Visual -->
|
|
||||||
<div class="col weights-col">
|
|
||||||
<div class="weight-group">
|
|
||||||
<div
|
|
||||||
class="weight-line"
|
|
||||||
:style="{
|
|
||||||
height: Math.abs(w1) * 2 + 2 + 'px',
|
|
||||||
opacity: Math.abs(w1) / 5 + 0.2
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<div class="weight-control">
|
|
||||||
<span class="label">权重 A: {{ w1 }}</span>
|
|
||||||
<el-slider
|
|
||||||
v-model="w1"
|
|
||||||
:min="-5"
|
|
||||||
:max="5"
|
|
||||||
:step="0.1"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="weight-group">
|
|
||||||
<div
|
|
||||||
class="weight-line"
|
|
||||||
:style="{
|
|
||||||
height: Math.abs(w2) * 2 + 2 + 'px',
|
|
||||||
opacity: Math.abs(w2) / 5 + 0.2
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<div class="weight-control">
|
|
||||||
<span class="label">权重 B: {{ w2 }}</span>
|
|
||||||
<el-slider
|
|
||||||
v-model="w2"
|
|
||||||
:min="-5"
|
|
||||||
:max="5"
|
|
||||||
:step="0.1"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Neuron Body -->
|
|
||||||
<div class="col neuron-col">
|
|
||||||
<div class="neuron-circle">
|
|
||||||
<div class="sum-symbol">
|
|
||||||
总分
|
|
||||||
</div>
|
|
||||||
<div class="sum-value">
|
|
||||||
{{ weightedSum.toFixed(1) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bias-control mt-2">
|
|
||||||
<span class="label">基础分 (Bias):</span>
|
|
||||||
<el-input-number
|
|
||||||
v-model="bias"
|
|
||||||
size="small"
|
|
||||||
:step="1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Output -->
|
|
||||||
<div class="col output-col">
|
|
||||||
<el-icon class="arrow-icon">
|
|
||||||
<Right />
|
|
||||||
</el-icon>
|
|
||||||
<div class="node-wrapper">
|
|
||||||
<el-tag
|
|
||||||
:type="output > 0 ? 'success' : 'info'"
|
|
||||||
effect="dark"
|
|
||||||
>
|
|
||||||
结果 (Output)
|
|
||||||
</el-tag>
|
|
||||||
<div
|
|
||||||
class="output-value"
|
|
||||||
:class="{ active: output > 0 }"
|
|
||||||
>
|
|
||||||
{{ output }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="weights-col">
|
||||||
<el-divider />
|
<div class="weight-arrow" v-for="inp in inputs" :key="inp.label">
|
||||||
|
<span class="arrow">→</span>
|
||||||
<div class="formula-bar">
|
<span class="w-tag">×{{ inp.weight }}</span>
|
||||||
<el-alert
|
</div>
|
||||||
type="info"
|
|
||||||
:closable="false"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<div class="formula-content">
|
|
||||||
<div>
|
|
||||||
<strong>总分计算: </strong>
|
|
||||||
<span class="calc-step">
|
|
||||||
(输入A {{ x1 }} × 权重 {{ w1 }}) + (输入B {{ x2 }} × 权重 {{ w2 }}) + 基础分 {{ bias }} =
|
|
||||||
{{ weightedSum.toFixed(1) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="mt-1">
|
|
||||||
<strong>判断结果: </strong>
|
|
||||||
<span class="calc-step">
|
|
||||||
{{ weightedSum.toFixed(1) }} {{ weightedSum > 0 ? '>' : '≤' }} 0
|
|
||||||
→ 输出 {{ output }} ({{ output > 0 ? '激活' : '静默' }})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-alert>
|
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
<div class="neuron-col">
|
||||||
|
<div class="neuron-circle">
|
||||||
|
<div class="n-sym">Σ</div>
|
||||||
|
<div class="n-val">{{ sum }}</div>
|
||||||
|
</div>
|
||||||
|
<span class="bias-tag">偏置 {{ bias }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="act-col">
|
||||||
|
<span class="arrow big">→</span>
|
||||||
|
<div class="act-box">sum > 0 ?</div>
|
||||||
|
<span class="arrow big">→</span>
|
||||||
|
</div>
|
||||||
|
<div class="output-col">
|
||||||
|
<div class="output-node" :class="{ on: output === 1 }">
|
||||||
|
<span class="out-val">{{ output }}</span>
|
||||||
|
<span class="out-lbl">{{ output ? '激活' : '静默' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="caption">
|
||||||
|
① 输入特征 ② 乘以权重(重要性) ③ 求和 + 偏置 ④ 超过阈值就激活输出 1,否则输出 0
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { Right } from '@element-plus/icons-vue'
|
const inputs = [{ label: '特征 x₁', val: 1, weight: 0.6 }, { label: '特征 x₂', val: 0, weight: 0.4 }]
|
||||||
|
const bias = -0.3
|
||||||
const x1 = ref(1)
|
const sum = computed(() => Number((inputs.reduce((s, i) => s + i.val * i.weight, 0) + bias).toFixed(2)))
|
||||||
const x2 = ref(0)
|
const output = computed(() => sum.value > 0 ? 1 : 0)
|
||||||
const w1 = ref(2.0)
|
|
||||||
const w2 = ref(-1.0)
|
|
||||||
const bias = ref(0)
|
|
||||||
|
|
||||||
const weightedSum = computed(() => {
|
|
||||||
return x1.value * w1.value + x2.value * w2.value + bias.value
|
|
||||||
})
|
|
||||||
|
|
||||||
const output = computed(() => {
|
|
||||||
return weightedSum.value > 0 ? 1 : 0
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.perceptron-demo {
|
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1.25rem; margin: 1rem 0; }
|
||||||
margin: 20px 0;
|
.perceptron-layout { display: flex; align-items: center; justify-content: center; gap: 0.5rem; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-radius: 6px; padding: 1.2rem 0.8rem; flex-wrap: wrap; }
|
||||||
}
|
.inputs-col, .weights-col, .neuron-col, .act-col, .output-col { display: flex; flex-direction: column; align-items: center; gap: 1rem; }
|
||||||
|
.input-node { display: flex; flex-direction: column; align-items: center; gap: 0.2rem; }
|
||||||
.card-header h4 {
|
.node-circle { width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; background: var(--vp-c-brand-soft); border: 2px solid var(--vp-c-brand); color: var(--vp-c-brand-1); }
|
||||||
margin: 0;
|
.node-label { font-size: 0.62rem; color: var(--vp-c-text-2); }
|
||||||
font-size: 16px;
|
.weight-arrow { display: flex; align-items: center; gap: 0.3rem; }
|
||||||
font-weight: 600;
|
.arrow { color: var(--vp-c-text-3); font-size: 1.1rem; }
|
||||||
}
|
.arrow.big { font-size: 1.4rem; }
|
||||||
|
.w-tag { font-size: 0.72rem; font-weight: bold; font-family: monospace; background: var(--vp-c-bg-alt); border: 1px solid var(--vp-c-divider); padding: 0.1rem 0.4rem; border-radius: 4px; color: var(--vp-c-brand-1); }
|
||||||
.subtitle {
|
.neuron-circle { width: 64px; height: 64px; border-radius: 50%; border: 3px solid var(--vp-c-brand); background: var(--vp-c-bg-alt); display: flex; flex-direction: column; align-items: center; justify-content: center; }
|
||||||
font-size: 13px;
|
.n-sym { font-size: 1.2rem; font-weight: bold; color: var(--vp-c-brand); }
|
||||||
color: var(--vp-c-text-2);
|
.n-val { font-size: 0.8rem; font-weight: bold; font-family: monospace; }
|
||||||
margin: 4px 0 0;
|
.bias-tag { font-size: 0.62rem; color: var(--vp-c-text-3); padding: 0.1rem 0.4rem; border: 1px dashed var(--vp-c-divider); border-radius: 4px; }
|
||||||
}
|
.act-col { flex-direction: row; }
|
||||||
|
.act-box { font-size: 0.72rem; font-family: monospace; background: var(--vp-c-bg-alt); border: 1px solid var(--vp-c-divider); border-radius: 6px; padding: 0.4rem 0.6rem; }
|
||||||
.neuron-viz-container {
|
.output-node { width: 54px; height: 54px; border-radius: 50%; border: 2px solid var(--vp-c-divider); background: var(--vp-c-bg-alt); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.1rem; }
|
||||||
display: flex;
|
.output-node.on { border-color: #059669; background: rgba(5,150,105,0.08); }
|
||||||
align-items: center;
|
.out-val { font-size: 1.3rem; font-weight: bold; }
|
||||||
justify-content: space-between;
|
.out-lbl { font-size: 0.58rem; color: var(--vp-c-text-2); }
|
||||||
gap: 20px;
|
.caption { font-size: 0.75rem; color: var(--vp-c-text-3); text-align: center; margin-top: 0.6rem; }
|
||||||
padding: 20px 0;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.col {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.weight-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.weight-line {
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--el-color-primary);
|
|
||||||
height: 2px;
|
|
||||||
min-height: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.weight-control {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.neuron-circle {
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 2px solid var(--el-color-primary);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: var(--vp-c-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sum-symbol {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sum-value {
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow-icon {
|
|
||||||
font-size: 24px;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.output-value {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.output-value.active {
|
|
||||||
color: var(--el-color-success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-1 {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-2 {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formula-content code {
|
|
||||||
background-color: var(--vp-c-bg-alt);
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,361 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="rule-learning-demo">
|
<div class="demo-card">
|
||||||
<el-card shadow="hover">
|
<div class="demo-header">
|
||||||
<template #header>
|
<span class="title">关键发展路径总结</span>
|
||||||
<div class="card-header">
|
</div>
|
||||||
<h4>规则 vs 学习</h4>
|
<div class="path-flow">
|
||||||
<p class="subtitle">
|
<div class="path-item" v-for="(item, i) in path" :key="i">
|
||||||
对比:你写阈值 (规则) vs 让模型从数据里"推断"阈值 (学习)
|
<div class="path-card" :style="{ borderLeftColor: item.color }">
|
||||||
</p>
|
<div class="path-top">
|
||||||
|
<span class="path-icon" :style="{ background: item.color }">{{ i + 1 }}</span>
|
||||||
|
<div>
|
||||||
|
<div class="path-name">{{ item.name }}</div>
|
||||||
|
<div class="path-years">{{ item.years }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="path-desc">{{ item.desc }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div v-if="i < path.length - 1" class="path-connector">
|
||||||
|
<svg width="20" height="24" viewBox="0 0 20 24"><path d="M10 0 L10 18 L5 13 M10 18 L15 13" fill="none" stroke="var(--vp-c-text-3)" stroke-width="1.5" stroke-linecap="round" /></svg>
|
||||||
<el-row :gutter="20">
|
</div>
|
||||||
<!-- Rule Based -->
|
</div>
|
||||||
<el-col
|
</div>
|
||||||
:xs="24"
|
|
||||||
:md="12"
|
|
||||||
class="mb-4-xs"
|
|
||||||
>
|
|
||||||
<el-card
|
|
||||||
shadow="never"
|
|
||||||
class="panel-card"
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
<div class="panel-title">
|
|
||||||
规则系统(手写 If/Else)
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="panel-content">
|
|
||||||
<div class="control-row">
|
|
||||||
<span class="label">阈值 size ></span>
|
|
||||||
<el-input-number
|
|
||||||
v-model="ruleThreshold"
|
|
||||||
:min="1"
|
|
||||||
:max="10"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
<span class="text-xs text-gray">(必须明确写出)</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-row mt-4">
|
|
||||||
<span class="label">测试输入 size</span>
|
|
||||||
<el-slider
|
|
||||||
v-model="testInput"
|
|
||||||
:min="1"
|
|
||||||
:max="10"
|
|
||||||
show-input
|
|
||||||
input-size="small"
|
|
||||||
class="flex-1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="result-box mt-4"
|
|
||||||
:class="{
|
|
||||||
good: ruleResult.label === '🍎',
|
|
||||||
bad: ruleResult.label === '🍒'
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div class="result-title">
|
|
||||||
输出
|
|
||||||
</div>
|
|
||||||
<div class="result-value">
|
|
||||||
{{ ruleResult.text }}
|
|
||||||
</div>
|
|
||||||
<div class="result-code">
|
|
||||||
if (size > {{ ruleThreshold }}) return 🍎 else return 🍒
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-alert
|
|
||||||
title="当环境变化(比如'苹果平均变小了'),你需要手动改规则;规则越多,维护成本越高。"
|
|
||||||
type="warning"
|
|
||||||
:closable="false"
|
|
||||||
class="mt-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
|
|
||||||
<!-- Machine Learning -->
|
|
||||||
<el-col
|
|
||||||
:xs="24"
|
|
||||||
:md="12"
|
|
||||||
>
|
|
||||||
<el-card
|
|
||||||
shadow="never"
|
|
||||||
class="panel-card"
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
<div class="panel-title">
|
|
||||||
机器学习(从样本推断边界)
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="panel-content">
|
|
||||||
<div class="control-row">
|
|
||||||
<el-input-number
|
|
||||||
v-model="newSize"
|
|
||||||
:min="1"
|
|
||||||
:max="10"
|
|
||||||
size="small"
|
|
||||||
placeholder="Size"
|
|
||||||
/>
|
|
||||||
<el-select
|
|
||||||
v-model="newLabel"
|
|
||||||
size="small"
|
|
||||||
placeholder="Label"
|
|
||||||
style="width: 120px"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
label="🍒 樱桃"
|
|
||||||
value="🍒"
|
|
||||||
/>
|
|
||||||
<el-option
|
|
||||||
label="🍎 苹果"
|
|
||||||
value="🍎"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
@click="addSample"
|
|
||||||
>
|
|
||||||
添加样本
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="samples-area mt-4">
|
|
||||||
<el-empty
|
|
||||||
v-if="trainingData.length === 0"
|
|
||||||
description="还没有样本:先添加 2-4 个样本再训练"
|
|
||||||
:image-size="40"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="sample-chips"
|
|
||||||
>
|
|
||||||
<el-tag
|
|
||||||
v-for="(p, i) in trainingData"
|
|
||||||
:key="p.id"
|
|
||||||
closable
|
|
||||||
:type="p.label === '🍎' ? 'danger' : 'info'"
|
|
||||||
effect="plain"
|
|
||||||
@close="removeSample(i)"
|
|
||||||
>
|
|
||||||
{{ p.size }} → {{ p.label }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="actions mt-4 flex gap-2">
|
|
||||||
<el-button
|
|
||||||
type="success"
|
|
||||||
:disabled="trainingData.length < 2"
|
|
||||||
@click="train"
|
|
||||||
>
|
|
||||||
训练(推断阈值)
|
|
||||||
</el-button>
|
|
||||||
<el-button @click="resetLearning">
|
|
||||||
重置
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="learnedThreshold !== null"
|
|
||||||
class="learned-result mt-4"
|
|
||||||
>
|
|
||||||
<el-alert
|
|
||||||
type="success"
|
|
||||||
:closable="false"
|
|
||||||
show-icon
|
|
||||||
title="学习完成!"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
模型推断出阈值应为: <strong>{{ learnedThreshold }}</strong>
|
|
||||||
</p>
|
|
||||||
<p class="text-xs">
|
|
||||||
(大于 {{ learnedThreshold }} 是苹果,否则是樱桃)
|
|
||||||
</p>
|
|
||||||
</el-alert>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
const path = [
|
||||||
|
{ name: '理论奠基', years: '1940s-1950s', desc: '图灵测试、达特茅斯会议,符号主义诞生', color: '#3b82f6' },
|
||||||
// Rule Based Logic
|
{ name: '符号主义主导', years: '1960s-1980s', desc: '专家系统兴起与两次 AI 寒冬', color: '#059669' },
|
||||||
const ruleThreshold = ref(5)
|
{ name: '机器学习转型', years: '1990s-2000s', desc: '统计方法取代规则,连接主义复苏', color: '#d97706' },
|
||||||
const testInput = ref(6)
|
{ name: '深度学习革命', years: '2010s', desc: 'AlexNet、AlphaGo、Transformer 架构,连接主义成为主流', color: '#dc2626' },
|
||||||
|
{ name: '大模型时代', years: '2018 至今', desc: 'GPT 系列、多模态融合,通用智能曙光初现', color: '#7c3aed' },
|
||||||
const ruleResult = computed(() => {
|
]
|
||||||
if (testInput.value > ruleThreshold.value) {
|
|
||||||
return { label: '🍎', text: '🍎 苹果' }
|
|
||||||
} else {
|
|
||||||
return { label: '🍒', text: '🍒 樱桃' }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// ML Logic
|
|
||||||
const newSize = ref(5)
|
|
||||||
const newLabel = ref('🍎')
|
|
||||||
const trainingData = ref([
|
|
||||||
{ id: 1, size: 2, label: '🍒' },
|
|
||||||
{ id: 2, size: 8, label: '🍎' }
|
|
||||||
])
|
|
||||||
const learnedThreshold = ref(null)
|
|
||||||
|
|
||||||
const addSample = () => {
|
|
||||||
trainingData.value.push({
|
|
||||||
id: Date.now(),
|
|
||||||
size: newSize.value,
|
|
||||||
label: newLabel.value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeSample = (index) => {
|
|
||||||
trainingData.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetLearning = () => {
|
|
||||||
trainingData.value = []
|
|
||||||
learnedThreshold.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const train = () => {
|
|
||||||
// Simple "training": find the boundary between cherry and apple
|
|
||||||
// Sort data by size
|
|
||||||
const sorted = [...trainingData.value].sort((a, b) => a.size - b.size)
|
|
||||||
|
|
||||||
// Find the first Apple
|
|
||||||
const firstAppleIndex = sorted.findIndex((item) => item.label === '🍎')
|
|
||||||
|
|
||||||
if (firstAppleIndex === -1) {
|
|
||||||
// All cherries
|
|
||||||
learnedThreshold.value = 10
|
|
||||||
} else if (firstAppleIndex === 0) {
|
|
||||||
// All apples
|
|
||||||
learnedThreshold.value = 0
|
|
||||||
} else {
|
|
||||||
// Boundary is between last cherry and first apple
|
|
||||||
const lastCherry = sorted[firstAppleIndex - 1]
|
|
||||||
const firstApple = sorted[firstAppleIndex]
|
|
||||||
learnedThreshold.value = (lastCherry.size + firstApple.size) / 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rule-learning-demo {
|
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1.25rem; margin: 1rem 0; }
|
||||||
margin: 20px 0;
|
.demo-header { margin-bottom: 1rem; }
|
||||||
}
|
.demo-header .title { font-weight: bold; font-size: 1rem; }
|
||||||
|
.path-flow { display: flex; flex-direction: column; align-items: stretch; }
|
||||||
.card-header h4 {
|
.path-item { display: flex; flex-direction: column; align-items: center; }
|
||||||
margin: 0;
|
.path-card { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-left: 4px solid; border-radius: 0 8px 8px 0; padding: 0.8rem 1rem; width: 100%; }
|
||||||
font-size: 16px;
|
.path-top { display: flex; align-items: center; gap: 0.7rem; margin-bottom: 0.35rem; }
|
||||||
font-weight: 600;
|
.path-icon { width: 24px; height: 24px; border-radius: 50%; color: white; font-size: 0.72rem; font-weight: bold; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||||
}
|
.path-name { font-weight: bold; font-size: 0.9rem; color: var(--vp-c-text-1); }
|
||||||
|
.path-years { font-size: 0.72rem; color: var(--vp-c-text-3); font-weight: bold; }
|
||||||
.subtitle {
|
.path-desc { font-size: 0.8rem; color: var(--vp-c-text-2); line-height: 1.4; padding-left: 2.2rem; }
|
||||||
font-size: 13px;
|
.path-connector { display: flex; justify-content: center; padding: 0.15rem 0; }
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
margin: 4px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-title {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-xs {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-gray {
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-1 {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-4 {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-4-xs {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 992px) {
|
|
||||||
.mb-4-xs {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-box {
|
|
||||||
background-color: var(--vp-c-bg-alt);
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-box.good {
|
|
||||||
border-color: var(--el-color-danger);
|
|
||||||
background-color: var(--el-color-danger-light-9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-box.bad {
|
|
||||||
border-color: var(--el-color-primary);
|
|
||||||
background-color: var(--el-color-primary-light-9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-title {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-value {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-code {
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
|
||||||
padding: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sample-chips {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
min-height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap-2 {
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,895 @@
|
|||||||
|
<template>
|
||||||
|
<div class="a2a-detailed-demo">
|
||||||
|
<div class="demo-header">
|
||||||
|
<span class="title">A2A 内部实现</span>
|
||||||
|
<span class="subtitle">对等网络架构的通信细节</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="intro-section">
|
||||||
|
<div class="section-title">A2A 可以做什么?</div>
|
||||||
|
<p class="intro-text">
|
||||||
|
A2A 让多个 AI Agent 可以相互协作,不再是单打独斗。一个复杂任务可以分配给多个专业 Agent,每个 Agent 做自己擅长的事。
|
||||||
|
</p>
|
||||||
|
<div class="popular-uses">
|
||||||
|
<div class="use-item">
|
||||||
|
<div class="use-title">软件开发流水线</div>
|
||||||
|
<div class="use-desc">需求分析 Agent → 代码 Agent → 测试 Agent → 部署 Agent</div>
|
||||||
|
</div>
|
||||||
|
<div class="use-item">
|
||||||
|
<div class="use-title">多厂商 Agent 集成</div>
|
||||||
|
<div class="use-desc">Google、Anthropic、OpenAI 的 Agent 可以相互调用</div>
|
||||||
|
</div>
|
||||||
|
<div class="use-item">
|
||||||
|
<div class="use-title">企业工作流</div>
|
||||||
|
<div class="use-desc">HR Agent、财务 Agent、审批 Agent 协同处理业务流程</div>
|
||||||
|
</div>
|
||||||
|
<div class="use-item">
|
||||||
|
<div class="use-title">智能客服升级</div>
|
||||||
|
<div class="use-desc">接待 Agent → 业务 Agent → 人工 Agent 逐级转接</div>
|
||||||
|
</div>
|
||||||
|
<div class="use-item">
|
||||||
|
<div class="use-title">科研协作</div>
|
||||||
|
<div class="use-desc">文献 Agent → 实验 Agent → 分析 Agent → 报告 Agent</div>
|
||||||
|
</div>
|
||||||
|
<div class="use-item">
|
||||||
|
<div class="use-title">自动化运维</div>
|
||||||
|
<div class="use-desc">监控 Agent → 诊断 Agent → 修复 Agent → 通知 Agent</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="usage-section">
|
||||||
|
<div class="section-title">如何使用 A2A?</div>
|
||||||
|
<p class="usage-intro">
|
||||||
|
A2A 目前还在早期阶段,主要由 Google 推动。如果你想尝试 A2A,需要开发支持 A2A 协议的 Agent 服务。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="usage-steps">
|
||||||
|
<div class="usage-step">
|
||||||
|
<div class="step-num">1</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<div class="step-title">实现 Agent Card 端点</div>
|
||||||
|
<div class="step-desc">
|
||||||
|
在你的 Agent 服务中暴露 <code>/.well-known/agent.json</code>,声明 Agent 的能力和版本
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="usage-step">
|
||||||
|
<div class="step-num">2</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<div class="step-title">实现 A2A API</div>
|
||||||
|
<div class="step-desc">
|
||||||
|
实现 <code>agents/get</code>、<code>tasks/send</code>、<code>tasks/get</code> 等核心 API
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="usage-step">
|
||||||
|
<div class="step-num">3</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<div class="step-title">部署并注册 Agent</div>
|
||||||
|
<div class="step-desc">
|
||||||
|
将 Agent 部署到服务器,并在 Agent 注册表中登记,让其他 Agent 可以发现它
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="usage-note">
|
||||||
|
<div class="note-title">当前状态</div>
|
||||||
|
<div class="note-content">
|
||||||
|
A2A 协议于 2025 年 4 月发布,目前还在快速发展中。Google 提供了参考实现,但生态还在建设中。建议关注 <a href="https://google.github.io/A2A" target="_blank">官方文档</a> 获取最新进展。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-content">
|
||||||
|
<div class="flow-section">
|
||||||
|
<div class="flow-title">
|
||||||
|
|
||||||
|
通信流程(5 步)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flow-steps">
|
||||||
|
<div
|
||||||
|
v-for="(step, index) in a2aFlowSteps"
|
||||||
|
:key="index"
|
||||||
|
class="flow-step-item"
|
||||||
|
>
|
||||||
|
<div class="step-header" @click="toggleStep(index)">
|
||||||
|
<span class="step-num">{{ index + 1 }}</span>
|
||||||
|
<span class="step-name">{{ step.name }}</span>
|
||||||
|
<span class="step-arrow">{{ expandedStep === index ? '▼' : '▶' }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="expandedStep === index" class="step-detail">
|
||||||
|
<div class="step-desc">{{ step.desc }}</div>
|
||||||
|
<div class="step-example">
|
||||||
|
<div class="example-title">{{ step.example.title }}</div>
|
||||||
|
<pre class="example-code"><code>{{ step.example.code }}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details class="tech-details">
|
||||||
|
<summary class="tech-summary">
|
||||||
|
|
||||||
|
<span class="summary-text">技术深究:Agent Card 名片格式</span>
|
||||||
|
</summary>
|
||||||
|
<div class="tech-content">
|
||||||
|
<div class="tech-intro">
|
||||||
|
Agent Card 是一个 JSON 文件,通常放在 <code>/.well-known/agent.json</code> 路径
|
||||||
|
</div>
|
||||||
|
<div class="tech-section">
|
||||||
|
<div class="tech-title">Agent Card 示例</div>
|
||||||
|
<pre class="tech-code"><code>{{ agentCardExample }}</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="tech-note">
|
||||||
|
|
||||||
|
<span>通过 Agent Card,Agent 之间可以相互发现,了解对方的能力和版本,实现互操作</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="tech-details">
|
||||||
|
<summary class="tech-summary">
|
||||||
|
|
||||||
|
<span class="summary-text">技术深究:HTTP + SSE 通信</span>
|
||||||
|
</summary>
|
||||||
|
<div class="tech-content">
|
||||||
|
<div class="tech-section">
|
||||||
|
<div class="tech-title">任务发送(HTTP POST)</div>
|
||||||
|
<pre class="tech-code"><code>{{ taskSendExample }}</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="tech-section">
|
||||||
|
<div class="tech-title">实时推送(SSE)</div>
|
||||||
|
<pre class="tech-code"><code>{{ sseExample }}</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="tech-note">
|
||||||
|
<span>SSE(Server-Sent Events)允许服务器主动推送消息,适合长时任务的状态更新</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="tech-details">
|
||||||
|
<summary class="tech-summary">
|
||||||
|
|
||||||
|
<span class="summary-text">技术深究:A2A 核心 API</span>
|
||||||
|
</summary>
|
||||||
|
<div class="tech-content">
|
||||||
|
<div class="api-list">
|
||||||
|
<div v-for="(api, index) in a2aApis" :key="index" class="api-item">
|
||||||
|
<div class="api-method">
|
||||||
|
<span class="method-badge">{{ api.method }}</span>
|
||||||
|
<span class="method-name">{{ api.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="api-desc">{{ api.desc }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="tech-details">
|
||||||
|
<summary class="tech-summary">
|
||||||
|
|
||||||
|
<span class="summary-text">技术深究:认证机制</span>
|
||||||
|
</summary>
|
||||||
|
<div class="tech-content">
|
||||||
|
<div class="auth-grid">
|
||||||
|
<div class="auth-card">
|
||||||
|
<div class="auth-header">
|
||||||
|
|
||||||
|
<span class="auth-name">API Key</span>
|
||||||
|
</div>
|
||||||
|
<div class="auth-desc">
|
||||||
|
简单的认证方式,适合内部 Agent 通信
|
||||||
|
</div>
|
||||||
|
<pre class="auth-example"><code>{{ apiKeyExample }}</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="auth-card">
|
||||||
|
<div class="auth-header">
|
||||||
|
|
||||||
|
<span class="auth-name">OAuth 2.0</span>
|
||||||
|
</div>
|
||||||
|
<div class="auth-desc">
|
||||||
|
企业级认证,支持令牌刷新和权限控制
|
||||||
|
</div>
|
||||||
|
<pre class="auth-example"><code>{{ oauthExample }}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const expandedStep = ref(0)
|
||||||
|
|
||||||
|
const toggleStep = (index) => {
|
||||||
|
expandedStep.value = expandedStep.value === index ? -1 : index
|
||||||
|
}
|
||||||
|
|
||||||
|
const a2aFlowSteps = [
|
||||||
|
{
|
||||||
|
name: '发现(agents/get)',
|
||||||
|
desc: 'Agent 之间通过 HTTP 请求获取对方的 Agent Card,了解对方的能力和版本',
|
||||||
|
example: {
|
||||||
|
title: 'HTTP 请求',
|
||||||
|
code: `// Agent A 获取 Agent B 的 Agent Card
|
||||||
|
GET /.well-known/agent.json HTTP/1.1
|
||||||
|
Host: agent-b.company.com
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
"name": "Code Agent",
|
||||||
|
"description": "专业代码生成 Agent",
|
||||||
|
"url": "https://agent-b.company.com",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"capabilities": {
|
||||||
|
"streaming": true,
|
||||||
|
"pushNotifications": true
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
{"id": "code-gen", "name": "代码生成"},
|
||||||
|
{"id": "code-review", "name": "代码审查"}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '发任务(tasks/send)',
|
||||||
|
desc: 'Agent A 调用 tasks/send 向 Agent B 发送任务,包含任务ID、描述、上下文等',
|
||||||
|
example: {
|
||||||
|
title: 'HTTP POST',
|
||||||
|
code: `// Agent A 发送任务给 Agent B
|
||||||
|
POST /tasks/send HTTP/1.1
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer xxx
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "task-12345",
|
||||||
|
"sessionId": "session-001",
|
||||||
|
"message": {
|
||||||
|
"role": "user",
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "请帮我写一个登录 API"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "resource",
|
||||||
|
"resource": "file:///specs/login.yaml"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '执行(Task Processing)',
|
||||||
|
desc: 'Agent B 接收任务后,可能调用自己的 LLM 或 MCP 工具来执行任务',
|
||||||
|
example: {
|
||||||
|
title: 'Agent B 内部处理',
|
||||||
|
code: `// Agent B 内部处理流程
|
||||||
|
1. 解析任务请求
|
||||||
|
2. 分析需要的技能(从 skills 中匹配)
|
||||||
|
3. 调用内部 LLM 生成代码
|
||||||
|
4. 可选:通过 MCP 调用外部工具验证代码
|
||||||
|
5. 生成最终结果
|
||||||
|
|
||||||
|
// 整个过程可能耗时较长,通过 SSE 推送进度`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '推送(SSE)',
|
||||||
|
desc: 'Agent B 通过 SSE(Server-Sent Events)实时推送任务进度和中间结果',
|
||||||
|
example: {
|
||||||
|
title: 'SSE 推送',
|
||||||
|
code: `// 服务器持续推送
|
||||||
|
event: taskProgress
|
||||||
|
data: {
|
||||||
|
"taskId": "task-12345",
|
||||||
|
"status": "processing",
|
||||||
|
"progress": 30,
|
||||||
|
"message": "正在生成登录逻辑..."
|
||||||
|
}
|
||||||
|
|
||||||
|
event: taskProgress
|
||||||
|
data: {
|
||||||
|
"taskId": "task-12345",
|
||||||
|
"status": "processing",
|
||||||
|
"progress": 60,
|
||||||
|
"message": "正在生成数据库操作..."
|
||||||
|
}
|
||||||
|
|
||||||
|
event: taskCompleted
|
||||||
|
data: {
|
||||||
|
"taskId": "task-12345",
|
||||||
|
"status": "completed",
|
||||||
|
"result": { ... }
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '返回结果(tasks/get)',
|
||||||
|
desc: '任务完成后,Agent A 可以通过 tasks/get 获取最终结果',
|
||||||
|
example: {
|
||||||
|
title: 'HTTP GET',
|
||||||
|
code: `// Agent A 获取任务结果
|
||||||
|
GET /tasks/task-12345 HTTP/1.1
|
||||||
|
Authorization: Bearer xxx
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
"id": "task-12345",
|
||||||
|
"status": "completed",
|
||||||
|
"result": {
|
||||||
|
"message": {
|
||||||
|
"role": "agent",
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "登录 API 已生成..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"file": {
|
||||||
|
"name": "login.py",
|
||||||
|
"mimeType": "text/plain",
|
||||||
|
"uri": "file:///generated/login.py"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const agentCardExample = `{
|
||||||
|
"name": "代码生成 Agent",
|
||||||
|
"description": "专业的前后端代码生成 Agent",
|
||||||
|
"url": "https://code-agent.company.com",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"capabilities": {
|
||||||
|
"streaming": true,
|
||||||
|
"pushNotifications": true
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
{
|
||||||
|
"id": "frontend",
|
||||||
|
"name": "前端开发",
|
||||||
|
"description": "React/Vue/Angular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "backend",
|
||||||
|
"name": "后端开发",
|
||||||
|
"description": "Node/Python/Go"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"authentication": {
|
||||||
|
"schemes": ["Bearer", "OAuth2"]
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
const taskSendExample = `POST /tasks/send HTTP/1.1
|
||||||
|
Host: agent-b.company.com
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "task-001",
|
||||||
|
"message": {
|
||||||
|
"role": "user",
|
||||||
|
"parts": [{ "type": "text", "text": "写一个登录接口" }]
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
const sseExample = `GET /tasks/task-001/sse HTTP/1.1
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
|
||||||
|
event: progress
|
||||||
|
data: {"status": "processing", "progress": 50}
|
||||||
|
|
||||||
|
event: completed
|
||||||
|
data: {"status": "completed", "result": {...}}`
|
||||||
|
|
||||||
|
const a2aApis = [
|
||||||
|
{ method: 'GET', name: 'agents/get', desc: '获取指定 Agent 的 Agent Card,了解其能力' },
|
||||||
|
{ method: 'POST', name: 'tasks/send', desc: '发送任务给目标 Agent,同步等待结果' },
|
||||||
|
{ method: 'POST', name: 'tasks/sendSubscribe', desc: '发送任务并订阅 SSE 推送,实时获取进度' },
|
||||||
|
{ method: 'GET', name: 'tasks/get', desc: '根据任务 ID 获取任务状态和结果' },
|
||||||
|
{ method: 'GET', name: 'tasks/cancel', desc: '取消正在执行的任务' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const apiKeyExample = `Authorization: Bearer sk-xxxxx
|
||||||
|
# 或
|
||||||
|
Authorization: ApiKey sk-xxxxx`
|
||||||
|
|
||||||
|
const oauthExample = `Authorization: Bearer {access_token}
|
||||||
|
# 支持刷新令牌
|
||||||
|
POST /oauth/token
|
||||||
|
{
|
||||||
|
"grant_type": "refresh_token",
|
||||||
|
"refresh_token": "xxx"
|
||||||
|
}`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.a2a-detailed-demo {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header .icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header .title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header .subtitle {
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-section {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-steps {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-step-item {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-header:hover {
|
||||||
|
background: var(--vp-c-bg-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-num {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #10b981;
|
||||||
|
color: white;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-arrow {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-detail {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-top: 1px solid var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-desc {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-example {
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-title {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-code {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
font-family: var(--vp-font-family-mono);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-details {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.6rem 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-icon {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-content {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-top: 1px solid var(--vp-c-divider);
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-intro {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-intro code {
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
padding: 0.1rem 0.3rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-section {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-title {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-code {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
font-family: var(--vp-font-family-mono);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-note {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.3rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-item {
|
||||||
|
padding: 0.4rem;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-method {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-badge {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
background: #10b981;
|
||||||
|
color: white;
|
||||||
|
padding: 0.1rem 0.3rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: var(--vp-font-family-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-name {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-desc {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-icon {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-name {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-desc {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-example pre {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
padding: 0.4rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
font-family: var(--vp-font-family-mono);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.auth-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-section .section-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-section .intro-text {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popular-uses {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-item {
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 3px solid var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-desc {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.popular-uses {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-section .section-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-intro {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-intro code {
|
||||||
|
background: var(--vp-c-bg-alt);
|
||||||
|
padding: 0.1rem 0.3rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-steps {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-step {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-step .step-num {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
background: var(--vp-c-brand);
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-step .step-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-step .step-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-step .step-desc {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-step .step-desc code {
|
||||||
|
background: var(--vp-c-bg-alt);
|
||||||
|
padding: 0.1rem 0.3rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-note {
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 3px solid var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-content {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-content a {
|
||||||
|
color: var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
<template>
|
||||||
|
<div class="a2a-visual-demo">
|
||||||
|
<div class="section-title">A2A 是什么?</div>
|
||||||
|
|
||||||
|
<div class="intro-text">
|
||||||
|
A2A(Agent-to-Agent Protocol)是 Google 于 2025 年 4 月推出的<strong>Agent 之间相互协作的通信标准</strong>。它让不同厂商、不同框架的 Agent 能够相互发现、分配任务、交换信息,就像给 AI 世界装上了"对讲机"。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-title">核心概念</div>
|
||||||
|
|
||||||
|
<div class="concepts-grid">
|
||||||
|
<div class="concept">
|
||||||
|
<div class="concept-title">Agent Card(Agent 名片)</div>
|
||||||
|
<div class="concept-desc">每个 Agent 公开的元数据,包括能力描述、版本号、端点地址等,相当于人的"名片"</div>
|
||||||
|
</div>
|
||||||
|
<div class="concept">
|
||||||
|
<div class="concept-title">Task(任务)</div>
|
||||||
|
<div class="concept-desc">Agent 之间传递的工作单元,可以包含多轮对话、文件附件等</div>
|
||||||
|
</div>
|
||||||
|
<div class="concept">
|
||||||
|
<div class="concept-title">Message(消息)</div>
|
||||||
|
<div class="concept-desc">Agent 之间的通信内容,支持文本、文件、语音等多模态</div>
|
||||||
|
</div>
|
||||||
|
<div class="concept">
|
||||||
|
<div class="concept-title">SSE(Server-Sent Events)</div>
|
||||||
|
<div class="concept-desc">服务器推送技术,用于实时任务进度更新</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-title">什么时候用 A2A?</div>
|
||||||
|
|
||||||
|
<div class="use-cases">
|
||||||
|
<div class="use-case">
|
||||||
|
<div class="use-case-title">当需要多个 Agent 协作完成复杂任务时</div>
|
||||||
|
<div class="use-case-desc">一个 Agent 负责需求分析,一个负责写代码,一个负责测试,各自发挥专长</div>
|
||||||
|
</div>
|
||||||
|
<div class="use-case">
|
||||||
|
<div class="use-case-title">当需要集成不同厂商的 Agent 时</div>
|
||||||
|
<div class="use-case-desc">Google 的 Agent、Anthropic 的 Agent、OpenAI 的 Agent 需要相互协作</div>
|
||||||
|
</div>
|
||||||
|
<div class="use-case">
|
||||||
|
<div class="use-case-title">当需要任务委托和进度追踪时</div>
|
||||||
|
<div class="use-case-desc">主 Agent 分配任务给专家 Agent,并实时接收进度更新</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-title">如何使用 A2A?</div>
|
||||||
|
|
||||||
|
<div class="usage-steps">
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">1</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<div class="step-title">发布 Agent Card</div>
|
||||||
|
<div class="step-desc">在 /.well-known/agent.json 路径暴露 Agent 的能力描述</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">2</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<div class="step-title">发现 Agent</div>
|
||||||
|
<div class="step-desc">通过 agents/get API 获取其他 Agent 的名片,了解其能力</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">3</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<div class="step-title">发送任务</div>
|
||||||
|
<div class="step-desc">通过 tasks/send API 发送任务,支持 SSE 接收进度更新</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">4</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<div class="step-title">获取结果</div>
|
||||||
|
<div class="step-desc">任务完成后,通过 tasks/get API 获取最终结果</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.a2a-visual-demo {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
padding: 1.25rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-text {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-text strong {
|
||||||
|
color: var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.concepts-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 0.6rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concept {
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concept-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concept-desc {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-cases {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-case {
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 3px solid var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-case-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-case-desc {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-steps {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-num {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
background: var(--vp-c-brand);
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-desc {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.concepts-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,284 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mcp-visual-demo">
|
||||||
|
<div class="section-title">MCP 是什么?</div>
|
||||||
|
|
||||||
|
<div class="intro-text">
|
||||||
|
MCP(Model Context Protocol)是 Anthropic 于 2024 年 11 月推出的<strong>AI 与外部工具连接的统一标准</strong>。它让 AI 应用可以调用外部工具、读取资源数据、使用预定义提示,就像给 AI 装上了"手"和"眼睛"。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-title">三大核心能力</div>
|
||||||
|
|
||||||
|
<div class="能力-table">
|
||||||
|
<div class="table-header">
|
||||||
|
<div class="col-能力">能力</div>
|
||||||
|
<div class="col-英文">英文</div>
|
||||||
|
<div class="col-作用">作用</div>
|
||||||
|
<div class="col-示例">示例</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-row">
|
||||||
|
<div class="col-能力"><strong>工具</strong></div>
|
||||||
|
<div class="col-英文">Tools</div>
|
||||||
|
<div class="col-作用">AI 可以调用的功能</div>
|
||||||
|
<div class="col-示例">查询天气、发送邮件、调用 API</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-row">
|
||||||
|
<div class="col-能力"><strong>资源</strong></div>
|
||||||
|
<div class="col-英文">Resources</div>
|
||||||
|
<div class="col-作用">AI 可以读取的数据</div>
|
||||||
|
<div class="col-示例">文件内容、数据库记录、配置信息</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-row">
|
||||||
|
<div class="col-能力"><strong>提示</strong></div>
|
||||||
|
<div class="col-英文">Prompts</div>
|
||||||
|
<div class="col-作用">预定义的提示模板</div>
|
||||||
|
<div class="col-示例">代码审查模板、写作模板</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-title">什么时候用 MCP?</div>
|
||||||
|
|
||||||
|
<div class="use-cases">
|
||||||
|
<div class="use-case">
|
||||||
|
<div class="use-case-title">当 AI 需要执行实际操作时</div>
|
||||||
|
<div class="use-case-desc">AI 不仅要回答问题,还要真正做事:发送邮件、操作文件、调用第三方 API</div>
|
||||||
|
</div>
|
||||||
|
<div class="use-case">
|
||||||
|
<div class="use-case-title">当 AI 需要访问私有数据时</div>
|
||||||
|
<div class="use-case-desc">读取本地文件、查询数据库、访问企业内部系统</div>
|
||||||
|
</div>
|
||||||
|
<div class="use-case">
|
||||||
|
<div class="use-case-title">当需要标准化工具接入时</div>
|
||||||
|
<div class="use-case-desc">一次开发,多个 AI 应用可用(Claude、Cursor、Windsurf 等)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-title">如何使用 MCP?</div>
|
||||||
|
|
||||||
|
<div class="usage-steps">
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">1</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<div class="step-title">开发 MCP Server</div>
|
||||||
|
<div class="step-desc">按 MCP 规范实现 Server,提供 tools/resources/prompts</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">2</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<div class="step-title">配置 AI 应用连接</div>
|
||||||
|
<div class="step-desc">在 AI 应用中添加 MCP Server 配置(本地或远程)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">3</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<div class="step-title">AI 自动调用</div>
|
||||||
|
<div class="step-desc">AI 根据任务需求,自动发现并调用合适的工具或读取资源</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mcp-visual-demo {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
padding: 1.25rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-text {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-text strong {
|
||||||
|
color: var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.能力-table {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
background: var(--vp-c-bg-alt);
|
||||||
|
border-bottom: 1px solid var(--vp-c-divider);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid var(--vp-c-divider);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-能力 {
|
||||||
|
width: 15%;
|
||||||
|
padding: 0.5rem 0.6rem;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-英文 {
|
||||||
|
width: 18%;
|
||||||
|
padding: 0.5rem 0.6rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-作用 {
|
||||||
|
width: 32%;
|
||||||
|
padding: 0.5rem 0.6rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-示例 {
|
||||||
|
width: 35%;
|
||||||
|
padding: 0.5rem 0.6rem;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-cases {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-case {
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 3px solid var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-case-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-case-desc {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-steps {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-num {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
background: var(--vp-c-brand);
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-desc {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.table-header,
|
||||||
|
.table-row {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-能力,
|
||||||
|
.col-英文,
|
||||||
|
.col-作用,
|
||||||
|
.col-示例 {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.35rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row {
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-能力 {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-英文 {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-作用 {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-示例 {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
<template>
|
||||||
|
<div class="protocol-comparison-demo">
|
||||||
|
<div class="demo-header">
|
||||||
|
<span class="title">MCP vs A2A</span>
|
||||||
|
<span class="subtitle">AI Agent 两大协议的定位差异</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="intro-text">
|
||||||
|
想象你在一个<span class="highlight">大型商场</span>:MCP 就像商场的"统一插座标准",让各种电器(工具)都能插上使用;A2A 就像商场的"内部对讲系统",让不同店铺(Agent)之间可以协作。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="protocol-cards">
|
||||||
|
<div class="protocol-card mcp">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">MCP</span>
|
||||||
|
<span class="card-badge">工具连接</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-fullname">Model Context Protocol</div>
|
||||||
|
<div class="card-desc">
|
||||||
|
AI 与外部工具、数据源的连接协议,让工具开发者写一次代码,所有 AI 应用都能用
|
||||||
|
</div>
|
||||||
|
<div class="card-meta">
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">发起方</span>
|
||||||
|
<span class="meta-value">Anthropic</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">发布时间</span>
|
||||||
|
<span class="meta-value">2024.11</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">架构</span>
|
||||||
|
<span class="meta-value">Client-Server</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">数据格式</span>
|
||||||
|
<span class="meta-value">JSON-RPC 2.0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-analogy">
|
||||||
|
<span class="analogy-label">类比</span>
|
||||||
|
<span class="analogy-text">USB-C 接口 —— 统一各种设备的充电方式</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="protocol-card a2a">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">A2A</span>
|
||||||
|
<span class="card-badge">Agent协作</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-fullname">Agent-to-Agent Protocol</div>
|
||||||
|
<div class="card-desc">
|
||||||
|
Agent 之间的通信协议,让不同厂商、不同框架的 Agent 能够无缝协作
|
||||||
|
</div>
|
||||||
|
<div class="card-meta">
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">发起方</span>
|
||||||
|
<span class="meta-value">Google</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">发布时间</span>
|
||||||
|
<span class="meta-value">2025.04</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">架构</span>
|
||||||
|
<span class="meta-value">Peer-to-Peer</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">数据格式</span>
|
||||||
|
<span class="meta-value">HTTP + JSON</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-analogy">
|
||||||
|
<span class="analogy-label">类比</span>
|
||||||
|
<span class="analogy-text">企业微信 —— 让同事之间可以发任务、聊天</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<strong>核心思想:</strong>MCP 和 A2A 不是竞争关系,而是互补关系。MCP 解决"AI 如何获取外部能力",A2A 解决"多个 AI 如何协作"。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.protocol-comparison-demo {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header .icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header .title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header .subtitle {
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-text {
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-text .highlight {
|
||||||
|
color: var(--vp-c-brand);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-card {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-card.mcp {
|
||||||
|
border-left: 3px solid #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-card.a2a {
|
||||||
|
border-left: 3px solid #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-card.mcp .card-badge {
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-card.a2a .card-badge {
|
||||||
|
background: rgba(16, 185, 129, 0.2);
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-fullname {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-label {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-value {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-analogy {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.4rem;
|
||||||
|
background: var(--vp-c-bg-alt);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analogy-label {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analogy-text {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
background: var(--vp-c-bg-alt);
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box .icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.protocol-cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
<template>
|
||||||
|
<div class="protocol-workflow-demo">
|
||||||
|
<div class="demo-header">
|
||||||
|
<span class="title">MCP + A2A 协作流程</span>
|
||||||
|
<span class="subtitle">两者如何配合完成复杂任务</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="intro-text">
|
||||||
|
想象你要<span class="highlight">装修房子</span>:你需要设计师(主 Agent)出方案,工人(专家 Agent)施工,还要从建材市场(工具)买材料。A2A 让设计师和工人能协作,MCP 让工人能买到材料。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="workflow-diagram">
|
||||||
|
<div class="user-node">
|
||||||
|
<span class="node-label">用户</span>
|
||||||
|
</div>
|
||||||
|
<div class="arrow">→</div>
|
||||||
|
<div class="agent-node main">
|
||||||
|
<span class="node-label">主 Agent</span>
|
||||||
|
<span class="node-role">需求分析</span>
|
||||||
|
</div>
|
||||||
|
<div class="arrow">→</div>
|
||||||
|
<div class="a2a-badge">
|
||||||
|
<span class="badge-text">A2A</span>
|
||||||
|
</div>
|
||||||
|
<div class="agent-node expert">
|
||||||
|
<span class="node-label">专家 Agent</span>
|
||||||
|
<span class="node-role">执行任务</span>
|
||||||
|
</div>
|
||||||
|
<div class="arrow">↔</div>
|
||||||
|
<div class="mcp-badge">
|
||||||
|
<span class="badge-text">MCP</span>
|
||||||
|
</div>
|
||||||
|
<div class="tool-node">
|
||||||
|
<span class="node-label">外部工具</span>
|
||||||
|
<span class="node-role">API/数据库</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flow-steps">
|
||||||
|
<div class="flow-step">
|
||||||
|
<span class="step-num">1</span>
|
||||||
|
<span class="step-text">用户向主 Agent 提出需求(如"分析这个 GitHub 仓库")</span>
|
||||||
|
</div>
|
||||||
|
<div class="flow-step">
|
||||||
|
<span class="step-num">2</span>
|
||||||
|
<span class="step-text">主 Agent 通过 <strong>A2A</strong> 委托专家 Agent 执行任务</span>
|
||||||
|
</div>
|
||||||
|
<div class="flow-step">
|
||||||
|
<span class="step-num">3</span>
|
||||||
|
<span class="step-text">专家 Agent 通过 <strong>MCP</strong> 调用外部工具获取数据</span>
|
||||||
|
</div>
|
||||||
|
<div class="flow-step">
|
||||||
|
<span class="step-num">4</span>
|
||||||
|
<span class="step-text">专家 Agent 通过 <strong>A2A</strong> 返回结果给主 Agent</span>
|
||||||
|
</div>
|
||||||
|
<div class="flow-step">
|
||||||
|
<span class="step-num">5</span>
|
||||||
|
<span class="step-text">主 Agent 汇总结果,回复用户</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-dot a2a"></span>
|
||||||
|
<span class="legend-text"><strong>A2A</strong>:Agent ↔ Agent 通信</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-dot mcp"></span>
|
||||||
|
<span class="legend-text"><strong>MCP</strong>:Agent ↔ 工具 通信</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<strong>核心思想:</strong>A2A 负责 Agent 之间的任务分配和协作,MCP 负责 Agent 与外部工具的交互,两者各司其职,互补协作。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.protocol-workflow-demo {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header .icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header .title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header .subtitle {
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-text {
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-text .highlight {
|
||||||
|
color: var(--vp-c-brand);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-diagram {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-node,
|
||||||
|
.agent-node,
|
||||||
|
.tool-node {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-node {
|
||||||
|
border: 1px dashed var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-node.main {
|
||||||
|
border: 2px solid var(--vp-c-brand);
|
||||||
|
background: rgba(100, 108, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-node.expert {
|
||||||
|
border: 2px solid #10b981;
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-node {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-role {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a2a-badge,
|
||||||
|
.mcp-badge {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.1rem;
|
||||||
|
padding: 0.3rem 0.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a2a-badge {
|
||||||
|
background: rgba(16, 185, 129, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.a2a-badge .badge-icon {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a2a-badge .badge-text {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-badge {
|
||||||
|
background: rgba(59, 130, 246, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-badge .badge-icon {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-badge .badge-text {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-steps {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-step {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-num {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--vp-c-brand);
|
||||||
|
color: white;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-text {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-text strong {
|
||||||
|
color: var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-dot.a2a {
|
||||||
|
background: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-dot.mcp {
|
||||||
|
background: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-text {
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-text strong {
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
background: var(--vp-c-bg-alt);
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box .icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.workflow-diagram {
|
||||||
|
justify-content: flex-start;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-steps {
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-step {
|
||||||
|
padding: 0.3rem 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
<template>
|
||||||
|
<div class="raf-root">
|
||||||
|
<div class="raf-layout">
|
||||||
|
<!-- Left: Client Side -->
|
||||||
|
<div class="raf-left">
|
||||||
|
<div class="raf-header">
|
||||||
|
<span class="raf-icon">💻</span>
|
||||||
|
<span class="raf-title">Client (Browser/App)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="raf-controls">
|
||||||
|
<div class="raf-scenarios">
|
||||||
|
<button
|
||||||
|
v-for="s in scenarios"
|
||||||
|
:key="s.id"
|
||||||
|
:class="['raf-chip', { active: currentScenario.id === s.id }]"
|
||||||
|
@click="selectScenario(s)"
|
||||||
|
:disabled="processing"
|
||||||
|
>
|
||||||
|
{{ s.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="raf-request-box">
|
||||||
|
<div class="raf-http-line">
|
||||||
|
<span :class="['raf-method', currentScenario.method]">{{ currentScenario.method }}</span>
|
||||||
|
<span class="raf-url">{{ currentScenario.url }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="currentScenario.body" class="raf-code-block">
|
||||||
|
{{ JSON.stringify(currentScenario.body, null, 2) }}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="raf-send-btn"
|
||||||
|
@click="sendRequest"
|
||||||
|
:disabled="processing"
|
||||||
|
>
|
||||||
|
{{ processing ? 'Sending...' : 'Send Request' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="raf-response-box" v-if="response">
|
||||||
|
<div class="raf-status-line">
|
||||||
|
<span class="raf-label">Response Status:</span>
|
||||||
|
<span :class="['raf-status-badge', getStatusColor(response.status)]">
|
||||||
|
{{ response.status }} {{ response.statusText }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="raf-code-block response-body">
|
||||||
|
{{ JSON.stringify(response.body, null, 2) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right: Server Side -->
|
||||||
|
<div class="raf-right">
|
||||||
|
<div class="raf-header server-header">
|
||||||
|
<span class="raf-icon">☁️</span>
|
||||||
|
<span class="raf-title">Server (API)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="raf-server-state">
|
||||||
|
<!-- Database View -->
|
||||||
|
<div class="raf-section">
|
||||||
|
<div class="raf-section-title">📦 Database (Users Resource)</div>
|
||||||
|
<div class="raf-db-view">
|
||||||
|
<transition-group name="list">
|
||||||
|
<div v-for="user in db" :key="user.id" class="raf-db-item">
|
||||||
|
<span class="raf-db-id">ID: {{ user.id }}</span>
|
||||||
|
<span class="raf-db-name">{{ user.name }}</span>
|
||||||
|
<span class="raf-db-role">({{ user.role }})</span>
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
<div v-if="db.length === 0" class="raf-empty">No users found</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logs -->
|
||||||
|
<div class="raf-section">
|
||||||
|
<div class="raf-section-title">📜 Server Logs</div>
|
||||||
|
<div class="raf-logs" ref="logsRef">
|
||||||
|
<div v-for="(log, i) in logs" :key="i" class="raf-log-line">
|
||||||
|
<span class="raf-log-time">[{{ log.time }}]</span>
|
||||||
|
<span :class="log.type">{{ log.msg }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, nextTick } from 'vue'
|
||||||
|
|
||||||
|
const processing = ref(false)
|
||||||
|
const response = ref(null)
|
||||||
|
const logs = ref([])
|
||||||
|
const logsRef = ref(null)
|
||||||
|
|
||||||
|
const db = ref([
|
||||||
|
{ id: 1, name: "Alice", role: "admin" },
|
||||||
|
{ id: 2, name: "Bob", role: "user" }
|
||||||
|
])
|
||||||
|
|
||||||
|
const scenarios = [
|
||||||
|
{ id: 'get-all', label: 'GET /users', method: 'GET', url: '/api/users', body: null },
|
||||||
|
{ id: 'get-one', label: 'GET /users/1', method: 'GET', url: '/api/users/1', body: null },
|
||||||
|
{ id: 'create', label: 'POST /users', method: 'POST', url: '/api/users', body: { name: "Charlie", role: "user" } },
|
||||||
|
{ id: 'not-found', label: 'GET /users/99', method: 'GET', url: '/api/users/99', body: null },
|
||||||
|
{ id: 'delete', label: 'DELETE /users/1', method: 'DELETE', url: '/api/users/1', body: null },
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentScenario = ref(scenarios[0])
|
||||||
|
|
||||||
|
function selectScenario(s) {
|
||||||
|
currentScenario.value = s
|
||||||
|
response.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLog(msg, type = 'info') {
|
||||||
|
const now = new Date()
|
||||||
|
const time = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`
|
||||||
|
logs.value.push({ time, msg, type })
|
||||||
|
nextTick(() => {
|
||||||
|
if (logsRef.value) logsRef.value.scrollTop = logsRef.value.scrollHeight
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusColor(status) {
|
||||||
|
if (status >= 200 && status < 300) return 'status-success'
|
||||||
|
if (status >= 400 && status < 500) return 'status-error'
|
||||||
|
return 'status-neutral'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendRequest() {
|
||||||
|
processing.value = true
|
||||||
|
response.value = null
|
||||||
|
addLog(`Received ${currentScenario.value.method} ${currentScenario.value.url}`, 'info')
|
||||||
|
|
||||||
|
await new Promise(r => setTimeout(r, 600)) // Simulate network latency
|
||||||
|
|
||||||
|
const { method, url, body } = currentScenario.value
|
||||||
|
|
||||||
|
// Router Logic Simulation
|
||||||
|
if (method === 'GET' && url === '/api/users') {
|
||||||
|
response.value = { status: 200, statusText: 'OK', body: db.value }
|
||||||
|
addLog('Matched route: GET /users -> listUsers()', 'success')
|
||||||
|
}
|
||||||
|
else if (method === 'GET' && url.match(/\/api\/users\/\d+/)) {
|
||||||
|
const id = parseInt(url.split('/').pop())
|
||||||
|
const user = db.value.find(u => u.id === id)
|
||||||
|
if (user) {
|
||||||
|
response.value = { status: 200, statusText: 'OK', body: user }
|
||||||
|
addLog(`Found user ${id}`, 'success')
|
||||||
|
} else {
|
||||||
|
response.value = { status: 404, statusText: 'Not Found', body: { error: "User not found" } }
|
||||||
|
addLog(`User ${id} not found in DB`, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (method === 'POST' && url === '/api/users') {
|
||||||
|
const newUser = { id: Math.max(0, ...db.value.map(u => u.id)) + 1, ...body }
|
||||||
|
db.value.push(newUser)
|
||||||
|
response.value = { status: 201, statusText: 'Created', body: newUser }
|
||||||
|
addLog(`Created user ${newUser.id}`, 'success')
|
||||||
|
}
|
||||||
|
else if (method === 'DELETE' && url.match(/\/api\/users\/\d+/)) {
|
||||||
|
const id = parseInt(url.split('/').pop())
|
||||||
|
const idx = db.value.findIndex(u => u.id === id)
|
||||||
|
if (idx !== -1) {
|
||||||
|
db.value.splice(idx, 1)
|
||||||
|
response.value = { status: 204, statusText: 'No Content', body: null }
|
||||||
|
addLog(`Deleted user ${id}`, 'success')
|
||||||
|
} else {
|
||||||
|
response.value = { status: 404, statusText: 'Not Found', body: { error: "User not found" } }
|
||||||
|
addLog(`User ${id} not found for deletion`, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processing.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.raf-root {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-family: var(--vp-font-family-mono);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-layout {
|
||||||
|
display: flex;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-left, .raf-right {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1.2rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-left {
|
||||||
|
border-right: 1px solid var(--vp-c-divider);
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-right {
|
||||||
|
background: var(--vp-c-bg-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-scenarios {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-chip {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-chip:hover {
|
||||||
|
border-color: var(--vp-c-brand);
|
||||||
|
color: var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-chip.active {
|
||||||
|
background: var(--vp-c-brand);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-request-box {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-http-line {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
font-family: monospace;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-method {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.raf-method.GET { color: #61affe; }
|
||||||
|
.raf-method.POST { color: #49cc90; }
|
||||||
|
.raf-method.DELETE { color: #f93e3e; }
|
||||||
|
|
||||||
|
.raf-code-block {
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: pre;
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-send-btn {
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--vp-c-brand);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
.raf-send-btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.raf-send-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-response-box {
|
||||||
|
margin-top: auto;
|
||||||
|
border-top: 1px solid var(--vp-c-divider);
|
||||||
|
padding-top: 1rem;
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-status-line {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-status-badge {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.status-success { background: #d1fae5; color: #065f46; }
|
||||||
|
.status-error { background: #fee2e2; color: #991b1b; }
|
||||||
|
.status-neutral { background: #f3f4f6; color: #374151; }
|
||||||
|
|
||||||
|
.raf-db-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-db-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
font-size: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-db-id { color: var(--vp-c-text-3); font-family: monospace; }
|
||||||
|
.raf-db-name { font-weight: bold; }
|
||||||
|
.raf-db-role { color: var(--vp-c-brand); font-size: 0.9em; }
|
||||||
|
|
||||||
|
.raf-logs {
|
||||||
|
height: 180px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-log-line {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raf-log-time { color: #6b7280; flex-shrink: 0; }
|
||||||
|
.info { color: #93c5fd; }
|
||||||
|
.success { color: #86efac; }
|
||||||
|
.error { color: #fca5a5; }
|
||||||
|
|
||||||
|
.raf-section-title {
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
.raf-section:first-child .raf-section-title { margin-top: 0; }
|
||||||
|
|
||||||
|
.raf-empty {
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
font-style: italic;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-enter-active,
|
||||||
|
.list-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.list-enter-from,
|
||||||
|
.list-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.raf-layout { flex-direction: column; }
|
||||||
|
.raf-left { border-right: none; border-bottom: 1px solid var(--vp-c-divider); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,69 +2,75 @@
|
|||||||
<div class="adder-demo">
|
<div class="adder-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<span class="title">加法器:用逻辑门做二进制加法</span>
|
<span class="title">加法器:用逻辑门做二进制加法</span>
|
||||||
<span class="subtitle">点击蓝色位按钮切换 0/1,观察进位如何逐位传递</span>
|
<span class="subtitle">就像手算竖式:从个位往高位算,逢二进一,进位往左传</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 名词解释 -->
|
|
||||||
<div class="legend">
|
|
||||||
<span class="legend-item"><span class="dot a" />A = 被加数</span>
|
|
||||||
<span class="legend-item"><span class="dot b" />B = 加数</span>
|
|
||||||
<span class="legend-item"><span class="dot s" />S = 和(Sum,本位结果)</span>
|
|
||||||
<span class="legend-item"><span class="dot c" />C = 进位(Carry,传给下一位)</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 输入控制 -->
|
|
||||||
<div class="control-panel">
|
<div class="control-panel">
|
||||||
<div class="input-group">
|
<label>
|
||||||
<span class="group-label">A(被加数)</span>
|
<span class="control-label">A(被加数)</span>
|
||||||
<div class="bits">
|
<input v-model.number="inputA" type="number" min="0" max="15" class="num-input" />
|
||||||
<button
|
</label>
|
||||||
v-for="(bit, i) in bitsA"
|
<span class="op">+</span>
|
||||||
:key="'a' + i"
|
<label>
|
||||||
class="bit-btn"
|
<span class="control-label">B(加数)</span>
|
||||||
:class="{ on: bit }"
|
<input v-model.number="inputB" type="number" min="0" max="15" class="num-input" />
|
||||||
@click="toggleBit('A', i)"
|
</label>
|
||||||
>
|
<span class="eq">=</span>
|
||||||
{{ bit }}
|
<span class="result-dec">{{ resultDec }}</span>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
|
||||||
<span class="decimal">= {{ decimalA }}</span>
|
<div class="why-what-box">
|
||||||
|
<p class="why-p">
|
||||||
|
<strong>为啥要看这些?</strong>CPU 只会处理 0 和 1,所以加法要「一位一位」算;每一列(第 0 位、第 1 位…)都需要一个小电路来算「这一位写几、要不要往左进位」。
|
||||||
|
</p>
|
||||||
|
<p class="what-p">
|
||||||
|
<strong>这些词是啥?</strong>
|
||||||
|
<span class="term">半加器</span>:只算这一位的 A+B(最右边没有进位进来)。
|
||||||
|
<span class="term">全加器</span>:算 A+B+上一位的进位。
|
||||||
|
<span class="term">S</span>:这一位写下的数字(0 或 1)。
|
||||||
|
<span class="term">Cout</span>:要不要往左边一位进 1(进就是 1,不进就是 0)。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="example-block">
|
||||||
|
<div class="example-row">
|
||||||
|
<span class="example-label">A(被加数)</span>
|
||||||
|
<span class="example-bits">
|
||||||
|
<span v-for="(b, i) in bitsA" :key="'a'+i" class="bit" :class="{ active: highlightedBit === (3 - i) }">{{ b }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="example-dec">= {{ inputA }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="op-sign">+</div>
|
<div class="example-row">
|
||||||
<div class="input-group">
|
<span class="example-label">B(加数)</span>
|
||||||
<span class="group-label">B(加数)</span>
|
<span class="example-bits">
|
||||||
<div class="bits">
|
<span v-for="(b, i) in bitsB" :key="'b'+i" class="bit" :class="{ active: highlightedBit === (3 - i) }">{{ b }}</span>
|
||||||
<button
|
</span>
|
||||||
v-for="(bit, i) in bitsB"
|
<span class="example-dec">= {{ inputB }}</span>
|
||||||
:key="'b' + i"
|
|
||||||
class="bit-btn"
|
|
||||||
:class="{ on: bit }"
|
|
||||||
@click="toggleBit('B', i)"
|
|
||||||
>
|
|
||||||
{{ bit }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<span class="decimal">= {{ decimalB }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="op-sign">=</div>
|
<div class="example-row result-row">
|
||||||
<div class="result-inline">
|
<span class="example-label">结果</span>
|
||||||
<span class="result-bin">{{ resultBinary }}</span>
|
<span class="example-bits">
|
||||||
<span class="result-dec">(十进制 {{ resultDecimal }})</span>
|
<span v-for="(b, i) in bitsSum" :key="'s'+i" class="bit" :class="{ active: highlightedBit === (3 - i) }">{{ b }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="example-dec">= {{ resultDec }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="bit-legend">
|
||||||
|
<span v-for="i in 4" :key="i" class="bit-legend-item">第{{ 4 - i }}位</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 每位加法器展示 -->
|
<div class="stages-label">逐位计算(从右往左:第 0 位 → 第 3 位,对应上面每一列)</div>
|
||||||
<div class="stages-label">逐位计算过程(从最低位开始)</div>
|
|
||||||
<div class="adder-stages">
|
<div class="adder-stages">
|
||||||
<div
|
<div
|
||||||
v-for="(stage, idx) in stageData"
|
v-for="(stage, idx) in stages"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
class="stage"
|
class="stage"
|
||||||
|
:class="{ 'stage-highlight': highlightedBit === stage.bitPos }"
|
||||||
|
@mouseenter="highlightedBit = stage.bitPos"
|
||||||
|
@mouseleave="highlightedBit = null"
|
||||||
>
|
>
|
||||||
<div class="stage-title">第 {{ stage.bitPos }} 位({{ stage.posName }})</div>
|
<div class="stage-title">第 {{ stage.bitPos }} 位({{ stage.posName }})</div>
|
||||||
|
|
||||||
<div class="stage-content">
|
<div class="stage-content">
|
||||||
<!-- 输入列 -->
|
|
||||||
<div class="io-col inputs-col">
|
<div class="io-col inputs-col">
|
||||||
<div class="io-row">
|
<div class="io-row">
|
||||||
<span class="io-badge a-badge">A</span>
|
<span class="io-badge a-badge">A</span>
|
||||||
@@ -74,55 +80,34 @@
|
|||||||
<span class="io-badge b-badge">B</span>
|
<span class="io-badge b-badge">B</span>
|
||||||
<span class="io-val">{{ stage.b }}</span>
|
<span class="io-val">{{ stage.b }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="stage.carryIn !== null" class="io-row carry-in-row">
|
<div v-if="stage.carryIn !== null" class="io-row">
|
||||||
<span class="io-badge cin-badge">Cin</span>
|
<span class="io-badge cin-badge">Cin</span>
|
||||||
<span class="io-val">{{ stage.carryIn }}</span>
|
<span class="io-val">{{ stage.carryIn }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 全加器框 -->
|
|
||||||
<div class="fa-box">
|
<div class="fa-box">
|
||||||
<div class="fa-label">{{ stage.carryIn !== null ? '全加器' : '半加器' }}</div>
|
<div class="fa-label">{{ stage.carryIn !== null ? '全加器' : '半加器' }}</div>
|
||||||
<div class="fa-hint">{{ stage.carryIn !== null ? 'Full Adder' : 'Half Adder' }}</div>
|
<div class="fa-hint">{{ stage.carryIn !== null ? 'A+B+进位' : '只算 A+B' }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 输出列 -->
|
|
||||||
<div class="io-col outputs-col">
|
<div class="io-col outputs-col">
|
||||||
<div class="io-row">
|
<div class="io-row">
|
||||||
<span class="io-badge s-badge">S</span>
|
<span class="io-badge s-badge" :title="'S = 这一位写下的数'">S</span>
|
||||||
<span class="io-val sum-val">{{ stage.sum }}</span>
|
<span class="io-val sum-val">{{ stage.sum }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="io-row">
|
<div class="io-row">
|
||||||
<span class="io-badge cout-badge">Cout</span>
|
<span class="io-badge cout-badge" :title="'Cout = 往左进 0 还是 1'">Cout</span>
|
||||||
<span class="io-val carry-val">{{ stage.carryOut }}</span>
|
<span class="io-val carry-val">{{ stage.carryOut }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="idx < stages.length - 1" class="carry-hint" :class="{ 'no-carry': !stage.carryOut }">
|
||||||
<!-- 进位传递提示 -->
|
{{ stage.carryOut ? `进位 ${stage.carryOut} 传给第 ${stage.bitPos + 1} 位 →` : '无进位' }}
|
||||||
<div v-if="idx < stageData.length - 1 && stage.carryOut" class="carry-hint">
|
|
||||||
进位 {{ stage.carryOut }} 传给第 {{ stage.bitPos + 1 }} 位 →
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="idx < stageData.length - 1" class="carry-hint no-carry">
|
|
||||||
无进位
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 结果 -->
|
|
||||||
<div class="result-bar">
|
|
||||||
<div class="result-row">
|
|
||||||
<span class="result-label">二进制结果</span>
|
|
||||||
<span class="result-bits">{{ resultBinary }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="result-row">
|
|
||||||
<span class="result-label">十进制验证</span>
|
|
||||||
<span class="result-eq">{{ decimalA }} + {{ decimalB }} = {{ resultDecimal }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<strong>核心思想:</strong>每位全加器接收 A、B 和上一位的进位(Cin),输出本位的和(S)与向上传递的进位(Cout)——和我们手算竖式加法"逢二进一"完全一致。
|
<strong>核心思想:</strong>每位全加器接收 A、B 和上一位的进位(Cin),输出本位的和(S)与传给下一位的进位(Cout),和手算竖式「逢二进一」一致。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -130,57 +115,66 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
const bitsA = ref([0, 0, 1, 1])
|
const POS_NAMES = ['最低位', '次低位', '次高位', '最高位']
|
||||||
const bitsB = ref([0, 0, 1, 0])
|
|
||||||
|
|
||||||
const toggleBit = (arr, i) => {
|
function clamp(n) {
|
||||||
if (arr === 'A') {
|
const v = Number(n)
|
||||||
bitsA.value[i] = bitsA.value[i] ? 0 : 1
|
if (Number.isNaN(v)) return 0
|
||||||
} else {
|
return Math.max(0, Math.min(15, Math.floor(v)))
|
||||||
bitsB.value[i] = bitsB.value[i] ? 0 : 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const decimalA = computed(() =>
|
const inputA = ref(3)
|
||||||
bitsA.value.reduce((acc, bit, i) => acc + bit * Math.pow(2, 3 - i), 0)
|
const inputB = ref(2)
|
||||||
)
|
const highlightedBit = ref(null)
|
||||||
|
|
||||||
const decimalB = computed(() =>
|
const clampedA = computed(() => clamp(inputA.value))
|
||||||
bitsB.value.reduce((acc, bit, i) => acc + bit * Math.pow(2, 3 - i), 0)
|
const clampedB = computed(() => clamp(inputB.value))
|
||||||
)
|
|
||||||
|
|
||||||
const stageData = computed(() => {
|
const bitsA = computed(() => (clampedA.value >>> 0).toString(2).padStart(4, '0').split(''))
|
||||||
const stages = []
|
const bitsB = computed(() => (clampedB.value >>> 0).toString(2).padStart(4, '0').split(''))
|
||||||
let carry = 0
|
|
||||||
const posNames = ['最低位', '次低位', '次高位', '最高位']
|
const stages = computed(() => {
|
||||||
for (let i = 3; i >= 0; i--) {
|
const A = clampedA.value
|
||||||
const a = bitsA.value[i]
|
const B = clampedB.value
|
||||||
const b = bitsB.value[i]
|
const result = []
|
||||||
const total = a + b + carry
|
let carryIn = null
|
||||||
const sum = total % 2
|
for (let i = 0; i < 4; i++) {
|
||||||
const carryOut = total >= 2 ? 1 : 0
|
const a = (A >> i) & 1
|
||||||
stages.push({
|
const b = (B >> i) & 1
|
||||||
bitPos: 3 - i,
|
let sum, carryOut
|
||||||
posName: posNames[3 - i],
|
if (carryIn === null) {
|
||||||
|
sum = a ^ b
|
||||||
|
carryOut = a & b
|
||||||
|
} else {
|
||||||
|
sum = (a ^ b) ^ carryIn
|
||||||
|
carryOut = (a & b) | (carryIn & (a ^ b))
|
||||||
|
}
|
||||||
|
result.push({
|
||||||
|
bitPos: i,
|
||||||
|
posName: POS_NAMES[i],
|
||||||
a,
|
a,
|
||||||
b,
|
b,
|
||||||
carryIn: stages.length > 0 ? carry : null,
|
carryIn: carryIn === null ? null : carryIn,
|
||||||
sum,
|
sum,
|
||||||
carryOut
|
carryOut
|
||||||
})
|
})
|
||||||
carry = carryOut
|
carryIn = carryOut
|
||||||
}
|
}
|
||||||
return stages
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
const sumBits = computed(() => stageData.value.map((s) => s.sum).reverse())
|
const bitsSum = computed(() => {
|
||||||
|
const S = stages.value.reduce((acc, s, i) => acc + (s.sum << i), 0)
|
||||||
const resultBinary = computed(() => {
|
return (S >>> 0).toString(2).padStart(4, '0').split('')
|
||||||
const lastCarry = stageData.value[stageData.value.length - 1]?.carryOut || 0
|
|
||||||
return (lastCarry ? lastCarry.toString() : '') + sumBits.value.join('')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const resultDecimal = computed(() => decimalA.value + decimalB.value)
|
const fourBitResult = computed(() =>
|
||||||
|
stages.value.reduce((acc, s, i) => acc + (s.sum << i), 0)
|
||||||
|
)
|
||||||
|
const overflow = computed(() => clampedA.value + clampedB.value > 15)
|
||||||
|
const resultDec = computed(() =>
|
||||||
|
overflow.value ? `${fourBitResult.value}(4 位溢出)` : String(fourBitResult.value)
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -193,147 +187,167 @@ const resultDecimal = computed(() => decimalA.value + decimalB.value)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.demo-header {
|
.demo-header {
|
||||||
display: flex;
|
margin-bottom: 0.35rem;
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 0.65rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header .title {
|
.demo-header .title {
|
||||||
|
display: block;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header .subtitle {
|
.demo-header .subtitle {
|
||||||
color: var(--vp-c-text-2);
|
display: block;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 名词解释 */
|
|
||||||
.legend {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.8rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 0.7rem;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
background: var(--vp-c-bg);
|
font-weight: normal;
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.5rem 0.7rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot {
|
|
||||||
display: inline-block;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot.a { background: var(--vp-c-brand); }
|
|
||||||
.dot.b { background: #8b5cf6; }
|
|
||||||
.dot.s { background: var(--vp-c-success, #16a34a); }
|
|
||||||
.dot.c { background: #d97706; }
|
|
||||||
|
|
||||||
/* 控制面板 */
|
|
||||||
.control-panel {
|
.control-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
gap: 0.6rem;
|
|
||||||
padding: 0.55rem 0.75rem;
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--vp-c-bg);
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
|
||||||
|
|
||||||
.input-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-label {
|
.control-panel label {
|
||||||
font-size: 0.8rem;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.num-input {
|
||||||
|
width: 3rem;
|
||||||
|
padding: 0.25rem 0.35rem;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel .op,
|
||||||
|
.control-panel .eq {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bits {
|
.control-panel .result-dec {
|
||||||
display: flex;
|
|
||||||
gap: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bit-btn {
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
background: var(--vp-c-bg-alt);
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
transition: all 0.2s;
|
color: var(--vp-c-brand-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bit-btn.on {
|
.why-what-box {
|
||||||
background: var(--vp-c-brand);
|
background: var(--vp-c-bg-alt);
|
||||||
color: white;
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-color: var(--vp-c-brand);
|
border-radius: 6px;
|
||||||
|
padding: 0.65rem 0.85rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
.decimal {
|
.why-what-box .why-p {
|
||||||
font-size: 0.82rem;
|
margin: 0 0 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.why-what-box .what-p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.why-what-box .term {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-block {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bit-legend {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-left: 6rem;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bit-legend-item {
|
||||||
|
min-width: 1.2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-row .example-bits {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bit {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 1.2em;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.1rem 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bit.active {
|
||||||
|
background: var(--vp-c-brand-2);
|
||||||
|
color: var(--vp-c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage.stage-highlight {
|
||||||
|
outline: 2px solid var(--vp-c-brand-1);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-label {
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
min-width: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-bits {
|
||||||
|
font-family: monospace;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-dec {
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-sign {
|
.result-row .example-bits {
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--vp-c-brand);
|
color: var(--vp-c-brand-1);
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-inline {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-bin {
|
|
||||||
font-family: monospace;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--vp-c-brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-dec {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 阶段 */
|
|
||||||
.stages-label {
|
.stages-label {
|
||||||
font-size: 0.82rem;
|
font-size: 0.85rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 0.4rem;
|
margin: 0.75rem 0 0.4rem;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.adder-stages {
|
.adder-stages {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
gap: 0.45rem;
|
gap: 0.5rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,7 +358,7 @@ const resultDecimal = computed(() => decimalA.value + decimalB.value)
|
|||||||
padding: 0.55rem;
|
padding: 0.55rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.4rem;
|
gap: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-title {
|
.stage-title {
|
||||||
@@ -401,16 +415,11 @@ const resultDecimal = computed(() => decimalA.value + decimalB.value)
|
|||||||
.sum-val { color: var(--vp-c-success, #16a34a); }
|
.sum-val { color: var(--vp-c-success, #16a34a); }
|
||||||
.carry-val { color: #d97706; }
|
.carry-val { color: #d97706; }
|
||||||
|
|
||||||
/* 全加器盒子 */
|
|
||||||
.fa-box {
|
.fa-box {
|
||||||
background: var(--vp-c-bg-alt);
|
background: var(--vp-c-bg-alt);
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 0.3rem 0.35rem;
|
padding: 0.3rem 0.35rem;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.1rem;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,9 +431,9 @@ const resultDecimal = computed(() => decimalA.value + decimalB.value)
|
|||||||
.fa-hint {
|
.fa-hint {
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
color: var(--vp-c-text-3);
|
color: var(--vp-c-text-3);
|
||||||
|
margin-top: 0.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 进位提示 */
|
|
||||||
.carry-hint {
|
.carry-hint {
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
color: #d97706;
|
color: #d97706;
|
||||||
@@ -436,48 +445,14 @@ const resultDecimal = computed(() => decimalA.value + decimalB.value)
|
|||||||
color: var(--vp-c-text-3);
|
color: var(--vp-c-text-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 结果栏 */
|
|
||||||
.result-bar {
|
|
||||||
background: var(--vp-c-bg-alt);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.55rem 0.75rem;
|
|
||||||
display: flex;
|
|
||||||
gap: 1.5rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.4rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-label {
|
|
||||||
font-size: 0.82rem;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-bits {
|
|
||||||
font-family: monospace;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--vp-c-brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-eq {
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--vp-c-success, #16a34a);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* info box */
|
|
||||||
.info-box {
|
.info-box {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
background: var(--vp-c-bg-alt);
|
background: var(--vp-c-bg-alt);
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
display: flex;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box strong {
|
.info-box strong {
|
||||||
|
|||||||
+44
-244
@@ -2,48 +2,13 @@
|
|||||||
<div class="logic-gate-demo">
|
<div class="logic-gate-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<span class="title">逻辑门:用开关做运算</span>
|
<span class="title">逻辑门:用开关做运算</span>
|
||||||
<span class="subtitle">切换输入 A / B,同屏观察四种门的输出</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-panel">
|
<p class="intro">
|
||||||
<span class="panel-hint">点按钮切换 0 / 1,右侧四个门同步更新:</span>
|
输入 A、B 只能是 0 或 1;四种门按不同规则输出一个 0 或 1。下面表格列出所有 4 种输入组合的结果。
|
||||||
<div class="input-item">
|
</p>
|
||||||
<span class="input-label">输入 A</span>
|
|
||||||
<button class="input-btn" :class="{ on: inputA }" @click="inputA = !inputA">
|
|
||||||
{{ inputA ? '1' : '0' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="input-item">
|
|
||||||
<span class="input-label">输入 B</span>
|
|
||||||
<button class="input-btn" :class="{ on: inputB }" @click="inputB = !inputB">
|
|
||||||
{{ inputB ? '1' : '0' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<span class="current-state">当前:A={{ inputA ? 1 : 0 }},B={{ inputB ? 1 : 0 }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gate-grid">
|
|
||||||
<div v-for="gate in gates" :key="gate.name" class="gate-card">
|
|
||||||
<div class="gate-top">
|
|
||||||
<span class="gate-name">{{ gate.name }}</span>
|
|
||||||
<span class="gate-formula">{{ gate.formula }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="gate-analogy">{{ gate.analogy }}</div>
|
|
||||||
<div class="gate-output-row">
|
|
||||||
<span class="output-label">输出</span>
|
|
||||||
<span
|
|
||||||
class="output-value"
|
|
||||||
:class="{ on: gateOutput(gate.name, inputA, inputB) }"
|
|
||||||
>
|
|
||||||
{{ gateOutput(gate.name, inputA, inputB) }}
|
|
||||||
</span>
|
|
||||||
<span class="output-hint">{{ gateOutput(gate.name, inputA, inputB) ? '(真 / 导通)' : '(假 / 断开)' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="truth-section">
|
<div class="truth-section">
|
||||||
<div class="table-title">四种门真值表对照(高亮行 = 当前输入)</div>
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -56,65 +21,37 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr v-for="row in truthRows" :key="`${row.a}-${row.b}`">
|
||||||
v-for="row in truthRows"
|
|
||||||
:key="`${row.a}-${row.b}`"
|
|
||||||
:class="{
|
|
||||||
highlight:
|
|
||||||
row.a === (inputA ? 1 : 0) && row.b === (inputB ? 1 : 0)
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<td>{{ row.a }}</td>
|
<td>{{ row.a }}</td>
|
||||||
<td>{{ row.b }}</td>
|
<td>{{ row.b }}</td>
|
||||||
<td>{{ gateOutput('AND', !!row.a, !!row.b) }}</td>
|
<td>{{ row.and }}</td>
|
||||||
<td>{{ gateOutput('OR', !!row.a, !!row.b) }}</td>
|
<td>{{ row.or }}</td>
|
||||||
<td>{{ gateOutput('NOT', !!row.a, !!row.b) }}</td>
|
<td>{{ row.not }}</td>
|
||||||
<td>{{ gateOutput('XOR', !!row.a, !!row.b) }}</td>
|
<td>{{ row.xor }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<ul class="col-meaning">
|
||||||
|
<li><strong>AND</strong>:两个都是 1 才输出 1(像串联:都通才通)</li>
|
||||||
|
<li><strong>OR</strong>:有一个 1 就输出 1(像并联:一通就通)</li>
|
||||||
|
<li><strong>NOT(A)</strong>:对 A 取反,0→1、1→0</li>
|
||||||
|
<li><strong>XOR</strong>:两个不同输出 1,相同输出 0</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<strong>核心思想:</strong>逻辑门用晶体管的"开关"组合实现基本运算——AND 像串联、OR 像并联、NOT 取反、XOR 判异。所有复杂计算都由这四种基础操作构建而来。
|
<strong>核心思想:</strong>逻辑门用晶体管的“开关”组合实现这四种运算,复杂计算都由它们组合而成。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
const inputA = ref(false)
|
|
||||||
const inputB = ref(false)
|
|
||||||
|
|
||||||
const gates = [
|
|
||||||
{ name: 'AND', formula: 'A && B', analogy: '串联:都为 1 才输出 1' },
|
|
||||||
{ name: 'OR', formula: 'A || B', analogy: '并联:任一为 1 就输出 1' },
|
|
||||||
{ name: 'NOT', formula: '!A', analogy: '取反:0→1,1→0' },
|
|
||||||
{ name: 'XOR', formula: 'A ⊕ B', analogy: '判异:不同为 1,相同为 0' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const truthRows = [
|
const truthRows = [
|
||||||
{ a: 0, b: 0 },
|
{ a: 0, b: 0, and: 0, or: 0, not: 1, xor: 0 },
|
||||||
{ a: 0, b: 1 },
|
{ a: 0, b: 1, and: 0, or: 1, not: 1, xor: 1 },
|
||||||
{ a: 1, b: 0 },
|
{ a: 1, b: 0, and: 0, or: 1, not: 0, xor: 1 },
|
||||||
{ a: 1, b: 1 }
|
{ a: 1, b: 1, and: 1, or: 1, not: 0, xor: 0 }
|
||||||
]
|
]
|
||||||
|
|
||||||
const gateOutput = (name, a, b) => {
|
|
||||||
switch (name) {
|
|
||||||
case 'AND':
|
|
||||||
return a && b ? 1 : 0
|
|
||||||
case 'OR':
|
|
||||||
return a || b ? 1 : 0
|
|
||||||
case 'NOT':
|
|
||||||
return a ? 0 : 1
|
|
||||||
case 'XOR':
|
|
||||||
return a !== b ? 1 : 0
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -127,11 +64,7 @@ const gateOutput = (name, a, b) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.demo-header {
|
.demo-header {
|
||||||
display: flex;
|
margin-bottom: 0.5rem;
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header .title {
|
.demo-header .title {
|
||||||
@@ -139,169 +72,29 @@ const gateOutput = (name, a, b) => {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header .subtitle {
|
.intro {
|
||||||
|
font-size: 0.9rem;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
font-size: 0.82rem;
|
margin: 0 0 0.75rem;
|
||||||
margin-left: 0.5rem;
|
line-height: 1.5;
|
||||||
}
|
|
||||||
|
|
||||||
.control-panel {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.6rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 0.5rem 0.65rem;
|
|
||||||
background: var(--vp-c-bg);
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-hint {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-state {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
margin-left: auto;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-item {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
background: var(--vp-c-bg-alt);
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 0.2rem 0.5rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-label {
|
|
||||||
font-size: 0.82rem;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-btn {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
background: var(--vp-c-bg-alt);
|
|
||||||
border-radius: 50%;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: bold;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-btn.on {
|
|
||||||
background: var(--vp-c-brand);
|
|
||||||
color: white;
|
|
||||||
border-color: var(--vp-c-brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gate-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gate-card {
|
|
||||||
background: var(--vp-c-bg);
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.55rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gate-top {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gate-name {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--vp-c-brand-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gate-formula {
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
color: var(--vp-c-text-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gate-analogy {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gate-output-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
margin-top: 0.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.output-label {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: var(--vp-c-text-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.output-hint {
|
|
||||||
font-size: 0.68rem;
|
|
||||||
color: var(--vp-c-text-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.output-value {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
border-radius: 4px;
|
|
||||||
background: var(--vp-c-bg-alt);
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.output-value.on {
|
|
||||||
background: var(--vp-c-success);
|
|
||||||
color: white;
|
|
||||||
border-color: var(--vp-c-success);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.truth-section {
|
.truth-section {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-title {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
font-size: 0.8rem;
|
font-size: 0.88rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
th,
|
th,
|
||||||
td {
|
td {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
padding: 0;
|
padding: 0.4rem 0.5rem;
|
||||||
height: 2rem;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
@@ -312,29 +105,36 @@ th {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.highlight {
|
.col-meaning {
|
||||||
background: var(--vp-c-brand-soft);
|
margin: 0;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-meaning li {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-meaning strong {
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box {
|
.info-box {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
background: var(--vp-c-bg-alt);
|
background: var(--vp-c-bg-alt);
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
display: flex;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box strong {
|
.info-box strong {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.gate-grid {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+119
-296
@@ -1,13 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="register-demo">
|
<div class="register-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<span class="title">寄存器:存储状态的功能单元</span>
|
<span class="title">寄存器:能「记住」一个 0 或 1 的小单元</span>
|
||||||
<span class="subtitle">改变输入不会改变存储值——必须主动"写入"</span>
|
<span class="subtitle">只有点「写入」时才会把当前输入记下来,平时改输入不会影响已存的值</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="why-what-box">
|
||||||
|
<p class="why-p">
|
||||||
|
<strong>为啥要看这个?</strong>CPU 算到一半要暂时「记住」中间结果,寄存器就是干这个的。它和「直接连线」不同:改输入不会立刻改变里面存的东西,必须主动点一次「写入」才会更新。
|
||||||
|
</p>
|
||||||
|
<p class="what-p">
|
||||||
|
<strong>这些词是啥?</strong>
|
||||||
|
<span class="term">输入</span>:你想写进去的 0 或 1。
|
||||||
|
<span class="term">写入</span>:点一下,把当前输入「锁进」寄存器。
|
||||||
|
<span class="term">存储值</span>:寄存器里现在记着的数(只有写入时才会变)。
|
||||||
|
<span class="term">输出</span>:从寄存器读出来的数,和存储值一样。
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-panel">
|
<div class="control-panel">
|
||||||
<div class="control-left">
|
<label class="ctrl-group">
|
||||||
<span class="ctrl-label">输入值</span>
|
<span class="ctrl-label">输入</span>
|
||||||
<button
|
<button
|
||||||
class="input-toggle"
|
class="input-toggle"
|
||||||
:class="{ on: inputData === 1 }"
|
:class="{ on: inputData === 1 }"
|
||||||
@@ -15,87 +28,33 @@
|
|||||||
>
|
>
|
||||||
{{ inputData }}
|
{{ inputData }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</label>
|
||||||
<button class="write-btn" :class="{ flash: isWriting }" @click="writeOnce">
|
<button class="write-btn" :class="{ flash: isWriting }" @click="writeOnce">
|
||||||
写入寄存器 →
|
写入
|
||||||
</button>
|
</button>
|
||||||
<div class="control-right">
|
<label class="ctrl-group">
|
||||||
<span class="chip">存储值:{{ storedData }}</span>
|
<span class="ctrl-label">存储</span>
|
||||||
<span class="chip" :class="{ chip_on: storedData === 1 }">输出:{{ storedData === 1 ? '1 ✓' : '0' }}</span>
|
<span class="stored-val" :class="{ on: storedData === 1 }">{{ storedData }}</span>
|
||||||
</div>
|
</label>
|
||||||
|
<span class="ctrl-group">
|
||||||
|
<span class="ctrl-label">输出</span>
|
||||||
|
<span class="output-val" :class="{ on: storedData === 1 }">{{ storedData }}</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="demo-content">
|
<div class="visualization-area">
|
||||||
<div class="flow-diagram">
|
<div class="flow-strip">
|
||||||
<div class="flow-node input-node">
|
<span class="flow-item">输入 {{ inputData }}</span>
|
||||||
<div class="node-label">输入(Data)</div>
|
<span class="flow-arrow" :class="{ active: isWriting }">{{ isWriting ? '写入中 →' : '— 点「写入」才更新 →' }}</span>
|
||||||
<div class="node-value" :class="{ on: inputData === 1 }">{{ inputData }}</div>
|
<span class="flow-item flow-store" :class="{ flash: isWriting }">存 {{ storedData }}</span>
|
||||||
<div class="node-hint">点左侧按钮切换</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flow-arrow" :class="{ active: isWriting }">
|
|
||||||
<div class="arrow-line" />
|
|
||||||
<div class="arrow-tag">{{ isWriting ? '写入中...' : '写入触发' }}</div>
|
|
||||||
<div class="arrow-head">→</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flow-node register-node" :class="{ flashing: isWriting }">
|
|
||||||
<div class="node-label">D 触发器(寄存器核心)</div>
|
|
||||||
<div class="node-value" :class="{ on: storedData === 1 }">{{ storedData }}</div>
|
|
||||||
<div class="node-hint">{{ isWriting ? '正在锁存...' : '保持 (Hold)' }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flow-arrow" :class="{ active: storedData === 1 }">
|
|
||||||
<div class="arrow-line" />
|
|
||||||
<div class="arrow-tag">输出</div>
|
|
||||||
<div class="arrow-head">→</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flow-node output-node" :class="{ on: storedData === 1 }">
|
|
||||||
<div class="node-label">输出(Output)</div>
|
|
||||||
<div class="bulb">{{ storedData === 1 ? '💡' : '🌑' }}</div>
|
|
||||||
<div class="node-hint">{{ storedData === 1 ? '亮(1)' : '灭(0)' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="state-table">
|
|
||||||
<div class="table-title">操作步骤说明</div>
|
|
||||||
<div class="state-rows">
|
|
||||||
<div class="state-row">
|
|
||||||
<span class="step-num">①</span>
|
|
||||||
<span>点"输入值"按钮切换输入(0/1)</span>
|
|
||||||
</div>
|
|
||||||
<div class="state-row">
|
|
||||||
<span class="step-num">②</span>
|
|
||||||
<span>此时存储值<strong>不变</strong>——这就是寄存器的意义</span>
|
|
||||||
</div>
|
|
||||||
<div class="state-row">
|
|
||||||
<span class="step-num">③</span>
|
|
||||||
<span>点"写入寄存器",输入值才被锁入</span>
|
|
||||||
</div>
|
|
||||||
<div class="state-row">
|
|
||||||
<span class="step-num">④</span>
|
|
||||||
<span>写入后再改输入,存储值依然<strong>保持</strong>不变</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="diff-display">
|
|
||||||
<div class="diff-item">
|
|
||||||
<div class="diff-label">当前输入</div>
|
|
||||||
<div class="diff-value" :class="{ on: inputData === 1 }">{{ inputData }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="diff-sep">≠</div>
|
|
||||||
<div class="diff-item">
|
|
||||||
<div class="diff-label">存储值</div>
|
|
||||||
<div class="diff-value" :class="{ on: storedData === 1 }">{{ storedData }}</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="inputData === storedData" class="diff-same">(当前相同)</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="flow-hint">
|
||||||
|
{{ inputData !== storedData ? '输入和存储不一样:点「写入」会把当前输入记进去。' : '输入和存储已一致。' }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<strong>核心思想:</strong>寄存器只在"写入"信号触发时更新,其余时刻持续锁定当前值。这就是 CPU 能在计算过程中稳定保存中间结果的原因。
|
<strong>核心思想:</strong>寄存器只在「写入」那一刻更新,其余时间一直保持原来的值,所以 CPU 能稳定保存中间结果。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -126,41 +85,62 @@ const writeOnce = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.demo-header {
|
.demo-header {
|
||||||
display: flex;
|
margin-bottom: 0.5rem;
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 0.8rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header .title {
|
.demo-header .title {
|
||||||
|
display: block;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header .subtitle {
|
.demo-header .subtitle {
|
||||||
color: var(--vp-c-text-2);
|
display: block;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
margin-left: 0.5rem;
|
color: var(--vp-c-text-2);
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.why-what-box {
|
||||||
|
background: var(--vp-c-bg-alt);
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.65rem 0.85rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.why-what-box .why-p {
|
||||||
|
margin: 0 0 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.why-what-box .what-p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.why-what-box .term {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- control panel ---- */
|
|
||||||
.control-panel {
|
.control-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.7rem;
|
gap: 0.6rem;
|
||||||
padding: 0.6rem 0.75rem;
|
padding: 0.5rem 0.7rem;
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
margin-bottom: 0.8rem;
|
margin-bottom: 0.75rem;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-left {
|
.ctrl-group {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.45rem;
|
gap: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ctrl-label {
|
.ctrl-label {
|
||||||
@@ -169,9 +149,9 @@ const writeOnce = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input-toggle {
|
.input-toggle {
|
||||||
width: 2rem;
|
width: 2.2rem;
|
||||||
height: 2rem;
|
height: 2.2rem;
|
||||||
border-radius: 50%;
|
border-radius: 6px;
|
||||||
border: 2px solid var(--vp-c-divider);
|
border: 2px solid var(--vp-c-divider);
|
||||||
background: var(--vp-c-bg-alt);
|
background: var(--vp-c-bg-alt);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -187,260 +167,102 @@ const writeOnce = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.write-btn {
|
.write-btn {
|
||||||
padding: 0.3rem 0.75rem;
|
padding: 0.35rem 0.8rem;
|
||||||
border-radius: 999px;
|
border-radius: 6px;
|
||||||
border: 2px solid var(--vp-c-warning);
|
border: 2px solid var(--vp-c-warning-1, #d97706);
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
color: var(--vp-c-warning-1, #d97706);
|
color: var(--vp-c-warning-1, #d97706);
|
||||||
font-size: 0.82rem;
|
font-size: 0.85rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.write-btn:hover {
|
.write-btn:hover {
|
||||||
background: var(--vp-c-warning-soft);
|
background: var(--vp-c-warning-soft, rgba(217, 119, 6, 0.1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.write-btn.flash {
|
.write-btn.flash {
|
||||||
background: var(--vp-c-warning);
|
background: var(--vp-c-warning-1, #d97706);
|
||||||
color: white;
|
color: white;
|
||||||
transform: scale(0.96);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-right {
|
.stored-val,
|
||||||
display: flex;
|
.output-val {
|
||||||
gap: 0.4rem;
|
display: inline-flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chip {
|
|
||||||
font-size: 0.78rem;
|
|
||||||
padding: 0.2rem 0.45rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--vp-c-bg-alt);
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chip_on {
|
|
||||||
border-color: var(--vp-c-brand);
|
|
||||||
color: var(--vp-c-brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- main content ---- */
|
|
||||||
.demo-content {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1.5fr 1fr;
|
|
||||||
gap: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- flow diagram ---- */
|
|
||||||
.flow-diagram {
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--vp-c-bg);
|
|
||||||
padding: 0.8rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.3rem;
|
justify-content: center;
|
||||||
flex-wrap: nowrap;
|
min-width: 2rem;
|
||||||
overflow-x: auto;
|
height: 2rem;
|
||||||
}
|
padding: 0 0.4rem;
|
||||||
|
border-radius: 6px;
|
||||||
.flow-node {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.3rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-label {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-value {
|
|
||||||
width: 2.4rem;
|
|
||||||
height: 2.4rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 2px solid var(--vp-c-divider);
|
border: 2px solid var(--vp-c-divider);
|
||||||
background: var(--vp-c-bg-alt);
|
background: var(--vp-c-bg-alt);
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
transition: all 0.3s;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-value.on {
|
.stored-val.on,
|
||||||
|
.output-val.on {
|
||||||
border-color: var(--vp-c-brand);
|
border-color: var(--vp-c-brand);
|
||||||
color: var(--vp-c-brand);
|
color: var(--vp-c-brand);
|
||||||
background: var(--vp-c-brand-soft);
|
background: var(--vp-c-brand-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-hint {
|
.visualization-area {
|
||||||
font-size: 0.7rem;
|
margin-bottom: 0.75rem;
|
||||||
color: var(--vp-c-text-3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.register-node .node-value {
|
.flow-strip {
|
||||||
width: 3rem;
|
|
||||||
height: 3rem;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
border: 3px solid var(--vp-c-text-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.register-node.flashing .node-value {
|
|
||||||
border-color: var(--vp-c-warning);
|
|
||||||
box-shadow: 0 0 10px var(--vp-c-warning-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bulb {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
filter: grayscale(100%);
|
|
||||||
opacity: 0.4;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.output-node.on .bulb {
|
|
||||||
filter: grayscale(0%);
|
|
||||||
opacity: 1;
|
|
||||||
text-shadow: 0 0 12px #facc15;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- arrows ---- */
|
|
||||||
.flow-arrow {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.2rem;
|
gap: 0.4rem;
|
||||||
flex-shrink: 0;
|
padding: 0.6rem 0.8rem;
|
||||||
}
|
|
||||||
|
|
||||||
.arrow-line {
|
|
||||||
width: 28px;
|
|
||||||
height: 2px;
|
|
||||||
background: var(--vp-c-divider);
|
|
||||||
transition: background 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flow-arrow.active .arrow-line {
|
|
||||||
background: var(--vp-c-brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow-tag {
|
|
||||||
font-size: 0.65rem;
|
|
||||||
color: var(--vp-c-text-3);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow-head {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--vp-c-text-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- state table ---- */
|
|
||||||
.state-table {
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
padding: 0.8rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-title {
|
.flow-item {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 0.85rem;
|
|
||||||
margin-bottom: 0.6rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.state-rows {
|
.flow-store {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.state-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 0.4rem;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-num {
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--vp-c-brand);
|
color: var(--vp-c-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.diff-display {
|
.flow-store.flash {
|
||||||
display: flex;
|
box-shadow: 0 0 0 2px var(--vp-c-warning-1);
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-top: 0.75rem;
|
|
||||||
padding: 0.5rem;
|
|
||||||
background: var(--vp-c-bg-alt);
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diff-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diff-label {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.diff-value {
|
|
||||||
width: 1.6rem;
|
|
||||||
height: 1.6rem;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
background: var(--vp-c-bg);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: bold;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.diff-value.on {
|
.flow-arrow {
|
||||||
border-color: var(--vp-c-brand);
|
|
||||||
color: var(--vp-c-brand);
|
|
||||||
background: var(--vp-c-brand-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.diff-sep {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: var(--vp-c-text-3);
|
color: var(--vp-c-text-3);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-arrow.active {
|
||||||
|
color: var(--vp-c-warning-1);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.diff-same {
|
.flow-hint {
|
||||||
font-size: 0.72rem;
|
margin: 0.4rem 0 0;
|
||||||
color: var(--vp-c-text-3);
|
font-size: 0.82rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- info box ---- */
|
|
||||||
.info-box {
|
.info-box {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
background: var(--vp-c-bg-alt);
|
background: var(--vp-c-bg-alt);
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
margin-top: 0.8rem;
|
|
||||||
display: flex;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box strong {
|
.info-box strong {
|
||||||
@@ -448,9 +270,10 @@ const writeOnce = () => {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 520px) {
|
||||||
.demo-content {
|
.control-panel {
|
||||||
grid-template-columns: 1fr;
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+72
-276
@@ -5,97 +5,43 @@
|
|||||||
<span class="subtitle">Gate 电压决定电流能否通过</span>
|
<span class="subtitle">Gate 电压决定电流能否通过</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-panel">
|
<div class="states">
|
||||||
<div class="control-left">
|
<div class="state-card">
|
||||||
<span class="control-label">栅极输入(Gate)</span>
|
<div class="state-label">Gate = 0(低电压)</div>
|
||||||
<button class="gate-toggle" :class="{ on: isOn }" @click="toggleSwitch">
|
<div class="channel-row">
|
||||||
{{ isOn ? '1(高电压)' : '0(低电压)' }}
|
<span class="terminal">源极</span>
|
||||||
</button>
|
<div class="channel-track off">
|
||||||
</div>
|
<span class="block-icon">✕ 断开</span>
|
||||||
<div class="control-right">
|
|
||||||
<span class="chip">通道:{{ isOn ? '导通' : '断开' }}</span>
|
|
||||||
<span class="chip">输出:{{ isOn ? '1' : '0' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="demo-content">
|
|
||||||
<div class="transistor-diagram">
|
|
||||||
<div class="gate-column">
|
|
||||||
<div class="gate-title">控制端 Gate</div>
|
|
||||||
<div class="gate-value" :class="{ on: isOn }">
|
|
||||||
{{ isOn ? '1' : '0' }}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="gate-arrow">↓ 控制</div>
|
<span class="terminal">漏极</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="output-line">输出:<strong>0</strong></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="main-channel">
|
<div class="state-card">
|
||||||
<div class="terminal-box">源极 Source</div>
|
<div class="state-label">Gate = 1(高电压)</div>
|
||||||
<div class="channel-track" :class="{ on: isOn }">
|
<div class="channel-row">
|
||||||
<span v-if="!isOn" class="block-icon">✕</span>
|
<span class="terminal">源极</span>
|
||||||
<template v-else>
|
<div class="channel-track on">
|
||||||
<span class="flow-dot d1" />
|
<span class="flow-dot d1" />
|
||||||
<span class="flow-dot d2" />
|
<span class="flow-dot d2" />
|
||||||
<span class="flow-dot d3" />
|
<span class="flow-dot d3" />
|
||||||
</template>
|
<span class="flow-label">导通</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="terminal-box">漏极 Drain</div>
|
<span class="terminal">漏极</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="result-line" :class="{ on: isOn }">
|
|
||||||
{{ isOn ? '电流通过:Source → Drain' : '电流被阻断:无法通过通道' }}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="output-line">输出:<strong>1</strong></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="truth-table">
|
|
||||||
<div class="table-title">晶体管状态表</div>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Gate 输入</th>
|
|
||||||
<th>通道状态</th>
|
|
||||||
<th>输出</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr :class="{ highlight: !isOn }">
|
|
||||||
<td>0(低电压)</td>
|
|
||||||
<td>断开</td>
|
|
||||||
<td>0</td>
|
|
||||||
</tr>
|
|
||||||
<tr :class="{ highlight: isOn }">
|
|
||||||
<td>1(高电压)</td>
|
|
||||||
<td>导通</td>
|
|
||||||
<td>1</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div class="table-hint">
|
|
||||||
点上方按钮切换 Gate,观察“通道状态”和“电流流动”如何同步变化。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="step-guide">
|
|
||||||
<div class="step-item">① 改变 Gate 电压(0/1)</div>
|
|
||||||
<div class="step-item">② 通道变为断开/导通</div>
|
|
||||||
<div class="step-item">③ 输出随之变成 0/1</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<strong>核心思想:</strong>晶体管本质是“电控开关”:Gate=1 时导通,Gate=0
|
<strong>核心思想:</strong>晶体管是“电控开关”:Gate=1 导通、Gate=0 断开,所有数字计算都建立在这种 0/1 开关之上。
|
||||||
时断开。所有数字计算都建立在这种 0/1 开关之上。
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
// 纯静态展示,无需交互
|
||||||
|
|
||||||
const isOn = ref(false)
|
|
||||||
|
|
||||||
const toggleSwitch = () => {
|
|
||||||
isOn.value = !isOn.value
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -111,7 +57,7 @@ const toggleSwitch = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-bottom: 0.8rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header .title {
|
.demo-header .title {
|
||||||
@@ -125,166 +71,72 @@ const toggleSwitch = () => {
|
|||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-panel {
|
.states {
|
||||||
display: flex;
|
display: grid;
|
||||||
align-items: center;
|
grid-template-columns: 1fr 1fr;
|
||||||
padding: 0.65rem 0.75rem;
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
margin-bottom: 0.8rem;
|
padding: 0.75rem;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-left {
|
.state-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.55rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-label {
|
.terminal {
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
}
|
flex-shrink: 0;
|
||||||
|
|
||||||
.gate-toggle {
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
background: var(--vp-c-bg-alt);
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 0.3rem 0.65rem;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gate-toggle.on {
|
|
||||||
background: var(--vp-c-success-soft);
|
|
||||||
color: var(--vp-c-success-1);
|
|
||||||
border-color: var(--vp-c-success);
|
|
||||||
color: var(--vp-c-success-1);
|
|
||||||
background: var(--vp-c-success-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-right {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.4rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chip {
|
|
||||||
font-size: 0.78rem;
|
|
||||||
padding: 0.2rem 0.45rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--vp-c-bg-alt);
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
}
|
|
||||||
.chip.active {
|
|
||||||
border-color: var(--vp-c-success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend-chip {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
padding: 0.16rem 0.42rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
background: var(--vp-c-bg);
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-content {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1.2fr 1fr;
|
|
||||||
gap: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transistor-diagram {
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--vp-c-bg);
|
|
||||||
padding: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gate-column {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.45rem;
|
|
||||||
margin-bottom: 0.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gate-title {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gate-value {
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: 2px solid var(--vp-c-divider);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gate-value.on {
|
|
||||||
border-color: var(--vp-c-success);
|
|
||||||
color: var(--vp-c-success-1);
|
|
||||||
background: var(--vp-c-success-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gate-arrow {
|
|
||||||
font-size: 0.78rem;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-channel {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1.6fr 1fr;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.55rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-box {
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--vp-c-bg-alt);
|
|
||||||
padding: 0.45rem;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-track {
|
.channel-track {
|
||||||
height: 2.4rem;
|
flex: 1;
|
||||||
|
min-height: 2.5rem;
|
||||||
border: 2px solid var(--vp-c-divider);
|
border: 2px solid var(--vp-c-divider);
|
||||||
border-radius: 999px;
|
border-radius: 6px;
|
||||||
background: #e5e7eb;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.channel-track.off {
|
||||||
|
background: var(--vp-c-bg-alt);
|
||||||
|
}
|
||||||
|
|
||||||
.channel-track.on {
|
.channel-track.on {
|
||||||
background: var(--vp-c-success-soft);
|
background: var(--vp-c-success-soft);
|
||||||
border-color: var(--vp-c-success);
|
border-color: var(--vp-c-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-icon {
|
.block-icon {
|
||||||
font-size: 1.1rem;
|
font-size: 0.9rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--vp-c-text-3);
|
color: var(--vp-c-text-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.flow-dot {
|
.flow-dot {
|
||||||
width: 0.42rem;
|
width: 0.4rem;
|
||||||
height: 0.42rem;
|
height: 0.4rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--vp-c-success);
|
background: var(--vp-c-success);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -8%;
|
left: -10%;
|
||||||
animation: flow 1.5s linear infinite;
|
animation: flow 1.5s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,92 +148,40 @@ const toggleSwitch = () => {
|
|||||||
animation-delay: 0.9s;
|
animation-delay: 0.9s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flow-label {
|
||||||
|
margin-left: 0.4rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-success-1);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes flow {
|
@keyframes flow {
|
||||||
from {
|
from {
|
||||||
left: -8%;
|
left: -10%;
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
left: 105%;
|
left: 105%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-line {
|
.output-line {
|
||||||
margin-top: 0.7rem;
|
margin-top: 0.5rem;
|
||||||
font-size: 0.82rem;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
padding: 0.45rem 0.55rem;
|
|
||||||
background: var(--vp-c-bg-alt);
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-line.on {
|
|
||||||
color: var(--vp-c-success-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.truth-table {
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--vp-c-bg);
|
|
||||||
padding: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-title {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 0.55rem;
|
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
.output-line strong {
|
||||||
width: 100%;
|
color: var(--vp-c-brand-1);
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 0.84rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
padding: 0.45rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
background: var(--vp-c-bg-alt);
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.highlight {
|
|
||||||
background: var(--vp-c-brand-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-hint {
|
|
||||||
margin-top: 0.55rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-guide {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 0.45rem;
|
|
||||||
margin-top: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-item {
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
background: var(--vp-c-bg);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.45rem 0.5rem;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box {
|
.info-box {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
background: var(--vp-c-bg-alt);
|
background: var(--vp-c-bg-alt);
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
margin-top: 0.75rem;
|
|
||||||
display: flex;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box strong {
|
.info-box strong {
|
||||||
@@ -389,12 +189,8 @@ tr.highlight {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 860px) {
|
@media (max-width: 640px) {
|
||||||
.demo-content {
|
.states {
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-guide {
|
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -382,29 +382,32 @@ function reset() {
|
|||||||
/* Graph */
|
/* Graph */
|
||||||
.gb-graph-wrap {
|
.gb-graph-wrap {
|
||||||
background: var(--vp-c-bg); border-top: 1px solid var(--vp-c-divider);
|
background: var(--vp-c-bg); border-top: 1px solid var(--vp-c-divider);
|
||||||
padding: 10px 12px;
|
padding: 14px 16px;
|
||||||
|
min-height: 200px;
|
||||||
|
overflow-x: auto;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
.gb-legend {
|
.gb-legend {
|
||||||
display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 8px;
|
display: flex; flex-wrap: wrap; gap: 14px; margin-bottom: 12px;
|
||||||
font-size: 0.74rem; color: var(--vp-c-text-2);
|
font-size: 0.8rem; color: var(--vp-c-text-2);
|
||||||
}
|
}
|
||||||
.leg-item { display: flex; align-items: center; gap: 5px; }
|
.leg-item { display: flex; align-items: center; gap: 6px; }
|
||||||
.leg-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
.leg-dot { width: 11px; height: 11px; border-radius: 50%; flex-shrink: 0; }
|
||||||
.main-c { background: #5b9cf6; }
|
.main-c { background: #5b9cf6; }
|
||||||
.feat-c { background: #f9e2af; }
|
.feat-c { background: #f9e2af; }
|
||||||
.merge-c { background: #a6e3a1; }
|
.merge-c { background: #a6e3a1; }
|
||||||
.leg-head {
|
.leg-head {
|
||||||
font-family: monospace; font-size: 0.68rem; font-weight: 700;
|
font-family: monospace; font-size: 0.72rem; font-weight: 700;
|
||||||
background: #5b9cf655; color: #5b9cf6; padding: 1px 5px; border-radius: 3px;
|
background: #5b9cf655; color: #5b9cf6; padding: 2px 6px; border-radius: 4px;
|
||||||
}
|
}
|
||||||
.head-leg { gap: 4px; }
|
.head-leg { gap: 6px; }
|
||||||
|
|
||||||
.svg-scroll { overflow-x: auto; }
|
.svg-scroll { overflow-x: auto; overflow-y: hidden; max-width: 100%; }
|
||||||
.gb-svg { display: block; overflow: visible; }
|
.gb-svg { display: block; overflow: visible; }
|
||||||
|
|
||||||
.gb-hint {
|
.gb-hint {
|
||||||
padding: 8px 12px; background: var(--vp-c-bg-alt);
|
padding: 10px 14px; background: var(--vp-c-bg-alt);
|
||||||
border-top: 1px solid var(--vp-c-divider);
|
border-top: 1px solid var(--vp-c-divider);
|
||||||
font-size: 0.8rem; color: var(--vp-c-text-2); line-height: 1.5;
|
font-size: 0.82rem; color: var(--vp-c-text-2); line-height: 1.5;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -127,6 +127,8 @@ const yLabels = computed(() => {
|
|||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-header {
|
.chart-header {
|
||||||
|
|||||||
@@ -1,39 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="gc-root">
|
<div class="gc-root">
|
||||||
<!-- Terminal -->
|
<div class="gc-layout">
|
||||||
<div class="gc-terminal">
|
<!-- 左侧:终端 + 按钮 -->
|
||||||
<div class="term-bar">
|
<div class="gc-left">
|
||||||
<span class="dot r" /><span class="dot y" /><span class="dot g" />
|
<div class="gc-terminal">
|
||||||
<span class="term-title">~/project (main)</span>
|
<div class="term-bar">
|
||||||
</div>
|
<span class="dot r" /><span class="dot y" /><span class="dot g" />
|
||||||
<div ref="termEl" class="term-body">
|
<span class="term-title">~/project (main)</span>
|
||||||
<div v-for="(l, i) in lines" :key="i" class="t-line">
|
</div>
|
||||||
<span v-if="l.kind === 'cmd'" class="t-ps">$ </span>
|
<div ref="termEl" class="term-body">
|
||||||
<span :class="'t-' + l.kind">{{ l.text }}</span>
|
<div v-for="(l, i) in lines" :key="i" class="t-line">
|
||||||
|
<span v-if="l.kind === 'cmd'" class="t-ps">$ </span>
|
||||||
|
<span :class="'t-' + l.kind">{{ l.text }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="t-line">
|
||||||
|
<span class="t-ps">$ </span>
|
||||||
|
<span class="t-typing">{{ typing }}<span class="t-cur">▋</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="t-line">
|
<div class="gc-btns">
|
||||||
<span class="t-ps">$ </span>
|
<button
|
||||||
<span class="t-typing">{{ typing }}<span class="t-cur">▋</span></span>
|
v-for="op in ops"
|
||||||
|
:key="op.id"
|
||||||
|
:disabled="running || !op.ok()"
|
||||||
|
:class="['gc-btn', { 'gc-btn--on': active === op.id, 'gc-btn--dim': !op.ok() }]"
|
||||||
|
@click="run(op)"
|
||||||
|
>
|
||||||
|
<code>{{ op.cmd }}</code>
|
||||||
|
</button>
|
||||||
|
<button class="gc-btn gc-btn--reset" :disabled="running" @click="reset">重置</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Buttons -->
|
<!-- 右侧:三区缩小展示 -->
|
||||||
<div class="gc-btns">
|
<div class="gc-right">
|
||||||
<button
|
<div class="gc-three-areas">
|
||||||
v-for="op in ops"
|
|
||||||
:key="op.id"
|
|
||||||
:disabled="running || !op.ok()"
|
|
||||||
:class="['gc-btn', { 'gc-btn--on': active === op.id, 'gc-btn--dim': !op.ok() }]"
|
|
||||||
@click="run(op)"
|
|
||||||
>
|
|
||||||
<code>{{ op.cmd }}</code>
|
|
||||||
</button>
|
|
||||||
<button class="gc-btn gc-btn--reset" :disabled="running" @click="reset">重置</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 三区可视化 -->
|
|
||||||
<div class="gc-three-areas">
|
|
||||||
<div class="area-col area-work" :class="{ 'area-highlight': pulseArea === 'work' }">
|
<div class="area-col area-work" :class="{ 'area-highlight': pulseArea === 'work' }">
|
||||||
<div class="area-header">
|
<div class="area-header">
|
||||||
<span class="area-icon">📝</span>
|
<span class="area-icon">📝</span>
|
||||||
@@ -55,7 +57,8 @@
|
|||||||
|
|
||||||
<div class="area-arrow" :class="{ 'arrow-lit': addDone }">
|
<div class="area-arrow" :class="{ 'arrow-lit': addDone }">
|
||||||
<code class="arrow-cmd">git add</code>
|
<code class="arrow-cmd">git add</code>
|
||||||
<span class="arrow-symbol">→</span>
|
<span class="arrow-symbol arrow-symbol--h" aria-hidden="true">→</span>
|
||||||
|
<span class="arrow-symbol arrow-symbol--v" aria-hidden="true">↓</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="area-col area-stage" :class="{ 'area-highlight': pulseArea === 'stage' }">
|
<div class="area-col area-stage" :class="{ 'area-highlight': pulseArea === 'stage' }">
|
||||||
@@ -79,7 +82,8 @@
|
|||||||
|
|
||||||
<div class="area-arrow" :class="{ 'arrow-lit': commitDone }">
|
<div class="area-arrow" :class="{ 'arrow-lit': commitDone }">
|
||||||
<code class="arrow-cmd">git commit</code>
|
<code class="arrow-cmd">git commit</code>
|
||||||
<span class="arrow-symbol">→</span>
|
<span class="arrow-symbol arrow-symbol--h" aria-hidden="true">→</span>
|
||||||
|
<span class="arrow-symbol arrow-symbol--v" aria-hidden="true">↓</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="area-col area-repo" :class="{ 'area-highlight': pulseArea === 'repo' }">
|
<div class="area-col area-repo" :class="{ 'area-highlight': pulseArea === 'repo' }">
|
||||||
@@ -102,6 +106,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Hint -->
|
<!-- Hint -->
|
||||||
<div v-if="hint" class="gc-hint">💡 {{ hint }}</div>
|
<div v-if="hint" class="gc-hint">💡 {{ hint }}</div>
|
||||||
@@ -116,7 +122,7 @@ const lines = ref([{ kind: 'dim', text: '# 你刚改了 3 个文件,现在演
|
|||||||
const typing = ref('')
|
const typing = ref('')
|
||||||
const running = ref(false)
|
const running = ref(false)
|
||||||
const active = ref(null)
|
const active = ref(null)
|
||||||
const hint = ref('点击下方命令按钮,按顺序执行。观察上方三区里文件如何随命令移动。')
|
const hint = ref('点击下方命令按钮,按顺序执行。观察右侧三区里文件如何随命令移动。')
|
||||||
const pulseArea = ref(null)
|
const pulseArea = ref(null)
|
||||||
|
|
||||||
const files = ref([
|
const files = ref([
|
||||||
@@ -275,7 +281,7 @@ function reset() {
|
|||||||
commitDone = false
|
commitDone = false
|
||||||
active.value = null
|
active.value = null
|
||||||
pulseArea.value = null
|
pulseArea.value = null
|
||||||
hint.value = '点击下方命令按钮,按顺序执行。观察上方三区里文件如何随命令移动。'
|
hint.value = '点击下方命令按钮,按顺序执行。观察右侧三区里文件如何随命令移动。'
|
||||||
typing.value = ''
|
typing.value = ''
|
||||||
running.value = false
|
running.value = false
|
||||||
}
|
}
|
||||||
@@ -291,6 +297,29 @@ function reset() {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 左右分栏:左终端+按钮,右三区缩小 */
|
||||||
|
.gc-layout {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
.gc-left {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.gc-right {
|
||||||
|
width: 260px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-left: 1px solid var(--vp-c-divider);
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.gc-layout { flex-direction: column; }
|
||||||
|
.gc-right { width: 100%; border-left: none; border-top: 1px solid var(--vp-c-divider); }
|
||||||
|
}
|
||||||
|
|
||||||
/* Terminal */
|
/* Terminal */
|
||||||
.gc-terminal { background: #141420; }
|
.gc-terminal { background: #141420; }
|
||||||
.term-bar {
|
.term-bar {
|
||||||
@@ -310,13 +339,14 @@ function reset() {
|
|||||||
min-height: 140px;
|
min-height: 140px;
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: auto;
|
||||||
padding: 0.8rem 1rem;
|
padding: 0.8rem 1rem;
|
||||||
font-family: 'Menlo', 'Monaco', monospace;
|
font-family: 'Menlo', 'Monaco', monospace;
|
||||||
font-size: 0.76rem;
|
font-size: 0.76rem;
|
||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
color: #cdd6f4;
|
color: #cdd6f4;
|
||||||
}
|
}
|
||||||
.t-line { display: flex; }
|
.t-line { display: flex; min-width: min-content; }
|
||||||
.t-ps { color: #a6e3a1; flex-shrink: 0; }
|
.t-ps { color: #a6e3a1; flex-shrink: 0; }
|
||||||
.t-cmd { color: #cdd6f4; }
|
.t-cmd { color: #cdd6f4; }
|
||||||
.t-dim { color: #585b70; }
|
.t-dim { color: #585b70; }
|
||||||
@@ -357,33 +387,28 @@ function reset() {
|
|||||||
.gc-btn--reset code { display: none; }
|
.gc-btn--reset code { display: none; }
|
||||||
.gc-btn--reset::after { content: '重置'; font-size: 0.7rem; color: #585b70; }
|
.gc-btn--reset::after { content: '重置'; font-size: 0.7rem; color: #585b70; }
|
||||||
|
|
||||||
/* 三区布局 */
|
/* 三区布局:右侧缩小、垂直堆叠 */
|
||||||
.gc-three-areas {
|
.gc-three-areas {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr auto 1fr auto 1fr;
|
flex-direction: column;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
align-items: stretch;
|
padding: 10px 12px;
|
||||||
padding: 12px 14px;
|
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
border-top: 1px solid var(--vp-c-divider);
|
font-size: 0.75rem;
|
||||||
min-height: 180px;
|
|
||||||
}
|
|
||||||
@media (max-width: 720px) {
|
|
||||||
.gc-three-areas {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
grid-template-rows: auto auto auto auto auto;
|
|
||||||
}
|
|
||||||
.area-arrow { transform: rotate(90deg); justify-self: center; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.area-col {
|
.area-col {
|
||||||
border: 1.5px solid var(--vp-c-divider);
|
border: 1.5px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
transition: border-color 0.25s, box-shadow 0.25s;
|
transition: border-color 0.25s, box-shadow 0.25s;
|
||||||
}
|
}
|
||||||
|
.gc-right .area-col {
|
||||||
|
min-height: 72px;
|
||||||
|
}
|
||||||
.area-col.area-highlight {
|
.area-col.area-highlight {
|
||||||
border-color: var(--vp-c-brand);
|
border-color: var(--vp-c-brand);
|
||||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--vp-c-brand) 14%, transparent);
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--vp-c-brand) 14%, transparent);
|
||||||
@@ -393,50 +418,71 @@ function reset() {
|
|||||||
.area-repo { border-left: 4px solid #5b9cf6; }
|
.area-repo { border-left: 4px solid #5b9cf6; }
|
||||||
|
|
||||||
.area-header {
|
.area-header {
|
||||||
padding: 6px 10px;
|
padding: 6px 8px;
|
||||||
background: var(--vp-c-bg-alt);
|
background: var(--vp-c-bg-alt);
|
||||||
border-bottom: 1px solid var(--vp-c-divider);
|
border-bottom: 1px solid var(--vp-c-divider);
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
.area-icon { font-size: 1rem; margin-right: 4px; }
|
.gc-right .area-header { padding: 5px 8px; }
|
||||||
|
.area-icon { font-size: 0.95rem; margin-right: 4px; flex-shrink: 0; }
|
||||||
|
.gc-right .area-icon { font-size: 0.85rem; }
|
||||||
.area-title {
|
.area-title {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 0.88rem;
|
font-size: 0.92rem;
|
||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
.gc-right .area-title { font-size: 0.8rem; }
|
||||||
.area-desc {
|
.area-desc {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.68rem;
|
font-size: 0.72rem;
|
||||||
color: var(--vp-c-text-3);
|
color: var(--vp-c-text-3);
|
||||||
margin-top: 2px;
|
margin-top: 4px;
|
||||||
line-height: 1.3;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
.gc-right .area-desc { font-size: 0.62rem; margin-top: 2px; }
|
||||||
|
|
||||||
.area-body {
|
.area-body {
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 72px;
|
min-height: 48px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
.gc-right .area-body { padding: 6px 8px; min-height: 40px; }
|
||||||
.area-label {
|
.area-label {
|
||||||
font-size: 0.68rem;
|
font-size: 0.72rem;
|
||||||
color: var(--vp-c-text-3);
|
color: var(--vp-c-text-3);
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
.gc-right .area-label { font-size: 0.62rem; margin-bottom: 4px; }
|
||||||
.area-empty {
|
.area-empty {
|
||||||
font-size: 0.74rem;
|
font-size: 0.8rem;
|
||||||
color: var(--vp-c-text-3);
|
color: var(--vp-c-text-3);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
padding: 6px 0;
|
||||||
}
|
}
|
||||||
|
.gc-right .area-empty { font-size: 0.7rem; padding: 4px 0; }
|
||||||
|
|
||||||
.file-row,
|
.file-row,
|
||||||
.commit-row {
|
.commit-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 4px 6px;
|
padding: 6px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 0.76rem;
|
font-size: 0.8rem;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
min-height: 28px;
|
||||||
|
}
|
||||||
|
.gc-right .file-row,
|
||||||
|
.gc-right .commit-row {
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
min-height: 24px;
|
||||||
}
|
}
|
||||||
.file-row:last-child,
|
.file-row:last-child,
|
||||||
.commit-row:last-child { margin-bottom: 0; }
|
.commit-row:last-child { margin-bottom: 0; }
|
||||||
@@ -450,73 +496,91 @@ function reset() {
|
|||||||
}
|
}
|
||||||
.file-badge {
|
.file-badge {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 0.72rem;
|
font-size: 0.78rem;
|
||||||
width: 14px;
|
width: 16px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.gc-right .file-badge { font-size: 0.68rem; width: 14px; }
|
||||||
.file-mod .file-badge { color: #f38ba8; }
|
.file-mod .file-badge { color: #f38ba8; }
|
||||||
.file-staged .file-badge { color: #a6e3a1; }
|
.file-staged .file-badge { color: #a6e3a1; }
|
||||||
.file-name { font-family: monospace; color: var(--vp-c-text-1); }
|
.file-name {
|
||||||
|
font-family: monospace;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.gc-right .file-name { font-size: 0.68rem; }
|
||||||
.file-state {
|
.file-state {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
font-size: 0.7rem;
|
font-size: 0.74rem;
|
||||||
color: var(--vp-c-text-3);
|
color: var(--vp-c-text-3);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.gc-right .file-state { font-size: 0.62rem; }
|
||||||
|
|
||||||
.commit-row {
|
.commit-row {
|
||||||
background: #5b9cf618;
|
background: #5b9cf618;
|
||||||
border-left: 3px solid #5b9cf6;
|
border-left: 3px solid #5b9cf6;
|
||||||
}
|
}
|
||||||
.commit-badge { color: #5b9cf6; font-weight: 700; flex-shrink: 0; }
|
.commit-badge { color: #5b9cf6; font-weight: 700; flex-shrink: 0; font-size: 0.9rem; }
|
||||||
|
.gc-right .commit-badge { font-size: 0.75rem; }
|
||||||
.commit-hash {
|
.commit-hash {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 0.7rem;
|
font-size: 0.78rem;
|
||||||
color: #5b9cf6;
|
color: #5b9cf6;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.gc-right .commit-hash { font-size: 0.66rem; }
|
||||||
.commit-msg {
|
.commit-msg {
|
||||||
font-size: 0.72rem;
|
font-size: 0.78rem;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 3em;
|
||||||
overflow: hidden;
|
word-wrap: break-word;
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
.gc-right .commit-msg { font-size: 0.66rem; min-width: 2em; }
|
||||||
.commit-head {
|
.commit-head {
|
||||||
font-size: 0.64rem;
|
font-size: 0.7rem;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
background: #5b9cf6;
|
background: #5b9cf6;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 1px 4px;
|
padding: 2px 6px;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.gc-right .commit-head { font-size: 0.6rem; padding: 1px 4px; }
|
||||||
|
|
||||||
|
/* 箭头:↓ + 命令 */
|
||||||
.area-arrow {
|
.area-arrow {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 4px;
|
gap: 6px;
|
||||||
padding: 0 8px;
|
padding: 6px 0;
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
}
|
}
|
||||||
|
.gc-right .area-arrow { padding: 4px 0; }
|
||||||
|
.area-arrow .arrow-symbol--h { display: none; }
|
||||||
|
.area-arrow .arrow-symbol--v {
|
||||||
|
display: inline;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--vp-c-brand);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.gc-right .area-arrow .arrow-symbol--v { font-size: 0.9rem; }
|
||||||
.area-arrow.arrow-lit { opacity: 1; }
|
.area-arrow.arrow-lit { opacity: 1; }
|
||||||
.arrow-cmd {
|
.arrow-cmd {
|
||||||
font-size: 0.66rem;
|
font-size: 0.72rem;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
color: var(--vp-c-brand);
|
color: var(--vp-c-brand);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.arrow-symbol {
|
.gc-right .arrow-cmd { font-size: 0.62rem; }
|
||||||
font-size: 1.2rem;
|
|
||||||
color: var(--vp-c-brand);
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gc-hint {
|
.gc-hint {
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
|
|||||||
@@ -281,10 +281,12 @@ function reset() {
|
|||||||
|
|
||||||
/* Repos */
|
/* Repos */
|
||||||
.gs-repos {
|
.gs-repos {
|
||||||
display: grid; grid-template-columns: 1fr auto 1fr;
|
display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||||
gap: 8px; padding: 10px 12px;
|
gap: 12px; padding: 16px 14px;
|
||||||
background: var(--vp-c-bg); border-top: 1px solid var(--vp-c-divider);
|
background: var(--vp-c-bg); border-top: 1px solid var(--vp-c-divider);
|
||||||
align-items: start;
|
align-items: start;
|
||||||
|
min-height: 200px;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.gs-repos { grid-template-columns: 1fr; }
|
.gs-repos { grid-template-columns: 1fr; }
|
||||||
@@ -293,7 +295,9 @@ function reset() {
|
|||||||
|
|
||||||
.repo-card {
|
.repo-card {
|
||||||
border: 1.5px solid var(--vp-c-divider); border-radius: 8px;
|
border: 1.5px solid var(--vp-c-divider); border-radius: 8px;
|
||||||
padding: 8px 10px; background: var(--vp-c-bg-soft);
|
padding: 12px 14px; background: var(--vp-c-bg-soft);
|
||||||
|
min-height: 180px; min-width: 0;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
transition: border-color .3s, box-shadow .3s;
|
transition: border-color .3s, box-shadow .3s;
|
||||||
}
|
}
|
||||||
.repo-remote { border-color: #60a5fa44; background: color-mix(in srgb, #60a5fa 4%, var(--vp-c-bg-soft)); }
|
.repo-remote { border-color: #60a5fa44; background: color-mix(in srgb, #60a5fa 4%, var(--vp-c-bg-soft)); }
|
||||||
@@ -301,26 +305,41 @@ function reset() {
|
|||||||
.repo-pulse-remote { border-color: #60a5fa !important; box-shadow: 0 0 0 3px #60a5fa22; }
|
.repo-pulse-remote { border-color: #60a5fa !important; box-shadow: 0 0 0 3px #60a5fa22; }
|
||||||
|
|
||||||
.repo-header {
|
.repo-header {
|
||||||
display: flex; align-items: center; gap: 5px; margin-bottom: 6px; flex-wrap: wrap;
|
display: flex; align-items: center; gap: 6px; margin-bottom: 10px; flex-wrap: wrap;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.repo-icon { font-size: 1.1rem; flex-shrink: 0; }
|
||||||
|
.repo-name { font-weight: 700; font-size: 0.88rem; flex-shrink: 0; }
|
||||||
|
.repo-path {
|
||||||
|
font-family: monospace; font-size: 0.7rem; color: var(--vp-c-text-3);
|
||||||
|
margin-left: auto; min-width: 0; overflow: hidden;
|
||||||
|
text-overflow: ellipsis; white-space: nowrap;
|
||||||
}
|
}
|
||||||
.repo-icon { font-size: 1rem; }
|
|
||||||
.repo-name { font-weight: 700; font-size: 0.8rem; }
|
|
||||||
.repo-path { font-family: monospace; font-size: 0.62rem; color: var(--vp-c-text-3); margin-left: auto; }
|
|
||||||
|
|
||||||
.commit-col { min-height: 48px; display: flex; flex-direction: column; gap: 4px; }
|
.commit-col {
|
||||||
.no-commits { color: var(--vp-c-text-3); font-size: 0.72rem; }
|
min-height: 80px; min-width: 0;
|
||||||
|
display: flex; flex-direction: column; gap: 6px; flex: 1;
|
||||||
|
}
|
||||||
|
.no-commits { color: var(--vp-c-text-3); font-size: 0.8rem; padding: 6px 0; }
|
||||||
.cmt-row {
|
.cmt-row {
|
||||||
display: flex; align-items: center; gap: 5px; font-size: 0.72rem;
|
display: flex; align-items: center; gap: 8px; font-size: 0.8rem;
|
||||||
padding: 2px 4px; border-radius: 3px; transition: background .3s;
|
padding: 8px 10px; border-radius: 6px; min-height: 36px;
|
||||||
|
min-width: 0; transition: background .3s;
|
||||||
}
|
}
|
||||||
.cmt-new { background: color-mix(in srgb, var(--vp-c-brand) 10%, transparent); }
|
.cmt-new { background: color-mix(in srgb, var(--vp-c-brand) 10%, transparent); }
|
||||||
.cmt-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
|
.cmt-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||||
.local-dot { background: var(--vp-c-brand); }
|
.local-dot { background: var(--vp-c-brand); }
|
||||||
.remote-dot { background: #60a5fa; }
|
.remote-dot { background: #60a5fa; }
|
||||||
.cmt-hash { color: var(--vp-c-brand); font-size: 0.68rem; }
|
.cmt-hash { color: var(--vp-c-brand); font-size: 0.76rem; flex-shrink: 0; }
|
||||||
.cmt-msg { color: var(--vp-c-text-2); }
|
.cmt-msg {
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.repo-footer { margin-top: 5px; font-size: 0.7rem; min-height: 16px; }
|
.repo-footer { margin-top: 10px; font-size: 0.76rem; min-height: 20px; }
|
||||||
.badge-ahead { color: var(--vp-c-brand); font-weight: 600; }
|
.badge-ahead { color: var(--vp-c-brand); font-weight: 600; }
|
||||||
.badge-sync { color: #a6e3a1; }
|
.badge-sync { color: #a6e3a1; }
|
||||||
.badge-online { color: #60a5fa; }
|
.badge-online { color: #60a5fa; }
|
||||||
@@ -342,8 +361,8 @@ function reset() {
|
|||||||
.arrow-pull .arrow-label { color: #60a5fa; }
|
.arrow-pull .arrow-label { color: #60a5fa; }
|
||||||
|
|
||||||
.gs-hint {
|
.gs-hint {
|
||||||
padding: 8px 12px; background: var(--vp-c-bg-alt);
|
padding: 10px 14px; background: var(--vp-c-bg-alt);
|
||||||
border-top: 1px solid var(--vp-c-divider);
|
border-top: 1px solid var(--vp-c-divider);
|
||||||
font-size: 0.8rem; color: var(--vp-c-text-2);
|
font-size: 0.82rem; color: var(--vp-c-text-2); line-height: 1.5;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -211,6 +211,15 @@ import BackpropagationDemo from './components/appendix/ai-history/Backpropagatio
|
|||||||
import AttentionMechanismDemo from './components/appendix/ai-history/AttentionMechanismDemo.vue'
|
import AttentionMechanismDemo from './components/appendix/ai-history/AttentionMechanismDemo.vue'
|
||||||
import DiscriminativeVsGenerativeDemo from './components/appendix/ai-history/DiscriminativeVsGenerativeDemo.vue'
|
import DiscriminativeVsGenerativeDemo from './components/appendix/ai-history/DiscriminativeVsGenerativeDemo.vue'
|
||||||
import GPTEvolutionDemo from './components/appendix/ai-history/GPTEvolutionDemo.vue'
|
import GPTEvolutionDemo from './components/appendix/ai-history/GPTEvolutionDemo.vue'
|
||||||
|
import FoundationDemo from './components/appendix/ai-history/FoundationDemo.vue'
|
||||||
|
|
||||||
|
// AI Protocols Components
|
||||||
|
import McpVisualDemo from './components/appendix/ai-protocols/McpVisualDemo.vue'
|
||||||
|
import A2AVisualDemo from './components/appendix/ai-protocols/A2AVisualDemo.vue'
|
||||||
|
import McpDetailedDemo from './components/appendix/ai-protocols/McpDetailedDemo.vue'
|
||||||
|
import A2ADetailedDemo from './components/appendix/ai-protocols/A2ADetailedDemo.vue'
|
||||||
|
import ProtocolComparisonDemo from './components/appendix/ai-protocols/ProtocolComparisonDemo.vue'
|
||||||
|
import ProtocolWorkflowDemo from './components/appendix/ai-protocols/ProtocolWorkflowDemo.vue'
|
||||||
|
|
||||||
import ImperativeVsDeclarativeDemo from './components/appendix/web-basics/ImperativeVsDeclarativeDemo.vue'
|
import ImperativeVsDeclarativeDemo from './components/appendix/web-basics/ImperativeVsDeclarativeDemo.vue'
|
||||||
import ComponentReusabilityDemo from './components/appendix/web-basics/ComponentReusabilityDemo.vue'
|
import ComponentReusabilityDemo from './components/appendix/web-basics/ComponentReusabilityDemo.vue'
|
||||||
@@ -543,12 +552,7 @@ import HttpsOptimizationDemo from './components/appendix/cloud-storage-cdn/Https
|
|||||||
import AccessAnalyticsDemo from './components/appendix/cloud-storage-cdn/AccessAnalyticsDemo.vue'
|
import AccessAnalyticsDemo from './components/appendix/cloud-storage-cdn/AccessAnalyticsDemo.vue'
|
||||||
|
|
||||||
// API Design Extra Components
|
// API Design Extra Components
|
||||||
import ErrorHandlingDemo from './components/appendix/api-design/ErrorHandlingDemo.vue'
|
import RestfulApiFlow from './components/appendix/api-design/RestfulApiFlow.vue'
|
||||||
import VersioningStrategyDemo from './components/appendix/api-design/VersioningStrategyDemo.vue'
|
|
||||||
import DocumentationDemo from './components/appendix/api-design/DocumentationDemo.vue'
|
|
||||||
import ResourceAnalogy from './components/appendix/api-design/ResourceAnalogy.vue'
|
|
||||||
import RequestStructureDemo from './components/appendix/api-design/RequestStructureDemo.vue'
|
|
||||||
import ResponseStructureDemo from './components/appendix/api-design/ResponseStructureDemo.vue'
|
|
||||||
|
|
||||||
// JavaScript Intro Components
|
// JavaScript Intro Components
|
||||||
import VariableBoxDemo from './components/appendix/javascript-intro/VariableBoxDemo.vue'
|
import VariableBoxDemo from './components/appendix/javascript-intro/VariableBoxDemo.vue'
|
||||||
@@ -638,6 +642,7 @@ export default {
|
|||||||
app.component('ApiDocumentDemo', ApiDocumentDemo)
|
app.component('ApiDocumentDemo', ApiDocumentDemo)
|
||||||
app.component('ApiPlayground', ApiPlayground)
|
app.component('ApiPlayground', ApiPlayground)
|
||||||
app.component('RealWorldApiDemo', RealWorldApiDemo)
|
app.component('RealWorldApiDemo', RealWorldApiDemo)
|
||||||
|
app.component('RestfulApiFlow', RestfulApiFlow)
|
||||||
|
|
||||||
// LLM Intro Components Registration
|
// LLM Intro Components Registration
|
||||||
app.component('EmbeddingDemo', EmbeddingDemo)
|
app.component('EmbeddingDemo', EmbeddingDemo)
|
||||||
@@ -804,6 +809,7 @@ export default {
|
|||||||
app.component('RenderingStrategyDemo', RenderingStrategyDemo)
|
app.component('RenderingStrategyDemo', RenderingStrategyDemo)
|
||||||
app.component('BigFrontendScopeDemo', BigFrontendScopeDemo)
|
app.component('BigFrontendScopeDemo', BigFrontendScopeDemo)
|
||||||
app.component('AiEvolutionDemo', AiEvolutionDemo)
|
app.component('AiEvolutionDemo', AiEvolutionDemo)
|
||||||
|
app.component('FoundationDemo', FoundationDemo)
|
||||||
app.component('RuleBasedVsLearningDemo', RuleBasedVsLearningDemo)
|
app.component('RuleBasedVsLearningDemo', RuleBasedVsLearningDemo)
|
||||||
app.component('PerceptronDemo', PerceptronDemo)
|
app.component('PerceptronDemo', PerceptronDemo)
|
||||||
app.component('AIEvolutionTimelineDemo', AIEvolutionTimelineDemo)
|
app.component('AIEvolutionTimelineDemo', AIEvolutionTimelineDemo)
|
||||||
@@ -820,6 +826,14 @@ export default {
|
|||||||
)
|
)
|
||||||
app.component('GPTEvolutionDemo', GPTEvolutionDemo)
|
app.component('GPTEvolutionDemo', GPTEvolutionDemo)
|
||||||
|
|
||||||
|
// AI Protocols Components Registration
|
||||||
|
app.component('McpVisualDemo', McpVisualDemo)
|
||||||
|
app.component('A2AVisualDemo', A2AVisualDemo)
|
||||||
|
app.component('McpDetailedDemo', McpDetailedDemo)
|
||||||
|
app.component('A2ADetailedDemo', A2ADetailedDemo)
|
||||||
|
app.component('ProtocolComparisonDemo', ProtocolComparisonDemo)
|
||||||
|
app.component('ProtocolWorkflowDemo', ProtocolWorkflowDemo)
|
||||||
|
|
||||||
app.component('ImperativeVsDeclarativeDemo', ImperativeVsDeclarativeDemo)
|
app.component('ImperativeVsDeclarativeDemo', ImperativeVsDeclarativeDemo)
|
||||||
app.component('ComponentReusabilityDemo', ComponentReusabilityDemo)
|
app.component('ComponentReusabilityDemo', ComponentReusabilityDemo)
|
||||||
app.component('FrameworkMotivationDemo', FrameworkMotivationDemo)
|
app.component('FrameworkMotivationDemo', FrameworkMotivationDemo)
|
||||||
|
|||||||
@@ -1,553 +1,124 @@
|
|||||||
# API 设计哲学(REST / GraphQL / gRPC)
|
# API 设计:前后端的通用语言
|
||||||
::: tip 🎯 核心问题
|
|
||||||
**前后端如何高效对话?** 这就像问:餐厅的菜单怎么设计,客人一看就懂?服务员怎么记单,不会出错?上菜怎么规范,客人满意?API 设计解决的就是"对话规则"的问题。
|
> 💡 **学习指南**:这一章我们聊聊前后端如何高效对话。如果你被后端接口的命名搞晕过,或者不知道该返回 200 还是 404,这篇文章就是为你准备的。我们将通过一个交互式 Demo,带你理解 RESTful API 的设计精髓。
|
||||||
:::
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 为什么要"API 设计"?
|
## 0. 先问一个问题:你有没有经历过这些噩梦?
|
||||||
|
|
||||||
### 1.1 从混乱到规范
|
**场景一:接口猜谜**
|
||||||
|
|
||||||
想象一下你走进一家餐厅:
|
后端给你一个接口 `/getUser`,你调用了,返回 `null`。
|
||||||
|
你是传错了参数?还是数据库没数据?还是服务器崩了?完全不知道。
|
||||||
|
|
||||||
- **菜单(API 文档)**:清楚标注了每道菜的口味、配料、价格
|
**场景二:状态码撒谎**
|
||||||
- **服务员(HTTP 协议)**:用标准化的方式记录你的点餐
|
|
||||||
- **后厨(服务端)**:按约定流程烹饪
|
|
||||||
- **上菜(响应)**:盘子摆盘规范,附带小票说明
|
|
||||||
|
|
||||||
**好的 API 设计就像这套点餐系统**——双方约定好"说什么话"、"怎么说话"、"出错怎么办",才能高效协作。
|
|
||||||
|
|
||||||
但很多团队的真实情况是:
|
|
||||||
|
|
||||||
- 接口命名随心所欲:`/getUserData`、`/fetchUserInfo`、`/queryUserById` 并存
|
|
||||||
- 错误处理五花八门:有的返回 HTTP 状态码,有的返回 `code: 500`,有的直接抛异常
|
|
||||||
- 响应结构千人千面:同一个项目里,有的用 `data`,有的用 `result`,有的用 `content`
|
|
||||||
|
|
||||||
<RestfulDesignDemo />
|
|
||||||
|
|
||||||
**结果就是**:前后端互相猜,联调痛苦,维护成本高,新人入职一脸懵。
|
|
||||||
|
|
||||||
::: tip 💡 通俗解释
|
|
||||||
**API**(Application Programming Interface,应用程序编程接口)就像"餐厅的服务协议":
|
|
||||||
|
|
||||||
- **RESTful** = 餐厅点餐:有菜单、有流程、有规范
|
|
||||||
- **GraphQL** = 自助餐:想要什么,自己组合
|
|
||||||
- **gRPC** = 快递:二进制格式,速度最快
|
|
||||||
|
|
||||||
**为什么需要设计?**
|
|
||||||
|
|
||||||
- 没有设计 = 服务员随机记单 → 后厨看不懂、客人等半天
|
|
||||||
- 有设计 = 标准化流程 → 效率高、错误少
|
|
||||||
:::
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. RESTful 设计:让你的 URL 会说话
|
|
||||||
|
|
||||||
### 2.1 什么是 RESTful?
|
|
||||||
|
|
||||||
**REST**(Representational State Transfer,表述性状态传递)是一种软件架构风格,由 Roy Fielding 在 2000 年提出。
|
|
||||||
|
|
||||||
**核心思想**:把网络上的所有事物都抽象为"资源"(Resource),用 URL 标识资源,用 HTTP 方法操作资源。
|
|
||||||
|
|
||||||
<ResourceAnalogyDemo />
|
|
||||||
|
|
||||||
::: tip 💡 资源是什么?
|
|
||||||
**资源**(Resource)是 REST 架构的核心概念:
|
|
||||||
|
|
||||||
- 有唯一标识(URL)
|
|
||||||
- 有表达方式(JSON/XML/HTML)
|
|
||||||
- 有操作方法(GET/POST/PUT/DELETE)
|
|
||||||
|
|
||||||
**比喻**:
|
|
||||||
|
|
||||||
- URL 是"仓库的货架地址":`/warehouse/products` 是"产品区"
|
|
||||||
- HTTP 方法是"允许的操作":GET(查看)、POST(入库)、PUT(更新)、DELETE(出库)
|
|
||||||
|
|
||||||
**关键点**:
|
|
||||||
|
|
||||||
- URL 是名词,不是动词:`/users` 而不是 `/getUsers`
|
|
||||||
- HTTP 方法已经表达了操作,不需要在 URL 里重复
|
|
||||||
:::
|
|
||||||
|
|
||||||
### 2.2 URL 设计的 7 个黄金法则
|
|
||||||
|
|
||||||
<HttpMethodsDemo />
|
|
||||||
|
|
||||||
| 法则 | 正确示例 | 错误示例 | 原因 |
|
|
||||||
| ---------------------- | --------------------------------------------- | ----------------------------------------- | ---------------------------------------- |
|
|
||||||
| **1. 用名词,不用动词** | `GET /users` | `GET /getUsers` | URL 是资源地址,不是操作 |
|
|
||||||
| **2. 用复数** | `GET /orders` | `GET /order` | 一致性好,表示集合 |
|
|
||||||
| **3. 小写字母** | `/user-profiles` | `/UserProfiles` | URL 大小写敏感,统一小写避免混乱 |
|
|
||||||
| **4. 用连字符** | `/user-profiles` | `/user_profiles` | 连字符是 URL 规范,下划线在某些场景会转义 |
|
|
||||||
| **5. 避免层级过深** | `/users/123/orders` | `/users/123/orders/456/items/789/status` | 超过 3 层考虑用查询参数或重构 |
|
|
||||||
| **6. 用查询参数过滤** | `GET /products?category=phone&price_max=5000` | `GET /products/category/phone/price/5000` | 过滤条件多且变动,不适合放路径 |
|
|
||||||
| **7. 版本号放 URL** | `/v1/users` 或 `v1.users.api.com` | 混用新旧接口无版本 | 便于灰度发布和向后兼容 |
|
|
||||||
|
|
||||||
::: tip 📊 从表格中你能看到什么?
|
|
||||||
**规则 1-4**是"一致性"原则:
|
|
||||||
|
|
||||||
- 统一名词、复数、大小写,让团队所有人写的 URL 风格一致
|
|
||||||
- 减少认知负担,一眼就知道这是什么资源
|
|
||||||
|
|
||||||
**规则 5-6**是"灵活性"原则:
|
|
||||||
|
|
||||||
- 层级太深 = 耦心度太高,难以维护
|
|
||||||
- 查询参数 = 更灵活,可以组合任意过滤条件
|
|
||||||
|
|
||||||
**规则 7**是"兼容性"原则:
|
|
||||||
|
|
||||||
- API 是长期契约,不能随意改
|
|
||||||
- 版本号让新旧接口并存,平滑升级
|
|
||||||
:::
|
|
||||||
|
|
||||||
### 2.3 HTTP 方法的选择
|
|
||||||
|
|
||||||
| 方法 | 用途 | 幂等性\* | 安全性\*\* | 典型场景 |
|
|
||||||
| ---------- | -------- | -------- | ---------- | -------------------------- |
|
|
||||||
| **GET** | 获取资源 | 是 | 是 | 查询列表、查看详情 |
|
|
||||||
| **POST** | 创建资源 | 否 | 否 | 新增用户、提交订单 |
|
|
||||||
| **PUT** | 全量更新 | 是 | 否 | 替换整个用户资料 |
|
|
||||||
| **PATCH** | 部分更新 | 否 | 否 | 修改用户昵称(只传一个字段) |
|
|
||||||
| **DELETE** | 删除资源 | 是 | 否 | 删除用户、取消订单 |
|
|
||||||
|
|
||||||
> **\*幂等性**:多次执行结果相同。比如 PUT 同一个资源 10 次,结果还是那一个资源。\***\*安全性**:不会改变服务器状态。GET 是安全的,POST/PUT/DELETE 都不安全。
|
|
||||||
|
|
||||||
::: details 🔧 幂等性为什么重要?
|
|
||||||
**场景**:用户点击"支付"按钮,但网络延迟,用户又点了一次。
|
|
||||||
|
|
||||||
- **幂等的操作**(PUT/DELETE):点击 10 次和点击 1 次,结果相同。不会重复扣款。
|
|
||||||
- **不幂等的操作**(POST):点击 10 次,可能创建 10 个订单。
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
|
|
||||||
- **客户端**:按钮点击后禁用,防止重复提交
|
|
||||||
- **服务端**:POST 操作用唯一 ID 校验,避免重复处理
|
|
||||||
|
|
||||||
**关键点**:幂等性是分布式系统正确性的重要保证。
|
|
||||||
:::
|
|
||||||
|
|
||||||
### 2.4 实战示例:电商系统的 RESTful API
|
|
||||||
|
|
||||||
```
|
|
||||||
# 用户模块
|
|
||||||
GET /v1/users # 获取用户列表(支持分页、过滤)
|
|
||||||
POST /v1/users # 创建新用户
|
|
||||||
GET /v1/users/{id} # 获取用户详情
|
|
||||||
PUT /v1/users/{id} # 全量更新用户信息
|
|
||||||
PATCH /v1/users/{id} # 部分更新(如:修改密码)
|
|
||||||
DELETE /v1/users/{id} # 删除用户
|
|
||||||
|
|
||||||
# 订单模块(嵌套资源,最多 3 层)
|
|
||||||
GET /v1/users/{id}/orders # 获取某用户的所有订单
|
|
||||||
POST /v1/users/{id}/orders # 为用户创建订单
|
|
||||||
GET /v1/orders/{orderId} # 获取订单详情(扁平化,避免过深)
|
|
||||||
PATCH /v1/orders/{orderId}/status # 更新订单状态(子资源操作)
|
|
||||||
|
|
||||||
# 商品模块(复杂过滤用查询参数)
|
|
||||||
GET /v1/products?category=electronics&price_min=100&price_max=5000&sort=price_desc&page=2&page_size=20
|
|
||||||
|
|
||||||
# 复杂报表(特殊场景,URL 可带动词)
|
|
||||||
POST /v1/reports/sales/export # 导出销售报表(非纯 CRUD,动词可接受)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 状态码:让错误"会说话"
|
|
||||||
|
|
||||||
### 3.1 为什么状态码很重要?
|
|
||||||
|
|
||||||
<StatusCodeDemo />
|
|
||||||
|
|
||||||
想象一下,如果你的 API 不管成功失败都返回 `200 OK`,客户端该怎么判断?
|
|
||||||
|
|
||||||
```json
|
|
||||||
// ❌ 错误的做法:HTTP 200,但业务失败
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "用户不存在"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**问题在哪?**
|
|
||||||
|
|
||||||
- 缓存层(CDN、浏览器)会缓存这个"成功的"响应
|
|
||||||
- 监控工具以为一切正常
|
|
||||||
- 前端需要额外解析 JSON 才知道有没有错
|
|
||||||
|
|
||||||
**正确的做法**:用 HTTP 状态码表示传输层状态,和业务成功/失败解耦。
|
|
||||||
|
|
||||||
### 3.2 常用状态码速查表
|
|
||||||
|
|
||||||
| 状态码 | 含义 | 使用场景 | 响应体内容 |
|
|
||||||
| ------------------------- | -------------- | ---------------------------------------------- | ------------------------ | --- |
|
|
||||||
| **2xx 成功** | | | | |
|
|
||||||
| 200 OK | 通用成功 | GET 查询成功、PUT/PATCH 更新成功 | 资源数据 |
|
|
||||||
| 201 Created | 创建成功 | POST 创建资源成功 | 新资源数据 + Location 头 |
|
|
||||||
| 202 Accepted | 已接受 | 异步任务提交成功(如:导出报表) | 任务状态/轮询地址 |
|
|
||||||
| 204 No Content | 无内容 | DELETE 删除成功、PUT 更新但无需返回数据 | 空(用缓存) |
|
|
||||||
| **3xx 重定向** | | | | |
|
|
||||||
| 301 Moved Permanently | 永久重定向 | 资源 URL 永久变更(如:v1 废弃,跳转 v2) | 新 URL |
|
|
||||||
| 302 Found | 临时重定向 | 临时跳转(较少用于 API) | 临时 URL |
|
|
||||||
| 304 Not Modified | 未修改 | 缓存有效(配合 If-None-Match/If-Modified-Since) | 空(用缓存) |
|
|
||||||
| **4xx 客户端错误** | | | | |
|
|
||||||
| 400 Bad Request | 请求格式错误 | 参数缺失、JSON 格式错误、字段类型不对 | 错误详情 |
|
|
||||||
| 401 Unauthorized | 未认证 | 缺少 Token、Token 过期、签名错误 | 认证方式说明 |
|
|
||||||
| 403 Forbidden | 禁止访问 | 已登录但无权限(如:普通用户访问管理员接口) | 无权限说明 |
|
|
||||||
| 404 Not Found | 资源不存在 | URL 错误、资源已删除 | 错误详情 |
|
|
||||||
| 405 Method Not Allowed | 方法不允许 | 如:对只读资源调用 POST | 允许的 Methods |
|
|
||||||
| 409 Conflict | 资源冲突 | 重复创建(唯一约束冲突)、乐观锁版本冲突 | 冲突详情 |
|
|
||||||
| 422 Unprocessable Entity | 语义错误 | 请求格式对,但业务校验失败(如:密码太短) | 校验错误详情 |
|
|
||||||
| 429 Too Many Requests | 请求过多 | 触发限流(Rate Limiting) | 重试时间 |
|
|
||||||
| **5xx 服务端错误** | | | | |
|
|
||||||
| 500 Internal Server Error | 服务器内部错误 | 未捕获的异常、代码 bug | 错误 ID(不要暴露堆栈) |
|
|
||||||
| 502 Bad Gateway | 网关错误 | 反向代理(Nginx)无法连接到后端服务 | - |
|
|
||||||
| 503 Service Unavailable | 服务不可用 | 服务正在维护、过载保护触发 | 恢复时间估计 |
|
|
||||||
| 504 Gateway Timeout | 网关超时 | 后端响应太慢,被代理层切断 | - |
|
|
||||||
|
|
||||||
::: tip 📊 从表格中你能看到什么?
|
|
||||||
**2xx(成功)** vs **3xx(重定向)** vs **4xx(客户端错误)** vs **5xx(服务端错误)**:
|
|
||||||
|
|
||||||
- **2xx**:一切正常,客户端可以继续
|
|
||||||
- **3xx**:资源换地方了,告诉客户端去哪找
|
|
||||||
- **4xx**:客户端搞错了,修正请求后重试
|
|
||||||
- **5xx**:服务端出问题了,客户端等一等再试,或者联系管理员
|
|
||||||
|
|
||||||
**关键点**:正确的状态码让客户端、浏览器、CDN、监控工具都能正确理解响应。
|
|
||||||
:::
|
|
||||||
|
|
||||||
### 3.3 状态码使用的"避坑指南"
|
|
||||||
|
|
||||||
**坑 1:所有错误都用 400**
|
|
||||||
|
|
||||||
```
|
|
||||||
❌ GET /users/999 → 400 (用户不存在应该返回 404)
|
|
||||||
❌ POST /login 密码错误 → 400 (应该返回 401 或 422)
|
|
||||||
❌ 删除已删除的资源 → 400 (应该返回 404 或 204)
|
|
||||||
```
|
|
||||||
|
|
||||||
**坑 2:业务状态混在 HTTP 状态码里**
|
|
||||||
|
|
||||||
```
|
|
||||||
❌ 订单支付失败 → 402 Payment Required (这个状态码是为数字钱包预留的,不要滥用)
|
|
||||||
✅ 订单支付失败 → 200 OK + body: { "code": "PAYMENT_FAILED", "message": "余额不足" }
|
|
||||||
```
|
|
||||||
|
|
||||||
**坑 3:暴露敏感信息**
|
|
||||||
|
|
||||||
```
|
|
||||||
❌ 500 响应里返回完整的堆栈跟踪、SQL 查询语句、数据库连接信息
|
|
||||||
✅ 只返回 "错误 ID",详细日志记录到服务器,通过错误 ID 关联
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 请求与响应:标准化的数据契约
|
|
||||||
|
|
||||||
### 4.1 请求结构设计
|
|
||||||
|
|
||||||
<RequestStructureDemo />
|
|
||||||
|
|
||||||
**HTTP 请求由 3 部分组成**:
|
|
||||||
|
|
||||||
```http
|
|
||||||
# 1. 请求行(方法 + URL + 协议版本)
|
|
||||||
POST /v1/users HTTP/1.1
|
|
||||||
|
|
||||||
# 2. 请求头(元数据)
|
|
||||||
Host: api.example.com
|
|
||||||
Content-Type: application/json
|
|
||||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
|
||||||
X-Request-ID: req-123456789
|
|
||||||
Accept: application/json
|
|
||||||
Accept-Language: zh-CN,zh;q=0.9
|
|
||||||
|
|
||||||
# 3. 请求体(仅 POST/PUT/PATCH 需要)
|
|
||||||
{
|
|
||||||
"name": "张三",
|
|
||||||
"email": "zhangsan@example.com",
|
|
||||||
"phone": "13800138000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 查询参数设计规范
|
|
||||||
|
|
||||||
```http
|
|
||||||
# 分页(必须)
|
|
||||||
GET /products?page=1&page_size=20
|
|
||||||
|
|
||||||
# 排序(可选)
|
|
||||||
GET /products?sort=created_at&order=desc
|
|
||||||
|
|
||||||
# 过滤(可选,支持多种操作符)
|
|
||||||
GET /products?price_min=100&price_max=5000 # 范围
|
|
||||||
GET /products?category=electronics,clothing # 多选(IN)
|
|
||||||
GET /products?status=active&is_featured=true # 布尔
|
|
||||||
GET /products?search=iphone # 全文搜索
|
|
||||||
|
|
||||||
# 字段选择(可选,减少数据传输)
|
|
||||||
GET /products?fields=id,name,price
|
|
||||||
|
|
||||||
# 组合使用
|
|
||||||
GET /products?page=1&page_size=20&sort=price&order=asc&category=electronics&price_max=5000&fields=id,name,price
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 请求头设计规范
|
|
||||||
|
|
||||||
| 头部字段 | 用途 | 示例 | 是否必需 |
|
|
||||||
| ------------------ | ------------ | ----------------------------------------- | --------------------- |
|
|
||||||
| `Authorization` | 认证信息 | `Bearer eyJhbGciOiJIUzI1NiIs...` | 受保护接口必需 |
|
|
||||||
| `Content-Type` | 请求体格式 | `application/json` | POST/PUT/PATCH 必需 |
|
|
||||||
| `Accept` | 期望响应格式 | `application/json` | 建议携带 |
|
|
||||||
| `Accept-Language` | 期望语言 | `zh-CN,zh;q=0.9,en;q=0.8` | 多语言应用必需 |
|
|
||||||
| `X-Request-ID` | 请求唯一标识 | `req-550e8400-e29b-41d4-a716-44665544000` | 建议携带,便于追踪 |
|
|
||||||
| `X-Client-Version` | 客户端版本 | `iOS-2.5.1` / `Web-3.0.0` | 建议携带,便于问题排查 |
|
|
||||||
| `If-None-Match` | 缓存校验 | `"abc123"` | 可选,用于乐观并发控制 |
|
|
||||||
|
|
||||||
### 4.2 响应结构设计
|
|
||||||
|
|
||||||
<ResponseStructureDemo />
|
|
||||||
|
|
||||||
**标准化响应结构**(无论成功与否,结构一致):
|
|
||||||
|
|
||||||
|
你收到了一个 HTTP 200 OK 的响应,心想“稳了”。
|
||||||
|
结果打开 Body 一看:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"code": 0,
|
"code": 500,
|
||||||
"message": "success",
|
"msg": "系统内部错误",
|
||||||
"data": { ... },
|
"data": null
|
||||||
"request_id": "req-123456789",
|
|
||||||
"timestamp": "2024-01-15T09:30:00.000Z"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
浏览器缓存了它,监控系统认为它成功了,只有你的前端代码在风中凌乱。
|
||||||
|
|
||||||
#### 响应字段说明
|
**场景三:版本地狱**
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 | 示例 |
|
项目迭代了三年,你的代码里充满了这样的 URL:
|
||||||
| ------------ | ------- | ---------------------------------------------------------- | ------------------------------------------- |
|
- `/api/v1/user/update`
|
||||||
| `code` | integer | 业务状态码,`0` 表示成功,非 `0` 表示失败 | `0`, `10001`, `20003` |
|
- `/api/v2/user/update_new`
|
||||||
| `message` | string | 状态描述,成功时为 `"success"`,失败时为错误描述 | `"success"`, `"用户不存在"` |
|
- `/api/user/update_final_real`
|
||||||
| `data` | any | 业务数据,成功时返回具体数据,失败时可返回 `null` 或错误详情 | `{ "id": 1, "name": "张三" }` |
|
|
||||||
| `request_id` | string | 请求唯一标识,用于问题追踪 | `"req-550e8400-e29b-41d4-a716-44665544000"` |
|
|
||||||
| `timestamp` | string | 响应时间戳,ISO 8601 格式 | `"2024-01-15T09:30:00.000Z"` |
|
|
||||||
|
|
||||||
#### 分页响应结构
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"message": "success",
|
|
||||||
"data": {
|
|
||||||
"list": [
|
|
||||||
{ "id": 1, "name": "商品A", "price": 100 },
|
|
||||||
{ "id": 2, "name": "商品B", "price": 200 }
|
|
||||||
],
|
|
||||||
"pagination": {
|
|
||||||
"page": 1,
|
|
||||||
"page_size": 20,
|
|
||||||
"total": 156,
|
|
||||||
"total_pages": 8,
|
|
||||||
"has_next": true,
|
|
||||||
"has_prev": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
::: tip 💡 为什么要 request_id?
|
|
||||||
**request_id** 是问题追踪的关键:
|
|
||||||
|
|
||||||
1. **用户反馈**:"支付失败,错误 ID 是 abc123"
|
|
||||||
→ 技术人员直接在日志里搜索 abc123,立即定位问题
|
|
||||||
|
|
||||||
2. **分布式追踪**:
|
|
||||||
- 请求经过 API Gateway → Service A → Service B
|
|
||||||
- 每个服务都记录相同的 request_id
|
|
||||||
- 日志系统可以把所有相关日志聚合起来
|
|
||||||
|
|
||||||
3. **性能分析**:
|
|
||||||
- 统计某个 request_id 的完整链路耗时
|
|
||||||
|
|
||||||
- 发现瓶颈在哪个服务
|
|
||||||
|
|
||||||
**关键点**:request_id 是可观测性的基础,没有它,分布式系统的问题排查是地狱模式。
|
|
||||||
:::
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 错误处理:优雅地"拒绝"
|
**API 设计就是为了解决这些问题。**
|
||||||
|
|
||||||
### 5.1 为什么错误处理如此重要?
|
它就像餐厅的菜单和点餐流程:规定了我们**怎么点菜(请求)**、**怎么上菜(响应)**、**没菜了怎么办(错误处理)**。
|
||||||
|
|
||||||
<ErrorHandlingDemo />
|
目前最流行的设计风格是 **RESTful**。
|
||||||
|
|
||||||
一个好的错误处理机制,能让客户端"看状态码就知道怎么回事",而不是去猜。
|
|
||||||
|
|
||||||
**错误的示范**:
|
|
||||||
|
|
||||||
```json
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
{
|
|
||||||
"error": "出错了"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**问题**:
|
|
||||||
|
|
||||||
- HTTP 状态码说"成功",但业务说"出错"
|
|
||||||
- 错误信息太笼统,无法定位问题
|
|
||||||
- 没有错误代码,难以程序化判断
|
|
||||||
|
|
||||||
**正确的示范**:
|
|
||||||
|
|
||||||
```json
|
|
||||||
HTTP/1.1 422 Unprocessable Entity
|
|
||||||
{
|
|
||||||
"code": 20003,
|
|
||||||
"message": "密码强度不足",
|
|
||||||
"errors": [
|
|
||||||
{
|
|
||||||
"field": "password",
|
|
||||||
"code": "VALIDATION_ERROR",
|
|
||||||
"message": "密码必须包含至少 1 个大写字母、1 个小写字母、1 个数字,且长度至少 8 位"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"request_id": "req-550e8400-e29b-41d4-a716-44665544000",
|
|
||||||
"timestamp": "2024-01-15T09:30:00.000Z",
|
|
||||||
"help_url": "https://docs.example.com/errors/20003"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 错误码设计规范
|
|
||||||
|
|
||||||
::: tip 💡 错误码的分层设计
|
|
||||||
**分层错误码**的好处:
|
|
||||||
|
|
||||||
- **可程序化判断**:前端根据 `code` 字段决定行为,而不是解析 `message`
|
|
||||||
- **国际化友好**:`code` 不变,`message` 可以根据用户语言返回不同文本
|
|
||||||
- **文档化**:每个错误码都有文档,开发者可以查
|
|
||||||
|
|
||||||
**结构**:1XXYY
|
|
||||||
|
|
||||||
- 第 1 位(1):固定,表示错误
|
|
||||||
- 第 2-3 位(XX):模块/层次
|
|
||||||
- 第 3-4 位(YY):具体错误
|
|
||||||
|
|
||||||
**示例**:
|
|
||||||
|
|
||||||
- `10001`:通用错误(参数错误)
|
|
||||||
- `10010`:用户模块(用户不存在)
|
|
||||||
- `20003`:业务错误(密码强度不足)
|
|
||||||
:::
|
|
||||||
|
|
||||||
#### 分层错误码体系
|
|
||||||
|
|
||||||
```
|
|
||||||
错误码格式:1XXYY
|
|
||||||
- 第 1 位(1):固定,表示错误
|
|
||||||
- 第 2-3 位(XX):模块/层次
|
|
||||||
- 第 4-5 位(YY):具体错误
|
|
||||||
```
|
|
||||||
|
|
||||||
| 模块代码 | 模块名称 | 说明 |
|
|
||||||
| -------- | ---------- | ------------------------ |
|
|
||||||
| 00 | 通用 | 系统级、通用错误 |
|
|
||||||
| 10 | 用户模块 | 注册、登录、用户信息相关 |
|
|
||||||
| 11 | 认证授权 | Token、权限相关 |
|
|
||||||
| 20 | 商品模块 | 商品 CRUD、库存相关 |
|
|
||||||
| 30 | 订单模块 | 下单、支付、物流相关 |
|
|
||||||
| 40 | 支付模块 | 支付渠道、退款相关 |
|
|
||||||
| 50 | 营销模块 | 优惠券、活动相关 |
|
|
||||||
| 90 | 第三方服务 | 短信、邮件、云存储等 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. 版本控制:API 的"向后兼容"
|
## 1. 核心概念:RESTful 是什么?
|
||||||
|
|
||||||
### 6.1 为什么要做 API 版本控制?
|
REST (Representational State Transfer) 听起来很学术,其实核心就三句话:
|
||||||
|
|
||||||
<VersioningStrategyDemo />
|
1. **资源 (Resource)**:网络上的所有东西都是资源(用户、订单、商品)。
|
||||||
|
2. **统一接口 (Uniform Interface)**:用标准的 HTTP 方法(GET, POST, DELETE)来操作这些资源。
|
||||||
|
3. **无状态 (Stateless)**:每次请求都包含所有必要信息,服务器不记“你是谁”(除非你带了 Token)。
|
||||||
|
|
||||||
场景:你的电商系统已经上线,App 有 100 万用户。现在需要修改订单接口,添加一个新字段,同时废弃一个旧字段。
|
### 比喻:餐厅点餐
|
||||||
|
|
||||||
**如果不做版本控制**:
|
- **URL 是桌号**:`/tables/5` (资源地址)
|
||||||
|
- **HTTP 方法是动作**:
|
||||||
- 新 App 调用新接口 → 正常工作
|
- `GET`:看菜单
|
||||||
- 旧 App 调用新接口 → 字段缺失,崩溃
|
- `POST`:下单
|
||||||
- 用户投诉 → 老板震怒 → 你背锅
|
- `PUT`:换一桌菜
|
||||||
|
- `DELETE`:吃完走人
|
||||||
**正确的做法**:
|
|
||||||
|
|
||||||
```
|
|
||||||
/v1/orders - 旧接口,继续服务旧 App
|
|
||||||
/v2/orders - 新接口,新功能在这里
|
|
||||||
```
|
|
||||||
|
|
||||||
旧 App 继续调用 `/v1/orders`,新功能上线不会崩。等旧 App 用户都升级了,再考虑废弃 v1。
|
|
||||||
|
|
||||||
### 6.2 4 种版本控制策略
|
|
||||||
|
|
||||||
| 策略 | 示例 | 优点 | 缺点 | 推荐度 |
|
|
||||||
| ----------------------- | ------------------------------------- | -------------------------- | ---------------------------------- | -------- |
|
|
||||||
| **URL Path** | `/v1/users` | 最直观、易于缓存、文档清晰 | URL 变化 | ⭐⭐⭐⭐ |
|
|
||||||
| **Header** | `API-Version: v1` | URL 不变 | 不直观,难以缓存,需要读 Header 文档 | ⭐⭐ |
|
|
||||||
| **Content Negotiation** | `Accept: application/vnd.api.v1+json` | 标准 HTTP 规范 | 复杂,理解成本高 | ⭐⭐ |
|
|
||||||
| **Query Parameter** | `/users?version=v1` | 简单 | 不专业,容易被忽视,缓存麻烦 | ⭐ |
|
|
||||||
|
|
||||||
**推荐做法**:URL Path 版本控制,简单直观,行业主流。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. 总结:API 设计 checklist
|
## 2. 交互演示:RESTful API 全流程
|
||||||
|
|
||||||
### 7.1 设计阶段
|
别光听概念,我们来动手玩一下。
|
||||||
|
下面是一个模拟的“用户管理系统”。试着点击不同的场景,观察 **客户端发出了什么** 以及 **服务端返回了什么**。
|
||||||
|
|
||||||
- [ ] URL 设计符合 RESTful 规范(名词、复数、小写、连字符)
|
<RestfulApiFlow />
|
||||||
- [ ] HTTP 方法使用正确(GET/POST/PUT/PATCH/DELETE)
|
|
||||||
- [ ] 状态码选择恰当(2xx/4xx/5xx 区分清楚)
|
|
||||||
- [ ] 错误码体系设计完成(分层、易扩展)
|
|
||||||
- [ ] 响应结构标准化(code/message/data 统一)
|
|
||||||
- [ ] 版本控制策略确定(URL Path 推荐)
|
|
||||||
- [ ] 分页/排序/过滤参数设计统一
|
|
||||||
|
|
||||||
### 7.2 实现阶段
|
### 💡 观察重点
|
||||||
|
|
||||||
- [ ] 所有接口都有完善的 OpenAPI 文档
|
1. **URL 是名词**:注意看 URL 都是 `/users` 或者 `/users/1`,没有动词(如 `/getUsers`)。因为 HTTP 方法(GET/POST)已经表示了动作。
|
||||||
- [ ] 参数校验规则清晰(类型、长度、必填)
|
2. **状态码会说话**:
|
||||||
- [ ] 敏感信息脱敏处理(密码、Token 等)
|
- 创建成功返回 `201 Created`,而不是 200。
|
||||||
- [ ] 错误日志记录完整(带 request_id)
|
- 删除成功返回 `204 No Content`(没有 Body)。
|
||||||
- [ ] 接口性能监控到位(响应时间、错误率)
|
- 找不到返回 `404 Not Found`。
|
||||||
- [ ] 限流熔断策略配置(防刷、降级)
|
3. **复数形式**:通常使用 `/users` 而不是 `/user`,表示这是“用户集合”下的资源。
|
||||||
|
|
||||||
### 7.3 维护阶段
|
|
||||||
|
|
||||||
- [ ] 接口变更走评审流程(兼容性检查)
|
|
||||||
- [ ] 废弃接口有明确的 Sunset 计划
|
|
||||||
- [ ] 客户端接入文档及时更新
|
|
||||||
- [ ] 错误码文档随代码同步维护
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 名词对照表
|
## 3. 设计黄金法则
|
||||||
|
|
||||||
| 英文术语 | 中文对照 | 解释 |
|
### 3.1 URL 设计:让路径清晰
|
||||||
| -------------------------- | ---------------- | ----------------------------------------------------- |
|
|
||||||
| **REST** | 表述性状态传递 | 一种软件架构风格,用 URL 标识资源,用 HTTP 方法操作资源 |
|
| 法则 | 正确 ✅ | 错误 ❌ | 原因 |
|
||||||
| **RESTful** | 符合 REST 规范的 | 遵循 REST 架构风格设计的 API |
|
| :--- | :--- | :--- | :--- |
|
||||||
| **Endpoint** | 端点 | API 的具体 URL 地址,如 `/users` |
|
| **用名词,不用动词** | `GET /products` | `GET /getProducts` | HTTP 方法已经是动词了 |
|
||||||
| **Resource** | 资源 | REST 架构中的核心概念,网络上的任何事物都可抽象为资源 |
|
| **用复数** | `/users/1` | `/user/1` | 保持一致性,`/users` 代表集合 |
|
||||||
| **URI** | 统一资源标识符 | 标识资源的字符串,URL 是 URI 的一种 |
|
| **层级不要太深** | `/users/1/orders` | `/users/1/orders/2/items/3` | 超过 3 层建议拆分或用查询参数 |
|
||||||
| **HTTP Method** | HTTP 方法 | GET、POST、PUT、PATCH、DELETE 等 |
|
| **使用连字符** | `/user-profiles` | `/userProfiles` | URL 对大小写敏感,连字符更易读 |
|
||||||
| **Status Code** | 状态码 | HTTP 响应中的 3 位数字,表示请求的处理结果 |
|
|
||||||
| **Payload** | 载荷 | HTTP 请求或响应的主体数据 |
|
### 3.2 HTTP 方法:动作要有语义
|
||||||
| **Header** | 头部 | HTTP 请求或响应的元数据 |
|
|
||||||
| **Query String** | 查询字符串 | URL 中 `?` 后面的参数部分 |
|
- **GET** (查):**安全且幂等**。不管调用多少次,服务器状态不变。
|
||||||
| **Path Parameter** | 路径参数 | URL 路径中的变量,如 `/users/{id}` |
|
- **POST** (增):**不安全,不幂等**。调用 10 次可能创建 10 个用户。
|
||||||
| **Pagination** | 分页 | 大数据量时分批返回的机制 |
|
- **PUT** (改-全量):**幂等**。把 ID=1 的用户替换为新数据,替换 10 次结果一样。
|
||||||
| **Idempotency** | 幂等性 | 多次执行结果相同的特性 |
|
- **PATCH** (改-局部):通常用于只修改一个字段(如只改密码)。
|
||||||
| **Deprecation** | 弃用 | 标记即将废弃的功能或接口 |
|
- **DELETE** (删):**幂等**。删除 ID=1 的用户,删 1 次和删 10 次,结果都是“用户没了”。
|
||||||
| **Backward Compatibility** | 向后兼容 | 新版本兼容旧版本的接口调用 |
|
|
||||||
| **Rate Limiting** | 限流 | 限制单位时间内的请求数量 |
|
### 3.3 状态码:别只用 200
|
||||||
| **OpenAPI** | 开放 API 规范 | 描述 REST API 的标准格式(原 Swagger) |
|
|
||||||
| **SDK** | 软件开发工具包 | 封装 API 调用的开发工具包 |
|
| 类别 | 状态码 | 含义 | 场景 |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **2xx 成功** | 200 OK | 通用成功 | GET, PUT |
|
||||||
|
| | 201 Created | 创建成功 | POST |
|
||||||
|
| | 204 No Content | 成功但无返回 | DELETE |
|
||||||
|
| **4xx 客户端错** | 400 Bad Request | 参数错 | 必填项没填,格式不对 |
|
||||||
|
| | 401 Unauthorized | 未登录 | 没有 Token 或 Token 过期 |
|
||||||
|
| | 403 Forbidden | 无权限 | 普通用户想删管理员 |
|
||||||
|
| | 404 Not Found | 找不到 | URL 错了或 ID 不存在 |
|
||||||
|
| **5xx 服务端错** | 500 Internal Error | 崩了 | 代码抛异常了,数据库挂了 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 总结
|
||||||
|
|
||||||
|
好的 API 设计是**“自解释”**的。
|
||||||
|
当你的前端同事看到 `DELETE /api/orders/123`,他不需要问你,就应该知道:
|
||||||
|
1. 这是一个删除操作。
|
||||||
|
2. 操作对象是 ID 为 123 的订单。
|
||||||
|
3. 如果成功,应该收到 204 或 200。
|
||||||
|
4. 如果失败,应该去看状态码是 4xx 还是 5xx。
|
||||||
|
|
||||||
|
这就是**约定优于配置**的力量。
|
||||||
|
|||||||
@@ -1,348 +1,154 @@
|
|||||||
# AI 简史与核心概念
|
---
|
||||||
> 💡 **学习指南**:本章节通过交互式演示,带你回顾 AI 如何从“只会算数的机器”进化成“能写诗的艺术家”。
|
title: 'AI 简史:从符号逻辑到千亿参数大模型'
|
||||||
>
|
description: 'AI 发展 70 年,经历了三次浪潮、两次寒冬,最终融合为今天的大模型时代。'
|
||||||
> 我们将聚焦于三次核心的思维跃迁:从**教它规则**,到**让它模仿**,最终实现**让它创造**。同时,我们也会梳理关键的历史节点,带你理清技术发展的脉络。
|
---
|
||||||
|
|
||||||
|
# AI 简史:从符号逻辑到千亿参数大模型
|
||||||
|
|
||||||
|
AI 发展 70 年,经历了**三次浪潮、两次寒冬**,从符号主义的逻辑推演,到连接主义的神经网络,再到行为主义的强化学习,最终融合为今天的大模型时代。以下是清晰的发展脉络与关键里程碑。
|
||||||
|
|
||||||
<AiEvolutionDemo />
|
<AiEvolutionDemo />
|
||||||
|
|
||||||
### 关键里程碑 (Timeline)
|
---
|
||||||
|
|
||||||
<AIEvolutionTimelineDemo />
|
## 一、理论奠基与符号主义的诞生(1940s-1950s)
|
||||||
|
|
||||||
## 0. 引言:机器能思考吗?
|
### 核心人物与理论
|
||||||
|
|
||||||
1950 年,艾伦·图灵提出了一个问题:**"机器能思考吗?"**
|
- **1943 年**:沃伦・麦卡洛克与沃尔特・皮茨提出 **MP 神经元模型**,首次用数学描述神经网络
|
||||||
|
- **1950 年**:艾伦・图灵发表《计算机器与智能》,提出**图灵测试**,定义机器智能标准
|
||||||
|
- **1956 年**:**达特茅斯会议**,约翰・麦卡锡首次提出"人工智能"概念,标志 AI 学科正式诞生
|
||||||
|
|
||||||
为了回答这个问题,人类尝试了三种截然不同的解法:
|
::: tip 符号主义兴起
|
||||||
|
**符号主义**(逻辑主义/计算机学派)主张 **智能 = 符号推理**,将知识编码为符号,通过逻辑规则推导解决问题,是**自上而下**的智能模拟路径。
|
||||||
|
:::
|
||||||
|
|
||||||
1. **教它规则**(逻辑):像教小孩一样,把所有规则写给它。
|
### 早期突破
|
||||||
2. **让它模仿**(概率):给它看大量数据,让它自己找规律。
|
|
||||||
3. **让它创造**(生成):不仅能分类,还能根据理解创造新东西。
|
|
||||||
|
|
||||||
本教程将带你亲手体验这三个阶段。
|
- **1956 年**:纽厄尔和西蒙开发**逻辑理论家**(Logic Theorist),首个能证明数学定理的 AI 程序
|
||||||
|
- **1958 年**:麦卡锡发明 **LISP 语言**,成为 AI 研究的重要工具
|
||||||
|
- **1959 年**:乔治・德沃尔与约瑟夫・恩格尔伯格开发首台**工业机器人**,标志 AI 从理论走向应用
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 符号主义:教机器"守规矩"(20世纪50年代 - 80年代)
|
## 二、符号主义黄金时代与第一次 AI 浪潮(1960s-1970s)
|
||||||
|
|
||||||
早期的 AI 科学家认为:智慧就是**逻辑推理**。
|
### 专家系统的辉煌
|
||||||
只要我们把世界上的所有知识都写成 `If...Then...` 的规则,机器就能像人一样聪明。
|
|
||||||
|
|
||||||
这被称为**专家系统**或**符号主义人工智能**。
|
符号主义在**专家系统**领域取得巨大成功,通过将领域专家知识编码为规则库,解决特定领域复杂问题。
|
||||||
|
|
||||||
### 1.1 什么是"基于规则"?
|
| 时间 | 标志性成果 | 意义 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **1965 年** | Dendral 系统 | 首个专家系统,用于化学分子结构分析 |
|
||||||
|
| **1977 年** | MYCIN 系统 | 诊断血液感染的专家系统,准确率达 69% |
|
||||||
|
| **1980 年** | XCON 系统 | 为 DEC 公司配置计算机,节省 4000 万美元/年 |
|
||||||
|
|
||||||
就像教小孩:
|
### 第一次 AI 寒冬(1974-1980)
|
||||||
|
|
||||||
- 如果看到红灯,就停下。
|
::: warning ❄️ 第一次 AI 寒冬
|
||||||
- 如果下雨,就带伞。
|
符号主义局限性显现:
|
||||||
|
- **知识获取瓶颈**:规则需人工编写,无法自动获取
|
||||||
|
- **脆性问题**:难以处理例外情况,稍微偏离规则就崩溃
|
||||||
|
- **计算能力不足**:当时的硬件无法支撑复杂推理
|
||||||
|
|
||||||
在代码中,这表现为:
|
美国 DARPA 削减 AI 研究经费,AI 进入第一次低谷期。
|
||||||
|
:::
|
||||||
```javascript
|
|
||||||
// 基于规则的 AI 示例
|
|
||||||
function decideTrafficLight(color) {
|
|
||||||
if (color === 'red') {
|
|
||||||
return 'stop'
|
|
||||||
} else if (color === 'yellow') {
|
|
||||||
return 'caution'
|
|
||||||
} else if (color === 'green') {
|
|
||||||
return 'go'
|
|
||||||
} else {
|
|
||||||
return 'unknown'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.2 专家系统的巅峰:MYCIN
|
|
||||||
|
|
||||||
1970 年代,斯坦福大学开发的 MYCIN 系统能诊断血液感染,准确率达到专家水平。
|
|
||||||
|
|
||||||
它的工作原理是:
|
|
||||||
|
|
||||||
```lisp
|
|
||||||
// MYCIN 系统的规则示例 (伪代码)
|
|
||||||
(IF
|
|
||||||
(organism IS gram-positive) AND
|
|
||||||
(morphology IS coccus) AND
|
|
||||||
(growth-chains IS chains)
|
|
||||||
THEN
|
|
||||||
(identity IS 0.7 streptococcus))
|
|
||||||
```
|
|
||||||
|
|
||||||
_数据示例 (知识库格式)_:
|
|
||||||
|
|
||||||
```json
|
|
||||||
// 专家系统知识库示例
|
|
||||||
{
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"id": "RULE-001",
|
|
||||||
"conditions": ["traffic_light == red", "speed > 0"],
|
|
||||||
"action": "brake",
|
|
||||||
"priority": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "RULE-002",
|
|
||||||
"conditions": ["weather == rainy", "visibility < 100m"],
|
|
||||||
"action": "turn_on_lights",
|
|
||||||
"priority": 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
// 系统按优先级依次匹配规则,遇到匹配就执行
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.3 交互演示:规则 vs 学习
|
|
||||||
|
|
||||||
下方的演示展示了两种方式的区别。
|
|
||||||
|
|
||||||
- **左边 (规则)**:你必须显式地写代码 `if (size > 6)`。如果世界变了(比如苹果变小了),你的代码就失效了。
|
|
||||||
- **右边 (学习)**:你不需要写规则。你只需要给机器看一堆苹果和樱桃的数据,点击 **Train**,它自己会"悟"出一个分界线。
|
|
||||||
|
|
||||||
<RuleBasedVsLearningDemo />
|
|
||||||
|
|
||||||
### 1.4 符号主义的局限性
|
|
||||||
|
|
||||||
规则看起来很完美,但现实世界太复杂了。
|
|
||||||
|
|
||||||
<CombinatorialExplosionDemo />
|
|
||||||
|
|
||||||
**问题 1:组合爆炸**
|
|
||||||
|
|
||||||
- 试图写下"识别猫"的所有规则?不可能!
|
|
||||||
- "有胡须"?老鼠也有。
|
|
||||||
- "有尖耳朵"?狗也有。
|
|
||||||
- "毛茸茸的"?兔子也是。
|
|
||||||
- 现实世界有无限边界情况,规则永远写不完。
|
|
||||||
|
|
||||||
**问题 2:无法处理不确定性**
|
|
||||||
|
|
||||||
- 如果规则冲突怎么办?
|
|
||||||
- 如果遇到没见过的情况怎么办?
|
|
||||||
- 规则系统很"脆弱",缺少人类常识。
|
|
||||||
|
|
||||||
> ⚠️ **教训**:试图用有限规则描述无限现实,注定失败。这导致了 1980 年代的**AI 寒冬**。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 连接主义:教机器"像人脑一样思考"(21世纪10年代至今)
|
## 三、专家系统复兴与第二次 AI 浪潮(1980s)
|
||||||
|
|
||||||
既然规则写不完,不如换个思路:**让机器自己学**?
|
### 商业应用爆发
|
||||||
科学家开始模仿人脑的结构——**神经元**。
|
|
||||||
|
|
||||||
这就是**连接主义**的核心思想。
|
- 日本"**第五代计算机计划**"(1982)推动全球 AI 投资热潮
|
||||||
|
- 美国 DEC、IBM 等公司推出商用专家系统开发工具
|
||||||
|
- 符号主义达到巅峰,成为 AI 领域绝对主流
|
||||||
|
|
||||||
### 2.1 人脑的启示
|
### 连接主义的早期尝试
|
||||||
|
|
||||||
人脑有约 860 亿个神经元,每个神经元通过突触连接成千上万个其他神经元。
|
|
||||||
|
|
||||||
**关键发现**:
|
|
||||||
|
|
||||||
- 单个神经元很"笨"(只是兴奋或不兴奋)
|
|
||||||
- 但几百亿个神经元连在一起,就产生了智能
|
|
||||||
|
|
||||||
### 2.2 感知机
|
|
||||||
|
|
||||||
1957 年,康奈尔大学的 Frank Rosenblatt 发明了感知机——这是最简单的人工神经元。
|
|
||||||
|
|
||||||
它的工作原理:
|
|
||||||
|
|
||||||
1. **接收输入**:从多个"突触"接收信号($x_1, x_2, ...$)
|
|
||||||
2. **加权求和**:每个输入有对应的**权重**,代表重要性
|
|
||||||
3. **激活判断**:如果总和超过某个**阈值(偏置)**,就激活(输出 1)
|
|
||||||
|
|
||||||
```text
|
|
||||||
如果不带公式,怎么理解?
|
|
||||||
|
|
||||||
简单来说就是:打分机制。
|
|
||||||
总分 = (输入1 × 权重1) + (输入2 × 权重2) + ... + 基础分
|
|
||||||
如果 总分 > 0,输出 1 (激活)
|
|
||||||
否则,输出 0 (静默)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.3 交互演示:玩转神经元
|
|
||||||
|
|
||||||
调整下方的**权重**和**偏置**,看看能否控制神经元的输出。
|
|
||||||
|
|
||||||
- **权重($w$)**:代表输入的"重要性"。$w$ 越大,这个输入对结果影响越大。
|
|
||||||
- **偏置($b$)**:代表神经元的"门槛"。$b$ 越小,神经元越容易兴奋(输出 1)。
|
|
||||||
|
|
||||||
<PerceptronDemo />
|
<PerceptronDemo />
|
||||||
|
|
||||||
### 2.4 从单神经元到深度学习
|
- **1958 年**:罗森布拉特发明**感知机**,首个可学习的神经网络模型
|
||||||
|
- **1969 年**:明斯基与佩珀特出版《感知机》,指出单层感知机无法解决**异或问题**,导致连接主义研究陷入停滞
|
||||||
|
|
||||||
单个神经元能做什么?只能做简单分类(比如判断"苹果还是樱桃")。
|
### 第二次 AI 寒冬(1987-1993)
|
||||||
|
|
||||||
但如果把神经元分层连接:
|
::: warning ❄️ 第二次 AI 寒冬
|
||||||
|
- 专家系统**维护成本高昂**,难以扩展到复杂领域
|
||||||
|
- 个人电脑崛起,第五代计算机计划失败
|
||||||
|
- AI 市场崩盘,研究经费再次大幅削减
|
||||||
|
:::
|
||||||
|
|
||||||
```
|
---
|
||||||
输入层 (图片像素)
|
|
||||||
↓
|
|
||||||
隐藏层 1 (识别边缘)
|
|
||||||
↓
|
|
||||||
隐藏层 2 (识别形状)
|
|
||||||
↓
|
|
||||||
隐藏层 3 (识别物体部件)
|
|
||||||
↓
|
|
||||||
输出层 (识别物体)
|
|
||||||
```
|
|
||||||
|
|
||||||
这就是**神经网络**。当网络有很多层时,我们称之为**深度学习**。
|
## 四、机器学习兴起与连接主义复苏(1990s-2000s)
|
||||||
|
|
||||||
<NeuralNetworkVisualizationDemo />
|
### 符号主义衰落,机器学习崛起
|
||||||
|
|
||||||
### 2.5 神经网络是如何学习的?
|
- **1997 年**:IBM **深蓝** 击败国际象棋世界冠军卡斯帕罗夫,是符号主义最后的辉煌
|
||||||
|
- 同时,**统计机器学习**开始取代基于规则的方法,支持向量机(SVM)、决策树等算法成为主流
|
||||||
|
|
||||||
不像专家系统需要人写规则,神经网络通过**看数据**自己学。
|
### 连接主义的重生
|
||||||
|
|
||||||
**学习过程(反向传播)**:
|
|
||||||
|
|
||||||
1. **前向传播**:输入数据,得到预测结果
|
|
||||||
2. **计算误差**:对比预测和真实答案
|
|
||||||
3. **反向传播**:根据误差调整每个神经元的权重
|
|
||||||
4. **重复**:重复几百万次,直到误差足够小
|
|
||||||
|
|
||||||
<BackpropagationDemo />
|
<BackpropagationDemo />
|
||||||
|
|
||||||
_数据示例 (训练数据格式)_:
|
- **1986 年**:鲁梅尔哈特等人提出**反向传播算法**,解决多层神经网络训练难题
|
||||||
|
- **1997 年**:李飞飞创立 **ImageNet 数据集**,为后续深度学习提供数据基础
|
||||||
```json
|
- **2006 年**:杰弗里・辛顿提出**深度信念网络**,通过逐层预训练解决梯度消失问题,开启深度学习时代
|
||||||
// 图像分类训练数据示例
|
|
||||||
{
|
|
||||||
"dataset": "cats_vs_dogs",
|
|
||||||
"samples": [
|
|
||||||
{
|
|
||||||
"image": "cat_001.jpg",
|
|
||||||
"label": 1, // 1 = 猫
|
|
||||||
"features": [0.2, 0.8, 0.5, ...] // 提取的特征向量
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"image": "dog_001.jpg",
|
|
||||||
"label": 0, // 0 = 狗
|
|
||||||
"features": [0.7, 0.3, 0.9, ...]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
// 神经网络会自动学习:什么样的 feature 组合更可能是猫
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.6 连接主义的突破:2012 年 AlexNet
|
|
||||||
|
|
||||||
2012 年,AlexNet 在 ImageNet 竞赛中以压倒性优势夺冠,标志着深度学习时代的到来。
|
|
||||||
|
|
||||||
**关键因素**:
|
|
||||||
|
|
||||||
- **大数据**:ImageNet 提供了 1400 万张标注图片
|
|
||||||
- **大算力**:GPU 的并行计算能力让训练深度网络成为可能
|
|
||||||
- **新算法**:ReLU 激活函数、Dropout 正则化等技术突破
|
|
||||||
|
|
||||||
### 2.7 连接主义的局限
|
|
||||||
|
|
||||||
深度学习很强大,但也不是完美的:
|
|
||||||
|
|
||||||
- **黑盒问题**:虽然能识别猫,但我们说不清"它是怎么识别的"
|
|
||||||
- **数据饥渴**:需要海量标注数据,获取成本高
|
|
||||||
- **缺乏常识**:能识别出这是“猫”,但理解不了“猫喜欢抓老鼠”或“猫通常怕狗”这种常识关系(因为它只是在做像素级的统计匹配,而非真正的概念理解)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 生成式人工智能:机器有了"创造力"(21世纪20年代至今)
|
## 五、深度学习革命与连接主义主导(2010s)
|
||||||
|
|
||||||
以前的 AI 主要是**判别式**(这是猫还是狗?)。
|
### 关键技术突破
|
||||||
现在的 AI 是**生成式**(画一只猫!)。
|
|
||||||
|
|
||||||
这一切的背后,是 **Transformer** 架构的诞生。它让 AI 学会了理解上下文,学会了"举一反三"。
|
<NeuralNetworkVisualizationDemo />
|
||||||
|
|
||||||
### 3.1 从"识别"到"创造"
|
| 时间 | 突破 | 影响 |
|
||||||
|
| --- | --- | --- |
|
||||||
传统深度学习(判别式模型):
|
| **2012 年** | AlexNet 在 ImageNet 竞赛中错误率降至 15.3% | 标志深度学习超越传统方法,引爆计算机视觉革命 |
|
||||||
|
| **2014 年** | GAN(生成对抗网络)提出 | AI 具备生成逼真图像、音频能力,推动生成式 AI 发展 |
|
||||||
- 输入:一张图
|
| **2015 年** | ResNet(残差网络)解决深层网络训练难题 | 网络层数突破 1000 层,进一步提升模型性能 |
|
||||||
- 输出:这是猫(概率 98%)
|
| **2016 年** | AlphaGo 击败围棋世界冠军李世石 | 结合深度强化学习与蒙特卡洛树搜索,实现复杂决策能力 |
|
||||||
|
| **2017 年** | **Transformer 架构**发布 | 基于自注意力机制,解决长距离依赖问题,为大模型奠定基础 |
|
||||||
生成式 AI:
|
|
||||||
|
|
||||||
- 输入:一句话"一只戴着墨镜的猫"
|
|
||||||
- 输出:生成一张对应的图片
|
|
||||||
|
|
||||||
<DiscriminativeVsGenerativeDemo />
|
|
||||||
|
|
||||||
### 3.2 Transformer:AI 的"瑞士军刀"
|
|
||||||
|
|
||||||
2017 年,Google 发表论文《Attention Is All You Need》(注意力机制就是你所需的全部),提出 Transformer 架构。
|
|
||||||
|
|
||||||
它的核心创新:**注意力机制**
|
|
||||||
|
|
||||||
**原理**:让模型在处理一个词时,能"关注"到句子中其他相关的词。
|
|
||||||
|
|
||||||
例如:"小明把苹果给了**他**的母亲"
|
|
||||||
|
|
||||||
当模型处理"他"时,注意力机制会让它关注到"小明",从而理解"他"指代的是小明。
|
|
||||||
|
|
||||||
<AttentionMechanismDemo />
|
<AttentionMechanismDemo />
|
||||||
|
|
||||||
### 3.3 GPT:从文本生成到通用智能
|
::: tip 行为主义的发展
|
||||||
|
**行为主义**(进化主义)主张智能来自与环境的互动,通过试错学习优化行为,**强化学习**是其核心技术。AlphaGo 就是深度学习与强化学习结合的代表作。
|
||||||
|
:::
|
||||||
|
|
||||||
2018 年,OpenAI 发布 GPT-1(生成式预训练变换器)。
|
---
|
||||||
|
|
||||||
**核心思想**:
|
## 六、大模型时代与通用智能曙光(2018 至今)
|
||||||
|
|
||||||
1. **预训练**:在海量文本上学习"预测下一个词"
|
### 预训练模型范式确立
|
||||||
2. **微调**:在特定任务上调整(比如问答、翻译)
|
|
||||||
|
|
||||||
从 GPT-1 (2018) → GPT-2 (2019) → GPT-3 (2020) → GPT-4 (2023)
|
|
||||||
|
|
||||||
- 参数量从 1.17 亿 → 1750 亿 → 1.8 万亿(估计)
|
|
||||||
- 能力从文本生成 → 多模态(图片、音频、视频)
|
|
||||||
|
|
||||||
<GPTEvolutionDemo />
|
<GPTEvolutionDemo />
|
||||||
|
|
||||||
### 3.4 生成式人工智能的局限
|
- **2018 年**:OpenAI 发布 **GPT-1**(1.17 亿参数),谷歌发布 **BERT**,确立"**预训练 + 微调**"新范式
|
||||||
|
- **2019 年**:**GPT-2**(15 亿参数)展现惊人的文本生成能力,引发对 AI 伦理的广泛讨论
|
||||||
|
- **2020 年**:**GPT-3**(1750 亿参数)通过"暴力美学"展现**涌现能力**,无需微调即可完成多种任务
|
||||||
|
|
||||||
虽然强大,但也存在问题:
|
### 生成式 AI 爆发
|
||||||
|
|
||||||
- **幻觉**:一本正经地胡说八道
|
- **2022 年 11 月**:**ChatGPT**(GPT-3.5)发布,通过 RLHF(人类反馈强化学习)大幅提升对话能力,成为现象级产品
|
||||||
- **偏见放大**:从训练数据中学到人类偏见
|
- **2023 年 3 月**:**GPT-4** 发布,具备**多模态能力**(文本 + 图像),进一步提升逻辑推理与安全性
|
||||||
- **不可解释**:仍然是个黑盒,不知道内部怎么运作
|
- **2023 年**:Stable Diffusion、Midjourney 等图像生成模型兴起,多模态大模型成为主流
|
||||||
|
- **2024 年**:**Sora** 等视频生成模型发布,AI 生成能力扩展到动态内容领域
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. AI 范式对比总结
|
## 七、AI 三大学派的融合与未来展望
|
||||||
|
|
||||||
| 时代 | 核心理念 | 代表产物 | 优势 | 局限 |
|
<DiscriminativeVsGenerativeDemo />
|
||||||
| :----------------- | :-------------- | :-------------------------- | :----------------------- | :--------------------------- |
|
|
||||||
| **符号主义** | 智慧 = 规则 | 深蓝(下棋)、MYCIN(诊断) | 可解释性强,逻辑清晰 | 无法处理模糊、复杂的现实世界 |
|
|
||||||
| **连接主义** | 智慧 = 神经网络 | AlphaGo、人脸识别 | 能处理复杂模式,性能强大 | 需要海量数据,是个"黑盒" |
|
|
||||||
| **生成式人工智能** | 智慧 = 通用理解 | ChatGPT、Midjourney | 能创造新内容,理解上下文 | 幻觉、偏见、不可解释 |
|
|
||||||
|
|
||||||
**AI 的进化趋势**:
|
### 未来趋势
|
||||||
|
|
||||||
1. **从人工到自动**:从人写规则 → 机器自动学习
|
- **多模态融合**:文本、图像、音频、视频等信息的统一处理
|
||||||
2. **从单一到通用**:从下棋专用 → 通用人工智能
|
- **高效大模型**:降低训练成本,提升推理效率,推动边缘部署
|
||||||
3. **从判别到生成**:从分类识别 → 创造新内容
|
- **可解释 AI**:解决黑箱问题,增强 AI 可信度
|
||||||
|
- **AGI 探索**:从专用智能向通用人工智能迈进,追求更全面的人类智能模拟
|
||||||
|
|
||||||
> 关于大语言模型的详细原理,请移步下一章:[大语言模型入门](./llm-intro.md)
|
|
||||||
|
|
||||||
---
|
AI 的发展是一条**螺旋式上升**的道路,每个时代的技术都为后续突破奠定基础。今天的大模型并非完全抛弃符号主义,而是在连接主义框架下,通过海量数据学习到了类似符号推理的能力,实现了**不同学派思想的深度融合**。
|
||||||
|
|
||||||
## 5. 名词速查表
|
|
||||||
|
|
||||||
| 名词 | 英文原文 | 解释 |
|
|
||||||
| :----------------- | :--------------------------------- | :-------------------------------------------------------------------------------------------------- |
|
|
||||||
| **符号主义** | Symbolic AI | 基于规则的人工智能。认为智能可以用逻辑规则表示。代表:专家系统、深蓝。 |
|
|
||||||
| **专家系统** | Expert Systems | 符号主义的代表产物。通过人工编写大量规则来模拟专家决策。代表:MYCIN(医疗诊断)。 |
|
|
||||||
| **连接主义** | Connectionism | 基于神经网络的人工智能。模仿人脑神经元连接结构,通过数据自动学习。 |
|
|
||||||
| **感知机** | Perceptron | 最简单的神经网络单元。接收多个输入,加权求和后通过激活函数输出。 |
|
|
||||||
| **神经网络** | Neural Network | 由多个感知机分层连接组成的模型。通过调整权重来学习数据中的模式。 |
|
|
||||||
| **深度学习** | Deep Learning | 使用**多层**神经网络的学习方法。能自动提取层次化特征(边缘 → 形状 → 物体)。 |
|
|
||||||
| **反向传播** | Backpropagation | 神经网络的学习算法。通过计算预测误差,反向调整每层的权重,逐步优化模型。 |
|
|
||||||
| **生成式人工智能** | Generative AI | 能**创造新内容**的人工智能(文本、图片、音频等),而非仅仅是分类或识别。代表:ChatGPT、Midjourney。 |
|
|
||||||
| **判别式人工智能** | Discriminative AI | 用于**分类**的人工智能(如:这是猫还是狗?)。传统深度学习大多是判别式的。 |
|
|
||||||
| **Transformer** | Transformer | 2017 年由 Google 提出的架构,基于注意力机制。是现代大语言模型(GPT、BERT)的基础。 |
|
|
||||||
| **注意力机制** | Attention Mechanism | 让模型在处理一个元素时,能动态"关注"其他相关元素的技术。是 Transformer 的核心。 |
|
|
||||||
| **GPT** | Generative Pre-trained Transformer | OpenAI 的系列模型。通过"预训练 + 微调"范式,在大量文本上学习生成能力。 |
|
|
||||||
| **预训练** | Pre-training | 在大规模无标注数据上进行初步训练,学习通用知识(如语言规律)。 |
|
|
||||||
| **微调** | Fine-tuning | 在预训练模型基础上,使用特定任务的小规模数据进行调整,使模型适应具体应用。 |
|
|
||||||
| **幻觉** | Hallucination | 生成式人工智能模型"自信地编造错误内容"的现象。如 ChatGPT 编造不存在的论文或事实。 |
|
|
||||||
| **通用人工智能** | Artificial General Intelligence | 像人类一样具备多领域智能、能自主学习推理的人工智能(尚未实现)。 |
|
|
||||||
|
|||||||
@@ -1,3 +1,318 @@
|
|||||||
# AI 协议(MCP 等)
|
# AI Agent 协议(MCP & A2A)
|
||||||
|
|
||||||
> 待实现
|
::: tip 核心问题
|
||||||
|
**AI Agent 如何与外部世界"对话"?** 就像互联网需要 HTTP 协议,AI Agent 也需要标准化的通信协议。本章介绍两个最主流的 Agent 协议:MCP 和 A2A,它们分别解决了 AI 与工具、Agent 与 Agent 之间的通信问题。
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 什么是协议?
|
||||||
|
|
||||||
|
在计算机领域,**协议(Protocol)** 是一套标准化的规则和约定,让不同的系统、程序能够相互"理解"和"通信"。
|
||||||
|
|
||||||
|
### 0.1 为什么需要协议?
|
||||||
|
|
||||||
|
想象一个场景:你给朋友寄快递,需要填写地址。如果每个人写的地址格式都不一样,快递员就没法投递。协议就是规定了"地址怎么写"的标准——省、市、区、街道、门牌号,按这个格式写,谁都能看懂。
|
||||||
|
|
||||||
|
计算机也是一样。两个程序要通信,必须约定好:
|
||||||
|
- 数据格式是什么?(JSON?二进制?)
|
||||||
|
- 怎么建立连接?(握手流程)
|
||||||
|
- 出错了怎么办?(错误处理)
|
||||||
|
|
||||||
|
### 0.2 计算机中常见的协议
|
||||||
|
|
||||||
|
| 协议 | 作用 | 你每天都在用 |
|
||||||
|
|------|------|-------------|
|
||||||
|
| **HTTP** | 网页传输协议 | 浏览器打开网页 |
|
||||||
|
| **HTTPS** | 加密的 HTTP | 网银、支付页面 |
|
||||||
|
| **TCP/IP** | 互联网基础协议 | 所有网络通信 |
|
||||||
|
| **DNS** | 域名解析协议 | 把 `google.com` 变成 IP 地址 |
|
||||||
|
| **SMTP** | 邮件发送协议 | 发送邮件 |
|
||||||
|
| **WebSocket** | 双向实时通信 | 聊天软件、在线游戏 |
|
||||||
|
| **SSH** | 安全远程登录 | 连接服务器 |
|
||||||
|
| **FTP** | 文件传输协议 | 上传/下载文件 |
|
||||||
|
|
||||||
|
这些协议构成了互联网的基石。没有它们,你无法浏览网页、发送邮件、观看视频。
|
||||||
|
|
||||||
|
### 0.3 协议的价值
|
||||||
|
|
||||||
|
协议的核心价值是**标准化**和**互操作性**:
|
||||||
|
|
||||||
|
- **标准化**:大家都按同一套规则办事,减少沟通成本
|
||||||
|
- **互操作性**:不同厂商、不同技术栈的系统可以无缝对接
|
||||||
|
|
||||||
|
比如 HTTP 协议,让 Chrome 浏览器可以访问 Nginx 服务器,让 Python 爬虫可以抓取 Java 网站的数据。不需要 Chrome 和 Nginx 互相"认识",只要都遵守 HTTP 协议就行。
|
||||||
|
|
||||||
|
### 0.4 AI Agent 也需要协议
|
||||||
|
|
||||||
|
AI Agent 要真正"干活",需要:
|
||||||
|
- 调用外部工具(查天气、发邮件、操作数据库)
|
||||||
|
- 与其他 Agent 协作(分工合作完成复杂任务)
|
||||||
|
|
||||||
|
这就需要标准化的协议来规定"AI 怎么调用工具"、"Agent 之间怎么对话"。这就是 **MCP** 和 **A2A** 的由来。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Agent 协议的层次
|
||||||
|
|
||||||
|
在深入了解具体协议之前,让我们先看看 Agent 生态中的通信层次:
|
||||||
|
|
||||||
|
| 层级 | 协议 | 解决的问题 | 类比 |
|
||||||
|
|------|------|-----------|------|
|
||||||
|
| **1** | Function Call | AI 如何调用本地函数 | 大脑发出指令 |
|
||||||
|
| **2** | **MCP** | AI 如何连接外部工具和数据源 | USB-C 接口 |
|
||||||
|
| **3** | **A2A** | Agent 之间如何协作通信 | 企业微信 |
|
||||||
|
|
||||||
|
::: tip 逐行解读这张表
|
||||||
|
**第1层(Function Call)**:这是大模型最基础的能力——通过输出结构化数据(JSON)来触发函数执行。它是"协议"的基础,但本身更像是一种能力而非标准协议。
|
||||||
|
|
||||||
|
**第2层(MCP)**:Model Context Protocol,由 Anthropic 于 2024 年 11 月发布。它标准化了 AI 与外部工具、数据源的连接方式,就像 USB-C 统一了各种设备的充电接口。
|
||||||
|
|
||||||
|
**第3层(A2A)**:Agent-to-Agent Protocol,由 Google 于 2025 年 4 月发布。它让不同的 Agent 能够相互发现、通信和协作,就像企业微信让同事之间可以发任务、聊天。
|
||||||
|
:::
|
||||||
|
|
||||||
|
本章重点介绍第 2、3 层的两个正式协议:MCP 和 A2A。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. MCP (Model Context Protocol)
|
||||||
|
|
||||||
|
### 2.1 协议基本信息
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| **全称** | Model Context Protocol |
|
||||||
|
| **发起方** | Anthropic |
|
||||||
|
| **发布时间** | 2024 年 11 月 25 日 |
|
||||||
|
| **官方文档** | [modelcontextprotocol.io](https://modelcontextprotocol.io) |
|
||||||
|
| **开源协议** | MIT License |
|
||||||
|
| **GitHub** | [github.com/modelcontextprotocol](https://github.com/modelcontextprotocol) |
|
||||||
|
|
||||||
|
::: tip 为什么叫"Context Protocol"?
|
||||||
|
**Context(上下文)** 是大模型理解任务的关键。MCP 的核心思想是:**让 AI 能够动态获取所需的上下文信息**,而不是把所有信息都塞进 Prompt。
|
||||||
|
|
||||||
|
比如,AI 需要读取一个文件时,不需要你把文件内容复制粘贴给它,而是通过 MCP 直接访问文件系统。
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 2.2 发布的背景
|
||||||
|
|
||||||
|
2024 年,随着 Claude 3.5 Sonnet 的发布,Anthropic 发现一个问题:**每个工具都要单独集成**。
|
||||||
|
|
||||||
|
想象一下:
|
||||||
|
- 你想让 AI 读取 GitHub 仓库 → 要写 GitHub 集成代码
|
||||||
|
- 你想让 AI 查询数据库 → 要写数据库集成代码
|
||||||
|
- 你想让 AI 操作文件系统 → 要写文件系统集成代码
|
||||||
|
|
||||||
|
每个集成都要重复写类似的代码:认证、错误处理、数据转换……
|
||||||
|
|
||||||
|
Anthropic 在官方博客中写道:
|
||||||
|
> "We're introducing the Model Context Protocol (MCP), an open protocol that standardizes how applications provide context to LLMs."
|
||||||
|
|
||||||
|
**核心目标**:让工具开发者写一次代码,所有支持 MCP 的 AI 应用都能使用。
|
||||||
|
|
||||||
|
### 2.3 MCP 是什么?
|
||||||
|
|
||||||
|
<McpVisualDemo />
|
||||||
|
|
||||||
|
**三大核心能力**:
|
||||||
|
|
||||||
|
| 能力 | 英文 | 作用 | 示例 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| **工具** | Tools | AI 可以调用的功能 | 查询天气、发送邮件 |
|
||||||
|
| **资源** | Resources | AI 可以读取的数据 | 文件内容、数据库记录 |
|
||||||
|
| **提示** | Prompts | 预定义的提示模板 | 代码审查模板、写作模板 |
|
||||||
|
|
||||||
|
### 2.4 MCP 的内部实现
|
||||||
|
|
||||||
|
<McpDetailedDemo />
|
||||||
|
|
||||||
|
### 2.5 类比理解:USB-C 接口
|
||||||
|
|
||||||
|
MCP 就像 **USB-C 接口**:
|
||||||
|
|
||||||
|
- **以前**:每个设备都有自己的充电口(圆口、扁口、磁吸……)
|
||||||
|
- **现在**:USB-C 统一了所有设备的充电和数据传输
|
||||||
|
- **MCP**:统一了 AI 与所有工具的连接方式
|
||||||
|
|
||||||
|
工具开发者只需要实现一次 MCP Server,所有支持 MCP 的 AI 应用(Claude、Cursor、Windsurf 等)都能直接使用。
|
||||||
|
|
||||||
|
### 2.6 MCP 的典型应用场景
|
||||||
|
|
||||||
|
| 场景 | 说明 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| **本地文件操作** | 让 AI 读取/修改本地文件 | 读取代码库、分析日志文件 |
|
||||||
|
| **数据库查询** | 让 AI 直接查询数据库 | SQL 查询、数据分析 |
|
||||||
|
| **API 调用** | 让 AI 调用第三方服务 | GitHub API、Slack、邮件 |
|
||||||
|
| **开发工具集成** | 让 AI 使用开发工具 | Git 操作、终端命令 |
|
||||||
|
|
||||||
|
**实际案例**:
|
||||||
|
- **Cursor/Windsurf**:通过 MCP 连接文件系统、Git、终端
|
||||||
|
- **Claude Desktop**:通过 MCP 连接笔记软件、邮件客户端
|
||||||
|
- **自动化脚本**:让 AI 执行自动化任务(备份、部署、数据同步)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. A2A (Agent-to-Agent Protocol)
|
||||||
|
|
||||||
|
### 3.1 协议基本信息
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| **全称** | Agent-to-Agent Protocol |
|
||||||
|
| **发起方** | Google |
|
||||||
|
| **发布时间** | 2025 年 4 月 9 日 |
|
||||||
|
| **官方文档** | [google.github.io/A2A](https://google.github.io/A2A) |
|
||||||
|
| **开源协议** | Apache 2.0 |
|
||||||
|
| **GitHub** | [github.com/google/A2A](https://github.com/google/A2A) |
|
||||||
|
|
||||||
|
::: tip 为什么是 Google 发起?
|
||||||
|
Google 在 Cloud Next 2025 大会上发布 A2A,与其企业级 AI 战略密切相关。
|
||||||
|
|
||||||
|
Google 认为:未来的企业 AI 不是单个超级 Agent,而是**多个专业 Agent 协作**——有的负责数据分析,有的负责代码生成,有的负责文档处理。
|
||||||
|
|
||||||
|
这些 Agent 需要一种标准化的方式相互通信,A2A 应运而生。
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 3.2 发布的背景
|
||||||
|
|
||||||
|
MCP 解决了"AI 如何连接工具"的问题,但还有一个问题:**多个 Agent 如何协作?**
|
||||||
|
|
||||||
|
想象一个场景:
|
||||||
|
- Agent A 是"需求分析专家"
|
||||||
|
- Agent B 是"代码生成专家"
|
||||||
|
- Agent C 是"测试专家"
|
||||||
|
|
||||||
|
用户说:"帮我开发一个登录功能"
|
||||||
|
|
||||||
|
Agent A 分析需求后,需要把任务分配给 Agent B;Agent B 写完代码后,需要让 Agent C 测试。它们之间如何通信?
|
||||||
|
|
||||||
|
Google 在官方博客中写道:
|
||||||
|
> "A2A is an open protocol that enables AI agents to communicate with each other, facilitating collaboration across different frameworks and vendors."
|
||||||
|
|
||||||
|
**核心目标**:让不同厂商、不同框架开发的 Agent 能够无缝协作。
|
||||||
|
|
||||||
|
### 3.3 A2A 是什么?
|
||||||
|
|
||||||
|
<A2AVisualDemo />
|
||||||
|
|
||||||
|
**三大核心概念**:
|
||||||
|
|
||||||
|
| 概念 | 英文 | 作用 | 类比 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| **Agent Card** | Agent 名片 | 描述 Agent 的能力 | 员工工牌 |
|
||||||
|
| **Task** | 任务 | 要执行的工作单元 | 工单 |
|
||||||
|
| **Message** | 消息 | Agent 之间的通信内容 | 聊天记录 |
|
||||||
|
|
||||||
|
### 3.4 A2A 的内部实现
|
||||||
|
|
||||||
|
<A2ADetailedDemo />
|
||||||
|
|
||||||
|
### 3.5 类比理解:企业微信
|
||||||
|
|
||||||
|
A2A 就像 **企业微信**:
|
||||||
|
|
||||||
|
- **Agent Card**:每个人的名片,显示姓名、部门、职责
|
||||||
|
- **发任务**:@某人,分配一个任务
|
||||||
|
- **聊天沟通**:任务执行过程中可以随时沟通
|
||||||
|
- **任务追踪**:能看到任务的进度和状态
|
||||||
|
|
||||||
|
不同的 Agent 就像不同的同事,A2A 让它们能够协作完成复杂项目。
|
||||||
|
|
||||||
|
### 3.6 A2A 的典型应用场景
|
||||||
|
|
||||||
|
| 场景 | 说明 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| **软件开发** | 多 Agent 协作完成开发任务 | 需求分析→代码→测试→部署 |
|
||||||
|
| **企业工作流** | 不同部门 Agent 协作处理业务 | HR Agent + 财务 Agent + 法务 Agent |
|
||||||
|
| **智能客服** | 多个专业 Agent 分工处理 | 接待→解答→转接→记录 |
|
||||||
|
| **数据分析** | 多个 Agent 协作分析数据 | 收集→清洗→分析→可视化→报告 |
|
||||||
|
|
||||||
|
**实际案例**:
|
||||||
|
- **Google Agent Space**:企业内部多个 Agent 协作处理文档、邮件、日程
|
||||||
|
- **软件开发团队**:需求 Agent → 代码 Agent → 测试 Agent → 部署 Agent
|
||||||
|
- **智能客服系统**:接待 Agent → 专业解答 Agent → 人工转接 Agent
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. MCP vs A2A:对比与关系
|
||||||
|
|
||||||
|
### 4.1 核心差异
|
||||||
|
|
||||||
|
| 维度 | MCP | A2A |
|
||||||
|
|------|-----|-----|
|
||||||
|
| **发起方** | Anthropic (2024.11) | Google (2025.04) |
|
||||||
|
| **定位** | AI 与工具的连接 | Agent 与 Agent 的协作 |
|
||||||
|
| **通信范围** | Client-Server | Peer-to-Peer |
|
||||||
|
| **数据格式** | JSON-RPC 2.0 | HTTP + JSON |
|
||||||
|
| **类比** | USB-C 接口 | 企业微信 |
|
||||||
|
|
||||||
|
### 4.2 两者的关系
|
||||||
|
|
||||||
|
MCP 和 A2A **不是竞争关系,而是互补关系**:
|
||||||
|
|
||||||
|
<ProtocolComparisonDemo />
|
||||||
|
|
||||||
|
### 4.3 如何选择?
|
||||||
|
|
||||||
|
| 场景 | 选择 |
|
||||||
|
|------|------|
|
||||||
|
| 让 AI 调用本地函数或工具 | Function Call |
|
||||||
|
| 使用第三方工具(数据库、API、文件系统) | MCP |
|
||||||
|
| 构建多 Agent 协作系统 | A2A |
|
||||||
|
| 同时需要工具集成和多 Agent 协作 | MCP + A2A |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 协议的未来趋势
|
||||||
|
|
||||||
|
### 5.1 生态发展
|
||||||
|
|
||||||
|
**MCP 生态**(截至 2025 年初):
|
||||||
|
- 官方提供的 Server:文件系统、SQLite、Git、PostgreSQL 等
|
||||||
|
- 社区贡献的 Server:Slack、Notion、Figma、Stripe 等
|
||||||
|
- 支持 MCP 的应用:Claude Desktop、Cursor、Windsurf、Zed 等
|
||||||
|
|
||||||
|
**A2A 生态**(刚发布):
|
||||||
|
- Google 自家的 Agent 产品率先支持
|
||||||
|
- 开源社区正在开发各种语言的 SDK
|
||||||
|
- 企业级应用正在探索中
|
||||||
|
|
||||||
|
### 5.2 标准化进程
|
||||||
|
|
||||||
|
目前 Agent 协议还处于"战国时代":
|
||||||
|
- MCP 和 A2A 是最主流的两个
|
||||||
|
- 还有其他新兴协议如 ANP、AGP 等
|
||||||
|
- 未来可能会融合或统一
|
||||||
|
|
||||||
|
类比互联网的发展:
|
||||||
|
- 早期:各种局域网协议并存
|
||||||
|
- 后来:TCP/IP 成为标准
|
||||||
|
- 现在:Agent 协议可能也会走向统一
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 小结
|
||||||
|
|
||||||
|
::: tip 核心要点
|
||||||
|
| 协议 | 一句话理解 | 发布时间 | 发起方 | 适用场景 |
|
||||||
|
|------|-----------|---------|--------|---------|
|
||||||
|
| **MCP** | AI 连接工具的"USB-C" | 2024.11 | Anthropic | 工具集成、数据源连接 |
|
||||||
|
| **A2A** | Agent 协作的"企业微信" | 2025.04 | Google | 多 Agent 协作、任务委托 |
|
||||||
|
|
||||||
|
**关键洞察**:
|
||||||
|
1. MCP 解决"AI 如何获取外部能力"的问题
|
||||||
|
2. A2A 解决"多个 AI 如何协作"的问题
|
||||||
|
3. 两者互补,未来可能会融合使用
|
||||||
|
4. 选择协议要根据具体场景,没有银弹
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 参考资料
|
||||||
|
|
||||||
|
1. **MCP 官方文档**: [modelcontextprotocol.io](https://modelcontextprotocol.io)
|
||||||
|
2. **MCP GitHub**: [github.com/modelcontextprotocol](https://github.com/modelcontextprotocol)
|
||||||
|
3. **Anthropic 发布博客**: "Introducing the Model Context Protocol" (2024-11-25)
|
||||||
|
4. **A2A 官方文档**: [google.github.io/A2A](https://google.github.io/A2A)
|
||||||
|
5. **A2A GitHub**: [github.com/google/A2A](https://github.com/google/A2A)
|
||||||
|
6. **Google Cloud Blog**: "Announcing the Agent-to-Agent Protocol" (2025-04-09)
|
||||||
|
|||||||
Reference in New Issue
Block a user