feat(docs): add NavGrid/NavCard components and restructure stage pages

- Add NavGrid.vue and NavCard.vue components for better navigation layout
- Restructure stage-0 index pages across languages into intro.md with new navigation components
- Remove old stage-0 index.md files and update stage-3 pages similarly
- Add new dependencies 'claude' and 'codex' to package.json
- Improve code formatting in multiple Vue components for better readability
- Update documentation content and structure for better user experience
This commit is contained in:
sanbuphy
2026-02-01 23:42:12 +08:00
parent a9a5c5c8a7
commit ad95658a11
171 changed files with 16366 additions and 7946 deletions
@@ -0,0 +1,205 @@
<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
const activeTab = ref('local')
const storageData = reactive({
local: [
{ key: 'theme', value: 'dark' },
{ key: 'user_id', value: '10086' },
{ key: 'is_first_visit', value: 'false' }
],
session: [
{ key: 'current_step', value: '2' },
{ key: 'temp_token', value: 'abc-123-xyz' }
],
cookies: [
{ key: 'session_id', value: 's%3A123456...', domain: 'example.com', expires: 'Session' },
{ key: 'ga_id', value: 'GA1.2.345...', domain: '.example.com', expires: '2025-12-31' }
]
})
const newEntry = reactive({ key: '', value: '' })
const addEntry = () => {
if (!newEntry.key || !newEntry.value) {
ElMessage.warning('Key and Value are required')
return
}
// Check duplicate
const list = storageData[activeTab.value]
if (list.some(item => item.key === newEntry.key)) {
ElMessage.error(`Key "${newEntry.key}" already exists!`)
return
}
const item = { key: newEntry.key, value: newEntry.value }
if (activeTab.value === 'cookies') {
item.domain = 'example.com'
item.expires = 'Session'
}
list.push(item)
newEntry.key = ''
newEntry.value = ''
ElMessage.success('Added successfully')
}
const deleteEntry = (index) => {
storageData[activeTab.value].splice(index, 1)
ElMessage.success('Deleted')
}
const clearAll = () => {
storageData[activeTab.value] = []
ElMessage.success('Cleared all data')
}
</script>
<template>
<el-card class="app-demo" shadow="hover">
<template #header>
<div class="header">
<span class="title">Application (应用面板)</span>
<el-button type="danger" size="small" icon="Delete" @click="clearAll">Clear All</el-button>
</div>
</template>
<div class="layout">
<div class="sidebar">
<div
class="nav-item"
:class="{ active: activeTab === 'local' }"
@click="activeTab = 'local'"
>
Local Storage
</div>
<div
class="nav-item"
:class="{ active: activeTab === 'session' }"
@click="activeTab = 'session'"
>
Session Storage
</div>
<div
class="nav-item"
:class="{ active: activeTab === 'cookies' }"
@click="activeTab = 'cookies'"
>
Cookies
</div>
</div>
<div class="content">
<div class="toolbar">
<el-input
v-model="newEntry.key"
placeholder="Key"
size="small"
style="width: 120px"
/>
<el-input
v-model="newEntry.value"
placeholder="Value"
size="small"
style="width: 120px"
/>
<el-button type="primary" size="small" @click="addEntry">Add</el-button>
</div>
<el-table :data="storageData[activeTab]" style="width: 100%" height="250" border>
<el-table-column prop="key" label="Key" width="120" />
<el-table-column prop="value" label="Value" min-width="150" />
<el-table-column v-if="activeTab === 'cookies'" prop="domain" label="Domain" width="110" />
<el-table-column label="Action" width="70" align="center">
<template #default="scope">
<el-button
type="danger"
icon="Close"
circle
size="small"
@click="deleteEntry(scope.$index)"
/>
</template>
</el-table-column>
</el-table>
<div class="info-bar" v-if="activeTab === 'local'">
持久化存储即便关闭浏览器数据也会保留
</div>
<div class="info-bar" v-else-if="activeTab === 'session'">
临时存储关闭标签页后数据会被清空
</div>
<div class="info-bar" v-else>
Cookies通常用于身份验证会随请求发送给服务器
</div>
</div>
</div>
</el-card>
</template>
<style scoped>
.app-demo {
margin: 20px 0;
}
.layout {
display: flex;
height: 350px;
border: 1px solid #ebeef5;
}
.sidebar {
width: 150px;
border-right: 1px solid #ebeef5;
background-color: #f9fafc;
}
.nav-item {
padding: 10px 12px;
cursor: pointer;
font-size: 13px;
color: #606266;
transition: background-color 0.2s;
}
.nav-item:hover {
background-color: #ecf5ff;
}
.nav-item.active {
background-color: #e6f7ff;
color: #409eff;
font-weight: bold;
border-left: 3px solid #409eff;
}
.content {
flex: 1;
padding: 12px;
display: flex;
flex-direction: column;
}
.toolbar {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.info-bar {
margin-top: auto;
padding-top: 8px;
font-size: 12px;
color: #909399;
border-top: 1px solid #ebeef5;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
@@ -0,0 +1,237 @@
<script setup>
import { ref, nextTick, watch } from 'vue'
const logs = ref([
{ type: 'log', message: 'Welcome to the interactive console demo!' },
{ type: 'info', message: 'Try typing simple JavaScript commands below.' },
{ type: 'warn', message: 'This is a simulated environment, not a real JS engine.' }
])
const inputCommand = ref('')
const consoleRef = ref(null)
const executeCommand = () => {
const cmd = inputCommand.value.trim()
if (!cmd) return
logs.value.push({ type: 'command', message: `> ${cmd}` })
try {
let result
// Simple simulation of common commands
if (cmd.startsWith('console.log')) {
const match = cmd.match(/console\.log\((.*)\)/)
const msg = match ? match[1].replace(/['"]/g, '') : ''
logs.value.push({ type: 'log', message: msg })
result = undefined
} else if (cmd.startsWith('console.warn')) {
const match = cmd.match(/console\.warn\((.*)\)/)
const msg = match ? match[1].replace(/['"]/g, '') : ''
logs.value.push({ type: 'warn', message: msg })
result = undefined
} else if (cmd.startsWith('console.error')) {
const match = cmd.match(/console\.error\((.*)\)/)
const msg = match ? match[1].replace(/['"]/g, '') : ''
logs.value.push({ type: 'error', message: msg })
result = undefined
} else if (cmd.startsWith('alert')) {
const match = cmd.match(/alert\((.*)\)/)
const msg = match ? match[1].replace(/['"]/g, '') : ''
alert(msg)
result = undefined
} else if (cmd === 'clear()') {
logs.value = []
result = 'Console was cleared'
} else {
// Safe eval for math and basic types
// Note: This is a demo, strict security is less critical but good practice to avoid real eval
// using Function constructor for basic math
try {
result = new Function('return ' + cmd)()
} catch (e) {
throw new Error(e.message)
}
}
if (result !== undefined) {
logs.value.push({ type: 'result', message: '< ' + String(result) })
}
} catch (err) {
logs.value.push({ type: 'error', message: 'Uncaught ReferenceError: ' + err.message })
}
inputCommand.value = ''
scrollToBottom()
}
const clearConsole = () => {
logs.value = []
}
const scrollToBottom = () => {
nextTick(() => {
if (consoleRef.value) {
consoleRef.value.scrollTop = consoleRef.value.scrollHeight
}
})
}
const shortcuts = [
{ label: 'console.log("Hello")', cmd: 'console.log("Hello World")' },
{ label: '1 + 1', cmd: '1 + 1' },
{ label: 'console.error("Oops")', cmd: 'console.error("Something went wrong!")' },
{ label: 'alert("Hi")', cmd: 'alert("Hello from DevTools!")' }
]
const runShortcut = (cmd) => {
inputCommand.value = cmd
executeCommand()
}
</script>
<template>
<el-card class="console-demo" shadow="hover">
<template #header>
<div class="header">
<span class="title">Console (控制台)</span>
<el-button size="small" @click="clearConsole" icon="Delete" circle title="Clear console" />
</div>
</template>
<div class="console-body" ref="consoleRef">
<div v-for="(log, index) in logs" :key="index" class="log-item" :class="log.type">
<span class="icon" v-if="log.type === 'error'"></span>
<span class="icon" v-else-if="log.type === 'warn'"></span>
<span class="icon" v-else-if="log.type === 'info'"></span>
<span class="icon" v-else-if="log.type === 'result'"></span>
<span class="content">{{ log.message }}</span>
</div>
</div>
<div class="input-area">
<el-input
v-model="inputCommand"
placeholder="输入 JS 代码,按回车执行..."
@keyup.enter="executeCommand"
>
<template #prepend>></template>
</el-input>
</div>
<div class="shortcuts">
<span class="label">快速尝试</span>
<el-button-group>
<el-button
v-for="s in shortcuts"
:key="s.label"
size="small"
@click="runShortcut(s.cmd)"
>
{{ s.label }}
</el-button>
</el-button-group>
</div>
</el-card>
</template>
<style scoped>
.console-demo {
margin: 20px 0;
border: 1px solid #dcdfe6;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.title {
font-weight: bold;
}
.console-body {
height: 250px;
overflow-y: auto;
background-color: #f5f7fa;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 8px;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: 13px;
margin-bottom: 12px;
}
.log-item {
padding: 4px 8px;
border-bottom: 1px solid #ebeef5;
display: flex;
align-items: flex-start;
}
.log-item.command {
color: #606266;
font-weight: bold;
}
.log-item.result {
color: #909399;
font-style: italic;
}
.log-item.error {
background-color: #fef0f0;
color: #f56c6c;
border-left: 4px solid #f56c6c;
}
.log-item.warn {
background-color: #fdf6ec;
color: #e6a23c;
border-left: 4px solid #e6a23c;
}
.log-item.info {
color: #409eff;
}
.log-item .icon {
margin-right: 8px;
flex-shrink: 0;
}
.log-item .content {
word-break: break-all;
}
.shortcuts {
margin-top: 12px;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.shortcuts .label {
font-size: 12px;
color: #909399;
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.console-body {
background-color: #1e1e1e;
border-color: #333;
color: #d4d4d4;
}
.log-item {
border-bottom-color: #333;
}
.log-item.command { color: #a8a8a8; }
.log-item.result { color: #808080; }
.log-item.error { background-color: #290000; color: #f14c4c; }
.log-item.warn { background-color: #332b00; color: #cca700; }
}
</style>
@@ -0,0 +1,238 @@
<script setup>
import { ref, reactive, computed } from 'vue'
const selectedElement = ref('box') // 'box' or 'text'
const styles = reactive({
box: {
backgroundColor: '#409eff',
padding: '20px',
borderRadius: '8px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
},
text: {
color: '#ffffff',
fontSize: '20px',
fontWeight: 'bold'
}
})
const domTree = [
{ tag: 'div', class: 'container', id: 'app' },
{ tag: 'div', class: 'box', id: 'target-box', parent: 'app' },
{ tag: 'span', class: 'text', text: 'Hello DevTools', parent: 'target-box' }
]
const computedStyle = computed(() => {
return styles[selectedElement.value]
})
const updateStyle = (prop, value) => {
styles[selectedElement.value][prop] = value
}
</script>
<template>
<el-card class="elements-demo" shadow="hover">
<template #header>
<div class="header">
<span class="title">Elements (元素面板)</span>
</div>
</template>
<div class="devtools-layout">
<!-- Left: DOM Tree -->
<div class="panel dom-panel">
<div class="panel-header">DOM Tree</div>
<div class="dom-content">
<div class="dom-line">
<span class="tag">&lt;div</span> <span class="attr">id</span>="app" <span class="attr">class</span>="container"<span class="tag">&gt;</span>
</div>
<div
class="dom-line indent"
:class="{ selected: selectedElement === 'box' }"
@click="selectedElement = 'box'"
>
<span class="tag">&lt;div</span> <span class="attr">id</span>="target-box" <span class="attr">class</span>="box"<span class="tag">&gt;</span>
</div>
<div
class="dom-line indent-2"
:class="{ selected: selectedElement === 'text' }"
@click="selectedElement = 'text'"
>
<span class="tag">&lt;span</span> <span class="attr">class</span>="text"<span class="tag">&gt;</span>Hello DevTools<span class="tag">&lt;/span&gt;</span>
</div>
<div class="dom-line indent">
<span class="tag">&lt;/div&gt;</span>
</div>
<div class="dom-line">
<span class="tag">&lt;/div&gt;</span>
</div>
</div>
</div>
<!-- Right: Styles -->
<div class="panel style-panel">
<div class="panel-header">Styles ({{ selectedElement === 'box' ? '.box' : '.text' }})</div>
<div class="style-content">
<div class="css-rule">
<span class="selector">{{ selectedElement === 'box' ? '.box' : '.text' }}</span> {
<div v-for="(value, prop) in styles[selectedElement]" :key="prop" class="css-prop">
<span class="prop-name">{{ prop }}</span>:
<span class="prop-value">
<!-- Simple editable input simulation -->
<input
v-model="styles[selectedElement][prop]"
class="style-input"
/>
</span>;
</div>
}
</div>
</div>
</div>
</div>
<!-- Preview Area -->
<div class="preview-area">
<div class="preview-label">页面预览 (Page Preview)</div>
<div class="preview-content">
<div :style="styles.box">
<span :style="styles.text">Hello DevTools</span>
</div>
</div>
</div>
<div class="footer-tip">
点击左侧 DOM 树中的元素在右侧 Styles 面板修改样式下方预览会实时更新
</div>
</el-card>
</template>
<style scoped>
.elements-demo {
margin: 20px 0;
}
.devtools-layout {
display: flex;
height: 250px;
border: 1px solid #dcdfe6;
font-family: monospace;
font-size: 13px;
}
.panel {
flex: 1;
display: flex;
flex-direction: column;
}
.dom-panel {
border-right: 1px solid #dcdfe6;
background-color: #fff;
}
.style-panel {
background-color: #f9f9f9;
}
.panel-header {
padding: 5px 10px;
background-color: #f0f2f5;
border-bottom: 1px solid #dcdfe6;
font-weight: bold;
color: #606266;
font-size: 12px;
}
.dom-content, .style-content {
padding: 10px;
overflow-y: auto;
flex: 1;
}
.dom-line {
padding: 2px 4px;
cursor: pointer;
white-space: nowrap;
}
.dom-line:hover {
background-color: #f0f9eb;
}
.dom-line.selected {
background-color: #d1e8ff; /* Selection color */
}
.indent { padding-left: 20px; }
.indent-2 { padding-left: 40px; }
.tag { color: #a626a4; }
.attr { color: #986801; }
.css-rule {
color: #333;
}
.selector { color: #d19a66; }
.prop-name { color: #e45649; }
.prop-value { color: #50a14f; }
.style-input {
border: none;
background: transparent;
color: inherit;
font-family: inherit;
width: 100px;
border-bottom: 1px dashed #ccc;
}
.style-input:focus {
outline: none;
border-bottom: 1px solid #409eff;
}
.preview-area {
margin-top: 15px;
border: 1px dashed #dcdfe6;
padding: 15px;
border-radius: 4px;
position: relative;
}
.preview-label {
position: absolute;
top: -10px;
left: 10px;
background: #fff;
padding: 0 5px;
font-size: 12px;
color: #909399;
}
.preview-content {
display: flex;
justify-content: center;
align-items: center;
min-height: 100px;
}
.header {
font-weight: bold;
}
.footer-tip {
margin-top: 10px;
font-size: 12px;
color: #909399;
text-align: center;
}
</style>
@@ -0,0 +1,232 @@
<script setup>
import { ref, computed } from 'vue'
const requests = ref([
{ name: 'index.html', method: 'GET', status: 200, type: 'document', size: '12KB', time: 120, start: 0 },
{ name: 'style.css', method: 'GET', status: 200, type: 'stylesheet', size: '24KB', time: 80, start: 100 },
{ name: 'app.js', method: 'GET', status: 200, type: 'script', size: '150KB', time: 250, start: 120 },
{ name: 'logo.png', method: 'GET', status: 200, type: 'png', size: '45KB', time: 150, start: 200 },
{ name: 'api/user', method: 'GET', status: 200, type: 'fetch', size: '500B', time: 300, start: 350 },
{ name: 'analytics', method: 'POST', status: 204, type: 'xhr', size: '0B', time: 50, start: 600 },
{ name: 'broken-image.jpg', method: 'GET', status: 404, type: 'jpeg', size: '0B', time: 40, start: 220 }
])
const maxTime = computed(() => {
return Math.max(...requests.value.map(r => r.start + r.time)) + 100
})
const getTimelineStyle = (req) => {
const left = (req.start / maxTime.value) * 100
const width = (req.time / maxTime.value) * 100
return {
left: `${left}%`,
width: `${Math.max(width, 1)}%`,
backgroundColor: req.status >= 400 ? '#f56c6c' : '#409eff'
}
}
const selectedRequest = ref(null)
const drawerVisible = ref(false)
const showDetails = (row) => {
selectedRequest.value = row
drawerVisible.value = true
}
const refresh = () => {
const original = [...requests.value]
requests.value = []
setTimeout(() => {
requests.value = original.map(r => ({
...r,
// Add random variation
time: Math.floor(r.time * (0.8 + Math.random() * 0.4)),
status: r.name.includes('broken') ? 404 : 200
}))
}, 300)
}
const addFailedRequest = () => {
requests.value.push({
name: 'api/error',
method: 'GET',
status: 500,
type: 'fetch',
size: '156B',
time: 120,
start: maxTime.value - 100
})
}
</script>
<template>
<el-card class="network-demo" shadow="hover">
<template #header>
<div class="header">
<span class="title">Network (网络面板)</span>
<div class="actions">
<el-button type="primary" size="small" icon="Refresh" @click="refresh">刷新页面</el-button>
<el-button type="danger" size="small" icon="Warning" @click="addFailedRequest">模拟请求失败</el-button>
</div>
</div>
</template>
<el-table
:data="requests"
style="width: 100%"
height="300"
@row-click="showDetails"
class="network-table"
>
<el-table-column prop="name" label="Name" min-width="120">
<template #default="scope">
<span :class="{ error: scope.row.status >= 400 }">{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column prop="status" label="Status" width="80">
<template #default="scope">
<el-tag :type="scope.row.status >= 400 ? 'danger' : 'success'" size="small">
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="type" label="Type" width="90" />
<el-table-column prop="size" label="Size" width="80" />
<el-table-column prop="time" label="Time" width="80">
<template #default="scope">{{ scope.row.time }}ms</template>
</el-table-column>
<el-table-column label="Waterfall" min-width="150">
<template #default="scope">
<div class="timeline-container">
<div class="timeline-bar" :style="getTimelineStyle(scope.row)"></div>
</div>
</template>
</el-table-column>
</el-table>
<div class="footer-tip">
💡 点击某一行可以查看请求详情
</div>
<!-- Detail Drawer -->
<el-drawer
v-model="drawerVisible"
:title="selectedRequest ? selectedRequest.name : 'Detail'"
direction="rtl"
size="50%"
:append-to-body="false"
class="detail-drawer"
>
<div v-if="selectedRequest">
<el-tabs>
<el-tab-pane label="Headers">
<div class="detail-section">
<h4>General</h4>
<p><strong>Request URL:</strong> https://example.com/{{ selectedRequest.name }}</p>
<p><strong>Request Method:</strong> {{ selectedRequest.method }}</p>
<p><strong>Status Code:</strong> {{ selectedRequest.status }}</p>
</div>
<div class="detail-section">
<h4>Response Headers</h4>
<p><strong>Content-Type:</strong> {{ selectedRequest.type === 'document' ? 'text/html' : selectedRequest.type === 'fetch' ? 'application/json' : 'text/plain' }}</p>
<p><strong>Cache-Control:</strong> max-age=3600</p>
</div>
</el-tab-pane>
<el-tab-pane label="Preview">
<div class="preview-box">
<div v-if="selectedRequest.status >= 400">
Failed to load response data
</div>
<div v-else-if="selectedRequest.type === 'fetch' || selectedRequest.type === 'xhr'">
<pre>{ "id": 123, "data": "Sample API response" }</pre>
</div>
<div v-else-if="selectedRequest.type === 'png' || selectedRequest.type === 'jpeg'">
<div class="fake-image">Image Preview</div>
</div>
<div v-else>
<pre>&lt;html&gt;...&lt;/html&gt;</pre>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="Response">
<div class="response-raw">
(Raw response data would appear here)
</div>
</el-tab-pane>
</el-tabs>
</div>
</el-drawer>
</el-card>
</template>
<style scoped>
.network-demo {
margin: 20px 0;
position: relative; /* For drawer absolute positioning if needed, though drawer usually fixed */
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.timeline-container {
width: 100%;
height: 16px;
background-color: #f0f2f5;
border-radius: 2px;
position: relative;
}
.timeline-bar {
position: absolute;
height: 100%;
border-radius: 2px;
opacity: 0.8;
}
.error {
color: #f56c6c;
}
.detail-section {
margin-bottom: 20px;
}
.detail-section h4 {
margin-bottom: 8px;
color: #303133;
}
.detail-section p {
margin: 4px 0;
font-size: 13px;
color: #606266;
word-break: break-all;
}
.preview-box {
background: #f5f7fa;
padding: 10px;
border-radius: 4px;
font-family: monospace;
}
.fake-image {
width: 100px;
height: 100px;
background: #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
}
.footer-tip {
margin-top: 10px;
font-size: 12px;
color: #909399;
text-align: center;
}
</style>
@@ -0,0 +1,428 @@
<script setup>
import { ref, computed } from 'vue'
const codeLines = [
'function calculateTotal(price, tax) {',
' const taxAmount = price * tax;',
' const total = price + taxAmount;',
' return total;',
'}',
'',
'const myPrice = 100;',
'const myTax = 0.1;',
'const result = calculateTotal(myPrice, myTax);',
'console.log("Total:", result);'
]
const breakpoints = ref([2]) // Line 2 has breakpoint initially
const currentLine = ref(-1)
const isRunning = ref(false)
const variables = ref({})
const logs = ref([])
const toggleBreakpoint = (index) => {
const i = breakpoints.value.indexOf(index)
if (i === -1) {
breakpoints.value.push(index)
} else {
breakpoints.value.splice(i, 1)
}
}
const reset = () => {
currentLine.value = -1
isRunning.value = false
variables.value = {}
logs.value = []
}
const run = () => {
reset()
isRunning.value = true
step()
}
const step = () => {
if (!isRunning.value) return
let nextLine = currentLine.value + 1
// Skip empty lines
while (nextLine < codeLines.length && codeLines[nextLine].trim() === '') {
nextLine++
}
if (nextLine >= codeLines.length) {
isRunning.value = false
currentLine.value = -1
return
}
currentLine.value = nextLine
// Execute logic for simulation
updateVariables(nextLine)
// Check breakpoint
if (breakpoints.value.includes(nextLine)) {
// Paused
} else {
// Auto continue if no breakpoint, but for demo we might want manual stepping or slow motion
// For this demo, "Run" just goes to first breakpoint or end.
// But "Step" button is manual.
// Let's make "Run" auto-advance until breakpoint.
setTimeout(() => {
if (breakpoints.value.includes(nextLine)) {
// Pause
} else {
step()
}
}, 200) // Small delay to see execution
}
}
const stepOver = () => {
if (!isRunning.value && currentLine.value === -1) {
run()
return
}
// Force move to next line regardless of breakpoint
let nextLine = currentLine.value + 1
while (nextLine < codeLines.length && codeLines[nextLine].trim() === '') {
nextLine++
}
if (nextLine >= codeLines.length) {
isRunning.value = false
currentLine.value = -1
return
}
currentLine.value = nextLine
updateVariables(nextLine)
}
const updateVariables = (lineIndex) => {
// Simulation logic based on line number
// 0: function def
// 1: taxAmount = ... (inside function)
// 2: total = ... (inside function)
// 3: return
// 6: myPrice = 100
// 7: myTax = 0.1
// 8: call function
// 9: log
// We simulate the execution flow roughly
if (lineIndex === 6) variables.value = { ...variables.value, myPrice: 100 }
if (lineIndex === 7) variables.value = { ...variables.value, myTax: 0.1 }
// When calling function at line 8, we jump to line 0?
// This simple line-by-line is hard for function calls without complex logic.
// Let's simplify: Flatten the logic or just simulate state at specific lines.
if (lineIndex === 8) {
// Simulate jumping into function?
// For simplicity, let's just pretend we are inside.
// Or actually, let's just change the code to be flat for easier understanding in demo.
}
}
// Let's use a simpler flat code example for the demo to be robust
const flatCodeLines = [
'let count = 0;',
'const max = 3;',
'while (count < max) {',
' count = count + 1;',
' console.log("Count is:", count);',
'}',
'console.log("Done");'
]
// 0: let count = 0
// 1: const max = 3
// 2: while check
// 3: count++
// 4: log
// 5: } -> jump back to 2
// 6: log Done
// Re-implement step logic for flat code
const demoState = ref({
line: -1,
vars: { count: undefined, max: undefined },
output: [],
history: [] // to track loop
})
const flatStep = () => {
const s = demoState.value
let next = s.line
// Logic flow
if (s.line === -1) next = 0
else if (s.line === 0) next = 1
else if (s.line === 1) next = 2
else if (s.line === 2) {
// Check condition
if (s.vars.count < s.vars.max) next = 3
else next = 6
}
else if (s.line === 3) next = 4
else if (s.line === 4) next = 5
else if (s.line === 5) next = 2 // Loop back
else if (s.line === 6) {
// End
s.line = -1
isRunning.value = false
return
}
s.line = next
// Execute line
if (next === 0) s.vars.count = 0
if (next === 1) s.vars.max = 3
if (next === 3) s.vars.count++
if (next === 4) s.output.push(`Count is: ${s.vars.count}`)
if (next === 6) s.output.push('Done')
}
const flatRun = () => {
if (isRunning.value) return
demoState.value.line = -1
demoState.value.vars = { count: undefined, max: undefined }
demoState.value.output = []
isRunning.value = true
const tick = () => {
if (!isRunning.value) return
// Peek next line
// ... (Logic duplication is tricky, let's just use flatStep)
flatStep()
if (breakpoints.value.includes(demoState.value.line)) {
// Pause
} else if (demoState.value.line !== -1) {
setTimeout(tick, 500)
}
}
tick()
}
const flatResume = () => {
if (!isRunning.value) return
const tick = () => {
flatStep()
if (breakpoints.value.includes(demoState.value.line)) {
// Pause again
} else if (demoState.value.line !== -1) {
setTimeout(tick, 500)
}
}
setTimeout(tick, 500)
}
const flatNext = () => {
if (!isRunning.value && demoState.value.line === -1) {
demoState.value.vars = { count: undefined, max: undefined }
demoState.value.output = []
isRunning.value = true
}
flatStep()
}
</script>
<template>
<el-card class="sources-demo" shadow="hover">
<template #header>
<div class="header">
<span class="title">Sources (源代码调试)</span>
<div class="controls">
<el-button-group>
<el-button type="success" size="small" icon="VideoPlay" @click="flatRun" :disabled="isRunning && demoState.line !== -1 && !breakpoints.includes(demoState.line)">Run</el-button>
<el-button type="primary" size="small" icon="VideoPause" @click="flatResume" :disabled="!breakpoints.includes(demoState.line)">Resume</el-button>
<el-button type="info" size="small" icon="ArrowRight" @click="flatNext">Step</el-button>
</el-button-group>
</div>
</div>
</template>
<div class="container">
<div class="code-area">
<div
v-for="(line, index) in flatCodeLines"
:key="index"
class="line"
:class="{
active: demoState.line === index,
breakpoint: breakpoints.includes(index)
}"
@click="toggleBreakpoint(index)"
>
<div class="line-num">{{ index + 1 }}</div>
<div class="code-text">{{ line }}</div>
</div>
</div>
<div class="sidebar">
<div class="section">
<div class="section-title">Scope (Variables)</div>
<div class="var-list">
<div class="var-item">
<span class="name">count:</span>
<span class="value">{{ demoState.vars.count !== undefined ? demoState.vars.count : 'undefined' }}</span>
</div>
<div class="var-item">
<span class="name">max:</span>
<span class="value">{{ demoState.vars.max !== undefined ? demoState.vars.max : 'undefined' }}</span>
</div>
</div>
</div>
<div class="section">
<div class="section-title">Console Output</div>
<div class="output-list">
<div v-for="(log, i) in demoState.output" :key="i" class="log-line">
{{ log }}
</div>
</div>
</div>
</div>
</div>
<div class="footer-tip">
点击行号设置断点点击 Run 开始执行代码将在断点处暂停
</div>
</el-card>
</template>
<style scoped>
.sources-demo {
margin: 20px 0;
}
.container {
display: flex;
height: 300px;
border: 1px solid #dcdfe6;
}
.code-area {
flex: 2;
background: #f5f7fa;
overflow-y: auto;
font-family: monospace;
border-right: 1px solid #dcdfe6;
}
.line {
display: flex;
cursor: pointer;
line-height: 24px;
}
.line:hover {
background-color: #ecf5ff;
}
.line.active {
background-color: #e8f3ff; /* Light blue background for execution line */
}
.line.active .code-text {
background-color: #cce5ff;
}
.line-num {
width: 40px;
text-align: right;
padding-right: 10px;
color: #909399;
border-right: 1px solid #ebeef5;
user-select: none;
position: relative;
}
.line.breakpoint .line-num::before {
content: '';
position: absolute;
left: 8px;
top: 6px;
width: 12px;
height: 12px;
background-color: #f56c6c;
border-radius: 50%;
}
/* Green arrow for current line */
.line.active .line-num::after {
content: '→';
position: absolute;
right: 2px;
color: #409eff;
font-weight: bold;
}
.code-text {
padding-left: 10px;
white-space: pre;
color: #303133;
flex: 1;
}
.sidebar {
flex: 1;
background: #fff;
display: flex;
flex-direction: column;
}
.section {
padding: 10px;
border-bottom: 1px solid #ebeef5;
}
.section-title {
font-weight: bold;
font-size: 12px;
color: #606266;
margin-bottom: 8px;
background: #f0f2f5;
padding: 4px;
}
.var-item {
font-family: monospace;
font-size: 13px;
margin-bottom: 4px;
}
.var-item .name {
color: #906fa5;
margin-right: 8px;
}
.var-item .value {
color: #409eff;
}
.output-list {
font-family: monospace;
font-size: 12px;
color: #606266;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-tip {
margin-top: 10px;
font-size: 12px;
color: #909399;
text-align: center;
}
</style>