feat: implement queue panel with table, batch modes, stats, and export
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+26
-5
@@ -17,6 +17,7 @@ from version import APP_NAME, APP_VERSION
|
|||||||
from app.widgets.export_panel import ExportPanel
|
from app.widgets.export_panel import ExportPanel
|
||||||
from app.widgets.image_canvas import ImageCanvas
|
from app.widgets.image_canvas import ImageCanvas
|
||||||
from app.widgets.image_list_panel import ImageListPanel
|
from app.widgets.image_list_panel import ImageListPanel
|
||||||
|
from app.widgets.queue_panel import QueuePanel
|
||||||
from app.widgets.template_panel import TemplatePanel
|
from app.widgets.template_panel import TemplatePanel
|
||||||
from app.widgets.transform_panel import TransformPanel
|
from app.widgets.transform_panel import TransformPanel
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
self.setCentralWidget(root)
|
self.setCentralWidget(root)
|
||||||
self._apply_stylesheet()
|
self._apply_stylesheet()
|
||||||
|
self._connect_signals()
|
||||||
|
|
||||||
def _create_header(self):
|
def _create_header(self):
|
||||||
"""Top header: app name, version label, settings button."""
|
"""Top header: app name, version label, settings button."""
|
||||||
@@ -210,12 +212,11 @@ class MainWindow(QMainWindow):
|
|||||||
self._queue_body.setObjectName("queueBody")
|
self._queue_body.setObjectName("queueBody")
|
||||||
|
|
||||||
body_layout = QVBoxLayout(self._queue_body)
|
body_layout = QVBoxLayout(self._queue_body)
|
||||||
body_layout.setContentsMargins(12, 8, 12, 8)
|
body_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
body_layout.setSpacing(0)
|
||||||
|
|
||||||
placeholder = QLabel("合成队列(实现中)")
|
self.queue_panel = QueuePanel()
|
||||||
placeholder.setAlignment(Qt.AlignCenter)
|
body_layout.addWidget(self.queue_panel)
|
||||||
placeholder.setObjectName("queuePlaceholder")
|
|
||||||
body_layout.addWidget(placeholder)
|
|
||||||
|
|
||||||
self._queue_expanded = True
|
self._queue_expanded = True
|
||||||
return self._queue_body
|
return self._queue_body
|
||||||
@@ -240,6 +241,26 @@ class MainWindow(QMainWindow):
|
|||||||
self._queue_container.setFixedHeight(36)
|
self._queue_container.setFixedHeight(36)
|
||||||
self._collapse_btn.setText("▼")
|
self._collapse_btn.setText("▼")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Signal wiring
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _connect_signals(self):
|
||||||
|
"""Connect inter-panel signals needed for the queue workflow."""
|
||||||
|
# Asset lists → queue rebuilds automatically
|
||||||
|
self.image_list_panel.garments_changed.connect(self.queue_panel.set_garments)
|
||||||
|
self.image_list_panel.prints_changed.connect(self.queue_panel.set_prints)
|
||||||
|
# Queue row selection → preview canvas
|
||||||
|
self.queue_panel.item_activated.connect(self._on_queue_item_activated)
|
||||||
|
|
||||||
|
def _on_queue_item_activated(self, item):
|
||||||
|
"""Load garment and print from a selected queue item into the canvas."""
|
||||||
|
self.image_canvas.load_garment(str(item.garment.path))
|
||||||
|
self.image_canvas.load_print(str(item.print_asset.path))
|
||||||
|
if item.transform:
|
||||||
|
self.image_canvas.set_transform(item.transform)
|
||||||
|
self.transform_panel.set_transform(item.transform)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Style (ref: docs/07-ui-design.md §10)
|
# Style (ref: docs/07-ui-design.md §10)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,450 @@
|
|||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
|
from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, Signal
|
||||||
|
from PySide6.QtGui import QColor
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
|
QComboBox,
|
||||||
|
QHeaderView,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QPushButton,
|
||||||
|
QSizePolicy,
|
||||||
|
QStyle,
|
||||||
|
QStyledItemDelegate,
|
||||||
|
QStyleOptionProgressBar,
|
||||||
|
QTableView,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
from core.composer import compose
|
||||||
|
from core.models import BatchMode, ExportOptions, ImageAsset, TransformState
|
||||||
|
from services.file_service import get_output_dir, make_safe_output_path
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# ── column layout ─────────────────────────────────────────────────────────────
|
||||||
|
_COL_NUM = 0
|
||||||
|
_COL_GARMENT = 1
|
||||||
|
_COL_PRINT = 2
|
||||||
|
_COL_FOLDER = 3
|
||||||
|
_COL_PARAM = 4
|
||||||
|
_COL_STATUS = 5
|
||||||
|
_COL_OUTPUT = 6
|
||||||
|
_HEADERS = ["#", "衣服", "印花", "来源文件夹", "参数", "状态", "进度/输出文件"]
|
||||||
|
|
||||||
|
_STATUS_COLOR = {
|
||||||
|
"已完成": QColor("#107c10"),
|
||||||
|
"导出中": QColor("#0078d4"),
|
||||||
|
"失败": QColor("#c42b1c"),
|
||||||
|
"待导出": QColor("#555555"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── data model ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class QueueItem:
|
||||||
|
"""One entry in the composition queue."""
|
||||||
|
garment: ImageAsset
|
||||||
|
print_asset: ImageAsset
|
||||||
|
transform: Optional[TransformState] = None # None → use template transform
|
||||||
|
param_source: str = "模板" # "模板" | "已微调"
|
||||||
|
status: str = "待导出" # "待导出"|"导出中"|"已完成"|"失败"
|
||||||
|
progress: float = 0.0
|
||||||
|
output_path: Optional[str] = None
|
||||||
|
error: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_pairs(
|
||||||
|
garments: List[ImageAsset],
|
||||||
|
prints: List[ImageAsset],
|
||||||
|
mode: BatchMode,
|
||||||
|
) -> List[Tuple[ImageAsset, ImageAsset]]:
|
||||||
|
if not garments or not prints:
|
||||||
|
return []
|
||||||
|
if mode == BatchMode.ONE_TO_ONE:
|
||||||
|
return list(zip(garments, prints))
|
||||||
|
if mode == BatchMode.MANY_GARMENTS:
|
||||||
|
p = prints[0]
|
||||||
|
return [(g, p) for g in garments]
|
||||||
|
if mode == BatchMode.MANY_PRINTS:
|
||||||
|
g = garments[0]
|
||||||
|
return [(g, p) for p in prints]
|
||||||
|
# FULL_COMBO
|
||||||
|
return [(g, p) for g in garments for p in prints]
|
||||||
|
|
||||||
|
|
||||||
|
class _QueueModel(QAbstractTableModel):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._items: List[QueueItem] = []
|
||||||
|
|
||||||
|
def set_items(self, items: List[QueueItem]):
|
||||||
|
self.beginResetModel()
|
||||||
|
self._items = list(items)
|
||||||
|
self.endResetModel()
|
||||||
|
|
||||||
|
def rowCount(self, parent=QModelIndex()):
|
||||||
|
return len(self._items)
|
||||||
|
|
||||||
|
def columnCount(self, parent=QModelIndex()):
|
||||||
|
return len(_HEADERS)
|
||||||
|
|
||||||
|
def headerData(self, section, orientation, role=Qt.DisplayRole):
|
||||||
|
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
|
||||||
|
return _HEADERS[section]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def data(self, index, role=Qt.DisplayRole):
|
||||||
|
if not index.isValid():
|
||||||
|
return None
|
||||||
|
item = self._items[index.row()]
|
||||||
|
col = index.column()
|
||||||
|
|
||||||
|
if role == Qt.DisplayRole:
|
||||||
|
if col == _COL_NUM:
|
||||||
|
return str(index.row() + 1)
|
||||||
|
if col == _COL_GARMENT:
|
||||||
|
return Path(item.garment.path).name
|
||||||
|
if col == _COL_PRINT:
|
||||||
|
return Path(item.print_asset.path).name
|
||||||
|
if col == _COL_FOLDER:
|
||||||
|
return Path(item.garment.path).parent.name
|
||||||
|
if col == _COL_PARAM:
|
||||||
|
return item.param_source
|
||||||
|
if col == _COL_STATUS:
|
||||||
|
return item.status
|
||||||
|
if col == _COL_OUTPUT:
|
||||||
|
if item.status == "已完成" and item.output_path:
|
||||||
|
return Path(item.output_path).name
|
||||||
|
if item.status == "失败":
|
||||||
|
return item.error or "未知错误"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if role == Qt.UserRole and col == _COL_OUTPUT:
|
||||||
|
return item.progress if item.status == "导出中" else None
|
||||||
|
|
||||||
|
if role == Qt.ForegroundRole and col == _COL_STATUS:
|
||||||
|
return _STATUS_COLOR.get(item.status)
|
||||||
|
|
||||||
|
if role == Qt.TextAlignmentRole and col == _COL_NUM:
|
||||||
|
return int(Qt.AlignCenter)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_item(self, row: int) -> Optional[QueueItem]:
|
||||||
|
return self._items[row] if 0 <= row < len(self._items) else None
|
||||||
|
|
||||||
|
def update_row(self, row: int):
|
||||||
|
if 0 <= row < len(self._items):
|
||||||
|
self.dataChanged.emit(
|
||||||
|
self.index(row, 0),
|
||||||
|
self.index(row, len(_HEADERS) - 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _ProgressDelegate(QStyledItemDelegate):
|
||||||
|
"""Paints a progress bar in the output column when status is '导出中'."""
|
||||||
|
|
||||||
|
def paint(self, painter, option, index):
|
||||||
|
progress = index.data(Qt.UserRole)
|
||||||
|
if progress is not None:
|
||||||
|
opt = QStyleOptionProgressBar()
|
||||||
|
opt.rect = option.rect
|
||||||
|
opt.minimum = 0
|
||||||
|
opt.maximum = 100
|
||||||
|
opt.progress = max(0, min(100, int(progress * 100)))
|
||||||
|
opt.text = "{}%".format(opt.progress)
|
||||||
|
opt.textVisible = True
|
||||||
|
QApplication.style().drawControl(QStyle.CE_ProgressBar, opt, painter)
|
||||||
|
else:
|
||||||
|
super().paint(painter, option, index)
|
||||||
|
|
||||||
|
|
||||||
|
# ── main widget ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class QueuePanel(QWidget):
|
||||||
|
"""Bottom queue: batch mode selector, stats, action buttons, item table."""
|
||||||
|
|
||||||
|
item_activated = Signal(object) # QueueItem when a row is selected
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._garments: List[ImageAsset] = []
|
||||||
|
self._prints: List[ImageAsset] = []
|
||||||
|
self._template_transform: Optional[TransformState] = None
|
||||||
|
self._export_options: ExportOptions = ExportOptions()
|
||||||
|
self._running = False
|
||||||
|
self._setup_ui()
|
||||||
|
self._apply_stylesheet()
|
||||||
|
|
||||||
|
# ── UI construction ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
col = QVBoxLayout(self)
|
||||||
|
col.setContentsMargins(0, 0, 0, 0)
|
||||||
|
col.setSpacing(0)
|
||||||
|
col.addWidget(self._make_ctrl_bar())
|
||||||
|
col.addWidget(self._make_table(), 1)
|
||||||
|
|
||||||
|
def _make_ctrl_bar(self) -> QWidget:
|
||||||
|
bar = QWidget()
|
||||||
|
bar.setObjectName("queueCtrlBar")
|
||||||
|
row = QHBoxLayout(bar)
|
||||||
|
row.setContentsMargins(8, 4, 8, 4)
|
||||||
|
row.setSpacing(8)
|
||||||
|
|
||||||
|
mode_lbl = QLabel("批量模式:")
|
||||||
|
mode_lbl.setObjectName("queueCtrlLabel")
|
||||||
|
|
||||||
|
self._mode_combo = QComboBox()
|
||||||
|
self._mode_combo.setObjectName("queueModeCombo")
|
||||||
|
self._mode_combo.addItem("全组合(矩阵)", BatchMode.FULL_COMBO)
|
||||||
|
self._mode_combo.addItem("多衣服 × 单印花", BatchMode.MANY_GARMENTS)
|
||||||
|
self._mode_combo.addItem("单衣服 × 多印花", BatchMode.MANY_PRINTS)
|
||||||
|
self._mode_combo.addItem("一一匹配", BatchMode.ONE_TO_ONE)
|
||||||
|
self._mode_combo.currentIndexChanged.connect(self._rebuild_queue)
|
||||||
|
|
||||||
|
self._stats_lbl = QLabel("共 0 项")
|
||||||
|
self._stats_lbl.setObjectName("queueStatsLabel")
|
||||||
|
self._stats_lbl.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
||||||
|
|
||||||
|
self._reset_btn = QPushButton("重置全部")
|
||||||
|
self._reset_btn.setObjectName("queueActionBtn")
|
||||||
|
self._reset_btn.clicked.connect(self._reset_all)
|
||||||
|
|
||||||
|
self._export_sel_btn = QPushButton("导出选中")
|
||||||
|
self._export_sel_btn.setObjectName("queueActionBtn")
|
||||||
|
self._export_sel_btn.clicked.connect(self._export_selected)
|
||||||
|
|
||||||
|
self._batch_btn = QPushButton("开始批量导出")
|
||||||
|
self._batch_btn.setObjectName("queueBatchBtn")
|
||||||
|
self._batch_btn.clicked.connect(self._toggle_batch)
|
||||||
|
|
||||||
|
row.addWidget(mode_lbl)
|
||||||
|
row.addWidget(self._mode_combo)
|
||||||
|
row.addWidget(self._stats_lbl, 1)
|
||||||
|
row.addWidget(self._reset_btn)
|
||||||
|
row.addWidget(self._export_sel_btn)
|
||||||
|
row.addWidget(self._batch_btn)
|
||||||
|
return bar
|
||||||
|
|
||||||
|
def _make_table(self) -> QTableView:
|
||||||
|
self._model = _QueueModel()
|
||||||
|
|
||||||
|
self._table = QTableView()
|
||||||
|
self._table.setObjectName("queueTable")
|
||||||
|
self._table.setModel(self._model)
|
||||||
|
self._table.setItemDelegateForColumn(_COL_OUTPUT, _ProgressDelegate())
|
||||||
|
self._table.setSelectionBehavior(QTableView.SelectRows)
|
||||||
|
self._table.setSelectionMode(QTableView.SingleSelection)
|
||||||
|
self._table.setAlternatingRowColors(True)
|
||||||
|
self._table.setEditTriggers(QTableView.NoEditTriggers)
|
||||||
|
self._table.verticalHeader().setVisible(False)
|
||||||
|
self._table.verticalHeader().setDefaultSectionSize(24)
|
||||||
|
|
||||||
|
hdr = self._table.horizontalHeader()
|
||||||
|
hdr.setSectionResizeMode(QHeaderView.Interactive)
|
||||||
|
hdr.setStretchLastSection(True)
|
||||||
|
|
||||||
|
self._table.setColumnWidth(_COL_NUM, 36)
|
||||||
|
self._table.setColumnWidth(_COL_GARMENT, 130)
|
||||||
|
self._table.setColumnWidth(_COL_PRINT, 130)
|
||||||
|
self._table.setColumnWidth(_COL_FOLDER, 100)
|
||||||
|
self._table.setColumnWidth(_COL_PARAM, 56)
|
||||||
|
self._table.setColumnWidth(_COL_STATUS, 66)
|
||||||
|
|
||||||
|
self._table.selectionModel().currentRowChanged.connect(self._on_row_changed)
|
||||||
|
return self._table
|
||||||
|
|
||||||
|
# ── public API ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def set_garments(self, garments: List[ImageAsset]):
|
||||||
|
self._garments = garments
|
||||||
|
self._rebuild_queue()
|
||||||
|
|
||||||
|
def set_prints(self, prints: List[ImageAsset]):
|
||||||
|
self._prints = prints
|
||||||
|
self._rebuild_queue()
|
||||||
|
|
||||||
|
def set_transform(self, state: TransformState):
|
||||||
|
"""Store the template transform applied to items that have not been fine-tuned."""
|
||||||
|
self._template_transform = state
|
||||||
|
|
||||||
|
def set_export_options(self, options: ExportOptions):
|
||||||
|
self._export_options = options
|
||||||
|
|
||||||
|
# ── internal slots ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _rebuild_queue(self):
|
||||||
|
mode = self._mode_combo.currentData()
|
||||||
|
sel_g = [g for g in self._garments if g.selected]
|
||||||
|
sel_p = [p for p in self._prints if p.selected]
|
||||||
|
pairs = _generate_pairs(sel_g, sel_p, mode)
|
||||||
|
self._model.set_items([QueueItem(garment=g, print_asset=p) for g, p in pairs])
|
||||||
|
self._update_stats()
|
||||||
|
|
||||||
|
def _update_stats(self):
|
||||||
|
items = self._model._items
|
||||||
|
n = len(items)
|
||||||
|
if n == 0:
|
||||||
|
self._stats_lbl.setText("共 0 项")
|
||||||
|
return
|
||||||
|
done = sum(1 for i in items if i.status == "已完成")
|
||||||
|
running = sum(1 for i in items if i.status == "导出中")
|
||||||
|
failed = sum(1 for i in items if i.status == "失败")
|
||||||
|
pending = sum(1 for i in items if i.status == "待导出")
|
||||||
|
self._stats_lbl.setText(
|
||||||
|
"共 {} 项 · 完成 {} · 进行 {} · 失败 {} · 待导出 {}".format(
|
||||||
|
n, done, running, failed, pending
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_row_changed(self, current, _previous):
|
||||||
|
item = self._model.get_item(current.row())
|
||||||
|
if item:
|
||||||
|
self.item_activated.emit(item)
|
||||||
|
|
||||||
|
def _reset_all(self):
|
||||||
|
for item in self._model._items:
|
||||||
|
item.status = "待导出"
|
||||||
|
item.progress = 0.0
|
||||||
|
item.output_path = None
|
||||||
|
item.error = None
|
||||||
|
self._model.set_items(list(self._model._items))
|
||||||
|
self._update_stats()
|
||||||
|
|
||||||
|
def _export_selected(self):
|
||||||
|
rows = self._table.selectionModel().selectedRows()
|
||||||
|
if not rows:
|
||||||
|
return
|
||||||
|
row = rows[0].row()
|
||||||
|
item = self._model.get_item(row)
|
||||||
|
if item and item.status == "待导出":
|
||||||
|
self._export_item(row, item)
|
||||||
|
self._update_stats()
|
||||||
|
|
||||||
|
def _toggle_batch(self):
|
||||||
|
if self._running:
|
||||||
|
self._running = False
|
||||||
|
else:
|
||||||
|
self._start_batch()
|
||||||
|
|
||||||
|
def _start_batch(self):
|
||||||
|
self._running = True
|
||||||
|
self._batch_btn.setText("停止")
|
||||||
|
items = self._model._items
|
||||||
|
|
||||||
|
for i, item in enumerate(items):
|
||||||
|
if not self._running:
|
||||||
|
break
|
||||||
|
if item.status != "待导出":
|
||||||
|
continue
|
||||||
|
self._export_item(i, item)
|
||||||
|
self._update_stats()
|
||||||
|
QApplication.processEvents()
|
||||||
|
|
||||||
|
self._running = False
|
||||||
|
self._batch_btn.setText("开始批量导出")
|
||||||
|
|
||||||
|
def _export_item(self, row: int, item: QueueItem):
|
||||||
|
transform = item.transform or self._template_transform
|
||||||
|
if not transform:
|
||||||
|
item.status = "失败"
|
||||||
|
item.error = "无印花参数(请先在画布中调整印花位置)"
|
||||||
|
self._model.update_row(row)
|
||||||
|
return
|
||||||
|
|
||||||
|
out_dir = (
|
||||||
|
self._export_options.output_dir
|
||||||
|
if self._export_options.output_dir
|
||||||
|
else str(get_output_dir())
|
||||||
|
)
|
||||||
|
fmt = self._export_options.output_format or "PNG"
|
||||||
|
output_path = make_safe_output_path(
|
||||||
|
out_dir, item.garment.path, item.print_asset.path, fmt
|
||||||
|
)
|
||||||
|
|
||||||
|
item.status = "导出中"
|
||||||
|
item.progress = 0.0
|
||||||
|
self._model.update_row(row)
|
||||||
|
QApplication.processEvents()
|
||||||
|
|
||||||
|
result = compose(
|
||||||
|
item.garment.path, item.print_asset.path,
|
||||||
|
transform, self._export_options, output_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
item.status = "已完成"
|
||||||
|
item.progress = 1.0
|
||||||
|
item.output_path = str(result.output_path)
|
||||||
|
logger.info("Queue export OK: %s", result.output_path)
|
||||||
|
else:
|
||||||
|
item.status = "失败"
|
||||||
|
item.error = result.error
|
||||||
|
logger.error("Queue export failed: %s", result.error)
|
||||||
|
|
||||||
|
self._model.update_row(row)
|
||||||
|
|
||||||
|
# ── stylesheet ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _apply_stylesheet(self):
|
||||||
|
self.setStyleSheet("""
|
||||||
|
#queueCtrlBar {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
#queueCtrlLabel {
|
||||||
|
font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #444444;
|
||||||
|
}
|
||||||
|
#queueModeCombo {
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid #d6d6d6;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
#queueStatsLabel {
|
||||||
|
font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
#queueActionBtn {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border: 1px solid #d6d6d6;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
#queueActionBtn:hover { background: #e0e0e0; }
|
||||||
|
#queueBatchBtn {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 3px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #0078d4;
|
||||||
|
}
|
||||||
|
#queueBatchBtn:hover { background-color: #106ebe; }
|
||||||
|
#queueBatchBtn:pressed { background-color: #005a9e; }
|
||||||
|
#queueTable {
|
||||||
|
border: none;
|
||||||
|
background-color: #ffffff;
|
||||||
|
alternate-background-color: #f9f9f9;
|
||||||
|
font-size: 12px;
|
||||||
|
gridline-color: #eeeeee;
|
||||||
|
}
|
||||||
|
#queueTable::item { padding: 2px 4px; }
|
||||||
|
#queueTable::item:selected {
|
||||||
|
background-color: #cce4f7;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
""")
|
||||||
@@ -424,24 +424,24 @@
|
|||||||
|
|
||||||
任务:
|
任务:
|
||||||
|
|
||||||
- [ ] 实现底部合成队列表格(建议使用 `QTableView` + 模型视图结构)
|
- [x] 实现底部合成队列表格(建议使用 `QTableView` + 模型视图结构)
|
||||||
- [ ] 实现队列头部批量模式切换:多衣服×单印花 / 单衣服×多印花 / 一一匹配 / 全组合(矩阵)
|
- [x] 实现队列头部批量模式切换:多衣服×单印花 / 单衣服×多印花 / 一一匹配 / 全组合(矩阵)
|
||||||
- [ ] 实现队列统计显示(共 N 项 · 完成 · 进行 · 失败 · 待导出)
|
- [x] 实现队列统计显示(共 N 项 · 完成 · 进行 · 失败 · 待导出)
|
||||||
- [ ] 实现「重置全部」按钮
|
- [x] 实现「重置全部」按钮
|
||||||
- [ ] 实现「导出选中」按钮
|
- [x] 实现「导出选中」按钮
|
||||||
- [ ] 实现「开始批量导出」按钮(导出中切换为停止/暂停)
|
- [x] 实现「开始批量导出」按钮(导出中切换为停止/暂停)
|
||||||
- [ ] 实现队列折叠/展开
|
- [x] 实现队列折叠/展开
|
||||||
- [ ] 显示任务状态(已完成 / 待导出 / 导出中 / 失败)
|
- [x] 显示任务状态(已完成 / 待导出 / 导出中 / 失败)
|
||||||
- [ ] 导出中任务显示进度条
|
- [x] 导出中任务显示进度条
|
||||||
- [ ] 显示失败原因
|
- [x] 显示失败原因
|
||||||
- [ ] 支持选中队列项并更新预览区和参数面板
|
- [x] 支持选中队列项并更新预览区和参数面板
|
||||||
|
|
||||||
验收:
|
验收:
|
||||||
|
|
||||||
- [ ] 队列统计显示总数、完成、进行、失败、待导出
|
- [x] 队列统计显示总数、完成、进行、失败、待导出
|
||||||
- [ ] 导出中任务显示进度条
|
- [x] 导出中任务显示进度条
|
||||||
- [ ] 失败行显示可理解原因
|
- [x] 失败行显示可理解原因
|
||||||
- [ ] 选中队列项可以加载对应状态
|
- [x] 选中队列项可以加载对应状态
|
||||||
|
|
||||||
## 14. 微调状态
|
## 14. 微调状态
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user