Files
haha/gui_app.py
ddrwode f651dbdf0f hahah
2026-01-20 09:36:47 +08:00

2513 lines
99 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import sys
import os
import re
import json
from pathlib import Path
from datetime import datetime, timedelta
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout,
QHBoxLayout, QTableWidget, QTableWidgetItem,
QFileDialog, QMessageBox, QHeaderView, QLabel, QMenu,
QInputDialog, QDialog, QDialogButtonBox, QFormLayout,
QTextEdit, QAbstractItemView, QStackedWidget,
QGraphicsOpacityEffect, QListWidget, QPushButton,
QCheckBox, QSpinBox, QLineEdit, QProgressBar, QMainWindow,
QComboBox, QFrame, QSplitter, QListWidgetItem)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QPropertyAnimation, QEvent, QTimer, QTime
from PyQt5.QtGui import QKeySequence, QFont, QTextCharFormat, QColor, QTextCursor
# 尝试导入 qfluentwidgets如果失败则使用标准 PyQt5 组件
try:
from qfluentwidgets import (
setTheme,
Theme,
PushButton,
PrimaryPushButton,
LineEdit,
SpinBox,
ProgressBar,
CardWidget,
InfoBar,
InfoBarPosition,
NavigationInterface,
NavigationItemPosition,
FluentIcon,
PillPushButton
)
try:
from qfluentwidgets import ToggleSwitch
except ImportError:
ToggleSwitch = None # 如果不存在,使用 QCheckBox 替代
FramelessWindow = None
try:
from qfluentwidgets import FramelessWindow
except ImportError:
try:
from qfluentwidgets import FluentWindow as FramelessWindow
except ImportError:
FramelessWindow = None
# 检查 FramelessWindow 是否有 setCentralWidget 方法
if FramelessWindow and not hasattr(FramelessWindow, 'setCentralWidget'):
# 如果没有,标记为不可用,使用替代实现
FramelessWindow = None
USE_FLUENT = True
if FramelessWindow is None:
USE_FLUENT = False
except ImportError:
# 如果 qfluentwidgets 不可用,使用标准 PyQt5 组件
USE_FLUENT = False
FramelessWindow = None
# 如果 FramelessWindow 仍然不可用或没有 setCentralWidget使用我们的替代实现
if not USE_FLUENT or FramelessWindow is None or not hasattr(FramelessWindow, 'setCentralWidget'):
if USE_FLUENT:
USE_FLUENT = False # 回退到标准实现
# 定义替代组件
PushButton = QPushButton
PrimaryPushButton = QPushButton
LineEdit = QLineEdit
SpinBox = QSpinBox
ProgressBar = QProgressBar
PillPushButton = QPushButton
ToggleSwitch = QCheckBox
# 定义简单的替代类
class CardWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setStyleSheet("""
QWidget {
background-color: #2b2b2b;
border-radius: 8px;
padding: 10px;
}
""")
class NavigationInterface(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setStyleSheet("""
QWidget {
background-color: #1e1e1e;
border-radius: 8px;
}
""")
self._nav_layout = QVBoxLayout(self)
self._nav_layout.setContentsMargins(5, 5, 5, 5)
self._nav_layout.setSpacing(5)
def addItem(self, routeKey, icon, text, onClick, position=None):
btn = QPushButton(text, self)
btn.clicked.connect(onClick)
btn.setStyleSheet("""
QPushButton {
text-align: left;
padding: 8px;
border: none;
background-color: transparent;
}
QPushButton:hover {
background-color: #3a3a3a;
}
""")
self._nav_layout.addWidget(btn)
def setMinimumWidth(self, width):
super().setMinimumWidth(width)
def setCompactMode(self, compact):
pass
class InfoBar:
@staticmethod
def success(title, content, parent, position, duration):
QMessageBox.information(parent, title, content)
@staticmethod
def error(title, content, parent, position, duration):
QMessageBox.critical(parent, title, content)
class InfoBarPosition:
TOP_RIGHT = None
class NavigationItemPosition:
TOP = None
class FluentIcon:
MENU = None
REMOVE = None
COPY = None
CLOSE = None
DOCUMENT = None
ADD = None
MULTI_SELECT = None
DELETE = None
SAVE = None
FOLDER = None
SHARE = None
PAGE_RIGHT = None
PLAY = None
UP = None
SYNC = None
PAUSE = None
STOP = None
BULLET_LIST = None
class Theme:
DARK = "dark"
LIGHT = "light"
def setTheme(theme):
pass
# 如果 FramelessWindow 不可用,创建替代实现
if FramelessWindow is None:
class FramelessWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowFlags(Qt.FramelessWindowHint)
import pandas as pd
APP_NAME = "拼多多MCN发布管理工具"
APP_VERSION = "1.4.0"
RUN_STATE_IDLE = "idle"
RUN_STATE_RUNNING = "running"
RUN_STATE_PAUSED = "paused"
RUN_STATE_STOPPING = "stopping"
RUN_STATE_ERROR = "error"
RUN_STATE_DONE = "done"
class TaskWorker(QThread):
"""任务执行工作线程"""
progress = pyqtSignal(int, int, str, str, dict) # current, total, message, level, context
task_status = pyqtSignal(int, str) # task_index, status (success/fail/running)
finished = pyqtSignal(int, int, list) # success_count, fail_count, error_messages
error = pyqtSignal(str)
def __init__(self, tasks, base_folder_path=None, batch_upload=False, input_delay=0, start_index=0):
super().__init__()
self.tasks = tasks
self.base_folder_path = base_folder_path
self.batch_upload = batch_upload
self.input_delay = input_delay
self.start_index = start_index
self._pause = False
self._stop = False
self._current_task_index = 0
self.start_time = None
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:
from main import Pdd
from loguru import logger
self.start_time = datetime.now()
success_count = 0
fail_count = 0
error_messages = []
total = len(self.tasks)
base_folder_path = getattr(self, 'base_folder_path', None)
for idx, data in enumerate(self.tasks, self.start_index):
if self._stop:
error_messages.append("任务已停止")
break
self._wait_if_paused()
self._current_task_index = idx
current_index = idx - self.start_index + 1
display_index = idx + 1
user_id = data.get('多多 id', '')
topics = data.get('话题', '')
time_start = data.get('计算后的发布时间', '')
url = data.get('达人链接', '')
index = data.get('序号', '')
# 发送任务开始状态
self.task_status.emit(idx, "running")
self.progress.emit(
current_index,
total,
f"正在执行任务 {display_index}/{total} - 多多: {user_id}",
"INFO",
{"task_index": idx, "user_id": user_id, "source": "TaskWorker"},
)
if not url or not user_id:
error_msg = f"任务 {display_index}: 缺少必需参数 - 多多={user_id}, 达人链接={url}"
logger.warning(error_msg)
error_messages.append(error_msg)
fail_count += 1
self.task_status.emit(idx, "fail")
self.progress.emit(
current_index,
total,
error_msg,
"WARNING",
{"task_index": idx, "user_id": user_id, "source": "TaskWorker"},
)
continue
# 处理话题格式
if topics:
topic_list = []
for sep in ['', '', '-']:
if sep in topics:
topic_list = [t.strip() for t in topics.split(sep) if t.strip()]
break
if not topic_list:
topic_list = [topics.strip()] if topics.strip() else []
ht = ' '.join([f"#{t}#" for t in topic_list if t])
else:
ht = ""
try:
logger.info(
f"开始执行任务 {display_index} - 多多: {user_id}, 达人链接: {url}, 大文件夹路径: {base_folder_path}, 序号: {index}"
)
pdd = Pdd(
url=url,
user_id=user_id,
time_start=time_start if time_start else None,
ht=ht,
index=index,
)
folder_path = base_folder_path if base_folder_path and os.path.exists(base_folder_path) else None
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"任务 {display_index}: 未找到多多ID={user_id} 的视频文件"
logger.warning(error_msg)
error_messages.append(error_msg)
fail_count += 1
self.task_status.emit(idx, "fail")
self.progress.emit(
current_index,
total,
error_msg,
"WARNING",
{"task_index": idx, "user_id": user_id, "source": "TaskWorker"},
)
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"任务 {display_index} 执行成功")
success_count += 1
self.task_status.emit(idx, "success")
self.progress.emit(
current_index,
total,
f"任务 {display_index} 执行成功",
"SUCCESS",
{"task_index": idx, "user_id": user_id, "source": "TaskWorker"},
)
except Exception as e:
error_msg = f"任务 {display_index} 执行失败 - 多多: {user_id}, 错误: {str(e)}"
logger.error(error_msg)
logger.exception("详细错误信息:")
error_messages.append(error_msg)
fail_count += 1
self.task_status.emit(idx, "fail")
self.progress.emit(
current_index,
total,
error_msg,
"ERROR",
{"task_index": idx, "user_id": user_id, "source": "TaskWorker"},
)
continue
self.finished.emit(success_count, fail_count, error_messages)
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 []
target_folder = os.path.join(base_folder_path, str(user_id))
if not os.path.isdir(target_folder):
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(APP_NAME)
self.setGeometry(100, 100, 1400, 900)
# 存储原始数据和处理后的数据
self.raw_data = []
self.processed_data = []
self.worker = None
self.task_status_map = {} # 任务状态映射 {row_index: "success/fail/running/idle"}
self.start_time = None
self.timer = QTimer()
self.timer.timeout.connect(self.update_elapsed_time)
self.run_state = RUN_STATE_IDLE
self.last_error = ""
self.current_task_index = None
self.log_records = []
self._init_session()
# 创建主窗口部件
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)
self.apply_ui_style()
if USE_FLUENT:
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()
layout.setSpacing(12)
layout.setContentsMargins(6, 6, 6, 6)
self.main_page.setLayout(layout)
# 日志页面布局
log_page_layout = QVBoxLayout()
log_page_layout.setSpacing(12)
log_page_layout.setContentsMargins(6, 6, 6, 6)
self.log_page.setLayout(log_page_layout)
# 顶部标题栏
title_card = CardWidget()
title_layout = QHBoxLayout(title_card)
title_layout.setContentsMargins(16, 8, 16, 8)
title_card.setObjectName("titleCard")
nav_toggle = PushButton("")
if USE_FLUENT and FluentIcon.MENU:
nav_toggle.setIcon(FluentIcon.MENU)
nav_toggle.setFixedWidth(32)
nav_toggle.clicked.connect(self.toggle_nav)
title_layout.addWidget(nav_toggle)
title_label = QLabel(APP_NAME)
title_font = QFont("Arial", 14, QFont.Bold)
title_label.setFont(title_font)
title_layout.addWidget(title_label)
self.version_label = QLabel(f"v{APP_VERSION}")
self.version_label.setStyleSheet("color: #9aa4b2; font-size: 11px; padding-left: 8px;")
title_layout.addWidget(self.version_label)
title_layout.addStretch()
self.status_indicator = QLabel("")
self.status_indicator.setStyleSheet("color: #67C23A; font-weight: bold; font-size: 14px;")
title_layout.addWidget(self.status_indicator)
self.title_state_badge = QLabel("空闲")
self.title_state_badge.setStyleSheet(
"padding: 2px 8px; border-radius: 8px; "
"background-color: #2b2f3a; color: #c9d1e5; font-size: 10px;"
)
title_layout.addWidget(self.title_state_badge)
if ToggleSwitch and USE_FLUENT:
self.theme_toggle = ToggleSwitch()
self.theme_toggle.setChecked(True)
self.theme_toggle.checkedChanged.connect(self.toggle_theme)
else:
self.theme_toggle = QCheckBox("暗色主题")
self.theme_toggle.setChecked(True)
self.theme_toggle.stateChanged.connect(lambda state: self.toggle_theme(state == Qt.Checked))
title_layout.addWidget(self.theme_toggle)
min_btn = PushButton("")
if USE_FLUENT and FluentIcon.REMOVE:
min_btn.setIcon(FluentIcon.REMOVE)
min_btn.setFixedWidth(32)
min_btn.clicked.connect(self.showMinimized)
title_layout.addWidget(min_btn)
max_btn = PushButton("")
if USE_FLUENT and FluentIcon.COPY:
max_btn.setIcon(FluentIcon.COPY)
max_btn.setFixedWidth(32)
max_btn.clicked.connect(self.toggle_maximize)
title_layout.addWidget(max_btn)
close_btn = PushButton("×")
if USE_FLUENT and FluentIcon.CLOSE:
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)
settings_layout.setContentsMargins(16, 12, 16, 12)
settings_layout.setSpacing(10)
settings_title = QLabel("基础设置")
settings_title.setStyleSheet(self.card_title_style())
settings_layout.addWidget(settings_title)
settings_divider = self.section_divider()
settings_layout.addWidget(settings_divider)
settings_form = QFormLayout()
settings_form.setHorizontalSpacing(12)
settings_form.setVerticalSpacing(8)
folder_row = QWidget()
folder_layout = QHBoxLayout(folder_row)
folder_layout.setContentsMargins(0, 0, 0, 0)
self.folder_path_input = LineEdit()
self.folder_path_input.setPlaceholderText("请输入大文件夹路径例如C:\\Users\\user\\data")
folder_browse_btn = PushButton("浏览...")
folder_browse_btn.clicked.connect(self.browse_folder)
folder_layout.addWidget(self.folder_path_input, 1)
folder_layout.addWidget(folder_browse_btn)
settings_form.addRow("大文件夹路径:", folder_row)
self.input_delay_spin = SpinBox()
self.input_delay_spin.setRange(0, 3600)
self.input_delay_spin.setValue(0)
self.input_delay_spin.setToolTip("每个视频处理前等待时间,便于手动填写信息")
settings_form.addRow("填写信息间隔(秒):", self.input_delay_spin)
settings_layout.addLayout(settings_form)
strategy_title = QLabel("运行策略")
strategy_title.setStyleSheet(self.card_title_style())
settings_layout.addWidget(strategy_title)
strategy_divider = self.section_divider()
settings_layout.addWidget(strategy_divider)
strategy_form = QFormLayout()
strategy_form.setHorizontalSpacing(12)
strategy_form.setVerticalSpacing(8)
self.start_index_spin = SpinBox()
self.start_index_spin.setRange(1, 999999)
self.start_index_spin.setValue(1)
self.start_index_spin.setToolTip("支持断点续跑,从指定行号开始")
strategy_form.addRow("从第几行开始执行:", self.start_index_spin)
self.fail_policy_combo = QComboBox()
self.fail_policy_combo.addItems(["遇错继续", "遇错暂停"])
self.fail_policy_combo.setToolTip("失败后是否自动暂停,便于人工处理")
strategy_form.addRow("失败处理策略:", self.fail_policy_combo)
settings_layout.addLayout(strategy_form)
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)
# 操作按钮卡片
action_card = CardWidget()
action_layout = QHBoxLayout(action_card)
action_layout.setContentsMargins(16, 10, 16, 10)
action_layout.setSpacing(10)
self.import_btn = PushButton("导入Excel")
if USE_FLUENT and FluentIcon.DOCUMENT:
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("添加行")
if USE_FLUENT and FluentIcon.ADD:
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("批量添加")
if USE_FLUENT and FluentIcon.MULTI_SELECT:
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("删除选中行")
if USE_FLUENT and FluentIcon.DELETE:
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("保存配置")
if USE_FLUENT and FluentIcon.SAVE:
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("加载配置")
if USE_FLUENT and FluentIcon.FOLDER:
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")
if USE_FLUENT and FluentIcon.SHARE:
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("导出模板")
if USE_FLUENT and FluentIcon.PAGE_RIGHT:
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("执行任务")
if USE_FLUENT and FluentIcon.PLAY:
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("一键上传")
if USE_FLUENT and FluentIcon.UP:
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)
self.apply_button_style([
self.import_btn,
self.add_row_btn,
self.batch_add_btn,
self.delete_row_btn,
self.save_btn,
self.load_btn,
self.export_btn,
self.export_template_btn,
self.execute_btn,
self.one_click_upload_btn,
])
layout.addWidget(action_card)
self.fade_in(action_card)
# 执行进度卡片(合并任务队列 + 进度条)
progress_center_card = CardWidget()
progress_center_layout = QVBoxLayout(progress_center_card)
progress_center_layout.setContentsMargins(16, 12, 16, 12)
progress_center_layout.setSpacing(8)
progress_center_title = QLabel("执行进度")
progress_center_title.setStyleSheet(self.card_title_style())
progress_center_layout.addWidget(progress_center_title)
progress_center_layout.addWidget(self.section_divider())
queue_title = QLabel("任务队列")
queue_title.setStyleSheet("color: #9aa4b2; font-size: 11px;")
progress_center_layout.addWidget(queue_title)
self.queue_list = QListWidget()
self.queue_list.setFixedHeight(90)
progress_center_layout.addWidget(self.queue_list)
progress_center_layout.addWidget(self.section_divider())
self.progress_bar = ProgressBar()
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
self.progress_bar.setFormat("%p% (%v/%m)")
progress_center_layout.addWidget(self.progress_bar)
self.current_task_label = QLabel("当前任务: --")
self.current_task_label.setStyleSheet("color: #9aa4b2; font-size: 11px;")
progress_center_layout.addWidget(self.current_task_label)
self.eta_label = QLabel("预计剩余时间: --")
self.eta_label.setStyleSheet("color: #888; font-size: 11px; padding-top: 4px;")
progress_center_layout.addWidget(self.eta_label)
layout.addWidget(progress_center_card)
self.fade_in(progress_center_card)
# 运行中心卡片(合并运行控制 + 状态概览)
run_center_card = CardWidget()
run_center_layout = QVBoxLayout(run_center_card)
run_center_layout.setContentsMargins(16, 12, 16, 12)
run_center_layout.setSpacing(10)
run_center_title = QLabel("运行中心")
run_center_title.setStyleSheet(self.card_title_style())
run_center_layout.addWidget(run_center_title)
run_center_layout.addWidget(self.section_divider())
run_controls_layout = QHBoxLayout()
run_controls_layout.addWidget(QLabel("运行控制:"))
self.retry_failed_btn = PillPushButton("重试失败")
if USE_FLUENT and FluentIcon.SYNC:
self.retry_failed_btn.setIcon(FluentIcon.SYNC)
self.retry_failed_btn.setEnabled(False)
self.retry_failed_btn.clicked.connect(self.retry_failed_tasks)
run_controls_layout.addWidget(self.retry_failed_btn)
self.retry_current_btn = PillPushButton("重试当前")
self.retry_current_btn.setEnabled(False)
self.retry_current_btn.clicked.connect(self.retry_current_task)
run_controls_layout.addWidget(self.retry_current_btn)
self.pause_btn = PillPushButton("暂停")
if USE_FLUENT and FluentIcon.PAUSE:
self.pause_btn.setIcon(FluentIcon.PAUSE)
self.pause_btn.setEnabled(False)
self.pause_btn.clicked.connect(self.pause_or_resume)
run_controls_layout.addWidget(self.pause_btn)
self.stop_btn = PillPushButton("停止")
if USE_FLUENT and FluentIcon.STOP:
self.stop_btn.setIcon(FluentIcon.STOP)
self.stop_btn.setEnabled(False)
self.stop_btn.clicked.connect(self.stop_worker)
run_controls_layout.addWidget(self.stop_btn)
self.apply_button_style([
self.retry_failed_btn,
self.retry_current_btn,
self.pause_btn,
self.stop_btn,
])
run_controls_layout.addStretch()
self.elapsed_time_label = QLabel("已用时间: 00:00:00")
self.elapsed_time_label.setStyleSheet("color: #888; font-size: 11px;")
run_controls_layout.addWidget(self.elapsed_time_label)
run_center_layout.addLayout(run_controls_layout)
run_center_layout.addWidget(self.section_divider())
overview_layout = QFormLayout()
overview_layout.setVerticalSpacing(8)
overview_layout.setLabelAlignment(Qt.AlignRight)
overview_layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
self.run_state_label = QLabel("就绪")
self.session_label = QLabel(self.session_id)
self.last_error_label = QLabel("--")
self.task_summary_label = QLabel("总计: 0 | 成功: 0 | 失败: 0 | 执行中: 0")
self.run_state_label.setStyleSheet("font-weight: bold; color: #e5eaf3;")
self.last_error_label.setStyleSheet("color: #e67e22;")
self.task_summary_label.setStyleSheet("color: #9aa4b2;")
overview_layout.addRow("运行状态", self.run_state_label)
overview_layout.addRow("会话编号", self.session_label)
overview_layout.addRow("最近异常", self.last_error_label)
overview_layout.addRow("任务概览", self.task_summary_label)
run_center_layout.addLayout(overview_layout)
layout.addWidget(run_center_card)
self.fade_in(run_center_card)
# 配置列表卡片
table_group = CardWidget()
table_layout = QVBoxLayout(table_group)
table_layout.setContentsMargins(16, 12, 16, 12)
table_layout.setSpacing(8)
table_title = QLabel("配置列表")
table_title.setStyleSheet(self.card_title_style())
table_layout.addWidget(table_title)
stats_layout = QHBoxLayout()
self.stats_label = QLabel("总计: 0 条配置")
self.stats_label.setStyleSheet("QLabel { color: #888; font-weight: bold; font-size: 11px; }")
stats_layout.addWidget(self.stats_label)
stats_layout.addStretch()
table_layout.addLayout(stats_layout)
table_toolbar = QHBoxLayout()
table_toolbar.addWidget(QLabel("快速筛选"))
self.table_search_input = QLineEdit()
self.table_search_input.setPlaceholderText("搜索多多id / 话题 / 链接 / 执行人")
self.table_search_input.textChanged.connect(self.apply_table_filter)
table_toolbar.addWidget(self.table_search_input, 2)
self.table_status_combo = QComboBox()
self.table_status_combo.addItems(["全部", "待执行", "执行中", "成功", "失败"])
self.table_status_combo.currentTextChanged.connect(self.apply_table_filter)
table_toolbar.addWidget(self.table_status_combo)
clear_filter_btn = PushButton("清除筛选")
clear_filter_btn.clicked.connect(self.clear_table_filter)
table_toolbar.addWidget(clear_filter_btn)
table_toolbar.addStretch()
table_layout.addLayout(table_toolbar)
# 创建表格(增加状态列)
self.table = QTableWidget()
self.table.setColumnCount(10)
self.table.setHorizontalHeaderLabels([
"状态", "多多 id", "序号", "话题", "定时发布", "间隔时间(分钟)",
"达人链接", "执行人", "情况", "计算后的发布时间"
])
header = self.table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.Fixed) # 状态列固定宽度
header.resizeSection(0, 60)
for i in range(1, 10):
header.setSectionResizeMode(i, QHeaderView.Stretch)
self.table.setEditTriggers(QTableWidget.DoubleClicked | QTableWidget.SelectedClicked | QTableWidget.EditKeyPressed)
self.table.setSelectionBehavior(QTableWidget.SelectRows)
self.table.setSelectionMode(QTableWidget.ExtendedSelection)
self.table.setAlternatingRowColors(True)
self.table.setWordWrap(False)
self.table.setShowGrid(False)
self.table.verticalHeader().setVisible(False)
self.table.verticalHeader().setDefaultSectionSize(28)
self.table.setStyleSheet("""
QTableWidget::item:hover { background-color: rgba(64, 158, 255, 60); }
QTableWidget::item:selected { background-color: rgba(76, 125, 255, 90); }
""")
self.table.cellChanged.connect(self.on_cell_changed)
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
self.table.customContextMenuRequested.connect(self.show_context_menu)
table_layout.addWidget(self.table)
layout.addWidget(table_group)
self.fade_in(table_group)
# 进度信息已合并到执行进度卡片
# 执行日志卡片
log_group = CardWidget()
log_layout = QVBoxLayout(log_group)
log_layout.setContentsMargins(16, 12, 16, 12)
log_layout.setSpacing(8)
log_title = QLabel("执行日志")
log_title.setStyleSheet(self.card_title_style())
log_layout.addWidget(log_title)
self.log_summary_label = QLabel("最近错误: --")
self.log_summary_label.setStyleSheet("color: #e67e22; font-size: 11px;")
log_layout.addWidget(self.log_summary_label)
self.log_count_label = QLabel("错误: 0 | 警告: 0")
self.log_count_label.setStyleSheet("color: #9aa4b2; font-size: 11px;")
log_layout.addWidget(self.log_count_label)
log_toolbar = QHBoxLayout()
clear_log_btn = PushButton("清空日志")
if USE_FLUENT and FluentIcon.DELETE:
clear_log_btn.setIcon(FluentIcon.DELETE)
clear_log_btn.clicked.connect(self.clear_log)
log_toolbar.addWidget(clear_log_btn)
export_log_btn = PushButton("导出日志")
if USE_FLUENT and FluentIcon.SHARE:
export_log_btn.setIcon(FluentIcon.SHARE)
export_log_btn.clicked.connect(self.export_log)
log_toolbar.addWidget(export_log_btn)
open_log_btn = PushButton("打开日志目录")
open_log_btn.clicked.connect(self.open_log_folder)
log_toolbar.addWidget(open_log_btn)
self.log_level_combo = QComboBox()
self.log_level_combo.addItems(["全部", "INFO", "SUCCESS", "WARNING", "ERROR", "DEBUG"])
self.log_level_combo.currentTextChanged.connect(self.render_log_output)
log_toolbar.addWidget(self.log_level_combo)
self.log_search_input = QLineEdit()
self.log_search_input.setPlaceholderText("搜索日志关键词")
self.log_search_input.textChanged.connect(self.render_log_output)
log_toolbar.addWidget(self.log_search_input)
log_toolbar.addStretch()
log_layout.addLayout(log_toolbar)
log_splitter = QSplitter(Qt.Horizontal)
self.log_list = QListWidget()
self.log_list.setMinimumWidth(280)
self.log_list.currentItemChanged.connect(self.on_log_item_changed)
log_splitter.addWidget(self.log_list)
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
# 使用等宽字体,优先使用系统可用字体
font_families = ["Consolas", "Monaco", "Courier New", "Courier", "monospace"]
log_font = None
for font_family in font_families:
if QFont(font_family).exactMatch():
log_font = QFont(font_family, 9)
break
if log_font is None:
log_font = QFont("monospace", 9)
self.log_output.setFont(log_font)
log_splitter.addWidget(self.log_output)
log_splitter.setStretchFactor(0, 2)
log_splitter.setStretchFactor(1, 5)
log_layout.addWidget(log_splitter)
log_page_layout.addWidget(log_group)
self.fade_in(log_group)
# 状态标签
self.status_label = QLabel("就绪")
self.status_label.setStyleSheet(
"padding: 8px 12px; border-radius: 8px; "
"background: qlineargradient(x1:0, y1:0, x2:1, y2:0, "
"stop:0 #2b2f3a, stop:1 #3a3f4b); color: #e5eaf3; font-weight: bold;"
)
layout.addWidget(self.status_label)
self.set_status("idle", "就绪")
self.statusBar().showMessage("就绪")
# 侧边导航
self.nav.addItem(
routeKey="config",
icon=FluentIcon.DOCUMENT if USE_FLUENT else None,
text="配置",
onClick=lambda: self.switch_page(self.main_page),
position=NavigationItemPosition.TOP if USE_FLUENT else None
)
self.nav.addItem(
routeKey="logs",
icon=FluentIcon.BULLET_LIST if USE_FLUENT else None,
text="日志",
onClick=lambda: self.switch_page(self.log_page),
position=NavigationItemPosition.TOP if USE_FLUENT else None
)
self.update_status_overview()
def find_default_folder(self):
"""自动查找桌面上的'多多发文文件'文件夹"""
try:
home = os.path.expanduser("~")
desktop_paths = [
os.path.join(home, "Desktop", "多多发文文件"),
os.path.join(home, "桌面", "多多发文文件"),
os.path.join(home, "Desktop"),
os.path.join(home, "桌面"),
]
for desktop_path in desktop_paths:
if os.path.exists(desktop_path) and os.path.isdir(desktop_path):
if "多多发文文件" in desktop_path:
return desktop_path
target_folder = os.path.join(desktop_path, "多多发文文件")
if os.path.exists(target_folder) and os.path.isdir(target_folder):
return target_folder
return None
except Exception as e:
print(f"查找默认文件夹时出错:{e}")
return None
def apply_ui_style(self):
"""统一界面风格,提升协调感"""
if USE_FLUENT:
return
base_style = """
QWidget {
font-family: "Arial";
font-size: 12px;
color: #e5eaf3;
background-color: #1e1e1e;
}
QLabel {
color: #d7dde8;
}
QLineEdit, QSpinBox, QComboBox, QTextEdit {
background-color: #2b2f3a;
border: 1px solid #3a3f4b;
border-radius: 6px;
padding: 6px 8px;
}
QPushButton {
background-color: #2b2f3a;
border: 1px solid #3a3f4b;
border-radius: 6px;
padding: 6px 12px;
}
QPushButton:hover {
background-color: #343a46;
}
QPushButton:pressed {
background-color: #2a2f39;
}
QTableWidget {
background-color: #1f232b;
border: 1px solid #2c313c;
border-radius: 6px;
gridline-color: transparent;
}
QHeaderView::section {
background-color: #2b2f3a;
color: #c9d1e5;
border: none;
padding: 6px 8px;
}
QListWidget {
background-color: #1f232b;
border: 1px solid #2c313c;
border-radius: 6px;
}
QListWidget::item:selected {
background-color: rgba(76, 125, 255, 90);
}
QTextEdit {
border: 1px solid #2c313c;
}
QProgressBar {
background-color: #2b2f3a;
border: 1px solid #3a3f4b;
border-radius: 6px;
text-align: center;
color: #c9d1e5;
}
QProgressBar::chunk {
background-color: #4c7dff;
border-radius: 6px;
}
QFrame[frameShape="4"] {
color: #2c313c;
}
"""
self.setStyleSheet(base_style)
def apply_button_style(self, buttons):
"""统一按钮尺寸与视觉节奏"""
for btn in buttons:
btn.setMinimumHeight(32)
@staticmethod
def card_title_style():
"""统一卡片标题风格"""
return "font-weight: bold; font-size: 12px; color: #e5eaf3;"
@staticmethod
def section_divider():
"""卡片内分隔线"""
line = QFrame()
line.setFrameShape(QFrame.HLine)
line.setFrameShadow(QFrame.Sunken)
line.setStyleSheet("color: #2c313c;")
return line
def _init_session(self):
"""初始化会话与审计日志"""
session_stamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
self.session_id = f"RUN-{session_stamp}"
log_dir = Path(__file__).resolve().parent / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
self.log_file_path = log_dir / f"session_{session_stamp}.log"
self.audit_log_path = log_dir / f"session_{session_stamp}.jsonl"
def update_status_overview(self):
"""更新状态概览卡片"""
success_count = sum(1 for s in self.task_status_map.values() if s == "success")
fail_count = sum(1 for s in self.task_status_map.values() if s == "fail")
running_count = sum(1 for s in self.task_status_map.values() if s == "running")
total_count = self.table.rowCount()
state_map = {
RUN_STATE_IDLE: "就绪",
RUN_STATE_RUNNING: "运行中",
RUN_STATE_PAUSED: "已暂停",
RUN_STATE_STOPPING: "正在停止",
RUN_STATE_ERROR: "异常",
RUN_STATE_DONE: "已完成",
}
self.run_state_label.setText(state_map.get(self.run_state, "就绪"))
if hasattr(self, "title_state_badge"):
title_state = state_map.get(self.run_state, "就绪")
color_map = {
RUN_STATE_IDLE: "#2b2f3a",
RUN_STATE_RUNNING: "#2b5cff",
RUN_STATE_PAUSED: "#5f6c7b",
RUN_STATE_STOPPING: "#b36b00",
RUN_STATE_ERROR: "#c0392b",
RUN_STATE_DONE: "#1f9d55",
}
bg = color_map.get(self.run_state, "#2b2f3a")
self.title_state_badge.setText(title_state)
self.title_state_badge.setStyleSheet(
"padding: 2px 8px; border-radius: 8px; "
f"background-color: {bg}; color: #ffffff; font-size: 10px;"
)
self.task_summary_label.setText(
f"总计: {total_count} | 成功: {success_count} | 失败: {fail_count} | 执行中: {running_count}"
)
self.last_error_label.setText(self.last_error or "--")
def update_current_task_label(self, row):
"""更新当前执行任务提示"""
if not hasattr(self, "current_task_label"):
return
if row is None or row >= self.table.rowCount():
self.current_task_label.setText("当前任务: --")
return
user_id = self.table.item(row, 1).text() if self.table.item(row, 1) else ""
self.current_task_label.setText(f"当前任务: 行{row + 1} 多多:{user_id}")
def browse_folder(self):
"""浏览选择文件夹"""
folder_path = QFileDialog.getExistingDirectory(self, "选择大文件夹路径")
if folder_path:
self.folder_path_input.setText(folder_path)
def import_excel(self):
"""导入Excel文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择Excel文件", "", "Excel Files (*.xlsx *.xls)"
)
if not file_path:
return
try:
df = pd.read_excel(file_path)
required_columns = ['多多 id', '话题(以中文"-"分隔)',
'定时发布', '间隔时间', '达人链接', '情况']
optional_columns = ['序号', '执行人']
missing_columns = []
column_mapping = {}
for req_col in required_columns:
found = False
for col in df.columns:
if req_col in str(col) or str(col) in req_col:
column_mapping[req_col] = col
found = True
break
if not found:
missing_columns.append(req_col)
for opt_col in optional_columns:
found = False
for col in df.columns:
if opt_col in str(col) or str(col) in opt_col:
column_mapping[opt_col] = col
found = True
break
if missing_columns:
QMessageBox.warning(
self, "错误",
f"Excel文件缺少以下必需的列\n{', '.join(missing_columns)}"
)
return
self.raw_data = []
for index, row in df.iterrows():
index_value = ''
if '序号' in column_mapping:
index_value = str(row[column_mapping['序号']]) if pd.notna(row[column_mapping['序号']]) else ''
executor_value = ''
if '执行人' in column_mapping:
executor_value = str(row[column_mapping['执行人']]) if pd.notna(row[column_mapping['执行人']]) else ''
data = {
'多多 id': str(row[column_mapping['多多 id']]) if pd.notna(row[column_mapping['多多 id']]) else '',
'序号': index_value,
'话题': str(row[column_mapping['话题(以中文"-"分隔)']]) if pd.notna(row[column_mapping['话题(以中文"-"分隔)']]) else '',
'定时发布': str(row[column_mapping['定时发布']]) if pd.notna(row[column_mapping['定时发布']]) else '',
'间隔时间': str(row[column_mapping['间隔时间']]) if pd.notna(row[column_mapping['间隔时间']]) else '',
'达人链接': str(row[column_mapping['达人链接']]) if pd.notna(row[column_mapping['达人链接']]) else '',
'执行人': executor_value,
'情况': str(row[column_mapping['情况']]) if pd.notna(row[column_mapping['情况']]) else '',
}
self.raw_data.append(data)
self.process_data()
self.display_data()
self.log(f"成功导入 {len(self.processed_data)} 条数据", "SUCCESS")
self.show_info("导入成功", f"已导入 {len(self.processed_data)} 条数据")
self.enable_action_buttons()
except Exception as e:
QMessageBox.critical(self, "错误", f"导入Excel文件时出错\n{str(e)}")
self.log(f"导入失败: {str(e)}", "ERROR")
self.show_error("导入失败", str(e))
import traceback
traceback.print_exc()
def process_data(self):
"""处理数据,计算相同多多 id 的间隔时间"""
self.processed_data = []
user_groups = {}
for data in self.raw_data:
user_id = data['多多 id']
if user_id not in user_groups:
user_groups[user_id] = []
user_groups[user_id].append(data)
def parse_time(time_str):
if not time_str:
return None
for fmt in ["%Y-%m-%d %H:%M:%S", "%Y/%m/%d %H:%M:%S",
"%Y-%m-%d %H:%M", "%Y/%m/%d %H:%M"]:
try:
return datetime.strptime(time_str, fmt)
except ValueError:
continue
return None
def parse_interval(interval_str):
if not interval_str:
return 0
numbers = re.findall(r'\d+', str(interval_str))
return int(numbers[0]) if numbers else 0
for user_id, user_data_list in user_groups.items():
for index, data in enumerate(user_data_list):
processed_item = data.copy()
current_time_str = data.get('定时发布', '').strip()
current_time = parse_time(current_time_str)
if current_time:
processed_item['计算后的发布时间'] = current_time_str
else:
if index > 0:
prev_calculated_time_str = self.processed_data[-1].get('计算后的发布时间', '').strip()
prev_calculated_time = parse_time(prev_calculated_time_str)
if prev_calculated_time:
interval_str = data.get('间隔时间', '').strip()
if not interval_str:
interval_str = user_data_list[index - 1].get('间隔时间', '').strip()
interval_minutes = parse_interval(interval_str)
if interval_minutes > 0:
calculated_time = prev_calculated_time + timedelta(minutes=interval_minutes)
processed_item['计算后的发布时间'] = calculated_time.strftime("%Y-%m-%d %H:%M:%S")
else:
processed_item['计算后的发布时间'] = ''
else:
processed_item['计算后的发布时间'] = ''
else:
processed_item['计算后的发布时间'] = ''
self.processed_data.append(processed_item)
def display_data(self):
"""在表格中显示数据"""
self.table.blockSignals(True)
self.table.setRowCount(len(self.processed_data))
for row, data in enumerate(self.processed_data):
# 状态列
status_item = QTableWidgetItem("待执行")
status_item.setFlags(status_item.flags() & ~Qt.ItemIsEditable)
status_item.setTextAlignment(Qt.AlignCenter)
self.table.setItem(row, 0, status_item)
self.task_status_map[row] = "idle"
# 多多 id
self.table.setItem(row, 1, QTableWidgetItem(str(data.get('多多 id', ''))))
# 序号
self.table.setItem(row, 2, QTableWidgetItem(str(data.get('序号', ''))))
# 话题
self.table.setItem(row, 3, QTableWidgetItem(str(data.get('话题', ''))))
# 定时发布
self.table.setItem(row, 4, QTableWidgetItem(str(data.get('定时发布', ''))))
# 间隔时间
self.table.setItem(row, 5, QTableWidgetItem(str(data.get('间隔时间', ''))))
# 达人链接
self.table.setItem(row, 6, QTableWidgetItem(str(data.get('达人链接', ''))))
# 执行人
self.table.setItem(row, 7, QTableWidgetItem(str(data.get('执行人', ''))))
# 情况
self.table.setItem(row, 8, QTableWidgetItem(str(data.get('情况', ''))))
# 计算后的发布时间
calc_item = QTableWidgetItem(str(data.get('计算后的发布时间', '')))
calc_item.setFlags(calc_item.flags() & ~Qt.ItemIsEditable)
self.table.setItem(row, 9, calc_item)
self.table.blockSignals(False)
self.update_stats()
self.apply_table_filter()
def update_task_status(self, task_index, status):
"""更新任务状态显示"""
if task_index < self.table.rowCount():
status_item = self.table.item(task_index, 0)
if status_item:
status_text = {
"idle": "待执行",
"running": "执行中",
"success": "✓ 成功",
"fail": "✗ 失败"
}.get(status, "未知")
status_item.setText(status_text)
self.task_status_map[task_index] = status
if status == "running":
self.current_task_index = task_index
self.update_current_task_label(task_index)
# 设置状态颜色
if status == "success":
status_item.setBackground(QColor(53, 194, 119))
status_item.setForeground(QColor(255, 255, 255))
elif status == "fail":
status_item.setBackground(QColor(231, 76, 60))
status_item.setForeground(QColor(255, 255, 255))
elif status == "running":
status_item.setBackground(QColor(76, 125, 255))
status_item.setForeground(QColor(255, 255, 255))
else:
status_item.setBackground(QColor(0, 0, 0, 0))
status_item.setForeground(QColor(136, 136, 136))
if status == "fail" and self.worker and self.fail_policy_combo.currentText() == "遇错暂停":
self.worker.pause()
self.pause_btn.setText("继续")
self.set_status("paused", "检测到失败,已自动暂停")
self.log("失败自动暂停:请检查任务后继续", "WARNING")
self.update_status_overview()
def on_cell_changed(self, row, column):
"""当单元格内容改变时,重新处理数据"""
if column == 0 or column == 9: # 状态列和计算后的发布时间列不允许直接编辑
return
self.sync_table_to_raw_data()
self.process_data()
self.update_table_from_processed_data()
self.update_stats()
def sync_table_to_raw_data(self):
"""从表格同步数据到raw_data"""
self.raw_data = []
for row in range(self.table.rowCount()):
data = {
'多多 id': self.table.item(row, 1).text() if self.table.item(row, 1) else '',
'序号': self.table.item(row, 2).text() if self.table.item(row, 2) else '',
'话题': self.table.item(row, 3).text() if self.table.item(row, 3) else '',
'定时发布': self.table.item(row, 4).text() if self.table.item(row, 4) else '',
'间隔时间': self.table.item(row, 5).text() if self.table.item(row, 5) else '',
'达人链接': self.table.item(row, 6).text() if self.table.item(row, 6) else '',
'执行人': self.table.item(row, 7).text() if self.table.item(row, 7) else '',
'情况': self.table.item(row, 8).text() if self.table.item(row, 8) else '',
}
self.raw_data.append(data)
def update_table_from_processed_data(self):
"""从processed_data更新表格只更新计算后的发布时间列"""
self.table.blockSignals(True)
for row in range(min(len(self.processed_data), self.table.rowCount())):
calculated_time = self.processed_data[row].get('计算后的发布时间', '')
if self.table.item(row, 9):
self.table.item(row, 9).setText(str(calculated_time))
else:
item = QTableWidgetItem(str(calculated_time))
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
self.table.setItem(row, 9, item)
self.table.blockSignals(False)
self.update_stats()
self.apply_table_filter()
def update_stats(self):
"""更新统计信息"""
count = self.table.rowCount()
success_count = sum(1 for s in self.task_status_map.values() if s == "success")
fail_count = sum(1 for s in self.task_status_map.values() if s == "fail")
running_count = sum(1 for s in self.task_status_map.values() if s == "running")
stats_text = f"总计: {count}"
if success_count > 0 or fail_count > 0 or running_count > 0:
stats_text += f" | 成功: {success_count} | 失败: {fail_count}"
if running_count > 0:
stats_text += f" | 执行中: {running_count}"
self.stats_label.setText(stats_text)
self.update_status_overview()
def apply_table_filter(self):
"""表格筛选(关键词 + 状态)"""
if not hasattr(self, "table_search_input") or not hasattr(self, "table_status_combo"):
return
keyword = self.table_search_input.text().strip().lower()
status_filter = self.table_status_combo.currentText()
for row in range(self.table.rowCount()):
status_text = self.table.item(row, 0).text() if self.table.item(row, 0) else ""
status_ok = status_filter == "全部" or status_text == status_filter
if not keyword:
keyword_ok = True
else:
row_texts = []
for col in range(1, 9):
item = self.table.item(row, col)
if item:
row_texts.append(item.text())
combined = " ".join(row_texts).lower()
keyword_ok = keyword in combined
self.table.setRowHidden(row, not (status_ok and keyword_ok))
def clear_table_filter(self):
"""清除表格筛选"""
if hasattr(self, "table_search_input"):
self.table_search_input.clear()
if hasattr(self, "table_status_combo"):
self.table_status_combo.setCurrentText("全部")
self.apply_table_filter()
def show_context_menu(self, position):
"""显示右键菜单"""
menu = QMenu(self)
add_action = menu.addAction("添加行")
add_action.triggered.connect(self.add_row)
batch_add_action = menu.addAction("批量添加行")
batch_add_action.triggered.connect(self.batch_add_rows)
menu.addSeparator()
delete_action = menu.addAction("删除选中行")
delete_action.triggered.connect(self.delete_selected_rows)
menu.addSeparator()
copy_action = menu.addAction("复制行")
copy_action.triggered.connect(self.copy_selected_rows)
paste_action = menu.addAction("粘贴行")
paste_action.triggered.connect(self.paste_rows)
menu.addSeparator()
# 重试选中行
retry_action = menu.addAction("重试选中任务")
retry_action.triggered.connect(self.retry_selected_tasks)
retry_action.setEnabled(self.worker is None)
menu.exec_(self.table.viewport().mapToGlobal(position))
def add_row(self):
"""添加一行空数据"""
row_count = self.table.rowCount()
self.table.insertRow(row_count)
for col in range(10):
if col == 0:
item = QTableWidgetItem("待执行")
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
item.setTextAlignment(Qt.AlignCenter)
elif col == 9:
item = QTableWidgetItem("")
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
else:
item = QTableWidgetItem("")
self.table.setItem(row_count, col, item)
self.task_status_map[row_count] = "idle"
self.sync_table_to_raw_data()
self.process_data()
self.display_data()
self.enable_action_buttons()
def batch_add_rows(self):
"""批量添加行"""
count, ok = QInputDialog.getInt(self, "批量添加", "请输入要添加的行数:", 1, 1, 1000, 1)
if ok and count > 0:
row_count = self.table.rowCount()
self.table.setRowCount(row_count + count)
for i in range(count):
for col in range(10):
if col == 0:
item = QTableWidgetItem("待执行")
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
item.setTextAlignment(Qt.AlignCenter)
elif col == 9:
item = QTableWidgetItem("")
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
else:
item = QTableWidgetItem("")
self.table.setItem(row_count + i, col, item)
self.task_status_map[row_count + i] = "idle"
self.sync_table_to_raw_data()
self.process_data()
self.display_data()
self.enable_action_buttons()
self.show_info("成功", f"已添加 {count}")
def delete_selected_rows(self):
"""删除选中的行"""
selected_rows = sorted({item.row() for item in self.table.selectedItems()}, reverse=True)
if not selected_rows:
QMessageBox.warning(self, "警告", "请先选择要删除的行")
return
reply = QMessageBox.question(
self, "确认",
f"确定要删除 {len(selected_rows)} 行吗?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
for row in selected_rows:
self.table.removeRow(row)
if row in self.task_status_map:
del self.task_status_map[row]
# 重建状态映射
new_map = {}
for i in range(self.table.rowCount()):
status_item = self.table.item(i, 0)
if status_item:
status_text = status_item.text()
if "成功" in status_text:
new_map[i] = "success"
elif "失败" in status_text:
new_map[i] = "fail"
elif "执行中" in status_text:
new_map[i] = "running"
else:
new_map[i] = "idle"
self.task_status_map = new_map
self.sync_table_to_raw_data()
self.process_data()
self.display_data()
self.enable_action_buttons()
def copy_selected_rows(self):
"""复制选中的行"""
selected_rows = sorted({item.row() for item in self.table.selectedItems()})
if not selected_rows:
QMessageBox.warning(self, "警告", "请先选择要复制的行")
return
rows_data = []
for row in selected_rows:
row_data = {}
for col in range(1, 10): # 跳过状态列
item = self.table.item(row, col)
header = self.table.horizontalHeaderItem(col).text()
row_data[header] = item.text() if item else ""
rows_data.append(row_data)
clipboard = QApplication.clipboard()
clipboard.setText(json.dumps(rows_data, ensure_ascii=False, indent=2))
self.show_info("成功", f"已复制 {len(rows_data)} 行到剪贴板")
def paste_rows(self):
"""粘贴行"""
clipboard = QApplication.clipboard()
text = clipboard.text()
try:
rows_data = json.loads(text)
if not isinstance(rows_data, list):
QMessageBox.warning(self, "错误", "剪贴板中没有有效的行数据")
return
column_mapping = {
"多多 id": 1, "序号": 2, "话题": 3,
"定时发布": 4, "间隔时间(分钟)": 5, "达人链接": 6,
"执行人": 7, "情况": 8, "计算后的发布时间": 9
}
current_row = self.table.rowCount()
self.table.setRowCount(current_row + len(rows_data))
for i, row_data in enumerate(rows_data):
if isinstance(row_data, dict):
# 状态列
status_item = QTableWidgetItem("待执行")
status_item.setFlags(status_item.flags() & ~Qt.ItemIsEditable)
status_item.setTextAlignment(Qt.AlignCenter)
self.table.setItem(current_row + i, 0, status_item)
self.task_status_map[current_row + i] = "idle"
for key, value in row_data.items():
col = column_mapping.get(key)
if col is not None:
item = QTableWidgetItem(str(value))
if col == 9:
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
self.table.setItem(current_row + i, col, item)
self.sync_table_to_raw_data()
self.process_data()
self.display_data()
self.enable_action_buttons()
self.show_info("成功", f"已粘贴 {len(rows_data)}")
except json.JSONDecodeError:
QMessageBox.warning(self, "错误", "剪贴板中的数据格式不正确")
except Exception as e:
QMessageBox.critical(self, "错误", f"粘贴时出错:{str(e)}")
def save_config(self):
"""保存配置到JSON文件"""
if not self.raw_data:
QMessageBox.warning(self, "警告", "没有可保存的数据")
return
file_path, _ = QFileDialog.getSaveFileName(
self, "保存配置", "配置.json", "JSON Files (*.json)"
)
if not file_path:
return
try:
self.sync_table_to_raw_data()
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(self.raw_data, f, ensure_ascii=False, indent=2)
self.log(f"配置已保存到: {file_path}", "SUCCESS")
self.show_info("成功", f"配置已保存到:\n{file_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"保存配置时出错:\n{str(e)}")
self.log(f"保存配置失败: {str(e)}", "ERROR")
def load_config(self):
"""从JSON文件加载配置"""
file_path, _ = QFileDialog.getOpenFileName(
self, "加载配置", "", "JSON Files (*.json)"
)
if not file_path:
return
try:
with open(file_path, 'r', encoding='utf-8') as f:
self.raw_data = json.load(f)
self.process_data()
self.display_data()
self.enable_action_buttons()
self.log(f"已加载 {len(self.raw_data)} 条配置", "SUCCESS")
self.show_info("成功", f"已加载 {len(self.raw_data)} 条配置")
except Exception as e:
QMessageBox.critical(self, "错误", f"加载配置时出错:\n{str(e)}")
self.log(f"加载配置失败: {str(e)}", "ERROR")
def enable_action_buttons(self):
"""根据数据状态启用/禁用按钮"""
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_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)
self.retry_failed_btn.setEnabled(not busy)
self.retry_current_btn.setEnabled(not busy)
self.start_index_spin.setEnabled(not busy)
self.fail_policy_combo.setEnabled(not busy)
def on_worker_progress(self, current, total, message, level, context):
"""后台任务进度更新"""
if total > 0:
percent = int((current / total) * 100)
self.progress_bar.setValue(percent)
# 计算预计剩余时间
if self.start_time and current > 0:
elapsed = (datetime.now() - self.start_time).total_seconds()
avg_time_per_task = elapsed / current
remaining_tasks = total - current
estimated_seconds = avg_time_per_task * remaining_tasks
hours = int(estimated_seconds // 3600)
minutes = int((estimated_seconds % 3600) // 60)
seconds = int(estimated_seconds % 60)
if hours > 0:
self.eta_label.setText(f"预计剩余时间: {hours:02d}:{minutes:02d}:{seconds:02d}")
else:
self.eta_label.setText(f"预计剩余时间: {minutes:02d}:{seconds:02d}")
self.log(message, level, context=context)
self.statusBar().showMessage(message)
def on_worker_finished(self, success_count, fail_count, error_messages):
"""后台任务完成"""
self.timer.stop()
self.progress_bar.setValue(100)
result_msg = f"任务执行完成!\n成功: {success_count}\n失败: {fail_count}"
if error_messages:
result_msg += f"\n\n错误详情:\n" + "\n".join(error_messages[:5])
if len(error_messages) > 5:
result_msg += f"\n... 还有 {len(error_messages) - 5} 个错误(请查看日志)"
self.set_status("success", f"执行完成 - 成功: {success_count}, 失败: {fail_count}")
self.set_busy(False)
self.worker = None
self.pause_btn.setText("暂停")
self.eta_label.setText("预计剩余时间: --")
self.last_error = ""
self.update_current_task_label(None)
self.log(f"任务执行完成 - 成功: {success_count}, 失败: {fail_count}", "SUCCESS")
QMessageBox.information(self, "执行结果", result_msg)
def on_worker_error(self, error_message):
"""后台任务异常"""
self.timer.stop()
self.progress_bar.setValue(0)
self.set_status("error", "执行失败")
self.last_error = error_message
self.set_busy(False)
self.worker = None
self.pause_btn.setText("暂停")
self.eta_label.setText("预计剩余时间: --")
self.update_current_task_label(None)
self.log(f"执行失败:{error_message}", "ERROR")
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("继续")
if USE_FLUENT and FluentIcon.PLAY:
self.pause_btn.setIcon(FluentIcon.PLAY)
self.set_status("paused", "已暂停")
self.log("任务已暂停", "WARNING")
else:
self.worker.resume()
self.pause_btn.setText("暂停")
if USE_FLUENT and FluentIcon.PAUSE:
self.pause_btn.setIcon(FluentIcon.PAUSE)
self.set_status("running", "运行中")
self.log("任务已继续", "INFO")
def stop_worker(self):
"""停止任务"""
if self.worker:
reply = QMessageBox.question(
self, "确认", "确定要停止当前任务吗?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
self.worker.stop()
self.set_status("warning", "正在停止...")
self.log("正在停止任务", "WARNING")
def retry_failed_tasks(self):
"""重试失败任务"""
if self.worker:
QMessageBox.warning(self, "提示", "请先停止当前任务")
return
# 找到失败的任务
failed_tasks = []
for row in range(self.table.rowCount()):
if self.task_status_map.get(row) == "fail":
failed_tasks.append(row)
if not failed_tasks:
QMessageBox.information(self, "提示", "没有失败的任务需要重试")
return
# 重试所有失败的任务
self.retry_selected_tasks_by_rows(failed_tasks)
def retry_current_task(self):
"""重试当前任务(以最近一次运行中的任务为准)"""
if self.worker:
QMessageBox.warning(self, "提示", "请先停止当前任务")
return
if self.current_task_index is None:
QMessageBox.information(self, "提示", "暂无可重试的当前任务")
return
self.retry_selected_tasks_by_rows([self.current_task_index])
def retry_selected_tasks(self):
"""重试选中的任务"""
if self.worker:
QMessageBox.warning(self, "提示", "请先停止当前任务")
return
selected_rows = sorted({item.row() for item in self.table.selectedItems()})
if not selected_rows:
QMessageBox.warning(self, "警告", "请先选择要重试的任务")
return
self.retry_selected_tasks_by_rows(selected_rows)
def retry_selected_tasks_by_rows(self, rows):
"""根据行号重试任务"""
if not rows:
return
self.sync_table_to_raw_data()
self.process_data()
tasks_to_retry = [dict(self.processed_data[row]) for row in rows if row < len(self.processed_data)]
if not tasks_to_retry:
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
reply = QMessageBox.question(
self, "确认",
f"确定要重试 {len(tasks_to_retry)} 个任务吗?",
QMessageBox.Yes | QMessageBox.No
)
if reply != QMessageBox.Yes:
return
# 重置这些任务的状态
for row in rows:
self.update_task_status(row, "idle")
self.update_queue(tasks_to_retry)
self.progress_bar.setValue(0)
self.set_status("running", "开始重试任务...")
self.set_busy(True)
self.start_time = datetime.now()
self.timer.start(1000) # 每秒更新一次时间
self.worker = TaskWorker(
tasks_to_retry,
base_folder_path=base_folder_path if base_folder_path else None,
batch_upload=False,
input_delay=self.input_delay_spin.value(),
start_index=min(rows)
)
self.worker.progress.connect(self.on_worker_progress)
self.worker.task_status.connect(self.update_task_status)
self.worker.finished.connect(self.on_worker_finished)
self.worker.error.connect(self.on_worker_error)
self.worker.start()
self.set_status("running", "运行中")
def update_elapsed_time(self):
"""更新已用时间"""
if self.start_time:
elapsed = datetime.now() - self.start_time
hours = elapsed.seconds // 3600
minutes = (elapsed.seconds % 3600) // 60
seconds = elapsed.seconds % 60
self.elapsed_time_label.setText(f"已用时间: {hours:02d}:{minutes:02d}:{seconds:02d}")
def clear_log(self):
"""清空日志"""
self.log_records = []
if hasattr(self, "log_list"):
self.log_list.clear()
self.log_output.clear()
if hasattr(self, "log_summary_label"):
self.log_summary_label.setText("最近错误: --")
if hasattr(self, "log_count_label"):
self.log_count_label.setText("错误: 0 | 警告: 0")
self.log("日志已清空", "INFO")
def log(self, message, level="INFO", context=None):
"""追加日志(带级别和颜色)"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] # 精确到毫秒
# 级别标签
level_labels = {
"INFO": "[INFO]",
"SUCCESS": "[SUCCESS]",
"WARNING": "[WARNING]",
"ERROR": "[ERROR]",
"DEBUG": "[DEBUG]"
}
level_label = level_labels.get(level, "[INFO]")
# 格式化日志
log_text = f"[{timestamp}] {level_label} {message}"
record = {
"timestamp": timestamp,
"level": level,
"message": message,
"session": self.session_id,
"context": context or {},
}
self.log_records.append(record)
self.render_log_output()
if level == "ERROR":
self.log_summary_label.setText(f"最近错误: {message}")
try:
with open(self.log_file_path, "a", encoding="utf-8") as f:
f.write(log_text + "\n")
with open(self.audit_log_path, "a", encoding="utf-8") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
except Exception:
pass
def export_log(self):
"""导出日志"""
file_path, _ = QFileDialog.getSaveFileName(
self, "保存日志文件", f"日志_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt", "Text Files (*.txt)"
)
if not file_path:
return
try:
records = self.get_filtered_log_records()
with open(file_path, 'w', encoding='utf-8') as f:
for record in records:
timestamp = record.get("timestamp", "")
level = record.get("level", "INFO")
message = record.get("message", "")
f.write(f"[{timestamp}] [{level}] {message}\n")
self.log(f"日志已导出到: {file_path}", "SUCCESS")
self.show_info("成功", f"日志已导出到:\n{file_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"导出日志时出错:\n{str(e)}")
self.log(f"导出日志失败: {str(e)}", "ERROR")
def get_filtered_log_records(self):
"""获取筛选后的日志记录"""
level_filter = self.log_level_combo.currentText() if hasattr(self, "log_level_combo") else "全部"
keyword = self.log_search_input.text().strip() if hasattr(self, "log_search_input") else ""
keyword = keyword.lower()
filtered = []
for record in self.log_records:
level = record.get("level", "INFO")
message = record.get("message", "")
context = record.get("context") or {}
if level_filter != "全部" and level != level_filter:
continue
if keyword:
context_text = " ".join([str(v) for v in context.values()]).lower()
if keyword not in message.lower() and keyword not in context_text:
continue
filtered.append(record)
return filtered
def render_log_output(self):
"""根据过滤条件渲染日志"""
if not hasattr(self, "log_output") or not hasattr(self, "log_list"):
return
records = self.get_filtered_log_records()
self.log_list.blockSignals(True)
self.log_list.clear()
error_count = 0
warning_count = 0
for record in records:
level = record.get("level", "INFO")
message = record.get("message", "")
timestamp = record.get("timestamp", "")
context = record.get("context") or {}
task_index = context.get("task_index")
user_id = context.get("user_id")
extra = []
if task_index is not None:
extra.append(f"任务{task_index + 1}")
if user_id:
extra.append(f"多多:{user_id}")
extra_text = " | " + " ".join(extra) if extra else ""
display_text = f"{timestamp} [{level}] {message}{extra_text}"
if len(display_text) > 90:
display_text = display_text[:90] + "..."
item = QListWidgetItem(display_text)
item.setData(Qt.UserRole, record)
if level == "SUCCESS":
item.setForeground(QColor(53, 194, 119))
elif level == "WARNING":
item.setForeground(QColor(243, 156, 18))
warning_count += 1
elif level == "ERROR":
item.setForeground(QColor(231, 76, 60))
error_count += 1
elif level == "DEBUG":
item.setForeground(QColor(149, 165, 166))
self.log_list.addItem(item)
self.log_list.blockSignals(False)
if records:
if self.log_list.currentRow() == -1:
self.log_list.setCurrentRow(len(records) - 1)
else:
self.log_output.clear()
if hasattr(self, "log_count_label"):
self.log_count_label.setText(f"错误: {error_count} | 警告: {warning_count}")
def on_log_item_changed(self, current, previous):
"""日志详情展示"""
if not current:
self.log_output.clear()
return
record = current.data(Qt.UserRole) or {}
timestamp = record.get("timestamp", "")
level = record.get("level", "INFO")
message = record.get("message", "")
session = record.get("session", self.session_id)
context = record.get("context") or {}
task_index = context.get("task_index")
user_id = context.get("user_id")
source = context.get("source", "--")
detail = (
f"时间: {timestamp}\n"
f"级别: {level}\n"
f"会话: {session}\n"
f"任务: {task_index + 1 if task_index is not None else '--'}\n"
f"多多ID: {user_id or '--'}\n"
f"来源: {source}\n"
f"内容: {message}\n"
)
self.log_output.setPlainText(detail)
def open_log_folder(self):
"""打开日志目录"""
try:
folder = str(self.log_file_path.parent)
if sys.platform.startswith("darwin"):
os.system(f'open "{folder}"')
elif os.name == "nt":
os.startfile(folder)
else:
os.system(f'xdg-open "{folder}"')
except Exception as e:
self.show_error("打开失败", str(e))
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: 8px 12px; border-radius: 8px; "
f"background: qlineargradient(x1:0, y1:0, x2:1, y2:0, {gradient}); "
"color: #e5eaf3; font-weight: bold;"
)
self.status_label.setText(message)
indicator_colors = {
"running": "#4c7dff",
"success": "#35c277",
"error": "#e74c3c",
"warning": "#f39c12",
"paused": "#8b99aa",
"idle": "#67C23A",
}
color = indicator_colors.get(level, "#67C23A")
self.status_indicator.setStyleSheet(
f"color: {color}; font-weight: bold; font-size: 14px;"
)
state_map = {
"running": RUN_STATE_RUNNING,
"success": RUN_STATE_DONE,
"error": RUN_STATE_ERROR,
"warning": RUN_STATE_STOPPING,
"paused": RUN_STATE_PAUSED,
"idle": RUN_STATE_IDLE,
}
self.run_state = state_map.get(level, RUN_STATE_IDLE)
self.update_status_overview()
# 运行状态时添加动画效果
if level == "running":
self.animate_status_indicator()
elif hasattr(self, "_status_anim"):
self._status_anim.stop()
del self._status_anim
def animate_status_indicator(self):
"""状态指示器动画"""
if hasattr(self, '_status_anim'):
return
if not isinstance(self.status_indicator.graphicsEffect(), QGraphicsOpacityEffect):
effect = QGraphicsOpacityEffect(self.status_indicator)
self.status_indicator.setGraphicsEffect(effect)
else:
effect = self.status_indicator.graphicsEffect()
self._status_anim = QPropertyAnimation(effect, b"opacity", self)
self._status_anim.setDuration(1000)
self._status_anim.setStartValue(1.0)
self._status_anim.setEndValue(0.3)
self._status_anim.setLoopCount(-1)
self._status_anim.start()
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 USE_FLUENT:
if checked:
setTheme(Theme.DARK)
self.log("已切换暗色主题", "INFO")
else:
setTheme(Theme.LIGHT)
self.log("已切换亮色主题", "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类"""
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)
QMessageBox.information(
self, "提示",
f"已自动使用桌面上的默认文件夹:\n{default_folder}"
)
else:
reply = QMessageBox.question(
self, "提示",
"未设置大文件夹路径,且未找到桌面上的'多多发文文件'文件夹。\n将使用空路径执行任务。\n是否继续?",
QMessageBox.Yes | QMessageBox.No
)
if reply != QMessageBox.Yes:
return
if base_folder_path and not os.path.exists(base_folder_path):
QMessageBox.warning(
self, "警告",
f"大文件夹路径不存在:\n{base_folder_path}\n\n请检查路径是否正确。"
)
return
reply = QMessageBox.question(
self, "确认",
f"确定要执行 {len(self.processed_data)} 个任务吗?\n大文件夹路径:{base_folder_path if base_folder_path else '未设置'}",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
if self.worker is not None:
QMessageBox.warning(self, "提示", "任务正在执行中,请稍后")
return
start_index = self.start_index_spin.value()
if start_index > len(self.processed_data):
QMessageBox.warning(self, "警告", "起始行超出数据范围")
return
tasks = [dict(item) for item in self.processed_data[start_index - 1:]]
self.update_queue(tasks)
self.progress_bar.setValue(0)
self.set_status("running", "开始执行任务...")
self.set_busy(True)
self.start_time = datetime.now()
self.timer.start(1000)
self.current_task_index = None
self.update_current_task_label(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(),
start_index=start_index - 1
)
self.worker.progress.connect(self.on_worker_progress)
self.worker.task_status.connect(self.update_task_status)
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:
start_index = self.start_index_spin.value()
if start_index > len(self.processed_data):
QMessageBox.warning(self, "警告", "起始行超出数据范围")
return
tasks = [dict(item) for item in self.processed_data[start_index - 1:]]
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.start_time = datetime.now()
self.timer.start(1000)
self.current_task_index = None
self.update_current_task_label(None)
start_index = self.start_index_spin.value() if not selected_rows else 1
self.worker = TaskWorker(
tasks,
base_folder_path=base_folder_path,
batch_upload=True,
input_delay=self.input_delay_spin.value(),
start_index=start_index - 1 if not selected_rows else 0
)
self.worker.progress.connect(self.on_worker_progress)
self.worker.task_status.connect(self.update_task_status)
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模板"""
file_path, _ = QFileDialog.getSaveFileName(
self, "保存Excel模板", "配置模板.xlsx", "Excel Files (*.xlsx)"
)
if not file_path:
return
try:
template_data = {
'多多 id': ['示例1050100241'],
'序号': ['示例1'],
'话题(以中文"-"分隔)': ['示例python-自动化-技术'],
'定时发布': ['示例2026-01-28 09:30:00'],
'间隔时间': ['示例30单位分钟'],
'达人链接': ['示例https://www.xiaohongshu.com/explore/xxx'],
'执行人': ['示例:张三'],
'情况': ['备注信息']
}
df = pd.DataFrame(template_data)
df.to_excel(file_path, index=False, engine='openpyxl')
self.log(f"Excel模板已导出到: {file_path}", "SUCCESS")
self.show_info("成功", f"Excel模板已导出到\n{file_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"导出Excel模板时出错\n{str(e)}")
self.log(f"导出模板失败: {str(e)}", "ERROR")
import traceback
traceback.print_exc()
def export_excel(self):
"""导出处理后的数据到Excel"""
if not self.processed_data:
QMessageBox.warning(self, "警告", "没有可导出的数据")
return
file_path, _ = QFileDialog.getSaveFileName(
self, "保存Excel文件", "", "Excel Files (*.xlsx)"
)
if not file_path:
return
try:
export_data = []
for data in self.processed_data:
export_data.append({
'多多 id': data.get('多多 id', ''),
'序号': data.get('序号', ''),
'话题(以中文"-"分隔)': data.get('话题', ''),
'定时发布': data.get('定时发布', ''),
'间隔时间': data.get('间隔时间', ''),
'达人链接': data.get('达人链接', ''),
'执行人': data.get('执行人', ''),
'情况': data.get('情况', ''),
'计算后的发布时间': data.get('计算后的发布时间', ''),
})
df = pd.DataFrame(export_data)
df.to_excel(file_path, index=False, engine='openpyxl')
self.log(f"数据已导出到: {file_path}", "SUCCESS")
self.show_info("成功", f"数据已导出到:\n{file_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"导出Excel文件时出错\n{str(e)}")
self.log(f"导出Excel失败: {str(e)}", "ERROR")
import traceback
traceback.print_exc()
def main():
app = QApplication(sys.argv)
if USE_FLUENT:
setTheme(Theme.DARK)
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()