feat: 展示PDD商品和采购订单信息 (#164)
This commit is contained in:
+54
-24
@@ -56,6 +56,18 @@ from qfluentwidgets import (
|
||||
|
||||
PAGE_SIZE = 50
|
||||
CHECK_COLUMN = 0
|
||||
TASK_TYPE_COLUMN = 1
|
||||
GOODS_ID_COLUMN = 2
|
||||
TITLE_COLUMN = 3
|
||||
SHOP_COLUMN = 4
|
||||
COLOR_COLUMN = 5
|
||||
SIZE_COLUMN = 6
|
||||
PRICE_COLUMN = 7
|
||||
QUANTITY_COLUMN = 8
|
||||
STATUS_COLUMN = 9
|
||||
ORDER_NO_COLUMN = 10
|
||||
DURATION_COLUMN = 11
|
||||
UPDATED_AT_COLUMN = 12
|
||||
REMOTE_TASK_ID_ROLE = Qt.UserRole + 1
|
||||
|
||||
|
||||
@@ -76,6 +88,7 @@ class TaskRow:
|
||||
status: str = "待执行"
|
||||
latest_run_status: str = ""
|
||||
duration_seconds: Optional[int] = None
|
||||
order_no: str = ""
|
||||
updated_at: str = ""
|
||||
|
||||
|
||||
@@ -88,6 +101,7 @@ class TaskTableModel(QAbstractTableModel):
|
||||
HEADERS = (
|
||||
"选择",
|
||||
"任务类型",
|
||||
"PDD商品ID",
|
||||
"商品标题",
|
||||
"店铺名",
|
||||
"颜色",
|
||||
@@ -95,6 +109,7 @@ class TaskTableModel(QAbstractTableModel):
|
||||
"价格",
|
||||
"数量",
|
||||
"状态",
|
||||
"订单号",
|
||||
"用时",
|
||||
"更新时间",
|
||||
)
|
||||
@@ -138,15 +153,24 @@ class TaskTableModel(QAbstractTableModel):
|
||||
return self._display_value(task, column)
|
||||
|
||||
if role == Qt.TextAlignmentRole:
|
||||
if column in (0, 1, 8, 9):
|
||||
if column in (
|
||||
CHECK_COLUMN,
|
||||
TASK_TYPE_COLUMN,
|
||||
STATUS_COLUMN,
|
||||
DURATION_COLUMN,
|
||||
):
|
||||
return Qt.AlignCenter
|
||||
if column in (6, 7):
|
||||
if column in (PRICE_COLUMN, QUANTITY_COLUMN):
|
||||
return Qt.AlignRight | Qt.AlignVCenter
|
||||
return Qt.AlignLeft | Qt.AlignVCenter
|
||||
|
||||
if role == Qt.ToolTipRole:
|
||||
if column == 2:
|
||||
if column == GOODS_ID_COLUMN:
|
||||
return task.goods_id or "尚无PDD商品ID"
|
||||
if column == TITLE_COLUMN:
|
||||
return task.title or "尚未获取标题"
|
||||
if column == ORDER_NO_COLUMN:
|
||||
return task.order_no or "尚无订单号"
|
||||
|
||||
if role == REMOTE_TASK_ID_ROLE:
|
||||
return task.remote_task_id
|
||||
@@ -367,32 +391,36 @@ class TaskTableModel(QAbstractTableModel):
|
||||
def _display_value(task: TaskRow, column: int) -> str:
|
||||
if column == CHECK_COLUMN:
|
||||
return ""
|
||||
if column == 1:
|
||||
if column == TASK_TYPE_COLUMN:
|
||||
return task.task_type
|
||||
if column == 2:
|
||||
if column == GOODS_ID_COLUMN:
|
||||
return task.goods_id or "—"
|
||||
if column == TITLE_COLUMN:
|
||||
return task.title or "尚未获取标题"
|
||||
if column == 3:
|
||||
if column == SHOP_COLUMN:
|
||||
return task.shop_name or "—"
|
||||
if column == 4:
|
||||
if column == COLOR_COLUMN:
|
||||
return task.color or "—"
|
||||
if column == 5:
|
||||
if column == SIZE_COLUMN:
|
||||
return task.size or "—"
|
||||
if column == 6:
|
||||
if column == PRICE_COLUMN:
|
||||
if task.price_cents is None:
|
||||
return "—"
|
||||
suffix = " 起" if task.price_is_starting else ""
|
||||
return f"¥{task.price_cents / 100:.2f}{suffix}"
|
||||
if column == 7:
|
||||
if column == QUANTITY_COLUMN:
|
||||
return "—" if task.quantity is None else str(task.quantity)
|
||||
if column == 8:
|
||||
if column == STATUS_COLUMN:
|
||||
return task.status
|
||||
if column == 9:
|
||||
if column == ORDER_NO_COLUMN:
|
||||
return task.order_no or "—"
|
||||
if column == DURATION_COLUMN:
|
||||
if task.latest_run_status == "running":
|
||||
return "进行中"
|
||||
if task.duration_seconds is None:
|
||||
return "—"
|
||||
return f"{max(0, task.duration_seconds)} 秒"
|
||||
if column == 10:
|
||||
if column == UPDATED_AT_COLUMN:
|
||||
return task.updated_at or "—"
|
||||
return ""
|
||||
|
||||
@@ -575,18 +603,20 @@ class PDDTaskPage(QWidget):
|
||||
|
||||
header = self.taskTable.horizontalHeader()
|
||||
header.setSectionResizeMode(QHeaderView.Interactive)
|
||||
header.setSectionResizeMode(2, QHeaderView.Stretch)
|
||||
header.setSectionResizeMode(TITLE_COLUMN, QHeaderView.Stretch)
|
||||
header.setMinimumSectionSize(64)
|
||||
self.taskTable.setColumnWidth(0, 64)
|
||||
self.taskTable.setColumnWidth(1, 82)
|
||||
self.taskTable.setColumnWidth(3, 108)
|
||||
self.taskTable.setColumnWidth(4, 108)
|
||||
self.taskTable.setColumnWidth(5, 82)
|
||||
self.taskTable.setColumnWidth(6, 96)
|
||||
self.taskTable.setColumnWidth(7, 70)
|
||||
self.taskTable.setColumnWidth(8, 116)
|
||||
self.taskTable.setColumnWidth(9, 82)
|
||||
self.taskTable.setColumnWidth(10, 156)
|
||||
self.taskTable.setColumnWidth(CHECK_COLUMN, 64)
|
||||
self.taskTable.setColumnWidth(TASK_TYPE_COLUMN, 82)
|
||||
self.taskTable.setColumnWidth(GOODS_ID_COLUMN, 135)
|
||||
self.taskTable.setColumnWidth(SHOP_COLUMN, 108)
|
||||
self.taskTable.setColumnWidth(COLOR_COLUMN, 108)
|
||||
self.taskTable.setColumnWidth(SIZE_COLUMN, 82)
|
||||
self.taskTable.setColumnWidth(PRICE_COLUMN, 96)
|
||||
self.taskTable.setColumnWidth(QUANTITY_COLUMN, 70)
|
||||
self.taskTable.setColumnWidth(STATUS_COLUMN, 116)
|
||||
self.taskTable.setColumnWidth(ORDER_NO_COLUMN, 190)
|
||||
self.taskTable.setColumnWidth(DURATION_COLUMN, 82)
|
||||
self.taskTable.setColumnWidth(UPDATED_AT_COLUMN, 156)
|
||||
self.taskTable.viewport().installEventFilter(self)
|
||||
|
||||
self.emptyStateCard = CardWidget(self)
|
||||
|
||||
@@ -1766,5 +1766,6 @@ def summary_to_row(summary: TaskSummary) -> TaskRow:
|
||||
summary.latest_run_status.value if summary.latest_run_status else ""
|
||||
),
|
||||
duration_seconds=summary.duration_seconds,
|
||||
order_no=summary.order_no or "",
|
||||
updated_at=summary.updated_at,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Iterable, List, Mapping
|
||||
from typing import Any, Dict, Iterable, List, Mapping, Optional
|
||||
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
|
||||
from PyQt5.QtGui import QCloseEvent, QKeySequence
|
||||
@@ -148,6 +148,21 @@ class ColorPriceView:
|
||||
price_inconsistent: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PurchaseResultView:
|
||||
"""采购任务的下单与核单结果。"""
|
||||
|
||||
mode: str
|
||||
confirmed_spec: str
|
||||
quantity: str
|
||||
unit_price: str
|
||||
total_price: str
|
||||
order_no: str
|
||||
ordered_at: str
|
||||
match_status: str
|
||||
payment_status: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TaskDetailViewData:
|
||||
"""详情窗口直接使用的展示数据。"""
|
||||
@@ -169,6 +184,7 @@ class TaskDetailViewData:
|
||||
last_error: str
|
||||
colors: List[ColorPriceView]
|
||||
sizes: List[str]
|
||||
purchase: Optional[PurchaseResultView] = None
|
||||
|
||||
|
||||
def _non_empty_text(value: Any) -> str:
|
||||
@@ -228,6 +244,72 @@ def _format_metric(value: Any) -> str:
|
||||
return "未采集"
|
||||
|
||||
|
||||
def _format_cent(value: Any) -> str:
|
||||
if isinstance(value, int) and not isinstance(value, bool) and value >= 0:
|
||||
return f"¥{value / 100:.2f}"
|
||||
return "—"
|
||||
|
||||
|
||||
def _purchase_result_view(
|
||||
task_type: TaskType, pdd_data: Mapping[str, Any]
|
||||
) -> Optional[PurchaseResultView]:
|
||||
"""整理采购结果;非采购任务不显示采购信息卡片。"""
|
||||
|
||||
if task_type is not TaskType.PURCHASE:
|
||||
return None
|
||||
purchase = pdd_data.get("purchase")
|
||||
if not isinstance(purchase, Mapping):
|
||||
purchase = {}
|
||||
confirmed = purchase.get("confirmed")
|
||||
if not isinstance(confirmed, Mapping):
|
||||
confirmed = {}
|
||||
options = confirmed.get("options")
|
||||
if not isinstance(options, Mapping):
|
||||
options = {}
|
||||
confirmed_spec = " / ".join(
|
||||
_non_empty_text(value) for value in options.values()
|
||||
if _non_empty_text(value)
|
||||
) or "—"
|
||||
mode_text = {"live": "真实采购", "dry_run": "采购演练"}
|
||||
match_text = {
|
||||
"matched": "已匹配",
|
||||
"not_submitted": "未提交",
|
||||
"not_found": "未找到",
|
||||
"ambiguous": "多个候选",
|
||||
"unknown": "结果不确定",
|
||||
}
|
||||
payment_text = {
|
||||
"unpaid": "待付款",
|
||||
"paid": "已支付",
|
||||
"other": "其他状态",
|
||||
"unknown": "未知",
|
||||
}
|
||||
quantity = confirmed.get("quantity")
|
||||
return PurchaseResultView(
|
||||
mode=mode_text.get(_non_empty_text(purchase.get("mode")), "—"),
|
||||
confirmed_spec=confirmed_spec,
|
||||
quantity=(
|
||||
str(quantity)
|
||||
if isinstance(quantity, int) and not isinstance(quantity, bool)
|
||||
else "—"
|
||||
),
|
||||
unit_price=_format_cent(confirmed.get("unit_price_cent")),
|
||||
total_price=_format_cent(confirmed.get("total_price_cent")),
|
||||
order_no=_non_empty_text(purchase.get("order_no")) or "—",
|
||||
ordered_at=(
|
||||
_non_empty_text(purchase.get("ordered_at_raw"))
|
||||
or _non_empty_text(purchase.get("ordered_at"))
|
||||
or "—"
|
||||
),
|
||||
match_status=match_text.get(
|
||||
_non_empty_text(purchase.get("match_status")), "—"
|
||||
),
|
||||
payment_status=payment_text.get(
|
||||
_non_empty_text(purchase.get("payment_status")), "—"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def build_task_detail_view_data(detail: TaskDetail) -> TaskDetailViewData:
|
||||
"""从完整任务记录生成详情展示数据。
|
||||
|
||||
@@ -310,6 +392,7 @@ def build_task_detail_view_data(detail: TaskDetail) -> TaskDetailViewData:
|
||||
last_error=last_error,
|
||||
colors=colors,
|
||||
sizes=sizes,
|
||||
purchase=_purchase_result_view(detail.task_type, pdd_data),
|
||||
)
|
||||
|
||||
|
||||
@@ -401,6 +484,8 @@ class TaskDetailWindow(QWidget):
|
||||
)
|
||||
)
|
||||
content_layout.addWidget(self._build_basic_card(content))
|
||||
if self.data.purchase is not None:
|
||||
content_layout.addWidget(self._build_purchase_card(content))
|
||||
content_layout.addWidget(
|
||||
self._build_spec_card("颜色", self.data.colors, content)
|
||||
)
|
||||
@@ -508,6 +593,37 @@ class TaskDetailWindow(QWidget):
|
||||
layout.setColumnStretch(1, 1)
|
||||
return card
|
||||
|
||||
def _build_purchase_card(self, parent: QWidget) -> CardWidget:
|
||||
"""显示采购任务已经保存在本地的下单和核单结果。"""
|
||||
|
||||
purchase = self.data.purchase
|
||||
assert purchase is not None
|
||||
card = CardWidget(parent)
|
||||
card.setObjectName("purchaseResultCard")
|
||||
layout = QGridLayout(card)
|
||||
layout.setContentsMargins(16, 14, 16, 14)
|
||||
layout.setHorizontalSpacing(18)
|
||||
layout.setVerticalSpacing(8)
|
||||
values = (
|
||||
("采购模式", purchase.mode),
|
||||
("确认规格", purchase.confirmed_spec),
|
||||
("确认数量", purchase.quantity),
|
||||
("商品单价", purchase.unit_price),
|
||||
("订单总价", purchase.total_price),
|
||||
("订单号", purchase.order_no),
|
||||
("下单时间", purchase.ordered_at),
|
||||
("订单匹配", purchase.match_status),
|
||||
("付款状态", purchase.payment_status),
|
||||
)
|
||||
layout.addWidget(SubtitleLabel("采购结果", card), 0, 0, 1, 2)
|
||||
for row, (label, value) in enumerate(values, start=1):
|
||||
layout.addWidget(CaptionLabel(label, card), row, 0)
|
||||
text = self._copyable_label(value, label, card)
|
||||
text.setWordWrap(True)
|
||||
layout.addWidget(text, row, 1)
|
||||
layout.setColumnStretch(1, 1)
|
||||
return card
|
||||
|
||||
def _copyable_label(
|
||||
self, value: str, field_name: str, parent: QWidget
|
||||
) -> CopyableBodyLabel:
|
||||
|
||||
@@ -141,6 +141,7 @@ class TaskSummary:
|
||||
shop_name: Optional[str] = None
|
||||
latest_run_status: Optional[RunStatus] = None
|
||||
duration_seconds: Optional[int] = None
|
||||
order_no: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
@@ -136,7 +136,12 @@ class TaskRepository:
|
||||
" WHEN latest_run.started_at IS NOT NULL AND latest_run.finished_at IS NOT NULL"
|
||||
" THEN MAX(0, CAST(strftime('%s', latest_run.finished_at)"
|
||||
" - strftime('%s', latest_run.started_at) AS INTEGER))"
|
||||
" ELSE NULL END AS duration_seconds"
|
||||
" ELSE NULL END AS duration_seconds,"
|
||||
" CASE WHEN pdd_tasks.pdd_data IS NOT NULL"
|
||||
" AND json_valid(pdd_tasks.pdd_data)"
|
||||
" THEN CAST(json_extract(pdd_tasks.pdd_data,"
|
||||
" '$.purchase.order_no') AS TEXT)"
|
||||
" ELSE NULL END AS order_no"
|
||||
" FROM pdd_tasks"
|
||||
" LEFT JOIN task_runs AS latest_run ON latest_run.id = ("
|
||||
" SELECT id FROM task_runs WHERE task_id = pdd_tasks.id"
|
||||
@@ -1830,9 +1835,13 @@ class TaskRepository:
|
||||
clauses.append(
|
||||
"(remote_task_id LIKE ? ESCAPE '\\'"
|
||||
" OR COALESCE(goods_id, '') LIKE ? ESCAPE '\\'"
|
||||
" OR COALESCE(title, '') LIKE ? ESCAPE '\\')"
|
||||
" OR COALESCE(title, '') LIKE ? ESCAPE '\\'"
|
||||
" OR COALESCE(CASE WHEN pdd_data IS NOT NULL"
|
||||
" AND json_valid(pdd_data)"
|
||||
" THEN json_extract(pdd_data, '$.purchase.order_no')"
|
||||
" END, '') LIKE ? ESCAPE '\\')"
|
||||
)
|
||||
parameters.extend((pattern, pattern, pattern))
|
||||
parameters.extend((pattern, pattern, pattern, pattern))
|
||||
return (" WHERE " + " AND ".join(clauses) if clauses else "", parameters)
|
||||
|
||||
@staticmethod
|
||||
@@ -1859,6 +1868,7 @@ class TaskRepository:
|
||||
else None
|
||||
),
|
||||
duration_seconds=row["duration_seconds"],
|
||||
order_no=row["order_no"],
|
||||
updated_at=row["updated_at"],
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user