feat(qml): task #15a 款式设计 tab 主体(8 字段 + Prompt 预览 + 生成)
qml_poc/qml/StyleDesignerTab.qml (新):
- 左侧 360px 卡片:8 字段 ComboBox 表单(jewelry.categories Repeater)
每个字段头插 "(不选)" 让用户能清空
onActivated → updateField(category, value) → 触发 reassemble
- 右侧上半 Prompt 预览:实时调 jewelry.previewPrompt(formData)
Text 只读展示 + 字数统计
- 右侧操作行:生成图片 PrimaryButton (调 imageGen.submitTask
默认 1:1 / 2K / 慢速模式 Pro 模型) + 重置字段 + 状态指示器
- 右侧下半预览:lastResultPath Image,双击系统查看器打开
- 监听 imageGen 4 个信号但只对 currentTaskId 匹配的做反应(taskId
唯一可区分图片生成 tab vs 款式设计 tab,互不冲突)
- 监听 jewelry.libraryChanged → optionsRepeater.reloadAll
(task #15b 词库管理对话框触发时刷新 ComboBox model)
MainWindow.qml: 款式设计占位 Item → StyleDesignerTab {}
未做(task #15b):
- 词库管理子对话框(每个类别能增删词条)
- jewelry.addItem / removeItem / resetAll Slot 都已就位,缺 UI
视觉验证:QML_DEBUG_TAB=1 直接进款式设计 tab,8 字段 ComboBox + Prompt
预览(PromptAssembler 在空表单时给 fallback "一款高端精品珠宝戒指设计…
高端珠宝渲染" 52 字)+ 生成按钮全就位,UI 无回归。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Showing
2 changed files
with
302 additions
and
11 deletions
| ... | @@ -95,17 +95,7 @@ Rectangle { | ... | @@ -95,17 +95,7 @@ Rectangle { |
| 95 | 95 | ||
| 96 | ImageGenTab {} | 96 | ImageGenTab {} |
| 97 | 97 | ||
| 98 | // 款式设计 — 占位 | 98 | StyleDesignerTab {} |
| 99 | Item { | ||
| 100 | Text { | ||
| 101 | anchors.centerIn: parent | ||
| 102 | text: "款式设计 tab 内容\n(QML PoC 暂未实现)" | ||
| 103 | color: App.Theme.textTertiary | ||
| 104 | font.family: App.Theme.fontFamily | ||
| 105 | font.pointSize: App.Theme.fontLg | ||
| 106 | horizontalAlignment: Text.AlignHCenter | ||
| 107 | } | ||
| 108 | } | ||
| 109 | 99 | ||
| 110 | HistoryTab {} | 100 | HistoryTab {} |
| 111 | } | 101 | } | ... | ... |
qml_poc/qml/StyleDesignerTab.qml
0 → 100644
| 1 | import QtQuick | ||
| 2 | import QtQuick.Controls.Basic | ||
| 3 | import QtQuick.Layouts | ||
| 4 | import "components" | ||
| 5 | import "." as App | ||
| 6 | |||
| 7 | Item { | ||
| 8 | id: tab | ||
| 9 | |||
| 10 | // ===== 状态 ===== | ||
| 11 | // 8 字段表单值(与 jewelry.categories 顺序一致) | ||
| 12 | property var formData: ({ | ||
| 13 | "主石形状": "", | ||
| 14 | "主石材质": "", | ||
| 15 | "金属": "", | ||
| 16 | "花头形式": "", | ||
| 17 | "戒臂结构": "", | ||
| 18 | "戒臂处理": "", | ||
| 19 | "辅石镶嵌": "", | ||
| 20 | "特殊元素": "", | ||
| 21 | }) | ||
| 22 | property string assembledPrompt: "" | ||
| 23 | property string currentTaskId: "" | ||
| 24 | property string lastResultPath: "" | ||
| 25 | property string statusText: "● 就绪" | ||
| 26 | property color statusColor: App.Theme.success | ||
| 27 | |||
| 28 | function updateField(category, value) { | ||
| 29 | var copy = Object.assign({}, tab.formData) | ||
| 30 | copy[category] = value || "" | ||
| 31 | tab.formData = copy | ||
| 32 | tab.assembledPrompt = jewelry.previewPrompt(copy) | ||
| 33 | } | ||
| 34 | |||
| 35 | function reassemble() { | ||
| 36 | tab.assembledPrompt = jewelry.previewPrompt(tab.formData) | ||
| 37 | } | ||
| 38 | |||
| 39 | function submit() { | ||
| 40 | if (tab.currentTaskId !== "") return | ||
| 41 | if (tab.assembledPrompt.trim().length === 0) { | ||
| 42 | tab.statusText = "● 选几个字段先" | ||
| 43 | tab.statusColor = App.Theme.danger | ||
| 44 | return | ||
| 45 | } | ||
| 46 | try { | ||
| 47 | // submitTask 同步返回 task_id,自己记下来给信号过滤用 | ||
| 48 | var taskId = imageGen.submitTask( | ||
| 49 | tab.assembledPrompt, | ||
| 50 | [], // 款式设计不带参考图 | ||
| 51 | "1:1", // 珠宝出图常用比例 | ||
| 52 | "2K", | ||
| 53 | "慢速模式" // Pro 模型,质量优先 | ||
| 54 | ) | ||
| 55 | tab.currentTaskId = taskId | ||
| 56 | tab.statusText = "● 已提交" | ||
| 57 | tab.statusColor = App.Theme.accent | ||
| 58 | } catch (e) { | ||
| 59 | tab.statusText = "● " + e | ||
| 60 | tab.statusColor = App.Theme.danger | ||
| 61 | } | ||
| 62 | } | ||
| 63 | |||
| 64 | Component.onCompleted: reassemble() | ||
| 65 | |||
| 66 | Connections { | ||
| 67 | target: jewelry | ||
| 68 | function onLibraryChanged(category) { | ||
| 69 | optionsRepeater.reloadAll() | ||
| 70 | } | ||
| 71 | } | ||
| 72 | |||
| 73 | // 监听生成信号 — 只对自己提交的 task 做反应(taskId 唯一可区分本 tab vs 图片生成 tab) | ||
| 74 | Connections { | ||
| 75 | target: imageGen | ||
| 76 | |||
| 77 | function onTaskProgress(taskId, progress, msg) { | ||
| 78 | if (taskId !== tab.currentTaskId) return | ||
| 79 | tab.statusText = "● " + (msg || "生成中…") | ||
| 80 | tab.statusColor = App.Theme.accent | ||
| 81 | } | ||
| 82 | function onTaskCompleted(taskId, resultPath, prompt, model) { | ||
| 83 | if (taskId !== tab.currentTaskId) return | ||
| 84 | tab.lastResultPath = resultPath | ||
| 85 | tab.currentTaskId = "" | ||
| 86 | tab.statusText = "● 已完成" | ||
| 87 | tab.statusColor = App.Theme.success | ||
| 88 | } | ||
| 89 | function onTaskFailed(taskId, error) { | ||
| 90 | if (taskId !== tab.currentTaskId) return | ||
| 91 | tab.currentTaskId = "" | ||
| 92 | tab.statusText = "● " + (error || "失败") | ||
| 93 | tab.statusColor = App.Theme.danger | ||
| 94 | } | ||
| 95 | } | ||
| 96 | |||
| 97 | RowLayout { | ||
| 98 | anchors.fill: parent | ||
| 99 | anchors.margins: App.Theme.space5 | ||
| 100 | spacing: App.Theme.space4 | ||
| 101 | |||
| 102 | // ===== 左侧:8 字段表单 ===== | ||
| 103 | Card { | ||
| 104 | Layout.preferredWidth: 360 | ||
| 105 | Layout.fillHeight: true | ||
| 106 | |||
| 107 | ColumnLayout { | ||
| 108 | anchors.fill: parent | ||
| 109 | anchors.margins: App.Theme.space4 | ||
| 110 | spacing: App.Theme.space3 | ||
| 111 | |||
| 112 | CaptionLabel { | ||
| 113 | text: "款式字段" | ||
| 114 | font.pointSize: App.Theme.fontLg | ||
| 115 | } | ||
| 116 | |||
| 117 | ScrollView { | ||
| 118 | Layout.fillWidth: true | ||
| 119 | Layout.fillHeight: true | ||
| 120 | clip: true | ||
| 121 | |||
| 122 | ColumnLayout { | ||
| 123 | width: parent.width | ||
| 124 | spacing: App.Theme.space3 | ||
| 125 | |||
| 126 | Repeater { | ||
| 127 | id: optionsRepeater | ||
| 128 | model: jewelry.categories // 8 个类别名 | ||
| 129 | |||
| 130 | // task #15b 词库变化时重新拉 | ||
| 131 | function reloadAll() { | ||
| 132 | // QML 自动重新评估 model:,主动 reset | ||
| 133 | model = [] | ||
| 134 | model = jewelry.categories | ||
| 135 | } | ||
| 136 | |||
| 137 | delegate: ColumnLayout { | ||
| 138 | Layout.fillWidth: true | ||
| 139 | spacing: 4 | ||
| 140 | |||
| 141 | CaptionLabel { | ||
| 142 | text: modelData | ||
| 143 | font.pointSize: App.Theme.fontBase | ||
| 144 | } | ||
| 145 | ThemedComboBox { | ||
| 146 | Layout.fillWidth: true | ||
| 147 | // 头插一个 "(不选)" 让用户能清空字段 | ||
| 148 | model: ["(不选)"].concat(jewelry.getOptions(modelData)) | ||
| 149 | onActivated: { | ||
| 150 | var v = currentText === "(不选)" ? "" : currentText | ||
| 151 | tab.updateField(modelData, v) | ||
| 152 | } | ||
| 153 | } | ||
| 154 | } | ||
| 155 | } | ||
| 156 | } | ||
| 157 | } | ||
| 158 | } | ||
| 159 | } | ||
| 160 | |||
| 161 | // ===== 右侧:Prompt 预览 + 生成 + 结果 ===== | ||
| 162 | ColumnLayout { | ||
| 163 | Layout.fillWidth: true | ||
| 164 | Layout.fillHeight: true | ||
| 165 | spacing: App.Theme.space4 | ||
| 166 | |||
| 167 | // Prompt 预览卡 | ||
| 168 | Card { | ||
| 169 | Layout.fillWidth: true | ||
| 170 | Layout.preferredHeight: 200 | ||
| 171 | |||
| 172 | ColumnLayout { | ||
| 173 | anchors.fill: parent | ||
| 174 | anchors.margins: App.Theme.space4 | ||
| 175 | spacing: App.Theme.space3 | ||
| 176 | |||
| 177 | RowLayout { | ||
| 178 | spacing: App.Theme.space3 | ||
| 179 | CaptionLabel { | ||
| 180 | text: "Prompt 预览" | ||
| 181 | font.pointSize: App.Theme.fontLg | ||
| 182 | } | ||
| 183 | Item { Layout.fillWidth: true } | ||
| 184 | Label { | ||
| 185 | text: tab.assembledPrompt.length + " 字" | ||
| 186 | font.family: App.Theme.fontFamily | ||
| 187 | font.pointSize: App.Theme.fontXs | ||
| 188 | color: App.Theme.textTertiary | ||
| 189 | } | ||
| 190 | } | ||
| 191 | |||
| 192 | ScrollView { | ||
| 193 | Layout.fillWidth: true | ||
| 194 | Layout.fillHeight: true | ||
| 195 | |||
| 196 | Rectangle { | ||
| 197 | anchors.fill: parent | ||
| 198 | color: App.Theme.bgSubtle | ||
| 199 | radius: App.Theme.radiusMd | ||
| 200 | |||
| 201 | Text { | ||
| 202 | anchors.fill: parent | ||
| 203 | anchors.margins: App.Theme.space3 | ||
| 204 | text: tab.assembledPrompt | ||
| 205 | color: App.Theme.textPrimary | ||
| 206 | font.family: App.Theme.fontFamily | ||
| 207 | font.pointSize: App.Theme.fontSm | ||
| 208 | wrapMode: Text.Wrap | ||
| 209 | } | ||
| 210 | } | ||
| 211 | } | ||
| 212 | } | ||
| 213 | } | ||
| 214 | |||
| 215 | // 操作行 | ||
| 216 | RowLayout { | ||
| 217 | spacing: App.Theme.space3 | ||
| 218 | Layout.fillWidth: true | ||
| 219 | |||
| 220 | PrimaryButton { | ||
| 221 | text: tab.currentTaskId !== "" ? "生成中…" : "生成图片" | ||
| 222 | enabled: tab.currentTaskId === "" | ||
| 223 | onClicked: tab.submit() | ||
| 224 | } | ||
| 225 | SecondaryButton { | ||
| 226 | text: "重置字段" | ||
| 227 | onClicked: { | ||
| 228 | var fresh = {} | ||
| 229 | for (var i = 0; i < jewelry.categories.length; i++) { | ||
| 230 | fresh[jewelry.categories[i]] = "" | ||
| 231 | } | ||
| 232 | tab.formData = fresh | ||
| 233 | tab.reassemble() | ||
| 234 | } | ||
| 235 | } | ||
| 236 | Label { | ||
| 237 | text: tab.statusText | ||
| 238 | font.family: App.Theme.fontFamily | ||
| 239 | font.pointSize: App.Theme.fontSm | ||
| 240 | color: tab.statusColor | ||
| 241 | } | ||
| 242 | Item { Layout.fillWidth: true } | ||
| 243 | } | ||
| 244 | |||
| 245 | // 预览大图 | ||
| 246 | Card { | ||
| 247 | Layout.fillWidth: true | ||
| 248 | Layout.fillHeight: true | ||
| 249 | Layout.minimumHeight: 240 | ||
| 250 | |||
| 251 | ColumnLayout { | ||
| 252 | anchors.fill: parent | ||
| 253 | anchors.margins: App.Theme.space4 | ||
| 254 | spacing: App.Theme.space3 | ||
| 255 | |||
| 256 | CaptionLabel { | ||
| 257 | text: "预览" | ||
| 258 | font.pointSize: App.Theme.fontLg | ||
| 259 | } | ||
| 260 | |||
| 261 | Rectangle { | ||
| 262 | Layout.fillWidth: true | ||
| 263 | Layout.fillHeight: true | ||
| 264 | color: App.Theme.bgSubtle | ||
| 265 | radius: App.Theme.radiusMd | ||
| 266 | |||
| 267 | Image { | ||
| 268 | anchors.fill: parent | ||
| 269 | anchors.margins: App.Theme.space3 | ||
| 270 | source: tab.lastResultPath ? "file:///" + tab.lastResultPath : "" | ||
| 271 | fillMode: Image.PreserveAspectFit | ||
| 272 | smooth: true | ||
| 273 | asynchronous: true | ||
| 274 | cache: false | ||
| 275 | visible: tab.lastResultPath !== "" | ||
| 276 | |||
| 277 | MouseArea { | ||
| 278 | anchors.fill: parent | ||
| 279 | cursorShape: Qt.PointingHandCursor | ||
| 280 | onDoubleClicked: { | ||
| 281 | if (tab.lastResultPath) { | ||
| 282 | Qt.openUrlExternally("file:///" + tab.lastResultPath) | ||
| 283 | } | ||
| 284 | } | ||
| 285 | } | ||
| 286 | } | ||
| 287 | |||
| 288 | Text { | ||
| 289 | anchors.centerIn: parent | ||
| 290 | visible: tab.lastResultPath === "" | ||
| 291 | text: "选择字段后点 \"生成图片\"" | ||
| 292 | color: App.Theme.textTertiary | ||
| 293 | font.family: App.Theme.fontFamily | ||
| 294 | font.pointSize: App.Theme.fontSm | ||
| 295 | } | ||
| 296 | } | ||
| 297 | } | ||
| 298 | } | ||
| 299 | } | ||
| 300 | } | ||
| 301 | } |
-
Please register or sign in to post a comment