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:
+205
@@ -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"><div</span> <span class="attr">id</span>="app" <span class="attr">class</span>="container"<span class="tag">></span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="dom-line indent"
|
||||
:class="{ selected: selectedElement === 'box' }"
|
||||
@click="selectedElement = 'box'"
|
||||
>
|
||||
<span class="tag"><div</span> <span class="attr">id</span>="target-box" <span class="attr">class</span>="box"<span class="tag">></span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="dom-line indent-2"
|
||||
:class="{ selected: selectedElement === 'text' }"
|
||||
@click="selectedElement = 'text'"
|
||||
>
|
||||
<span class="tag"><span</span> <span class="attr">class</span>="text"<span class="tag">></span>Hello DevTools<span class="tag"></span></span>
|
||||
</div>
|
||||
|
||||
<div class="dom-line indent">
|
||||
<span class="tag"></div></span>
|
||||
</div>
|
||||
|
||||
<div class="dom-line">
|
||||
<span class="tag"></div></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><html>...</html></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>
|
||||
Reference in New Issue
Block a user