1150 lines
49 KiB
Python
1150 lines
49 KiB
Python
"""Tab 3: Shopee update UI."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from ... import product_status
|
||
from ..models import ApplyTaskTableModel
|
||
from ..widgets import *
|
||
from ..workers import ApplyWorker as _RealApplyWorker, WriteBackWorker as _RealWriteBackWorker
|
||
|
||
|
||
def ApplyWorker(*args, **kwargs):
|
||
return _call_package_attr("ApplyWorker", _RealApplyWorker, *args, **kwargs)
|
||
|
||
|
||
def WriteBackWorker(*args, **kwargs):
|
||
return _call_package_attr("WriteBackWorker", _RealWriteBackWorker, *args, **kwargs)
|
||
|
||
class ApplyTab(QWidget):
|
||
"""Tab 3: list generated tasks and confirm the update scope."""
|
||
|
||
UPDATE_MODE_ITEMS = [
|
||
("只更新标题", "title"),
|
||
("只更新封面", "cover"),
|
||
("更新标题和封面", "title_cover"),
|
||
]
|
||
STATUS_FILTERS = [
|
||
("已生成", "generated"),
|
||
("失败", "failed"),
|
||
("已更新", "applied"),
|
||
("略过", "skipped"),
|
||
("全部状态", "all"),
|
||
]
|
||
|
||
def __init__(
|
||
self,
|
||
parent=None,
|
||
db_path=None,
|
||
config=None,
|
||
status_callback=None,
|
||
open_accounts_callback=None,
|
||
open_settings_callback=None,
|
||
refresh_workflow_callback=None,
|
||
):
|
||
super().__init__(parent)
|
||
self.config = appconfig.load_config() if config is None else config
|
||
self.config_path = self.config.get("config_path") or appconfig.CONFIG_PATH
|
||
self.db_path = _database_path(db_path, self.config)
|
||
self.status_callback = status_callback
|
||
self.open_accounts_callback = open_accounts_callback
|
||
self.open_settings_callback = open_settings_callback
|
||
self.refresh_workflow_callback = refresh_workflow_callback
|
||
self.apply_worker = None
|
||
self.apply_thread = None
|
||
self.result_write_back_worker = None
|
||
self.result_write_back_thread = None
|
||
self.last_apply_summary = None
|
||
|
||
self.batch_filter = QComboBox()
|
||
self.batch_filter.setObjectName("applyBatchFilter")
|
||
self.shop_filter = QComboBox()
|
||
self.shop_filter.setObjectName("applyShopFilter")
|
||
self.item_filter = QLineEdit()
|
||
self.item_filter.setObjectName("applyItemFilter")
|
||
self.item_filter.setPlaceholderText("商品ID")
|
||
self.status_filter = QComboBox()
|
||
self.status_filter.setObjectName("applyStatusFilter")
|
||
for label, value in self.STATUS_FILTERS:
|
||
self.status_filter.addItem(label, value)
|
||
self.product_status_filter = QComboBox()
|
||
self.product_status_filter.setObjectName("applyProductStatusFilter")
|
||
for label, value in product_status.PRODUCT_STATUS_FILTER_ITEMS:
|
||
self.product_status_filter.addItem(label, value)
|
||
self.refresh_button = QPushButton("刷新")
|
||
|
||
filter_layout = QHBoxLayout()
|
||
filter_layout.addWidget(QLabel("批次"))
|
||
filter_layout.addWidget(self.batch_filter, 2)
|
||
filter_layout.addWidget(QLabel("店铺"))
|
||
filter_layout.addWidget(self.shop_filter, 1)
|
||
filter_layout.addWidget(QLabel("商品ID"))
|
||
filter_layout.addWidget(self.item_filter, 1)
|
||
filter_layout.addWidget(QLabel("商品状态"))
|
||
filter_layout.addWidget(self.product_status_filter, 1)
|
||
filter_layout.addWidget(QLabel("更新状态"))
|
||
filter_layout.addWidget(self.status_filter, 1)
|
||
filter_layout.addWidget(self.refresh_button)
|
||
|
||
self.summary_label = QLabel("任务 0 条")
|
||
self.batch_progress_label = _build_batch_progress_overview("applyBatchProgressOverview")
|
||
self.risk_label = QLabel("可先点击「检查本轮更新」确认当前筛选范围;点击「开始更新」后会再次确认并按批提交线上。")
|
||
(
|
||
self.empty_state_card,
|
||
self.empty_state_label,
|
||
self.empty_state_button,
|
||
) = _build_empty_state_card("applyEmptyStateCard")
|
||
if self.open_accounts_callback is not None:
|
||
self.empty_state_button.clicked.connect(self.open_accounts_callback)
|
||
self.task_table = QTableView()
|
||
self.model = ApplyTaskTableModel(self.task_table)
|
||
self.task_table.setModel(self.model)
|
||
self.task_table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||
self.task_table.setSelectionMode(QAbstractItemView.SingleSelection)
|
||
self.task_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||
self.task_table.setContextMenuPolicy(Qt.CustomContextMenu)
|
||
self.task_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||
self.task_table.verticalHeader().setVisible(False)
|
||
self.run_log_view = QPlainTextEdit()
|
||
self.run_log_view.setObjectName("applyRunLogView")
|
||
self.run_log_view.setReadOnly(True)
|
||
self.run_log_view.setMaximumHeight(128)
|
||
self.run_log_view.setPlaceholderText("运行日志")
|
||
|
||
self.preview_update_button = QPushButton("检查本轮更新")
|
||
self.preview_update_button.setObjectName("previewUpdateButton")
|
||
self.start_update_button = QPushButton("开始更新")
|
||
self.start_update_button.setObjectName("startUpdateButton")
|
||
self.start_update_button.setMinimumWidth(118)
|
||
self.start_update_button.setStyleSheet(
|
||
_warning_outline_button_style("startUpdateButton")
|
||
)
|
||
self.stop_update_button = QPushButton("停止")
|
||
self.reset_update_button = QPushButton("重置更新状态")
|
||
self.reset_update_button.setObjectName("resetUpdateButton")
|
||
self.reset_update_button.setVisible(False)
|
||
self.write_back_button = QPushButton("回写结果到 Excel")
|
||
self.stop_update_button.setEnabled(False)
|
||
self.write_back_button.setEnabled(False)
|
||
self.update_mode_combo = QComboBox()
|
||
self.update_mode_combo.setObjectName("applyUpdateModeCombo")
|
||
self.update_mode_combo.setToolTip("选择本轮要更新标题、封面,或同时更新标题和封面")
|
||
for label, value in self.UPDATE_MODE_ITEMS:
|
||
self.update_mode_combo.addItem(label, value)
|
||
self._set_combo_by_data(
|
||
self.update_mode_combo,
|
||
self._shopee_update_config().get("update_mode", "title"),
|
||
)
|
||
|
||
action_layout = QHBoxLayout()
|
||
action_layout.addWidget(QLabel("更新内容"))
|
||
action_layout.addWidget(self.update_mode_combo)
|
||
action_layout.addSpacing(16)
|
||
action_layout.addWidget(self.preview_update_button)
|
||
action_layout.addWidget(self.start_update_button)
|
||
action_layout.addWidget(self.stop_update_button)
|
||
action_layout.addStretch(1)
|
||
action_layout.addWidget(self.write_back_button)
|
||
|
||
layout = QVBoxLayout(self)
|
||
layout.setContentsMargins(18, 18, 18, 18)
|
||
layout.addLayout(filter_layout)
|
||
layout.addWidget(self.risk_label)
|
||
layout.addWidget(self.summary_label)
|
||
layout.addWidget(self.batch_progress_label)
|
||
layout.addWidget(self.empty_state_card)
|
||
layout.addWidget(self.task_table, 1)
|
||
layout.addWidget(QLabel("运行日志"))
|
||
layout.addWidget(self.run_log_view)
|
||
layout.addLayout(action_layout)
|
||
|
||
self.batch_filter.currentIndexChanged.connect(self.refresh_tasks)
|
||
self.shop_filter.currentIndexChanged.connect(self.refresh_tasks)
|
||
self.item_filter.textChanged.connect(self.refresh_tasks)
|
||
self.product_status_filter.currentIndexChanged.connect(self.refresh_tasks)
|
||
self.status_filter.currentIndexChanged.connect(self.refresh_tasks)
|
||
self.refresh_button.clicked.connect(self.refresh_tasks)
|
||
self.preview_update_button.clicked.connect(self.preview_update)
|
||
self.start_update_button.clicked.connect(self.start_update)
|
||
self.update_mode_combo.currentIndexChanged.connect(self._on_update_mode_changed)
|
||
self.stop_update_button.clicked.connect(self.stop_update)
|
||
self.reset_update_button.clicked.connect(self.reset_apply_status)
|
||
self.task_table.customContextMenuRequested.connect(self.show_task_context_menu)
|
||
self.write_back_button.clicked.connect(self.write_back_results)
|
||
|
||
self.refresh_tasks()
|
||
self._show_current_run_log_empty()
|
||
|
||
def _set_status(self, message, level=None):
|
||
_emit_status(self.status_callback, message, level=level)
|
||
|
||
def refresh_tasks(self, checked=False):
|
||
try:
|
||
db.init_db(self.db_path)
|
||
batches = db.list_batches(path=self.db_path)
|
||
account_rows = accounts.list_accounts(path=self.db_path, config=self.config)
|
||
selected_batch = self.batch_filter.currentData()
|
||
selected_shop = self.shop_filter.currentData()
|
||
selected_status = self.status_filter.currentData() or "generated"
|
||
selected_product_status = self.product_status_filter.currentData() or "all"
|
||
item_query = self.item_filter.text().strip()
|
||
self._populate_batch_filter(batches, selected_batch)
|
||
selected_batch = self.batch_filter.currentData()
|
||
all_batch_tasks = db.list_tasks(batch_id=selected_batch, path=self.db_path)
|
||
batch_tasks = [
|
||
task for task in all_batch_tasks
|
||
if self._is_update_task(task)
|
||
]
|
||
self._populate_shop_filter(batch_tasks, account_rows, selected_shop)
|
||
selected_shop = self.shop_filter.currentData()
|
||
filtered_tasks = [
|
||
task for task in batch_tasks
|
||
if self._matches_shop(task, selected_shop)
|
||
and self._matches_item(task, item_query)
|
||
and self._matches_product_status(task, selected_product_status)
|
||
and self._matches_status(task, selected_status)
|
||
]
|
||
except Exception as exc:
|
||
self.model.set_tasks([], [])
|
||
self.summary_label.setText("任务读取失败")
|
||
_set_batch_progress_overview(self.batch_progress_label, [])
|
||
_set_empty_state(self.empty_state_card, self.empty_state_label, self.empty_state_button)
|
||
self._set_status(f"更新任务读取失败:{exc}")
|
||
return
|
||
self.model.set_tasks(filtered_tasks, account_rows)
|
||
self.summary_label.setText(
|
||
f"任务 {len(filtered_tasks)}/{len(batch_tasks)} 条"
|
||
)
|
||
_set_batch_progress_overview(self.batch_progress_label, all_batch_tasks)
|
||
self._update_empty_state(batch_tasks, filtered_tasks, account_rows)
|
||
self._update_write_back_button()
|
||
|
||
def _update_empty_state(self, batch_tasks, filtered_tasks, account_rows):
|
||
if not account_rows:
|
||
_set_empty_state(
|
||
self.empty_state_card,
|
||
self.empty_state_label,
|
||
self.empty_state_button,
|
||
"第一步:前往『账号管理』配置并登录账号,再回到③更新蝦皮。",
|
||
self.open_accounts_callback is not None,
|
||
)
|
||
return
|
||
if not batch_tasks:
|
||
_set_empty_state(
|
||
self.empty_state_card,
|
||
self.empty_state_label,
|
||
self.empty_state_button,
|
||
"还没有可更新任务。请先在②AI生成完成新标题或新封面。",
|
||
)
|
||
return
|
||
if not filtered_tasks:
|
||
_set_empty_state(
|
||
self.empty_state_card,
|
||
self.empty_state_label,
|
||
self.empty_state_button,
|
||
"当前筛选没有可更新任务,请调整批次、店铺、商品ID或状态筛选。",
|
||
)
|
||
return
|
||
_set_empty_state(self.empty_state_card, self.empty_state_label, self.empty_state_button)
|
||
|
||
def start_update(self, checked=False):
|
||
self._start_update(dry_run=False)
|
||
|
||
def preview_update(self, checked=False):
|
||
self._start_update(dry_run=True)
|
||
|
||
def _start_update(self, dry_run=False):
|
||
if self.apply_thread is not None:
|
||
self._set_status("更新正在进行...")
|
||
return
|
||
base_candidates = self._update_candidates()
|
||
if not base_candidates:
|
||
self._set_status("当前筛选结果没有可更新任务")
|
||
return
|
||
update_cfg = self._shopee_update_config()
|
||
update_mode = self._current_update_mode()
|
||
update_plan = product_status.build_apply_plan(base_candidates, update_mode)
|
||
executable_tasks = update_plan["executable"]
|
||
if not executable_tasks:
|
||
preflight_error = self._update_preflight_error(update_plan, update_mode)
|
||
title = (
|
||
"商品状态不允许更新"
|
||
if update_plan["status_scope_excluded"]
|
||
else "更新内容未生成"
|
||
)
|
||
QMessageBox.warning(self, title, preflight_error)
|
||
self._set_status(preflight_error.replace("\n", " "))
|
||
return
|
||
dry_run = bool(dry_run)
|
||
answer = QMessageBox.question(
|
||
self,
|
||
"确认检查本轮更新" if dry_run else "确认开始更新",
|
||
self._confirmation_message(
|
||
executable_tasks,
|
||
dry_run=dry_run,
|
||
content_plan=update_plan,
|
||
),
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
QMessageBox.No,
|
||
)
|
||
if answer != QMessageBox.Yes:
|
||
self._set_status("已取消检查本轮更新" if dry_run else "已取消开始更新")
|
||
return
|
||
current_plan = product_status.build_apply_plan(
|
||
self._update_candidates(),
|
||
update_mode,
|
||
)
|
||
if current_plan["fingerprint"] != update_plan["fingerprint"]:
|
||
self._set_status("当前任务数据已变化,请重新开始更新")
|
||
return
|
||
executable_tasks = current_plan["executable"]
|
||
if not executable_tasks:
|
||
preflight_error = self._update_preflight_error(current_plan, update_mode)
|
||
self._set_status(preflight_error.replace("\n", " "))
|
||
return
|
||
batch_size = max(1, int(update_cfg.get("max_items_per_run", 1) or 1))
|
||
worker = ApplyWorker(
|
||
executable_tasks,
|
||
db_path=self.db_path,
|
||
config=self.config,
|
||
dry_run=dry_run,
|
||
update_mode=update_mode,
|
||
max_parallel_accounts=max(
|
||
1,
|
||
int(update_cfg.get("max_parallel_accounts", 1) or 1),
|
||
),
|
||
batch_size=batch_size,
|
||
diagnostic_log_dir=diagnostics.DEFAULT_LOG_DIR,
|
||
product_status_counts=current_plan["status_counts"],
|
||
status_scope_excluded=current_plan["status_scope_excluded"],
|
||
content_scope_excluded=current_plan["content_scope_excluded"],
|
||
apply_plan_fingerprint=current_plan["fingerprint"],
|
||
)
|
||
worker.progress.connect(self._on_apply_progress)
|
||
worker.row_updated.connect(self._on_apply_row_updated)
|
||
worker.log.connect(self._on_apply_log)
|
||
worker.failed.connect(self._on_apply_failed)
|
||
worker.finished.connect(self._on_apply_finished)
|
||
worker.cancelled.connect(self._on_apply_cancelled)
|
||
thread = run_worker(worker, thread_name="ApplyWorker", start=False)
|
||
thread.finished.connect(lambda: self._forget_apply_thread(thread))
|
||
self.apply_worker = worker
|
||
self.apply_thread = thread
|
||
self._set_apply_running(True)
|
||
self._reset_run_log(
|
||
executable_tasks,
|
||
dry_run=dry_run,
|
||
batch_size=batch_size,
|
||
content_plan=current_plan,
|
||
)
|
||
skip_status = self._content_skip_status(current_plan)
|
||
if dry_run:
|
||
self._set_status(f"开始检查本轮更新:{len(executable_tasks)} 条{skip_status}")
|
||
else:
|
||
self._set_status(
|
||
f"开始更新:{len(executable_tasks)} 条,按每批最多 {batch_size} 条执行{skip_status}"
|
||
)
|
||
thread.start()
|
||
|
||
def stop_update(self, checked=False):
|
||
if self.apply_worker is not None:
|
||
self.apply_worker.cancel()
|
||
self._set_status("正在停止更新...")
|
||
|
||
def write_back_results(self, checked=False):
|
||
batch_ids = self._active_batch_ids()
|
||
if not batch_ids:
|
||
self._set_status("没有可回写结果的批次")
|
||
return
|
||
self._start_result_write_back(batch_ids, auto=False)
|
||
|
||
def show_task_context_menu(self, position):
|
||
index = self.task_table.indexAt(position)
|
||
if not index.isValid():
|
||
return
|
||
self.task_table.selectRow(index.row())
|
||
self.task_table.setCurrentIndex(index)
|
||
task = self.model.task_at(index.row())
|
||
if task is None:
|
||
return
|
||
menu = QMenu(self.task_table)
|
||
reset_action = menu.addAction("重置更新状态")
|
||
reset_action.setEnabled(
|
||
self.apply_thread is None
|
||
and self.result_write_back_thread is None
|
||
and getattr(task, "status", "") != "running"
|
||
)
|
||
reset_action.triggered.connect(self.reset_apply_status)
|
||
menu.addSeparator()
|
||
delete_action = menu.addAction("删除本条记录")
|
||
delete_action.setEnabled(
|
||
self.apply_thread is None
|
||
and self.result_write_back_thread is None
|
||
and getattr(task, "status", "") != "running"
|
||
)
|
||
delete_action.triggered.connect(self.delete_selected_task)
|
||
menu.exec(self.task_table.viewport().mapToGlobal(position))
|
||
|
||
def delete_selected_task(self, checked=False):
|
||
if self.apply_thread is not None or self.result_write_back_thread is not None:
|
||
self._set_status("更新或回写正在进行,不能删除记录", level="warning")
|
||
return
|
||
task = self._selected_task()
|
||
if task is None:
|
||
self._set_status("请选择要删除的记录", level="warning")
|
||
return
|
||
if getattr(task, "status", "") == "running":
|
||
self._set_status("任务正在处理,不能删除", level="warning")
|
||
return
|
||
batch = db.get_batch(task.batch_id, path=self.db_path)
|
||
batch_text = self._batch_label(batch) if batch is not None else task.batch_id
|
||
lines = [
|
||
"确定要删除本条本地记录吗?",
|
||
f"商品ID:{task.item_id}",
|
||
f"店铺:{self.model.account_name_for(task)}({task.alias})",
|
||
f"所属批次:{batch_text}",
|
||
]
|
||
if int(getattr(task, "committed", 0) or 0) == 1:
|
||
lines.extend(
|
||
[
|
||
"",
|
||
"该商品已提交过蝦皮。本次仅删除本地记录,不会回滚蝦皮线上商品。",
|
||
]
|
||
)
|
||
lines.extend(
|
||
[
|
||
"",
|
||
"删除后,该记录不会出现在①、②、③的列表、筛选、采集、生成、更新或回写范围。",
|
||
"不会删除原 Excel、本地图片或账号 Chrome 数据。",
|
||
]
|
||
)
|
||
answer = QMessageBox.question(
|
||
self,
|
||
"删除本条记录",
|
||
"\n".join(lines),
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
QMessageBox.No,
|
||
)
|
||
if answer != QMessageBox.Yes:
|
||
self._set_status("已取消删除本条记录")
|
||
return
|
||
try:
|
||
result = db.delete_task(
|
||
task.id,
|
||
reason="用户在更新蝦皮页删除单条记录",
|
||
path=self.db_path,
|
||
)
|
||
except Exception as exc:
|
||
QMessageBox.warning(self, "删除本条记录", str(exc))
|
||
self._set_status(f"删除本条记录失败:{exc}", level="danger")
|
||
return
|
||
self.refresh_tasks()
|
||
if self.refresh_workflow_callback is not None:
|
||
self.refresh_workflow_callback()
|
||
message = f"已删除本地记录:商品 {result['item_id']}"
|
||
self._set_status(message, level="success")
|
||
QMessageBox.information(self, "删除本条记录", message)
|
||
|
||
def reset_apply_status(self, checked=False):
|
||
if self.apply_thread is not None or self.result_write_back_thread is not None:
|
||
self._set_status("更新或回写正在进行,不能重置")
|
||
return
|
||
task = self._selected_task()
|
||
if task is None:
|
||
self._set_status("请选择要重置更新状态的任务")
|
||
return
|
||
if not (getattr(task, "new_title", None) or getattr(task, "new_cover_path", None)):
|
||
self._set_status("选中任务没有新标题或新封面,不能重置为可更新")
|
||
return
|
||
lines = [
|
||
"确定重置当前选中任务的本地更新状态吗?",
|
||
"",
|
||
f"商品ID:{task.item_id}",
|
||
f"店铺:{self.model.account_name_for(task)}",
|
||
"",
|
||
"将保留新标题和新封面路径,只把本地状态退回可更新。",
|
||
"不会触碰蝦皮,也不会自动回写 Excel。",
|
||
]
|
||
if getattr(task, "committed", 0):
|
||
lines.extend([
|
||
"",
|
||
"注意:该记录已经提交过线上。本地重置不会回滚蝦皮,重复更新会再次提交线上。",
|
||
])
|
||
answer = QMessageBox.question(
|
||
self,
|
||
"重置更新状态",
|
||
"\n".join(lines),
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
QMessageBox.No,
|
||
)
|
||
if answer != QMessageBox.Yes:
|
||
self._set_status("已取消重置更新状态")
|
||
return
|
||
try:
|
||
db.reset_apply_status(task.id, path=self.db_path)
|
||
message = (
|
||
"action=reset_apply_status step=db_write result=success "
|
||
f"detail=退回可更新 task_id={task.id}"
|
||
)
|
||
run_id = _write_reset_run_log(self.db_path, task, "reset_apply_status", message)
|
||
except Exception as exc:
|
||
QMessageBox.warning(self, "重置更新状态", str(exc))
|
||
self._set_status(f"重置更新状态失败:{exc}")
|
||
return
|
||
self.refresh_tasks()
|
||
self._append_run_log(message)
|
||
self._set_status(
|
||
f"已重置更新状态:商品 {task.item_id},run_id={run_id}"
|
||
)
|
||
|
||
def _selected_task(self):
|
||
index = self.task_table.currentIndex()
|
||
if index.isValid():
|
||
return self.model.task_at(index.row())
|
||
if self.model.rowCount() > 0:
|
||
return self.model.task_at(0)
|
||
return None
|
||
|
||
def _is_update_candidate_task(self, task):
|
||
return (
|
||
getattr(task, "stage", None) == "generated"
|
||
and getattr(task, "status", None) in {"success", "pending", "failed"}
|
||
)
|
||
|
||
def _update_candidates(self):
|
||
return [
|
||
task
|
||
for task in self.model.tasks
|
||
if self._is_update_candidate_task(task)
|
||
]
|
||
|
||
def _populate_batch_filter(self, batches, selected_batch):
|
||
had_previous_items = self.batch_filter.count() > 0
|
||
self.batch_filter.blockSignals(True)
|
||
self.batch_filter.clear()
|
||
self.batch_filter.addItem("全部批次", None)
|
||
for batch in batches:
|
||
self.batch_filter.addItem(self._batch_label(batch), batch.id)
|
||
self.batch_filter.setCurrentIndex(
|
||
_batch_filter_current_index(
|
||
self.batch_filter,
|
||
batches,
|
||
selected_batch,
|
||
had_previous_items,
|
||
)
|
||
)
|
||
self.batch_filter.blockSignals(False)
|
||
|
||
def _populate_shop_filter(self, tasks, account_rows, selected_shop):
|
||
account_by_alias = {
|
||
str(account.alias).strip(): account
|
||
for account in account_rows
|
||
if str(account.alias).strip()
|
||
}
|
||
aliases = []
|
||
for task in tasks:
|
||
alias = str(task.alias).strip()
|
||
if alias and alias not in aliases:
|
||
aliases.append(alias)
|
||
previous = selected_shop if selected_shop in aliases else None
|
||
self.shop_filter.blockSignals(True)
|
||
self.shop_filter.clear()
|
||
self.shop_filter.addItem("全部店铺", None)
|
||
for alias in sorted(aliases, key=lambda value: self._shop_label(value, account_by_alias)):
|
||
self.shop_filter.addItem(self._shop_label(alias, account_by_alias), alias)
|
||
index = self.shop_filter.findData(previous)
|
||
self.shop_filter.setCurrentIndex(index if index >= 0 else 0)
|
||
self.shop_filter.blockSignals(False)
|
||
|
||
def _batch_label(self, batch):
|
||
source_files = batch.source_files
|
||
first_file = os.path.basename(source_files[0]) if source_files else batch.id
|
||
return f"{batch.created_at} · {first_file}"
|
||
|
||
def _shop_label(self, alias, account_by_alias):
|
||
account = account_by_alias.get(alias)
|
||
if account is not None:
|
||
return f"{account.account_name} ({alias})"
|
||
return alias
|
||
|
||
def _status_label(self):
|
||
return self.status_filter.currentText() or "已生成"
|
||
|
||
def _batch_filter_label(self):
|
||
return self.batch_filter.currentText() or "全部批次"
|
||
|
||
def _shop_filter_label(self):
|
||
return self.shop_filter.currentText() or "全部店铺"
|
||
|
||
def _item_filter_label(self):
|
||
return self.item_filter.text().strip() or "全部商品"
|
||
|
||
def _product_status_filter_label(self):
|
||
return self.product_status_filter.currentText() or "全部商品状态"
|
||
|
||
def _is_update_task(self, task):
|
||
if task.stage in {"generated", "applied"}:
|
||
return True
|
||
return bool((task.new_title or task.new_cover_path) and task.status in {"failed", "skipped"})
|
||
|
||
def _matches_shop(self, task, selected_shop):
|
||
return selected_shop is None or str(task.alias).strip() == selected_shop
|
||
|
||
def _matches_item(self, task, item_query):
|
||
if not item_query:
|
||
return True
|
||
return item_query in str(getattr(task, "item_id", ""))
|
||
|
||
def _matches_product_status(self, task, selected_product_status):
|
||
if selected_product_status in (None, "all"):
|
||
return True
|
||
return product_status.raw_status(getattr(task, "product_status", None)) == selected_product_status
|
||
|
||
def _matches_status(self, task, selected_status):
|
||
if selected_status in (None, "all"):
|
||
return True
|
||
if selected_status == "generated":
|
||
return task.stage == "generated" and task.status in {"success", "pending"}
|
||
if selected_status == "failed":
|
||
return task.status == "failed"
|
||
if selected_status == "applied":
|
||
return task.stage == "applied"
|
||
if selected_status == "skipped":
|
||
return task.status == "skipped"
|
||
return True
|
||
|
||
def _confirmation_message(self, tasks, dry_run=False, content_plan=None):
|
||
update_cfg = self._shopee_update_config()
|
||
update_mode = self._current_update_mode()
|
||
update_mode_text = self._update_mode_label(update_mode)
|
||
cover_warning = "\n本轮将更新线上封面,请确认封面内容无误。" if appconfig.update_mode_includes_cover(update_mode) else ""
|
||
batch_size = max(1, int(update_cfg.get("max_items_per_run", 1) or 1))
|
||
batch_count = (len(tasks) + batch_size - 1) // batch_size if tasks else 0
|
||
max_parallel_accounts = max(
|
||
1,
|
||
int(update_cfg.get("max_parallel_accounts", 1) or 1),
|
||
)
|
||
parallel_text = (
|
||
f"最多 {max_parallel_accounts} 个账号"
|
||
if max_parallel_accounts > 1
|
||
else "关闭"
|
||
)
|
||
intro = (
|
||
"即将检查当前筛选结果。\n\n"
|
||
if dry_run
|
||
else "即将按当前筛选结果分批更新蝦皮线上商品。\n\n"
|
||
)
|
||
skipped_text = self._content_skip_text(content_plan)
|
||
skipped_block = f"\n{skipped_text}\n" if skipped_text else ""
|
||
return (
|
||
intro
|
||
+ f"批次:{self._batch_filter_label()}\n"
|
||
+ f"店铺:{self._shop_filter_label()}\n"
|
||
+ f"商品ID:{self._item_filter_label()}\n"
|
||
+ f"商品状态:{self._product_status_filter_label()}\n"
|
||
+ f"更新状态:{self._status_label()}\n"
|
||
+ f"更新内容:{update_mode_text}\n"
|
||
+ f"任务数:{len(tasks)}\n"
|
||
+ f"预计批次:{batch_count}\n"
|
||
+ skipped_block
|
||
+ "\n"
|
||
+ "执行设置:"
|
||
+ f"每批最大更新条数={batch_size},"
|
||
+ "成功后自动关闭程序新开编辑页,"
|
||
+ f"多账号并行={parallel_text}\n\n"
|
||
+ cover_warning
|
||
+ ("\n\n" if cover_warning else "")
|
||
+ (
|
||
"检查只写运行日志,不打开蝦皮、不点击「更新」、不改任务状态。"
|
||
if dry_run
|
||
else f"确认后会打开商品编辑页、替换标题/允许时替换封面,并按每批最多 {batch_size} 条点击「更新」提交线上;点击停止后不再开始下一条或下一批。"
|
||
)
|
||
)
|
||
|
||
def _shopee_update_config(self):
|
||
return appconfig.shopee_update_config(self.config)
|
||
|
||
def _current_update_mode(self):
|
||
return appconfig.normalize_update_mode(
|
||
self.update_mode_combo.currentData(),
|
||
allow_cover_update=False,
|
||
)
|
||
|
||
def _update_mode_label(self, mode):
|
||
mode = appconfig.normalize_update_mode(mode)
|
||
return {
|
||
"title": "只更新标题",
|
||
"cover": "只更新封面",
|
||
"title_cover": "更新标题和封面",
|
||
}.get(mode, "只更新标题")
|
||
|
||
def _set_combo_by_data(self, combo, value):
|
||
index = combo.findData(value)
|
||
combo.setCurrentIndex(index if index >= 0 else 0)
|
||
|
||
def _on_update_mode_changed(self, index=None):
|
||
previous = self._shopee_update_config().get("update_mode", "title")
|
||
if self._save_update_mode_setting(show_status=True):
|
||
return
|
||
self.update_mode_combo.blockSignals(True)
|
||
self._set_combo_by_data(self.update_mode_combo, previous)
|
||
self.update_mode_combo.blockSignals(False)
|
||
|
||
def _save_update_mode_setting(self, show_status=True):
|
||
update_mode = self._current_update_mode()
|
||
update_cfg = self._shopee_update_config()
|
||
update_cfg["update_mode"] = update_mode
|
||
payload = {
|
||
key: value
|
||
for key, value in self.config.items()
|
||
if key not in {"config_path", "ai_models_path", "cmhub_config_path", "data_dir"}
|
||
}
|
||
payload["shopee_update"] = update_cfg
|
||
try:
|
||
saved = appconfig.save_config(payload, path=self.config_path)
|
||
except Exception as exc:
|
||
self._set_status(f"更新内容设置保存失败:{exc}")
|
||
return False
|
||
internal = {
|
||
key: value
|
||
for key, value in self.config.items()
|
||
if key in {"config_path", "ai_models_path", "cmhub_config_path", "data_dir"}
|
||
}
|
||
self.config.clear()
|
||
self.config.update(saved)
|
||
self.config.update(internal)
|
||
if self.config_path != appconfig.CONFIG_PATH:
|
||
self.config["config_path"] = self.config_path
|
||
if show_status:
|
||
self._set_status(f"更新内容已设置为:{self._update_mode_label(update_mode)}")
|
||
return True
|
||
|
||
def _partition_update_content_tasks(self, tasks, update_mode):
|
||
missing_title = [
|
||
task for task in tasks
|
||
if appconfig.update_mode_includes_title(update_mode)
|
||
and not str(getattr(task, "new_title", "") or "").strip()
|
||
]
|
||
missing_cover = [
|
||
task for task in tasks
|
||
if appconfig.update_mode_includes_cover(update_mode)
|
||
and not str(getattr(task, "new_cover_path", "") or "").strip()
|
||
]
|
||
skipped = {id(task) for task in missing_title + missing_cover}
|
||
return {
|
||
"executable": [task for task in tasks if id(task) not in skipped],
|
||
"missing_title": missing_title,
|
||
"missing_cover": missing_cover,
|
||
}
|
||
|
||
def _update_preflight_error(self, update_plan, update_mode):
|
||
if update_plan["executable"]:
|
||
return None
|
||
lines = []
|
||
status_text = self._status_skip_text(update_plan)
|
||
if status_text:
|
||
lines.append("当前筛选结果没有状态正常且可更新的商品。")
|
||
lines.append(status_text)
|
||
lines.append("未上架、审核中或状态未知的商品为安全起见不会更新。")
|
||
content_error = self._update_content_error(update_plan, update_mode)
|
||
if content_error:
|
||
if lines:
|
||
lines.append("")
|
||
lines.append(content_error)
|
||
return "\n".join(lines) or "当前筛选结果没有可更新任务。"
|
||
|
||
def _update_content_error(self, tasks_or_plan, update_mode):
|
||
if isinstance(tasks_or_plan, dict):
|
||
content_plan = tasks_or_plan
|
||
else:
|
||
content_plan = self._partition_update_content_tasks(tasks_or_plan, update_mode)
|
||
if content_plan["executable"]:
|
||
return None
|
||
lines = []
|
||
mode_text = self._update_mode_label(update_mode)
|
||
missing_title = content_plan["missing_title"]
|
||
missing_cover = content_plan["missing_cover"]
|
||
if missing_title:
|
||
lines.append(f"当前筛选结果中有 {len(missing_title)} 条缺少新标题,不能执行“{mode_text}”。")
|
||
lines.append("请先回到②AI生成选择“只生成标题”或“生成标题和封面”。")
|
||
lines.append(f"示例商品ID:{self._sample_item_ids(missing_title)}")
|
||
if missing_cover:
|
||
if lines:
|
||
lines.append("")
|
||
lines.append(f"当前筛选结果中有 {len(missing_cover)} 条缺少新封面,不能执行“{mode_text}”。")
|
||
lines.append("请先回到②AI生成选择“只生成封面”或“生成标题和封面”。")
|
||
lines.append(f"示例商品ID:{self._sample_item_ids(missing_cover)}")
|
||
return "\n".join(lines)
|
||
|
||
def _content_skip_text(self, content_plan):
|
||
if not content_plan:
|
||
return ""
|
||
status_text = self._status_skip_text(content_plan)
|
||
missing_title = content_plan.get("missing_title", [])
|
||
missing_cover = content_plan.get("missing_cover", [])
|
||
skipped_count = len({id(task) for task in missing_title + missing_cover})
|
||
lines = [status_text] if status_text else []
|
||
if skipped_count:
|
||
lines.append(f"本轮将跳过 {skipped_count} 条缺少所选更新内容的记录:")
|
||
if missing_title:
|
||
lines.append(
|
||
f"缺少新标题:{len(missing_title)} 条(示例商品ID:{self._sample_item_ids(missing_title)})"
|
||
)
|
||
if missing_cover:
|
||
lines.append(
|
||
f"缺少新封面:{len(missing_cover)} 条(示例商品ID:{self._sample_item_ids(missing_cover)})"
|
||
)
|
||
return "\n".join(lines)
|
||
|
||
def _content_skip_status(self, content_plan):
|
||
if not content_plan:
|
||
return ""
|
||
status_skipped = int(content_plan.get("status_scope_excluded", 0) or 0)
|
||
missing_title = content_plan.get("missing_title", [])
|
||
missing_cover = content_plan.get("missing_cover", [])
|
||
skipped_count = len({id(task) for task in missing_title + missing_cover})
|
||
if not skipped_count and not status_skipped:
|
||
return ""
|
||
parts = []
|
||
if status_skipped:
|
||
parts.append(f"状态异常 {status_skipped}")
|
||
if skipped_count:
|
||
parts.append(
|
||
f"缺标题 {len(missing_title)} / 缺封面 {len(missing_cover)}"
|
||
)
|
||
return ",跳过 {total} 条({details})".format(
|
||
total=status_skipped + skipped_count,
|
||
details=";".join(parts),
|
||
)
|
||
|
||
def _status_skip_text(self, update_plan):
|
||
if not update_plan:
|
||
return ""
|
||
rows = [
|
||
("未上架", update_plan.get("unlisted", [])),
|
||
("审核中", update_plan.get("reviewing", [])),
|
||
("状态未知", update_plan.get("unknown", [])),
|
||
]
|
||
skipped = sum(len(tasks) for _label, tasks in rows)
|
||
if not skipped:
|
||
return ""
|
||
lines = [f"本轮将排除 {skipped} 条商品状态异常记录:"]
|
||
for label, tasks in rows:
|
||
if tasks:
|
||
lines.append(
|
||
f"{label}:{len(tasks)} 条(示例商品ID:{self._sample_item_ids(tasks)})"
|
||
)
|
||
return "\n".join(lines)
|
||
|
||
def _sample_item_ids(self, tasks):
|
||
values = [
|
||
str(getattr(task, "item_id", "") or "").strip()
|
||
for task in tasks[:5]
|
||
]
|
||
values = [value for value in values if value]
|
||
suffix = f" 等 {len(tasks)} 条" if len(tasks) > 5 else ""
|
||
return "、".join(values) + suffix if values else "无"
|
||
|
||
def _set_apply_running(self, running):
|
||
self.preview_update_button.setEnabled(not running)
|
||
self.start_update_button.setEnabled(not running)
|
||
self.stop_update_button.setEnabled(running)
|
||
self.reset_update_button.setEnabled(not running)
|
||
self.refresh_button.setEnabled(not running)
|
||
self.batch_filter.setEnabled(not running)
|
||
self.shop_filter.setEnabled(not running)
|
||
self.item_filter.setEnabled(not running)
|
||
self.product_status_filter.setEnabled(not running)
|
||
self.status_filter.setEnabled(not running)
|
||
self.update_mode_combo.setEnabled(not running)
|
||
self._update_write_back_button()
|
||
|
||
def _set_result_write_back_running(self, running):
|
||
self.preview_update_button.setEnabled(not running)
|
||
self.start_update_button.setEnabled(not running)
|
||
self.reset_update_button.setEnabled(not running)
|
||
self.refresh_button.setEnabled(not running)
|
||
self.batch_filter.setEnabled(not running)
|
||
self.shop_filter.setEnabled(not running)
|
||
self.item_filter.setEnabled(not running)
|
||
self.product_status_filter.setEnabled(not running)
|
||
self.status_filter.setEnabled(not running)
|
||
self.update_mode_combo.setEnabled(not running)
|
||
self.write_back_button.setEnabled(False if running else bool(self._active_batch_ids()))
|
||
|
||
def _forget_apply_thread(self, thread):
|
||
if self.apply_thread is thread:
|
||
self.apply_thread = None
|
||
self.apply_worker = None
|
||
|
||
def _forget_result_write_back_thread(self, thread):
|
||
if self.result_write_back_thread is thread:
|
||
self.result_write_back_thread = None
|
||
self.result_write_back_worker = None
|
||
self._update_write_back_button()
|
||
|
||
def _on_apply_progress(self, payload):
|
||
self._set_status("更新进度:" + self._apply_progress_text(payload))
|
||
|
||
def _on_apply_log(self, message):
|
||
self._append_run_log(message)
|
||
self._set_status(message)
|
||
|
||
def _on_apply_row_updated(self, task_id, fields):
|
||
self.refresh_tasks()
|
||
|
||
def _on_apply_failed(self, task_id, error):
|
||
self._set_status(f"任务 {task_id} 更新失败:{error}")
|
||
|
||
def _on_apply_finished(self, payload):
|
||
self._set_apply_running(False)
|
||
self.refresh_tasks()
|
||
if payload.get("blocked"):
|
||
self._show_apply_blocked(payload)
|
||
return
|
||
self.last_apply_summary = dict(payload)
|
||
prefix = "检查本轮更新完成:" if payload.get("dry_run") else "更新完成:"
|
||
message = prefix + self._apply_progress_text(payload)
|
||
batch_ids = payload.get("batch_ids") or self._active_batch_ids()
|
||
if (not payload.get("dry_run")) and payload.get("done", 0) > 0 and batch_ids:
|
||
if self._start_result_write_back(
|
||
batch_ids,
|
||
auto=True,
|
||
apply_summary=payload,
|
||
):
|
||
self._set_status(f"{message},正在自动回写结果到 Excel...")
|
||
return
|
||
self._set_status(message)
|
||
self._show_apply_summary(payload)
|
||
|
||
def _on_apply_cancelled(self, payload):
|
||
self._set_apply_running(False)
|
||
self.refresh_tasks()
|
||
self._set_status("更新已停止:" + self._apply_progress_text(payload))
|
||
|
||
def _apply_progress_text(self, payload):
|
||
success_label = "可更新" if payload.get("dry_run") else "成功"
|
||
return "完成{done}/{total},{success_label}{applied},略过{skipped},失败{failed}".format(
|
||
done=payload.get("done", 0),
|
||
total=payload.get("total", 0),
|
||
success_label=success_label,
|
||
applied=payload.get("applied", 0),
|
||
skipped=payload.get("skipped", 0),
|
||
failed=payload.get("failed", 0),
|
||
)
|
||
|
||
def _append_run_log(self, message):
|
||
self.run_log_view.appendPlainText(str(message))
|
||
scroll_bar = self.run_log_view.verticalScrollBar()
|
||
scroll_bar.setValue(scroll_bar.maximum())
|
||
|
||
def _show_current_run_log_empty(self):
|
||
self.run_log_view.setPlainText("本轮日志会在开始运行后显示")
|
||
|
||
def _reset_run_log(self, tasks, dry_run=False, batch_size=1, content_plan=None):
|
||
self.run_log_view.clear()
|
||
action = "检查" if dry_run else "更新"
|
||
mode_text = self._update_mode_label(self._current_update_mode())
|
||
self._append_run_log(
|
||
f"本轮{action}开始:任务 {len(tasks)} 条,更新内容:{mode_text},每批 {batch_size} 条"
|
||
)
|
||
skip_status = self._content_skip_status(content_plan)
|
||
if skip_status:
|
||
excluded_label = (
|
||
"预检排除记录"
|
||
if content_plan and content_plan.get("status_scope_excluded")
|
||
else "缺失记录"
|
||
)
|
||
self._append_run_log(
|
||
f"本轮预检:可执行 {len(tasks)} 条{skip_status};{excluded_label}未进入本轮执行。"
|
||
)
|
||
|
||
def _load_latest_run_log(self):
|
||
try:
|
||
logs = db.list_run_logs(limit=1, run_type="apply", path=self.db_path)
|
||
if not logs:
|
||
return
|
||
events = db.list_run_log_events(logs[0].id, limit=30, path=self.db_path)
|
||
except Exception:
|
||
return
|
||
lines = [
|
||
f"{event.created_at} [{event.level}] {event.message}"
|
||
for event in reversed(events)
|
||
]
|
||
self.run_log_view.setPlainText("\n".join(lines))
|
||
scroll_bar = self.run_log_view.verticalScrollBar()
|
||
scroll_bar.setValue(scroll_bar.maximum())
|
||
|
||
def _show_apply_blocked(self, payload):
|
||
lines = ["更新前检查未通过。"]
|
||
if payload.get("no_accounts"):
|
||
lines.append("当前没有配置账号。")
|
||
duplicate_ports = payload.get("duplicate_ports") or []
|
||
if duplicate_ports:
|
||
for item in duplicate_ports:
|
||
lines.append(
|
||
"以下账号调试端口冲突:端口 {port} -> {aliases}".format(
|
||
port=item.get("debug_port"),
|
||
aliases="、".join(item.get("aliases") or []),
|
||
)
|
||
)
|
||
not_running = payload.get("not_running") or []
|
||
if not_running:
|
||
lines.append(
|
||
"以下账号 Chrome 未启动或调试端口不可访问:"
|
||
+ "、".join(self._account_label(item) for item in not_running)
|
||
)
|
||
logged_out = payload.get("logged_out") or []
|
||
if logged_out:
|
||
lines.append(
|
||
"以下账号未登录蝦皮:"
|
||
+ "、".join(self._account_label(item) for item in logged_out)
|
||
)
|
||
self._show_account_guide("\n".join(lines))
|
||
|
||
def _show_account_guide(self, message):
|
||
full_message = (
|
||
f"{message}\n\n"
|
||
"本轮更新已中止,不会自动打开账号 Chrome,也不会提交任何商品。\n"
|
||
"请先到「账号管理」配置账号、启动对应账号 Chrome,并确认已人工登录蝦皮。"
|
||
)
|
||
QMessageBox.warning(self, "账号未就绪", full_message)
|
||
self._set_status(full_message.replace("\n", " "))
|
||
if self.open_accounts_callback is not None:
|
||
self.open_accounts_callback()
|
||
|
||
def _account_label(self, item):
|
||
if isinstance(item, dict):
|
||
name = item.get("account_name") or item.get("alias") or ""
|
||
alias = item.get("alias") or ""
|
||
reason = item.get("reason")
|
||
else:
|
||
name = getattr(item, "account_name", "") or getattr(item, "alias", "")
|
||
alias = getattr(item, "alias", "")
|
||
reason = getattr(item, "reason", None)
|
||
label = f"{name}({alias})" if alias and name != alias else (name or alias)
|
||
return f"{label}: {reason}" if reason else label
|
||
|
||
def _active_batch_ids(self):
|
||
selected_batch = self.batch_filter.currentData()
|
||
if selected_batch:
|
||
return [selected_batch]
|
||
batch_ids = []
|
||
for task in self.model.tasks:
|
||
batch_id = getattr(task, "batch_id", None)
|
||
if batch_id and batch_id not in batch_ids:
|
||
batch_ids.append(batch_id)
|
||
return batch_ids
|
||
|
||
def _update_write_back_button(self):
|
||
if getattr(self, "write_back_button", None) is None:
|
||
return
|
||
enabled = (
|
||
self.apply_thread is None
|
||
and self.result_write_back_thread is None
|
||
and bool(self._active_batch_ids())
|
||
)
|
||
self.write_back_button.setEnabled(enabled)
|
||
|
||
def _start_result_write_back(self, batch_ids, auto=False, apply_summary=None):
|
||
if self.result_write_back_thread is not None:
|
||
self._set_status("Excel 结果回写正在进行...")
|
||
return False
|
||
worker = WriteBackWorker(
|
||
batch_ids,
|
||
db_path=self.db_path,
|
||
mode="results",
|
||
diagnostic_log_dir=diagnostics.DEFAULT_LOG_DIR,
|
||
)
|
||
worker.failed.connect(
|
||
lambda task_id, error, auto=auto, apply_summary=apply_summary:
|
||
self._on_result_write_back_failed(
|
||
task_id,
|
||
error,
|
||
auto=auto,
|
||
apply_summary=apply_summary,
|
||
)
|
||
)
|
||
worker.finished.connect(
|
||
lambda payload, auto=auto, apply_summary=apply_summary:
|
||
self._on_result_write_back_finished(
|
||
payload,
|
||
auto=auto,
|
||
apply_summary=apply_summary,
|
||
)
|
||
)
|
||
thread = run_worker(worker, thread_name="ResultWriteBackWorker", start=False)
|
||
thread.finished.connect(lambda: self._forget_result_write_back_thread(thread))
|
||
self.result_write_back_worker = worker
|
||
self.result_write_back_thread = thread
|
||
self._set_result_write_back_running(True)
|
||
self._set_status("正在自动回写更新结果到 Excel..." if auto else "正在回写更新结果到 Excel...")
|
||
thread.start()
|
||
return True
|
||
|
||
def _on_result_write_back_failed(self, task_id, error, auto=False, apply_summary=None):
|
||
message = f"Excel {'自动' if auto else ''}回写更新结果失败:{error}"
|
||
if "被占用" in str(error):
|
||
message += "\n请关闭原 Excel 后点击「回写结果到 Excel」手动重试;SQLite 已保留更新结果。"
|
||
if auto and apply_summary:
|
||
message = self._apply_summary_message(apply_summary, error=message)
|
||
QMessageBox.warning(self, "回写结果到 Excel", message)
|
||
self._set_status(message.replace("\n", " "))
|
||
|
||
def _on_result_write_back_finished(self, payload, auto=False, apply_summary=None):
|
||
self._set_result_write_back_running(False)
|
||
self.refresh_tasks()
|
||
if payload.get("ok") is False:
|
||
error = payload.get("error") or "未知错误"
|
||
retry_hint = ",可点击「回写结果到 Excel」手动重试" if auto else ""
|
||
self._set_status(f"Excel {'自动' if auto else ''}回写更新结果失败:{error}{retry_hint}")
|
||
return
|
||
self._set_status(
|
||
"Excel {prefix}回写更新结果完成:文件{files},行{rows}".format(
|
||
prefix="自动" if auto else "",
|
||
files=payload.get("files", 0),
|
||
rows=payload.get("rows", 0),
|
||
)
|
||
)
|
||
if auto and apply_summary:
|
||
self._show_apply_summary(apply_summary, write_back_payload=payload)
|
||
elif not auto:
|
||
QMessageBox.information(
|
||
self,
|
||
"回写结果到 Excel",
|
||
"结果回写完成:文件{files},行{rows}".format(
|
||
files=payload.get("files", 0),
|
||
rows=payload.get("rows", 0),
|
||
),
|
||
)
|
||
|
||
def _show_apply_summary(self, apply_summary, write_back_payload=None):
|
||
QMessageBox.information(
|
||
self,
|
||
"检查本轮更新完成" if apply_summary.get("dry_run") else "更新完成",
|
||
self._apply_summary_message(apply_summary, write_back_payload),
|
||
)
|
||
|
||
def _apply_summary_message(self, apply_summary, write_back_payload=None, error=None):
|
||
dry_run = bool(apply_summary.get("dry_run"))
|
||
lines = [
|
||
"检查本轮更新完成,未打开蝦皮、未提交线上、未改任务状态。"
|
||
if dry_run
|
||
else "更新完成。",
|
||
"{success_label}:{applied},失败:{failed},略过:{skipped}".format(
|
||
success_label="可更新" if dry_run else "成功",
|
||
applied=apply_summary.get("applied", 0),
|
||
failed=apply_summary.get("failed", 0),
|
||
skipped=apply_summary.get("skipped", 0),
|
||
),
|
||
]
|
||
if write_back_payload:
|
||
lines.append(
|
||
"Excel 回写:文件{files},行{rows}".format(
|
||
files=write_back_payload.get("files", 0),
|
||
rows=write_back_payload.get("rows", 0),
|
||
)
|
||
)
|
||
if error:
|
||
lines.append(str(error))
|
||
return "\n".join(lines)
|