- 修复添加账号按钮无反应问题
- 添加账号备注字段(可选)
- 添加账号设置按钮(修改密码/备注)
- 修复用户反馈���能
- 添加定时任务执行日志
- 修复容器重启后账号加载问题
- 修复所有JavaScript语法错误
- 优化账号加载机制(4层保障)
🤖 Generated with Claude Code
1439 lines
79 KiB
Plaintext
1439 lines
79 KiB
Plaintext
<!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; }
|
||
.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; width: 56px; height: 56px; border-radius: 50%; background: var(--md-primary); color: white; border: none; font-size: 24px; cursor: pointer; box-shadow: var(--md-shadow-2); transition: all 0.2s; z-index: 100; }
|
||
.fab:hover { background: var(--md-primary-dark); box-shadow: var(--md-shadow-3); transform: scale(1.05); }
|
||
|
||
.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 id="usernameDisplay">{{ username }}</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>
|
||
<span id="accountLimit" style="font-size: 12px; opacity: 0.8;"></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>
|
||
|
||
<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="添加账号">+</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="checkbox-wrapper">
|
||
<input type="checkbox" id="newAccountRemember" checked>
|
||
<span>记住密码</span>
|
||
</label>
|
||
</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()">×</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>
|
||
// ==================== 全局变量 ====================
|
||
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 };
|
||
const socket = io();
|
||
|
||
// 图片预览相关
|
||
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 connected'); });
|
||
socket.on('accounts_list', function(accountsList) {
|
||
accounts = {};
|
||
accountsList.forEach(acc => { accounts[acc.id] = acc; });
|
||
renderAccounts();
|
||
updateAccountLimitDisplay();
|
||
});
|
||
socket.on('account_update', function(acc) {
|
||
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.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');
|
||
if (limitEl) {
|
||
if (vipInfo.is_vip) {
|
||
limitEl.textContent = '(' + accountLimit.current + '/∞)';
|
||
} else {
|
||
limitEl.textContent = '(' + accountLimit.current + '/' + accountLimit.max + ')';
|
||
}
|
||
}
|
||
}
|
||
|
||
// ==================== 账号渲染 ====================
|
||
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 = '';
|
||
if (isRunning && acc.detail_status) {
|
||
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="takeScreenshot(\'' + acc.id + '\')" ' + (!isRunning ? 'disabled' : '') + ' title="手动截图">📷</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 = document.getElementById('newAccountRemember').checked;
|
||
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('/api/logout', {method: 'POST'}).then(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> |