From f1cbfdbcee74c15f685d3d641bcf127187862d1c Mon Sep 17 00:00:00 2001 From: jimmy-sketch Date: Sat, 20 Dec 2025 17:29:10 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=20CSES=20=E8=AF=BE=E7=A8=8B=E8=A1=A8=E7=9A=84?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Language/modules/extraction_settings.py | 84 ++++ app/common/extraction/cses_parser.py | 252 ++++++++++++ app/common/extraction/extract.py | 152 ++++++++ app/page_building/settings_window_page.py | 1 + .../extraction_settings/time_settings.py | 360 ++++++++++++++++++ 5 files changed, 849 insertions(+) create mode 100644 app/common/extraction/cses_parser.py create mode 100644 app/view/settings/extraction_settings/time_settings.py diff --git a/app/Language/modules/extraction_settings.py b/app/Language/modules/extraction_settings.py index b1cfe54d..42c42179 100644 --- a/app/Language/modules/extraction_settings.py +++ b/app/Language/modules/extraction_settings.py @@ -201,6 +201,90 @@ }, } +# 时间设置语言配置 +time_settings = { + "ZH_CN": { + "title": {"name": "时间设置", "description": "设置课间禁用和课程表导入"}, + "class_break_settings": { + "name": "课间禁用设置", + "description": "设置课间禁用功能", + }, + "cses_import_settings": { + "name": "CSES课程表导入", + "description": "从CSES格式文件导入课程表", + }, + "class_break_function": { + "name": "课间禁用功能", + "description": "开启后,在下课时间段内抽取需要安全验证", + }, + "cses_import": { + "name": "课程表导入", + "description": "从CSES格式文件导入上课时间段,用于课间禁用功能", + }, + "import_from_file": {"name": "从文件导入"}, + "importing": {"name": "导入中..."}, + "view_template": {"name": "查看模板"}, + "no_schedule_imported": {"name": "未导入课程表"}, + "schedule_imported": {"name": "已导入 {} 个非上课时间段"}, + "copy_to_clipboard": {"name": "复制到剪贴板"}, + "save_as_file": {"name": "保存为文件"}, + "close": {"name": "关闭"}, + "copy_success": {"name": "复制成功"}, + "template_copied": {"name": "模板已复制到剪贴板"}, + "save_success": {"name": "保存成功"}, + "template_saved": {"name": "模板已保存到: {}"}, + "import_success": {"name": "导入成功"}, + "import_failed": {"name": "导入失败"}, + "import_error": {"name": "导入过程中发生错误: {}"}, + "template_title": {"name": "CSES课程表模板"}, + "select_cses_file": {"name": "选择CSES课程表文件"}, + "yaml_files": {"name": "YAML文件 (*.yaml *.yml)"}, + "all_files": {"name": "所有文件 (*.*)"}, + "save_template": {"name": "保存CSES模板"}, + "cses_template": {"name": "cses_template.yaml"}, + }, + "EN_US": { + "title": {"name": "Time Settings", "description": "Set class break restrictions and schedule import"}, + "class_break_settings": { + "name": "Class Break Settings", + "description": "Configure class break restrictions", + }, + "cses_import_settings": { + "name": "CSES Schedule Import", + "description": "Import schedule from CSES format files", + }, + "class_break_function": { + "name": "Class Break Restriction", + "description": "When enabled, drawing during break times requires safety verification", + }, + "cses_import": { + "name": "Schedule Import", + "description": "Import class time slots from CSES format files for break time functionality", + }, + "import_from_file": {"name": "Import from File"}, + "importing": {"name": "Importing..."}, + "view_template": {"name": "View Template"}, + "no_schedule_imported": {"name": "No schedule imported"}, + "schedule_imported": {"name": "Imported {} non-class time periods"}, + "copy_to_clipboard": {"name": "Copy to Clipboard"}, + "save_as_file": {"name": "Save as File"}, + "close": {"name": "Close"}, + "copy_success": {"name": "Copy Successful"}, + "template_copied": {"name": "Template copied to clipboard"}, + "save_success": {"name": "Save Successful"}, + "template_saved": {"name": "Template saved to: {}"}, + "import_success": {"name": "Import Successful"}, + "import_failed": {"name": "Import Failed"}, + "import_error": {"name": "Error during import: {}"}, + "template_title": {"name": "CSES Schedule Template"}, + "select_cses_file": {"name": "Select CSES Schedule File"}, + "yaml_files": {"name": "YAML files (*.yaml *.yml)"}, + "all_files": {"name": "All files (*.*)"}, + "save_template": {"name": "Save CSES Template"}, + "cses_template": {"name": "cses_template.yaml"}, + }, +} + # 闪抽设置 quick_draw_settings = { "ZH_CN": { diff --git a/app/common/extraction/cses_parser.py b/app/common/extraction/cses_parser.py new file mode 100644 index 00000000..e3bd385d --- /dev/null +++ b/app/common/extraction/cses_parser.py @@ -0,0 +1,252 @@ +# ================================================== +# CSES (Course Schedule Exchange Schema) 解析器 +# ================================================== +import yaml +import json +from datetime import datetime, time +from typing import Dict, List, Optional +from loguru import logger + + +class CSESParser: + """CSES格式课程表解析器""" + + def __init__(self): + self.schedule_data = None + + def load_from_file(self, file_path: str) -> bool: + """从文件加载CSES数据 + + Args: + file_path: CSES文件路径 + + Returns: + bool: 加载成功返回True,否则返回False + """ + try: + with open(file_path, 'r', encoding='utf-8') as f: + self.schedule_data = yaml.safe_load(f) + return self._validate_schedule() + except Exception as e: + logger.error(f"加载CSES文件失败: {e}") + return False + + def load_from_content(self, content: str) -> bool: + """从字符串内容加载CSES数据 + + Args: + content: YAML格式的CSES内容 + + Returns: + bool: 加载成功返回True,否则返回False + """ + try: + self.schedule_data = yaml.safe_load(content) + return self._validate_schedule() + except Exception as e: + logger.error(f"解析CSES内容失败: {e}") + return False + + def _validate_schedule(self) -> bool: + """验证课程表数据的有效性 + + Returns: + bool: 数据有效返回True,否则返回False + """ + if not self.schedule_data: + logger.error("课程表数据为空") + return False + + # 基本结构验证 + if 'schedule' not in self.schedule_data: + logger.error("缺少'schedule'字段") + return False + + schedule = self.schedule_data['schedule'] + if not isinstance(schedule, dict): + logger.error("'schedule'字段必须是字典类型") + return False + + # 验证时间段配置 + if 'timeslots' not in schedule: + logger.error("缺少'timeslots'字段") + return False + + timeslots = schedule['timeslots'] + if not isinstance(timeslots, list): + logger.error("'timeslots'字段必须是列表类型") + return False + + # 验证每个时间段 + for i, timeslot in enumerate(timeslots): + if not self._validate_timeslot(timeslot, i): + return False + + return True + + def _validate_timeslot(self, timeslot: dict, index: int) -> bool: + """验证单个时间段的配置 + + Args: + timeslot: 时间段配置字典 + index: 时间段索引 + + Returns: + bool: 有效返回True,否则返回False + """ + required_fields = ['name', 'start_time', 'end_time'] + for field in required_fields: + if field not in timeslot: + logger.error(f"时间段{index}缺少'{field}'字段") + return False + + # 验证时间格式 + try: + start_time = self._parse_time(timeslot['start_time']) + end_time = self._parse_time(timeslot['end_time']) + + if start_time >= end_time: + logger.error(f"时间段{index}的开始时间必须早于结束时间") + return False + + except ValueError as e: + logger.error(f"时间段{index}时间格式错误: {e}") + return False + + return True + + def _parse_time(self, time_str: str) -> time: + """解析时间字符串 + + Args: + time_str: 时间字符串 (HH:MM 或 HH:MM:SS) + + Returns: + time: 时间对象 + + Raises: + ValueError: 时间格式错误 + """ + try: + if ':' in time_str: + parts = time_str.split(':') + if len(parts) == 2: + return time(int(parts[0]), int(parts[1])) + elif len(parts) == 3: + return time(int(parts[0]), int(parts[1]), int(parts[2])) + raise ValueError(f"无效的时间格式: {time_str}") + except (ValueError, IndexError): + raise ValueError(f"无法解析时间: {time_str}") + + def get_non_class_times(self) -> Dict[str, str]: + """获取非上课时间段配置 + + 将CSES格式的时间段转换为SecRandom使用的非上课时间段格式 + + Returns: + Dict[str, str]: 非上课时间段字典,格式为 {"name": "HH:MM:SS-HH:MM:SS"} + """ + if not self.schedule_data: + return {} + + non_class_times = {} + schedule = self.schedule_data['schedule'] + timeslots = schedule['timeslots'] + + # 按开始时间排序 + sorted_timeslots = sorted(timeslots, key=lambda x: x['start_time']) + + # 构建上课时间段列表 + class_periods = [] + for timeslot in sorted_timeslots: + start_time = self._format_time_for_secrandom(timeslot['start_time']) + end_time = self._format_time_for_secrandom(timeslot['end_time']) + class_periods.append((start_time, end_time)) + + # 生成非上课时间段 + # 1. 第一节课之前的时间 + if class_periods: + first_start = class_periods[0][0] + if first_start != "00:00:00": + non_class_times["before_first_class"] = f"00:00:00-{first_start}" + + # 2. 课间时间(两节课之间) + for i in range(len(class_periods) - 1): + current_end = class_periods[i][1] + next_start = class_periods[i + 1][0] + if current_end != next_start: + period_name = f"break_{i+1}" + non_class_times[period_name] = f"{current_end}-{next_start}" + + # 3. 最后一节课之后的时间 + if class_periods: + last_end = class_periods[-1][1] + if last_end != "23:59:59": + non_class_times["after_last_class"] = f"{last_end}-23:59:59" + + logger.info(f"成功解析CSES课程表,生成{len(non_class_times)}个非上课时间段") + return non_class_times + + def _format_time_for_secrandom(self, time_str: str) -> str: + """将时间字符串格式化为SecRandom需要的格式 (HH:MM:SS) + + Args: + time_str: 原始时间字符串 (HH:MM 或 HH:MM:SS) + + Returns: + str: 格式化后的时间字符串 (HH:MM:SS) + """ + if time_str.count(':') == 1: # HH:MM 格式 + return f"{time_str}:00" + return time_str + + def get_class_info(self) -> List[Dict]: + """获取课程信息列表 + + Returns: + List[Dict]: 课程信息列表 + """ + if not self.schedule_data: + return [] + + schedule = self.schedule_data['schedule'] + timeslots = schedule.get('timeslots', []) + + class_info = [] + for timeslot in timeslots: + info = { + 'name': timeslot.get('name', ''), + 'start_time': timeslot.get('start_time', ''), + 'end_time': timeslot.get('end_time', ''), + 'teacher': timeslot.get('teacher', ''), + 'location': timeslot.get('location', ''), + 'day_of_week': timeslot.get('day_of_week', ''), + } + class_info.append(info) + + return class_info + + def get_summary(self) -> str: + """获取课程表摘要信息 + + Returns: + str: 摘要信息 + """ + if not self.schedule_data: + return "未加载课程表" + + schedule = self.schedule_data['schedule'] + timeslots = schedule.get('timeslots', []) + + if not timeslots: + return "课程表为空" + + # 获取最早和最晚时间 + start_times = [slot['start_time'] for slot in timeslots] + end_times = [slot['end_time'] for slot in timeslots] + + summary = f"课程表包含{len(timeslots)}个时间段," + summary += f"最早开始时间:{min(start_times)}," + summary += f"最晚结束时间:{max(end_times)}" + + return summary \ No newline at end of file diff --git a/app/common/extraction/extract.py b/app/common/extraction/extract.py index b54b6a7d..187c7805 100644 --- a/app/common/extraction/extract.py +++ b/app/common/extraction/extract.py @@ -13,6 +13,7 @@ from PySide6.QtCore import QDateTime from app.tools.path_utils import * +from app.common.extraction.cses_parser import CSESParser # ================================================== @@ -164,3 +165,154 @@ def _parse_time_string_to_seconds(time_str: str) -> int: seconds = time_parts[2] if len(time_parts) > 2 else 0 return hours * 3600 + minutes * 60 + seconds + + +# ================================================== +# CSES导入功能 +# ================================================== +def import_cses_schedule(file_path: str) -> tuple[bool, str]: + """从CSES文件导入课程表 + + Args: + file_path: CSES文件路径 + + Returns: + tuple[bool, str]: (是否成功, 结果消息) + """ + try: + # 创建CSES解析器 + parser = CSESParser() + + # 加载CSES文件 + if not parser.load_from_file(file_path): + return False, "CSES文件格式错误或文件无法读取" + + # 获取非上课时间段配置 + non_class_times = parser.get_non_class_times() + if not non_class_times: + return False, "未能从课程表中提取有效的时间段信息" + + # 保存到设置文件 + success = _save_non_class_times_to_settings(non_class_times) + if not success: + return False, "保存设置失败" + + # 获取摘要信息 + summary = parser.get_summary() + return True, f"成功导入课程表: {summary}" + + except Exception as e: + logger.error(f"导入CSES文件失败: {e}") + return False, f"导入失败: {str(e)}" + + +def import_cses_schedule_from_content(content: str) -> tuple[bool, str]: + """从CSES内容字符串导入课程表 + + Args: + content: CSES格式的YAML内容 + + Returns: + tuple[bool, str]: (是否成功, 结果消息) + """ + try: + # 创建CSES解析器 + parser = CSESParser() + + # 加载CSES内容 + if not parser.load_from_content(content): + return False, "CSES内容格式错误" + + # 获取非上课时间段配置 + non_class_times = parser.get_non_class_times() + if not non_class_times: + return False, "未能从课程表中提取有效的时间段信息" + + # 保存到设置文件 + success = _save_non_class_times_to_settings(non_class_times) + if not success: + return False, "保存设置失败" + + # 获取摘要信息 + summary = parser.get_summary() + return True, f"成功导入课程表: {summary}" + + except Exception as e: + logger.error(f"导入CSES内容失败: {e}") + return False, f"导入失败: {str(e)}" + + +def _save_non_class_times_to_settings(non_class_times: Dict[str, str]) -> bool: + """保存非上课时间段到设置文件 + + Args: + non_class_times: 非上课时间段字典 + + Returns: + bool: 保存成功返回True,否则返回False + """ + try: + settings_path = get_settings_path() + + # 读取现有设置 + if file_exists(settings_path): + with open_file(settings_path, "r", encoding="utf-8") as f: + settings = json.load(f) + else: + settings = {} + + # 更新非上课时间段配置 + settings["non_class_times"] = non_class_times + + # 写入设置文件 + with open_file(settings_path, "w", encoding="utf-8") as f: + json.dump(settings, f, ensure_ascii=False, indent=2) + + logger.info(f"成功保存{len(non_class_times)}个非上课时间段到设置文件") + return True + + except Exception as e: + logger.error(f"保存非上课时间段失败: {e}") + return False + + +def get_cses_import_template() -> str: + """获取CSES导入模板内容 + + Returns: + str: CSES格式的模板内容 + """ + template = """# CSES (Course Schedule Exchange Schema) 课程表模板 +# 更多详情请参考: https://github.com/SmartTeachCN/CSES + +schedule: + timeslots: + - name: "第一节课" + start_time: "08:00" + end_time: "08:45" + teacher: "张老师" + location: "教室A" + day_of_week: 1 + + - name: "第二节课" + start_time: "08:55" + end_time: "09:40" + teacher: "李老师" + location: "教室B" + day_of_week: 1 + + - name: "第三节课" + start_time: "10:00" + end_time: "10:45" + teacher: "王老师" + location: "教室C" + day_of_week: 1 + + - name: "第四节课" + start_time: "10:55" + end_time: "11:40" + teacher: "赵老师" + location: "教室D" + day_of_week: 1 +""" + return template diff --git a/app/page_building/settings_window_page.py b/app/page_building/settings_window_page.py index 5138d77b..dcb6dca6 100644 --- a/app/page_building/settings_window_page.py +++ b/app/page_building/settings_window_page.py @@ -53,6 +53,7 @@ def __init__(self, parent: QFrame = None): "instant_draw_settings", "title" ), "lottery_settings": get_content_name_async("lottery_settings", "title"), + "time_settings": get_content_name_async("time_settings", "title"), } super().__init__(page_config, parent) self.set_base_path("app.view.settings.extraction_settings") diff --git a/app/view/settings/extraction_settings/time_settings.py b/app/view/settings/extraction_settings/time_settings.py new file mode 100644 index 00000000..1666ec14 --- /dev/null +++ b/app/view/settings/extraction_settings/time_settings.py @@ -0,0 +1,360 @@ +# ================================================== +# 导入库 +# ================================================== +import os +import json +from datetime import datetime + +from loguru import logger +from PySide6.QtWidgets import * +from PySide6.QtGui import * +from PySide6.QtCore import * +from PySide6.QtNetwork import * +from qfluentwidgets import * + +from app.tools.variable import * +from app.tools.path_utils import * +from app.tools.personalised import * +from app.tools.settings_default import * +from app.tools.settings_access import * +from app.tools.settings_access import get_safe_font_size +from app.Language.obtain_language import * +from app.common.extraction.extract import ( + import_cses_schedule, + import_cses_schedule_from_content, + get_cses_import_template +) + + +# ================================================== +# 时间设置 +# ================================================== +class time_settings(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + # 创建垂直布局 + self.vBoxLayout = QVBoxLayout(self) + self.vBoxLayout.setContentsMargins(0, 0, 0, 0) + self.vBoxLayout.setSpacing(10) + + # 添加课间禁用设置组件 + self.class_break_widget = class_break_settings(self) + self.vBoxLayout.addWidget(self.class_break_widget) + + # 添加CSES导入组件 + self.cses_import_widget = cses_import_settings(self) + self.vBoxLayout.addWidget(self.cses_import_widget) + + +class class_break_settings(GroupHeaderCardWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setTitle(get_content_name_async("time_settings", "class_break_settings", "name")) + self.setBorderRadius(8) + + # 课间禁用开关 + self.class_break_switch = SwitchButton() + self.class_break_switch.setOffText(get_content_name_async("time_settings", "disable")) + self.class_break_switch.setOnText(get_content_name_async("time_settings", "enable")) + + # 从设置中读取当前状态 + current_enabled = self._get_class_break_enabled() + self.class_break_switch.setChecked(current_enabled) + + self.class_break_switch.checkedChanged.connect(self.on_class_break_changed) + + # 添加设置项到分组 + self.addGroup( + get_theme_icon("ic_fluent_clock_lock_20_filled"), + get_content_name_async("time_settings", "class_break_function", "name"), + get_content_name_async("time_settings", "class_break_function", "description"), + self.class_break_switch, + ) + + def _get_class_break_enabled(self) -> bool: + """获取课间禁用功能是否启用""" + try: + settings_path = get_settings_path() + if not file_exists(settings_path): + return False + + with open_file(settings_path, "r", encoding="utf-8") as f: + settings = json.load(f) + + program_functionality = settings.get("program_functionality", {}) + return program_functionality.get("instant_draw_disable", False) + except Exception as e: + logger.error(f"读取课间禁用设置失败: {e}") + return False + + def on_class_break_changed(self, is_checked: bool): + """当课间禁用开关状态改变时的处理""" + try: + settings_path = get_settings_path() + + # 读取现有设置 + if file_exists(settings_path): + with open_file(settings_path, "r", encoding="utf-8") as f: + settings = json.load(f) + else: + settings = {} + + # 更新程序功能设置 + if "program_functionality" not in settings: + settings["program_functionality"] = {} + + settings["program_functionality"]["instant_draw_disable"] = is_checked + + # 写入设置文件 + with open_file(settings_path, "w", encoding="utf-8") as f: + json.dump(settings, f, ensure_ascii=False, indent=2) + + logger.info(f"课间禁用功能已{'开启' if is_checked else '关闭'}") + + except Exception as e: + logger.error(f"保存课间禁用设置失败: {e}") + # 恢复开关状态 + self.class_break_switch.setChecked(not is_checked) + + +class cses_import_settings(GroupHeaderCardWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setTitle(get_content_name_async("time_settings", "cses_import_settings", "name")) + self.setBorderRadius(8) + + # 导入文件按钮 + self.import_file_button = PushButton(get_content_name_async("time_settings", "import_from_file")) + self.import_file_button.setIcon(get_theme_icon("ic_fluent_folder_open_20_filled")) + self.import_file_button.clicked.connect(self.on_import_file_clicked) + + # 查看模板按钮 + self.view_template_button = PushButton(get_content_name_async("time_settings", "view_template")) + self.view_template_button.setIcon(get_theme_icon("ic_fluent_document_20_filled")) + self.view_template_button.clicked.connect(self.on_view_template_clicked) + + # 当前课程表信息标签 + self.schedule_info_label = QLabel(get_content_name_async("time_settings", "no_schedule_imported")) + self._update_schedule_info() + + # 创建按钮布局 + button_layout = QHBoxLayout() + button_layout.addWidget(self.import_file_button) + button_layout.addWidget(self.view_template_button) + button_layout.addStretch() + + # 创建信息布局 + info_layout = QVBoxLayout() + info_layout.addLayout(button_layout) + info_layout.addWidget(self.schedule_info_label) + + # 创建容器控件来包含布局 + info_widget = QWidget() + info_widget.setLayout(info_layout) + + # 添加设置项到分组 + self.addGroup( + get_theme_icon("ic_fluent_calendar_ltr_20_filled"), + get_content_name_async("time_settings", "cses_import", "name"), + get_content_name_async("time_settings", "cses_import", "description"), + info_widget, + ) + + def _update_schedule_info(self): + """更新课程表信息显示""" + try: + settings_path = get_settings_path() + if not file_exists(settings_path): + self.schedule_info_label.setText(get_content_name_async("time_settings", "no_schedule_imported")) + return + + with open_file(settings_path, "r", encoding="utf-8") as f: + settings = json.load(f) + + non_class_times = settings.get("non_class_times", {}) + if non_class_times: + count = len(non_class_times) + self.schedule_info_label.setText( + get_content_name_async("time_settings", "schedule_imported").format(count) + ) + else: + self.schedule_info_label.setText(get_content_name_async("time_settings", "no_schedule_imported")) + + except Exception as e: + logger.error(f"更新课程表信息失败: {e}") + self.schedule_info_label.setText("获取课程表信息失败") + + def on_import_file_clicked(self): + """当点击导入文件按钮时的处理""" + # 打开文件选择对话框 + file_path, _ = QFileDialog.getOpenFileName( + self, + get_content_name_async("time_settings", "select_cses_file"), + "", + f"{get_content_name_async('time_settings', 'yaml_files')};;{get_content_name_async('time_settings', 'all_files')}" + ) + + if file_path: + self._import_cses_file(file_path) + + def _import_cses_file(self, file_path: str): + """导入CSES文件""" + try: + # 显示等待对话框 + self.import_file_button.setEnabled(False) + self.import_file_button.setText(get_content_name_async("time_settings", "importing")) + + # 调用导入函数 + success, message = import_cses_schedule(file_path) + + if success: + # 显示成功信息 + InfoBar.success( + title=get_content_name_async("time_settings", "import_success"), + content=message, + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=3000, + parent=self + ) + + # 更新课程表信息 + self._update_schedule_info() + + else: + # 显示错误信息 + InfoBar.error( + title=get_content_name_async("time_settings", "import_failed"), + content=message, + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=5000, + parent=self + ) + + except Exception as e: + logger.error(f"导入CSES文件失败: {e}") + InfoBar.error( + title=get_content_name_async("time_settings", "import_failed"), + content=get_content_name_async("time_settings", "import_error").format(str(e)), + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=5000, + parent=self + ) + + finally: + # 恢复按钮状态 + self.import_file_button.setEnabled(True) + self.import_file_button.setText(get_content_name_async("time_settings", "import_from_file")) + + def on_view_template_clicked(self): + """当点击查看模板按钮时的处理""" + try: + # 获取模板内容 + template_content = get_cses_import_template() + + # 创建对话框显示模板 + dialog = QDialog(self) + dialog.setWindowTitle(get_content_name_async("time_settings", "template_title")) + dialog.setMinimumSize(600, 500) + + # 创建布局 + layout = QVBoxLayout(dialog) + + # 创建文本编辑器 + text_edit = QTextEdit() + text_edit.setPlainText(template_content) + text_edit.setReadOnly(True) + text_edit.setFont(QFont("Consolas", 10)) + + # 创建按钮 + button_layout = QHBoxLayout() + + copy_button = PushButton(get_content_name_async("time_settings", "copy_to_clipboard")) + copy_button.clicked.connect(lambda: self._copy_to_clipboard(template_content)) + + save_button = PushButton(get_content_name_async("time_settings", "save_as_file")) + save_button.clicked.connect(lambda: self._save_template_file(template_content)) + + close_button = PushButton(get_content_name_async("time_settings", "close")) + close_button.clicked.connect(dialog.close) + + button_layout.addWidget(copy_button) + button_layout.addWidget(save_button) + button_layout.addStretch() + button_layout.addWidget(close_button) + + layout.addWidget(text_edit) + layout.addLayout(button_layout) + + dialog.exec() + + except Exception as e: + logger.error(f"显示模板失败: {e}") + InfoBar.error( + title=get_content_name_async("time_settings", "import_failed"), + content=f"无法显示模板: {str(e)}", + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=3000, + parent=self + ) + + def _copy_to_clipboard(self, content: str): + """复制内容到剪贴板""" + try: + clipboard = QApplication.clipboard() + clipboard.setText(content) + InfoBar.success( + title="复制成功", + content="模板已复制到剪贴板", + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=2000, + parent=self + ) + except Exception as e: + logger.error(f"复制到剪贴板失败: {e}") + + def _save_template_file(self, content: str): + """保存模板为文件""" + try: + # 打开保存文件对话框 + file_path, _ = QFileDialog.getSaveFileName( + self, + "保存CSES模板", + "cses_template.yaml", + "YAML文件 (*.yaml *.yml);;所有文件 (*.*)" + ) + + if file_path: + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + + InfoBar.success( + title="保存成功", + content=f"模板已保存到: {file_path}", + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=3000, + parent=self + ) + + except Exception as e: + logger.error(f"保存模板文件失败: {e}") + InfoBar.error( + title="保存失败", + content=f"无法保存模板文件: {str(e)}", + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=3000, + parent=self + ) \ No newline at end of file From dd7ab5a4f660c736be2053fb81000b6b8718d832 Mon Sep 17 00:00:00 2001 From: jimmy-sketch Date: Sat, 20 Dec 2025 17:53:17 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E5=B0=86cses=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E6=9F=A5=E7=9C=8B=E7=AA=97=E5=8F=A3=E5=8F=98?= =?UTF-8?q?=E4=B8=BA=E4=BD=BF=E7=94=A8=E7=BB=9F=E4=B8=80=E7=9A=84=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/extraction/extract.py | 10 ++ app/page_building/another_window.py | 29 ++++ .../another_window/cses_template_viewer.py | 144 ++++++++++++++++++ .../extraction_settings/time_settings.py | 101 +----------- 4 files changed, 187 insertions(+), 97 deletions(-) create mode 100644 app/view/another_window/cses_template_viewer.py diff --git a/app/common/extraction/extract.py b/app/common/extraction/extract.py index 187c7805..1ce46175 100644 --- a/app/common/extraction/extract.py +++ b/app/common/extraction/extract.py @@ -316,3 +316,13 @@ def get_cses_import_template() -> str: day_of_week: 1 """ return template + + +# ================================================== +# 导出函数列表 +# ================================================== +__all__ = [ + 'import_cses_schedule', + 'import_cses_schedule_from_content', + 'get_cses_import_template' +] diff --git a/app/page_building/another_window.py b/app/page_building/another_window.py index 21a0cf09..e2ef46e7 100644 --- a/app/page_building/another_window.py +++ b/app/page_building/another_window.py @@ -13,6 +13,7 @@ from app.view.another_window.prize.prize_name_setting import PrizeNameSettingWindow from app.view.another_window.prize.prize_weight_setting import PrizeWeightSettingWindow from app.view.another_window.remaining_list import RemainingListPage +from app.view.another_window.cses_template_viewer import CsesTemplateViewerWindow from app.Language.obtain_language import * from app.tools.variable import * @@ -48,6 +49,34 @@ def create_set_class_name_window(): return +# ================================================== +# CSES模板查看窗口 +# ================================================== +class cses_template_viewer_window_template(PageTemplate): + """CSES模板查看窗口类 + 使用PageTemplate创建CSES模板查看页面""" + + def __init__(self, parent=None): + super().__init__(content_widget_class=CsesTemplateViewerWindow, parent=parent) + + +def create_cses_template_viewer_window(): + """ + 创建CSES模板查看窗口 + + Returns: + 创建的窗口实例 + """ + title = get_content_name_async("time_settings", "template_title") + window = SimpleWindowTemplate(title, width=700, height=500) + window.add_page_from_template("cses_template_viewer", cses_template_viewer_window_template) + window.switch_to_page("cses_template_viewer") + _window_instances["cses_template_viewer"] = window + window.windowClosed.connect(lambda: _window_instances.pop("cses_template_viewer", None)) + window.show() + return + + # ================================================== # 导入学生名单导入窗口 # ================================================== diff --git a/app/view/another_window/cses_template_viewer.py b/app/view/another_window/cses_template_viewer.py new file mode 100644 index 00000000..5d3004e2 --- /dev/null +++ b/app/view/another_window/cses_template_viewer.py @@ -0,0 +1,144 @@ +# ================================================== +# 导入库 +# ================================================== +from loguru import logger +from PySide6.QtWidgets import * +from PySide6.QtGui import * +from PySide6.QtCore import * +from qfluentwidgets import * + +from app.tools.variable import * +from app.tools.path_utils import * +from app.tools.personalised import * +from app.Language.obtain_language import * +from app.common.extraction.extract import get_cses_import_template + + +class CsesTemplateViewerWindow(QWidget): + """CSES模板查看器窗口""" + + def __init__(self, parent=None): + """初始化CSES模板查看器窗口""" + super().__init__(parent) + self.parent_window = parent + self.init_ui() + + def init_ui(self): + """初始化UI""" + # 设置窗口标题 + self.setWindowTitle(get_content_name_async("time_settings", "template_title")) + + # 创建主布局 + self.main_layout = QVBoxLayout(self) + self.main_layout.setContentsMargins(20, 20, 20, 20) + self.main_layout.setSpacing(15) + + # 创建标题标签 + title_label = QLabel(get_content_name_async("time_settings", "template_title")) + title_label.setStyleSheet("font-size: 16px; font-weight: bold;") + + # 创建文本编辑器 + self.text_edit = QTextEdit() + self.text_edit.setReadOnly(True) + self.text_edit.setFont(QFont("Consolas", 10)) + + # 获取并设置模板内容 + try: + template_content = get_cses_import_template() + self.text_edit.setPlainText(template_content) + except Exception as e: + logger.error(f"获取模板内容失败: {e}") + self.text_edit.setPlainText(f"无法加载模板: {str(e)}") + + # 创建按钮布局 + button_layout = QHBoxLayout() + + # 复制到剪贴板按钮 + self.copy_button = PushButton(get_content_name_async("time_settings", "copy_to_clipboard")) + self.copy_button.setIcon(get_theme_icon("ic_fluent_copy_20_filled")) + self.copy_button.clicked.connect(self.on_copy_clicked) + + # 保存为文件按钮 + self.save_button = PushButton(get_content_name_async("time_settings", "save_as_file")) + self.save_button.setIcon(get_theme_icon("ic_fluent_save_20_filled")) + self.save_button.clicked.connect(self.on_save_clicked) + + # 关闭按钮 + self.close_button = PushButton(get_content_name_async("time_settings", "close")) + self.close_button.clicked.connect(self.close) + + button_layout.addWidget(self.copy_button) + button_layout.addWidget(self.save_button) + button_layout.addStretch() + button_layout.addWidget(self.close_button) + + # 添加到主布局 + self.main_layout.addWidget(title_label) + self.main_layout.addWidget(self.text_edit) + self.main_layout.addLayout(button_layout) + + def on_copy_clicked(self): + """复制到剪贴板""" + try: + content = self.text_edit.toPlainText() + clipboard = QApplication.clipboard() + clipboard.setText(content) + + InfoBar.success( + title=get_content_name_async("time_settings", "copy_success"), + content=get_content_name_async("time_settings", "template_copied"), + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=2000, + parent=self + ) + except Exception as e: + logger.error(f"复制到剪贴板失败: {e}") + InfoBar.error( + title=get_content_name_async("time_settings", "import_failed"), + content=f"复制失败: {str(e)}", + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=3000, + parent=self + ) + + def on_save_clicked(self): + """保存为文件""" + try: + # 打开保存文件对话框 + file_path, _ = QFileDialog.getSaveFileName( + self, + get_content_name_async("time_settings", "save_template"), + get_content_name_async("time_settings", "cses_template"), + f"{get_content_name_async('time_settings', 'yaml_files')};;{get_content_name_async('time_settings', 'all_files')}" + ) + + if file_path: + content = self.text_edit.toPlainText() + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + + InfoBar.success( + title=get_content_name_async("time_settings", "save_success"), + content=get_content_name_async("time_settings", "template_saved").format(file_path), + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=3000, + parent=self + ) + + except Exception as e: + logger.error(f"保存模板文件失败: {e}") + InfoBar.error( + title=get_content_name_async("time_settings", "import_failed"), + content=f"保存失败: {str(e)}", + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=3000, + parent=self + ) \ No newline at end of file diff --git a/app/view/settings/extraction_settings/time_settings.py b/app/view/settings/extraction_settings/time_settings.py index 1666ec14..b1097948 100644 --- a/app/view/settings/extraction_settings/time_settings.py +++ b/app/view/settings/extraction_settings/time_settings.py @@ -19,11 +19,8 @@ from app.tools.settings_access import * from app.tools.settings_access import get_safe_font_size from app.Language.obtain_language import * -from app.common.extraction.extract import ( - import_cses_schedule, - import_cses_schedule_from_content, - get_cses_import_template -) +from app.common.extraction.extract import import_cses_schedule, get_cses_import_template +from app.page_building.another_window import create_cses_template_viewer_window # ================================================== @@ -254,44 +251,8 @@ def _import_cses_file(self, file_path: str): def on_view_template_clicked(self): """当点击查看模板按钮时的处理""" try: - # 获取模板内容 - template_content = get_cses_import_template() - - # 创建对话框显示模板 - dialog = QDialog(self) - dialog.setWindowTitle(get_content_name_async("time_settings", "template_title")) - dialog.setMinimumSize(600, 500) - - # 创建布局 - layout = QVBoxLayout(dialog) - - # 创建文本编辑器 - text_edit = QTextEdit() - text_edit.setPlainText(template_content) - text_edit.setReadOnly(True) - text_edit.setFont(QFont("Consolas", 10)) - - # 创建按钮 - button_layout = QHBoxLayout() - - copy_button = PushButton(get_content_name_async("time_settings", "copy_to_clipboard")) - copy_button.clicked.connect(lambda: self._copy_to_clipboard(template_content)) - - save_button = PushButton(get_content_name_async("time_settings", "save_as_file")) - save_button.clicked.connect(lambda: self._save_template_file(template_content)) - - close_button = PushButton(get_content_name_async("time_settings", "close")) - close_button.clicked.connect(dialog.close) - - button_layout.addWidget(copy_button) - button_layout.addWidget(save_button) - button_layout.addStretch() - button_layout.addWidget(close_button) - - layout.addWidget(text_edit) - layout.addLayout(button_layout) - - dialog.exec() + # 使用独立窗口模板创建CSES模板查看器 + create_cses_template_viewer_window() except Exception as e: logger.error(f"显示模板失败: {e}") @@ -303,58 +264,4 @@ def on_view_template_clicked(self): position=InfoBarPosition.TOP, duration=3000, parent=self - ) - - def _copy_to_clipboard(self, content: str): - """复制内容到剪贴板""" - try: - clipboard = QApplication.clipboard() - clipboard.setText(content) - InfoBar.success( - title="复制成功", - content="模板已复制到剪贴板", - orient=Qt.Horizontal, - isClosable=True, - position=InfoBarPosition.TOP, - duration=2000, - parent=self - ) - except Exception as e: - logger.error(f"复制到剪贴板失败: {e}") - - def _save_template_file(self, content: str): - """保存模板为文件""" - try: - # 打开保存文件对话框 - file_path, _ = QFileDialog.getSaveFileName( - self, - "保存CSES模板", - "cses_template.yaml", - "YAML文件 (*.yaml *.yml);;所有文件 (*.*)" - ) - - if file_path: - with open(file_path, 'w', encoding='utf-8') as f: - f.write(content) - - InfoBar.success( - title="保存成功", - content=f"模板已保存到: {file_path}", - orient=Qt.Horizontal, - isClosable=True, - position=InfoBarPosition.TOP, - duration=3000, - parent=self - ) - - except Exception as e: - logger.error(f"保存模板文件失败: {e}") - InfoBar.error( - title="保存失败", - content=f"无法保存模板文件: {str(e)}", - orient=Qt.Horizontal, - isClosable=True, - position=InfoBarPosition.TOP, - duration=3000, - parent=self ) \ No newline at end of file From 3d983e030a42007e40eb72fcaf47cb55c8cf88cc Mon Sep 17 00:00:00 2001 From: jimmy-sketch Date: Sat, 20 Dec 2025 18:01:15 +0800 Subject: [PATCH 3/3] =?UTF-8?q?style:=20=E6=A0=BC=E5=BC=8F=E5=8C=96?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=B9=B6=E8=B0=83=E6=95=B4=E5=A4=9A=E8=A1=8C?= =?UTF-8?q?=E5=AD=97=E7=AC=A6=E4=B8=B2=E5=92=8C=E5=8F=82=E6=95=B0=E6=8D=A2?= =?UTF-8?q?=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Language/modules/extraction_settings.py | 5 +- app/common/extraction/cses_parser.py | 162 +++++++++--------- app/common/extraction/extract.py | 50 +++--- app/page_building/another_window.py | 8 +- .../another_window/cses_template_viewer.py | 52 +++--- .../extraction_settings/time_settings.py | 110 +++++++----- 6 files changed, 216 insertions(+), 171 deletions(-) diff --git a/app/Language/modules/extraction_settings.py b/app/Language/modules/extraction_settings.py index 2ae874f1..1770e86e 100644 --- a/app/Language/modules/extraction_settings.py +++ b/app/Language/modules/extraction_settings.py @@ -248,7 +248,10 @@ "cses_template": {"name": "cses_template.yaml"}, }, "EN_US": { - "title": {"name": "Time Settings", "description": "Set class break restrictions and schedule import"}, + "title": { + "name": "Time Settings", + "description": "Set class break restrictions and schedule import", + }, "class_break_settings": { "name": "Class Break Settings", "description": "Configure class break restrictions", diff --git a/app/common/extraction/cses_parser.py b/app/common/extraction/cses_parser.py index e3bd385d..d5241418 100644 --- a/app/common/extraction/cses_parser.py +++ b/app/common/extraction/cses_parser.py @@ -10,33 +10,33 @@ class CSESParser: """CSES格式课程表解析器""" - + def __init__(self): self.schedule_data = None - + def load_from_file(self, file_path: str) -> bool: """从文件加载CSES数据 - + Args: file_path: CSES文件路径 - + Returns: bool: 加载成功返回True,否则返回False """ try: - with open(file_path, 'r', encoding='utf-8') as f: + with open(file_path, "r", encoding="utf-8") as f: self.schedule_data = yaml.safe_load(f) return self._validate_schedule() except Exception as e: logger.error(f"加载CSES文件失败: {e}") return False - + def load_from_content(self, content: str) -> bool: """从字符串内容加载CSES数据 - + Args: content: YAML格式的CSES内容 - + Returns: bool: 加载成功返回True,否则返回False """ @@ -46,90 +46,90 @@ def load_from_content(self, content: str) -> bool: except Exception as e: logger.error(f"解析CSES内容失败: {e}") return False - + def _validate_schedule(self) -> bool: """验证课程表数据的有效性 - + Returns: bool: 数据有效返回True,否则返回False """ if not self.schedule_data: logger.error("课程表数据为空") return False - + # 基本结构验证 - if 'schedule' not in self.schedule_data: + if "schedule" not in self.schedule_data: logger.error("缺少'schedule'字段") return False - - schedule = self.schedule_data['schedule'] + + schedule = self.schedule_data["schedule"] if not isinstance(schedule, dict): logger.error("'schedule'字段必须是字典类型") return False - + # 验证时间段配置 - if 'timeslots' not in schedule: + if "timeslots" not in schedule: logger.error("缺少'timeslots'字段") return False - - timeslots = schedule['timeslots'] + + timeslots = schedule["timeslots"] if not isinstance(timeslots, list): logger.error("'timeslots'字段必须是列表类型") return False - + # 验证每个时间段 for i, timeslot in enumerate(timeslots): if not self._validate_timeslot(timeslot, i): return False - + return True - + def _validate_timeslot(self, timeslot: dict, index: int) -> bool: """验证单个时间段的配置 - + Args: timeslot: 时间段配置字典 index: 时间段索引 - + Returns: bool: 有效返回True,否则返回False """ - required_fields = ['name', 'start_time', 'end_time'] + required_fields = ["name", "start_time", "end_time"] for field in required_fields: if field not in timeslot: logger.error(f"时间段{index}缺少'{field}'字段") return False - + # 验证时间格式 try: - start_time = self._parse_time(timeslot['start_time']) - end_time = self._parse_time(timeslot['end_time']) - + start_time = self._parse_time(timeslot["start_time"]) + end_time = self._parse_time(timeslot["end_time"]) + if start_time >= end_time: logger.error(f"时间段{index}的开始时间必须早于结束时间") return False - + except ValueError as e: logger.error(f"时间段{index}时间格式错误: {e}") return False - + return True - + def _parse_time(self, time_str: str) -> time: """解析时间字符串 - + Args: time_str: 时间字符串 (HH:MM 或 HH:MM:SS) - + Returns: time: 时间对象 - + Raises: ValueError: 时间格式错误 """ try: - if ':' in time_str: - parts = time_str.split(':') + if ":" in time_str: + parts = time_str.split(":") if len(parts) == 2: return time(int(parts[0]), int(parts[1])) elif len(parts) == 3: @@ -137,116 +137,116 @@ def _parse_time(self, time_str: str) -> time: raise ValueError(f"无效的时间格式: {time_str}") except (ValueError, IndexError): raise ValueError(f"无法解析时间: {time_str}") - + def get_non_class_times(self) -> Dict[str, str]: """获取非上课时间段配置 - + 将CSES格式的时间段转换为SecRandom使用的非上课时间段格式 - + Returns: Dict[str, str]: 非上课时间段字典,格式为 {"name": "HH:MM:SS-HH:MM:SS"} """ if not self.schedule_data: return {} - + non_class_times = {} - schedule = self.schedule_data['schedule'] - timeslots = schedule['timeslots'] - + schedule = self.schedule_data["schedule"] + timeslots = schedule["timeslots"] + # 按开始时间排序 - sorted_timeslots = sorted(timeslots, key=lambda x: x['start_time']) - + sorted_timeslots = sorted(timeslots, key=lambda x: x["start_time"]) + # 构建上课时间段列表 class_periods = [] for timeslot in sorted_timeslots: - start_time = self._format_time_for_secrandom(timeslot['start_time']) - end_time = self._format_time_for_secrandom(timeslot['end_time']) + start_time = self._format_time_for_secrandom(timeslot["start_time"]) + end_time = self._format_time_for_secrandom(timeslot["end_time"]) class_periods.append((start_time, end_time)) - + # 生成非上课时间段 # 1. 第一节课之前的时间 if class_periods: first_start = class_periods[0][0] if first_start != "00:00:00": non_class_times["before_first_class"] = f"00:00:00-{first_start}" - + # 2. 课间时间(两节课之间) for i in range(len(class_periods) - 1): current_end = class_periods[i][1] next_start = class_periods[i + 1][0] if current_end != next_start: - period_name = f"break_{i+1}" + period_name = f"break_{i + 1}" non_class_times[period_name] = f"{current_end}-{next_start}" - + # 3. 最后一节课之后的时间 if class_periods: last_end = class_periods[-1][1] if last_end != "23:59:59": non_class_times["after_last_class"] = f"{last_end}-23:59:59" - + logger.info(f"成功解析CSES课程表,生成{len(non_class_times)}个非上课时间段") return non_class_times - + def _format_time_for_secrandom(self, time_str: str) -> str: """将时间字符串格式化为SecRandom需要的格式 (HH:MM:SS) - + Args: time_str: 原始时间字符串 (HH:MM 或 HH:MM:SS) - + Returns: str: 格式化后的时间字符串 (HH:MM:SS) """ - if time_str.count(':') == 1: # HH:MM 格式 + if time_str.count(":") == 1: # HH:MM 格式 return f"{time_str}:00" return time_str - + def get_class_info(self) -> List[Dict]: """获取课程信息列表 - + Returns: List[Dict]: 课程信息列表 """ if not self.schedule_data: return [] - - schedule = self.schedule_data['schedule'] - timeslots = schedule.get('timeslots', []) - + + schedule = self.schedule_data["schedule"] + timeslots = schedule.get("timeslots", []) + class_info = [] for timeslot in timeslots: info = { - 'name': timeslot.get('name', ''), - 'start_time': timeslot.get('start_time', ''), - 'end_time': timeslot.get('end_time', ''), - 'teacher': timeslot.get('teacher', ''), - 'location': timeslot.get('location', ''), - 'day_of_week': timeslot.get('day_of_week', ''), + "name": timeslot.get("name", ""), + "start_time": timeslot.get("start_time", ""), + "end_time": timeslot.get("end_time", ""), + "teacher": timeslot.get("teacher", ""), + "location": timeslot.get("location", ""), + "day_of_week": timeslot.get("day_of_week", ""), } class_info.append(info) - + return class_info - + def get_summary(self) -> str: """获取课程表摘要信息 - + Returns: str: 摘要信息 """ if not self.schedule_data: return "未加载课程表" - - schedule = self.schedule_data['schedule'] - timeslots = schedule.get('timeslots', []) - + + schedule = self.schedule_data["schedule"] + timeslots = schedule.get("timeslots", []) + if not timeslots: return "课程表为空" - + # 获取最早和最晚时间 - start_times = [slot['start_time'] for slot in timeslots] - end_times = [slot['end_time'] for slot in timeslots] - + start_times = [slot["start_time"] for slot in timeslots] + end_times = [slot["end_time"] for slot in timeslots] + summary = f"课程表包含{len(timeslots)}个时间段," summary += f"最早开始时间:{min(start_times)}," summary += f"最晚结束时间:{max(end_times)}" - - return summary \ No newline at end of file + + return summary diff --git a/app/common/extraction/extract.py b/app/common/extraction/extract.py index 1ce46175..a6f0ceb0 100644 --- a/app/common/extraction/extract.py +++ b/app/common/extraction/extract.py @@ -172,35 +172,35 @@ def _parse_time_string_to_seconds(time_str: str) -> int: # ================================================== def import_cses_schedule(file_path: str) -> tuple[bool, str]: """从CSES文件导入课程表 - + Args: file_path: CSES文件路径 - + Returns: tuple[bool, str]: (是否成功, 结果消息) """ try: # 创建CSES解析器 parser = CSESParser() - + # 加载CSES文件 if not parser.load_from_file(file_path): return False, "CSES文件格式错误或文件无法读取" - + # 获取非上课时间段配置 non_class_times = parser.get_non_class_times() if not non_class_times: return False, "未能从课程表中提取有效的时间段信息" - + # 保存到设置文件 success = _save_non_class_times_to_settings(non_class_times) if not success: return False, "保存设置失败" - + # 获取摘要信息 summary = parser.get_summary() return True, f"成功导入课程表: {summary}" - + except Exception as e: logger.error(f"导入CSES文件失败: {e}") return False, f"导入失败: {str(e)}" @@ -208,35 +208,35 @@ def import_cses_schedule(file_path: str) -> tuple[bool, str]: def import_cses_schedule_from_content(content: str) -> tuple[bool, str]: """从CSES内容字符串导入课程表 - + Args: content: CSES格式的YAML内容 - + Returns: tuple[bool, str]: (是否成功, 结果消息) """ try: # 创建CSES解析器 parser = CSESParser() - + # 加载CSES内容 if not parser.load_from_content(content): return False, "CSES内容格式错误" - + # 获取非上课时间段配置 non_class_times = parser.get_non_class_times() if not non_class_times: return False, "未能从课程表中提取有效的时间段信息" - + # 保存到设置文件 success = _save_non_class_times_to_settings(non_class_times) if not success: return False, "保存设置失败" - + # 获取摘要信息 summary = parser.get_summary() return True, f"成功导入课程表: {summary}" - + except Exception as e: logger.error(f"导入CSES内容失败: {e}") return False, f"导入失败: {str(e)}" @@ -244,33 +244,33 @@ def import_cses_schedule_from_content(content: str) -> tuple[bool, str]: def _save_non_class_times_to_settings(non_class_times: Dict[str, str]) -> bool: """保存非上课时间段到设置文件 - + Args: non_class_times: 非上课时间段字典 - + Returns: bool: 保存成功返回True,否则返回False """ try: settings_path = get_settings_path() - + # 读取现有设置 if file_exists(settings_path): with open_file(settings_path, "r", encoding="utf-8") as f: settings = json.load(f) else: settings = {} - + # 更新非上课时间段配置 settings["non_class_times"] = non_class_times - + # 写入设置文件 with open_file(settings_path, "w", encoding="utf-8") as f: json.dump(settings, f, ensure_ascii=False, indent=2) - + logger.info(f"成功保存{len(non_class_times)}个非上课时间段到设置文件") return True - + except Exception as e: logger.error(f"保存非上课时间段失败: {e}") return False @@ -278,7 +278,7 @@ def _save_non_class_times_to_settings(non_class_times: Dict[str, str]) -> bool: def get_cses_import_template() -> str: """获取CSES导入模板内容 - + Returns: str: CSES格式的模板内容 """ @@ -322,7 +322,7 @@ def get_cses_import_template() -> str: # 导出函数列表 # ================================================== __all__ = [ - 'import_cses_schedule', - 'import_cses_schedule_from_content', - 'get_cses_import_template' + "import_cses_schedule", + "import_cses_schedule_from_content", + "get_cses_import_template", ] diff --git a/app/page_building/another_window.py b/app/page_building/another_window.py index e2ef46e7..8d7303c3 100644 --- a/app/page_building/another_window.py +++ b/app/page_building/another_window.py @@ -69,10 +69,14 @@ def create_cses_template_viewer_window(): """ title = get_content_name_async("time_settings", "template_title") window = SimpleWindowTemplate(title, width=700, height=500) - window.add_page_from_template("cses_template_viewer", cses_template_viewer_window_template) + window.add_page_from_template( + "cses_template_viewer", cses_template_viewer_window_template + ) window.switch_to_page("cses_template_viewer") _window_instances["cses_template_viewer"] = window - window.windowClosed.connect(lambda: _window_instances.pop("cses_template_viewer", None)) + window.windowClosed.connect( + lambda: _window_instances.pop("cses_template_viewer", None) + ) window.show() return diff --git a/app/view/another_window/cses_template_viewer.py b/app/view/another_window/cses_template_viewer.py index 5d3004e2..81e1b3db 100644 --- a/app/view/another_window/cses_template_viewer.py +++ b/app/view/another_window/cses_template_viewer.py @@ -27,7 +27,7 @@ def init_ui(self): """初始化UI""" # 设置窗口标题 self.setWindowTitle(get_content_name_async("time_settings", "template_title")) - + # 创建主布局 self.main_layout = QVBoxLayout(self) self.main_layout.setContentsMargins(20, 20, 20, 20) @@ -36,12 +36,12 @@ def init_ui(self): # 创建标题标签 title_label = QLabel(get_content_name_async("time_settings", "template_title")) title_label.setStyleSheet("font-size: 16px; font-weight: bold;") - + # 创建文本编辑器 self.text_edit = QTextEdit() self.text_edit.setReadOnly(True) self.text_edit.setFont(QFont("Consolas", 10)) - + # 获取并设置模板内容 try: template_content = get_cses_import_template() @@ -49,29 +49,33 @@ def init_ui(self): except Exception as e: logger.error(f"获取模板内容失败: {e}") self.text_edit.setPlainText(f"无法加载模板: {str(e)}") - + # 创建按钮布局 button_layout = QHBoxLayout() - + # 复制到剪贴板按钮 - self.copy_button = PushButton(get_content_name_async("time_settings", "copy_to_clipboard")) + self.copy_button = PushButton( + get_content_name_async("time_settings", "copy_to_clipboard") + ) self.copy_button.setIcon(get_theme_icon("ic_fluent_copy_20_filled")) self.copy_button.clicked.connect(self.on_copy_clicked) - + # 保存为文件按钮 - self.save_button = PushButton(get_content_name_async("time_settings", "save_as_file")) + self.save_button = PushButton( + get_content_name_async("time_settings", "save_as_file") + ) self.save_button.setIcon(get_theme_icon("ic_fluent_save_20_filled")) self.save_button.clicked.connect(self.on_save_clicked) - + # 关闭按钮 self.close_button = PushButton(get_content_name_async("time_settings", "close")) self.close_button.clicked.connect(self.close) - + button_layout.addWidget(self.copy_button) button_layout.addWidget(self.save_button) button_layout.addStretch() button_layout.addWidget(self.close_button) - + # 添加到主布局 self.main_layout.addWidget(title_label) self.main_layout.addWidget(self.text_edit) @@ -83,7 +87,7 @@ def on_copy_clicked(self): content = self.text_edit.toPlainText() clipboard = QApplication.clipboard() clipboard.setText(content) - + InfoBar.success( title=get_content_name_async("time_settings", "copy_success"), content=get_content_name_async("time_settings", "template_copied"), @@ -91,7 +95,7 @@ def on_copy_clicked(self): isClosable=True, position=InfoBarPosition.TOP, duration=2000, - parent=self + parent=self, ) except Exception as e: logger.error(f"复制到剪贴板失败: {e}") @@ -102,7 +106,7 @@ def on_copy_clicked(self): isClosable=True, position=InfoBarPosition.TOP, duration=3000, - parent=self + parent=self, ) def on_save_clicked(self): @@ -113,24 +117,26 @@ def on_save_clicked(self): self, get_content_name_async("time_settings", "save_template"), get_content_name_async("time_settings", "cses_template"), - f"{get_content_name_async('time_settings', 'yaml_files')};;{get_content_name_async('time_settings', 'all_files')}" + f"{get_content_name_async('time_settings', 'yaml_files')};;{get_content_name_async('time_settings', 'all_files')}", ) - + if file_path: content = self.text_edit.toPlainText() - with open(file_path, 'w', encoding='utf-8') as f: + with open(file_path, "w", encoding="utf-8") as f: f.write(content) - + InfoBar.success( title=get_content_name_async("time_settings", "save_success"), - content=get_content_name_async("time_settings", "template_saved").format(file_path), + content=get_content_name_async( + "time_settings", "template_saved" + ).format(file_path), orient=Qt.Horizontal, isClosable=True, position=InfoBarPosition.TOP, duration=3000, - parent=self + parent=self, ) - + except Exception as e: logger.error(f"保存模板文件失败: {e}") InfoBar.error( @@ -140,5 +146,5 @@ def on_save_clicked(self): isClosable=True, position=InfoBarPosition.TOP, duration=3000, - parent=self - ) \ No newline at end of file + parent=self, + ) diff --git a/app/view/settings/extraction_settings/time_settings.py b/app/view/settings/extraction_settings/time_settings.py index b1097948..9ebcdd2b 100644 --- a/app/view/settings/extraction_settings/time_settings.py +++ b/app/view/settings/extraction_settings/time_settings.py @@ -46,25 +46,33 @@ def __init__(self, parent=None): class class_break_settings(GroupHeaderCardWidget): def __init__(self, parent=None): super().__init__(parent) - self.setTitle(get_content_name_async("time_settings", "class_break_settings", "name")) + self.setTitle( + get_content_name_async("time_settings", "class_break_settings", "name") + ) self.setBorderRadius(8) # 课间禁用开关 self.class_break_switch = SwitchButton() - self.class_break_switch.setOffText(get_content_name_async("time_settings", "disable")) - self.class_break_switch.setOnText(get_content_name_async("time_settings", "enable")) - + self.class_break_switch.setOffText( + get_content_name_async("time_settings", "disable") + ) + self.class_break_switch.setOnText( + get_content_name_async("time_settings", "enable") + ) + # 从设置中读取当前状态 current_enabled = self._get_class_break_enabled() self.class_break_switch.setChecked(current_enabled) - + self.class_break_switch.checkedChanged.connect(self.on_class_break_changed) # 添加设置项到分组 self.addGroup( get_theme_icon("ic_fluent_clock_lock_20_filled"), get_content_name_async("time_settings", "class_break_function", "name"), - get_content_name_async("time_settings", "class_break_function", "description"), + get_content_name_async( + "time_settings", "class_break_function", "description" + ), self.class_break_switch, ) @@ -88,26 +96,26 @@ def on_class_break_changed(self, is_checked: bool): """当课间禁用开关状态改变时的处理""" try: settings_path = get_settings_path() - + # 读取现有设置 if file_exists(settings_path): with open_file(settings_path, "r", encoding="utf-8") as f: settings = json.load(f) else: settings = {} - + # 更新程序功能设置 if "program_functionality" not in settings: settings["program_functionality"] = {} - + settings["program_functionality"]["instant_draw_disable"] = is_checked - + # 写入设置文件 with open_file(settings_path, "w", encoding="utf-8") as f: json.dump(settings, f, ensure_ascii=False, indent=2) - + logger.info(f"课间禁用功能已{'开启' if is_checked else '关闭'}") - + except Exception as e: logger.error(f"保存课间禁用设置失败: {e}") # 恢复开关状态 @@ -117,21 +125,33 @@ def on_class_break_changed(self, is_checked: bool): class cses_import_settings(GroupHeaderCardWidget): def __init__(self, parent=None): super().__init__(parent) - self.setTitle(get_content_name_async("time_settings", "cses_import_settings", "name")) + self.setTitle( + get_content_name_async("time_settings", "cses_import_settings", "name") + ) self.setBorderRadius(8) # 导入文件按钮 - self.import_file_button = PushButton(get_content_name_async("time_settings", "import_from_file")) - self.import_file_button.setIcon(get_theme_icon("ic_fluent_folder_open_20_filled")) + self.import_file_button = PushButton( + get_content_name_async("time_settings", "import_from_file") + ) + self.import_file_button.setIcon( + get_theme_icon("ic_fluent_folder_open_20_filled") + ) self.import_file_button.clicked.connect(self.on_import_file_clicked) # 查看模板按钮 - self.view_template_button = PushButton(get_content_name_async("time_settings", "view_template")) - self.view_template_button.setIcon(get_theme_icon("ic_fluent_document_20_filled")) + self.view_template_button = PushButton( + get_content_name_async("time_settings", "view_template") + ) + self.view_template_button.setIcon( + get_theme_icon("ic_fluent_document_20_filled") + ) self.view_template_button.clicked.connect(self.on_view_template_clicked) # 当前课程表信息标签 - self.schedule_info_label = QLabel(get_content_name_async("time_settings", "no_schedule_imported")) + self.schedule_info_label = QLabel( + get_content_name_async("time_settings", "no_schedule_imported") + ) self._update_schedule_info() # 创建按钮布局 @@ -162,7 +182,9 @@ def _update_schedule_info(self): try: settings_path = get_settings_path() if not file_exists(settings_path): - self.schedule_info_label.setText(get_content_name_async("time_settings", "no_schedule_imported")) + self.schedule_info_label.setText( + get_content_name_async("time_settings", "no_schedule_imported") + ) return with open_file(settings_path, "r", encoding="utf-8") as f: @@ -172,11 +194,15 @@ def _update_schedule_info(self): if non_class_times: count = len(non_class_times) self.schedule_info_label.setText( - get_content_name_async("time_settings", "schedule_imported").format(count) + get_content_name_async("time_settings", "schedule_imported").format( + count + ) ) else: - self.schedule_info_label.setText(get_content_name_async("time_settings", "no_schedule_imported")) - + self.schedule_info_label.setText( + get_content_name_async("time_settings", "no_schedule_imported") + ) + except Exception as e: logger.error(f"更新课程表信息失败: {e}") self.schedule_info_label.setText("获取课程表信息失败") @@ -188,9 +214,9 @@ def on_import_file_clicked(self): self, get_content_name_async("time_settings", "select_cses_file"), "", - f"{get_content_name_async('time_settings', 'yaml_files')};;{get_content_name_async('time_settings', 'all_files')}" + f"{get_content_name_async('time_settings', 'yaml_files')};;{get_content_name_async('time_settings', 'all_files')}", ) - + if file_path: self._import_cses_file(file_path) @@ -199,11 +225,13 @@ def _import_cses_file(self, file_path: str): try: # 显示等待对话框 self.import_file_button.setEnabled(False) - self.import_file_button.setText(get_content_name_async("time_settings", "importing")) - + self.import_file_button.setText( + get_content_name_async("time_settings", "importing") + ) + # 调用导入函数 success, message = import_cses_schedule(file_path) - + if success: # 显示成功信息 InfoBar.success( @@ -213,12 +241,12 @@ def _import_cses_file(self, file_path: str): isClosable=True, position=InfoBarPosition.TOP, duration=3000, - parent=self + parent=self, ) - + # 更新课程表信息 self._update_schedule_info() - + else: # 显示错误信息 InfoBar.error( @@ -228,32 +256,36 @@ def _import_cses_file(self, file_path: str): isClosable=True, position=InfoBarPosition.TOP, duration=5000, - parent=self + parent=self, ) - + except Exception as e: logger.error(f"导入CSES文件失败: {e}") InfoBar.error( title=get_content_name_async("time_settings", "import_failed"), - content=get_content_name_async("time_settings", "import_error").format(str(e)), + content=get_content_name_async("time_settings", "import_error").format( + str(e) + ), orient=Qt.Horizontal, isClosable=True, position=InfoBarPosition.TOP, duration=5000, - parent=self + parent=self, ) - + finally: # 恢复按钮状态 self.import_file_button.setEnabled(True) - self.import_file_button.setText(get_content_name_async("time_settings", "import_from_file")) + self.import_file_button.setText( + get_content_name_async("time_settings", "import_from_file") + ) def on_view_template_clicked(self): """当点击查看模板按钮时的处理""" try: # 使用独立窗口模板创建CSES模板查看器 create_cses_template_viewer_window() - + except Exception as e: logger.error(f"显示模板失败: {e}") InfoBar.error( @@ -263,5 +295,5 @@ def on_view_template_clicked(self): isClosable=True, position=InfoBarPosition.TOP, duration=3000, - parent=self - ) \ No newline at end of file + parent=self, + )