Files
haha/gui_app.py

1500 lines
59 KiB
Python
Raw Normal View History

2026-01-17 20:38:27 +08:00
import sys
import os
import re
import json
2026-01-20 04:09:09 +08:00
from pathlib import Path
2026-01-17 20:38:27 +08:00
from datetime import datetime, timedelta
2026-01-20 04:09:09 +08:00
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout,
QHBoxLayout, QTableWidget, QTableWidgetItem,
2026-01-17 20:38:27 +08:00
QFileDialog, QMessageBox, QHeaderView, QLabel, QMenu,
QInputDialog, QDialog, QDialogButtonBox, QFormLayout,
2026-01-20 04:09:09 +08:00
QTextEdit, QAbstractItemView, QStackedWidget,
QGraphicsOpacityEffect, QListWidget)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QPropertyAnimation, QEvent
from PyQt5.QtGui import QKeySequence, QFont
from qfluentwidgets import (
setTheme,
Theme,
PushButton,
PrimaryPushButton,
LineEdit,
SpinBox,
ProgressBar,
CardWidget,
InfoBar,
InfoBarPosition,
NavigationInterface,
NavigationItemPosition,
FluentIcon,
ToggleSwitch,
PillPushButton
)
try:
from qfluentwidgets import FramelessWindow
except ImportError: # 兼容旧版本
from qfluentwidgets import FluentWindow as FramelessWindow
2026-01-17 20:38:27 +08:00
import pandas as pd
2026-01-18 06:11:21 +08:00
class TaskWorker(QThread):
progress = pyqtSignal(int, int, str)
finished = pyqtSignal(int, int, list)
error = pyqtSignal(str)
2026-01-20 04:09:09 +08:00
def __init__(self, tasks, base_folder_path=None, batch_upload=False, input_delay=0):
2026-01-18 06:11:21 +08:00
super().__init__()
self.tasks = tasks
2026-01-19 17:24:30 +08:00
self.base_folder_path = base_folder_path
2026-01-20 04:09:09 +08:00
self.batch_upload = batch_upload
self.input_delay = input_delay
self._pause = False
self._stop = False
def pause(self):
self._pause = True
def resume(self):
self._pause = False
def stop(self):
self._stop = True
def _wait_if_paused(self):
while self._pause and not self._stop:
self.msleep(200)
2026-01-18 06:11:21 +08:00
def run(self):
try:
from main import Pdd
from loguru import logger
success_count = 0
fail_count = 0
error_messages = []
total = len(self.tasks)
2026-01-19 17:24:30 +08:00
# 获取大文件夹路径(从主窗口传递)
base_folder_path = getattr(self, 'base_folder_path', None)
2026-01-18 06:11:21 +08:00
for idx, data in enumerate(self.tasks, 1):
2026-01-20 04:09:09 +08:00
if self._stop:
error_messages.append("任务已停止")
break
self._wait_if_paused()
2026-01-19 17:24:30 +08:00
user_id = data.get('多多 id', '')
2026-01-18 06:11:21 +08:00
topics = data.get('话题', '')
time_start = data.get('计算后的发布时间', '')
url = data.get('达人链接', '')
2026-01-19 17:24:30 +08:00
index = data.get('序号', '')
2026-01-18 06:11:21 +08:00
2026-01-19 17:24:30 +08:00
self.progress.emit(idx, total, f"正在执行任务 {idx}/{total} - 多多: {user_id}")
2026-01-18 06:11:21 +08:00
if not url or not user_id:
2026-01-19 17:24:30 +08:00
error_msg = f"任务 {idx}: 缺少必需参数 - 多多={user_id}, 达人链接={url}"
2026-01-18 06:11:21 +08:00
logger.warning(error_msg)
error_messages.append(error_msg)
fail_count += 1
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(
2026-01-19 17:24:30 +08:00
f"开始执行任务 {idx} - 多多: {user_id}, 达人链接: {url}, 大文件夹路径: {base_folder_path}, 序号: {index}"
2026-01-18 06:11:21 +08:00
)
pdd = Pdd(
url=url,
user_id=user_id,
time_start=time_start if time_start else None,
ht=ht,
index=index,
)
2026-01-19 17:24:30 +08:00
# 使用输入框中的大文件夹路径
folder_path = base_folder_path if base_folder_path and os.path.exists(base_folder_path) else None
2026-01-18 06:11:21 +08:00
2026-01-20 04:09:09 +08:00
if self.batch_upload:
logger.info("批量上传模式收集当前多多ID的视频文件")
video_items = self.collect_user_videos(folder_path, user_id, url, time_start, ht)
if not video_items:
error_msg = f"任务 {idx}: 未找到多多ID={user_id} 的视频文件"
logger.warning(error_msg)
error_messages.append(error_msg)
fail_count += 1
continue
logger.info(f"调用 pdd.action1(folder_path=video_items, input_delay={self.input_delay})")
pdd.action1(folder_path=video_items, input_delay=self.input_delay)
else:
logger.info(f"调用 pdd.action(folder_path={folder_path})")
pdd.action(folder_path=folder_path)
2026-01-18 06:11:21 +08:00
logger.info(f"任务 {idx} 执行成功")
success_count += 1
except Exception as e:
2026-01-19 17:24:30 +08:00
error_msg = f"任务 {idx} 执行失败 - 多多: {user_id}, 错误: {str(e)}"
2026-01-18 06:11:21 +08:00
logger.error(error_msg)
logger.exception("详细错误信息:")
error_messages.append(error_msg)
fail_count += 1
continue
self.finished.emit(success_count, fail_count, error_messages)
except Exception as e:
self.error.emit(str(e))
2026-01-20 04:09:09 +08:00
@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):
# 允许在大文件夹下按子目录遍历匹配用户ID
for name in os.listdir(base_folder_path):
candidate = os.path.join(base_folder_path, name)
if os.path.isdir(candidate) and str(user_id) in name:
target_folder = candidate
break
2026-01-18 06:11:21 +08:00
2026-01-20 04:09:09 +08:00
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):
2026-01-17 20:38:27 +08:00
def __init__(self):
super().__init__()
self.setWindowTitle("拼多多MCN发布管理工具")
self.setGeometry(100, 100, 1200, 800)
# 存储原始数据和处理后的数据
self.raw_data = []
self.processed_data = []
2026-01-18 06:11:21 +08:00
self.worker = None
2026-01-17 20:38:27 +08:00
# 创建主窗口部件
main_widget = QWidget()
self.setCentralWidget(main_widget)
2026-01-20 04:09:09 +08:00
# 根布局:侧边导航 + 内容区
root_layout = QHBoxLayout()
root_layout.setContentsMargins(10, 10, 10, 10)
root_layout.setSpacing(10)
main_widget.setLayout(root_layout)
setTheme(Theme.DARK)
# 侧边导航
self.nav = NavigationInterface(self)
self.nav.setMinimumWidth(200)
root_layout.addWidget(self.nav)
# 内容堆栈
self.stack = QStackedWidget()
root_layout.addWidget(self.stack, 1)
# 页面容器
self.main_page = QWidget()
self.log_page = QWidget()
self.stack.addWidget(self.main_page)
self.stack.addWidget(self.log_page)
# 主页面布局
2026-01-17 20:38:27 +08:00
layout = QVBoxLayout()
2026-01-20 04:09:09 +08:00
layout.setSpacing(10)
self.main_page.setLayout(layout)
# 日志页面布局
log_page_layout = QVBoxLayout()
log_page_layout.setSpacing(10)
self.log_page.setLayout(log_page_layout)
# 顶部标题栏(自定义按钮)
title_card = CardWidget()
title_layout = QHBoxLayout(title_card)
title_layout.setContentsMargins(12, 6, 12, 6)
title_card.setObjectName("titleCard")
nav_toggle = PushButton()
nav_toggle.setIcon(FluentIcon.MENU)
nav_toggle.setFixedWidth(32)
nav_toggle.clicked.connect(self.toggle_nav)
title_layout.addWidget(nav_toggle)
title_label = QLabel("发布配置工具 - 批量版")
title_font = QFont("Arial", 14, QFont.Bold)
title_label.setFont(title_font)
title_layout.addWidget(title_label)
pro_badge = QLabel("PRO")
pro_badge.setStyleSheet(
"padding: 2px 6px; border-radius: 6px; "
"background: qlineargradient(x1:0, y1:0, x2:1, y2:0, "
"stop:0 #7f5cff, stop:1 #b37bff); color: #ffffff; font-weight: bold;"
)
title_layout.addWidget(pro_badge)
title_layout.addStretch()
self.status_indicator = QLabel("")
self.status_indicator.setStyleSheet("color: #67C23A; font-weight: bold;")
title_layout.addWidget(self.status_indicator)
self.theme_toggle = ToggleSwitch()
self.theme_toggle.setChecked(True)
self.theme_toggle.checkedChanged.connect(self.toggle_theme)
title_layout.addWidget(QLabel("暗色"))
title_layout.addWidget(self.theme_toggle)
min_btn = PushButton()
min_btn.setIcon(FluentIcon.REMOVE)
min_btn.setFixedWidth(32)
min_btn.clicked.connect(self.showMinimized)
title_layout.addWidget(min_btn)
max_btn = PushButton()
max_btn.setIcon(FluentIcon.COPY)
max_btn.setFixedWidth(32)
max_btn.clicked.connect(self.toggle_maximize)
title_layout.addWidget(max_btn)
close_btn = PushButton()
close_btn.setIcon(FluentIcon.CLOSE)
close_btn.setFixedWidth(32)
close_btn.clicked.connect(self.close)
title_layout.addWidget(close_btn)
layout.addWidget(title_card)
self.fade_in(title_card)
# 允许拖拽标题栏移动窗口
title_card.installEventFilter(self)
# 基础设置卡片
settings_card = CardWidget()
settings_layout = QVBoxLayout(settings_card)
2026-01-19 17:24:30 +08:00
folder_layout = QHBoxLayout()
folder_label = QLabel("大文件夹路径:")
2026-01-20 04:09:09 +08:00
self.folder_path_input = LineEdit()
2026-01-19 17:24:30 +08:00
self.folder_path_input.setPlaceholderText("请输入大文件夹路径例如C:\\Users\\user\\data")
2026-01-20 04:09:09 +08:00
folder_browse_btn = PushButton("浏览...")
2026-01-19 17:24:30 +08:00
folder_browse_btn.clicked.connect(self.browse_folder)
folder_layout.addWidget(folder_label)
folder_layout.addWidget(self.folder_path_input)
folder_layout.addWidget(folder_browse_btn)
2026-01-20 04:09:09 +08:00
settings_layout.addLayout(folder_layout)
delay_layout = QHBoxLayout()
delay_label = QLabel("填写信息间隔(秒):")
self.input_delay_spin = SpinBox()
self.input_delay_spin.setRange(0, 3600)
self.input_delay_spin.setValue(0)
self.input_delay_spin.setToolTip("每个视频处理前等待时间,便于手动填写信息")
delay_layout.addWidget(delay_label)
delay_layout.addWidget(self.input_delay_spin)
delay_layout.addStretch()
settings_layout.addLayout(delay_layout)
layout.addWidget(settings_card)
self.fade_in(settings_card)
2026-01-19 17:24:30 +08:00
# 自动查找桌面上的"多多发文文件"文件夹
default_folder = self.find_default_folder()
if default_folder:
self.folder_path_input.setText(default_folder)
2026-01-20 04:09:09 +08:00
# 操作按钮卡片
action_card = CardWidget()
action_layout = QHBoxLayout(action_card)
self.import_btn = PushButton("导入Excel")
self.import_btn.setIcon(FluentIcon.DOCUMENT)
self.import_btn.setShortcut(QKeySequence("Ctrl+O"))
self.import_btn.clicked.connect(self.import_excel)
action_layout.addWidget(self.import_btn)
self.add_row_btn = PushButton("添加行")
self.add_row_btn.setIcon(FluentIcon.ADD)
self.add_row_btn.setShortcut(QKeySequence("Ctrl+N"))
self.add_row_btn.clicked.connect(self.add_row)
action_layout.addWidget(self.add_row_btn)
self.batch_add_btn = PushButton("批量添加")
self.batch_add_btn.setIcon(FluentIcon.MULTI_SELECT)
self.batch_add_btn.clicked.connect(self.batch_add_rows)
action_layout.addWidget(self.batch_add_btn)
self.delete_row_btn = PushButton("删除选中行")
self.delete_row_btn.setIcon(FluentIcon.DELETE)
self.delete_row_btn.setShortcut(QKeySequence("Delete"))
self.delete_row_btn.clicked.connect(self.delete_selected_rows)
action_layout.addWidget(self.delete_row_btn)
self.save_btn = PushButton("保存配置")
self.save_btn.setIcon(FluentIcon.SAVE)
self.save_btn.setShortcut(QKeySequence("Ctrl+S"))
self.save_btn.clicked.connect(self.save_config)
action_layout.addWidget(self.save_btn)
self.load_btn = PushButton("加载配置")
self.load_btn.setIcon(FluentIcon.FOLDER)
self.load_btn.setShortcut(QKeySequence("Ctrl+L"))
self.load_btn.clicked.connect(self.load_config)
action_layout.addWidget(self.load_btn)
self.export_btn = PushButton("导出Excel")
self.export_btn.setIcon(FluentIcon.SHARE)
self.export_btn.clicked.connect(self.export_excel)
self.export_btn.setEnabled(False)
action_layout.addWidget(self.export_btn)
self.export_template_btn = PushButton("导出模板")
self.export_template_btn.setIcon(FluentIcon.PAGE_RIGHT)
self.export_template_btn.clicked.connect(self.export_template)
action_layout.addWidget(self.export_template_btn)
action_layout.addStretch()
self.execute_btn = PrimaryPushButton("执行任务")
self.execute_btn.setIcon(FluentIcon.PLAY)
self.execute_btn.setShortcut(QKeySequence("F5"))
self.execute_btn.clicked.connect(self.execute_tasks)
self.execute_btn.setEnabled(False)
action_layout.addWidget(self.execute_btn)
self.one_click_upload_btn = PrimaryPushButton("一键上传")
self.one_click_upload_btn.setIcon(FluentIcon.UP)
self.one_click_upload_btn.clicked.connect(self.one_click_upload)
self.one_click_upload_btn.setEnabled(False)
action_layout.addWidget(self.one_click_upload_btn)
layout.addWidget(action_card)
self.fade_in(action_card)
# 任务队列卡片
queue_card = CardWidget()
queue_layout = QVBoxLayout(queue_card)
queue_title = QLabel("任务队列")
queue_title.setStyleSheet("font-weight: bold;")
queue_layout.addWidget(queue_title)
self.queue_list = QListWidget()
self.queue_list.setFixedHeight(120)
queue_layout.addWidget(self.queue_list)
layout.addWidget(queue_card)
self.fade_in(queue_card)
# 运行控制卡片
run_card = CardWidget()
run_layout = QHBoxLayout(run_card)
run_layout.addWidget(QLabel("运行控制:"))
self.pause_btn = PillPushButton("暂停")
self.pause_btn.setIcon(FluentIcon.PAUSE)
self.pause_btn.setEnabled(False)
self.pause_btn.clicked.connect(self.pause_or_resume)
run_layout.addWidget(self.pause_btn)
self.stop_btn = PillPushButton("停止")
self.stop_btn.setIcon(FluentIcon.STOP)
self.stop_btn.setEnabled(False)
self.stop_btn.clicked.connect(self.stop_worker)
run_layout.addWidget(self.stop_btn)
run_layout.addStretch()
layout.addWidget(run_card)
self.fade_in(run_card)
# 配置列表卡片
table_group = CardWidget()
table_layout = QVBoxLayout(table_group)
table_title = QLabel("配置列表")
table_title.setStyleSheet("font-weight: bold;")
table_layout.addWidget(table_title)
stats_layout = QHBoxLayout()
self.stats_label = QLabel("总计: 0 条配置")
self.stats_label.setStyleSheet("QLabel { color: #666; font-weight: bold; }")
stats_layout.addWidget(self.stats_label)
stats_layout.addStretch()
table_layout.addLayout(stats_layout)
2026-01-17 20:38:27 +08:00
# 创建表格
self.table = QTableWidget()
self.table.setColumnCount(9)
self.table.setHorizontalHeaderLabels([
2026-01-20 04:09:09 +08:00
"多多 id", "序号", "话题", "定时发布", "间隔时间(分钟)",
2026-01-19 17:24:30 +08:00
"达人链接", "执行人", "情况", "计算后的发布时间"
2026-01-17 20:38:27 +08:00
])
2026-01-20 04:09:09 +08:00
2026-01-17 20:38:27 +08:00
# 设置表格列宽自适应
header = self.table.horizontalHeader()
header.setSectionResizeMode(QHeaderView.Stretch)
2026-01-20 04:09:09 +08:00
2026-01-17 20:38:27 +08:00
# 允许编辑
self.table.setEditTriggers(QTableWidget.DoubleClicked | QTableWidget.SelectedClicked | QTableWidget.EditKeyPressed)
2026-01-20 04:09:09 +08:00
2026-01-17 20:38:27 +08:00
# 允许选择多行
self.table.setSelectionBehavior(QTableWidget.SelectRows)
self.table.setSelectionMode(QTableWidget.ExtendedSelection)
2026-01-20 04:09:09 +08:00
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.table.setSelectionMode(QAbstractItemView.ExtendedSelection)
2026-01-18 06:11:21 +08:00
# UI优化
self.table.setAlternatingRowColors(True)
self.table.setWordWrap(False)
2026-01-20 04:09:09 +08:00
self.table.setStyleSheet("""
QTableWidget::item:hover { background-color: rgba(64, 158, 255, 60); }
""")
2026-01-17 20:38:27 +08:00
# 连接单元格变化信号,实时更新处理后的数据
self.table.cellChanged.connect(self.on_cell_changed)
2026-01-20 04:09:09 +08:00
2026-01-17 20:38:27 +08:00
# 设置右键菜单
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
self.table.customContextMenuRequested.connect(self.show_context_menu)
2026-01-20 04:09:09 +08:00
table_layout.addWidget(self.table)
layout.addWidget(table_group)
self.fade_in(table_group)
2026-01-17 20:38:27 +08:00
2026-01-18 06:11:21 +08:00
# 进度条
2026-01-20 04:09:09 +08:00
self.progress_bar = ProgressBar()
2026-01-18 06:11:21 +08:00
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
2026-01-20 04:09:09 +08:00
self.progress_bar.setFormat("%p% (%v/%m)")
2026-01-18 06:11:21 +08:00
layout.addWidget(self.progress_bar)
2026-01-20 04:09:09 +08:00
# 执行日志卡片
log_group = CardWidget()
log_layout = QVBoxLayout(log_group)
log_title = QLabel("执行日志")
log_title.setStyleSheet("font-weight: bold;")
log_layout.addWidget(log_title)
log_toolbar = QHBoxLayout()
clear_log_btn = QPushButton("清空日志")
clear_log_btn.clicked.connect(self.clear_log)
clear_log_btn.setStyleSheet("QPushButton { padding: 4px; }")
log_toolbar.addWidget(clear_log_btn)
log_toolbar.addStretch()
log_layout.addLayout(log_toolbar)
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
self.log_output.setMaximumHeight(200)
log_layout.addWidget(self.log_output)
# 日志页只显示日志卡片
log_page_layout.addWidget(log_group)
self.fade_in(log_group)
self.fade_in(log_group)
2026-01-17 20:38:27 +08:00
# 状态标签
self.status_label = QLabel("就绪")
2026-01-20 04:09:09 +08:00
self.status_label.setStyleSheet(
"padding: 6px 10px; border-radius: 6px; "
"background: qlineargradient(x1:0, y1:0, x2:1, y2:0, "
"stop:0 #2b2f3a, stop:1 #3a3f4b); color: #e5eaf3;"
)
2026-01-17 20:38:27 +08:00
layout.addWidget(self.status_label)
2026-01-20 04:09:09 +08:00
self.set_status("idle", "就绪")
# 状态栏
self.statusBar().showMessage("就绪")
# 侧边导航
self.nav.addItem(
routeKey="config",
icon=FluentIcon.DOCUMENT,
text="配置",
onClick=lambda: self.switch_page(self.main_page),
position=NavigationItemPosition.TOP
)
self.nav.addItem(
routeKey="logs",
icon=FluentIcon.BULLET_LIST,
text="日志",
onClick=lambda: self.switch_page(self.log_page),
position=NavigationItemPosition.TOP
)
2026-01-17 20:38:27 +08:00
2026-01-19 17:24:30 +08:00
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 browse_folder(self):
"""浏览选择文件夹"""
folder_path = QFileDialog.getExistingDirectory(self, "选择大文件夹路径")
if folder_path:
self.folder_path_input.setText(folder_path)
2026-01-20 04:09:09 +08:00
2026-01-19 17:24:30 +08:00
2026-01-17 20:38:27 +08:00
def import_excel(self):
"""导入Excel文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择Excel文件", "", "Excel Files (*.xlsx *.xls)"
)
if not file_path:
return
try:
# 读取Excel文件
df = pd.read_excel(file_path)
# 检查必需的列
2026-01-19 17:24:30 +08:00
required_columns = ['多多 id', '话题(以中文"-"分隔)',
2026-01-17 20:38:27 +08:00
'定时发布', '间隔时间', '达人链接', '情况']
2026-01-19 17:24:30 +08:00
optional_columns = ['序号', '执行人'] # 可选字段
2026-01-17 20:38:27 +08:00
# 检查列名是否存在(允许部分匹配)
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():
2026-01-19 17:24:30 +08:00
# 获取序号字段(如果存在)
2026-01-17 20:38:27 +08:00
index_value = ''
2026-01-19 17:24:30 +08:00
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 ''
2026-01-17 20:38:27 +08:00
data = {
2026-01-19 17:24:30 +08:00
'多多 id': str(row[column_mapping['多多 id']]) if pd.notna(row[column_mapping['多多 id']]) else '',
'序号': index_value,
2026-01-17 20:38:27 +08:00
'话题': 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 '',
2026-01-19 17:24:30 +08:00
'执行人': executor_value,
2026-01-17 20:38:27 +08:00
'情况': str(row[column_mapping['情况']]) if pd.notna(row[column_mapping['情况']]) else '',
}
self.raw_data.append(data)
# 处理数据:计算间隔时间
self.process_data()
# 显示数据
self.display_data()
self.status_label.setText(f"成功导入 {len(self.processed_data)} 条数据")
2026-01-20 04:09:09 +08:00
self.show_info("导入成功", f"已导入 {len(self.processed_data)} 条数据")
2026-01-17 20:38:27 +08:00
self.enable_action_buttons()
self.enable_action_buttons()
except Exception as e:
QMessageBox.critical(self, "错误", f"导入Excel文件时出错\n{str(e)}")
2026-01-20 04:09:09 +08:00
self.show_error("导入失败", str(e))
2026-01-17 20:38:27 +08:00
import traceback
traceback.print_exc()
def process_data(self):
2026-01-19 17:24:30 +08:00
"""处理数据,计算相同多多 id 的间隔时间"""
2026-01-17 20:38:27 +08:00
self.processed_data = []
2026-01-19 17:24:30 +08:00
# 按多多 id 分组
2026-01-17 20:38:27 +08:00
user_groups = {}
for data in self.raw_data:
2026-01-19 17:24:30 +08:00
user_id = data['多多 id']
2026-01-17 20:38:27 +08:00
if user_id not in user_groups:
user_groups[user_id] = []
user_groups[user_id].append(data)
2026-01-19 17:24:30 +08:00
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
2026-01-17 20:38:27 +08:00
# 处理每个用户组
for user_id, user_data_list in user_groups.items():
2026-01-19 17:24:30 +08:00
# 处理每条数据,按顺序计算
2026-01-17 20:38:27 +08:00
for index, data in enumerate(user_data_list):
processed_item = data.copy()
2026-01-19 17:24:30 +08:00
current_time_str = data.get('定时发布', '').strip()
current_time = parse_time(current_time_str)
2026-01-17 20:38:27 +08:00
2026-01-19 17:24:30 +08:00
if current_time:
# 如果当前数据有定时发布时间,直接使用
processed_item['计算后的发布时间'] = current_time_str
2026-01-17 20:38:27 +08:00
else:
2026-01-19 17:24:30 +08:00
# 如果当前数据没有定时发布时间,尝试根据前一条数据计算
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()
2026-01-17 20:38:27 +08:00
2026-01-19 17:24:30 +08:00
interval_minutes = parse_interval(interval_str)
2026-01-17 20:38:27 +08:00
2026-01-19 17:24:30 +08:00
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:
# 前一条数据也没有计算后的发布时间,无法计算
2026-01-17 20:38:27 +08:00
processed_item['计算后的发布时间'] = ''
else:
2026-01-19 17:24:30 +08:00
# 第一条数据没有定时发布时间,无法计算
2026-01-17 20:38:27 +08:00
processed_item['计算后的发布时间'] = ''
self.processed_data.append(processed_item)
def display_data(self):
"""在表格中显示数据"""
2026-01-18 06:11:21 +08:00
self.table.blockSignals(True)
2026-01-17 20:38:27 +08:00
self.table.setRowCount(len(self.processed_data))
for row, data in enumerate(self.processed_data):
2026-01-19 17:24:30 +08:00
# 多多 id
self.table.setItem(row, 0, QTableWidgetItem(str(data.get('多多 id', ''))))
2026-01-17 20:38:27 +08:00
2026-01-19 17:24:30 +08:00
# 序号
self.table.setItem(row, 1, QTableWidgetItem(str(data.get('序号', ''))))
2026-01-17 20:38:27 +08:00
# 话题
2026-01-19 17:24:30 +08:00
self.table.setItem(row, 2, QTableWidgetItem(str(data.get('话题', ''))))
2026-01-17 20:38:27 +08:00
# 定时发布
2026-01-19 17:24:30 +08:00
self.table.setItem(row, 3, QTableWidgetItem(str(data.get('定时发布', ''))))
2026-01-17 20:38:27 +08:00
# 间隔时间
2026-01-19 17:24:30 +08:00
self.table.setItem(row, 4, QTableWidgetItem(str(data.get('间隔时间', ''))))
2026-01-17 20:38:27 +08:00
# 达人链接
2026-01-19 17:24:30 +08:00
self.table.setItem(row, 5, QTableWidgetItem(str(data.get('达人链接', ''))))
# 执行人
self.table.setItem(row, 6, QTableWidgetItem(str(data.get('执行人', ''))))
2026-01-17 20:38:27 +08:00
# 情况
self.table.setItem(row, 7, QTableWidgetItem(str(data.get('情况', ''))))
# 计算后的发布时间
2026-01-18 06:11:21 +08:00
calc_item = QTableWidgetItem(str(data.get('计算后的发布时间', '')))
calc_item.setFlags(calc_item.flags() & ~Qt.ItemIsEditable)
self.table.setItem(row, 8, calc_item)
self.table.blockSignals(False)
2026-01-20 04:09:09 +08:00
self.update_stats()
2026-01-17 20:38:27 +08:00
def on_cell_changed(self, row, column):
"""当单元格内容改变时,重新处理数据"""
if column == 8: # 计算后的发布时间列不允许直接编辑
return
# 从表格中读取数据
self.sync_table_to_raw_data()
# 重新处理数据
self.process_data()
# 更新显示(跳过计算后的发布时间列的更新,避免循环)
self.update_table_from_processed_data()
2026-01-20 04:09:09 +08:00
self.update_stats()
2026-01-17 20:38:27 +08:00
def sync_table_to_raw_data(self):
"""从表格同步数据到raw_data"""
self.raw_data = []
for row in range(self.table.rowCount()):
data = {
2026-01-19 17:24:30 +08:00
'多多 id': self.table.item(row, 0).text() if self.table.item(row, 0) else '',
'序号': self.table.item(row, 1).text() if self.table.item(row, 1) else '',
'话题': 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 '',
2026-01-17 20:38:27 +08:00
'情况': self.table.item(row, 7).text() if self.table.item(row, 7) else '',
}
self.raw_data.append(data)
def update_table_from_processed_data(self):
"""从processed_data更新表格只更新计算后的发布时间列"""
# 临时断开信号避免触发cellChanged
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, 8):
self.table.item(row, 8).setText(str(calculated_time))
else:
2026-01-18 06:11:21 +08:00
item = QTableWidgetItem(str(calculated_time))
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
self.table.setItem(row, 8, item)
2026-01-17 20:38:27 +08:00
self.table.blockSignals(False)
2026-01-20 04:09:09 +08:00
self.update_stats()
def update_stats(self):
"""更新统计信息"""
count = self.table.rowCount()
self.stats_label.setText(f"总计: {count} 条配置")
2026-01-17 20:38:27 +08:00
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.exec_(self.table.viewport().mapToGlobal(position))
def add_row(self):
"""添加一行空数据"""
row_count = self.table.rowCount()
self.table.insertRow(row_count)
# 初始化空单元格
for col in range(9):
2026-01-18 06:11:21 +08:00
item = QTableWidgetItem("")
if col == 8:
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
self.table.setItem(row_count, col, item)
2026-01-17 20:38:27 +08:00
# 更新数据
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(9):
2026-01-18 06:11:21 +08:00
item = QTableWidgetItem("")
if col == 8:
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
self.table.setItem(row_count + i, col, item)
2026-01-17 20:38:27 +08:00
# 更新数据
self.sync_table_to_raw_data()
self.process_data()
self.display_data()
# 启用按钮
self.enable_action_buttons()
QMessageBox.information(self, "成功", f"已添加 {count}")
def delete_selected_rows(self):
"""删除选中的行"""
selected_rows = set()
for item in self.table.selectedItems():
selected_rows.add(item.row())
if not selected_rows:
QMessageBox.warning(self, "警告", "请先选择要删除的行")
return
# 按行号降序排列,从后往前删除
rows_to_delete = sorted(selected_rows, reverse=True)
reply = QMessageBox.question(
self, "确认",
f"确定要删除 {len(rows_to_delete)} 行吗?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
for row in rows_to_delete:
self.table.removeRow(row)
# 更新数据
self.sync_table_to_raw_data()
self.process_data()
self.display_data()
# 更新按钮状态
self.enable_action_buttons()
def copy_selected_rows(self):
"""复制选中的行"""
selected_rows = set()
for item in self.table.selectedItems():
selected_rows.add(item.row())
if not selected_rows:
QMessageBox.warning(self, "警告", "请先选择要复制的行")
return
# 将数据序列化为JSON字符串存储到剪贴板
rows_data = []
for row in sorted(selected_rows):
row_data = {}
for col in range(9):
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))
QMessageBox.information(self, "成功", 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 = {
2026-01-19 17:24:30 +08:00
"多多 id": 0, "序号": 1, "话题": 2,
"定时发布": 3, "间隔时间(分钟)": 4, "达人链接": 5,
"执行人": 6, "情况": 7, "计算后的发布时间": 8
2026-01-17 20:38:27 +08:00
}
# 在末尾插入新行
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):
for key, value in row_data.items():
col = column_mapping.get(key)
if col is not None:
2026-01-18 06:11:21 +08:00
item = QTableWidgetItem(str(value))
if col == 8:
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
self.table.setItem(current_row + i, col, item)
2026-01-17 20:38:27 +08:00
# 更新数据
self.sync_table_to_raw_data()
self.process_data()
self.display_data()
# 启用按钮
self.enable_action_buttons()
QMessageBox.information(self, "成功", 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()
# 保存到JSON
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(self.raw_data, f, ensure_ascii=False, indent=2)
QMessageBox.information(self, "成功", f"配置已保存到:\n{file_path}")
self.status_label.setText(f"配置已保存:{file_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"保存配置时出错:\n{str(e)}")
def load_config(self):
"""从JSON文件加载配置"""
file_path, _ = QFileDialog.getOpenFileName(
self, "加载配置", "", "JSON Files (*.json)"
)
if not file_path:
return
try:
# 读取JSON
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()
QMessageBox.information(self, "成功", f"已加载 {len(self.raw_data)} 条配置")
self.status_label.setText(f"已加载配置:{file_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"加载配置时出错:\n{str(e)}")
def enable_action_buttons(self):
"""根据数据状态启用/禁用按钮"""
has_data = len(self.raw_data) > 0
self.execute_btn.setEnabled(has_data)
self.export_btn.setEnabled(has_data)
2026-01-20 04:09:09 +08:00
self.one_click_upload_btn.setEnabled(has_data)
2026-01-18 06:11:21 +08:00
def set_busy(self, busy):
"""执行任务时禁用交互,防止卡死"""
2026-01-20 04:09:09 +08:00
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)
2026-01-18 06:11:21 +08:00
self.execute_btn.setEnabled(not busy and len(self.raw_data) > 0)
self.export_btn.setEnabled(not busy and len(self.raw_data) > 0)
2026-01-20 04:09:09 +08:00
self.one_click_upload_btn.setEnabled(not busy and len(self.raw_data) > 0)
2026-01-18 06:11:21 +08:00
self.table.setEnabled(not busy)
2026-01-20 04:09:09 +08:00
self.pause_btn.setEnabled(busy)
self.stop_btn.setEnabled(busy)
2026-01-18 06:11:21 +08:00
def on_worker_progress(self, current, total, message):
"""后台任务进度更新"""
if total > 0:
percent = int((current / total) * 100)
self.progress_bar.setValue(percent)
2026-01-20 04:09:09 +08:00
self.set_status("running", message)
self.log(message)
self.statusBar().showMessage(message)
2026-01-18 06:11:21 +08:00
def on_worker_finished(self, success_count, fail_count, error_messages):
"""后台任务完成"""
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.progress_bar.setValue(100)
2026-01-20 04:09:09 +08:00
self.set_status("success", f"执行完成 - 成功: {success_count}, 失败: {fail_count}")
2026-01-18 06:11:21 +08:00
self.set_busy(False)
self.worker = None
2026-01-20 04:09:09 +08:00
self.log(self.status_label.text())
self.pause_btn.setText("暂停")
2026-01-18 06:11:21 +08:00
QMessageBox.information(self, "执行结果", result_msg)
def on_worker_error(self, error_message):
"""后台任务异常"""
self.progress_bar.setValue(0)
2026-01-20 04:09:09 +08:00
self.set_status("error", "执行失败")
2026-01-18 06:11:21 +08:00
self.set_busy(False)
self.worker = None
2026-01-20 04:09:09 +08:00
self.pause_btn.setText("暂停")
self.log(f"执行失败:{error_message}")
2026-01-18 06:11:21 +08:00
QMessageBox.critical(self, "错误", f"执行任务时出错:\n{error_message}")
2026-01-20 04:09:09 +08:00
def pause_or_resume(self):
"""暂停/继续"""
if not self.worker:
return
if self.pause_btn.text() == "暂停":
self.worker.pause()
self.pause_btn.setText("继续")
self.pause_btn.setIcon(FluentIcon.PLAY)
self.set_status("paused", "已暂停")
self.log("任务已暂停")
else:
self.worker.resume()
self.pause_btn.setText("暂停")
self.pause_btn.setIcon(FluentIcon.PAUSE)
self.set_status("running", "运行中")
self.log("任务已继续")
def stop_worker(self):
"""停止任务"""
if self.worker:
self.worker.stop()
self.set_status("warning", "正在停止...")
self.log("正在停止任务")
def clear_log(self):
"""清空日志"""
self.log_output.clear()
def log(self, message):
"""追加日志"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.log_output.append(f"[{timestamp}] {message}")
self.log_output.verticalScrollBar().setValue(
self.log_output.verticalScrollBar().maximum()
)
def set_status(self, level, message):
"""状态栏渐变提示"""
gradients = {
"running": "stop:0 #2b5cff, stop:1 #4c7dff",
"success": "stop:0 #1f9d55, stop:1 #35c277",
"error": "stop:0 #c0392b, stop:1 #e74c3c",
"warning": "stop:0 #b36b00, stop:1 #f39c12",
"paused": "stop:0 #5f6c7b, stop:1 #8b99aa",
"idle": "stop:0 #2b2f3a, stop:1 #3a3f4b",
}
gradient = gradients.get(level, gradients["idle"])
self.status_label.setStyleSheet(
"padding: 6px 10px; border-radius: 6px; "
f"background: qlineargradient(x1:0, y1:0, x2:1, y2:0, {gradient}); "
"color: #e5eaf3;"
)
self.status_label.setText(message)
indicator_colors = {
"running": "#4c7dff",
"success": "#35c277",
"error": "#e74c3c",
"warning": "#f39c12",
"paused": "#8b99aa",
"idle": "#67C23A",
}
self.status_indicator.setStyleSheet(
f"color: {indicator_colors.get(level, '#67C23A')}; font-weight: bold;"
)
def toggle_nav(self):
"""侧边导航折叠切换"""
start = self.nav.width()
end = 60 if start > 80 else 210
anim = QPropertyAnimation(self.nav, b"maximumWidth", self)
anim.setDuration(250)
anim.setStartValue(start)
anim.setEndValue(end)
anim.valueChanged.connect(lambda v: self.nav.setMinimumWidth(int(v)))
anim.start()
self._nav_anim = anim
if hasattr(self.nav, "setCompactMode"):
self.nav.setCompactMode(end <= 80)
def switch_page(self, widget):
"""切换页面并淡入"""
self.stack.setCurrentWidget(widget)
self.fade_in(widget)
def eventFilter(self, obj, event):
"""标题栏拖拽移动窗口"""
if obj.objectName() == "titleCard":
if event.type() == QEvent.MouseButtonPress and event.button() == Qt.LeftButton:
self._drag_pos = event.globalPos() - self.frameGeometry().topLeft()
return True
if event.type() == QEvent.MouseMove and event.buttons() == Qt.LeftButton and hasattr(self, "_drag_pos"):
self.move(event.globalPos() - self._drag_pos)
return True
if event.type() == QEvent.MouseButtonRelease:
self._drag_pos = None
return super().eventFilter(obj, event)
def update_queue(self, tasks):
"""更新任务队列显示"""
self.queue_list.clear()
for i, item in enumerate(tasks, 1):
user_id = item.get("多多 id", "")
time_start = item.get("计算后的发布时间", "")
self.queue_list.addItem(f"{i}. {user_id} / {time_start}")
def fade_in(self, widget):
"""卡片淡入动画"""
effect = QGraphicsOpacityEffect(widget)
widget.setGraphicsEffect(effect)
anim = QPropertyAnimation(effect, b"opacity", widget)
anim.setDuration(350)
anim.setStartValue(0.0)
anim.setEndValue(1.0)
anim.start()
widget._fade_anim = anim # 防止被回收
def toggle_theme(self, checked):
"""主题切换"""
if checked:
setTheme(Theme.DARK)
self.set_status("idle", "已切换暗色主题")
self.show_info("主题切换", "已切换暗色主题")
else:
setTheme(Theme.LIGHT)
self.set_status("idle", "已切换亮色主题")
self.show_info("主题切换", "已切换亮色主题")
def toggle_maximize(self):
"""窗口最大化切换"""
if self.isMaximized():
self.showNormal()
else:
self.showMaximized()
def show_info(self, title, content):
"""显示成功提示"""
InfoBar.success(
title=title,
content=content,
parent=self,
position=InfoBarPosition.TOP_RIGHT,
duration=2500
)
def show_error(self, title, content):
"""显示错误提示"""
InfoBar.error(
title=title,
content=content,
parent=self,
position=InfoBarPosition.TOP_RIGHT,
duration=3000
)
2026-01-17 20:38:27 +08:00
def execute_tasks(self):
"""执行任务调用main.py中的Pdd类"""
2026-01-18 06:11:21 +08:00
# 同步最新编辑内容
self.sync_table_to_raw_data()
self.process_data()
self.update_table_from_processed_data()
2026-01-17 20:38:27 +08:00
if not self.processed_data:
QMessageBox.warning(self, "警告", "没有可执行的数据")
return
2026-01-19 17:24:30 +08:00
# 检查大文件夹路径
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
2026-01-17 20:38:27 +08:00
reply = QMessageBox.question(
self, "确认",
2026-01-19 17:24:30 +08:00
f"确定要执行 {len(self.processed_data)} 个任务吗?\n大文件夹路径:{base_folder_path if base_folder_path else '未设置'}",
2026-01-17 20:38:27 +08:00
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
2026-01-18 06:11:21 +08:00
if self.worker is not None:
QMessageBox.warning(self, "提示", "任务正在执行中,请稍后")
return
2026-01-20 04:09:09 +08:00
self.update_queue(tasks)
2026-01-18 06:11:21 +08:00
self.progress_bar.setValue(0)
2026-01-20 04:09:09 +08:00
self.set_status("running", "开始执行任务...")
2026-01-18 06:11:21 +08:00
self.set_busy(True)
tasks = [dict(item) for item in self.processed_data]
2026-01-20 04:09:09 +08:00
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()
)
2026-01-18 06:11:21 +08:00
self.worker.progress.connect(self.on_worker_progress)
self.worker.finished.connect(self.on_worker_finished)
self.worker.error.connect(self.on_worker_error)
self.worker.start()
2026-01-20 04:09:09 +08:00
self.set_status("running", "运行中")
def one_click_upload(self):
"""一键上传按多多ID文件夹批量上传视频"""
# 同步最新编辑内容
self.sync_table_to_raw_data()
self.process_data()
self.update_table_from_processed_data()
if not self.processed_data:
QMessageBox.warning(self, "警告", "没有可执行的数据")
return
base_folder_path = self.folder_path_input.text().strip()
if not base_folder_path:
default_folder = self.find_default_folder()
if default_folder:
base_folder_path = default_folder
self.folder_path_input.setText(default_folder)
else:
QMessageBox.warning(self, "警告", "未设置大文件夹路径")
return
if base_folder_path and not os.path.exists(base_folder_path):
QMessageBox.warning(self, "警告", f"大文件夹路径不存在:\n{base_folder_path}")
return
# 仅使用选中行,否则全部
selected_rows = sorted({item.row() for item in self.table.selectedItems()})
if selected_rows:
tasks = [dict(self.processed_data[i]) for i in selected_rows if i < len(self.processed_data)]
else:
tasks = [dict(item) for item in self.processed_data]
reply = QMessageBox.question(
self,
"确认",
f"确定要一键上传 {len(tasks)} 个任务吗?\n大文件夹路径:{base_folder_path}",
QMessageBox.Yes | QMessageBox.No
)
if reply != QMessageBox.Yes:
return
if self.worker is not None:
QMessageBox.warning(self, "提示", "任务正在执行中,请稍后")
return
self.update_queue(tasks)
self.progress_bar.setValue(0)
self.set_status("running", "开始一键上传任务...")
self.set_busy(True)
self.worker = TaskWorker(
tasks,
base_folder_path=base_folder_path,
batch_upload=True,
input_delay=self.input_delay_spin.value()
)
self.worker.progress.connect(self.on_worker_progress)
self.worker.finished.connect(self.on_worker_finished)
self.worker.error.connect(self.on_worker_error)
self.worker.start()
self.set_status("running", "运行中")
2026-01-17 20:38:27 +08:00
def export_template(self):
"""导出Excel模板"""
file_path, _ = QFileDialog.getSaveFileName(
self, "保存Excel模板", "配置模板.xlsx", "Excel Files (*.xlsx)"
)
if not file_path:
return
try:
# 创建模板数据(空行)
template_data = {
2026-01-19 17:24:30 +08:00
'多多 id': ['示例1050100241'],
'序号': ['示例1'],
2026-01-17 20:38:27 +08:00
'话题(以中文"-"分隔)': ['示例python-自动化-技术'],
'定时发布': ['示例2026-01-28 09:30:00'],
'间隔时间': ['示例30单位分钟'],
'达人链接': ['示例https://www.xiaohongshu.com/explore/xxx'],
2026-01-19 17:24:30 +08:00
'执行人': ['示例:张三'],
2026-01-17 20:38:27 +08:00
'情况': ['备注信息']
}
# 创建DataFrame并导出
df = pd.DataFrame(template_data)
df.to_excel(file_path, index=False, engine='openpyxl')
QMessageBox.information(
self, "成功",
f"Excel模板已导出到\n{file_path}\n\n"
"提示:\n"
"1. 索引字段用于标识文件序号\n"
"2. 话题使用中文破折号\"\"或短横线\"\"分隔\n"
"3. 相同用户ID的第一条数据使用定时发布时间后续根据间隔时间自动计算\n"
2026-01-19 17:24:30 +08:00
"4. 间隔时间单位为分钟\n"
"5. 大文件夹路径请在GUI程序顶部的输入框中设置"
2026-01-17 20:38:27 +08:00
)
self.status_label.setText(f"Excel模板已导出到{file_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"导出Excel模板时出错\n{str(e)}")
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({
2026-01-19 17:24:30 +08:00
'多多 id': data.get('多多 id', ''),
'序号': data.get('序号', ''),
2026-01-17 20:38:27 +08:00
'话题(以中文"-"分隔)': data.get('话题', ''),
'定时发布': data.get('定时发布', ''),
'间隔时间': data.get('间隔时间', ''),
'达人链接': data.get('达人链接', ''),
2026-01-19 17:24:30 +08:00
'执行人': data.get('执行人', ''),
2026-01-17 20:38:27 +08:00
'情况': data.get('情况', ''),
'计算后的发布时间': data.get('计算后的发布时间', ''),
})
# 创建DataFrame并导出
df = pd.DataFrame(export_data)
df.to_excel(file_path, index=False, engine='openpyxl')
QMessageBox.information(self, "成功", f"数据已导出到:\n{file_path}")
self.status_label.setText(f"数据已导出到:{file_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"导出Excel文件时出错\n{str(e)}")
import traceback
traceback.print_exc()
def main():
app = QApplication(sys.argv)
2026-01-20 04:09:09 +08:00
setTheme(Theme.DARK)
2026-01-17 20:38:27 +08:00
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()