Files
zsglpt/templates/index.html
root dc5f9e939b 修复定时任务日志弹窗显示问题
问题:点击日志按钮无反应

修复:
1. 重新格式化viewScheduleLogs函数(原本全部压缩成一行)
2. 添加详细的console.log调试日志
3. 每个步骤都有日志输出,便于调试

调试日志包括:
- 开始查看日志
- 找到任务
- API响应状态
- 收到的数据
- 打开弹窗

现在可以通过浏览器控制台查看完整的执行流程。

位置: templates/index.html viewScheduleLogs函数

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-10 17:14:08 +08:00

1735 lines
94 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">
<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; }
.feedback-status-closed { background: #ECEFF1; color: #546E7A; }
@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 class="form-group">
<label class="form-label">备注(可选)</label>
<input type="text" class="form-input" id="newAccountRemark" 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="editAccountModal">
<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="editAccountUsername" disabled>
</div>
<div class="form-group">
<label class="form-label">新密码(留空则不修改)</label>
<input type="password" class="form-input" id="editAccountPassword" placeholder="留空表示不修改密码">
</div>
<div class="form-group">
<label class="form-label">备注</label>
<input type="text" class="form-input" id="editAccountRemark" placeholder="可选填写备注信息">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-text" onclick="closeModal('editAccountModal')">取消</button>
<button class="btn btn-primary" onclick="updateAccount()">保存</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 class="modal-overlay" id="scheduleLogsModal">
<div class="modal" style="max-width: 800px;">
<div class="modal-header">
<h3 class="modal-title" id="scheduleLogsTitle">任务执行日志</h3>
</div>
<div class="modal-body">
<div id="scheduleLogsList" style="max-height: 400px; overflow-y: auto;"></div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="closeModal('scheduleLogsModal')">关闭</button>
</div>
</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="margin-top: 24px;">
<div style="font-weight: 500; margin-bottom: 12px; text-align: center;">会员权限对比</div>
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
<thead>
<tr style="background: #f5f5f5;">
<th style="padding: 12px 8px; text-align: left; border-bottom: 2px solid #ddd;">功能</th>
<th style="padding: 12px 8px; text-align: center; border-bottom: 2px solid #ddd; color: #999;">普通用户</th>
<th style="padding: 12px 8px; text-align: center; border-bottom: 2px solid #ddd; color: #FFD700;">VIP会员</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 10px 8px; border-bottom: 1px solid #eee;">账号管理数量</td>
<td style="padding: 10px 8px; text-align: center; border-bottom: 1px solid #eee; color: #999;">最多5个</td>
<td style="padding: 10px 8px; text-align: center; border-bottom: 1px solid #eee; color: #4CAF50; font-weight: 500;">✅ 无限制</td>
</tr>
<tr>
<td style="padding: 10px 8px; border-bottom: 1px solid #eee;">定时任务</td>
<td style="padding: 10px 8px; text-align: center; border-bottom: 1px solid #eee; color: #999;">❌ 不可用</td>
<td style="padding: 10px 8px; text-align: center; border-bottom: 1px solid #eee; color: #4CAF50; font-weight: 500;">✅ 可用</td>
</tr>
<tr>
<td style="padding: 10px 8px; border-bottom: 1px solid #eee;">批量操作</td>
<td style="padding: 10px 8px; text-align: center; border-bottom: 1px solid #eee; color: #999;">❌ 不可用</td>
<td style="padding: 10px 8px; text-align: center; border-bottom: 1px solid #eee; color: #4CAF50; font-weight: 500;">✅ 可用</td>
</tr>
<tr>
<td style="padding: 10px 8px; border-bottom: 1px solid #eee;">截图功能</td>
<td style="padding: 10px 8px; text-align: center; border-bottom: 1px solid #eee; color: #999;">❌ 不可用</td>
<td style="padding: 10px 8px; text-align: center; border-bottom: 1px solid #eee; color: #4CAF50; font-weight: 500;">✅ 可用</td>
</tr>
<tr>
<td style="padding: 10px 8px; border-bottom: 1px solid #eee;">详细进度显示</td>
<td style="padding: 10px 8px; text-align: center; border-bottom: 1px solid #eee; color: #FFA500;">基础显示</td>
<td style="padding: 10px 8px; text-align: center; border-bottom: 1px solid #eee; color: #4CAF50; font-weight: 500;">✅ 详细显示</td>
</tr>
<tr>
<td style="padding: 10px 8px;">技术支持</td>
<td style="padding: 10px 8px; text-align: center; color: #FFA500;">标准支持</td>
<td style="padding: 10px 8px; text-align: center; color: #4CAF50; font-weight: 500;">✅ 优先支持</td>
</tr>
</tbody>
</table>
</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 = '';
// ==================== 初始化 ====================
function loadAccounts() {
console.log('[加载] 正在获取账号列表...');
fetch('/api/accounts')
.then(r => r.json())
.then(accountsList => {
console.log('[加载] 收到账号列表:', accountsList.length, '个账号');
accounts = {};
accountsList.forEach(acc => { accounts[acc.id] = acc; });
renderAccounts();
updateAccountLimitDisplay();
})
.catch(err => {
console.error('[加载] 获取账号列表失败:', err);
});
}
document.addEventListener('DOMContentLoaded', function() {
initTabs();
loadVipStatus();
loadAccounts(); // 主动加载账号列表
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);
// 连接成功后主动加载一次账号列表(双重保险)
setTimeout(function() {
if (Object.keys(accounts).length === 0) {
console.log('[Socket] WebSocket连接成功但无账号主动加载...');
loadAccounts();
}
}, 500);
});
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) {
console.log('[Socket] 收到accounts_list事件:', accountsList.length, '个账号');
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-primary btn-small" onclick="openEditAccountModal(\'' + acc.id + '\')" title="设置" style="background: #FFA726; border-color: #FFA726;" ' + (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('newAccountRemark').value = '';
openModal('addAccountModal');
}
function addAccount() {
const username = document.getElementById('newAccountUsername').value.trim();
const password = document.getElementById('newAccountPassword').value;
const remember = true; const remark = document.getElementById('newAccountRemark').value.trim(); // 默认记住密码
if (!username || !password) { showToast('请填写账号和密码', 'error'); return; }
fetch('/api/accounts', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username, password, remember, remark})
}).then(r => r.json()).then(data => {
if (data.error) showToast(data.error, 'error');
else {
showToast('账号添加成功', 'success');
closeModal('addAccountModal');
checkAccountLimit();
}
});
}
let currentEditAccountId = null;
function openEditAccountModal(accountId) {
const acc = accounts[accountId];
if (!acc) return;
currentEditAccountId = accountId;
document.getElementById('editAccountUsername').value = acc.username;
document.getElementById('editAccountPassword').value = '';
document.getElementById('editAccountRemark').value = acc.remark || '';
openModal('editAccountModal');
}
function updateAccount() {
if (!currentEditAccountId) return;
const password = document.getElementById('editAccountPassword').value.trim();
const remark = document.getElementById('editAccountRemark').value.trim();
// 如果密码和备注都没有修改,直接关闭
if (!password && remark === (accounts[currentEditAccountId].remark || '')) {
showToast('没有修改', 'info');
closeModal('editAccountModal');
return;
}
// 准备更新数据
const updates = {};
// 如果填写了新密码调用PUT /api/accounts/:id 更新密码
if (password) {
fetch('/api/accounts/' + currentEditAccountId, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({password, remember: true})
}).then(r => r.json()).then(data => {
if (data.error) {
showToast(data.error, 'error');
} else {
showToast('密码修改成功', 'success');
// 更新备注(如果有)
if (remark !== (accounts[currentEditAccountId].remark || '')) {
updateRemark(currentEditAccountId, remark);
} else {
closeModal('editAccountModal');
}
}
}).catch(err => {
showToast('修改失败: ' + err.message, 'error');
});
} else if (remark !== (accounts[currentEditAccountId].remark || '')) {
// 只更新备注
updateRemark(currentEditAccountId, remark);
}
}
function updateRemark(accountId, remark) {
fetch('/api/accounts/' + accountId + '/remark', {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({remark})
}).then(r => r.json()).then(data => {
if (data.success) {
accounts[accountId].remark = remark;
renderAccounts();
showToast('备注修改成功', 'success');
closeModal('editAccountModal');
} else {
showToast('备注修改失败', 'error');
}
});
}
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="viewScheduleLogs(' + s.id + ')" title="查看日志">日志</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 viewScheduleLogs(scheduleId) {
console.log('[日志弹窗] 开始查看日志, scheduleId=', scheduleId);
const schedule = schedules.find(s => s.id === scheduleId);
if (!schedule) {
console.log('[日志弹窗] 未找到任务');
return;
}
console.log('[日志弹窗] 找到任务:', schedule);
document.getElementById("scheduleLogsTitle").textContent = "【" + (schedule.name || "未命名任务") + "】 执行日志";
console.log('[日志弹窗] 开始请求API');
fetch("/api/schedules/" + scheduleId + "/logs?limit=20")
.then(r => {
console.log('[日志弹窗] API响应状态:', r.status);
return r.json();
})
.then(logs => {
console.log('[日志弹窗] 收到日志数据:', logs);
const container = document.getElementById("scheduleLogsList");
if (!logs || logs.length === 0) {
console.log('[日志弹窗] 无日志,显示空状态');
container.innerHTML = "<div class=\"empty-state\"><p>暂无执行日志</p></div>";
} else {
console.log('[日志弹窗] 渲染', logs.length, '条日志');
let html = "<div style=\"display: flex; flex-direction: column; gap: 12px;\">";
logs.forEach(log => {
const statusClass = log.status === "success" ? "status-completed" : (log.status === "failed" ? "status-error" : "status-running");
const statusText = log.status === "success" ? "成功" : (log.status === "failed" ? "失败" : "进行中");
html += "<div style=\"border: 1px solid var(--md-divider); border-radius: 8px; padding: 12px; background: var(--md-surface);\">" +
"<div style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;\">" +
"<span style=\"font-size: 14px; color: var(--md-on-surface-medium);\"">" + (log.created_at || "") + "</span>" +
"<span class=\"status-chip " + statusClass + "\">" + statusText + "</span>" +
"</div>" +
"<div style=\"font-size: 13px; line-height: 1.6;\">" +
"<div><strong>账号数:</strong> " + (log.total_accounts || 0) + " 个</div>" +
"<div><strong>成功:</strong> " + (log.success_count || 0) + " 个 | <strong>失败:</strong> " + (log.failed_count || 0) + " 个</div>" +
"<div><strong>耗时:</strong> " + formatDuration(log.duration || 0) + "</div>" +
(log.error_message ? "<div style=\"color: var(--md-error); margin-top: 4px;\"><strong>错误:</strong> " + escapeHtml(log.error_message) + "</div>" : "") +
"</div></div>";
});
html += "</div>";
container.innerHTML = html;
}
console.log('[日志弹窗] 准备打开弹窗');
openModal("scheduleLogsModal");
console.log('[日志弹窗] 弹窗已打开');
})
.catch(err => {
console.error('[日志弹窗] 请求失败:', err);
showToast("加载日志失败", "error");
});
}
function formatDuration(seconds) {
if (seconds < 60) return seconds + "秒";
const minutes = Math.floor(seconds / 60);
const secs = seconds % 60;
return minutes + "分" + secs + "秒";
}
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').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) {
// 根据status字段确定状态显示
let statusClass = 'feedback-status-pending';
let statusText = '待处理';
if (f.status === 'replied') {
statusClass = 'feedback-status-replied';
statusText = '已回复';
} else if (f.status === 'closed') {
statusClass = 'feedback-status-closed';
statusText = '已关闭';
}
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.admin_reply ? '<div style="margin-top: 8px; padding: 8px; background: #f5f5f5; border-radius: 4px; font-size: 13px;"><strong>回复:</strong> ' + escapeHtml(f.admin_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.message) {
showToast('反馈已提交,感谢!', 'success');
closeModal('feedbackModal');
document.getElementById('feedbackTitle').value = '';
document.getElementById('feedbackDesc').value = '';
document.getElementById('feedbackContact').value = '';
} else {
showToast(data.error || '提交失败', 'error');
}
}).catch(function(err) {
showToast('提交失败,请重试', '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('/api/logout', {method: 'POST'})
.then(() => { window.location.href = '/login'; })
.catch(() => { 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>