vite-plugin-vue-insight 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HuangBingQuan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # vite-plugin-vue-insight
2
+
3
+ > 点击定位源码、高亮 DOM、查看组件状态、分享链接 — Vue 3 + Vite 调试助手
4
+
5
+ 一个 Vite 插件,在开发模式下按住 `⌥+Shift`(macOS)或 `Alt+Shift`(Windows/Linux)并点击页面元素,即可:
6
+
7
+ - 🔦 **DOM 高亮** — 点击的元素显示脉动红色边框
8
+ - 📁 **源码定位** — 自动打开编辑器中对应的 `.vue` 文件并跳转到指定行
9
+ - ⚛️ **组件状态** — 在控制台打印组件的 props 和响应式数据
10
+ - 🔗 **可分享链接** — 生成 `vscode://` 协议的链接并复制到剪贴板,团队成员点击即可在本地打开对应代码
11
+
12
+ ## 安装
13
+
14
+ ```bash
15
+ npm install vite-plugin-vue-insight --save-dev
16
+ ```
17
+
18
+ ## 使用
19
+
20
+ ```js
21
+ // vite.config.js
22
+ import { defineConfig } from 'vite'
23
+ import vue from '@vitejs/plugin-vue'
24
+ import { vueInsightPlugin } from 'vite-plugin-vue-insight'
25
+
26
+ export default defineConfig(({ mode }) => ({
27
+ plugins: [
28
+ // 只在开发模式下启用
29
+ ...(mode === 'development' ? [vueInsightPlugin()] : []),
30
+ vue(),
31
+ ],
32
+ }))
33
+ ```
34
+
35
+ 启动 dev server 后,按住 `⌥+Shift`(macOS)或 `Alt+Shift`(Windows/Linux)并点击页面上的任意元素即可开始调试。
36
+
37
+ ## 选项
38
+
39
+ ```js
40
+ vueInsightPlugin({
41
+ editor: 'vscode', // 编辑器类型:'vscode' | 'cursor' | 'webstorm' | 'vscode-insiders'
42
+ namespace: 'data-v-insight', // DOM 属性名前缀
43
+ skipComponents: true, // 是否跳过 Vue 组件标签(只保留原生 HTML 标签的标记)
44
+ })
45
+ ```
46
+
47
+ | 选项 | 类型 | 默认值 | 说明 |
48
+ |------|------|--------|------|
49
+ | `editor` | `string` | `'vscode'` | 点击后打开的编辑器 |
50
+ | `namespace` | `string` | `'data-v-insight'` | 注入到 HTML 标签上的属性名前缀 |
51
+ | `skipComponents` | `boolean` | `true` | 是否跳过组件标签(如 `<MyComponent>`) |
52
+
53
+ ## 工作原理
54
+
55
+ 整个插件分为两个阶段:
56
+
57
+ ### 1. 构建时(Build Time)
58
+
59
+ 通过 Vite 的 `transform` 钩子,在编译 `.vue` 文件时:
60
+
61
+ - 解析 SFC 中的 `<template>` 部分
62
+ - 在每个 HTML 标签上注入自定义属性,例如:
63
+
64
+ ```html
65
+ <div data-v-insight-file="src/App.vue" data-v-insight-line="5" data-v-insight-component="App">
66
+ ```
67
+
68
+ - 组件标签和虚拟标签(`<template>`、`<slot>`、`<component>`)默认跳过
69
+
70
+ ### 2. 运行时(Runtime)
71
+
72
+ 通过 `transformIndexHtml` 钩子向 `index.html` 注入一段客户端脚本:
73
+
74
+ - **激活**:检测 `⌥+Shift` 或 `Alt+Shift` 组合键,进入检查模式(鼠标变为十字准星)
75
+ - **预览**:鼠标悬停时显示蓝色半透明边框
76
+ - **点击**:脉动红色边框高亮 + 控制台打印文件路径、行号、组件名
77
+ - **编辑器**:通过 `fetch('/__open-in-editor')` 请求打开 Vite 的编辑器接口
78
+ - **状态**:读取 `__vueParentComponent` 内部引用,提取 props 和响应式状态
79
+ - **分享**:生成 `vscode://file/path:line` 链接并自动复制到剪贴板
80
+
81
+ ## 示例输出
82
+
83
+ 控制台打印效果:
84
+
85
+ ```
86
+ 🔍 Vue Insight
87
+
88
+ 📁 src/components/HelloWorld.vue
89
+ 📍 第 15 行
90
+ 🧩 HelloWorld
91
+ 🏷️ button
92
+ 🔗 vscode://file/src/components/HelloWorld.vue:15:1 (已复制)
93
+
94
+ ⚛️ HelloWorld — 组件状态
95
+ 📦 Props: { msg: "Welcome" }
96
+ 🔄 Reactive State: { count: 0 (ref) }
97
+ ```
98
+
99
+ ## 许可证
100
+
101
+ MIT
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "vite-plugin-vue-insight",
3
+ "version": "0.1.0",
4
+ "description": "点击定位源码、高亮 DOM、查看组件状态、分享链接 — Vue 3 + Vite 调试助手",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "module": "./src/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./src/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "src"
15
+ ],
16
+ "scripts": {
17
+ "dev": "vite",
18
+ "build": "vite build",
19
+ "preview": "vite preview"
20
+ },
21
+ "peerDependencies": {
22
+ "vite": "^5.0.0 || ^6.0.0",
23
+ "vue": "^3.2.0"
24
+ },
25
+ "dependencies": {
26
+ "@vue/compiler-sfc": "^3.2.0"
27
+ },
28
+ "keywords": [
29
+ "vite-plugin",
30
+ "vue",
31
+ "inspector",
32
+ "debug",
33
+ "devtools",
34
+ "source-location",
35
+ "component-state"
36
+ ],
37
+ "license": "MIT",
38
+ "author": {
39
+ "name": "HuangBingQuan",
40
+ "email": "17671241237@163.com",
41
+ "url": "https://github.com/bingquan040601"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/bingquan040601/vite-plugin-vue-insight.git"
46
+ }
47
+ }
package/src/index.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * vite-plugin-vue-insight
3
+ *
4
+ * 入口文件
5
+ */
6
+ export { vueInsightPlugin } from './vite-plugin.js'
@@ -0,0 +1,448 @@
1
+ /**
2
+ * vite-plugin-vue-insight — Vite 插件
3
+ *
4
+ * dev 模式下扫描 .vue template,在每个 HTML 标签注入 data-v-insight-* 属性,
5
+ * 配合客户端运行时实现:Alt+Shift 定位源码 + 高亮 DOM + 打印组件状态 + 分享链接。
6
+ *
7
+ * @package vite-plugin-vue-insight
8
+ */
9
+
10
+ import { parse as parseSFC } from '@vue/compiler-sfc'
11
+ import path from 'node:path'
12
+
13
+ // ─── 工具函数 ───────────────────────────────────────────────────────────────────
14
+
15
+ function findUnquoted(str, char, startPos) {
16
+ let inQuote = false, quoteChar = ''
17
+ for (let i = startPos; i < str.length; i++) {
18
+ const ch = str[i]
19
+ if (inQuote) { if (ch === quoteChar) inQuote = false }
20
+ else { if (ch === '"' || ch === "'") { inQuote = true; quoteChar = ch }
21
+ else if (ch === char) return i }
22
+ }
23
+ return -1
24
+ }
25
+
26
+ function isComponentName(name) {
27
+ return name[0] === name[0].toUpperCase() && name[0] !== name[0].toLowerCase()
28
+ }
29
+
30
+ const VIRTUAL_TAGS = new Set(['template', 'slot', 'component'])
31
+
32
+ // ─── 插件选项 ──────────────────────────────────────────────────────────────────
33
+
34
+ /**
35
+ * @typedef {Object} InsightOptions
36
+ * @property {'vscode'|'cursor'|'webstorm'|'vscode-insiders'} [editor='vscode']
37
+ * @property {string} [namespace='data-v-insight'] 属性名前缀
38
+ * @property {boolean} [skipComponents=true] 跳过组件标签注入
39
+ */
40
+
41
+ // ─── Vite Plugin ────────────────────────────────────────────────────────────────
42
+
43
+ /**
44
+ * @param {InsightOptions} [options]
45
+ * @returns {import('vite').Plugin}
46
+ */
47
+ export function vueInsightPlugin(options = {}) {
48
+ const opts = {
49
+ editor: 'vscode',
50
+ namespace: 'data-v-insight',
51
+ skipComponents: true,
52
+ ...options,
53
+ }
54
+
55
+ /** @type {string} */
56
+ let root = ''
57
+
58
+ /** 客户端配置,运行时透传给 inspector-client.js */
59
+ const clientConfig = JSON.stringify({
60
+ editor: opts.editor,
61
+ attrPrefix: opts.namespace,
62
+ modifiers: { alt: true, shift: true },
63
+ highlightColor: '#ff6b6b',
64
+ highlightDuration: 4000,
65
+ })
66
+
67
+ return {
68
+ name: 'vite-plugin-vue-insight',
69
+ enforce: 'pre',
70
+
71
+ configResolved(config) {
72
+ root = config.root
73
+ },
74
+
75
+ transform(code, id) {
76
+ if (!id.endsWith('.vue') || id.includes('node_modules')) return null
77
+
78
+ let descriptor
79
+ try {
80
+ const result = parseSFC(code)
81
+ if (result.errors.length > 0) return null
82
+ descriptor = result.descriptor
83
+ } catch {
84
+ return null
85
+ }
86
+
87
+ if (!descriptor.template || descriptor.template.lang) return null
88
+
89
+ const template = descriptor.template
90
+ const templateContent = template.content
91
+ const len = templateContent.length
92
+ if (len === 0) return null
93
+
94
+ const contentStart = template.loc.start.offset
95
+ const baseLine = code.substring(0, contentStart).split('\n').length
96
+ const relativePath = path.relative(root, id).replace(/\\/g, '/')
97
+ const absolutePath = path.resolve(id).replace(/\\/g, '/')
98
+ const componentName = path.basename(id, '.vue')
99
+
100
+ const P = opts.namespace // 属性前缀
101
+
102
+ /** @type {Array<{pos: number, text: string}>} */
103
+ const insertions = []
104
+ let i = 0
105
+
106
+ while (i < len) {
107
+ const tagStart = templateContent.indexOf('<', i)
108
+ if (tagStart === -1 || tagStart >= len - 1) break
109
+
110
+ // 跳过注释
111
+ if (tagStart + 3 < len &&
112
+ templateContent[tagStart + 1] === '!' &&
113
+ templateContent[tagStart + 2] === '-' &&
114
+ templateContent[tagStart + 3] === '-') {
115
+ const close = templateContent.indexOf('-->', tagStart + 4)
116
+ i = close !== -1 ? close + 3 : len; continue
117
+ }
118
+
119
+ // 跳过结束标签
120
+ if (templateContent[tagStart + 1] === '/') {
121
+ const close = templateContent.indexOf('>', tagStart + 2)
122
+ i = close !== -1 ? close + 1 : len; continue
123
+ }
124
+
125
+ // 跳过 <!DOCTYPE> 等
126
+ if (templateContent[tagStart + 1] === '!' || templateContent[tagStart + 1] === '?') {
127
+ const close = templateContent.indexOf('>', tagStart + 2)
128
+ i = close !== -1 ? close + 1 : len; continue
129
+ }
130
+
131
+ // 提取标签名
132
+ let tagNameEnd = tagStart + 1
133
+ while (tagNameEnd < len && /[\w.-]/.test(templateContent[tagNameEnd])) { tagNameEnd++ }
134
+
135
+ if (tagNameEnd === tagStart + 1) { i = tagStart + 1; continue }
136
+
137
+ const tagName = templateContent.substring(tagStart + 1, tagNameEnd)
138
+
139
+ // 跳过组件标签和虚拟标签
140
+ if (isComponentName(tagName) || VIRTUAL_TAGS.has(tagName)) {
141
+ const openEnd = findUnquoted(templateContent, '>', tagNameEnd)
142
+ i = openEnd !== -1 ? openEnd + 1 : len; continue
143
+ }
144
+
145
+ // 计算 SFC 行号
146
+ let linesInContent = 0
147
+ for (let j = 0; j < tagStart; j++) { if (templateContent[j] === '\n') linesInContent++ }
148
+ const sfcLine = baseLine + linesInContent
149
+
150
+ // 插入属性
151
+ const sfcPos = contentStart + tagNameEnd
152
+ const attrStr =
153
+ ` ${P}-file="${relativePath}"` +
154
+ ` ${P}-abspath="${absolutePath}"` +
155
+ ` ${P}-line="${sfcLine}"` +
156
+ ` ${P}-component="${componentName}"`
157
+
158
+ insertions.push({ pos: sfcPos, text: attrStr })
159
+
160
+ const openEnd = findUnquoted(templateContent, '>', tagNameEnd)
161
+ i = openEnd !== -1 ? openEnd + 1 : len
162
+ }
163
+
164
+ if (insertions.length === 0) return null
165
+
166
+ insertions.sort((a, b) => b.pos - a.pos)
167
+ let result = code
168
+ for (const ins of insertions) {
169
+ result = result.slice(0, ins.pos) + ins.text + result.slice(ins.pos)
170
+ }
171
+
172
+ return { code: result, map: null }
173
+ },
174
+
175
+ /**
176
+ * 注入客户端运行时 + 配置
177
+ */
178
+ transformIndexHtml() {
179
+ return [
180
+ {
181
+ tag: 'script',
182
+ attrs: { type: 'module' },
183
+ children: inspectorClientCode,
184
+ injectTo: 'body-prepend',
185
+ },
186
+ ]
187
+ },
188
+
189
+ /**
190
+ * 服务端中间件:处理 /__open-in-editor 请求,
191
+ * 调用系统命令打开编辑器并跳转到指定文件的指定行。
192
+ */
193
+ configureServer(server) {
194
+ server.middlewares.use('/__open-in-editor', async (req, res) => {
195
+ try {
196
+ const url = new URL(req.url, 'http://localhost')
197
+ const fileParam = url.searchParams.get('file')
198
+ if (!fileParam) {
199
+ res.statusCode = 400
200
+ res.end('Missing file parameter')
201
+ return
202
+ }
203
+
204
+ const parts = fileParam.split(':')
205
+ const filePath = parts[0]
206
+ const line = parts[1] || '1'
207
+ const column = parts[2] || '1'
208
+
209
+ const { spawn } = await import('node:child_process')
210
+
211
+ /** @type {string} */ let cmd
212
+ /** @type {string[]} */ let args
213
+
214
+ switch (opts.editor) {
215
+ case 'cursor':
216
+ cmd = 'cursor'
217
+ args = ['-g', `${filePath}:${line}:${column}`]
218
+ break
219
+ case 'webstorm':
220
+ cmd = 'webstorm'
221
+ args = ['--line', line, '--column', column, filePath]
222
+ break
223
+ case 'vscode-insiders':
224
+ cmd = 'code-insiders'
225
+ args = ['-g', `${filePath}:${line}:${column}`]
226
+ break
227
+ case 'vscode':
228
+ default:
229
+ cmd = 'code'
230
+ args = ['-g', `${filePath}:${line}:${column}`]
231
+ break
232
+ }
233
+
234
+ const child = spawn(cmd, args, { stdio: 'ignore', detached: true })
235
+ child.unref()
236
+
237
+ res.statusCode = 200
238
+ res.end('OK')
239
+ } catch {
240
+ res.statusCode = 500
241
+ res.end('Failed to open editor')
242
+ }
243
+ })
244
+ },
245
+ }
246
+ }
247
+
248
+ // ─── 客户端运行时(内联) ──────────────────────────────────────────────────────
249
+ // 构建时可通过 esbuild 压缩替换此处
250
+
251
+ const inspectorClientCode = `
252
+ window.__VUE_INSIGHT__ = ${JSON.stringify({
253
+ editor: 'vscode',
254
+ attrPrefix: 'data-v-insight',
255
+ modifiers: { alt: true, shift: true },
256
+ highlightColor: '#ff6b6b',
257
+ highlightDuration: 4000,
258
+ })};
259
+
260
+ ;(() => {
261
+ const C = window.__VUE_INSIGHT__
262
+ const P = C.attrPrefix // 属性前缀
263
+
264
+ // ── 平台检测 ──
265
+ const isMac = /Mac|iPhone|iPad|iPod/i.test(navigator.platform || navigator.userAgentData?.platform || '')
266
+ const shortcutLabel = isMac ? '⌥+Shift (Option+Shift)' : 'Alt+Shift'
267
+
268
+ let isInspecting = false
269
+ let currentOverlay = null
270
+ let hoveredElement = null
271
+ let rafId = null
272
+ let hasShownInstructions = false
273
+
274
+ // ── 样式注入 ──
275
+ if (!document.getElementById('__v-insight-styles')) {
276
+ const s = document.createElement('style')
277
+ s.id = '__v-insight-styles'
278
+ s.textContent = '@keyframes __v-insight-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(1.03)}}'
279
+ document.head.appendChild(s)
280
+ }
281
+
282
+ // ── 高亮 ──
283
+ function showOverlay(el, isPreview) {
284
+ removeOverlay()
285
+ const r = el.getBoundingClientRect()
286
+ const o = document.createElement('div')
287
+ o.style.cssText = isPreview
288
+ ? \`position:fixed;z-index:2147483646;pointer-events:none;top:\${r.top}px;left:\${r.left}px;width:\${r.width}px;height:\${r.height}px;border:1.5px solid #38bdf8;border-radius:2px;background:rgba(56,189,248,0.08);transition:all .05s\`
289
+ : \`position:fixed;z-index:2147483647;pointer-events:none;top:\${r.top}px;left:\${r.left}px;width:\${r.width}px;height:\${r.height}px;border:2px solid \${C.highlightColor};border-radius:2px;box-shadow:0 0 0 4px \${C.highlightColor}33,0 0 24px \${C.highlightColor}44;animation:__v-insight-pulse 1.2s ease-in-out 3\`
290
+ document.body.appendChild(o)
291
+ currentOverlay = o
292
+ if (!isPreview) setTimeout(removeOverlay, C.highlightDuration)
293
+ }
294
+
295
+ function removeOverlay() { currentOverlay?.remove(); currentOverlay = null }
296
+
297
+ // ── Vue 状态提取 ──
298
+ function getCompInfo(el) {
299
+ try {
300
+ const internal = el.__vueParentComponent
301
+ if (!internal) return null
302
+ const displayName = internal.type?.name || internal.type?.__name || internal.type?._componentTag || 'Anonymous'
303
+ const rawProps = internal.props || {}
304
+ const props = {}
305
+ for (const k of Object.keys(rawProps)) { try { props[k] = structuredClone(rawProps[k]) } catch { props[k] = String(rawProps[k]) } }
306
+ const rawState = internal.setupState || {}
307
+ const state = {}
308
+ for (const k of Object.keys(rawState)) {
309
+ if (k.startsWith('__') || k === '\$' || k === 'props') continue
310
+ try {
311
+ const v = rawState[k]
312
+ if (v && typeof v === 'object' && '__v_isRef' in v) state[\`\${k} (ref)\`] = structuredClone(v.value)
313
+ else if (v && typeof v === 'object' && '__v_isReactive' in v) state[\`\${k} (reactive)\`] = structuredClone(v)
314
+ else if (typeof v === 'function') state[k] = \`ƒ \${v.name || 'anonymous'}()\`
315
+ else state[k] = structuredClone(v)
316
+ } catch { state[k] = '[unserializable]' }
317
+ }
318
+ const rawData = internal.data || {}
319
+ for (const k of Object.keys(rawData)) { if (!(k in state)) { try { state[k] = structuredClone(rawData[k]) } catch { state[k] = String(rawData[k]) } } }
320
+ return { displayName, props, state }
321
+ } catch { return null }
322
+ }
323
+
324
+ // ── 可分享链接 ──
325
+ const SCHEMES = { vscode: 'vscode', 'vscode-insiders': 'vscode-insiders', cursor: 'cursor', webstorm: 'webstorm' }
326
+
327
+ function genLink(abspath, line) {
328
+ const scheme = SCHEMES[C.editor] || 'vscode'
329
+ return scheme + '://file/' + encodeURIComponent(abspath).replace(/%2F/g, '/') + ':' + line + ':1'
330
+ }
331
+
332
+ function copy(text) {
333
+ if (navigator.clipboard?.writeText) navigator.clipboard.writeText(text).catch(() => {})
334
+ }
335
+
336
+ // ── Console 打印 ──
337
+ function printInfo(el) {
338
+ const file = el.getAttribute(P + '-file')
339
+ const abspath = el.getAttribute(P + '-abspath')
340
+ const line = el.getAttribute(P + '-line')
341
+ const comp = el.getAttribute(P + '-component')
342
+
343
+ console.log('%c 🔍 Vue Insight ', 'font-size:15px;font-weight:800;color:#fff;background:#e53e3e;padding:4px 10px;border-radius:4px')
344
+ console.log('')
345
+
346
+ const tag = (bg) => 'font-size:13px;font-weight:700;color:#fff;background:' + bg + ';padding:1px 8px;border-radius:3px;margin-right:4px'
347
+ const val = 'font-size:13px;font-weight:600'
348
+
349
+ console.log('%c 📁 %c ' + file, tag('#2563eb'), val)
350
+ console.log('%c 📍 %c 第 ' + line + ' 行', tag('#7c3aed'), val)
351
+ console.log('%c 🧩 %c ' + comp, tag('#0891b2'), val)
352
+ console.log('%c 🏷️ %c ' + el.tagName.toLowerCase(), tag('#be185d'), val)
353
+
354
+ if (abspath && line) {
355
+ const url = genLink(abspath, line)
356
+ copy(url)
357
+ console.log('')
358
+ console.log('%c 🔗 %c ' + url + ' %c (已复制)', tag('#10b981'), 'font-size:12px;font-weight:500;color:#10b981;user-select:all', 'font-size:11px;color:#6b7280')
359
+ }
360
+
361
+ const ci = getCompInfo(el)
362
+ if (ci && (Object.keys(ci.props).length || Object.keys(ci.state).length)) {
363
+ console.groupCollapsed('%c⚛️ ' + ci.displayName + ' — 组件状态', 'font-size:14px;font-weight:700')
364
+ if (Object.keys(ci.props).length) console.log('%c📦 Props:', 'font-size:13px;font-weight:700', ci.props)
365
+ if (Object.keys(ci.state).length) console.log('%c🔄 Reactive State:', 'font-size:13px;font-weight:700', ci.state)
366
+ console.groupEnd()
367
+ } else {
368
+ console.log('%cℹ️ 未提取到组件状态', 'font-size:12px;color:#94a3b8')
369
+ }
370
+ console.log('─'.repeat(44))
371
+ }
372
+
373
+ // ── 事件处理 ──
374
+ function checkMod(e) { const m = C.modifiers; return (!m.alt || e.altKey) && (!m.shift || e.shiftKey) && (!m.ctrl || e.ctrlKey) && (!m.meta || e.metaKey) }
375
+
376
+ function preventSelect(e) { e.preventDefault() }
377
+
378
+ function onKD(e) {
379
+ if (!hasShownInstructions && checkMod(e)) {
380
+ hasShownInstructions = true
381
+ console.log('%c 🔍 Vue Insight 已激活 ', 'font-size:16px;font-weight:800;color:#fff;background:#e53e3e;padding:4px 12px;border-radius:4px')
382
+ console.log('%c 按住 ' + shortcutLabel + ' 点击页面元素 ➔ 高亮 + 源码 + 状态 + 分享', 'font-size:13px;font-weight:600')
383
+ }
384
+ if (checkMod(e) && !isInspecting) {
385
+ isInspecting = true
386
+ document.body.style.cursor = 'crosshair'
387
+ document.addEventListener('selectstart', preventSelect)
388
+ document.addEventListener('mousemove', onMM, true)
389
+ }
390
+ }
391
+
392
+ function onKU(e) {
393
+ if (isInspecting && !e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
394
+ isInspecting = false
395
+ document.body.style.cursor = ''
396
+ document.removeEventListener('selectstart', preventSelect)
397
+ document.removeEventListener('mousemove', onMM, true)
398
+ hoveredElement = null; removeOverlay()
399
+ }
400
+ }
401
+
402
+ function onMM(e) {
403
+ if (!isInspecting || rafId) return
404
+ const el = e.target.closest('[' + P + '-file]')
405
+ rafId = requestAnimationFrame(function() {
406
+ rafId = null
407
+ if (el && hoveredElement !== el) {
408
+ hoveredElement = el; removeOverlay();
409
+ showOverlay(el, true)
410
+ } else if (!el) {
411
+ hoveredElement = null; removeOverlay()
412
+ }
413
+ })
414
+ }
415
+
416
+ function onCL(e) {
417
+ if (!isInspecting) return
418
+ e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation()
419
+ isInspecting = false
420
+ document.body.style.cursor = ''
421
+ document.removeEventListener('selectstart', preventSelect)
422
+ document.removeEventListener('mousemove', onMM, true)
423
+ hoveredElement = null
424
+
425
+ const el = e.target.closest('[' + P + '-file]')
426
+ if (!el) { removeOverlay(); return }
427
+ showOverlay(el, false)
428
+ printInfo(el)
429
+
430
+ const file = el.getAttribute(P + '-file')
431
+ const abspath = el.getAttribute(P + '-abspath')
432
+ const line = el.getAttribute(P + '-line')
433
+ const target = abspath || file
434
+ if (target && line) {
435
+ fetch('/__open-in-editor?file=' + encodeURIComponent(target) + ':' + line + ':1', { method: 'HEAD' }).catch(function() {
436
+ location.href = genLink(abspath, line)
437
+ })
438
+ }
439
+ }
440
+
441
+ // ── 初始化 ──
442
+ document.addEventListener('keydown', onKD, true)
443
+ document.addEventListener('keyup', onKU, true)
444
+ document.addEventListener('click', onCL, true)
445
+
446
+ console.log('%c 🔍 Vue Insight 已加载 — 按住 ' + shortcutLabel + ' 点击页面元素开始调试 ', 'font-size:13px;font-weight:600;color:#e2e8f0;background:#1e293b;padding:3px 10px;border-radius:4px;border-left:4px solid #e53e3e')
447
+ })();
448
+ `