import os
import time
import re
import random
import string
import shutil
import threading
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse, unquote
from PIL import Image
from concurrent.futures import ThreadPoolExecutor, as_completed
from flask import Flask, render_template, request, redirect, url_for, send_from_directory, session, jsonify
app = Flask(__name__)
app.secret_key = 'your_secret_key_here'
app.config['UPLOAD_FOLDER'] = 'output_pdfs'
app.config['TEMP_FOLDER'] = 'temp_images'
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB
# 确保文件夹存在
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
os.makedirs(app.config['TEMP_FOLDER'], exist_ok=True)
# 全局任务队列和状态
task_queue = []
current_task = None
task_lock = threading.Lock()
task_status = {
'current': None,
'queue': [],
'completed': [],
'processing': False,
'progress': {} # 存储每个任务的进度 {task_id: {'total': 10, 'downloaded': 5}}
}
def download_image(img_url, file_path, retry_delay=5, max_retries=3):
"""下载图片并保存到指定路径"""
attempts = 0
while attempts < max_retries:
try:
img_response = requests.get(img_url, stream=True, timeout=10)
if img_response.status_code == 200:
with open(file_path, 'wb') as file:
for chunk in img_response.iter_content(1024):
file.write(chunk)
return True
else:
raise Exception(f"状态码: {img_response.status_code}")
except Exception as e:
attempts += 1
print(f"下载失败 ({attempts}/{max_retries}),原因: {e}")
if attempts < max_retries:
time.sleep(retry_delay)
return False
def sanitize_filename(filename):
"""清理文件名中的非法字符"""
decoded = unquote(filename)
sanitized = re.sub(r'[<>:"/\\|?*]', '_', decoded)
sanitized = sanitized.strip()
if len(sanitized) > 100:
sanitized = sanitized[:100]
if not sanitized:
sanitized = "webpage"
return sanitized
def get_filename_from_url(url):
"""从URL中提取合适的文件名"""
parsed = urlparse(url)
path = parsed.path
path_parts = [part for part in path.split('/') if part]
if path_parts:
filename = path_parts[-1]
else:
filename = parsed.netloc.split(':')[0]
sanitized = sanitize_filename(filename)
if not sanitized.lower().endswith('.pdf'):
sanitized += '.pdf'
return sanitized
def download_images_from_page(page_url, save_folder, task_id, max_workers=10):
"""从页面下载所有图片并保存到指定文件夹"""
os.makedirs(save_folder, exist_ok=True)
response = requests.get(page_url, timeout=10)
if response.status_code != 200:
print(f"无法访问页面,状态码: {response.status_code}")
return []
soup = BeautifulSoup(response.text, 'html.parser')
images = soup.find_all('img')
print(f"找到 {len(images)} 张图片。")
# 更新任务进度 - 总图片数
with task_lock:
task_status['progress'][task_id] = {
'total': len(images),
'downloaded': 0,
'pdf_name': get_filename_from_url(page_url)
}
download_tasks = []
for index, img in enumerate(images, start=1):
img_url = img.get('src')
if img_url:
img_url = urljoin(page_url, img_url)
file_ext = os.path.splitext(img_url)[1][:5] # 获取文件扩展名
file_path = os.path.join(save_folder, f"image_{index}{file_ext or '.jpg'}")
download_tasks.append((index, img_url, file_path))
image_paths = [None] * len(download_tasks)
lock = threading.Lock()
def worker(task):
idx, url, path = task
success = download_image(url, path)
with lock:
if success:
image_paths[idx-1] = path
print(f"图片 {idx} 下载完成")
# 更新下载进度
with task_lock:
if task_id in task_status['progress']:
task_status['progress'][task_id]['downloaded'] += 1
return success
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [executor.submit(worker, task) for task in download_tasks]
for future in as_completed(futures):
pass
successful_paths = [path for path in image_paths if path is not None]
print(f"成功下载 {len(successful_paths)} 张图片")
return successful_paths
def create_pdf(image_paths, output_pdf):
"""将图片打包成PDF"""
image_list = []
for img_path in image_paths:
if os.path.exists(img_path):
try:
img = Image.open(img_path).convert("RGB")
image_list.append(img)
except Exception as e:
print(f"无法处理图片 {img_path}: {e}")
if image_list:
image_list[0].save(output_pdf, save_all=True, append_images=image_list[1:])
print(f"PDF 文件已保存: {output_pdf}")
return True
else:
print("没有有效图片可生成PDF")
return False
def cleanup_images(image_paths):
"""删除图片文件"""
for img_path in image_paths:
if os.path.exists(img_path):
try:
os.remove(img_path)
except Exception as e:
print(f"删除文件失败 {img_path}: {e}")
print(f"已删除 {len(image_paths)} 个图片文件。")
def process_task(task):
"""处理单个URL任务"""
url = task['url']
task_id = task['id']
# 创建唯一的临时文件夹
timestamp = int(time.time())
random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
temp_dir = os.path.join(app.config['TEMP_FOLDER'], f"temp_{timestamp}_{random_str}")
os.makedirs(temp_dir, exist_ok=True)
# 生成PDF文件名
pdf_filename = get_filename_from_url(url)
pdf_path = os.path.join(app.config['UPLOAD_FOLDER'], pdf_filename)
print(f"开始处理URL: {url}")
start_time = time.time()
# 下载图片
image_paths = download_images_from_page(url, temp_dir, task_id)
# 生成PDF
success = False
if image_paths:
success = create_pdf(image_paths, pdf_path)
# 清理临时图片
cleanup_images(image_paths)
# 删除临时文件夹
try:
shutil.rmtree(temp_dir)
except Exception as e:
print(f"删除临时文件夹失败: {e}")
processing_time = time.time() - start_time
# 清理进度信息
with task_lock:
if task_id in task_status['progress']:
del task_status['progress'][task_id]
return {
'url': url,
'pdf_path': pdf_path,
'filename': pdf_filename,
'success': success,
'time': processing_time,
'id': task_id
}
def process_queue():
"""后台线程处理任务队列"""
global current_task, task_status
while True:
with task_lock:
if task_queue:
# 从队列中取出下一个任务
task = task_queue.pop(0)
current_task = task
task_status['current'] = task
task_status['processing'] = True
else:
current_task = None
task_status['current'] = None
task_status['processing'] = False
break
# 处理任务
if task:
try:
result = process_task(task)
with task_lock:
task_status['completed'].append(result)
task_status['current'] = None
except Exception as e:
print(f"处理任务失败: {e}")
with task_lock:
task_status['completed'].append({
'url': task['url'],
'error': str(e),
'success': False,
'id': task['id']
})
task_status['current'] = None
time.sleep(1) # 防止CPU过度占用
@app.route('/', methods=['GET'])
def index():
"""首页,显示URL输入表单"""
return render_template('index.html')
@app.route('/add-task', methods=['POST'])
def add_task():
"""添加新任务到队列"""
url = request.form.get('url')
if not url:
return jsonify({'error': 'URL不能为空'}), 400
# 创建任务对象
task_id = ''.join(random.choices(string.ascii_letters + string.digits, k=10))
task = {
'url': url,
'added_time': time.time(),
'id': task_id
}
# 添加到任务队列
with task_lock:
task_queue.append(task)
task_status['queue'].append(task)
# 如果没有处理线程在运行,启动一个
if not task_status.get('processing', False):
threading.Thread(target=process_queue, daemon=True).start()
return jsonify({
'message': '任务已添加到队列',
'task_id': task_id,
'pdf_name': get_filename_from_url(url)
})
@app.route('/task-status')
def get_task_status():
"""获取任务状态"""
with task_lock:
# 准备返回数据
current = task_status['current']
queue = task_status['queue']
completed = task_status['completed']
progress = task_status['progress']
# 简化数据
status = {
'current': current,
'queue': [{
'url': t['url'],
'id': t['id'],
'pdf_name': get_filename_from_url(t['url'])
} for t in queue],
'completed': [{
'url': c['url'],
'success': c['success'],
'filename': c.get('filename', ''),
'id': c.get('id', '')
} for c in completed],
'progress': progress
}
return jsonify(status)
@app.route('/download/<filename>')
def download_file(filename):
"""下载生成的PDF文件"""
return send_from_directory(app.config['UPLOAD_FOLDER'], filename, as_attachment=True)
@app.route('/cleanup')
def cleanup():
"""清理生成的文件"""
try:
# 删除所有临时图片文件夹
temp_dir = app.config['TEMP_FOLDER']
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
os.makedirs(temp_dir)
return "清理成功", 200
except Exception as e:
return f"清理失败: {str(e)}", 500
if __name__ == '__main__':
app.run(debug=True, threaded=True)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网页图片转PDF工具</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<style>
:root {
--primary-gradient: linear-gradient(45deg, #1a2a6c, #b21f1f);
--secondary-gradient: linear-gradient(45deg, #4776E6, #8E54E9);
--success-gradient: linear-gradient(45deg, #28a745, #20c997);
--processing-gradient: linear-gradient(45deg, #007bff, #00b4d8);
--warning-gradient: linear-gradient(45deg, #ffc107, #ff9a3d);
--dark-bg: #0f172a;
}
body {
background: var(--dark-bg);
min-height: 100vh;
padding-top: 40px;
padding-bottom: 40px;
color: #f1f5f9;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.card {
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
border: none;
background: rgba(30, 41, 59, 0.7);
backdrop-filter: blur(10px);
overflow: hidden;
transition: transform 0.3s, box-shadow 0.3s;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.6);
}
.card-header {
background: var(--primary-gradient);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding: 25px 30px;
}
.card-body {
padding: 30px;
}
.btn-primary {
background: var(--primary-gradient);
border: none;
transition: all 0.3s;
font-weight: 600;
letter-spacing: 0.5px;
}
.btn-primary:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(178, 31, 31, 0.4);
}
.btn-outline-light {
border-color: rgba(255, 255, 255, 0.3);
color: #f1f5f9;
transition: all 0.3s;
}
.btn-outline-light:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.6);
}
.btn-outline-danger {
border-color: rgba(220, 53, 69, 0.5);
color: #dc3545;
transition: all 0.3s;
}
.btn-outline-danger:hover {
background: rgba(220, 53, 69, 0.1);
border-color: rgba(220, 53, 69, 0.8);
}
.task-list {
max-height: 500px;
overflow-y: auto;
background: rgba(15, 23, 42, 0.5);
border-radius: 12px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.task-item {
background: rgba(30, 41, 59, 0.7);
border-radius: 10px;
padding: 20px;
margin-bottom: 15px;
transition: all 0.3s;
border-left: 4px solid #b21f1f;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.task-item:hover {
background: rgba(51, 65, 85, 0.7);
transform: translateX(5px);
}
.task-item.processing {
border-left: 4px solid #007bff;
background: rgba(0, 123, 255, 0.1);
}
.task-item.completed {
border-left: 4px solid #28a745;
background: rgba(40, 167, 69, 0.1);
}
.status-badge {
padding: 5px 12px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
letter-spacing: 0.5px;
}
.status-queued {
background: var(--warning-gradient);
color: white;
}
.status-processing {
background: var(--processing-gradient);
color: white;
}
.status-completed {
background: var(--success-gradient);
color: white;
}
.progress-container {
background: rgba(15, 23, 42, 0.7);
border-radius: 10px;
padding: 15px;
margin-top: 15px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.progress {
height: 12px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.1);
overflow: visible;
margin-bottom: 8px;
}
.progress-bar {
background: var(--primary-gradient);
border-radius: 10px;
transition: width 0.5s ease;
box-shadow: 0 2px 5px rgba(178, 31, 31, 0.3);
}
.progress-text {
font-size: 0.9rem;
color: #cbd5e1;
display: flex;
justify-content: space-between;
}
.form-control {
background: rgba(15, 23, 42, 0.7);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
padding: 12px 15px;
border-radius: 10px;
transition: all 0.3s;
}
.form-control:focus {
background: rgba(15, 23, 42, 0.9);
border-color: rgba(178, 31, 31, 0.6);
box-shadow: 0 0 0 0.25rem rgba(178, 31, 31, 0.25);
color: white;
}
::placeholder {
color: rgba(255, 255, 255, 0.5) !important;
}
.footer {
text-align: center;
padding: 25px 0;
color: rgba(255, 255, 255, 0.6);
font-size: 0.9rem;
}
.download-btn {
background: var(--secondary-gradient);
border: none;
transition: all 0.3s;
}
.download-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(71, 118, 230, 0.4);
}
.task-pdf-name {
font-weight: 600;
font-size: 1.1rem;
color: #e2e8f0;
margin-bottom: 5px;
display: flex;
align-items: center;
}
.task-pdf-name i {
margin-right: 8px;
font-size: 1.2rem;
}
.task-url {
font-size: 0.9rem;
color: #94a3b8;
margin-bottom: 10px;
word-break: break-all;
}
.empty-queue {
text-align: center;
padding: 40px 20px;
opacity: 0.7;
}
.empty-queue i {
font-size: 3.5rem;
margin-bottom: 15px;
color: #475569;
}
.section-title {
display: flex;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.section-title i {
margin-right: 10px;
font-size: 1.5rem;
}
.glow-effect {
animation: glow 2s infinite alternate;
}
@keyframes glow {
from {
box-shadow: 0 0 5px rgba(178, 31, 31, 0.5);
}
to {
box-shadow: 0 0 20px rgba(178, 31, 31, 0.8);
}
}
/* 自定义滚动条 */
.task-list::-webkit-scrollbar {
width: 8px;
}
.task-list::-webkit-scrollbar-track {
background: rgba(15, 23, 42, 0.5);
border-radius: 4px;
}
.task-list::-webkit-scrollbar-thumb {
background: var(--primary-gradient);
border-radius: 4px;
}
.task-list::-webkit-scrollbar-thumb:hover {
background: #b21f1f;
}
.alert {
border-radius: 10px;
}
</style>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-9">
<div class="card glow-effect">
<div class="card-header text-center">
<h1 class="display-4 fw-bold"><i class="bi bi-file-earmark-pdf"></i> 网页图片转PDF工具</h1>
<p class="mb-0">高效下载网页图片并转换为PDF文件</p>
</div>
<div class="card-body">
<!-- 任务输入区域 -->
<div class="mb-5">
<div class="section-title">
<i class="bi bi-plus-circle"></i>
<h3>添加新任务</h3>
</div>
<div class="input-group">
<input type="text" class="form-control" id="url-input"
placeholder="输入网页URL (例如: https://example.com)"
aria-label="网页URL">
<button class="btn btn-primary" type="button" id="add-task-btn">
<i class="bi bi-plus-lg"></i> 添加到队列
</button>
</div>
<div id="message-area" class="mt-3"></div>
</div>
<!-- 进度指示器 -->
<div class="mb-5" id="progress-container" style="display: none;">
<div class="section-title">
<i class="bi bi-graph-up"></i>
<h3>当前任务进度</h3>
</div>
<div class="progress-container">
<div class="progress mb-3">
<div class="progress-bar" role="progressbar" id="progress-bar" style="width: 0%"></div>
</div>
<div class="progress-text">
<span id="progress-text">等待开始...</span>
<span id="timer">00:00</span>
</div>
<div class="mt-2 d-flex justify-content-between">
<small id="pdf-name">PDF名称: -</small>
<small id="images-count">图片: 0/0</small>
</div>
</div>
</div>
<!-- 任务状态区域 -->
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="section-title">
<i class="bi bi-list-task"></i>
<h3>任务队列</h3>
</div>
<div>
<button class="btn btn-sm btn-outline-light me-2" id="refresh-btn">
<i class="bi bi-arrow-repeat"></i> 刷新
</button>
<button class="btn btn-sm btn-outline-danger" id="cleanup-btn">
<i class="bi bi-trash"></i> 清理
</button>
</div>
</div>
<div class="task-list" id="task-list">
<div class="empty-queue" id="empty-queue">
<i class="bi bi-inbox"></i>
<h5 class="text-muted">任务队列为空</h5>
<p class="text-muted">添加URL开始处理</p>
</div>
</div>
</div>
</div>
</div>
<div class="footer mt-4">
<p>© 2023 网页图片转PDF工具 | 实时进度显示 | 高效转换</p>
</div>
</div>
</div>
</div>
<!-- 任务项模板 -->
<template id="task-template">
<div class="task-item">
<div class="task-pdf-name">
<i class="bi bi-file-earmark-pdf"></i>
<span class="pdf-name">PDF名称</span>
</div>
<div class="task-url">URL: <span class="task-url-value"></span></div>
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="status-badge status-queued">等待中</span>
<small class="ms-2 task-time">刚刚添加</small>
</div>
<div class="task-actions">
<button class="btn btn-sm download-btn">
<i class="bi bi-download"></i> 下载
</button>
</div>
</div>
<!-- 进度条容器 -->
<div class="progress-container" style="display: none;">
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
<div class="progress-text">
<span class="progress-text">下载图片: 0/0</span>
<span class="progress-percent">0%</span>
</div>
</div>
</div>
</template>
<script>
// DOM元素
const urlInput = document.getElementById('url-input');
const addTaskBtn = document.getElementById('add-task-btn');
const taskList = document.getElementById('task-list');
const emptyQueue = document.getElementById('empty-queue');
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
const pdfNameElement = document.getElementById('pdf-name');
const imagesCountElement = document.getElementById('images-count');
const timer = document.getElementById('timer');
const refreshBtn = document.getElementById('refresh-btn');
const cleanupBtn = document.getElementById('cleanup-btn');
const messageArea = document.getElementById('message-area');
// 任务模板
const taskTemplate = document.getElementById('task-template').content;
// 当前任务状态
let startTime = null;
let timerInterval = null;
let taskElementsMap = new Map(); // 存储任务ID与DOM元素的映射
// 添加任务
addTaskBtn.addEventListener('click', () => {
const url = urlInput.value.trim();
if (!url) {
showMessage('请输入有效的URL', 'error');
return;
}
// 验证URL格式
if (!isValidUrl(url)) {
showMessage('URL格式无效,请使用完整URL (例如: https://example.com)', 'error');
return;
}
// 发送请求到服务器
fetch('/add-task', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `url=${encodeURIComponent(url)}`
})
.then(response => response.json())
.then(data => {
if (data.error) {
showMessage(data.error, 'error');
} else {
showMessage(`任务已添加到队列: ${url}`, 'success');
urlInput.value = '';
urlInput.focus();
// 添加任务到DOM
addTaskToDOM({
url: url,
id: data.task_id,
pdf_name: data.pdf_name
}, 'queued');
}
})
.catch(error => {
showMessage('添加任务失败: ' + error, 'error');
});
});
// 刷新任务列表
refreshBtn.addEventListener('click', updateTaskList);
// 清理任务
cleanupBtn.addEventListener('click', () => {
fetch('/cleanup')
.then(response => response.text())
.then(data => {
showMessage(data, 'success');
})
.catch(error => {
showMessage('清理失败: ' + error, 'error');
});
});
// 初始加载任务列表
updateTaskList();
// 定期更新任务状态
setInterval(updateTaskList, 2000);
// URL验证函数
function isValidUrl(url) {
try {
new URL(url);
return true;
} catch (e) {
return false;
}
}
// 显示消息
function showMessage(message, type) {
messageArea.innerHTML = `
<div class="alert alert-${type === 'error' ? 'danger' : 'success'} alert-dismissible fade show">
<i class="bi ${type === 'error' ? 'bi-exclamation-triangle' : 'bi-check-circle'} me-2"></i>
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
// 5秒后自动消失
setTimeout(() => {
messageArea.innerHTML = '';
}, 5000);
}
// 更新任务列表
function updateTaskList() {
fetch('/task-status')
.then(response => response.json())
.then(data => {
// 更新当前任务进度
updateCurrentProgress(data);
// 更新队列中的任务
updateQueuedTasks(data);
// 更新已完成的任务
updateCompletedTasks(data);
});
}
// 更新当前任务进度
function updateCurrentProgress(data) {
if (data.current) {
progressContainer.style.display = 'block';
progressBar.style.width = '50%';
progressText.textContent = `正在处理: ${data.current.url.substring(0, 40)}${data.current.url.length > 40 ? '...' : ''}`;
// 获取PDF名称
const pdfName = getFilenameFromUrl(data.current.url);
pdfNameElement.textContent = `PDF名称: ${pdfName}`;
// 更新图片下载进度
if (data.progress && data.progress[data.current.id]) {
const progress = data.progress[data.current.id];
const downloaded = progress.downloaded || 0;
const total = progress.total || 1;
const percent = Math.round((downloaded / total) * 50);
progressBar.style.width = `${percent}%`;
imagesCountElement.textContent = `图片: ${downloaded}/${total}`;
}
// 启动计时器
if (!startTime) {
startTime = new Date();
startTimer();
}
} else {
progressContainer.style.display = 'none';
// 停止计时器
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
startTime = null;
timer.textContent = '00:00';
}
}
}
// 更新队列中的任务
function updateQueuedTasks(data) {
data.queue.forEach(task => {
// 如果任务不在DOM中,添加它
if (!taskElementsMap.has(task.id)) {
addTaskToDOM(task, 'queued');
}
// 更新任务状态
const taskElement = taskElementsMap.get(task.id);
if (taskElement) {
// 更新进度信息
const progressContainer = taskElement.querySelector('.progress-container');
const progressBar = taskElement.querySelector('.progress-bar');
const progressText = taskElement.querySelector('.progress-text .progress-text');
const progressPercent = taskElement.querySelector('.progress-text .progress-percent');
if (data.progress && data.progress[task.id]) {
const progress = data.progress[task.id];
const downloaded = progress.downloaded || 0;
const total = progress.total || 1;
const percent = Math.round((downloaded / total) * 100);
progressContainer.style.display = 'block';
progressBar.style.width = `${percent}%`;
progressText.textContent = `下载图片: ${downloaded}/${total}`;
progressPercent.textContent = `${percent}%`;
} else {
progressContainer.style.display = 'none';
}
}
});
}
// 更新已完成的任务
function updateCompletedTasks(data) {
data.completed.forEach(task => {
// 如果任务不在DOM中,添加它
if (!taskElementsMap.has(task.id)) {
addTaskToDOM(task, 'completed');
}
// 更新任务状态为已完成
const taskElement = taskElementsMap.get(task.id);
if (taskElement) {
taskElement.classList.add('completed');
taskElement.classList.remove('processing');
const statusBadge = taskElement.querySelector('.status-badge');
statusBadge.className = 'status-badge status-completed';
statusBadge.textContent = task.success ? '完成' : '失败';
const taskTime = taskElement.querySelector('.task-time');
taskTime.textContent = task.success ? '转换成功' : '转换失败';
// 隐藏进度条
const progressContainer = taskElement.querySelector('.progress-container');
progressContainer.style.display = 'none';
// 设置下载按钮
const downloadBtn = taskElement.querySelector('.download-btn');
if (task.success) {
downloadBtn.style.display = 'block';
downloadBtn.addEventListener('click', () => {
window.location.href = `/download/${task.filename}`;
});
} else {
downloadBtn.style.display = 'none';
}
}
});
}
// 添加任务到DOM
function addTaskToDOM(task, status) {
const taskElement = taskTemplate.cloneNode(true);
const taskNode = taskElement.querySelector('.task-item');
const pdfNameElement = taskElement.querySelector('.pdf-name');
const taskUrlElement = taskElement.querySelector('.task-url-value');
const statusBadge = taskElement.querySelector('.status-badge');
const taskTime = taskElement.querySelector('.task-time');
const downloadBtn = taskElement.querySelector('.download-btn');
// 设置任务信息
pdfNameElement.textContent = task.pdf_name || getFilenameFromUrl(task.url);
taskUrlElement.textContent = task.url;
taskUrlElement.title = task.url;
// 设置状态
if (status === 'queued') {
statusBadge.className = 'status-badge status-queued';
statusBadge.textContent = '等待中';
taskTime.textContent = '等待处理';
taskNode.classList.add('queued');
} else if (status === 'processing') {
statusBadge.className = 'status-badge status-processing';
statusBadge.textContent = '处理中';
taskTime.textContent = '正在处理...';
taskNode.classList.add('processing');
} else if (status === 'completed') {
statusBadge.className = 'status-badge status-completed';
statusBadge.textContent = task.success ? '完成' : '失败';
taskTime.textContent = task.success ? '转换成功' : '转换失败';
taskNode.classList.add('completed');
// 设置下载按钮
if (task.success) {
downloadBtn.style.display = 'block';
downloadBtn.addEventListener('click', () => {
window.location.href = `/download/${task.filename}`;
});
} else {
downloadBtn.style.display = 'none';
}
}
// 添加到任务列表
taskList.appendChild(taskElement);
// 隐藏空队列提示
emptyQueue.style.display = 'none';
// 存储任务元素
taskElementsMap.set(task.id, taskNode);
}
// 启动计时器
function startTimer() {
if (timerInterval) return;
timerInterval = setInterval(() => {
const now = new Date();
const elapsed = Math.floor((now - startTime) / 1000);
const minutes = Math.floor(elapsed / 60).toString().padStart(2, '0');
const seconds = (elapsed % 60).toString().padStart(2, '0');
timer.textContent = `${minutes}:${seconds}`;
}, 1000);
}
// 从URL获取PDF文件名
function getFilenameFromUrl(url) {
try {
const parsed = new URL(url);
const path = parsed.pathname;
const pathParts = path.split('/').filter(part => part);
let filename = pathParts.length ? pathParts.pop() : parsed.hostname;
// 清理文件名
filename = filename.replace(/[<>:"/\\|?*]/g, '_').trim();
if (filename.length > 100) filename = filename.substring(0, 100);
if (!filename) filename = "webpage";
if (!filename.toLowerCase().endsWith('.pdf')) filename += '.pdf';
return filename;
} catch (e) {
return 'webpage.pdf';
}
}
</script>
</body>
</html>