第一版完整版
This commit is contained in:
27942
2026-01-24 18:13:30 +08:00
parent a24d1cc4f8
commit 2193f11e03

View File

@@ -12,7 +12,7 @@ from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QFileDialog, QTableWidgetItem, QMessageBox,
QDateTimeEdit, QGridLayout, QStackedWidget, QButtonGroup,
QStyle, QComboBox, QFrame, QShortcut, QMenu,
QStyle, QComboBox, QFrame, QShortcut, QMenu, QAbstractButton,
QAbstractItemView, QTableView, QStyledItemDelegate,
QStyleOptionProgressBar, QStyleOptionButton, QHeaderView,
QTabWidget, QSplitter, QSizePolicy
@@ -638,6 +638,9 @@ class ConfigTableModel(QAbstractTableModel):
key = mapping.get(col, "")
if role == Qt.DisplayRole:
return str(config.get(key, "")) if key else ""
elif role == Qt.TextAlignmentRole:
# 所有单元格居中对齐
return Qt.AlignCenter
elif role == Qt.ToolTipRole:
# 为达人链接和文件路径列提供 tooltip 显示完整内容
if col in (5, 8) and key:
@@ -691,9 +694,8 @@ class ConfigTableModel(QAbstractTableModel):
class TableActionDelegate(QStyledItemDelegate):
"""Model/View 操作列 + 进度列 delegate"""
def __init__(self, parent, on_edit, on_delete):
def __init__(self, parent, on_delete):
super().__init__(parent)
self.on_edit = on_edit
self.on_delete = on_delete
def paint(self, painter, option, index):
@@ -716,16 +718,10 @@ class TableActionDelegate(QStyledItemDelegate):
return
if index.column() == 10:
rect = option.rect
btn_w = (rect.width() - 12) // 2
edit_rect = rect.adjusted(6, 4, -rect.width() + btn_w + 6, -4)
delete_rect = rect.adjusted(6 + btn_w + 6, 4, -6, -4)
edit_btn = QStyleOptionButton()
edit_btn.rect = edit_rect
edit_btn.text = "编辑"
delete_rect = rect.adjusted(6, 4, -6, -4)
delete_btn = QStyleOptionButton()
delete_btn.rect = delete_rect
delete_btn.text = "删除"
QApplication.style().drawControl(QStyle.CE_PushButton, edit_btn, painter)
QApplication.style().drawControl(QStyle.CE_PushButton, delete_btn, painter)
return
super().paint(painter, option, index)
@@ -735,12 +731,7 @@ class TableActionDelegate(QStyledItemDelegate):
return super().editorEvent(event, model, option, index)
if event.type() == event.MouseButtonRelease:
rect = option.rect
btn_w = (rect.width() - 12) // 2
edit_rect = rect.adjusted(6, 4, -rect.width() + btn_w + 6, -4)
delete_rect = rect.adjusted(6 + btn_w + 6, 4, -6, -4)
if edit_rect.contains(event.pos()):
self.on_edit(index)
return True
delete_rect = rect.adjusted(6, 4, -6, -4)
if delete_rect.contains(event.pos()):
self.on_delete(index)
return True
@@ -964,11 +955,11 @@ class MainWindow(QMainWindow):
import_row = QHBoxLayout()
import_row.addWidget(QLabel("Excel文件:"))
self.excel_path_input = LineEdit()
self.excel_path_input.setReadOnly(True)
self.excel_path_input.setPlaceholderText("点击导入按钮选择Excel配置文件")
import_row.addWidget(self.excel_path_input)
self.excel_browse_btn = PushButton("浏览")
self.excel_browse_btn.clicked.connect(self.browse_excel)
import_row.addWidget(self.excel_browse_btn)
self.import_btn = PrimaryPushButton("导入")
self.import_btn = PrimaryPushButton("导入配置")
self.import_btn.setToolTip("选择Excel文件并导入配置")
self.import_btn.clicked.connect(self.import_excel)
import_row.addWidget(self.import_btn)
self.download_template_btn = PushButton("下载模板")
@@ -1047,6 +1038,13 @@ class MainWindow(QMainWindow):
table_title = QLabel("配置列表从Excel导入后显示可直接在表格中编辑")
table_title.setFont(QFont("Microsoft YaHei", 11, QFont.Bold))
table_layout.addWidget(table_title)
self.table_edit_hint = QLabel("编辑模式:当前行已高亮,其它行已锁定。修改后点击“确认”保存,点击“退出”还原。")
self.table_edit_hint.setVisible(False)
self.table_edit_hint.setStyleSheet(
"background-color: #fff7ed; color: #9a3412; "
"border: 1px solid #fed7aa; border-radius: 6px; padding: 6px 8px;"
)
table_layout.addWidget(self.table_edit_hint)
search_row = QHBoxLayout()
self.table_search_input = LineEdit()
@@ -1143,8 +1141,8 @@ class MainWindow(QMainWindow):
self.config_table.horizontalHeader().setSortIndicatorShown(True)
self.config_table.verticalHeader().setVisible(False)
self.config_table.verticalHeader().setDefaultSectionSize(42)
# 设置表格编辑触发方式:双击进入编辑(单击只选择行)
self.config_table.setEditTriggers(TableWidget.DoubleClicked)
# 禁用直接编辑,只能通过编辑按钮进入编辑模式
self.config_table.setEditTriggers(TableWidget.NoEditTriggers)
self.config_table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.config_table.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.config_table.setContextMenuPolicy(Qt.CustomContextMenu)
@@ -1385,6 +1383,54 @@ class MainWindow(QMainWindow):
if current_item:
self.config_table.closePersistentEditor(current_item)
self.config_table.setCurrentItem(None)
# 恢复为只读模式
self.config_table.setEditTriggers(TableWidget.NoEditTriggers)
self._cleanup_edit_mode_state()
def _set_row_highlight(self, row, enabled):
"""高亮/取消高亮某一行"""
highlight_color = QColor(255, 247, 216)
for col in range(9):
item = self.config_table.item(row, col)
if not item:
continue
if enabled:
item.setBackground(highlight_color)
else:
item.setData(Qt.BackgroundRole, None)
def _set_other_rows_locked(self, edit_row, locked):
"""锁定/解锁编辑行以外的行"""
for r in range(self.config_table.rowCount()):
if r == edit_row:
# 编辑行保持可用
continue
# 操作列按钮禁用/启用
action_widget = self.config_table.cellWidget(r, 10)
if action_widget:
action_widget.setEnabled(not locked)
# 数据列禁用/启用
for col in range(9):
item = self.config_table.item(r, col)
if not item:
continue
if locked:
item.setFlags(item.flags() & ~Qt.ItemIsEnabled)
else:
item.setFlags((item.flags() | Qt.ItemIsEnabled) & ~Qt.ItemIsEditable)
def _cleanup_edit_mode_state(self):
"""清理编辑态UI状态"""
edit_row = getattr(self, '_editing_row', None)
if edit_row is None:
return
self._set_row_highlight(edit_row, False)
self._set_other_rows_locked(edit_row, False)
self._editing_row = None
if getattr(self, "table_edit_hint", None):
self.table_edit_hint.setVisible(False)
if hasattr(self, '_edit_selection_mode_backup'):
self.config_table.setSelectionMode(self._edit_selection_mode_backup)
def on_table_item_changed(self, item):
"""表格内容变更回调"""
@@ -1420,6 +1466,14 @@ class MainWindow(QMainWindow):
# 确保标志被重置
self.is_updating_table = False
def _create_centered_item(self, text):
"""创建居中对齐的表格单元格"""
item = QTableWidgetItem(str(text))
item.setTextAlignment(Qt.AlignCenter)
# 默认不可编辑,必须通过编辑按钮进入编辑模式
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
return item
def _set_status_item(self, row, text):
"""设置状态列图标与文本"""
try:
@@ -1427,6 +1481,8 @@ class MainWindow(QMainWindow):
self.config_table.itemChanged.disconnect(self.on_table_item_changed)
try:
item = QTableWidgetItem(text)
item.setTextAlignment(Qt.AlignCenter) # 居中对齐
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
if "完成" in text or "成功" in text:
item.setIcon(self.style().standardIcon(QStyle.SP_DialogApplyButton))
elif "失败" in text or "错误" in text:
@@ -1466,28 +1522,142 @@ class MainWindow(QMainWindow):
self.config_table.setCellWidget(row, 9, progress)
def _set_action_buttons(self, row, config_index):
"""设置操作列按钮"""
"""设置操作列按钮(编辑和删除)"""
wrapper = QWidget()
layout = QHBoxLayout(wrapper)
layout.setContentsMargins(4, 0, 4, 0)
layout.setSpacing(4)
edit_btn = PushButton("编辑")
edit_btn.setFixedWidth(50)
delete_btn = PushButton("删除")
delete_btn.setFixedWidth(50)
# 使用默认参数捕获当前值,避免闭包问题
edit_btn.clicked.connect(lambda checked, r=row: self._enter_edit_mode(r))
edit_btn.clicked.connect(lambda checked, r=row, idx=config_index: self._enter_edit_mode(r, idx))
delete_btn.clicked.connect(lambda checked, idx=config_index: self.delete_row_by_index(idx))
layout.addWidget(edit_btn)
layout.addWidget(delete_btn)
self.config_table.setCellWidget(row, 10, wrapper)
def _enter_edit_mode(self, row):
"""进入指定行的编辑模式"""
def _set_edit_mode_buttons(self, row, config_index):
"""设置编辑模式按钮(确认和退出)"""
wrapper = QWidget()
layout = QHBoxLayout(wrapper)
layout.setContentsMargins(4, 0, 4, 0)
layout.setSpacing(4)
confirm_btn = PushButton("确认")
confirm_btn.setFixedWidth(50)
cancel_btn = PushButton("退出")
cancel_btn.setFixedWidth(50)
# 使用默认参数捕获当前值
confirm_btn.clicked.connect(lambda checked, r=row, idx=config_index: self._confirm_edit(r, idx))
cancel_btn.clicked.connect(lambda checked, r=row, idx=config_index: self._cancel_edit(r, idx))
layout.addWidget(confirm_btn)
layout.addWidget(cancel_btn)
self.config_table.setCellWidget(row, 10, wrapper)
def _enter_edit_mode(self, row, config_index):
"""进入编辑模式"""
if row < 0 or row >= self.config_table.rowCount():
return
# 选中第一个可编辑的单元格多多ID列
item = self.config_table.item(row, 0)
if item:
self.config_table.setCurrentItem(item)
self.config_table.editItem(item)
# 允许当前行进入编辑时的交互触发
if not hasattr(self, '_edit_triggers_backup'):
self._edit_triggers_backup = self.config_table.editTriggers()
self.config_table.setEditTriggers(QAbstractItemView.DoubleClicked | QAbstractItemView.SelectedClicked)
if not hasattr(self, '_edit_selection_mode_backup'):
self._edit_selection_mode_backup = self.config_table.selectionMode()
self.config_table.setSelectionMode(QAbstractItemView.SingleSelection)
self._editing_row = row
# 保存原始数据用于还原
if not hasattr(self, '_edit_backup'):
self._edit_backup = {}
original_data = {}
for col in range(9): # 前9列是数据列
item = self.config_table.item(row, col)
original_data[col] = item.text() if item else ""
self._edit_backup[row] = original_data
# 启用该行的编辑
for col in range(9):
item = self.config_table.item(row, col)
if item:
item.setFlags(item.flags() | Qt.ItemIsEditable)
# 高亮编辑行并锁定其他行
self._set_row_highlight(row, True)
self._set_other_rows_locked(row, True)
# 切换到编辑模式按钮
self._set_edit_mode_buttons(row, config_index)
# 自动聚焦第一个可编辑单元格
first_item = None
for col in range(9):
item = self.config_table.item(row, col)
if item and (item.flags() & Qt.ItemIsEditable):
first_item = item
self.config_table.setCurrentCell(row, col)
break
if first_item:
self.config_table.setFocus(Qt.TabFocusReason)
self.config_table.scrollToItem(first_item, QAbstractItemView.PositionAtCenter)
self.config_table.editItem(first_item)
if self.table_edit_hint:
self.table_edit_hint.setVisible(True)
self._show_infobar("info", "编辑模式", f"正在编辑第 {row + 1} 行,修改后点击确认保存或退出还原")
def _confirm_edit(self, row, config_index):
"""确认编辑并保存"""
if row < 0 or row >= self.config_table.rowCount():
return
# 同步表格数据到configs
self._sync_config_from_row(row)
# 禁用该行的编辑
for col in range(9):
item = self.config_table.item(row, col)
if item:
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
# 清除备份
if hasattr(self, '_edit_backup') and row in self._edit_backup:
del self._edit_backup[row]
# 恢复操作按钮
self._set_action_buttons(row, config_index)
self._show_infobar("success", "保存成功", f"{row + 1} 行数据已保存")
# 退出编辑模式后恢复为只读
self.config_table.setEditTriggers(TableWidget.NoEditTriggers)
self._cleanup_edit_mode_state()
def _cancel_edit(self, row, config_index):
"""取消编辑并还原数据"""
if row < 0 or row >= self.config_table.rowCount():
return
# 还原原始数据
if hasattr(self, '_edit_backup') and row in self._edit_backup:
original_data = self._edit_backup[row]
# 临时断开信号防止触发同步
try:
self.config_table.itemChanged.disconnect(self.on_table_item_changed)
except:
pass
try:
for col, value in original_data.items():
item = self.config_table.item(row, col)
if item:
item.setText(value)
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
finally:
self.config_table.itemChanged.connect(self.on_table_item_changed)
del self._edit_backup[row]
# 恢复操作按钮
self._set_action_buttons(row, config_index)
self._show_infobar("info", "已退出", f"{row + 1} 行数据已还原")
# 退出编辑模式后恢复为只读
self.config_table.setEditTriggers(TableWidget.NoEditTriggers)
self._cleanup_edit_mode_state()
def delete_row_by_index(self, row):
"""删除指定行"""
@@ -2061,8 +2231,9 @@ class MainWindow(QMainWindow):
"""按多多ID应用定时发布+间隔时间规则
规则说明:
第一种情况第一条数据有定时时间有间隔时间5分钟然后第二条数据的定时时间就是在第一条的上面加5分钟第三条在第二条定时时间上加5分钟
第二种情况第一条数据没有定时时间第二条数据有定时时间还有间隔时间5分钟那么第三条数据在第二条定时时间上加5分钟
1. 如果某条有定时时间,就按照定时时间发布,并记录为基准时间
2. 如果某条没有定时时间,也没有间隔时间,就是立即发布(不设置定时)
3. 如果某条没有定时时间,但有间隔时间,就是在上一条发布时间的基础上加上间隔时间发布
注意按多多ID分组同一个多多ID的所有数据按行顺序处理
"""
@@ -2073,56 +2244,42 @@ class MainWindow(QMainWindow):
user_id = config.get("多多id", "")
if not user_id:
continue
# 只按多多ID分组不按序号分组
grouped[user_id].append(item)
updated_count = 0
for user_id, items in grouped.items():
items.sort(key=lambda x: x["row_idx"])
base_time = None
interval_seconds = 0
base_time = None # 上一条的发布时间(基准时间)
for entry in items:
config = entry["config"]
row_idx = entry["row_idx"]
schedule_text = (config.get("定时发布") or "").strip()
interval_value = config.get("间隔时间", 0)
# 解析间隔时间
# 解析间隔时间(分钟转秒)
current_interval = self._parse_interval_seconds(interval_value)
# 解析定时时间
parsed_time = self._parse_schedule_time(schedule_text) if schedule_text else None
# 情况1当前条目有定时时间和间隔时间
# 例如第1条有定时时间09:00和间隔时间5分钟或第2条有定时时间09:10和间隔时间5分钟
if parsed_time and current_interval > 0:
# 设置基准时间和间隔时间,用于后续数据自动计算
# 情况1当前条目有定时时间 -> 使用该定时时间,并记录为基准时间
if parsed_time:
base_time = parsed_time
interval_seconds = current_interval
# 不更新当前条目的定时时间(保持用户设置的原值)
# 但更新 base_time 和 interval_seconds 用于后续计算
# 不修改当前条目的定时时间,保持用户设置的原值
# 情况2当前条目有定时时间但有间隔时间
# 例如:用户只设置了定时时间,没有设置间隔时间
elif parsed_time and current_interval == 0:
# 只更新基准时间,清空间隔时间(后续不会自动计算)
base_time = parsed_time
interval_seconds = 0
# 情况2当前条目有定时时间但有间隔时间
elif not parsed_time and current_interval > 0:
if base_time:
# 有基准时间 -> 新时间 = 基准时间 + 间隔时间
base_time = base_time + timedelta(seconds=current_interval)
new_text = self._format_schedule_time(base_time)
config["定时发布"] = new_text
self._update_table_cell(row_idx, 3, new_text, highlight=True)
updated_count += 1
# 如果没有基准时间,则保持空(立即发布)
# 情况3当前条目没有定时时间但存在基准时间和间隔时间
# 例如第1条有定时时间09:00和间隔时间5分钟第2条没有定时时间 -> 第2条自动计算为09:05
# 或者第2条有定时时间09:10和间隔时间5分钟第3条没有定时时间 -> 第3条自动计算为09:15
elif not parsed_time and base_time and interval_seconds > 0:
# 计算新的定时时间 = 基准时间 + 间隔时间
base_time = base_time + timedelta(seconds=interval_seconds)
new_text = self._format_schedule_time(base_time)
config["定时发布"] = new_text
self._update_table_cell(row_idx, 3, new_text, highlight=True)
updated_count += 1
# 注意base_time 已经更新为新的计算值,用于下一条数据的计算
# 情况4当前条目没有定时时间也没有基准时间或间隔时间
# 例如第1条没有定时时间和间隔时间第2条也没有 -> 不做任何处理
# 情况3当前条目没有定时时间也没有间隔时间 -> 立即发布(保持空)
# 不做任何处理,保持原样
return updated_count
@@ -2191,6 +2348,8 @@ class MainWindow(QMainWindow):
self.config_table.itemChanged.disconnect(self.on_table_item_changed)
try:
item = QTableWidgetItem(str(value))
item.setTextAlignment(Qt.AlignCenter) # 居中对齐
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
if highlight:
item.setBackground(QColor("#E6F4FF"))
self.config_table.setItem(row, col, item)
@@ -2225,6 +2384,8 @@ class MainWindow(QMainWindow):
try:
# 第7列是"情况"列
status_item = QTableWidgetItem(status)
status_item.setTextAlignment(Qt.AlignCenter) # 居中对齐
status_item.setFlags(status_item.flags() & ~Qt.ItemIsEditable)
# 根据状态设置不同的颜色
if status == "已完成":
@@ -2654,10 +2815,7 @@ class MainWindow(QMainWindow):
for col, width in widths.items():
if col < self.config_table.columnCount():
self.config_table.setColumnWidth(col, width)
# 设置达人链接列和文件路径列不自动调整宽度
header = self.config_table.horizontalHeader()
header.setSectionResizeMode(5, QHeaderView.Fixed) # 达人链接列固定宽度
header.setSectionResizeMode(8, QHeaderView.Fixed) # 文件路径列固定宽度
# 所有列都可以手动调整宽度Interactive模式
def _apply_table_view_column_widths(self):
"""应用 Model/View 列宽"""
@@ -2677,11 +2835,7 @@ class MainWindow(QMainWindow):
for col, width in widths.items():
if self.table_view.model() and col < self.table_view.model().columnCount():
self.table_view.setColumnWidth(col, width)
# 设置达人链接列和文件路径列固定宽度
header = self.table_view.horizontalHeader()
if header:
header.setSectionResizeMode(5, QHeaderView.Fixed) # 达人链接列固定宽度
header.setSectionResizeMode(8, QHeaderView.Fixed) # 文件路径列固定宽度
# 所有列都可以手动调整宽度Interactive模式
def _refresh_log_match_selector(self, items):
"""更新日志匹配下拉"""
@@ -2731,14 +2885,6 @@ class MainWindow(QMainWindow):
else:
InfoBar.error(title=title, content=content, parent=self, position=InfoBarPosition.TOP_RIGHT)
def browse_excel(self):
"""浏览Excel文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择Excel文件", "", "Excel文件 (*.xlsx *.xls)"
)
if file_path:
self.excel_path_input.setText(file_path)
def browse_folder(self):
"""浏览文件夹"""
folder_path = QFileDialog.getExistingDirectory(self, "选择文件夹")
@@ -2783,7 +2929,7 @@ class MainWindow(QMainWindow):
self.log_text.append(" - 序号: 文件序号用于匹配文件夹中的文件1-视频名称.mp4")
self.log_text.append(" - 话题: 发布时的话题标签")
self.log_text.append(" - 定时发布: 定时发布时间格式yyyy-MM-dd HH:mm")
self.log_text.append(" - 间隔时间: 同一ID下各条目的发布间隔分钟")
self.log_text.append(" - 间隔时间: 在上一条基础上延迟的分钟数(无定时时间时生效")
self.log_text.append(" - 达人链接: 达人主页链接")
self.log_text.append(" - 执行人: 负责人")
self.log_text.append(" - 情况: 执行状态(待执行/执行中/已完成/失败)")
@@ -2793,15 +2939,17 @@ class MainWindow(QMainWindow):
logger.error(f"保存模板失败: {e}")
def import_excel(self):
"""导入Excel配置文件"""
excel_path = self.excel_path_input.text().strip()
if not excel_path:
self._show_infobar("warning", "警告", "请先选择Excel文件")
return
"""导入Excel配置文件(直接弹出文件选择对话框)"""
# 弹出文件选择对话框
excel_path, _ = QFileDialog.getOpenFileName(
self, "选择Excel配置文件", "", "Excel文件 (*.xlsx *.xls)"
)
if not os.path.exists(excel_path):
self._show_infobar("warning", "警告", f"Excel文件不存在: {excel_path}")
return
if not excel_path:
return # 用户取消选择
# 显示选择的文件路径
self.excel_path_input.setText(excel_path)
try:
# 读取Excel文件添加更多异常处理
@@ -2917,21 +3065,21 @@ class MainWindow(QMainWindow):
self.config_table.setRowCount(len(self.page_row_indices))
for table_row, config_index in enumerate(self.page_row_indices):
config = self.configs[config_index]
self.config_table.setItem(table_row, 0, QTableWidgetItem(str(config.get('多多id', ''))))
self.config_table.setItem(table_row, 1, QTableWidgetItem(str(config.get('序号', ''))))
self.config_table.setItem(table_row, 2, QTableWidgetItem(str(config.get('话题', ''))))
self.config_table.setItem(table_row, 3, QTableWidgetItem(str(config.get('定时发布', ''))))
self.config_table.setItem(table_row, 4, QTableWidgetItem(str(config.get('间隔时间', 0))))
self.config_table.setItem(table_row, 0, self._create_centered_item(str(config.get('多多id', ''))))
self.config_table.setItem(table_row, 1, self._create_centered_item(str(config.get('序号', ''))))
self.config_table.setItem(table_row, 2, self._create_centered_item(str(config.get('话题', ''))))
self.config_table.setItem(table_row, 3, self._create_centered_item(str(config.get('定时发布', ''))))
self.config_table.setItem(table_row, 4, self._create_centered_item(str(config.get('间隔时间', 0))))
# 达人链接列:设置 tooltip 显示完整内容
url_text = str(config.get('达人链接', ''))
url_item = QTableWidgetItem(url_text)
url_item = self._create_centered_item(url_text)
url_item.setToolTip(url_text) # 悬停显示完整链接
self.config_table.setItem(table_row, 5, url_item)
self.config_table.setItem(table_row, 6, QTableWidgetItem(str(config.get('执行人', ''))))
self.config_table.setItem(table_row, 6, self._create_centered_item(str(config.get('执行人', ''))))
self._set_status_item(table_row, str(config.get('情况', '待执行')))
# 文件路径列第8列索引为8如果配置中没有则显示空设置 tooltip
file_path = str(config.get('文件路径', ''))
file_path_item = QTableWidgetItem(file_path)
file_path_item = self._create_centered_item(file_path)
file_path_item.setToolTip(file_path) # 悬停显示完整路径
self.config_table.setItem(table_row, 8, file_path_item)
self._set_progress_item(table_row, str(config.get('情况', '待执行')))