Compare commits

...

17 Commits

Author SHA1 Message Date
96350bb8e2 添加数据验证逻辑,抛出自定义异常以处理验证失败情况;更新错误处理逻辑以显示详细错误信息 2026-01-20 23:19:10 +08:00
28e35ea429 添加单元格合并异常处理逻辑 2026-01-18 15:23:18 +08:00
a708bbfa72 重构主窗口界面,使用接口规范化界面创建和错误处理逻辑 2026-01-06 18:38:54 +08:00
db53baba23 构建脚本中的输出信息,翻译为英文 2026-01-06 16:59:30 +08:00
d95bdef3f5 优化生成构建信息的逻辑,添加异常处理以应对 git 未安装或非 git 仓库的情况 2026-01-05 21:22:54 +08:00
1a00811cfc 更新模板文件,替换成最新版本 2026-01-05 21:15:44 +08:00
836ccbe4aa 更新签名图片加载逻辑 2026-01-05 19:26:28 +08:00
a2f453a638 更新依赖项安装命令,使用清华镜像源 2026-01-05 18:45:23 +08:00
7f23d64eb2 添加签名图片处理功能 2026-01-04 13:01:37 +08:00
475d23f49e Updates Excel templates with latest revisions 2025-11-26 22:40:56 +08:00
85697f35c6 纸质答辩默认模式设为Word 2025-08-22 20:41:20 +08:00
de0655adbc 标题添加“答辩” 2025-08-22 20:36:18 +08:00
8f8e1ed1aa 模板更新至9.0 2025-08-22 20:32:17 +08:00
39fcaf35ca 删除提问功能、替换答辩模板 2025-08-22 20:07:17 +08:00
da723409ca 重构 2025-08-22 19:03:51 +08:00
438cb8a1d9 添加9.0的模板功能 2025-08-09 14:03:57 +08:00
5749dbbcee 更新达成度模板 2025-07-06 10:19:17 +08:00
23 changed files with 856 additions and 85 deletions

View File

@@ -1,37 +1,36 @@
@echo off @echo off
setlocal setlocal
chcp 65001
echo === 尝试激活虚拟环境 === echo === Activating virtual environment ===
if exist ".venv\Scripts\activate.bat" ( if exist ".venv\Scripts\activate.bat" (
call .venv\Scripts\activate.bat call .venv\Scripts\activate.bat
) else ( ) else (
echo 未发现虚拟环境,尝试创建中... echo Virtual environment not found. Creating...
python -m venv .venv python -m venv .venv
if errorlevel 1 ( if errorlevel 1 (
echo [错误] 创建虚拟环境失败,请确认是否已安装 Python。 echo [ERROR] Failed to create virtual environment. Please ensure Python is installed.
pause pause
exit /b 1 exit /b 1
) )
echo 虚拟环境创建成功,开始激活... echo Virtual environment created. Activating...
call .venv\Scripts\activate.bat call .venv\Scripts\activate.bat
) )
echo === 安装依赖项 === echo === Installing dependencies ===
pip install -r requirements.txt pip install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple -r requirements.txt
if errorlevel 1 ( if errorlevel 1 (
echo [错误] pip 安装依赖失败! echo [ERROR] pip failed to install dependencies.
pause pause
exit /b 1 exit /b 1
) )
echo === 使用 pyinstaller 构建 === echo === Building with pyinstaller ===
python .\utils\hook.py python .\utils\hook.py
pyinstaller .\main.spec pyinstaller .\main.spec
if errorlevel 1 ( if errorlevel 1 (
echo [错误] 构建失败! echo [ERROR] Build failed.
pause pause
exit /b 1 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 # 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 # it under the terms of the GNU General Public License as published by
@@ -155,9 +155,14 @@ class DocxWriter:
for performance in self.excel_reader.achievement_level[r_index]: for performance in self.excel_reader.achievement_level[r_index]:
non_none_count = 3 - performance.scores.count(None) non_none_count = 3 - performance.scores.count(None)
if non_none_count > 1: if non_none_count > 1:
try:
cell_start = table.cell(row, col_span) cell_start = table.cell(row, col_span)
cell_end = table.cell(row, col_span + non_none_count - 1) cell_end = table.cell(row, col_span + non_none_count - 1)
cell_start.merge(cell_end) cell_start.merge(cell_end)
except IndexError:
pass
# self.signal(f"单元格合并失败:({row}, {col_span}),需要自行检查表格准确性",
# LOGLEVEL.WARNING)
col_span += non_none_count col_span += non_none_count
start = rows - X + 3 + self.excel_reader.kpi_number start = rows - X + 3 + self.excel_reader.kpi_number
@@ -239,8 +244,8 @@ class DocxWriter:
f". 课程目标达成情况的合理性评价") f". 课程目标达成情况的合理性评价")
self.set_run_font(run, 14, 'Times New Roman', '黑体', True) self.set_run_font(run, 14, 'Times New Roman', '黑体', True)
rows = 9 rows = 11
cols = 4 cols = 6
table = doc.add_table(rows=rows, cols=cols) table = doc.add_table(rows=rows, cols=cols)
# 设置外侧框线粗1.5磅内侧框线粗0.5磅 # 设置外侧框线粗1.5磅内侧框线粗0.5磅
self.set_table_borders(table) self.set_table_borders(table)
@@ -250,9 +255,21 @@ class DocxWriter:
cell_end = table.cell(0, cols - 1) cell_end = table.cell(0, cols - 1)
cell_start.merge(cell_end) cell_start.merge(cell_end)
# 合并第二行至最后 # 合并第二行至最后
for i in range(1, 9): for i in range(1, rows):
if i == 2: match i:
continue 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_start = table.cell(i, 1)
cell_end = table.cell(i, cols - 1) cell_end = table.cell(i, cols - 1)
cell_start.merge(cell_end) cell_start.merge(cell_end)
@@ -276,8 +293,36 @@ class DocxWriter:
for t_index, table in enumerate(doc.tables): for t_index, table in enumerate(doc.tables):
self.set_table_borders(table) 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): for r_index, row in enumerate(table.rows):
row.height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST row.height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST
# 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) row.height = Cm(0.7)
for c_index, cell in enumerate(row.cells): for c_index, cell in enumerate(row.cells):
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
@@ -336,6 +381,8 @@ class DocxWriter:
(6, 1), (6, 1),
(7, 1), (7, 1),
(8, 1), (8, 1),
(9, 1),
(10, 1),
] ]
if r_index == 0: if r_index == 0:
for run in paragraph.runs: for run in paragraph.runs:
@@ -379,6 +426,7 @@ class DocxWriter:
""" """
设置单元格边框 设置单元格边框
kwargs: top, bottom, left, right, inside_h, inside_v kwargs: top, bottom, left, right, inside_h, inside_v
值为0时移除边框值大于0时设置边框粗细
""" """
tc = cell._tc tc = cell._tc
tcPr = tc.get_or_add_tcPr() tcPr = tc.get_or_add_tcPr()
@@ -391,6 +439,10 @@ class DocxWriter:
if value is not None: if value is not None:
tag = 'w:{}'.format(key) tag = 'w:{}'.format(key)
border = OxmlElement(tag) border = OxmlElement(tag)
if value == 0:
# 移除边框
border.set(qn('w:val'), 'nil')
else:
border.set(qn('w:val'), 'single') border.set(qn('w:val'), 'single')
border.set(qn('w:sz'), str(int(value * 8))) border.set(qn('w:sz'), str(int(value * 8)))
border.set(qn('w:space'), '0') border.set(qn('w:space'), '0')
@@ -490,6 +542,15 @@ class DocxWriter:
run = paragraph.add_run() run = paragraph.add_run()
run.add_picture(image_stream, width=width, height=height) 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): def is_chinese(self, char):
"""判断字符是否为中文""" """判断字符是否为中文"""
if '\u4e00' <= char <= '\u9fff': 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 # 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 # it under the terms of the GNU General Public License as published by
@@ -15,6 +15,7 @@
import datetime import datetime
import traceback import traceback
import io
from typing import Optional, Callable from typing import Optional, Callable
import openpyxl 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.workbook.workbook import Workbook
from openpyxl.worksheet.worksheet import Worksheet from openpyxl.worksheet.worksheet import Worksheet
from packaging import version from packaging import version
from PIL import Image
from module import LOGLEVEL, COMPATIBLE_VERSION from module import LOGLEVEL, COMPATIBLE_VERSION
from module.schema import Performance from module.schema import Performance
@@ -48,6 +50,35 @@ class ExcelReader:
question_data: dict[str, list[tuple[str, int]]] question_data: dict[str, list[tuple[str, int]]]
ignore_version_check: bool ignore_version_check: bool
pic_list: list 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, def __init__(self, file_path: str, version_check: bool = False,
signal: Callable[[str, str], None] = lambda x, y: print(x)): signal: Callable[[str, str], None] = lambda x, y: print(x)):
@@ -73,6 +104,9 @@ class ExcelReader:
self.ignore_version_check = version_check self.ignore_version_check = version_check
self.pic_list = [] self.pic_list = []
self.signal = signal self.signal = signal
self.suggestion_template_list = []
self.major_director_signature_image = None
self.course_leader_signature_image = None
def parse_excel(self): def parse_excel(self):
try: try:
@@ -80,6 +114,7 @@ class ExcelReader:
sheet: Worksheet = wb['初始录入'] sheet: Worksheet = wb['初始录入']
# 读取版本号 # 读取版本号
e_version = sheet['V4'].value if sheet['V4'].value is not None else sheet['U4'].value e_version = sheet['V4'].value if sheet['V4'].value is not None else sheet['U4'].value
e_version = sheet['H1'].value if e_version is None else e_version
if e_version is None: if e_version is None:
e_version = "0" e_version = "0"
status, _ = check_version(e_version, COMPATIBLE_VERSION) status, _ = check_version(e_version, COMPATIBLE_VERSION)
@@ -101,6 +136,13 @@ class ExcelReader:
# 读取课程负责人 # 读取课程负责人
self.course_lead_teacher_name = sheet["D8"].value 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 max_class_size = 4
match CUR_VERSION: match CUR_VERSION:
@@ -225,9 +267,26 @@ class ExcelReader:
else: else:
self.question_data[key] = [values] self.question_data[key] = [values]
self.validate_data() # 读取建议模板
if CUR_VERSION >= version.parse("9.0"):
sheet = wb['初始录入']
for i in range(29, 34):
self.suggestion_template_list.append(sheet[f'I{i}'].value)
if len(self.suggestion_template_list) != 5:
for i in range(len(self.suggestion_template_list), 5):
self.suggestion_template_list.append(None)
if vd_lst := self.validate_data():
raise self.ValidError("\n\n".join(vd_lst))
self.gen_picture() self.gen_picture()
except self.ValidError as ve:
raise Exception(f"""
数据验证失败:\n\n{str(ve)}
""")
except Exception as e: except Exception as e:
error_message = traceback.format_exc() error_message = traceback.format_exc()
raise Exception(f""" raise Exception(f"""
@@ -238,13 +297,40 @@ class ExcelReader:
def set_file_path(self, file_path: str): def set_file_path(self, file_path: str):
self.file_path = file_path self.file_path = file_path
def validate_data(self): def validate_data(self) -> list[str]:
lst: list[str] = []
self.signal("正在验证数据", LOGLEVEL.INFO) 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): def run(self):
self.parse_excel() 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): def clear_all_data(self):
self.kpi_list = [] self.kpi_list = []
self.kpi_number = 0 self.kpi_number = 0
@@ -264,6 +350,8 @@ class ExcelReader:
self.hml_list = [] self.hml_list = []
self.question_data = {} self.question_data = {}
self.pic_list = [] self.pic_list = []
self.major_director_signature_image = None
self.course_leader_signature_image = None
def set_version_check(self, version_check: bool): def set_version_check(self, version_check: bool):
self.ignore_version_check = version_check self.ignore_version_check = version_check
@@ -471,8 +559,8 @@ class ExcelReader:
yield analysis_results yield analysis_results
yield "改进措施" yield "改进措施"
yield ("注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n" yield ("注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n"
"\n\n\n在这填入您的改进措施\n\n\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 "如果您看到了本段文字,请联系开发者" yield "如果您看到了本段文字,请联系开发者"
def get_word_template_part_2(self): def get_word_template_part_2(self):
@@ -561,20 +649,21 @@ class ExcelReader:
f"达成值为{self.achievement_level[i][min_p_rate_index].achievement}" f"达成值为{self.achievement_level[i][min_p_rate_index].achievement}"
f"{''.join(o_c_str)}的达成情况较好;") f"{''.join(o_c_str)}的达成情况较好;")
analysis_results = analysis_results[:-1] + "" analysis_results = analysis_results[:-1] + ""
analysis_results += "\n3.结果分析: \n在此填写您的结果分析\n\n" analysis_results += ("\n3.结果分析: \n"
f"{self.suggestion_template_list[2] if self.suggestion_template_list[2] is not None else '\n\n\n在此填写您的结果分析\n\n\n'}")
yield analysis_results yield analysis_results
yield "改进措施" yield "改进措施"
yield "注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n\n\n\n" yield ("注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n"
for i in range(88888): f"{self.suggestion_template_list[1] if self.suggestion_template_list[1] is not None else '\n\n\n在这填入您的改进措施\n\n\n'}")
while True:
yield "如果您看到了本段文字,请联系开发者" yield "如果您看到了本段文字,请联系开发者"
@staticmethod def get_word_template_part_3(self):
def get_word_template_part_3():
yield "课程目标达成情况合理性评价" yield "课程目标达成情况合理性评价"
yield "评价样本的合理性" yield "评价样本的合理性"
yield "R全体样本 £抽样样本" yield "R全体样本 £抽样样本"
yield "评价依据的合理性" yield "评价依据的合理性"
yield "考核方法 R合适 £不合适 " yield "考核方法 R合适 £不合适"
yield "考核内容是否支撑课程目标 R是 £否" yield "考核内容是否支撑课程目标 R是 £否"
yield "评分标准 R明确 £不明确" yield "评分标准 R明确 £不明确"
yield "计算过程的合理性" yield "计算过程的合理性"
@@ -587,17 +676,23 @@ class ExcelReader:
yield "R合理 £基本合理 £不合理" yield "R合理 £基本合理 £不合理"
yield "专业负责人/系主任(签字)" yield "专业负责人/系主任(签字)"
yield ("整改意见:\n" yield ("整改意见:\n"
"\n\n\n\n\n"
"\n\n\n" "\n\n\n"
"\n\t\t\t\t\t\t\t\t\t签字:\t\t\t日期:{}\n". f"{" " * 8}{self.suggestion_template_list[3] if self.suggestion_template_list[3] is not None else '\n\n\n'}\n\n\n")
format(datetime.datetime.now().strftime("%Y-%m-%d"))) yield ""
yield "签字:"
yield ""
yield "日期:"
yield datetime.datetime.now().strftime("%Y-%m-%d")
yield "课程负责人(签字)" yield "课程负责人(签字)"
yield ("拟整改计划与措施:\n" yield ("拟整改计划与措施:\n"
"\n\n\n\n\n"
"\n\n\n" "\n\n\n"
"\n\t\t\t\t\t\t\t\t\t签字:\t\t\t日期:{}\n". f"{" " * 8}{self.suggestion_template_list[4] if self.suggestion_template_list[4] is not None else '\n\n\n'}\n\n\n")
format(datetime.datetime.now().strftime("%Y-%m-%d"))) yield ""
for i in range(88888): yield "签字:"
yield ""
yield "日期:"
yield datetime.datetime.now().strftime("%Y-%m-%d")
while True:
yield "如果您看到了本段文字,请联系开发者" yield "如果您看到了本段文字,请联系开发者"

View File

@@ -17,6 +17,7 @@ import pathlib
from copy import deepcopy from copy import deepcopy
from docx import Document from docx import Document
from docx.enum.text import WD_BREAK
from docx.shared import Cm, Mm from docx.shared import Cm, Mm
from module.schema import Course, Student from module.schema import Course, Student
@@ -37,10 +38,11 @@ class DocPaper:
section.right_margin = Cm(2) section.right_margin = Cm(2)
def add_paper(self, course: Course, student: Student): def add_paper(self, course: Course, student: Student):
temp_table = self._template.tables[0] new_table = deepcopy(self._template.tables[0])
new_table = deepcopy(temp_table)
para = self._doc.add_paragraph() para = self._doc.add_paragraph()
para._p.addprevious(new_table._element) para._p.addprevious(new_table._element)
para.add_run().add_break(WD_BREAK.PAGE)
data_list = { data_list = {
'%CNAME%': course.name, '%CNAME%': course.name,
@@ -48,9 +50,7 @@ class DocPaper:
'%SNAME%': student.name, '%SNAME%': student.name,
'%NO%': student.no, '%NO%': student.no,
'%SO%': student.so, '%SO%': student.so,
'%Q1%': student.picked_questions[0].topic, '%Q%': '\n'.join([f'\t{idx + 1}{i.topic}' for idx, i in enumerate(student.picked_questions)])
'%Q2%': student.picked_questions[1].topic,
'%Q3%': student.picked_questions[2].topic
} }
# 替换表格中的占位符 # 替换表格中的占位符

View File

@@ -1,3 +1,4 @@
pytest
openpyxl~=3.1.5 openpyxl~=3.1.5
pyside6~=6.9.0 pyside6~=6.9.0
python-docx~=1.1.2 python-docx~=1.1.2

View File

@@ -0,0 +1,109 @@
{
"name": "achievement",
"version": "9.0",
"compatibleVersion": [
"9.0"
],
"config": [
{
"name": "version",
"position": "H1",
"type": "single"
},
{
"name": "class_full_name",
"position": "D10",
"type": "single"
},
{
"name": "course_name",
"position": "D5",
"type": "single"
},
{
"name": "teacher_name",
"position": "D7",
"type": "single"
},
{
"name": "master_name",
"position": "D8",
"type": "single"
},
{
"name": "class_single_name",
"position": "K",
"type": "range",
"start": 2,
"end": 5
},
{
"name": "class_single_number",
"position": "M",
"type": "range",
"start": 2,
"end": 5
},
{
"name": "kpi_number",
"position": "H8",
"type": "single"
},
{
"name": "hml",
"position": "H",
"type": "range",
"start": 22,
"end": null
},
{
"name": "hml_goal",
"position": "I",
"type": "range",
"start": 22,
"end": null
},
{
"name": "hml_indicate",
"position": "Q",
"type": "range",
"start": 22,
"end": null
},
{
}
]
}

View File

112
toolbox/models/config.py Normal file
View File

@@ -0,0 +1,112 @@
# Copyright (c) 2025 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# #
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# #
# 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 json
from abc import ABC, abstractmethod
from os import PathLike
from typing import Optional
class BaseExcelConfig(ABC):
@property
@abstractmethod
def name(self) -> str:
...
@property
@abstractmethod
def position(self) -> str:
...
class RangeExcelConfigMixin(ABC):
@property
@abstractmethod
def start(self) -> int:
...
@property
@abstractmethod
def end(self) -> Optional[int]:
...
@property
@abstractmethod
def fposition(self) -> str:
...
class SingleExcelConfigItem(BaseExcelConfig):
def __init__(self, name: str, position: str):
self._name = name
self._position = position
@property
def name(self) -> str:
return self._name
@property
def position(self) -> str:
return self._position
class RangeExcelConfigItem(SingleExcelConfigItem, RangeExcelConfigMixin):
def __init__(self, name: str, position: str, start: int, end: Optional[int] = None):
super().__init__(name, position)
self._start = start
self._end = end
@property
def start(self) -> int:
return self._start
@property
def end(self) -> Optional[int]:
return self._end
@property
def fposition(self) -> str:
return self.position + '{}'
class AESConfig:
_config_list: list[SingleExcelConfigItem | RangeExcelConfigItem]
def __init__(self, file_path: str | PathLike):
self._file_path = file_path
self._config_list = []
self._init_config()
def __getitem__(self, item: str):
return self.get_config(item)
def _init_config(self):
with open(self._file_path, 'r', encoding='utf-8') as f:
config = json.load(f)
f.close()
for item in config['config']:
itype = item.pop('type', None)
if itype == 'range':
self._config_list.append(RangeExcelConfigItem(**item))
elif itype == 'single':
self._config_list.append(SingleExcelConfigItem(**item))
def get_config(self, name: str) -> Optional[RangeExcelConfigItem | SingleExcelConfigItem]:
for config in self._config_list:
if config.name == name:
return config
return None

View File

@@ -0,0 +1,32 @@
# Copyright (c) 2025 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# #
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from dataclasses import dataclass
@dataclass
class ClassInfo:
full_name = ""
class_name: str
class_number: int
@dataclass
class CourseInfo:
course_name: str
# 任课教师
course_teacher_name: str
# 课程负责人
course_master_name: str

View File

@@ -0,0 +1,119 @@
# Copyright (c) 2025 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# #
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# #
# 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 json
from abc import ABC, abstractmethod
from os import PathLike
from typing import Optional
import openpyxl
from openpyxl.workbook import Workbook
from openpyxl.worksheet.worksheet import Worksheet
from toolbox.models.data_model import ClassInfo
from toolbox.models.config import AESConfig
class BaseExcelService(ABC):
_file_path: str | PathLike
_workbook: Optional[Workbook]
_sheet: None
@abstractmethod
def open(self, *args, **kwargs) -> 'BaseExcelService':
...
@abstractmethod
def save(self) -> 'BaseExcelService':
...
@abstractmethod
def close(self) -> None:
...
@abstractmethod
def active_sheet(self, sheet_name: str) -> 'BaseExcelService':
...
@property
@abstractmethod
def cur_active_sheet(self) -> Worksheet:
...
class ExcelService(BaseExcelService):
def __init__(self, file_path: str | PathLike):
self._file_path = file_path
self._workbook = None
self._sheet = None
def open(self, *args, **kwargs):
self._workbook = openpyxl.load_workbook(self._file_path, *args, **kwargs)
return self
def save(self):
self._workbook.save(self._file_path)
return self
def close(self):
self._workbook.close()
def active_sheet(self, sheet_name: str):
self._sheet = self._workbook[sheet_name]
return self
@property
def cur_active_sheet(self):
return self._sheet
def load_value(self, cell: str):
if self._sheet is None:
raise ValueError("No active sheet. Please set an active sheet first.")
return self._sheet[cell].value
class AchievementExcelService(ExcelService):
version = ''
config = {}
def __init__(self, file_path: str | PathLike):
super().__init__(file_path)
self.open(read_only=True, data_only=True)
def load_config(self, config_path: str | PathLike):
self.config = AESConfig(config_path)
def read_class_info(self) -> list[ClassInfo]:
lst = []
self.active_sheet('初始录入')
full_name = self.load_value(self.config['class_full_name'].position)
for i in range(self.config['class_single_name'].start, self.config['class_single_name'].end):
name = self.load_value(self.config['class_single_name'].fposition.format(i))
number = self.load_value(self.config['class_single_number'].fposition.format(i))
if name is None or number is None:
break
ci = ClassInfo(name, number)
ci.full_name = full_name
lst.append(ci)
if len(lst) == 0:
raise ValueError("No class information found in the Excel file.")
return lst
def read_course_info(self):
...

19
toolbox/tests/__init__.py Normal file
View File

@@ -0,0 +1,19 @@
# Copyright (c) 2025 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# #
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# #
# 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 pathlib import Path
PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__))
TEST_FILE_PATH = Path(os.path.join(PACKAGE_DIR, 'files'))

View File

@@ -0,0 +1,41 @@
# Copyright (c) 2025 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# #
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from toolbox.models.config import AESConfig, SingleExcelConfigItem, RangeExcelConfigItem
from toolbox.tests import TEST_FILE_PATH
def test_config_model():
aesc = AESConfig(TEST_FILE_PATH / 'test_config_model_01.json')
a = aesc.get_config('A')
assert isinstance(a, SingleExcelConfigItem)
assert a.position == 'H1'
b = aesc.get_config('B')
assert isinstance(b, SingleExcelConfigItem)
assert b.position == 'D10'
c = aesc.get_config('C')
assert isinstance(c, RangeExcelConfigItem)
assert c.position == 'K'
assert c.fposition.format(1) == 'K1'
assert c.start == 2
assert c.end == 5
d = aesc.get_config('D')
assert isinstance(d, RangeExcelConfigItem)
assert d.position == 'Q'
assert d.fposition.format(d.start) == 'Q22'
assert d.end is None

View File

@@ -0,0 +1,79 @@
# Copyright (c) 2025 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# #
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# #
# 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 shutil
from pathlib import Path
import pytest
from openpyxl.workbook import Workbook
from toolbox.services.excel_service import ExcelService, AchievementExcelService
from toolbox.tests import TEST_FILE_PATH
SAVE_TEMP_FILE = False
class TestExcelService:
es = ExcelService(TEST_FILE_PATH / 'test_excel_services_01.xlsx')
es.open(data_only=True)
def test_open(self):
assert isinstance(self.es._workbook, Workbook)
assert self.es._sheet is None
def test_open_failed(self):
with pytest.raises(FileNotFoundError):
ExcelService(TEST_FILE_PATH / 'non_existent_file.xlsx').open()
def test_active_sheet(self):
self.es.active_sheet('Sheet1')
assert self.es._sheet.title == 'Sheet1'
self.es.active_sheet('Sheet2')
assert self.es._sheet.title == 'Sheet2'
def test_cur_active_sheet(self):
self.es.active_sheet('Sheet1')
assert self.es.cur_active_sheet.title == 'Sheet1'
self.es.active_sheet('Sheet2')
assert self.es.cur_active_sheet.title == 'Sheet2'
def test_save_and_close(self):
temp_excel_file = TEST_FILE_PATH / 'test_excel_services_01_temp.xlsx'
shutil.copy(self.es._file_path, temp_excel_file)
self.es.active_sheet('Sheet1').cur_active_sheet['A1'] = 'Modified'
self.es.save().close()
es2 = ExcelService(temp_excel_file).open().active_sheet('Sheet1')
assert es2.cur_active_sheet['A1'].value == 'Modified'
es2.close()
if not SAVE_TEMP_FILE:
Path(TEST_FILE_PATH / 'test_excel_services_01_temp.xlsx').unlink()
class TestAchievementExcelService:
aes = AchievementExcelService(TEST_FILE_PATH / 'test_achievement_excel_service_01.xlsm')
aes.load_config(TEST_FILE_PATH / 'test_achievement.default.excel_01.json')
def test_read_class_info(self):
cis = self.aes.read_class_info()
assert len(cis) == 2
assert cis[0].full_name == '22工程管理12'
assert cis[1].full_name == '22工程管理12'
assert cis[0].class_name == '22工程管理1'
assert cis[0].class_number == 34
assert cis[1].class_name == '22工程管理2'
assert cis[1].class_number == 36

View File

@@ -109,3 +109,12 @@ class MyGroupHeaderCardWidget(GroupHeaderCardWidget):
self.groupLayout.addWidget(group) self.groupLayout.addWidget(group)
self.groupWidgets.append(group) self.groupWidgets.append(group)
return group return group
class NotImplementedWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.layout = QVBoxLayout(self)
self.label = DisplayLabel("🚧", self)
self.label.setAlignment(Qt.AlignCenter)
self.layout.addWidget(self.label)

View File

@@ -13,6 +13,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # 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 PySide6.QtGui import QIcon, QShowEvent
from qfluentwidgets import FluentIcon, MSFluentWindow, NavigationItemPosition, MessageBox, setThemeColor from qfluentwidgets import FluentIcon, MSFluentWindow, NavigationItemPosition, MessageBox, setThemeColor
@@ -25,35 +29,94 @@ from ui.pyui.test_ui import TestWidget
from utils.function import RELEASE_ENV 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): class MainWindow(MSFluentWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
setThemeColor(MAIN_THEME_COLOR) setThemeColor(MAIN_THEME_COLOR)
self.setCustomBackgroundColor(BLUE_BACKGROUND_COLOR, BLUE_BACKGROUND_COLOR) self.setCustomBackgroundColor(BLUE_BACKGROUND_COLOR, BLUE_BACKGROUND_COLOR)
self.interface_specs = self.build_interface_specs()
self.achievementInterface = AchievementWidget('Achievement Interface', self) self.interfaces = self.create_interfaces(self.interface_specs)
self.defenseInterface = DefenseWidget('Defense Interface', self) self.bind_error_handlers()
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.initNavigation() self.initNavigation()
self.initWindow() self.initWindow()
def initNavigation(self): def build_interface_specs(self) -> list[InterfaceSpec]:
self.addSubInterface(self.achievementInterface, FluentIcon.SPEED_HIGH, '达成度') return [
self.addSubInterface(self.defenseInterface, FluentIcon.FEEDBACK, '答辩题目') InterfaceSpec(
self.addSubInterface(self.pickerInterface, FluentIcon.PEOPLE, '提问') key="achievement",
if not RELEASE_ENV: factory=lambda: AchievementWidget('Achievement Interface', self),
self.addSubInterface(self.testInterface, FluentIcon.VIEW, '测试') 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): def initWindow(self):
self.resize(900, 700) 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 # 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 # 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.QtCore import Qt, Signal, QThread
from PySide6.QtWidgets import QVBoxLayout, QFileDialog, QHBoxLayout from PySide6.QtWidgets import QVBoxLayout, QFileDialog, QHBoxLayout
from qfluentwidgets import GroupHeaderCardWidget, FluentIcon, PushButton, LineEdit, IconWidget, BodyLabel, \ from qfluentwidgets import GroupHeaderCardWidget, FluentIcon, PushButton, LineEdit, IconWidget, BodyLabel, \
PrimaryPushButton, SwitchButton, HyperlinkButton PrimaryPushButton, SwitchButton, HyperlinkButton, InfoBar, InfoBarPosition
from module import LOGLEVEL from module import LOGLEVEL
from module.worker import ARGWorker from module.worker import ARGWorker
@@ -40,7 +40,7 @@ class InputSettingCard(MyGroupHeaderCardWidget):
self.setBorderRadius(8) self.setBorderRadius(8)
self.btnHBoxLayout = QHBoxLayout(self) self.btnHBoxLayout = QHBoxLayout(self)
self.openTemplateButton = HyperlinkButton("", "下载模板") self.openTemplateButton = HyperlinkButton("", "模板下载")
self.chooseFileButton = PushButton("打开") self.chooseFileButton = PushButton("打开")
self.chooseFileButton.setFixedWidth(120) self.chooseFileButton.setFixedWidth(120)
@@ -259,3 +259,23 @@ class AchievementWidget(Widget):
def show_info(self, content: str, level: str): def show_info(self, content: str, level: str):
if level == LOGLEVEL.INFO: if level == LOGLEVEL.INFO:
self.pib.set_title(content) 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

@@ -25,7 +25,7 @@ from qfluentwidgets import GroupHeaderCardWidget, PushButton, IconWidget, BodyLa
from module.worker import DTGWorker from module.worker import DTGWorker
from ui import MAIN_THEME_COLOR from ui import MAIN_THEME_COLOR
from ui.components.infobar import ProgressInfoBar from ui.components.infobar import ProgressInfoBar
from ui.components.widget import Widget, MyGroupHeaderCardWidget from ui.components.widget import Widget, MyGroupHeaderCardWidget, NotImplementedWidget
from ui.pyui.sub.defense import ODModeExportSettings, ODModeSettings from ui.pyui.sub.defense import ODModeExportSettings, ODModeSettings
from utils.function import open_template from utils.function import open_template
@@ -65,9 +65,9 @@ class InitSettingCard(MyGroupHeaderCardWidget):
self.sBtnHBoxLayout = QHBoxLayout(self) self.sBtnHBoxLayout = QHBoxLayout(self)
self.qBtnHBoxLayout = QHBoxLayout(self) self.qBtnHBoxLayout = QHBoxLayout(self)
self.sTemplateButton = HyperlinkButton("", "下载模板") self.sTemplateButton = HyperlinkButton("", "模板下载")
self.chooseStudentButton = PushButton("打开") self.chooseStudentButton = PushButton("打开")
self.qTemplateButton = HyperlinkButton("", "下载模板") self.qTemplateButton = HyperlinkButton("", "模板下载")
self.chooseQuestionButton = PushButton("打开") self.chooseQuestionButton = PushButton("打开")
self.chooseStudentButton.setFixedWidth(120) self.chooseStudentButton.setFixedWidth(120)
@@ -136,7 +136,7 @@ class ExportSettingsCard(GroupHeaderCardWidget):
self.radioGroup.addButton(self.wordRadio) self.radioGroup.addButton(self.wordRadio)
self.radioHbox.addWidget(self.pdfRadio) self.radioHbox.addWidget(self.pdfRadio)
self.radioHbox.addWidget(self.wordRadio) self.radioHbox.addWidget(self.wordRadio)
self.pdfRadio.setChecked(True) self.wordRadio.setChecked(True)
self.hintIcon = IconWidget(FluentIcon.INFO.icon(color=MAIN_THEME_COLOR)) self.hintIcon = IconWidget(FluentIcon.INFO.icon(color=MAIN_THEME_COLOR))
self.hintLabel = BodyLabel("点击开始按钮以开始生成 👉") self.hintLabel = BodyLabel("点击开始按钮以开始生成 👉")
self.chooseExportDirectoryButton.setFixedWidth(120) self.chooseExportDirectoryButton.setFixedWidth(120)
@@ -345,7 +345,7 @@ class DefenseWidget(Widget):
self.stack = QStackedWidget(self) self.stack = QStackedWidget(self)
self.menu = SegmentedWidget(self) self.menu = SegmentedWidget(self)
self.dpMode = DPMode(self) self.dpMode = DPMode(self)
self.doMode = DOMode(self) self.doMode = NotImplementedWidget(self)
self.addSubInterface(self.dpMode, 'DPMode', '书面答辩') self.addSubInterface(self.dpMode, 'DPMode', '书面答辩')
self.addSubInterface(self.doMode, 'DOMode', '口头答辩') self.addSubInterface(self.doMode, 'DOMode', '口头答辩')

View File

@@ -36,7 +36,7 @@ class PickStudentMode(QWidget):
self.vbox.setContentsMargins(0, 0, 0, 0) self.vbox.setContentsMargins(0, 0, 0, 0)
self.btnHBox = QHBoxLayout(self) self.btnHBox = QHBoxLayout(self)
self.openTemplateBtn = HyperlinkButton("", "下载模板") self.openTemplateBtn = HyperlinkButton("", "模板下载")
self.chooseBtn = PushButton("打开") self.chooseBtn = PushButton("打开")
self.startButton = PrimaryPushButton(FluentIcon.PLAY_SOLID, "开始") self.startButton = PrimaryPushButton(FluentIcon.PLAY_SOLID, "开始")
self.bottomLayout = QHBoxLayout() self.bottomLayout = QHBoxLayout()

View File

@@ -25,10 +25,10 @@ class ODModeSettings(MyGroupHeaderCardWidget):
self.setTitle('输入选项') self.setTitle('输入选项')
self.sBtn = PushButton('打开', self) self.sBtn = PushButton('打开', self)
self.sBtnTemplate = HyperlinkButton('', '下载模板') self.sBtnTemplate = HyperlinkButton('', '模板下载')
self.qBtn = PushButton('打开', self) self.qBtn = PushButton('打开', self)
self.bBtnTemplate = HyperlinkButton('', '下载模板') self.bBtnTemplate = HyperlinkButton('', '模板下载')
self.qNumber = SpinBox(self) self.qNumber = SpinBox(self)
self.qNumber.setRange(0, 999) self.qNumber.setRange(0, 999)

View File

@@ -18,11 +18,23 @@ from datetime import datetime
def gen_build_info(): 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: with open('build_info.py', 'w', encoding='utf-8') as f:
f.write(f"# Auto-generated build info\n") f.write(f"# Auto-generated build info\n")
f.write(f"BUILD_TIME = '{datetime.now().isoformat(sep=' ', timespec='seconds')}'\n") f.write(f"BUILD_TIME = '{datetime.now().isoformat(sep=' ', timespec='seconds')}'\n")
f.write(f"GIT_HASH = '{hash_str}'\n") f.write(f"GIT_HASH = '{hash_str}'\n")
gen_build_info() if __name__ == '__main__':
gen_build_info()