feat(qml): task #14c 提示词收藏 + 删除(持久化到 config.json)
config_util.py 加 save_config(path, config) → bool 原子写:tmp 文件 + replace,失败记日志返回 False(不抛异常) ImageGenBridge: - 构造函数加 saved_prompts / config_path 参数 - Property savedPrompts: list[str] - Slot addSavedPrompt(prompt): 去重 + 插入头部 + 持久化 + 信号 - Slot removeSavedPrompt(prompt): 移除 + 持久化 + 信号 - _persist_saved_prompts: load_config_safe → 改 saved_prompts → save_config main_qml.py: 装 saved_prompts + config_path 给 ImageGenBridge ImageGenTab.qml: - "收藏" enabled by promptArea.text.trim().length > 0,onClicked add - "删除" enabled by savedPrompts.length > 0,删除当前 ComboBox 选中项 - 快速选择 ComboBox model: imageGen.savedPrompts (响应 savedPromptsChanged 自动刷) - onActivated 仅在 currentText 非空时填到 promptArea 视觉验证:QML_AUTO_LOGIN=1 启动主窗口,ComboBox 已显示从 config.json 读到的 "主石换成闪耀的祖母绿",收藏按钮在 prompt 空时正确灰。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Showing
4 changed files
with
72 additions
and
5 deletions
| ... | @@ -21,6 +21,7 @@ from core.generation import MODEL_BY_MODE, MODEL_PRO | ... | @@ -21,6 +21,7 @@ from core.generation import MODEL_BY_MODE, MODEL_PRO |
| 21 | class ImageGenBridge(QObject): | 21 | class ImageGenBridge(QObject): |
| 22 | apiKeyChanged = Signal() | 22 | apiKeyChanged = Signal() |
| 23 | busyChanged = Signal() | 23 | busyChanged = Signal() |
| 24 | savedPromptsChanged = Signal() | ||
| 24 | 25 | ||
| 25 | taskSubmitted = Signal(str) # task_id | 26 | taskSubmitted = Signal(str) # task_id |
| 26 | taskCompleted = Signal(str, str, str, str) # task_id, result_path, prompt, model | 27 | taskCompleted = Signal(str, str, str, str) # task_id, result_path, prompt, model |
| ... | @@ -28,13 +29,16 @@ class ImageGenBridge(QObject): | ... | @@ -28,13 +29,16 @@ class ImageGenBridge(QObject): |
| 28 | taskProgress = Signal(str, float, str) # task_id, progress, status_text | 29 | taskProgress = Signal(str, float, str) # task_id, progress, status_text |
| 29 | 30 | ||
| 30 | def __init__(self, task_queue_manager, history_manager, auth_bridge, | 31 | def __init__(self, task_queue_manager, history_manager, auth_bridge, |
| 31 | api_key: str = "", parent=None): | 32 | api_key: str = "", saved_prompts=None, config_path=None, |
| 33 | parent=None): | ||
| 32 | super().__init__(parent) | 34 | super().__init__(parent) |
| 33 | self._logger = logging.getLogger(__name__) | 35 | self._logger = logging.getLogger(__name__) |
| 34 | self._tqm = task_queue_manager | 36 | self._tqm = task_queue_manager |
| 35 | self._history = history_manager | 37 | self._history = history_manager |
| 36 | self._auth = auth_bridge | 38 | self._auth = auth_bridge |
| 37 | self._api_key = api_key | 39 | self._api_key = api_key |
| 40 | self._saved_prompts = list(saved_prompts or []) | ||
| 41 | self._config_path = config_path # Path 或 None;None 时 add/remove 不持久化 | ||
| 38 | 42 | ||
| 39 | # 转发 TaskQueueManager 信号 → 桥层 QML 友好信号 | 43 | # 转发 TaskQueueManager 信号 → 桥层 QML 友好信号 |
| 40 | self._tqm.task_added.connect(self._on_task_added) | 44 | self._tqm.task_added.connect(self._on_task_added) |
| ... | @@ -52,6 +56,10 @@ class ImageGenBridge(QObject): | ... | @@ -52,6 +56,10 @@ class ImageGenBridge(QObject): |
| 52 | def busy(self) -> bool: | 56 | def busy(self) -> bool: |
| 53 | return self._tqm.get_running_count() > 0 | 57 | return self._tqm.get_running_count() > 0 |
| 54 | 58 | ||
| 59 | @Property("QVariantList", notify=savedPromptsChanged) | ||
| 60 | def savedPrompts(self) -> list: | ||
| 61 | return list(self._saved_prompts) | ||
| 62 | |||
| 55 | # ---- Slots ---------------------------------------------------------- | 63 | # ---- Slots ---------------------------------------------------------- |
| 56 | 64 | ||
| 57 | @Slot(str) | 65 | @Slot(str) |
| ... | @@ -60,6 +68,33 @@ class ImageGenBridge(QObject): | ... | @@ -60,6 +68,33 @@ class ImageGenBridge(QObject): |
| 60 | self._api_key = key | 68 | self._api_key = key |
| 61 | self.apiKeyChanged.emit() | 69 | self.apiKeyChanged.emit() |
| 62 | 70 | ||
| 71 | @Slot(str) | ||
| 72 | def addSavedPrompt(self, prompt: str) -> None: | ||
| 73 | prompt = (prompt or "").strip() | ||
| 74 | if not prompt or prompt in self._saved_prompts: | ||
| 75 | return | ||
| 76 | self._saved_prompts.insert(0, prompt) | ||
| 77 | self._persist_saved_prompts() | ||
| 78 | self.savedPromptsChanged.emit() | ||
| 79 | |||
| 80 | @Slot(str) | ||
| 81 | def removeSavedPrompt(self, prompt: str) -> None: | ||
| 82 | if prompt not in self._saved_prompts: | ||
| 83 | return | ||
| 84 | self._saved_prompts.remove(prompt) | ||
| 85 | self._persist_saved_prompts() | ||
| 86 | self.savedPromptsChanged.emit() | ||
| 87 | |||
| 88 | def _persist_saved_prompts(self) -> None: | ||
| 89 | """更新 config.json 的 saved_prompts 字段(保留其他字段)。""" | ||
| 90 | if self._config_path is None: | ||
| 91 | return | ||
| 92 | from config_util import load_config_safe, save_config | ||
| 93 | cfg, _ = load_config_safe(self._config_path) | ||
| 94 | cfg["saved_prompts"] = list(self._saved_prompts) | ||
| 95 | if not save_config(self._config_path, cfg): | ||
| 96 | self._logger.warning(f"saved_prompts 持久化失败: {self._config_path}") | ||
| 97 | |||
| 63 | @Slot(str, list, str, str, str, result=str) | 98 | @Slot(str, list, str, str, str, result=str) |
| 64 | def submitTask(self, prompt: str, reference_images: list, | 99 | def submitTask(self, prompt: str, reference_images: list, |
| 65 | aspect_ratio: str, image_size: str, mode: str) -> str: | 100 | aspect_ratio: str, image_size: str, mode: str) -> str: | ... | ... |
| ... | @@ -198,3 +198,20 @@ def _backup(src: Path, reason: str) -> None: | ... | @@ -198,3 +198,20 @@ def _backup(src: Path, reason: str) -> None: |
| 198 | shutil.copy2(src, dst) | 198 | shutil.copy2(src, dst) |
| 199 | except Exception: | 199 | except Exception: |
| 200 | pass | 200 | pass |
| 201 | |||
| 202 | |||
| 203 | def save_config(config_path: Path, config: dict) -> bool: | ||
| 204 | """原子写回 config.json。失败记日志,返回 False(不抛异常)。""" | ||
| 205 | config_path = Path(config_path) | ||
| 206 | try: | ||
| 207 | config_path.parent.mkdir(parents=True, exist_ok=True) | ||
| 208 | tmp = config_path.with_suffix(config_path.suffix + ".tmp") | ||
| 209 | tmp.write_text( | ||
| 210 | json.dumps(config, ensure_ascii=False, indent=2), | ||
| 211 | encoding="utf-8", | ||
| 212 | ) | ||
| 213 | tmp.replace(config_path) | ||
| 214 | return True | ||
| 215 | except Exception as e: | ||
| 216 | logger.error(f"save_config 失败 {config_path}: {e}") | ||
| 217 | return False | ... | ... |
| ... | @@ -103,6 +103,7 @@ def main(): | ... | @@ -103,6 +103,7 @@ def main(): |
| 103 | 103 | ||
| 104 | api_key = config.get("api_key", "") or "" | 104 | api_key = config.get("api_key", "") or "" |
| 105 | db_config = config.get("db_config") # None 时桥层走 PoC 模式 | 105 | db_config = config.get("db_config") # None 时桥层走 PoC 模式 |
| 106 | saved_prompts = config.get("saved_prompts", []) or [] | ||
| 106 | config_dir = get_config_dir() | 107 | config_dir = get_config_dir() |
| 107 | 108 | ||
| 108 | history_manager = HistoryManager() | 109 | history_manager = HistoryManager() |
| ... | @@ -130,6 +131,8 @@ def main(): | ... | @@ -130,6 +131,8 @@ def main(): |
| 130 | history_manager=history_manager, | 131 | history_manager=history_manager, |
| 131 | auth_bridge=auth_bridge, | 132 | auth_bridge=auth_bridge, |
| 132 | api_key=api_key, | 133 | api_key=api_key, |
| 134 | saved_prompts=saved_prompts, | ||
| 135 | config_path=config_path, | ||
| 133 | ) | 136 | ) |
| 134 | history_bridge = HistoryBridge(history_manager=history_manager) | 137 | history_bridge = HistoryBridge(history_manager=history_manager) |
| 135 | task_queue_bridge = TaskQueueBridge(task_queue_manager=task_queue_manager) | 138 | task_queue_bridge = TaskQueueBridge(task_queue_manager=task_queue_manager) | ... | ... |
| ... | @@ -264,7 +264,11 @@ Item { | ... | @@ -264,7 +264,11 @@ Item { |
| 264 | 264 | ||
| 265 | RowLayout { | 265 | RowLayout { |
| 266 | spacing: App.Theme.space2 | 266 | spacing: App.Theme.space2 |
| 267 | SecondaryButton { text: "⭐ 收藏"; enabled: false } | 267 | SecondaryButton { |
| 268 | text: "⭐ 收藏" | ||
| 269 | enabled: promptArea.text.trim().length > 0 | ||
| 270 | onClicked: imageGen.addSavedPrompt(promptArea.text.trim()) | ||
| 271 | } | ||
| 268 | Label { | 272 | Label { |
| 269 | text: "快速选择:" | 273 | text: "快速选择:" |
| 270 | font.family: App.Theme.fontFamily | 274 | font.family: App.Theme.fontFamily |
| ... | @@ -274,10 +278,18 @@ Item { | ... | @@ -274,10 +278,18 @@ Item { |
| 274 | ThemedComboBox { | 278 | ThemedComboBox { |
| 275 | id: quickPromptCombo | 279 | id: quickPromptCombo |
| 276 | Layout.fillWidth: true | 280 | Layout.fillWidth: true |
| 277 | model: ["主石换成闪耀的祖母绿", "改成玫瑰金材质", "增加更多碎钻"] | 281 | model: imageGen.savedPrompts |
| 278 | onActivated: promptArea.text = currentText | 282 | onActivated: if (currentText) promptArea.text = currentText |
| 283 | } | ||
| 284 | SecondaryButton { | ||
| 285 | text: "删除" | ||
| 286 | enabled: imageGen.savedPrompts.length > 0 | ||
| 287 | onClicked: { | ||
| 288 | if (quickPromptCombo.currentText) { | ||
| 289 | imageGen.removeSavedPrompt(quickPromptCombo.currentText) | ||
| 290 | } | ||
| 291 | } | ||
| 279 | } | 292 | } |
| 280 | SecondaryButton { text: "删除"; enabled: false } | ||
| 281 | } | 293 | } |
| 282 | 294 | ||
| 283 | ScrollView { | 295 | ScrollView { | ... | ... |
-
Please register or sign in to post a comment