80128a44 by 柴进

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:
  - ":star: 收藏" 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>
1 parent ca43df8e
...@@ -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 {
......