# Copyright (c) 2025 Jeffrey Hsu - JITToolBox # # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os from functools import wraps from typing import Callable, Literal from PySide6.QtCore import Qt, Signal, QThread from PySide6.QtWidgets import QVBoxLayout, QFileDialog, QHBoxLayout from qfluentwidgets import GroupHeaderCardWidget, FluentIcon, PushButton, LineEdit, IconWidget, BodyLabel, \ PrimaryPushButton, SwitchButton, HyperlinkButton from module import LOGLEVEL from module.worker import ARGWorker from ui import MAIN_THEME_COLOR from ui.components.infobar import ProgressInfoBar from ui.components.widget import Widget, MyGroupHeaderCardWidget from utils.function import open_template class InputSettingCard(MyGroupHeaderCardWidget): chooseSignal = Signal(str) def __init__(self, parent=None): super().__init__(parent) self.setTitle("输入选项") self.setBorderRadius(8) self.btnHBoxLayout = QHBoxLayout(self) self.openTemplateButton = HyperlinkButton("", "模板下载") self.chooseFileButton = PushButton("打开") self.chooseFileButton.setFixedWidth(120) self.btnHBoxLayout.addWidget(self.openTemplateButton) self.btnHBoxLayout.addWidget(self.chooseFileButton) self.inputGroup = self.addGroup(FluentIcon.DOCUMENT, "目标文件", "选择达成度计算表", self.btnHBoxLayout) # ============================ self.chooseFileButton.clicked.connect(self.choose_file) self.openTemplateButton.clicked.connect(lambda: open_template('template-achievement-file.xlsm', self)) def choose_file(self): file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "Excel 文件 (*.xlsm);") if file_path: self.inputGroup.setContent("已选择文件:" + file_path) self.chooseSignal.emit(file_path) class OutputSettingCard(GroupHeaderCardWidget): updateSignal = Signal(str, str) startSignal = Signal() def __init__(self, parent=None): super().__init__(parent) self.setTitle("输出选项") self.setBorderRadius(8) self.chooseExportDirectoryButton = PushButton("选择") self.exportFileNameLineEdit = LineEdit() self.startButton = PrimaryPushButton(FluentIcon.PLAY_SOLID, "开始") self.autoOpenSwitch = SwitchButton() self.disableCompatibilityCheckSwitch = SwitchButton() self.autoOpenSwitch.setChecked(True) self.bottomLayout = QHBoxLayout() self.hintIcon = IconWidget(FluentIcon.INFO.icon(color=MAIN_THEME_COLOR)) self.hintLabel = BodyLabel("点击开始按钮以开始生成 👉") self.startButton.setEnabled(False) # 设置底部工具栏布局 self.hintIcon.setFixedSize(16, 16) self.hintIcon.autoFillBackground() self.bottomLayout.setSpacing(10) self.bottomLayout.setContentsMargins(24, 15, 24, 20) self.bottomLayout.addWidget(self.hintIcon, 0, Qt.AlignLeft) self.bottomLayout.addWidget(self.hintLabel, 0, Qt.AlignLeft) self.bottomLayout.addStretch(1) self.bottomLayout.addWidget(self.startButton, 0, Qt.AlignRight) self.bottomLayout.setAlignment(Qt.AlignVCenter) self.chooseExportDirectoryButton.setFixedWidth(120) self.exportFileNameLineEdit.setPlaceholderText("请输入文件名") self.startButton.setFixedWidth(120) self.exportFileNameLineEdit.setFixedWidth(360) self.exportFileNameLineEdit.setPlaceholderText("输入导出文件名,例如:21工程管理(1)达成度报告") self.dirGroup = self.addGroup(FluentIcon.FOLDER, "导出目录", "选择导出文件的目录", self.chooseExportDirectoryButton) self.fnGroup = self.addGroup(FluentIcon.DOCUMENT, "导出文件名", "输入导出文件名", self.exportFileNameLineEdit) self.aoGroup = self.addGroup(FluentIcon.VIEW, "自动打开", "生成完成后自动打开文件", self.autoOpenSwitch) self.ccGroup = self.addGroup(FluentIcon.DEVELOPER_TOOLS, "关闭兼容性检查", "⚠ 注意:该功能为实验性内容,仅当你明确了解其影响并知道自己在做什么时,才建议关闭兼容性检查!", self.disableCompatibilityCheckSwitch) self.ccGroup.setSeparatorVisible(True) self.vBoxLayout.addLayout(self.bottomLayout) # ============================= self.chooseExportDirectoryButton.clicked.connect(self.choose_dir) self.startButton.clicked.connect(self.startSignal) def choose_dir(self) -> None: dir_path = QFileDialog.getExistingDirectory(self, "选择文件夹", "") if dir_path: self.dirGroup.setContent(f"当前保存的文件目录:{dir_path}") self.updateSignal.emit('d', dir_path) def update_path_and_filename_by_input_path(self, path: str): dir_path = path[:path.rfind("/")] self.dirGroup.setContent(f"已选择目录:{dir_path}") self.updateSignal.emit('d', dir_path) file_name = path[path.rfind("/") + 1:path.rfind(".")] + "-达成度报告" self.exportFileNameLineEdit.setText(file_name) self.updateSignal.emit('f', file_name) class AchievementWidget(Widget): error = Signal(str, str) def __init__(self, key: str, parent=None): super().__init__(key, parent) self.inputGroup = InputSettingCard(self) self.outputGroup = OutputSettingCard(self) self.vbox = QVBoxLayout(self) self.vbox.addWidget(self.inputGroup) self.vbox.addWidget(self.outputGroup) self.vbox.addStretch(1) # ================================= self.pib = ProgressInfoBar(parent=self, isClosable=False, duration=-1) self.pib.hide() # ================================= self.thread = None self.worker = None self.successFlag = True # ================================== self.input_file_path = "" self.output_file_path = "" self.output_file_name = "" # ================================== self.inputGroup.chooseSignal.connect(self.input_signal_receive) self.outputGroup.updateSignal.connect(self.export_signal_receive) self.outputGroup.startSignal.connect(self.start_generate) def enable_start_check(func: Callable): @wraps(func) def wrapper(self, *args, **kwargs): result = func(self, *args, **kwargs) fields = [ self.input_file_path, self.output_file_path, self.output_file_name, ] if all(fields): self.outputGroup.startButton.setEnabled(True) else: self.outputGroup.startButton.setEnabled(False) return result return wrapper @enable_start_check def set_value( self, key: Literal['input_file_path', 'output_file_path', 'output_file_name'], value: str ) -> None: setattr(self, key, value) def input_signal_receive(self, file_path: str) -> None: self.set_value('input_file_path', file_path) self.outputGroup.update_path_and_filename_by_input_path(file_path) def export_signal_receive(self, s_type: Literal['d', 'f'], value: str) -> None: if s_type == 'd': self.set_value('output_file_path', value) elif s_type == 'f': self.set_value('output_file_name', value) def start_generate(self): self.thread = QThread() self.worker = ARGWorker( self.input_file_path, self.output_file_path, self.output_file_name, disable_cc=self.outputGroup.disableCompatibilityCheckSwitch.isChecked() ) self.worker.moveToThread(self.thread) self.outputGroup.startButton.setEnabled(False) self.thread.started.connect(self.worker.run) self.show_info_bar() self.successFlag = True self.worker.info.connect(self.show_info) self.worker.error.connect(self.show_error) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) self.thread.finished.connect(self.clear_thread_worker_refs) self.thread.finished.connect(self.after_generate) # 启动线程 self.thread.start() def clear_thread_worker_refs(self): self.thread = None self.worker = None def after_generate(self): if self.outputGroup.autoOpenSwitch.isChecked() and self.successFlag: try: os.startfile(self.output_file_path + "/" + self.output_file_name + ".docx") except Exception as e: self.show_error("?? 不好出错了", str(e)) if self.successFlag: self.pib.show_success( content="正在打开文件" if self.outputGroup.autoOpenSwitch.isChecked() else "文件已保存") else: self.pib.show_error() self.outputGroup.startButton.setEnabled(True) def show_error(self, title: str, content: str): self.successFlag = False self.error.emit(title, content) def show_info_bar(self): self.pib.set_title('请稍后') self.pib.show() self.pib.set_progress(101) def show_info(self, content: str, level: str): if level == LOGLEVEL.INFO: self.pib.set_title(content)