Files
ProxyCat/web/templates/index.html
T
2025-03-03 14:43:25 +08:00

3160 lines
127 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n="page_title">ProxyCat 控制面板</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/css/font-awesome.min.css" rel="stylesheet">
<link href="/static/css/base.css" rel="stylesheet">
<link href="/static/css/status-card.css" rel="stylesheet">
<link href="/static/css/progress.css" rel="stylesheet">
<link href="/static/css/logs.css" rel="stylesheet">
<link href="/static/css/dark-mode.css" rel="stylesheet">
<link href="/static/css/buttons.css" rel="stylesheet">
<link href="/static/css/forms.css" rel="stylesheet">
<link href="/static/css/nav-tabs.css" rel="stylesheet">
<link href="/static/css/search.css" rel="stylesheet">
<link href="/static/css/animations.css" rel="stylesheet">
<link href="/static/css/responsive.css" rel="stylesheet">
<link href="/static/css/service-control.css" rel="stylesheet">
<style>
.btn {
border-radius: 6px;
padding: 8px 16px;
font-weight: 500;
transition: all 0.2s;
}
.btn:hover {
transform: translateY(-1px);
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-success {
background-color: var(--secondary-color);
border-color: var(--secondary-color);
}
.btn-warning {
background-color: var(--warning-color);
border-color: var(--warning-color);
}
.btn-danger {
background-color: var(--danger-color);
border-color: var(--danger-color);
}
.nav-tabs {
border-bottom: 2px solid #dee2e6;
margin-bottom: 20px;
}
.nav-tabs .nav-link {
border: none;
color: #6c757d;
padding: 10px 20px;
font-weight: 500;
transition: all 0.2s;
}
.nav-tabs .nav-link:hover {
border: none;
color: var(--primary-color);
}
.nav-tabs .nav-link.active {
border: none;
color: var(--primary-color);
position: relative;
}
.nav-tabs .nav-link.active:after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background-color: var(--primary-color);
}
.log-container {
height: 500px;
position: relative;
margin: 1rem 0;
}
.log-search {
position: relative;
margin-bottom: 20px;
}
.log-search input {
padding: 12px 45px;
border-radius: 25px;
font-size: 15px;
font-weight: 500;
border: 2px solid #e9ecef;
transition: all 0.3s ease;
}
.log-search input:focus {
border-color: #4a90e2;
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.2);
}
.log-search .clear-search {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #999;
cursor: pointer;
padding: 5px;
display: none;
}
.log-search .clear-search:hover {
color: var(--danger-color);
}
@keyframes slideIn {
from { transform: translateY(-20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.card {
transition: all 0.3s ease;
border: none;
border-radius: 15px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
animation: slideIn 0.5s ease-out;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2);
}
.status-card {
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
min-height: 160px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.status-card .card-title {
color: #2c3e50;
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 15px;
}
.status-card .card-text {
color: #34495e;
font-size: 1.2rem;
margin-bottom: 15px;
}
.table {
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
}
.table thead th {
background: #f8f9fa;
border-bottom: 2px solid #dee2e6;
color: #2c3e50;
font-weight: 600;
padding: 15px;
}
.table tbody td {
padding: 12px 15px;
vertical-align: middle;
border-bottom: 1px solid #eee;
}
.form-control, .form-select {
border-radius: 8px;
border: 2px solid #e9ecef;
padding: 10px 15px;
transition: all 0.3s ease;
}
.form-control:focus, .form-select:focus {
border-color: #4a90e2;
box-shadow: 0 0 0 0.2rem rgba(74, 144, 226, 0.25);
}
.log-container {
height: 500px;
background: #fff;
border-radius: 10px;
padding: 20px;
border: 2px solid #e9ecef;
overflow-y: auto;
}
.log-entry {
padding: 10px 15px;
border-radius: 6px;
margin-bottom: 10px;
font-family: 'Monaco', 'Menlo', monospace;
transition: all 0.2s ease;
}
.log-search {
position: relative;
margin-bottom: 20px;
}
.log-search input {
padding-right: 40px;
padding-left: 40px;
border-radius: 25px;
}
.log-search i {
position: absolute;
left: 15px;
top: 50%;
transform: translateY(-50%);
color: #6c757d;
}
.log-search .clear-search {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #6c757d;
cursor: pointer;
padding: 5px;
transition: all 0.2s ease;
}
.log-search .clear-search:hover {
color: #dc3545;
transform: translateY(-50%) scale(1.1);
}
@media (max-width: 768px) {
.status-card {
margin-bottom: 20px;
}
.btn {
width: 100%;
margin-bottom: 10px;
}
.table-responsive {
margin-bottom: 20px;
}
.log-container {
height: 300px;
}
}
@media (prefers-color-scheme: dark) {
body {
background-color: #1a1a1a;
color: #ffffff;
}
.card {
background-color: #2d2d2d;
color: #ffffff;
}
.table {
background-color: #2d2d2d;
color: #ffffff;
}
.table thead th {
background-color: #3d3d3d;
color: #ffffff;
}
.form-control, .form-select {
background-color: #2d2d2d;
color: #ffffff;
border-color: #3d3d3d;
}
}
/* 添加新的动画效果 */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 改进卡片样式 */
.status-card {
animation: fadeInUp 0.5s ease;
background: linear-gradient(145deg, #ffffff, #f0f0f0);
border: none;
transition: all 0.3s ease;
}
.status-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
}
/* 改进按钮样式 */
.btn {
position: relative;
overflow: hidden;
z-index: 1;
}
.btn::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255,255,255,0.1);
transform: scaleX(0);
transform-origin: right;
transition: transform 0.3s ease;
z-index: -1;
}
.btn:hover::after {
transform: scaleX(1);
transform-origin: left;
}
/* 改进日志容器样式 */
.log-container {
background: linear-gradient(to bottom right, #ffffff, #f8f9fa);
border: 1px solid rgba(0,0,0,0.1);
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
}
.log-entry {
padding: 10px 15px;
border-radius: 6px;
margin-bottom: 10px;
font-family: 'Monaco', 'Menlo', monospace;
}
/* 暗色模式优化 */
@media (prefers-color-scheme: dark) {
body {
background: #1a1a1a;
color: #e0e0e0;
}
.status-card {
background: linear-gradient(145deg, #2d2d2d, #252525);
}
.card {
background-color: #2d2d2d;
border: 1px solid #3d3d3d;
}
.log-container {
background: linear-gradient(to bottom right, #2d2d2d, #252525);
border-color: #3d3d3d;
}
.form-control, .form-select {
background-color: #2d2d2d;
border-color: #3d3d3d;
color: #e0e0e0;
}
.table {
color: #e0e0e0;
}
.table thead th {
background-color: #252525;
border-bottom-color: #3d3d3d;
}
}
/* 响应式布局优化 */
@media (max-width: 768px) {
.status-card {
margin-bottom: 20px;
}
.btn-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.btn-group .btn {
width: 100%;
margin: 0;
}
.log-container {
height: 350px;
}
}
/* 添加卡片阴影过渡效果 */
.card {
box-shadow: 0 2px 15px rgba(0,0,0,0.05);
transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
}
.card:hover {
box-shadow: 0 5px 25px rgba(0,0,0,0.1);
transform: translateY(-3px);
}
/* 美化进度条 */
.progress {
height: 10px;
background: linear-gradient(to right, #f0f0f0, #e0e0e0);
border-radius: 10px;
overflow: hidden;
}
.progress-bar {
background: linear-gradient(45deg, #4a90e2, #357abd);
box-shadow: 0 0 10px rgba(74, 144, 226, 0.5);
transition: width 0.5s ease;
}
/* 添加波纹效果 */
.ripple {
position: relative;
overflow: hidden;
}
.ripple:after {
content: "";
display: block;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
pointer-events: none;
background-image: radial-gradient(circle, #fff 10%, transparent 10.01%);
background-repeat: no-repeat;
background-position: 50%;
transform: scale(10, 10);
opacity: 0;
transition: transform .5s, opacity 1s;
}
.ripple:active:after {
transform: scale(0, 0);
opacity: .3;
transition: 0s;
}
/* 美化表单元素 */
.form-control, .form-select {
border-radius: 8px;
border: 2px solid #e9ecef;
padding: 12px 15px;
transition: all 0.3s ease;
font-size: 0.95rem;
}
.form-control:focus, .form-select:focus {
border-color: #4a90e2;
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.15);
}
/* 添加标签悬停效果 */
.nav-tabs .nav-link {
position: relative;
transition: all 0.3s ease;
}
.nav-tabs .nav-link:before {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background-color: #4a90e2;
transform: scaleX(0);
transition: transform 0.3s ease;
}
.nav-tabs .nav-link:hover:before {
transform: scaleX(0.5);
}
.nav-tabs .nav-link.active:before {
transform: scaleX(1);
}
/* 添加加载动画 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: none;
justify-content: center;
align-items: center;
z-index: 9999;
backdrop-filter: blur(5px);
}
.loading-spinner {
width: 50px;
height: 50px;
border: 5px solid #f3f3f3;
border-top: 5px solid #4a90e2;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 美化滚动条 */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 5px;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 5px;
border: 2px solid #f1f1f1;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.loading-overlay {
background: rgba(0, 0, 0, 0.8);
}
.loading-spinner {
border-color: #333;
border-top-color: #4a90e2;
}
::-webkit-scrollbar-track {
background: #2d2d2d;
}
::-webkit-scrollbar-thumb {
background: #666;
border-color: #2d2d2d;
}
::-webkit-scrollbar-thumb:hover {
background: #888;
}
.progress {
background: linear-gradient(to right, #2d2d2d, #252525);
}
}
/* 添加切换动画 */
.tab-pane {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* 美化表格 */
.table {
border-collapse: separate;
border-spacing: 0;
border-radius: 10px;
overflow: hidden;
}
.table th {
background: linear-gradient(to bottom, #f8f9fa, #f0f0f0);
border: none;
padding: 15px;
font-weight: 600;
}
.table td {
padding: 12px 15px;
border-top: 1px solid #dee2e6;
transition: all 0.2s ease;
}
.table tr:hover td {
background-color: rgba(74, 144, 226, 0.05);
}
/* 添加新的渐变背景效果 */
body {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
min-height: 100vh;
}
/* 优化卡片容器,防止跳动 */
.container {
padding: 2rem 1rem;
min-height: calc(100vh - 4rem);
}
/* 改进标题样式 */
.page-title {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(45deg, #4a90e2, #357abd);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
text-align: center;
margin-bottom: 2rem;
}
/* 优化状态卡片,固定高度防止跳动 */
.status-card {
min-height: 180px;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 1.5rem;
margin-bottom: 1rem;
background: linear-gradient(145deg, #ffffff, #f8f9fa);
}
/* 改进按钮组样式 */
.btn-group {
display: inline-flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.btn-group .btn {
flex: 1;
min-width: 120px;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
/* 添加图标悬停效果 */
.btn i {
transition: transform 0.3s ease;
}
.btn:hover i {
transform: scale(1.2);
}
/* 优化表单布局 */
.form-group {
margin-bottom: 1.5rem;
position: relative;
}
.form-label {
font-weight: 600;
margin-bottom: 0.5rem;
display: block;
}
/* 添加输入框图标 */
.form-icon {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: #6c757d;
pointer-events: none;
}
.form-icon + .form-control {
padding-left: 2.5rem;
}
/* 优化日志容器,防止跳动 */
.log-container {
height: 500px;
position: relative;
margin: 1rem 0;
}
.log-entry {
margin: 0.5rem 0;
padding: 1rem;
border-radius: 0.5rem;
position: relative;
overflow: hidden;
}
/* 暗色模式优化 */
@media (prefers-color-scheme: dark) {
body {
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
}
.page-title {
background: linear-gradient(45deg, #4a90e2, #64b5f6);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.status-card {
background: linear-gradient(145deg, #2d2d2d, #252525);
}
/* 改进暗色模式下的阴影效果 */
.card {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
.card:hover {
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
}
}
/* 添加平滑过渡效果 */
.smooth-transition {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 优化表格响应式布局 */
.table-responsive {
margin: 1rem 0;
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.05);
}
/* 添加卡片内容过渡效果 */
.card-content {
opacity: 1;
transform: translateY(0);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.card-content.loading {
opacity: 0.6;
transform: translateY(10px);
}
/* 优化加载动画容器 */
.loading-overlay {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(4px);
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.loading-overlay.show {
opacity: 1;
visibility: visible;
}
.log-message {
font-weight: 600; /* 为日志消息内容添加加粗效果 */
line-height: 1.5; /* 增加行高提高可读性 */
}
/* 进度条容器样式 */
.progress-container {
margin-top: auto; /* 将进度条推到底部 */
}
.progress {
height: 8px;
border-radius: 4px;
background: linear-gradient(to right, #f0f0f0, #e0e0e0);
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.status-card {
background: linear-gradient(145deg, #2d2d2d, #252525);
}
.status-card .card-title {
color: #e0e0e0;
}
.status-card .card-text {
color: #ffffff;
}
.progress {
background: linear-gradient(to right, #2d2d2d, #252525);
}
}
/* 只保留加载相关的样式 */
.loading-overlay {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(4px);
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.loading-overlay.show {
opacity: 1;
visibility: visible;
}
.card-content {
opacity: 1;
transform: translateY(0);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.card-content.loading {
opacity: 0.6;
transform: translateY(10px);
}
/* 暗色模式下的加载遮罩层 */
@media (prefers-color-scheme: dark) {
.loading-overlay {
background: rgba(0, 0, 0, 0.7);
}
}
/* 服务控制卡片样式 */
.service-control {
padding: 1.5rem;
background: linear-gradient(145deg, #ffffff, #f8f9fa);
border-radius: 15px;
transition: all 0.3s ease;
}
.service-title {
font-size: 1.5rem;
font-weight: 600;
color: #2c3e50;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
}
.service-description {
font-size: 0.95rem;
color: #6c757d;
}
.service-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.service-btn {
position: relative;
flex: 1;
min-width: 140px;
padding: 0.8rem 1.5rem;
border: none;
border-radius: 12px;
font-weight: 500;
font-size: 1rem;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-content {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
color: white;
}
.btn-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
transition: transform 0.3s ease;
}
.start-btn {
background: #28a745;
}
.start-btn .btn-bg {
background: linear-gradient(45deg, #2ecc71, #27ae60);
}
.stop-btn {
background: #dc3545;
}
.stop-btn .btn-bg {
background: linear-gradient(45deg, #e74c3c, #c0392b);
}
.restart-btn {
background: #ffc107;
}
.restart-btn .btn-bg {
background: linear-gradient(45deg, #f1c40f, #f39c12);
}
.service-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.service-btn:hover .btn-bg {
transform: scale(1.1);
}
.service-btn:active {
transform: translateY(1px);
}
/* 添加按钮图标动画 */
.service-btn i {
transition: transform 0.3s ease;
}
.service-btn:hover i {
transform: scale(1.2);
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.service-control {
background: linear-gradient(145deg, #2d2d2d, #252525);
}
.service-title {
color: #e0e0e0;
}
.service-description {
color: #a0a0a0;
}
.start-btn {
background: #1a472a;
}
.start-btn .btn-bg {
background: linear-gradient(45deg, #2ecc71, #27ae60);
opacity: 0.9;
}
.stop-btn {
background: #5c1e1e;
}
.stop-btn .btn-bg {
background: linear-gradient(45deg, #e74c3c, #c0392b);
opacity: 0.9;
}
.restart-btn {
background: #5c4d1a;
}
.restart-btn .btn-bg {
background: linear-gradient(45deg, #f1c40f, #f39c12);
opacity: 0.9;
}
}
/* 响应式布局优化 */
@media (max-width: 768px) {
.service-control {
padding: 1rem;
}
.service-actions {
margin-top: 1rem;
flex-direction: column;
}
.service-btn {
width: 100%;
margin: 0.5rem 0;
}
}
/* 添加加载状态样式 */
.service-btn.loading {
pointer-events: none;
opacity: 0.8;
}
.service-btn.loading i {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 添加通知样式 */
.notification-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
}
.notification {
background: #fff;
border-radius: 8px;
padding: 15px 25px;
margin-bottom: 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
display: flex;
align-items: center;
gap: 10px;
animation: slideInRight 0.3s ease-out;
max-width: 350px;
border-left: 4px solid;
}
.notification.success {
border-left-color: #2ecc71;
}
.notification.error {
border-left-color: #e74c3c;
}
.notification i {
font-size: 20px;
}
.notification.success i {
color: #2ecc71;
}
.notification.error i {
color: #e74c3c;
}
.notification-message {
color: #2c3e50;
font-size: 14px;
margin: 0;
}
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
/* 暗色模式下的通知样式 */
@media (prefers-color-scheme: dark) {
.notification {
background: #2d2d2d;
}
.notification-message {
color: #e0e0e0;
}
}
</style>
</head>
<body>
<div class="container mt-4 fade-in">
<div class="row">
<div class="col-12">
<h1 class="text-center mb-4" data-i18n="title">ProxyCat 控制面板</h1>
</div>
</div>
<div class="row mb-4">
<!-- 当前代理卡片 -->
<div class="col-md-4">
<div class="card status-card">
<i class="fa fa-exchange card-icon"></i>
<div class="card-body">
<h5 class="card-title">
<i class="fa fa-random"></i>
<span data-i18n="current_proxy_title">当前代理</span>
</h5>
<p class="card-text" id="current-proxy" data-i18n="loading">加载中...</p>
<div class="button-container">
<button class="btn-sm btn-primary" onclick="switchProxy()" data-i18n="manual_switch_btn">
<i class="fa fa-refresh"></i>
<span>手动切换</span>
</button>
</div>
</div>
</div>
</div>
<!-- 服务控制卡片 -->
<div class="col-md-4">
<div class="card status-card">
<i class="fa fa-server card-icon"></i>
<div class="card-body">
<h5 class="card-title">
<i class="fa fa-power-off"></i>
<span data-i18n="service_control_title">服务控制</span>
</h5>
<div class="service-status">
<div class="status-indicator">
<span class="status-dot"></span>
<span class="status-text" data-i18n="service_running">运行中</span>
</div>
</div>
<div class="service-actions">
<button class="service-btn start-btn" onclick="controlService('start')">
<i class="fa fa-play"></i>
<span data-i18n="start_service">启动</span>
</button>
<button class="service-btn stop-btn" onclick="controlService('stop')">
<i class="fa fa-stop"></i>
<span data-i18n="stop_service">停止</span>
</button>
<button class="service-btn restart-btn" onclick="controlService('restart')">
<i class="fa fa-refresh"></i>
<span data-i18n="restart_service">重启</span>
</button>
</div>
</div>
</div>
</div>
<!-- 下次切换卡片 -->
<div class="col-md-4">
<div class="card status-card">
<i class="fa fa-clock-o card-icon"></i>
<div class="card-body">
<h5 class="card-title">
<i class="fa fa-refresh"></i>
<span data-i18n="next_switch_title">下次切换</span>
</h5>
<p class="card-text" id="next-switch" data-i18n="loading">加载中...</p>
<div class="progress-container">
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: 0%" aria-label="切换进度"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<!-- 本地监听地址卡片 -->
<div class="col-md-6">
<div class="card info-card">
<i class="fa fa-globe card-icon"></i>
<div class="card-body">
<h5 class="card-title">
<i class="fa fa-plug"></i>
<span data-i18n="local_addresses_title">本地监听地址</span>
</h5>
<div class="address-container">
<div class="address-item">
<div class="address-label">
<i class="fa fa-desktop"></i>
<span data-i18n="local_http_text">HTTP 代理</span>
</div>
<div class="address-value" id="http-address">http://127.0.0.1:1080</div>
<button class="copy-btn" onclick="copyToClipboard('http-address')">
<i class="fa fa-copy"></i>
</button>
</div>
<div class="address-item">
<div class="address-label">
<i class="fa fa-lock"></i>
<span data-i18n="local_socks5_text">SOCKS5 代理</span>
</div>
<div class="address-value" id="socks5-address">socks5://127.0.0.1:1080</div>
<button class="copy-btn" onclick="copyToClipboard('socks5-address')">
<i class="fa fa-copy"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 关于信息卡片 -->
<div class="col-md-6">
<div class="card info-card">
<i class="fa fa-info-circle card-icon"></i>
<div class="card-body">
<h5 class="card-title">
<i class="fa fa-info"></i>
<span data-i18n="about_info_title">关于信息</span>
</h5>
<div class="about-container">
<div class="about-item">
<i class="fa fa-code-fork"></i>
<div class="about-content">
<span data-i18n="version_text">当前版本:</span>
<span id="current-version">-</span>
<span id="version-status" class="ms-2"></span>
</div>
</div>
<div class="about-item">
<i class="fa fa-github"></i>
<div class="about-content d-flex align-items-center">
<span data-i18n="star_project_text">开源项目求 Star:</span>
<a href="https://github.com/honmashironeko/ProxyCat" target="_blank" class="github-link ms-1">ProxyCat</a>
</div>
</div>
<div class="about-item">
<i class="fa fa-wechat"></i>
<div class="about-content">
<span data-i18n="public_account_text">公众号:</span>
<span class="wechat-name" data-i18n="public_account_name">樱花庄的本间白猫</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#config" data-i18n="config_tab">配置设置</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#proxies" data-i18n="proxies_tab">代理管理</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#ip-lists" data-i18n="ip_lists_tab">IP名单</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#logs" data-i18n="logs_tab">运行日志</a>
</li>
</ul>
<div class="tab-content mt-3">
<!-- 配置设置标签页 -->
<div class="tab-pane fade show active" id="config">
<div class="row">
<!-- 基本配置卡片 -->
<div class="col-md-4">
<div class="card">
<i class="fa fa-cog card-icon"></i>
<div class="card-body">
<h5 class="card-title">
<i class="fa fa-wrench"></i>
<span data-i18n="basic_config_title">基本配置</span>
</h5>
<form id="basic-config-form">
<div class="mb-3">
<label class="form-label" data-i18n="port_label">端口</label>
<input type="number" class="form-control" name="port">
</div>
<div class="mb-3">
<label class="form-label" data-i18n="switch_interval_label">切换间隔(秒)</label>
<input type="number" class="form-control" name="interval">
</div>
<div class="mb-3">
<label class="form-label" data-i18n="run_mode_label">运行模式</label>
<select class="form-select" name="mode">
<option value="cycle" data-i18n="cycle_mode">循环模式</option>
<option value="load_balance" data-i18n="load_balance_mode">负载均衡</option>
</select>
</div>
<div class="mb-3">
<label class="form-label" data-i18n="language_label">语言</label>
<select class="form-select" name="language">
<option value="cn" data-i18n="chinese">中文</option>
<option value="en" data-i18n="english">English</option>
</select>
</div>
</form>
</div>
</div>
</div>
<!-- API代理设置卡片 -->
<div class="col-md-4">
<div class="card">
<i class="fa fa-cloud card-icon"></i>
<div class="card-body">
<h5 class="card-title">
<i class="fa fa-plug"></i>
<span data-i18n="api_proxy_settings_title">API代理设置</span>
</h5>
<form id="api-config-form">
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="use_getip" id="use-getip">
<label class="form-check-label" for="use-getip" data-i18n="use_api_label">使用API获取代理</label>
</div>
</div>
<div class="mb-3">
<label class="form-label" data-i18n="api_url_label">API地址</label>
<input type="text" class="form-control" name="getip_url">
</div>
<div class="mb-3">
<label class="form-label" data-i18n="proxy_auth_username_label">代理认证用户名</label>
<input type="text" class="form-control" name="proxy_username">
</div>
<div class="mb-3">
<label class="form-label" data-i18n="proxy_auth_password_label">代理认证密码</label>
<input type="text" class="form-control" name="proxy_password">
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="check_proxies" id="check-proxies">
<label class="form-check-label" for="check-proxies" data-i18n="enable_proxy_check_label">启用代理检测</label>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- 用户管理卡片 -->
<div class="col-md-4">
<div class="card">
<i class="fa fa-users card-icon"></i>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="card-title mb-0" data-i18n="user_management_title">用户管理</h5>
<button class="btn btn-sm btn-primary" onclick="addUser()" data-i18n="add_user_btn">
<i class="fa fa-plus me-1"></i>添加用户
</button>
</div>
<div class="table-responsive">
<table class="table" id="user-table">
<thead>
<tr>
<th data-i18n="username_column">用户名</th>
<th data-i18n="password_column">密码</th>
<th data-i18n="actions_column">操作</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- 保留新的浮动保存按钮 -->
<div class="floating-save-container">
<button class="btn btn-primary floating-save-btn" onclick="saveAllConfig()">
<i class="fa fa-save"></i>
<span data-i18n="save_config">保存配置</span>
</button>
</div>
</div>
<!-- 代理管理标签页 -->
<div class="tab-pane fade" id="proxies">
<div class="card">
<i class="fa fa-random card-icon"></i>
<div class="card-body">
<h5 class="card-title">
<i class="fa fa-list"></i>
<span data-i18n="proxy_management">代理管理</span>
</h5>
<div class="proxy-management">
<div class="proxy-lists">
<div class="row">
<div class="col-md-4">
<div class="proxy-list-section">
<div class="proxy-list-header">
<i class="fa fa-globe"></i>
<span>HTTP代理</span>
</div>
<textarea class="form-control" id="http-proxy-list" rows="8"
placeholder="每行一个地址,格式:ip:port&#10;例如:127.0.0.1:8080"></textarea>
</div>
</div>
<div class="col-md-4">
<div class="proxy-list-section">
<div class="proxy-list-header">
<i class="fa fa-lock"></i>
<span>HTTPS代理</span>
</div>
<textarea class="form-control" id="https-proxy-list" rows="8"
placeholder="每行一个地址,格式:ip:port&#10;例如:127.0.0.1:8443"></textarea>
</div>
</div>
<div class="col-md-4">
<div class="proxy-list-section">
<div class="proxy-list-header">
<i class="fa fa-shield"></i>
<span>SOCKS5代理</span>
</div>
<textarea class="form-control" id="socks5-proxy-list" rows="8"
placeholder="每行一个地址,格式:ip:port&#10;例如:127.0.0.1:1080"></textarea>
</div>
</div>
</div>
<div class="mt-4">
<div class="proxy-list-section">
<div class="proxy-list-header">
<i class="fa fa-code"></i>
<span data-i18n="full_proxy">完整代理地址</span>
<div class="proxy-list-help" data-i18n="proxy_help">支持完整格式的代理地址,包含协议和认证信息</div>
</div>
<textarea class="form-control" id="full-proxy-list" rows="4"
placeholder="每行一个地址,格式:protocol://[username:password@]ip:port&#10;例如:http://user:[email protected]:8080"></textarea>
</div>
</div>
</div>
<div class="mb-3 mt-4">
<label class="form-label" data-i18n="test_target_label">测试目标地址</label>
<input type="text" class="form-control" id="test-target-url" placeholder="https://www.baidu.com">
<div class="form-text" data-i18n="test_url_help">用于检测代理有效性的目标地址</div>
</div>
<div class="proxy-actions">
<button class="btn btn-primary" onclick="saveProxies()">
<i class="fa fa-save"></i>
<span data-i18n="save_proxy_btn">保存代理</span>
</button>
<button class="btn btn-warning" onclick="checkProxies()">
<i class="fa fa-check-circle"></i>
<span data-i18n="check_proxy_btn">检测代理</span>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- IP名单标签页 -->
<div class="tab-pane fade" id="ip-lists">
<div class="card">
<i class="fa fa-shield card-icon"></i>
<div class="card-body">
<h5 class="card-title">
<i class="fa fa-users"></i>
<span data-i18n="ip_lists">IP名单</span>
</h5>
<div class="ip-lists-container">
<div class="ip-lists-row">
<div class="ip-list-section">
<div class="ip-list-title" data-i18n="whitelist_title">白名单</div>
<textarea class="form-control" id="whitelist" rows="10"></textarea>
</div>
<div class="ip-list-section">
<div class="ip-list-title" data-i18n="blacklist_title">黑名单</div>
<textarea class="form-control" id="blacklist" rows="10"></textarea>
</div>
</div>
<div class="ip-auth-settings">
<div class="mb-3">
<label class="form-label" data-i18n="ip_auth_priority_label">IP认证优先级</label>
<select class="form-select" name="ip_auth_priority">
<option value="whitelist" data-i18n="whitelist_first">白名单优先</option>
<option value="blacklist" data-i18n="blacklist_first">黑名单优先</option>
</select>
</div>
<div class="ip-lists-actions">
<button class="btn btn-primary" onclick="saveIpLists()">
<i class="fa fa-save"></i>
<span data-i18n="save_list_btn">保存名单</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 运行日志标签页 -->
<div class="tab-pane fade" id="logs">
<div class="card">
<i class="fa fa-file-text card-icon"></i>
<div class="card-body">
<h5 class="card-title">
<i class="fa fa-history"></i>
<span data-i18n="run_logs">运行日志</span>
</h5>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center">
<label class="form-label me-2" data-i18n="log_level_label">日志级别</label>
<select class="form-select me-3" id="log-level" onchange="updateLogs(true)">
<option value="ALL" data-i18n="log_level_all">全部</option>
<option value="INFO" data-i18n="log_level_info">信息</option>
<option value="WARNING" data-i18n="log_level_warning">警告</option>
<option value="ERROR" data-i18n="log_level_error">错误</option>
<option value="CRITICAL" data-i18n="log_level_critical">严重错误</option>
</select>
</div>
<div class="d-flex align-items-center">
<button class="btn btn-danger" onclick="clearLogs()" data-i18n="clear_logs_btn">清除日志</button>
</div>
</div>
<div class="log-search">
<i class="fa fa-search"></i>
<input type="text" id="log-search-input" class="form-control" data-i18n-placeholder="search_logs" placeholder="搜索日志...">
<button type="button" id="clear-search" class="clear-search" style="display: none;">
<i class="fa fa-times"></i>
</button>
</div>
</div>
<div class="log-container" id="log-container"></div>
</div>
</div>
</div>
</div>
</div>
<div class="language-switch">
<button id="languageToggle" class="btn btn-outline-secondary">
<span class="cn-text">English</span>
<span class="en-text" style="display: none;">中文</span>
</button>
</div>
<div class="loading-overlay" style="display: none;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">加载中...</span>
</div>
</div>
<div class="notification-container" id="notification-container"></div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
<script>
let lastLogId = 0;
let searchTimeout = null;
const searchInput = document.getElementById('log-search-input');
const clearSearchBtn = document.getElementById('clear-search');
function getToken() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('token') || '';
}
function appendToken(url) {
const token = getToken();
const separator = url.includes('?') ? '&' : '?';
return token ? `${url}${separator}token=${token}` : url;
}
function updateAddresses(config) {
const username = config.username || '';
const password = config.password || '';
const port = config.port || '1080';
let httpAddress = `http://127.0.0.1:${port}`;
let socks5Address = `socks5://127.0.0.1:${port}`;
if (username && password) {
httpAddress = `http://${username}:${password}@127.0.0.1:${port}`;
socks5Address = `socks5://${username}:${password}@127.0.0.1:${port}`;
}
$('#http-address').text(httpAddress);
$('#socks5-address').text(socks5Address);
}
function updateStatus() {
$.get(appendToken('/api/status'), function(data) {
// 只在值发生变化时更新显示
const currentProxy = $('#current-proxy').text();
const newProxy = data.current_proxy || translations[currentLanguage].no_proxy;
if (currentProxy !== newProxy) {
$('#current-proxy').text(newProxy);
}
// 更新运行状态文本和样式
const statusText = document.querySelector('.status-text');
const statusDot = document.querySelector('.status-dot');
const status = data.service_status || 'stopped';
if (statusText) {
statusText.textContent = translations[currentLanguage].service_status[status];
}
if (statusDot) {
statusDot.classList.remove('running', 'stopped');
void statusDot.offsetWidth; // 触发重排以重置动画
statusDot.classList.add(status);
}
// 更新进度条和其他状态...
if (data.interval && data.time_left !== undefined) {
const timeLeft = Math.max(0, Math.ceil(data.time_left));
$('#next-switch').text(`${timeLeft} ${translations[currentLanguage].seconds}`);
// 更新进度条
const progress = ((data.interval - data.time_left) / data.interval) * 100;
$('.progress-bar').css('width', `${Math.min(100, progress)}%`);
}
// 更新运行模式
const modeText = translations[currentLanguage].proxy_mode[data.mode];
if ($('#proxy-mode').text() !== modeText) {
$('#proxy-mode').text(modeText);
}
// 更新按钮状态
updateServiceButtons(status);
});
}
function updateCountdown() {
$.get(appendToken('/api/status'), function(data) {
if (data.time_left !== undefined && data.interval) {
const timeLeft = Math.max(0, Math.ceil(data.time_left));
const progress = Math.min(100, ((data.interval - data.time_left) / data.interval) * 100);
$('.progress-bar').css('width', progress + '%');
$('#next-switch').text(timeLeft + ' ' + translations[currentLanguage]['seconds']);
}
});
}
function loadConfig() {
$.get(appendToken('/api/status'), function(data) {
if (data.config) {
const config = data.config;
// 更新表单值
$('input[name="port"]').val(config.port);
$('select[name="mode"]').val(config.mode);
$('input[name="interval"]').val(config.interval);
$('input[name="username"]').val(config.username);
$('input[name="password"]').val(config.password);
// 修正checkbox的状态设置
$('input[name="use_getip"]').prop('checked', config.use_getip.toLowerCase() === 'true');
$('input[name="getip_url"]').val(config.getip_url);
$('input[name="proxy_username"]').val(config.proxy_username);
$('input[name="proxy_password"]').val(config.proxy_password);
$('input[name="proxy_file"]').val(config.proxy_file);
$('input[name="check_proxies"]').prop('checked', config.check_proxies.toLowerCase() === 'true');
$('select[name="language"]').val(config.language);
$('input[name="whitelist_file"]').val(config.whitelist_file);
$('input[name="blacklist_file"]').val(config.blacklist_file);
$('select[name="ip_auth_priority"]').val(config.ip_auth_priority);
$('select[name="display_level"]').val(config.display_level);
// 根据 use_getip 的状态更新 getip_url 输入框的禁用状态
const useGetip = config.use_getip.toLowerCase() === 'true';
$('input[name="getip_url"]').prop('disabled', !useGetip);
// 更新其他相关UI元素
updateUIState();
}
});
}
function loadProxies() {
$.get(appendToken('/api/proxies'), function(data) {
const httpProxies = [];
const httpsProxies = [];
const socks5Proxies = [];
const fullProxies = [];
data.proxies.forEach(proxy => {
if (proxy.includes('@') || (proxy.includes('://') && !proxy.match(/^(http|https|socks5):\/\/[^@]+:\d+$/))) {
// 带认证信息或其他完整格式的代理放入完整代理列表
fullProxies.push(proxy);
} else if (proxy.startsWith('http://')) {
// HTTP代理
httpProxies.push(proxy.replace('http://', ''));
} else if (proxy.startsWith('https://')) {
// HTTPS代理
httpsProxies.push(proxy.replace('https://', ''));
} else if (proxy.startsWith('socks5://')) {
// SOCKS5代理
socks5Proxies.push(proxy.replace('socks5://', ''));
} else {
// 没有协议前缀的默认作为HTTP代理
httpProxies.push(proxy);
}
});
// 更新各个文本框的内容
document.getElementById('http-proxy-list').value = httpProxies.join('\n');
document.getElementById('https-proxy-list').value = httpsProxies.join('\n');
document.getElementById('socks5-proxy-list').value = socks5Proxies.join('\n');
document.getElementById('full-proxy-list').value = fullProxies.join('\n');
});
}
function loadIpLists() {
$.get(appendToken('/api/ip_lists'), function(data) {
$('#whitelist').val(data.whitelist.join('\n'));
$('#blacklist').val(data.blacklist.join('\n'));
});
}
function updateLogs(forceRefresh = false) {
const level = $('#log-level').val();
const searchText = $('#log-search-input').val().toLowerCase();
$.get(appendToken(`/api/logs?level=${level}&search=${encodeURIComponent(searchText)}`), function(data) {
const container = $('#log-container');
container.empty();
data.logs.forEach(log => {
let message = log.message;
if (searchText) {
const regex = new RegExp(`(${searchText})`, 'gi');
message = message.replace(regex, '<span class="log-highlight">$1</span>');
}
const logEntry = $(`<div class="log-entry log-${log.level}">
<span class="log-time">${log.time}</span> -
<span class="log-level">${log.level}</span> -
<span class="log-message">${message}</span>
</div>`);
container.append(logEntry);
});
if (searchText && data.logs.length > 0) {
const firstHighlight = container.find('.log-highlight').first();
if (firstHighlight.length) {
container.scrollTop(firstHighlight.position().top);
}
}
});
}
function switchProxy() {
$.get(appendToken('/api/switch_proxy'), function(data) {
if (data.status === 'success') {
updateStatus();
} else {
console.error('Failed to switch proxy:', data.message);
}
});
}
function saveProxies() {
// 获取各个代理列表的内容
const httpProxies = document.getElementById('http-proxy-list').value
.split('\n')
.filter(line => line.trim())
.map(line => `http://${line.trim()}`);
const httpsProxies = document.getElementById('https-proxy-list').value
.split('\n')
.filter(line => line.trim())
.map(line => `https://${line.trim()}`);
const socks5Proxies = document.getElementById('socks5-proxy-list').value
.split('\n')
.filter(line => line.trim())
.map(line => `socks5://${line.trim()}`);
const fullProxies = document.getElementById('full-proxy-list').value
.split('\n')
.filter(line => line.trim());
// 合并所有代理列表
const allProxies = [...httpProxies, ...httpsProxies, ...socks5Proxies, ...fullProxies];
// 获取测试URL
const testUrl = document.getElementById('test-target-url').value.trim();
// 首先更新配置
$.ajax({
url: appendToken('/api/config'),
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ test_url: testUrl }),
success: function(configResponse) {
// 然后保存代理列表
$.ajax({
url: appendToken('/api/proxies'),
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ proxies: allProxies }),
success: function(response) {
if (response.status === 'success' && configResponse.status === 'success') {
showNotification(translations[currentLanguage].proxy_save_success, 'success');
// 重新加载代理列表以确保显示正确
loadProxies();
} else {
showNotification(translations[currentLanguage].proxy_save_failed.format(response.message), 'error');
}
},
error: function(xhr) {
showNotification(translations[currentLanguage].proxy_save_failed.format(xhr.responseText), 'error');
}
});
},
error: function(xhr) {
showNotification(translations[currentLanguage].config_save_failed.format(xhr.responseText), 'error');
}
});
}
function checkProxies() {
const testUrl = $('#test-target-url').val().trim() || 'https://www.baidu.com';
// 显示加载状态
const saveBtn = document.querySelector('.proxy-actions .btn-primary');
const checkBtn = document.querySelector('.proxy-actions .btn-warning');
saveBtn.disabled = true;
checkBtn.disabled = true;
checkBtn.innerHTML = `<i class="fa fa-spinner fa-spin"></i> ${translations[currentLanguage].checking_proxy}`;
$.get(appendToken('/api/check_proxies'), { test_url: testUrl }, function(data) {
if (data.status === 'success') {
loadProxies();
showNotification(translations[currentLanguage].proxy_check_result.replace('{}', data.total), 'success');
} else {
showNotification(translations[currentLanguage].proxy_check_failed.format(data.message), 'error');
}
}).always(function() {
// 恢复按钮状态
saveBtn.disabled = false;
checkBtn.disabled = false;
checkBtn.innerHTML = `<i class="fa fa-check-circle"></i> <span>${translations[currentLanguage].check_proxy_btn}</span>`;
});
}
function saveIpLists() {
const whitelist = $('#whitelist').val().split('\n').filter(line => line.trim());
const blacklist = $('#blacklist').val().split('\n').filter(line => line.trim());
const ipAuthPriority = $('select[name="ip_auth_priority"]').val();
// 显示加载状态
const saveBtn = document.querySelector('#save-ip-list-btn');
if (saveBtn) {
const originalContent = saveBtn.innerHTML;
saveBtn.innerHTML = `<i class="fa fa-spinner fa-spin"></i> ${translations[currentLanguage].loading}`;
saveBtn.disabled = true;
}
// 先保存白名单
$.ajax({
url: appendToken('/api/ip_lists'),
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
type: 'whitelist',
list: whitelist
})
})
.then(function(whitelistResponse) {
// 再保存黑名单
return $.ajax({
url: appendToken('/api/ip_lists'),
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
type: 'blacklist',
list: blacklist
})
});
})
.then(function(response) {
if (response.status === 'success') {
showNotification(translations[currentLanguage].ip_list_save_success, 'success');
// 重新加载IP名单以确保显示正确
loadIpLists();
} else {
showNotification(translations[currentLanguage].ip_list_save_failed.format(response.message), 'error');
}
})
.catch(function(xhr) {
showNotification(translations[currentLanguage].ip_list_save_failed.format(xhr.responseText), 'error');
})
.always(function() {
// 恢复按钮状态
const saveBtn = document.querySelector('#save-ip-list-btn');
if (saveBtn) {
saveBtn.innerHTML = `<i class="fa fa-save"></i> <span data-i18n="save_list_btn">${translations[currentLanguage].save_list_btn}</span>`;
saveBtn.disabled = false;
}
});
}
function controlService(action) {
const btn = document.querySelector(`.${action}-btn`);
if (btn) {
btn.classList.add('loading');
}
$.ajax({
url: appendToken('/api/service'),
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ action: action }),
success: function(response) {
if (response.status === 'success') {
// 立即更新状态显示
const statusText = document.querySelector('.status-text');
const statusDot = document.querySelector('.status-dot');
if (statusText) {
statusText.textContent = translations[currentLanguage].service_status[response.service_status];
}
if (statusDot) {
statusDot.classList.remove('running');
statusDot.classList.remove('stopped');
void statusDot.offsetWidth; // 触发重排以重置动画
statusDot.classList.add(response.service_status === 'running' ? 'running' : 'stopped');
}
// 更新按钮状态
updateServiceButtons(response.service_status);
// 更新状态后立即触发一次状态更新
updateStatus();
}
showToast(response.message);
},
error: function(xhr) {
showToast(translations[currentLanguage].operation_failed.replace('{}', xhr.responseText));
},
complete: function() {
if (btn) {
btn.classList.remove('loading');
// 移除所有可能的过渡状态类
btn.classList.remove('stopping', 'starting', 'restarting');
}
}
});
}
function clearLogs() {
if (confirm(translations[currentLanguage].confirm_clear_logs)) {
$.ajax({
url: appendToken('/api/logs/clear'),
method: 'POST',
success: function(response) {
if (response.status === 'success') {
showNotification(translations[currentLanguage].logs_cleared, 'success');
$('#log-container').empty();
} else {
showNotification(translations[currentLanguage].clear_logs_failed.format(response.message), 'error');
}
},
error: function(xhr) {
showNotification(translations[currentLanguage].clear_logs_failed.format(xhr.responseText), 'error');
}
});
}
}
// 添加保存所有配置的函数
function saveAllConfig() {
// 显示加载状态
const saveBtn = document.querySelector('.floating-save-btn');
const originalContent = saveBtn.innerHTML;
saveBtn.innerHTML = `<i class="fa fa-spinner fa-spin"></i> <span data-i18n="saving">保存中...</span>`;
saveBtn.disabled = true;
// 收集所有配置
const config = {
port: $('input[name="port"]').val(),
mode: $('select[name="mode"]').val(),
interval: $('input[name="interval"]').val(),
username: $('input[name="username"]').val(),
password: $('input[name="password"]').val(),
// 修改这里,确保正确获取 checkbox 的值并转换为字符串
use_getip: $('input[name="use_getip"]').prop('checked').toString(),
getip_url: $('input[name="getip_url"]').val(),
proxy_username: $('input[name="proxy_username"]').val(),
proxy_password: $('input[name="proxy_password"]').val(),
proxy_file: $('input[name="proxy_file"]').val(),
check_proxies: $('input[name="check_proxies"]').prop('checked').toString(),
language: $('select[name="language"]').val(),
whitelist_file: $('input[name="whitelist_file"]').val(),
blacklist_file: $('input[name="blacklist_file"]').val(),
ip_auth_priority: $('select[name="ip_auth_priority"]').val(),
display_level: $('select[name="display_level"]').val(),
test_url: $('input[name="test_url"]').val()
};
// 保存配置
$.ajax({
url: appendToken('/api/config'),
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(config),
success: function(response) {
if (response.status === 'success') {
showNotification(translations[currentLanguage].config_save_success, 'success');
if (response.port_changed) {
showNotification(translations[currentLanguage].restarting_service, 'success');
// 调用服务重启API
$.ajax({
url: appendToken('/api/service'),
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ action: 'restart' }),
success: function(restartResponse) {
if (restartResponse.status === 'success') {
showNotification(translations[currentLanguage].service_restart_success, 'success');
} else {
showNotification(translations[currentLanguage].service_restart_failed, 'error');
}
updateStatus();
},
error: function(xhr) {
showNotification(translations[currentLanguage].service_restart_failed.format(xhr.responseText), 'error');
}
});
}
// 重新加载配置以更新UI状态
loadConfig();
} else {
showNotification(translations[currentLanguage].config_save_failed.format(response.message), 'error');
}
},
error: function(xhr) {
showNotification(translations[currentLanguage].config_save_failed.format(xhr.responseText), 'error');
},
complete: function() {
// 恢复按钮状态
saveBtn.innerHTML = originalContent;
saveBtn.disabled = false;
}
});
}
// 添加一个简单的 Toast 提示函数
function showToast(type, message) {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.innerHTML = `
<i class="fa fa-${type === 'success' ? 'check' : 'exclamation'}-circle"></i>
<span>${message}</span>
`;
document.body.appendChild(toast);
// 添加显示类触发动画
setTimeout(() => toast.classList.add('show'), 100);
// 3秒后移除
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
const translations = {
cn: {
'title': 'ProxyCat 控制面板',
'page_title': 'ProxyCat 控制面板',
'current_proxy_title': '当前代理',
'run_mode_title': '运行模式',
'proxy_total_title': '代理总数',
'next_switch_title': '下次切换',
'service_control_title': '服务控制',
'proxy_settings_title': '代理设置',
'local_addresses_title': '本地监听地址',
'about_info_title': '关于信息',
'star_project_text': '开源项目求 Star: ',
'public_account_text': '公众号: ',
'public_account_name': '樱花庄的本间白猫',
'github_link': 'https://github.com/honmashironeko/ProxyCat',
'local_http_text': '本地监听地址 (HTTP): ',
'local_socks5_text': '本地监听地址 (SOCKS5): ',
'current_proxy': '当前代理',
'run_mode': '运行模式',
'proxy_count': '代理总数',
'service_control': '服务控制',
'start_service': '启动服务',
'stop_service': '停止服务',
'restart_service': '重启服务',
'config_settings': '配置设置',
'proxy_management': '代理管理',
'ip_lists': 'IP名单',
'run_logs': '运行日志',
'basic_config': '基本配置',
'port': '端口',
'switch_interval': '切换间隔(秒)',
'run_mode_text': '运行模式',
'cycle_mode': '循环模式',
'load_balance_mode': '负载均衡',
'language_text': '语言',
'auth_config': '认证配置',
'username': '用户名',
'password': '密码',
'proxy_settings': '代理设置',
'use_api': '使用API获取代理',
'api_address': 'API地址',
'enable_proxy_check': '启用代理检测',
'proxy_list': '代理列表',
'save_proxy': '保存代理',
'check_proxy': '检测代理',
'whitelist': '白名单',
'blacklist': '黑名单',
'ip_auth_priority': 'IP认证优先级',
'whitelist_first': '白名单优先',
'blacklist_first': '黑名单优先',
'save_list': '保存名单',
'log_level': '日志级别',
'all': '全部',
'info': '信息',
'warning': '警告',
'critical': '严重错误',
'save_config': '保存配置',
'proxy_username': '代理用户名',
'proxy_password': '代理密码',
'clear_logs': '清除日志',
'confirm_clear_logs': '确定要清除所有日志吗?此操作不可恢复。',
'logs_cleared': '日志已清除',
'clear_logs_failed': '清除日志失败:{}',
'config_tab': '配置设置',
'proxies_tab': '代理管理',
'ip_lists_tab': 'IP名单',
'logs_tab': '运行日志',
'port_label': '端口',
'save_success': '保存成功',
'save_failed': '保存失败',
'confirm_restart': '配置已更改,需要重启服务器才能生效。是否立即重启?',
'service_start_success': '服务启动成功',
'service_stop_success': '服务停止成功',
'service_restart_success': '服务重启成功',
'operation_failed': '操作失败:{}',
'loading': '加载中...',
'run_mode_title': '运行模式',
'proxy_total': '代理总数',
'next_switch_title': '下次切换',
'basic_config_title': '基本配置',
'switch_interval_label': '切换间隔(秒)',
'run_mode_label': '运行模式',
'cycle_mode': '循环模式',
'load_balance_mode': '负载均衡',
'auth_config_title': '认证配置',
'use_api_label': '使用API获取代理',
'api_url_label': 'API地址',
'enable_proxy_check_label': '启用代理检测',
'proxy_list_label': '代理列表',
'save_proxy_btn': '保存代理',
'check_proxy_btn': '检测代理',
'whitelist_title': '白名单',
'blacklist_title': '黑名单',
'ip_auth_priority_label': 'IP认证优先级',
'whitelist_first': '白名单优先',
'blacklist_first': '黑名单优先',
'save_list_btn': '保存名单',
'proxy_check_success': '代理检测完成,有效代理数:{}',
'proxy_check_failed': '代理检测失败:{}',
'proxy_save_success': '代理保存成功',
'proxy_save_failed': '代理保存失败:{}',
'ip_list_save_success': 'IP名单保存成功',
'ip_list_save_failed': 'IP名单保存失败:{}',
'manual_switch_btn': '手动切换',
'switch_success': '切换成功',
'switch_failed': '切换失败:{}',
'no_proxy': '没有代理',
'seconds': '秒',
'username_label': '用户名',
'password_label': '密码',
'proxy_username_label': '代理用户名',
'proxy_password_label': '代理密码',
'progress_bar_label': '切换进度',
'auth_username_label': '认证用户名',
'auth_password_label': '认证密码',
'proxy_auth_username_label': '代理认证用户名',
'proxy_auth_password_label': '代理认证密码',
'language_switch_success': '语言切换成功',
'language_switch_failed': '语言切换失败:{}',
'refresh_failed': '刷新数据失败:{}',
'log_level_label': '日志级别',
'log_level_all': '全部',
'log_level_info': '信息',
'log_level_warning': '警告',
'log_level_error': '错误',
'log_level_critical': '严重错误',
'clear_logs_btn': '清除日志',
'confirm_clear_logs': '确定要清除所有日志吗?此操作不可恢复。',
'logs_cleared': '日志已清除',
'clear_logs_failed': '清除日志失败:{}',
'logs_tab': '运行日志',
'config_save_success': '配置保存成功',
'config_save_failed': '配置保存失败:{}',
'proxy_save_success': '代理保存成功',
'proxy_save_failed': '代理保存失败:{}',
'ip_list_save_success': 'IP名单保存成功',
'ip_list_save_failed': 'IP名单保存失败:{}',
'service_start_success': '服务启动成功',
'service_stop_success': '服务停止成功',
'service_restart_success': '服务重启成功',
'confirm_restart': '配置已更改,需要重启服务器才能生效。是否立即重启?',
'invalid_token': '无效的访问令牌,请检查 URL 中的 token 参数',
'version_info_not_found': '未找到版本信息',
'update_check_error': '检查更新失败:{}',
'version_check_failed': '版本检查失败:{}',
'proxy_check_start': '开始检查代理...',
'proxy_check_complete': '代理检查完成',
'proxy_check_result': '代理检查完成,有效代理:{}个',
'proxy_check_failed': '代理检查失败:{}',
'language_label': '语言',
'test_url_label': '代理检测地址',
'test_url_help': '用于检测代理有效性的目标地址',
'invalid_test_url': '无效的检测地址',
'user_management_title': '用户管理',
'username_column': '用户名',
'password_column': '密码',
'actions_column': '操作',
'add_user_btn': '添加用户',
'enter_username': '请输入用户名',
'enter_password': '请输入密码',
'confirm_delete_user': '确定要删除该用户吗?',
'users_save_success': '用户保存成功',
'users_save_failed': '用户保存失败:{}',
'enter_new_password': '请输入新密码',
'password_changed': '密码修改成功',
'password_change_failed': '密码修改失败:{}',
'api_proxy_settings_title': 'API代理设置',
'test_target_label': '测试目标地址',
'test_url_help': '用于检测代理有效性的目标地址',
'test_target_placeholder': 'https://www.baidu.com',
'service_control_description': '管理代理服务器的运行状态',
'service_running': '运行中',
'service_stopped': '已停止',
'service_running_desc': '系统正常运行,代理切换功能已启用',
'service_stopped_desc': '系统已停止,代理切换功能未启用',
'service_status': {
'running': '运行中',
'stopped': '已停止'
},
'no_proxy': '无代理',
'seconds': '秒',
'service_control_title': '服务控制',
'start_service': '启动',
'stop_service': '停止',
'restart_service': '重启',
'loading': '加载中...',
'manual_switch_btn': '手动切换',
'cycle_mode': '循环模式',
'load_balance_mode': '负载均衡',
'next_switch': '下次切换',
'proxy_mode': {
'cycle': '循环模式',
'load_balance': '负载均衡'
},
'service_action': {
'start_success': '服务启动成功',
'stop_success': '服务停止成功',
'restart_success': '服务重启成功'
},
'operation_failed': '操作失败: {}',
'no_proxy': '无代理',
'proxy_management': '代理管理',
'http_proxy': 'HTTP代理',
'https_proxy': 'HTTPS代理',
'socks5_proxy': 'SOCKS5代理',
'full_proxy': '完整代理地址',
'proxy_help': '支持完整格式的代理地址,包含协议和认证信息',
'proxy_placeholder': {
'http': '每行一个地址,格式:ip:port\n例如:127.0.0.1:8080',
'https': '每行一个地址,格式:ip:port\n例如:127.0.0.1:8443',
'socks5': '每行一个地址,格式:ip:port\n例如:127.0.0.1:1080',
'full': '每行一个地址,格式:protocol://[username:password@]ip:port\n例如:http://user:[email protected]:8080'
},
'save_proxy_btn': '保存代理',
'check_proxy_btn': '检测代理',
'checking_proxy': '检测中...',
'proxy_check_success': '代理检测完成,有效代理数:{}',
'proxy_check_failed': '代理检测失败:{}',
'version_text': '当前版本:',
'latest_version': '已是最新版本',
'new_version_available': '发现新版本',
'version_check_failed': '版本检查失败',
'restarting_service': '正在重启服务...',
'service_restart_success': '服务重启成功',
'service_restart_failed': '服务重启失败',
'search_logs': '搜索日志...'
},
en: {
'title': 'ProxyCat Control Panel',
'page_title': 'ProxyCat Control Panel',
'current_proxy_title': 'Current Proxy',
'run_mode_title': 'Run Mode',
'proxy_total_title': 'Total Proxies',
'next_switch_title': 'Next Switch',
'service_control_title': 'Service Control',
'proxy_settings_title': 'Proxy Settings',
'local_addresses_title': 'Local Listening Addresses',
'about_info_title': 'About',
'star_project_text': 'Star Project on GitHub: ',
'public_account_text': 'WeChat Official Account: ',
'public_account_name': 'Honma Shirone Cat',
'github_link': 'https://github.com/honmashironeko/ProxyCat',
'local_http_text': 'Local HTTP Address: ',
'local_socks5_text': 'Local SOCKS5 Address: ',
'current_proxy': 'Current Proxy',
'run_mode': 'Run Mode',
'proxy_count': 'Proxy Count',
'service_control': 'Service Control',
'start_service': 'Start Service',
'stop_service': 'Stop Service',
'restart_service': 'Restart Service',
'config_settings': 'Config Settings',
'proxy_management': 'Proxy Management',
'ip_lists': 'IP Lists',
'run_logs': 'Run Logs',
'basic_config': 'Basic Config',
'port': 'Port',
'switch_interval': 'Switch Interval(s)',
'run_mode_text': 'Run Mode',
'cycle_mode': 'Cycle Mode',
'load_balance_mode': 'Load Balance',
'language_text': 'Language',
'auth_config': 'Auth Config',
'username': 'Username',
'password': 'Password',
'proxy_settings': 'Proxy Settings',
'use_api': 'Use API',
'api_address': 'API Address',
'enable_proxy_check': 'Enable Proxy Check',
'proxy_list': 'Proxy List',
'save_proxy': 'Save Proxy',
'check_proxy': 'Check Proxy',
'whitelist': 'Whitelist',
'blacklist': 'Blacklist',
'ip_auth_priority': 'IP Auth Priority',
'whitelist_first': 'Whitelist First',
'blacklist_first': 'Blacklist First',
'save_list': 'Save List',
'log_level': 'Log Level',
'all': 'All',
'info': 'Info',
'warning': 'Warning',
'error': 'Error',
'critical': 'Critical',
'save_config': 'Save Config',
'proxy_username': 'Proxy Username',
'proxy_password': 'Proxy Password',
'clear_logs': 'Clear Logs',
'confirm_clear_logs': 'Are you sure you want to clear all logs? This action cannot be undone.',
'logs_cleared': 'Logs cleared',
'clear_logs_failed': 'Failed to clear logs: {}',
'config_tab': 'Configuration',
'proxies_tab': 'Proxy Management',
'ip_lists_tab': 'IP Lists',
'logs_tab': 'Logs',
'port_label': 'Port',
'save_success': 'Save successful',
'save_failed': 'Save failed',
'confirm_restart': 'Configuration has changed, server restart is required. Restart now?',
'service_start_success': 'Service started successfully',
'service_stop_success': 'Service stopped successfully',
'service_restart_success': 'Service restarted successfully',
'operation_failed': 'Operation failed: {}',
'loading': 'Loading...',
'run_mode_title': 'Run Mode',
'proxy_total': 'Total Proxies',
'next_switch_title': 'Next Switch',
'basic_config_title': 'Basic Configuration',
'switch_interval_label': 'Switch Interval(s)',
'run_mode_label': 'Run Mode',
'cycle_mode': 'Cycle Mode',
'load_balance_mode': 'Load Balance',
'auth_config_title': 'Authentication',
'use_api_label': 'Use API for Proxy',
'api_url_label': 'API URL',
'enable_proxy_check_label': 'Enable Proxy Check',
'proxy_list_label': 'Proxy List',
'save_proxy_btn': 'Save Proxies',
'check_proxy_btn': 'Check Proxies',
'whitelist_title': 'Whitelist',
'blacklist_title': 'Blacklist',
'ip_auth_priority_label': 'IP Auth Priority',
'whitelist_first': 'Whitelist First',
'blacklist_first': 'Blacklist First',
'save_list_btn': 'Save Lists',
'proxy_check_success': 'Proxy check completed, valid proxies: {}',
'proxy_check_failed': 'Proxy check failed: {}',
'proxy_save_success': 'Proxies saved successfully',
'proxy_save_failed': 'Save failed: {}',
'ip_list_save_success': 'IP lists saved successfully',
'ip_list_save_failed': 'Save failed: {}',
'manual_switch_btn': 'Manual Switch',
'switch_success': 'Switch successful',
'switch_failed': 'Switch failed: {}',
'no_proxy': 'No Proxy',
'seconds': 's',
'username_label': 'Username',
'password_label': 'Password',
'proxy_username_label': 'Proxy Username',
'proxy_password_label': 'Proxy Password',
'progress_bar_label': 'Switch Progress',
'auth_username_label': 'Auth Username',
'auth_password_label': 'Auth Password',
'proxy_auth_username_label': 'Proxy Auth Username',
'proxy_auth_password_label': 'Proxy Auth Password',
'language_switch_success': 'Language switched successfully',
'language_switch_failed': 'Failed to switch language: {}',
'refresh_failed': 'Failed to refresh data: {}',
'log_level_label': 'Log Level',
'log_level_all': 'All',
'log_level_info': 'Info',
'log_level_warning': 'Warning',
'log_level_error': 'Error',
'log_level_critical': 'Critical',
'clear_logs_btn': 'Clear Logs',
'confirm_clear_logs': 'Are you sure you want to clear all logs? This action cannot be undone.',
'logs_cleared': 'Logs cleared',
'clear_logs_failed': 'Failed to clear logs: {}',
'logs_tab': 'Logs',
'config_save_success': 'Configuration saved successfully',
'config_save_failed': 'Failed to save configuration: {}',
'proxy_save_success': 'Proxies saved successfully',
'proxy_save_failed': 'Failed to save proxies: {}',
'ip_list_save_success': 'IP lists saved successfully',
'ip_list_save_failed': 'Failed to save IP lists: {}',
'service_start_success': 'Service started successfully',
'service_stop_success': 'Service stopped successfully',
'service_restart_success': 'Service restarted successfully',
'confirm_restart': 'Configuration has changed, server restart is required. Restart now?',
'invalid_token': 'Invalid access token, please check the token parameter in URL',
'version_info_not_found': 'Version information not found',
'update_check_error': 'Failed to check for updates: {}',
'version_check_failed': 'Version check failed: {}',
'proxy_check_start': 'Starting proxy check...',
'proxy_check_complete': 'Proxy check completed',
'proxy_check_result': 'Proxy check completed, valid proxies: {}',
'proxy_check_failed': 'Proxy check failed: {}',
'language_label': 'Language',
'test_url_label': 'Proxy Test URL',
'test_url_help': 'Target URL for proxy validity check',
'invalid_test_url': 'Invalid test URL',
'user_management_title': 'User Management',
'username_column': 'Username',
'password_column': 'Password',
'actions_column': 'Actions',
'add_user_btn': 'Add User',
'enter_username': 'Enter username',
'enter_password': 'Enter password',
'confirm_delete_user': 'Are you sure you want to delete this user?',
'users_save_success': 'Users saved successfully',
'users_save_failed': 'Failed to save users: {}',
'enter_new_password': 'Enter new password',
'password_changed': 'Password changed successfully',
'password_change_failed': 'Failed to change password: {}',
'api_proxy_settings_title': 'API Proxy Settings',
'test_target_label': 'Test Target URL',
'test_url_help': 'Target URL for proxy validity check',
'test_target_placeholder': 'https://www.baidu.com',
'service_control_description': 'Manage proxy server running status',
'service_status': {
'running': 'Running',
'stopped': 'Stopped'
},
'no_proxy': 'No Proxy',
'seconds': 's',
'service_control_title': 'Service Control',
'start_service': 'Start',
'stop_service': 'Stop',
'restart_service': 'Restart',
'proxy_management': 'Proxy Management',
'http_proxy': 'HTTP Proxy',
'https_proxy': 'HTTPS Proxy',
'socks5_proxy': 'SOCKS5 Proxy',
'full_proxy': 'Full Proxy Address',
'proxy_help': 'Support full format proxy addresses, including protocol and authentication',
'proxy_placeholder': {
'http': 'One address per line, format: ip:port\nExample: 127.0.0.1:8080',
'https': 'One address per line, format: ip:port\nExample: 127.0.0.1:8443',
'socks5': 'One address per line, format: ip:port\nExample: 127.0.0.1:1080',
'full': 'One address per line, format: protocol://[username:password@]ip:port\nExample: http://user:[email protected]:8080'
},
'save_proxy_btn': 'Save Proxies',
'check_proxy_btn': 'Check Proxies',
'checking_proxy': 'Checking...',
'proxy_check_success': 'Proxy check completed, valid proxies: {}',
'proxy_check_failed': 'Proxy check failed: {}',
'version_text': 'Current Version:',
'latest_version': 'Up to date',
'new_version_available': 'New version available',
'version_check_failed': 'Version check failed',
'restarting_service': 'Restarting service...',
'service_restart_success': 'Service restarted successfully',
'service_restart_failed': 'Failed to restart service',
'search_logs': 'Search logs...'
}
};
let currentLanguage = 'cn';
function updatePageLanguage(language) {
const texts = translations[language];
// 更新所有带有 data-i18n 属性的元素
for (const [key, value] of Object.entries(texts)) {
const elements = document.querySelectorAll(`[data-i18n="${key}"]`);
elements.forEach(el => el.textContent = value);
// 更新带有 data-i18n-placeholder 属性的元素
const placeholderElements = document.querySelectorAll(`[data-i18n-placeholder="${key}"]`);
placeholderElements.forEach(el => el.setAttribute('placeholder', value));
}
// 更新语言切换按钮显示
document.querySelector('.cn-text').style.display = language === 'cn' ? 'inline' : 'none';
document.querySelector('.en-text').style.display = language === 'en' ? 'inline' : 'none';
// 更新日志级别选项
updateLogLevelOptions();
// 更新代理列表标题
const proxyHeaders = document.querySelectorAll('.proxy-list-header span');
proxyHeaders.forEach(header => {
const headerText = header.textContent.toLowerCase();
if (headerText.includes('http') && !headerText.includes('https')) {
header.textContent = translations[language].http_proxy;
} else if (headerText.includes('https')) {
header.textContent = translations[language].https_proxy;
} else if (headerText.includes('socks5')) {
header.textContent = translations[language].socks5_proxy;
}
});
// 更新代理列表占位符
document.getElementById('http-proxy-list').setAttribute('placeholder', translations[language].proxy_placeholder.http);
document.getElementById('https-proxy-list').setAttribute('placeholder', translations[language].proxy_placeholder.https);
document.getElementById('socks5-proxy-list').setAttribute('placeholder', translations[language].proxy_placeholder.socks5);
// 更新帮助文本
const helpText = document.querySelector('.proxy-list-help');
if (helpText) {
helpText.textContent = translations[language].proxy_help;
}
// 更新占位符文本
const placeholders = {
'http-proxy-list': translations[language].proxy_placeholder.http,
'https-proxy-list': translations[language].proxy_placeholder.https,
'socks5-proxy-list': translations[language].proxy_placeholder.socks5,
'full-proxy-list': translations[language].proxy_placeholder.full
};
Object.entries(placeholders).forEach(([id, text]) => {
const element = document.getElementById(id);
if (element) {
element.setAttribute('placeholder', text);
}
});
// 更新按钮文本
const saveBtn = document.querySelector('.proxy-actions .btn-primary span');
const checkBtn = document.querySelector('.proxy-actions .btn-warning span');
if (saveBtn) {
saveBtn.textContent = translations[language].save_proxy_btn;
}
if (checkBtn) {
checkBtn.textContent = translations[language].check_proxy_btn;
}
}
// 修改初始化语言函数
async function initializeLanguage() {
try {
const response = await fetch(appendToken('/api/status'));
const data = await response.json();
currentLanguage = data.config.language || 'cn';
// 更新所有翻译文本
updatePageLanguage(currentLanguage);
// 特别更新服务状态文本
const status = data.service_status || 'running';
const statusText = document.querySelector('.status-text');
if (statusText) {
statusText.textContent = translations[currentLanguage].service_status[status];
}
// 更新按钮状态
updateServiceButtons(status);
// 确保在DOM加载完成后更新
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
updatePageLanguage(currentLanguage);
const statusText = document.querySelector('.status-text');
if (statusText) {
statusText.textContent = translations[currentLanguage].service_status[status];
}
});
}
} catch (error) {
console.error('Failed to initialize language:', error);
}
}
// 修改语言切换处理函数
document.getElementById('languageToggle').addEventListener('click', async function() {
const newLanguage = currentLanguage === 'cn' ? 'en' : 'cn';
try {
const response = await fetch(appendToken('/api/language'), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ language: newLanguage })
});
const data = await response.json();
if (data.status === 'success') {
currentLanguage = newLanguage;
// 更新所有翻译文本
updatePageLanguage(newLanguage);
// 特别更新服务状态文本
const statusResponse = await fetch(appendToken('/api/status'));
const statusData = await statusResponse.json();
const status = statusData.service_status || 'running';
const statusText = document.querySelector('.status-text');
if (statusText) {
statusText.textContent = translations[newLanguage].service_status[status];
}
} else {
console.error('Failed to switch language:', data.message);
}
} catch (error) {
console.error('Failed to switch language:', error);
}
});
document.addEventListener('DOMContentLoaded', initializeLanguage);
loadConfig();
loadProxies();
loadIpLists();
setInterval(updateStatus, 2000);
setInterval(updateCountdown, 1000);
setInterval(updateLogs, 2000);
updateStatus();
updateCountdown();
updateLogs();
function showMessage(key, ...args) {
const message = translations[currentLanguage][key];
if (args.length > 0) {
try {
showNotification(message.replace('{}', args.join(', ')), key.includes('success') ? 'success' : 'error');
} catch (e) {
showNotification(message, key.includes('success') ? 'success' : 'error');
}
} else {
showNotification(message, key.includes('success') ? 'success' : 'error');
}
}
function updateLogLevelOptions() {
const logLevelSelect = document.getElementById('log-level');
const options = logLevelSelect.options;
for (let i = 0; i < options.length; i++) {
const option = options[i];
const key = `log_level_${option.value.toLowerCase()}`;
if (translations[currentLanguage][key]) {
option.text = translations[currentLanguage][key];
}
}
}
async function checkVersion() {
try {
const lastCheck = localStorage.getItem('lastVersionCheck');
const now = Date.now();
if (!lastCheck || (now - parseInt(lastCheck)) > 24 * 60 * 60 * 1000) {
const response = await fetch(appendToken('/api/version'));
const data = await response.json();
if (data.status === 'success' && !data.is_latest) {
const message = translations[currentLanguage].version_update_available
.replace('{}', data.current_version)
.replace('{}', data.latest_version);
alert(message);
}
localStorage.setItem('lastVersionCheck', now.toString());
}
} catch (error) {
console.error('Failed to check version:', error);
}
}
document.addEventListener('DOMContentLoaded', function() {
setTimeout(checkVersion, 3000);
});
$(document).ready(function() {
$('#test-target-url').attr('placeholder',
translations[currentLanguage]['test_target_placeholder']);
});
async function switchLanguage(newLanguage) {
try {
const response = await fetch(appendToken('/api/language'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ language: newLanguage })
});
const data = await response.json();
if (data.status === 'success') {
currentLanguage = newLanguage;
updatePageLanguage(newLanguage);
await Promise.all([refreshStatus(), refreshLogs()]);
}
} catch (error) {
console.error('Failed to switch language:', error);
}
}
function loadUsers() {
$.get(appendToken('/api/users'), function(data) {
const tbody = $('#user-table tbody');
tbody.empty();
Object.entries(data.users).forEach(([username, password]) => {
tbody.append(`
<tr>
<td>${username}</td>
<td>${password}</td>
<td>
<button class="btn btn-sm btn-warning" onclick="editUserPassword('${username}')">
<i class="fa fa-edit"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="deleteUser('${username}')">
<i class="fa fa-trash"></i>
</button>
</td>
</tr>
`);
});
// 更新本地监听地址
updateLocalAddresses();
});
}
function addUser() {
const username = prompt(translations[currentLanguage].enter_username);
if (!username) return;
const password = prompt(translations[currentLanguage].enter_password);
if (!password) return;
const users = {};
$('#user-table tbody tr').each(function() {
const tds = $(this).find('td');
users[$(tds[0]).text()] = $(tds[1]).text();
});
users[username] = password;
saveUsers(users);
}
function deleteUser(username) {
if (!confirm(translations[currentLanguage].confirm_delete_user)) return;
const users = {};
$('#user-table tbody tr').each(function() {
const tds = $(this).find('td');
const user = $(tds[0]).text();
if (user !== username) {
users[user] = $(tds[1]).text();
}
});
saveUsers(users);
}
function saveUsers(users) {
$.ajax({
url: appendToken('/api/users'),
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ users: users }),
success: function(response) {
if (response.status === 'success') {
showMessage('users_save_success');
loadUsers();
} else {
showMessage('users_save_failed', response.message);
}
},
error: function(xhr, status, error) {
showMessage('users_save_failed', error);
}
});
}
function editUserPassword(username) {
const newPassword = prompt(translations[currentLanguage].enter_new_password);
if (!newPassword) return;
const users = {};
$('#user-table tbody tr').each(function() {
const tds = $(this).find('td');
const user = $(tds[0]).text();
users[user] = user === username ? newPassword : $(tds[1]).text();
});
saveUsers(users);
}
$(document).ready(function() {
loadUsers();
});
searchInput.addEventListener('input', function() {
clearSearchBtn.style.display = this.value ? 'block' : 'none';
if (searchTimeout) {
clearTimeout(searchTimeout);
}
searchTimeout = setTimeout(() => {
updateLogs(true);
}, 300);
});
clearSearchBtn.addEventListener('click', function() {
searchInput.value = '';
this.style.display = 'none';
updateLogs(true);
});
// 添加平滑滚动效果
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
document.querySelector(this.getAttribute('href')).scrollIntoView({
behavior: 'smooth'
});
});
});
// 添加加载动画
function showLoading() {
document.querySelector('.loading-overlay').style.display = 'flex';
}
function hideLoading() {
document.querySelector('.loading-overlay').style.display = 'none';
}
// 优化AJAX请求
function makeRequest(url, method = 'GET', data = null) {
showLoading();
return $.ajax({
url: appendToken(url),
method: method,
contentType: 'application/json',
data: data ? JSON.stringify(data) : null
}).always(hideLoading);
}
// 添加到现有的 JavaScript 代码中
function getRandomUser() {
const users = $('#user-table tbody tr').map(function() {
const username = $(this).find('td:first').text();
const password = $(this).find('td:eq(1)').text();
return `${username}:${password}`;
}).get();
return users.length ? users[Math.floor(Math.random() * users.length)] : '';
}
function updateLocalAddresses() {
const port = $('input[name="port"]').val() || '1080';
const randomAuth = getRandomUser();
const authString = randomAuth ? `${randomAuth}@` : '';
const httpAddress = `http://${authString}127.0.0.1:${port}`;
const socks5Address = `socks5://${authString}127.0.0.1:${port}`;
// 只在地址发生变化时更新显示
const currentHttp = $('#http-address').text();
const currentSocks5 = $('#socks5-address').text();
if (currentHttp !== httpAddress) {
$('#http-address').text(httpAddress);
}
if (currentSocks5 !== socks5Address) {
$('#socks5-address').text(socks5Address);
}
}
function copyToClipboard(elementId) {
const text = $(`#${elementId}`).text();
navigator.clipboard.writeText(text).then(() => {
const btn = $(`#${elementId}`).next('.copy-btn');
const originalHtml = btn.html();
btn.html('<i class="fa fa-check"></i>');
btn.addClass('copy-success');
setTimeout(() => {
btn.html(originalHtml);
btn.removeClass('copy-success');
}, 1500);
}).catch(err => {
console.error('复制失败:', err);
});
}
// 修改定时更新逻辑
let statusUpdateInterval;
function startStatusUpdate() {
// 清除现有的定时器
if (statusUpdateInterval) {
clearInterval(statusUpdateInterval);
}
// 启动新的定时更新,降低更新频率
statusUpdateInterval = setInterval(updateStatus, 2000); // 改为2秒更新一次
}
// 添加输入框焦点事件处理
$(document).ready(function() {
// 输入框获得焦点时暂停状态更新
$('input, textarea').on('focus', function() {
if (statusUpdateInterval) {
clearInterval(statusUpdateInterval);
}
});
// 输入框失去焦点时恢复状态更新
$('input, textarea').on('blur', function() {
startStatusUpdate();
});
// 其他初始化代码...
});
// 添加到现有的 JavaScript 代码中
function updateServiceStatus(status) {
const statusDot = document.querySelector('.status-dot');
const statusText = document.querySelector('.status-text');
if (status === 'running') {
statusDot.classList.remove('stopped');
statusText.textContent = translations[currentLanguage].service_status.running;
document.querySelector('.start-btn').disabled = true;
document.querySelector('.stop-btn').disabled = false;
document.querySelector('.restart-btn').disabled = false;
} else {
statusDot.classList.add('stopped');
statusText.textContent = translations[currentLanguage].service_status.stopped;
document.querySelector('.start-btn').disabled = false;
document.querySelector('.stop-btn').disabled = true;
document.querySelector('.restart-btn').disabled = true;
}
}
// 分离按钮状态更新逻辑
function updateServiceButtons(status) {
const startBtn = document.querySelector('.start-btn');
const stopBtn = document.querySelector('.stop-btn');
const restartBtn = document.querySelector('.restart-btn');
if (status === 'running') {
startBtn.disabled = true;
stopBtn.disabled = false;
restartBtn.disabled = false;
} else {
startBtn.disabled = false;
stopBtn.disabled = true;
restartBtn.disabled = true;
}
}
function checkVersion() {
$.get(appendToken('/api/version'), function(data) {
if (data.status === 'success') {
$('#current-version').text(data.current_version);
const versionStatus = $('#version-status');
if (data.is_latest) {
versionStatus.html(`<span class="text-success"><i class="fa fa-check-circle"></i> ${translations[currentLanguage].latest_version}</span>`);
} else {
versionStatus.html(`<span class="text-warning"><i class="fa fa-exclamation-circle"></i> ${translations[currentLanguage].new_version_available} (${data.latest_version})</span>`);
}
} else {
$('#version-status').html(`<span class="text-danger"><i class="fa fa-times-circle"></i> ${translations[currentLanguage].version_check_failed}</span>`);
}
}).fail(function() {
$('#version-status').html(`<span class="text-danger"><i class="fa fa-times-circle"></i> ${translations[currentLanguage].version_check_failed}</span>`);
});
}
// 在页面加载完成后调用版本检查
$(document).ready(function() {
checkVersion();
// 每30分钟检查一次版本更新
setInterval(checkVersion, 30 * 60 * 1000);
});
// 添加一个函数来更新UI状态
function updateUIState() {
const useGetip = $('input[name="use_getip"]').prop('checked');
$('input[name="getip_url"]').prop('disabled', !useGetip);
}
// 为 use_getip checkbox 添加变更事件监听器
$('input[name="use_getip"]').on('change', function() {
updateUIState();
});
function showNotification(message, type = 'success') {
const container = document.getElementById('notification-container');
const notification = document.createElement('div');
notification.className = `notification ${type}`;
const icon = document.createElement('i');
icon.className = `fa fa-${type === 'success' ? 'check-circle' : 'exclamation-circle'}`;
const messageElement = document.createElement('p');
messageElement.className = 'notification-message';
messageElement.textContent = message;
notification.appendChild(icon);
notification.appendChild(messageElement);
container.appendChild(notification);
// 3秒后自动移除通知
setTimeout(() => {
notification.style.animation = 'fadeOut 0.3s ease-out';
setTimeout(() => {
container.removeChild(notification);
}, 300);
}, 3000);
}
async function controlService(action) {
try {
const button = event.target.closest('.service-btn');
if (button) {
// 添加加载状态
button.classList.add('loading');
const response = await fetch('/api/service', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ action })
});
const data = await response.json();
if (data.status === 'success') {
showNotification(data.message, 'success');
// 更新服务状态显示
updateServiceStatus(data.service_status);
} else {
showNotification(data.message, 'error');
}
}
} catch (error) {
showNotification('操作失败,请稍后重试', 'error');
} finally {
// 移除加载状态
const button = event.target.closest('.service-btn');
if (button) {
button.classList.remove('loading');
}
}
}
function updateServiceStatus(status) {
const statusDot = document.querySelector('.status-dot');
const statusText = document.querySelector('.status-text');
if (status === 'running') {
statusDot.style.backgroundColor = '#2ecc71';
statusText.textContent = '运行中';
} else {
statusDot.style.backgroundColor = '#e74c3c';
statusText.textContent = '已停止';
}
}
</script>
</body>
</html>