Files
zsglpt/templates/index.html.backup2
Yu Yon b5344cd55e 修复所有bug并添加新功能
- 修复添加账号按钮无反应问题
- 添加账号备注字段(可选)
- 添加账号设置按钮(修改密码/备注)
- 修复用户反馈���能
- 添加定时任务执行日志
- 修复容器重启后账号加载问题
- 修复所有JavaScript语法错误
- 优化账号加载机制(4层保障)

🤖 Generated with Claude Code
2025-12-10 11:19:16 +08:00

1474 lines
80 KiB
Plaintext
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">
<title>知识管理平台</title>
<script src="/static/js/socket.io.min.js"></script>
<style>
:root {
--md-primary: #1976D2;
--md-primary-light: #63A4FF;
--md-primary-dark: #004BA0;
--md-success: #4CAF50;
--md-warning: #FF9800;
--md-error: #F44336;
--md-surface: #FFFFFF;
--md-background: #F5F5F5;
--md-on-primary: #FFFFFF;
--md-on-surface: #212121;
--md-on-surface-medium: #757575;
--md-on-surface-light: #9E9E9E;
--md-divider: #E0E0E0;
--md-shadow-1: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
--md-shadow-2: 0 3px 6px rgba(0,0,0,0.15), 0 2px 4px rgba(0,0,0,0.12);
--md-shadow-3: 0 10px 20px rgba(0,0,0,0.15), 0 3px 6px rgba(0,0,0,0.10);
--sidebar-width: 220px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--md-background);
color: var(--md-on-surface);
min-height: 100vh;
line-height: 1.5;
}
.header {
background: var(--md-primary);
color: var(--md-on-primary);
padding: 0 24px;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: var(--md-shadow-2);
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
}
.header-title { font-size: 20px; font-weight: 500; display: flex; align-items: center; gap: 8px; }
.header-actions { display: flex; align-items: center; gap: 16px; }
.header-actions .btn { color: white; }
.header-actions .btn-outlined { border-color: rgba(255,255,255,0.7); }
.header-actions .btn-outlined:hover { background: rgba(255,255,255,0.1); }
.header-actions .btn-text:hover { background: rgba(255,255,255,0.1); }
.user-info { display: flex; align-items: center; gap: 8px; font-size: 14px; }
.vip-badge { background: linear-gradient(135deg, #FFD700, #FFA500); color: #000; padding: 2px 8px; border-radius: 999px; font-size: 12px; font-weight: 600; cursor: pointer; }
.vip-badge:hover { box-shadow: 0 2px 8px rgba(255,215,0,0.5); }
.normal-badge { background: #E0E0E0; color: #616161; padding: 2px 8px; border-radius: 999px; font-size: 12px; cursor: pointer; }
.normal-badge:hover { background: #BDBDBD; }
.vip-expire-warning { color: #FF9800; font-size: 11px; margin-left: 4px; }
.vip-feature { position: relative; }
.vip-feature.locked::after { content: '🔒'; position: absolute; top: -8px; right: -8px; font-size: 12px; }
.vip-feature.locked { opacity: 0.6; pointer-events: none; }
.vip-tooltip { position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); background: #333; color: white; padding: 8px 12px; border-radius: 4px; font-size: 12px; white-space: nowrap; opacity: 0; visibility: hidden; transition: all 0.2s; z-index: 100; }
.vip-feature:hover .vip-tooltip { opacity: 1; visibility: visible; }
.upgrade-banner { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 12px 24px; border-radius: 8px; margin-bottom: 24px; display: flex; align-items: center; justify-content: space-between; box-shadow: var(--md-shadow-2); }
.upgrade-banner-text { font-size: 14px; }
.upgrade-banner-text strong { font-size: 16px; }
.upgrade-banner .btn { background: white; color: #667eea; }
.upgrade-banner .btn:hover { background: #f5f5f5; }
.layout { display: flex; padding-top: 56px; min-height: 100vh; }
.sidebar {
width: var(--sidebar-width);
background: var(--md-surface);
border-right: 1px solid var(--md-divider);
position: fixed;
top: 56px;
bottom: 0;
left: 0;
overflow-y: auto;
}
.nav-item {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 24px;
cursor: pointer;
transition: all 0.2s;
border-left: 3px solid transparent;
color: var(--md-on-surface-medium);
}
.nav-item:hover { background: rgba(0,0,0,0.04); }
.nav-item.active { background: rgba(25,118,210,0.08); color: var(--md-primary); border-left-color: var(--md-primary); }
.nav-icon { font-size: 20px; width: 24px; text-align: center; }
.nav-label { font-size: 14px; font-weight: 500; }
.main-content { flex: 1; margin-left: var(--sidebar-width); padding: 24px; max-width: calc(100% - var(--sidebar-width)); }
.tab-pane { display: none; }
.tab-pane.active { display: block; }
.card { background: var(--md-surface); border-radius: 8px; box-shadow: var(--md-shadow-1); padding: 24px; margin-bottom: 24px; }
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; flex-wrap: wrap; gap: 12px; }
.card-title { font-size: 18px; font-weight: 500; }
.stats-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 16px; margin-bottom: 24px; }
.stat-card { background: var(--md-surface); border-radius: 8px; padding: 16px; box-shadow: var(--md-shadow-1); text-align: center; }
.stat-value { font-size: 28px; font-weight: 600; color: var(--md-primary); }
.stat-label { font-size: 13px; color: var(--md-on-surface-medium); margin-top: 4px; }
.stat-card.success .stat-value { color: var(--md-success); }
.stat-card.error .stat-value { color: var(--md-error); }
.toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; padding: 16px; background: var(--md-surface); border-radius: 8px; box-shadow: var(--md-shadow-1); margin-bottom: 24px; }
.toolbar-group { display: flex; align-items: center; gap: 8px; }
.toolbar-divider { width: 1px; height: 32px; background: var(--md-divider); margin: 0 8px; }
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 4px; padding: 8px 16px; border: none; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; white-space: nowrap; }
.btn-primary { background: var(--md-primary); color: var(--md-on-primary); }
.btn-primary:hover { background: var(--md-primary-dark); box-shadow: var(--md-shadow-2); }
.btn-success { background: var(--md-success); color: white; }
.btn-success:hover { background: #388E3C; }
.btn-outlined { background: transparent; color: var(--md-primary); border: 1px solid var(--md-primary); }
.btn-outlined:hover { background: rgba(25,118,210,0.08); }
.btn-text { background: transparent; color: var(--md-primary); }
.btn-text:hover { background: rgba(25,118,210,0.08); }
.btn-danger { background: var(--md-error); color: white; }
.btn-danger:hover { background: #D32F2F; }
.btn-icon { width: 36px; height: 36px; padding: 0; border-radius: 50%; }
.btn-small { padding: 4px 8px; font-size: 12px; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.form-group { margin-bottom: 16px; }
.form-label { display: block; font-size: 14px; font-weight: 500; color: var(--md-on-surface-medium); margin-bottom: 4px; }
.form-input, .form-select { width: 100%; padding: 10px 12px; border: 1px solid var(--md-divider); border-radius: 4px; font-size: 14px; transition: border-color 0.2s; }
.form-input:focus, .form-select:focus { outline: none; border-color: var(--md-primary); box-shadow: 0 0 0 2px rgba(25,118,210,0.2); }
.select-inline { padding: 6px 10px; border: 1px solid var(--md-divider); border-radius: 4px; font-size: 14px; background: white; }
.checkbox-wrapper { display: flex; align-items: center; gap: 8px; cursor: pointer; }
.checkbox-wrapper input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--md-primary); }
.switch { display: flex; align-items: center; gap: 8px; cursor: pointer; }
.switch-track { width: 36px; height: 20px; background: var(--md-on-surface-light); border-radius: 10px; position: relative; transition: background 0.2s; }
.switch input:checked + .switch-track { background: var(--md-primary); }
.switch-thumb { width: 16px; height: 16px; background: white; border-radius: 50%; position: absolute; top: 2px; left: 2px; transition: transform 0.2s; box-shadow: var(--md-shadow-1); }
.switch input:checked + .switch-track .switch-thumb { transform: translateX(16px); }
.switch input { display: none; }
.accounts-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 16px; }
.account-card { background: var(--md-surface); border-radius: 8px; box-shadow: var(--md-shadow-1); overflow: hidden; transition: box-shadow 0.2s; }
.account-card:hover { box-shadow: var(--md-shadow-2); }
.account-card-header { display: flex; align-items: center; gap: 16px; padding: 16px; border-bottom: 1px solid var(--md-divider); }
.account-info { flex: 1; min-width: 0; }
.account-username { font-size: 16px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.account-remark { font-size: 13px; color: var(--md-on-surface-medium); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.status-chip { display: inline-flex; align-items: center; padding: 4px 12px; border-radius: 999px; font-size: 12px; font-weight: 500; }
.status-idle { background: #EEEEEE; color: #616161; }
.status-running { background: #E3F2FD; color: #1565C0; }
.status-queued { background: #FFF3E0; color: #EF6C00; }
.status-stopping { background: #FFEBEE; color: #C62828; }
.status-completed { background: #E8F5E9; color: #2E7D32; }
.status-error { background: #FFEBEE; color: #C62828; }
.progress-section { padding: 16px; background: #FAFAFA; border-bottom: 1px solid var(--md-divider); }
.progress-stage { font-size: 13px; color: var(--md-primary); font-weight: 500; margin-bottom: 8px; }
.progress-bar { height: 4px; background: #E0E0E0; border-radius: 2px; overflow: hidden; margin-bottom: 8px; }
.progress-fill { height: 100%; background: var(--md-primary); transition: width 0.3s ease; }
.progress-details { display: flex; flex-wrap: wrap; gap: 16px; font-size: 12px; color: var(--md-on-surface-medium); }
.account-card-actions { display: flex; align-items: center; gap: 8px; padding: 16px; flex-wrap: wrap; }
.account-card-actions .select-inline { flex: 1; min-width: 100px; }
.schedule-card { background: var(--md-surface); border-radius: 8px; box-shadow: var(--md-shadow-1); padding: 16px; margin-bottom: 16px; display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.schedule-info { flex: 1; min-width: 200px; }
.schedule-name { font-size: 16px; font-weight: 500; margin-bottom: 4px; }
.schedule-meta { font-size: 13px; color: var(--md-on-surface-medium); display: flex; flex-wrap: wrap; gap: 16px; }
.schedule-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.screenshots-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; }
.screenshot-item { background: var(--md-surface); border-radius: 8px; box-shadow: var(--md-shadow-1); overflow: hidden; transition: transform 0.2s; }
.screenshot-item:hover { transform: translateY(-2px); box-shadow: var(--md-shadow-2); }
.screenshot-img { width: 100%; aspect-ratio: 16/9; object-fit: cover; cursor: pointer; }
.screenshot-info { padding: 12px; }
.screenshot-name { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 4px; }
.screenshot-time { font-size: 12px; color: var(--md-on-surface-medium); margin-bottom: 8px; }
.screenshot-actions { display: flex; gap: 4px; flex-wrap: wrap; }
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 2000; opacity: 0; visibility: hidden; transition: all 0.2s; }
.modal-overlay.active { opacity: 1; visibility: visible; }
.modal { background: var(--md-surface); border-radius: 16px; box-shadow: var(--md-shadow-3); width: 90%; max-width: 480px; max-height: 90vh; overflow: auto; transform: translateY(-20px); transition: transform 0.2s; }
.modal-overlay.active .modal { transform: translateY(0); }
.modal-header { padding: 24px; border-bottom: 1px solid var(--md-divider); }
.modal-title { font-size: 20px; font-weight: 500; }
.modal-body { padding: 24px; }
.modal-footer { padding: 16px 24px; border-top: 1px solid var(--md-divider); display: flex; justify-content: flex-end; gap: 8px; }
.weekday-selector { display: flex; flex-wrap: wrap; gap: 8px; }
.weekday-chip { display: flex; align-items: center; gap: 4px; padding: 6px 12px; border: 1px solid var(--md-divider); border-radius: 999px; cursor: pointer; transition: all 0.2s; font-size: 13px; }
.weekday-chip:has(input:checked) { background: var(--md-primary); border-color: var(--md-primary); color: white; }
.weekday-chip input { display: none; }
.account-select-list { max-height: 200px; overflow-y: auto; border: 1px solid var(--md-divider); border-radius: 4px; }
.account-select-item { display: flex; align-items: center; gap: 8px; padding: 8px 16px; border-bottom: 1px solid var(--md-divider); }
.account-select-item:last-child { border-bottom: none; }
.toast-container { position: fixed; bottom: 24px; right: 24px; z-index: 3000; display: flex; flex-direction: column; gap: 8px; }
.toast { background: #323232; color: white; padding: 16px 24px; border-radius: 4px; box-shadow: var(--md-shadow-2); animation: slideIn 0.3s ease; }
.toast.success { background: var(--md-success); }
.toast.error { background: var(--md-error); }
.toast.warning { background: var(--md-warning); }
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
.fab { position: fixed; bottom: 32px; right: 32px; min-width: 140px; height: 56px; border-radius: 28px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; font-size: 16px; font-weight: 600; cursor: pointer; box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); transition: all 0.3s; z-index: 100; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 0 20px; }
.fab-icon { font-size: 24px; }
.fab-text { font-size: 15px; letter-spacing: 0.5px; }
.fab:hover { background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6); transform: translateY(-2px); }
.empty-state { text-align: center; padding: 32px; color: var(--md-on-surface-medium); }
.empty-state-icon { font-size: 48px; margin-bottom: 16px; }
/* 图片预览增强样式 */
.image-preview-modal { background: rgba(0,0,0,0.9); }
.image-preview-container { position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; overflow: hidden; }
.preview-image { max-width: 90%; max-height: 85vh; object-fit: contain; cursor: grab; transition: transform 0.1s ease-out; user-select: none; -webkit-user-drag: none; }
.preview-image.dragging { cursor: grabbing; transition: none; }
.preview-controls { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); display: flex; gap: 8px; background: rgba(0,0,0,0.7); padding: 8px 16px; border-radius: 999px; z-index: 2001; }
.preview-controls .btn { color: white; background: transparent; border: 1px solid rgba(255,255,255,0.3); }
.preview-controls .btn:hover { background: rgba(255,255,255,0.1); }
.preview-close { position: fixed; top: 24px; right: 24px; width: 48px; height: 48px; border-radius: 50%; background: rgba(0,0,0,0.5); color: white; border: none; font-size: 24px; cursor: pointer; z-index: 2001; }
.preview-close:hover { background: rgba(0,0,0,0.7); }
.zoom-info { position: fixed; top: 24px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.7); color: white; padding: 8px 16px; border-radius: 4px; font-size: 14px; z-index: 2001; }
/* 反馈历史样式 */
.feedback-list { max-height: 300px; overflow-y: auto; }
.feedback-item { padding: 12px; border-bottom: 1px solid var(--md-divider); }
.feedback-item:last-child { border-bottom: none; }
.feedback-item-title { font-weight: 500; margin-bottom: 4px; }
.feedback-item-time { font-size: 12px; color: var(--md-on-surface-medium); }
.feedback-item-status { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; margin-left: 8px; }
.feedback-status-pending { background: #FFF3E0; color: #EF6C00; }
.feedback-status-replied { background: #E8F5E9; color: #2E7D32; }
@media (max-width: 768px) {
.sidebar { width: 100%; position: fixed; top: auto; bottom: 0; height: 60px; border-right: none; border-top: 1px solid var(--md-divider); display: flex; overflow-x: auto; }
.nav-item { flex-direction: column; padding: 8px 16px; border-left: none; border-top: 3px solid transparent; min-width: 80px; text-align: center; }
.nav-item.active { border-left-color: transparent; border-top-color: var(--md-primary); }
.nav-label { font-size: 11px; }
.main-content { margin-left: 0; margin-bottom: 60px; max-width: 100%; }
.accounts-grid { grid-template-columns: 1fr; }
.toolbar { flex-direction: column; align-items: stretch; }
.toolbar-divider { display: none; }
.fab { bottom: 80px; }
.preview-controls { bottom: 80px; }
}
</style>
</head>
<body>
<header class="header">
<div class="header-title"><span>📚</span><span>知识管理平台</span></div>
<div class="header-actions">
<div class="user-info">
<span class="user-avatar">👤</span><span id="usernameDisplay" style="font-weight: 500;">加载中...</span>
<span id="vipBadge" class="vip-badge" style="display: none;" onclick="showVipInfo()" title="点击查看VIP详情">VIP</span>
<span id="normalBadge" class="normal-badge" style="display: none;" onclick="showUpgradeModal()" title="点击升级VIP">普通用户</span>
<span id="vipExpireWarning" class="vip-expire-warning" style="display: none;"></span>
</div>
<button class="btn btn-text" onclick="openFeedbackModal()">反馈</button>
<button class="btn btn-outlined" onclick="logout()">退出</button>
</div>
</header>
<div class="layout">
<nav class="sidebar">
<div class="nav-item active" data-tab="accounts"><span class="nav-icon">👤</span><span class="nav-label">账号管理</span></div>
<div class="nav-item" data-tab="schedule"><span class="nav-icon">⏰</span><span class="nav-label">定时任务</span></div>
<div class="nav-item" data-tab="screenshots"><span class="nav-icon">📸</span><span class="nav-label">截图管理</span></div>
</nav>
<main class="main-content">
<section id="tab-accounts" class="tab-pane active">
<div class="stats-row">
<div class="stat-card success"><div class="stat-value" id="statCompleted">0</div><div class="stat-label">今日完成</div></div>
<div class="stat-card"><div class="stat-value" id="statRunning">0</div><div class="stat-label">正在运行</div></div>
<div class="stat-card error"><div class="stat-value" id="statFailed">0</div><div class="stat-label">今日失败</div></div>
<div class="stat-card"><div class="stat-value" id="statItems">0</div><div class="stat-label">浏览内容</div></div>
<div class="stat-card"><div class="stat-value" id="statAttachments">0</div><div class="stat-label">查看附件</div></div>
<div class="stat-card"><div class="stat-value" id="accountLimitLarge">0/3</div><div class="stat-label">账号数量</div></div>
</div>
<div class="upgrade-banner" id="upgradeBanner" style="display: none;">
<div class="upgrade-banner-text">
<strong>升级VIP解锁更多功能</strong><br>
<span>无限账号 · 优先排队 · 定时任务 · 批量操作</span>
</div>
<button class="btn" onclick="showUpgradeModal()">了解VIP特权</button>
</div>
<div class="toolbar">
<div class="toolbar-group">
<label class="checkbox-wrapper"><input type="checkbox" id="selectAll" onchange="toggleSelectAll()"><span>全选</span></label>
<span style="color: var(--md-on-surface-medium); font-size: 13px;">已选 <span id="selectedCount">0</span> 个</span>
</div>
<div class="toolbar-divider"></div>
<div class="toolbar-group">
<select class="select-inline" id="batchBrowseType">
<option value="应读">应读</option>
<option value="未读">未读</option>
<option value="注册前未读">注册前未读</option>
</select>
<label class="switch">
<input type="checkbox" id="batchScreenshot" checked>
<span class="switch-track"><span class="switch-thumb"></span></span>
<span>截图</span>
</label>
</div>
<div class="toolbar-divider"></div>
<div class="toolbar-group">
<button class="btn btn-primary" onclick="batchStart()">批量启动</button>
<button class="btn btn-outlined" onclick="batchStop()">批量停止</button>
<button class="btn btn-success" onclick="startAllAccounts()">全部启动</button>
<button class="btn btn-danger" onclick="stopAllAccounts()">全部停止</button>
</div>
<div class="toolbar-divider"></div>
<div class="toolbar-group">
<button class="btn btn-text" onclick="clearAllAccounts()" title="清空所有账号">🗑️ 清空</button>
</div>
</div>
<div class="accounts-grid" id="accountsList"></div>
<div class="empty-state" id="emptyAccounts" style="display: none;">
<div class="empty-state-icon">📭</div>
<p>暂无账号,点击右下角按钮添加</p>
</div>
</section>
<section id="tab-schedule" class="tab-pane">
<div class="card">
<div class="card-header">
<h2 class="card-title">我的定时任务</h2>
<button class="btn btn-primary" onclick="openScheduleModal()">新建任务</button>
</div>
<div id="scheduleList"></div>
<div class="empty-state" id="emptySchedules" style="display: none;">
<div class="empty-state-icon">⏰</div>
<p>暂无定时任务,点击上方按钮新建</p>
</div>
</div>
</section>
<section id="tab-screenshots" class="tab-pane">
<div class="card">
<div class="card-header">
<h2 class="card-title">截图管理</h2>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<button class="btn btn-outlined" onclick="refreshScreenshots()">刷新</button>
<button class="btn btn-danger" onclick="clearScreenshots()">清空全部</button>
</div>
</div>
<div class="screenshots-grid" id="screenshotsList"></div>
<div class="empty-state" id="emptyScreenshots" style="display: none;">
<div class="empty-state-icon">📷</div>
<p>暂无截图</p>
</div>
</div>
</section>
</main>
</div>
<button class="fab" onclick="openAddAccountModal()" title="添加账号"><span class="fab-icon">+</span><span class="fab-text">添加账号</span></button>
<div class="toast-container" id="toastContainer"></div>
<!-- 添加账号弹窗 -->
<div class="modal-overlay" id="addAccountModal">
<div class="modal">
<div class="modal-header"><h3 class="modal-title">添加账号</h3></div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">账号</label>
<input type="text" class="form-input" id="newAccountUsername" placeholder="请输入账号">
</div>
<div class="form-group">
<label class="form-label">密码</label>
<input type="password" class="form-input" id="newAccountPassword" placeholder="请输入密码">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-text" onclick="closeModal('addAccountModal')">取消</button>
<button class="btn btn-primary" onclick="addAccount()">添加</button>
</div>
</div>
</div>
<!-- 定时任务弹窗 -->
<div class="modal-overlay" id="scheduleModal">
<div class="modal" style="max-width: 560px;">
<div class="modal-header"><h3 class="modal-title" id="scheduleModalTitle">新建定时任务</h3></div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">任务名称</label>
<input type="text" class="form-input" id="scheduleName" placeholder="我的定时任务">
</div>
<div class="form-group">
<label class="form-label">执行时间</label>
<input type="time" class="form-input" id="scheduleTime" value="08:00">
</div>
<div class="form-group">
<label class="form-label">执行日期</label>
<div class="weekday-selector" id="weekdaySelector">
<label class="weekday-chip"><input type="checkbox" value="1" checked><span>周一</span></label>
<label class="weekday-chip"><input type="checkbox" value="2" checked><span>周二</span></label>
<label class="weekday-chip"><input type="checkbox" value="3" checked><span>周三</span></label>
<label class="weekday-chip"><input type="checkbox" value="4" checked><span>周四</span></label>
<label class="weekday-chip"><input type="checkbox" value="5" checked><span>周五</span></label>
<label class="weekday-chip"><input type="checkbox" value="6"><span>周六</span></label>
<label class="weekday-chip"><input type="checkbox" value="7"><span>周日</span></label>
</div>
</div>
<div class="form-group">
<label class="form-label">浏览类型</label>
<select class="form-select" id="scheduleBrowseType">
<option value="应读">应读</option>
<option value="未读">未读</option>
<option value="注册前未读">注册前未读</option>
</select>
</div>
<div class="form-group">
<label class="form-label">参与账号</label>
<div class="account-select-list" id="scheduleAccountList"></div>
</div>
<div class="form-group">
<label class="checkbox-wrapper">
<input type="checkbox" id="scheduleScreenshot" checked>
<span>任务完成后截图</span>
</label>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-text" onclick="closeModal('scheduleModal')">取消</button>
<button class="btn btn-primary" onclick="saveSchedule()">保存</button>
</div>
</div>
</div>
<!-- 反馈弹窗 -->
<div class="modal-overlay" id="feedbackModal">
<div class="modal" style="max-width: 560px;">
<div class="modal-header">
<h3 class="modal-title">问题反馈</h3>
</div>
<div class="modal-body">
<div style="display: flex; gap: 8px; margin-bottom: 16px;">
<button class="btn btn-outlined" id="btnNewFeedback" onclick="showFeedbackForm()">提交反馈</button>
<button class="btn btn-text" id="btnMyFeedbacks" onclick="showMyFeedbacks()">我的反馈</button>
</div>
<div id="feedbackFormSection">
<div class="form-group">
<label class="form-label">标题</label>
<input type="text" class="form-input" id="feedbackTitle" placeholder="简要描述问题">
</div>
<div class="form-group">
<label class="form-label">详细描述</label>
<textarea class="form-input" id="feedbackDesc" rows="4" placeholder="请详细描述您遇到的问题"></textarea>
</div>
<div class="form-group">
<label class="form-label">联系方式(可选)</label>
<input type="text" class="form-input" id="feedbackContact" placeholder="方便我们联系您">
</div>
</div>
<div id="feedbackListSection" style="display: none;">
<div class="feedback-list" id="feedbackList"></div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-text" onclick="closeModal('feedbackModal')">关闭</button>
<button class="btn btn-primary" id="btnSubmitFeedback" onclick="submitFeedback()">提交</button>
</div>
</div>
</div>
<!-- VIP信息弹窗 -->
<div class="modal-overlay" id="vipInfoModal">
<div class="modal">
<div class="modal-header"><h3 class="modal-title">VIP会员信息</h3></div>
<div class="modal-body">
<div style="text-align: center; padding: 16px 0;">
<div style="font-size: 48px; margin-bottom: 16px;">👑</div>
<div id="vipStatusText" style="font-size: 18px; font-weight: 500; margin-bottom: 8px;">VIP会员</div>
<div id="vipExpireText" style="color: var(--md-on-surface-medium);"></div>
</div>
<div style="background: #f5f5f5; border-radius: 8px; padding: 16px; margin-top: 16px;">
<div style="font-weight: 500; margin-bottom: 12px;">VIP专属特权</div>
<div style="display: grid; gap: 8px; font-size: 14px;">
<div>✅ 无限账号管理</div>
<div>✅ 自定义定时任务</div>
<div>✅ 批量启动/停止</div>
<div>✅ 详细进度显示</div>
<div>✅ 截图管理</div>
<div>✅ 优先技术支持</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-text" onclick="closeModal('vipInfoModal')">关闭</button>
</div>
</div>
</div>
<!-- VIP升级弹窗 -->
<div class="modal-overlay" id="upgradeModal">
<div class="modal" style="max-width: 520px;">
<div class="modal-header"><h3 class="modal-title">升级VIP会员</h3></div>
<div class="modal-body">
<div style="text-align: center; padding: 16px 0;">
<div style="font-size: 48px; margin-bottom: 16px;">🚀</div>
<div style="font-size: 18px; font-weight: 500; margin-bottom: 8px;">解锁全部功能</div>
<div style="color: var(--md-on-surface-medium);">升级VIP享受更多专属特权</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 24px 0;">
<div style="background: #f5f5f5; border-radius: 8px; padding: 16px; text-align: center;">
<div style="font-size: 14px; color: var(--md-on-surface-medium);">普通用户</div>
<div style="font-size: 13px; margin-top: 12px; text-align: left;">
<div style="margin-bottom: 4px;">📌 最多3个账号</div>
<div style="margin-bottom: 4px; opacity: 0.5;">❌ 定时任务</div>
<div style="margin-bottom: 4px; opacity: 0.5;">❌ 批量操作</div>
<div style="opacity: 0.5;">❌ 优先支持</div>
</div>
</div>
<div style="background: linear-gradient(135deg, #fff8e1 0%, #ffecb3 100%); border-radius: 8px; padding: 16px; text-align: center; border: 2px solid #FFC107;">
<div style="font-size: 14px; color: #FF8F00; font-weight: 600;">VIP会员</div>
<div style="font-size: 13px; margin-top: 12px; text-align: left;">
<div style="margin-bottom: 4px;">✅ 无限账号</div>
<div style="margin-bottom: 4px;">✅ 自定义定时任务</div>
<div style="margin-bottom: 4px;">✅ 批量启动/停止</div>
<div>✅ 优先技术支持</div>
</div>
</div>
</div>
<div style="background: #E3F2FD; border-radius: 8px; padding: 16px; text-align: center;">
<div style="font-size: 14px; color: var(--md-primary); margin-bottom: 8px;">如需开通VIP请联系管理员</div>
<div style="font-size: 13px; color: var(--md-on-surface-medium);">或通过反馈功能留言申请</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-text" onclick="closeModal('upgradeModal')">关闭</button>
<button class="btn btn-primary" onclick="closeModal('upgradeModal'); openFeedbackModal();">申请VIP</button>
</div>
</div>
</div>
<!-- 图片预览弹窗 -->
<div class="modal-overlay image-preview-modal" id="imagePreviewModal">
<button class="preview-close" onclick="closeImagePreview()">&times;</button>
<div class="zoom-info" id="zoomInfo">100%</div>
<div class="image-preview-container" id="imagePreviewContainer">
<img id="previewImage" class="preview-image" src="" alt="预览图片">
</div>
<div class="preview-controls">
<button class="btn btn-small" onclick="zoomOut()"></button>
<button class="btn btn-small" onclick="resetZoom()">重置</button>
<button class="btn btn-small" onclick="zoomIn()"></button>
<button class="btn btn-small" onclick="downloadCurrentImage()">下载</button>
</div>
</div>
<script>
// ==================== 全局401处理 ====================
const originalFetch = window.fetch;
window.fetch = function(...args) {
return originalFetch.apply(this, args).then(response => {
if (response.status === 401) {
// session失效跳转到登录页
window.location.href = '/login';
throw new Error('Session expired');
}
return response;
});
};
// ==================== 全局变量 ====================
let accounts = {};
let schedules = [];
let selectedAccounts = new Set();
let editingScheduleId = null;
let accountLimit = { current: 0, max: 5 };
let vipInfo = { is_vip: false, days_left: 0, expire_time: null };
// Socket.IO配置增强连接稳定性
const socket = io({
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
reconnectionAttempts: 5,
transports: ['websocket', 'polling']
});
// 图片预览相关
let currentScale = 1;
let currentTranslateX = 0;
let currentTranslateY = 0;
let isDragging = false;
let startX = 0;
let startY = 0;
let currentImageSrc = '';
// ==================== 初始化 ====================
document.addEventListener('DOMContentLoaded', function() {
initTabs();
loadVipStatus();
loadStats();
loadSchedules();
loadScreenshots();
checkAccountLimit();
setInterval(loadStats, 30000);
setupImagePreviewEvents();
});
function initTabs() {
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', function() {
const tab = this.dataset.tab;
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
this.classList.add('active');
document.getElementById('tab-' + tab).classList.add('active');
if (tab === 'screenshots') loadScreenshots();
if (tab === 'schedule') loadSchedules();
});
});
}
// ==================== Socket.IO ====================
socket.on('connect', function() {
console.log('[Socket] WebSocket已连接ID:', socket.id);
});
socket.on('disconnect', function(reason) {
console.log('[Socket] 断开连接,原因:', reason);
});
socket.on('connect_error', function(error) {
console.error('[Socket] 连接错误:', error);
});
socket.on('accounts_list', function(accountsList) {
accounts = {};
accountsList.forEach(acc => { accounts[acc.id] = acc; });
renderAccounts();
updateAccountLimitDisplay();
});
socket.on('account_update', function(acc) {
console.log('[Socket] 收到account_update:', acc.id, acc.status, acc.detail_status);
accounts[acc.id] = acc;
updateAccountCard(acc);
updateRunningCount();
});
socket.on('task_progress', function(data) { updateAccountProgress(data); });
// ==================== 数据加载 ====================
function loadVipStatus() {
fetch('/api/user/vip').then(r => r.json()).then(data => {
vipInfo = data;
// 更新显示的用户名
if (data.username) {
document.getElementById('usernameDisplay').textContent = data.username;
}
if (data.is_vip) {
document.getElementById('vipBadge').style.display = 'inline';
document.getElementById('normalBadge').style.display = 'none';
document.getElementById('upgradeBanner').style.display = 'none';
// VIP即将到期提示 (7天内)
if (data.days_left <= 7 && data.days_left > 0) {
const warning = document.getElementById('vipExpireWarning');
warning.textContent = '(' + data.days_left + '天后到期)';
warning.style.display = 'inline';
}
// VIP账号无限制
accountLimit.max = 999;
} else {
document.getElementById('vipBadge').style.display = 'none';
document.getElementById('normalBadge').style.display = 'inline';
document.getElementById('upgradeBanner').style.display = 'flex';
// 普通用户最多3个账号
accountLimit.max = 3;
// 禁用VIP专属功能
applyVipRestrictions();
}
updateAccountLimitDisplay();
});
}
function applyVipRestrictions() {
// 非VIP用户限制定时任务功能
const scheduleNav = document.querySelector('[data-tab="schedule"]');
if (scheduleNav && !vipInfo.is_vip) {
scheduleNav.classList.add('vip-feature');
scheduleNav.innerHTML = '<span class="nav-icon">⏰</span><span class="nav-label">定时任务</span><span class="vip-tooltip">VIP专属功能</span>';
scheduleNav.onclick = function(e) {
e.preventDefault();
showUpgradeModal();
};
}
}
function showVipInfo() {
if (vipInfo.is_vip) {
document.getElementById('vipStatusText').textContent = 'VIP会员';
if (vipInfo.days_left > 365) {
document.getElementById('vipExpireText').textContent = '永久VIP';
} else {
document.getElementById('vipExpireText').textContent = '到期时间: ' + (vipInfo.expire_time || '未知') + ' (剩余' + vipInfo.days_left + '天)';
}
}
openModal('vipInfoModal');
}
function showUpgradeModal() {
openModal('upgradeModal');
}
function loadStats() {
fetch('/api/run_stats').then(r => r.json()).then(data => {
document.getElementById('statCompleted').textContent = data.today_completed || 0;
document.getElementById('statRunning').textContent = data.current_running || 0;
document.getElementById('statFailed').textContent = data.today_failed || 0;
document.getElementById('statItems').textContent = data.today_items || 0;
document.getElementById('statAttachments').textContent = data.today_attachments || 0;
});
}
function checkAccountLimit() {
fetch('/api/user/vip').then(r => r.json()).then(data => {
if (data.account_limit) {
accountLimit.max = data.account_limit;
}
updateAccountLimitDisplay();
});
}
function updateAccountLimitDisplay() {
accountLimit.current = Object.keys(accounts).length;
const limitEl = document.getElementById('accountLimit');
const limitLargeEl = document.getElementById('accountLimitLarge');
var displayText = vipInfo.is_vip ? (accountLimit.current + '/∞') : (accountLimit.current + '/' + accountLimit.max);
if (limitEl) { limitEl.textContent = '(' + displayText + ')'; }
if (limitLargeEl) { limitLargeEl.textContent = displayText; }
}
// ==================== 账号渲染 ====================
function renderAccounts() {
const container = document.getElementById('accountsList');
const empty = document.getElementById('emptyAccounts');
const accountList = Object.values(accounts);
if (accountList.length === 0) {
container.innerHTML = '';
empty.style.display = 'block';
return;
}
empty.style.display = 'none';
container.innerHTML = accountList.map(acc => createAccountCard(acc)).join('');
updateSelectedCount();
updateAccountLimitDisplay();
}
function createAccountCard(acc) {
const isRunning = acc.is_running;
const statusClass = getStatusClass(acc.status);
const checked = selectedAccounts.has(acc.id) ? 'checked' : '';
let progressHtml = '';
const shouldShowProgress = acc.detail_status || (acc.status && acc.status !== '未开始');
if (shouldShowProgress) {
const progress = calculateProgress(acc);
progressHtml = '<div class="progress-section">' +
'<div class="progress-stage">' + (acc.detail_status || '运行中') + '</div>' +
'<div class="progress-bar"><div class="progress-fill" style="width: ' + progress + '%"></div></div>' +
'<div class="progress-details">' +
'<span>内容: ' + (acc.progress_items || 0) + '/' + (acc.total_items || '?') + '</span>' +
'<span>附件: ' + (acc.progress_attachments || 0) + '/' + (acc.total_attachments || '?') + '</span>' +
(acc.elapsed_display ? '<span>运行: ' + acc.elapsed_display + '</span>' : '') +
'</div></div>';
}
return '<div class="account-card" data-id="' + acc.id + '">' +
'<div class="account-card-header">' +
'<label class="checkbox-wrapper"><input type="checkbox" class="account-checkbox" data-id="' + acc.id + '" ' + checked + ' onchange="toggleAccountSelect(\'' + acc.id + '\')"></label>' +
'<div class="account-info">' +
'<div class="account-username">' + escapeHtml(acc.username) + '</div>' +
'<div class="account-remark">' + escapeHtml(acc.remark || '无备注') + '</div>' +
'</div>' +
'<span class="status-chip ' + statusClass + '">' + (acc.status || '未开始') + '</span>' +
'</div>' + progressHtml +
'<div class="account-card-actions">' +
'<select class="select-inline browse-type" data-id="' + acc.id + '">' +
'<option value="应读">应读</option><option value="未读">未读</option><option value="注册前未读">注册前未读</option>' +
'</select>' +
'<button class="btn btn-primary btn-small" onclick="startAccount(\'' + acc.id + '\')" ' + (isRunning ? 'disabled' : '') + '>启动</button>' +
'<button class="btn btn-outlined btn-small" onclick="stopAccount(\'' + acc.id + '\')" ' + (!isRunning ? 'disabled' : '') + '>停止</button>' +
'<button class="btn btn-text btn-small" onclick="deleteAccount(\'' + acc.id + '\')" title="删除">🗑️</button>' +
'</div></div>';
}
function updateAccountCard(acc) {
const card = document.querySelector('.account-card[data-id="' + acc.id + '"]');
if (card) {
const temp = document.createElement('div');
temp.innerHTML = createAccountCard(acc);
card.replaceWith(temp.firstElementChild);
}
}
function updateAccountProgress(data) {
const acc = accounts[data.account_id];
if (acc) {
acc.detail_status = data.stage;
acc.progress_items = data.browsed_items;
acc.progress_attachments = data.viewed_attachments;
acc.elapsed_display = data.elapsed_display;
updateAccountCard(acc);
}
}
function getStatusClass(status) {
if (!status || status === '未开始') return 'status-idle';
if (status === '运行中') return 'status-running';
if (status === '排队中') return 'status-queued';
if (status === '正在停止') return 'status-stopping';
if (status.includes('完成')) return 'status-completed';
if (status.includes('失败') || status.includes('错误')) return 'status-error';
return 'status-idle';
}
function calculateProgress(acc) {
const items = acc.progress_items || 0;
const attachments = acc.progress_attachments || 0;
const totalItems = acc.total_items || 0;
const totalAttachments = acc.total_attachments || 0;
const total = totalItems + totalAttachments;
if (total === 0) return 0;
return Math.min(100, Math.round((items + attachments) / total * 100));
}
function updateRunningCount() {
const running = Object.values(accounts).filter(a => a.is_running).length;
document.getElementById('statRunning').textContent = running;
}
// ==================== 账号选择 ====================
function toggleAccountSelect(id) {
if (selectedAccounts.has(id)) selectedAccounts.delete(id);
else selectedAccounts.add(id);
updateSelectedCount();
}
function toggleSelectAll() {
const selectAll = document.getElementById('selectAll').checked;
selectedAccounts.clear();
if (selectAll) Object.keys(accounts).forEach(id => selectedAccounts.add(id));
document.querySelectorAll('.account-checkbox').forEach(cb => { cb.checked = selectAll; });
updateSelectedCount();
}
function updateSelectedCount() {
document.getElementById('selectedCount').textContent = selectedAccounts.size;
}
// ==================== 账号操作 ====================
function openAddAccountModal() {
if (accountLimit.current >= accountLimit.max) {
if (!vipInfo.is_vip) {
showToast('普通用户最多添加3个账号升级VIP可无限添加', 'warning');
setTimeout(showUpgradeModal, 1500);
} else {
showToast('已达到账号数量上限 (' + accountLimit.max + '个)', 'warning');
}
return;
}
document.getElementById('newAccountUsername').value = '';
document.getElementById('newAccountPassword').value = '';
document.getElementById('newAccountRemember').checked = true;
openModal('addAccountModal');
}
function addAccount() {
const username = document.getElementById('newAccountUsername').value.trim();
const password = document.getElementById('newAccountPassword').value;
const remember = true; // 默认记住密码
if (!username || !password) { showToast('请填写账号和密码', 'error'); return; }
fetch('/api/accounts', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username, password, remember})
}).then(r => r.json()).then(data => {
if (data.error) showToast(data.error, 'error');
else {
showToast('账号添加成功', 'success');
closeModal('addAccountModal');
checkAccountLimit();
}
});
}
function startAccount(id) {
const browseType = document.querySelector('.browse-type[data-id="' + id + '"]').value;
fetch('/api/accounts/' + id + '/start', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({browse_type: browseType, enable_screenshot: true})
}).then(r => r.json()).then(data => {
if (data.error) showToast(data.error, 'error');
});
}
function stopAccount(id) {
fetch('/api/accounts/' + id + '/stop', {method: 'POST'}).then(r => r.json()).then(data => {
if (data.error) showToast(data.error, 'error');
});
}
function deleteAccount(id) {
if (!confirm('确定要删除此账号吗?')) return;
fetch('/api/accounts/' + id, {method: 'DELETE'}).then(r => r.json()).then(data => {
if (data.success) {
delete accounts[id];
selectedAccounts.delete(id);
renderAccounts();
showToast('账号已删除', 'success');
} else showToast(data.error || '删除失败', 'error');
});
}
function takeScreenshot(id) {
fetch('/api/accounts/' + id + '/screenshot', {method: 'POST'}).then(r => r.json()).then(data => {
if (data.success) showToast('截图成功', 'success');
else showToast(data.error || '截图失败', 'error');
});
}
// ==================== 批量操作 ====================
function batchStart() {
if (!vipInfo.is_vip) {
showToast('批量操作是VIP专属功能', 'warning');
setTimeout(showUpgradeModal, 1500);
return;
}
if (selectedAccounts.size === 0) { showToast('请先选择账号', 'warning'); return; }
const browseType = document.getElementById('batchBrowseType').value;
const enableScreenshot = document.getElementById('batchScreenshot').checked;
fetch('/api/accounts/batch/start', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({account_ids: Array.from(selectedAccounts), browse_type: browseType, enable_screenshot: enableScreenshot})
}).then(r => r.json()).then(data => {
if (data.success) showToast('已启动 ' + data.started_count + ' 个账号', 'success');
else showToast(data.error || '操作失败', 'error');
});
}
function batchStop() {
if (!vipInfo.is_vip) {
showToast('批量操作是VIP专属功能', 'warning');
setTimeout(showUpgradeModal, 1500);
return;
}
if (selectedAccounts.size === 0) { showToast('请先选择账号', 'warning'); return; }
fetch('/api/accounts/batch/stop', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({account_ids: Array.from(selectedAccounts)})
}).then(r => r.json()).then(data => {
if (data.success) showToast('已停止 ' + data.stopped_count + ' 个账号', 'success');
});
}
function startAllAccounts() {
if (!vipInfo.is_vip) {
showToast('全部启动是VIP专属功能', 'warning');
setTimeout(showUpgradeModal, 1500);
return;
}
if (Object.keys(accounts).length === 0) { showToast('没有账号', 'warning'); return; }
if (!confirm('确定要启动全部账号吗?')) return;
const browseType = document.getElementById('batchBrowseType').value;
const enableScreenshot = document.getElementById('batchScreenshot').checked;
const allIds = Object.keys(accounts);
fetch('/api/accounts/batch/start', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({account_ids: allIds, browse_type: browseType, enable_screenshot: enableScreenshot})
}).then(r => r.json()).then(data => {
if (data.success) showToast('已启动 ' + data.started_count + ' 个账号', 'success');
else showToast(data.error || '操作失败', 'error');
});
}
function stopAllAccounts() {
if (!vipInfo.is_vip) {
showToast('全部停止是VIP专属功能', 'warning');
setTimeout(showUpgradeModal, 1500);
return;
}
if (Object.keys(accounts).length === 0) { showToast('没有账号', 'warning'); return; }
if (!confirm('确定要停止全部账号吗?')) return;
const allIds = Object.keys(accounts);
fetch('/api/accounts/batch/stop', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({account_ids: allIds})
}).then(r => r.json()).then(data => {
if (data.success) showToast('已停止 ' + data.stopped_count + ' 个账号', 'success');
});
}
function clearAllAccounts() {
if (Object.keys(accounts).length === 0) { showToast('没有账号', 'warning'); return; }
if (!confirm('确定要清空所有账号吗?此操作不可恢复!')) return;
if (!confirm('再次确认:真的要删除所有账号吗?')) return;
fetch('/api/accounts/clear', {method: 'POST'}).then(r => r.json()).then(data => {
if (data.success) {
accounts = {};
selectedAccounts.clear();
renderAccounts();
showToast('已清空所有账号', 'success');
} else showToast(data.error || '操作失败', 'error');
});
}
// ==================== 定时任务 ====================
function loadSchedules() {
fetch('/api/schedules').then(r => r.json()).then(data => {
schedules = data;
renderSchedules();
});
}
function renderSchedules() {
const container = document.getElementById('scheduleList');
const empty = document.getElementById('emptySchedules');
if (schedules.length === 0) { container.innerHTML = ''; empty.style.display = 'block'; return; }
empty.style.display = 'none';
container.innerHTML = schedules.map(s => createScheduleCard(s)).join('');
}
function createScheduleCard(s) {
const weekdayNames = ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日'];
const weekdays = (s.weekdays || '').split(',').filter(Boolean).map(d => weekdayNames[parseInt(d)]).join(' ');
const accountCount = (s.account_ids || []).length;
return '<div class="schedule-card">' +
'<div class="schedule-info">' +
'<div class="schedule-name">' + escapeHtml(s.name || '未命名任务') + '</div>' +
'<div class="schedule-meta">' +
'<span>⏰ ' + (s.schedule_time || '08:00') + '</span>' +
'<span>📅 ' + (weekdays || '无') + '</span>' +
'<span>📋 ' + (s.browse_type || '应读') + '</span>' +
'<span>👤 ' + accountCount + ' 个账号</span>' +
'</div></div>' +
'<label class="switch"><input type="checkbox" ' + (s.enabled ? 'checked' : '') + ' onchange="toggleSchedule(' + s.id + ', this.checked)"><span class="switch-track"><span class="switch-thumb"></span></span></label>' +
'<div class="schedule-actions">' +
'<button class="btn btn-text btn-small" onclick="runScheduleNow(' + s.id + ')">执行</button>' +
'<button class="btn btn-text btn-small" onclick="editSchedule(' + s.id + ')">编辑</button>' +
'<button class="btn btn-text btn-small" style="color: var(--md-error);" onclick="deleteSchedule(' + s.id + ')">删除</button>' +
'</div></div>';
}
function openScheduleModal(scheduleData) {
if (!vipInfo.is_vip) {
showToast('定时任务是VIP专属功能', 'warning');
setTimeout(showUpgradeModal, 1500);
return;
}
editingScheduleId = scheduleData ? scheduleData.id : null;
document.getElementById('scheduleModalTitle').textContent = scheduleData ? '编辑定时任务' : '新建定时任务';
document.getElementById('scheduleName').value = scheduleData ? (scheduleData.name || '') : '';
document.getElementById('scheduleTime').value = scheduleData ? (scheduleData.schedule_time || '08:00') : '08:00';
document.getElementById('scheduleBrowseType').value = scheduleData ? (scheduleData.browse_type || '应读') : '应读';
document.getElementById('scheduleScreenshot').checked = scheduleData ? (scheduleData.enable_screenshot !== 0) : true;
const weekdays = scheduleData ? (scheduleData.weekdays || '').split(',') : ['1','2','3','4','5'];
document.querySelectorAll('#weekdaySelector input').forEach(function(input) { input.checked = weekdays.includes(input.value); });
const selectedAccountIds = scheduleData ? (scheduleData.account_ids || []) : [];
let accountListHtml = '';
Object.values(accounts).forEach(function(acc) {
accountListHtml += '<div class="account-select-item"><input type="checkbox" id="schedule-acc-' + acc.id + '" value="' + acc.id + '" ' + (selectedAccountIds.includes(acc.id) ? 'checked' : '') + '><label for="schedule-acc-' + acc.id + '">' + escapeHtml(acc.username) + '</label></div>';
});
document.getElementById('scheduleAccountList').innerHTML = accountListHtml || '<div style="padding: 16px; color: #999;">暂无账号</div>';
openModal('scheduleModal');
}
function saveSchedule() {
const name = document.getElementById('scheduleName').value.trim() || '我的定时任务';
const scheduleTime = document.getElementById('scheduleTime').value;
const browseType = document.getElementById('scheduleBrowseType').value;
const enableScreenshot = document.getElementById('scheduleScreenshot').checked ? 1 : 0;
const weekdays = [];
document.querySelectorAll('#weekdaySelector input:checked').forEach(function(input) { weekdays.push(input.value); });
const accountIds = [];
document.querySelectorAll('#scheduleAccountList input:checked').forEach(function(input) { accountIds.push(input.value); });
if (weekdays.length === 0) { showToast('请选择至少一个执行日期', 'warning'); return; }
const data = {name: name, schedule_time: scheduleTime, weekdays: weekdays.join(','), browse_type: browseType, enable_screenshot: enableScreenshot, account_ids: accountIds};
const url = editingScheduleId ? '/api/schedules/' + editingScheduleId : '/api/schedules';
const method = editingScheduleId ? 'PUT' : 'POST';
fetch(url, {method: method, headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)}).then(r => r.json()).then(function(result) {
if (result.success || result.id) { showToast('保存成功', 'success'); closeModal('scheduleModal'); loadSchedules(); }
else showToast(result.error || '保存失败', 'error');
});
}
function editSchedule(id) {
const schedule = schedules.find(function(s) { return s.id === id; });
if (schedule) openScheduleModal(schedule);
}
function deleteSchedule(id) {
if (!confirm('确定要删除此定时任务吗?')) return;
fetch('/api/schedules/' + id, {method: 'DELETE'}).then(r => r.json()).then(function(data) {
if (data.success) { showToast('已删除', 'success'); loadSchedules(); }
else showToast(data.error || '删除失败', 'error');
});
}
function toggleSchedule(id, enabled) {
fetch('/api/schedules/' + id + '/toggle', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({enabled: enabled})
}).then(r => r.json()).then(function(data) {
if (data.success) showToast(enabled ? '已启用' : '已禁用', 'success');
});
}
function runScheduleNow(id) {
fetch('/api/schedules/' + id + '/run', {method: 'POST'}).then(r => r.json()).then(function(data) {
if (data.success) showToast(data.message || '已开始执行', 'success');
else showToast(data.error || '执行失败', 'error');
});
}
// ==================== 截图管理 ====================
function loadScreenshots() {
fetch('/api/screenshots').then(r => r.json()).then(function(data) {
const container = document.getElementById('screenshotsList');
const empty = document.getElementById('emptyScreenshots');
if (data.length === 0) { container.innerHTML = ''; empty.style.display = 'block'; return; }
empty.style.display = 'none';
let html = '';
data.forEach(function(s) {
html += '<div class="screenshot-item">' +
'<img class="screenshot-img" src="/screenshots/' + s.filename + '" alt="' + escapeHtml(s.display_name) + '" loading="lazy" onclick="openImagePreview(\'/screenshots/' + s.filename + '\')">' +
'<div class="screenshot-info">' +
'<div class="screenshot-name">' + escapeHtml(s.display_name) + '</div>' +
'<div class="screenshot-time">' + s.created + '</div>' +
'<div class="screenshot-actions">' +
'<button class="btn btn-text btn-small" onclick="downloadScreenshot(\'' + s.filename + '\')">下载</button>' +
'<button class="btn btn-text btn-small" onclick="copyScreenshotImage(\'/screenshots/' + s.filename + '\')">复制图片</button>' +
'<button class="btn btn-text btn-small" style="color: var(--md-error);" onclick="deleteScreenshot(\'' + s.filename + '\')">删除</button>' +
'</div></div></div>';
});
container.innerHTML = html;
});
}
function refreshScreenshots() { loadScreenshots(); showToast('已刷新', 'success'); }
function clearScreenshots() {
if (!confirm('确定要清空所有截图吗?')) return;
fetch('/api/screenshots/clear', {method: 'POST'}).then(r => r.json()).then(function(data) {
if (data.success) { showToast('已清空', 'success'); loadScreenshots(); }
});
}
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);
}
function copyScreenshotImage(imgSrc) {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function() {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob(function(blob) {
if (blob) {
const item = new ClipboardItem({'image/png': blob});
navigator.clipboard.write([item]).then(function() {
showToast('图片已复制到剪贴板', 'success');
}).catch(function() {
fallbackCopyLink(imgSrc);
});
} else {
fallbackCopyLink(imgSrc);
}
}, 'image/png');
};
img.onerror = function() { fallbackCopyLink(imgSrc); };
img.src = imgSrc;
}
function fallbackCopyLink(imgSrc) {
const url = window.location.origin + imgSrc;
navigator.clipboard.writeText(url).then(function() {
showToast('已复制图片链接', 'warning');
}).catch(function() {
showToast('复制失败', 'error');
});
}
function deleteScreenshot(filename) {
if (!confirm('确定要删除此截图吗?')) return;
fetch('/api/screenshots/' + filename, {method: 'DELETE'}).then(r => r.json()).then(function(data) {
if (data.success) { showToast('已删除', 'success'); loadScreenshots(); }
else showToast(data.error || '删除失败', 'error');
});
}
// ==================== 图片预览 ====================
function openImagePreview(src) {
currentImageSrc = src;
currentScale = 1;
currentTranslateX = 0;
currentTranslateY = 0;
const img = document.getElementById('previewImage');
img.src = src;
img.style.transform = 'scale(1) translate(0px, 0px)';
document.getElementById('zoomInfo').textContent = '100%';
openModal('imagePreviewModal');
}
function closeImagePreview() {
closeModal('imagePreviewModal');
}
function updateImageTransform() {
const img = document.getElementById('previewImage');
img.style.transform = 'scale(' + currentScale + ') translate(' + currentTranslateX + 'px, ' + currentTranslateY + 'px)';
document.getElementById('zoomInfo').textContent = Math.round(currentScale * 100) + '%';
}
function zoomIn() {
currentScale = Math.min(5, currentScale + 0.25);
updateImageTransform();
}
function zoomOut() {
currentScale = Math.max(0.25, currentScale - 0.25);
updateImageTransform();
}
function resetZoom() {
currentScale = 1;
currentTranslateX = 0;
currentTranslateY = 0;
updateImageTransform();
}
function downloadCurrentImage() {
if (currentImageSrc) {
const link = document.createElement('a');
link.href = currentImageSrc;
link.download = currentImageSrc.split('/').pop();
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
function setupImagePreviewEvents() {
const container = document.getElementById('imagePreviewContainer');
const img = document.getElementById('previewImage');
// 鼠标滚轮缩放
container.addEventListener('wheel', function(e) {
e.preventDefault();
if (e.deltaY < 0) zoomIn();
else zoomOut();
});
// 鼠标拖拽
img.addEventListener('mousedown', function(e) {
if (currentScale > 1) {
isDragging = true;
startX = e.clientX - currentTranslateX;
startY = e.clientY - currentTranslateY;
img.classList.add('dragging');
}
});
document.addEventListener('mousemove', function(e) {
if (isDragging) {
currentTranslateX = e.clientX - startX;
currentTranslateY = e.clientY - startY;
updateImageTransform();
}
});
document.addEventListener('mouseup', function() {
isDragging = false;
img.classList.remove('dragging');
});
// 触摸事件
let touchStartDistance = 0;
let touchStartScale = 1;
img.addEventListener('touchstart', function(e) {
if (e.touches.length === 2) {
touchStartDistance = Math.hypot(
e.touches[0].clientX - e.touches[1].clientX,
e.touches[0].clientY - e.touches[1].clientY
);
touchStartScale = currentScale;
} else if (e.touches.length === 1 && currentScale > 1) {
isDragging = true;
startX = e.touches[0].clientX - currentTranslateX;
startY = e.touches[0].clientY - currentTranslateY;
}
});
img.addEventListener('touchmove', function(e) {
e.preventDefault();
if (e.touches.length === 2) {
const distance = Math.hypot(
e.touches[0].clientX - e.touches[1].clientX,
e.touches[0].clientY - e.touches[1].clientY
);
currentScale = Math.min(5, Math.max(0.5, touchStartScale * (distance / touchStartDistance)));
updateImageTransform();
} else if (e.touches.length === 1 && isDragging) {
currentTranslateX = e.touches[0].clientX - startX;
currentTranslateY = e.touches[0].clientY - startY;
updateImageTransform();
}
});
img.addEventListener('touchend', function() {
isDragging = false;
});
// 双击重置
img.addEventListener('dblclick', function() {
resetZoom();
});
}
// ==================== 反馈 ====================
function openFeedbackModal() {
showFeedbackForm();
openModal('feedbackModal');
}
function showFeedbackForm() {
document.getElementById('feedbackFormSection').style.display = 'block';
document.getElementById('feedbackListSection').style.display = 'none';
document.getElementById('btnSubmitFeedback').style.display = 'inline-flex';
document.getElementById('btnNewFeedback').classList.add('btn-primary');
document.getElementById('btnNewFeedback').classList.remove('btn-outlined');
document.getElementById('btnMyFeedbacks').classList.remove('btn-primary');
document.getElementById('btnMyFeedbacks').classList.add('btn-text');
document.getElementById('feedbackTitle').value = '';
document.getElementById('feedbackDesc').value = '';
document.getElementById('feedbackContact').value = '';
}
function showMyFeedbacks() {
document.getElementById('feedbackFormSection').style.display = 'none';
document.getElementById('feedbackListSection').style.display = 'block';
document.getElementById('btnSubmitFeedback').style.display = 'none';
document.getElementById('btnMyFeedbacks').classList.add('btn-primary');
document.getElementById('btnMyFeedbacks').classList.remove('btn-text');
document.getElementById('btnNewFeedback').classList.remove('btn-primary');
document.getElementById('btnNewFeedback').classList.add('btn-outlined');
loadMyFeedbacks();
}
function loadMyFeedbacks() {
fetch('/api/feedback/my').then(r => r.json()).then(function(data) {
const container = document.getElementById('feedbackList');
if (!data || data.length === 0) {
container.innerHTML = '<div class="empty-state"><p>暂无反馈记录</p></div>';
return;
}
let html = '';
data.forEach(function(f) {
const statusClass = f.reply ? 'feedback-status-replied' : 'feedback-status-pending';
const statusText = f.reply ? '已回复' : '待处理';
html += '<div class="feedback-item">' +
'<div class="feedback-item-title">' + escapeHtml(f.title) +
'<span class="feedback-item-status ' + statusClass + '">' + statusText + '</span></div>' +
'<div class="feedback-item-time">' + f.created_at + '</div>' +
(f.reply ? '<div style="margin-top: 8px; padding: 8px; background: #f5f5f5; border-radius: 4px; font-size: 13px;"><strong>回复:</strong> ' + escapeHtml(f.reply) + '</div>' : '') +
'</div>';
});
container.innerHTML = html;
}).catch(function() {
document.getElementById('feedbackList').innerHTML = '<div class="empty-state"><p>加载失败</p></div>';
});
}
function submitFeedback() {
const title = document.getElementById('feedbackTitle').value.trim();
const description = document.getElementById('feedbackDesc').value.trim();
const contact = document.getElementById('feedbackContact').value.trim();
if (!title || !description) { showToast('请填写标题和描述', 'warning'); return; }
fetch('/api/feedback', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({title: title, description: description, contact: contact})
}).then(r => r.json()).then(function(data) {
if (data.success) {
showToast('反馈已提交,感谢!', 'success');
closeModal('feedbackModal');
} else showToast(data.error || '提交失败', 'error');
});
}
// ==================== 通用函数 ====================
function openModal(id) { document.getElementById(id).classList.add('active'); }
function closeModal(id) { document.getElementById(id).classList.remove('active'); }
function showToast(message, type) {
type = type || 'info';
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = 'toast ' + type;
toast.textContent = message;
container.appendChild(toast);
setTimeout(function() { toast.remove(); }, 3000);
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function logout() {
// 使用原始fetch避免401拦截器
originalFetch('/api/logout', {method: 'POST'}).finally(function() {
// 无论成功失败都跳转到登录页
window.location.href = '/login';
});
}
// 点击overlay关闭弹窗
document.querySelectorAll('.modal-overlay').forEach(function(overlay) {
overlay.addEventListener('click', function(e) {
if (e.target === this && !this.classList.contains('image-preview-modal')) {
this.classList.remove('active');
}
});
});
</script>
</body>
</html>