Files
nl-utils/v1/views/word_to_pdf.py

656 lines
23 KiB
Python
Raw Normal View History

2025-06-07 15:54:17 +08:00
import os
import comtypes.client
import random
import datetime
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QFrame, QListWidget, QListWidgetItem,
QFileDialog, QProgressBar, QMessageBox, QSpacerItem)
2025-06-07 23:43:57 +08:00
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSize, QTimer
from PyQt5.QtGui import QPixmap, QIcon, QDragEnterEvent, QDropEvent, QDragLeaveEvent
2025-06-07 15:54:17 +08:00
def generate_unique_filename(original_path):
"""生成唯一的文件名格式原名称_日期_随机数.pdf"""
base_name = os.path.basename(original_path)
name_without_ext = os.path.splitext(base_name)[0]
# 获取当前日期
current_date = datetime.datetime.now().strftime("%Y%m%d")
# 生成4位随机数
random_num = random.randint(1000, 9999)
# 组合成新文件名
new_filename = f"{name_without_ext}_{current_date}_{random_num}.pdf"
return os.path.join(os.path.dirname(original_path), new_filename)
class ConversionThread(QThread):
progress_updated = pyqtSignal(int, int, str) # current, total, filename
conversion_done = pyqtSignal(str, bool, str) # filename, success, message
finished_all = pyqtSignal()
def __init__(self, files):
super().__init__()
self.files = files
def run(self):
word = None
try:
word = comtypes.client.CreateObject('Word.Application')
word.Visible = False
total = len(self.files)
for i, file_path in enumerate(self.files):
self.progress_updated.emit(i + 1, total, os.path.basename(file_path))
try:
# 使用新的文件名生成逻辑
pdf_path = generate_unique_filename(file_path)
doc = word.Documents.Open(os.path.abspath(file_path))
doc.SaveAs(pdf_path, FileFormat=17) # 17 = PDF
doc.Close()
self.conversion_done.emit(file_path, True, pdf_path) # 传递新文件路径
except Exception as e:
self.conversion_done.emit(file_path, False, str(e))
self.finished_all.emit()
finally:
if word:
word.Quit()
class WordToPDFPage(QWidget):
def __init__(self):
super().__init__()
self.files = []
self.converted_files = {}
self.conversion_thread = None
self.init_ui()
2025-06-07 23:43:57 +08:00
self.setStyleSheet(self.get_page_style())
2025-06-07 15:54:17 +08:00
def init_ui(self):
self.setObjectName("wordToPdfPage")
self.setAcceptDrops(True)
layout = QHBoxLayout()
layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(20)
# 左侧面板 - 文件上传
2025-06-07 23:43:57 +08:00
left_panel = QWidget()
2025-06-07 15:54:17 +08:00
left_layout = QVBoxLayout(left_panel)
left_layout.setContentsMargins(0, 0, 0, 0)
left_layout.setSpacing(15)
2025-06-07 23:43:57 +08:00
# 上传区域(增强拖拽效果)
2025-06-07 15:54:17 +08:00
upload_area = QFrame()
upload_area.setObjectName("uploadArea")
upload_area.setFixedHeight(200)
2025-06-07 23:43:57 +08:00
upload_area.setAcceptDrops(True)
2025-06-07 15:54:17 +08:00
upload_layout = QVBoxLayout(upload_area)
upload_layout.setAlignment(Qt.AlignCenter)
upload_icon = QLabel()
2025-06-07 23:43:57 +08:00
upload_icon.setPixmap(QPixmap(":/icons/upload.png").scaled(64, 64, Qt.KeepAspectRatio, Qt.SmoothTransformation))
2025-06-07 15:54:17 +08:00
upload_icon.setAlignment(Qt.AlignCenter)
2025-06-07 23:43:57 +08:00
self.upload_text = QLabel("拖拽文件或文件夹到此处\n或点击上传")
self.upload_text.setAlignment(Qt.AlignCenter)
self.upload_text.setWordWrap(True)
2025-06-07 15:54:17 +08:00
upload_btn = QPushButton("选择文件或文件夹")
upload_btn.setObjectName("uploadBtn")
upload_btn.clicked.connect(self.handle_upload)
upload_layout.addWidget(upload_icon)
2025-06-07 23:43:57 +08:00
upload_layout.addWidget(self.upload_text)
2025-06-07 15:54:17 +08:00
upload_layout.addWidget(upload_btn)
# 文件列表
2025-06-07 23:43:57 +08:00
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")
2025-06-07 15:54:17 +08:00
self.file_list = QListWidget()
self.file_list.setObjectName("fileList")
self.file_list.setSelectionMode(QListWidget.ExtendedSelection)
2025-06-07 23:43:57 +08:00
file_list_layout.addWidget(file_title)
file_list_layout.addWidget(self.file_list)
2025-06-07 15:54:17 +08:00
# 操作按钮
btn_layout = QHBoxLayout()
2025-06-07 23:43:57 +08:00
btn_layout.setSpacing(10)
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
self.convert_btn = QPushButton("开始转换")
self.convert_btn.setObjectName("convertBtn")
self.convert_btn.clicked.connect(self.start_conversion)
2025-06-07 15:54:17 +08:00
delete_btn = QPushButton("删除选中")
delete_btn.setObjectName("deleteBtn")
delete_btn.clicked.connect(self.delete_selected)
2025-06-07 23:43:57 +08:00
btn_layout.addWidget(self.convert_btn)
2025-06-07 15:54:17 +08:00
btn_layout.addWidget(delete_btn)
# 组装左侧面板
left_layout.addWidget(upload_area)
2025-06-07 23:43:57 +08:00
left_layout.addWidget(file_list_frame)
2025-06-07 15:54:17 +08:00
left_layout.addLayout(btn_layout)
# 右侧面板 - 转换结果
2025-06-07 23:43:57 +08:00
right_panel = QWidget()
2025-06-07 15:54:17 +08:00
right_layout = QVBoxLayout(right_panel)
right_layout.setContentsMargins(0, 0, 0, 0)
right_layout.setSpacing(15)
2025-06-07 23:43:57 +08:00
# 进度区域
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")
2025-06-07 15:54:17 +08:00
self.total_progress = QProgressBar()
2025-06-07 23:43:57 +08:00
self.total_progress.setObjectName("totalProgress")
2025-06-07 15:54:17 +08:00
self.total_progress.setTextVisible(False)
2025-06-07 23:43:57 +08:00
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")
2025-06-07 15:54:17 +08:00
self.file_progress = QProgressBar()
2025-06-07 23:43:57 +08:00
self.file_progress.setObjectName("fileProgress")
2025-06-07 15:54:17 +08:00
self.file_progress.setTextVisible(False)
2025-06-07 23:43:57 +08:00
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)
2025-06-07 15:54:17 +08:00
# 结果列表
2025-06-07 23:43:57 +08:00
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")
2025-06-07 15:54:17 +08:00
self.result_list = QListWidget()
self.result_list.setObjectName("resultList")
2025-06-07 23:43:57 +08:00
self.result_list.setSelectionMode(QListWidget.ExtendedSelection)
result_layout.addWidget(result_title)
result_layout.addWidget(self.result_list)
2025-06-07 15:54:17 +08:00
# 下载按钮
self.download_btn = QPushButton("全部下载")
self.download_btn.setObjectName("downloadBtn")
self.download_btn.setEnabled(False)
self.download_btn.clicked.connect(self.download_all)
2025-06-07 23:43:57 +08:00
# 日志区域
log_frame = QFrame()
log_frame.setObjectName("logFrame")
log_layout = QVBoxLayout(log_frame)
log_layout.setContentsMargins(0, 0, 0, 0)
log_layout.setSpacing(5)
2025-06-07 15:54:17 +08:00
log_title = QLabel("转换日志")
2025-06-07 23:43:57 +08:00
log_title.setObjectName("sectionTitle")
2025-06-07 15:54:17 +08:00
self.log_content = QListWidget()
self.log_content.setObjectName("logContent")
2025-06-07 23:43:57 +08:00
self.log_content.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
2025-06-07 15:54:17 +08:00
log_layout.addWidget(log_title)
log_layout.addWidget(self.log_content)
# 组装右侧面板
2025-06-07 23:43:57 +08:00
right_layout.addWidget(progress_frame)
right_layout.addWidget(result_frame)
2025-06-07 15:54:17 +08:00
right_layout.addWidget(self.download_btn)
2025-06-07 23:43:57 +08:00
right_layout.addWidget(log_frame)
2025-06-07 15:54:17 +08:00
# 主布局
layout.addWidget(left_panel)
layout.addWidget(right_panel)
self.setLayout(layout)
2025-06-07 23:43:57 +08:00
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;
}
"""
2025-06-07 15:54:17 +08:00
def dragEnterEvent(self, event: QDragEnterEvent):
if event.mimeData().hasUrls():
event.acceptProposedAction()
2025-06-07 23:43:57 +08:00
# 高亮显示拖拽区域
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或点击上传")
2025-06-07 15:54:17 +08:00
def dropEvent(self, event: QDropEvent):
2025-06-07 23:43:57 +08:00
# 恢复拖拽区域样式
self.findChild(QFrame, "uploadArea").setProperty("dragActive", False)
self.findChild(QFrame, "uploadArea").style().polish(self.findChild(QFrame, "uploadArea"))
self.upload_text.setText("拖拽文件或文件夹到此处\n或点击上传")
2025-06-07 15:54:17 +08:00
urls = event.mimeData().urls()
files = []
2025-06-07 23:43:57 +08:00
added_count = 0
2025-06-07 15:54:17 +08:00
for url in urls:
path = url.toLocalFile()
if os.path.isdir(path):
# 处理文件夹
for root, _, filenames in os.walk(path):
for f in filenames:
if f.lower().endswith(('.doc', '.docx')):
files.append(os.path.join(root, f))
elif os.path.isfile(path) and path.lower().endswith(('.doc', '.docx')):
files.append(path)
if files:
2025-06-07 23:43:57 +08:00
added_count = self.add_files(files)
self.add_log(f"添加了 {added_count} 个文件", "success")
else:
self.add_log("未找到有效的Word文件", "warning")
2025-06-07 15:54:17 +08:00
def handle_upload(self):
"""处理文件或文件夹上传"""
2025-06-07 23:43:57 +08:00
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")
2025-06-07 15:54:17 +08:00
else:
2025-06-07 23:43:57 +08:00
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")
2025-06-07 15:54:17 +08:00
def add_files(self, files):
"""添加文件到列表,避免重复"""
2025-06-07 23:43:57 +08:00
added_count = 0
2025-06-07 15:54:17 +08:00
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)
2025-06-07 23:43:57 +08:00
added_count += 1
return added_count
2025-06-07 15:54:17 +08:00
def delete_selected(self):
"""删除选中的文件"""
2025-06-07 23:43:57 +08:00
selected_items = self.file_list.selectedItems()
if not selected_items:
self.add_log("请先选择要删除的文件", "warning")
return
for item in selected_items:
2025-06-07 15:54:17 +08:00
self.files.remove(item.data(Qt.UserRole))
self.file_list.takeItem(self.file_list.row(item))
2025-06-07 23:43:57 +08:00
self.add_log(f"删除了 {len(selected_items)} 个文件", "info")
2025-06-07 15:54:17 +08:00
def start_conversion(self):
"""开始转换文件"""
if not self.files:
2025-06-07 23:43:57 +08:00
self.add_log("请先添加要转换的文件!", "warning")
2025-06-07 15:54:17 +08:00
return
# 重置状态
self.converted_files = {}
self.result_list.clear()
self.log_content.clear()
self.download_btn.setEnabled(False)
2025-06-07 23:43:57 +08:00
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")
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
# 创建并启动转换线程
2025-06-07 15:54:17 +08:00
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)
self.conversion_thread.finished_all.connect(self.handle_finished_all)
self.conversion_thread.start()
def update_progress(self, current, total, filename):
"""更新进度条"""
self.total_progress.setMaximum(total)
self.total_progress.setValue(current)
2025-06-07 23:43:57 +08:00
# 重置文件进度为0
2025-06-07 15:54:17 +08:00
self.file_progress.setValue(0)
2025-06-07 23:43:57 +08:00
self.file_progress.setMaximum(100)
# 更新状态
self.status_label.setText(f"正在转换: {os.path.basename(filename)}")
self.add_log(f"开始转换: {os.path.basename(filename)} ({current}/{total})", "info")
# 模拟文件进度动画
self.animate_file_progress()
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
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()
2025-06-07 15:54:17 +08:00
def handle_conversion_done(self, file_path, success, message):
"""处理单个文件转换完成"""
basename = os.path.basename(file_path)
if success:
# 成功时message包含新的PDF路径
pdf_path = message
self.converted_files[file_path] = pdf_path
# 从待转换列表移除
for i in range(self.file_list.count()):
item = self.file_list.item(i)
if item.data(Qt.UserRole) == file_path:
self.file_list.takeItem(i)
break
# 添加到结果列表
item = QListWidgetItem(f"{basename}{os.path.basename(pdf_path)}")
item.setData(Qt.UserRole, pdf_path)
self.result_list.addItem(item)
2025-06-07 23:43:57 +08:00
self.add_log(f"✅ 转换成功: {basename} -> {os.path.basename(pdf_path)}", "success")
2025-06-07 15:54:17 +08:00
else:
# 失败时显示错误信息
2025-06-07 23:43:57 +08:00
self.add_log(f"❌ 转换失败: {basename} - {message}", "error")
2025-06-07 15:54:17 +08:00
def handle_finished_all(self):
"""所有文件转换完成"""
2025-06-07 23:43:57 +08:00
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")
2025-06-07 15:54:17 +08:00
def download_all(self):
"""下载所有转换后的文件"""
if not self.converted_files:
2025-06-07 23:43:57 +08:00
self.add_log("没有可下载的文件", "warning")
2025-06-07 15:54:17 +08:00
return
folder = QFileDialog.getExistingDirectory(self, "选择保存位置")
if not folder:
2025-06-07 23:43:57 +08:00
self.add_log("下载已取消", "info")
2025-06-07 15:54:17 +08:00
return
success = 0
errors = []
2025-06-07 23:43:57 +08:00
# 开始下载
self.status_label.setText("正在下载文件...")
self.add_log(f"开始下载 {len(self.converted_files)} 个文件到 {folder}", "info")
2025-06-07 15:54:17 +08:00
for pdf_path in self.converted_files.values():
try:
file_name = os.path.basename(pdf_path)
destination = os.path.join(folder, file_name)
# 复制文件
with open(pdf_path, 'rb') as src_file:
with open(destination, 'wb') as dest_file:
dest_file.write(src_file.read())
success += 1
2025-06-07 23:43:57 +08:00
self.add_log(f"已下载: {file_name}", "success")
2025-06-07 15:54:17 +08:00
except Exception as e:
errors.append(f"{os.path.basename(pdf_path)}: {str(e)}")
2025-06-07 23:43:57 +08:00
self.add_log(f"下载失败: {os.path.basename(pdf_path)} - {str(e)}", "error")
# 更新状态
self.status_label.setText(f"下载完成! 成功: {success}, 失败: {len(errors)}")
2025-06-07 15:54:17 +08:00
# 显示结果
if errors:
2025-06-07 23:43:57 +08:00
self.add_log(f"⚠️ 部分文件下载失败 ({len(errors)} 个)", "warning")
2025-06-07 15:54:17 +08:00
else:
2025-06-07 23:43:57 +08:00
self.add_log(f"✅ 全部文件下载成功!", "success")
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)