feat(apply): open product page on left double click
This commit is contained in:
@@ -429,6 +429,27 @@ def _product_url(account, item_id):
|
||||
)
|
||||
|
||||
|
||||
def _find_exact_product_tab(account, item_id, host):
|
||||
"""只返回该账号已打开且商品路径精确匹配的 target。"""
|
||||
|
||||
expected = urlparse(_product_url(account, item_id))
|
||||
expected_host = expected.netloc.lower()
|
||||
expected_path = expected.path.rstrip("/")
|
||||
for target in http_get("/json", host=host):
|
||||
if target.get("type") != "page" or not target.get("webSocketDebuggerUrl"):
|
||||
continue
|
||||
try:
|
||||
current = urlparse(str(target.get("url") or ""))
|
||||
except Exception:
|
||||
continue
|
||||
if (
|
||||
current.netloc.lower() == expected_host
|
||||
and current.path.rstrip("/") == expected_path
|
||||
):
|
||||
return target
|
||||
return None
|
||||
|
||||
|
||||
def _item_id(task):
|
||||
value = _get(task, "item_id", "itemid", "product_id")
|
||||
if not value:
|
||||
@@ -815,6 +836,9 @@ def _wait_ready(cdp, timeout=60):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
current_url = str(last_state.get("current_url") or _current_url(cdp) or "")
|
||||
if _is_login_url(current_url):
|
||||
raise EditorError("账号未登录,请先到④账号管理人工登录蝦皮")
|
||||
invalid_text = _product_unavailable_toast_text(read_page_toasts(cdp))
|
||||
if invalid_text:
|
||||
raise EditorError(product_unavailable_error_message(invalid_text))
|
||||
@@ -1105,6 +1129,45 @@ def open_product(account, item_id, on_step=None, bring_to_front=True) -> CDP:
|
||||
raise
|
||||
|
||||
|
||||
def open_or_focus_product_tab(account, item_id):
|
||||
"""聚焦现有精确商品页,或新建商品页且不刷新用户已有页面。
|
||||
|
||||
此方法不能复用 ``open_product``:真实更新需要重新导航,人工查看不能
|
||||
刷新用户正在编辑、尚未保存的商品页。
|
||||
"""
|
||||
|
||||
host = _cdp_host(account)
|
||||
item_id = str(item_id)
|
||||
url = _product_url(account, item_id)
|
||||
target = _find_exact_product_tab(account, item_id, host)
|
||||
created_by_app = target is None
|
||||
if target is None:
|
||||
target = create_tab_info(url, host=host, background=False)
|
||||
|
||||
cdp = CDP(target["webSocketDebuggerUrl"])
|
||||
cdp.target_id = target.get("id")
|
||||
cdp.created_by_app = created_by_app
|
||||
cdp.cdp_host = host
|
||||
try:
|
||||
_ensure_page_domains(cdp)
|
||||
if created_by_app:
|
||||
install_toast_observer(cdp)
|
||||
cdp.send("Page.bringToFront")
|
||||
if created_by_app:
|
||||
_wait_ready(cdp)
|
||||
return {
|
||||
"item_id": item_id,
|
||||
"created": created_by_app,
|
||||
"target_id": target.get("id"),
|
||||
}
|
||||
except Exception:
|
||||
if created_by_app:
|
||||
_close_open_product_failure(cdp)
|
||||
raise
|
||||
finally:
|
||||
cdp.close()
|
||||
|
||||
|
||||
def read_title(cdp) -> str:
|
||||
"""Read the current Shopee title input value."""
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ if QT_IMPORT_ERROR is None:
|
||||
ProductSuiteGenerateWorker,
|
||||
ProductSuiteHistoryExportWorker,
|
||||
ProductSuiteImportImagesWorker,
|
||||
ProductTabOpenWorker,
|
||||
WriteBackWorker,
|
||||
)
|
||||
from .tabs.accounts import AccountDialog, AccountsTab
|
||||
|
||||
+85
-2
@@ -2,10 +2,16 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import QModelIndex, Signal
|
||||
|
||||
from ... import product_status
|
||||
from ..models import ApplyTaskTableModel
|
||||
from ..widgets import *
|
||||
from ..workers import ApplyWorker as _RealApplyWorker, WriteBackWorker as _RealWriteBackWorker
|
||||
from ..workers import (
|
||||
ApplyWorker as _RealApplyWorker,
|
||||
ProductTabOpenWorker as _RealProductTabOpenWorker,
|
||||
WriteBackWorker as _RealWriteBackWorker,
|
||||
)
|
||||
|
||||
|
||||
def ApplyWorker(*args, **kwargs):
|
||||
@@ -15,6 +21,27 @@ def ApplyWorker(*args, **kwargs):
|
||||
def WriteBackWorker(*args, **kwargs):
|
||||
return _call_package_attr("WriteBackWorker", _RealWriteBackWorker, *args, **kwargs)
|
||||
|
||||
|
||||
def ProductTabOpenWorker(*args, **kwargs):
|
||||
return _call_package_attr(
|
||||
"ProductTabOpenWorker", _RealProductTabOpenWorker, *args, **kwargs
|
||||
)
|
||||
|
||||
|
||||
class _ApplyTaskTableView(QTableView):
|
||||
"""任务表仅向外发出左键双击事件,供人工查看商品页。"""
|
||||
|
||||
leftDoubleClicked = Signal(QModelIndex)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
super().mouseDoubleClickEvent(event)
|
||||
if event.button() != Qt.LeftButton:
|
||||
return
|
||||
index = self.indexAt(event.position().toPoint())
|
||||
if index.isValid():
|
||||
self.leftDoubleClicked.emit(index)
|
||||
|
||||
|
||||
class ApplyTab(QWidget):
|
||||
"""Tab 3: list generated tasks and confirm the update scope."""
|
||||
|
||||
@@ -53,6 +80,8 @@ class ApplyTab(QWidget):
|
||||
self.apply_thread = None
|
||||
self.result_write_back_worker = None
|
||||
self.result_write_back_thread = None
|
||||
self.product_tab_open_worker = None
|
||||
self.product_tab_open_thread = None
|
||||
self.last_apply_summary = None
|
||||
|
||||
self.batch_filter = QComboBox()
|
||||
@@ -95,7 +124,7 @@ class ApplyTab(QWidget):
|
||||
) = _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.task_table = _ApplyTaskTableView()
|
||||
self.model = ApplyTaskTableModel(self.task_table)
|
||||
self.task_table.setModel(self.model)
|
||||
self.task_table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
@@ -168,6 +197,7 @@ class ApplyTab(QWidget):
|
||||
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.leftDoubleClicked.connect(self.open_or_focus_selected_product)
|
||||
self.task_table.customContextMenuRequested.connect(self.show_task_context_menu)
|
||||
self.write_back_button.clicked.connect(self.write_back_results)
|
||||
|
||||
@@ -444,6 +474,59 @@ class ApplyTab(QWidget):
|
||||
self._set_status(message, level="success")
|
||||
QMessageBox.information(self, "删除本条记录", message)
|
||||
|
||||
def open_or_focus_selected_product(self, index):
|
||||
if self.apply_thread is not None or self.result_write_back_thread is not None:
|
||||
self._set_status("更新或回写正在进行,暂不能打开商品详情页", level="warning")
|
||||
return
|
||||
if self.product_tab_open_thread is not None:
|
||||
self._set_status("正在打开商品详情页,请稍候", level="info")
|
||||
return
|
||||
if not index.isValid():
|
||||
self._set_status("请选择要查看的商品记录", level="warning")
|
||||
return
|
||||
self.task_table.selectRow(index.row())
|
||||
self.task_table.setCurrentIndex(index)
|
||||
task = self.model.task_at(index.row())
|
||||
if task is None:
|
||||
self._set_status("未找到要查看的商品记录", level="warning")
|
||||
return
|
||||
worker = ProductTabOpenWorker(
|
||||
task.alias,
|
||||
task.item_id,
|
||||
db_path=self.db_path,
|
||||
config=self.config,
|
||||
)
|
||||
worker.finished.connect(self._on_product_tab_open_finished)
|
||||
worker.failed.connect(self._on_product_tab_open_failed)
|
||||
thread = run_worker(worker, thread_name="ProductTabOpenWorker", start=False)
|
||||
thread.finished.connect(lambda: self._forget_product_tab_open_thread(thread))
|
||||
self.product_tab_open_worker = worker
|
||||
self.product_tab_open_thread = thread
|
||||
self._set_status(f"正在打开商品 {task.item_id} 的详情页...", level="info")
|
||||
thread.start()
|
||||
|
||||
def _on_product_tab_open_finished(self, payload):
|
||||
if not payload.get("ok"):
|
||||
self._set_status(
|
||||
payload.get("message") or "打开商品详情页失败,请检查账号登录状态。",
|
||||
level="warning",
|
||||
)
|
||||
return
|
||||
action = "已打开新页面" if payload.get("created") else "已聚焦现有页面"
|
||||
account_name = payload.get("account_name") or payload.get("alias") or "账号"
|
||||
self._set_status(
|
||||
f"商品 {payload.get('item_id')}:{account_name}{action}",
|
||||
level="success",
|
||||
)
|
||||
|
||||
def _on_product_tab_open_failed(self, task_id, error):
|
||||
self._set_status("打开商品详情页失败,请检查账号 Chrome 和登录状态。", level="danger")
|
||||
|
||||
def _forget_product_tab_open_thread(self, thread):
|
||||
if self.product_tab_open_thread is thread:
|
||||
self.product_tab_open_thread = None
|
||||
self.product_tab_open_worker = None
|
||||
|
||||
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("更新或回写正在进行,不能重置")
|
||||
|
||||
@@ -15,7 +15,9 @@ except ModuleNotFoundError: # pragma: no cover - GUI import guard
|
||||
from .. import (
|
||||
ai,
|
||||
appconfig,
|
||||
chrome,
|
||||
cmhub_models,
|
||||
editor,
|
||||
image_studio,
|
||||
image_studio_export,
|
||||
image_studio_generation,
|
||||
@@ -1237,6 +1239,81 @@ class GenerateWorker(BaseWorker):
|
||||
except Exception:
|
||||
return
|
||||
|
||||
class ProductTabOpenWorker(BaseWorker):
|
||||
"""后台打开单个商品详情页供人工查看,不修改本地任务数据。"""
|
||||
|
||||
def __init__(self, alias, item_id, db_path=None, config=None):
|
||||
super().__init__()
|
||||
self.alias = str(alias or "").strip()
|
||||
self.item_id = str(item_id or "").strip()
|
||||
self.db_path = db_path
|
||||
self.config = config
|
||||
|
||||
def execute(self):
|
||||
if not self.alias:
|
||||
return self._blocked("ACCOUNT_NOT_FOUND", "任务未关联账号,请先检查导入数据。")
|
||||
if not self.item_id:
|
||||
return self._blocked("ITEM_ID_EMPTY", "任务缺少商品ID,无法打开商品详情页。")
|
||||
try:
|
||||
account = db.get_account_by_alias(self.alias, path=self.db_path)
|
||||
except Exception:
|
||||
return self._blocked(
|
||||
"ACCOUNT_LOOKUP_FAILED",
|
||||
"账号信息读取失败,请先到④账号管理检查账号配置。",
|
||||
)
|
||||
if account is None:
|
||||
return self._blocked(
|
||||
"ACCOUNT_NOT_FOUND",
|
||||
f"未找到账号「{self.alias}」,请先到④账号管理配置并登录。",
|
||||
)
|
||||
if not chrome.is_running(account.debug_port):
|
||||
return self._blocked(
|
||||
"CHROME_NOT_RUNNING",
|
||||
f"账号「{account.account_name}」的 Chrome 未启动,请先到④账号管理启动并人工登录蝦皮。",
|
||||
account=account,
|
||||
)
|
||||
try:
|
||||
result = editor.open_or_focus_product_tab(account, self.item_id)
|
||||
except editor.EditorError as exc:
|
||||
return self._blocked(
|
||||
"OPEN_PRODUCT_FAILED",
|
||||
self._editor_error_message(exc, account),
|
||||
account=account,
|
||||
)
|
||||
except Exception:
|
||||
return self._blocked(
|
||||
"CDP_UNAVAILABLE",
|
||||
f"无法连接账号「{account.account_name}」的 Chrome,请先到④账号管理确认已启动并登录。",
|
||||
account=account,
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"alias": account.alias,
|
||||
"account_name": account.account_name,
|
||||
"item_id": self.item_id,
|
||||
"created": bool(result.get("created")),
|
||||
"target_id": result.get("target_id"),
|
||||
}
|
||||
|
||||
def _blocked(self, reason, message, account=None):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": reason,
|
||||
"message": message,
|
||||
"alias": getattr(account, "alias", self.alias),
|
||||
"account_name": getattr(account, "account_name", ""),
|
||||
"item_id": self.item_id,
|
||||
}
|
||||
|
||||
def _editor_error_message(self, exc, account):
|
||||
detail = diagnostics.redact_log_text(str(exc) or "")
|
||||
if "商品失效" in detail:
|
||||
return detail
|
||||
if "登录" in detail:
|
||||
return f"账号「{account.account_name}」未登录,请先到④账号管理人工登录蝦皮。"
|
||||
return "打开商品详情页失败,请先到④账号管理确认 Chrome 已启动、已人工登录,并检查商品状态。"
|
||||
|
||||
|
||||
class ApplyWorker(BaseWorker):
|
||||
"""Apply generated title/cover changes, optionally previewing or grouping by account."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user