gui
第一版
This commit is contained in:
744
gui_app.py
744
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_())
|
||||
|
||||
7
main.py
7
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:
|
||||
|
||||
@@ -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
733
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("-"))
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user