1324 lines
49 KiB
HTML
1324 lines
49 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Telegram 下载控制台</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
|
min-height: 100vh;
|
|
color: #e8e8e8;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.header {
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
padding: 20px;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 2rem;
|
|
background: linear-gradient(90deg, #00d9ff, #00ff88);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.header p {
|
|
color: #888;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.card {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 16px;
|
|
padding: 24px;
|
|
margin-bottom: 20px;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
margin-bottom: 20px;
|
|
color: #00d9ff;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.card-title::before {
|
|
content: '';
|
|
width: 4px;
|
|
height: 20px;
|
|
background: linear-gradient(180deg, #00d9ff, #00ff88);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
color: #aaa;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.form-group input, .form-group select {
|
|
width: 100%;
|
|
padding: 12px 16px;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
color: #fff;
|
|
font-size: 1rem;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.form-group input:focus, .form-group select:focus {
|
|
outline: none;
|
|
border-color: #00d9ff;
|
|
box-shadow: 0 0 0 3px rgba(0, 217, 255, 0.1);
|
|
}
|
|
|
|
.form-group input::placeholder {
|
|
color: #666;
|
|
}
|
|
|
|
.date-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 16px;
|
|
}
|
|
|
|
.btn {
|
|
padding: 12px 24px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(90deg, #00d9ff, #00ff88);
|
|
color: #1a1a2e;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 20px rgba(0, 217, 255, 0.3);
|
|
}
|
|
|
|
.btn-primary:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
color: #fff;
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: rgba(255, 255, 255, 0.15);
|
|
}
|
|
|
|
.btn-warning {
|
|
background: linear-gradient(90deg, #ff9500, #ff5e3a);
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-warning:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 20px rgba(255, 149, 0, 0.3);
|
|
}
|
|
|
|
.btn-success {
|
|
background: linear-gradient(90deg, #00ff88, #00d9ff);
|
|
color: #1a1a2e;
|
|
}
|
|
|
|
.btn-success:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 20px rgba(0, 255, 136, 0.3);
|
|
}
|
|
|
|
.btn-large {
|
|
padding: 16px 32px;
|
|
font-size: 1.1rem;
|
|
width: 100%;
|
|
}
|
|
|
|
.btn-group {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.status-bar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 16px;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
border-radius: 8px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.status-item {
|
|
text-align: center;
|
|
}
|
|
|
|
.status-value {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: #00ff88;
|
|
}
|
|
|
|
.status-label {
|
|
font-size: 0.8rem;
|
|
color: #888;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.download-list {
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.download-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 12px;
|
|
background: rgba(0, 0, 0, 0.2);
|
|
border-radius: 8px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.download-item:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.download-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.download-name {
|
|
font-size: 0.9rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 6px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #00d9ff, #00ff88);
|
|
border-radius: 3px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.download-stats {
|
|
margin-left: 16px;
|
|
text-align: right;
|
|
min-width: 100px;
|
|
}
|
|
|
|
.download-percent {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: #00d9ff;
|
|
}
|
|
|
|
.download-speed {
|
|
font-size: 0.8rem;
|
|
color: #888;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: #666;
|
|
}
|
|
|
|
.empty-state svg {
|
|
width: 64px;
|
|
height: 64px;
|
|
margin-bottom: 16px;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.current-config {
|
|
background: rgba(0, 217, 255, 0.1);
|
|
border: 1px solid rgba(0, 217, 255, 0.2);
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.config-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 8px 0;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
.config-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.config-key {
|
|
color: #888;
|
|
}
|
|
|
|
.config-value {
|
|
color: #00ff88;
|
|
font-family: 'Monaco', 'Menlo', monospace;
|
|
}
|
|
|
|
.alert {
|
|
padding: 12px 16px;
|
|
border-radius: 8px;
|
|
margin-bottom: 16px;
|
|
display: none;
|
|
}
|
|
|
|
.alert-success {
|
|
background: rgba(0, 255, 136, 0.1);
|
|
border: 1px solid rgba(0, 255, 136, 0.3);
|
|
color: #00ff88;
|
|
}
|
|
|
|
.alert-error {
|
|
background: rgba(255, 100, 100, 0.1);
|
|
border: 1px solid rgba(255, 100, 100, 0.3);
|
|
color: #ff6464;
|
|
}
|
|
|
|
.hint {
|
|
font-size: 0.8rem;
|
|
color: #666;
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.input-with-btn {
|
|
display: flex;
|
|
gap: 12px;
|
|
}
|
|
|
|
.input-with-btn input {
|
|
flex: 1;
|
|
}
|
|
|
|
.input-with-btn .btn {
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.validation-result {
|
|
margin-top: 12px;
|
|
padding: 12px 16px;
|
|
border-radius: 8px;
|
|
display: none;
|
|
}
|
|
|
|
.validation-success {
|
|
background: rgba(0, 255, 136, 0.1);
|
|
border: 1px solid rgba(0, 255, 136, 0.3);
|
|
}
|
|
|
|
.validation-error {
|
|
background: rgba(255, 100, 100, 0.1);
|
|
border: 1px solid rgba(255, 100, 100, 0.3);
|
|
}
|
|
|
|
.channel-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.channel-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
background: linear-gradient(135deg, #00d9ff, #00ff88);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.channel-details h3 {
|
|
font-size: 1.1rem;
|
|
color: #fff;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.channel-details p {
|
|
font-size: 0.85rem;
|
|
color: #00ff88;
|
|
}
|
|
|
|
.spinner {
|
|
display: inline-block;
|
|
width: 16px;
|
|
height: 16px;
|
|
border: 2px solid rgba(255,255,255,0.3);
|
|
border-radius: 50%;
|
|
border-top-color: #00d9ff;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* History List Styles */
|
|
.history-list {
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.history-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 12px;
|
|
background: rgba(0, 0, 0, 0.2);
|
|
border-radius: 8px;
|
|
margin-bottom: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
border: 1px solid transparent;
|
|
}
|
|
|
|
.history-item:hover {
|
|
background: rgba(0, 217, 255, 0.1);
|
|
border-color: rgba(0, 217, 255, 0.3);
|
|
}
|
|
|
|
.history-item:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.history-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
background: linear-gradient(135deg, #00d9ff33, #00ff8833);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.2rem;
|
|
margin-right: 12px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.history-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.history-title {
|
|
font-size: 0.95rem;
|
|
color: #fff;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.history-meta {
|
|
font-size: 0.8rem;
|
|
color: #888;
|
|
display: flex;
|
|
gap: 12px;
|
|
}
|
|
|
|
.history-meta span {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.history-delete {
|
|
width: 32px;
|
|
height: 32px;
|
|
border: none;
|
|
background: rgba(255, 100, 100, 0.1);
|
|
border-radius: 6px;
|
|
color: #ff6464;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
opacity: 0;
|
|
transition: all 0.2s ease;
|
|
flex-shrink: 0;
|
|
margin-left: 8px;
|
|
}
|
|
|
|
.history-item:hover .history-delete {
|
|
opacity: 1;
|
|
}
|
|
|
|
.history-delete:hover {
|
|
background: rgba(255, 100, 100, 0.2);
|
|
}
|
|
|
|
.history-empty {
|
|
text-align: center;
|
|
padding: 30px;
|
|
color: #666;
|
|
}
|
|
|
|
.history-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.history-header .card-title {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.btn-text {
|
|
background: none;
|
|
border: none;
|
|
color: #ff6464;
|
|
cursor: pointer;
|
|
font-size: 0.85rem;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.btn-text:hover {
|
|
background: rgba(255, 100, 100, 0.1);
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.date-row {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.status-bar {
|
|
flex-wrap: wrap;
|
|
gap: 16px;
|
|
}
|
|
|
|
.status-item {
|
|
flex: 1;
|
|
min-width: 80px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>📥 Telegram 下载控制台</h1>
|
|
<p>配置频道和时间范围,一键下载媒体文件</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">当前配置</div>
|
|
<div id="alert-box" class="alert"></div>
|
|
<div class="current-config" id="current-config">
|
|
<div class="config-item">
|
|
<span class="config-key">频道</span>
|
|
<span class="config-value" id="cfg-channel">加载中...</span>
|
|
</div>
|
|
<div class="config-item">
|
|
<span class="config-key">时间过滤</span>
|
|
<span class="config-value" id="cfg-filter">加载中...</span>
|
|
</div>
|
|
<div class="config-item">
|
|
<span class="config-key">保存路径</span>
|
|
<span class="config-value" id="cfg-path">加载中...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 历史频道 -->
|
|
<div class="card" id="history-card">
|
|
<div class="history-header">
|
|
<div class="card-title">📜 历史频道</div>
|
|
<button type="button" class="btn-text" id="clear-history-btn" onclick="clearHistory()" style="display: none;">
|
|
🗑️ 清空历史
|
|
</button>
|
|
</div>
|
|
<div class="history-list" id="history-list">
|
|
<div class="history-empty">
|
|
<p>暂无历史记录</p>
|
|
<p style="font-size: 0.8rem; margin-top: 8px;">配置过的频道会显示在这里</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">新建下载任务</div>
|
|
<form id="download-form">
|
|
<div class="form-group">
|
|
<label>频道链接或用户名</label>
|
|
<div class="input-with-btn">
|
|
<input type="text" id="channel-input" placeholder="例如: https://t.me/happycat03 或 happycat03" required>
|
|
<button type="button" class="btn btn-secondary" id="validate-btn" onclick="validateChannel()">
|
|
🔍 验证频道
|
|
</button>
|
|
</div>
|
|
<p class="hint">支持 t.me 链接、@用户名 或直接输入用户名</p>
|
|
</div>
|
|
|
|
<!-- 频道验证结果 -->
|
|
<div id="validation-result" class="validation-result">
|
|
<div class="channel-info">
|
|
<div class="channel-icon" id="channel-icon">📺</div>
|
|
<div class="channel-details">
|
|
<h3 id="channel-title">频道名称</h3>
|
|
<p id="channel-type">类型: 频道</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="date-row">
|
|
<div class="form-group">
|
|
<label>开始日期</label>
|
|
<input type="date" id="start-date" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>结束日期(可选)</label>
|
|
<input type="date" id="end-date">
|
|
<p class="hint">留空表示下载到最新</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="btn-group" style="flex-wrap: wrap;">
|
|
<button type="button" class="btn btn-primary" id="restart-btn" onclick="saveAndRestart()">
|
|
🔄 保存并重启下载
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" onclick="location.href='/'">
|
|
📊 查看下载进度
|
|
</button>
|
|
</div>
|
|
<p class="hint" style="margin-top: 12px; text-align: center;">
|
|
💡 建议先点击"验证频道"确认频道有效后,再保存配置
|
|
</p>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">下载控制</div>
|
|
<div style="margin-bottom: 20px;">
|
|
<button type="button" class="btn btn-large btn-warning" id="pause-btn" onclick="toggleDownload()">
|
|
⏸️ 暂停下载
|
|
</button>
|
|
</div>
|
|
<div id="pause-status" style="text-align: center; margin-bottom: 16px; color: #888; font-size: 0.9rem;">
|
|
状态: <span id="current-state" style="color: #00ff88;">下载中</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">📊 任务进度</div>
|
|
<!-- 当前任务信息 -->
|
|
<div id="task-info" class="current-config" style="display: none;">
|
|
<div class="config-item">
|
|
<span class="config-key">当前频道</span>
|
|
<span class="config-value" id="task-chat">-</span>
|
|
</div>
|
|
<div class="config-item">
|
|
<span class="config-key">任务状态</span>
|
|
<span class="config-value" id="task-status">-</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="status-bar">
|
|
<div class="status-item">
|
|
<div class="status-value" id="download-speed">0 B/s</div>
|
|
<div class="status-label">下载速度</div>
|
|
</div>
|
|
<div class="status-item">
|
|
<div class="status-value" id="downloading-count">0</div>
|
|
<div class="status-label">下载中</div>
|
|
</div>
|
|
<div class="status-item">
|
|
<div class="status-value" id="completed-count">0</div>
|
|
<div class="status-label">已完成</div>
|
|
</div>
|
|
<div class="status-item">
|
|
<div class="status-value" id="skipped-count" style="color: #ff9500;">0</div>
|
|
<div class="status-label">已跳过</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="download-list" id="download-list">
|
|
<div class="empty-state" id="empty-state">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
<polyline points="7 10 12 15 17 10"></polyline>
|
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
|
</svg>
|
|
<p id="empty-state-text">暂无下载任务</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="text-align: center; padding: 20px; color: #666; font-size: 0.8rem;">
|
|
Telegram Media Downloader v<span id="version">2.2.5</span>
|
|
<br>
|
|
<span style="color: #00ff88;">✅ 点击"保存并重启下载"将自动保存配置并重启程序开始下载</span>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// 存储验证后的频道信息
|
|
let validatedChannel = null;
|
|
|
|
// 解析频道链接
|
|
function parseChannelId(input) {
|
|
input = input.trim();
|
|
// https://t.me/xxx 格式
|
|
const tmeMatch = input.match(/t\.me\/([a-zA-Z0-9_]+)/);
|
|
if (tmeMatch) return tmeMatch[1];
|
|
// @xxx 格式
|
|
if (input.startsWith('@')) return input.substring(1);
|
|
// 直接返回
|
|
return input;
|
|
}
|
|
|
|
// 加载当前配置
|
|
function loadConfig() {
|
|
fetch('/api/get_config')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
document.getElementById('cfg-channel').textContent = data.chat_id || '未配置';
|
|
document.getElementById('cfg-filter').textContent = data.download_filter || '无';
|
|
document.getElementById('cfg-path').textContent = data.save_path || '默认路径';
|
|
})
|
|
.catch(() => {
|
|
document.getElementById('cfg-channel').textContent = '获取失败';
|
|
});
|
|
}
|
|
|
|
// 类型图标映射
|
|
const typeIconMap = {
|
|
'CHANNEL': '📺',
|
|
'SUPERGROUP': '👥',
|
|
'GROUP': '👥',
|
|
'PRIVATE': '👤',
|
|
'BOT': '🤖'
|
|
};
|
|
|
|
// 格式化时间
|
|
function formatTime(isoString) {
|
|
if (!isoString) return '';
|
|
const date = new Date(isoString);
|
|
const now = new Date();
|
|
const diff = now - date;
|
|
|
|
if (diff < 60000) return '刚刚';
|
|
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
|
|
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前';
|
|
if (diff < 604800000) return Math.floor(diff / 86400000) + '天前';
|
|
|
|
return date.toLocaleDateString('zh-CN');
|
|
}
|
|
|
|
// 加载历史频道
|
|
function loadChannelHistory() {
|
|
fetch('/api/channel_history')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
const listEl = document.getElementById('history-list');
|
|
const clearBtn = document.getElementById('clear-history-btn');
|
|
|
|
if (!data.success || !data.history || data.history.length === 0) {
|
|
listEl.innerHTML = `
|
|
<div class="history-empty">
|
|
<p>暂无历史记录</p>
|
|
<p style="font-size: 0.8rem; margin-top: 8px;">配置过的频道会显示在这里</p>
|
|
</div>
|
|
`;
|
|
clearBtn.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
clearBtn.style.display = 'block';
|
|
|
|
listEl.innerHTML = data.history.map(item => `
|
|
<div class="history-item" onclick="selectHistory('${item.chat_id}', '${(item.chat_title || '').replace(/'/g, "\\'")}', '${item.chat_type || ''}')">
|
|
<div class="history-icon">${typeIconMap[item.chat_type] || '📁'}</div>
|
|
<div class="history-info">
|
|
<div class="history-title">${item.chat_title || item.chat_id}</div>
|
|
<div class="history-meta">
|
|
<span>@${item.chat_id}</span>
|
|
<span>🕐 ${formatTime(item.last_used)}</span>
|
|
${item.use_count > 1 ? `<span>📊 使用 ${item.use_count} 次</span>` : ''}
|
|
</div>
|
|
</div>
|
|
<button class="history-delete" onclick="event.stopPropagation(); deleteHistory('${item.chat_id}')" title="删除">
|
|
🗑️
|
|
</button>
|
|
</div>
|
|
`).join('');
|
|
})
|
|
.catch(err => {
|
|
console.error('Failed to load history:', err);
|
|
});
|
|
}
|
|
|
|
// 选择历史频道
|
|
function selectHistory(chatId, chatTitle, chatType) {
|
|
document.getElementById('channel-input').value = chatId;
|
|
|
|
// 设置已验证状态
|
|
validatedChannel = {
|
|
valid: true,
|
|
chat_id: chatId,
|
|
chat_title: chatTitle || chatId,
|
|
chat_type: chatType
|
|
};
|
|
|
|
// 显示验证结果
|
|
showValidationResult(validatedChannel);
|
|
|
|
showAlert(`已选择: ${chatTitle || chatId}`, 'success');
|
|
|
|
// 滚动到表单
|
|
document.getElementById('download-form').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
|
|
// 删除历史记录
|
|
function deleteHistory(chatId) {
|
|
if (!confirm('确定要删除这条历史记录吗?')) return;
|
|
|
|
fetch(`/api/channel_history/${encodeURIComponent(chatId)}`, {
|
|
method: 'DELETE'
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
loadChannelHistory();
|
|
showAlert('已删除', 'success');
|
|
} else {
|
|
showAlert('删除失败: ' + (data.error || '未知错误'), 'error');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
showAlert('删除失败: ' + err.message, 'error');
|
|
});
|
|
}
|
|
|
|
// 清空历史记录
|
|
function clearHistory() {
|
|
if (!confirm('确定要清空所有历史记录吗?此操作不可恢复。')) return;
|
|
|
|
fetch('/api/channel_history/clear', {
|
|
method: 'POST'
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
loadChannelHistory();
|
|
showAlert('历史记录已清空', 'success');
|
|
} else {
|
|
showAlert('清空失败: ' + (data.error || '未知错误'), 'error');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
showAlert('清空失败: ' + err.message, 'error');
|
|
});
|
|
}
|
|
|
|
// 显示提示
|
|
function showAlert(message, type) {
|
|
const alertBox = document.getElementById('alert-box');
|
|
alertBox.textContent = message;
|
|
alertBox.className = 'alert alert-' + type;
|
|
alertBox.style.display = 'block';
|
|
setTimeout(() => {
|
|
alertBox.style.display = 'none';
|
|
}, 5000);
|
|
}
|
|
|
|
// 显示验证结果
|
|
function showValidationResult(result) {
|
|
const resultEl = document.getElementById('validation-result');
|
|
const titleEl = document.getElementById('channel-title');
|
|
const typeEl = document.getElementById('channel-type');
|
|
const iconEl = document.getElementById('channel-icon');
|
|
|
|
if (result.valid) {
|
|
validatedChannel = result;
|
|
resultEl.className = 'validation-result validation-success';
|
|
titleEl.textContent = result.chat_title;
|
|
|
|
// 根据类型显示不同图标
|
|
const typeMap = {
|
|
'CHANNEL': { icon: '📺', name: '频道' },
|
|
'SUPERGROUP': { icon: '👥', name: '超级群组' },
|
|
'GROUP': { icon: '👥', name: '群组' },
|
|
'PRIVATE': { icon: '👤', name: '私聊' },
|
|
'BOT': { icon: '🤖', name: '机器人' }
|
|
};
|
|
const typeInfo = typeMap[result.chat_type] || { icon: '📁', name: result.chat_type };
|
|
iconEl.textContent = typeInfo.icon;
|
|
typeEl.textContent = `类型: ${typeInfo.name}`;
|
|
typeEl.style.color = '#00ff88';
|
|
} else {
|
|
validatedChannel = null;
|
|
resultEl.className = 'validation-result validation-error';
|
|
titleEl.textContent = '验证失败';
|
|
iconEl.textContent = '❌';
|
|
typeEl.textContent = result.error;
|
|
typeEl.style.color = '#ff6464';
|
|
}
|
|
|
|
resultEl.style.display = 'block';
|
|
}
|
|
|
|
// 验证频道
|
|
function validateChannel() {
|
|
const channelInput = document.getElementById('channel-input').value;
|
|
const channelId = parseChannelId(channelInput);
|
|
|
|
if (!channelId) {
|
|
showAlert('请输入频道链接或用户名', 'error');
|
|
return;
|
|
}
|
|
|
|
const btn = document.getElementById('validate-btn');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner"></span> 验证中...';
|
|
|
|
// 隐藏之前的结果
|
|
document.getElementById('validation-result').style.display = 'none';
|
|
validatedChannel = null;
|
|
|
|
fetch('/api/validate_chat', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ chat_id: channelId })
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
showValidationResult(data);
|
|
if (data.valid) {
|
|
showAlert(`✅ 已验证: ${data.chat_title}`, 'success');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
showValidationResult({ valid: false, error: err.message });
|
|
showAlert('验证失败: ' + err.message, 'error');
|
|
})
|
|
.finally(() => {
|
|
btn.disabled = false;
|
|
btn.textContent = '🔍 验证频道';
|
|
});
|
|
}
|
|
|
|
// 当输入框内容变化时,清除验证状态
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
document.getElementById('channel-input').addEventListener('input', function() {
|
|
validatedChannel = null;
|
|
document.getElementById('validation-result').style.display = 'none';
|
|
});
|
|
});
|
|
|
|
// 保存并重启
|
|
function saveAndRestart() {
|
|
const channelInput = document.getElementById('channel-input').value;
|
|
const startDate = document.getElementById('start-date').value;
|
|
const endDate = document.getElementById('end-date').value;
|
|
|
|
const channelId = parseChannelId(channelInput);
|
|
|
|
if (!channelId) {
|
|
showAlert('请输入有效的频道链接或用户名', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!startDate) {
|
|
showAlert('请选择开始日期', 'error');
|
|
return;
|
|
}
|
|
|
|
// 如果还没验证,先进行验证
|
|
if (!validatedChannel) {
|
|
const btn = document.getElementById('restart-btn');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner"></span> 验证频道中...';
|
|
|
|
fetch('/api/validate_chat', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ chat_id: channelId })
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.valid) {
|
|
showValidationResult(data);
|
|
// 验证成功,继续保存
|
|
doSaveAndRestart(channelId, startDate, endDate, data.chat_title, data.chat_type);
|
|
} else {
|
|
showValidationResult(data);
|
|
showAlert('频道验证失败: ' + data.error, 'error');
|
|
btn.disabled = false;
|
|
btn.textContent = '🔄 保存并重启下载';
|
|
}
|
|
})
|
|
.catch(err => {
|
|
showAlert('验证失败: ' + err.message, 'error');
|
|
btn.disabled = false;
|
|
btn.textContent = '🔄 保存并重启下载';
|
|
});
|
|
} else {
|
|
// 已验证,直接保存
|
|
doSaveAndRestart(channelId, startDate, endDate, validatedChannel.chat_title, validatedChannel.chat_type);
|
|
}
|
|
}
|
|
|
|
// 执行保存并重启
|
|
function doSaveAndRestart(channelId, startDate, endDate, chatTitle, chatType) {
|
|
const btn = document.getElementById('restart-btn');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner"></span> 正在重启...';
|
|
|
|
fetch('/api/save_and_restart', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
chat_id: channelId,
|
|
start_date: startDate,
|
|
end_date: endDate,
|
|
chat_title: chatTitle,
|
|
chat_type: chatType || ''
|
|
})
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('✅ ' + data.message + ' 页面将在5秒后刷新...', 'success');
|
|
// 等待程序重启后刷新页面
|
|
setTimeout(() => {
|
|
location.reload();
|
|
}, 5000);
|
|
} else {
|
|
showAlert('操作失败: ' + (data.error || '未知错误'), 'error');
|
|
btn.disabled = false;
|
|
btn.textContent = '🔄 保存并重启下载';
|
|
}
|
|
})
|
|
.catch(err => {
|
|
showAlert('操作失败: ' + err.message, 'error');
|
|
btn.disabled = false;
|
|
btn.textContent = '🔄 保存并重启下载';
|
|
});
|
|
}
|
|
|
|
// 表单提交时使用保存并重启
|
|
document.getElementById('download-form').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
saveAndRestart();
|
|
});
|
|
|
|
// 更新下载状态
|
|
function updateDownloadStatus() {
|
|
fetch('/get_download_status')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
// 后端已处理:暂停时返回0,直接显示即可
|
|
document.getElementById('download-speed').textContent = data.download_speed || '0 B/s';
|
|
|
|
// 保存当前暂停状态供任务进度使用
|
|
window.currentDownloadState = data.state; // 'pause' = 下载中, 'continue' = 已暂停
|
|
})
|
|
.catch(() => {});
|
|
|
|
// 获取任务进度
|
|
fetch('/api/task_progress')
|
|
.then(r => r.json())
|
|
.then(progress => {
|
|
const taskInfo = document.getElementById('task-info');
|
|
const taskChat = document.getElementById('task-chat');
|
|
const taskStatus = document.getElementById('task-status');
|
|
const skippedCount = document.getElementById('skipped-count');
|
|
const emptyStateText = document.getElementById('empty-state-text');
|
|
|
|
// 更新跳过计数
|
|
skippedCount.textContent = progress.skipped_files || 0;
|
|
|
|
// 检查是否暂停
|
|
const isPaused = window.currentDownloadState === 'continue';
|
|
|
|
// 如果有当前任务
|
|
if (progress.current_chat) {
|
|
taskInfo.style.display = 'block';
|
|
taskChat.textContent = progress.current_chat_title || progress.current_chat;
|
|
|
|
const downloading = progress.downloading_files || 0;
|
|
const completed = progress.completed_files || 0;
|
|
const skipped = progress.skipped_files || 0;
|
|
|
|
// 根据检查状态、下载状态和暂停状态综合判断
|
|
if (progress.is_checking && isPaused) {
|
|
// 检查中 + 用户暂停了下载
|
|
taskStatus.textContent = `⏸️🔍 已暂停下载,正在检查文件... (已跳过${skipped}个)`;
|
|
taskStatus.style.color = '#ff9500';
|
|
|
|
if (emptyStateText) {
|
|
emptyStateText.textContent = `下载已暂停,文件检查继续中...`;
|
|
}
|
|
} else if (progress.is_checking && downloading > 0) {
|
|
// 边检查边下载
|
|
taskStatus.textContent = `🔍🚀 正在检查文件并下载 (${downloading}个下载中, 已跳过${skipped}个)`;
|
|
taskStatus.style.color = '#00d9ff';
|
|
|
|
if (emptyStateText) {
|
|
emptyStateText.textContent = `正在检查文件并下载...`;
|
|
}
|
|
} else if (progress.is_checking) {
|
|
// 仅检查中
|
|
taskStatus.textContent = `🔍 正在检查文件... (已跳过 ${skipped} 个)`;
|
|
taskStatus.style.color = '#00d9ff';
|
|
|
|
if (emptyStateText) {
|
|
emptyStateText.textContent = `正在检查文件,已跳过 ${skipped} 个已下载文件...`;
|
|
}
|
|
} else if (isPaused) {
|
|
// 检查完成,用户点击了暂停
|
|
taskStatus.textContent = `⏸️ 已暂停下载 (已完成${completed}个, 跳过${skipped}个)`;
|
|
taskStatus.style.color = '#ff9500';
|
|
|
|
if (emptyStateText) {
|
|
emptyStateText.textContent = '检查完成,下载已暂停';
|
|
}
|
|
} else if (downloading > 0) {
|
|
// 检查完成,正在下载
|
|
taskStatus.textContent = `🚀 下载中 (${downloading}个下载中, 已完成${completed}个, 跳过${skipped}个)`;
|
|
taskStatus.style.color = '#00ff88';
|
|
|
|
if (emptyStateText) {
|
|
emptyStateText.textContent = `正在下载...`;
|
|
}
|
|
} else if (completed > 0 || skipped > 0) {
|
|
// 全部完成
|
|
taskStatus.textContent = `✅ 任务完成 (已完成${completed}个, 跳过${skipped}个)`;
|
|
taskStatus.style.color = '#00ff88';
|
|
|
|
if (emptyStateText) {
|
|
emptyStateText.textContent = `所有任务已完成`;
|
|
}
|
|
} else {
|
|
taskStatus.textContent = '⏳ 等待中';
|
|
taskStatus.style.color = '#888';
|
|
}
|
|
} else {
|
|
// 无当前任务时,根据暂停状态显示
|
|
if (isPaused) {
|
|
taskInfo.style.display = 'block';
|
|
taskChat.textContent = '-';
|
|
taskStatus.textContent = '⏸️ 已暂停下载';
|
|
taskStatus.style.color = '#ff9500';
|
|
} else {
|
|
taskInfo.style.display = 'none';
|
|
}
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
|
|
fetch('/get_download_list?already_down=false')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
const downloadingCount = data.filter(d => d.download_progress < 100).length;
|
|
const completedCount = data.filter(d => d.download_progress >= 100).length;
|
|
|
|
document.getElementById('downloading-count').textContent = downloadingCount;
|
|
document.getElementById('completed-count').textContent = completedCount;
|
|
|
|
const listEl = document.getElementById('download-list');
|
|
|
|
if (data.length === 0) {
|
|
// 保留空状态但不覆盖检查中的提示
|
|
const existingEmpty = listEl.querySelector('.empty-state');
|
|
if (!existingEmpty) {
|
|
listEl.innerHTML = `
|
|
<div class="empty-state" id="empty-state">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
<polyline points="7 10 12 15 17 10"></polyline>
|
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
|
</svg>
|
|
<p id="empty-state-text">暂无下载任务</p>
|
|
</div>
|
|
`;
|
|
}
|
|
return;
|
|
}
|
|
|
|
listEl.innerHTML = data.map(item => `
|
|
<div class="download-item">
|
|
<div class="download-info">
|
|
<div class="download-name" title="${item.filename}">${item.filename}</div>
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" style="width: ${item.download_progress}%"></div>
|
|
</div>
|
|
</div>
|
|
<div class="download-stats">
|
|
<div class="download-percent">${item.download_progress}%</div>
|
|
<div class="download-speed">${item.download_speed}</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
// 设置默认日期
|
|
document.getElementById('start-date').value = '2024-01-01';
|
|
|
|
// 初始化
|
|
loadConfig();
|
|
loadChannelHistory();
|
|
updateDownloadStatus();
|
|
setInterval(updateDownloadStatus, 2000);
|
|
|
|
// 获取版本
|
|
fetch('/get_app_version')
|
|
.then(r => r.text())
|
|
.then(v => document.getElementById('version').textContent = v)
|
|
.catch(() => {});
|
|
|
|
// 当前下载状态
|
|
let isDownloading = true;
|
|
|
|
// 切换下载状态(暂停/继续)
|
|
function toggleDownload() {
|
|
const btn = document.getElementById('pause-btn');
|
|
const stateEl = document.getElementById('current-state');
|
|
|
|
btn.disabled = true;
|
|
|
|
const action = isDownloading ? 'pause' : 'continue';
|
|
|
|
fetch('/set_download_state?state=' + action, {
|
|
method: 'POST',
|
|
credentials: 'include'
|
|
})
|
|
.then(r => {
|
|
if (r.redirected || r.status === 401 || r.status === 302) {
|
|
// 需要登录,跳转到登录页
|
|
window.location.href = '/login';
|
|
throw new Error('需要登录');
|
|
}
|
|
return r.text();
|
|
})
|
|
.then(response => {
|
|
// 响应是下一个操作: "pause" 表示当前是下载中,"continue" 表示当前是暂停
|
|
const taskStatus = document.getElementById('task-status');
|
|
|
|
if (response === 'pause') {
|
|
isDownloading = true;
|
|
window.currentDownloadState = 'pause';
|
|
btn.textContent = '⏸️ 暂停下载';
|
|
btn.className = 'btn btn-large btn-warning';
|
|
stateEl.textContent = '下载中';
|
|
stateEl.style.color = '#00ff88';
|
|
// 任务状态由 updateDownloadStatus 自动更新(保持检查状态或显示下载中)
|
|
showAlert('已恢复下载', 'success');
|
|
} else if (response === 'continue') {
|
|
isDownloading = false;
|
|
window.currentDownloadState = 'continue';
|
|
btn.textContent = '▶️ 继续下载';
|
|
btn.className = 'btn btn-large btn-success';
|
|
stateEl.textContent = '已暂停';
|
|
stateEl.style.color = '#ff9500';
|
|
// 暂停时清零速度显示
|
|
document.getElementById('download-speed').textContent = '0 B/s';
|
|
// 任务状态由 updateDownloadStatus 自动更新(检查中继续显示检查,检查完成后显示暂停)
|
|
showAlert('已暂停下载(文件检查将继续完成)', 'success');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
if (err.message !== '需要登录') {
|
|
showAlert('操作失败: ' + err.message, 'error');
|
|
}
|
|
})
|
|
.finally(() => {
|
|
btn.disabled = false;
|
|
});
|
|
}
|
|
|
|
// 获取当前下载状态
|
|
function checkDownloadState() {
|
|
fetch('/get_download_status')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
const btn = document.getElementById('pause-btn');
|
|
const stateEl = document.getElementById('current-state');
|
|
|
|
// 保存全局状态
|
|
window.currentDownloadState = data.state;
|
|
|
|
// data.state 是 "pause" 表示当前下载中可以暂停,"continue" 表示当前暂停可以继续
|
|
if (data.state === 'pause') {
|
|
isDownloading = true;
|
|
btn.textContent = '⏸️ 暂停下载';
|
|
btn.className = 'btn btn-large btn-warning';
|
|
stateEl.textContent = '下载中';
|
|
stateEl.style.color = '#00ff88';
|
|
} else {
|
|
isDownloading = false;
|
|
btn.textContent = '▶️ 继续下载';
|
|
btn.className = 'btn btn-large btn-success';
|
|
stateEl.textContent = '已暂停';
|
|
stateEl.style.color = '#ff9500';
|
|
// 暂停时清零速度显示
|
|
document.getElementById('download-speed').textContent = '0 B/s';
|
|
// 任务状态由 updateDownloadStatus 自动更新(根据检查状态和暂停状态综合判断)
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
// 初始化时检查状态
|
|
checkDownloadState();
|
|
</script>
|
|
</body>
|
|
</html>
|