Compare commits

...

9 Commits

8 changed files with 302 additions and 65 deletions

View File

@@ -1,37 +1,36 @@
@echo off
setlocal
chcp 65001
echo === 尝试激活虚拟环境 ===
echo === Activating virtual environment ===
if exist ".venv\Scripts\activate.bat" (
call .venv\Scripts\activate.bat
) else (
echo 未发现虚拟环境,尝试创建中...
echo Virtual environment not found. Creating...
python -m venv .venv
if errorlevel 1 (
echo [错误] 创建虚拟环境失败,请确认是否已安装 Python。
echo [ERROR] Failed to create virtual environment. Please ensure Python is installed.
pause
exit /b 1
)
echo 虚拟环境创建成功,开始激活...
echo Virtual environment created. Activating...
call .venv\Scripts\activate.bat
)
echo === 安装依赖项 ===
pip install -r requirements.txt
echo === Installing dependencies ===
pip install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple -r requirements.txt
if errorlevel 1 (
echo [错误] pip 安装依赖失败!
echo [ERROR] pip failed to install dependencies.
pause
exit /b 1
)
echo === 使用 pyinstaller 构建 ===
echo === Building with pyinstaller ===
python .\utils\hook.py
pyinstaller .\main.spec
if errorlevel 1 (
echo [错误] 构建失败!
echo [ERROR] Build failed.
pause
exit /b 1
)
echo === 构建完成 ===
echo === Build completed ===

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
# Copyright (c) 2025-2026 Jeffrey Hsu - JITToolBox
# #
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -155,10 +155,15 @@ class DocxWriter:
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
try:
cell_start = table.cell(row, col_span)
cell_end = table.cell(row, col_span + non_none_count - 1)
cell_start.merge(cell_end)
except IndexError:
pass
# self.signal(f"单元格合并失败:({row}, {col_span}),需要自行检查表格准确性",
# LOGLEVEL.WARNING)
col_span += non_none_count
start = rows - X + 3 + self.excel_reader.kpi_number
if len(self.excel_reader.class_list) == 1:
@@ -239,8 +244,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,13 +255,25 @@ 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 +293,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)
@@ -336,6 +381,8 @@ class DocxWriter:
(6, 1),
(7, 1),
(8, 1),
(9, 1),
(10, 1),
]
if r_index == 0:
for run in paragraph.runs:
@@ -379,6 +426,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 +439,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 +542,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':

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
# Copyright (c) 2025-2026 Jeffrey Hsu - JITToolBox
# #
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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,34 @@ 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)
class ValidError(Exception):
def __init__(self, message):
self.message = message
def __str__(self):
return self.message
def __init__(self, file_path: str, version_check: bool = False,
signal: Callable[[str, str], None] = lambda x, y: print(x)):
@@ -75,6 +105,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 +136,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:
@@ -239,9 +278,15 @@ class ExcelReader:
for i in range(len(self.suggestion_template_list), 5):
self.suggestion_template_list.append(None)
self.validate_data()
if vd_lst := self.validate_data():
raise self.ValidError("\n\n".join(vd_lst))
self.gen_picture()
except self.ValidError as ve:
raise Exception(f"""
数据验证失败:\n\n{str(ve)}
""")
except Exception as e:
error_message = traceback.format_exc()
raise Exception(f"""
@@ -252,13 +297,40 @@ class ExcelReader:
def set_file_path(self, file_path: str):
self.file_path = file_path
def validate_data(self):
def validate_data(self) -> list[str]:
lst: list[str] = []
self.signal("正在验证数据", LOGLEVEL.INFO)
return 0
if len(self.kpi_list) != self.kpi_number:
self.signal("\"课程目标\"\"目标支撑的毕业要求指标点\"数量与期望目标数量不符", LOGLEVEL.ERROR)
lst.append(
f"\"课程目标\"\"目标支撑的毕业要求指标点\"数量与期望目标数量不符请检查Excel表格中的\"课程目标\"\"目标支撑的毕业要求指标点\"列是否填写完整。"
f"期望得到 {self.kpi_number} 个,实际检测到 {len(self.kpi_list)} 个。"
f"如想暂时不填请在Excel表格对应的位置添加一个空格"
)
return lst
def run(self):
self.parse_excel()
def _load_signature_images(self):
signature_cells = {
"major_director_signature_image": ("I34", "K34"),
"course_leader_signature_image": ("I35", "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, (check_cell, image_cell) in signature_cells.items():
if sheet_with_images[check_cell].value and loader.image_in(image_cell):
setattr(self, attr, loader.get(image_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 +350,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 +560,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 +655,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 +663,7 @@ class ExcelReader:
yield "评价样本的合理性"
yield "R全体样本 £抽样样本"
yield "评价依据的合理性"
yield "考核方法 R合适 £不合适 "
yield "考核方法 R合适 £不合适"
yield "考核内容是否支撑课程目标 R是 £否"
yield "评分标准 R明确 £不明确"
yield "计算过程的合理性"
@@ -603,14 +677,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 "如果您看到了本段文字,请联系开发者"

View File

@@ -13,6 +13,10 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import os
from dataclasses import dataclass
from typing import Callable
from PySide6.QtGui import QIcon, QShowEvent
from qfluentwidgets import FluentIcon, MSFluentWindow, NavigationItemPosition, MessageBox, setThemeColor
@@ -20,40 +24,99 @@ from ui import MAIN_THEME_COLOR, BLUE_BACKGROUND_COLOR
from ui.pyui.about_ui import AboutWidget
from ui.pyui.achievement_ui import AchievementWidget
from ui.pyui.defense_ui import DefenseWidget
# from ui.pyui.picker_ui import PickerWidget
from ui.pyui.picker_ui import PickerWidget
from ui.pyui.test_ui import TestWidget
from utils.function import RELEASE_ENV
@dataclass(frozen=True)
class InterfaceSpec:
key: str
factory: Callable[[], object]
icon: FluentIcon
nav_text: str
position: NavigationItemPosition = NavigationItemPosition.TOP
enabled: bool = True
class MainWindow(MSFluentWindow):
def __init__(self):
super().__init__()
setThemeColor(MAIN_THEME_COLOR)
self.setCustomBackgroundColor(BLUE_BACKGROUND_COLOR, BLUE_BACKGROUND_COLOR)
self.achievementInterface = AchievementWidget('Achievement Interface', self)
self.defenseInterface = DefenseWidget('Defense Interface', self)
self.aboutInterface = AboutWidget('About Interface', self)
# self.pickerInterface = PickerWidget('Picker Interface', self)
if not RELEASE_ENV:
self.testInterface = TestWidget('Test Interface', self)
self.achievementInterface.error.connect(self.showError)
self.defenseInterface.errorSignal.connect(self.showError)
# self.pickerInterface.errorSignal.connect(self.showError)
self.interface_specs = self.build_interface_specs()
self.interfaces = self.create_interfaces(self.interface_specs)
self.bind_error_handlers()
self.initNavigation()
self.initWindow()
def initNavigation(self):
self.addSubInterface(self.achievementInterface, FluentIcon.SPEED_HIGH, '达成度')
self.addSubInterface(self.defenseInterface, FluentIcon.FEEDBACK, '答辩')
# self.addSubInterface(self.pickerInterface, FluentIcon.PEOPLE, '提问')
if not RELEASE_ENV:
self.addSubInterface(self.testInterface, FluentIcon.VIEW, '测试')
def build_interface_specs(self) -> list[InterfaceSpec]:
return [
InterfaceSpec(
key="achievement",
factory=lambda: AchievementWidget('Achievement Interface', self),
icon=FluentIcon.SPEED_HIGH,
nav_text='达成度',
enabled=True,
),
InterfaceSpec(
key="defense",
factory=lambda: DefenseWidget('Defense Interface', self),
icon=FluentIcon.FEEDBACK,
nav_text='答辩',
enabled=True,
),
InterfaceSpec(
key="picker",
factory=lambda: PickerWidget('Picker Interface', self),
icon=FluentIcon.PEOPLE,
nav_text='提问',
enabled=not RELEASE_ENV,
),
InterfaceSpec(
key="test",
factory=lambda: TestWidget('Test Interface', self),
icon=FluentIcon.VIEW,
nav_text='测试',
enabled=not RELEASE_ENV,
),
InterfaceSpec(
key="about",
factory=lambda: AboutWidget('About Interface', self),
icon=FluentIcon.INFO,
nav_text='关于',
position=NavigationItemPosition.BOTTOM,
enabled=True
),
]
self.addSubInterface(self.aboutInterface, FluentIcon.INFO, '关于', position=NavigationItemPosition.BOTTOM)
def create_interfaces(self, specs: list[InterfaceSpec]) -> dict[str, object]:
interfaces: dict[str, object] = {}
for spec in specs:
if not spec.enabled:
continue
widget = spec.factory()
interfaces[spec.key] = widget
setattr(self, f"{spec.key}Interface", widget)
return interfaces
def bind_error_handlers(self):
achievement = self.interfaces.get("achievement")
defense = self.interfaces.get("defense")
if achievement and hasattr(achievement, "error"):
achievement.error.connect(self.showError)
if defense and hasattr(defense, "errorSignal"):
defense.errorSignal.connect(self.showError)
def initNavigation(self):
for spec in self.interface_specs:
widget = self.interfaces.get(spec.key)
if not widget:
continue
self.addSubInterface(widget, spec.icon, spec.nav_text, position=spec.position)
def initWindow(self):
self.resize(900, 700)

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
# Copyright (c) 2025-2026 Jeffrey Hsu - JITToolBox
# #
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -20,7 +20,7 @@ from typing import Callable, Literal
from PySide6.QtCore import Qt, Signal, QThread
from PySide6.QtWidgets import QVBoxLayout, QFileDialog, QHBoxLayout
from qfluentwidgets import GroupHeaderCardWidget, FluentIcon, PushButton, LineEdit, IconWidget, BodyLabel, \
PrimaryPushButton, SwitchButton, HyperlinkButton
PrimaryPushButton, SwitchButton, HyperlinkButton, InfoBar, InfoBarPosition
from module import LOGLEVEL
from module.worker import ARGWorker
@@ -259,3 +259,23 @@ class AchievementWidget(Widget):
def show_info(self, content: str, level: str):
if level == LOGLEVEL.INFO:
self.pib.set_title(content)
elif level == LOGLEVEL.WARNING:
InfoBar.warning(
title='提示',
content=content,
orient=Qt.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=5000,
parent=self
)
elif level == LOGLEVEL.ERROR:
InfoBar.error(
title='错误',
content=content,
orient=Qt.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=-1,
parent=self
)

View File

@@ -18,11 +18,23 @@ from datetime import datetime
def gen_build_info():
hash_str = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('utf-8').strip()
try:
hash_str = subprocess.check_output(
['git', 'rev-parse', '--short', 'HEAD'],
stderr=subprocess.DEVNULL
).decode('utf-8').strip()
except FileNotFoundError:
# git 未安装
hash_str = 'unknown'
except subprocess.CalledProcessError:
# 不是 git 仓库(如从压缩包下载)
hash_str = 'unknown'
with open('build_info.py', 'w', encoding='utf-8') as f:
f.write(f"# Auto-generated build info\n")
f.write(f"BUILD_TIME = '{datetime.now().isoformat(sep=' ', timespec='seconds')}'\n")
f.write(f"GIT_HASH = '{hash_str}'\n")
gen_build_info()
if __name__ == '__main__':
gen_build_info()