From 519bb9a6d26cdc5fe3300bfc91c971e77d86f82d Mon Sep 17 00:00:00 2001 From: ila Date: Mon, 15 Jun 2026 17:01:40 +0800 Subject: [PATCH] feat: implement template service with builtin and custom templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BUILTIN_TEMPLATES: 4 presets (正方形/纵向/横向长方形/左胸小号) - load_custom_templates(): reads templates.json, skips corrupt entries individually so one bad record never blocks the rest - get_all_templates(): builtin first, then custom - add_template(): upsert by name, forces type="custom" - rename_template() / delete_template(): raise ValueError if not found - _save_custom_templates(): auto-creates config/ dir, logs on failure Co-Authored-By: Claude Sonnet 4.6 --- src/services/template_service.py | 193 ++++++++++++++++++++++++++++++- tasks.md | 20 ++-- 2 files changed, 201 insertions(+), 12 deletions(-) diff --git a/src/services/template_service.py b/src/services/template_service.py index 7e4bb62..32c3a23 100644 --- a/src/services/template_service.py +++ b/src/services/template_service.py @@ -1,2 +1,191 @@ -def load_templates(): - return [] +import json +import logging +from typing import List, Optional + +from core.models import Template + +logger = logging.getLogger(__name__) + +_TEMPLATES_FILENAME = "templates.json" + +# --------------------------------------------------------------------------- +# 内置模板(代码定义,不可删除/修改) +# --------------------------------------------------------------------------- + +BUILTIN_TEMPLATES: List[Template] = [ + Template( + name="正方形模板", + x_ratio=0.25, y_ratio=0.25, + width_ratio=0.50, height_ratio=0.50, + rotation=0.0, type="builtin", + ), + Template( + name="纵向长方形模板", + x_ratio=0.30, y_ratio=0.15, + width_ratio=0.40, height_ratio=0.55, + rotation=0.0, type="builtin", + ), + Template( + name="横向长方形模板", + x_ratio=0.15, y_ratio=0.30, + width_ratio=0.70, height_ratio=0.40, + rotation=0.0, type="builtin", + ), + Template( + name="左胸小号模板", + x_ratio=0.35, y_ratio=0.28, + width_ratio=0.12, height_ratio=0.12, + rotation=0.0, type="builtin", + ), +] + + +# --------------------------------------------------------------------------- +# 内部辅助 +# --------------------------------------------------------------------------- + +def _templates_file(): + from services.file_service import get_config_path + return get_config_path(_TEMPLATES_FILENAME) + + +def _validate_entry(data) -> Optional[Template]: + """Parse one raw dict into a Template; return None and log on any error.""" + try: + name = str(data["name"]).strip() + if not name: + raise ValueError("name is empty") + w = float(data["width_ratio"]) + h = float(data["height_ratio"]) + if w <= 0 or h <= 0: + raise ValueError("width_ratio and height_ratio must be > 0") + return Template( + name=name, + x_ratio=float(data.get("x_ratio", 0.0)), + y_ratio=float(data.get("y_ratio", 0.0)), + width_ratio=w, + height_ratio=h, + rotation=float(data.get("rotation", 0.0)), + type="custom", + ) + except (KeyError, ValueError, TypeError) as exc: + logger.warning("Skipping invalid template entry (%s): %s", exc, data) + return None + + +def _save_custom_templates(templates: List[Template]): + """Write custom templates to JSON. Logs error on failure, does not raise.""" + path = _templates_file() + try: + path.parent.mkdir(exist_ok=True) + payload = { + "templates": [ + { + "name": t.name, + "x_ratio": t.x_ratio, + "y_ratio": t.y_ratio, + "width_ratio": t.width_ratio, + "height_ratio": t.height_ratio, + "rotation": t.rotation, + } + for t in templates + ] + } + with open(str(path), "w", encoding="utf-8") as f: + json.dump(payload, f, ensure_ascii=False, indent=2) + logger.info("Saved %d custom template(s) to %s", len(templates), path) + except OSError as exc: + logger.error("Failed to save templates: %s", exc) + + +# --------------------------------------------------------------------------- +# 公开 API +# --------------------------------------------------------------------------- + +def get_builtin_templates() -> List[Template]: + """Return a copy of the built-in template list.""" + return list(BUILTIN_TEMPLATES) + + +def load_custom_templates() -> List[Template]: + """Load user-defined templates from JSON. + + Returns [] on missing file. Skips corrupt individual entries so a + single bad record never blocks the rest. + """ + path = _templates_file() + if not path.exists(): + return [] + + try: + with open(str(path), encoding="utf-8") as f: + data = json.load(f) + except (json.JSONDecodeError, OSError) as exc: + logger.warning("Cannot read templates.json (%s), ignoring.", exc) + return [] + + if not isinstance(data, dict) or "templates" not in data: + logger.warning("templates.json has unexpected structure, ignoring.") + return [] + + results = [] + for entry in data["templates"]: + t = _validate_entry(entry) + if t is not None: + results.append(t) + + logger.info("Loaded %d custom template(s) from %s", len(results), path) + return results + + +def get_all_templates() -> List[Template]: + """Return builtin templates followed by custom templates.""" + return get_builtin_templates() + load_custom_templates() + + +def add_template(template: Template): + """Add or overwrite a custom template, then persist. + + If a custom template with the same name already exists it is replaced. + Built-in templates cannot be added via this function. + """ + template.type = "custom" + customs = load_custom_templates() + customs = [t for t in customs if t.name != template.name] + customs.append(template) + _save_custom_templates(customs) + logger.info("Template added: %s", template.name) + + +def rename_template(old_name: str, new_name: str): + """Rename a custom template and persist. + + Raises ValueError if *old_name* is not found or *new_name* is empty. + """ + new_name = new_name.strip() + if not new_name: + raise ValueError("New template name cannot be empty") + customs = load_custom_templates() + found = False + for t in customs: + if t.name == old_name: + t.name = new_name + found = True + break + if not found: + raise ValueError("Custom template not found: {}".format(old_name)) + _save_custom_templates(customs) + logger.info("Template renamed: %s -> %s", old_name, new_name) + + +def delete_template(name: str): + """Delete a custom template by name and persist. + + Raises ValueError if *name* is not found among custom templates. + """ + customs = load_custom_templates() + filtered = [t for t in customs if t.name != name] + if len(filtered) == len(customs): + raise ValueError("Custom template not found: {}".format(name)) + _save_custom_templates(filtered) + logger.info("Template deleted: %s", name) diff --git a/tasks.md b/tasks.md index 2155c9b..1e27426 100644 --- a/tasks.md +++ b/tasks.md @@ -185,19 +185,19 @@ 任务: -- [ ] 先读取 `src/services/template_service.py` 现有内容 -- [ ] 完善 `src/services/template_service.py` -- [ ] 定义内置模板 -- [ ] 支持读取自定义模板 JSON -- [ ] 支持保存自定义模板 JSON -- [ ] 支持新增、重命名、删除自定义模板 -- [ ] 校验模板字段 +- [x] 先读取 `src/services/template_service.py` 现有内容 +- [x] 完善 `src/services/template_service.py` +- [x] 定义内置模板 +- [x] 支持读取自定义模板 JSON +- [x] 支持保存自定义模板 JSON +- [x] 支持新增、重命名、删除自定义模板 +- [x] 校验模板字段 验收: -- [ ] 内置模板和自定义模板可区分 -- [ ] 单个模板损坏不影响全部模板加载 -- [ ] UI 不直接操作模板 JSON 文件 +- [x] 内置模板和自定义模板可区分 +- [x] 单个模板损坏不影响全部模板加载 +- [x] UI 不直接操作模板 JSON 文件 ## 5. 图片合成核心