mirror of
https://github.com/wisdgod/cursor-api.git
synced 2025-10-09 00:20:06 +08:00
1307 lines
36 KiB
HTML
1307 lines
36 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<link rel="icon" type="image/x-icon" href="data:image/x-icon;,">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>代理信息管理</title>
|
||
<!-- 引入共享样式 -->
|
||
<link rel="stylesheet" href="/static/shared-styles.css">
|
||
<script src="/static/shared.js"></script>
|
||
<style>
|
||
/* 代理列表布局样式 */
|
||
.proxy-system {
|
||
background: var(--card-background);
|
||
border-radius: var(--border-radius);
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
margin-top: var(--spacing);
|
||
overflow: hidden;
|
||
height: calc(100vh - 250px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 8px;
|
||
border-bottom: 1px solid var(--border-color);
|
||
background: var(--card-background);
|
||
}
|
||
|
||
.toolbar .search-box {
|
||
flex: 1;
|
||
margin-right: 8px;
|
||
position: relative;
|
||
}
|
||
|
||
.toolbar .search-box input {
|
||
width: 100%;
|
||
padding-left: 32px;
|
||
}
|
||
|
||
.toolbar .search-box::before {
|
||
content: "🔍";
|
||
position: absolute;
|
||
left: 10px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.proxy-list {
|
||
flex: 1;
|
||
overflow: auto;
|
||
position: relative;
|
||
user-select: none;
|
||
padding: 0 12px 12px 12px;
|
||
}
|
||
|
||
.proxy-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin-top: 0;
|
||
}
|
||
|
||
.proxy-table th {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
background: var(--card-background);
|
||
padding: 10px;
|
||
text-align: left;
|
||
font-weight: 500;
|
||
border-bottom: 1px solid var(--border-color);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.proxy-table td {
|
||
padding: 8px 10px;
|
||
border-bottom: 1px solid var(--border-color);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.proxy-table tr {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.proxy-table tr:hover {
|
||
background: var(--primary-color-alpha);
|
||
}
|
||
|
||
.proxy-table tr.selected {
|
||
background: var(--primary-color-alpha);
|
||
}
|
||
|
||
.proxy-table tr.general {
|
||
font-weight: bold;
|
||
}
|
||
|
||
.status-bar {
|
||
padding: 8px;
|
||
background: var(--card-background);
|
||
border-top: 1px solid var(--border-color);
|
||
color: var(--text-secondary);
|
||
font-size: 14px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
/* 右键菜单 */
|
||
.context-menu {
|
||
position: absolute;
|
||
background: var(--card-background);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: var(--border-radius);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||
padding: 4px 0;
|
||
z-index: 1000;
|
||
min-width: 180px;
|
||
display: none;
|
||
}
|
||
|
||
.context-menu-item {
|
||
padding: 8px 16px;
|
||
cursor: pointer;
|
||
transition: all var(--transition-fast);
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.context-menu-item:hover {
|
||
background: var(--primary-color-alpha);
|
||
}
|
||
|
||
.context-menu-divider {
|
||
height: 1px;
|
||
background: var(--border-color);
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.context-menu-shortcut {
|
||
margin-left: auto;
|
||
color: var(--text-secondary);
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* 筛选面板 */
|
||
.filter-panel {
|
||
padding: 16px;
|
||
background: var(--card-background);
|
||
border-radius: var(--border-radius);
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
margin-top: var(--spacing);
|
||
}
|
||
|
||
.filter-row {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.filter-group {
|
||
flex: 1;
|
||
min-width: 200px;
|
||
}
|
||
|
||
/* 多选下拉框样式 */
|
||
.multiselect {
|
||
position: relative;
|
||
width: 100%;
|
||
}
|
||
|
||
.multiselect-dropdown {
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
background: var(--input-background);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: var(--border-radius);
|
||
cursor: pointer;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
min-height: 44px;
|
||
}
|
||
|
||
.multiselect-dropdown:after {
|
||
content: "▼";
|
||
font-size: 12px;
|
||
}
|
||
|
||
.multiselect-dropdown.active:after {
|
||
content: "▲";
|
||
}
|
||
|
||
.multiselect-options {
|
||
position: absolute;
|
||
top: 100%;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 100;
|
||
background: var(--card-background);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
display: none;
|
||
}
|
||
|
||
.multiselect-options.show {
|
||
display: block;
|
||
}
|
||
|
||
.multiselect-option {
|
||
padding: 8px 12px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: baseline;
|
||
}
|
||
|
||
.multiselect-option:hover {
|
||
background: var(--primary-color-alpha);
|
||
}
|
||
|
||
.multiselect-option input {
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.multiselect-option label {
|
||
margin: 0;
|
||
}
|
||
|
||
.selected-options {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
min-height: 20px;
|
||
}
|
||
|
||
.selected-option {
|
||
background: var(--primary-color-alpha);
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.selected-option button {
|
||
background: none;
|
||
border: none;
|
||
font-size: 14px;
|
||
margin-left: 4px;
|
||
cursor: pointer;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* 对话框样式 */
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
background: var(--card-background);
|
||
padding: 20px;
|
||
border-radius: var(--border-radius);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
z-index: 1000;
|
||
width: 90%;
|
||
max-width: 500px;
|
||
}
|
||
|
||
.modal-backdrop {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 999;
|
||
}
|
||
|
||
.modal-header {
|
||
margin-bottom: 15px;
|
||
border-bottom: 1px solid var(--border-color);
|
||
padding-bottom: 10px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.modal-header h3 {
|
||
margin: 0;
|
||
}
|
||
|
||
.modal-close {
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--text-secondary);
|
||
font-size: 20px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.modal-body {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.modal-footer {
|
||
margin-top: 15px;
|
||
padding-top: 15px;
|
||
border-top: 1px solid var(--border-color);
|
||
text-align: right;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
}
|
||
|
||
/* 向上展开的子菜单样式 */
|
||
.proxy-menu.open-upward .proxy-submenu {
|
||
top: auto;
|
||
bottom: 0;
|
||
}
|
||
|
||
/* 向左展开的子菜单样式 */
|
||
.proxy-menu.open-leftward .proxy-submenu {
|
||
right: 100%;
|
||
left: auto;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.filter-row {
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<h1>代理信息管理</h1>
|
||
|
||
<div class="container">
|
||
<div class="form-group">
|
||
<label>认证令牌:</label>
|
||
<input type="password" id="authToken" placeholder="输入 AUTH_TOKEN">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 筛选面板 -->
|
||
<div class="filter-panel">
|
||
<div class="filter-row">
|
||
<div class="filter-group">
|
||
<label>代理类型:</label>
|
||
<div class="multiselect">
|
||
<div class="multiselect-dropdown">
|
||
<div class="selected-options" id="selectedProxyTypes"></div>
|
||
</div>
|
||
<div class="multiselect-options">
|
||
<div class="multiselect-option">
|
||
<input type="checkbox" id="type-non" value="non" checked> <label for="type-non">不使用代理</label>
|
||
</div>
|
||
<div class="multiselect-option">
|
||
<input type="checkbox" id="type-sys" value="sys" checked> <label for="type-sys">系统代理</label>
|
||
</div>
|
||
<div class="multiselect-option">
|
||
<input type="checkbox" id="type-http" value="http" checked> <label for="type-http">HTTP</label>
|
||
</div>
|
||
<div class="multiselect-option">
|
||
<input type="checkbox" id="type-https" value="https" checked> <label for="type-https">HTTPS</label>
|
||
</div>
|
||
<div class="multiselect-option">
|
||
<input type="checkbox" id="type-socks4" value="socks4" checked> <label for="type-socks4">SOCKS4</label>
|
||
</div>
|
||
<div class="multiselect-option">
|
||
<input type="checkbox" id="type-socks5" value="socks5" checked> <label for="type-socks5">SOCKS5</label>
|
||
</div>
|
||
<div class="multiselect-option">
|
||
<input type="checkbox" id="type-socks5h" value="socks5h" checked> <label
|
||
for="type-socks5h">SOCKS5H</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="button-group" style="flex: 1;">
|
||
<button id="refreshBtn" onclick="getProxyInfo()" class="primary">
|
||
<span>刷新列表</span>
|
||
<span class="context-menu-shortcut">F5</span>
|
||
</button>
|
||
<button id="addProxyBtn" onclick="addProxy()" class="secondary">添加代理</button>
|
||
<button id="exportBtn" onclick="exportProxies()" class="secondary">导出配置</button>
|
||
<button id="importBtn" onclick="importProxies()" class="secondary">导入配置</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 代理列表区域 -->
|
||
<div class="proxy-system">
|
||
<div class="toolbar">
|
||
<div class="search-box">
|
||
<input type="text" id="searchInput" placeholder="搜索代理名称...">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="proxy-list">
|
||
<table class="proxy-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 20%;">代理名称</th>
|
||
<th style="width: 20%;">代理类型</th>
|
||
<th style="width: 50%;">代理地址</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="proxyListBody">
|
||
<!-- 动态生成的代理列表 -->
|
||
</tbody>
|
||
</table>
|
||
<div id="emptyState" style="display: none; padding: 40px; text-align: center; color: var(--text-secondary);">
|
||
<div style="font-size: 48px; margin-bottom: 16px;">🌐</div>
|
||
<h3>没有找到代理</h3>
|
||
<p>请添加代理或更改筛选条件</p>
|
||
<button onclick="addProxy()" class="primary">添加代理</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="status-bar">
|
||
<div id="selectionStatus">已选择: 0 个项目</div>
|
||
<div id="totalCount">共 0 个代理</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右键菜单 -->
|
||
<div id="contextMenu" class="context-menu">
|
||
<div class="context-menu-item" onclick="setAsGeneral()">
|
||
<span>设为通用代理</span>
|
||
<span class="context-menu-shortcut">Ctrl+G</span>
|
||
</div>
|
||
<div class="context-menu-item" onclick="copyProxyUrl()">
|
||
<span>复制代理地址</span>
|
||
<span class="context-menu-shortcut">Ctrl+C</span>
|
||
</div>
|
||
<div class="context-menu-divider"></div>
|
||
<div class="context-menu-item" onclick="deleteSelectedProxies()">
|
||
<span>删除</span>
|
||
<span class="context-menu-shortcut">Delete</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 添加代理对话框 -->
|
||
<div class="modal-backdrop" id="addProxyModal-backdrop"></div>
|
||
<div class="modal" id="addProxyModal">
|
||
<div class="modal-header">
|
||
<h3>添加代理</h3>
|
||
<button class="modal-close" onclick="closeModal('addProxyModal')">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label>代理名称:</label>
|
||
<input type="text" id="proxyName" placeholder="输入代理名称">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>代理类型:</label>
|
||
<select id="proxyType" onchange="toggleProxyUrl()">
|
||
<option value="non">不使用代理</option>
|
||
<option value="sys">系统代理</option>
|
||
<option value="custom" selected>自定义代理</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group" id="proxyUrlGroup">
|
||
<label>代理地址:</label>
|
||
<input type="text" id="proxyUrl" placeholder="例如: http://localhost:7890">
|
||
<div class="help-text">支持的格式: http://localhost:7890, socks5://username:password@localhost:1080</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button onclick="closeModal('addProxyModal')" class="secondary">取消</button>
|
||
<button onclick="confirmAddProxy()" class="primary">添加</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 确认删除对话框 -->
|
||
<div class="modal-backdrop" id="confirmModal-backdrop"></div>
|
||
<div class="modal" id="confirmModal">
|
||
<div class="modal-header">
|
||
<h3>确认删除</h3>
|
||
<button class="modal-close" onclick="closeModal('confirmModal')">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p id="confirmMessage">确定要删除选中的代理吗?</p>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button onclick="closeModal('confirmModal')" class="secondary">取消</button>
|
||
<button onclick="confirmDeleteProxies()" class="danger">删除</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 导入导出对话框 -->
|
||
<div class="modal-backdrop" id="importModal-backdrop"></div>
|
||
<div class="modal" id="importModal">
|
||
<div class="modal-header">
|
||
<h3>导入代理配置</h3>
|
||
<button class="modal-close" onclick="closeModal('importModal')">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label>选择JSON文件:</label>
|
||
<input type="file" id="importFile" accept=".json">
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button onclick="closeModal('importModal')" class="secondary">取消</button>
|
||
<button onclick="confirmImport()" class="primary">导入</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 全局通知区域 -->
|
||
<div id="toast-container" class="toast-container"></div>
|
||
|
||
<script>
|
||
// 全局变量
|
||
let allProxies = {};
|
||
let generalProxy = '';
|
||
let selectedProxies = new Set();
|
||
let proxiesToDelete = [];
|
||
|
||
// 初始化TokenHandling
|
||
initializeTokenHandling('authToken');
|
||
|
||
// 初始化
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// 获取代理信息
|
||
getProxyInfo();
|
||
|
||
// 监听搜索输入
|
||
document.getElementById('searchInput').addEventListener('input', filterProxies);
|
||
|
||
// 初始化多选下拉框
|
||
initMultiSelect();
|
||
|
||
// 设置键盘快捷键
|
||
setupKeyboardShortcuts();
|
||
|
||
// 设置上下文菜单
|
||
setupContextMenu();
|
||
|
||
updateStatusBar();
|
||
});
|
||
|
||
// 初始化多选下拉框
|
||
// 初始化多选下拉框
|
||
function initMultiSelect() {
|
||
// 点击下拉框显示/隐藏选项
|
||
const dropdown = document.querySelector('.multiselect-dropdown');
|
||
const options = document.querySelector('.multiselect-options');
|
||
|
||
dropdown.addEventListener('click', function () {
|
||
dropdown.classList.toggle('active');
|
||
options.classList.toggle('show');
|
||
});
|
||
|
||
// 点击外部关闭下拉框
|
||
document.addEventListener('click', function (e) {
|
||
if (!e.target.closest('.multiselect')) {
|
||
dropdown.classList.remove('active');
|
||
options.classList.remove('show');
|
||
}
|
||
});
|
||
|
||
// 监听复选框变化
|
||
const checkboxes = document.querySelectorAll('.multiselect-option input');
|
||
checkboxes.forEach(checkbox => {
|
||
checkbox.addEventListener('change', function () {
|
||
updateSelectedOptions();
|
||
filterProxies();
|
||
});
|
||
});
|
||
|
||
// 添加对整个选项区域的点击事件处理
|
||
const optionDivs = document.querySelectorAll('.multiselect-option');
|
||
optionDivs.forEach(div => {
|
||
div.addEventListener('click', function (e) {
|
||
|
||
// 获取该选项内的复选框并切换其状态
|
||
const checkbox = this.querySelector('input[type="checkbox"]');
|
||
checkbox.checked = !checkbox.checked;
|
||
|
||
// 手动触发 change 事件
|
||
const event = new Event('change');
|
||
checkbox.dispatchEvent(event);
|
||
});
|
||
});
|
||
|
||
// 初始化已选项
|
||
updateSelectedOptions();
|
||
}
|
||
// 更新已选选项显示
|
||
function updateSelectedOptions() {
|
||
const selectedContainer = document.getElementById('selectedProxyTypes');
|
||
const checkboxes = document.querySelectorAll('.multiselect-option input:checked');
|
||
|
||
let html = '';
|
||
checkboxes.forEach(checkbox => {
|
||
const label = checkbox.nextElementSibling.textContent;
|
||
html += `
|
||
<span class="selected-option">
|
||
${label}
|
||
</span>
|
||
`;
|
||
});
|
||
|
||
selectedContainer.innerHTML = html || '<span style="color: var(--text-secondary);">请选择代理类型...</span>';
|
||
}
|
||
|
||
// 检测是Mac还是其他系统,Mac使用metaKey(Command键),其他系统使用ctrlKey
|
||
function isModifierKey(e) {
|
||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||
return isMac ? e.metaKey : e.ctrlKey;
|
||
}
|
||
|
||
// 设置键盘快捷键
|
||
function setupKeyboardShortcuts() {
|
||
document.addEventListener('keydown', function (e) {
|
||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
|
||
return;
|
||
}
|
||
|
||
switch (e.key) {
|
||
case 'Delete':
|
||
e.preventDefault();
|
||
deleteSelectedProxies();
|
||
break;
|
||
case 'F5':
|
||
e.preventDefault();
|
||
getProxyInfo();
|
||
break;
|
||
case 'g':
|
||
if (isModifierKey(e)) {
|
||
e.preventDefault();
|
||
setAsGeneral();
|
||
}
|
||
break;
|
||
case 'c':
|
||
if (isModifierKey(e)) {
|
||
e.preventDefault();
|
||
copyProxyUrl();
|
||
}
|
||
break;
|
||
case 'Escape':
|
||
closeAllModals();
|
||
break;
|
||
}
|
||
});
|
||
}
|
||
|
||
// 设置上下文菜单
|
||
function setupContextMenu() {
|
||
const proxyList = document.querySelector('.proxy-list');
|
||
const contextMenu = document.getElementById('contextMenu');
|
||
|
||
proxyList.addEventListener('contextmenu', function (e) {
|
||
// 获取点击的行
|
||
const row = e.target.closest('tr');
|
||
|
||
// 如果没有行或者是表头行(thead中的tr)则不显示菜单
|
||
if (!row || row.closest('thead')) return;
|
||
|
||
// 确保行有数据属性,这是数据行的标识
|
||
if (!row.dataset.name) return;
|
||
|
||
e.preventDefault();
|
||
|
||
const proxyName = row.dataset.name;
|
||
|
||
if (!selectedProxies.has(proxyName)) {
|
||
clearSelection();
|
||
selectProxy(proxyName);
|
||
}
|
||
|
||
// 显示菜单并智能定位
|
||
contextMenu.style.display = 'block';
|
||
positionContextMenu(contextMenu, e.pageX, e.pageY);
|
||
});
|
||
|
||
document.addEventListener('click', function () {
|
||
contextMenu.style.display = 'none';
|
||
});
|
||
|
||
// 阻止右键菜单内部点击导致菜单关闭
|
||
contextMenu.addEventListener('click', function (e) {
|
||
e.stopPropagation();
|
||
});
|
||
}
|
||
|
||
// 改进菜单位置计算,确保考虑滚动位置
|
||
function positionContextMenu(menu, x, y) {
|
||
// 确保菜单可见以获取正确的尺寸
|
||
menu.style.display = 'block';
|
||
menu.style.visibility = 'hidden';
|
||
|
||
const windowWidth = window.innerWidth;
|
||
const windowHeight = window.innerHeight;
|
||
const scrollY = window.scrollY || document.documentElement.scrollTop;
|
||
const scrollX = window.scrollX || document.documentElement.scrollLeft;
|
||
|
||
const menuWidth = menu.offsetWidth;
|
||
const menuHeight = menu.offsetHeight;
|
||
|
||
// 计算坐标,考虑窗口边界和滚动位置
|
||
let adjustedX = x;
|
||
if (x + menuWidth > windowWidth + scrollX - 10) {
|
||
adjustedX = windowWidth + scrollX - menuWidth - 10;
|
||
}
|
||
|
||
let adjustedY = y;
|
||
if (y + menuHeight > windowHeight + scrollY - 10) {
|
||
adjustedY = windowHeight + scrollY - menuHeight - 10;
|
||
}
|
||
|
||
// 应用计算后的位置
|
||
menu.style.left = Math.max(scrollX + 10, adjustedX) + 'px';
|
||
menu.style.top = Math.max(scrollY + 10, adjustedY) + 'px';
|
||
menu.style.visibility = 'visible';
|
||
}
|
||
|
||
// 获取代理信息
|
||
async function getProxyInfo() {
|
||
showToast('正在加载代理列表...', 'info');
|
||
|
||
const data = await makeAuthenticatedRequest('/proxies/get');
|
||
|
||
if (data && data.proxies) {
|
||
allProxies = data.proxies.proxies || {};
|
||
generalProxy = data.proxies.general || '';
|
||
renderProxies();
|
||
updateStatusBar();
|
||
showToast('代理列表已更新', 'success');
|
||
} else {
|
||
showToast('获取代理列表失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 渲染代理列表
|
||
function renderProxies() {
|
||
const proxyListBody = document.getElementById('proxyListBody');
|
||
const emptyState = document.getElementById('emptyState');
|
||
|
||
const proxyEntries = Object.entries(allProxies);
|
||
|
||
if (proxyEntries.length === 0) {
|
||
proxyListBody.innerHTML = '';
|
||
emptyState.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
emptyState.style.display = 'none';
|
||
|
||
proxyListBody.innerHTML = proxyEntries.map(([name, value]) => {
|
||
const isGeneral = name === generalProxy;
|
||
const type = getProxyType(value);
|
||
const url = (type === 'non' || type === 'sys') ? '-' : value;
|
||
|
||
return `
|
||
<tr data-name="${name}" class="${selectedProxies.has(name) ? 'selected' : ''} ${isGeneral ? 'general' : ''}">
|
||
<td>
|
||
<span class="file-icon">🌐</span>
|
||
${name}${isGeneral ? ' (通用)' : ''}
|
||
</td>
|
||
<td>${formatProxyType(type)}</td>
|
||
<td>${url}</td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
|
||
// 添加点击事件处理
|
||
const rows = document.querySelectorAll('#proxyListBody tr');
|
||
rows.forEach(row => {
|
||
row.addEventListener('click', function (e) {
|
||
const name = this.dataset.name;
|
||
|
||
if (isModifierKey(e)) {
|
||
if (selectedProxies.has(name)) {
|
||
deselectProxy(name);
|
||
} else {
|
||
selectProxy(name, true);
|
||
}
|
||
} else {
|
||
clearSelection();
|
||
selectProxy(name);
|
||
}
|
||
|
||
updateSelectionStatus();
|
||
});
|
||
});
|
||
}
|
||
|
||
// 获取代理类型
|
||
function getProxyType(value) {
|
||
if (value === 'non') return 'non';
|
||
if (value === 'sys') return 'sys';
|
||
try {
|
||
const proxyUrl = new URL(value);
|
||
const protocol = proxyUrl.protocol.replace(':', '');
|
||
return protocol;
|
||
} catch (e) {
|
||
return 'unknown';
|
||
}
|
||
}
|
||
|
||
// 格式化代理类型
|
||
function formatProxyType(type) {
|
||
const types = {
|
||
'non': '不使用代理',
|
||
'sys': '系统代理',
|
||
'http': 'HTTP',
|
||
'https': 'HTTPS',
|
||
'socks4': 'SOCKS4',
|
||
'socks5': 'SOCKS5',
|
||
'socks5h': 'SOCKS5H',
|
||
'unknown': '未知'
|
||
};
|
||
return types[type] || type;
|
||
}
|
||
|
||
// 验证代理地址格式
|
||
function validateProxyUrl(url, type) {
|
||
if (type === 'non' || type === 'sys') return true;
|
||
|
||
try {
|
||
const proxyUrl = new URL(url);
|
||
const protocol = proxyUrl.protocol.replace(':', '');
|
||
|
||
// 检查协议是否合法
|
||
const validProtocols = ['http', 'https', 'socks4', 'socks5', 'socks5h'];
|
||
if (!validProtocols.includes(protocol)) {
|
||
showToast(`不支持的代理协议: ${protocol}`, 'warning');
|
||
return false;
|
||
}
|
||
|
||
// 检查主机和端口
|
||
if (!proxyUrl.hostname || !proxyUrl.port) {
|
||
showToast('代理地址必须包含主机名和端口号', 'warning');
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
} catch (e) {
|
||
showToast('代理地址格式无效', 'warning');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 筛选代理
|
||
function filterProxies() {
|
||
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
||
const selectedTypes = Array.from(document.querySelectorAll('.multiselect-option input:checked')).map(cb => cb.value);
|
||
|
||
const filteredProxies = Object.entries(allProxies).filter(([name, value]) => {
|
||
const type = getProxyType(value);
|
||
const nameMatch = name.toLowerCase().includes(searchTerm);
|
||
const typeMatch = selectedTypes.includes(type);
|
||
|
||
return nameMatch && typeMatch;
|
||
});
|
||
|
||
const filteredObj = Object.fromEntries(filteredProxies);
|
||
renderFilteredProxies(filteredObj);
|
||
updateStatusBar(filteredProxies.length);
|
||
}
|
||
|
||
// 渲染筛选后的代理
|
||
function renderFilteredProxies(proxies) {
|
||
const temp = allProxies;
|
||
allProxies = proxies;
|
||
renderProxies();
|
||
allProxies = temp;
|
||
}
|
||
|
||
// 更新状态栏
|
||
function updateStatusBar(filteredCount) {
|
||
const total = Object.keys(allProxies).length;
|
||
const selected = selectedProxies.size;
|
||
const selectionStatus = document.getElementById('selectionStatus');
|
||
const totalCount = document.getElementById('totalCount');
|
||
|
||
if (selectionStatus) selectionStatus.textContent = `已选择: ${selected} 个项目`;
|
||
if (totalCount) totalCount.textContent = `共 ${filteredCount || total} 个代理`;
|
||
}
|
||
|
||
// 选择代理
|
||
function selectProxy(name, append = false) {
|
||
if (!name) return;
|
||
|
||
if (!append) {
|
||
clearSelection();
|
||
}
|
||
|
||
selectedProxies.add(name);
|
||
const row = document.querySelector(`tr[data-name="${name}"]`);
|
||
if (row) {
|
||
row.classList.add('selected');
|
||
}
|
||
|
||
updateSelectionStatus();
|
||
}
|
||
|
||
// 取消选择代理
|
||
function deselectProxy(name) {
|
||
if (!name) return;
|
||
|
||
selectedProxies.delete(name);
|
||
const row = document.querySelector(`tr[data-name="${name}"]`);
|
||
if (row) {
|
||
row.classList.remove('selected');
|
||
}
|
||
|
||
updateSelectionStatus();
|
||
}
|
||
|
||
// 清除所有选择
|
||
function clearSelection() {
|
||
selectedProxies.clear();
|
||
const rows = document.querySelectorAll('#proxyListBody tr.selected');
|
||
rows.forEach(row => {
|
||
row.classList.remove('selected');
|
||
});
|
||
|
||
updateSelectionStatus();
|
||
}
|
||
|
||
// 更新选择状态
|
||
function updateSelectionStatus() {
|
||
updateStatusBar();
|
||
}
|
||
|
||
// 添加代理
|
||
function addProxy() {
|
||
document.getElementById('proxyName').value = '';
|
||
document.getElementById('proxyType').value = 'custom';
|
||
document.getElementById('proxyUrl').value = '';
|
||
toggleProxyUrl();
|
||
showModal('addProxyModal');
|
||
}
|
||
|
||
// 切换代理地址输入框显示
|
||
function toggleProxyUrl() {
|
||
const type = document.getElementById('proxyType').value;
|
||
const urlGroup = document.getElementById('proxyUrlGroup');
|
||
urlGroup.style.display = type === 'custom' ? 'block' : 'none';
|
||
}
|
||
|
||
// 确认添加代理
|
||
async function confirmAddProxy() {
|
||
const name = document.getElementById('proxyName').value.trim();
|
||
const type = document.getElementById('proxyType').value;
|
||
const url = document.getElementById('proxyUrl').value.trim();
|
||
|
||
if (!name) {
|
||
showToast('请输入代理名称', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (name === 'no' || name === 'system' || name === 'default') {
|
||
showToast('不能使用预留的代理名称', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (type === 'custom') {
|
||
if (!url) {
|
||
showToast('请输入代理地址', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (!validateProxyUrl(url, type)) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
closeModal('addProxyModal');
|
||
showToast('正在添加代理...', 'info');
|
||
|
||
const proxy = {
|
||
[name]: type === 'non' || type === 'sys' ? type : url
|
||
};
|
||
|
||
const data = await makeAuthenticatedRequest('/proxies/add', {
|
||
body: JSON.stringify({
|
||
proxies: proxy
|
||
})
|
||
});
|
||
|
||
if (data) {
|
||
showToast(data.message, 'success');
|
||
getProxyInfo();
|
||
} else {
|
||
showToast('添加代理失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 删除选中的代理
|
||
function deleteSelectedProxies() {
|
||
if (selectedProxies.size === 0) {
|
||
showToast('请先选择要删除的代理', 'info');
|
||
return;
|
||
}
|
||
|
||
proxiesToDelete = [...selectedProxies];
|
||
document.getElementById('confirmMessage').textContent =
|
||
proxiesToDelete.length > 1 ? `确定要删除选中的 ${proxiesToDelete.length} 个代理吗?` : '确定要删除选中的代理吗?';
|
||
|
||
closeContextMenu();
|
||
showModal('confirmModal');
|
||
|
||
}
|
||
|
||
// 确认删除代理
|
||
async function confirmDeleteProxies() {
|
||
if (proxiesToDelete.length === 0) return;
|
||
|
||
closeModal('confirmModal');
|
||
showToast('正在删除代理...', 'info');
|
||
|
||
const data = await makeAuthenticatedRequest('/proxies/delete', {
|
||
body: JSON.stringify({
|
||
names: proxiesToDelete,
|
||
expectation: 'detailed'
|
||
})
|
||
});
|
||
|
||
if (data) {
|
||
const failedCount = data.failed_names?.length || 0;
|
||
const successCount = proxiesToDelete.length - failedCount;
|
||
const message = `删除成功: ${successCount} 个代理${failedCount ? `,失败: ${failedCount} 个` : ''}`;
|
||
|
||
showToast(message, 'success');
|
||
clearSelection();
|
||
getProxyInfo();
|
||
} else {
|
||
showToast('删除代理失败', 'error');
|
||
}
|
||
|
||
proxiesToDelete = [];
|
||
}
|
||
|
||
// 设置为通用代理
|
||
async function setAsGeneral() {
|
||
if (selectedProxies.size !== 1) {
|
||
showToast('请选择一个代理设为通用代理', 'info');
|
||
return;
|
||
}
|
||
|
||
closeContextMenu();
|
||
|
||
const name = [...selectedProxies][0];
|
||
showToast('正在设置通用代理...', 'info');
|
||
|
||
const data = await makeAuthenticatedRequest('/proxies/set-general', {
|
||
body: JSON.stringify({
|
||
name: name
|
||
})
|
||
});
|
||
|
||
if (data) {
|
||
showToast('通用代理设置成功', 'success');
|
||
getProxyInfo();
|
||
} else {
|
||
showToast('设置通用代理失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 关闭右键菜单
|
||
function closeContextMenu() {
|
||
const contextMenu = document.getElementById('contextMenu');
|
||
contextMenu.style.display = 'none';
|
||
}
|
||
|
||
// 复制代理地址
|
||
function copyProxyUrl() {
|
||
if (selectedProxies.size === 0) {
|
||
showToast('请先选择代理', 'info');
|
||
return;
|
||
}
|
||
|
||
const name = [...selectedProxies][0];
|
||
const value = allProxies[name];
|
||
|
||
closeContextMenu();
|
||
|
||
if (typeof value === 'string' && value !== 'non' && value !== 'sys') {
|
||
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
||
navigator.clipboard.writeText(value)
|
||
.then(() => {
|
||
showToast('代理地址已复制到剪贴板', 'success');
|
||
})
|
||
.catch(err => {
|
||
// 如果剪贴板API失败,使用备用方法
|
||
fallbackCopy(value);
|
||
});
|
||
} else {
|
||
fallbackCopy(value);
|
||
}
|
||
} else {
|
||
showToast('没有可复制的代理地址', 'warning');
|
||
}
|
||
}
|
||
|
||
// 备用复制方法
|
||
function fallbackCopy(text) {
|
||
// 创建一个临时文本区域
|
||
const textArea = document.createElement('textarea');
|
||
textArea.value = text;
|
||
|
||
// 设置文本区域的样式,使其不可见
|
||
textArea.style.position = 'fixed';
|
||
textArea.style.left = '-999999px';
|
||
textArea.style.top = '-999999px';
|
||
document.body.appendChild(textArea);
|
||
|
||
// 选择文本并复制
|
||
textArea.focus();
|
||
textArea.select();
|
||
|
||
let success = false;
|
||
try {
|
||
// 执行复制命令
|
||
success = document.execCommand('copy');
|
||
} catch (err) {
|
||
console.error('复制失败:', err);
|
||
}
|
||
|
||
// 移除临时元素
|
||
document.body.removeChild(textArea);
|
||
|
||
// 显示复制结果
|
||
if (success) {
|
||
showToast('代理地址已复制到剪贴板', 'success');
|
||
} else {
|
||
showToast('复制失败,请手动复制', 'error');
|
||
}
|
||
}
|
||
|
||
// 导出代理配置
|
||
function exportProxies() {
|
||
const config = {
|
||
proxies: allProxies,
|
||
general: generalProxy
|
||
};
|
||
|
||
const jsonData = JSON.stringify(config, null, 2);
|
||
const blob = new Blob([jsonData], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = 'proxies_export.json';
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
|
||
showToast('代理配置已导出', 'success');
|
||
}
|
||
|
||
// 导入代理配置
|
||
function importProxies() {
|
||
showModal('importModal');
|
||
}
|
||
|
||
// 确认导入
|
||
async function confirmImport() {
|
||
const fileInput = document.getElementById('importFile');
|
||
|
||
if (!fileInput.files || fileInput.files.length === 0) {
|
||
showToast('请选择要导入的文件', 'warning');
|
||
return;
|
||
}
|
||
|
||
const file = fileInput.files[0];
|
||
const reader = new FileReader();
|
||
|
||
reader.onload = async function (e) {
|
||
try {
|
||
const config = JSON.parse(e.target.result);
|
||
|
||
closeModal('importModal');
|
||
showToast('正在导入代理配置...', 'info');
|
||
|
||
const data = await makeAuthenticatedRequest('/proxies/update', {
|
||
body: JSON.stringify({
|
||
proxies: config
|
||
})
|
||
});
|
||
|
||
if (data) {
|
||
showToast('代理配置导入成功', 'success');
|
||
fileInput.value = '';
|
||
getProxyInfo();
|
||
}
|
||
} catch (error) {
|
||
showToast('导入失败: ' + error.message, 'error');
|
||
}
|
||
};
|
||
|
||
reader.readAsText(file);
|
||
}
|
||
|
||
// 显示对话框
|
||
function showModal(modalId) {
|
||
const modal = document.getElementById(modalId);
|
||
const backdrop = document.getElementById(`${modalId}-backdrop`);
|
||
|
||
if (modal && backdrop) {
|
||
modal.style.display = 'block';
|
||
backdrop.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
// 关闭对话框
|
||
function closeModal(modalId) {
|
||
const modal = document.getElementById(modalId);
|
||
const backdrop = document.getElementById(`${modalId}-backdrop`);
|
||
|
||
if (modal && backdrop) {
|
||
modal.style.display = 'none';
|
||
backdrop.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// 关闭所有对话框
|
||
function closeAllModals() {
|
||
const modals = document.querySelectorAll('.modal');
|
||
const backdrops = document.querySelectorAll('.modal-backdrop');
|
||
|
||
modals.forEach(modal => {
|
||
modal.style.display = 'none';
|
||
});
|
||
|
||
backdrops.forEach(backdrop => {
|
||
backdrop.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
// 显示通知消息
|
||
function showToast(message, type = 'info', duration = 3000) {
|
||
// 获取或创建通知容器
|
||
let container = document.getElementById('toast-container');
|
||
if (!container) {
|
||
container = document.createElement('div');
|
||
container.id = 'toast-container';
|
||
container.className = 'toast-container';
|
||
document.body.appendChild(container);
|
||
}
|
||
|
||
// 创建新的通知元素
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast ${type}`;
|
||
|
||
// 添加文本容器
|
||
toast.innerHTML = `<div class="toast-content">${message}</div>`;
|
||
|
||
// 添加到容器
|
||
container.appendChild(toast);
|
||
|
||
// 强制一次重排,确保过渡动画生效
|
||
void toast.offsetWidth;
|
||
|
||
// 显示通知,使用requestAnimationFrame确保平滑过渡
|
||
requestAnimationFrame(() => {
|
||
toast.classList.add('show');
|
||
});
|
||
|
||
// 设置自动移除
|
||
setTimeout(() => {
|
||
toast.classList.remove('show');
|
||
|
||
// 通知消失动画完成后移除元素
|
||
setTimeout(() => {
|
||
if (container.contains(toast)) {
|
||
container.removeChild(toast);
|
||
}
|
||
|
||
// 如果没有更多通知,移除容器
|
||
if (container.children.length === 0) {
|
||
if (document.body.contains(container)) {
|
||
document.body.removeChild(container);
|
||
}
|
||
}
|
||
}, 400);
|
||
}, duration);
|
||
|
||
// 限制最大显示数量,如果超过则移除最早的
|
||
const maxToasts = 5; // 最大同时显示的通知数
|
||
const toasts = container.querySelectorAll('.toast');
|
||
if (toasts.length > maxToasts) {
|
||
const oldestToast = toasts[0]; // 最早的通知
|
||
oldestToast.classList.remove('show');
|
||
setTimeout(() => {
|
||
if (container.contains(oldestToast)) {
|
||
container.removeChild(oldestToast);
|
||
}
|
||
}, 400);
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
|
||
</html> |