合并达成度功能

This commit is contained in:
2025-05-20 18:30:00 +08:00
parent 8d063fd08a
commit 0a9bd74d8e
20 changed files with 14934 additions and 47 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 432 KiB

View File

@@ -3,6 +3,7 @@ import sys
from PySide6.QtWidgets import QApplication
from ui.main import MainWindow
import module.resources
if __name__ == '__main__':
app = QApplication(sys.argv)
@@ -15,5 +16,5 @@ if __name__ == '__main__':
pass
finally:
window.show()
sys.exit(app.exec())

View File

@@ -20,7 +20,7 @@ splash = Splash(
'images\\splash.png',
binaries=a.binaries,
datas=a.datas,
text_pos=(5,298),
text_pos=(5,378),
text_size=12,
text_color='black',
minify_script=True,
@@ -47,5 +47,6 @@ exe = EXE(
target_arch=None,
codesign_identity=None,
entitlements_file=None,
name='答辩题目生成器',
name='建工工具箱',
icon=['images\\logo.png'],
)

22
module/__init__.py Normal file
View File

@@ -0,0 +1,22 @@
VER_NUM = '1.3.14'
VERSION = f'Release {VER_NUM}'
COMPATIBLE_VERSION = ['7.3', '7.4', '7.6', '7.7', '8.0']
class CONSOLE:
FONT_FAMILY = ("'JetBrains Mono', 'Consolas', 'Menlo', 'Monaco', 'Courier New', "
"'Ubuntu Mono', 'DejaVu Sans Mono', 'Liberation Mono', monospace")
BACKGROUND = "#282A36"
RED = "#FF5555"
YELLOW = "#F1FA8C"
WHITE = "#DADBD7"
GREEN = "#50FA7B"
BLACK = "#63656C"
LINE_HEIGHT = "1.2"
class LOGLEVEL:
INFO = "INFO"
WARNING = "WARNING"
ERROR = "ERROR"
SUCCESS = "SUCCESS"

505
module/achievement_doc.py Normal file
View File

@@ -0,0 +1,505 @@
import io
import os
import traceback
from typing import Callable
from docx import Document
from docx.enum.section import WD_ORIENT
from docx.enum.table import WD_ALIGN_VERTICAL, WD_ROW_HEIGHT_RULE
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Pt, Cm
from docx.text.run import Run
from module import LOGLEVEL
from module.achievement_excel import ExcelReader
class DocxWriter:
full_file_path: str
def __init__(self, save_path: str, filename: str, excel_reader: ExcelReader,
signal: Callable[[str, str], None] = lambda x, y: print(x)):
super().__init__()
self.save_path = save_path
self.filename = filename
self.excel_reader = excel_reader
self.full_file_path = os.path.join(save_path, filename) + '.docx'
self.signal = signal
def write(self) -> int:
try:
if not os.path.exists(self.save_path):
os.makedirs(self.save_path)
if os.path.exists(self.full_file_path):
self.signal(f"文件'{self.filename}'已存在,将覆盖原文件", LOGLEVEL.WARNING)
doc = Document()
for section in doc.sections:
new_width, new_height = Cm(29.7), Cm(21) # A4纸张
# 设置纸张方向为横向
section.orientation = WD_ORIENT.LANDSCAPE
# 设置页边距例如上下左右均为2厘米
section.top_margin = Cm(2)
section.bottom_margin = Cm(2)
section.left_margin = Cm(2)
section.right_margin = Cm(1)
section.page_width = new_width
section.page_height = new_height
paragraph = doc.add_paragraph()
# 设置段落居中对齐
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = paragraph.add_run(f"{self.excel_reader.course_name}》课程目标达成情况评价报告")
self.set_run_font(run, 16, '黑体', '黑体', True)
paragraph = doc.add_paragraph()
run = paragraph.add_run(
f"1.班级{'(年级)' if len(self.excel_reader.class_list) == 1 else ''}课程目标达成情况分析")
self.set_run_font(run, 14, 'Times New Roman', '黑体', True)
# 班级课程目标达成情况分析表
class_info_str = "班级:{} 学生人数:{} 任课教师:{}"
for r_index in range(len(self.excel_reader.class_list)):
self.signal(f"正在生成 {self.excel_reader.class_list[r_index]} 表格", LOGLEVEL.INFO)
paragraph = doc.add_paragraph()
# 设置段落为分散对齐
paragraph.alignment = WD_ALIGN_PARAGRAPH.DISTRIBUTE
run = paragraph.add_run(class_info_str.format(
self.excel_reader.class_list[r_index],
self.excel_reader.class_number[r_index],
self.excel_reader.course_teacher_name
))
self.set_run_font(run, 10.5, 'Times New Roman', '宋体')
if len(self.excel_reader.class_list) == 1:
rows = 14 + self.excel_reader.class_number[r_index] + self.excel_reader.kpi_number
else:
rows = 12 + self.excel_reader.class_number[r_index] + self.excel_reader.kpi_number
cols = 2 + self.calculate_columns(self.excel_reader.achievement_level[r_index]) # 加上第一列
table = doc.add_table(rows=rows, cols=cols)
# 设置外侧框线粗1.5磅内侧框线粗0.5磅
self.set_table_borders(table)
# 合并前六行头两个单元格
for row in range(6):
cell_start = table.cell(row, 0)
cell_end = table.cell(row, 1)
cell_start.merge(cell_end)
# 合并前三行的单元格
for row in range(3):
col_span = 2 # 从第三列开始
for performance in self.excel_reader.achievement_level[r_index]:
non_none_count = 3 - performance.scores.count(None)
if non_none_count > 1:
cell_start = table.cell(row, col_span)
cell_end = table.cell(row, col_span + non_none_count - 1)
cell_start.merge(cell_end)
col_span += non_none_count
X = self.excel_reader.kpi_number + 5 # X的值
if len(self.excel_reader.class_list) == 1:
X += 2
# 第一、二行合并第二个单元格
for row in range(rows - X, rows - X + 2):
table.cell(row, 0).merge(table.cell(row, 1))
# 第三行的合并操作
# 第一个单元格与第三+len(a[0])-1行第二个单元格合并
table.cell(rows - X + 2, 0).merge(table.cell(rows - X + 2 + self.excel_reader.kpi_number - 1, 1))
for i in range(self.excel_reader.kpi_number):
# 第三个与第四个单元格合并
table.cell(rows - X + 2 + i, 2).merge(table.cell(rows - X + i + 2, 3))
# 从第五个单元格合并到最后
start_cell = table.cell(rows - X + 2 + i, 4)
end_cell = table.cell(rows - X + 2 + i, cols - 1)
start_cell.merge(end_cell)
# 第三+len(a[0])行的合并操作
# 合并第一个到第二个单元格
table.cell(rows - X + 2 + self.excel_reader.kpi_number, 0).merge(
table.cell(rows - X + 2 + self.excel_reader.kpi_number, 1))
# 合并第三个到最后一个单元格
start_cell = table.cell(rows - X + 2 + self.excel_reader.kpi_number, 2)
end_cell = table.cell(rows - X + 2 + self.excel_reader.kpi_number, cols - 1)
start_cell.merge(end_cell)
if len(self.excel_reader.class_list) == 1:
# 合并前两个单元格
for i in range(2):
start = rows - X + 3 + i + self.excel_reader.kpi_number
table.cell(start, 0).merge(table.cell(start, 1))
# 依次合并后面的单元格
for row in range(start, start + self.excel_reader.kpi_number):
col_span = 2 # 从第三列开始
for performance in self.excel_reader.achievement_level[r_index]:
non_none_count = 3 - performance.scores.count(None)
if non_none_count > 1:
cell_start = table.cell(row, col_span)
cell_end = table.cell(row, col_span + non_none_count - 1)
cell_start.merge(cell_end)
col_span += non_none_count
start = rows - X + 3 + self.excel_reader.kpi_number
if len(self.excel_reader.class_list) == 1:
start += 2
end = rows
for row in range(start, end):
start_cell = table.cell(row, 1)
end_cell = table.cell(row, cols - 1)
start_cell.merge(end_cell)
# 填充表格数据
self.put_data_to_table(table, self.excel_reader.get_word_template, r_index)
doc.add_page_break()
if len(self.excel_reader.class_list) > 1:
paragraph = doc.add_paragraph()
run = paragraph.add_run("2.年级课程目标达成情况分析")
self.set_run_font(run, 14, 'Times New Roman', '黑体', True)
# 2.年级课程目标达成情况分析
class_info_str = "班级:{} 课程负责人:{}"
paragraph = doc.add_paragraph()
paragraph.alignment = WD_ALIGN_PARAGRAPH.DISTRIBUTE
run = paragraph.add_run(class_info_str.format(
self.excel_reader.total_class_str,
self.excel_reader.course_lead_teacher_name
))
self.set_run_font(run, 10.5, "Times New Roman", '宋体')
rows = 10 + len(self.excel_reader.class_list)
cols = 2 + self.calculate_columns(self.excel_reader.achievement_level[0]) # 加上第一列
table = doc.add_table(rows=rows, cols=cols)
# 设置外侧框线粗1.5磅内侧框线粗0.5磅
self.set_table_borders(table)
# 合并前六行头两个单元格
for row in range(rows - 2):
cell_start = table.cell(row, 0)
cell_end = table.cell(row, 1)
cell_start.merge(cell_end)
# 合并前三行的单元格
for row in range(3):
col_span = 2 # 从第三列开始
for performance in self.excel_reader.achievement_level[0]:
non_none_count = 3 - performance.scores.count(None)
if non_none_count > 1:
cell_start = table.cell(row, col_span)
cell_end = table.cell(row, col_span + non_none_count - 1)
cell_start.merge(cell_end)
col_span += non_none_count
# 合并中间X行的单元格
for row in range(6, 8 + len(self.excel_reader.class_list)):
col_span = 2
for performance in self.excel_reader.achievement_level[0]:
non_none_count = 3 - performance.scores.count(None)
if non_none_count > 1:
cell_start = table.cell(row, col_span)
cell_end = table.cell(row, col_span + non_none_count - 1)
cell_start.merge(cell_end)
col_span += non_none_count
# 合并最后两行的单元格
for row in range(2):
cell_start = table.cell(rows - row - 1, 1)
cell_end = table.cell(rows - row - 1, cols - 1)
cell_start.merge(cell_end)
# 填充数据
self.put_data_to_table(table, self.excel_reader.get_word_template_part_2)
doc.add_page_break()
# 3. 课程目标达成情况的合理性评价
paragraph = doc.add_paragraph()
run = paragraph.add_run(f"{3 if len(self.excel_reader.class_list) > 1 else 2}"
f". 课程目标达成情况的合理性评价")
self.set_run_font(run, 14, 'Times New Roman', '黑体', True)
rows = 9
cols = 4
table = doc.add_table(rows=rows, cols=cols)
# 设置外侧框线粗1.5磅内侧框线粗0.5磅
self.set_table_borders(table)
# 合并第一行
cell_start = table.cell(0, 0)
cell_end = table.cell(0, cols - 1)
cell_start.merge(cell_end)
# 合并第二行至最后
for i in range(1, 9):
if i == 2:
continue
cell_start = table.cell(i, 1)
cell_end = table.cell(i, cols - 1)
cell_start.merge(cell_end)
# 填充数据
self.put_data_to_table(table, self.excel_reader.get_word_template_part_3)
# 应用样式
self.signal("正在应用文字样式", LOGLEVEL.INFO)
# 遍历文档中的所有段落
for index, paragraph in enumerate(doc.paragraphs):
if index == 0:
self.set_paragraph_format(paragraph, WD_LINE_SPACING.ONE_POINT_FIVE)
else:
self.set_paragraph_format(paragraph)
# 遍历文档中的所有表格及其单元格
t_len = len(doc.tables)
part_1_table_index = [x for x in range(len(self.excel_reader.class_list))]
part_2_table_index = [len(part_1_table_index)] if len(self.excel_reader.class_list) != 1 else []
part_3_table_index = [t_len - 1]
for t_index, table in enumerate(doc.tables):
self.set_table_borders(table)
for r_index, row in enumerate(table.rows):
row.height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST
row.height = Cm(0.7)
for c_index, cell in enumerate(row.cells):
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
self.set_cell_margins(cell, start=57, end=57)
for paragraph in cell.paragraphs:
for run in paragraph.runs:
self.set_run_font(run, 10.5, 'Times New Roman', '宋体')
self.set_paragraph_format(paragraph)
special_cell = []
if t_index in part_1_table_index:
ll = [(9 + x + self.excel_reader.class_number[t_index], 4) for x in
range(self.excel_reader.kpi_number)]
start_line = 9 + self.excel_reader.kpi_number
special_cell.extend(ll)
special_cell.extend([
(1, 2),
(2, 2),
(start_line + self.excel_reader.class_number[t_index], 2),
])
if len(self.excel_reader.class_list) == 1:
special_cell.extend([
(start_line + 3 + self.excel_reader.class_number[t_index], 1),
(start_line + 4 + self.excel_reader.class_number[t_index], 1),
])
lst = [(start_line + 3 + self.excel_reader.class_number[t_index], 1)]
else:
special_cell.extend([
(start_line + 1 + self.excel_reader.class_number[t_index], 1),
(start_line + 2 + self.excel_reader.class_number[t_index], 1),
])
lst = [(start_line + 1 + self.excel_reader.class_number[t_index], 1)]
if (r_index, c_index) in lst:
self.insert_image_from_data(cell,
self.excel_reader.pic_list[t_index],
Cm(7 * self.excel_reader.kpi_number)
)
if (len(self.excel_reader.class_list) == 1 and
r_index == start_line + 2 + self.excel_reader.class_number[t_index]):
self.change_font_to_wingding2(paragraph)
elif t_index in part_2_table_index:
special_cell = [
(1, 2),
(2, 2),
(8 + len(self.excel_reader.class_list), 1),
(9 + len(self.excel_reader.class_list), 1),
]
if r_index == 7 + len(self.excel_reader.class_list):
self.change_font_to_wingding2(paragraph)
elif t_index in part_3_table_index:
special_cell = [
(1, 1),
(2, 1),
(3, 1),
(4, 1),
(5, 1),
(6, 1),
(7, 1),
(8, 1),
]
if r_index == 0:
for run in paragraph.runs:
self.set_run_font(run, 10.5, 'Times New Roman', '宋体', True)
else:
self.change_font_to_wingding2(paragraph)
if any(r_index == pair[0] and c_index >= pair[1] for pair in special_cell):
paragraph.alignment = WD_ALIGN_PARAGRAPH.LEFT
else:
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
doc.save(self.full_file_path)
except PermissionError as e:
raise Exception(f"""
简要错误信息:{str(e)}
这可能是由于Word文件被占用所导致的请关闭相应文件后再试。
""")
except Exception as _:
error_message = traceback.format_exc()
raise Exception(f"""
原始错误信息:
{error_message}
""")
def set_save_path(self, save_path: str):
self.save_path = save_path
self.full_file_path = os.path.join(save_path, self.filename)
def set_filename(self, filename: str):
self.filename = filename
self.full_file_path = os.path.join(self.save_path, filename)
def set_run_font(self, run: Run, size, western_font, chinese_font, bold=False):
run.font.size = Pt(size)
run.font.name = western_font
run.bold = bold
run._element.rPr.rFonts.set(qn('w:eastAsia'), chinese_font)
def set_cell_border(self, cell, **kwargs):
"""
设置单元格边框
kwargs: top, bottom, left, right, inside_h, inside_v
"""
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
# 创建一个新的'TC边框'元素
tcBorders = OxmlElement('w:tcBorders')
# 对于每个边框方向
for key, value in kwargs.items():
if value is not None:
tag = 'w:{}'.format(key)
border = OxmlElement(tag)
border.set(qn('w:val'), 'single')
border.set(qn('w:sz'), str(int(value * 8)))
border.set(qn('w:space'), '0')
border.set(qn('w:color'), 'auto')
tcBorders.append(border)
# 将边框添加到单元格属性中
tcPr.append(tcBorders)
def set_paragraph_format(self, paragraph, line_spacing_rule: WD_LINE_SPACING = WD_LINE_SPACING.SINGLE):
paragraph_format = paragraph.paragraph_format
paragraph_format.line_spacing_rule = line_spacing_rule # 单倍行距
paragraph_format.space_before = Pt(0) # 段前间距
paragraph_format.space_after = Pt(0) # 段后间距
def calculate_columns(self, performance_list):
"""
计算表格的列数
:param performance_list:
:return:
"""
col_count = 0
for performance in performance_list:
col_count += 3 - performance.scores.count(None) # 非None值的数量
return col_count
def put_data_to_table(self, table, gen_func, *args):
"""填充表格数据"""
data = gen_func(*args)
for i, row in enumerate(table.rows):
for j, cell in enumerate(row.cells):
if cell.text == "":
# 填充数据
# 这里需要根据您的数据结构来调整
d = next(data)
cell.text = str(d)
def set_cell_margins(self, cell, **kwargs):
"""
设置单元格的边距。
cell: 要修改的单元格实例
kwargs: 边距值以twips为单位1/1440英寸
示例set_cell_margins(cell, top=50, start=50, bottom=50, end=50)
"""
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
tcMar = OxmlElement('w:tcMar')
for m in ["top", "start", "bottom", "end"]:
if m in kwargs:
node = OxmlElement("w:{}".format(m))
node.set(qn('w:w'), str(kwargs.get(m)))
node.set(qn('w:type'), 'dxa')
tcMar.append(node)
tcPr.append(tcMar)
def is_start_of_merged_cells(self, cell):
""" 检查单元格是否是合并单元格的起始单元格 """
merge = cell._element.xpath('./w:tcPr/w:vMerge')
if merge:
return merge[0].get(qn('w:val')) == 'restart'
return False
def is_merged_vertically(self, cell):
""" 检查单元格是否垂直合并 """
merge = cell._element.xpath('./w:tcPr/w:vMerge')
return bool(merge)
def set_table_borders(self, table, outside_sz=1.5, inside_sz=0.5):
"""
设置整个表格的边框样式
outside_sz: 外侧边框大小
inside_sz: 内部分割线大小
"""
for row in table.rows:
for cell in row.cells:
self.set_cell_border(cell, top=inside_sz, bottom=inside_sz, left=inside_sz, right=inside_sz)
# 设置外侧边框
for cell in table.rows[0].cells:
self.set_cell_border(cell, top=outside_sz)
for cell in table.rows[-1].cells:
self.set_cell_border(cell, bottom=outside_sz)
for row in table.rows:
self.set_cell_border(row.cells[0], left=outside_sz)
self.set_cell_border(row.cells[-1], right=outside_sz)
def insert_image_from_data(self, cell, image_data, width=Cm(21), height=Cm(4.5)):
# 创建一个临时文件
with io.BytesIO(image_data) as image_stream:
image_stream.seek(0)
# 插入图片
# 在单元格中添加一个段落
paragraph = cell.paragraphs[0]
# 在段落中插入图片
run = paragraph.add_run()
run.add_picture(image_stream, width=width, height=height)
def is_chinese(self, char):
"""判断字符是否为中文"""
if '\u4e00' <= char <= '\u9fff':
return True
return False
def change_font_to_wingding2(self, paragraph):
for run in paragraph.runs:
if not run.text.strip(): # 跳过不包含文本的runs
continue
new_text = ""
for char in run.text:
if char in ['R', '£']:
# 对于特殊字符创建新的run
if new_text:
new_run = paragraph.add_run(new_text)
self.set_run_font(new_run, 10.5, 'Times New Roman', '宋体')
new_text = ""
special_run = paragraph.add_run(char)
self.set_run_font(special_run, 10.5, 'Wingdings 2', '宋体')
else:
# 累积其他字符
new_text += char
if new_text:
# 创建剩余文本的run
new_run = paragraph.add_run(new_text)
self.set_run_font(new_run, 10.5, 'Times New Roman', '宋体')
run.clear()

589
module/achievement_excel.py Normal file
View File

@@ -0,0 +1,589 @@
import datetime
import traceback
from typing import Optional, Callable
import openpyxl
from openpyxl.utils import get_column_letter, column_index_from_string
from openpyxl.workbook.workbook import Workbook
from openpyxl.worksheet.worksheet import Worksheet
from packaging import version
from module import LOGLEVEL, COMPATIBLE_VERSION
from module.schema import Performance
from utils.function import format_ranges, get_rank, min_score_people_name, get_class_index_range, check_version, \
gen_picture
class ExcelReader:
kpi_list: list[str]
hml_list: list[str]
kpi_number: int
file_path: str
course_name: str
course_teacher_name: str
total_class_str: str
class_list: list[str]
class_number: list[int]
course_objectives: list[str]
course_objectives_number: list[int]
evaluation_stage: list[tuple[Optional[str], Optional[str]]]
achievement_level: list[list[Performance]]
target_score: list[list[float]]
n_evaluation_methods: list[Optional[str]]
question_data: dict[str, list[tuple[str, int]]]
ignore_version_check: bool
pic_list: list
def __init__(self, file_path: str, version_check: bool = False,
signal: Callable[[str, str], None] = lambda x, y: print(x)):
super().__init__()
self.file_path = file_path
self.kpi_list = []
self.class_list = []
self.class_number = []
self.course_objectives = []
self.course_objectives_number = []
self.course_name = ""
self.course_teacher_name = ""
self.course_lead_teacher_name = ""
self.evaluation_stage = []
self.achievement_level = []
self.kpi_number = 0
self.stu_kpi_achieve_list = []
self.target_score = []
self.n_evaluation_methods = []
self.total_class_str = ""
self.hml_list = []
self.question_data = {}
self.ignore_version_check = version_check
self.pic_list = []
self.signal = signal
def parse_excel(self):
try:
wb: Workbook = openpyxl.load_workbook(self.file_path, read_only=True, data_only=True)
sheet: Worksheet = wb['初始录入']
# 读取版本号
e_version = sheet['V4'].value if sheet['V4'].value is not None else sheet['U4'].value
if e_version is None:
e_version = "0"
status, _ = check_version(e_version, COMPATIBLE_VERSION)
CUR_VERSION = version.parse(e_version)
if not status:
if not self.ignore_version_check:
raise NotImplementedError(f"版本不适配,当前表格版本:{e_version},适配版本:{COMPATIBLE_VERSION}")
self.signal("已忽略表格兼容性:您的表格版本不在适配版本中,可能导致未知错误。", LOGLEVEL.WARNING)
# 读取教学班级
self.total_class_str = sheet["D10"].value
# 读取课程名称
match CUR_VERSION:
case _ if CUR_VERSION <= version.parse("7.3"):
self.course_name = sheet["D6"].value
case _ if CUR_VERSION >= version.parse("7.4"):
self.course_name = sheet["D5"].value
# 读取任课教师
self.course_teacher_name = sheet["D7"].value
# 读取课程负责人
self.course_lead_teacher_name = sheet["D8"].value
# 读取班级和人数
max_class_size = 4
match CUR_VERSION:
case _ if CUR_VERSION <= version.parse("7.6"):
max_class_size = 3
for i in range(2, 2 + max_class_size):
name = sheet[f"K{i}"]
number = sheet[f"M{i}"]
if name.value is None or number.value is None:
break
self.class_list.append(name.value)
self.class_number.append(int(number.value))
# 读取课程指标个数
self.kpi_number = sheet["H8"].value
# 读取课程目标和指标点
hml_start = 22
match CUR_VERSION:
case _ if CUR_VERSION <= version.parse("7.6"):
hml_start = 21
for i in range(hml_start, hml_start + 5):
hml = sheet[f"H{i}"]
objective = sheet[f"I{i}"]
kpi = sheet[f"Q{i}"]
if kpi.value is None or objective.value is None:
break
self.hml_list.append(hml.value)
self.kpi_list.append(kpi.value)
self.course_objectives.append(objective.value)
# 读取考核方式及权重
k_start = 7
match CUR_VERSION:
case _ if CUR_VERSION <= version.parse("7.6"):
k_start = 6
for i in range(k_start, k_start + 6):
if i in (k_start + 1, k_start + 2, k_start + 4):
continue
way = sheet[f"K{i}"]
per = sheet[f"L{i}"]
self.evaluation_stage.append((way.value, per.value))
# 读取平时考核方式
end = k_start + 5
if CUR_VERSION >= version.parse("7.7"):
end += 1
for i in range(k_start, end):
val = sheet[f"M{i}"].value
self.n_evaluation_methods.append(val)
sheet = wb['达成度']
# 目标值表格列 | 目标X |
kpi_row_tuple = ( # | 平时 | 大作业 | 试卷 | 个人达成值 |
('D', 'E', 'F', 'G'), # 目标一 平均分 | |
('H', 'I', 'J', 'K'), # 目标二 得分率 | |
('L', 'M', 'N', 'O'), # 目标三 达成度 | |
('P', 'Q', 'R', 'S'), # 目标四
('T', 'U', 'V', 'W') # 目标五
)
# 获取达成度中的目标分值、班级平均分,得分率和达成度
for i in range(self.kpi_number):
# 目标分值
self.target_score.append([
sheet[kpi_row_tuple[i][0] + f"6"].value,
sheet[kpi_row_tuple[i][1] + f"6"].value,
sheet[kpi_row_tuple[i][2] + f"6"].value
])
base_row = 8
for j in range(len(self.class_number)):
# 平均分
avg = (
sheet[kpi_row_tuple[i][0] + f"{base_row + j * 3}"].value,
sheet[kpi_row_tuple[i][1] + f"{base_row + j * 3}"].value,
sheet[kpi_row_tuple[i][2] + f"{base_row + j * 3}"].value
)
# 得分率
per = (
sheet[kpi_row_tuple[i][0] + f"{base_row + 1 + j * 3}"].value,
sheet[kpi_row_tuple[i][1] + f"{base_row + 1 + j * 3}"].value,
sheet[kpi_row_tuple[i][2] + f"{base_row + 1 + j * 3}"].value
)
# 达成度
kpi_c = sheet[kpi_row_tuple[i][0] + f"{base_row + 2 + j * 3}"].value
if len(self.achievement_level) - 1 < j:
self.achievement_level.append([Performance(avg, per, kpi_c)])
else:
self.achievement_level[j].append(Performance(avg, per, kpi_c))
# 获取学生的各项目标值
# 读取 A18:O87 范围的数据
match CUR_VERSION:
case _ if CUR_VERSION <= version.parse("7.6"):
rows = sheet[f'B18:{kpi_row_tuple[self.kpi_number - 1][3]}{17 + sum(self.class_number)}']
case _:
rows = sheet[f'B21:{kpi_row_tuple[self.kpi_number - 1][3]}{20 + sum(self.class_number)}']
# 将数据按行保存到列表中
for row in rows:
row_data = [cell.value for cell in row]
self.stu_kpi_achieve_list.append(row_data)
# 获取试题信息
sheet = wb['成绩录入']
start_col = 'BC' # 起始列
row_keys = 9 # 键所在的行
row_values = [7, 8] # 值所在的行
# 确定终止列
col = start_col
while True:
if not sheet[col + str(row_keys)].value:
break
col = get_column_letter(column_index_from_string(col) + 1)
# 读取数据
for col_index in range(column_index_from_string(start_col),
column_index_from_string(col)):
col_letter = get_column_letter(col_index)
key = sheet[col_letter + str(row_keys)].value
if key: # 确保键不为空
values = tuple(sheet[col_letter + str(r)].value for r in row_values)
if key in self.question_data:
self.question_data[key].append(values)
else:
self.question_data[key] = [values]
self.validate_data()
self.gen_picture()
except Exception as e:
error_message = traceback.format_exc()
raise f"""
原始错误:
{error_message}
"""
def set_file_path(self, file_path: str):
self.file_path = file_path
def validate_data(self):
self.signal("正在验证数据", LOGLEVEL.INFO)
return 0
def run(self):
self.parse_excel()
def clear_all_data(self):
self.kpi_list = []
self.kpi_number = 0
self.file_path = ""
self.course_name = ""
self.course_teacher_name = []
self.class_list = []
self.class_number = []
self.course_objectives = []
self.course_objectives_number = []
self.evaluation_stage = []
self.achievement_level = []
self.kpi_number = 0
self.stu_kpi_achieve_list = []
self.target_score = []
self.n_evaluation_methods = []
self.hml_list = []
self.question_data = {}
self.pic_list = []
def set_version_check(self, version_check: bool):
self.ignore_version_check = version_check
def gen_picture(self):
self.signal("正在生成散点图", LOGLEVEL.INFO)
for i in range(len(self.class_list)):
l_index, r_index = get_class_index_range(self.class_number, i)
self.pic_list.append(gen_picture(
self.stu_kpi_achieve_list,
self.kpi_number,
l_index,
r_index
))
def get_word_template(self, class_index):
yield "课程目标"
for i in range(self.kpi_number):
yield f"课程目标{i + 1}{self.hml_list[i]}"
yield ("总成绩=\n{}".
format(
"\n+".join([f"{n if n != '试卷' else '期末'}成绩×{int(p * 100)}%" for n, p in self.evaluation_stage if
p is not None])))
for i in self.course_objectives:
yield i
yield "支撑毕业要求指标点"
for i in self.kpi_list:
yield i
yield "考核类型"
for i in range(self.kpi_number):
s_lst = self.achievement_level[class_index][i].scores
for index, (j, s) in enumerate(zip(self.evaluation_stage, s_lst)):
if s is None:
continue
if index == 2:
if len(self.n_evaluation_methods) == 6:
yield f"期末考核\n{self.n_evaluation_methods[5]}"
else:
yield f"期末考核\n{j[0]}"
else:
yield f"{j[0]}考核"
yield "目标分值(分)"
for i in self.target_score:
for j in i:
if j is None:
continue
yield j
yield "权重"
for i in range(self.kpi_number):
s_lst = self.achievement_level[class_index][i].scores
for j, s in zip(self.evaluation_stage, s_lst):
if s is None:
continue
yield j[1]
yield "学号"
yield "姓名"
for i in range(self.kpi_number):
s_lst = self.achievement_level[class_index][i].scores
for (index, j), s in zip(enumerate(self.evaluation_stage), s_lst):
if s is None:
continue
match index:
case 0:
yield "\n".join([x for x in self.n_evaluation_methods[:3] if x is not None])
case 1:
yield "\n".join([x for x in self.n_evaluation_methods[3:5] if x is not None])
case 2:
if (len(self.n_evaluation_methods) == 6 and self.n_evaluation_methods[5] != "试卷" or
len(self.n_evaluation_methods) == 5 and self.evaluation_stage[2][0] != "试卷"):
yield self.evaluation_stage[2][0]
else:
# 中文数字到数字的映射
chinese_num_map = {'': 1, '': 2, '': 3,
'': 4, '': 5, '': 6,
'': 7, '': 8, '': 9}
q_lst = self.question_data[f'目标{i + 1}']
q_dict = {key: [item[1] for item in q_lst if item[0] == key] for key in
set(key for key, _ in q_lst)}
q_str_lst = [f"{k[0]}{k[1:]}{format_ranges(v)}" for k, v in q_dict.items()]
q_str_lst.sort(key=lambda x: chinese_num_map[x[0]])
q_str = "\n".join(q_str_lst)
yield q_str
case _:
yield "如果你能看到本行文字,请联系开发者"
l_range, r_range = get_class_index_range(self.class_number, class_index)
data = self.stu_kpi_achieve_list[l_range:r_range]
# 要移除的索引位置
remove_indices = [5, 9, 13, 17, 21]
# 更新后的数据
updated_data = [[item for j, item in enumerate(row) if j not in remove_indices] for row in data]
for i in updated_data:
for j in i:
if j is None:
continue
yield j
yield "平均得分"
for i in range(self.kpi_number):
avg_scores_lst = self.achievement_level[class_index][i].scores
for j in avg_scores_lst:
if j is None:
continue
yield j
yield "得分率"
for i in range(self.kpi_number):
per_scores_lst = self.achievement_level[class_index][i].rates
for j in per_scores_lst:
if j is None:
continue
yield j
yield "课程目标达成值"
for i in range(self.kpi_number):
yield f"课程目标{i + 1}达成值"
rates_lst = []
for x in range(self.kpi_number):
rates_lst.append(self.achievement_level[class_index][x].rates)
lst = [f"{r}×{p}" for (_, p), r in zip(self.evaluation_stage, rates_lst[i]) if r is not None]
total = sum(p for (_, p), r in zip(self.evaluation_stage, rates_lst[i]) if r is not None)
yield "({})/{}={}".format(
"+".join(lst),
total if total != 1 else 1,
self.achievement_level[class_index][i].achievement)
yield "考核材料清单"
y_str = (
f"1.{self.evaluation_stage[0][0]}成绩:{''.join([x for x in self.n_evaluation_methods[:3] if x is not None])}"
f"2.{self.evaluation_stage[1][0]}成绩:{''.join([x for x in self.n_evaluation_methods[3:5] if x is not None])}"
)
# 7.7
if len(self.n_evaluation_methods) == 6 and self.n_evaluation_methods[5] is not None:
y_str += f"3.{self.evaluation_stage[2][0]}成绩:{self.n_evaluation_methods[5]}"
else:
y_str += f"3.{self.evaluation_stage[2][0] if self.evaluation_stage[2][0] != '试卷' else '期末'}成绩:{self.evaluation_stage[2][0]}"
yield y_str
if len(self.class_list) == 1:
yield "课程目标达成值"
for i in range(self.kpi_number):
yield self.achievement_level[0][i].achievement
yield "课程目标达成结论"
for i in range(self.kpi_number):
if self.achievement_level[0][i].achievement >= 0.65:
yield "R达成≥0.65 £未达成(<0.65"
else:
yield "£达成≥0.65 R未达成<0.65"
yield "结果分析"
analysis_results = f"1.总体目标:本门课程有{self.kpi_number}个课程目标,"
for index, p in enumerate(self.achievement_level[class_index]):
analysis_results += f"课程目标{index + 1}达成值为{p.achievement}"
if index + 1 == self.kpi_number:
analysis_results = analysis_results[:-1] + ""
for i in range(self.kpi_number):
analysis_results += f"课程目标{i + 1}学生"
if self.achievement_level[class_index][i].rates[0] is not None:
analysis_results += \
(f"{self.evaluation_stage[0][0]}环节中达到"
f"{get_rank(self.achievement_level[class_index][i].rates[0])}程度,")
if self.achievement_level[class_index][i].rates[1] is not None:
analysis_results += \
(f"{self.evaluation_stage[1][0]}环节中"
f"表现{get_rank(self.achievement_level[class_index][i].rates[1])}")
if self.achievement_level[class_index][i].rates[2] is not None:
analysis_results += \
(f"{self.evaluation_stage[2][0] if self.evaluation_stage[2][0] != '试卷' else '考试'}"
f"环节中为{get_rank(self.achievement_level[class_index][i].rates[2])}水平,")
analysis_results += f"总体目标{'达成' if self.achievement_level[class_index][i].achievement >= 0.65 else '未达成'}"
analysis_results = analysis_results[:-1] + ""
c_lst = [x.achievement for x in self.achievement_level[class_index]]
min_index = min(enumerate(c_lst), key=lambda x: x[1])[0]
analysis_results += f"分析{self.kpi_number}个课程目标的达成值发现,课程目标{min_index + 1}的达成情况较差。"
analysis_results += "\n2.个体差异:"
for i in range(self.kpi_number):
l, r = get_class_index_range(self.class_number, class_index)
lst = min_score_people_name(self.stu_kpi_achieve_list, i, l, r)
analysis_results += f"对于课程目标{i + 1}"
if self.achievement_level[class_index][i].rates[0] is not None:
analysis_results += \
(f"{''.join(lst[0][:2])}{'' if len(lst[0]) > 2 else ''}"
f"{self.evaluation_stage[0][0]}考核中能力较弱,")
if self.achievement_level[class_index][i].rates[1] is not None:
analysis_results += \
(f"{''.join(lst[1][:2])}{'' if len(lst[1]) > 2 else ''}"
f"{self.evaluation_stage[1][0]}考核中能力较弱,")
if self.achievement_level[class_index][i].rates[2] is not None:
analysis_results += \
(f"{''.join(lst[2][:2])}{'' if len(lst[2]) > 2 else ''}"
f"{self.evaluation_stage[2][0] if self.evaluation_stage[2][0] != '试卷' else '考试'}中能力较弱;")
analysis_results = analysis_results[:-1] + ""
yield analysis_results
yield "改进措施"
yield ("注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n"
"\n\n\n在这填入您的改进措施\n\n\n")
for i in range(88888):
yield "如果您看到了本段文字,请联系开发者"
def get_word_template_part_2(self):
# self.get_word_template_part0()
yield "课程目标"
for i in range(self.kpi_number):
yield f"课程目标{i + 1} {self.hml_list[i]}"
yield ("总成绩=\n{}".
format(
"\n+".join([f"{n if n != '试卷' else '期末'}成绩×{int(p * 100)}%" for n, p in self.evaluation_stage if
p is not None])))
for i in self.course_objectives:
yield i
yield "支撑毕业要求指标点"
for i in self.kpi_list:
yield i
yield "考核类型"
for i in range(self.kpi_number):
s_lst = self.achievement_level[0][i].scores
for index, (j, s) in enumerate(zip(self.evaluation_stage, s_lst)):
if s is None:
continue
if index == 2:
yield f"期末考核\n{j[0]}"
else:
yield f"{j[0]}考核"
yield "目标分值(分)"
for i in self.target_score:
for j in i:
if j is None:
continue
yield j
yield "权重"
for i in range(self.kpi_number):
s_lst = self.achievement_level[0][i].scores
for j, s in zip(self.evaluation_stage, s_lst):
if s is None:
continue
yield j[1]
min_rates = []
for index, i in enumerate(self.class_list):
yield i
for a_index, j in enumerate(self.achievement_level[index]):
if len(min_rates) - 1 < a_index:
min_rates.append(j.achievement)
else:
min_rates[a_index] = min(min_rates[a_index], j.achievement)
yield j.achievement
yield "课程目标达成值\n(取各班级最小值)"
for i in min_rates:
yield i
yield "课程目标达成结论"
for i in min_rates:
if i >= 0.65:
yield "R达成≥0.65 £未达成(<0.65"
else:
yield "£达成≥0.65 R未达成<0.65"
yield "结果分析"
analysis_results = f"1.总体情况:\n 本门课程有{self.kpi_number}个课程目标,"
for i in range(self.kpi_number):
analysis_results += f"课程目标{i + 1}达成值为{min_rates[i]}"
analysis_results = analysis_results[:-1] + ""
passed_c_lst = [f"课程目标{index + 1}" for index, x in enumerate(min_rates) if x >= 0.65]
not_passed_c_lst = [f"课程目标{index + 1}" for index, x in enumerate(min_rates) if x < 0.65]
if len(passed_c_lst) > 0:
analysis_results += (f"{''.join(passed_c_lst)}达到0.65的达成标准,"
f"表明学生在{''.join(passed_c_lst)}方面基本达到了本门课程提出的能力要求"
f"{'' if len(not_passed_c_lst) > 0 else ''}")
if len(not_passed_c_lst) > 0:
analysis_results += (f"{''.join(not_passed_c_lst)}未达到0.65的达成标准,"
f"表明学生在{''.join(not_passed_c_lst)}方面未能达到课程提出的能力要求,"
f"学生能力还有待进一步加强。")
analysis_results += "\n2.班级差异:"
avg_c_lst = [sum(i.achievement for i in x) / len(x) for x in self.achievement_level]
top_index = max(enumerate(avg_c_lst), key=lambda x: x[1])[0]
bottom_index = min(enumerate(avg_c_lst), key=lambda x: x[1])[0]
analysis_results += (f"\n 1班级对比各班级之间比较而言"
f"{self.class_list[top_index]}班的课程目标达成值相对较高,"
f"{self.class_list[bottom_index]}班的课程目标达成值较低。")
analysis_results += f"\n 2同班级不同课程目标比较分析{self.kpi_number}个课程目标的达成值发现,"
for i in range(len(self.class_list)):
min_p_rate_index = min(enumerate(self.achievement_level[i]), key=lambda x: x[1].achievement)[0]
o_c_str = [f'课程目标{x + 1}' for x in range(self.kpi_number) if x != min_p_rate_index]
analysis_results += (f"{self.class_list[i]}班的课程目标{min_p_rate_index + 1}达成情况最低,"
f"达成值为{self.achievement_level[i][min_p_rate_index].achievement}"
f"{''.join(o_c_str)}的达成情况较好;")
analysis_results = analysis_results[:-1] + ""
analysis_results += "\n3.结果分析: \n在此填写您的结果分析\n\n"
yield analysis_results
yield "改进措施"
yield "注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n\n\n\n"
for i in range(88888):
yield "如果您看到了本段文字,请联系开发者"
@staticmethod
def get_word_template_part_3():
yield "课程目标达成情况合理性评价"
yield "评价样本的合理性"
yield "R全体样本 £抽样样本"
yield "评价依据的合理性"
yield "考核方法 R合适 £不合适 "
yield "考核内容是否支撑课程目标 R是 £否"
yield "评分标准 R明确 £不明确"
yield "计算过程的合理性"
yield "R合理正确 £合理但不够准确 £不合理"
yield "问题分析的合理性"
yield "R合理深入 £合理但不够深入 £不合理"
yield "改进措施的合理性"
yield "R合理可行 £合理但不太可行 £不合理"
yield "合理性评价结果"
yield "R合理 £基本合理 £不合理"
yield "专业负责人/系主任(签字)"
yield ("整改意见:\n"
"\n\n\n\n\n"
"\n\n\n"
"\n\t\t\t\t\t\t\t\t\t签字:\t\t\t日期:{}\n".
format(datetime.datetime.now().strftime("%Y-%m-%d")))
yield "课程负责人(签字)"
yield ("拟整改计划与措施:\n"
"\n\n\n\n\n"
"\n\n\n"
"\n\t\t\t\t\t\t\t\t\t签字:\t\t\t日期:{}\n".
format(datetime.datetime.now().strftime("%Y-%m-%d")))
for i in range(88888):
yield "如果您看到了本段文字,请联系开发者"
if __name__ == '__main__':
excel_reader = ExcelReader('../files/21工管-房屋建筑学-点名册-1227.xlsm')
excel_reader.parse_excel()
s, e = get_class_index_range(excel_reader.class_number, 1)
r = min_score_people_name(excel_reader.stu_kpi_achieve_list, 0, s, e)
gen_picture(excel_reader.stu_kpi_achieve_list, 3, s, e)
print()

View File

@@ -15,9 +15,9 @@ class DocPaper:
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)
section.bottom_margin = Cm(2)
section.left_margin = Cm(2)
section.right_margin = Cm(2)
def add_paper(self, course: Course, student: Student):
temp_table = self._template.tables[0]

13219
module/resources.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import random
from typing import Optional
from typing import Optional, Tuple
from openpyxl.reader.excel import load_workbook
@@ -31,6 +32,7 @@ class Question:
if row[0] is None and row[1] is None:
break
questions.append(Question(*row))
wb.close()
return questions
@property
@@ -64,6 +66,7 @@ class Student:
students = []
for row in ws.iter_rows(min_row=6, max_col=5, values_only=True):
students.append(Student(*row))
wb.close()
return [x for x in students if x.valid]
@property
@@ -115,6 +118,7 @@ class Course:
wb = load_workbook(path, read_only=True)
ws = wb.active
name: str = ws['E3'].value
wb.close()
return Course(name[5:])
@property
@@ -122,5 +126,15 @@ class Course:
return self._name
class Performance:
def __init__(self, scores: Tuple[float, float, float], rates: Tuple[float, float, float], achievement: float):
self.scores = scores # (平时平均分, 大作业平均分, 试卷平均分)
self.rates = rates # (平时得分率, 大作业得分率, 试卷得分率)
self.achievement = achievement # 达成度
def __str__(self):
return f"Performance(scores={self.scores}, rates={self.rates}, achievement={self.achievement})"
if __name__ == '__main__':
...

View File

@@ -1,8 +1,11 @@
import os
from pathlib import Path
import traceback
from PySide6.QtCore import QObject, Signal
from win32com import client
from module.achievement_doc import DocxWriter
from module.achievement_excel import ExcelReader
from module.doc import DocPaper
from module.schema import Course, Student, Question
from utils.function import resource_path
@@ -11,6 +14,7 @@ from utils.function import resource_path
class DTGWorker(QObject):
progress = Signal(int)
finished = Signal()
error = Signal(str, str)
def __init__(
self,
@@ -26,19 +30,63 @@ class DTGWorker(QObject):
self.output_filename = output_filename
def run(self):
course = Course.load_from_xls(self.input_filepath)
students = Student.load_from_xls(self.input_filepath)
questions = Question.load_from_xls(self.input_question_filepath)
try:
course = Course.load_from_xls(self.input_filepath)
students = Student.load_from_xls(self.input_filepath)
questions = Question.load_from_xls(self.input_question_filepath)
d = DocPaper(self.output_filename, template_path=resource_path("template/template.docx"))
for index, student in enumerate(students):
if (p := int((index + 1) / len(students) * 100)) != 100:
self.progress.emit(p)
else:
self.progress.emit(99)
student.pick_question(questions)
d.add_paper(course, student)
d.save(self.output_filepath)
self.progress.emit(100)
os.startfile(Path(self.output_filepath) / f"{self.output_filename}.docx")
self.finished.emit()
d = DocPaper(self.output_filename, template_path=resource_path("template/template.docx"))
for index, student in enumerate(students):
if (p := int((index + 1) / len(students) * 100)) != 100:
self.progress.emit(p)
else:
self.progress.emit(99)
student.pick_question(questions)
d.add_paper(course, student)
d.save(self.output_filepath)
self.progress.emit(100)
word_file = self.output_filepath + "/" + self.output_filename + ".docx"
pdf_file = self.output_filepath + "/" + self.output_filename + ".pdf"
if os.path.exists(pdf_file):
os.remove(pdf_file)
word = client.Dispatch("Word.Application")
doc = word.Documents.Open(word_file)
doc.SaveAs(pdf_file, 17)
doc.Close()
word.Quit()
os.remove(word_file)
os.startfile(pdf_file)
except Exception as _:
error_msg = traceback.format_exc()
self.error.emit("😢 不好出错了", error_msg)
self.progress.emit(-1)
finally:
self.finished.emit()
class ARGWorker(QObject):
finished = Signal()
error = Signal(str, str)
def __init__(self, input_filepath: str, output_filepath: str, output_filename: str, disable_cc: bool = False):
super().__init__()
self.input_filepath = input_filepath
self.output_filepath = output_filepath
self.output_filename = output_filename
self.disable_compatibility_check = disable_cc
def run(self):
try:
excel = ExcelReader(self.input_filepath, self.disable_compatibility_check)
excel.run()
doc = DocxWriter(self.output_filepath, self.output_filename, excel)
doc.write()
except Exception as e:
self.error.emit("😢 不好出错了", str(e))
finally:
self.finished.emit()

View File

@@ -1,4 +1,6 @@
openpyxl
pyside6
python-docx
PySide6-Fluent-Widgets[full]
openpyxl~=3.1.5
pyside6~=6.9.0
python-docx~=1.1.2
matplotlib~=3.10.3
PySide6-Fluent-Widgets[full]
packaging~=25.0

6
resources.qrc Normal file
View File

@@ -0,0 +1,6 @@
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource>
<file>./images/logo.png</file>
</qresource>
</RCC>

Binary file not shown.

View File

@@ -1,6 +1,8 @@
from qfluentwidgets import FluentIcon, MSFluentWindow, NavigationItemPosition
from PySide6.QtGui import QIcon
from qfluentwidgets import FluentIcon, MSFluentWindow, NavigationItemPosition, MessageBox
from ui.pyui.about_ui import AboutWidget
from ui.pyui.achievement_ui import AchievementWidget
from ui.pyui.defense_ui import DefenseWidget
@@ -8,16 +10,25 @@ class MainWindow(MSFluentWindow):
def __init__(self):
super().__init__()
self.homeInterface = DefenseWidget('Defense Interface', self)
self.achievementInterface = AchievementWidget('Achievement Interface', self)
self.defenseInterface = DefenseWidget('Defense Interface', self)
self.aboutInterface = AboutWidget('About Interface', self)
self.achievementInterface.error.connect(self.showError)
self.defenseInterface.errorSignal.connect(self.showError)
self.initNavigation()
self.initWindow()
def initNavigation(self):
self.addSubInterface(self.homeInterface, FluentIcon.HOME, '首页')
self.addSubInterface(self.achievementInterface, FluentIcon.SPEED_HIGH, '达成度')
self.addSubInterface(self.defenseInterface, FluentIcon.FEEDBACK, '答辩')
self.addSubInterface(self.aboutInterface, FluentIcon.INFO, '关于', position=NavigationItemPosition.BOTTOM)
def initWindow(self):
self.resize(900, 700)
self.setWindowTitle('答辩题目生成器')
self.setWindowTitle('建工工具箱')
self.setWindowIcon(QIcon(':/images/logo.png'))
def showError(self, title: str, message: str):
MessageBox(title, message, self).exec()

View File

@@ -1,6 +1,6 @@
from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import QVBoxLayout
from qfluentwidgets import PrimaryPushSettingCard, FluentIcon, GroupHeaderCardWidget, PushButton
from PySide6.QtGui import QDesktopServices, Qt
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout
from qfluentwidgets import PrimaryPushSettingCard, FluentIcon, GroupHeaderCardWidget, PushButton, ImageLabel, TitleLabel
from ui.components.widget import Widget
@@ -9,11 +9,19 @@ class AboutWidget(Widget):
def __init__(self, key: str, parent=None):
super().__init__(key, parent)
self.logoImage = ImageLabel(':/images/logo.png')
self.logoImage.scaledToHeight(100)
self.appNameLabel = TitleLabel('建工工具箱🛠️')
self.hBox = QHBoxLayout()
self.hBox.addWidget(self.logoImage, 0, Qt.AlignLeft)
self.hBox.addWidget(self.appNameLabel, 1, Qt.AlignLeft)
self.version_card = PrimaryPushSettingCard(
text="获取源码",
icon=FluentIcon.INFO,
title="关于",
content="作者许方杰。当前版本1.0.0\n使用 GPLv3 开源协议,作者不对使用本软件造成的任何损失负责。"
content="作者许方杰。当前版本1.0.0\n本软件使用 GPLv3 开源协议进行分发,作者不对使用本软件造成的任何损失负责。"
)
self.button_list = [
PushButton("访问网站"),
@@ -31,6 +39,7 @@ class AboutWidget(Widget):
self.group_card.setTitle("第三方框架")
self.vbox = QVBoxLayout(self)
self.vbox.addLayout(self.hBox)
self.vbox.addWidget(self.version_card)
self.vbox.addWidget(self.group_card)
self.vbox.addStretch(1)

243
ui/pyui/achievement_ui.py Normal file
View File

@@ -0,0 +1,243 @@
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, QProgressBar
from qfluentwidgets import GroupHeaderCardWidget, FluentIcon, PushButton, LineEdit, IconWidget, InfoBarIcon, BodyLabel, \
PrimaryPushButton, SwitchButton, MessageBox, InfoBar, InfoBarPosition, IndeterminateProgressBar
from module.worker import ARGWorker
from ui.components.widget import Widget
class InputSettingCard(GroupHeaderCardWidget):
chooseSignal = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("输入选项")
self.setBorderRadius(8)
self.chooseFileButton = PushButton("打开")
self.chooseFileButton.setFixedWidth(120)
self.inputGroup = self.addGroup(FluentIcon.DOCUMENT, "目标文件", "选择达成度计算表", self.chooseFileButton)
# ============================
self.chooseFileButton.clicked.connect(self.choose_file)
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(InfoBarIcon.INFORMATION)
self.hintLabel = BodyLabel("点击开始按钮以开始生成 👉")
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.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.infoBar = None
# =================================
self.thread = None
self.worker = None
# ==================================
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.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.outputGroup.startButton.setEnabled(True)
if self.outputGroup.autoOpenSwitch.isChecked():
try:
os.startfile(self.output_file_path + "/" + self.output_file_name + ".docx")
except Exception as e:
self.show_error("?? 不好出错了", str(e))
if self.infoBar:
self.infoBar.close()
self.infoBar = InfoBar.success(
title='成功!',
content="正在打开文件" if self.outputGroup.autoOpenSwitch.isChecked() else "文件已保存",
orient=Qt.Horizontal,
isClosable=True,
position=InfoBarPosition.BOTTOM,
duration=5000,
parent=self
)
def show_error(self, title: str, content: str):
self.error.emit(title, content)
def show_info_bar(self, info: str):
self.infoBar = InfoBar(
icon=InfoBarIcon.INFORMATION,
title='请稍后',
content=info,
orient=Qt.Horizontal,
isClosable=False,
position=InfoBarPosition.BOTTOM,
duration=-1,
parent=self
)
self.infoBar.addWidget(IndeterminateProgressBar(start=True))
self.infoBar.show()

View File

@@ -4,7 +4,7 @@ from functools import wraps
from PySide6.QtCore import Qt, Signal, QThread
from PySide6.QtWidgets import QHBoxLayout, QVBoxLayout, QFileDialog
from qfluentwidgets import GroupHeaderCardWidget, PushButton, IconWidget, InfoBarIcon, \
BodyLabel, PrimaryPushButton, FluentIcon, LineEdit, InfoBar, InfoBarPosition, ProgressBar
BodyLabel, PrimaryPushButton, FluentIcon, LineEdit, InfoBar, InfoBarPosition, ProgressBar, IndeterminateProgressBar
from module.worker import DTGWorker
from ui.components.widget import Widget
@@ -16,7 +16,7 @@ class InitSettingCard(GroupHeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("初始化")
self.setTitle("输入选项")
self.setBorderRadius(8)
self.chooseStudentButton = PushButton("打开")
@@ -53,7 +53,7 @@ class ExportSettingsCard(GroupHeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("导出")
self.setTitle("输入选项")
self.setBorderRadius(8)
self.chooseExportDirectoryButton = PushButton("选择")
@@ -100,14 +100,16 @@ class ExportSettingsCard(GroupHeaderCardWidget):
def update_export_setting_by_signal(self, path: str) -> None:
f_dir = path[:path.rfind('/')]
f_name = path[path.rfind('/') + 1: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"{f_name}-答辩题目")
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)
@@ -125,6 +127,8 @@ class DefenseWidget(Widget):
self.thread = None
self.worker = None
self.successFlag = True
# ===================================
self.input_student_filepath = None
@@ -157,17 +161,21 @@ class DefenseWidget(Widget):
self.pb.setValue(value)
if value == 100:
self.infoBar.close()
self.infoBar = InfoBar.success(
title='成功!',
content="正在打开文件...",
self.infoBar = InfoBar(
icon=InfoBarIcon.INFORMATION,
title='正在转换文件',
content="",
orient=Qt.Horizontal,
isClosable=True,
isClosable=False,
position=InfoBarPosition.BOTTOM,
duration=5000,
duration=-1,
parent=self
)
self.infoBar.addWidget(IndeterminateProgressBar(start=True))
self.infoBar.show()
self.pb = None
self.exportCard.startButton.setEnabled(True)
elif value == -1:
self.successFlag = False
def enable_start_check(func: Callable):
@wraps(func)
@@ -198,7 +206,6 @@ class DefenseWidget(Widget):
self.output_filepath = value
elif key == "output_filename":
self.output_filename = value
setattr(self, key, value)
def input_signal_receive(self, s_type: Literal['s', 'q'], value: str):
if s_type == "s":
@@ -224,16 +231,19 @@ class DefenseWidget(Widget):
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.connect(self.set_pb_value)
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()
@@ -241,3 +251,30 @@ class DefenseWidget(Widget):
def clear_thread_worker_refs(self):
self.thread = None
self.worker = None
def after_generate(self):
self.exportCard.startButton.setEnabled(True)
self.infoBar.close()
if self.successFlag:
self.infoBar = InfoBar.success(
title='成功!',
content="正在打开文件...",
orient=Qt.Horizontal,
isClosable=True,
position=InfoBarPosition.BOTTOM,
duration=5000,
parent=self
)
else:
self.infoBar = InfoBar.error(
title='失败!',
content="",
orient=Qt.Horizontal,
isClosable=True,
position=InfoBarPosition.BOTTOM,
duration=5000,
parent=self
)
def show_error(self, title: str, content: str):
self.errorSignal.emit(title, content)

View File

@@ -1,5 +1,185 @@
import os
import sys
import io
import matplotlib
from matplotlib import pyplot as plt
import re
def format_ranges(nums):
"""格式化区间,支持特殊标记"""
if not nums:
return ""
def parse_value(val):
"""解析值,返回主数值和次要标记"""
if isinstance(val, int):
return val, ""
match = re.match(r"(\d+)[(](.*?)[)]", str(val))
if match:
return int(match.group(1)), match.group(2)
return int(val), ""
# 排序规则:主数值优先,次要标记其次
sorted_nums = sorted(nums, key=lambda x: parse_value(x))
result = []
start = sorted_nums[0]
end = sorted_nums[0]
def is_continuous(val1, val2):
"""判断两个值是否连续"""
main1, sub1 = parse_value(val1)
main2, sub2 = parse_value(val2)
# 主数值连续,且次要标记为空(特殊标记视为不连续)
return main2 == main1 + 1 and not sub2
for i in range(1, len(sorted_nums)):
if is_continuous(end, sorted_nums[i]):
end = sorted_nums[i]
else:
# 处理一个区间
if parse_value(start)[0] == parse_value(end)[0]: # 单值或特殊标记
result.append(str(start))
elif parse_value(end)[0] - parse_value(start)[0] >= 2: # 连续区间
result.append(f"{parse_value(start)[0]}~{parse_value(end)[0]}")
else: # 非连续的单值
result.extend(map(str, [start, end]))
start = end = sorted_nums[i]
# 添加最后一段区间
if parse_value(start)[0] == parse_value(end)[0]: # 单值或特殊标记
result.append(str(start))
elif parse_value(end)[0] - parse_value(start)[0] >= 2: # 连续区间
result.append(f"{parse_value(start)[0]}~{parse_value(end)[0]}")
else: # 非连续的单值
result.extend(map(str, [start, end]))
return "".join(result)
def support_version_str(lst: list[str]) -> str:
"""支持的版本字符串"""
if len(lst) == 1:
return lst[0]
elif len(lst) == 2:
return f"{lst[0]}{lst[1]}"
else:
return f"{lst[0]}~{lst[-1]}"
def get_rank(score: int | float) -> str:
"""根据分数获取等级"""
if score >= 0.9:
return "优秀"
elif score >= 0.8:
return "良好"
elif score >= 0.7:
return "中等"
elif score >= 0.6:
return "及格"
else:
return "不及格"
def min_score_people_name(data: list, kpi_number: int, start_index: int, end_index: int) -> list[tuple[str]]:
"""获取最低分人名"""
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13
# 20060510XX 覃XX 40.5 45 49 0.738 16 17 0 0.330 24.5 15 14 0.695
remove_indices = [5, 9, 13, 17, 21]
# 更新后的数据
# 0 1 2 3 4 5 6 7 8 9 10
# 20060510XX 覃XX 40.5 45 49 16 17 0 24.5 15 14
index = 2 + kpi_number * 3
updated_data = [[item for j, item in enumerate(row) if j not in remove_indices] for row in data]
result = [(), (), ()]
min_scores = [min(updated_data[start_index:end_index],
key=lambda x: x[index + i] if x[index + i] is not None else 0)[index + i] for i in range(3)]
for row in updated_data[start_index:end_index]:
for i in range(3):
if row[index + i] == min_scores[i]:
result[i] += (row[1],)
return result
def get_class_index_range(lst: list[int], num: int):
"""获取班级的索引范围"""
if num < 0 or num > len(lst):
raise ValueError("OutOfRange")
# 计算前num-1项和加一
first_value = sum(lst[:num])
# 计算前num项和加一
second_value = sum(lst[:num + 1]) if num < len(lst) else "OutOfRange"
return (first_value, second_value)
def check_version(version, compatible_versions):
# 将版本字符串转换为数字列表以便比较
version_nums = [int(v) for v in version.split('.')]
min_version_nums = [int(v) for v in compatible_versions[0].split('.')]
max_version_nums = [int(v) for v in compatible_versions[-1].split('.')]
# 比较版本
if version_nums < min_version_nums:
return False, "不支持低于{}的版本,如需尝试生成请勾选‘关闭兼容性检查’".format(compatible_versions[0])
elif version_nums > max_version_nums:
return True, "版本过高可能会导致兼容性问题"
else:
return True, ""
def gen_picture(data: list[list[str]], kpi_number: int, start_index: int, end_index: int):
# 设置全局字体
matplotlib.rcParams['font.family'] = ['Times New Roman', 'KaiTi']
matplotlib.rcParams['font.size'] = 10
# 将厘米转换为英寸
width_in_inches = 8 * 0.393701
height_in_inches = 6 * 0.393701
# 创建一个图形和三个子图
fig, axs = plt.subplots(1, kpi_number, figsize=(width_in_inches * kpi_number, height_in_inches))
# 遍历每个指定的索引来生成散点图
for ax, index in zip(axs, range(kpi_number)):
# 处理数据
data_indices = [5, 9, 13, 17, 21]
update_data = [[float(item) for j, item in enumerate(row) if j in data_indices] for row in data]
x = [i for i in range(end_index - start_index)]
y = [row[index] for row in update_data[start_index:end_index]]
# 绘制散点图
ax.scatter(x, y, label=f'目标{index + 1}达成值', s=10)
# 添加期望值直线
ax.axhline(y=0.65, color='r', linestyle='-', label='期望值')
# 设置轴标签
ax.set_xlabel('学生学号')
ax.set_ylabel('达成值')
# 设置y轴范围和刻度
ax.set_ylim(0, 1)
ax.set_yticks([i * 0.2 for i in range(6)])
# 设置图例
ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.35), ncol=2, frameon=False)
# 设置坐标原点为(0, 0)
ax.set_xlim(x.index(x[0]), None)
# 调整子图间距
plt.tight_layout()
# 创建一个内存中的文件对象
buf = io.BytesIO()
# 保存图形
plt.savefig(buf, format='png', dpi=300, bbox_inches='tight')
# 将文件对象转换为字节对象
res = buf.getvalue()
buf.close()
return res
if __name__ == '__main__':
nums = [1, 2, 3, '4(1)', '42']
formatted_ranges = format_ranges(nums)
print(formatted_ranges)
def resource_path(relative_path: str) -> str: