304 lines
12 KiB
Python
304 lines
12 KiB
Python
from functools import wraps
|
||
from typing import Literal, Callable
|
||
|
||
from PySide6.QtCore import Qt, Signal, QThread, QEvent
|
||
from PySide6.QtWidgets import QHBoxLayout, QVBoxLayout, QFileDialog, QButtonGroup, QWidget, QApplication
|
||
from qfluentwidgets import GroupHeaderCardWidget, PushButton, IconWidget, BodyLabel, PrimaryPushButton, FluentIcon, \
|
||
LineEdit, RadioButton, HyperlinkButton, FlyoutViewBase, TeachingTip, TeachingTipTailPosition
|
||
|
||
from module.worker import DTGWorker
|
||
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 ChooseTemplateView(FlyoutViewBase):
|
||
closed = Signal()
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.vBoxLayout = QVBoxLayout(self)
|
||
QApplication.instance().installEventFilter(self)
|
||
|
||
def paintEvent(self, e):
|
||
...
|
||
|
||
def eventFilter(self, watched, event):
|
||
if event.type() == QEvent.MouseButtonPress:
|
||
if not self.rect().contains(self.mapFromGlobal(event.globalPosition().toPoint())):
|
||
self.closed.emit()
|
||
return super().eventFilter(watched, event)
|
||
|
||
def addTemplate(self, content: str, cb: Callable[[], None]):
|
||
label = HyperlinkButton("", content)
|
||
self.vBoxLayout.addWidget(label)
|
||
label.clicked.connect(cb)
|
||
label.clicked.connect(self.closed.emit)
|
||
|
||
|
||
class InitSettingCard(MyGroupHeaderCardWidget):
|
||
chooseSignal = Signal(str, str)
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
|
||
self.setTitle("输入选项")
|
||
self.setBorderRadius(8)
|
||
|
||
self.sBtnHBoxLayout = QHBoxLayout(self)
|
||
self.qBtnHBoxLayout = QHBoxLayout(self)
|
||
self.sTemplateButton = HyperlinkButton("", "下载模板")
|
||
self.chooseStudentButton = PushButton("打开")
|
||
self.qTemplateButton = HyperlinkButton("", "下载模板")
|
||
self.chooseQuestionButton = PushButton("打开")
|
||
|
||
self.chooseStudentButton.setFixedWidth(120)
|
||
self.chooseQuestionButton.setFixedWidth(120)
|
||
self.sBtnHBoxLayout.addWidget(self.sTemplateButton)
|
||
self.sBtnHBoxLayout.addWidget(self.chooseStudentButton)
|
||
self.qBtnHBoxLayout.addWidget(self.qTemplateButton)
|
||
self.qBtnHBoxLayout.addWidget(self.chooseQuestionButton)
|
||
|
||
self.stuGroup = self.addGroup(FluentIcon.DOCUMENT, "学生名单", "选择学生名单文件", self.sBtnHBoxLayout)
|
||
self.QueGroup = self.addGroup(FluentIcon.DOCUMENT, "题库", "选择题库文件", self.qBtnHBoxLayout)
|
||
|
||
self.chooseStudentButton.clicked.connect(
|
||
lambda: self.choose_file(self.stuGroup.setContent, "已选择文件:", lambda x: self.chooseSignal.emit('s', x)))
|
||
self.chooseQuestionButton.clicked.connect(
|
||
lambda: self.choose_file(self.QueGroup.setContent, "已选择文件:", lambda x: self.chooseSignal.emit('q', x)))
|
||
self.qTemplateButton.clicked.connect(lambda: open_template('template-defense-paper-questions.xlsm', self))
|
||
self.sTemplateButton.clicked.connect(self.show_template_list_view)
|
||
|
||
def choose_file(
|
||
self,
|
||
set_value: Callable[[str], None] = None,
|
||
prefix: str = "",
|
||
cb: Callable[[str], None] = None
|
||
) -> None:
|
||
file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "Excel 文件 (*.xlsx *.xlsm);")
|
||
if file_path and set_value:
|
||
set_value(prefix + file_path)
|
||
if cb:
|
||
cb(file_path)
|
||
|
||
def show_template_list_view(self):
|
||
view = ChooseTemplateView(self)
|
||
view.addTemplate("普通模板", lambda: open_template("template-defense-paper-student-1.xlsm", self))
|
||
view.addTemplate("达成度模板", lambda: open_template("template-defense-paper-student-2.xlsm", self))
|
||
w = TeachingTip.make(
|
||
target=self.sTemplateButton,
|
||
view=view,
|
||
tailPosition=TeachingTipTailPosition.TOP,
|
||
duration=-1,
|
||
parent=self
|
||
)
|
||
view.closed.connect(w.close)
|
||
|
||
|
||
class ExportSettingsCard(GroupHeaderCardWidget):
|
||
startSignal = Signal()
|
||
updateSignal = Signal(str, str)
|
||
|
||
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.pdfRadio = RadioButton("PDF")
|
||
self.wordRadio = RadioButton("Word")
|
||
self.radioWidget = QWidget(self)
|
||
self.radioHbox = QHBoxLayout(self.radioWidget)
|
||
self.radioGroup = QButtonGroup(self.radioWidget)
|
||
|
||
self.radioGroup.addButton(self.pdfRadio)
|
||
self.radioGroup.addButton(self.wordRadio)
|
||
self.radioHbox.addWidget(self.pdfRadio)
|
||
self.radioHbox.addWidget(self.wordRadio)
|
||
self.pdfRadio.setChecked(True)
|
||
self.hintIcon = IconWidget(FluentIcon.INFO.icon(color=MAIN_THEME_COLOR))
|
||
self.hintLabel = BodyLabel("点击开始按钮以开始生成 👉")
|
||
self.chooseExportDirectoryButton.setFixedWidth(120)
|
||
self.startButton.setFixedWidth(120)
|
||
self.exportFileNameLineEdit.setFixedWidth(360)
|
||
self.exportFileNameLineEdit.setPlaceholderText("输入导出文件名,例如:21工程管理(1)答辩题目")
|
||
self.bottomLayout = QHBoxLayout()
|
||
self.startButton.setEnabled(False)
|
||
|
||
# 设置底部工具栏布局
|
||
self.hintIcon.setFixedSize(16, 16)
|
||
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.dirGroup = self.addGroup(FluentIcon.FOLDER, "导出目录", "选择导出文件的目录",
|
||
self.chooseExportDirectoryButton)
|
||
self.fnGroup = self.addGroup(FluentIcon.DOCUMENT, "导出文件名", "输入导出文件的名称",
|
||
self.exportFileNameLineEdit)
|
||
self.exportFormatGroup = self.addGroup(FluentIcon.DOCUMENT, "导出文件格式", "选择导出文件的格式",
|
||
self.radioWidget)
|
||
self.exportFormatGroup.setSeparatorVisible(True)
|
||
|
||
self.vBoxLayout.addLayout(self.bottomLayout)
|
||
|
||
# ==============
|
||
self.chooseExportDirectoryButton.clicked.connect(self.choose_dir)
|
||
self.startButton.clicked.connect(self.startSignal.emit)
|
||
self.exportFileNameLineEdit.textChanged.connect(lambda x: self.updateSignal.emit('n', x))
|
||
|
||
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_export_setting_by_signal(self, path: str) -> None:
|
||
f_dir = path[:path.rfind('/')]
|
||
f_name = path[path.rfind('/') + 1:path.rfind('.')] + '-答辩题目'
|
||
self.dirGroup.setContent(f"当前保存的文件目录:{f_dir}")
|
||
self.updateSignal.emit('d', f_dir)
|
||
self.exportFileNameLineEdit.setText(f_name)
|
||
self.updateSignal.emit('n', f_name)
|
||
|
||
|
||
class DefenseWidget(Widget):
|
||
errorSignal = Signal(str, str)
|
||
|
||
def __init__(self, key: str, parent=None):
|
||
super().__init__(key, parent)
|
||
|
||
self.initCard = InitSettingCard(self)
|
||
self.exportCard = ExportSettingsCard(self)
|
||
self.vbox = QVBoxLayout(self)
|
||
|
||
self.vbox.addWidget(self.initCard)
|
||
self.vbox.addWidget(self.exportCard)
|
||
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_student_filepath = None
|
||
self.input_question_filepath = None
|
||
self.output_filepath = None
|
||
self.output_filename = None
|
||
|
||
# ====================================
|
||
self.initCard.chooseSignal.connect(self.input_signal_receive)
|
||
self.exportCard.updateSignal.connect(self.export_update_signal_receive)
|
||
self.exportCard.startSignal.connect(self.start_generate)
|
||
|
||
def show_info_bar(self):
|
||
self.pib.show()
|
||
self.pib.set_title('请稍后')
|
||
|
||
def set_pb_value(self, value: int) -> None:
|
||
if value == -1:
|
||
self.successFlag = False
|
||
self.pib.set_progress(value)
|
||
|
||
def set_pb_msg(self, value: str) -> None:
|
||
self.pib.set_title(value)
|
||
|
||
def enable_start_check(func: Callable):
|
||
@wraps(func)
|
||
def wrapper(self, *args, **kwargs):
|
||
result = func(self, *args, **kwargs)
|
||
|
||
fields = [
|
||
self.input_student_filepath,
|
||
self.input_question_filepath,
|
||
self.output_filepath,
|
||
self.output_filename
|
||
]
|
||
if all(fields):
|
||
self.exportCard.startButton.setEnabled(True)
|
||
else:
|
||
self.exportCard.startButton.setEnabled(False)
|
||
return result
|
||
|
||
return wrapper
|
||
|
||
@enable_start_check
|
||
def set_value(self, key: str, value: str) -> None:
|
||
if key == "input_student_filepath":
|
||
self.input_student_filepath = value
|
||
elif key == "input_question_filepath":
|
||
self.input_question_filepath = value
|
||
elif key == "output_filepath":
|
||
self.output_filepath = value
|
||
elif key == "output_filename":
|
||
self.output_filename = value
|
||
|
||
def input_signal_receive(self, s_type: Literal['s', 'q'], value: str):
|
||
if s_type == "s":
|
||
self.set_value("input_student_filepath", value)
|
||
self.exportCard.update_export_setting_by_signal(value)
|
||
elif s_type == "q":
|
||
self.set_value("input_question_filepath", value)
|
||
|
||
def export_update_signal_receive(self, s_type: Literal['d', 'n'], value: str):
|
||
if s_type == "d":
|
||
self.set_value("output_filepath", value)
|
||
elif s_type == "n":
|
||
self.set_value("output_filename", value)
|
||
|
||
def start_generate(self):
|
||
self.thread = QThread()
|
||
self.worker = DTGWorker(
|
||
self.input_student_filepath,
|
||
self.input_question_filepath,
|
||
self.output_filepath,
|
||
self.output_filename,
|
||
self.exportCard.radioGroup.checkedButton().text().lower()
|
||
)
|
||
self.worker.moveToThread(self.thread)
|
||
|
||
self.show_info_bar()
|
||
self.successFlag = True
|
||
self.exportCard.startButton.setEnabled(False)
|
||
|
||
# 线程启动与信号连接
|
||
self.thread.started.connect(self.worker.run)
|
||
self.worker.progress[int].connect(self.set_pb_value)
|
||
self.worker.progress[str].connect(self.set_pb_msg)
|
||
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):
|
||
self.exportCard.startButton.setEnabled(True)
|
||
if self.successFlag:
|
||
self.pib.show_success(content="正在打开文件...")
|
||
else:
|
||
self.pib.show_error()
|
||
|
||
def show_error(self, title: str, content: str):
|
||
self.errorSignal.emit(title, content)
|