主要功能: - 多用户管理系统 - 浏览器自动化(Playwright) - 任务编排和执行 - Docker容器化部署 - 数据持久化和日志管理 技术栈: - Flask 3.0.0 - Playwright 1.40.0 - SQLite with connection pooling - Docker + Docker Compose 部署说明详见README.md
2335 lines
72 KiB
HTML
2335 lines
72 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<title>知识管理平台自动化工具 - Web版</title>
|
||
<script src="/static/js/socket.io.min.js"></script>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||
background: linear-gradient(135deg, #56CCF2 0%, #2F80ED 100%);
|
||
min-height: 100vh;
|
||
padding: 10px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1600px;
|
||
margin: 0 auto;
|
||
background: white;
|
||
border-radius: 10px;
|
||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.header {
|
||
background: linear-gradient(135deg, #56CCF2 0%, #2F80ED 100%);
|
||
color: white;
|
||
padding: 20px 15px;
|
||
}
|
||
|
||
.header-content {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.header-left h1 {
|
||
font-size: 18px;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.header-left p {
|
||
opacity: 0.9;
|
||
font-size: 12px;
|
||
display: none;
|
||
}
|
||
|
||
.header-right {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.header-right > span {
|
||
display: inline;
|
||
font-size: 12px;
|
||
color: #fff;
|
||
}
|
||
|
||
#vip-status {
|
||
padding: 4px 10px;
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.btn-logout {
|
||
padding: 6px 12px;
|
||
background: rgba(255,255,255,0.2);
|
||
color: white;
|
||
border: 1px solid white;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
transition: all 0.3s;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.btn-logout:hover {
|
||
background: rgba(255,255,255,0.3);
|
||
}
|
||
|
||
.content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
row-gap: 1px !important;
|
||
column-gap: 1px !important;
|
||
padding: 15px;
|
||
}
|
||
|
||
.left-column {
|
||
display: flex;
|
||
flex-direction: column;
|
||
row-gap: 1px !important;
|
||
column-gap: 1px !important;
|
||
}
|
||
|
||
.right-column {
|
||
display: flex;
|
||
flex-direction: column;
|
||
row-gap: 1px !important;
|
||
column-gap: 1px !important;
|
||
}
|
||
|
||
.panel {
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
margin-bottom: 15px;
|
||
color: #333;
|
||
border-bottom: 2px solid #2F80ED;
|
||
padding-bottom: 8px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
row-gap: 1px !important;
|
||
column-gap: 1px !important;
|
||
}
|
||
|
||
.panel-title > span:first-child {
|
||
flex: 1;
|
||
min-width: 80px;
|
||
}
|
||
|
||
.panel-title-actions {
|
||
display: flex;
|
||
row-gap: 1px !important;
|
||
column-gap: 1px !important;
|
||
}
|
||
|
||
.add-account {
|
||
display: flex;
|
||
flex-direction: column;
|
||
row-gap: 1px !important;
|
||
column-gap: 1px !important;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.add-account-inputs {
|
||
display: flex;
|
||
row-gap: 1px !important;
|
||
column-gap: 1px !important;
|
||
}
|
||
|
||
.add-account input {
|
||
flex: 1;
|
||
padding: 10px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 5px;
|
||
font-size: 14px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.add-account button {
|
||
padding: 10px 20px;
|
||
background: #2F80ED;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
transition: all 0.3s;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.add-account button:hover {
|
||
background: #1a6dd6;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.accounts-list {
|
||
max-height: 500px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.account-item {
|
||
background: white;
|
||
padding: 12px;
|
||
margin-bottom: 10px;
|
||
border-radius: 5px;
|
||
border-left: 4px solid #2F80ED;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.account-item:hover {
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.account-info {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.account-username {
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 5px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.account-status {
|
||
font-size: 11px;
|
||
padding: 3px 8px;
|
||
border-radius: 3px;
|
||
display: inline-block;
|
||
margin-right: 5px;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.status-running {
|
||
background: #28a745;
|
||
color: white;
|
||
}
|
||
|
||
.status-queued {
|
||
background: #ffc107;
|
||
color: #333;
|
||
}
|
||
|
||
.status-idle {
|
||
background: #6c757d;
|
||
color: white;
|
||
}
|
||
|
||
.status-error {
|
||
background: #dc3545;
|
||
color: white;
|
||
}
|
||
|
||
.status-completed {
|
||
background: #17a2b8;
|
||
color: white;
|
||
}
|
||
|
||
.account-actions {
|
||
display: flex;
|
||
row-gap: 1px !important;
|
||
column-gap: 1px !important;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.account-actions select {
|
||
font-size: 11px;
|
||
padding: 5px 8px;
|
||
border-radius: 3px;
|
||
border: 1px solid #ddd;
|
||
flex: 1;
|
||
min-width: 90px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 5px 10px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 11px;
|
||
transition: all 0.2s;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.btn-start {
|
||
background: #28a745;
|
||
color: white;
|
||
}
|
||
|
||
.btn-stop {
|
||
background: #dc3545;
|
||
color: white;
|
||
}
|
||
|
||
.btn-delete {
|
||
background: #6c757d;
|
||
color: white;
|
||
}
|
||
|
||
.btn-screenshot {
|
||
background: #007bff;
|
||
color: white;
|
||
padding: 5px 8px;
|
||
}
|
||
|
||
.btn-refresh {
|
||
background: #28a745;
|
||
color: white;
|
||
padding: 4px 8px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.btn:hover {
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.browse-type-selector {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
row-gap: 1px !important;
|
||
column-gap: 1px !important;
|
||
margin: 12px 0;
|
||
}
|
||
|
||
.browse-type-selector label {
|
||
padding: 10px 5px;
|
||
background: white;
|
||
border: 2px solid #ddd;
|
||
border-radius: 5px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.browse-type-selector input:checked + label {
|
||
background: #2F80ED;
|
||
color: white;
|
||
border-color: #2F80ED;
|
||
}
|
||
|
||
.browse-type-selector input {
|
||
display: none;
|
||
}
|
||
|
||
.batch-operations {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
row-gap: 1px !important;
|
||
column-gap: 1px !important;
|
||
margin: 12px 0;
|
||
}
|
||
|
||
.batch-operations button {
|
||
padding: 10px 5px;
|
||
font-size: 13px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 10px;
|
||
}
|
||
|
||
.stat-card {
|
||
background: white;
|
||
padding: 15px 10px;
|
||
border-radius: 8px;
|
||
text-align: center;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 28px;
|
||
font-weight: bold;
|
||
color: #2F80ED;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: #666;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
/* 运行统计小卡片 */
|
||
.stats-grid-mini {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, 1fr);
|
||
row-gap: 1px !important;
|
||
column-gap: 1px !important;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.stat-card-mini {
|
||
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||
padding: 12px 8px;
|
||
border-radius: 8px;
|
||
text-align: center;
|
||
border: 1px solid #e0e0e0;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.stat-card-mini:hover {
|
||
box-shadow: 0 4px 12px rgba(47, 128, 237, 0.2);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.stat-value-mini {
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
color: #2F80ED;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.stat-value-mini.running-color {
|
||
color: #28a745;
|
||
}
|
||
|
||
.stat-value-mini.error-color {
|
||
color: #dc3545;
|
||
}
|
||
|
||
.stat-label-mini {
|
||
font-size: 11px;
|
||
color: #666;
|
||
}
|
||
|
||
/* 截图网格布局 - Windows大图标风格 */
|
||
.screenshots-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||
row-gap: 1px !important;
|
||
column-gap: 1px !important;
|
||
max-height: calc(100vh - 250px);
|
||
overflow-y: auto;
|
||
padding: 5px;
|
||
}
|
||
|
||
.screenshot-item {
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 10px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
row-gap: 1px !important;
|
||
column-gap: 1px !important;
|
||
border: 2px solid transparent;
|
||
transition: all 0.2s;
|
||
position: relative;
|
||
}
|
||
|
||
.screenshot-thumbnail {
|
||
width: 100%;
|
||
aspect-ratio: 16/9;
|
||
border-radius: 5px;
|
||
object-fit: cover;
|
||
background: #f0f0f0;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
border: 2px solid transparent;
|
||
}
|
||
|
||
.screenshot-thumbnail:hover {
|
||
border-color: #2F80ED;
|
||
box-shadow: 0 4px 12px rgba(47, 128, 237, 0.3);
|
||
transform: scale(1.02);
|
||
}
|
||
|
||
.screenshot-info {
|
||
width: 100%;
|
||
text-align: center;
|
||
}
|
||
|
||
.screenshot-name {
|
||
font-size: 11px;
|
||
color: #333;
|
||
font-weight: 500;
|
||
word-break: break-all;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 1;
|
||
-webkit-box-orient: vertical;
|
||
line-height: 1.3;
|
||
max-height: 1.3em;
|
||
}
|
||
|
||
.screenshot-meta {
|
||
font-size: 9px;
|
||
color: #999;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.screenshot-actions {
|
||
display: none;
|
||
position: absolute;
|
||
top: 5px;
|
||
right: 5px;
|
||
gap: 3px;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
padding: 3px;
|
||
border-radius: 5px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||
}
|
||
|
||
.screenshot-item:hover .screenshot-actions {
|
||
display: flex;
|
||
}
|
||
|
||
.screenshot-actions .btn {
|
||
padding: 3px 6px;
|
||
font-size: 10px;
|
||
}
|
||
|
||
.btn-copy {
|
||
background: #17a2b8;
|
||
color: white;
|
||
}
|
||
|
||
.btn-download {
|
||
background: #007bff;
|
||
color: white;
|
||
}
|
||
|
||
.btn-delete-shot {
|
||
background: #dc3545;
|
||
color: white;
|
||
}
|
||
|
||
.screenshot-panel {
|
||
height: calc(100vh - 150px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: transparent; /* 移除面板背景 */
|
||
}
|
||
|
||
.screenshot-panel .screenshots-grid {
|
||
flex: 1;
|
||
}
|
||
|
||
/* 日志内容样式 */
|
||
.log-content {
|
||
background: #1e1e1e;
|
||
color: #d4d4d4;
|
||
padding: 15px;
|
||
border-radius: 5px;
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
font-family: 'Consolas', 'Monaco', monospace;
|
||
font-size: 11px;
|
||
}
|
||
|
||
/* 浏览类型选择弹窗 */
|
||
.browse-type-modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 10000;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.browse-type-modal.active {
|
||
display: flex;
|
||
}
|
||
|
||
.browse-type-modal-content {
|
||
background: white;
|
||
border-radius: 12px;
|
||
width: 90%;
|
||
max-width: 450px;
|
||
max-height: 90vh;
|
||
overflow: auto;
|
||
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.browse-type-modal-header {
|
||
padding: 20px;
|
||
border-bottom: 1px solid #e0e0e0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.browse-type-modal-header h3 {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
color: #333;
|
||
}
|
||
|
||
.browse-type-modal-close {
|
||
background: none;
|
||
border: none;
|
||
font-size: 32px;
|
||
color: #999;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
width: 32px;
|
||
height: 32px;
|
||
line-height: 32px;
|
||
text-align: center;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.browse-type-modal-close:hover {
|
||
color: #333;
|
||
transform: rotate(90deg);
|
||
}
|
||
|
||
.browse-type-modal-body {
|
||
padding: 20px;
|
||
}
|
||
|
||
.browse-type-options {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.browse-type-option {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 15px;
|
||
border: 2px solid #ddd;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.browse-type-option:hover {
|
||
border-color: #2F80ED;
|
||
background: #f8f9ff;
|
||
}
|
||
|
||
.browse-type-option input[type="radio"] {
|
||
margin-right: 15px;
|
||
width: 20px;
|
||
height: 20px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.browse-type-option input[type="radio"]:checked + .browse-type-option-content {
|
||
color: #2F80ED;
|
||
}
|
||
|
||
.browse-type-option input[type="radio"]:checked ~ * {
|
||
font-weight: bold;
|
||
}
|
||
|
||
.browse-type-option-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.browse-type-option-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
margin-bottom: 4px;
|
||
color: #333;
|
||
}
|
||
|
||
.browse-type-option-desc {
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
|
||
.browse-type-modal-footer {
|
||
padding: 15px 20px;
|
||
border-top: 1px solid #e0e0e0;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
}
|
||
|
||
.browse-type-modal-footer .btn {
|
||
padding: 10px 20px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 图片预览浮窗 */
|
||
.image-preview-modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.9);
|
||
z-index: 9999;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.image-preview-modal.active {
|
||
display: flex;
|
||
}
|
||
|
||
.image-preview-content {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: relative;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.image-preview-content img {
|
||
max-width: 90%;
|
||
max-height: 90%;
|
||
object-fit: contain;
|
||
border-radius: 8px;
|
||
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
|
||
cursor: move;
|
||
transition: transform 0.1s ease-out;
|
||
user-select: none;
|
||
-webkit-user-select: none;
|
||
-moz-user-select: none;
|
||
touch-action: none;
|
||
}
|
||
|
||
.image-preview-close {
|
||
position: absolute;
|
||
top: 20px;
|
||
right: 20px;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
color: white;
|
||
border: 2px solid white;
|
||
border-radius: 50%;
|
||
width: 40px;
|
||
height: 40px;
|
||
font-size: 28px;
|
||
line-height: 36px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
z-index: 10000;
|
||
}
|
||
|
||
.image-preview-close:hover {
|
||
background: rgba(255, 255, 255, 0.3);
|
||
transform: rotate(90deg);
|
||
}
|
||
|
||
.image-preview-filename {
|
||
position: absolute;
|
||
bottom: 20px;
|
||
left: 0;
|
||
right: 0;
|
||
text-align: center;
|
||
color: white;
|
||
font-size: 14px;
|
||
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
|
||
z-index: 10000;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.image-preview-controls {
|
||
position: absolute;
|
||
top: 20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
display: flex;
|
||
gap: 10px;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
padding: 10px;
|
||
border-radius: 25px;
|
||
z-index: 10000;
|
||
}
|
||
|
||
.image-preview-controls button {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
color: white;
|
||
border: 2px solid white;
|
||
border-radius: 50%;
|
||
width: 40px;
|
||
height: 40px;
|
||
font-size: 20px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.image-preview-controls button:hover {
|
||
background: rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
.image-preview-hint {
|
||
position: absolute;
|
||
top: 80px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: rgba(0, 0, 0, 0.7);
|
||
color: white;
|
||
padding: 8px 16px;
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
z-index: 10000;
|
||
pointer-events: none;
|
||
opacity: 0;
|
||
transition: opacity 0.3s;
|
||
}
|
||
|
||
.image-preview-hint.show {
|
||
opacity: 1;
|
||
}
|
||
|
||
|
||
.log-entry {
|
||
margin-bottom: 6px;
|
||
padding: 4px;
|
||
border-left: 2px solid #2F80ED;
|
||
padding-left: 8px;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.log-timestamp {
|
||
color: #888;
|
||
margin-right: 8px;
|
||
font-size: 10px;
|
||
}
|
||
|
||
.log-message {
|
||
color: #d4d4d4;
|
||
}
|
||
|
||
.empty-message {
|
||
text-align: center;
|
||
padding: 30px 20px;
|
||
color: #888;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.vip-badge {
|
||
background: linear-gradient(135deg, #56CCF2 0%, #2F80ED 100%);
|
||
color: white;
|
||
padding: 4px 10px;
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.normal-badge {
|
||
background: #e0e0e0;
|
||
color: #666;
|
||
padding: 4px 10px;
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.account-limit-warning {
|
||
background: #fff3cd;
|
||
border: 1px solid #ffc107;
|
||
padding: 12px;
|
||
border-radius: 8px;
|
||
margin: 12px 0;
|
||
color: #856404;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* 平板及以上屏幕 */
|
||
@media (min-width: 768px) {
|
||
body {
|
||
padding: 20px;
|
||
}
|
||
|
||
.header {
|
||
padding: 25px 20px;
|
||
}
|
||
|
||
.header-left h1 {
|
||
font-size: 24px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.header-left p {
|
||
display: block;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.header-right > span {
|
||
display: inline;
|
||
}
|
||
|
||
#vip-status {
|
||
padding: 5px 12px;
|
||
font-size: 12px;
|
||
margin-left: 15px;
|
||
}
|
||
|
||
.btn-logout {
|
||
padding: 8px 16px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.content {
|
||
flex-direction: row;
|
||
gap: 20px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.left-column {
|
||
flex: 0 0 45%;
|
||
}
|
||
|
||
.right-column {
|
||
flex: 1;
|
||
}
|
||
|
||
.panel {
|
||
padding: 20px;
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.add-account {
|
||
flex-direction: row;
|
||
}
|
||
|
||
.add-account-inputs {
|
||
flex: 1;
|
||
}
|
||
|
||
.stats {
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 32px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.stats-grid-mini {
|
||
grid-template-columns: repeat(3, 1fr);
|
||
}
|
||
|
||
.stat-value-mini {
|
||
font-size: 22px;
|
||
}
|
||
|
||
.stat-label-mini {
|
||
font-size: 11px;
|
||
}
|
||
|
||
.screenshots-grid {
|
||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||
row-gap: 1px !important;
|
||
column-gap: 1px !important;
|
||
}
|
||
|
||
.log-content {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.btn-copy {
|
||
display: inline-block;
|
||
}
|
||
}
|
||
|
||
/* PC屏幕 */
|
||
@media (min-width: 1024px) {
|
||
.header-left h1 {
|
||
font-size: 32px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.header-left p {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.header {
|
||
padding: 30px;
|
||
}
|
||
|
||
.btn-logout {
|
||
padding: 8px 20px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.left-column {
|
||
flex: 0 0 40%;
|
||
}
|
||
|
||
.right-column {
|
||
flex: 1;
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.browse-type-selector label {
|
||
font-size: 14px;
|
||
padding: 12px;
|
||
}
|
||
|
||
.batch-operations button {
|
||
font-size: 14px;
|
||
padding: 10px 15px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 6px 12px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.account-username {
|
||
font-size: 15px;
|
||
}
|
||
|
||
.account-status {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.account-actions select {
|
||
font-size: 12px;
|
||
padding: 6px 10px;
|
||
}
|
||
|
||
.btn-screenshot {
|
||
padding: 6px 10px;
|
||
}
|
||
|
||
.screenshots-grid {
|
||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||
row-gap: 1px !important;
|
||
column-gap: 1px !important;
|
||
}
|
||
|
||
.btn-copy {
|
||
display: inline-block;
|
||
}
|
||
}
|
||
|
||
/* Toast 悬浮通知样式 */
|
||
.toast-container {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
z-index: 99999;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.toast {
|
||
background: white;
|
||
color: #333;
|
||
padding: 12px 20px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
min-width: 250px;
|
||
max-width: 400px;
|
||
animation: slideIn 0.3s ease-out;
|
||
pointer-events: auto;
|
||
border-left: 4px solid #2F80ED;
|
||
}
|
||
|
||
.toast.success {
|
||
border-left-color: #52c41a;
|
||
}
|
||
|
||
.toast.error {
|
||
border-left-color: #f5222d;
|
||
}
|
||
|
||
.toast.warning {
|
||
border-left-color: #faad14;
|
||
}
|
||
|
||
.toast.info {
|
||
border-left-color: #1890ff;
|
||
}
|
||
|
||
.toast-icon {
|
||
font-size: 20px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.toast.success .toast-icon::before {
|
||
content: "✓";
|
||
color: #52c41a;
|
||
}
|
||
|
||
.toast.error .toast-icon::before {
|
||
content: "✕";
|
||
color: #f5222d;
|
||
}
|
||
|
||
.toast.warning .toast-icon::before {
|
||
content: "⚠";
|
||
color: #faad14;
|
||
}
|
||
|
||
.toast.info .toast-icon::before {
|
||
content: "ℹ";
|
||
color: #1890ff;
|
||
}
|
||
|
||
.toast-message {
|
||
flex: 1;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from {
|
||
transform: translateX(400px);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translateX(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
@keyframes slideOut {
|
||
from {
|
||
transform: translateX(0);
|
||
opacity: 1;
|
||
}
|
||
to {
|
||
transform: translateX(400px);
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
.toast.removing {
|
||
animation: slideOut 0.3s ease-out forwards;
|
||
}
|
||
|
||
/* 截图开关样式 */
|
||
.screenshot-switch-section {
|
||
margin: 20px 0 10px 0;
|
||
padding: 15px;
|
||
background: linear-gradient(135deg, #667eea10 0%, #764ba210 100%);
|
||
border-radius: 10px;
|
||
border: 1px solid #667eea30;
|
||
}
|
||
|
||
.screenshot-switch-label {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
|
||
.screenshot-switch-label input[type="checkbox"] {
|
||
margin: 0;
|
||
margin-right: 12px;
|
||
width: 20px;
|
||
height: 20px;
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.screenshot-switch-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.screenshot-switch-title {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: #2d3748;
|
||
}
|
||
|
||
.screenshot-switch-desc {
|
||
font-size: 13px;
|
||
color: #718096;
|
||
}
|
||
|
||
.screenshot-switch-label:hover .screenshot-switch-title {
|
||
color: #667eea;
|
||
}
|
||
</style>
|
||
|
||
<style>
|
||
/* 强制覆盖截图间距 */
|
||
.screenshots-grid {
|
||
row-gap: 1px !important;
|
||
column-gap: 1px !important;
|
||
}
|
||
|
||
/* Toast 悬浮通知样式 */
|
||
.toast-container {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
z-index: 99999;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.toast {
|
||
background: white;
|
||
color: #333;
|
||
padding: 12px 20px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
min-width: 250px;
|
||
max-width: 400px;
|
||
animation: slideIn 0.3s ease-out;
|
||
pointer-events: auto;
|
||
border-left: 4px solid #2F80ED;
|
||
}
|
||
|
||
.toast.success {
|
||
border-left-color: #52c41a;
|
||
}
|
||
|
||
.toast.error {
|
||
border-left-color: #f5222d;
|
||
}
|
||
|
||
.toast.warning {
|
||
border-left-color: #faad14;
|
||
}
|
||
|
||
.toast.info {
|
||
border-left-color: #1890ff;
|
||
}
|
||
|
||
.toast-icon {
|
||
font-size: 20px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.toast.success .toast-icon::before {
|
||
content: "✓";
|
||
color: #52c41a;
|
||
}
|
||
|
||
.toast.error .toast-icon::before {
|
||
content: "✕";
|
||
color: #f5222d;
|
||
}
|
||
|
||
.toast.warning .toast-icon::before {
|
||
content: "⚠";
|
||
color: #faad14;
|
||
}
|
||
|
||
.toast.info .toast-icon::before {
|
||
content: "ℹ";
|
||
color: #1890ff;
|
||
}
|
||
|
||
.toast-message {
|
||
flex: 1;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from {
|
||
transform: translateX(400px);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translateX(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
@keyframes slideOut {
|
||
from {
|
||
transform: translateX(0);
|
||
opacity: 1;
|
||
}
|
||
to {
|
||
transform: translateX(400px);
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
.toast.removing {
|
||
animation: slideOut 0.3s ease-out forwards;
|
||
}
|
||
|
||
/* 截图开关样式 */
|
||
.screenshot-switch-section {
|
||
margin: 20px 0 10px 0;
|
||
padding: 15px;
|
||
background: linear-gradient(135deg, #667eea10 0%, #764ba210 100%);
|
||
border-radius: 10px;
|
||
border: 1px solid #667eea30;
|
||
}
|
||
|
||
.screenshot-switch-label {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
|
||
.screenshot-switch-label input[type="checkbox"] {
|
||
margin: 0;
|
||
margin-right: 12px;
|
||
width: 20px;
|
||
height: 20px;
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.screenshot-switch-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.screenshot-switch-title {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: #2d3748;
|
||
}
|
||
|
||
.screenshot-switch-desc {
|
||
font-size: 13px;
|
||
color: #718096;
|
||
}
|
||
|
||
.screenshot-switch-label:hover .screenshot-switch-title {
|
||
color: #667eea;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Toast 通知容器 -->
|
||
<div class="toast-container" id="toast-container"></div>
|
||
|
||
<div class="container">
|
||
<div class="header">
|
||
<div class="header-content">
|
||
<div class="header-left">
|
||
<h1>知识管理平台</h1>
|
||
<p>基于Playwright的多账号自动化管理系统</p>
|
||
</div>
|
||
<div class="header-right">
|
||
<span>欢迎,</span><span id="current-username" style="font-weight: bold; color: #fff;"></span>
|
||
<div id="vip-status">
|
||
<span id="vip-badge"></span>
|
||
</div>
|
||
<button class="btn-logout" onclick="logout()">退出</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<!-- 左侧列 -->
|
||
<div class="left-column">
|
||
<!-- 账号管理面板 -->
|
||
<div class="panel">
|
||
<div class="panel-title">账号管理</div>
|
||
|
||
<!-- 添加账号 -->
|
||
<div class="add-account">
|
||
<div class="add-account-inputs">
|
||
<input type="text" id="username" placeholder="用户名">
|
||
<input type="password" id="password" placeholder="密码">
|
||
</div>
|
||
<button onclick="addAccount()">添加账号</button>
|
||
</div>
|
||
|
||
<!-- 批量操作 -->
|
||
<div class="batch-operations">
|
||
<button class="btn btn-start" onclick="startAllAccounts()">全部开始</button>
|
||
<button class="btn btn-stop" onclick="stopAllAccounts()">全部停止</button>
|
||
<button class="btn btn-delete" onclick="clearAllAccounts()">清空账号</button>
|
||
</div>
|
||
|
||
<!-- 账号列表 -->
|
||
<div class="accounts-list" id="accountsList"></div>
|
||
</div>
|
||
|
||
<!-- 运行统计面板 -->
|
||
<div class="panel">
|
||
<div class="panel-title">运行统计</div>
|
||
<div class="stats-grid-mini">
|
||
<div class="stat-card-mini">
|
||
<div class="stat-value-mini" id="todayCompleted">0</div>
|
||
<div class="stat-label-mini">今日完成</div>
|
||
</div>
|
||
<div class="stat-card-mini">
|
||
<div class="stat-value-mini running-color" id="currentRunning">0</div>
|
||
<div class="stat-label-mini">正在运行</div>
|
||
</div>
|
||
<div class="stat-card-mini">
|
||
<div class="stat-value-mini error-color" id="todayFailed">0</div>
|
||
<div class="stat-label-mini">今日失败</div>
|
||
</div>
|
||
<div class="stat-card-mini">
|
||
<div class="stat-value-mini" id="todayItems">0</div>
|
||
<div class="stat-label-mini">浏览内容</div>
|
||
</div>
|
||
<div class="stat-card-mini">
|
||
<div class="stat-value-mini" id="todayAttachments">0</div>
|
||
<div class="stat-label-mini">查看附件</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧列 -->
|
||
<div class="right-column">
|
||
<!-- 截图管理面板 -->
|
||
<div class="panel screenshot-panel">
|
||
<div class="panel-title">
|
||
<span>截图管理</span>
|
||
<div class="panel-title-actions">
|
||
<button class="btn btn-refresh" onclick="loadScreenshots()">刷新</button>
|
||
<button class="btn btn-delete" onclick="clearAllScreenshots()">清空</button>
|
||
</div>
|
||
</div>
|
||
<div class="screenshots-grid" id="screenshotsList">
|
||
<div class="empty-message">加载中...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 浏览类型选择弹窗 -->
|
||
<div class="browse-type-modal" id="browseTypeModal" onclick="closeBrowseTypeModal(event)">
|
||
<div class="browse-type-modal-content">
|
||
<div class="browse-type-modal-header">
|
||
<h3>选择浏览类型</h3>
|
||
<button class="browse-type-modal-close" onclick="closeBrowseTypeModal(event)">×</button>
|
||
</div>
|
||
<div class="browse-type-modal-body">
|
||
<div class="browse-type-options">
|
||
<label class="browse-type-option">
|
||
<input type="radio" name="modal_browse_type" value="注册前未读">
|
||
<div class="browse-type-option-content">
|
||
<div class="browse-type-option-title">注册前未读</div>
|
||
<div class="browse-type-option-desc">浏览注册前的未读内容</div>
|
||
</div>
|
||
</label>
|
||
<label class="browse-type-option">
|
||
<input type="radio" name="modal_browse_type" value="应读" checked>
|
||
<div class="browse-type-option-content">
|
||
<div class="browse-type-option-title">应读</div>
|
||
<div class="browse-type-option-desc">浏览应该阅读的内容(推荐)</div>
|
||
</div>
|
||
</label>
|
||
<label class="browse-type-option">
|
||
<input type="radio" name="modal_browse_type" value="未读">
|
||
<div class="browse-type-option-content">
|
||
<div class="browse-type-option-title">未读</div>
|
||
<div class="browse-type-option-desc">浏览所有未读内容</div>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
|
||
<!-- 截图开关 -->
|
||
<div class="screenshot-switch-section">
|
||
<label class="screenshot-switch-label">
|
||
<input type="checkbox" id="enableScreenshotCheckbox" checked>
|
||
<span class="screenshot-switch-text">
|
||
<span class="screenshot-switch-title">任务完成后自动截图</span>
|
||
<span class="screenshot-switch-desc">启用后将在任务完成时自动保存截图</span>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="browse-type-modal-footer">
|
||
<button class="btn btn-secondary" onclick="closeBrowseTypeModal(event)">取消</button>
|
||
<button class="btn btn-start" onclick="confirmStartAll()">确认启动</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 图片预览浮窗 -->
|
||
<div class="image-preview-modal" id="imagePreviewModal" onclick="closeImagePreview(event)">
|
||
<div class="image-preview-content" id="imagePreviewContent">
|
||
<div class="image-preview-close" onclick="closeImagePreview(event)" title="关闭 (ESC)">×</div>
|
||
<div class="image-preview-controls">
|
||
<button onclick="zoomIn(event)" title="放大 (滚轮向上)">+</button>
|
||
<button onclick="zoomOut(event)" title="缩小 (滚轮向下)">−</button>
|
||
<button onclick="resetZoom(event)" title="重置 (双击图片)">↻</button>
|
||
</div>
|
||
<div class="image-preview-hint" id="imagePreviewHint">拖动图片 | 滚轮缩放 | 双击重置</div>
|
||
<img id="previewImage" src="" alt="预览">
|
||
<div class="image-preview-filename" id="previewFilename"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Toast 悬浮通知函数
|
||
function showToast(message, type = 'info', duration = 3000) {
|
||
const container = document.getElementById('toast-container');
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast ${type}`;
|
||
toast.innerHTML = `
|
||
<div class="toast-icon"></div>
|
||
<div class="toast-message">${message}</div>
|
||
`;
|
||
|
||
container.appendChild(toast);
|
||
|
||
// 自动移除
|
||
setTimeout(() => {
|
||
toast.classList.add('removing');
|
||
setTimeout(() => {
|
||
container.removeChild(toast);
|
||
}, 300);
|
||
}, duration);
|
||
}
|
||
|
||
// 向后兼容的alert别名(用于需要确认的重要提示)
|
||
const originalAlert = window.alert;
|
||
|
||
const socket = io();
|
||
let accounts = [];
|
||
|
||
// 连接成功
|
||
socket.on('connect', () => {
|
||
console.log('WebSocket已连接到服务器');
|
||
loadVipInfo();
|
||
loadAccounts();
|
||
loadScreenshots();
|
||
loadRunStats();
|
||
});
|
||
|
||
// 连接错误
|
||
socket.on('connect_error', (error) => {
|
||
console.error('WebSocket连接错误:', error);
|
||
});
|
||
|
||
// 断开连接
|
||
socket.on('disconnect', (reason) => {
|
||
console.warn('WebSocket已断开:', reason);
|
||
});
|
||
|
||
// 接收账号列表
|
||
socket.on('accounts_list', (data) => {
|
||
accounts = data;
|
||
renderAccounts();
|
||
loadRunStats();
|
||
});
|
||
|
||
// 接收账号更新
|
||
socket.on('account_update', (account) => {
|
||
const index = accounts.findIndex(a => a.id === account.id);
|
||
if (index !== -1) {
|
||
const oldStatus = accounts[index].status;
|
||
accounts[index] = account;
|
||
renderAccounts();
|
||
loadRunStats();
|
||
|
||
// 如果账号从运行中变为已完成,自动刷新截图列表
|
||
if (oldStatus === '运行中' && account.status === '已完成') {
|
||
console.log('账号任务完成,刷新截图列表');
|
||
setTimeout(loadScreenshots, 3000); // 3秒后刷新,给截图时间生成
|
||
}
|
||
}
|
||
});
|
||
|
||
// VIP functions
|
||
let vipInfo = {is_vip: false, expire_time: null, days_left: 0};
|
||
|
||
async function loadVipInfo() {
|
||
try {
|
||
const response = await fetch('/api/user/vip');
|
||
if (response.ok) {
|
||
vipInfo = await response.json();
|
||
updateVipDisplay();
|
||
// 显示用户名
|
||
if (vipInfo.username) {
|
||
document.getElementById('current-username').textContent = vipInfo.username;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load VIP info:', error);
|
||
}
|
||
}
|
||
|
||
function updateVipDisplay() {
|
||
const badge = document.getElementById('vip-badge');
|
||
if (vipInfo.is_vip) {
|
||
const daysLeft = vipInfo.days_left;
|
||
if (vipInfo.expire_time === '2099-12-31 23:59:59') {
|
||
badge.innerHTML = '<span class="vip-badge">永久VIP</span>';
|
||
} else {
|
||
badge.innerHTML = `<span class="vip-badge">VIP (${daysLeft}天)</span>`;
|
||
}
|
||
} else {
|
||
badge.innerHTML = '<span class="normal-badge">普通用户</span>';
|
||
checkAccountLimit();
|
||
}
|
||
}
|
||
|
||
function checkAccountLimit() {
|
||
if (!vipInfo.is_vip && accounts.length >= 1) {
|
||
const warning = document.createElement('div');
|
||
warning.className = 'account-limit-warning';
|
||
warning.innerHTML = '您是普通用户,已达到1个账号上限。如需添加更多账号,请联系管理员开通VIP。';
|
||
const accountsList = document.getElementById('accountsList');
|
||
if (accountsList && !document.querySelector('.account-limit-warning')) {
|
||
accountsList.insertAdjacentElement('beforebegin', warning);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function loadAccounts() {
|
||
const response = await fetch('/api/accounts');
|
||
accounts = await response.json();
|
||
renderAccounts();
|
||
}
|
||
|
||
// 渲染账号列表
|
||
function renderAccounts() {
|
||
const list = document.getElementById('accountsList');
|
||
// 移除旧的警告
|
||
const oldWarning = document.querySelector('.account-limit-warning');
|
||
if (oldWarning) oldWarning.remove();
|
||
|
||
if (accounts.length === 0) {
|
||
list.innerHTML = '<div class="empty-message">暂无账号,请先添加账号</div>';
|
||
return;
|
||
}
|
||
|
||
list.innerHTML = accounts.map(account => `
|
||
<div class="account-item">
|
||
<div class="account-info">
|
||
<div class="account-username">${account.username}</div>
|
||
<span class="account-status status-${getStatusClass(account.status)}">${account.status}</span>
|
||
${account.total_items > 0 ? `<span style="font-size: 11px; color: #666;">内容: ${account.total_items} 附件: ${account.total_attachments}</span>` : ''}
|
||
${account.remark ? `<div style="font-size: 11px; color: #888; margin-top: 3px;">备注: ${account.remark}</div>` : ''}
|
||
</div>
|
||
<div class="account-actions">
|
||
<select id="browse-type-${account.id}" ${account.is_running ? 'disabled' : ''}>
|
||
<option value="注册前未读">注册前未读</option>
|
||
<option value="应读" selected>应读</option>
|
||
<option value="未读">未读</option>
|
||
</select>
|
||
<button class="btn btn-start" onclick="startAccount('${account.id}')" ${account.is_running ? 'disabled' : ''}>
|
||
启动
|
||
</button>
|
||
<button class="btn btn-stop" onclick="stopAccount('${account.id}')" ${!account.is_running ? 'disabled' : ''}>
|
||
停止
|
||
</button>
|
||
<button class="btn btn-screenshot" onclick="takeScreenshot('${account.id}')" ${account.is_running ? 'disabled' : ''} title="手动截图">
|
||
📸
|
||
</button>
|
||
<button class="btn btn-delete" onclick="deleteAccount('${account.id}')" ${account.is_running ? 'disabled' : ''}>
|
||
删除
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
// 重新检查账号限制
|
||
checkAccountLimit();
|
||
}
|
||
|
||
// 获取状态样式类
|
||
function getStatusClass(status) {
|
||
if (status === '运行中') return 'running';
|
||
if (status === '排队中') return 'queued';
|
||
if (status === '已完成') return 'completed';
|
||
if (status.includes('失败') || status.includes('出错')) return 'error';
|
||
return 'idle';
|
||
}
|
||
|
||
// 添加账号
|
||
async function addAccount() {
|
||
const username = document.getElementById('username').value.trim();
|
||
const password = document.getElementById('password').value.trim();
|
||
|
||
if (!username || !password) {
|
||
alert('用户名和密码不能为空');
|
||
return;
|
||
}
|
||
|
||
const response = await fetch('/api/accounts', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ username, password, remember: true })
|
||
});
|
||
|
||
if (response.ok) {
|
||
document.getElementById('username').value = '';
|
||
document.getElementById('password').value = '';
|
||
loadVipInfo();
|
||
loadAccounts();
|
||
} else {
|
||
const error = await response.json();
|
||
showToast(error.error || '添加失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 启动账号
|
||
async function startAccount(accountId, browseType, enableScreenshot = null) {
|
||
// 如果没有指定浏览类型,从账号的下拉菜单获取
|
||
if (!browseType) {
|
||
const select = document.getElementById(`browse-type-${accountId}`);
|
||
browseType = select ? select.value : '应读';
|
||
}
|
||
|
||
// 如果没有明确指定截图开关,则从弹窗的checkbox获取(如果存在)
|
||
if (enableScreenshot === null) {
|
||
const checkbox = document.getElementById('enableScreenshotCheckbox');
|
||
enableScreenshot = checkbox ? checkbox.checked : true; // 默认启用
|
||
}
|
||
|
||
const response = await fetch(`/api/accounts/${accountId}/start`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
browse_type: browseType,
|
||
enable_screenshot: enableScreenshot
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
showToast(error.error || '启动失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 停止账号
|
||
async function stopAccount(accountId) {
|
||
await fetch(`/api/accounts/${accountId}/stop`, {
|
||
method: 'POST'
|
||
});
|
||
}
|
||
|
||
// 删除账号
|
||
async function deleteAccount(accountId, skipConfirm = false) {
|
||
// 如果不是批量删除,则需要确认
|
||
if (!skipConfirm && !confirm('确定要删除这个账号吗?')) return;
|
||
|
||
await fetch(`/api/accounts/${accountId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
// 如果不是批量删除,才刷新列表
|
||
if (!skipConfirm) {
|
||
loadVipInfo();
|
||
loadAccounts();
|
||
}
|
||
}
|
||
|
||
// 添加日志
|
||
// 加载运行统计
|
||
async function loadRunStats() {
|
||
try {
|
||
const response = await fetch('/api/run_stats');
|
||
if (response.ok) {
|
||
const stats = await response.json();
|
||
document.getElementById('todayCompleted').textContent = stats.today_completed || 0;
|
||
document.getElementById('currentRunning').textContent = stats.current_running || 0;
|
||
document.getElementById('todayFailed').textContent = stats.today_failed || 0;
|
||
document.getElementById('todayItems').textContent = stats.today_items || 0;
|
||
document.getElementById('todayAttachments').textContent = stats.today_attachments || 0;
|
||
}
|
||
} catch (error) {
|
||
console.error('加载运行统计失败:', error);
|
||
}
|
||
}
|
||
|
||
// 加载截图列表
|
||
async function loadScreenshots() {
|
||
try {
|
||
const response = await fetch("/api/screenshots?_t=" + Date.now());
|
||
const screenshots = await response.json();
|
||
|
||
const list = document.getElementById('screenshotsList');
|
||
|
||
if (!screenshots || screenshots.length === 0) {
|
||
list.innerHTML = '<div class="empty-message">暂无截图</div>';
|
||
return;
|
||
}
|
||
|
||
list.innerHTML = screenshots.map(s => `
|
||
<div class="screenshot-item">
|
||
<img src="/screenshots/${s.filename}"
|
||
class="screenshot-thumbnail"
|
||
onclick="openImagePreview('/screenshots/${s.filename}', '${s.filename}')"
|
||
title="点击查看大图"
|
||
alt="截图">
|
||
<div class="screenshot-info">
|
||
<div class="screenshot-name">${s.display_name || s.filename}</div>
|
||
<div class="screenshot-meta">${s.created}</div>
|
||
</div>
|
||
<div class="screenshot-actions">
|
||
<button class="btn btn-download" onclick="event.stopPropagation(); downloadScreenshot('${s.filename}')" title="下载">⬇</button>
|
||
<button class="btn btn-copy" onclick="event.stopPropagation(); copyScreenshotLink('${s.filename}')" title="复制">📋</button>
|
||
<button class="btn btn-delete-shot" onclick="event.stopPropagation(); deleteScreenshot('${s.filename}')" title="删除">🗑</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
} catch (error) {
|
||
console.error('加载截图失败:', error);
|
||
document.getElementById('screenshotsList').innerHTML = '<div class="empty-message">加载失败</div>';
|
||
}
|
||
}
|
||
|
||
// 手动截图
|
||
async function takeScreenshot(accountId) {
|
||
if (!confirm('确定要为此账号手动截图吗?')) return;
|
||
|
||
const browseTypeSelect = document.getElementById(`browse-type-${accountId}`);
|
||
const browseType = browseTypeSelect ? browseTypeSelect.value : '应读';
|
||
|
||
const response = await fetch(`/api/accounts/${accountId}/screenshot`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ browse_type: browseType })
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('截图任务已启动,请稍后刷新截图列表查看', 'success');
|
||
setTimeout(loadScreenshots, 5000);
|
||
} else {
|
||
const error = await response.json();
|
||
showToast(error.error || '截图失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 下载截图
|
||
function downloadScreenshot(filename) {
|
||
const link = document.createElement('a');
|
||
link.href = `/screenshots/${filename}`;
|
||
link.download = filename;
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
}
|
||
|
||
// 复制截图到剪贴板
|
||
async function copyScreenshotLink(filename) {
|
||
try {
|
||
const imageUrl = `/screenshots/${filename}`;
|
||
|
||
// 获取图片
|
||
const response = await fetch(imageUrl);
|
||
const blob = await response.blob();
|
||
|
||
// 检查浏览器是否支持 Clipboard API
|
||
if (!navigator.clipboard || !navigator.clipboard.write) {
|
||
throw new Error('浏览器不支持剪贴板API');
|
||
}
|
||
|
||
// 对于 JPEG 图片,需要转换为 PNG 格式才能复制到剪贴板
|
||
// 因为 Clipboard API 对 image/jpeg 的支持不够好
|
||
if (blob.type === 'image/jpeg' || filename.toLowerCase().endsWith('.jpg')) {
|
||
// 创建一个临时的 Image 对象
|
||
const img = new Image();
|
||
const canvas = document.createElement('canvas');
|
||
const ctx = canvas.getContext('2d');
|
||
|
||
// 创建一个 Promise 来等待图片加载
|
||
await new Promise((resolve, reject) => {
|
||
img.onload = resolve;
|
||
img.onerror = reject;
|
||
img.src = URL.createObjectURL(blob);
|
||
});
|
||
|
||
// 设置 canvas 尺寸
|
||
canvas.width = img.width;
|
||
canvas.height = img.height;
|
||
|
||
// 绘制图片到 canvas
|
||
ctx.drawImage(img, 0, 0);
|
||
|
||
// 从 canvas 获取 PNG blob
|
||
const pngBlob = await new Promise(resolve => {
|
||
canvas.toBlob(resolve, 'image/png');
|
||
});
|
||
|
||
// 复制 PNG 格式到剪贴板
|
||
await navigator.clipboard.write([
|
||
new ClipboardItem({
|
||
'image/png': pngBlob
|
||
})
|
||
]);
|
||
|
||
// 清理
|
||
URL.revokeObjectURL(img.src);
|
||
} else {
|
||
// PNG 格式直接复制
|
||
await navigator.clipboard.write([
|
||
new ClipboardItem({
|
||
[blob.type]: blob
|
||
})
|
||
]);
|
||
}
|
||
|
||
showToast('图片已复制到剪贴板!', 'success');
|
||
} catch (err) {
|
||
console.error('复制失败:', err);
|
||
|
||
// 降级方案:复制图片链接
|
||
const url = window.location.origin + `/screenshots/${filename}`;
|
||
try {
|
||
await navigator.clipboard.writeText(url);
|
||
showToast('图片复制失败,已复制图片链接到剪贴板', 'warning', 4000);
|
||
} catch (e) {
|
||
showToast('复制失败,请右键图片选择复制图像或手动下载', 'error');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 删除截图
|
||
async function deleteScreenshot(filename) {
|
||
if (!confirm(`确定删除截图 ${filename}?`)) return;
|
||
|
||
const response = await fetch(`/api/screenshots/${filename}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
if (response.ok) {
|
||
loadScreenshots();
|
||
} else {
|
||
showToast('删除失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 清空所有截图
|
||
async function clearAllScreenshots() {
|
||
if (!confirm('确定清空所有截图吗?此操作不可恢复!')) return;
|
||
|
||
const response = await fetch('/api/screenshots/clear', {
|
||
method: 'POST'
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
showToast(`已删除 ${result.deleted} 个截图`, 'success');
|
||
loadScreenshots();
|
||
} else {
|
||
showToast('清空失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 打开浏览类型选择弹窗
|
||
function startAllAccounts() {
|
||
if (accounts.length === 0) {
|
||
showToast('没有可启动的账号', 'warning');
|
||
return;
|
||
}
|
||
|
||
const idleAccounts = accounts.filter(a => !a.is_running);
|
||
|
||
if (idleAccounts.length === 0) {
|
||
showToast('所有账号都在运行中', 'info');
|
||
return;
|
||
}
|
||
|
||
// 显示弹窗
|
||
const modal = document.getElementById('browseTypeModal');
|
||
modal.classList.add('active');
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
|
||
// 关闭浏览类型选择弹窗
|
||
function closeBrowseTypeModal(event) {
|
||
if (event.target.id === 'browseTypeModal' ||
|
||
event.target.classList.contains('browse-type-modal-close') ||
|
||
event.target.textContent === '取消') {
|
||
const modal = document.getElementById('browseTypeModal');
|
||
modal.classList.remove('active');
|
||
document.body.style.overflow = '';
|
||
event.stopPropagation();
|
||
}
|
||
}
|
||
|
||
// 确认启动所有账号
|
||
async function confirmStartAll() {
|
||
const browseType = document.querySelector('input[name="modal_browse_type"]:checked').value;
|
||
const idleAccounts = accounts.filter(a => !a.is_running);
|
||
|
||
// 关闭弹窗
|
||
const modal = document.getElementById('browseTypeModal');
|
||
modal.classList.remove('active');
|
||
document.body.style.overflow = '';
|
||
|
||
// 启动所有账号
|
||
for (const account of idleAccounts) {
|
||
await startAccount(account.id, browseType);
|
||
await new Promise(resolve => setTimeout(resolve, 500));
|
||
}
|
||
|
||
showToast(`已启动 ${idleAccounts.length} 个账号,浏览类型: ${browseType}`, 'success');
|
||
}
|
||
|
||
// 全部停止
|
||
async function stopAllAccounts() {
|
||
const runningAccounts = accounts.filter(a => a.is_running);
|
||
|
||
if (runningAccounts.length === 0) {
|
||
showToast('没有正在运行的账号', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (!confirm(`确定要停止 ${runningAccounts.length} 个正在运行的账号吗?`)) return;
|
||
|
||
for (const account of runningAccounts) {
|
||
await stopAccount(account.id);
|
||
}
|
||
|
||
showToast(`已停止 ${runningAccounts.length} 个账号`, 'success');
|
||
}
|
||
|
||
// 清空所有账号
|
||
async function clearAllAccounts() {
|
||
if (accounts.length === 0) {
|
||
showToast('没有账号可清空', 'warning');
|
||
return;
|
||
}
|
||
|
||
const runningAccounts = accounts.filter(a => a.is_running);
|
||
if (runningAccounts.length > 0) {
|
||
showToast(`有 ${runningAccounts.length} 个账号正在运行,请先停止所有账号`, 'warning');
|
||
return;
|
||
}
|
||
|
||
// 只确认一次
|
||
if (!confirm(`确定要删除所有 ${accounts.length} 个账号吗?此操作不可恢复!`)) return;
|
||
|
||
const accountIds = [...accounts.map(a => a.id)];
|
||
|
||
// 批量删除,传入 skipConfirm=true 跳过每个账号的确认
|
||
for (const accountId of accountIds) {
|
||
await deleteAccount(accountId, true); // 跳过确认,不刷新界面
|
||
}
|
||
|
||
// 所有账号删除完成后,统一刷新一次
|
||
loadVipInfo();
|
||
loadAccounts();
|
||
showToast('已清空所有账号', 'success');
|
||
}
|
||
|
||
// 退出登录
|
||
async function logout() {
|
||
const response = await fetch('/api/logout', {
|
||
method: 'POST'
|
||
});
|
||
|
||
if (response.ok) {
|
||
window.location.href = '/login';
|
||
}
|
||
}
|
||
|
||
// 清空日志
|
||
// 图片缩放和拖动状态
|
||
let imageScale = 1;
|
||
let imageTranslateX = 0;
|
||
let imageTranslateY = 0;
|
||
let isDragging = false;
|
||
let startX = 0;
|
||
let startY = 0;
|
||
let lastTouchDistance = 0;
|
||
|
||
// 打开图片预览浮窗
|
||
function openImagePreview(imageUrl, filename) {
|
||
const modal = document.getElementById('imagePreviewModal');
|
||
const previewImage = document.getElementById('previewImage');
|
||
const previewFilename = document.getElementById('previewFilename');
|
||
const hint = document.getElementById('imagePreviewHint');
|
||
|
||
previewImage.src = imageUrl;
|
||
previewFilename.textContent = filename;
|
||
modal.classList.add('active');
|
||
|
||
// 重置缩放和位置
|
||
imageScale = 1;
|
||
imageTranslateX = 0;
|
||
imageTranslateY = 0;
|
||
updateImageTransform();
|
||
|
||
// 禁止页面滚动
|
||
document.body.style.overflow = 'hidden';
|
||
|
||
// 显示提示3秒后隐藏
|
||
hint.classList.add('show');
|
||
setTimeout(() => {
|
||
hint.classList.remove('show');
|
||
}, 3000);
|
||
|
||
// 添加事件监听
|
||
setupImageEvents();
|
||
}
|
||
|
||
// 关闭图片预览浮窗
|
||
function closeImagePreview(event) {
|
||
// 只有点击背景或关闭按钮时才关闭
|
||
if (event.target.classList.contains('image-preview-modal') ||
|
||
event.target.classList.contains('image-preview-close')) {
|
||
const modal = document.getElementById('imagePreviewModal');
|
||
modal.classList.remove('active');
|
||
|
||
// 恢复页面滚动
|
||
document.body.style.overflow = '';
|
||
|
||
// 清空图片源(释放内存)
|
||
setTimeout(() => {
|
||
document.getElementById('previewImage').src = '';
|
||
}, 300);
|
||
|
||
// 移除事件监听
|
||
removeImageEvents();
|
||
}
|
||
}
|
||
|
||
// 更新图片变换
|
||
function updateImageTransform() {
|
||
const previewImage = document.getElementById('previewImage');
|
||
previewImage.style.transform = `translate(${imageTranslateX}px, ${imageTranslateY}px) scale(${imageScale})`;
|
||
}
|
||
|
||
// 放大
|
||
function zoomIn(event) {
|
||
event.stopPropagation();
|
||
imageScale = Math.min(imageScale * 1.2, 5);
|
||
updateImageTransform();
|
||
}
|
||
|
||
// 缩小
|
||
function zoomOut(event) {
|
||
event.stopPropagation();
|
||
imageScale = Math.max(imageScale / 1.2, 0.5);
|
||
updateImageTransform();
|
||
}
|
||
|
||
// 重置缩放
|
||
function resetZoom(event) {
|
||
event.stopPropagation();
|
||
imageScale = 1;
|
||
imageTranslateX = 0;
|
||
imageTranslateY = 0;
|
||
updateImageTransform();
|
||
}
|
||
|
||
// 鼠标滚轮缩放
|
||
function handleWheel(event) {
|
||
event.preventDefault();
|
||
if (event.deltaY < 0) {
|
||
imageScale = Math.min(imageScale * 1.1, 5);
|
||
} else {
|
||
imageScale = Math.max(imageScale / 1.1, 0.5);
|
||
}
|
||
updateImageTransform();
|
||
}
|
||
|
||
// 鼠标拖动开始
|
||
function handleMouseDown(event) {
|
||
if (event.target.id !== 'previewImage') return;
|
||
event.preventDefault(); // 防止默认行为(如文本选择、拖拽等)
|
||
isDragging = true;
|
||
startX = event.clientX - imageTranslateX;
|
||
startY = event.clientY - imageTranslateY;
|
||
event.target.style.cursor = 'grabbing';
|
||
}
|
||
|
||
// 鼠标拖动
|
||
function handleMouseMove(event) {
|
||
if (!isDragging) return;
|
||
event.preventDefault(); // 防止默认行为
|
||
imageTranslateX = event.clientX - startX;
|
||
imageTranslateY = event.clientY - startY;
|
||
updateImageTransform();
|
||
}
|
||
|
||
// 鼠标拖动结束
|
||
function handleMouseUp(event) {
|
||
if (isDragging) {
|
||
isDragging = false;
|
||
const previewImage = document.getElementById('previewImage');
|
||
if (previewImage) {
|
||
previewImage.style.cursor = 'move';
|
||
}
|
||
}
|
||
}
|
||
|
||
// 触摸开始
|
||
function handleTouchStart(event) {
|
||
if (event.target.id !== 'previewImage') return;
|
||
|
||
if (event.touches.length === 1) {
|
||
// 单指拖动
|
||
isDragging = true;
|
||
startX = event.touches[0].clientX - imageTranslateX;
|
||
startY = event.touches[0].clientY - imageTranslateY;
|
||
} else if (event.touches.length === 2) {
|
||
// 双指缩放
|
||
isDragging = false;
|
||
const touch1 = event.touches[0];
|
||
const touch2 = event.touches[1];
|
||
lastTouchDistance = Math.hypot(
|
||
touch2.clientX - touch1.clientX,
|
||
touch2.clientY - touch1.clientY
|
||
);
|
||
}
|
||
}
|
||
|
||
// 触摸移动
|
||
function handleTouchMove(event) {
|
||
event.preventDefault();
|
||
|
||
if (event.touches.length === 1 && isDragging) {
|
||
// 单指拖动
|
||
imageTranslateX = event.touches[0].clientX - startX;
|
||
imageTranslateY = event.touches[0].clientY - startY;
|
||
updateImageTransform();
|
||
} else if (event.touches.length === 2) {
|
||
// 双指缩放
|
||
const touch1 = event.touches[0];
|
||
const touch2 = event.touches[1];
|
||
const distance = Math.hypot(
|
||
touch2.clientX - touch1.clientX,
|
||
touch2.clientY - touch1.clientY
|
||
);
|
||
|
||
if (lastTouchDistance > 0) {
|
||
const scale = distance / lastTouchDistance;
|
||
imageScale = Math.max(0.5, Math.min(5, imageScale * scale));
|
||
updateImageTransform();
|
||
}
|
||
lastTouchDistance = distance;
|
||
}
|
||
}
|
||
|
||
// 触摸结束
|
||
function handleTouchEnd(event) {
|
||
isDragging = false;
|
||
if (event.touches.length < 2) {
|
||
lastTouchDistance = 0;
|
||
}
|
||
}
|
||
|
||
// 双击重置
|
||
function handleDblClick(event) {
|
||
if (event.target.id === 'previewImage') {
|
||
resetZoom(event);
|
||
}
|
||
}
|
||
|
||
// 设置图片事件监听
|
||
function setupImageEvents() {
|
||
const previewImage = document.getElementById('previewImage');
|
||
const modal = document.getElementById('imagePreviewModal');
|
||
|
||
// PC端事件
|
||
modal.addEventListener('wheel', handleWheel, { passive: false });
|
||
previewImage.addEventListener('mousedown', handleMouseDown);
|
||
document.addEventListener('mousemove', handleMouseMove);
|
||
document.addEventListener('mouseup', handleMouseUp);
|
||
previewImage.addEventListener('dblclick', handleDblClick);
|
||
|
||
// 移动端事件
|
||
previewImage.addEventListener('touchstart', handleTouchStart, { passive: false });
|
||
previewImage.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||
previewImage.addEventListener('touchend', handleTouchEnd);
|
||
}
|
||
|
||
// 移除图片事件监听
|
||
function removeImageEvents() {
|
||
const previewImage = document.getElementById('previewImage');
|
||
const modal = document.getElementById('imagePreviewModal');
|
||
|
||
if (modal) {
|
||
modal.removeEventListener('wheel', handleWheel);
|
||
}
|
||
if (previewImage) {
|
||
previewImage.removeEventListener('mousedown', handleMouseDown);
|
||
previewImage.removeEventListener('dblclick', handleDblClick);
|
||
previewImage.removeEventListener('touchstart', handleTouchStart);
|
||
previewImage.removeEventListener('touchmove', handleTouchMove);
|
||
previewImage.removeEventListener('touchend', handleTouchEnd);
|
||
}
|
||
document.removeEventListener('mousemove', handleMouseMove);
|
||
document.removeEventListener('mouseup', handleMouseUp);
|
||
}
|
||
|
||
// ESC键关闭浮窗
|
||
document.addEventListener('keydown', (event) => {
|
||
if (event.key === 'Escape') {
|
||
// 关闭浏览类型选择弹窗
|
||
const browseTypeModal = document.getElementById('browseTypeModal');
|
||
if (browseTypeModal.classList.contains('active')) {
|
||
browseTypeModal.classList.remove('active');
|
||
document.body.style.overflow = '';
|
||
return;
|
||
}
|
||
|
||
// 关闭图片预览浮窗
|
||
const imageModal = document.getElementById('imagePreviewModal');
|
||
if (imageModal.classList.contains('active')) {
|
||
imageModal.classList.remove('active');
|
||
document.body.style.overflow = '';
|
||
setTimeout(() => {
|
||
document.getElementById('previewImage').src = '';
|
||
}, 300);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 页面加载时初始化
|
||
window.addEventListener('load', () => {
|
||
loadVipInfo();
|
||
loadAccounts();
|
||
loadScreenshots();
|
||
|
||
// 定期刷新截图列表(每5秒,更快响应)
|
||
setInterval(loadScreenshots, 5000);
|
||
|
||
// 定期刷新账号状态(每3秒)
|
||
setInterval(loadAccounts, 3000);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|