2347 lines
61 KiB
Vue
2347 lines
61 KiB
Vue
<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'"
|
||
>></span
|
||
>
|
||
<span class="icon" v-else><</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">></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">></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">></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">></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;
|
||
overflow-y: auto;
|
||
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>
|