Compare commits

..

5 Commits

6 changed files with 169 additions and 48 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 -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple -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,10 +155,15 @@ 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:
cell_start = table.cell(row, col_span) try:
cell_end = table.cell(row, col_span + non_none_count - 1) cell_start = table.cell(row, col_span)
cell_start.merge(cell_end) cell_end = table.cell(row, col_span + non_none_count - 1)
col_span += non_none_count 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 start = rows - X + 3 + self.excel_reader.kpi_number
if len(self.excel_reader.class_list) == 1: if len(self.excel_reader.class_list) == 1:
@@ -257,7 +262,7 @@ class DocxWriter:
table.cell(i, 4).merge(table.cell(i, 5)) table.cell(i, 4).merge(table.cell(i, 5))
table.cell(i, 1).width = Cm(7.42) table.cell(i, 1).width = Cm(7.42)
table.cell(i, 2).width = Cm(7.42) table.cell(i, 2).width = Cm(7.42)
table.cell(i, 4).width = Cm(7.41) table.cell(i, 4).width = Cm(7.41)
case 8 | 10: case 8 | 10:
table.cell(i - 1, 0).merge(table.cell(i, 0)) table.cell(i - 1, 0).merge(table.cell(i, 0))
table.cell(i, 1).width = Cm(11.23) table.cell(i, 1).width = Cm(11.23)
@@ -267,8 +272,8 @@ class DocxWriter:
case _: 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)
# 填充数据 # 填充数据
self.put_data_to_table(table, self.excel_reader.get_word_template_part_3) self.put_data_to_table(table, self.excel_reader.get_word_template_part_3)
# 应用样式 # 应用样式
@@ -370,7 +375,7 @@ class DocxWriter:
special_cell = [ special_cell = [
(1, 1), (1, 1),
(2, 1), (2, 1),
(3, 1), (3, 1),
(4, 1), (4, 1),
(5, 1), (5, 1),
(6, 1), (6, 1),

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
@@ -73,6 +73,13 @@ class ExcelReader:
image = io.BytesIO(self._images[cell]()) image = io.BytesIO(self._images[cell]())
return Image.open(image) 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)):
super().__init__() super().__init__()
@@ -271,9 +278,15 @@ class ExcelReader:
for i in range(len(self.suggestion_template_list), 5): for i in range(len(self.suggestion_template_list), 5):
self.suggestion_template_list.append(None) 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() 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"""
@@ -284,9 +297,18 @@ 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()
@@ -655,7 +677,7 @@ class ExcelReader:
yield "专业负责人/系主任(签字)" yield "专业负责人/系主任(签字)"
yield ("整改意见:\n" yield ("整改意见:\n"
"\n\n\n" "\n\n\n"
f"{" "*8}{self.suggestion_template_list[3] if self.suggestion_template_list[3] is not None else '\n\n\n'}\n\n\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 "" yield ""
@@ -664,7 +686,7 @@ class ExcelReader:
yield "课程负责人(签字)" yield "课程负责人(签字)"
yield ("拟整改计划与措施:\n" yield ("拟整改计划与措施:\n"
"\n\n\n" "\n\n\n"
f"{" "*8}{self.suggestion_template_list[4] if self.suggestion_template_list[4] is not None else '\n\n\n'}\n\n\n") 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 "" yield ""

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
@@ -20,40 +24,99 @@ from ui import MAIN_THEME_COLOR, BLUE_BACKGROUND_COLOR
from ui.pyui.about_ui import AboutWidget from ui.pyui.about_ui import AboutWidget
from ui.pyui.achievement_ui import AchievementWidget from ui.pyui.achievement_ui import AchievementWidget
from ui.pyui.defense_ui import DefenseWidget 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 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
@@ -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

@@ -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()