diff --git a/gui_app.py b/gui_app.py index 1b93cf1..f325b31 100644 --- a/gui_app.py +++ b/gui_app.py @@ -2,15 +2,38 @@ import sys import os import re import json +from pathlib import Path from datetime import datetime, timedelta -from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, - QHBoxLayout, QPushButton, QTableWidget, QTableWidgetItem, +from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, + QHBoxLayout, QTableWidget, QTableWidgetItem, QFileDialog, QMessageBox, QHeaderView, QLabel, QMenu, QInputDialog, QDialog, QDialogButtonBox, QFormLayout, - QLineEdit, QSpinBox, QMessageBox, QToolBar, QAction, - QProgressBar) -from PyQt5.QtCore import Qt, QThread, pyqtSignal -from PyQt5.QtGui import QKeySequence + QTextEdit, QAbstractItemView, QStackedWidget, + QGraphicsOpacityEffect, QListWidget) +from PyQt5.QtCore import Qt, QThread, pyqtSignal, QPropertyAnimation, QEvent +from PyQt5.QtGui import QKeySequence, QFont +from qfluentwidgets import ( + setTheme, + Theme, + PushButton, + PrimaryPushButton, + LineEdit, + SpinBox, + ProgressBar, + CardWidget, + InfoBar, + InfoBarPosition, + NavigationInterface, + NavigationItemPosition, + FluentIcon, + ToggleSwitch, + PillPushButton +) + +try: + from qfluentwidgets import FramelessWindow +except ImportError: # 兼容旧版本 + from qfluentwidgets import FluentWindow as FramelessWindow import pandas as pd @@ -19,10 +42,27 @@ class TaskWorker(QThread): finished = pyqtSignal(int, int, list) error = pyqtSignal(str) - def __init__(self, tasks, base_folder_path=None): + def __init__(self, tasks, base_folder_path=None, batch_upload=False, input_delay=0): super().__init__() self.tasks = tasks self.base_folder_path = base_folder_path + self.batch_upload = batch_upload + self.input_delay = input_delay + self._pause = False + self._stop = False + + def pause(self): + self._pause = True + + def resume(self): + self._pause = False + + def stop(self): + self._stop = True + + def _wait_if_paused(self): + while self._pause and not self._stop: + self.msleep(200) def run(self): try: @@ -38,6 +78,10 @@ class TaskWorker(QThread): base_folder_path = getattr(self, 'base_folder_path', None) for idx, data in enumerate(self.tasks, 1): + if self._stop: + error_messages.append("任务已停止") + break + self._wait_if_paused() user_id = data.get('多多 id', '') topics = data.get('话题', '') time_start = data.get('计算后的发布时间', '') @@ -82,8 +126,20 @@ class TaskWorker(QThread): # 使用输入框中的大文件夹路径 folder_path = base_folder_path if base_folder_path and os.path.exists(base_folder_path) else None - logger.info(f"调用 pdd.action(folder_path={folder_path})") - pdd.action(folder_path=folder_path) + if self.batch_upload: + logger.info("批量上传模式:收集当前多多ID的视频文件") + video_items = self.collect_user_videos(folder_path, user_id, url, time_start, ht) + if not video_items: + error_msg = f"任务 {idx}: 未找到多多ID={user_id} 的视频文件" + logger.warning(error_msg) + error_messages.append(error_msg) + fail_count += 1 + continue + logger.info(f"调用 pdd.action1(folder_path=video_items, input_delay={self.input_delay})") + pdd.action1(folder_path=video_items, input_delay=self.input_delay) + else: + logger.info(f"调用 pdd.action(folder_path={folder_path})") + pdd.action(folder_path=folder_path) logger.info(f"任务 {idx} 执行成功") success_count += 1 except Exception as e: @@ -98,8 +154,43 @@ class TaskWorker(QThread): except Exception as e: self.error.emit(str(e)) + @staticmethod + def collect_user_videos(base_folder_path, user_id, url, time_start, ht): + """收集同一多多ID下的视频文件""" + if not base_folder_path or not user_id: + return [] -class MainWindow(QMainWindow): + target_folder = os.path.join(base_folder_path, str(user_id)) + if not os.path.isdir(target_folder): + # 允许在大文件夹下按子目录遍历匹配用户ID + for name in os.listdir(base_folder_path): + candidate = os.path.join(base_folder_path, name) + if os.path.isdir(candidate) and str(user_id) in name: + target_folder = candidate + break + + if not os.path.isdir(target_folder): + return [] + + video_extensions = {'.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.webm'} + video_items = [] + for root, _, files in os.walk(target_folder): + for file in files: + ext = os.path.splitext(file)[1].lower() + if ext in video_extensions: + path = os.path.join(root, file) + video_items.append({ + "path": Path(path), + "url": url, + "time_start": time_start, + "ht": ht + }) + # 按文件名排序,保证顺序稳定 + video_items.sort(key=lambda x: x["path"].name) + return video_items + + +class MainWindow(FramelessWindow): def __init__(self): super().__init__() self.setWindowTitle("拼多多MCN发布管理工具") @@ -113,132 +204,342 @@ class MainWindow(QMainWindow): # 创建主窗口部件 main_widget = QWidget() self.setCentralWidget(main_widget) - - # 创建布局 + + # 根布局:侧边导航 + 内容区 + root_layout = QHBoxLayout() + root_layout.setContentsMargins(10, 10, 10, 10) + root_layout.setSpacing(10) + main_widget.setLayout(root_layout) + + setTheme(Theme.DARK) + + # 侧边导航 + self.nav = NavigationInterface(self) + self.nav.setMinimumWidth(200) + root_layout.addWidget(self.nav) + + # 内容堆栈 + self.stack = QStackedWidget() + root_layout.addWidget(self.stack, 1) + + # 页面容器 + self.main_page = QWidget() + self.log_page = QWidget() + self.stack.addWidget(self.main_page) + self.stack.addWidget(self.log_page) + + # 主页面布局 layout = QVBoxLayout() - main_widget.setLayout(layout) + layout.setSpacing(10) + self.main_page.setLayout(layout) + + # 日志页面布局 + log_page_layout = QVBoxLayout() + log_page_layout.setSpacing(10) + self.log_page.setLayout(log_page_layout) + + # 顶部标题栏(自定义按钮) + title_card = CardWidget() + title_layout = QHBoxLayout(title_card) + title_layout.setContentsMargins(12, 6, 12, 6) + title_card.setObjectName("titleCard") + + nav_toggle = PushButton() + nav_toggle.setIcon(FluentIcon.MENU) + nav_toggle.setFixedWidth(32) + nav_toggle.clicked.connect(self.toggle_nav) + title_layout.addWidget(nav_toggle) + + title_label = QLabel("发布配置工具 - 批量版") + title_font = QFont("Arial", 14, QFont.Bold) + title_label.setFont(title_font) + title_layout.addWidget(title_label) + + pro_badge = QLabel("PRO") + pro_badge.setStyleSheet( + "padding: 2px 6px; border-radius: 6px; " + "background: qlineargradient(x1:0, y1:0, x2:1, y2:0, " + "stop:0 #7f5cff, stop:1 #b37bff); color: #ffffff; font-weight: bold;" + ) + title_layout.addWidget(pro_badge) + title_layout.addStretch() + + self.status_indicator = QLabel("●") + self.status_indicator.setStyleSheet("color: #67C23A; font-weight: bold;") + title_layout.addWidget(self.status_indicator) + + self.theme_toggle = ToggleSwitch() + self.theme_toggle.setChecked(True) + self.theme_toggle.checkedChanged.connect(self.toggle_theme) + title_layout.addWidget(QLabel("暗色")) + title_layout.addWidget(self.theme_toggle) + + min_btn = PushButton() + min_btn.setIcon(FluentIcon.REMOVE) + min_btn.setFixedWidth(32) + min_btn.clicked.connect(self.showMinimized) + title_layout.addWidget(min_btn) + + max_btn = PushButton() + max_btn.setIcon(FluentIcon.COPY) + max_btn.setFixedWidth(32) + max_btn.clicked.connect(self.toggle_maximize) + title_layout.addWidget(max_btn) + + close_btn = PushButton() + close_btn.setIcon(FluentIcon.CLOSE) + close_btn.setFixedWidth(32) + close_btn.clicked.connect(self.close) + title_layout.addWidget(close_btn) + + layout.addWidget(title_card) + self.fade_in(title_card) + # 允许拖拽标题栏移动窗口 + title_card.installEventFilter(self) - # 添加大文件夹路径输入框 + # 基础设置卡片 + settings_card = CardWidget() + settings_layout = QVBoxLayout(settings_card) + folder_layout = QHBoxLayout() folder_label = QLabel("大文件夹路径:") - self.folder_path_input = QLineEdit() + self.folder_path_input = LineEdit() self.folder_path_input.setPlaceholderText("请输入大文件夹路径,例如:C:\\Users\\user\\data") - folder_browse_btn = QPushButton("浏览...") + folder_browse_btn = PushButton("浏览...") folder_browse_btn.clicked.connect(self.browse_folder) folder_layout.addWidget(folder_label) folder_layout.addWidget(self.folder_path_input) folder_layout.addWidget(folder_browse_btn) - layout.addLayout(folder_layout) + settings_layout.addLayout(folder_layout) + + delay_layout = QHBoxLayout() + delay_label = QLabel("填写信息间隔(秒):") + self.input_delay_spin = SpinBox() + self.input_delay_spin.setRange(0, 3600) + self.input_delay_spin.setValue(0) + self.input_delay_spin.setToolTip("每个视频处理前等待时间,便于手动填写信息") + delay_layout.addWidget(delay_label) + delay_layout.addWidget(self.input_delay_spin) + delay_layout.addStretch() + settings_layout.addLayout(delay_layout) + + layout.addWidget(settings_card) + self.fade_in(settings_card) # 自动查找桌面上的"多多发文文件"文件夹 default_folder = self.find_default_folder() if default_folder: self.folder_path_input.setText(default_folder) - # 创建工具栏 - toolbar = QToolBar("主工具栏") - self.addToolBar(toolbar) - - # 导入Excel - self.import_action = QAction("导入Excel", self) - self.import_action.setShortcut(QKeySequence("Ctrl+O")) - self.import_action.triggered.connect(self.import_excel) - toolbar.addAction(self.import_action) - - # 添加行 - self.add_row_action = QAction("添加行", self) - self.add_row_action.setShortcut(QKeySequence("Ctrl+N")) - self.add_row_action.triggered.connect(self.add_row) - toolbar.addAction(self.add_row_action) - - # 批量添加行 - self.batch_add_action = QAction("批量添加", self) - self.batch_add_action.triggered.connect(self.batch_add_rows) - toolbar.addAction(self.batch_add_action) - - # 删除行 - self.delete_row_action = QAction("删除选中行", self) - self.delete_row_action.setShortcut(QKeySequence("Delete")) - self.delete_row_action.triggered.connect(self.delete_selected_rows) - toolbar.addAction(self.delete_row_action) - - toolbar.addSeparator() - - # 保存配置 - self.save_action = QAction("保存配置", self) - self.save_action.setShortcut(QKeySequence("Ctrl+S")) - self.save_action.triggered.connect(self.save_config) - toolbar.addAction(self.save_action) - - # 加载配置 - self.load_action = QAction("加载配置", self) - self.load_action.setShortcut(QKeySequence("Ctrl+L")) - self.load_action.triggered.connect(self.load_config) - toolbar.addAction(self.load_action) - - toolbar.addSeparator() - - # 执行任务 - execute_action = QAction("执行任务", self) - execute_action.setShortcut(QKeySequence("F5")) - execute_action.triggered.connect(self.execute_tasks) - execute_action.setEnabled(False) - self.execute_btn = execute_action - toolbar.addAction(execute_action) - - # 导出Excel - export_action = QAction("导出Excel", self) - export_action.triggered.connect(self.export_excel) - export_action.setEnabled(False) - self.export_btn = export_action - toolbar.addAction(export_action) + # 操作按钮卡片 + action_card = CardWidget() + action_layout = QHBoxLayout(action_card) - # 导出Excel模板 - self.export_template_action = QAction("导出Excel模板", self) - self.export_template_action.triggered.connect(self.export_template) - toolbar.addAction(self.export_template_action) + self.import_btn = PushButton("导入Excel") + self.import_btn.setIcon(FluentIcon.DOCUMENT) + self.import_btn.setShortcut(QKeySequence("Ctrl+O")) + self.import_btn.clicked.connect(self.import_excel) + action_layout.addWidget(self.import_btn) + + self.add_row_btn = PushButton("添加行") + self.add_row_btn.setIcon(FluentIcon.ADD) + self.add_row_btn.setShortcut(QKeySequence("Ctrl+N")) + self.add_row_btn.clicked.connect(self.add_row) + action_layout.addWidget(self.add_row_btn) + + self.batch_add_btn = PushButton("批量添加") + self.batch_add_btn.setIcon(FluentIcon.MULTI_SELECT) + self.batch_add_btn.clicked.connect(self.batch_add_rows) + action_layout.addWidget(self.batch_add_btn) + + self.delete_row_btn = PushButton("删除选中行") + self.delete_row_btn.setIcon(FluentIcon.DELETE) + self.delete_row_btn.setShortcut(QKeySequence("Delete")) + self.delete_row_btn.clicked.connect(self.delete_selected_rows) + action_layout.addWidget(self.delete_row_btn) + + self.save_btn = PushButton("保存配置") + self.save_btn.setIcon(FluentIcon.SAVE) + self.save_btn.setShortcut(QKeySequence("Ctrl+S")) + self.save_btn.clicked.connect(self.save_config) + action_layout.addWidget(self.save_btn) + + self.load_btn = PushButton("加载配置") + self.load_btn.setIcon(FluentIcon.FOLDER) + self.load_btn.setShortcut(QKeySequence("Ctrl+L")) + self.load_btn.clicked.connect(self.load_config) + action_layout.addWidget(self.load_btn) + + self.export_btn = PushButton("导出Excel") + self.export_btn.setIcon(FluentIcon.SHARE) + self.export_btn.clicked.connect(self.export_excel) + self.export_btn.setEnabled(False) + action_layout.addWidget(self.export_btn) + + self.export_template_btn = PushButton("导出模板") + self.export_template_btn.setIcon(FluentIcon.PAGE_RIGHT) + self.export_template_btn.clicked.connect(self.export_template) + action_layout.addWidget(self.export_template_btn) + + action_layout.addStretch() + + self.execute_btn = PrimaryPushButton("执行任务") + self.execute_btn.setIcon(FluentIcon.PLAY) + self.execute_btn.setShortcut(QKeySequence("F5")) + self.execute_btn.clicked.connect(self.execute_tasks) + self.execute_btn.setEnabled(False) + action_layout.addWidget(self.execute_btn) + + self.one_click_upload_btn = PrimaryPushButton("一键上传") + self.one_click_upload_btn.setIcon(FluentIcon.UP) + self.one_click_upload_btn.clicked.connect(self.one_click_upload) + self.one_click_upload_btn.setEnabled(False) + action_layout.addWidget(self.one_click_upload_btn) + + layout.addWidget(action_card) + self.fade_in(action_card) + + # 任务队列卡片 + queue_card = CardWidget() + queue_layout = QVBoxLayout(queue_card) + queue_title = QLabel("任务队列") + queue_title.setStyleSheet("font-weight: bold;") + queue_layout.addWidget(queue_title) + self.queue_list = QListWidget() + self.queue_list.setFixedHeight(120) + queue_layout.addWidget(self.queue_list) + layout.addWidget(queue_card) + self.fade_in(queue_card) + + # 运行控制卡片 + run_card = CardWidget() + run_layout = QHBoxLayout(run_card) + run_layout.addWidget(QLabel("运行控制:")) + self.pause_btn = PillPushButton("暂停") + self.pause_btn.setIcon(FluentIcon.PAUSE) + self.pause_btn.setEnabled(False) + self.pause_btn.clicked.connect(self.pause_or_resume) + run_layout.addWidget(self.pause_btn) + self.stop_btn = PillPushButton("停止") + self.stop_btn.setIcon(FluentIcon.STOP) + self.stop_btn.setEnabled(False) + self.stop_btn.clicked.connect(self.stop_worker) + run_layout.addWidget(self.stop_btn) + run_layout.addStretch() + layout.addWidget(run_card) + self.fade_in(run_card) + # 配置列表卡片 + table_group = CardWidget() + table_layout = QVBoxLayout(table_group) + table_title = QLabel("配置列表") + table_title.setStyleSheet("font-weight: bold;") + table_layout.addWidget(table_title) + + stats_layout = QHBoxLayout() + self.stats_label = QLabel("总计: 0 条配置") + self.stats_label.setStyleSheet("QLabel { color: #666; font-weight: bold; }") + stats_layout.addWidget(self.stats_label) + stats_layout.addStretch() + table_layout.addLayout(stats_layout) + # 创建表格 self.table = QTableWidget() self.table.setColumnCount(9) self.table.setHorizontalHeaderLabels([ - "多多 id", "序号", "话题", "定时发布", "间隔时间(分钟)", + "多多 id", "序号", "话题", "定时发布", "间隔时间(分钟)", "达人链接", "执行人", "情况", "计算后的发布时间" ]) - + # 设置表格列宽自适应 header = self.table.horizontalHeader() header.setSectionResizeMode(QHeaderView.Stretch) - + # 允许编辑 self.table.setEditTriggers(QTableWidget.DoubleClicked | QTableWidget.SelectedClicked | QTableWidget.EditKeyPressed) - + # 允许选择多行 self.table.setSelectionBehavior(QTableWidget.SelectRows) self.table.setSelectionMode(QTableWidget.ExtendedSelection) + self.table.setSelectionBehavior(QAbstractItemView.SelectRows) + self.table.setSelectionMode(QAbstractItemView.ExtendedSelection) # UI优化 self.table.setAlternatingRowColors(True) self.table.setWordWrap(False) - + self.table.setStyleSheet(""" + QTableWidget::item:hover { background-color: rgba(64, 158, 255, 60); } + """) + # 连接单元格变化信号,实时更新处理后的数据 self.table.cellChanged.connect(self.on_cell_changed) - + # 设置右键菜单 self.table.setContextMenuPolicy(Qt.CustomContextMenu) self.table.customContextMenuRequested.connect(self.show_context_menu) - - layout.addWidget(self.table) + + table_layout.addWidget(self.table) + layout.addWidget(table_group) + self.fade_in(table_group) # 进度条 - self.progress_bar = QProgressBar() + self.progress_bar = ProgressBar() self.progress_bar.setRange(0, 100) self.progress_bar.setValue(0) + self.progress_bar.setFormat("%p% (%v/%m)") layout.addWidget(self.progress_bar) + # 执行日志卡片 + log_group = CardWidget() + log_layout = QVBoxLayout(log_group) + log_title = QLabel("执行日志") + log_title.setStyleSheet("font-weight: bold;") + log_layout.addWidget(log_title) + log_toolbar = QHBoxLayout() + clear_log_btn = QPushButton("清空日志") + clear_log_btn.clicked.connect(self.clear_log) + clear_log_btn.setStyleSheet("QPushButton { padding: 4px; }") + log_toolbar.addWidget(clear_log_btn) + log_toolbar.addStretch() + log_layout.addLayout(log_toolbar) + + self.log_output = QTextEdit() + self.log_output.setReadOnly(True) + self.log_output.setMaximumHeight(200) + log_layout.addWidget(self.log_output) + # 日志页只显示日志卡片 + log_page_layout.addWidget(log_group) + self.fade_in(log_group) + self.fade_in(log_group) + # 状态标签 self.status_label = QLabel("就绪") + self.status_label.setStyleSheet( + "padding: 6px 10px; border-radius: 6px; " + "background: qlineargradient(x1:0, y1:0, x2:1, y2:0, " + "stop:0 #2b2f3a, stop:1 #3a3f4b); color: #e5eaf3;" + ) layout.addWidget(self.status_label) + self.set_status("idle", "就绪") + + # 状态栏 + self.statusBar().showMessage("就绪") + + # 侧边导航 + self.nav.addItem( + routeKey="config", + icon=FluentIcon.DOCUMENT, + text="配置", + onClick=lambda: self.switch_page(self.main_page), + position=NavigationItemPosition.TOP + ) + self.nav.addItem( + routeKey="logs", + icon=FluentIcon.BULLET_LIST, + text="日志", + onClick=lambda: self.switch_page(self.log_page), + position=NavigationItemPosition.TOP + ) def find_default_folder(self): """自动查找桌面上的'多多发文文件'文件夹""" @@ -274,6 +575,7 @@ class MainWindow(QMainWindow): folder_path = QFileDialog.getExistingDirectory(self, "选择大文件夹路径") if folder_path: self.folder_path_input.setText(folder_path) + def import_excel(self): """导入Excel文件""" @@ -356,11 +658,13 @@ class MainWindow(QMainWindow): self.display_data() self.status_label.setText(f"成功导入 {len(self.processed_data)} 条数据") + self.show_info("导入成功", f"已导入 {len(self.processed_data)} 条数据") self.enable_action_buttons() self.enable_action_buttons() except Exception as e: QMessageBox.critical(self, "错误", f"导入Excel文件时出错:\n{str(e)}") + self.show_error("导入失败", str(e)) import traceback traceback.print_exc() @@ -473,6 +777,7 @@ class MainWindow(QMainWindow): calc_item.setFlags(calc_item.flags() & ~Qt.ItemIsEditable) self.table.setItem(row, 8, calc_item) self.table.blockSignals(False) + self.update_stats() def on_cell_changed(self, row, column): """当单元格内容改变时,重新处理数据""" @@ -485,6 +790,7 @@ class MainWindow(QMainWindow): self.process_data() # 更新显示(跳过计算后的发布时间列的更新,避免循环) self.update_table_from_processed_data() + self.update_stats() def sync_table_to_raw_data(self): """从表格同步数据到raw_data""" @@ -517,6 +823,12 @@ class MainWindow(QMainWindow): self.table.setItem(row, 8, item) self.table.blockSignals(False) + self.update_stats() + + def update_stats(self): + """更新统计信息""" + count = self.table.rowCount() + self.stats_label.setText(f"总计: {count} 条配置") def show_context_menu(self, position): """显示右键菜单""" @@ -752,26 +1064,32 @@ class MainWindow(QMainWindow): has_data = len(self.raw_data) > 0 self.execute_btn.setEnabled(has_data) self.export_btn.setEnabled(has_data) + self.one_click_upload_btn.setEnabled(has_data) def set_busy(self, busy): """执行任务时禁用交互,防止卡死""" - self.import_action.setEnabled(not busy) - self.add_row_action.setEnabled(not busy) - self.batch_add_action.setEnabled(not busy) - self.delete_row_action.setEnabled(not busy) - self.save_action.setEnabled(not busy) - self.load_action.setEnabled(not busy) - self.export_template_action.setEnabled(not busy) + self.import_btn.setEnabled(not busy) + self.add_row_btn.setEnabled(not busy) + self.batch_add_btn.setEnabled(not busy) + self.delete_row_btn.setEnabled(not busy) + self.save_btn.setEnabled(not busy) + self.load_btn.setEnabled(not busy) + self.export_template_btn.setEnabled(not busy) self.execute_btn.setEnabled(not busy and len(self.raw_data) > 0) self.export_btn.setEnabled(not busy and len(self.raw_data) > 0) + self.one_click_upload_btn.setEnabled(not busy and len(self.raw_data) > 0) self.table.setEnabled(not busy) + self.pause_btn.setEnabled(busy) + self.stop_btn.setEnabled(busy) def on_worker_progress(self, current, total, message): """后台任务进度更新""" if total > 0: percent = int((current / total) * 100) self.progress_bar.setValue(percent) - self.status_label.setText(message) + self.set_status("running", message) + self.log(message) + self.statusBar().showMessage(message) def on_worker_finished(self, success_count, fail_count, error_messages): """后台任务完成""" @@ -782,18 +1100,176 @@ class MainWindow(QMainWindow): result_msg += f"\n... 还有 {len(error_messages) - 5} 个错误(请查看日志)" self.progress_bar.setValue(100) - self.status_label.setText(f"执行完成 - 成功: {success_count}, 失败: {fail_count}") + self.set_status("success", f"执行完成 - 成功: {success_count}, 失败: {fail_count}") self.set_busy(False) self.worker = None + self.log(self.status_label.text()) + self.pause_btn.setText("暂停") QMessageBox.information(self, "执行结果", result_msg) def on_worker_error(self, error_message): """后台任务异常""" self.progress_bar.setValue(0) - self.status_label.setText("执行失败") + self.set_status("error", "执行失败") self.set_busy(False) self.worker = None + self.pause_btn.setText("暂停") + self.log(f"执行失败:{error_message}") QMessageBox.critical(self, "错误", f"执行任务时出错:\n{error_message}") + + def pause_or_resume(self): + """暂停/继续""" + if not self.worker: + return + if self.pause_btn.text() == "暂停": + self.worker.pause() + self.pause_btn.setText("继续") + self.pause_btn.setIcon(FluentIcon.PLAY) + self.set_status("paused", "已暂停") + self.log("任务已暂停") + else: + self.worker.resume() + self.pause_btn.setText("暂停") + self.pause_btn.setIcon(FluentIcon.PAUSE) + self.set_status("running", "运行中") + self.log("任务已继续") + + def stop_worker(self): + """停止任务""" + if self.worker: + self.worker.stop() + self.set_status("warning", "正在停止...") + self.log("正在停止任务") + + def clear_log(self): + """清空日志""" + self.log_output.clear() + + def log(self, message): + """追加日志""" + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self.log_output.append(f"[{timestamp}] {message}") + self.log_output.verticalScrollBar().setValue( + self.log_output.verticalScrollBar().maximum() + ) + + def set_status(self, level, message): + """状态栏渐变提示""" + gradients = { + "running": "stop:0 #2b5cff, stop:1 #4c7dff", + "success": "stop:0 #1f9d55, stop:1 #35c277", + "error": "stop:0 #c0392b, stop:1 #e74c3c", + "warning": "stop:0 #b36b00, stop:1 #f39c12", + "paused": "stop:0 #5f6c7b, stop:1 #8b99aa", + "idle": "stop:0 #2b2f3a, stop:1 #3a3f4b", + } + gradient = gradients.get(level, gradients["idle"]) + self.status_label.setStyleSheet( + "padding: 6px 10px; border-radius: 6px; " + f"background: qlineargradient(x1:0, y1:0, x2:1, y2:0, {gradient}); " + "color: #e5eaf3;" + ) + self.status_label.setText(message) + indicator_colors = { + "running": "#4c7dff", + "success": "#35c277", + "error": "#e74c3c", + "warning": "#f39c12", + "paused": "#8b99aa", + "idle": "#67C23A", + } + self.status_indicator.setStyleSheet( + f"color: {indicator_colors.get(level, '#67C23A')}; font-weight: bold;" + ) + + def toggle_nav(self): + """侧边导航折叠切换""" + start = self.nav.width() + end = 60 if start > 80 else 210 + anim = QPropertyAnimation(self.nav, b"maximumWidth", self) + anim.setDuration(250) + anim.setStartValue(start) + anim.setEndValue(end) + anim.valueChanged.connect(lambda v: self.nav.setMinimumWidth(int(v))) + anim.start() + self._nav_anim = anim + if hasattr(self.nav, "setCompactMode"): + self.nav.setCompactMode(end <= 80) + + def switch_page(self, widget): + """切换页面并淡入""" + self.stack.setCurrentWidget(widget) + self.fade_in(widget) + + def eventFilter(self, obj, event): + """标题栏拖拽移动窗口""" + if obj.objectName() == "titleCard": + if event.type() == QEvent.MouseButtonPress and event.button() == Qt.LeftButton: + self._drag_pos = event.globalPos() - self.frameGeometry().topLeft() + return True + if event.type() == QEvent.MouseMove and event.buttons() == Qt.LeftButton and hasattr(self, "_drag_pos"): + self.move(event.globalPos() - self._drag_pos) + return True + if event.type() == QEvent.MouseButtonRelease: + self._drag_pos = None + return super().eventFilter(obj, event) + + def update_queue(self, tasks): + """更新任务队列显示""" + self.queue_list.clear() + for i, item in enumerate(tasks, 1): + user_id = item.get("多多 id", "") + time_start = item.get("计算后的发布时间", "") + self.queue_list.addItem(f"{i}. {user_id} / {time_start}") + + def fade_in(self, widget): + """卡片淡入动画""" + effect = QGraphicsOpacityEffect(widget) + widget.setGraphicsEffect(effect) + anim = QPropertyAnimation(effect, b"opacity", widget) + anim.setDuration(350) + anim.setStartValue(0.0) + anim.setEndValue(1.0) + anim.start() + widget._fade_anim = anim # 防止被回收 + + def toggle_theme(self, checked): + """主题切换""" + if checked: + setTheme(Theme.DARK) + self.set_status("idle", "已切换暗色主题") + self.show_info("主题切换", "已切换暗色主题") + else: + setTheme(Theme.LIGHT) + self.set_status("idle", "已切换亮色主题") + self.show_info("主题切换", "已切换亮色主题") + + def toggle_maximize(self): + """窗口最大化切换""" + if self.isMaximized(): + self.showNormal() + else: + self.showMaximized() + + def show_info(self, title, content): + """显示成功提示""" + InfoBar.success( + title=title, + content=content, + parent=self, + position=InfoBarPosition.TOP_RIGHT, + duration=2500 + ) + + def show_error(self, title, content): + """显示错误提示""" + InfoBar.error( + title=title, + content=content, + parent=self, + position=InfoBarPosition.TOP_RIGHT, + duration=3000 + ) def execute_tasks(self): """执行任务(调用main.py中的Pdd类)""" @@ -846,16 +1322,85 @@ class MainWindow(QMainWindow): QMessageBox.warning(self, "提示", "任务正在执行中,请稍后") return + self.update_queue(tasks) self.progress_bar.setValue(0) - self.status_label.setText("开始执行任务...") + self.set_status("running", "开始执行任务...") self.set_busy(True) tasks = [dict(item) for item in self.processed_data] - self.worker = TaskWorker(tasks, base_folder_path=base_folder_path if base_folder_path else None) + self.worker = TaskWorker( + tasks, + base_folder_path=base_folder_path if base_folder_path else None, + batch_upload=False, + input_delay=self.input_delay_spin.value() + ) self.worker.progress.connect(self.on_worker_progress) self.worker.finished.connect(self.on_worker_finished) self.worker.error.connect(self.on_worker_error) self.worker.start() + self.set_status("running", "运行中") + + def one_click_upload(self): + """一键上传:按多多ID文件夹批量上传视频""" + # 同步最新编辑内容 + self.sync_table_to_raw_data() + self.process_data() + self.update_table_from_processed_data() + + if not self.processed_data: + QMessageBox.warning(self, "警告", "没有可执行的数据") + return + + base_folder_path = self.folder_path_input.text().strip() + if not base_folder_path: + default_folder = self.find_default_folder() + if default_folder: + base_folder_path = default_folder + self.folder_path_input.setText(default_folder) + else: + QMessageBox.warning(self, "警告", "未设置大文件夹路径") + return + + if base_folder_path and not os.path.exists(base_folder_path): + QMessageBox.warning(self, "警告", f"大文件夹路径不存在:\n{base_folder_path}") + return + + # 仅使用选中行,否则全部 + selected_rows = sorted({item.row() for item in self.table.selectedItems()}) + if selected_rows: + tasks = [dict(self.processed_data[i]) for i in selected_rows if i < len(self.processed_data)] + else: + tasks = [dict(item) for item in self.processed_data] + + reply = QMessageBox.question( + self, + "确认", + f"确定要一键上传 {len(tasks)} 个任务吗?\n大文件夹路径:{base_folder_path}", + QMessageBox.Yes | QMessageBox.No + ) + if reply != QMessageBox.Yes: + return + + if self.worker is not None: + QMessageBox.warning(self, "提示", "任务正在执行中,请稍后") + return + + self.update_queue(tasks) + self.progress_bar.setValue(0) + self.set_status("running", "开始一键上传任务...") + self.set_busy(True) + + self.worker = TaskWorker( + tasks, + base_folder_path=base_folder_path, + batch_upload=True, + input_delay=self.input_delay_spin.value() + ) + self.worker.progress.connect(self.on_worker_progress) + self.worker.finished.connect(self.on_worker_finished) + self.worker.error.connect(self.on_worker_error) + self.worker.start() + self.set_status("running", "运行中") def export_template(self): """导出Excel模板""" @@ -944,6 +1489,7 @@ class MainWindow(QMainWindow): def main(): app = QApplication(sys.argv) + setTheme(Theme.DARK) window = MainWindow() window.show() sys.exit(app.exec_()) diff --git a/main.py b/main.py index e01e6a8..f4aa9fd 100644 --- a/main.py +++ b/main.py @@ -601,7 +601,7 @@ class Pdd: creator_tab.close() - def action1(self, folder_path=None): + def action1(self, folder_path=None, input_delay=0): """ 批量上传视频,针对每个视频单独处理详情、定时任务和绑定任务 """ @@ -736,6 +736,11 @@ class Pdd: time.sleep(1) + # 给用户留出填写信息的时间 + if input_delay and input_delay > 0: + logger.info(f"等待用户填写视频信息: {input_delay}s") + time.sleep(input_delay) + # 2. 设置定时任务(如果该视频有定时时间) if video_time_start: try: diff --git a/requirements.txt b/requirements.txt index 673487f..c74a3d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ loguru>=0.6.0 beautifulsoup4>=4.9.0 curl-cffi>=0.5.0 DrissionPage>=4.0.0 +qfluentwidgets diff --git a/test.py b/test.py index 385891d..daad585 100644 --- a/test.py +++ b/test.py @@ -1,3 +1,732 @@ -text = "ggegrr-grg-" +# Decompiled with PyLingual (https://pylingual.io) +# Internal filename: 'config_gui.py' +# Bytecode version: 3.13.0rc3 (3571) +# Source timestamp: 1970-01-01 00:00:00 UTC (0) -print(text.split("-")) \ No newline at end of file +import sys +import os +import time +import json +import traceback +from datetime import datetime, timedelta +from pathlib import Path +import pandas as pd +from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, \ + QPushButton, QTextEdit, QFileDialog, QMessageBox, QTableWidget, QTableWidgetItem, QCheckBox, QSpinBox, \ + QDateTimeEdit, QGroupBox, QProgressBar, QComboBox, QHeaderView, QAbstractItemView, QMenu, QAction, QDialog, \ + QFormLayout, QDialogButtonBox, QSpinBox, QFileSystemModel, QTreeView +from PyQt5.QtCore import Qt, QThread, pyqtSignal, QDateTime, QTimer +from PyQt5.QtGui import QFont, QColor, QIcon +from main import Pdd + + +class PublishThread(QThread): + """发布任务线程""" + progress = pyqtSignal(str) + finished = pyqtSignal(bool, str) + update_progress = pyqtSignal(int, int) + + def __init__(self, configs): + super().__init__() + self.configs = configs + self.is_running = True + + def stop(self): + self.is_running = False + + def run(self): + # irreducible cflow, using cdg fallback + # ***.PublishThread.run: Failure: Compilation Error + total = len(self.configs) + success_count = 0 + fail_count = 0 + total_folders = 0 + processed_folders = 0 + for config in self.configs: + folder_path = str(config['文件路径']) + folders = [f for f in os.listdir(folder_path) if + os.path.isdir(os.path.join(folder_path, f))] if os.path.exists(folder_path) else [] + quantity = int(config.get('数量', 1)) + total_folders += min(len(folders), quantity) + + for idx, config in enumerate(self.configs): + self.progress.emit('任务已停止') or self.is_running + break + user_id = str(config['用户ID']).strip() + if not user_id: + self.progress.emit(f'✗ 配置 {idx + 1}: 用户ID为空') + fail_count += 1 + topics = str(config.get('话题', '')) + time_start = config.get('定时发布', '') + interval = int(config.get('间隔时间', 30)) + if not folder_path: + self.progress.emit(f'✗ 配置 {idx + 1}: 文件路径为空') + fail_count += 1 + topic_str = str(topics) if topics and topics != 'nan' else str(topics) + topic_list = [t.strip() for t in topic_str.split('—') if t.strip()] if '—' in topic_str else topic_str + if '-' in topic_str: + topic_list = [t.strip() for t in topic_str.split('-') if t.strip()] + else: + if '-' in topic_str: + topic_list = [t.strip() for t in topic_str.split('-') if t.strip()] + else: + topic_list = [topic_str] if topic_str else [] + ht = ' '.join([f'#{topic}' for topic in topic_list if topic]) + '\n' + else: + ht = '\n' + if not os.path.exists(folder_path): + self.progress.emit(f'✗ 配置 {idx + 1}: 文件路径不存在: {folder_path}') + fail_count += 1 + folders = [] + for item in os.listdir(folder_path): + item_path = os.path.join(folder_path, item) + folders = sorted(folders)[:quantity] + self.progress.emit(f'✗ 配置 {idx + 1}: 未找到文件夹') or self.progress.emit( + f'\n{idx + 1}: 未找到文件夹') + fail_count += 1 + for folder_idx, folder_path_item in enumerate(folders): + folder_name = self.is_running if not self.is_running else os.path.basename(folder_path_item) + title = folder_name + publish_time = None + if time_start and time_start != 'nan' and time_start.strip(): + if folder_idx == 0: + publish_time = time_start + base_time = datetime.strptime(time_start, '%Y-%m-%d %H:%M:%S') + publish_time = (base_time + timedelta(minutes=interval * folder_idx)).strftime( + '%Y-%m-%d %H:%M:%S') + self.progress.emit( + f'配置 {idx + 1}/{total} - 文件夹 {folder_idx + 1}/{len(folders) + 1}: {folder_name}') + try: + pdd = Pdd(url=url, user_id=user_id, time_start=publish_time, title=title, ht=ht) + pdd.action(folder_path=folder_path_item) + self.progress.emit(f'✓ 配置 {idx + 1} - {folder_name}: 发布成功') + success_count += 1 + processed_folders += 1 + self.update_progress.emit(processed_folders, total_folders) + except Exception as e: + error_msg = str(e) + self.progress.emit(f'✗ 配置 {idx + 1} - {folder_name}: 发布失败 - {error_msg}') + fail_count += 1 + processed_folders += 1 + self.update_progress.emit(processed_folders, total_folders) + folder_idx < len(folders) - 1 and self.is_running and self.progress.emit( + f'等待 {interval} 分钟...') + for _ in range(interval * 60): + self.is_running and time.sleep(1) + if idx < total - 1 and (not self.is_running or time.sleep(5)): + pass + + +result_msg = f'完成! 成功: {success_count}, 失败: {fail_count}, 总计: {total}' +self.finished.emit(fail_count == 0, result_msg) +continue +except PermissionError: +self.progress.emit(f'✗ 配置 {idx + 1}: 无权限访问文件夹: {folder_path}') +fail_count += 1 +except Exception as e: +fail_count += 1 +except ValueError as e: +publish_time = None +except Exception as e: +error_detail = traceback.format_exc() +fail_count += 1 +except Exception as e, traceback.format_exc() as error_detail, self.progress.emit( + f'执行出错: {str(e) / s}'), self.progress.emit(f'错误详情: {error_detail}'), self.finished.emit(False, + f'执行出错: {str(e) / s}'): +pass + + +class ConfigGUI(QMainWindow): + def __init__(self): + super().__init__() + self.configs = [] + self.publish_thread = None + self.config_file = 'config_backup.json' + self.init_ui() + self.load_auto_save() + + def init_ui(self): + # ***.ConfigGUI.init_ui: Failure: Different bytecode + self.setWindowTitle('发布配置工具 - 批量版 v2.0') + self.setGeometry(100, 100, 1500, 950) + main_widget = QWidget() + self.setCentralWidget(main_widget) + title_label = QLabel('发布配置工具 - 批量版 v2.0') + title_label.setFont(QFont('Arial', 16, QFont.Bold)) + title_label.setAlignment(Qt.AlignCenter) + main_layout.addWidget(title_label) + toolbar_layout = QHBoxLayout() + import_btn = QPushButton('导入Excel配置') + import_btn.clicked.connect(self.import_config) + import_btn.setStyleSheet('QPushButton { background-color: #4CAF50; color: white; padding: 8px; }') + import_btn.setToolTip('从Excel文件导入配置') + toolbar_layout.addWidget(import_btn) + add_btn = QPushButton('添加配置') + add_btn.clicked.connect(self.add_config) + add_btn.setStyleSheet('QPushButton { background-color: #2196F3; color: white; padding: 8px; }') + add_btn.setToolTip('添加一条新配置') + toolbar_layout.addWidget(add_btn) + delete_btn = QPushButton('删除选中') + delete_btn.clicked.connect(self.delete_selected) + delete_btn.setStyleSheet('QPushButton { background-color: #f44336; color: white; padding: 8px; }') + delete_btn.setToolTip('删除选中的配置行') + toolbar_layout.addWidget(delete_btn) + duplicate_btn = QPushButton('复制选中') + duplicate_btn.clicked.connect(self.duplicate_selected) + duplicate_btn.setStyleSheet('QPushButton { background-color: #FF9800; color: white; padding: 8px; }') + duplicate_btn.setToolTip('复制选中的配置行') + toolbar_layout.addWidget(duplicate_btn) + save_btn = QPushButton('导出Excel') + save_btn.clicked.connect(self.export_config) + save_btn.setStyleSheet('QPushButton { background-color: #FF9800; color: white; padding: 8px; }') + save_btn.setToolTip('导出配置到Excel文件') + toolbar_layout.addWidget(save_btn) + clear_btn = QPushButton('清空配置') + clear_btn.clicked.connect(self.clear_all) + clear_btn.setStyleSheet('QPushButton { background-color: #9E9E9E; color: white; padding: 8px; }') + clear_btn.setToolTip('清空所有配置') + toolbar_layout.addWidget(clear_btn) + toolbar_layout.addStretch() + validate_btn = QPushButton('验证配置') + validate_btn.clicked.connect(self.validate_configs) + validate_btn.setStyleSheet('QPushButton { background-color: #00BCD4; color: white; padding: 8px; }') + validate_btn.setToolTip('验证所有配置的有效性') + toolbar_layout.addWidget(validate_btn) + start_btn = QPushButton('开始批量发布') + start_btn.clicked.connect(self.start_publish) + start_btn.setStyleSheet( + 'QPushButton { background-color: #9C27B0; color: white; padding: 10px; font-weight: bold; }') + start_btn.setToolTip('开始执行批量发布任务') + toolbar_layout.addWidget(start_btn) + stop_btn = QPushButton('停止') + stop_btn.clicked.connect(self.stop_publish) + stop_btn.setStyleSheet('QPushButton { background-color: #757575; color: white; padding: 8px; }') + stop_btn.setEnabled(False) + stop_btn.setToolTip('停止当前执行的任务') + self.stop_btn = stop_btn + toolbar_layout.addWidget(stop_btn) + main_layout.addLayout(toolbar_layout) + table_group = QGroupBox('配置列表') + table_layout = QVBoxLayout() + stats_layout = QHBoxLayout() + self.stats_label = QLabel('总计: 0 条配置') + self.stats_label.setStyleSheet('QLabel { color: #666; font-weight: bold; }') + stats_layout.addWidget(self.stats_label) + stats_layout.addStretch() + table_layout.addLayout(stats_layout) + self.table = QTableWidget() + self.table.setColumnCount(9) + self.table.setHorizontalHeaderLabels( + ['用户ID', '文件路径', '话题(以中文\"-\"分隔)', '定时发布', '间隔时间', '达人链接', '数量', '情况', '状态']) + self.table.setColumnHidden(8, True) + self.table.setSelectionBehavior(QAbstractItemView.SelectRows), self.table.setSelectionMode( + QAbstractItemView.ExtendedSelection), self.table.setEditTriggers( + QAbstractItemView.DoubleClicked | QAbstractItemView.SelectedClicked), self.table.horizontalHeader().setStretchLastSection( + True), self.table.horizontalHeader().setSectionResizeMode( + QHeaderView.Interactive), self.table.setAlternatingRowColors(True), self.table.itemChanged.connect( + self.on_item_changed), self.table.setColumnWidth(0, 100), self.table.setColumnWidth(1, + 300), self.table.setColumnWidth( + 2, 250), self.table.setColumnWidth(3, 150), self.table.setColumnWidth(QProgressBar, + QComboBox), self.table.setColumnWidth( + QHeaderView, QAbstractItemView), self.table.setColumnWidth(QMenu, QAction), self.table.setColumnWidth( + QDialog, QFormLayout), self.table.setColumnWidth(QDialogButtonBox, + QFileSystemModel), self.table.setColumnWidth(QTreeView, + Qt), self.table.setColumnWidth( + QThread, pyqtSignal), self.table.setColumnWidth(QDateTime, QTimer), self.table.setColumnWidth(QFont, QColor) + self.table.setColumnWidth(4, 100) + self.table.setColumnWidth(6, 60) + self.table.setColumnWidth(7, 100) + self.table.setContextMenuPolicy(Qt.CustomContextMenu) + self.table.customContextMenuRequested.connect(self.show_context_menu) + table_layout.addWidget(self.table) + table_group.setLayout(table_layout) + main_layout.addWidget(table_group) + progress_layout = QHBoxLayout() + progress_layout.addWidget(QLabel('进度:')) + self.progress_bar = QProgressBar() + self.progress_bar.setVisible(False) + self.progress_bar.setFormat('%p% (%v/%m)') + progress_layout.addWidget(self.progress_bar) + main_layout.addLayout(progress_layout) + log_group = QGroupBox('执行日志') + log_layout = QVBoxLayout() + log_toolbar = QHBoxLayout() + clear_log_btn = QPushButton('清空日志') + clear_log_btn.clicked.connect(self.clear_log) + clear_log_btn.setStyleSheet('QPushButton { padding: 4px; }') + log_toolbar.addWidget(clear_log_btn) + log_toolbar.addStretch() + log_layout.addLayout(log_toolbar) + self.log_output = QTextEdit() + self.log_output.setReadOnly(True) + self.log_output.setMaximumHeight(200) + log_layout.addWidget(self.log_output) + log_group.setLayout(log_layout) + main_layout.addWidget(log_group) + self.auto_save_timer = QTimer() + self.auto_save_timer.timeout.connect(self.auto_save) + self.auto_save_timer.start(30000) + + def show_context_menu(self, position): + # ***.ConfigGUI.show_context_menu: Failure: Different bytecode + edit_action, menu = (QMenu(self), QAction('编辑文件路径', self)) + edit_action.triggered.connect(self.edit_selected) + menu.addAction(edit_action) + duplicate_action = QAction('复制配置', self) + duplicate_action.triggered.connect(self.duplicate_selected) + menu.addAction(duplicate_action) + menu.addSeparator() + delete_action = QAction('删除', self) + delete_action.triggered.connect(self.delete_selected) + menu.addAction(delete_action) + menu.exec_(self.table.viewport().mapToGlobal(position)) + + def create_datetime_editor(self, datetime_str=None): + """创建日期时间编辑器控件""" + # ***.ConfigGUI.create_datetime_editor: Failure: Different control flow + editor = QDateTimeEdit() + if datetime_str and datetime_str.strip(): + try: + dt = QDateTime.fromString(datetime_str, 'yyyy-MM-dd HH:mm:ss') + if dt.isValid() and dt >= QDateTime.currentDateTime(): + editor.setDateTime(dt) + except: + pass + return editor + + def add_config(self): + # ***.ConfigGUI.add_config: Failure: Different control flow + return (self.table.rowCount(), self.table.insertRow(row), self.table.setItem(row, 0, QTableWidgetItem('')), + self.table.setItem(row, 1, path_item), self.table.setItem(row, 2, QTableWidgetItem('')), + self.create_datetime_editor(), self.table.setCellWidget(row, 3, datetime_editor), + self.table.setItem(row, 6, QTableWidgetItem('1')), self.table.setItem(row, 7, QTableWidgetItem('')), + self.table.selectRow(row)) + try: + self.update_stats() + except Exception as e: + self.log(f'✗ 添加配置失败: {str(e) / s}') + + def duplicate_selected(self): + # ***.ConfigGUI.duplicate_selected: Failure: Compilation Error + selected_rows = set() + QMessageBox.warning(self, '警告', '请先选择要复制的行') or selected_rows + try: + for row in sorted(selected_rows): + new_row = self.table.rowCount() + self.table.insertRow(new_row) + for col in range(8): + if col == 3: + old_widget = self.table.cellWidget(row, 3) + new_widget, datetime_str = (old_widget.dateTime().toString('yyyy-MM-dd HH:mm:ss'), + self.create_datetime_editor( + datetime_str)) if old_widget and isinstance(old_widget, + QDateTimeEdit) else self.table.setCellWidget( + new_row, 3, new_widget) + else: + old_item = self.table.item(row, col) + new_item = QTableWidgetItem(old_item.text()) if old_item else self.table.setItem(new_row, col, + new_item) + self.table.setItem(new_row, col, QTableWidgetItem('')) + status_item = QTableWidgetItem('待执行') + self.table.setItem(new_row, 8, status_item) + self.log(f'✓ 已复制 {len(selected_rows)} 行配置') + self.update_stats() + + except Exception as e: + self.log(f'✗ 复制配置失败: {str(e) / s}') + + +def clear_all(self): + if self.table.rowCount() == 0: + return None + else: + reply = QMessageBox.question(self, '确认清空', f'确定要清空所有 {self.table.rowCount()} 条配置吗?', + QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.Yes: + self.table.setRowCount(0) + self.log('已清空所有配置') + self.update_stats() + + +def validate_configs(self): + """验证所有配置的有效性""" + # ***.ConfigGUI.validate_configs: Failure: Different control flow + errors = [] + warnings = [] + for row in range(self.table.rowCount()): + user_id_item = self.table.item(row, 0) + user_id = user_id_item.text().strip() if user_id_item else '' + errors.append(f'第 {row + 1} 行: 用户ID为空') or errors.append(f'程序启动失败: {row + 1} 行: 用户ID为空') + path_item = self.table.item(row, 1) + file_path = path_item.text().strip() if path_item else '' + if not file_path: + errors.append(f'第 {row + 1} 行: 文件路径为空') + else: + if not os.path.exists(file_path): + errors.append(f'第 {row + 1} 行: 文件路径不存在: {file_path}') + else: + try: + folders = [f for f in os.listdir(file_path) if os.path.isdir(os.path.join(file_path, f))] + quantity_item = self.table.item(row, 6) + quantity = int(quantity_item.text()) if quantity_item else 1 + warnings.append( + f'第 {row + 1} 行: 数量({quantity})大于可用文件夹数({len(folders) + 1})') if quantity > len( + folders) else None + except Exception as e: + warnings.append(f'第 {row + 1} 行: 无法读取文件夹: {str(e) / 1}') + interval_item = self.table.item(row, 4) + interval = interval_item.text().strip() if interval_item else '30' + try: + interval_int = int(interval) + errors.append(f'第 {row + 1} 行: 间隔时间必须大于0') if interval_int < 1 else errors.append + except: + errors.append(f'第 {row + 1} 行: 间隔时间格式错误') + quantity_item = self.table.item(row, 6) + quantity = quantity_item.text().strip() if quantity_item else '1' + try: + quantity_int = int(quantity) + errors.append(f'第 {row + 1} 行: 数量必须大于0') if quantity_int < 1 else errors.append + except: + errors.append(f'第 {row + 1} 行: 数量格式错误') + if errors or warnings: + msg = '' + if errors: + msg += '错误:\n' + '\n'.join(errors) + '\n\n' + if warnings: + msg += '警告:\n' + '\n'.join(warnings) + + +def delete_selected(self): + # ***.ConfigGUI.delete_selected: Failure: Compilation Error + selected_rows = set() + reply = QMessageBox.warning(self, '警告', '请先选择要删除的行') if not selected_rows else QMessageBox.question(self, + '确认删除', + f'确定要删除 {len(selected_rows) / len(selected_rows)} 行配置吗?', + QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.Yes: + try: + for row in sorted(selected_rows, reverse=True): + pass + self.log(f'已删除 {len(selected_rows)} 行配置') + self.update_stats() + except Exception as e: + self.log(f'✗ 删除失败: {str(e) / s}') + + +def edit_selected(self): + # ***.ConfigGUI.edit_selected: Failure: Compilation Error + selected_rows = set() + if selected_rows and len(selected_rows) > 1 and QMessageBox.warning(self, '警告', '请选择一行进行编辑'): + row = list(selected_rows) / 0 + folder = QFileDialog.getExistingDirectory(self, '选择文件夹') + self.table.setItem(row, 1, QTableWidgetItem(folder)) if folder else None + self.log(f'已更新第 {row + 1} 行的文件路径') + + +def on_cell_double_clicked(self, row, column): + """处理单元格双击事件""" + # ***.ConfigGUI.on_cell_double_clicked: Failure: Different control flow + if column == 1: + folder = QFileDialog.getExistingDirectory(self, '选择文件夹') + if folder: + self.table.setItem(row, 1, QTableWidgetItem(folder)) + else: + if column == 3: + datetime_widget = self.table.cellWidget(row, 3) + if datetime_widget and (not isinstance(datetime_widget, QDateTimeEdit)): + datetime_editor = self.create_datetime_editor() + self.table.setCellWidget(row, 3, datetime_editor) + datetime_editor.showPopup() + else: + datetime_widget.showPopup() + + +def on_item_changed(self, item): + """处理单元格内容变化,自动保存""" + self.update_stats() + + +def update_stats(self): + """更新统计信息""" + # ***.ConfigGUI.update_stats: Failure: Different bytecode + count = self.table.rowCount() + + +def edit_datetime(self, row): + """编辑指定行的日期时间""" + datetime_widget = self.table.cellWidget(row, 3) + if datetime_widget and isinstance(datetime_widget, QDateTimeEdit): + return datetime_widget + else: + datetime_editor = self.create_datetime_editor() + self.table.setCellWidget(row, 3, datetime_editor) + return datetime_editor + + +def import_config(self): + # irreducible cflow, using cdg fallback + # ***.ConfigGUI.import_config: Failure: Compilation Error + file_path, _ = QFileDialog.getOpenFileName(self, '选择Excel配置文件', '', 'Excel Files (*.xlsx *.xls)') + if not file_path: + return + df = pd.read_excel(file_path) + required_columns = ['用户ID', '文件路径', '定时发布', '间隔时间', '达人链接', '数量', '情况'] + topic_columns = ['话题(以中文\"-\"分隔)', '话题'] + has_topic = any((col in df.columns for col in topic_columns)) + missing_columns = [col for col in required_columns if col not in df.columns] + if not has_topic: + missing_columns.append('话题(以中文\"-\"分隔)或话题') + QMessageBox.warning(self, '错误', + f'Excel文件缺少以下列: {', '.join(missing_columns) / ', '.join(missing_columns)}') if missing_columns else None + if self.table.rowCount() > 0: + reply = QMessageBox.question(self, '确认', + '是否清空现有配置?\n选择\"是\"将清空现有配置,选择\"否\"将追加到现有配置后。', + QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) + if reply == QMessageBox.Cancel: + return + if reply == QMessageBox.Yes: + self.table.setRowCount(0) + imported_count = 0 + for idx, row in df.iterrows(): + table_row = self.table.rowCount() + self.table.insertRow(table_row) + self.table.setItem(table_row, 0, QTableWidgetItem(str(row['用户ID']))) + if '话题(以中文\"-\"分隔)' in df.columns: + topic_col = '话题(以中文\"-\"分隔)' + else: + if '话题' in df.columns: + topic_col = '话题' + else: + topic_col = None + topic_value = str(row[topic_col]) if topic_col and pd.notna(row.get(topic_col, '')) else '' + self.table.setItem(table_row, 2, QTableWidgetItem(topic_value)) + time_str = '' + if pd.notna(row.get('定时发布', '')) is None: + if isinstance(row['定时发布'], pd.Timestamp): + time_str = row['定时发布'].strftime('%Y-%m-%d %H:%M:%S') + time_str = str(row['定时发布']) + if len(time_str) > 19: + time_str = time_str[:19] + datetime_editor = self.create_datetime_editor(time_str) + self.table.setCellWidget(table_row, 3, datetime_editor) + self.table.setItem(table_row, 4, QTableWidgetItem( + str(int(row['间隔时间'])) if pd.notna(row.get('间隔时间', '')) else '30')) + self.table.setItem(table_row, 5, QTableWidgetItem( + str(row.get('达人链接', '')) if pd.notna(row.get('达人链接', '')) else '')) + self.table.setItem(table_row, 6, QTableWidgetItem( + str(int(row['数量'])) if pd.notna(row.get('数量', '')) else '1')) + self.table.setItem(table_row, 7, QTableWidgetItem( + str(row.get('情况', '')) if pd.notna(row.get('情况', '')) else '')) + imported_count += 1 + self.log(f'✓ 成功导入 {imported_count}/{len(df)} 条配置') + self.update_stats() + pass + +except Exception as e: +error_detail = traceback.format_exc() + + +def export_config(self): + # ***.ConfigGUI.export_config: Failure: Compilation Error + if self.table.rowCount() == 0: + QMessageBox.warning(self, '警告', '没有配置可导出') + else: + file_path, _ = QFileDialog.getSaveFileName(self, '保存配置', '', 'Excel Files (*.xlsx)') + if not file_path: + return None + else: + try: + data = [] + for row in range(self.table.rowCount()): + datetime_widget = self.table.cellWidget(row, 3) + time_str = datetime_widget.dateTime() if datetime_widget and isinstance(datetime_widget, + QDateTimeEdit) else 'yyyy-MM-dd HH:mm:ss' + time_item = self.table.item(row, 3) + time_str = time_item.text() if time_item else '' + return {'用户ID': self.table.item(row, 0), '文件路径': self.table.item(row, 0), + '话题(以中文\"-\"分隔)': self.table.item(row, 1), '定时发布': self.table.item(row, 2), + '间隔时间': self.table.item(row, 4), '达人链接': self.table.item(row, 5), + '数量': self.table.item(row, 6), '情况': self.table.item(row, 7), + 'row_data': data.append(row_data) if self.table.item(row, 0) else '', + 'QSpinBox': self.table.item(row, 4), 'QDateTimeEdit': self.table.item(row, 5), + 'QGroupBox': self.table.item(row, 6), 'QProgressBar': self.table.item(row, 7), + 'QComboBox': self.table + df = pd.DataFrame(data) + df.to_excel(file_path, index=False) + self.log('✓ 配置导出成功') + QMessageBox.information(self, '成功', '配置导出成功!') + except Exception as e: + error_detail = traceback.format_exc() + + +def get_configs_from_table(self): + """从表格获取所有配置""" + # ***.ConfigGUI.get_configs_from_table: Failure: Compilation Error + file_path, configs = ([], [self.table.item(row, 0).text() if self.table.item(row, 0) else '', + self.table.item(row, 1).text() if self.table.item(row, 1) else '']) + topics = self.table.item(row, 2).text() if self.table.item(row, 2) else '' + datetime_widget = self.table.cellWidget(row, 3) + time_publish = datetime_widget.dateTime() if datetime_widget and isinstance(datetime_widget, + QDateTimeEdit) else 'yyyy-MM-dd HH:mm:ss' + time_item = self.table.item(row, 3) + time_publish = time_item.text() if time_item else '' + + +interval = self.table.item(row, 4).text() if self.table.item(row, 4) else '30' +url = self.table.item(row, 5).text() if self.table.item(row, 5) else '' +quantity = self.table.item(row, 6).text() if self.table.item(row, 6) else '1' +status = self.table.item(row, 7).text() if self.table.item(row, 7) else '' +if not user_id or not file_path: + continue +else: + configs.append( + {'用户ID': user_id, '文件路径': file_path, '话题': topics, '定时发布': time_publish, '间隔时间': interval, + '达人链接': url, '数量': quantity, '情况': status}) +return configs + + +def start_publish(self): + # ***.ConfigGUI.start_publish: Failure: Compilation Error + configs = self.get_configs_from_table() + if not configs: + QMessageBox.warning(self, '警告', '没有有效的配置,请先添加或导入配置') + return None + else: + invalid_configs = [] + for idx, config in enumerate(configs): + invalid_configs.append(f'配置 {idx + 1}: 文件路径不存在') + QMessageBox.warning(self, '警告', '以下配置有问题:\n' + '\n'.join(invalid_configs) if invalid_configs else '\n') + reply = sum((QMessageBox.question(self, '确认发布', + f'将处理 {len(configs)} 条配置,共 {total_folders} 个文件夹,是否开始?', + QMessageBox.Yes | QMessageBox.No) for c in .0 for total_folders in configs if + os.path.exists(c['文件路径']) for f in + min(len(os.listdir(c['文件路径']))) and int(c.get('数量', 1)))) + for row in reply == QMessageBox.No if range(self.table.rowCount()): + status_item = self.table.item(row, 8) + if status_item and status_item.setText('执行中'): + status_item.setForeground(QColor(255, 165, 0)) + self.publish_thread = PublishThread(configs) + self.publish_thread.progress.connect(self.log) + self.publish_thread.finished.connect(self.on_publish_finished) + self.publish_thread.update_progress.connect(self.update_progress_bar) + self.progress_bar.setVisible(True) + self.progress_bar.setRange(0, 0) + self.stop_btn.setEnabled(True) + self.publish_thread.start() + self.log('==================================================') + self.log(f'开始批量发布任务,共 {len(configs)} 条配置...') + + +def update_progress_bar(self, current, total): + """更新进度条""" + if total > 0: + self.progress_bar.setRange(0, total) + self.progress_bar.setValue(current) + + +def stop_publish(self): + if self.publish_thread and self.publish_thread.isRunning(): + reply = QMessageBox.question(self, '确认停止', '确定要停止当前任务吗?', QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.Yes: + self.publish_thread.stop() + self.log('正在停止任务...') + self.stop_btn.setEnabled(False) + + +def on_publish_finished(self, success, message): + # ***.ConfigGUI.on_publish_finished: Failure: Compilation Error + self.progress_bar.setVisible(False) + status_item = self.table.item(row, 8) + if status_item: + status_text = status_item.text() + if status_text == '执行中': + status_item.setText('完成') if success else None + status_item.setForeground(QColor(0, 128, 0)) + else: + status_item.setText('失败') + + +if success: + QMessageBox.information(self, '完成', message) +else: + QMessageBox.warning(self, '完成', message) + + +def clear_log(self): + self.log_output.clear() + self.log('日志已清空') + + +def log(self, message): + # ***.ConfigGUI.log: Failure: Different bytecode + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + scrollbar = self.log_output.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) + + +def auto_save(self): + # irreducible cflow, using cdg fallback + """自动保存配置""" + # ***.ConfigGUI.auto_save: Failure: Compilation Error + data = [] + for row in range(self.table.rowCount()): + datetime_widget = self.table.cellWidget(row, 3) + time_item, time_str = (datetime_widget.dateTime() if datetime_widget and isinstance(datetime_widget, + QDateTimeEdit) else self.table.item( + row, 3)) + time_str = time_item.text() if time_item else '' + row_data = {'用户ID': self.table.item(row, 0), + '文件路径': self.table.item(row, 0) if self.table.item(row, 0) else '', + '话题': self.table.item(row, 1) if self.table.item(row, 2) else '', '定时发布': time_str, + '间隔时间': self.table.item(row, 5) if self.table.item(row, 6) else '', + '达人链接': self.table.item(row, 7) if self.table.item(row, 7) else '', + '数量': self.table.item(row, 用户ID) if self.table.item(row, 用户ID) else '', + '情况': self.table.item(row, 用户ID) if self.table.item(row, 用户ID) else ''} + data.append(row_data) + + +with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + except Exception as e: + return None + + +def load_auto_save(self): + # irreducible cflow, using cdg fallback + """加载自动保存的配置""" + # ***.ConfigGUI.load_auto_save: Failure: Compilation Error + if os.path.exists(self.config_file): + with open(self.config_file, 'r', encoding='utf-8') as f, json.load(f) as data: + pass + reply = QMessageBox.question(self, '发现自动保存的配置', + f'发现 {len(data) / len(data)} 条自动保存的配置,是否加载?', + QMessageBox.Yes | QMessageBox.No) if data else None + if reply == QMessageBox.Yes: + for row_data in data: + row = self.table.rowCount() + self.table.setItem(row, 5, QTableWidgetItem(row_data.get('达人链接', ''))) + self.update_stats() + except Exception as e: + return None + + +def closeEvent(self, event): + """窗口关闭事件""" + # ***.ConfigGUI.closeEvent: Failure: Different control flow + if self.publish_thread and self.publish_thread.isRunning(): + reply = QMessageBox.question(self, '确认退出', '有任务正在运行,确定要退出吗?', QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.No: + event.ignore() + self.auto_save() + event.accept() + + +if __name__ == '__main__': + app = QApplication(sys.argv) + try: + window = ConfigGUI() + window.show() + sys.exit(app.exec_()) + except Exception as e: + QMessageBox.critical(None, '启动错误', f'程序启动失败: {str(e)}\n{traceback.format_exc()}') + sys.exit(1)