Files
cursor-api/static/proxies.html
2025-03-05 04:21:37 +08:00

1307 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>