feat(product-suite): add original image batch management
This commit is contained in:
+292
-30
@@ -7,7 +7,7 @@ import re
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from PySide6.QtCore import QByteArray, QBuffer, QIODevice, QSize, Qt, QTimer, Signal
|
||||
from PySide6.QtCore import QByteArray, QBuffer, QIODevice, QRect, QSize, Qt, QTimer, Signal
|
||||
from PySide6.QtGui import QColor, QIcon, QImage, QImageReader, QKeySequence, QPainter, QPixmap
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
@@ -32,6 +32,9 @@ from PySide6.QtWidgets import (
|
||||
QScrollArea,
|
||||
QSizePolicy,
|
||||
QSplitter,
|
||||
QStyle,
|
||||
QStyledItemDelegate,
|
||||
QStyleOptionButton,
|
||||
QTabBar,
|
||||
QToolButton,
|
||||
QVBoxLayout,
|
||||
@@ -51,6 +54,7 @@ from ..workers import (
|
||||
|
||||
|
||||
ORIGINAL_DOWNLOAD_CONCURRENCY = 2
|
||||
ORIGINAL_CHECK_STATE_ROLE = Qt.UserRole + 1
|
||||
_PRODUCT_SUITE_THREAD_REFS = {}
|
||||
_URL_RE = re.compile(r"https?://[^\s,,;;))\]]+", re.IGNORECASE)
|
||||
|
||||
@@ -143,11 +147,48 @@ class ProductSuitePreviewDialog(QDialog):
|
||||
)
|
||||
|
||||
|
||||
class ProductOriginalDelegate(QStyledItemDelegate):
|
||||
CHECK_HIT_SIZE = 26
|
||||
|
||||
@classmethod
|
||||
def checkbox_hit_rect(cls, item_rect):
|
||||
return QRect(
|
||||
item_rect.left() + 2,
|
||||
item_rect.top() + 2,
|
||||
cls.CHECK_HIT_SIZE,
|
||||
cls.CHECK_HIT_SIZE,
|
||||
)
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
super().paint(painter, option, index)
|
||||
if index.data(Qt.UserRole) is None:
|
||||
return
|
||||
checked = index.data(ORIGINAL_CHECK_STATE_ROLE) == Qt.Checked
|
||||
style = QApplication.style()
|
||||
width = style.pixelMetric(QStyle.PM_IndicatorWidth)
|
||||
height = style.pixelMetric(QStyle.PM_IndicatorHeight)
|
||||
hit_rect = self.checkbox_hit_rect(option.rect)
|
||||
checkbox = QStyleOptionButton()
|
||||
checkbox.rect = QRect(
|
||||
hit_rect.center().x() - width // 2,
|
||||
hit_rect.center().y() - height // 2,
|
||||
width,
|
||||
height,
|
||||
)
|
||||
checkbox.state = QStyle.State_Enabled if option.state & QStyle.State_Enabled else QStyle.State_None
|
||||
checkbox.state |= QStyle.State_On if checked else QStyle.State_Off
|
||||
style.drawControl(QStyle.CE_CheckBox, checkbox, painter)
|
||||
|
||||
|
||||
class ProductOriginalList(QListWidget):
|
||||
MAX_VISIBLE_ASSETS = 16
|
||||
|
||||
filesDropped = Signal(list)
|
||||
clipboardImage = Signal(bytes)
|
||||
orderChanged = Signal(list)
|
||||
deleteRequested = Signal(int)
|
||||
deleteAssetsRequested = Signal(list)
|
||||
checkedAssetsChanged = Signal(list)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -159,12 +200,16 @@ class ProductOriginalList(QListWidget):
|
||||
self.setIconSize(QSize(82, 64))
|
||||
self.setGridSize(QSize(112, 98))
|
||||
self.setSpacing(4)
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.setAcceptDrops(True)
|
||||
self.setDragEnabled(True)
|
||||
self.setDropIndicatorShown(True)
|
||||
self.setDragDropMode(QListWidget.InternalMove)
|
||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self._show_menu)
|
||||
self.itemChanged.connect(lambda item: self.checkedAssetsChanged.emit(self.checked_asset_ids()))
|
||||
self.setItemDelegate(ProductOriginalDelegate(self))
|
||||
self.setMouseTracking(True)
|
||||
self.itemEntered.connect(self._show_hover_remove)
|
||||
self._hovered_asset_id = None
|
||||
@@ -198,6 +243,36 @@ class ProductOriginalList(QListWidget):
|
||||
super().dropEvent(event)
|
||||
self.orderChanged.emit(self.asset_ids())
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
self.update_content_height()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
position = event.position().toPoint()
|
||||
item = self.itemAt(position)
|
||||
if (
|
||||
event.button() == Qt.LeftButton
|
||||
and item is not None
|
||||
and item.data(Qt.UserRole) is not None
|
||||
and ProductOriginalDelegate.checkbox_hit_rect(self.visualItemRect(item)).contains(position)
|
||||
):
|
||||
self._set_item_checked(item, not self._item_checked(item))
|
||||
event.accept()
|
||||
return
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
position = event.position().toPoint()
|
||||
item = self.itemAt(position)
|
||||
if (
|
||||
item is not None
|
||||
and item.data(Qt.UserRole) is not None
|
||||
and ProductOriginalDelegate.checkbox_hit_rect(self.visualItemRect(item)).contains(position)
|
||||
):
|
||||
event.accept()
|
||||
return
|
||||
super().mouseDoubleClickEvent(event)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.matches(QKeySequence.Paste):
|
||||
image = QApplication.clipboard().image()
|
||||
@@ -210,10 +285,19 @@ class ProductOriginalList(QListWidget):
|
||||
self.clipboardImage.emit(bytes(payload))
|
||||
return
|
||||
if event.key() in {Qt.Key_Delete, Qt.Key_Backspace}:
|
||||
checked_ids = self.checked_asset_ids()
|
||||
if checked_ids:
|
||||
self.deleteAssetsRequested.emit(checked_ids)
|
||||
return
|
||||
item = self.currentItem()
|
||||
if item is not None and item.data(Qt.UserRole) is not None:
|
||||
self.deleteRequested.emit(int(item.data(Qt.UserRole)))
|
||||
return
|
||||
if event.key() == Qt.Key_Space:
|
||||
item = self.currentItem()
|
||||
if item is not None and item.data(Qt.UserRole) is not None:
|
||||
self._set_item_checked(item, not self._item_checked(item))
|
||||
return
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def asset_ids(self):
|
||||
@@ -223,14 +307,102 @@ class ProductOriginalList(QListWidget):
|
||||
if self.item(row).data(Qt.UserRole) is not None
|
||||
]
|
||||
|
||||
def checked_asset_ids(self):
|
||||
return [
|
||||
int(self.item(row).data(Qt.UserRole))
|
||||
for row in range(self.count())
|
||||
if self.item(row).data(Qt.UserRole) is not None
|
||||
and self._item_checked(self.item(row))
|
||||
]
|
||||
|
||||
def set_checked_asset_ids(self, asset_ids):
|
||||
checked_ids = {int(asset_id) for asset_id in asset_ids or []}
|
||||
previous = self.blockSignals(True)
|
||||
try:
|
||||
for row in range(self.count()):
|
||||
item = self.item(row)
|
||||
asset_id = item.data(Qt.UserRole)
|
||||
if asset_id is not None:
|
||||
item.setData(
|
||||
ORIGINAL_CHECK_STATE_ROLE,
|
||||
Qt.Checked if int(asset_id) in checked_ids else Qt.Unchecked,
|
||||
)
|
||||
finally:
|
||||
self.blockSignals(previous)
|
||||
self.viewport().update()
|
||||
self.checkedAssetsChanged.emit(self.checked_asset_ids())
|
||||
|
||||
def select_all_assets(self):
|
||||
self.set_checked_asset_ids(self.asset_ids())
|
||||
|
||||
def invert_asset_checks(self):
|
||||
checked = set(self.checked_asset_ids())
|
||||
self.set_checked_asset_ids([asset_id for asset_id in self.asset_ids() if asset_id not in checked])
|
||||
|
||||
def clear_checks(self):
|
||||
self.set_checked_asset_ids([])
|
||||
|
||||
def content_column_count(self):
|
||||
step = max(1, self.gridSize().width() + self.spacing())
|
||||
available = max(1, self.viewport().width() - self.spacing())
|
||||
return max(1, (available + self.spacing()) // step)
|
||||
|
||||
def content_row_count(self):
|
||||
count = min(self.MAX_VISIBLE_ASSETS, max(1, self.count()))
|
||||
columns = self.content_column_count()
|
||||
return max(1, (count + columns - 1) // columns)
|
||||
|
||||
def update_content_height(self):
|
||||
rows = self.content_row_count()
|
||||
target = (
|
||||
rows * self.gridSize().height()
|
||||
+ (rows + 1) * self.spacing()
|
||||
+ 2 * self.frameWidth()
|
||||
)
|
||||
if target != self.height():
|
||||
self.setFixedHeight(target)
|
||||
|
||||
def _show_menu(self, position):
|
||||
item = self.itemAt(position)
|
||||
if item is None or item.data(Qt.UserRole) is None:
|
||||
return
|
||||
menu = QMenu(self)
|
||||
remove_action = menu.addAction("删除图片")
|
||||
if menu.exec(self.viewport().mapToGlobal(position)) is remove_action:
|
||||
self.deleteRequested.emit(int(item.data(Qt.UserRole)))
|
||||
actions = []
|
||||
for label, asset_ids in self.context_delete_options(int(item.data(Qt.UserRole))):
|
||||
actions.append((menu.addAction(label), asset_ids))
|
||||
selected = menu.exec(self.viewport().mapToGlobal(position))
|
||||
for action, asset_ids in actions:
|
||||
if selected is action:
|
||||
if len(asset_ids) == 1 and asset_ids[0] not in self.checked_asset_ids():
|
||||
self.deleteRequested.emit(asset_ids[0])
|
||||
else:
|
||||
self.deleteAssetsRequested.emit(asset_ids)
|
||||
return
|
||||
|
||||
def context_delete_options(self, clicked_asset_id):
|
||||
clicked_asset_id = int(clicked_asset_id)
|
||||
checked_ids = self.checked_asset_ids()
|
||||
if clicked_asset_id in checked_ids:
|
||||
if len(checked_ids) > 1:
|
||||
return [("删除选中的%d张图片…" % len(checked_ids), checked_ids)]
|
||||
return [("删除选中图片…", checked_ids)]
|
||||
options = [("删除这张图片…", [clicked_asset_id])]
|
||||
if checked_ids:
|
||||
label = (
|
||||
"删除选中的%d张图片…" % len(checked_ids)
|
||||
if len(checked_ids) > 1
|
||||
else "删除选中图片…"
|
||||
)
|
||||
options.append((label, checked_ids))
|
||||
return options
|
||||
|
||||
@staticmethod
|
||||
def _item_checked(item):
|
||||
return item.data(ORIGINAL_CHECK_STATE_ROLE) == Qt.Checked
|
||||
|
||||
@staticmethod
|
||||
def _set_item_checked(item, checked):
|
||||
item.setData(ORIGINAL_CHECK_STATE_ROLE, Qt.Checked if checked else Qt.Unchecked)
|
||||
|
||||
def _show_hover_remove(self, item):
|
||||
value = item.data(Qt.UserRole)
|
||||
@@ -407,6 +579,7 @@ class ProductSuiteTab(QWidget):
|
||||
self._next_key = 1
|
||||
self._next_serial = 1
|
||||
self._displayed_state = None
|
||||
self._original_list_context = None
|
||||
self._loading = False
|
||||
self._result_refresh_pending = False
|
||||
|
||||
@@ -569,9 +742,26 @@ class ProductSuiteTab(QWidget):
|
||||
self.original_count_label.setStyleSheet("color: #6b7280;")
|
||||
title_row.addWidget(self.original_count_label)
|
||||
title_row.addStretch(1)
|
||||
self.original_selected_label = QLabel("已选 0 张")
|
||||
self.original_selected_label.setObjectName("suiteOriginalSelectedLabel")
|
||||
self.original_selected_label.setStyleSheet("color: #6b7280;")
|
||||
title_row.addWidget(self.original_selected_label)
|
||||
self.select_all_originals_button = QToolButton()
|
||||
self.select_all_originals_button.setObjectName("suiteSelectAllOriginalsButton")
|
||||
self.select_all_originals_button.setText("全选")
|
||||
self.select_all_originals_button.setToolTip("选择当前商品的全部原图")
|
||||
self.select_all_originals_button.setAccessibleName("全选商品原图")
|
||||
self.select_all_originals_button.setMinimumSize(48, 28)
|
||||
title_row.addWidget(self.select_all_originals_button)
|
||||
self.invert_originals_button = QToolButton()
|
||||
self.invert_originals_button.setObjectName("suiteInvertOriginalsButton")
|
||||
self.invert_originals_button.setText("反选")
|
||||
self.invert_originals_button.setToolTip("反转当前商品原图的勾选状态")
|
||||
self.invert_originals_button.setAccessibleName("反选商品原图")
|
||||
self.invert_originals_button.setMinimumSize(48, 28)
|
||||
title_row.addWidget(self.invert_originals_button)
|
||||
layout.addLayout(title_row)
|
||||
self.original_list = ProductOriginalList()
|
||||
self.original_list.setFixedHeight(210)
|
||||
layout.addWidget(self.original_list)
|
||||
return frame
|
||||
|
||||
@@ -772,8 +962,16 @@ class ProductSuiteTab(QWidget):
|
||||
self.original_list.clipboardImage.connect(self.import_clipboard_image)
|
||||
self.original_list.orderChanged.connect(self.reorder_originals)
|
||||
self.original_list.deleteRequested.connect(self.delete_original)
|
||||
self.original_list.deleteAssetsRequested.connect(self.delete_originals)
|
||||
self.original_list.checkedAssetsChanged.connect(self._on_original_checks_changed)
|
||||
self.original_list.itemClicked.connect(self._on_original_clicked)
|
||||
self.original_list.itemDoubleClicked.connect(self._preview_original)
|
||||
self.select_all_originals_button.clicked.connect(
|
||||
lambda checked=False: self.original_list.select_all_assets()
|
||||
)
|
||||
self.invert_originals_button.clicked.connect(
|
||||
lambda checked=False: self.original_list.invert_asset_checks()
|
||||
)
|
||||
for combo in (
|
||||
self.platform_combo,
|
||||
self.country_combo,
|
||||
@@ -1132,31 +1330,61 @@ class ProductSuiteTab(QWidget):
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
def _refresh_originals(self, state):
|
||||
def _refresh_originals(self, state, *, preserve_checks=True):
|
||||
context = (state.key, state.project_id) if state is not None else None
|
||||
checked_ids = (
|
||||
set(self.original_list.checked_asset_ids())
|
||||
if preserve_checks and context == self._original_list_context
|
||||
else set()
|
||||
)
|
||||
previous = self.original_list.blockSignals(True)
|
||||
self.original_list.clear()
|
||||
assets = self._original_assets(state, include_missing=False)
|
||||
for index, asset in enumerate(assets, 1):
|
||||
label = "主图" if index == 1 else "参考%d" % (index - 1)
|
||||
item = QListWidgetItem(label)
|
||||
item.setData(Qt.UserRole, int(asset.id))
|
||||
if _asset_usable(asset):
|
||||
item.setIcon(QIcon(_image_pixmap(asset.local_path, QSize(82, 64))))
|
||||
item.setToolTip("%s,双击预览;拖动可调整顺序" % label)
|
||||
else:
|
||||
item.setIcon(QIcon(_placeholder_pixmap("待下载", QSize(82, 64))))
|
||||
item.setToolTip("%s尚未下载,单击后在后台拉取" % label)
|
||||
self.original_list.addItem(item)
|
||||
for index in range(len(assets) + 1, 7):
|
||||
label = "主图" if index == 1 else "参考%d" % (index - 1)
|
||||
item = QListWidgetItem(label)
|
||||
item.setData(Qt.UserRole, None)
|
||||
item.setIcon(QIcon(_placeholder_pixmap("添加", QSize(82, 64))))
|
||||
item.setToolTip("点击添加%s" % label)
|
||||
item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
|
||||
self.original_list.addItem(item)
|
||||
try:
|
||||
for index, asset in enumerate(assets, 1):
|
||||
label = "主图" if index == 1 else "参考%d" % (index - 1)
|
||||
item = QListWidgetItem(label)
|
||||
item.setData(Qt.UserRole, int(asset.id))
|
||||
item.setData(
|
||||
ORIGINAL_CHECK_STATE_ROLE,
|
||||
Qt.Checked if int(asset.id) in checked_ids else Qt.Unchecked,
|
||||
)
|
||||
if _asset_usable(asset):
|
||||
item.setIcon(QIcon(_image_pixmap(asset.local_path, QSize(82, 64))))
|
||||
item.setToolTip("%s,勾选可批量删除;双击预览;拖动可调整顺序" % label)
|
||||
else:
|
||||
item.setIcon(QIcon(_placeholder_pixmap("待下载", QSize(82, 64))))
|
||||
item.setToolTip("%s尚未下载;勾选可批量删除,单击缩略图后台拉取" % label)
|
||||
self.original_list.addItem(item)
|
||||
for index in range(len(assets) + 1, 7):
|
||||
label = "主图" if index == 1 else "参考%d" % (index - 1)
|
||||
item = QListWidgetItem(label)
|
||||
item.setData(Qt.UserRole, None)
|
||||
item.setIcon(QIcon(_placeholder_pixmap("添加", QSize(82, 64))))
|
||||
item.setToolTip("点击添加%s" % label)
|
||||
item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
|
||||
self.original_list.addItem(item)
|
||||
finally:
|
||||
self.original_list.blockSignals(previous)
|
||||
self._original_list_context = context
|
||||
self.original_list.update_content_height()
|
||||
self.original_list.viewport().update()
|
||||
self.original_count_label.setText("%d/16" % len(assets))
|
||||
self._refresh_original_selection_controls()
|
||||
self._refresh_totals(state)
|
||||
|
||||
def _on_original_checks_changed(self, asset_ids):
|
||||
self._refresh_original_selection_controls()
|
||||
|
||||
def _refresh_original_selection_controls(self):
|
||||
checked_count = len(self.original_list.checked_asset_ids())
|
||||
asset_count = len(self.original_list.asset_ids())
|
||||
self.original_selected_label.setText("已选 %d 张" % checked_count)
|
||||
state = self._displayed_state
|
||||
enabled = bool(asset_count) and state is not None and not state.generation_running()
|
||||
self.select_all_originals_button.setEnabled(enabled)
|
||||
self.invert_originals_button.setEnabled(enabled)
|
||||
|
||||
def _original_assets(self, state, *, include_missing=False):
|
||||
if state is None or state.project_id is None:
|
||||
return []
|
||||
@@ -1241,19 +1469,52 @@ class ProductSuiteTab(QWidget):
|
||||
self._refresh_originals(state)
|
||||
|
||||
def delete_original(self, asset_id):
|
||||
self.delete_originals([asset_id])
|
||||
|
||||
def delete_originals(self, asset_ids):
|
||||
state = self._displayed_state
|
||||
if state is None or state.generation_running():
|
||||
if state is None or state.project_id is None:
|
||||
self._status("当前商品没有可删除的原图", "warning")
|
||||
return
|
||||
if state.generation_running():
|
||||
self._status("生成中不能删除当前任务的商品原图", "warning")
|
||||
return
|
||||
if not self._confirm("删除商品原图", "确认从当前商品的原图列表移除这张图片吗?"):
|
||||
normalized_ids = []
|
||||
seen = set()
|
||||
for value in asset_ids or []:
|
||||
asset_id = int(value)
|
||||
if asset_id not in seen:
|
||||
seen.add(asset_id)
|
||||
normalized_ids.append(asset_id)
|
||||
if not normalized_ids:
|
||||
self._status("请先勾选要删除的商品原图", "warning")
|
||||
return
|
||||
downloading = set(state.download_queue) | set(state.downloads)
|
||||
if downloading.intersection(normalized_ids):
|
||||
self._message("暂不能删除商品原图", "选中的图片仍在后台下载,请等待下载结束后再删除。")
|
||||
return
|
||||
visible_ids = [int(asset.id) for asset in self._original_assets(state, include_missing=False)]
|
||||
message = (
|
||||
"确认从当前商品原图列表移除选中的%d张图片吗?\n\n"
|
||||
"此操作不会删除蝦皮线上图片,也不会删除本地源文件。"
|
||||
"以后重新拉取蝦皮主图时,线上仍存在的图片可能重新出现。"
|
||||
% len(normalized_ids)
|
||||
)
|
||||
if visible_ids and visible_ids[0] in normalized_ids:
|
||||
message += "\n\n选中内容包含当前主图,删除后下一张图片将成为主图。"
|
||||
if not self._confirm("删除商品原图", message, destructive=True):
|
||||
return
|
||||
try:
|
||||
image_studio.remove_asset_if_unused(asset_id, path=self.db_path)
|
||||
image_studio.remove_original_assets_if_unused(
|
||||
state.project_id,
|
||||
normalized_ids,
|
||||
path=self.db_path,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._message("不能删除商品原图", _user_error(exc))
|
||||
return
|
||||
self._refresh_originals(state)
|
||||
self._status("商品原图已移除", "success")
|
||||
self._refresh_originals(state, preserve_checks=False)
|
||||
self._status("已移除%d张商品原图" % len(normalized_ids), "success")
|
||||
|
||||
def reorder_originals(self, visible_ids):
|
||||
state = self._displayed_state
|
||||
@@ -1828,6 +2089,7 @@ class ProductSuiteTab(QWidget):
|
||||
self.item_id_edit.setEnabled(not generation_running and state.pull_worker is None)
|
||||
self.add_images_button.setEnabled(not generation_running and state.import_worker is None)
|
||||
self.original_list.setEnabled(not generation_running)
|
||||
self._refresh_original_selection_controls()
|
||||
for widget in (
|
||||
self.platform_combo,
|
||||
self.country_combo,
|
||||
|
||||
@@ -575,6 +575,84 @@ def remove_asset_if_unused(asset_id, path=None, conn=None):
|
||||
return asset
|
||||
|
||||
|
||||
def remove_original_assets_if_unused(project_id, asset_ids, path=None, conn=None):
|
||||
"""Atomically remove unreferenced original assets from one project.
|
||||
|
||||
Local files are intentionally retained. If any requested asset is invalid or
|
||||
referenced, no asset row is removed.
|
||||
"""
|
||||
|
||||
project_id = int(project_id)
|
||||
ordered_ids = []
|
||||
seen = set()
|
||||
for value in asset_ids or []:
|
||||
asset_id = int(value)
|
||||
if asset_id not in seen:
|
||||
seen.add(asset_id)
|
||||
ordered_ids.append(asset_id)
|
||||
if not ordered_ids:
|
||||
raise db.DbError("请选择要删除的商品原图")
|
||||
|
||||
placeholders = ",".join("?" for _ in ordered_ids)
|
||||
with _connection(conn, path) as database:
|
||||
with database:
|
||||
project = get_project(project_id, conn=database)
|
||||
if project is None:
|
||||
raise db.DbError("商品套图项目不存在或已删除")
|
||||
rows = database.execute(
|
||||
f"""
|
||||
SELECT * FROM image_studio_assets
|
||||
WHERE project_id = ? AND kind = ? AND id IN ({placeholders})
|
||||
""",
|
||||
[project_id, ASSET_KIND_ORIGINAL, *ordered_ids],
|
||||
).fetchall()
|
||||
by_id = {int(row["id"]): row for row in rows}
|
||||
if set(by_id) != set(ordered_ids):
|
||||
raise db.DbError("选中的商品原图不存在或不属于当前项目")
|
||||
|
||||
referenced_ids = [
|
||||
asset_id
|
||||
for asset_id in ordered_ids
|
||||
if asset_reference_counts(asset_id, conn=database)["total"]
|
||||
]
|
||||
if referenced_ids:
|
||||
raise db.DbError("选中的图片正在被生成任务或终选引用,不能移除")
|
||||
|
||||
database.execute(
|
||||
f"""
|
||||
DELETE FROM image_studio_assets
|
||||
WHERE project_id = ? AND kind = ? AND id IN ({placeholders})
|
||||
""",
|
||||
[project_id, ASSET_KIND_ORIGINAL, *ordered_ids],
|
||||
)
|
||||
remaining_rows = database.execute(
|
||||
"""
|
||||
SELECT id FROM image_studio_assets
|
||||
WHERE project_id = ? AND kind = ?
|
||||
ORDER BY source_order, id
|
||||
""",
|
||||
(project_id, ASSET_KIND_ORIGINAL),
|
||||
).fetchall()
|
||||
now = _now()
|
||||
for source_order, row in enumerate(remaining_rows, 1):
|
||||
database.execute(
|
||||
"""
|
||||
UPDATE image_studio_assets
|
||||
SET source_order = ?, updated_at = ?
|
||||
WHERE id = ? AND project_id = ? AND kind = ?
|
||||
""",
|
||||
(
|
||||
source_order,
|
||||
now,
|
||||
int(row["id"]),
|
||||
project_id,
|
||||
ASSET_KIND_ORIGINAL,
|
||||
),
|
||||
)
|
||||
|
||||
return [_row_to_dataclass(by_id[asset_id], ImageStudioAsset) for asset_id in ordered_ids]
|
||||
|
||||
|
||||
def sync_original_asset_urls(project_id, image_urls, path=None, conn=None, max_assets=16):
|
||||
"""Store the read-only Shopee main image URL snapshot as remote-only assets."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user