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:
2026-06-16 09:18:39 +08:00
co-authored by Claude Sonnet 4.6
parent abc92cbdcb
commit 1054ab6236
2 changed files with 305 additions and 14 deletions
+293 -2
View File
@@ -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; }
""")
+12 -12
View File
@@ -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