<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>三次贝塞尔曲线编辑器</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
color: #e0e0e0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
h1 {
text-align: center;
margin-bottom: 20px;
color: #fff;
font-weight: 300;
letter-spacing: 2px;
}
.editor-wrapper {
display: grid;
grid-template-columns: 1fr 320px;
gap: 20px;
}
@media (max-width: 900px) {
.editor-wrapper { grid-template-columns: 1fr; }
}
.svg-container {
background: #0f0f1a;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}
svg {
display: block;
width: 100%;
height: 500px;
cursor: crosshair;
}
.panel {
background: rgba(255,255,255,0.05);
border-radius: 12px;
padding: 20px;
backdrop-filter: blur(10px);
}
.panel h3 {
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
color: #888;
margin-bottom: 12px;
border-bottom: 1px solid rgba(255,255,255,0.1);
padding-bottom: 8px;
}
.coord-list { margin-bottom: 20px; }
.coord-item {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.coord-dot {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 10px;
}
.coord-label { flex: 1; font-weight: 500; }
.coord-value { font-family: 'SF Mono', monospace; font-size: 13px; color: #aaa; }
.formula-box {
background: rgba(0,0,0,0.3);
border-radius: 8px;
padding: 12px;
font-family: 'SF Mono', monospace;
font-size: 12px;
line-height: 1.8;
margin-bottom: 20px;
overflow-x: auto;
}
.btn-group { display: flex; gap: 10px; flex-wrap: wrap; }
.btn {
flex: 1;
padding: 10px 16px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
}
.btn-secondary {
background: rgba(255,255,255,0.1);
color: #fff;
}
.btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
.instructions {
margin-top: 20px;
font-size: 13px;
color: #888;
line-height: 1.6;
}
.grid-line { stroke: rgba(255,255,255,0.05); stroke-width: 1; }
.control-line { stroke: rgba(255,255,255,0.3); stroke-width: 1; stroke-dasharray: 4,4; }
.bezier-curve { fill: none; stroke: url(#curveGradient); stroke-width: 3; stroke-linecap: round; }
.point { cursor: grab; transition: r 0.15s; }
.point:hover { r: 12; }
.point.dragging { cursor: grabbing; }
.endpoint { fill: #667eea; }
.controlpoint { fill: #f093fb; }
.point-label {
font-size: 11px;
fill: #fff;
pointer-events: none;
font-weight: 600;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<h1>三次贝塞尔曲线编辑器</h1>
<div class="editor-wrapper">
<div class="svg-container">
<svg ref="svgEl" viewBox="0 0 800 500" @mousedown="startDrag" @mousemove="onDrag" @mouseup="endDrag" @mouseleave="endDrag"
@touchstart.prevent="startDrag" @touchmove.prevent="onDrag" @touchend="endDrag">
<defs>
<linearGradient id="curveGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#667eea"/>
<stop offset="100%" stop-color="#f093fb"/>
</linearGradient>
</defs>
<!-- Grid -->
<line v-for="i in 20" :key="'v'+i" :x1="i*40" y1="0" :x2="i*40" y2="500" class="grid-line"/>
<line v-for="i in 12" :key="'h'+i" x1="0" :y1="i*40" x2="800" :y2="i*40" class="grid-line"/>
<!-- Control Lines -->
<g v-if="showLines">
<line :x1="points.p0.x" :y1="points.p0.y" :x2="points.p1.x" :y2="points.p1.y" class="control-line"/>
<line :x1="points.p1.x" :y1="points.p1.y" :x2="points.p2.x" :y2="points.p2.y" class="control-line"/>
<line :x1="points.p2.x" :y1="points.p2.y" :x2="points.p3.x" :y2="points.p3.y" class="control-line"/>
</g>
<!-- Bezier Curve -->
<path :d="curvePath" class="bezier-curve"/>
<!-- Points -->
<circle v-for="(p, key) in points" :key="key" :cx="p.x" :cy="p.y" r="10"
:class="['point', key.includes('0') || key.includes('3') ? 'endpoint' : 'controlpoint', {dragging: dragging === key}]"
:data-point="key"/>
<!-- Labels -->
<text v-for="(p, key) in points" :key="'l'+key" :x="p.x" :y="p.y - 16" text-anchor="middle" class="point-label">
{{ key.toUpperCase() }}
</text>
</svg>
</div>
<div class="panel">
<h3>控制点坐标</h3>
<div class="coord-list">
<div class="coord-item" v-for="(p, key) in points" :key="key">
<span class="coord-dot" :style="{background: key.includes('0') || key.includes('3') ? '#667eea' : '#f093fb'}"></span>
<span class="coord-label">{{ key.toUpperCase() }}</span>
<span class="coord-value">({{ Math.round(p.x) }}, {{ Math.round(p.y) }})</span>
</div>
</div>
<h3>贝塞尔曲线方程</h3>
<div class="formula-box">
<div>B(t) = (1-t)^3 * P0 + 3(1-t)^2 * t * P1</div>
<div style="padding-left:32px">+ 3(1-t) * t^2 * P2 + t^3 * P3</div>
<div style="margin-top:8px;color:#667eea">t ∈ [0, 1]</div>
</div>
<h3>SVG Path</h3>
<div class="formula-box" style="word-break:break-all">{{ curvePath }}</div>
<div class="btn-group">
<button class="btn btn-primary" @click="reset">重置</button>
<button class="btn btn-secondary" @click="showLines = !showLines">
{{ showLines ? '隐藏' : '显示' }}连线
</button>
</div>
<div class="instructions">
<strong>操作说明:</strong><br>
- 拖拽圆点移动控制点<br>
- 紫色点为端点(P0, P3)<br>
- 粉色点为控制点(P1, P2)<br>
- 支持鼠标和触摸操作
</div>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, reactive, computed } = Vue;
createApp({
setup() {
const svgEl = ref(null);
const showLines = ref(true);
const dragging = ref(null);
const points = reactive({
p0: { x: 100, y: 400 },
p1: { x: 200, y: 100 },
p2: { x: 600, y: 100 },
p3: { x: 700, y: 400 }
});
const initial = JSON.parse(JSON.stringify(points));
const curvePath = computed(() => {
const {p0, p1, p2, p3} = points;
return `M ${p0.x} ${p0.y} C ${p1.x} ${p1.y}, ${p2.x} ${p2.y}, ${p3.x} ${p3.y}`;
});
function getPos(e) {
const svg = svgEl.value;
const rect = svg.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
return {
x: (clientX - rect.left) * (svg.viewBox.baseVal.width || 800) / rect.width,
y: (clientY - rect.top) * (svg.viewBox.baseVal.height || 500) / rect.height
};
}
function startDrag(e) {
const target = e.target;
if (target.dataset.point) {
dragging.value = target.dataset.point;
}
}
function onDrag(e) {
if (!dragging.value) return;
const pos = getPos(e);
points[dragging.value].x = Math.max(0, Math.min(800, pos.x));
points[dragging.value].y = Math.max(0, Math.min(500, pos.y));
}
function endDrag() { dragging.value = null; }
function reset() { Object.assign(points, JSON.parse(JSON.stringify(initial))); }
return { svgEl, points, curvePath, showLines, dragging, startDrag, onDrag, endDrag, reset };
}
}).mount('#app');
</script>
<script>
/**
* Iframe 元素高亮注入脚本
* 需要在目标网站中引入此脚本来支持跨域 iframe 高亮功能
*
* 使用方法:
* 1. 将此脚本添加到目标网站的 HTML 中
* 2. 或通过浏览器扩展、用户脚本等方式注入
*/
(function () {
"use strict";
// 检查是否在 iframe 中
if (window.self === window.top) {
return; // 不在 iframe 中,不执行
}
// 检查是否已经初始化过
if (window.__iframeHighlightInitialized) {
return;
}
window.__iframeHighlightInitialized = true;
console.log("Iframe 高亮脚本已加载");
// 创建高亮覆盖层
var overlay = document.createElement("div");
overlay.id = "iframe-highlight-overlay";
overlay.style.cssText = "\n position: fixed;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n pointer-events: none;\n z-index: 999999;\n overflow: hidden;\n ";
// 创建悬停高亮框(虚线边框)
var highlightBox = document.createElement("div");
highlightBox.id = "iframe-highlight-box";
highlightBox.style.cssText = "\n position: absolute;\n border: 2px dashed #007AFF;\n background: rgba(0, 122, 255, 0.08);\n pointer-events: none;\n display: none;\n transition: all 0.1s ease;\n box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.8);\n border-radius: 2px;\n ";
// 创建选中节点的常驻高亮框(实线边框)
var selectedBox = document.createElement("div");
selectedBox.id = "iframe-selected-box";
selectedBox.style.cssText = "\n position: absolute;\n border: 2px solid #007AFF;\n pointer-events: none;\n display: none;\n box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.9), 0 0 8px rgba(255, 107, 53, 0.4);\n border-radius: 2px;\n z-index: 1000000;\n ";
// 创建悬停标签显示
var tagLabel = document.createElement("div");
tagLabel.id = "iframe-tag-label";
tagLabel.style.cssText = "\n position: absolute;\n background: #007AFF;\n color: white;\n padding: 2px 6px;\n font-size: 11px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n border-radius: 2px;\n pointer-events: none;\n display: none;\n white-space: nowrap;\n z-index: 1000001;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n font-weight: 500;\n ";
// 创建选中节点标签
var selectedLabel = document.createElement("div");
selectedLabel.id = "iframe-selected-label";
selectedLabel.style.cssText = "\n position: absolute;\n background: #007AFF;\n color: white;\n padding: 3px 8px;\n font-size: 11px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n border-radius: 3px;\n pointer-events: none;\n display: none;\n white-space: nowrap;\n z-index: 1000002;\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);\n font-weight: 600;\n ";
overlay.appendChild(highlightBox);
overlay.appendChild(selectedBox);
overlay.appendChild(tagLabel);
overlay.appendChild(selectedLabel);
document.body.appendChild(overlay);
// 存储当前选中的元素
var selectedElement = null;
var highlightEnabled = false;
// 更新选中元素的高亮显示
function updateSelectedHighlight(element) {
console.log("updateSelectedHighlight called with:", element);
if (!element) {
selectedBox.style.display = "none";
selectedLabel.style.display = "none";
selectedElement = null;
console.log("Cleared selected highlight");
return;
}
selectedElement = element;
var rect = element.getBoundingClientRect();
console.log("Selected element rect:", rect);
// 更新选中高亮框位置
selectedBox.style.display = "block";
selectedBox.style.left = "".concat(rect.left - 2, "px");
selectedBox.style.top = "".concat(rect.top - 2, "px");
selectedBox.style.width = "".concat(rect.width + 4, "px");
selectedBox.style.height = "".concat(rect.height + 4, "px");
// 更新选中标签位置和内容
selectedLabel.style.display = "block";
selectedLabel.textContent = "\u2713 <".concat(element.tagName.toLowerCase(), ">");
// 计算标签位置,确保不超出视窗
var labelTop = rect.top - 28;
var labelLeft = rect.left;
// 如果标签会超出顶部,显示在元素下方
if (labelTop < 5) {
labelTop = rect.bottom + 5;
}
// 如果标签会超出右侧,向左调整
var labelWidth = selectedLabel.offsetWidth || 100; // 预估宽度
if (labelLeft + labelWidth > window.innerWidth - 10) {
labelLeft = window.innerWidth - labelWidth - 10;
}
selectedLabel.style.left = "".concat(Math.max(5, labelLeft), "px");
selectedLabel.style.top = "".concat(labelTop, "px");
console.log("Selected highlight positioned at:", {
left: selectedBox.style.left,
top: selectedBox.style.top,
width: selectedBox.style.width,
height: selectedBox.style.height
});
}
function getElementSelector(element) {
if (!(element instanceof Element)) throw new Error('Argument must be a DOM element');
var segments = [];
var current = element;
while (current !== document.documentElement) {
var selector = '';
// 优先检查唯一ID
if (current.id && document.querySelectorAll("#".concat(current.id)).length === 1) {
segments.unshift("#".concat(current.id));
break; // ID唯一,无需继续向上
}
// 生成类名选择器(取第一个有效类名)
var classes = Array.from(current.classList).filter(function (c) {
return !c.startsWith('js-');
});
var className = classes.length > 0 ? ".".concat(classes[0]) : '';
// 生成位置索引(nth-child)
var tag = current.tagName.toLowerCase();
if (!className) {
var siblings = Array.from(current.parentNode.children);
var index = siblings.findIndex(function (el) {
return el === current;
}) + 1;
selector = "".concat(tag, ":nth-child(").concat(index, ")");
} else {
selector = className;
}
segments.unshift(selector);
current = current.parentElement;
}
// 处理根元素
if (current === document.documentElement) {
segments.unshift('html');
}
return segments.join(' > ');
}
// 获取元素文本内容
function getElementText(element) {
var _element$textContent;
if (element.tagName === "INPUT") {
return element.value || element.placeholder || "";
}
if (element.tagName === "TEXTAREA") {
return element.value || element.placeholder || "";
}
var text = ((_element$textContent = element.textContent) === null || _element$textContent === void 0 ? void 0 : _element$textContent.trim()) || "";
return text.length > 50 ? text.substring(0, 50) + "..." : text;
}
// 获取元素属性信息
function getElementAttributes(element) {
var attrs = {};
for (var i = 0; i < element.attributes.length; i++) {
var attr = element.attributes[i];
attrs[attr.name] = attr.value;
}
return attrs;
}
// 鼠标悬停事件处理
function handleMouseOver(e) {
if (!highlightEnabled) return;
var target = e.target;
if (!target || target === overlay || target === highlightBox || target === tagLabel || target === selectedBox || target === selectedLabel) {
return;
}
// 避免高亮 html 和 body 元素
if (target === document.documentElement || target === document.body) {
return;
}
// 如果是已选中的元素,不显示悬停高亮
if (target === selectedElement) {
highlightBox.style.display = "none";
tagLabel.style.display = "none";
return;
}
var rect = target.getBoundingClientRect();
var selector = getElementSelector(target);
var text = getElementText(target);
var attributes = getElementAttributes(target);
// 更新悬停高亮框位置
highlightBox.style.display = "block";
highlightBox.style.left = "".concat(rect.left - 2, "px");
highlightBox.style.top = "".concat(rect.top - 2, "px");
highlightBox.style.width = "".concat(rect.width + 4, "px");
highlightBox.style.height = "".concat(rect.height + 4, "px");
// 更新标签位置和内容
tagLabel.style.display = "block";
tagLabel.textContent = "<".concat(target.tagName.toLowerCase(), ">");
// 计算标签位置,确保不超出视窗
var labelTop = rect.top - 22;
var labelLeft = rect.left;
// 如果标签会超出顶部,显示在元素下方
if (labelTop < 0) {
labelTop = rect.bottom + 5;
}
// 如果标签会超出右侧,向左调整
if (labelLeft + tagLabel.offsetWidth > window.innerWidth) {
labelLeft = window.innerWidth - tagLabel.offsetWidth - 5;
}
tagLabel.style.left = "".concat(Math.max(0, labelLeft), "px");
tagLabel.style.top = "".concat(labelTop, "px");
// 发送消息到父窗口
var elementInfo = {
tagName: target.tagName.toLowerCase(),
rect: {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y
},
selector: selector,
text: text,
attributes: attributes,
url: window.location.href,
path: window.location.pathname,
timestamp: Date.now()
};
try {
window.parent.postMessage({
type: "iframe-element-hover",
data: elementInfo,
source: "iframe-highlight-injector"
}, "*");
} catch (error) {
console.warn("无法发送消息到父窗口:", error);
}
}
// 鼠标离开事件处理
function handleMouseOut(e) {
if (!highlightEnabled) return;
var relatedTarget = e.relatedTarget;
// 如果鼠标移动到高亮相关元素上,不隐藏高亮
if (relatedTarget && (relatedTarget === highlightBox || relatedTarget === tagLabel || relatedTarget === overlay || relatedTarget === selectedBox || relatedTarget === selectedLabel)) {
return;
}
highlightBox.style.display = "none";
tagLabel.style.display = "none";
try {
window.parent.postMessage({
type: "iframe-element-hover",
data: null,
source: "iframe-highlight-injector"
}, "*");
} catch (error) {
console.warn("无法发送消息到父窗口:", error);
}
}
// 点击事件处理
function handleClick(e) {
var target = e.target;
if (!target || target === overlay || target === highlightBox || target === tagLabel || target === selectedBox || target === selectedLabel) {
return;
}
// 避免处理 html 和 body 元素
if (target === document.documentElement || target === document.body) {
return;
}
// 检查是否是交互元素,这些元素需要保留默认行为
var isInteractiveElement = ['input', 'textarea', 'select', 'button', 'a'].includes(target.tagName.toLowerCase());
// 如果高亮功能启用,对于非交互元素阻止默认行为和事件传播
if (highlightEnabled) {
e.preventDefault();
e.stopPropagation();
}
var rect = target.getBoundingClientRect();
var selector = getElementSelector(target);
var text = getElementText(target);
var attributes = getElementAttributes(target);
console.log("Element clicked:", {
tagName: target.tagName,
selector: selector,
rect: rect
});
// 立即更新选中高亮
updateSelectedHighlight(target);
// 隐藏悬停高亮,因为现在是选中状态
highlightBox.style.display = "none";
tagLabel.style.display = "none";
var elementInfo = {
tagName: target.tagName.toLowerCase(),
rect: {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y
},
selector: selector,
text: text,
attributes: attributes,
url: window.location.href,
path: window.location.pathname,
timestamp: Date.now()
};
try {
window.parent.postMessage({
type: "iframe-element-click",
data: elementInfo,
source: "iframe-highlight-injector"
}, "*");
} catch (error) {
console.warn("无法发送消息到父窗口:", error);
}
}
// 监听来自父窗口的消息
function handleParentMessage(event) {
console.log("Received message from parent:", event.data);
if (event.data.type === "iframe-highlight-toggle") {
var enabled = event.data.enabled;
console.log("Highlight toggle:", enabled);
if (enabled) {
enableHighlight();
} else {
disableHighlight();
}
} else if (event.data.type === "enable-iframe-highlight") {
console.log("Enable iframe highlight");
enableHighlight();
} else if (event.data.type === "disable-iframe-highlight") {
console.log("Disable iframe highlight");
disableHighlight();
} else if (event.data.type === "toggle-iframe-highlight") {
var _enabled = event.data.enabled !== undefined ? event.data.enabled : !highlightEnabled;
console.log("Toggle iframe highlight to:", _enabled);
if (_enabled) {
enableHighlight();
} else {
disableHighlight();
}
} else if (event.data.type === "update-selected-element") {
var selector = event.data.selector;
console.log("Update selected element with selector:", selector);
if (selector) {
try {
var element = document.querySelector(selector);
console.log("Found element by selector:", element);
updateSelectedHighlight(element);
} catch (error) {
console.warn("Failed to select element:", error);
updateSelectedHighlight(null);
}
} else {
updateSelectedHighlight(null);
}
} else if (event.data.type === "clear-selected-element") {
console.log("Clear selected element");
updateSelectedHighlight(null);
}
}
// 启用高亮功能
function enableHighlight() {
console.log("Enabling highlight");
document.addEventListener("mouseover", handleMouseOver, true);
document.addEventListener("mouseout", handleMouseOut, true);
document.addEventListener("click", handleClick, true);
highlightEnabled = true;
overlay.style.display = "block";
}
// 禁用高亮功能
function disableHighlight() {
console.log("Disabling highlight");
highlightEnabled = false;
// 保持事件监听器,但通过 highlightEnabled 变量控制行为
// 这样可以保留选中状态的显示
highlightBox.style.display = "none";
tagLabel.style.display = "none";
// 不隐藏 selectedBox 和 selectedLabel,保留选中状态
}
// 完全禁用高亮功能(移除所有监听器)
function fullyDisableHighlight() {
console.log("Fully disabling highlight");
highlightEnabled = false;
document.removeEventListener("mouseover", handleMouseOver, true);
document.removeEventListener("mouseout", handleMouseOut, true);
document.removeEventListener("click", handleClick, true);
overlay.style.display = "none";
highlightBox.style.display = "none";
tagLabel.style.display = "none";
selectedBox.style.display = "none";
selectedLabel.style.display = "none";
}
// 添加事件监听
enableHighlight();
window.addEventListener("message", handleParentMessage);
// 暴露全局函数供外部调用
window.__iframeHighlightControl = {
enable: enableHighlight,
disable: disableHighlight,
fullyDisable: fullyDisableHighlight,
isEnabled: function isEnabled() {
return highlightEnabled;
},
getSelectedElement: function getSelectedElement() {
return selectedElement;
},
updateSelected: updateSelectedHighlight,
// 通过消息发送开关控制
sendToggleMessage: function sendToggleMessage(enabled) {
window.parent.postMessage({
type: 'iframe-highlight-status',
enabled: enabled || highlightEnabled,
source: 'iframe-highlight-injector'
}, '*');
}
};
// 通知父窗口脚本已加载
try {
window.parent.postMessage({
type: "iframe-highlight-ready",
data: {
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now()
},
source: "iframe-highlight-injector"
}, "*");
} catch (error) {
console.warn("无法发送就绪消息到父窗口:", error);
}
// 清理函数
window.__iframeHighlightCleanup = function () {
fullyDisableHighlight();
window.removeEventListener("message", handleParentMessage);
if (overlay.parentElement) {
overlay.parentElement.removeChild(overlay);
}
delete window.__iframeHighlightInitialized;
delete window.__iframeHighlightCleanup;
};
})();
</script>
<script>
/**
* Iframe 元素高亮注入脚本
* 需要在目标网站中引入此脚本来支持跨域 iframe 高亮功能
*
* 使用方法:
* 1. 将此脚本添加到目标网站的 HTML 中
* 2. 或通过浏览器扩展、用户脚本等方式注入
*/
(function () {
"use strict";
// 检查是否在 iframe 中
if (window.self === window.top) {
return; // 不在 iframe 中,不执行
}
// 检查是否已经初始化过
if (window.__iframeHighlightInitialized) {
return;
}
window.__iframeHighlightInitialized = true;
console.log("Iframe 高亮脚本已加载");
// 创建高亮覆盖层
var overlay = document.createElement("div");
overlay.id = "iframe-highlight-overlay";
overlay.style.cssText = "\n position: fixed;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n pointer-events: none;\n z-index: 999999;\n overflow: hidden;\n ";
// 创建悬停高亮框(虚线边框)
var highlightBox = document.createElement("div");
highlightBox.id = "iframe-highlight-box";
highlightBox.style.cssText = "\n position: absolute;\n border: 2px dashed #007AFF;\n background: rgba(0, 122, 255, 0.08);\n pointer-events: none;\n display: none;\n transition: all 0.1s ease;\n box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.8);\n border-radius: 2px;\n ";
// 创建选中节点的常驻高亮框(实线边框)
var selectedBox = document.createElement("div");
selectedBox.id = "iframe-selected-box";
selectedBox.style.cssText = "\n position: absolute;\n border: 2px solid #007AFF;\n pointer-events: none;\n display: none;\n box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.9), 0 0 8px rgba(255, 107, 53, 0.4);\n border-radius: 2px;\n z-index: 1000000;\n ";
// 创建悬停标签显示
var tagLabel = document.createElement("div");
tagLabel.id = "iframe-tag-label";
tagLabel.style.cssText = "\n position: absolute;\n background: #007AFF;\n color: white;\n padding: 2px 6px;\n font-size: 11px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n border-radius: 2px;\n pointer-events: none;\n display: none;\n white-space: nowrap;\n z-index: 1000001;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n font-weight: 500;\n ";
// 创建选中节点标签
var selectedLabel = document.createElement("div");
selectedLabel.id = "iframe-selected-label";
selectedLabel.style.cssText = "\n position: absolute;\n background: #007AFF;\n color: white;\n padding: 3px 8px;\n font-size: 11px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n border-radius: 3px;\n pointer-events: none;\n display: none;\n white-space: nowrap;\n z-index: 1000002;\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);\n font-weight: 600;\n ";
overlay.appendChild(highlightBox);
overlay.appendChild(selectedBox);
overlay.appendChild(tagLabel);
overlay.appendChild(selectedLabel);
document.body.appendChild(overlay);
// 存储当前选中的元素
var selectedElement = null;
var highlightEnabled = false;
// 更新选中元素的高亮显示
function updateSelectedHighlight(element) {
console.log("updateSelectedHighlight called with:", element);
if (!element) {
selectedBox.style.display = "none";
selectedLabel.style.display = "none";
selectedElement = null;
console.log("Cleared selected highlight");
return;
}
selectedElement = element;
var rect = element.getBoundingClientRect();
console.log("Selected element rect:", rect);
// 更新选中高亮框位置
selectedBox.style.display = "block";
selectedBox.style.left = "".concat(rect.left - 2, "px");
selectedBox.style.top = "".concat(rect.top - 2, "px");
selectedBox.style.width = "".concat(rect.width + 4, "px");
selectedBox.style.height = "".concat(rect.height + 4, "px");
// 更新选中标签位置和内容
selectedLabel.style.display = "block";
selectedLabel.textContent = "\u2713 <".concat(element.tagName.toLowerCase(), ">");
// 计算标签位置,确保不超出视窗
var labelTop = rect.top - 28;
var labelLeft = rect.left;
// 如果标签会超出顶部,显示在元素下方
if (labelTop < 5) {
labelTop = rect.bottom + 5;
}
// 如果标签会超出右侧,向左调整
var labelWidth = selectedLabel.offsetWidth || 100; // 预估宽度
if (labelLeft + labelWidth > window.innerWidth - 10) {
labelLeft = window.innerWidth - labelWidth - 10;
}
selectedLabel.style.left = "".concat(Math.max(5, labelLeft), "px");
selectedLabel.style.top = "".concat(labelTop, "px");
console.log("Selected highlight positioned at:", {
left: selectedBox.style.left,
top: selectedBox.style.top,
width: selectedBox.style.width,
height: selectedBox.style.height
});
}
function getElementSelector(element) {
if (!(element instanceof Element)) throw new Error('Argument must be a DOM element');
var segments = [];
var current = element;
while (current !== document.documentElement) {
var selector = '';
// 优先检查唯一ID
if (current.id && document.querySelectorAll("#".concat(current.id)).length === 1) {
segments.unshift("#".concat(current.id));
break; // ID唯一,无需继续向上
}
// 生成类名选择器(取第一个有效类名)
var classes = Array.from(current.classList).filter(function (c) {
return !c.startsWith('js-');
});
var className = classes.length > 0 ? ".".concat(classes[0]) : '';
// 生成位置索引(nth-child)
var tag = current.tagName.toLowerCase();
if (!className) {
var siblings = Array.from(current.parentNode.children);
var index = siblings.findIndex(function (el) {
return el === current;
}) + 1;
selector = "".concat(tag, ":nth-child(").concat(index, ")");
} else {
selector = className;
}
segments.unshift(selector);
current = current.parentElement;
}
// 处理根元素
if (current === document.documentElement) {
segments.unshift('html');
}
return segments.join(' > ');
}
// 获取元素文本内容
function getElementText(element) {
var _element$textContent;
if (element.tagName === "INPUT") {
return element.value || element.placeholder || "";
}
if (element.tagName === "TEXTAREA") {
return element.value || element.placeholder || "";
}
var text = ((_element$textContent = element.textContent) === null || _element$textContent === void 0 ? void 0 : _element$textContent.trim()) || "";
return text.length > 50 ? text.substring(0, 50) + "..." : text;
}
// 获取元素属性信息
function getElementAttributes(element) {
var attrs = {};
for (var i = 0; i < element.attributes.length; i++) {
var attr = element.attributes[i];
attrs[attr.name] = attr.value;
}
return attrs;
}
// 鼠标悬停事件处理
function handleMouseOver(e) {
if (!highlightEnabled) return;
var target = e.target;
if (!target || target === overlay || target === highlightBox || target === tagLabel || target === selectedBox || target === selectedLabel) {
return;
}
// 避免高亮 html 和 body 元素
if (target === document.documentElement || target === document.body) {
return;
}
// 如果是已选中的元素,不显示悬停高亮
if (target === selectedElement) {
highlightBox.style.display = "none";
tagLabel.style.display = "none";
return;
}
var rect = target.getBoundingClientRect();
var selector = getElementSelector(target);
var text = getElementText(target);
var attributes = getElementAttributes(target);
// 更新悬停高亮框位置
highlightBox.style.display = "block";
highlightBox.style.left = "".concat(rect.left - 2, "px");
highlightBox.style.top = "".concat(rect.top - 2, "px");
highlightBox.style.width = "".concat(rect.width + 4, "px");
highlightBox.style.height = "".concat(rect.height + 4, "px");
// 更新标签位置和内容
tagLabel.style.display = "block";
tagLabel.textContent = "<".concat(target.tagName.toLowerCase(), ">");
// 计算标签位置,确保不超出视窗
var labelTop = rect.top - 22;
var labelLeft = rect.left;
// 如果标签会超出顶部,显示在元素下方
if (labelTop < 0) {
labelTop = rect.bottom + 5;
}
// 如果标签会超出右侧,向左调整
if (labelLeft + tagLabel.offsetWidth > window.innerWidth) {
labelLeft = window.innerWidth - tagLabel.offsetWidth - 5;
}
tagLabel.style.left = "".concat(Math.max(0, labelLeft), "px");
tagLabel.style.top = "".concat(labelTop, "px");
// 发送消息到父窗口
var elementInfo = {
tagName: target.tagName.toLowerCase(),
rect: {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y
},
selector: selector,
text: text,
attributes: attributes,
url: window.location.href,
path: window.location.pathname,
timestamp: Date.now()
};
try {
window.parent.postMessage({
type: "iframe-element-hover",
data: elementInfo,
source: "iframe-highlight-injector"
}, "*");
} catch (error) {
console.warn("无法发送消息到父窗口:", error);
}
}
// 鼠标离开事件处理
function handleMouseOut(e) {
if (!highlightEnabled) return;
var relatedTarget = e.relatedTarget;
// 如果鼠标移动到高亮相关元素上,不隐藏高亮
if (relatedTarget && (relatedTarget === highlightBox || relatedTarget === tagLabel || relatedTarget === overlay || relatedTarget === selectedBox || relatedTarget === selectedLabel)) {
return;
}
highlightBox.style.display = "none";
tagLabel.style.display = "none";
try {
window.parent.postMessage({
type: "iframe-element-hover",
data: null,
source: "iframe-highlight-injector"
}, "*");
} catch (error) {
console.warn("无法发送消息到父窗口:", error);
}
}
// 点击事件处理
function handleClick(e) {
var target = e.target;
if (!target || target === overlay || target === highlightBox || target === tagLabel || target === selectedBox || target === selectedLabel) {
return;
}
// 避免处理 html 和 body 元素
if (target === document.documentElement || target === document.body) {
return;
}
// 检查是否是交互元素,这些元素需要保留默认行为
var isInteractiveElement = ['input', 'textarea', 'select', 'button', 'a'].includes(target.tagName.toLowerCase());
// 如果高亮功能启用,对于非交互元素阻止默认行为和事件传播
if (highlightEnabled) {
e.preventDefault();
e.stopPropagation();
}
var rect = target.getBoundingClientRect();
var selector = getElementSelector(target);
var text = getElementText(target);
var attributes = getElementAttributes(target);
console.log("Element clicked:", {
tagName: target.tagName,
selector: selector,
rect: rect
});
// 立即更新选中高亮
updateSelectedHighlight(target);
// 隐藏悬停高亮,因为现在是选中状态
highlightBox.style.display = "none";
tagLabel.style.display = "none";
var elementInfo = {
tagName: target.tagName.toLowerCase(),
rect: {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y
},
selector: selector,
text: text,
attributes: attributes,
url: window.location.href,
path: window.location.pathname,
timestamp: Date.now()
};
try {
window.parent.postMessage({
type: "iframe-element-click",
data: elementInfo,
source: "iframe-highlight-injector"
}, "*");
} catch (error) {
console.warn("无法发送消息到父窗口:", error);
}
}
// 监听来自父窗口的消息
function handleParentMessage(event) {
console.log("Received message from parent:", event.data);
if (event.data.type === "iframe-highlight-toggle") {
var enabled = event.data.enabled;
console.log("Highlight toggle:", enabled);
if (enabled) {
enableHighlight();
} else {
disableHighlight();
}
} else if (event.data.type === "enable-iframe-highlight") {
console.log("Enable iframe highlight");
enableHighlight();
} else if (event.data.type === "disable-iframe-highlight") {
console.log("Disable iframe highlight");
disableHighlight();
} else if (event.data.type === "toggle-iframe-highlight") {
var _enabled = event.data.enabled !== undefined ? event.data.enabled : !highlightEnabled;
console.log("Toggle iframe highlight to:", _enabled);
if (_enabled) {
enableHighlight();
} else {
disableHighlight();
}
} else if (event.data.type === "update-selected-element") {
var selector = event.data.selector;
console.log("Update selected element with selector:", selector);
if (selector) {
try {
var element = document.querySelector(selector);
console.log("Found element by selector:", element);
updateSelectedHighlight(element);
} catch (error) {
console.warn("Failed to select element:", error);
updateSelectedHighlight(null);
}
} else {
updateSelectedHighlight(null);
}
} else if (event.data.type === "clear-selected-element") {
console.log("Clear selected element");
updateSelectedHighlight(null);
}
}
// 启用高亮功能
function enableHighlight() {
console.log("Enabling highlight");
document.addEventListener("mouseover", handleMouseOver, true);
document.addEventListener("mouseout", handleMouseOut, true);
document.addEventListener("click", handleClick, true);
highlightEnabled = true;
overlay.style.display = "block";
}
// 禁用高亮功能
function disableHighlight() {
console.log("Disabling highlight");
highlightEnabled = false;
// 保持事件监听器,但通过 highlightEnabled 变量控制行为
// 这样可以保留选中状态的显示
highlightBox.style.display = "none";
tagLabel.style.display = "none";
// 不隐藏 selectedBox 和 selectedLabel,保留选中状态
}
// 完全禁用高亮功能(移除所有监听器)
function fullyDisableHighlight() {
console.log("Fully disabling highlight");
highlightEnabled = false;
document.removeEventListener("mouseover", handleMouseOver, true);
document.removeEventListener("mouseout", handleMouseOut, true);
document.removeEventListener("click", handleClick, true);
overlay.style.display = "none";
highlightBox.style.display = "none";
tagLabel.style.display = "none";
selectedBox.style.display = "none";
selectedLabel.style.display = "none";
}
// 添加事件监听
enableHighlight();
window.addEventListener("message", handleParentMessage);
// 暴露全局函数供外部调用
window.__iframeHighlightControl = {
enable: enableHighlight,
disable: disableHighlight,
fullyDisable: fullyDisableHighlight,
isEnabled: function isEnabled() {
return highlightEnabled;
},
getSelectedElement: function getSelectedElement() {
return selectedElement;
},
updateSelected: updateSelectedHighlight,
// 通过消息发送开关控制
sendToggleMessage: function sendToggleMessage(enabled) {
window.parent.postMessage({
type: 'iframe-highlight-status',
enabled: enabled || highlightEnabled,
source: 'iframe-highlight-injector'
}, '*');
}
};
// 通知父窗口脚本已加载
try {
window.parent.postMessage({
type: "iframe-highlight-ready",
data: {
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now()
},
source: "iframe-highlight-injector"
}, "*");
} catch (error) {
console.warn("无法发送就绪消息到父窗口:", error);
}
// 清理函数
window.__iframeHighlightCleanup = function () {
fullyDisableHighlight();
window.removeEventListener("message", handleParentMessage);
if (overlay.parentElement) {
overlay.parentElement.removeChild(overlay);
}
delete window.__iframeHighlightInitialized;
delete window.__iframeHighlightCleanup;
};
})();
</script>
</body>
</html>
index.html
md
README.md
index.html