From 77feaffa48f0798a9a10db0ed22f6097829b0944 Mon Sep 17 00:00:00 2001 From: Supercmd Date: Sat, 16 May 2026 08:27:41 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=90=8D?= =?UTF-8?q?=E5=8D=95=E8=A1=A8=E6=A0=BC=E6=90=9C=E7=B4=A2=E7=AD=9B=E9=80=89?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=20(#232)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在点名表格和抽奖表格中添加搜索栏,支持按关键词实时筛选表格行。 - 点名表格:支持按学号、姓名、性别、小组、标签筛选 - 抽奖表格:支持按序号、奖品、权重、标签、数量筛选 - 使用 SearchLineEdit 组件,带防抖处理(300ms) - 添加中/英/日三语国际化翻译 - 刷新数据后自动重新应用搜索过滤 Closes #232 --- app/Language/modules/list_management.py | 48 ++++++++++++++++++ .../settings/list_management/lottery_table.py | 49 +++++++++++++++++++ .../list_management/roll_call_table.py | 49 +++++++++++++++++++ 3 files changed, 146 insertions(+) diff --git a/app/Language/modules/list_management.py b/app/Language/modules/list_management.py index 36ea586f..5f9d56f2 100644 --- a/app/Language/modules/list_management.py +++ b/app/Language/modules/list_management.py @@ -122,6 +122,14 @@ "name": "选择班级", "description": "选择要显示的点名班级", }, + "search": { + "name": "搜索名单", + "description": "在名单中搜索学生", + }, + "search_placeholder": { + "name": "输入学号、姓名、性别、小组或标签进行筛选", + "description": "搜索框占位符", + }, "HeaderLabels": { "name": ["存在", "学号", "姓名", "性别", "小组", "标签"], "description": "点名表格的列标题", @@ -151,6 +159,14 @@ "name": "Select class", "description": "Select the class to show", }, + "search": { + "name": "Search list", + "description": "Search students in the list", + }, + "search_placeholder": { + "name": "Filter by ID, name, gender, group or tags", + "description": "Search box placeholder", + }, "HeaderLabels": { "name": { "0": "Exist", @@ -190,6 +206,14 @@ "name": "クラスを選択", "description": "表示する点呼クラスを選択", }, + "search": { + "name": "リスト検索", + "description": "リスト内の学生を検索", + }, + "search_placeholder": { + "name": "学籍番号、氏名、性別、グループ、タグで絞り込み", + "description": "検索ボックスのプレースホルダー", + }, "HeaderLabels": { "name": ["存在", "学籍番号", "氏名", "性別", "グループ", "タグ"], "description": "点呼テーブルの列タイトル", @@ -311,6 +335,14 @@ "name": "选择奖池", "description": "选择要显示的抽奖奖池", }, + "search": { + "name": "搜索名单", + "description": "在名单中搜索奖品", + }, + "search_placeholder": { + "name": "输入序号、奖品、权重、标签或数量进行筛选", + "description": "搜索框占位符", + }, "HeaderLabels": { "name": ["存在", "序号", "奖品", "权重", "标签", "数量"], "description": "抽奖表格的列标题", @@ -340,6 +372,14 @@ "name": "Select pool", "description": "Select the pool to show", }, + "search": { + "name": "Search list", + "description": "Search prizes in the list", + }, + "search_placeholder": { + "name": "Filter by serial, prize, weight, tags or count", + "description": "Search box placeholder", + }, "HeaderLabels": { "name": { "0": "Exist", @@ -379,6 +419,14 @@ "name": "賞プールを選択", "description": "表示する抽選賞プールを選択", }, + "search": { + "name": "リスト検索", + "description": "リスト内の賞品を検索", + }, + "search_placeholder": { + "name": "番号、賞品、重み、タグ、数量で絞り込み", + "description": "検索ボックスのプレースホルダー", + }, "HeaderLabels": { "name": ["存在", "番号", "賞品", "重み", "タグ", "数量"], "description": "抽選テーブルの列タイトル", diff --git a/app/view/settings/list_management/lottery_table.py b/app/view/settings/list_management/lottery_table.py index 100d9764..8626ddf3 100644 --- a/app/view/settings/list_management/lottery_table.py +++ b/app/view/settings/list_management/lottery_table.py @@ -35,9 +35,17 @@ def __init__(self, parent=None): self.parent = parent self.setTitle(get_content_name_async("lottery_table", "title")) self.setBorderRadius(8) + self._search_timer = QTimer(self) + self._search_timer.setSingleShot(True) + self._search_timer.setInterval(300) + self._search_timer.timeout.connect(self._apply_search_filter) + # 创建抽奖名单选择区域 QTimer.singleShot(APPLY_DELAY, self.create_lottery_selection) + # 创建搜索栏 + QTimer.singleShot(APPLY_DELAY, self.create_search_bar) + # 创建表格区域 QTimer.singleShot(APPLY_DELAY, self.create_table) @@ -86,6 +94,46 @@ def create_lottery_selection(self): self.lottery_comboBox, ) + def create_search_bar(self): + """创建搜索栏""" + self.search_line_edit = SearchLineEdit() + self.search_line_edit.setPlaceholderText( + get_content_name_async("lottery_table", "search_placeholder") + ) + self.search_line_edit.setClearButtonEnabled(True) + self.search_line_edit.textChanged.connect(self._on_search_text_changed) + + self.addGroup( + get_theme_icon("ic_fluent_search_20_filled"), + get_content_name_async("lottery_table", "search"), + get_content_description_async("lottery_table", "search"), + self.search_line_edit, + ) + + def _on_search_text_changed(self): + """搜索文本变化时启动防抖计时器""" + self._search_timer.start() + + def _apply_search_filter(self): + """根据搜索关键词过滤表格行""" + if not hasattr(self, "table") or self.table is None: + return + + keyword = self.search_line_edit.text().strip().lower() + + for row in range(self.table.rowCount()): + if not keyword: + self.table.setRowHidden(row, False) + continue + + matched = False + for col in range(self.table.columnCount()): + item = self.table.item(row, col) + if item and keyword in item.text().lower(): + matched = True + break + self.table.setRowHidden(row, not matched) + def create_table(self): """创建表格区域""" # 创建表格 @@ -283,6 +331,7 @@ def refresh_data(self): finally: # 恢复信号 self.table.blockSignals(False) + self._apply_search_filter() def save_table_data(self, row, col): """保存表格编辑的数据""" diff --git a/app/view/settings/list_management/roll_call_table.py b/app/view/settings/list_management/roll_call_table.py index b9df18e7..6b5de6f6 100644 --- a/app/view/settings/list_management/roll_call_table.py +++ b/app/view/settings/list_management/roll_call_table.py @@ -35,9 +35,17 @@ def __init__(self, parent=None): self.parent = parent self.setTitle(get_content_name_async("roll_call_table", "title")) self.setBorderRadius(8) + self._search_timer = QTimer(self) + self._search_timer.setSingleShot(True) + self._search_timer.setInterval(300) + self._search_timer.timeout.connect(self._apply_search_filter) + # 创建班级选择区域 QTimer.singleShot(APPLY_DELAY, self.create_class_selection) + # 创建搜索栏 + QTimer.singleShot(APPLY_DELAY, self.create_search_bar) + # 创建表格区域 QTimer.singleShot(APPLY_DELAY, self.create_table) @@ -80,6 +88,46 @@ def create_class_selection(self): self.class_comboBox, ) + def create_search_bar(self): + """创建搜索栏""" + self.search_line_edit = SearchLineEdit() + self.search_line_edit.setPlaceholderText( + get_content_name_async("roll_call_table", "search_placeholder") + ) + self.search_line_edit.setClearButtonEnabled(True) + self.search_line_edit.textChanged.connect(self._on_search_text_changed) + + self.addGroup( + get_theme_icon("ic_fluent_search_20_filled"), + get_content_name_async("roll_call_table", "search"), + get_content_description_async("roll_call_table", "search"), + self.search_line_edit, + ) + + def _on_search_text_changed(self): + """搜索文本变化时启动防抖计时器""" + self._search_timer.start() + + def _apply_search_filter(self): + """根据搜索关键词过滤表格行""" + if not hasattr(self, "table") or self.table is None: + return + + keyword = self.search_line_edit.text().strip().lower() + + for row in range(self.table.rowCount()): + if not keyword: + self.table.setRowHidden(row, False) + continue + + matched = False + for col in range(self.table.columnCount()): + item = self.table.item(row, col) + if item and keyword in item.text().lower(): + matched = True + break + self.table.setRowHidden(row, not matched) + def create_table(self): """创建表格区域""" # 创建表格 @@ -272,6 +320,7 @@ def refresh_data(self): finally: # 恢复信号 self.table.blockSignals(False) + self._apply_search_filter() def save_table_data(self, row, col): """保存表格编辑的数据""" From 747d2ee43c7ac0e793c3a1eb9c3e332701d051d8 Mon Sep 17 00:00:00 2001 From: Supercmd Date: Sat, 16 May 2026 09:11:05 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E8=AE=B0=E5=BD=95=E5=AF=BC=E5=87=BA=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=20(#232)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在点名历史记录和抽奖历史记录表格中添加导出按钮, 支持将当前表格数据导出为 xlsx、csv、txt 格式。 - 点名历史记录:支持导出全部记录、按时间查看、个人统计三种模式的数据 - 抽奖历史记录:支持导出全部记录、按时间查看、奖品统计三种模式的数据 - 导出内容与用户当前看到的表格一致(包括筛选和排序状态) - 添加中/英/日三语国际化翻译 - 添加 QFileDialog 翻译配置 Closes #232 --- .gitignore | 1 + app/Language/modules/history.py | 48 ++++++ app/Language/modules/list_management.py | 72 ++++++++- .../settings/history/lottery_history_table.py | 147 ++++++++++++++++++ .../history/roll_call_history_table.py | 147 ++++++++++++++++++ pyproject.toml | 2 +- uv.lock | 14 +- 7 files changed, 417 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 10549a19..cbea4b57 100644 --- a/.gitignore +++ b/.gitignore @@ -184,6 +184,7 @@ cython_debug/ # VSCode .vscode/settings.json +.vscode/launch.json /.trae /app/config /app/config diff --git a/app/Language/modules/history.py b/app/Language/modules/history.py index 44ddae27..f645d346 100644 --- a/app/Language/modules/history.py +++ b/app/Language/modules/history.py @@ -226,6 +226,14 @@ "name": "公平抽取", "description": "公平抽取模式", }, + "export": { + "name": "导出记录", + "description": "将当前表格数据导出为文件", + }, + "export_button": { + "name": "导出", + "description": "导出按钮文本", + }, }, "EN_US": { "title": { @@ -320,6 +328,14 @@ "name": "Fair pick", "description": "Fair picking mode", }, + "export": { + "name": "Export records", + "description": "Export current table data to a file", + }, + "export_button": { + "name": "Export", + "description": "Export button text", + }, "select_weight": { "name": "Show weight", "description": "Whether to show weight in table", @@ -419,6 +435,14 @@ "name": "公平抽出", "description": "公平抽出モード", }, + "export": { + "name": "記録をエクスポート", + "description": "現在のテーブルデータをファイルにエクスポート", + }, + "export_button": { + "name": "エクスポート", + "description": "エクスポートボタンテキスト", + }, "select_weight": { "name": "重みを表示", "description": "テーブルに重みを表示するかどうか", @@ -460,6 +484,14 @@ "name": ["抽奖时间", "抽取数量", "课程", "权重"], "description": "抽奖历史记录表格列标题(单次记录)", }, + "export": { + "name": "导出记录", + "description": "将当前表格数据导出为文件", + }, + "export_button": { + "name": "导出", + "description": "导出按钮文本", + }, }, "EN_US": { "title": { @@ -508,6 +540,14 @@ }, "description": "Lottery history table title column header weight (individual)", }, + "export": { + "name": "Export records", + "description": "Export current table data to a file", + }, + "export_button": { + "name": "Export", + "description": "Export button text", + }, }, "JA_JP": { "title": { @@ -556,5 +596,13 @@ }, "description": "抽選履歴テーブルの列タイトル(単一記録)", }, + "export": { + "name": "記録をエクスポート", + "description": "現在のテーブルデータをファイルにエクスポート", + }, + "export_button": { + "name": "エクスポート", + "description": "エクスポートボタンテキスト", + }, }, } diff --git a/app/Language/modules/list_management.py b/app/Language/modules/list_management.py index 5f9d56f2..2722d6f2 100644 --- a/app/Language/modules/list_management.py +++ b/app/Language/modules/list_management.py @@ -797,7 +797,17 @@ "name": "Excel 文件 (*.xlsx);;CSV 文件 (*.csv);;TXT 文件(仅姓名) (*.txt)", "description": "保存学生名单对话框过滤器", }, - } + }, + "export_history": { + "caption": { + "name": "导出点名记录", + "description": "导出点名记录对话框标题", + }, + "filter": { + "name": "Excel 文件 (*.xlsx);;CSV 文件 (*.csv);;TXT 文件 (*.txt)", + "description": "导出点名记录对话框过滤器", + }, + }, }, "lottery": { "export_prize_name": { @@ -809,7 +819,17 @@ "name": "Excel 文件 (*.xlsx);;CSV 文件 (*.csv);;TXT 文件(仅奖品名) (*.txt)", "description": "保存奖品名单对话框过滤器", }, - } + }, + "export_history": { + "caption": { + "name": "导出抽奖记录", + "description": "导出抽奖记录对话框标题", + }, + "filter": { + "name": "Excel 文件 (*.xlsx);;CSV 文件 (*.csv);;TXT 文件 (*.txt)", + "description": "导出抽奖记录对话框过滤器", + }, + }, }, }, "EN_US": { @@ -823,7 +843,17 @@ "name": "Excel files (*.xlsx);;CSV files (*.csv);;TXT files (name only) (*.txt)", "description": "Save student list dialog filter", }, - } + }, + "export_history": { + "caption": { + "name": "Export picking records", + "description": "Export picking records dialog title", + }, + "filter": { + "name": "Excel files (*.xlsx);;CSV files (*.csv);;TXT files (*.txt)", + "description": "Export picking records dialog filter", + }, + }, }, "lottery": { "export_prize_name": { @@ -835,7 +865,17 @@ "name": "Excel files (*.xlsx);;CSV files (*.csv);;TXT files (only prizes) (*.txt)", "description": "Save prize list dialog filter", }, - } + }, + "export_history": { + "caption": { + "name": "Export lottery records", + "description": "Export lottery records dialog title", + }, + "filter": { + "name": "Excel files (*.xlsx);;CSV files (*.csv);;TXT files (*.txt)", + "description": "Export lottery records dialog filter", + }, + }, }, }, "JA_JP": { @@ -849,7 +889,17 @@ "name": "Excelファイル (*.xlsx);;CSVファイル (*.csv);;TXTファイル(氏名のみ) (*.txt)", "description": "学生リスト保存ダイアログフィルター", }, - } + }, + "export_history": { + "caption": { + "name": "点呼記録をエクスポート", + "description": "点呼記録エクスポートダイアログタイトル", + }, + "filter": { + "name": "Excelファイル (*.xlsx);;CSVファイル (*.csv);;TXTファイル (*.txt)", + "description": "点呼記録エクスポートダイアログフィルター", + }, + }, }, "lottery": { "export_prize_name": { @@ -861,7 +911,17 @@ "name": "Excelファイル (*.xlsx);;CSVファイル (*.csv);;TXTファイル(賞品名のみ) (*.txt)", "description": "賞品リスト保存ダイアログフィルター", }, - } + }, + "export_history": { + "caption": { + "name": "抽選記録をエクスポート", + "description": "抽選記録エクスポートダイアログタイトル", + }, + "filter": { + "name": "Excelファイル (*.xlsx);;CSVファイル (*.csv);;TXTファイル (*.txt)", + "description": "抽選記録エクスポートダイアログフィルター", + }, + }, }, }, } diff --git a/app/view/settings/history/lottery_history_table.py b/app/view/settings/history/lottery_history_table.py index 0037c0c2..182bf84d 100644 --- a/app/view/settings/history/lottery_history_table.py +++ b/app/view/settings/history/lottery_history_table.py @@ -141,6 +141,20 @@ def create_pool_selection(self): self.mode_subject_widget, ) + # 创建导出按钮 + self.export_button = PushButton( + get_content_name_async("lottery_history_table", "export_button") + ) + self.export_button.setFixedWidth(120) + self.export_button.clicked.connect(self.export_history_data) + + self.addGroup( + get_theme_icon("ic_fluent_document_arrow_down_20_filled"), + get_content_name_async("lottery_history_table", "export"), + get_content_description_async("lottery_history_table", "export"), + self.export_button, + ) + def create_table(self): """创建表格区域""" # 创建表格 @@ -977,3 +991,136 @@ def _update_subject_list(self): except Exception as e: logger.exception(f"更新课程列表失败: {e}") self.available_subjects = [] + + def export_history_data(self): + """导出当前表格数据到文件""" + if not self.current_pool_name: + return + + if self.table.rowCount() == 0: + return + + file_path, selected_filter = QFileDialog.getSaveFileName( + self, + get_any_position_value_async( + "qfiledialog", + "lottery", + "export_history", + "caption", + "name", + ), + f"{self.current_pool_name}_抽奖记录-SecRandom", + get_any_position_value_async( + "qfiledialog", + "lottery", + "export_history", + "filter", + "name", + ), + ) + + if not file_path: + return + + export_type = ( + "excel" + if "Excel 文件 (*.xlsx)" in selected_filter + else "csv" + if "CSV 文件 (*.csv)" in selected_filter + else "txt" + ) + + if export_type == "excel" and not file_path.endswith(".xlsx"): + file_path += ".xlsx" + elif export_type == "csv" and not file_path.endswith(".csv"): + file_path += ".csv" + elif export_type == "txt" and not file_path.endswith(".txt"): + file_path += ".txt" + + try: + headers = [] + for col in range(self.table.columnCount()): + header_item = self.table.horizontalHeaderItem(col) + headers.append(header_item.text() if header_item else f"列{col}") + + export_data = [] + for row in range(self.table.rowCount()): + row_data = {} + for col in range(self.table.columnCount()): + item = self.table.item(row, col) + row_data[headers[col]] = item.text() if item else "" + export_data.append(row_data) + + if export_type == "excel": + import pandas as pd + + df = pd.DataFrame(export_data) + df.to_excel(file_path, index=False, engine="openpyxl") + elif export_type == "csv": + import pandas as pd + + df = pd.DataFrame(export_data) + df.to_csv(file_path, index=False, encoding="utf-8-sig") + else: + name_col_idx = None + for col in range(self.table.columnCount()): + header_item = self.table.horizontalHeaderItem(col) + if header_item and header_item.text() in ("名称", "Name"): + name_col_idx = col + break + + with open(file_path, "w", encoding="utf-8") as f: + for row in range(self.table.rowCount()): + if name_col_idx is not None: + item = self.table.item(row, name_col_idx) + f.write(f"{item.text()}\n" if item else "\n") + else: + for col in range(self.table.columnCount()): + item = self.table.item(row, col) + f.write(f"{item.text()}\t" if item else "\t") + f.write("\n") + + config = NotificationConfig( + title=get_any_position_value_async( + "notification", + "lottery", + "export", + "title", + "success", + "name", + ), + content=get_any_position_value_async( + "notification", + "lottery", + "export", + "content", + "success", + "name", + ).format(path=file_path), + duration=3000, + ) + show_notification(NotificationType.SUCCESS, config, parent=self) + logger.info(f"抽奖历史记录导出成功: {file_path}") + + except Exception as e: + logger.exception(f"导出抽奖历史记录失败: {e}") + config = NotificationConfig( + title=get_any_position_value_async( + "notification", + "lottery", + "export", + "title", + "failure", + "name", + ), + content=get_any_position_value_async( + "notification", + "lottery", + "export", + "content", + "error", + "name", + ).format(message=str(e)), + duration=3000, + ) + show_notification(NotificationType.ERROR, config, parent=self) diff --git a/app/view/settings/history/roll_call_history_table.py b/app/view/settings/history/roll_call_history_table.py index 63cbcde4..f49841a3 100644 --- a/app/view/settings/history/roll_call_history_table.py +++ b/app/view/settings/history/roll_call_history_table.py @@ -147,6 +147,20 @@ def create_class_selection(self): self.mode_subject_widget, ) + # 创建导出按钮 + self.export_button = PushButton( + get_content_name_async("roll_call_history_table", "export_button") + ) + self.export_button.setFixedWidth(120) + self.export_button.clicked.connect(self.export_history_data) + + self.addGroup( + get_theme_icon("ic_fluent_document_arrow_down_20_filled"), + get_content_name_async("roll_call_history_table", "export"), + get_content_description_async("roll_call_history_table", "export"), + self.export_button, + ) + def create_table(self): """创建表格区域""" # 创建表格 @@ -1143,3 +1157,136 @@ def update_table_headers(self): self.table.setColumnCount(len(headers)) self.table.setHorizontalHeaderLabels(headers) + + def export_history_data(self): + """导出当前表格数据到文件""" + if not self.current_class_name: + return + + if self.table.rowCount() == 0: + return + + file_path, selected_filter = QFileDialog.getSaveFileName( + self, + get_any_position_value_async( + "qfiledialog", + "roll_call", + "export_history", + "caption", + "name", + ), + f"{self.current_class_name}_点名记录-SecRandom", + get_any_position_value_async( + "qfiledialog", + "roll_call", + "export_history", + "filter", + "name", + ), + ) + + if not file_path: + return + + export_type = ( + "excel" + if "Excel 文件 (*.xlsx)" in selected_filter + else "csv" + if "CSV 文件 (*.csv)" in selected_filter + else "txt" + ) + + if export_type == "excel" and not file_path.endswith(".xlsx"): + file_path += ".xlsx" + elif export_type == "csv" and not file_path.endswith(".csv"): + file_path += ".csv" + elif export_type == "txt" and not file_path.endswith(".txt"): + file_path += ".txt" + + try: + headers = [] + for col in range(self.table.columnCount()): + header_item = self.table.horizontalHeaderItem(col) + headers.append(header_item.text() if header_item else f"列{col}") + + export_data = [] + for row in range(self.table.rowCount()): + row_data = {} + for col in range(self.table.columnCount()): + item = self.table.item(row, col) + row_data[headers[col]] = item.text() if item else "" + export_data.append(row_data) + + if export_type == "excel": + import pandas as pd + + df = pd.DataFrame(export_data) + df.to_excel(file_path, index=False, engine="openpyxl") + elif export_type == "csv": + import pandas as pd + + df = pd.DataFrame(export_data) + df.to_csv(file_path, index=False, encoding="utf-8-sig") + else: + name_col_idx = None + for col in range(self.table.columnCount()): + header_item = self.table.horizontalHeaderItem(col) + if header_item and header_item.text() in ("姓名", "Name"): + name_col_idx = col + break + + with open(file_path, "w", encoding="utf-8") as f: + for row in range(self.table.rowCount()): + if name_col_idx is not None: + item = self.table.item(row, name_col_idx) + f.write(f"{item.text()}\n" if item else "\n") + else: + for col in range(self.table.columnCount()): + item = self.table.item(row, col) + f.write(f"{item.text()}\t" if item else "\t") + f.write("\n") + + config = NotificationConfig( + title=get_any_position_value_async( + "notification", + "roll_call", + "export", + "title", + "success", + "name", + ), + content=get_any_position_value_async( + "notification", + "roll_call", + "export", + "content", + "success", + "name", + ).format(path=file_path), + duration=3000, + ) + show_notification(NotificationType.SUCCESS, config, parent=self) + logger.info(f"点名历史记录导出成功: {file_path}") + + except Exception as e: + logger.exception(f"导出点名历史记录失败: {e}") + config = NotificationConfig( + title=get_any_position_value_async( + "notification", + "roll_call", + "export", + "title", + "failure", + "name", + ), + content=get_any_position_value_async( + "notification", + "roll_call", + "export", + "content", + "error", + "name", + ).format(message=str(e)), + duration=3000, + ) + show_notification(NotificationType.ERROR, config, parent=self) diff --git a/pyproject.toml b/pyproject.toml index abbc07fa..6b69bead 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires-python = "==3.13.5" dependencies = [ # === UI / 界面 === - "PySide6-Fluent-Widgets==1.11.0", + "PySide6-Fluent-Widgets==1.11.2", "pyside6>6.6.3.1", "pysidesix-frameless-window>=0.7.4", "darkdetect==0.8.0", diff --git a/uv.lock b/uv.lock index 06468682..46c3842e 100644 --- a/uv.lock +++ b/uv.lock @@ -3602,30 +3602,30 @@ wheels = [ [[package]] name = "pyside6-fluent-widgets" -version = "1.11.0" +version = "1.11.2" source = { registry = "https://mirrors.aliyun.com/pypi/simple" } dependencies = [ { name = "darkdetect" }, { name = "pyside6" }, { name = "pysidesix-frameless-window" }, ] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f7/79/89ede298a98552317b71c199f7001f728401ea5e5f05ccece5cb390f71f6/pyside6_fluent_widgets-1.11.0.tar.gz", hash = "sha256:ac74b7d08fd78b06304416ac6d62cc47818949d79605899eb64c53bd99c6c322" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f1/22/01a72ab00873fac2575e8045cd4dfcb003afc0f0764982c706817be5629a/pyside6_fluent_widgets-1.11.2.tar.gz", hash = "sha256:cf49ff76b9b2ad1dc24f071a1b2a3f5f0a67d7adf655915071ddfb7342caf175" } wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/3e/4a/49d2b3b7bb35e006de4a3a2192c78a005cd1d44bd18e33ae12fc6cb3672c/pyside6_fluent_widgets-1.11.0-py3-none-any.whl", hash = "sha256:d13763304137fb843b955dbaad9e0ffafca306018769d0ea9094b9f707ff7938" }, + { url = "https://mirrors.aliyun.com/pypi/packages/78/cb/ad62113621b3e619e4fa069a383e5060a6169e93a9df73f15f2cf68bdf5a/pyside6_fluent_widgets-1.11.2-py3-none-any.whl", hash = "sha256:7d3bb3d79b743cbb16eef0a0471f1d0c47d6c8c8dd78402820bd3cdae4da1ea0" }, ] [[package]] name = "pysidesix-frameless-window" -version = "0.7.7" +version = "0.8.1" source = { registry = "https://mirrors.aliyun.com/pypi/simple" } dependencies = [ { name = "pycocoa", marker = "sys_platform == 'darwin'" }, { name = "pyobjc", marker = "sys_platform == 'darwin'" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/cf/ba/c41e25e8884224d15b14ffad34c921f350a6b0cfce823f150a514a0d3788/pysidesix_frameless_window-0.7.7.tar.gz", hash = "sha256:4f975bae89a6ebd8babeed693341f242b268c94816e81602e026eb95703d4eb4" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/e7/44/ee4b9ead46ec5fcc4d9a303f6ac82cb17b0e188bfe629ef962c4046cded5/pysidesix_frameless_window-0.8.1.tar.gz", hash = "sha256:95eefa64abdaca9d730bc097fd39e2cd07d3443a47a1645cc936a0076996d7cd" } wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/70/e2/fb1e36cbd8ed5c0db161ace24ad40b5278ac9af6fecc9bcd6ab68712fb1c/pysidesix_frameless_window-0.7.7-py3-none-any.whl", hash = "sha256:cd6b2961cc30d9290f84462bc59de674bea6a72df5d36fb57d5fb49d660c13e8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/60/02/13dc76880e9f54102b8d4859a1a14061c6914c12d8c8554dc49ef55ab209/pysidesix_frameless_window-0.8.1-py3-none-any.whl", hash = "sha256:0445e7ce095c3631893ec54edb470a47c2bb05ea64db736f446211550b7c2237" }, ] [[package]] @@ -3888,7 +3888,7 @@ requires-dist = [ { name = "pypng", specifier = "~=0.20220715.0" }, { name = "pyqrcode", specifier = "~=1.2.1" }, { name = "pyside6", specifier = ">6.6.3.1" }, - { name = "pyside6-fluent-widgets", specifier = "==1.11.0" }, + { name = "pyside6-fluent-widgets", specifier = "==1.11.2" }, { name = "pysidesix-frameless-window", specifier = ">=0.7.4" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=6.0" }, { name = "pythonnet", specifier = ">=3.0.5" }, From 2c5f549bc99ffa6fb5456a14ab0842d135a8d482 Mon Sep 17 00:00:00 2001 From: Supercmd Date: Sat, 16 May 2026 09:20:19 +0800 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=E7=BC=BA=E5=A4=B1?= =?UTF-8?q?=E7=9A=84=20config=20=E5=AF=BC=E5=85=A5=E4=BF=AE=E5=A4=8D=20Not?= =?UTF-8?q?ificationConfig=20=E6=9C=AA=E5=AE=9A=E4=B9=89=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/view/settings/history/lottery_history_table.py | 1 + app/view/settings/history/roll_call_history_table.py | 1 + 2 files changed, 2 insertions(+) diff --git a/app/view/settings/history/lottery_history_table.py b/app/view/settings/history/lottery_history_table.py index 182bf84d..39b34c78 100644 --- a/app/view/settings/history/lottery_history_table.py +++ b/app/view/settings/history/lottery_history_table.py @@ -13,6 +13,7 @@ from app.tools.variable import * from app.tools.path_utils import * from app.tools.personalised import * +from app.tools.config import * from app.tools.settings_default import * from app.tools.settings_access import * from app.Language.obtain_language import * diff --git a/app/view/settings/history/roll_call_history_table.py b/app/view/settings/history/roll_call_history_table.py index f49841a3..9b99ddf7 100644 --- a/app/view/settings/history/roll_call_history_table.py +++ b/app/view/settings/history/roll_call_history_table.py @@ -13,6 +13,7 @@ from app.tools.variable import * from app.tools.path_utils import * from app.tools.personalised import * +from app.tools.config import * from app.tools.settings_default import * from app.tools.settings_access import * from app.Language.obtain_language import * From a3018b62e1e0174cd5f6d2e981bf0dd7386210a0 Mon Sep 17 00:00:00 2001 From: Supercmd Date: Sat, 16 May 2026 09:22:55 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E6=89=A9=E5=B1=95=E5=90=8D=E5=8C=B9=E9=85=8D=E6=9B=BF=E4=BB=A3?= =?UTF-8?q?=E7=A1=AC=E7=BC=96=E7=A0=81=E4=B8=AD=E6=96=87=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E4=B8=B2=E5=88=A4=E6=96=AD=E5=AF=BC=E5=87=BA=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复非中文语言环境下导出格式判断失败的问题。 将 'Excel 文件 (*.xlsx)' 改为 '.xlsx' 匹配, 确保所有语言下都能正确识别导出格式。 --- app/view/settings/history/lottery_history_table.py | 4 ++-- app/view/settings/history/roll_call_history_table.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/view/settings/history/lottery_history_table.py b/app/view/settings/history/lottery_history_table.py index 39b34c78..51336cb9 100644 --- a/app/view/settings/history/lottery_history_table.py +++ b/app/view/settings/history/lottery_history_table.py @@ -1025,9 +1025,9 @@ def export_history_data(self): export_type = ( "excel" - if "Excel 文件 (*.xlsx)" in selected_filter + if ".xlsx" in selected_filter else "csv" - if "CSV 文件 (*.csv)" in selected_filter + if ".csv" in selected_filter else "txt" ) diff --git a/app/view/settings/history/roll_call_history_table.py b/app/view/settings/history/roll_call_history_table.py index 9b99ddf7..55e98d6a 100644 --- a/app/view/settings/history/roll_call_history_table.py +++ b/app/view/settings/history/roll_call_history_table.py @@ -1191,9 +1191,9 @@ def export_history_data(self): export_type = ( "excel" - if "Excel 文件 (*.xlsx)" in selected_filter + if ".xlsx" in selected_filter else "csv" - if "CSV 文件 (*.csv)" in selected_filter + if ".csv" in selected_filter else "txt" ) From 3431520ee30d46809816c007b8a61b5ca1513830 Mon Sep 17 00:00:00 2001 From: Supercmd Date: Wed, 20 May 2026 21:28:12 +0800 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E7=BF=BB=E8=AF=91=E7=BC=BA=E5=A4=B1=E5=AF=BC=E8=87=B4=E7=9A=84?= =?UTF-8?q?=E5=B4=A9=E6=BA=83=E5=B9=B6=E8=A1=A5=E5=85=85=20JA=5FJP=20?= =?UTF-8?q?=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 notification 模块补充缺失的 JA_JP 翻译 - 为 get_any_position_value_async 返回值添加空值保护, 防止翻译缺失时 .format() 调用 None 导致 AttributeError --- app/Language/modules/list_management.py | 178 ++++++++++++++++++ .../settings/history/lottery_history_table.py | 8 +- .../history/roll_call_history_table.py | 8 +- 3 files changed, 186 insertions(+), 8 deletions(-) diff --git a/app/Language/modules/list_management.py b/app/Language/modules/list_management.py index 2722d6f2..02365872 100644 --- a/app/Language/modules/list_management.py +++ b/app/Language/modules/list_management.py @@ -782,6 +782,184 @@ }, }, }, + "JA_JP": { + "roll_call": { + "class_name_setting": { + "title": { + "name": "クラス名設定", + "description": "クラス名設定通知タイトル", + }, + "content": { + "name": "クラス名設定ウィンドウを開きました", + "description": "クラス名設定通知内容", + }, + }, + "import_student_name": { + "title": { + "name": "学生リストインポート", + "description": "学生リストインポート通知タイトル", + }, + "content": { + "name": "学生リストインポートウィンドウを開きました", + "description": "学生リストインポート通知内容", + }, + }, + "name_setting": { + "title": { + "name": "氏名設定", + "description": "氏名設定通知タイトル", + }, + "content": { + "name": "氏名設定ウィンドウを開きました", + "description": "氏名設定通知内容", + }, + }, + "gender_setting": { + "title": { + "name": "性別設定", + "description": "性別設定通知タイトル", + }, + "content": { + "name": "性別設定ウィンドウを開きました", + "description": "性別設定通知内容", + }, + }, + "group_setting": { + "title": { + "name": "グループ設定", + "description": "グループ設定通知タイトル", + }, + "content": { + "name": "グループ設定ウィンドウを開きました", + "description": "グループ設定通知内容", + }, + }, + "tag_setting": { + "title": { + "name": "タグ設定", + "description": "タグ設定通知タイトル", + }, + "content": { + "name": "タグ設定ウィンドウを開きました", + "description": "タグ設定通知内容", + }, + }, + "export": { + "title": { + "success": { + "name": "エクスポート成功", + "description": "エクスポート成功通知タイトル", + }, + "failure": { + "name": "エクスポート失敗", + "description": "エクスポート失敗通知タイトル", + }, + }, + "content": { + "success": { + "name": "学生リストをエクスポートしました: {path}", + "description": "エクスポート成功通知内容", + }, + "failure": { + "name": "エクスポートするクラスを先に選択してください", + "description": "エクスポート失敗通知内容(クラス未選択)", + }, + "error": { + "name": "{message}", + "description": "エクスポートエラー通知内容", + }, + }, + }, + }, + "lottery": { + "pool_name_setting": { + "title": { + "name": "賞プール名設定", + "description": "賞プール名設定通知タイトル", + }, + "content": { + "name": "賞プール名設定ウィンドウを開きました", + "description": "賞プール名設定通知内容", + }, + }, + "import_prize_name": { + "title": { + "name": "賞品リストインポート", + "description": "賞品リストインポート通知タイトル", + }, + "content": { + "name": "賞品リストインポートウィンドウを開きました", + "description": "賞品リストインポート通知内容", + }, + }, + "prize_setting": { + "title": { + "name": "賞品設定", + "description": "賞品設定通知タイトル", + }, + "content": { + "name": "賞品設定ウィンドウを開きました", + "description": "賞品設定通知内容", + }, + }, + "prize_weight_setting": { + "title": { + "name": "賞品重み設定", + "description": "賞品重み設定通知タイトル", + }, + "content": { + "name": "賞品重み設定ウィンドウを開きました", + "description": "賞品重み設定通知内容", + }, + }, + "tag_setting": { + "title": { + "name": "タグ設定", + "description": "タグ設定通知タイトル", + }, + "content": { + "name": "タグ設定ウィンドウを開きました", + "description": "タグ設定通知内容", + }, + }, + "prize_count_setting": { + "title": { + "name": "賞品数量設定", + "description": "賞品数量設定通知タイトル", + }, + "content": { + "name": "賞品数量設定ウィンドウを開きました", + "description": "賞品数量設定通知内容", + }, + }, + "export": { + "title": { + "success": { + "name": "エクスポート成功", + "description": "エクスポート成功通知タイトル", + }, + "failure": { + "name": "エクスポート失敗", + "description": "エクスポート失敗通知タイトル", + }, + }, + "content": { + "success": { + "name": "賞品リストをエクスポートしました: {path}", + "description": "エクスポート成功通知内容", + }, + "failure": { + "name": "エクスポートする賞プールを先に選択してください", + "description": "エクスポート失敗通知内容(賞プール未選択)", + }, + "error": { + "name": "{message}", + "description": "エクスポートエラー通知内容", + }, + }, + }, + }, + }, } # QFileDialog 文本配置 diff --git a/app/view/settings/history/lottery_history_table.py b/app/view/settings/history/lottery_history_table.py index 51336cb9..f4abba89 100644 --- a/app/view/settings/history/lottery_history_table.py +++ b/app/view/settings/history/lottery_history_table.py @@ -1090,14 +1090,14 @@ def export_history_data(self): "success", "name", ), - content=get_any_position_value_async( + content=(get_any_position_value_async( "notification", "lottery", "export", "content", "success", "name", - ).format(path=file_path), + ) or "").format(path=file_path), duration=3000, ) show_notification(NotificationType.SUCCESS, config, parent=self) @@ -1114,14 +1114,14 @@ def export_history_data(self): "failure", "name", ), - content=get_any_position_value_async( + content=(get_any_position_value_async( "notification", "lottery", "export", "content", "error", "name", - ).format(message=str(e)), + ) or "").format(message=str(e)), duration=3000, ) show_notification(NotificationType.ERROR, config, parent=self) diff --git a/app/view/settings/history/roll_call_history_table.py b/app/view/settings/history/roll_call_history_table.py index 55e98d6a..3a272274 100644 --- a/app/view/settings/history/roll_call_history_table.py +++ b/app/view/settings/history/roll_call_history_table.py @@ -1256,14 +1256,14 @@ def export_history_data(self): "success", "name", ), - content=get_any_position_value_async( + content=(get_any_position_value_async( "notification", "roll_call", "export", "content", "success", "name", - ).format(path=file_path), + ) or "").format(path=file_path), duration=3000, ) show_notification(NotificationType.SUCCESS, config, parent=self) @@ -1280,14 +1280,14 @@ def export_history_data(self): "failure", "name", ), - content=get_any_position_value_async( + content=(get_any_position_value_async( "notification", "roll_call", "export", "content", "error", "name", - ).format(message=str(e)), + ) or "").format(message=str(e)), duration=3000, ) show_notification(NotificationType.ERROR, config, parent=self) From f7b22325599f7354c90a7d4bfc13896e90ce8216 Mon Sep 17 00:00:00 2001 From: Supercmd Date: Wed, 20 May 2026 22:23:06 +0800 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=AF=BC=E5=87=BA=E7=9A=84=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复导出仅导出已加载行而非全部数据的问题(Critical) 导出前检查是否所有数据已加载,未加载时强制加载全部数据 - 修复预期导出错误使用 logger.exception 导致误报 Sentry 的问题 改用 logger.error() 符合项目 Sentry 日志策略 - 修复默认文件名硬编码中文字符串的问题 添加 export_default_filename i18n 键,支持三语 - 修复 TXT 导出硬编码中文表头匹配的问题 改用列索引识别名称列,不再依赖表头文本 - 抽取共享导出工具 export_utils.py 消除代码重复 - 添加 get_path() 和 mkdir() 路径保护,与项目既有模式一致 --- app/Language/modules/history.py | 24 ++++ app/view/settings/history/export_utils.py | 128 +++++++++++++++++ .../settings/history/lottery_history_table.py | 136 ++---------------- .../history/roll_call_history_table.py | 136 ++---------------- 4 files changed, 172 insertions(+), 252 deletions(-) create mode 100644 app/view/settings/history/export_utils.py diff --git a/app/Language/modules/history.py b/app/Language/modules/history.py index f645d346..4aeef9b4 100644 --- a/app/Language/modules/history.py +++ b/app/Language/modules/history.py @@ -234,6 +234,10 @@ "name": "导出", "description": "导出按钮文本", }, + "export_default_filename": { + "name": "点名记录", + "description": "导出文件默认文件名中的描述文字", + }, }, "EN_US": { "title": { @@ -336,6 +340,10 @@ "name": "Export", "description": "Export button text", }, + "export_default_filename": { + "name": "Picking records", + "description": "Description text in the default export filename", + }, "select_weight": { "name": "Show weight", "description": "Whether to show weight in table", @@ -443,6 +451,10 @@ "name": "エクスポート", "description": "エクスポートボタンテキスト", }, + "export_default_filename": { + "name": "点呼記録", + "description": "エクスポートファイルのデフォルトファイル名の説明文", + }, "select_weight": { "name": "重みを表示", "description": "テーブルに重みを表示するかどうか", @@ -492,6 +504,10 @@ "name": "导出", "description": "导出按钮文本", }, + "export_default_filename": { + "name": "抽奖记录", + "description": "导出文件默认文件名中的描述文字", + }, }, "EN_US": { "title": { @@ -548,6 +564,10 @@ "name": "Export", "description": "Export button text", }, + "export_default_filename": { + "name": "Lottery records", + "description": "Description text in the default export filename", + }, }, "JA_JP": { "title": { @@ -604,5 +624,9 @@ "name": "エクスポート", "description": "エクスポートボタンテキスト", }, + "export_default_filename": { + "name": "抽選記録", + "description": "エクスポートファイルのデフォルトファイル名の説明文", + }, }, } diff --git a/app/view/settings/history/export_utils.py b/app/view/settings/history/export_utils.py new file mode 100644 index 00000000..d66bc7fa --- /dev/null +++ b/app/view/settings/history/export_utils.py @@ -0,0 +1,128 @@ +from loguru import logger +from PySide6.QtWidgets import QFileDialog + +from app.tools.config import NotificationConfig, NotificationType, show_notification +from app.tools.path_utils import get_path +from app.Language.obtain_language import ( + get_any_position_value_async, + get_content_name_async, +) + + +def _get_name_column_index(current_mode: int): + if current_mode == 0: + return 1 + elif current_mode == 1: + return 2 + return None + + +def export_history_table_data( + table_widget, + current_mode: int, + i18n_domain: str, + current_name: str, + parent_widget=None, +): + if table_widget.rowCount() == 0: + return + + file_path, selected_filter = QFileDialog.getSaveFileName( + parent_widget, + get_any_position_value_async( + "qfiledialog", i18n_domain, "export_history", "caption", "name" + ), + f"{current_name}_{get_content_name_async(f'{i18n_domain}_history_table', 'export_default_filename')}-SecRandom", + get_any_position_value_async( + "qfiledialog", i18n_domain, "export_history", "filter", "name" + ), + ) + + if not file_path: + return + + export_type = ( + "excel" + if ".xlsx" in selected_filter + else "csv" + if ".csv" in selected_filter + else "txt" + ) + + if export_type == "excel" and not file_path.endswith(".xlsx"): + file_path += ".xlsx" + elif export_type == "csv" and not file_path.endswith(".csv"): + file_path += ".csv" + elif export_type == "txt" and not file_path.endswith(".txt"): + file_path += ".txt" + + try: + target_path = get_path(file_path) + target_path.parent.mkdir(parents=True, exist_ok=True) + + headers = [] + for col in range(table_widget.columnCount()): + header_item = table_widget.horizontalHeaderItem(col) + headers.append(header_item.text() if header_item else f"列{col}") + + export_data = [] + for row in range(table_widget.rowCount()): + row_data = {} + for col in range(table_widget.columnCount()): + item = table_widget.item(row, col) + row_data[headers[col]] = item.text() if item else "" + export_data.append(row_data) + + if export_type == "excel": + import pandas as pd + + df = pd.DataFrame(export_data) + df.to_excel(str(target_path), index=False, engine="openpyxl") + elif export_type == "csv": + import pandas as pd + + df = pd.DataFrame(export_data) + df.to_csv(str(target_path), index=False, encoding="utf-8-sig") + else: + name_col_idx = _get_name_column_index(current_mode) + with open(str(target_path), "w", encoding="utf-8") as f: + for row in range(table_widget.rowCount()): + if name_col_idx is not None: + item = table_widget.item(row, name_col_idx) + f.write(f"{item.text()}\n" if item else "\n") + else: + for col in range(table_widget.columnCount()): + item = table_widget.item(row, col) + f.write(f"{item.text()}\t" if item else "\t") + f.write("\n") + + config = NotificationConfig( + title=get_any_position_value_async( + "notification", i18n_domain, "export", "title", "success", "name" + ), + content=( + get_any_position_value_async( + "notification", i18n_domain, "export", "content", "success", "name" + ) + or "" + ).format(path=file_path), + duration=3000, + ) + show_notification(NotificationType.SUCCESS, config, parent=parent_widget) + logger.info(f"历史记录导出成功: {file_path}") + + except Exception as e: + logger.error(f"导出历史记录失败: {e}") + config = NotificationConfig( + title=get_any_position_value_async( + "notification", i18n_domain, "export", "title", "failure", "name" + ), + content=( + get_any_position_value_async( + "notification", i18n_domain, "export", "content", "error", "name" + ) + or "" + ).format(message=str(e)), + duration=3000, + ) + show_notification(NotificationType.ERROR, config, parent=parent_widget) diff --git a/app/view/settings/history/lottery_history_table.py b/app/view/settings/history/lottery_history_table.py index f4abba89..d1ed0f6c 100644 --- a/app/view/settings/history/lottery_history_table.py +++ b/app/view/settings/history/lottery_history_table.py @@ -25,6 +25,7 @@ get_lottery_session_data, get_lottery_prize_stats_data, ) +from .export_utils import export_history_table_data # ================================================== @@ -994,134 +995,17 @@ def _update_subject_list(self): self.available_subjects = [] def export_history_data(self): - """导出当前表格数据到文件""" if not self.current_pool_name: return - if self.table.rowCount() == 0: - return - - file_path, selected_filter = QFileDialog.getSaveFileName( - self, - get_any_position_value_async( - "qfiledialog", - "lottery", - "export_history", - "caption", - "name", - ), - f"{self.current_pool_name}_抽奖记录-SecRandom", - get_any_position_value_async( - "qfiledialog", - "lottery", - "export_history", - "filter", - "name", - ), - ) - - if not file_path: - return + if self.current_row < self.total_rows: + self.force_load_all = True + self.refresh_data() - export_type = ( - "excel" - if ".xlsx" in selected_filter - else "csv" - if ".csv" in selected_filter - else "txt" + export_history_table_data( + table_widget=self.table, + current_mode=self.current_mode, + i18n_domain="lottery", + current_name=self.current_pool_name, + parent_widget=self, ) - - if export_type == "excel" and not file_path.endswith(".xlsx"): - file_path += ".xlsx" - elif export_type == "csv" and not file_path.endswith(".csv"): - file_path += ".csv" - elif export_type == "txt" and not file_path.endswith(".txt"): - file_path += ".txt" - - try: - headers = [] - for col in range(self.table.columnCount()): - header_item = self.table.horizontalHeaderItem(col) - headers.append(header_item.text() if header_item else f"列{col}") - - export_data = [] - for row in range(self.table.rowCount()): - row_data = {} - for col in range(self.table.columnCount()): - item = self.table.item(row, col) - row_data[headers[col]] = item.text() if item else "" - export_data.append(row_data) - - if export_type == "excel": - import pandas as pd - - df = pd.DataFrame(export_data) - df.to_excel(file_path, index=False, engine="openpyxl") - elif export_type == "csv": - import pandas as pd - - df = pd.DataFrame(export_data) - df.to_csv(file_path, index=False, encoding="utf-8-sig") - else: - name_col_idx = None - for col in range(self.table.columnCount()): - header_item = self.table.horizontalHeaderItem(col) - if header_item and header_item.text() in ("名称", "Name"): - name_col_idx = col - break - - with open(file_path, "w", encoding="utf-8") as f: - for row in range(self.table.rowCount()): - if name_col_idx is not None: - item = self.table.item(row, name_col_idx) - f.write(f"{item.text()}\n" if item else "\n") - else: - for col in range(self.table.columnCount()): - item = self.table.item(row, col) - f.write(f"{item.text()}\t" if item else "\t") - f.write("\n") - - config = NotificationConfig( - title=get_any_position_value_async( - "notification", - "lottery", - "export", - "title", - "success", - "name", - ), - content=(get_any_position_value_async( - "notification", - "lottery", - "export", - "content", - "success", - "name", - ) or "").format(path=file_path), - duration=3000, - ) - show_notification(NotificationType.SUCCESS, config, parent=self) - logger.info(f"抽奖历史记录导出成功: {file_path}") - - except Exception as e: - logger.exception(f"导出抽奖历史记录失败: {e}") - config = NotificationConfig( - title=get_any_position_value_async( - "notification", - "lottery", - "export", - "title", - "failure", - "name", - ), - content=(get_any_position_value_async( - "notification", - "lottery", - "export", - "content", - "error", - "name", - ) or "").format(message=str(e)), - duration=3000, - ) - show_notification(NotificationType.ERROR, config, parent=self) diff --git a/app/view/settings/history/roll_call_history_table.py b/app/view/settings/history/roll_call_history_table.py index 3a272274..69bd00b7 100644 --- a/app/view/settings/history/roll_call_history_table.py +++ b/app/view/settings/history/roll_call_history_table.py @@ -27,6 +27,7 @@ get_roll_call_student_stats_data, check_class_has_gender_or_group, ) +from .export_utils import export_history_table_data # ================================================== @@ -1160,134 +1161,17 @@ def update_table_headers(self): self.table.setHorizontalHeaderLabels(headers) def export_history_data(self): - """导出当前表格数据到文件""" if not self.current_class_name: return - if self.table.rowCount() == 0: - return + if self.current_row < self.total_rows: + self.force_load_all = True + self.refresh_data() - file_path, selected_filter = QFileDialog.getSaveFileName( - self, - get_any_position_value_async( - "qfiledialog", - "roll_call", - "export_history", - "caption", - "name", - ), - f"{self.current_class_name}_点名记录-SecRandom", - get_any_position_value_async( - "qfiledialog", - "roll_call", - "export_history", - "filter", - "name", - ), + export_history_table_data( + table_widget=self.table, + current_mode=self.current_mode, + i18n_domain="roll_call", + current_name=self.current_class_name, + parent_widget=self, ) - - if not file_path: - return - - export_type = ( - "excel" - if ".xlsx" in selected_filter - else "csv" - if ".csv" in selected_filter - else "txt" - ) - - if export_type == "excel" and not file_path.endswith(".xlsx"): - file_path += ".xlsx" - elif export_type == "csv" and not file_path.endswith(".csv"): - file_path += ".csv" - elif export_type == "txt" and not file_path.endswith(".txt"): - file_path += ".txt" - - try: - headers = [] - for col in range(self.table.columnCount()): - header_item = self.table.horizontalHeaderItem(col) - headers.append(header_item.text() if header_item else f"列{col}") - - export_data = [] - for row in range(self.table.rowCount()): - row_data = {} - for col in range(self.table.columnCount()): - item = self.table.item(row, col) - row_data[headers[col]] = item.text() if item else "" - export_data.append(row_data) - - if export_type == "excel": - import pandas as pd - - df = pd.DataFrame(export_data) - df.to_excel(file_path, index=False, engine="openpyxl") - elif export_type == "csv": - import pandas as pd - - df = pd.DataFrame(export_data) - df.to_csv(file_path, index=False, encoding="utf-8-sig") - else: - name_col_idx = None - for col in range(self.table.columnCount()): - header_item = self.table.horizontalHeaderItem(col) - if header_item and header_item.text() in ("姓名", "Name"): - name_col_idx = col - break - - with open(file_path, "w", encoding="utf-8") as f: - for row in range(self.table.rowCount()): - if name_col_idx is not None: - item = self.table.item(row, name_col_idx) - f.write(f"{item.text()}\n" if item else "\n") - else: - for col in range(self.table.columnCount()): - item = self.table.item(row, col) - f.write(f"{item.text()}\t" if item else "\t") - f.write("\n") - - config = NotificationConfig( - title=get_any_position_value_async( - "notification", - "roll_call", - "export", - "title", - "success", - "name", - ), - content=(get_any_position_value_async( - "notification", - "roll_call", - "export", - "content", - "success", - "name", - ) or "").format(path=file_path), - duration=3000, - ) - show_notification(NotificationType.SUCCESS, config, parent=self) - logger.info(f"点名历史记录导出成功: {file_path}") - - except Exception as e: - logger.exception(f"导出点名历史记录失败: {e}") - config = NotificationConfig( - title=get_any_position_value_async( - "notification", - "roll_call", - "export", - "title", - "failure", - "name", - ), - content=(get_any_position_value_async( - "notification", - "roll_call", - "export", - "content", - "error", - "name", - ) or "").format(message=str(e)), - duration=3000, - ) - show_notification(NotificationType.ERROR, config, parent=self)