From 7f23d64eb2e3c0a0965bd930c571301eb7edde9d Mon Sep 17 00:00:00 2001 From: Jeffrey Hsu Date: Sun, 4 Jan 2026 13:01:37 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=AD=BE=E5=90=8D=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E5=A4=84=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- module/achievement/doc.py | 84 ++++++++++++++++++++++++++++++------- module/achievement/excel.py | 76 +++++++++++++++++++++++++++++---- 2 files changed, 138 insertions(+), 22 deletions(-) diff --git a/module/achievement/doc.py b/module/achievement/doc.py index 9b455eb..b01b050 100644 --- a/module/achievement/doc.py +++ b/module/achievement/doc.py @@ -239,8 +239,8 @@ class DocxWriter: f". 课程目标达成情况的合理性评价") self.set_run_font(run, 14, 'Times New Roman', '黑体', True) - rows = 9 - cols = 4 + rows = 11 + cols = 6 table = doc.add_table(rows=rows, cols=cols) # 设置外侧框线粗1.5磅,内侧框线粗0.5磅 self.set_table_borders(table) @@ -250,12 +250,24 @@ class DocxWriter: 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) + for i in range(1, rows): + match i: + case 2: + table.cell(i, 2).merge(table.cell(i, 3)) + table.cell(i, 4).merge(table.cell(i, 5)) + table.cell(i, 1).width = Cm(7.42) + table.cell(i, 2).width = Cm(7.42) + table.cell(i, 4).width = Cm(7.41) + case 8 | 10: + table.cell(i - 1, 0).merge(table.cell(i, 0)) + table.cell(i, 1).width = Cm(11.23) + table.cell(i, 2).width = Cm(1.48) + table.cell(i, 3).width = Cm(3.4) + table.cell(i, 4).width = Cm(1.39) + case _: + 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) @@ -276,9 +288,37 @@ class DocxWriter: for t_index, table in enumerate(doc.tables): self.set_table_borders(table) + # part_3_table_index 表格第9和11行(索引8和10)特殊边框处理 + if t_index in part_3_table_index: + for r_idx in [8, 10]: + row = table.rows[r_idx] + prev_row = table.rows[r_idx - 1] + # 上一行(第8行和第10行,索引7和9)第2-6列移除下边框 + for c_idx in range(1, 6): + self.set_cell_border(prev_row.cells[c_idx], bottom=0) + # 第2列(索引1):没有上边框和右边框 + self.set_cell_border(row.cells[1], top=0, right=0) + # 第3-5列(索引2-4):没有上边框和左右边框 + for c_idx in [2, 3, 4]: + self.set_cell_border(row.cells[c_idx], top=0, left=0, right=0) + # 第6列(索引5):没有上边框和左边框 + self.set_cell_border(row.cells[5], top=0, left=0) + # 插入签名图片 + if self.excel_reader.major_director_signature_image is not None: + self.insert_pil_image(table.cell(8, 3), + self.excel_reader.major_director_signature_image, + height=Cm(1.2)) + if self.excel_reader.course_leader_signature_image is not None: + self.insert_pil_image(table.cell(10, 3), + self.excel_reader.course_leader_signature_image, + height=Cm(1.2)) for r_index, row in enumerate(table.rows): row.height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST - row.height = Cm(0.7) + # part_3_table_index 表格第9和11行(索引8和10)行高为1.2cm + if t_index in part_3_table_index and r_index in [8, 10]: + row.height = Cm(1.2) + else: + 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) @@ -330,12 +370,14 @@ class DocxWriter: special_cell = [ (1, 1), (2, 1), - (3, 1), + (3, 1), (4, 1), (5, 1), (6, 1), (7, 1), (8, 1), + (9, 1), + (10, 1), ] if r_index == 0: for run in paragraph.runs: @@ -379,6 +421,7 @@ class DocxWriter: """ 设置单元格边框 kwargs: top, bottom, left, right, inside_h, inside_v + 值为0时移除边框,值大于0时设置边框粗细 """ tc = cell._tc tcPr = tc.get_or_add_tcPr() @@ -391,10 +434,14 @@ class DocxWriter: 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') + if value == 0: + # 移除边框 + border.set(qn('w:val'), 'nil') + else: + 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) # 将边框添加到单元格属性中 @@ -490,6 +537,15 @@ class DocxWriter: run = paragraph.add_run() run.add_picture(image_stream, width=width, height=height) + def insert_pil_image(self, cell, pil_image, width=None, height=Cm(4.5)): + """插入PIL Image对象到单元格""" + image_stream = io.BytesIO() + pil_image.save(image_stream, format='PNG') + 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': diff --git a/module/achievement/excel.py b/module/achievement/excel.py index a51dd95..ef5391e 100644 --- a/module/achievement/excel.py +++ b/module/achievement/excel.py @@ -15,6 +15,7 @@ import datetime import traceback +import io from typing import Optional, Callable import openpyxl @@ -22,6 +23,7 @@ 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 PIL import Image from module import LOGLEVEL, COMPATIBLE_VERSION from module.schema import Performance @@ -49,6 +51,27 @@ class ExcelReader: ignore_version_check: bool pic_list: list suggestion_template_list: list[Optional[str]] + major_director_signature_image: Optional[Image.Image] + course_leader_signature_image: Optional[Image.Image] + + class _SheetImageLoader: + """Lightweight image loader scoped for ExcelReader use.""" + + def __init__(self, sheet: Worksheet): + self._images: dict[str, Callable[[], bytes]] = {} + for image in getattr(sheet, "_images", []): + row = image.anchor._from.row + 1 + col = get_column_letter(image.anchor._from.col + 1) + self._images[f"{col}{row}"] = image._data + + def image_in(self, cell: str) -> bool: + return cell in self._images + + def get(self, cell: str) -> Image.Image: + if cell not in self._images: + raise ValueError(f"Cell {cell} doesn't contain an image") + image = io.BytesIO(self._images[cell]()) + return Image.open(image) def __init__(self, file_path: str, version_check: bool = False, signal: Callable[[str, str], None] = lambda x, y: print(x)): @@ -75,6 +98,8 @@ class ExcelReader: self.pic_list = [] self.signal = signal self.suggestion_template_list = [] + self.major_director_signature_image = None + self.course_leader_signature_image = None def parse_excel(self): try: @@ -104,6 +129,13 @@ class ExcelReader: # 读取课程负责人 self.course_lead_teacher_name = sheet["D8"].value + need_signature_images = CUR_VERSION >= version.parse("9.4") and sheet["H10"].value == "是" + if need_signature_images: + self._load_signature_images() + else: + self.major_director_signature_image = None + self.course_leader_signature_image = None + # 读取班级和人数 max_class_size = 4 match CUR_VERSION: @@ -259,6 +291,24 @@ class ExcelReader: def run(self): self.parse_excel() + def _load_signature_images(self): + signature_cells = { + "major_director_signature_image": "K34", + "course_leader_signature_image": "K35", + } + wb_with_images: Workbook = openpyxl.load_workbook(self.file_path, data_only=True) + try: + sheet_with_images: Worksheet = wb_with_images["初始录入"] + loader = self._SheetImageLoader(sheet_with_images) + + for attr, cell in signature_cells.items(): + if loader.image_in(cell): + setattr(self, attr, loader.get(cell)) + else: + setattr(self, attr, None) + finally: + wb_with_images.close() + def clear_all_data(self): self.kpi_list = [] self.kpi_number = 0 @@ -278,6 +328,8 @@ class ExcelReader: self.hml_list = [] self.question_data = {} self.pic_list = [] + self.major_director_signature_image = None + self.course_leader_signature_image = None def set_version_check(self, version_check: bool): self.ignore_version_check = version_check @@ -486,7 +538,7 @@ class ExcelReader: yield "改进措施" yield ("注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n" f"{self.suggestion_template_list[0] if self.suggestion_template_list[0] is not None else '\n\n\n在这填入您的改进措施\n\n\n'}") - for i in range(88888): + while True: yield "如果您看到了本段文字,请联系开发者" def get_word_template_part_2(self): @@ -581,7 +633,7 @@ class ExcelReader: yield "改进措施" yield ("注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n" f"{self.suggestion_template_list[1] if self.suggestion_template_list[1] is not None else '\n\n\n在这填入您的改进措施\n\n\n'}") - for i in range(88888): + while True: yield "如果您看到了本段文字,请联系开发者" def get_word_template_part_3(self): @@ -589,7 +641,7 @@ class ExcelReader: yield "评价样本的合理性" yield "R全体样本 £抽样样本" yield "评价依据的合理性" - yield "考核方法 R合适 £不合适 " + yield "考核方法 R合适 £不合适" yield "考核内容是否支撑课程目标 R是 £否" yield "评分标准 R明确 £不明确" yield "计算过程的合理性" @@ -603,14 +655,22 @@ class ExcelReader: yield "专业负责人/系主任(签字)" yield ("整改意见:\n" "\n\n\n" - f"{self.suggestion_template_list[3] if self.suggestion_template_list[3] is not None else '\n\n\n'}" - f"\n\n\n\t\t\t\t\t\t\t\t\t签字:\t\t\t日期:{datetime.datetime.now().strftime("%Y-%m-%d")}\n") + f"{" "*8}{self.suggestion_template_list[3] if self.suggestion_template_list[3] is not None else '\n\n\n'}\n\n\n") + yield "" + yield "签字:" + yield "" + yield "日期:" + yield datetime.datetime.now().strftime("%Y-%m-%d") yield "课程负责人(签字)" yield ("拟整改计划与措施:\n" "\n\n\n" - f"{self.suggestion_template_list[4] if self.suggestion_template_list[4] is not None else '\n\n\n'}" - f"\n\n\n\t\t\t\t\t\t\t\t\t签字:\t\t\t日期:{datetime.datetime.now().strftime("%Y-%m-%d")}\n") - for i in range(88888): + f"{" "*8}{self.suggestion_template_list[4] if self.suggestion_template_list[4] is not None else '\n\n\n'}\n\n\n") + yield "" + yield "签字:" + yield "" + yield "日期:" + yield datetime.datetime.now().strftime("%Y-%m-%d") + while True: yield "如果您看到了本段文字,请联系开发者"