5d893c50 by 柴进

feat(qml): task #14d 下载图片 + 双击预览(task #14 全部完成)

ImageGenBridge.saveFile(src, dest) → bool
  shutil.copy2 wrap,源文件不存在 / 写盘失败时记日志返回 False

ImageGenTab.qml:
  - SaveFileDialog (FileDialog SaveFile mode + defaultSuffix png)
  - "下载图片" enabled by lastResultPath !== "",onClicked 弹保存对话框
  - onAccepted 调 imageGen.saveFile,成功状态绿色 "● 已保存到 <path>"
  - 预览 Image 包 MouseArea,onDoubleClicked Qt.openUrlExternally("file:///" + path)

至此 task #14 完整闭环:
  #14a 核心生成(prompt → submit → 进度 → 预览)
  #14b 参考图录入(添加 / 粘贴 / 拖拽 + 缩略图删除)
  #14c 提示词收藏 / 删除(持久化 config.json)
  #14d 下载图片 + 双击预览打开系统查看器

视觉验证:QML_AUTO_LOGIN=1 启动主窗口,UI 完整无回归,下载按钮在无生成图时正确灰。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 80128a44
...@@ -6,6 +6,7 @@ QML 调 imageGen.submitTask(prompt, refs, aspect, size, mode) → 桥层把 mode ...@@ -6,6 +6,7 @@ QML 调 imageGen.submitTask(prompt, refs, aspect, size, mode) → 桥层把 mode
6 然后把 result_path 通过 taskCompleted 信号转出(QML 只拿到文件路径,不传 bytes)。 6 然后把 result_path 通过 taskCompleted 信号转出(QML 只拿到文件路径,不传 bytes)。
7 """ 7 """
8 import logging 8 import logging
9 import shutil
9 import tempfile 10 import tempfile
10 import time 11 import time
11 import uuid 12 import uuid
...@@ -153,6 +154,23 @@ class ImageGenBridge(QObject): ...@@ -153,6 +154,23 @@ class ImageGenBridge(QObject):
153 self._logger.info(f"剪贴板图片已存: {path}") 154 self._logger.info(f"剪贴板图片已存: {path}")
154 return str(path) 155 return str(path)
155 156
157 @Slot(str, str, result=bool)
158 def saveFile(self, src: str, dest: str) -> bool:
159 """复制 src 到 dest(用户从下载对话框选择目标路径)。失败返回 False。"""
160 try:
161 src_path = Path(src)
162 dest_path = Path(dest)
163 if not src_path.exists():
164 self._logger.warning(f"saveFile 源文件不存在: {src}")
165 return False
166 dest_path.parent.mkdir(parents=True, exist_ok=True)
167 shutil.copy2(str(src_path), str(dest_path))
168 self._logger.info(f"图片已下载: {dest}")
169 return True
170 except Exception as e:
171 self._logger.error(f"saveFile 失败 src={src} dest={dest}: {e}")
172 return False
173
156 @Slot("QVariantList", result="QVariantList") 174 @Slot("QVariantList", result="QVariantList")
157 def normalizeFileUrls(self, urls) -> list: 175 def normalizeFileUrls(self, urls) -> list:
158 """QML DropArea 给的是 file:/// QUrl 列表,转成本地路径字符串列表(过滤非图片)。""" 176 """QML DropArea 给的是 file:/// QUrl 列表,转成本地路径字符串列表(过滤非图片)。"""
......
...@@ -39,6 +39,25 @@ Item { ...@@ -39,6 +39,25 @@ Item {
39 onAccepted: tab.addRefPaths(imageGen.normalizeFileUrls(selectedFiles)) 39 onAccepted: tab.addRefPaths(imageGen.normalizeFileUrls(selectedFiles))
40 } 40 }
41 41
42 FileDialog {
43 id: saveImageDialog
44 title: "保存生成的图片"
45 fileMode: FileDialog.SaveFile
46 nameFilters: ["PNG 图片 (*.png)"]
47 defaultSuffix: "png"
48 onAccepted: {
49 var dest = selectedFile.toString().replace("file:///", "")
50 var ok = imageGen.saveFile(tab.lastResultPath, dest)
51 if (ok) {
52 tab.statusText = "● 已保存到 " + dest
53 tab.statusColor = App.Theme.success
54 } else {
55 tab.statusText = "● 保存失败"
56 tab.statusColor = App.Theme.danger
57 }
58 }
59 }
60
42 // ===== 桥层信号 ===== 61 // ===== 桥层信号 =====
43 Connections { 62 Connections {
44 target: imageGen 63 target: imageGen
...@@ -379,7 +398,8 @@ Item { ...@@ -379,7 +398,8 @@ Item {
379 } 398 }
380 SecondaryButton { 399 SecondaryButton {
381 text: "下载图片" 400 text: "下载图片"
382 enabled: false 401 enabled: tab.lastResultPath !== ""
402 onClicked: saveImageDialog.open()
383 } 403 }
384 Label { 404 Label {
385 text: tab.statusText 405 text: tab.statusText
...@@ -422,6 +442,16 @@ Item { ...@@ -422,6 +442,16 @@ Item {
422 asynchronous: true 442 asynchronous: true
423 cache: false // 同路径下次生成会被覆盖,强制重读 443 cache: false // 同路径下次生成会被覆盖,强制重读
424 visible: tab.lastResultPath !== "" 444 visible: tab.lastResultPath !== ""
445
446 MouseArea {
447 anchors.fill: parent
448 cursorShape: Qt.PointingHandCursor
449 onDoubleClicked: {
450 if (tab.lastResultPath) {
451 Qt.openUrlExternally("file:///" + tab.lastResultPath)
452 }
453 }
454 }
425 } 455 }
426 456
427 Column { 457 Column {
......