Refactor proxy panel to include a new links tab with dynamic content loading and improved styling for better user interaction

This commit is contained in:
go0p
2026-08-06 15:11:35 +08:00
committed by go0p
parent b79dda7f03
commit 1af5cca417
3 changed files with 587 additions and 86 deletions
+10 -1
View File
@@ -26,7 +26,16 @@
{
"matches": ["<all_urls>","http://mitm/"],
"run_at": "document_start",
"js": ["proxy/content.js"]
"js": [
"proxy/links_finder.js"
]
},
{
"matches": ["<all_urls>","http://mitm/"],
"run_at": "document_end",
"js": [
"proxy/content.js"
]
}
],
"permissions": [
+364 -85
View File
@@ -201,7 +201,7 @@ console.log("Content script starting...");
createPanel() {
if (!document.body) {
console.log("Body not available during panel creation");
console.log("Body not ready, waiting...");
return;
}
@@ -312,6 +312,12 @@ console.log("Content script starting...");
background-color 0.2s ease !important;
}
/* 链接面板展开状态 */
.floating-panel.expanded[data-active-tab="links"] {
width: 280px !important;
max-height: 600px !important;
}
.panel-header {
height: 40px !important;
min-height: 40px !important;
@@ -488,24 +494,215 @@ console.log("Content script starting...");
.action-button span {
color: #666 !important;
}
/* 添加垂直标签样式 */
.tabs-container {
display: flex !important;
height: 100% !important;
min-height: 200px !important;
}
.tab-list {
width: 40px !important;
background: #f8f9fa !important;
border-right: 1px solid #eee !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
padding-top: 8px !important;
position: sticky !important;
top: 0 !important;
align-self: flex-start !important;
height: 100% !important;
flex-shrink: 0 !important;
}
.tab-button {
width: 32px !important;
height: 32px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin-bottom: 4px !important;
border-radius: 4px !important;
cursor: pointer !important;
transition: all 0.2s !important;
background: transparent !important;
border: none !important;
padding: 0 !important;
}
.tab-button:hover {
background: #fff7e6 !important;
}
.tab-button.active {
background: #fff7e6 !important;
color: #ff6b00 !important;
}
.tab-content {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
min-width: 0 !important;
height: 100% !important;
}
.tab-panel {
display: none !important;
height: 100% !important;
overflow: hidden !important;
flex-direction: column !important;
}
.tab-panel.active {
display: flex !important;
}
/* Links Panel Styles */
.links-stats {
position: sticky !important;
top: 0 !important;
background: white !important;
z-index: 1 !important;
padding: 12px !important;
border-bottom: 1px solid #eee !important;
flex-shrink: 0 !important;
}
.stats-item {
display: flex !important;
align-items: center !important;
gap: 8px !important;
}
.links-filters {
position: sticky !important;
top: 43px !important;
background: white !important;
z-index: 1 !important;
padding: 12px !important;
display: flex !important;
flex-wrap: wrap !important;
gap: 8px !important;
border-bottom: 1px solid #eee !important;
flex-shrink: 0 !important;
}
.filter-btn {
padding: 4px 8px !important;
border-radius: 4px !important;
border: 1px solid #eee !important;
background: white !important;
cursor: pointer !important;
transition: all 0.2s !important;
font-size: 12px !important;
color: #666 !important;
display: flex !important;
align-items: center !important;
gap: 4px !important;
white-space: nowrap !important;
}
.filter-btn:hover {
background: #fff7e6 !important;
border-color: #ffd591 !important;
color: #ff6b00 !important;
}
.filter-btn.active {
background: #fff7e6 !important;
border-color: #ff6b00 !important;
color: #ff6b00 !important;
}
.filter-btn span {
color: inherit !important;
}
.links-list {
flex: 1 !important;
overflow-y: auto !important;
overflow-x: hidden !important;
padding: 12px !important;
}
.link-item {
display: flex !important;
align-items: flex-start !important;
padding: 8px !important;
border-bottom: 1px solid #eee !important;
gap: 8px !important;
transition: all 0.2s ease !important;
}
.link-item[data-hidden="true"] {
display: none !important;
}
.link-icon {
font-size: 16px !important;
min-width: 24px !important;
text-align: center !important;
}
.link-content {
flex: 1 !important;
min-width: 0 !important;
}
.link-url {
font-size: 12px !important;
color: #1890ff !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
.link-text, .link-alt {
font-size: 12px !important;
color: #666 !important;
margin-top: 4px !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
.copy-btn {
padding: 4px !important;
border-radius: 4px !important;
border: none !important;
background: transparent !important;
cursor: pointer !important;
transition: all 0.2s !important;
font-size: 14px !important;
}
.copy-btn:hover {
background: #f5f5f5 !important;
}
`;
// 创建面板内容
const panel = document.createElement("div");
panel.className = "floating-panel yak-proxy-root";
// 初始面板内容
panel.innerHTML = `
<div class="panel-header">
<div class="header-content">
<img src="${YAK_ICON_URL}" class="yak-icon" alt="Yak" />
<div class="active-proxy-info">
<!-- 当前代理信息将动态更新 -->
<div class="panel-header">
<div class="header-content">
<img src="${YAK_ICON_URL}" class="yak-icon" alt="Yak" />
<div class="active-proxy-info">
<span>🟢</span>
<span>直接连接</span>
</div>
</div>
</div>
</div>
<div class="panel-content">
<!-- 内容将由 _buildPanelHtml 方法动态生成 -->
</div>
`;
<div class="panel-content">
<!-- 内容将由 updatePanel 方法动态更新 -->
</div>
`;
// 将样式和面板添加到 shadow DOM
shadow.appendChild(style);
@@ -529,6 +726,10 @@ console.log("Content script starting...");
header.addEventListener("click", (e) => {
if (!this.isDragging) {
floatingPanel.classList.toggle("expanded");
// 如果展开且内容为空,则更新面板
if (floatingPanel.classList.contains("expanded")) {
this.updatePanel();
}
}
});
@@ -691,37 +892,44 @@ console.log("Content script starting...");
return this._updateQueue;
},
// 将 HTML 构建逻辑抽离成单独的方法
// 修改面板内容的构建方法
_buildPanelHtml(currentProxy, configs) {
let html = `
<div class="proxy-item ${
currentProxy.currentMode === "direct" ? "active" : ""
}"
data-id="direct"
title="直接连接">
<span>🟢</span>
<span>直接连接</span>
${
currentProxy.currentMode === "direct"
? '<div class="proxy-status"></div>'
: ""
}
</div>
<div class="proxy-item ${
currentProxy.currentMode === "system" ? "active" : ""
}"
data-id="system"
title="系统代理">
<span>⚙️</span>
<span>系统代理</span>
${
currentProxy.currentMode === "system"
? '<div class="proxy-status"></div>'
: ""
}
</div>
<div class="divider"></div>
`;
<div class="tabs-container">
<div class="tab-list">
<button class="tab-button active" data-tab="proxy" title="代理设置">🌐</button>
<button class="tab-button" data-tab="links" title="页面链接">🔗</button>
</div>
<div class="tab-content">
<div class="tab-panel active" data-panel="proxy">
<div class="proxy-item ${
currentProxy.currentMode === "direct" ? "active" : ""
}"
data-id="direct"
title="直接连接">
<span>🟢</span>
<span>直接连接</span>
${
currentProxy.currentMode === "direct"
? '<div class="proxy-status"></div>'
: ""
}
</div>
<div class="proxy-item ${
currentProxy.currentMode === "system" ? "active" : ""
}"
data-id="system"
title="系统代理">
<span>⚙️</span>
<span>系统代理</span>
${
currentProxy.currentMode === "system"
? '<div class="proxy-status"></div>'
: ""
}
</div>
<div class="divider"></div>
`;
// 添加自定义代理配置
configs.forEach((config) => {
@@ -730,64 +938,55 @@ console.log("Content script starting...");
currentProxy.currentMode === "fixed_servers" && config.enabled;
const proxyIcon = config.proxyType === "pac_script" ? "📜" : "🌐";
// 构建 title 提示信息
let tooltipText;
if (config.proxyType === "pac_script") {
tooltipText = "PAC Script";
} else {
const scheme = config.scheme
? `${config.scheme.toUpperCase()} `
: "";
tooltipText = `${scheme}${config.host}:${config.port}`;
}
const tooltipText = config.proxyType === "pac_script"
? "PAC Script"
: `${config.scheme ? `${config.scheme.toUpperCase()} ` : ""}${config.host}:${config.port}`;
html += `
<div class="proxy-item ${isActive ? "active" : ""}"
data-id="${config.id}"
title="${tooltipText}">
<span>${proxyIcon}</span>
<span>${config.name || "未命名代理"}</span>
${isActive ? '<div class="proxy-status"></div>' : ""}
</div>
`;
<div class="proxy-item ${isActive ? "active" : ""}"
data-id="${config.id}"
title="${tooltipText}">
<span>${proxyIcon}</span>
<span>${config.name || "未命名代理"}</span>
${isActive ? '<div class="proxy-status"></div>' : ""}
</div>
`;
}
});
html += `
<div class="divider"></div>
<div class="action-button add-proxy">
<span></span>
<span>添加代理</span>
<div class="divider"></div>
<div class="action-button add-proxy">
<span></span>
<span>添加代理</span>
</div>
<div class="action-button settings">
<span>⚙️</span>
<span>设置</span>
</div>
</div>
<div class="tab-panel" data-panel="links">
<!-- Links panel content will be dynamically populated -->
</div>
</div>
</div>
<div class="action-button settings">
<span>⚙️</span>
<span>设置</span>
</div>
`;
`;
return html;
},
_bindEventListeners(panel, configs, currentProxy) {
// 代理项点击事件
// 现有的事件监听器
panel.querySelectorAll(".proxy-item").forEach((item) => {
const id = item.dataset.id;
// 使用事件委托来提高性能
const clickHandler = async (e) => {
item.addEventListener("click", async (e) => {
e.stopPropagation();
if (this._updating) return;
if (this._updating) {
console.log("Panel is updating, ignoring click");
return;
}
// 添加点击反馈
const originalOpacity = item.style.opacity;
item.style.opacity = "0.7";
try {
// 立即更新 UI 状态,不等待响应
panel.querySelectorAll(".proxy-item").forEach((i) => {
i.classList.remove("active");
i.querySelector("span").style.color = "#666";
@@ -805,21 +1004,62 @@ console.log("Content script starting...");
});
} else {
const config = configs.find((c) => c.id === id);
if (config) {
await switchProxy(config);
}
if (config) await switchProxy(config);
}
} catch (error) {
console.error("Error handling proxy item click:", error);
// 发生错误时恢复原状
await this.updatePanel();
} finally {
item.style.opacity = originalOpacity;
}
};
});
});
// 使用 { once: true } 确保事件监听器不会重复
item.addEventListener("click", clickHandler, {once: true});
// Tab 切换事件
panel.querySelectorAll(".tab-button").forEach((button) => {
console.log("Setting up tab button click handler for:", button.dataset.tab);
button.addEventListener("click", async (e) => {
e.stopPropagation();
const tabId = button.dataset.tab;
console.log("Tab button clicked:", tabId);
// 更新按钮状态
panel.querySelectorAll(".tab-button").forEach((btn) => {
btn.classList.remove("active");
});
button.classList.add("active");
// 更新面板显示
panel.querySelectorAll(".tab-panel").forEach((panel) => {
panel.classList.remove("active");
});
const targetPanel = panel.querySelector(`[data-panel="${tabId}"]`);
console.log("Target panel found:", !!targetPanel);
targetPanel.classList.add("active");
// 更新面板尺寸
const floatingPanel = this.panel.shadowRoot.querySelector(".floating-panel");
floatingPanel.setAttribute("data-active-tab", tabId);
// 如果是链接面板,检查内容并初始化
if (tabId === "links") {
console.log("Links tab selected, checking panel content...");
// 检查面板是否有实际的链接内容
const hasContent = targetPanel.querySelector('.links-stats, .links-filters, .links-list');
if (!hasContent) {
console.log("Links panel needs initialization...");
await this._initLinksPanel(targetPanel);
} else {
console.log("Links panel content found:", hasContent);
// 如果内容存在但为空,重新初始化
const linksList = targetPanel.querySelector('.links-list');
if (linksList && !linksList.children.length) {
console.log("Links list is empty, reinitializing...");
await this._initLinksPanel(targetPanel);
}
}
}
});
});
// 添加代理按钮
@@ -846,6 +1086,45 @@ console.log("Content script starting...");
});
},
// 初始化链接面板
async _initLinksPanel(panel) {
try {
console.log("Creating LinksFinder instance...");
if (!window.LinksFinder) {
console.error("LinksFinder is not defined! Window properties:", Object.keys(window));
throw new Error("LinksFinder class not found");
}
// 清理旧的事件监听器
const oldButtons = panel.querySelectorAll('.filter-btn, .copy-btn');
oldButtons.forEach(btn => {
btn.replaceWith(btn.cloneNode(true));
});
// 创建链接查找器实例
const linksFinder = new window.LinksFinder();
console.log("Building links panel...");
// 构建并插入面板内容
panel.innerHTML = linksFinder.buildLinksPanel();
console.log("Binding events...");
// 绑定事件
linksFinder.bindEvents(panel);
console.log("Links panel initialization completed");
} catch (error) {
console.error("Error initializing links panel:", error);
panel.innerHTML = `
<div style="padding: 16px; text-align: center; color: #ff4d4f;">
<span>😢</span>
<p>加载链接失败: ${error.message}</p>
<p style="font-size: 12px; color: #999;">详细错误: ${error.stack}</p>
</div>
`;
}
},
_initDragFeature(header, panel) {
let isDragging = false;
let startY = 0;
+213
View File
@@ -0,0 +1,213 @@
// Links Finder Module
class LinksFinder {
constructor() {
this.links = [];
this.lastUpdate = null;
}
// 获取页面所有链接
getAllLinks() {
const links = [];
const seen = new Set();
// 获取所有 a 标签
document.querySelectorAll('a').forEach(a => {
const href = a.href;
if (href && !seen.has(href) && href.startsWith('http')) {
seen.add(href);
links.push({
type: 'anchor',
url: href,
text: a.textContent.trim() || href,
});
}
});
// 获取所有图片链接
document.querySelectorAll('img').forEach(img => {
const src = img.src;
if (src && !seen.has(src) && src.startsWith('http')) {
seen.add(src);
links.push({
type: 'image',
url: src,
alt: img.alt || 'Image',
});
}
});
// 获取所有脚本链接
document.querySelectorAll('script').forEach(script => {
const src = script.src;
if (src && !seen.has(src) && src.startsWith('http')) {
seen.add(src);
links.push({
type: 'script',
url: src,
});
}
});
// 获取所有样式表链接
document.querySelectorAll('link[rel="stylesheet"]').forEach(link => {
const href = link.href;
if (href && !seen.has(href) && href.startsWith('http')) {
seen.add(href);
links.push({
type: 'stylesheet',
url: href,
});
}
});
this.links = links;
this.lastUpdate = new Date();
return links;
}
// 按类型过滤链接
filterLinksByType(type) {
return this.links.filter(link => link.type === type);
}
// 获取链接统计信息
getLinkStats() {
const stats = {
total: this.links.length,
byType: {}
};
this.links.forEach(link => {
if (!stats.byType[link.type]) {
stats.byType[link.type] = 0;
}
stats.byType[link.type]++;
});
return stats;
}
// 构建链接面板的 HTML
buildLinksPanel() {
const links = this.getAllLinks();
const stats = this.getLinkStats();
let html = `
<div class="links-stats">
<div class="stats-item">
<span>🔗</span>
<span>总链接: ${stats.total}</span>
</div>
</div>
<div class="links-filters">
<button class="filter-btn active" data-type="all">
<span>🔍</span>
<span>全部 (${stats.total})</span>
</button>
${Object.entries(stats.byType).map(([type, count]) => `
<button class="filter-btn" data-type="${type}">
<span>${this._getTypeIcon(type)}</span>
<span>${this._getTypeName(type)} (${count})</span>
</button>
`).join('')}
</div>
<div class="links-list">
${links.map(link => this._buildLinkItem(link)).join('')}
</div>
`;
return html;
}
// 获取链接类型图标
_getTypeIcon(type) {
const icons = {
anchor: '🔗',
image: '🖼️',
script: '📜',
stylesheet: '🎨'
};
return icons[type] || '🔗';
}
// 获取链接类型名称
_getTypeName(type) {
const names = {
anchor: '链接',
image: '图片',
script: '脚本',
stylesheet: '样式'
};
return names[type] || type;
}
// 构建单个链接项的 HTML
_buildLinkItem(link) {
return `
<div class="link-item" data-type="${link.type}">
<div class="link-icon">${this._getTypeIcon(link.type)}</div>
<div class="link-content">
<div class="link-url" title="${link.url}">${link.url}</div>
${link.text ? `<div class="link-text" title="${link.text}">${link.text}</div>` : ''}
${link.alt ? `<div class="link-alt" title="${link.alt}">${link.alt}</div>` : ''}
</div>
<button class="copy-btn" data-url="${link.url}" title="复制链接">📋</button>
</div>
`;
}
// 绑定事件处理
bindEvents(container) {
// 过滤按钮点击事件
container.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const type = btn.dataset.type;
console.log('Filter clicked:', type);
// 更新按钮状态
container.querySelectorAll('.filter-btn').forEach(b => {
b.classList.remove('active');
});
btn.classList.add('active');
// 过滤链接显示
container.querySelectorAll('.link-item').forEach(item => {
if (type === 'all' || item.dataset.type === type) {
item.removeAttribute('data-hidden');
} else {
item.setAttribute('data-hidden', 'true');
}
});
});
});
// 复制按钮点击事件
container.querySelectorAll('.copy-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const url = btn.dataset.url;
try {
await navigator.clipboard.writeText(url);
const originalText = btn.textContent;
btn.textContent = '✓';
btn.style.setProperty('color', '#52c41a', 'important');
setTimeout(() => {
btn.textContent = originalText;
btn.style.removeProperty('color');
}, 1000);
} catch (err) {
console.error('Failed to copy:', err);
btn.textContent = '❌';
setTimeout(() => {
btn.textContent = '📋';
}, 1000);
}
});
});
}
}
console.log("LinksFinder module loaded");
// 导出模块
window.LinksFinder = LinksFinder;