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>
Showing
2 changed files
with
49 additions
and
1 deletions
| ... | @@ -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 { | ... | ... |
-
Please register or sign in to post a comment