Files
zsglpt/templates/index.html
Yu Yon 0fd7137cea Initial commit: 知识管理平台
主要功能:
- 多用户管理系统
- 浏览器自动化(Playwright)
- 任务编排和执行
- Docker容器化部署
- 数据持久化和日志管理

技术栈:
- Flask 3.0.0
- Playwright 1.40.0
- SQLite with connection pooling
- Docker + Docker Compose

部署说明详见README.md
2025-11-16 19:03:07 +08:00

2335 lines
72 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-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>