feat: implement export panel UI with output settings and single export
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,296 @@
|
||||
from PySide6.QtWidgets import QWidget
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
QFileDialog,
|
||||
QFormLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from core.composer import compose
|
||||
from core.models import ExportOptions, TransformState
|
||||
from services.file_service import get_output_dir, make_safe_output_path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExportPanel(QWidget):
|
||||
pass
|
||||
"""Right-panel section: output settings and export / template actions."""
|
||||
|
||||
apply_as_template = Signal() # "应用为模板" clicked
|
||||
reset_to_template = Signal() # "重置为模板" clicked
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._garment_path: Optional[str] = None
|
||||
self._print_path: Optional[str] = None
|
||||
self._transform: Optional[TransformState] = None
|
||||
self._setup_ui()
|
||||
self._apply_stylesheet()
|
||||
|
||||
# ── UI construction ──────────────────────────────────────────────────────
|
||||
|
||||
def _setup_ui(self):
|
||||
col = QVBoxLayout(self)
|
||||
col.setContentsMargins(0, 0, 0, 0)
|
||||
col.setSpacing(0)
|
||||
|
||||
header = QLabel("输出设置")
|
||||
header.setObjectName("exportSectionHeader")
|
||||
col.addWidget(header)
|
||||
|
||||
body = QWidget()
|
||||
body.setObjectName("exportBody")
|
||||
body_col = QVBoxLayout(body)
|
||||
body_col.setContentsMargins(10, 8, 10, 10)
|
||||
body_col.setSpacing(8)
|
||||
|
||||
body_col.addLayout(self._make_dir_row())
|
||||
body_col.addLayout(self._make_format_form())
|
||||
body_col.addWidget(self._make_hint_label())
|
||||
body_col.addWidget(self._make_export_btn())
|
||||
body_col.addLayout(self._make_secondary_btns())
|
||||
|
||||
col.addWidget(body)
|
||||
|
||||
def _make_dir_row(self) -> QHBoxLayout:
|
||||
row = QHBoxLayout()
|
||||
row.setSpacing(4)
|
||||
|
||||
self._dir_input = QLineEdit()
|
||||
self._dir_input.setObjectName("exportDirInput")
|
||||
self._dir_input.setReadOnly(True)
|
||||
self._dir_input.setPlaceholderText("输出目录")
|
||||
self._dir_input.setText(str(get_output_dir()))
|
||||
|
||||
browse_btn = QPushButton("浏览…")
|
||||
browse_btn.setObjectName("exportBrowseBtn")
|
||||
browse_btn.setFixedWidth(52)
|
||||
browse_btn.clicked.connect(self._browse_output_dir)
|
||||
|
||||
row.addWidget(self._dir_input, 1)
|
||||
row.addWidget(browse_btn)
|
||||
return row
|
||||
|
||||
def _make_format_form(self) -> QFormLayout:
|
||||
form = QFormLayout()
|
||||
form.setSpacing(6)
|
||||
form.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
|
||||
self._format_combo = QComboBox()
|
||||
self._format_combo.setObjectName("exportCombo")
|
||||
self._format_combo.addItem("PNG", "PNG")
|
||||
self._format_combo.addItem("JPG", "JPG")
|
||||
self._format_combo.currentIndexChanged.connect(self._on_format_changed)
|
||||
form.addRow("格式", self._format_combo)
|
||||
|
||||
self._quality_combo = QComboBox()
|
||||
self._quality_combo.setObjectName("exportCombo")
|
||||
for label, val in [("最高 (95)", 95), ("高 (90)", 90), ("中 (80)", 80), ("低 (60)", 60)]:
|
||||
self._quality_combo.addItem(label, val)
|
||||
self._quality_combo.setCurrentIndex(1) # default: 高 (90)
|
||||
self._quality_combo.setEnabled(False) # enabled only for JPG
|
||||
form.addRow("质量", self._quality_combo)
|
||||
|
||||
return form
|
||||
|
||||
def _make_hint_label(self) -> QLabel:
|
||||
self._hint_lbl = QLabel("加载衣服和印花图片后即可导出。")
|
||||
self._hint_lbl.setObjectName("exportHint")
|
||||
self._hint_lbl.setWordWrap(True)
|
||||
return self._hint_lbl
|
||||
|
||||
def _make_export_btn(self) -> QPushButton:
|
||||
self._export_btn = QPushButton("导出当前单张")
|
||||
self._export_btn.setObjectName("exportPrimaryBtn")
|
||||
self._export_btn.setEnabled(False)
|
||||
self._export_btn.clicked.connect(self._do_export)
|
||||
return self._export_btn
|
||||
|
||||
def _make_secondary_btns(self) -> QHBoxLayout:
|
||||
row = QHBoxLayout()
|
||||
row.setSpacing(6)
|
||||
|
||||
apply_btn = QPushButton("应用为模板")
|
||||
apply_btn.setObjectName("exportSecBtn")
|
||||
apply_btn.setToolTip("将当前印花参数另存为新模板")
|
||||
apply_btn.clicked.connect(lambda: self.apply_as_template.emit())
|
||||
|
||||
reset_btn = QPushButton("重置为模板")
|
||||
reset_btn.setObjectName("exportSecBtn")
|
||||
reset_btn.setToolTip("将印花参数还原为所选模板的默认值")
|
||||
reset_btn.clicked.connect(lambda: self.reset_to_template.emit())
|
||||
|
||||
row.addWidget(apply_btn)
|
||||
row.addWidget(reset_btn)
|
||||
return row
|
||||
|
||||
# ── public API ───────────────────────────────────────────────────────────
|
||||
|
||||
def set_garment(self, path: Optional[str]):
|
||||
"""Update current garment path; enables export button when both paths set."""
|
||||
self._garment_path = path
|
||||
self._update_export_btn()
|
||||
|
||||
def set_print(self, path: Optional[str]):
|
||||
"""Update current print path; enables export button when both paths set."""
|
||||
self._print_path = path
|
||||
self._update_export_btn()
|
||||
|
||||
def set_transform(self, state: TransformState):
|
||||
"""Store current print transform (used on export)."""
|
||||
self._transform = state
|
||||
|
||||
def set_hint(self, text: str):
|
||||
"""Update the operation-scope hint label."""
|
||||
self._hint_lbl.setText(text)
|
||||
|
||||
# ── slot handlers ────────────────────────────────────────────────────────
|
||||
|
||||
def _on_format_changed(self, _index: int):
|
||||
fmt = self._format_combo.currentData()
|
||||
self._quality_combo.setEnabled(fmt == "JPG")
|
||||
|
||||
def _browse_output_dir(self):
|
||||
folder = QFileDialog.getExistingDirectory(
|
||||
self, "选择输出目录", self._dir_input.text()
|
||||
)
|
||||
if folder:
|
||||
self._dir_input.setText(folder)
|
||||
|
||||
def _update_export_btn(self):
|
||||
self._export_btn.setEnabled(
|
||||
bool(self._garment_path and self._print_path)
|
||||
)
|
||||
|
||||
def _do_export(self):
|
||||
if not self._garment_path or not self._print_path:
|
||||
return
|
||||
if not self._transform:
|
||||
QMessageBox.information(self, "提示", "当前没有可导出的印花参数。")
|
||||
return
|
||||
|
||||
out_dir = self._dir_input.text().strip() or str(get_output_dir())
|
||||
fmt = self._format_combo.currentData()
|
||||
quality = self._quality_combo.currentData() if fmt == "JPG" else 95
|
||||
|
||||
out_dir_path = Path(out_dir)
|
||||
try:
|
||||
out_dir_path.mkdir(parents=True, exist_ok=True)
|
||||
except OSError as exc:
|
||||
logger.error("Output dir not writable: %s", exc)
|
||||
QMessageBox.warning(
|
||||
self, "目录错误",
|
||||
"输出目录无法创建:\n{}".format(exc),
|
||||
)
|
||||
return
|
||||
|
||||
options = ExportOptions(
|
||||
output_dir=out_dir,
|
||||
output_format=fmt,
|
||||
quality=quality,
|
||||
)
|
||||
output_path = make_safe_output_path(
|
||||
out_dir, self._garment_path, self._print_path, fmt
|
||||
)
|
||||
|
||||
result = compose(
|
||||
self._garment_path, self._print_path,
|
||||
self._transform, options, output_path,
|
||||
)
|
||||
|
||||
if result.success:
|
||||
logger.info("Exported: %s", result.output_path)
|
||||
QMessageBox.information(
|
||||
self, "导出成功",
|
||||
"已导出:\n{}".format(result.output_path),
|
||||
)
|
||||
else:
|
||||
logger.error("Export failed: %s", result.error)
|
||||
QMessageBox.warning(
|
||||
self, "导出失败",
|
||||
"导出失败:\n{}".format(result.error),
|
||||
)
|
||||
|
||||
# ── stylesheet ───────────────────────────────────────────────────────────
|
||||
|
||||
def _apply_stylesheet(self):
|
||||
self.setStyleSheet("""
|
||||
#exportSectionHeader {
|
||||
font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #555555;
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #eeeeee;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
#exportBody {
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
#exportDirInput {
|
||||
font-size: 11px;
|
||||
border: 1px solid #d6d6d6;
|
||||
border-radius: 3px;
|
||||
padding: 2px 4px;
|
||||
background: #fafafa;
|
||||
color: #444444;
|
||||
}
|
||||
#exportBrowseBtn {
|
||||
font-size: 12px;
|
||||
padding: 3px 6px;
|
||||
border: 1px solid #d6d6d6;
|
||||
border-radius: 3px;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
#exportBrowseBtn:hover { background: #e0e0e0; }
|
||||
#exportCombo {
|
||||
font-size: 12px;
|
||||
border: 1px solid #d6d6d6;
|
||||
border-radius: 3px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
#exportHint {
|
||||
font-size: 11px;
|
||||
color: #777777;
|
||||
background: #fffbe6;
|
||||
border: 1px solid #ffe58f;
|
||||
border-radius: 3px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
#exportPrimaryBtn {
|
||||
font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
background-color: #0078d4;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 7px 0;
|
||||
}
|
||||
#exportPrimaryBtn:hover { background-color: #106ebe; }
|
||||
#exportPrimaryBtn:pressed { background-color: #005a9e; }
|
||||
#exportPrimaryBtn:disabled {
|
||||
background-color: #c8c8c8;
|
||||
color: #888888;
|
||||
}
|
||||
#exportSecBtn {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #d6d6d6;
|
||||
border-radius: 3px;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
#exportSecBtn:hover { background: #e0e0e0; }
|
||||
#exportSecBtn:pressed { background: #d0d0d0; }
|
||||
""")
|
||||
|
||||
@@ -398,21 +398,21 @@
|
||||
|
||||
任务:
|
||||
|
||||
- [ ] 先读取 `src/app/widgets/export_panel.py` 现有内容
|
||||
- [ ] 完善 `src/app/widgets/export_panel.py`
|
||||
- [ ] 实现输出目录选择
|
||||
- [ ] 实现输出格式选择
|
||||
- [ ] 实现质量选择
|
||||
- [ ] 实现导出当前单张
|
||||
- [ ] 实现应用为模板入口
|
||||
- [ ] 实现重置为模板入口
|
||||
- [ ] 显示当前操作影响范围提示
|
||||
- [x] 先读取 `src/app/widgets/export_panel.py` 现有内容
|
||||
- [x] 完善 `src/app/widgets/export_panel.py`
|
||||
- [x] 实现输出目录选择
|
||||
- [x] 实现输出格式选择
|
||||
- [x] 实现质量选择
|
||||
- [x] 实现导出当前单张
|
||||
- [x] 实现应用为模板入口
|
||||
- [x] 实现重置为模板入口
|
||||
- [x] 显示当前操作影响范围提示
|
||||
|
||||
验收:
|
||||
|
||||
- [ ] 缺少衣服或印花时导出按钮禁用
|
||||
- [ ] 输出目录不可写时提示用户并记录日志
|
||||
- [ ] 单张导出调用 `core/composer.py`
|
||||
- [x] 缺少衣服或印花时导出按钮禁用
|
||||
- [x] 输出目录不可写时提示用户并记录日志
|
||||
- [x] 单张导出调用 `core/composer.py`
|
||||
|
||||
## 13. 合成队列 UI
|
||||
|
||||
|
||||
Reference in New Issue
Block a user