feat(apply): open product page on left double click

This commit is contained in:
chengma
2026-07-20 11:22:26 +08:00
parent 9f43e5547a
commit 7dbfcb5a24
11 changed files with 445 additions and 5 deletions
+63
View File
@@ -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."""
+1
View File
@@ -34,6 +34,7 @@ if QT_IMPORT_ERROR is None:
ProductSuiteGenerateWorker,
ProductSuiteHistoryExportWorker,
ProductSuiteImportImagesWorker,
ProductTabOpenWorker,
WriteBackWorker,
)
from .tabs.accounts import AccountDialog, AccountsTab
+85 -2
View File
@@ -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("更新或回写正在进行,不能重置")
+77
View File
@@ -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."""