commit 6961b70a7deab7e335ffdda33c777e1344404015 Author: Jeffrey Hsu Date: Fri May 16 16:15:18 2025 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f710f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.venv +build +dist +.vscode +.idea +__pycache__ +*.pyc +files \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..00be303 --- /dev/null +++ b/main.py @@ -0,0 +1,25 @@ +import os +import shutil +import sys +from pathlib import Path + +from PySide6.QtWidgets import QApplication + +from ui.main import MainWindow + +appdata_dir = Path(os.environ["APPDATA"]) +dtg_dir = appdata_dir / "dtg" +target_csv = dtg_dir / "questions.csv" + +if not target_csv.exists(): + dtg_dir.mkdir(parents=True, exist_ok=True) + source_csv = Path("template/questions.csv") + if not source_csv.exists(): + raise FileNotFoundError(f"源文件不存在:{source_csv.resolve()}") + shutil.copy(source_csv, target_csv) + +if __name__ == '__main__': + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec()) diff --git a/main.spec b/main.spec new file mode 100644 index 0000000..2791bbb --- /dev/null +++ b/main.spec @@ -0,0 +1,38 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['main.py'], + pathex=[], + binaries=[], + datas=[('template', 'template')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + name='答辩题目生成器', +) diff --git a/module/doc.py b/module/doc.py new file mode 100644 index 0000000..dc25a7c --- /dev/null +++ b/module/doc.py @@ -0,0 +1,62 @@ +import pathlib +from copy import deepcopy + +from docx import Document +from docx.shared import Cm + +from module.schema import Course, Student, Question + + +class DocPaper: + def __init__(self, filename: str = 'Paper', template_path: str = '../template/template.docx'): + self._doc = Document() + self._template = Document(template_path) + self._filename = filename + + section = self._doc.sections[0] + section.top_margin = Cm(2) + section.bottom_margin = Cm(1) + section.left_margin = Cm(1.4) + section.right_margin = Cm(1.4) + + def add_paper(self, course: Course, student: Student): + temp_table = self._template.tables[0] + new_table = deepcopy(temp_table) + para = self._doc.add_paragraph() + para._p.addprevious(new_table._element) + + data_list = { + '%CNAME%': course.name, + '%CLASS%': student.class_name, + '%SNAME%': student.name, + '%NO%': student.no, + '%SO%': student.so, + '%Q1%': student.picked_questions[0].topic, + '%Q2%': student.picked_questions[1].topic, + '%Q3%': student.picked_questions[2].topic + } + + # 替换表格中的占位符 + for row in new_table.rows: + for cell in row.cells: + for para in cell.paragraphs: + for run in para.runs: + for key, val in data_list.items(): + if key in run.text: + run.text = run.text.replace(key, val) + break + + def save(self, path: str = './'): + self._doc.save(str(pathlib.Path(path) / f"{self._filename}.docx")) + + +if __name__ == '__main__': + course = Course.load_from_xls('../files/21工程管理-工程造价Ⅱ-点名册-系统0828.xlsx') + students = Student.load_from_xls('../files/21工程管理-工程造价Ⅱ-点名册-系统0828.xlsx') + questions = Question.load_from_csv() + + d = DocPaper() + for student in students: + student.pick_question(questions) + d.add_paper(course, student) + d.save() diff --git a/module/schema.py b/module/schema.py new file mode 100644 index 0000000..022e789 --- /dev/null +++ b/module/schema.py @@ -0,0 +1,115 @@ +import random +from typing import Optional +from openpyxl.reader.excel import load_workbook + + +class Question: + def __init__(self, no: str, topic: str): + self._no: str = no + self._topic: str = topic + + def __str__(self): + return f"Question" + + def __repr__(self): + return self.__str__() + + @staticmethod + def load_from_csv(path: Optional[str] = None) -> list['Question']: + questions = [] + with open(path if path else '../files/questions.csv', encoding='gbk') as f: + for line in f.readlines(): + questions.append(Question(*line.strip('\n').split(','))) + return questions + + @property + def no(self) -> str: + return self._no + + @property + def topic(self) -> str: + return self._topic + + +class Student: + def __init__(self, no: str, so: str, name: str, major: str, class_name: str): + self._no: str = no + self._so: str = so + self._name: str = name + self._major: str = major + self._class_name: str = class_name + self._picked_questions: list[Question] = [] + + def __str__(self): + return f"Student" + + def __repr__(self): + return self.__str__() + + @staticmethod + def load_from_xls(path: str) -> list['Student']: + wb = load_workbook(path, read_only=True) + ws = wb.active + students = [] + for row in ws.iter_rows(min_row=6, max_col=5, values_only=True): + students.append(Student(*row)) + return [x for x in students if x.valid] + + @property + def valid(self) -> bool: + return bool(self._no and self._so and self._name and self._major and self._class_name) + + @property + def picked_questions(self) -> list[Question]: + return self._picked_questions + + @property + def no(self) -> str: + return self._no + + @property + def so(self) -> str: + return self._so + + @property + def name(self) -> str: + return self._name + + @property + def major(self) -> str: + return self._major + + @property + def class_name(self) -> str: + return self._class_name + + def pick_question(self, questions: list[Question], num: int = 3) -> None: + if len(questions) < num: + raise ValueError("Not enough questions to pick from.") + self._picked_questions = random.sample(questions, num) + + +class Course: + def __init__(self, name: str): + self._name = name + + def __str__(self): + return f"Course" + + def __repr__(self): + return self.__str__() + + @staticmethod + def load_from_xls(path: str) -> 'Course': + wb = load_workbook(path, read_only=True) + ws = wb.active + name: str = ws['E3'].value + return Course(name[5:]) + + @property + def name(self) -> str: + return self._name + + +if __name__ == '__main__': + ... \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e2a3b9b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +openpyxl +pyside6 +python-docx diff --git a/template/questions.csv b/template/questions.csv new file mode 100644 index 0000000..396f8b6 --- /dev/null +++ b/template/questions.csv @@ -0,0 +1,90 @@ +0.0.01,γƵĻݡ +0.0.03,γƵĻ衣 +0.0.05,γƲõҪЩ +1.1.04,½ĿĿ֣ +1.1.15,˵ɡ +1.1.18,˵ij +1.1.22,˵۵㺬塣 +2.0.06,;һļ֣ԵҪʲô +2.1.06,ʩк +2.2.04,ʡƼ۶еġۺϵۡļ֣ôģ +2.2.07,Ļ㷽һļ֣˵ +2.2.08,ʲôDzϵԤ۸ +2.2.22,ϵԤ۸Щɲ֣ +3.0.08,ʲôƸ㣿һ˭ƣ +3.1.06,ʩͼԤһ˭ƣҪЩ +3.1.15,õͶЩ +3.2.12,ʲôǡԱȡ˭ɣк壿 +3.3.04,˭ɣҪЩ +3.3.08,ҪЩ +4.1.02,ʲôǹ嵥 +4.1.05,ʲôб깤嵥ʲôѱ۹嵥 +4.1.09,嵥ࣿ +4.1.09,ʲôбƼۣ˭ƣ +4.1.11,嵥Ŀȷ +4.1.13,嵥еĿƸȷ +4.2.05,ʲôۺϵۣ +4.2.12,ۺϵ۰Щɲ֣ɲμõ +4.2.14,ۺϵγɣ +4.3.22,ļ㾫кҪ +5.0.02,ͳ﷨ᵽġһ桱ָʲô +5.0.12,ͨĵ㽨μ㽨 +5.0.15,ݶμ㽨 +5.0.16,̨μ㽨 +5.0.19,¥ݸμ㽨 +5.0.24,μ㽨 +5.0.28,Ʈ㽨ʲô +5.01.01,һļࣿ +5.01.04,ڻʱοǹͷ£ +5.01.05,ȸȷ +5.01.08,ʱɡʪλ֣ +5.01.18,ڹڻк +5.01.37,ƽصʲôμ㹤 +5.01.57,жȱ˻ˣ +5.03.05,ԤƸֽ׮μ㹤 +5.03.08,ʲô׮μ㹤 +5.03.16,׹ע׮һӦЩĹ +5.03.24,׹ע׮һӦЩĹ +5.03.39,׮μ㹤 +5.04.08,שǽλ֣ +5.04.11,שǽʱǽ߸ȡ +5.04.15,ͬȵı׼שǽļȷֱǶ١ +5.04.19,שǽʱӦ۳Щݣ +5.04.21,שŽŸμ㹤 +5.04.33,ש̨׸μ㹤 +5.05.04,ʲôǸֽεõ +5.05.05,ֽмּ㷽ѡã +5.06.04,µĻ㣬кԼ +5.06.04,ǽμ㹤 +5.06.08,ֽμ㹤λμ㹤׶ +5.06.12,߸ȷ +5.06.18,μ㹤 +5.06.34,ֽȸȡ +5.06.46,ֱμ㹤 +5.06.55,ǽ㹤ʱЩӦ۳ +5.06.73,ֱֽ¥ݸμ㹤 +5.06.78,ֽ̨Ϊשμ㹤 +5.06.82,ֽμ㹤 +5.10.05,ķˮμ㹤 +5.10.09,Էˮμ㹤 +5.13.07,Ϳ湤ļк +5.13.23,¥¥ݿ㹤ļк +5.14.05,ǽĨҸμ㹤 +5.14.11,ǽĨҸμ㹤 +5.15.05,УǺ͸к +5.15.15,ĨҸμ㹤 +5.19.03,ʱӦ㳬߷ѣ +5.20.05,ۺϽּܺ͵ֱּܷʲôʱã +5.21.03,ģ幤мּ㷽ѡã +6.1.03,嵥Ƽ£۰ļ󲿷֣ +6.1.14,еѰЩɲ֣ +6.1.23,۴ʩѺܼ۴ʩк +6.1.32,ʲôнδ֧꣬ʣн˭У +6.1.33,ʲôн˭ȷ +6.1.37,ʲôDzݹۣڽɽ㣿 +6.1.42,Щǵļʲô +6.1.65,ЩΪɾã +6.2.04,ȷ +6.3.11,˹ѵʵ֣ +6.3.23,ܳаѸμȡ +7.0.05,жϱijЩ diff --git a/template/template.docx b/template/template.docx new file mode 100644 index 0000000..e5231cf Binary files /dev/null and b/template/template.docx differ diff --git a/ui/main.py b/ui/main.py new file mode 100644 index 0000000..54dba71 --- /dev/null +++ b/ui/main.py @@ -0,0 +1,110 @@ +import os +import platform +import subprocess +import sys +from pathlib import Path + +from PySide6.QtCore import QObject, QThread, Signal +from PySide6.QtWidgets import QMainWindow, QFileDialog + +from module.doc import DocPaper +from module.schema import Course, Student, Question +from ui.pyui.main import Ui_MainWindow + + +def resource_path(relative_path: str) -> str: + if hasattr(sys, '_MEIPASS'): + base_path = sys._MEIPASS + else: + base_path = os.path.abspath(".") + return os.path.join(base_path, relative_path) + + +class Worker(QObject): + progress = Signal(str) + finished = Signal() + + def __init__(self, input_filepath: str, output_filepath: str, output_filename: str): + super().__init__() + self.input_filepath = input_filepath + self.output_filepath = output_filepath + self.output_filename = output_filename + + def run(self): + self.progress.emit("第一步:正在读取课程信息...") + course = Course.load_from_xls(self.input_filepath) + self.progress.emit("第二步:正在读取学生信息...") + students = Student.load_from_xls(self.input_filepath) + self.progress.emit("第三步:正在读取题目信息...") + questions = Question.load_from_csv(str(Path(os.environ["APPDATA"]) / "dtg" / "questions.csv")) + + d = DocPaper(self.output_filename, template_path=resource_path("template/template.docx")) + for student in students: + self.progress.emit(f"第四步:({student.no}/{len(students)})正在处理学生:{student.name},学号:{student.so}") + student.pick_question(questions) + d.add_paper(course, student) + self.progress.emit("第五步:正在保存文件...") + d.save(self.output_filepath) + self.progress.emit("第六步:文件保存成功!") + os.startfile(Path(self.output_filepath) / f"{self.output_filename}.docx") + + +class MainWindow(QMainWindow, Ui_MainWindow): + def __init__(self): + super().__init__() + self.setupUi(self) + self.setWindowTitle("答辩题目生成器") + + self.select_student_list.clicked.connect(self.choose_student_list) + self.select_output_dir.clicked.connect(self.choose_output_dir) + self.edit_topics.clicked.connect(self.edit_questions) + self.start.clicked.connect(self.start_generate) + + def on_button_clicked(self): + self.statusBar().showMessage("这是状态栏信息") + + def choose_student_list(self): + file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "Excel 文件 (*.xlsx);") + if file_path: + self.student_list_path.setText(file_path) + + input_dir_path = file_path[:file_path.rfind('/')] + default_output_filename = file_path[file_path.rfind('/') + 1:file_path.rfind('-点名册')] + + self.output_path.setText(input_dir_path) + self.output_filename.setText(default_output_filename) + + def choose_output_dir(self): + dir_path = QFileDialog.getExistingDirectory(self, "选择文件夹", "") + if dir_path: + self.output_path.setText(dir_path) + + def edit_questions(self): + csv_path = Path(os.environ["APPDATA"]) / "dtg" / "questions.csv" + + if platform.system() == "Windows": + os.startfile(csv_path.resolve()) + elif platform.system() == "Darwin": # macOS + subprocess.run(["open", csv_path.resolve()]) + else: # Linux + subprocess.run(["xdg-open", csv_path.resolve()]) + + self.update_statusbar("已打开题目文件,请在外部程序编辑后保存。") + + def start_generate(self): + self.thread = QThread() + self.worker = Worker(self.student_list_path.text(), self.output_path.text(), self.output_filename.text()) + self.worker.moveToThread(self.thread) + + # 线程启动与信号连接 + self.thread.started.connect(self.worker.run) + self.worker.progress.connect(self.update_statusbar) + self.worker.finished.connect(self.thread.quit) + self.worker.finished.connect(self.worker.deleteLater) + self.thread.finished.connect(self.thread.deleteLater) + + # 启动线程 + self.thread.start() + + def update_statusbar(self, message): + self.statusBar().showMessage(message) diff --git a/ui/main.ui b/ui/main.ui new file mode 100644 index 0000000..32ab679 --- /dev/null +++ b/ui/main.ui @@ -0,0 +1,159 @@ + + + MainWindow + + + Qt::WindowModality::ApplicationModal + + + + 0 + 0 + 500 + 200 + + + + + 500 + 200 + + + + + 500 + 200 + + + + MainWindow + + + + + + + + + 学生名单 + + + + + + + + + + 选择 + + + + + + + + + 导出设置 + + + + + + + + + 40 + 0 + + + + + 40 + 16777215 + + + + 目 录 + + + + + + + + + + 选择 + + + + + + + + + + + + 40 + 0 + + + + + 40 + 16777215 + + + + 文件名 + + + + + + + + + + + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + 编辑题库 + + + + + + + 导出 + + + + + + + + + + + + diff --git a/ui/pyui/main.py b/ui/pyui/main.py new file mode 100644 index 0000000..36e4443 --- /dev/null +++ b/ui/pyui/main.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'main.ui' +## +## Created by: Qt User Interface Compiler version 6.9.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QGridLayout, QGroupBox, QHBoxLayout, + QLabel, QLineEdit, QMainWindow, QPushButton, + QSizePolicy, QSpacerItem, QStatusBar, QWidget) + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + if not MainWindow.objectName(): + MainWindow.setObjectName(u"MainWindow") + MainWindow.setWindowModality(Qt.WindowModality.ApplicationModal) + MainWindow.resize(500, 200) + MainWindow.setMinimumSize(QSize(500, 200)) + MainWindow.setMaximumSize(QSize(500, 200)) + self.centralwidget = QWidget(MainWindow) + self.centralwidget.setObjectName(u"centralwidget") + self.gridLayout_2 = QGridLayout(self.centralwidget) + self.gridLayout_2.setObjectName(u"gridLayout_2") + self.horizontalLayout_3 = QHBoxLayout() + self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") + self.label = QLabel(self.centralwidget) + self.label.setObjectName(u"label") + + self.horizontalLayout_3.addWidget(self.label) + + self.student_list_path = QLineEdit(self.centralwidget) + self.student_list_path.setObjectName(u"student_list_path") + + self.horizontalLayout_3.addWidget(self.student_list_path) + + self.select_student_list = QPushButton(self.centralwidget) + self.select_student_list.setObjectName(u"select_student_list") + + self.horizontalLayout_3.addWidget(self.select_student_list) + + + self.gridLayout_2.addLayout(self.horizontalLayout_3, 0, 0, 1, 1) + + self.groupBox = QGroupBox(self.centralwidget) + self.groupBox.setObjectName(u"groupBox") + self.gridLayout = QGridLayout(self.groupBox) + self.gridLayout.setObjectName(u"gridLayout") + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.label_2 = QLabel(self.groupBox) + self.label_2.setObjectName(u"label_2") + self.label_2.setMinimumSize(QSize(40, 0)) + self.label_2.setMaximumSize(QSize(40, 16777215)) + + self.horizontalLayout.addWidget(self.label_2) + + self.output_path = QLineEdit(self.groupBox) + self.output_path.setObjectName(u"output_path") + + self.horizontalLayout.addWidget(self.output_path) + + self.select_output_dir = QPushButton(self.groupBox) + self.select_output_dir.setObjectName(u"select_output_dir") + + self.horizontalLayout.addWidget(self.select_output_dir) + + + self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1) + + self.horizontalLayout_2 = QHBoxLayout() + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.label_3 = QLabel(self.groupBox) + self.label_3.setObjectName(u"label_3") + self.label_3.setMinimumSize(QSize(40, 0)) + self.label_3.setMaximumSize(QSize(40, 16777215)) + + self.horizontalLayout_2.addWidget(self.label_3) + + self.output_filename = QLineEdit(self.groupBox) + self.output_filename.setObjectName(u"output_filename") + + self.horizontalLayout_2.addWidget(self.output_filename) + + + self.gridLayout.addLayout(self.horizontalLayout_2, 1, 0, 1, 1) + + + self.gridLayout_2.addWidget(self.groupBox, 1, 0, 1, 1) + + self.horizontalLayout_4 = QHBoxLayout() + self.horizontalLayout_4.setObjectName(u"horizontalLayout_4") + self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + self.horizontalLayout_4.addItem(self.horizontalSpacer) + + self.edit_topics = QPushButton(self.centralwidget) + self.edit_topics.setObjectName(u"edit_topics") + + self.horizontalLayout_4.addWidget(self.edit_topics) + + self.start = QPushButton(self.centralwidget) + self.start.setObjectName(u"start") + + self.horizontalLayout_4.addWidget(self.start) + + + self.gridLayout_2.addLayout(self.horizontalLayout_4, 2, 0, 1, 1) + + MainWindow.setCentralWidget(self.centralwidget) + self.statusbar = QStatusBar(MainWindow) + self.statusbar.setObjectName(u"statusbar") + MainWindow.setStatusBar(self.statusbar) + + self.retranslateUi(MainWindow) + + QMetaObject.connectSlotsByName(MainWindow) + # setupUi + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None)) + self.label.setText(QCoreApplication.translate("MainWindow", u"\u5b66\u751f\u540d\u5355", None)) + self.select_student_list.setText(QCoreApplication.translate("MainWindow", u"\u9009\u62e9", None)) + self.groupBox.setTitle(QCoreApplication.translate("MainWindow", u"\u5bfc\u51fa\u8bbe\u7f6e", None)) + self.label_2.setText(QCoreApplication.translate("MainWindow", u"\u76ee \u5f55", None)) + self.select_output_dir.setText(QCoreApplication.translate("MainWindow", u"\u9009\u62e9", None)) + self.label_3.setText(QCoreApplication.translate("MainWindow", u"\u6587\u4ef6\u540d", None)) + self.edit_topics.setText(QCoreApplication.translate("MainWindow", u"\u7f16\u8f91\u9898\u5e93", None)) + self.start.setText(QCoreApplication.translate("MainWindow", u"\u5bfc\u51fa", None)) + # retranslateUi +