262 lines
10 KiB
Python
262 lines
10 KiB
Python
# 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 <https://www.gnu.org/licenses/>.
|
||
|
||
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)
|