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 +21 -0
- package/README.md +101 -0
- package/package.json +47 -0
- package/src/index.js +6 -0
- package/src/vite-plugin.js +448 -0
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,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
|
+
`
|