Files
test-repo/docs/.vitepress/theme/components/appendix/browser-devtools/BrowserDevToolsDemo.vue
T

2347 lines
61 KiB
Vue
Raw Normal View History

<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
const activeTab = ref('elements') // 默认改为 Elements,匹配截图
const hoverInfo = ref('')
const isDark = ref(false)
const isAutoPlaying = ref(false)
const cursorX = ref(0)
const cursorY = ref(0)
const cursorVisible = ref(false)
const highlightVisible = ref(false)
const highlightStyle = ref({})
let tourTimeout = null
const demoRef = ref(null)
// 导览选项
const tourOptions = [
{ value: '', label: '选择导览场景...', disabled: true },
{ value: 'elements', label: '1. 元素面板 (Elements)' },
{ value: 'console', label: '2. 控制台 (Console)' },
{ value: 'sources', label: '3. 源代码 (Sources)' },
{ value: 'network', label: '4. 网络 (Network)' },
{ value: 'application', label: '5. 应用 (Application)' }
]
const selectedTour = ref('')
const tabs = [
{
id: 'elements',
label: '元素',
desc: '查看和修改页面 HTML 结构与 CSS 样式'
},
{
id: 'console',
label: '控制台',
desc: '查看日志、错误信息,执行 JavaScript 代码'
},
{
id: 'sources',
label: '源代码/来源',
desc: '查看源代码,设置断点调试 JavaScript'
},
{
id: 'network',
label: '网络',
desc: '监控网络请求,查看接口数据和加载性能'
},
{ id: 'performance', label: '性能', desc: '分析页面运行性能' },
{ id: 'memory', label: '内存', desc: '检测内存泄漏' },
{
id: 'application',
label: '应用',
desc: '查看本地存储(Storage)、Cookies、缓存等'
},
{ id: 'security', label: '隐私与安全', desc: '查看证书和安全问题' },
{ id: 'lighthouse', label: 'Lighthouse', desc: '页面质量审计' },
{ id: 'recorder', label: '记录器', desc: '录制用户操作' }
]
// --- Console Data ---
const consoleSidebarItems = [
{ label: '6 条消息', icon: 'list', count: 6, type: 'all' },
{ label: '6 条用户消息', icon: 'user', count: 6, type: 'user' },
{ label: '无错误', icon: 'error', count: 0, type: 'error' },
{ label: '无警告', icon: 'warn', count: 0, type: 'warn' },
{ label: '无信息', icon: 'info', count: 0, type: 'info' },
{ label: '6 条详细消息', icon: 'verbose', count: 6, type: 'verbose' }
]
const consoleLogs = ref([
{ type: 'log', msg: '[vite] connecting...', source: 'client:733' },
{ type: 'log', msg: '[vite] connected.', source: 'client:827' },
{
type: 'log',
msg: 'Config Layers for 404.md:\n========================\n1. locale config (root)\n2. .vitepress/config (root)',
source: 'shared.js:194'
},
{
type: 'log',
msg: 'Config Layers for zh-cn/appendix/browser-devtools/index.md:\n=======================================================\n1. locale config (zh-cn)\n2. .vitepress/config (root)',
source: 'shared.js:194'
},
{
type: 'log',
msg: '[vite] hot updated: .vitepress/theme/components/appendix/browser-devtools/BrowserDevToolsDemo.vue',
source: 'client:810'
},
{
type: 'log',
msg: '[vite] hot updated: .vitepress/theme/components/appendix/browser-devtools/BrowserDevToolsDemo.vue?vue&type=style&index=0&scoped=d906459f&lang.css',
source: 'client:810'
}
])
const consoleInput = ref('')
// --- Elements Data (Aligned with screenshot) ---
const domTree = ref([
{
tag: 'html',
attrs: { class: 'mac', lang: 'zh-CN', dir: 'ltr' },
expanded: true,
children: [
{
tag: 'head',
expanded: false,
children: [{ tag: 'title', text: 'DevTools Demo' }]
},
{
tag: 'body',
expanded: true,
children: [
{
tag: 'div',
attrs: { id: 'app', 'data-v-app': '' },
expanded: true,
children: [
{ tag: 'div', text: '' },
{
tag: 'script',
attrs: {
type: 'module',
src: '/easy-vibe/node_modules/vitepress/dist/client/app/index.js'
}
},
{
tag: 'div',
attrs: { id: 'el-popper-container-3083' },
text: ''
}
]
},
{
tag: 'div',
attrs: { style: 'all: initial' },
expanded: false,
children: []
},
{
tag: 'div',
attrs: {
id: 'immersive-translate-browser-popup',
style: 'all: initial'
},
expanded: false,
children: []
}
]
}
]
}
])
const cssRules = ref([
{
selector: 'body',
styles: {
'background-color': 'rgb(255, 255, 255)',
color: '#24292f',
margin: '0',
'font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto'
}
},
{ selector: '#app', styles: { padding: '20px' } },
{ selector: '.mac', styles: { 'font-synthesis': 'none' } }
])
// --- Sources Data ---
const fileSystem = ref([
{ name: 'index.html', type: 'file' },
{
name: 'src',
type: 'folder',
expanded: true,
children: [
{ name: 'main.js', type: 'file' },
{ name: 'App.vue', type: 'file' },
{ name: 'utils.js', type: 'file' }
]
}
])
const activeFile = ref('main.js')
const fileContent = ref(`import { createApp } from 'vue'
import App from './App.vue'
console.log('App mounted successfully.')
const app = createApp(App)
app.mount('#app')`)
// --- Network Data ---
const networkRequests = ref([
{
id: 1,
name: 'index.html',
status: 200,
type: 'document',
size: '2.4kB',
time: '120ms',
waterfall: 10,
headers: { 'Content-Type': 'text/html; charset=utf-8', Server: 'Vite' },
requestHeaders: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
Accept: 'text/html'
},
preview:
'<!DOCTYPE html>\n<html lang="zh-CN">\n<head>...</head>\n<body>...</body>\n</html>'
},
{
id: 2,
name: 'main.js',
status: 200,
type: 'script',
size: '15.2kB',
time: '80ms',
waterfall: 40,
headers: {
'Content-Type': 'application/javascript',
'Cache-Control': 'no-cache'
},
requestHeaders: {
'User-Agent': 'Mozilla/5.0 ...',
Referer: 'http://localhost:3000/'
},
preview:
'import { createApp } from "vue";\nimport App from "./App.vue";\ncreateApp(App).mount("#app");'
},
{
id: 3,
name: 'style.css',
status: 200,
type: 'stylesheet',
size: '4.1kB',
time: '45ms',
waterfall: 50,
headers: { 'Content-Type': 'text/css' },
requestHeaders: {
'User-Agent': 'Mozilla/5.0 ...',
Referer: 'http://localhost:3000/'
},
preview:
'body { margin: 0; font-family: sans-serif; }\n#app { padding: 20px; }'
},
{
id: 4,
name: 'api/user',
status: 200,
type: 'fetch',
size: '500B',
time: '200ms',
waterfall: 120,
headers: { 'Content-Type': 'application/json' },
requestHeaders: {
Authorization: 'Bearer eyJhbGci...',
Accept: 'application/json',
'Content-Type': 'application/json'
},
preview:
'{\n "id": 1001,\n "username": "developer",\n "role": "admin",\n "permissions": ["read", "write"]\n}'
},
{
id: 5,
name: 'logo.png',
status: 304,
type: 'png',
size: '12kB',
time: '20ms',
waterfall: 60,
headers: { 'Content-Type': 'image/png' },
requestHeaders: {
'User-Agent': 'Mozilla/5.0 ...',
Accept: 'image/webp,image/apng'
},
preview: '[Binary Data - Image]'
}
])
const selectedRequest = ref(null)
const activeDetailTab = ref('headers')
const selectRequest = (req) => {
if (selectedRequest.value && selectedRequest.value.id === req.id) {
selectedRequest.value = null // Toggle off
} else {
selectedRequest.value = req
activeDetailTab.value = 'headers' // Reset to default tab
}
}
// --- Application Data ---
const localStorageData = ref([
{ key: 'theme', value: 'light' },
{ key: 'user_token', value: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' },
{ key: 'sidebar_collapsed', value: 'false' }
])
// --- Actions ---
const showInfo = (text) => {
if (isAutoPlaying.value) return
hoverInfo.value = text
}
const clearInfo = () => {
if (isAutoPlaying.value) return
hoverInfo.value = ''
}
const runConsoleCommand = () => {
if (!consoleInput.value.trim()) return
consoleLogs.value.push({ type: 'command', msg: consoleInput.value })
try {
const val = consoleInput.value
if (val === '1+1') consoleLogs.value.push({ type: 'result', msg: '2' })
else if (val.includes('alert'))
consoleLogs.value.push({ type: 'result', msg: 'undefined' })
else
consoleLogs.value.push({
type: 'error',
msg: 'ReferenceError: Command not found in mock'
})
} catch (e) {
consoleLogs.value.push({ type: 'error', msg: e.message })
}
consoleInput.value = ''
nextTick(() => {
const output = demoRef.value?.querySelector('.console-output')
if (output) output.scrollTop = output.scrollHeight
})
}
// --- Auto Tour Logic ---
const handleTourSelect = async () => {
if (!selectedTour.value) return
const target = selectedTour.value
// 如果已经在播放,先停止
if (isAutoPlaying.value) {
stopTour()
await new Promise((r) => setTimeout(r, 100))
}
// 切换到目标 Tab
activeTab.value = target
// 启动导览
startTour(target)
}
const moveCursorTo = (selector, infoText, waitTime = 2000) => {
return new Promise((resolve) => {
const container = demoRef.value
if (!container) return resolve()
// Find element
const el = container.querySelector(selector)
if (el) {
const containerRect = container.getBoundingClientRect()
const rect = el.getBoundingClientRect()
// Calculate center
const targetX = rect.left - containerRect.left + rect.width / 2
const targetY = rect.top - containerRect.top + rect.height / 2
// Move cursor
cursorX.value = targetX
cursorY.value = targetY
cursorVisible.value = true
// Show highlight after a slight delay to simulate travel time
setTimeout(() => {
if (!isAutoPlaying.value) return resolve()
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
hoverInfo.value = infoText
// Wait and then clear
tourTimeout = setTimeout(() => {
highlightVisible.value = false
resolve()
}, waitTime)
}, 500) // faster movement
} else {
console.warn('Selector not found:', selector)
resolve() // Skip if not found
}
})
}
const stopTour = () => {
isAutoPlaying.value = false
cursorVisible.value = false
highlightVisible.value = false
hoverInfo.value = ''
selectedTour.value = ''
if (tourTimeout) clearTimeout(tourTimeout)
}
const startTour = async (targetTab) => {
if (isAutoPlaying.value) return
isAutoPlaying.value = true
cursorVisible.value = true
hoverInfo.value = ''
try {
// Dispatch based on target tab
if (targetTab === 'console') await runConsoleTour()
else if (targetTab === 'elements') await runElementsTour()
else if (targetTab === 'sources') await runSourcesTour()
else if (targetTab === 'network') await runNetworkTour()
else if (targetTab === 'application') await runApplicationTour()
stopTour()
} catch (e) {
console.error(e)
stopTour()
}
}
const runConsoleTour = async () => {
await moveCursorTo(
'.tab[data-id="console"]',
'控制台 (Console):查看日志、交互式运行代码'
)
if (!isAutoPlaying.value) return
await moveCursorTo(
'.console-toolbar',
'工具栏:可清空日志、设置 Log 级别、过滤内容'
)
if (!isAutoPlaying.value) return
await moveCursorTo(
'.console-sidebar',
'侧边栏:按类型聚合消息 (Errors, Warnings)'
)
if (!isAutoPlaying.value) return
await moveCursorTo(
'.log-line:nth-child(1)',
'日志流:显示代码输出,点击右侧链接可跳转源码'
)
if (!isAutoPlaying.value) return
await moveCursorTo(
'.bottom-drawer-header',
'抽屉 (Drawer):查看搜索结果、Issues 等辅助信息'
)
if (!isAutoPlaying.value) return
await moveCursorTo(
'.console-input-area',
'即时执行:在这里输入 JS 表达式并回车运行'
)
}
const runElementsTour = async () => {
await moveCursorTo(
'.tab[data-id="elements"]',
'元素面板 (Elements):实时查看和修改 DOM/CSS'
)
if (!isAutoPlaying.value) return
await moveCursorTo(
'.dom-tree-panel',
'DOM 树:页面的 HTML 结构,可折叠/展开/拖拽'
)
if (!isAutoPlaying.value) return
await moveCursorTo(
'.dom-node[data-tag="div"]',
'选中元素:点击元素以在右侧查看其样式'
)
if (!isAutoPlaying.value) return
await moveCursorTo(
'.styles-panel',
'样式面板 (Styles):查看计算后的样式和 CSS 规则'
)
if (!isAutoPlaying.value) return
await moveCursorTo(
'.style-rule:first-child',
'CSS 规则:可直接修改属性值,实时预览效果'
)
}
const runSourcesTour = async () => {
await moveCursorTo(
'.tab[data-id="sources"]',
'源代码 (Sources):文件浏览与断点调试'
)
if (!isAutoPlaying.value) return
await moveCursorTo('.file-navigator', '文件系统:查看加载的所有资源文件')
if (!isAutoPlaying.value) return
await moveCursorTo('.code-editor', '编辑器:查看源码,点击行号设置断点')
if (!isAutoPlaying.value) return
await moveCursorTo(
'.debugger-sidebar',
'调试器:查看变量 (Watch)、调用栈 (Call Stack)'
)
}
const runNetworkTour = async () => {
await moveCursorTo('.tab[data-id="network"]', '网络 (Network):抓包分析')
if (!isAutoPlaying.value) return
await moveCursorTo(
'.network-toolbar',
'过滤器:按类型筛选请求 (XHR/Fetch, CSS, JS)'
)
if (!isAutoPlaying.value) return
await moveCursorTo(
'.network-grid-header',
'请求列表:查看状态码、类型、大小、耗时'
)
if (!isAutoPlaying.value) return
// Simulate clicking the API request
await moveCursorTo('.network-row:nth-child(4)', '点击请求行查看详情')
if (!isAutoPlaying.value) return
// Trigger selection
selectedRequest.value = networkRequests.value[3] // api/user
await moveCursorTo(
'.detail-header',
'详情面板:查看 Headers, Preview, Response'
)
if (!isAutoPlaying.value) return
// 1. Headers Tab
activeDetailTab.value = 'headers'
await moveCursorTo(
'.detail-title:nth-child(1)',
'Headers: 查看请求/响应头信息'
)
if (!isAutoPlaying.value) return
await moveCursorTo(
'.detail-section:nth-child(1)',
'General:查看 URL、Method (GET/POST) 和状态码 (200)'
)
if (!isAutoPlaying.value) return
await moveCursorTo(
'.detail-section:nth-child(2)',
'Response Headers:服务器返回的头信息 (Content-Type)'
)
if (!isAutoPlaying.value) return
await moveCursorTo(
'.detail-section:nth-child(3)',
'Request Headers:浏览器发送的头信息 (User-Agent, Cookies)'
)
if (!isAutoPlaying.value) return
// 2. Preview Tab
await moveCursorTo(
'.detail-title:nth-child(2)',
'Preview: 格式化预览接口返回的数据'
)
if (!isAutoPlaying.value) return
activeDetailTab.value = 'preview'
await moveCursorTo('.preview-content', 'Preview Content: 查看 JSON 结构')
if (!isAutoPlaying.value) return
// 3. Response Tab
await moveCursorTo('.detail-title:nth-child(3)', 'Response: 查看原始响应数据')
if (!isAutoPlaying.value) return
activeDetailTab.value = 'response'
await moveCursorTo('.preview-content', 'Response Body: 原始文本内容')
if (!isAutoPlaying.value) return
await moveCursorTo(
'.waterfall-cell',
'瀑布流 (Waterfall):请求生命周期耗时分析'
)
}
const runApplicationTour = async () => {
await moveCursorTo(
'.tab[data-id="application"]',
'应用 (Application):存储与缓存管理'
)
if (!isAutoPlaying.value) return
await moveCursorTo(
'.storage-sidebar',
'存储类型:Local Storage, Cookies, IndexedDB'
)
if (!isAutoPlaying.value) return
await moveCursorTo(
'.storage-content',
'数据视图:查看 Key-Value 数据,支持增删改查'
)
}
onUnmounted(() => {
if (tourTimeout) clearTimeout(tourTimeout)
})
</script>
<template>
<div
class="browser-devtools-demo"
:class="{ 'dark-mode': isDark }"
ref="demoRef"
>
<!-- Top Controls (Custom for Demo) -->
<div class="demo-controls">
<div class="control-label">Chrome DevTools 模拟器</div>
<div class="control-actions">
<select
v-model="selectedTour"
@change="handleTourSelect"
class="tour-select"
:disabled="isAutoPlaying"
>
<option
v-for="opt in tourOptions"
:key="opt.value"
:value="opt.value"
:disabled="opt.disabled"
>
{{ opt.label }}
</option>
</select>
<button v-if="isAutoPlaying" class="stop-btn" @click="stopTour">
停止演示
</button>
</div>
</div>
<!-- Virtual Cursor & Highlight -->
<div
class="virtual-cursor"
v-if="cursorVisible"
:style="{ transform: `translate(${cursorX}px, ${cursorY}px)` }"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
style="filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2))"
>
<path
d="M5.65376 12.3673H5.46026L5.31717 12.4976L0.500002 16.8829L0.500002 1.19823L11.4818 12.3673H5.65376Z"
fill="#000"
stroke="white"
stroke-width="1.5"
/>
</svg>
</div>
<div
class="highlight-box"
v-if="highlightVisible"
:style="highlightStyle"
></div>
<!-- Main UI Container -->
<div class="devtools-container">
<!-- Header -->
<div class="devtools-header">
<div class="header-left">
<div
class="icon-btn element-picker"
title="选择页面中的元素以进行检查"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="#6e6e6e">
<path
d="M4 4h9v2H4V4zm0 4h5v2H4V8zm0 4h5v2H4v-2zm12-5l-4 4h3v4h2v-4h3l-4-4z"
/>
</svg>
</div>
<div class="icon-btn device-toggle" title="切换设备工具栏">
<svg width="16" height="16" viewBox="0 0 24 24" fill="#6e6e6e">
<path
d="M17 1.01L7 1c-1.1 0-2 .9-2 2v18c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM17 19H7V5h10v14z"
/>
</svg>
</div>
<div class="separator"></div>
<div class="tabs">
<div
v-for="tab in tabs"
:key="tab.id"
class="tab"
:class="{ active: activeTab === tab.id }"
:data-id="tab.id"
@click="activeTab = tab.id"
@mouseenter="showInfo(tab.desc)"
@mouseleave="clearInfo"
>
{{ tab.label }}
</div>
</div>
</div>
<div class="header-right">
<div class="icon-btn settings" title="设置"></div>
<div class="icon-btn close" title="关闭">×</div>
</div>
</div>
<!-- Body Area -->
<div class="devtools-body">
<!-- 1. Console Panel -->
<div v-if="activeTab === 'console'" class="panel console-panel-layout">
<div class="console-toolbar" @mouseenter="showInfo('控制台工具栏')">
<div class="icon-btn clear" title="清除控制台">🚫</div>
<div class="separator"></div>
<div class="dropdown-trigger">top </div>
<div class="icon-btn eye" title="创建实时表达式">👁</div>
<div class="filter-box">
<span class="filter-icon">🔍</span><input placeholder="过滤" />
</div>
<div class="dropdown-trigger">默认级别 </div>
</div>
<div class="console-main-area">
<div class="console-sidebar">
<div
v-for="(item, idx) in consoleSidebarItems"
:key="idx"
class="sidebar-item"
:class="{ active: item.type === 'all' }"
>
<span class="item-icon">{{
item.icon === 'error'
? '❌'
: item.icon === 'warn'
? '⚠️'
: item.icon === 'info'
? '️'
: item.icon === 'verbose'
? '💬'
: item.icon === 'user'
? '👤'
: '📋'
}}</span>
<span class="item-label">{{ item.label }}</span>
</div>
</div>
<div class="console-content-wrapper">
<div class="console-output">
<div
v-for="(log, idx) in consoleLogs"
:key="idx"
class="log-line"
:class="log.type"
>
<div class="log-gutter">
<span class="icon error" v-if="log.type === 'error'"
></span
>
<span class="icon warn" v-else-if="log.type === 'warn'"
></span
>
<span class="icon" v-else-if="log.type === 'command'"
>&gt;</span
>
<span class="icon" v-else>&lt;</span>
</div>
<div class="log-content">
<pre>{{ log.msg }}</pre>
</div>
<div class="log-source">
<span class="source">{{ log.source }}</span>
</div>
</div>
<!-- Input area inside scrollable area for Chrome feel -->
<div class="console-input-area">
<span class="prompt">&gt;</span>
<input
v-model="consoleInput"
@keyup.enter="runConsoleCommand"
class="input-field"
placeholder=""
/>
</div>
</div>
</div>
</div>
<!-- Bottom Drawer -->
<div class="bottom-drawer">
<div class="bottom-drawer-header">
<div
class="icon-btn more"
style="padding: 0 4px; margin-right: 4px"
>
</div>
<div class="drawer-tab">控制台</div>
<div class="drawer-tab">AI 辅助</div>
<div class="drawer-tab">新变化</div>
<div class="drawer-tab">问题</div>
<div class="drawer-tab active">
搜索 <span class="close-icon">×</span>
</div>
</div>
<div class="drawer-content">
<div class="search-panel">
<div class="search-bar">
<span class="prompt">🔍</span>
<input
placeholder="A terminal is just a grid of same-sized cells..."
class="search-input"
/>
<div class="search-actions">Aa ab .*</div>
</div>
<div class="search-results">
<div class="no-results">
<div class="no-results-title">未找到匹配项</div>
<div class="no-results-desc">
没有与您的搜索查询相符的结果
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 2. Elements Panel -->
<div v-else-if="activeTab === 'elements'" class="panel elements-panel">
<div class="dom-tree-panel">
<div class="dom-tree-content">
<div class="dom-node" data-tag="html">
<div class="line-content">
<span class="arrow expanded"></span>
<span class="tag-name">html</span>
<span class="attr-name">class</span>=<span class="attr-val"
>"mac"</span
>
<span class="attr-name">lang</span>=<span class="attr-val"
>"zh-CN"</span
>
<span class="attr-name">dir</span>=<span class="attr-val"
>"ltr"</span
>
<span class="attr-name">style</span>=<span class="attr-val"
>"--ev-doc-font-size: 14px..."</span
>
</div>
<div class="children">
<div class="dom-node" data-tag="head">
<div class="line-content">
<span class="arrow"></span>
<span class="tag-name">head</span>
<span class="dots">...</span>
</div>
</div>
<div class="dom-node" data-tag="body">
<div class="line-content">
<span class="arrow expanded"></span>
<span class="tag-name">body</span>
<span class="node-trail">== $0</span>
</div>
<div class="children">
<div class="dom-node selected" data-tag="div">
<div class="line-content">
<span class="arrow expanded"></span>
<span class="tag-name">div</span>
<span class="attr-name">id</span>=<span
class="attr-val"
>"app"</span
>
<span class="attr-name">data-v-app</span>
</div>
<div class="children">
<div class="dom-node">
<div class="line-content">
<span class="indent"></span
><span class="tag-name">div</span
><span class="dots">...</span
><span class="tag-name">/div</span>
</div>
</div>
<div class="dom-node">
<div class="line-content">
<span class="indent"></span
><span class="tag-name">script</span>
<span class="attr-name">type</span>=<span
class="attr-val"
>"module"</span
>
<span class="attr-name">src</span>=<span
class="attr-val"
>"/easy-vibe/..."</span
><span class="tag-name">/script</span>
</div>
</div>
<div class="dom-node">
<div class="line-content">
<span class="indent"></span
><span class="tag-name">div</span>
<span class="attr-name">id</span>=<span
class="attr-val"
>"el-popper-container-3083"</span
><span class="tag-name">&gt;</span
><span class="dots">...</span
><span class="tag-name">/div</span>
</div>
</div>
</div>
<div class="line-content">
<span class="tag-name">/div</span>
</div>
</div>
<div class="dom-node">
<div class="line-content">
<span class="arrow"></span>
<span class="tag-name">div</span>
<span class="attr-name">style</span>=<span
class="attr-val"
>"all: initial;"</span
><span class="tag-name">&gt;</span
><span class="dots">...</span
><span class="tag-name">/div</span>
</div>
</div>
<div class="dom-node">
<div class="line-content">
<span class="arrow"></span>
<span class="tag-name">div</span>
<span class="attr-name">id</span>=<span
class="attr-val"
>"immersive-translate-browser-popup"</span
>
<span class="attr-name">style</span>=<span
class="attr-val"
>"all: initial;"</span
><span class="tag-name">&gt;</span
><span class="dots">...</span
><span class="tag-name">/div</span>
</div>
</div>
</div>
<div class="line-content">
<span class="tag-name">/body</span>
</div>
</div>
</div>
<div class="line-content">
<span class="tag-name">/html</span>
</div>
</div>
</div>
<div class="breadcrumbs">html.mac > body > div#app</div>
<!-- Bottom Drawer (Shared) -->
<div class="bottom-drawer" style="border-top: 1px solid #ccc">
<div class="bottom-drawer-header">
<div
class="icon-btn more"
style="padding: 0 4px; margin-right: 4px"
>
</div>
<div class="drawer-tab">控制台</div>
<div class="drawer-tab">AI 辅助</div>
<div class="drawer-tab">新变化</div>
<div class="drawer-tab">问题</div>
<div class="drawer-tab active">
搜索 <span class="close-icon">×</span>
</div>
</div>
<div class="drawer-content">
<div class="search-panel">
<div class="search-bar">
<span class="prompt">🔍</span>
<input
placeholder="A terminal is just a grid of same-sized cells..."
class="search-input"
/>
<div class="search-actions">Aa ab .*</div>
</div>
<div class="search-results">
<div class="no-results">
<div class="no-results-title">未找到匹配项</div>
<div class="no-results-desc">
没有与您的搜索查询相符的结果
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="styles-panel">
<div class="styles-tabs">
<div class="style-tab active">样式</div>
<div class="style-tab">计算样式</div>
<div class="style-tab">布局</div>
<div class="style-tab">事件监听器</div>
<div class="style-tab">»</div>
</div>
<div class="styles-content">
<!-- Box Model Mock -->
<div class="box-model-container">
<div class="box-margin">
<div class="label">margin</div>
<div class="val-top">-</div>
<div class="val-left">-</div>
<div class="val-right">-</div>
<div class="val-bottom">-</div>
<div class="box-border">
<div class="label">border</div>
<div class="val-top">-</div>
<div class="val-left">-</div>
<div class="val-right">-</div>
<div class="val-bottom">-</div>
<div class="box-padding">
<div class="label">padding</div>
<div class="val-top">-</div>
<div class="val-left">-</div>
<div class="val-right">-</div>
<div class="val-bottom">-</div>
<div class="box-content">
<div class="val-content">1600 x 3461</div>
</div>
</div>
</div>
</div>
</div>
<div class="filter-bar">
<input placeholder="过滤" />
<span class="filter-opt">:hov</span>
<span class="filter-opt">.cls</span>
<span class="filter-opt">+</span>
</div>
<div
class="style-rule"
v-for="(rule, idx) in cssRules"
:key="idx"
>
<div class="selector">{{ rule.selector }} {</div>
<div
class="property"
v-for="(val, key) in rule.styles"
:key="key"
>
<span class="prop-name">{{ key }}</span
>: <span class="prop-val">{{ val }}</span
>;
</div>
<div class="selector">}</div>
</div>
</div>
</div>
</div>
<!-- 3. Sources Panel -->
<div v-else-if="activeTab === 'sources'" class="panel sources-panel">
<div class="file-navigator">
<div class="nav-header">
<span class="nav-tab active">Page</span>
<span class="nav-tab">Filesystem</span>
</div>
<div class="file-tree">
<div class="file-item file">
<span class="icon">📄</span> index.html
</div>
<div class="file-item folder expanded">
<span class="folder-icon"></span>
<span class="icon">📁</span> src
<div class="folder-children">
<div class="file-item file active">
<span class="icon">📄</span> main.js
</div>
<div class="file-item file">
<span class="icon">📄</span> App.vue
</div>
</div>
</div>
</div>
</div>
<div class="code-editor">
<div class="editor-tabs">
<div class="editor-tab active">
<span class="icon">📄</span> main.js
<span class="close">×</span>
</div>
</div>
<div class="editor-content">
<div class="line-numbers">
1<br />2<br />3<br />4<br />5<br />6
</div>
<div class="code-text">
<pre>{{ fileContent }}</pre>
</div>
</div>
</div>
<div class="debugger-sidebar">
<div class="debug-section">
<div class="section-title">
<span class="arrow"></span> Watch
</div>
<div class="section-content empty">No watch expressions</div>
</div>
<div class="debug-section">
<div class="section-title">
<span class="arrow"></span> Breakpoints
</div>
<div class="section-content">
<label><input type="checkbox" checked /> main.js:12</label>
</div>
</div>
<div class="debug-section">
<div class="section-title">
<span class="arrow"></span> Scope
</div>
</div>
</div>
</div>
<!-- 4. Network Panel -->
<div v-else-if="activeTab === 'network'" class="panel network-panel">
<div class="network-toolbar">
<div class="record-icon">🔴</div>
<div class="separator"></div>
<span class="filter-btn active">All</span>
<span class="filter-btn">Fetch/XHR</span>
<span class="filter-btn">JS</span>
<span class="filter-btn">CSS</span>
<span class="filter-btn">Img</span>
</div>
<div class="network-split-view">
<div class="network-grid">
<div class="network-grid-header">
<div class="col name">Name</div>
<div class="col status">Status</div>
<div class="col type">Type</div>
<div class="col size">Size</div>
<div class="col time">Time</div>
<div class="col waterfall">Waterfall</div>
</div>
<div class="network-rows">
<div
class="network-row"
v-for="(req, idx) in networkRequests"
:key="idx"
:class="{
selected: selectedRequest && selectedRequest.id === req.id
}"
@click="selectRequest(req)"
>
<div class="col name">{{ req.name }}</div>
<div class="col status">{{ req.status }}</div>
<div class="col type">{{ req.type }}</div>
<div class="col size">{{ req.size }}</div>
<div class="col time">{{ req.time }}</div>
<div class="col waterfall">
<div
class="waterfall-bar"
:style="{
width: req.waterfall + 'px',
left: idx * 10 + 'px'
}"
></div>
</div>
</div>
</div>
</div>
<!-- Network Detail Panel (Right Side) -->
<div class="network-detail" v-if="selectedRequest">
<div class="detail-header">
<span
class="detail-title"
:class="{ active: activeDetailTab === 'headers' }"
@click="activeDetailTab = 'headers'"
>Headers</span
>
<span
class="detail-title"
:class="{ active: activeDetailTab === 'preview' }"
@click="activeDetailTab = 'preview'"
>Preview</span
>
<span
class="detail-title"
:class="{ active: activeDetailTab === 'response' }"
@click="activeDetailTab = 'response'"
>Response</span
>
<span class="close-detail" @click="selectedRequest = null"
>×</span
>
</div>
<div class="detail-content">
<div v-if="activeDetailTab === 'headers'">
<div class="detail-section">
<div class="section-label">General</div>
<div class="detail-row">
<span class="key">Request URL:</span>
<span class="val"
>http://localhost:3000/{{ selectedRequest.name }}</span
>
</div>
<div class="detail-row">
<span class="key">Request Method:</span>
<span class="val">GET</span>
</div>
<div class="detail-row">
<span class="key">Status Code:</span>
<span class="val status-code"
>{{ selectedRequest.status }} OK</span
>
</div>
</div>
<div class="detail-section">
<div class="section-label">Response Headers</div>
<div
class="detail-row"
v-for="(val, key) in selectedRequest.headers"
:key="key"
>
<span class="key">{{ key }}:</span>
<span class="val">{{ val }}</span>
</div>
</div>
<div
class="detail-section"
v-if="selectedRequest.requestHeaders"
>
<div class="section-label">Request Headers</div>
<div
class="detail-row"
v-for="(val, key) in selectedRequest.requestHeaders"
:key="key"
>
<span class="key">{{ key }}:</span>
<span class="val">{{ val }}</span>
</div>
</div>
</div>
<div v-if="activeDetailTab === 'preview'">
<div class="detail-section">
<div class="section-label">Preview</div>
<div class="preview-content">
{{ selectedRequest.preview }}
</div>
</div>
</div>
<div v-if="activeDetailTab === 'response'">
<div class="detail-section">
<div class="section-label">Response</div>
<div class="preview-content">
{{ selectedRequest.preview }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 5. Application Panel -->
<div
v-else-if="activeTab === 'application'"
class="panel application-panel"
>
<div class="storage-sidebar">
<div class="sidebar-section">
<div class="section-title">Application</div>
<div class="section-item">Manifest</div>
<div class="section-item">Service Workers</div>
</div>
<div class="sidebar-section">
<div class="section-title">Storage</div>
<div class="section-item active">
<span class="arrow"></span> Local Storage
</div>
<div class="section-item indent">http://localhost</div>
<div class="section-item">
<span class="arrow"></span> Session Storage
</div>
<div class="section-item">
<span class="arrow"></span> Cookies
</div>
</div>
</div>
<div class="storage-content">
<div class="storage-table">
<div class="table-header">
<div class="col key">Key</div>
<div class="col value">Value</div>
</div>
<div
class="table-row"
v-for="(item, idx) in localStorageData"
:key="idx"
>
<div class="col key">{{ item.key }}</div>
<div class="col value">{{ item.value }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- Info Bar Overlay -->
<div class="info-bar" v-if="hoverInfo">
<span class="info-icon">💡</span> {{ hoverInfo }}
</div>
</div>
</div>
</template>
<style scoped>
/* Reset & Base - COMPACT MODE */
.browser-devtools-demo {
border: 1px solid #d0d7de;
border-radius: 6px;
background-color: #ffffff;
color: #202124;
font-family: 'Segoe UI', '.SFNSDisplay', 'Roboto', sans-serif;
font-size: 11px; /* Smaller font */
overflow: hidden;
display: flex;
flex-direction: column;
height: 400px; /* Reduced height */
position: relative;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
user-select: none;
}
/* Demo Controls (Top Bar) */
.demo-controls {
padding: 6px 12px;
background: #f6f8fa;
border-bottom: 1px solid #d0d7de;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 10;
height: 32px;
}
.control-label {
font-weight: 600;
color: #24292f;
font-size: 12px;
}
.control-actions {
display: flex;
gap: 8px;
}
.tour-select {
padding: 2px 6px;
border: 1px solid #d0d7de;
border-radius: 4px;
font-size: 11px;
color: #24292f;
min-width: 180px;
cursor: pointer;
}
.stop-btn {
background: #cf222e;
color: white;
border: none;
padding: 2px 8px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
font-size: 11px;
}
/* DevTools Container */
.devtools-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Header & Tabs */
.devtools-header {
background-color: #f3f3f3;
border-bottom: 1px solid #ccc;
display: flex;
justify-content: space-between;
height: 24px; /* Reduced header height */
padding: 0 4px;
}
.header-left,
.header-right {
display: flex;
align-items: center;
height: 100%;
}
.icon-btn {
padding: 0 6px;
cursor: pointer;
color: #6e6e6e;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.icon-btn:hover {
color: #202124;
background-color: #eaeaea;
}
.separator {
width: 1px;
height: 14px;
background-color: #ccc;
margin: 0 6px;
}
.tabs {
display: flex;
height: 100%;
overflow-x: auto;
}
.tab {
padding: 0 8px;
cursor: pointer;
display: flex;
align-items: center;
color: #5f6368;
border-bottom: 2px solid transparent;
height: 100%;
font-size: 11px;
white-space: nowrap;
}
.tab:hover {
background-color: #e8eaed;
color: #202124;
}
.tab.active {
color: #1a73e8;
border-bottom: 2px solid #1a73e8;
font-weight: 500;
}
/* Body Layout */
.devtools-body {
flex: 1;
display: flex;
overflow: hidden;
background-color: #fff;
position: relative;
}
.panel {
flex: 1;
display: flex;
overflow: hidden;
width: 100%;
}
/* --- 1. Console Panel --- */
.console-panel-layout {
flex-direction: column;
}
.console-toolbar {
height: 24px; /* Reduced */
border-bottom: 1px solid #e0e0e0;
display: flex;
align-items: center;
padding: 0 4px;
background: #f1f3f4;
}
.filter-box {
display: flex;
align-items: center;
border: 1px solid #ccc;
background: #fff;
border-radius: 2px;
padding: 0 4px;
margin: 0 8px;
flex: 1;
max-width: 300px;
height: 18px;
}
.filter-box input {
border: none;
outline: none;
width: 100%;
font-size: 11px;
}
.dropdown-trigger {
font-size: 11px;
color: #5f6368;
padding: 0 6px;
cursor: pointer;
}
.console-main-area {
flex: 1;
display: flex;
overflow: hidden;
}
.console-sidebar {
width: 160px;
border-right: 1px solid #e0e0e0;
background: #f3f3f3;
padding-top: 2px;
}
.sidebar-item {
display: flex;
align-items: center;
padding: 1px 8px;
cursor: pointer;
color: #5f6368;
height: 20px;
}
.sidebar-item:hover {
background: #e8eaed;
}
.sidebar-item.active {
background: #d2e3fc;
color: #1a73e8;
}
.item-icon {
margin-right: 6px;
width: 14px;
text-align: center;
}
.console-content-wrapper {
flex: 1;
display: flex;
flex-direction: column;
background: #fff;
}
.console-output {
flex: 1;
font-family: Consolas, 'Lucida Console', monospace;
font-size: 11px;
}
.log-line {
border-bottom: 1px solid #f0f0f0;
display: flex;
padding: 2px 0;
min-height: 18px;
}
.log-line.error {
background: #fef0f0;
border-bottom-color: #ffd6d6;
color: #d93025;
}
.log-line.warn {
background: #fff8e1;
border-bottom-color: #ffeba0;
color: #5f4b0e;
}
.log-gutter {
width: 20px;
text-align: center;
flex-shrink: 0;
padding-top: 1px;
}
.log-content {
flex: 1;
white-space: pre-wrap;
padding-right: 4px;
line-height: 1.3;
}
.log-source {
margin-left: 10px;
margin-right: 10px;
text-align: right;
flex-shrink: 0;
color: #80868b;
text-decoration: underline;
cursor: pointer;
}
.console-input-area {
display: flex;
align-items: center;
border-top: 1px solid #e0e0e0;
padding: 2px 4px;
min-height: 22px;
}
.console-input-area .prompt {
color: #1a73e8;
margin-right: 6px;
font-weight: bold;
}
.input-field {
border: none;
outline: none;
flex: 1;
font-family: Consolas, monospace;
font-size: 11px;
}
/* Bottom Drawer */
.bottom-drawer {
height: 120px;
border-top: 1px solid #ccc;
display: flex;
flex-direction: column;
background: #fff;
}
.bottom-drawer-header {
height: 24px;
background: #f3f3f3;
border-bottom: 1px solid #ccc;
display: flex;
align-items: center;
}
.drawer-tab {
padding: 0 12px;
height: 100%;
display: flex;
align-items: center;
cursor: pointer;
color: #5f6368;
border-right: 1px solid transparent;
font-size: 11px;
}
.drawer-tab.active {
background: #fff;
color: #202124;
border-right: 1px solid #ccc;
}
.close-icon {
margin-left: 6px;
font-size: 12px;
}
.drawer-content {
flex: 1;
overflow: hidden;
}
.search-panel {
display: flex;
flex-direction: column;
height: 100%;
}
.search-bar {
padding: 4px;
border-bottom: 1px solid #e0e0e0;
display: flex;
align-items: center;
}
.search-input {
flex: 1;
border: none;
outline: none;
font-size: 11px;
}
.search-actions {
color: #5f6368;
cursor: pointer;
}
.search-results {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
}
.no-results {
text-align: center;
color: #5f6368;
}
.no-results-title {
font-weight: bold;
font-size: 12px;
margin-bottom: 4px;
}
.no-results-desc {
font-size: 11px;
}
/* --- 2. Elements Panel --- */
.elements-panel {
display: flex;
flex-direction: row;
}
.dom-tree-panel {
flex: 2;
border-right: 1px solid #d0d7de;
display: flex;
flex-direction: column;
overflow: auto;
padding: 4px 0;
font-family: Consolas, Menlo, monospace;
font-size: 12px;
background: #fff;
}
.dom-node {
padding-left: 14px;
line-height: 18px;
cursor: default;
white-space: nowrap;
}
.dom-node.selected {
background-color: #cfe8fc;
}
.line-content {
display: flex;
align-items: center;
}
.node-trail {
color: #5f6368;
margin-left: 6px;
}
.arrow {
color: #5f6368;
font-size: 10px;
display: inline-block;
width: 14px;
margin-left: -14px;
text-align: center;
}
.tag-name {
color: #a90d91;
}
.attr-name {
color: #994500;
margin-left: 4px;
}
.attr-val {
color: #1a1aa6;
}
.dots {
background: #eee;
border-radius: 2px;
padding: 0 2px;
color: #777;
font-size: 10px;
margin: 0 2px;
}
.indent {
display: inline-block;
width: 14px;
}
.breadcrumbs {
border-top: 1px solid #ccc;
padding: 2px 8px;
font-size: 11px;
color: #5f6368;
background: #fff;
border-bottom: 1px solid #eee;
}
.styles-panel {
flex: 1;
display: flex;
flex-direction: column;
background: #fff;
border-left: 1px solid #d0d7de;
min-width: 260px;
}
.styles-tabs {
display: flex;
background: #f1f3f4;
border-bottom: 1px solid #ccc;
height: 26px;
}
.style-tab {
padding: 0 10px;
display: flex;
align-items: center;
color: #5f6368;
cursor: pointer;
border-bottom: 2px solid transparent;
font-size: 11px;
}
.style-tab:hover {
background-color: #e8eaed;
color: #202124;
}
.style-tab.active {
color: #1a73e8;
border-bottom: 2px solid #1a73e8;
font-weight: 500;
}
.styles-content {
padding: 0;
overflow: auto;
background: #fff;
flex: 1;
}
/* Refined Box Model */
.box-model-container {
padding: 16px;
display: flex;
justify-content: center;
background-color: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
margin-bottom: 4px;
}
.box-margin {
background-color: rgba(249, 204, 157, 0.4);
border: 1px dashed #caaa87;
padding: 16px; /* Increased padding for values */
position: relative;
font-size: 9px;
color: #222;
}
.box-border {
background-color: rgba(255, 221, 150, 0.4);
border: 1px solid #dac689;
padding: 16px;
position: relative;
}
.box-padding {
background-color: rgba(195, 223, 183, 0.4);
border: 1px dashed #9bc89b;
padding: 16px;
position: relative;
}
.box-content {
background-color: rgba(174, 213, 243, 0.4);
border: 1px solid #7eb0d8;
padding: 4px 8px;
min-width: 60px;
text-align: center;
}
.label {
position: absolute;
top: 2px;
left: 4px;
font-size: 8px;
color: #555;
pointer-events: none;
}
/* Positioning values */
.val-top {
position: absolute;
top: 2px;
left: 0;
right: 0;
text-align: center;
}
.val-bottom {
position: absolute;
bottom: 2px;
left: 0;
right: 0;
text-align: center;
}
.val-left {
position: absolute;
top: 0;
bottom: 0;
left: 2px;
display: flex;
align-items: center;
}
.val-right {
position: absolute;
top: 0;
bottom: 0;
right: 2px;
display: flex;
align-items: center;
}
.filter-bar {
display: flex;
border: 1px solid #ccc;
border-radius: 2px;
padding: 2px 4px;
margin: 8px;
background: #fff;
align-items: center;
}
.filter-bar input {
border: none;
outline: none;
flex: 1;
font-size: 11px;
}
.filter-opt {
padding: 0 4px;
color: #5f6368;
cursor: pointer;
font-weight: bold;
margin-left: 4px;
}
.style-rule {
margin-bottom: 8px;
font-family: Consolas, Menlo, monospace;
font-size: 11px;
border-bottom: 1px solid #eee;
padding: 4px 8px 8px 8px;
}
.selector {
color: #a90d91;
font-weight: bold;
}
.property {
padding-left: 14px;
line-height: 1.4;
}
.prop-name {
color: #994500;
}
.prop-val {
color: #222;
}
/* --- 3. Sources Panel --- */
.sources-panel {
display: flex;
}
.file-navigator {
width: 180px;
border-right: 1px solid #ccc;
background: #fff;
display: flex;
flex-direction: column;
}
.nav-header {
background: #f3f3f3;
border-bottom: 1px solid #ccc;
display: flex;
height: 24px;
}
.nav-tab {
padding: 0 8px;
cursor: pointer;
color: #5f6368;
font-size: 11px;
display: flex;
align-items: center;
}
.nav-tab.active {
background: #fff;
color: #202124;
border-right: 1px solid #ccc;
}
.file-tree {
padding: 4px;
overflow: auto;
font-family: 'Segoe UI', sans-serif;
font-size: 11px;
}
.file-item {
padding: 1px 4px;
cursor: pointer;
display: flex;
align-items: center;
white-space: nowrap;
}
.file-item:hover {
background: #f3f3f3;
}
.file-item.active {
background: #cfe8fc;
}
.file-item .icon {
margin-right: 6px;
opacity: 0.7;
font-size: 12px;
}
.folder-children {
padding-left: 16px;
}
.code-editor {
flex: 1;
display: flex;
flex-direction: column;
background: #fff;
}
.editor-tabs {
display: flex;
background: #f3f3f3;
border-bottom: 1px solid #ccc;
height: 24px;
}
.editor-tab {
padding: 0 8px;
background: #fff;
border-right: 1px solid #ccc;
display: flex;
align-items: center;
font-size: 11px;
color: #333;
}
.editor-content {
flex: 1;
display: flex;
font-family: Consolas, monospace;
font-size: 11px;
overflow: auto;
}
.line-numbers {
width: 30px;
background: #f3f3f3;
border-right: 1px solid #ddd;
text-align: right;
padding: 4px;
color: #999;
line-height: 1.5;
}
.code-text {
flex: 1;
padding: 4px;
line-height: 1.5;
color: #222;
}
.debugger-sidebar {
width: 200px;
border-left: 1px solid #ccc;
background: #f3f3f3;
display: flex;
flex-direction: column;
}
.debug-section {
border-bottom: 1px solid #ccc;
}
.section-title {
padding: 2px 8px;
background: #e0e0e0;
font-weight: 700;
font-size: 11px;
color: #333;
cursor: pointer;
}
.section-content {
padding: 2px 8px;
background: #fff;
}
/* --- 4. Network Panel --- */
.network-panel {
display: flex;
flex-direction: column;
}
.network-toolbar {
height: 24px;
background: #f3f3f3;
border-bottom: 1px solid #ccc;
display: flex;
align-items: center;
padding: 0 8px;
gap: 8px;
}
.record-icon {
color: #d93025;
font-size: 10px;
cursor: pointer;
}
.filter-btn {
cursor: pointer;
padding: 1px 6px;
border-radius: 2px;
color: #5f6368;
}
.filter-btn:hover {
background: #e0e0e0;
color: #202124;
}
.filter-btn.active {
background: #cdcdcd;
font-weight: 600;
color: #202124;
}
.network-split-view {
flex: 1;
display: flex;
overflow: hidden;
}
.network-grid {
flex: 1;
display: flex;
flex-direction: column;
font-size: 11px;
overflow: hidden;
}
.network-grid-header {
display: flex;
background: #f8f9fa;
border-bottom: 1px solid #ccc;
padding: 1px 0;
font-weight: bold;
color: #333;
}
.network-rows {
flex: 1;
overflow: auto;
background: #fff;
}
.network-row {
display: flex;
border-bottom: 1px solid #f0f0f0;
padding: 1px 0;
cursor: default;
}
.network-row:nth-child(even) {
background: #f9f9f9;
}
.network-row:hover {
background: #e8f0fe;
}
.network-row.selected {
background: #cfe8fc;
}
.col {
padding: 1px 6px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border-right: 1px solid #f0f0f0;
display: flex;
align-items: center;
}
.col.name {
width: 140px;
}
.col.status {
width: 40px;
color: #5f6368;
}
.col.type {
width: 60px;
color: #5f6368;
}
.col.size {
width: 50px;
color: #5f6368;
}
.col.time {
width: 50px;
color: #5f6368;
}
.col.waterfall {
flex: 1;
position: relative;
}
.waterfall-bar {
height: 6px;
background: #8ab4f8;
position: absolute;
top: 50%;
transform: translateY(-50%);
border-radius: 2px;
border: 1px solid #4285f4;
}
/* Network Detail Panel */
.network-detail {
width: 300px;
border-left: 1px solid #ccc;
background: #fff;
display: flex;
flex-direction: column;
font-size: 11px;
}
.detail-header {
height: 24px;
background: #f3f3f3;
border-bottom: 1px solid #ccc;
display: flex;
align-items: center;
padding: 0 8px;
}
.detail-title {
margin-right: 12px;
color: #5f6368;
font-weight: bold;
cursor: pointer;
border-bottom: 2px solid transparent;
line-height: 22px;
}
.detail-title:hover {
color: #333;
}
.detail-title.active {
color: #1a73e8;
border-bottom: 2px solid #1a73e8;
}
.close-detail {
margin-left: auto;
cursor: pointer;
font-size: 14px;
color: #5f6368;
}
.detail-content {
flex: 1;
overflow: auto;
padding: 8px;
}
.detail-section {
margin-bottom: 12px;
}
.section-label {
font-weight: bold;
margin-bottom: 4px;
color: #333;
}
.detail-row {
display: flex;
margin-bottom: 2px;
line-height: 1.4;
word-break: break-all;
}
.detail-row .key {
color: #5f6368;
margin-right: 6px;
flex-shrink: 0;
min-width: 80px;
}
.detail-row .val {
color: #222;
}
.status-code {
color: #1a73e8;
font-weight: bold;
}
.preview-content {
font-family: Consolas, monospace;
background: #f8f9fa;
padding: 6px;
border-radius: 2px;
border: 1px solid #eee;
white-space: pre-wrap;
color: #24292e;
}
/* --- 5. Application Panel --- */
.application-panel {
display: flex;
}
.storage-sidebar {
width: 180px;
border-right: 1px solid #ccc;
background: #fff;
padding: 0;
overflow: auto;
}
.sidebar-section {
margin-bottom: 8px;
}
.section-title {
font-weight: bold;
color: #5f6368;
padding: 2px 8px;
}
.section-item {
padding: 1px 8px;
cursor: pointer;
display: flex;
align-items: center;
color: #333;
}
.section-item:hover {
background: #f3f3f3;
}
.section-item.active {
background: #cfe8fc;
}
.section-item.indent {
padding-left: 20px;
}
.section-item .arrow {
margin-right: 4px;
width: 10px;
}
.storage-content {
flex: 1;
background: #fff;
overflow: auto;
display: flex;
flex-direction: column;
}
.storage-table {
width: 100%;
font-size: 11px;
border-collapse: collapse;
}
.table-header {
display: flex;
background: #f3f3f3;
border-bottom: 1px solid #ccc;
font-weight: bold;
}
.table-row {
display: flex;
border-bottom: 1px solid #eee;
}
.table-row:nth-child(even) {
background: #f9f9f9;
}
.table-row:hover {
background: #eef;
}
.storage-table .col {
padding: 2px 8px;
border-right: 1px solid #eee;
}
.storage-table .col.key {
width: 150px;
font-weight: 600;
}
.storage-table .col.value {
flex: 1;
font-family: Consolas, monospace;
}
/* Overlays */
.info-bar {
background-color: #323232;
color: white;
padding: 6px 12px;
font-size: 12px;
display: flex;
align-items: center;
gap: 8px;
position: absolute;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
border-radius: 24px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 9999;
white-space: nowrap;
pointer-events: none;
}
.info-icon {
font-size: 14px;
}
.virtual-cursor {
position: absolute;
top: 0;
left: 0;
z-index: 10000;
pointer-events: none;
transition: transform 0.6s cubic-bezier(0.25, 1, 0.5, 1);
margin-top: -5px;
margin-left: -3px;
}
.highlight-box {
position: absolute;
border: 2px solid #1a73e8;
background-color: rgba(26, 115, 232, 0.15);
pointer-events: none;
z-index: 9998;
box-sizing: border-box;
transition: all 0.3s ease;
border-radius: 2px;
}
</style>