feat: implement template panel UI with picker, save, save-as, reset

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 09:15:33 +08:00
co-authored by Claude Sonnet 4.6
parent 8bb6a8e53a
commit abc92cbdcb
2 changed files with 244 additions and 13 deletions
+233 -2
View File
@@ -1,5 +1,236 @@
from PySide6.QtWidgets import QWidget
import logging
from typing import Optional
from PySide6.QtCore import Signal
from PySide6.QtWidgets import (
QComboBox,
QHBoxLayout,
QInputDialog,
QLabel,
QMessageBox,
QPushButton,
QVBoxLayout,
QWidget,
)
from core.models import Template, TransformState
from services.template_service import add_template, get_all_templates
logger = logging.getLogger(__name__)
_BUILTIN_PREFIX = "★ "
_CUSTOM_PREFIX = "◆ "
class TemplatePanel(QWidget):
pass
"""Right-panel section: template picker, save, save-as, reset."""
template_applied = Signal(object) # emits TransformState
def __init__(self, parent=None):
super().__init__(parent)
self._current_state: Optional[TransformState] = None
self._garment_w: float = 0.0
self._garment_h: float = 0.0
self._updating = False
self._setup_ui()
self._load_templates()
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("templateSectionHeader")
col.addWidget(header)
body = QWidget()
body.setObjectName("templateBody")
body_col = QVBoxLayout(body)
body_col.setContentsMargins(10, 8, 10, 10)
body_col.setSpacing(8)
self._combo = QComboBox()
self._combo.setObjectName("templateCombo")
self._combo.currentIndexChanged.connect(self._on_template_selected)
body_col.addWidget(self._combo)
btn_row = QHBoxLayout()
btn_row.setSpacing(6)
self._save_btn = QPushButton("保存")
self._save_btn.setObjectName("templateBtn")
self._save_btn.setToolTip("覆盖当前自定义模板")
self._save_btn.clicked.connect(self._save_template)
self._saveas_btn = QPushButton("另存为")
self._saveas_btn.setObjectName("templateBtn")
self._saveas_btn.setToolTip("将当前参数保存为新模板")
self._saveas_btn.clicked.connect(self._save_as_template)
btn_row.addWidget(self._save_btn)
btn_row.addWidget(self._saveas_btn)
body_col.addLayout(btn_row)
self._reset_btn = QPushButton("重置为模板")
self._reset_btn.setObjectName("templateBtn")
self._reset_btn.setToolTip("将印花参数还原为所选模板的默认值")
self._reset_btn.clicked.connect(self._reset_to_template)
body_col.addWidget(self._reset_btn)
col.addWidget(body)
# ── template list management ─────────────────────────────────────────────
def _load_templates(self, select_name: Optional[str] = None):
"""Reload combo from template_service; restore selection by name if given."""
templates = get_all_templates()
self._updating = True
try:
self._combo.blockSignals(True)
self._combo.clear()
for t in templates:
prefix = _BUILTIN_PREFIX if t.type == "builtin" else _CUSTOM_PREFIX
self._combo.addItem(prefix + t.name, userData=t)
if select_name:
for i in range(self._combo.count()):
t = self._combo.itemData(i)
if t and t.name == select_name:
self._combo.setCurrentIndex(i)
break
finally:
self._combo.blockSignals(False)
self._updating = False
self._update_button_states()
def _update_button_states(self):
t = self._current_template()
is_builtin = (t is None) or (t.type == "builtin")
self._save_btn.setEnabled(not is_builtin)
def _current_template(self) -> Optional[Template]:
return self._combo.currentData()
# ── public API ───────────────────────────────────────────────────────────
def set_garment_size(self, w: float, h: float):
"""Update garment dimensions (pixels) used for ratio <-> pixel conversion."""
self._garment_w = w
self._garment_h = h
def set_transform(self, state: TransformState):
"""Store current print transform (used when saving a template)."""
self._current_state = state
# ── signal handlers ──────────────────────────────────────────────────────
def _on_template_selected(self, index: int):
if self._updating:
return
self._update_button_states()
t = self._combo.itemData(index)
if t and self._garment_w > 0 and self._garment_h > 0:
state = t.to_transform_state(self._garment_w, self._garment_h)
logger.debug("Template applied: %s", t.name)
self.template_applied.emit(state)
def _reset_to_template(self):
"""Re-apply the currently selected template."""
t = self._current_template()
if not t:
return
if self._garment_w <= 0 or self._garment_h <= 0:
QMessageBox.information(self, "提示", "请先加载衣服图片。")
return
state = t.to_transform_state(self._garment_w, self._garment_h)
self.template_applied.emit(state)
def _save_template(self):
"""Overwrite the selected custom template with the current transform."""
t = self._current_template()
if not t or t.type == "builtin":
QMessageBox.warning(
self, "无法保存",
"内置模板不可修改。\n请使用「另存为」创建自定义模板。",
)
return
if not self._current_state:
QMessageBox.information(self, "提示", "当前没有可保存的印花参数。")
return
if self._garment_w <= 0 or self._garment_h <= 0:
QMessageBox.information(self, "提示", "请先加载衣服图片。")
return
add_template(self._state_to_template(t.name))
logger.info("Template saved: %s", t.name)
self._load_templates(select_name=t.name)
def _save_as_template(self):
"""Create a new custom template from the current transform."""
if not self._current_state:
QMessageBox.information(self, "提示", "当前没有可保存的印花参数。")
return
if self._garment_w <= 0 or self._garment_h <= 0:
QMessageBox.information(self, "提示", "请先加载衣服图片。")
return
name, ok = QInputDialog.getText(self, "另存为模板", "请输入模板名称:")
if not ok:
return
name = name.strip()
if not name:
QMessageBox.warning(self, "名称无效", "模板名称不能为空。")
return
add_template(self._state_to_template(name))
logger.info("Template saved-as: %s", name)
self._load_templates(select_name=name)
# ── helpers ──────────────────────────────────────────────────────────────
def _state_to_template(self, name: str) -> Template:
s = self._current_state
gw, gh = self._garment_w, self._garment_h
return Template(
name=name,
x_ratio=s.x / gw,
y_ratio=s.y / gh,
width_ratio=s.width / gw,
height_ratio=s.height / gh,
rotation=s.rotation,
type="custom",
)
def _apply_stylesheet(self):
self.setStyleSheet("""
#templateSectionHeader {
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;
}
#templateBody {
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
#templateCombo {
font-size: 12px;
border: 1px solid #d6d6d6;
border-radius: 3px;
padding: 2px 4px;
}
#templateBtn {
font-size: 12px;
padding: 4px 8px;
border: 1px solid #d6d6d6;
border-radius: 3px;
background: #f0f0f0;
}
#templateBtn:hover { background: #e0e0e0; }
#templateBtn:pressed { background: #d0d0d0; }
#templateBtn:disabled { color: #aaaaaa; background: #f7f7f7; }
""")
+11 -11
View File
@@ -373,20 +373,20 @@
任务:
- [ ] 先读取 `src/app/widgets/template_panel.py` 现有内容
- [ ] 完善 `src/app/widgets/template_panel.py`
- [ ] 显示模板下拉框
- [ ] 支持选择模板
- [ ] 支持保存模板
- [ ] 支持另存为模板
- [ ] 支持重置为模板
- [ ] 通过 `template_service` 操作模板
- [x] 先读取 `src/app/widgets/template_panel.py` 现有内容
- [x] 完善 `src/app/widgets/template_panel.py`
- [x] 显示模板下拉框
- [x] 支持选择模板
- [x] 支持保存模板
- [x] 支持另存为模板
- [x] 支持重置为模板
- [x] 通过 `template_service` 操作模板
验收:
- [ ] 选择模板后更新预览和参数面板
- [ ] UI 不直接修改模板 JSON
- [ ] 自定义模板重启后仍可用
- [x] 选择模板后更新预览和参数面板
- [x] UI 不直接修改模板 JSON
- [x] 自定义模板重启后仍可用
## 12. 导出面板 UI