This commit is contained in:
2025-06-07 23:43:57 +08:00
parent 12a583295d
commit 2e29f7a1f7
7 changed files with 3245 additions and 392 deletions

View File

@@ -5,8 +5,8 @@ import datetime
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QFrame, QListWidget, QListWidgetItem,
QFileDialog, QProgressBar, QMessageBox, QSpacerItem)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSize
from PyQt5.QtGui import QPixmap, QIcon, QDragEnterEvent, QDropEvent
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSize, QTimer
from PyQt5.QtGui import QPixmap, QIcon, QDragEnterEvent, QDropEvent, QDragLeaveEvent
def generate_unique_filename(original_path):
@@ -67,6 +67,7 @@ class WordToPDFPage(QWidget):
self.converted_files = {}
self.conversion_thread = None
self.init_ui()
self.setStyleSheet(self.get_page_style())
def init_ui(self):
self.setObjectName("wordToPdfPage")
@@ -77,81 +78,143 @@ class WordToPDFPage(QWidget):
layout.setSpacing(20)
# 左侧面板 - 文件上传
left_panel = QFrame()
left_panel.setObjectName("leftPanel")
left_panel.setMinimumWidth(300)
left_panel = QWidget()
left_layout = QVBoxLayout(left_panel)
left_layout.setContentsMargins(0, 0, 0, 0)
left_layout.setSpacing(15)
# 上传区域
# 上传区域(增强拖拽效果)
upload_area = QFrame()
upload_area.setObjectName("uploadArea")
upload_area.setFixedHeight(200)
upload_area.setAcceptDrops(True)
upload_layout = QVBoxLayout(upload_area)
upload_layout.setAlignment(Qt.AlignCenter)
upload_icon = QLabel()
# 确保你有正确的图标路径
# upload_icon.setPixmap(QPixmap(":/icons/upload.png").scaled(64, 64, Qt.KeepAspectRatio, Qt.SmoothTransformation))
upload_icon.setPixmap(QPixmap(":/icons/upload.png").scaled(64, 64, Qt.KeepAspectRatio, Qt.SmoothTransformation))
upload_icon.setAlignment(Qt.AlignCenter)
upload_text = QLabel("拖拽文件或文件夹到此处\n或点击上传")
upload_text.setAlignment(Qt.AlignCenter)
upload_text.setWordWrap(True)
self.upload_text = QLabel("拖拽文件或文件夹到此处\n或点击上传")
self.upload_text.setAlignment(Qt.AlignCenter)
self.upload_text.setWordWrap(True)
upload_btn = QPushButton("选择文件或文件夹")
upload_btn.setObjectName("uploadBtn")
upload_btn.clicked.connect(self.handle_upload)
upload_layout.addWidget(upload_icon)
upload_layout.addWidget(upload_text)
upload_layout.addWidget(self.upload_text)
upload_layout.addWidget(upload_btn)
# 文件列表
file_list_frame = QFrame()
file_list_frame.setObjectName("fileListFrame")
file_list_layout = QVBoxLayout(file_list_frame)
file_list_layout.setContentsMargins(0, 0, 0, 0)
file_list_layout.setSpacing(5)
file_title = QLabel("待转换文件")
file_title.setObjectName("sectionTitle")
self.file_list = QListWidget()
self.file_list.setObjectName("fileList")
self.file_list.setSelectionMode(QListWidget.ExtendedSelection)
file_list_layout.addWidget(file_title)
file_list_layout.addWidget(self.file_list)
# 操作按钮
btn_layout = QHBoxLayout()
btn_layout.setSpacing(10)
convert_btn = QPushButton("开始转换")
convert_btn.setObjectName("convertBtn")
convert_btn.clicked.connect(self.start_conversion)
self.convert_btn = QPushButton("开始转换")
self.convert_btn.setObjectName("convertBtn")
self.convert_btn.clicked.connect(self.start_conversion)
delete_btn = QPushButton("删除选中")
delete_btn.setObjectName("deleteBtn")
delete_btn.clicked.connect(self.delete_selected)
btn_layout.addWidget(convert_btn)
btn_layout.addWidget(self.convert_btn)
btn_layout.addWidget(delete_btn)
# 组装左侧面板
left_layout.addWidget(upload_area)
left_layout.addWidget(self.file_list)
left_layout.addWidget(file_list_frame)
left_layout.addLayout(btn_layout)
# 右侧面板 - 转换结果
right_panel = QFrame()
right_panel.setObjectName("rightPanel")
right_panel = QWidget()
right_layout = QVBoxLayout(right_panel)
right_layout.setContentsMargins(0, 0, 0, 0)
right_layout.setSpacing(15)
# 进度条
# 进度区域
progress_frame = QFrame()
progress_frame.setObjectName("progressFrame")
progress_layout = QVBoxLayout(progress_frame)
progress_layout.setContentsMargins(15, 15, 15, 15)
progress_layout.setSpacing(10)
progress_title = QLabel("转换进度")
progress_title.setObjectName("sectionTitle")
# 总体进度
total_layout = QHBoxLayout()
total_layout.setSpacing(10)
total_label = QLabel("总体进度:")
total_label.setObjectName("progressLabel")
self.total_progress = QProgressBar()
self.total_progress.setObjectName("totalProgress")
self.total_progress.setTextVisible(False)
total_layout.addWidget(total_label)
total_layout.addWidget(self.total_progress)
# 文件进度
file_layout = QHBoxLayout()
file_layout.setSpacing(10)
file_label = QLabel("当前文件:")
file_label.setObjectName("progressLabel")
self.file_progress = QProgressBar()
self.file_progress.setObjectName("fileProgress")
self.file_progress.setTextVisible(False)
file_layout.addWidget(file_label)
file_layout.addWidget(self.file_progress)
# 状态标签
self.status_label = QLabel("就绪")
self.status_label.setObjectName("statusLabel")
self.status_label.setAlignment(Qt.AlignCenter)
progress_layout.addWidget(progress_title)
progress_layout.addLayout(total_layout)
progress_layout.addLayout(file_layout)
progress_layout.addWidget(self.status_label)
# 结果列表
result_frame = QFrame()
result_frame.setObjectName("resultFrame")
result_layout = QVBoxLayout(result_frame)
result_layout.setContentsMargins(0, 0, 0, 0)
result_layout.setSpacing(5)
result_title = QLabel("转换结果")
result_title.setObjectName("sectionTitle")
self.result_list = QListWidget()
self.result_list.setObjectName("resultList")
self.result_list.setSelectionMode(QListWidget.ExtendedSelection)
result_layout.addWidget(result_title)
result_layout.addWidget(self.result_list)
# 下载按钮
self.download_btn = QPushButton("全部下载")
@@ -159,27 +222,28 @@ class WordToPDFPage(QWidget):
self.download_btn.setEnabled(False)
self.download_btn.clicked.connect(self.download_all)
# 日志
log_area = QFrame()
log_area.setObjectName("logArea")
log_layout = QVBoxLayout(log_area)
# 日志区域
log_frame = QFrame()
log_frame.setObjectName("logFrame")
log_layout = QVBoxLayout(log_frame)
log_layout.setContentsMargins(0, 0, 0, 0)
log_layout.setSpacing(5)
log_title = QLabel("转换日志")
log_title.setObjectName("logTitle")
log_title.setObjectName("sectionTitle")
self.log_content = QListWidget()
self.log_content.setObjectName("logContent")
self.log_content.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
log_layout.addWidget(log_title)
log_layout.addWidget(self.log_content)
# 组装右侧面板
right_layout.addWidget(self.total_progress)
right_layout.addWidget(self.file_progress)
right_layout.addWidget(self.result_list)
right_layout.addWidget(progress_frame)
right_layout.addWidget(result_frame)
right_layout.addWidget(self.download_btn)
right_layout.addWidget(log_area)
right_layout.addWidget(log_frame)
# 主布局
layout.addWidget(left_panel)
@@ -187,13 +251,178 @@ class WordToPDFPage(QWidget):
self.setLayout(layout)
def get_page_style(self):
return """
/* ===== Word转PDF页面样式 ===== */
#wordToPdfPage {
background-color: #1a202c;
}
/* 卡片样式 */
#uploadArea,
#fileListFrame,
#progressFrame,
#resultFrame,
#logFrame {
background-color: #2d3748;
border: 1px solid #4a5568;
border-radius: 8px;
padding: 15px;
}
/* 上传区域 */
#uploadArea {
border: 2px dashed #4a5568;
background-color: rgba(45, 55, 72, 0.5);
transition: all 0.3s ease;
}
#uploadArea:hover {
border-color: #818cf8;
background-color: rgba(129, 140, 248, 0.1);
}
#uploadArea[dragActive="true"] {
border-color: #4ade80;
background-color: rgba(74, 222, 128, 0.1);
}
/* 标题样式 */
#sectionTitle {
font-size: 16px;
font-weight: 600;
color: #e2e8f0;
margin-bottom: 10px;
}
/* 文件列表 */
#fileList, #resultList, #logContent {
background-color: rgba(26, 32, 44, 0.3);
border: 1px solid #4a5568;
border-radius: 6px;
min-height: 200px;
max-height: 250px;
font-size: 13px;
}
#fileList::item, #resultList::item, #logContent::item {
padding: 8px 12px;
border-bottom: 1px solid rgba(74, 85, 104, 0.5);
}
#fileList::item:selected,
#resultList::item:selected,
#logContent::item:selected {
background-color: rgba(129, 140, 248, 0.15);
}
/* 进度条 */
#totalProgress, #fileProgress {
height: 8px;
border-radius: 4px;
background-color: #2d3748;
}
#totalProgress::chunk {
background-color: #818cf8;
border-radius: 4px;
}
#fileProgress::chunk {
background-color: #4ade80;
border-radius: 4px;
}
#progressLabel {
color: #a0aec0;
font-size: 13px;
min-width: 70px;
}
#statusLabel {
font-size: 14px;
color: #e2e8f0;
margin-top: 10px;
padding: 8px;
border-radius: 6px;
background-color: rgba(26, 32, 44, 0.3);
}
/* 按钮样式 */
#uploadBtn, #convertBtn, #deleteBtn, #downloadBtn {
min-height: 40px;
font-weight: 500;
}
#uploadBtn {
min-width: 180px;
}
#convertBtn {
background-color: #4f46e5;
}
#convertBtn:disabled {
background-color: #4a5568;
}
#downloadBtn {
background-color: #10b981;
}
#downloadBtn:hover {
background-color: #059669;
}
#deleteBtn {
background-color: #ef4444;
}
#deleteBtn:hover {
background-color: #dc2626;
}
/* 日志样式 */
#logContent::item[type="info"] {
color: #a0aec0;
}
#logContent::item[type="success"] {
color: #34d399;
}
#logContent::item[type="warning"] {
color: #fbbf24;
}
#logContent::item[type="error"] {
color: #f87171;
}
"""
def dragEnterEvent(self, event: QDragEnterEvent):
if event.mimeData().hasUrls():
event.acceptProposedAction()
# 高亮显示拖拽区域
self.findChild(QFrame, "uploadArea").setProperty("dragActive", True)
self.findChild(QFrame, "uploadArea").style().polish(self.findChild(QFrame, "uploadArea"))
self.upload_text.setText("释放文件以添加")
def dragLeaveEvent(self, event: QDragLeaveEvent):
# 恢复拖拽区域样式
self.findChild(QFrame, "uploadArea").setProperty("dragActive", False)
self.findChild(QFrame, "uploadArea").style().polish(self.findChild(QFrame, "uploadArea"))
self.upload_text.setText("拖拽文件或文件夹到此处\n或点击上传")
def dropEvent(self, event: QDropEvent):
# 恢复拖拽区域样式
self.findChild(QFrame, "uploadArea").setProperty("dragActive", False)
self.findChild(QFrame, "uploadArea").style().polish(self.findChild(QFrame, "uploadArea"))
self.upload_text.setText("拖拽文件或文件夹到此处\n或点击上传")
urls = event.mimeData().urls()
files = []
added_count = 0
for url in urls:
path = url.toLocalFile()
@@ -207,46 +436,65 @@ class WordToPDFPage(QWidget):
files.append(path)
if files:
self.add_files(files)
added_count = self.add_files(files)
self.add_log(f"添加了 {added_count} 个文件", "success")
else:
self.add_log("未找到有效的Word文件", "warning")
def handle_upload(self):
"""处理文件或文件夹上传"""
# 使用getExistingDirectory获取文件夹
folder = QFileDialog.getExistingDirectory(self, "选择文件夹")
if folder:
files = []
for root, _, filenames in os.walk(folder):
for f in filenames:
if f.lower().endswith(('.doc', '.docx')):
files.append(os.path.join(root, f))
self.add_files(files)
options = QFileDialog.Options()
files, _ = QFileDialog.getOpenFileNames(
self, "选择Word文件", "", "Word Files (*.doc *.docx);;All Files (*)", options=options
)
if files:
added_count = self.add_files(files)
self.add_log(f"添加了 {added_count} 个文件", "success")
else:
# 如果用户没有选择文件夹,则选择文件
files, _ = QFileDialog.getOpenFileNames(
self, "选择Word文件", "", "Word Files (*.doc *.docx);;All Files (*)"
)
if files:
self.add_files(files)
folder = QFileDialog.getExistingDirectory(self, "选择包含Word文件的文件夹")
if folder:
files = []
for root, _, filenames in os.walk(folder):
for f in filenames:
if f.lower().endswith(('.doc', '.docx')):
files.append(os.path.join(root, f))
if files:
added_count = self.add_files(files)
self.add_log(f"从文件夹添加了 {added_count} 个文件", "success")
else:
self.add_log("文件夹中没有找到Word文件", "warning")
def add_files(self, files):
"""添加文件到列表,避免重复"""
added_count = 0
for file in files:
if file not in self.files and os.path.exists(file):
self.files.append(file)
item = QListWidgetItem(os.path.basename(file))
item.setData(Qt.UserRole, file)
self.file_list.addItem(item)
added_count += 1
return added_count
def delete_selected(self):
"""删除选中的文件"""
for item in self.file_list.selectedItems():
selected_items = self.file_list.selectedItems()
if not selected_items:
self.add_log("请先选择要删除的文件", "warning")
return
for item in selected_items:
self.files.remove(item.data(Qt.UserRole))
self.file_list.takeItem(self.file_list.row(item))
self.add_log(f"删除了 {len(selected_items)} 个文件", "info")
def start_conversion(self):
"""开始转换文件"""
if not self.files:
QMessageBox.warning(self, "警告", "请先添加要转换的文件!")
self.add_log("请先添加要转换的文件!", "warning")
return
# 重置状态
@@ -254,7 +502,18 @@ class WordToPDFPage(QWidget):
self.result_list.clear()
self.log_content.clear()
self.download_btn.setEnabled(False)
self.convert_btn.setEnabled(False)
# 重置进度条
self.total_progress.setValue(0)
self.file_progress.setValue(0)
self.status_label.setText("正在准备转换...")
# 添加开始日志
self.add_log("开始转换任务", "info")
self.add_log(f"共 {len(self.files)} 个文件待处理", "info")
# 创建并启动转换线程
self.conversion_thread = ConversionThread(self.files.copy())
self.conversion_thread.progress_updated.connect(self.update_progress)
self.conversion_thread.conversion_done.connect(self.handle_conversion_done)
@@ -266,10 +525,28 @@ class WordToPDFPage(QWidget):
self.total_progress.setMaximum(total)
self.total_progress.setValue(current)
self.file_progress.setMaximum(100)
# 重置文件进度为0
self.file_progress.setValue(0)
self.file_progress.setMaximum(100)
self.log_content.addItem(f"正在转换: {filename} ({current}/{total})")
# 更新状态
self.status_label.setText(f"正在转换: {os.path.basename(filename)}")
self.add_log(f"开始转换: {os.path.basename(filename)} ({current}/{total})", "info")
# 模拟文件进度动画
self.animate_file_progress()
def animate_file_progress(self):
"""模拟文件转换进度动画(实际应用中应由实际进度驱动)"""
self.file_progress_value = 0
def update_progress():
if self.file_progress_value < 100:
self.file_progress_value += 2
self.file_progress.setValue(self.file_progress_value)
QTimer.singleShot(50, update_progress)
update_progress()
def handle_conversion_done(self, file_path, success, message):
"""处理单个文件转换完成"""
@@ -291,28 +568,50 @@ class WordToPDFPage(QWidget):
item.setData(Qt.UserRole, pdf_path)
self.result_list.addItem(item)
self.log_content.addItem(f"✅ 转换成功: {basename} -> {os.path.basename(pdf_path)}")
self.add_log(f"✅ 转换成功: {basename} -> {os.path.basename(pdf_path)}", "success")
else:
# 失败时显示错误信息
self.log_content.addItem(f"❌ 转换失败: {basename} - {message}")
self.add_log(f"❌ 转换失败: {basename} - {message}", "error")
def handle_finished_all(self):
"""所有文件转换完成"""
self.log_content.addItem(f"🎉 全部转换完成! 共转换 {len(self.converted_files)} 个文件")
self.download_btn.setEnabled(True)
success_count = len(self.converted_files)
fail_count = len(self.files) - success_count
# 更新状态
self.status_label.setText(f"转换完成! 成功: {success_count}, 失败: {fail_count}")
self.total_progress.setValue(self.total_progress.maximum())
self.file_progress.setValue(100)
self.convert_btn.setEnabled(True)
self.download_btn.setEnabled(bool(self.converted_files))
# 添加完成日志
if success_count:
self.add_log(f"🎉 全部转换完成! 成功转换 {success_count} 个文件", "success")
if fail_count:
self.add_log(f"⚠️ 有 {fail_count} 个文件转换失败", "warning")
if self.converted_files:
self.add_log("点击'全部下载'按钮保存转换后的文件", "info")
def download_all(self):
"""下载所有转换后的文件"""
if not self.converted_files:
self.add_log("没有可下载的文件", "warning")
return
folder = QFileDialog.getExistingDirectory(self, "选择保存位置")
if not folder:
self.add_log("下载已取消", "info")
return
success = 0
errors = []
# 开始下载
self.status_label.setText("正在下载文件...")
self.add_log(f"开始下载 {len(self.converted_files)} 个文件到 {folder}", "info")
for pdf_path in self.converted_files.values():
try:
file_name = os.path.basename(pdf_path)
@@ -323,16 +622,35 @@ class WordToPDFPage(QWidget):
with open(destination, 'wb') as dest_file:
dest_file.write(src_file.read())
success += 1
self.add_log(f"已下载: {file_name}", "success")
except Exception as e:
errors.append(f"{os.path.basename(pdf_path)}: {str(e)}")
self.add_log(f"下载失败: {os.path.basename(pdf_path)} - {str(e)}", "error")
# 更新状态
self.status_label.setText(f"下载完成! 成功: {success}, 失败: {len(errors)}")
# 显示结果
if errors:
error_msg = "\n".join(errors[:10]) # 最多显示10个错误
if len(errors) > 10:
error_msg += f"\n...等共 {len(errors)} 个错误"
QMessageBox.warning(self, "部分文件下载失败", f"成功下载 {success} 个文件\n失败文件:\n{error_msg}")
self.add_log(f"⚠️ 部分文件下载失败 ({len(errors)} 个)", "warning")
else:
QMessageBox.information(self, "下载完成", f"已成功下载 {success} 个文件到 {folder}")
self.add_log(f"✅ 全部文件下载成功!", "success")
self.log_content.addItem(f"已下载 {success} 个文件到 {folder}")
self.add_log(f"文件已保存到: {folder}", "info")
def add_log(self, message, log_type="info"):
"""添加日志项"""
item = QListWidgetItem(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] {message}")
item.setData(Qt.UserRole + 1, log_type) # 存储日志类型用于样式
self.log_content.addItem(item)
self.log_content.scrollToBottom()
def paintEvent(self, event):
"""为拖拽区域添加悬停效果"""
if self.underMouse():
self.findChild(QFrame, "uploadArea").setProperty("hover", True)
self.findChild(QFrame, "uploadArea").style().polish(self.findChild(QFrame, "uploadArea"))
else:
self.findChild(QFrame, "uploadArea").setProperty("hover", False)
self.findChild(QFrame, "uploadArea").style().polish(self.findChild(QFrame, "uploadArea"))
super().paintEvent(event)