2513 lines
99 KiB
Python
2513 lines
99 KiB
Python
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()
|