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
class ImageGenBridge(QObject):
apiKeyChanged = Signal()
busyChanged = Signal()
savedPromptsChanged = Signal()
taskSubmitted = Signal(str) # task_id
taskCompleted = Signal(str, str, str, str) # task_id, result_path, prompt, model
......@@ -28,13 +29,16 @@ class ImageGenBridge(QObject):
taskProgress = Signal(str, float, str) # task_id, progress, status_text
def __init__(self, task_queue_manager, history_manager, auth_bridge,
api_key: str = "", parent=None):
api_key: str = "", saved_prompts=None, config_path=None,
parent=None):
super().__init__(parent)
self._logger = logging.getLogger(__name__)
self._tqm = task_queue_manager
self._history = history_manager
self._auth = auth_bridge
self._api_key = api_key
self._saved_prompts = list(saved_prompts or [])
self._config_path = config_path # Path 或 None;None 时 add/remove 不持久化
# 转发 TaskQueueManager 信号 → 桥层 QML 友好信号
self._tqm.task_added.connect(self._on_task_added)
......@@ -52,6 +56,10 @@ class ImageGenBridge(QObject):
def busy(self) -> bool:
return self._tqm.get_running_count() > 0
@Property("QVariantList", notify=savedPromptsChanged)
def savedPrompts(self) -> list:
return list(self._saved_prompts)
# ---- Slots ----------------------------------------------------------
@Slot(str)
......@@ -60,6 +68,33 @@ class ImageGenBridge(QObject):
self._api_key = key
self.apiKeyChanged.emit()
@Slot(str)
def addSavedPrompt(self, prompt: str) -> None:
prompt = (prompt or "").strip()
if not prompt or prompt in self._saved_prompts:
return
self._saved_prompts.insert(0, prompt)
self._persist_saved_prompts()
self.savedPromptsChanged.emit()
@Slot(str)
def removeSavedPrompt(self, prompt: str) -> None:
if prompt not in self._saved_prompts:
return
self._saved_prompts.remove(prompt)
self._persist_saved_prompts()
self.savedPromptsChanged.emit()
def _persist_saved_prompts(self) -> None:
"""更新 config.json 的 saved_prompts 字段(保留其他字段)。"""
if self._config_path is None:
return
from config_util import load_config_safe, save_config
cfg, _ = load_config_safe(self._config_path)
cfg["saved_prompts"] = list(self._saved_prompts)
if not save_config(self._config_path, cfg):
self._logger.warning(f"saved_prompts 持久化失败: {self._config_path}")
@Slot(str, list, str, str, str, result=str)
def submitTask(self, prompt: str, reference_images: list,
aspect_ratio: str, image_size: str, mode: str) -> str:
......
......@@ -198,3 +198,20 @@ def _backup(src: Path, reason: str) -> None:
shutil.copy2(src, dst)
except Exception:
pass
def save_config(config_path: Path, config: dict) -> bool:
"""原子写回 config.json。失败记日志,返回 False(不抛异常)。"""
config_path = Path(config_path)
try:
config_path.parent.mkdir(parents=True, exist_ok=True)
tmp = config_path.with_suffix(config_path.suffix + ".tmp")
tmp.write_text(
json.dumps(config, ensure_ascii=False, indent=2),
encoding="utf-8",
)
tmp.replace(config_path)
return True
except Exception as e:
logger.error(f"save_config 失败 {config_path}: {e}")
return False
......
......@@ -103,6 +103,7 @@ def main():
api_key = config.get("api_key", "") or ""
db_config = config.get("db_config") # None 时桥层走 PoC 模式
saved_prompts = config.get("saved_prompts", []) or []
config_dir = get_config_dir()
history_manager = HistoryManager()
......@@ -130,6 +131,8 @@ def main():
history_manager=history_manager,
auth_bridge=auth_bridge,
api_key=api_key,
saved_prompts=saved_prompts,
config_path=config_path,
)
history_bridge = HistoryBridge(history_manager=history_manager)
task_queue_bridge = TaskQueueBridge(task_queue_manager=task_queue_manager)
......
......@@ -264,7 +264,11 @@ Item {
RowLayout {
spacing: App.Theme.space2
SecondaryButton { text: "⭐ 收藏"; enabled: false }
SecondaryButton {
text: "⭐ 收藏"
enabled: promptArea.text.trim().length > 0
onClicked: imageGen.addSavedPrompt(promptArea.text.trim())
}
Label {
text: "快速选择:"
font.family: App.Theme.fontFamily
......@@ -274,10 +278,18 @@ Item {
ThemedComboBox {
id: quickPromptCombo
Layout.fillWidth: true
model: ["主石换成闪耀的祖母绿", "改成玫瑰金材质", "增加更多碎钻"]
onActivated: promptArea.text = currentText
model: imageGen.savedPrompts
onActivated: if (currentText) promptArea.text = currentText
}
SecondaryButton {
text: "删除"
enabled: imageGen.savedPrompts.length > 0
onClicked: {
if (quickPromptCombo.currentText) {
imageGen.removeSavedPrompt(quickPromptCombo.currentText)
}
}
}
SecondaryButton { text: "删除"; enabled: false }
}
ScrollView {
......