feat: update docs and components, fix DLQ demo bug

This commit is contained in:
sanbuphy
2026-01-18 12:21:49 +08:00
parent 26ed39e1eb
commit e41063a1cd
159 changed files with 54236 additions and 2525 deletions
@@ -22,7 +22,7 @@
<div class="icon">🐛</div>
<div class="icon">🧩</div>
</div>
<!-- 资源管理器 (Sidebar) -->
<div class="sidebar">
<div class="sidebar-title">EXPLORER</div>
@@ -54,38 +54,82 @@
<div v-for="n in 15" :key="n">{{ n }}</div>
</div>
<div class="code-lines">
<div class="line"><span class="kwd">import</span> { <span class="var">ref</span>, <span class="var">onMounted</span>, <span class="var">nextTick</span> } <span class="kwd">from</span> <span class="str">'vue'</span></div>
<div class="line">
<span class="kwd">import</span> {
<span class="var">ref</span>,
<span class="var">onMounted</span>,
<span class="var">nextTick</span> }
<span class="kwd">from</span> <span class="str">'vue'</span>
</div>
<div class="line"></div>
<div class="line"><span class="kwd">const</span> <span class="var">chartRef</span> = <span class="func">ref</span>(<span class="kwd">null</span>)</div>
<div class="line"><span class="kwd">const</span> <span class="var">data</span> = <span class="func">ref</span>([])</div>
<div class="line">
<span class="kwd">const</span>
<span class="var">chartRef</span> =
<span class="func">ref</span>(<span class="kwd">null</span>)
</div>
<div class="line">
<span class="kwd">const</span> <span class="var">data</span> =
<span class="func">ref</span>([])
</div>
<div class="line"></div>
<div class="line"><span class="kwd">const</span> <span class="func">initChart</span> = <span class="kwd">async</span> () => {</div>
<div class="line">&nbsp;&nbsp;<span class="comment">// 等待数据加载完成</span></div>
<div class="line">&nbsp;&nbsp;<span class="kwd">await</span> <span class="func">fetchData</span>()</div>
<div class="line">
<span class="kwd">const</span>
<span class="func">initChart</span> =
<span class="kwd">async</span> () => {
</div>
<div class="line">
&nbsp;&nbsp;<span class="comment">// 等待数据加载完成</span>
</div>
<div class="line">
&nbsp;&nbsp;<span class="kwd">await</span>
<span class="func">fetchData</span>()
</div>
<div class="line">&nbsp;&nbsp;</div>
<div class="line" ref="targetCode">
&nbsp;&nbsp;<span class="comment">// 👈 等待 DOM 更新后再渲染图表</span>
&nbsp;&nbsp;<span class="comment"
>// 👈 等待 DOM 更新后再渲染图表</span
>
</div>
<div class="line" ref="targetCode2">
&nbsp;&nbsp;<span class="kwd">await</span> <span class="func">nextTick</span>()
&nbsp;&nbsp;<span class="kwd">await</span>
<span class="func">nextTick</span>()
</div>
<div class="line">&nbsp;&nbsp;</div>
<div class="line">&nbsp;&nbsp;<span class="kwd">const</span> <span class="var">chart</span> = <span class="var">echarts</span>.<span class="func">init</span>(<span class="var">chartRef</span>.<span class="var">value</span>)</div>
<div class="line">&nbsp;&nbsp;<span class="var">chart</span>.<span class="func">setOption</span>({ ... })</div>
<div class="line">
&nbsp;&nbsp;<span class="kwd">const</span>
<span class="var">chart</span> =
<span class="var">echarts</span>.<span class="func">init</span
>(<span class="var">chartRef</span>.<span class="var"
>value</span
>)
</div>
<div class="line">
&nbsp;&nbsp;<span class="var">chart</span>.<span class="func"
>setOption</span
>({ ... })
</div>
<div class="line">}</div>
</div>
</div>
</div>
<!-- 截图选框 (Overlay) - Moved to main-layout level -->
<div class="screenshot-overlay" v-if="step === 'selecting' || step === 'captured'">
<div class="selection-box" :class="{ flashed: step === 'captured' }">
<div
class="screenshot-overlay"
v-if="step === 'selecting' || step === 'captured'"
>
<div
class="selection-box"
:class="{ flashed: step === 'captured' }"
>
<div class="selection-handle top-left"></div>
<div class="selection-handle top-right"></div>
<div class="selection-handle bottom-left"></div>
<div class="selection-handle bottom-right"></div>
<div class="cursor-crosshair" v-if="step === 'selecting'"></div>
<div class="selection-size" v-if="step === 'selecting'">220 × 350</div>
<div class="selection-size" v-if="step === 'selecting'">
220 × 350
</div>
</div>
</div>
</div>
@@ -110,16 +154,16 @@
<div class="chat-model-selector">
<span>GPT-4o</span> <span class="arrow"></span>
</div>
<div class="messages-container" ref="messagesContainer">
<div class="msg-row user" v-if="stepInt >= 5">
<div class="avatar">U</div>
<div class="msg-bubble">
<div class="pasted-image" v-if="stepInt >= 5">
<div class="ui-snapshot">
<div class="snapshot-rect menu-rect"></div>
<div class="snapshot-text">Menu Bar.png</div>
</div>
<div class="ui-snapshot">
<div class="snapshot-rect menu-rect"></div>
<div class="snapshot-text">Menu Bar.png</div>
</div>
</div>
<div class="msg-text">{{ typedText }}</div>
</div>
@@ -127,39 +171,74 @@
<div class="msg-row ai" v-if="stepInt >= 7">
<div class="avatar gpt">
<svg viewBox="0 0 41 41" class="gpt-logo"><path d="M37.532 16.87a9.963 9.963 0 00-.856-8.184c-3.15-5.49-10.25-7.38-15.738-4.23-.718.412-1.35.914-1.896 1.488a9.965 9.965 0 00-7.144-1.156 9.972 9.972 0 00-6.73 4.966c-3.15 5.49-1.26 12.59 4.23 15.738.412.237.854.43 1.306.586a9.963 9.963 0 00.856 8.184c3.15 5.49 10.25 7.38 15.738 4.23.718-.412 1.35-.914 1.896-1.488a9.965 9.965 0 007.144 1.156 9.972 9.972 0 006.73-4.966c3.15-5.49 1.26-12.59-4.23-15.738a9.953 9.953 0 00-1.306-.586zM20.5 29.5a9 9 0 110-18 9 9 0 010 18z" fill="currentColor"></path></svg>
<svg viewBox="0 0 41 41" class="gpt-logo">
<path
d="M37.532 16.87a9.963 9.963 0 00-.856-8.184c-3.15-5.49-10.25-7.38-15.738-4.23-.718.412-1.35.914-1.896 1.488a9.965 9.965 0 00-7.144-1.156 9.972 9.972 0 00-6.73 4.966c-3.15 5.49-1.26 12.59 4.23 15.738.412.237.854.43 1.306.586a9.963 9.963 0 00.856 8.184c3.15 5.49 10.25 7.38 15.738 4.23.718-.412 1.35-.914 1.896-1.488a9.965 9.965 0 007.144 1.156 9.972 9.972 0 006.73-4.966c3.15-5.49 1.26-12.59-4.23-15.738a9.953 9.953 0 00-1.306-.586zM20.5 29.5a9 9 0 110-18 9 9 0 010 18z"
fill="currentColor"
></path>
</svg>
</div>
<div class="msg-bubble ai-bubble">
<p>这是 VS Code <strong>顶部菜单栏 (Menu Bar)</strong>包含了软件的所有功能入口</p>
<p>
这是 VS Code
<strong>顶部菜单栏 (Menu Bar)</strong
>包含了软件的所有功能入口
</p>
<p><strong>常用菜单解释</strong></p>
<ul>
<li><strong>File (文件)</strong>新建打开保存文件或项目</li>
<li><strong>Edit (编辑)</strong>复制粘贴查找替换撤销重做</li>
<li><strong>View (视图)</strong>控制界面显示比如打开侧边栏终端等</li>
<li><strong>Terminal (终端)</strong>打开内置命令行工具</li>
<li>
<strong>File (文件)</strong>新建打开保存文件或项目
</li>
<li>
<strong>Edit (编辑)</strong>复制粘贴查找替换撤销重做
</li>
<li>
<strong>View (视图)</strong
>控制界面显示比如打开侧边栏终端等
</li>
<li>
<strong>Terminal (终端)</strong>打开内置命令行工具
</li>
</ul>
<p>💡 <strong>小技巧</strong>如果不记得某个功能在哪可以按 <code>F1</code> <code>Ctrl+Shift+P</code> 打开命令面板直接搜索功能名字</p>
<p>
💡 <strong>小技巧</strong>如果不记得某个功能在哪可以按
<code>F1</code>
<code>Ctrl+Shift+P</code> 打开命令面板直接搜索功能名字
</p>
</div>
</div>
</div>
<div class="chat-input-area">
<div class="input-wrapper">
<div class="input-preview" v-if="stepInt === 4 || (stepInt === 5 && isTyping)">
<div
class="input-preview"
v-if="stepInt === 4 || (stepInt === 5 && isTyping)"
>
<div class="mini-snapshot-ui">
<div class="mini-menu-rect"></div>
</div>
</div>
<div class="fake-input">
<span v-if="stepInt < 5" class="placeholder">Message ChatGPT...</span>
<span v-else class="typing-text">{{ typedText }}<span class="cursor" v-if="isTyping">|</span></span>
<span v-if="stepInt < 5" class="placeholder"
>Message ChatGPT...</span
>
<span v-else class="typing-text"
>{{ typedText
}}<span class="cursor" v-if="isTyping">|</span></span
>
</div>
<button class="send-btn" :class="{ active: typedText.length > 5 }"></button>
<button
class="send-btn"
:class="{ active: typedText.length > 5 }"
>
</button>
</div>
</div>
</div>
</div>
<!-- 全局重置按钮 -->
<button class="reset-btn" v-if="step === 'finished'" @click="reset">
🔄 重播
@@ -215,41 +294,41 @@ const getWindowClass = (winName) => {
const startDemo = async () => {
step.value = 'selecting'
// 1. 模拟截图过程 (1.5s)
await wait(1500)
step.value = 'captured'
// 2. 截图闪烁确认 (0.5s)
await wait(600)
// 3. 窗口切换 (0.8s)
step.value = 'switching'
await wait(800)
// 4. ChatGPT 界面准备 (粘贴动作)
step.value = 'pasting'
await wait(800)
// 5. 打字
step.value = 'typing'
isTyping.value = true
const question = "帮我看下这张图,左边红框里那一块是干嘛用的?"
const question = '帮我看下这张图,左边红框里那一块是干嘛用的?'
for (let i = 0; i < question.length; i++) {
typedText.value += question[i]
await wait(50)
}
isTyping.value = false
await wait(300)
// 6. 发送
step.value = 'sending'
await wait(500)
// 7. AI 回复
step.value = 'responding'
await wait(2500) // 模拟阅读时间
step.value = 'finished'
}
@@ -258,14 +337,16 @@ const reset = () => {
typedText.value = ''
}
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
</script>
<style scoped>
.ai-help-demo {
margin: 40px 0;
perspective: 1000px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
.desktop-container {
@@ -275,7 +356,7 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
background: #333; /* 桌面背景 */
border-radius: 12px;
overflow: hidden;
box-shadow: 0 20px 50px rgba(0,0,0,0.3);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
}
/* 通用窗口样式 */
@@ -286,7 +367,7 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
width: 90%;
height: 90%;
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1);
@@ -319,8 +400,16 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
}
@keyframes zoomIn {
from { transform: scale(1.1); opacity: 0; filter: blur(4px); }
to { transform: scale(1); opacity: 1; filter: blur(0); }
from {
transform: scale(1.1);
opacity: 0;
filter: blur(4px);
}
to {
transform: scale(1);
opacity: 1;
filter: blur(0);
}
}
/* ================= VS Code 样式 ================= */
@@ -340,13 +429,31 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
color: #999;
}
.controls { display: flex; gap: 6px; margin-right: 16px; }
.dot { width: 10px; height: 10px; border-radius: 50%; }
.red { background: #ff5f56; }
.yellow { background: #ffbd2e; }
.green { background: #27c93f; }
.controls {
display: flex;
gap: 6px;
margin-right: 16px;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.red {
background: #ff5f56;
}
.yellow {
background: #ffbd2e;
}
.green {
background: #27c93f;
}
.main-layout { flex: 1; display: flex; overflow: hidden; }
.main-layout {
flex: 1;
display: flex;
overflow: hidden;
}
.activity-bar {
width: 40px;
@@ -357,8 +464,16 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
padding-top: 10px;
gap: 15px;
}
.activity-bar .icon { font-size: 18px; opacity: 0.5; filter: grayscale(1); }
.activity-bar .icon.active { opacity: 1; border-left: 2px solid white; filter: grayscale(0); }
.activity-bar .icon {
font-size: 18px;
opacity: 0.5;
filter: grayscale(1);
}
.activity-bar .icon.active {
opacity: 1;
border-left: 2px solid white;
filter: grayscale(0);
}
.sidebar {
width: 180px;
@@ -367,14 +482,41 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
font-size: 12px;
color: #ccc;
}
.sidebar-title { padding: 10px; font-weight: bold; font-size: 11px; }
.tree-item { padding: 4px 10px; display: flex; align-items: center; gap: 6px; cursor: pointer; }
.tree-item.active { background: #37373d; }
.tree-item.indent { padding-left: 20px; }
.tree-item .arrow { font-size: 10px; color: #999; }
.sidebar-title {
padding: 10px;
font-weight: bold;
font-size: 11px;
}
.tree-item {
padding: 4px 10px;
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
}
.tree-item.active {
background: #37373d;
}
.tree-item.indent {
padding-left: 20px;
}
.tree-item .arrow {
font-size: 10px;
color: #999;
}
.editor-area { flex: 1; display: flex; flex-direction: column; background: #1e1e1e; position: relative; }
.tab-bar { height: 35px; background: #252526; display: flex; }
.editor-area {
flex: 1;
display: flex;
flex-direction: column;
background: #1e1e1e;
position: relative;
}
.tab-bar {
height: 35px;
background: #252526;
display: flex;
}
.tab {
background: #1e1e1e;
padding: 0 15px;
@@ -384,7 +526,10 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
font-size: 12px;
border-top: 1px solid #007acc;
}
.tab .close { margin-left: 8px; font-size: 14px; }
.tab .close {
margin-left: 8px;
font-size: 14px;
}
.code-content {
flex: 1;
@@ -401,14 +546,27 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
color: #6e7681;
user-select: none;
}
.code-lines { flex: 1; padding-left: 5px; }
.code-lines {
flex: 1;
padding-left: 5px;
}
/* 语法高亮 */
.kwd { color: #569cd6; }
.var { color: #9cdcfe; }
.func { color: #dcdcaa; }
.str { color: #ce9178; }
.comment { color: #6a9955; }
.kwd {
color: #569cd6;
}
.var {
color: #9cdcfe;
}
.func {
color: #dcdcaa;
}
.str {
color: #ce9178;
}
.comment {
color: #6a9955;
}
/* 截图覆盖层 */
.screenshot-overlay {
@@ -417,7 +575,7 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.3);
background: rgba(0, 0, 0, 0.3);
z-index: 10;
cursor: crosshair;
}
@@ -430,7 +588,7 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
height: 320px;
border: 3px solid #ff5f56; /* 醒目的红框 */
background: rgba(255, 95, 86, 0.1);
box-shadow: 0 0 0 9999px rgba(0,0,0,0.5); /* 遮罩效果 */
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5); /* 遮罩效果 */
animation: selectAnim 0.5s ease-out;
}
@@ -440,13 +598,23 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
}
@keyframes selectAnim {
from { width: 0; height: 0; }
to { width: 280px; height: 320px; }
from {
width: 0;
height: 0;
}
to {
width: 280px;
height: 320px;
}
}
@keyframes flash {
0% { background: rgba(255, 255, 255, 0.8); }
100% { background: rgba(255, 255, 255, 0.1); }
0% {
background: rgba(255, 255, 255, 0.8);
}
100% {
background: rgba(255, 255, 255, 0.1);
}
}
.selection-size {
@@ -528,7 +696,9 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
overflow: hidden;
text-overflow: ellipsis;
}
.history-item.active { background: #343541; }
.history-item.active {
background: #343541;
}
.chat-main {
flex: 1;
@@ -545,7 +715,7 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
padding: 0 20px;
font-weight: 600;
color: #d2d6db;
border-bottom: 1px solid rgba(255,255,255,0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
font-size: 14px;
}
@@ -563,8 +733,13 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
gap: 15px;
max-width: 80%;
}
.msg-row.user { align-self: flex-end; flex-direction: row-reverse; }
.msg-row.ai { align-self: flex-start; }
.msg-row.user {
align-self: flex-end;
flex-direction: row-reverse;
}
.msg-row.ai {
align-self: flex-start;
}
.avatar {
width: 30px;
@@ -576,9 +751,19 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
font-weight: bold;
font-size: 14px;
}
.user .avatar { background: #5436da; color: white; border-radius: 50%; }
.ai .avatar { background: #19c37d; color: white; }
.gpt-logo { width: 20px; height: 20px; }
.user .avatar {
background: #5436da;
color: white;
border-radius: 50%;
}
.ai .avatar {
background: #19c37d;
color: white;
}
.gpt-logo {
width: 20px;
height: 20px;
}
.msg-bubble {
background: transparent;
@@ -586,12 +771,14 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
font-size: 14px;
line-height: 1.6;
}
.user .msg-bubble { text-align: right; }
.user .msg-bubble {
text-align: right;
}
.ai-bubble {
background: #444654;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.pasted-image {
@@ -610,17 +797,17 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
.chat-input-area {
padding: 20px;
background-image: linear-gradient(180deg, rgba(53,55,64,0), #353740 50%);
background-image: linear-gradient(180deg, rgba(53, 55, 64, 0), #353740 50%);
}
.input-wrapper {
background: #40414f;
border: 1px solid rgba(0,0,0,0.1);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
padding: 10px 12px;
display: flex;
align-items: center;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
position: relative;
}
@@ -644,7 +831,9 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
display: flex;
align-items: center;
}
.placeholder { color: #8e8ea0; }
.placeholder {
color: #8e8ea0;
}
.send-btn {
background: #19c37d;
@@ -659,7 +848,9 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
opacity: 0.5;
transition: opacity 0.2s;
}
.send-btn.active { opacity: 1; }
.send-btn.active {
opacity: 1;
}
/* 引导层 */
.guide-overlay {
@@ -668,7 +859,7 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.4);
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
@@ -682,10 +873,12 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
border-radius: 30px;
font-weight: bold;
cursor: pointer;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
transition: transform 0.2s;
}
.start-btn:hover { transform: scale(1.05); }
.start-btn:hover {
transform: scale(1.05);
}
.reset-btn {
position: absolute;
@@ -697,11 +890,17 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
@@ -32,7 +32,7 @@ const run = async () => {
isRunning.value = true
logs.value = []
activeStep.value = 'start'
await wait(600)
if (currentScenario.value === 'editor') {
@@ -43,12 +43,12 @@ const run = async () => {
// Has extension
activeStep.value = 'extension'
await wait(800)
if (currentScenario.value === 'extension') {
logs.value.push('> python main.py')
await wait(600)
logs.value.push("Error: command 'python' not found")
logs.value.push("系统: 找不到 Python 解释器")
logs.value.push('系统: 找不到 Python 解释器')
activeStep.value = 'error-env'
} else {
// Full
@@ -63,7 +63,7 @@ const run = async () => {
isRunning.value = false
}
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const setScenario = (key) => {
if (isRunning.value) return
@@ -77,13 +77,15 @@ const setScenario = (key) => {
<div class="arch-demo">
<div class="demo-header">
<div class="title">🛠 IDE 核心机制模拟器</div>
<div class="subtitle">点击下方标签体验不同配置下的运行结果理解为什么缺一不可</div>
<div class="subtitle">
点击下方标签体验不同配置下的运行结果理解为什么缺一不可
</div>
</div>
<!-- Tab Selection -->
<div class="tabs">
<div
v-for="(conf, key) in scenarios"
<div
v-for="(conf, key) in scenarios"
:key="key"
class="tab"
:class="{ active: currentScenario === key }"
@@ -99,16 +101,23 @@ const setScenario = (key) => {
</div>
<div class="diagram-container">
<!-- Layer 1: VS Code -->
<div class="component vscode" :class="{ dim: activeStep === 'env' }">
<div class="comp-label">1. 外壳 (VS Code)</div>
<div class="editor-window">
<div class="file-tab">main.py</div>
<div class="code-area">
<span style="color: #c586c0">print</span>(<span style="color: #ce9178">"Hello"</span>)
<span style="color: #c586c0">print</span>(<span
style="color: #ce9178"
>"Hello"</span
>)
</div>
<button class="run-btn-small" @click="run" :disabled="isRunning" title="点击运行">
<button
class="run-btn-small"
@click="run"
:disabled="isRunning"
title="点击运行"
>
{{ isRunning ? '...' : '▶ 运行' }}
</button>
</div>
@@ -119,12 +128,34 @@ const setScenario = (key) => {
<!-- Connector 1 -->
<div class="connector">
<div class="line" :class="{ active: ['extension', 'env', 'result', 'error-env'].includes(activeStep) }"></div>
<div class="arrow-tip" :class="{ active: ['extension', 'env', 'result', 'error-env'].includes(activeStep) }"></div>
<div
class="line"
:class="{
active: ['extension', 'env', 'result', 'error-env'].includes(
activeStep
)
}"
></div>
<div
class="arrow-tip"
:class="{
active: ['extension', 'env', 'result', 'error-env'].includes(
activeStep
)
}"
>
</div>
</div>
<!-- Layer 2: Extension -->
<div class="component extension" :class="{ missing: currentScenario === 'editor', active: activeStep === 'extension' }">
<div
class="component extension"
:class="{
missing: currentScenario === 'editor',
active: activeStep === 'extension'
}"
>
<div class="comp-label">2. 中介 (插件)</div>
<div class="comp-box">
<div v-if="currentScenario === 'editor'" class="missing-content">
@@ -133,7 +164,14 @@ const setScenario = (key) => {
<div v-else class="active-content">
<div class="icon">🧩</div>
<div class="text">Python 插件</div>
<div class="action" v-if="activeStep === 'extension' || activeStep === 'env' || activeStep === 'error-env'">
<div
class="action"
v-if="
activeStep === 'extension' ||
activeStep === 'env' ||
activeStep === 'error-env'
"
>
生成指令: <code>python main.py</code>
</div>
</div>
@@ -142,12 +180,26 @@ const setScenario = (key) => {
<!-- Connector 2 -->
<div class="connector">
<div class="line" :class="{ active: ['env', 'result'].includes(activeStep) }"></div>
<div class="arrow-tip" :class="{ active: ['env', 'result'].includes(activeStep) }"></div>
<div
class="line"
:class="{ active: ['env', 'result'].includes(activeStep) }"
></div>
<div
class="arrow-tip"
:class="{ active: ['env', 'result'].includes(activeStep) }"
>
</div>
</div>
<!-- Layer 3: Environment -->
<div class="component env" :class="{ missing: currentScenario !== 'full', active: activeStep === 'env' }">
<div
class="component env"
:class="{
missing: currentScenario !== 'full',
active: activeStep === 'env'
}"
>
<div class="comp-label">3. 引擎 (环境)</div>
<div class="comp-box">
<div v-if="currentScenario !== 'full'" class="missing-content">
@@ -168,7 +220,6 @@ const setScenario = (key) => {
🚫 找不到程序
</div>
</div>
</div>
<!-- Output Console -->
@@ -177,17 +228,30 @@ const setScenario = (key) => {
<span class="term-icon">_</span> 终端 (Terminal)
</div>
<div class="term-body">
<div v-for="(l, i) in logs" :key="i" class="log-line" :class="{ error: l.includes('Error') || l.includes('失败') }">
<div
v-for="(l, i) in logs"
:key="i"
class="log-line"
:class="{ error: l.includes('Error') || l.includes('失败') }"
>
{{ l }}
</div>
<div v-if="logs.length === 0" class="placeholder">点击上方运行按钮开始...</div>
<div v-if="logs.length === 0" class="placeholder">
点击上方“运行”按钮开始...
</div>
</div>
</div>
<div class="result-bar" :class="{ success: scenarios[currentScenario].result.includes('成功'), error: !scenarios[currentScenario].result.includes('成功') }" v-if="!isRunning && logs.length > 0">
<div
class="result-bar"
:class="{
success: scenarios[currentScenario].result.includes('成功'),
error: !scenarios[currentScenario].result.includes('成功')
}"
v-if="!isRunning && logs.length > 0"
>
{{ scenarios[currentScenario].result }}
</div>
</div>
</template>
@@ -199,7 +263,7 @@ const setScenario = (key) => {
background: var(--vp-c-bg-soft);
margin: 24px 0;
font-family: var(--vp-font-family-base);
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.demo-header {
@@ -244,7 +308,7 @@ const setScenario = (key) => {
.tab.active {
background: var(--vp-c-bg);
color: var(--vp-c-brand);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
font-weight: bold;
}
@@ -297,7 +361,7 @@ const setScenario = (key) => {
background: #1e1e1e;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 1px solid #333;
}
.vscode .file-tab {
@@ -493,20 +557,36 @@ const setScenario = (key) => {
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideIn {
from { opacity: 0; transform: translate(-10px, -50%); }
to { opacity: 1; transform: translate(0, -50%); }
from {
opacity: 0;
transform: translate(-10px, -50%);
}
to {
opacity: 1;
transform: translate(0, -50%);
}
}
.spin {
display: inline-block;
animation: spin 2s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Mobile Responsive */
@@ -522,4 +602,4 @@ const setScenario = (key) => {
text-align: center;
}
}
</style>
</style>
@@ -103,9 +103,27 @@ const closeTab = (index) => {
const activeSidebarView = ref('EXPLORER') // 'EXPLORER' | 'EXTENSIONS'
const extensions = ref([
{ id: 'python', name: 'Python', description: 'IntelliSense, linting, debugging...', installed: false, installing: false },
{ id: 'cpp', name: 'C/C++', description: 'C/C++ IntelliSense, debugging...', installed: false, installing: false },
{ id: 'vue', name: 'Vue - Official', description: 'Language support for Vue 3', installed: false, installing: false },
{
id: 'python',
name: 'Python',
description: 'IntelliSense, linting, debugging...',
installed: false,
installing: false
},
{
id: 'cpp',
name: 'C/C++',
description: 'C/C++ IntelliSense, debugging...',
installed: false,
installing: false
},
{
id: 'vue',
name: 'Vue - Official',
description: 'Language support for Vue 3',
installed: false,
installing: false
}
])
const searchQuery = ref('')
@@ -113,22 +131,23 @@ const searchQuery = ref('')
const typeText = async (text, setter) => {
for (let i = 0; i < text.length; i++) {
setter(text.substring(0, i + 1))
await new Promise(r => setTimeout(r, 100)) // typing speed
await new Promise((r) => setTimeout(r, 100)) // typing speed
}
}
const filteredExtensions = computed(() => {
if (!searchQuery.value) return extensions.value
const query = searchQuery.value.toLowerCase()
return extensions.value.filter(ext =>
ext.name.toLowerCase().includes(query) ||
ext.description.toLowerCase().includes(query)
return extensions.value.filter(
(ext) =>
ext.name.toLowerCase().includes(query) ||
ext.description.toLowerCase().includes(query)
)
})
const installExtension = (id) => {
const ext = extensions.value.find(e => e.id === id)
if(ext && !ext.installed && !ext.installing) {
const ext = extensions.value.find((e) => e.id === id)
if (ext && !ext.installed && !ext.installing) {
ext.installing = true
setTimeout(() => {
ext.installing = false
@@ -302,64 +321,67 @@ const vClickOutside = {
}
const startTour = async () => {
if (isAutoPlaying.value) return
isAutoPlaying.value = true
cursorVisible.value = true
if (isAutoPlaying.value) return
isAutoPlaying.value = true
cursorVisible.value = true
// Reset UI state to ensure all elements are visible
activeFileIndex.value = 0
activePanel.value = 'TERMINAL'
sidebarVisible.value = false
panelVisible.value = true
activeMenu.value = null
hoverInfo.value = ''
searchQuery.value = '' // Reset search
activeSidebarView.value = 'EXPLORER' // Reset sidebar view
// Reset UI state to ensure all elements are visible
activeFileIndex.value = 0
activePanel.value = 'TERMINAL'
sidebarVisible.value = false
panelVisible.value = true
activeMenu.value = null
hoverInfo.value = ''
searchQuery.value = '' // Reset search
activeSidebarView.value = 'EXPLORER' // Reset sidebar view
const container = vscodeMockRef.value
if (!container) return
const container = vscodeMockRef.value
if (!container) return
const moveCursorTo = (selector, infoText, action = null) => {
return new Promise((resolve) => {
if (action) action()
const moveCursorTo = (selector, infoText, action = null) => {
return new Promise((resolve) => {
if (action) action()
// Small delay to allow UI updates (like opening menus)
setTimeout(() => {
const el = container.querySelector(selector)
if (el) {
// Recalculate container rect in case of scroll
const containerRect = container.getBoundingClientRect()
const rect = el.getBoundingClientRect()
// Calculate relative position
cursorX.value = rect.left - containerRect.left + rect.width / 2
cursorY.value = rect.top - containerRect.top + rect.height / 2
// Small delay to allow UI updates (like opening menus)
setTimeout(() => {
const el = container.querySelector(selector)
if (el) {
// Recalculate container rect in case of scroll
const containerRect = container.getBoundingClientRect()
const rect = el.getBoundingClientRect()
// Update highlight box
// Use box-sizing: border-box in CSS
// Match exact size (border will be drawn inside the element area)
highlightStyle.value = {
top: rect.top - containerRect.top + 'px',
left: rect.left - containerRect.left + 'px',
width: rect.width + 'px',
height: rect.height + 'px'
}
highlightVisible.value = true
// Calculate relative position
cursorX.value = rect.left - containerRect.left + rect.width / 2
cursorY.value = rect.top - containerRect.top + rect.height / 2
hoverInfo.value = infoText
} else {
// Fallback if element not found: just proceed after delay
// This prevents the tour from hanging forever
console.warn(`Tour element not found: ${selector}`)
// Update highlight box
// Use box-sizing: border-box in CSS
// Match exact size (border will be drawn inside the element area)
highlightStyle.value = {
top: rect.top - containerRect.top + 'px',
left: rect.left - containerRect.left + 'px',
width: rect.width + 'px',
height: rect.height + 'px'
}
tourTimeout = setTimeout(() => {
highlightVisible.value = true
hoverInfo.value = infoText
} else {
// Fallback if element not found: just proceed after delay
// This prevents the tour from hanging forever
console.warn(`Tour element not found: ${selector}`)
}
tourTimeout = setTimeout(
() => {
highlightVisible.value = false
resolve()
}, el ? 2500 : 500) // Shorter delay if skipped
}, 800) // Increased delay for better stability
})
}
},
el ? 2500 : 500
) // Shorter delay if skipped
}, 800) // Increased delay for better stability
})
}
// --- Tour Segments ---
@@ -369,25 +391,17 @@ const startTour = async () => {
if (!isAutoPlaying.value) return
// Menus
await moveCursorTo(
'.menu-bar-container',
'菜单栏:所有功能'
)
await moveCursorTo('.menu-bar-container', '菜单栏:所有功能')
if (!isAutoPlaying.value) return
// Demonstrate clicking a menu
await moveCursorTo(
'.menu-item:nth-child(1)',
'文件菜单:文件操作',
() => toggleMenu('File')
await moveCursorTo('.menu-item:nth-child(1)', '文件菜单:文件操作', () =>
toggleMenu('File')
)
if (!isAutoPlaying.value) return
// Show a specific item in the dropdown
await moveCursorTo(
'.dropdown-item:nth-child(1)',
'新建文件:创建空文件'
)
await moveCursorTo('.dropdown-item:nth-child(1)', '新建文件:创建空文件')
if (!isAutoPlaying.value) return
// Close menu
@@ -397,55 +411,36 @@ const startTour = async () => {
await moveCursorTo('.nav-arrows', '导航按钮:后退/前进')
if (!isAutoPlaying.value) return
await moveCursorTo(
'.search-box',
'命令中心:快速搜索'
)
await moveCursorTo('.search-box', '命令中心:快速搜索')
if (!isAutoPlaying.value) return
await moveCursorTo(
'.layout-controls',
'布局控制:切换视图'
)
await moveCursorTo('.layout-controls', '布局控制:切换视图')
}
const runActivityBarTour = async () => {
// --- 2. Activity Bar (Left) ---
await moveCursorTo(
'.activity-bar',
'活动栏:切换视图'
)
await moveCursorTo('.activity-bar', '活动栏:切换视图')
if (!isAutoPlaying.value) return
await moveCursorTo(
'.icon[title="Explorer"]',
'资源管理器:管理文件',
() => { sidebarVisible.value = true }
() => {
sidebarVisible.value = true
}
)
if (!isAutoPlaying.value) return
await moveCursorTo(
'.icon[title="Search"]',
'全局搜索:查找替换'
)
await moveCursorTo('.icon[title="Search"]', '全局搜索:查找替换')
if (!isAutoPlaying.value) return
await moveCursorTo(
'.icon[title="Source Control"]',
'源代码管理:Git'
)
await moveCursorTo('.icon[title="Source Control"]', '源代码管理:Git')
if (!isAutoPlaying.value) return
await moveCursorTo(
'.icon[title="Run and Debug"]',
'运行和调试:调试代码'
)
await moveCursorTo('.icon[title="Run and Debug"]', '运行和调试:调试代码')
if (!isAutoPlaying.value) return
await moveCursorTo(
'.icon[title="Extensions"]',
'扩展商店:安装插件'
)
await moveCursorTo('.icon[title="Extensions"]', '扩展商店:安装插件')
if (!isAutoPlaying.value) return
await moveCursorTo('.icon[title="Accounts"]', '账户:同步设置')
@@ -460,7 +455,7 @@ const startTour = async () => {
sidebarVisible.value = true
await new Promise((r) => setTimeout(r, 300))
}
await moveCursorTo('.sidebar', '侧边栏:详细内容')
if (!isAutoPlaying.value) return
@@ -470,10 +465,7 @@ const startTour = async () => {
)
if (!isAutoPlaying.value) return
await moveCursorTo(
'.sidebar-section:nth-child(3)',
'项目文件树:项目结构'
)
await moveCursorTo('.sidebar-section:nth-child(3)', '项目文件树:项目结构')
}
const runEditorTour = async () => {
@@ -487,22 +479,13 @@ const startTour = async () => {
}
// --- 4. Editor Area ---
await moveCursorTo(
'.tabs',
'标签页:已打开文件'
)
await moveCursorTo('.tabs', '标签页:已打开文件')
if (!isAutoPlaying.value) return
await moveCursorTo(
'.breadcrumbs',
'路径导航:文件路径'
)
await moveCursorTo('.breadcrumbs', '路径导航:文件路径')
if (!isAutoPlaying.value) return
await moveCursorTo(
'.code-wrapper',
'编辑区:编写代码'
)
await moveCursorTo('.code-wrapper', '编辑区:编写代码')
if (!isAutoPlaying.value) return
await moveCursorTo('.minimap', '缩略图:预览代码')
@@ -513,33 +496,21 @@ const startTour = async () => {
await moveCursorTo('.bottom-panel', '底部面板:集成工具')
if (!isAutoPlaying.value) return
await moveCursorTo(
'.panel-tabs',
'面板切换:切换工具'
)
await moveCursorTo('.panel-tabs', '面板切换:切换工具')
if (!isAutoPlaying.value) return
await moveCursorTo(
'.terminal-content',
'终端:运行命令'
)
await moveCursorTo('.terminal-content', '终端:运行命令')
}
const runStatusTour = async () => {
// --- 6. Status Bar ---
await moveCursorTo(
'.status-bar',
'状态栏:全局信息'
)
await moveCursorTo('.status-bar', '状态栏:全局信息')
if (!isAutoPlaying.value) return
await moveCursorTo('.status-left', '左侧信息:Git/错误')
if (!isAutoPlaying.value) return
await moveCursorTo(
'.status-right',
'右侧信息:环境信息'
)
await moveCursorTo('.status-right', '右侧信息:环境信息')
}
try {
@@ -553,8 +524,8 @@ const startTour = async () => {
}
if (mode === 'all' || mode === 'extensions') {
// --- Extensions Tour ---
await moveCursorTo(
// --- Extensions Tour ---
await moveCursorTo(
'.icon[title="Extensions"]',
'扩展商店:安装插件',
() => toggleSidebarView('EXTENSIONS')
@@ -565,7 +536,7 @@ const startTour = async () => {
'.sidebar-search input',
'搜索插件:输入 python',
async () => {
await typeText('python', (v) => searchQuery.value = v)
await typeText('python', (v) => (searchQuery.value = v))
}
)
if (!isAutoPlaying.value) return
@@ -576,17 +547,13 @@ const startTour = async () => {
() => installExtension('python')
)
if (!isAutoPlaying.value) return
// Switch back to explorer for next steps if in 'all' mode
if (mode === 'all') {
await moveCursorTo(
'.icon[title="Explorer"]',
'返回资源管理器',
() => {
toggleSidebarView('EXPLORER')
searchQuery.value = '' // Clear search when leaving
}
)
await moveCursorTo('.icon[title="Explorer"]', '返回资源管理器', () => {
toggleSidebarView('EXPLORER')
searchQuery.value = '' // Clear search when leaving
})
}
}
@@ -725,9 +692,7 @@ onUnmounted(() => {
</div>
<div
class="menu-bar-container"
@mouseenter.stop="
showInfo('菜单栏:功能入口')
"
@mouseenter.stop="showInfo('菜单栏:功能入口')"
@mouseleave="clearInfo"
>
<div
@@ -832,7 +797,9 @@ onUnmounted(() => {
<div class="top-icons">
<div
class="icon"
:class="{ active: activeSidebarView === 'EXPLORER' && sidebarVisible }"
:class="{
active: activeSidebarView === 'EXPLORER' && sidebarVisible
}"
title="Explorer"
@click="toggleSidebarView('EXPLORER')"
@mouseenter.stop="showInfo('资源管理器:文件管理')"
@@ -985,7 +952,9 @@ onUnmounted(() => {
</div>
<div
class="icon"
:class="{ active: activeSidebarView === 'EXTENSIONS' && sidebarVisible }"
:class="{
active: activeSidebarView === 'EXTENSIONS' && sidebarVisible
}"
title="Extensions"
@click="toggleSidebarView('EXTENSIONS')"
@mouseenter.stop="showInfo('扩展:插件')"
@@ -1105,9 +1074,7 @@ onUnmounted(() => {
<div
class="sidebar"
v-show="sidebarVisible"
@mouseenter.stop="
showInfo('侧边栏:详细内容')
"
@mouseenter.stop="showInfo('侧边栏:详细内容')"
@mouseleave="clearInfo"
>
<div v-if="activeSidebarView === 'EXPLORER'" class="sidebar-content">
@@ -1169,33 +1136,56 @@ onUnmounted(() => {
</div>
</div>
</div>
<div v-else-if="activeSidebarView === 'EXTENSIONS'" class="sidebar-content">
<div class="sidebar-header">
<div
v-else-if="activeSidebarView === 'EXTENSIONS'"
class="sidebar-content"
>
<div class="sidebar-header">
<span>EXTENSIONS</span>
<span class="sidebar-dots"></span>
</div>
<div class="sidebar-search">
<input type="text" placeholder="Search Extensions in Marketplace" :value="searchQuery" readonly />
<input
type="text"
placeholder="Search Extensions in Marketplace"
:value="searchQuery"
readonly
/>
</div>
<div class="sidebar-section expanded">
<div class="section-header"> POPULAR</div>
<div class="extension-list">
<div v-for="ext in filteredExtensions" :key="ext.id" class="extension-item">
<div
v-for="ext in filteredExtensions"
:key="ext.id"
class="extension-item"
>
<div class="extension-icon"></div>
<div class="extension-info">
<div class="extension-name">
{{ ext.name }}
<span v-if="ext.installed" class="installed-badge"></span>
<span v-if="ext.installed" class="installed-badge"
></span
>
</div>
<div class="extension-desc">{{ ext.description }}</div>
<div class="extension-actions">
<button
class="install-btn"
:class="{ installing: ext.installing, installed: ext.installed }"
<button
class="install-btn"
:class="{
installing: ext.installing,
installed: ext.installed
}"
@click.stop="installExtension(ext.id)"
>
{{ ext.installed ? 'Manage' : (ext.installing ? 'Installing' : 'Install') }}
{{
ext.installed
? 'Manage'
: ext.installing
? 'Installing'
: 'Install'
}}
</button>
</div>
</div>
@@ -1436,9 +1426,7 @@ onUnmounted(() => {
<!-- Status Bar -->
<div
class="status-bar"
@mouseenter.stop="
showInfo('状态栏:环境信息')
"
@mouseenter.stop="showInfo('状态栏:环境信息')"
@mouseleave="clearInfo"
>
<div class="status-left">
@@ -2416,8 +2404,17 @@ onUnmounted(() => {
}
@keyframes highlightPulse {
0% { transform: scale(1); opacity: 0.7; }
50% { transform: scale(1.02); opacity: 0.9; }
100% { transform: scale(1); opacity: 0.7; }
0% {
transform: scale(1);
opacity: 0.7;
}
50% {
transform: scale(1.02);
opacity: 0.9;
}
100% {
transform: scale(1);
opacity: 0.7;
}
}
</style>