第一版
This commit is contained in:
27942
2026-01-20 04:09:09 +08:00
parent 00f260bce1
commit c35b3d21d5
4 changed files with 1383 additions and 102 deletions

View File

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

View File

@@ -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:

View File

@@ -5,3 +5,4 @@ loguru>=0.6.0
beautifulsoup4>=4.9.0
curl-cffi>=0.5.0
DrissionPage>=4.0.0
qfluentwidgets

733
test.py
View File

@@ -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("-"))
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
# ***<module>.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):
# ***<module>.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):
# ***<module>.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):
"""创建日期时间编辑器控件"""
# ***<module>.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):
# ***<module>.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):
# ***<module>.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):
"""验证所有配置的有效性"""
# ***<module>.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):
# ***<module>.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):
# ***<module>.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):
"""处理单元格双击事件"""
# ***<module>.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):
"""更新统计信息"""
# ***<module>.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
# ***<module>.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):
# ***<module>.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):
"""从表格获取所有配置"""
# ***<module>.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):
# ***<module>.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):
# ***<module>.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):
# ***<module>.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
"""自动保存配置"""
# ***<module>.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
"""加载自动保存的配置"""
# ***<module>.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):
"""窗口关闭事件"""
# ***<module>.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)