2e9bf50f by 柴进

feat(qml): task #15 款式设计 tab 补齐缺失按钮

回顾旧 StyleDesignerTab,task #15a 漏了一批按钮,现在补全:

顶部工具行(左侧卡片):
  - :game_die: 随机:跳过 lockedFields,每个非锁定字段随机取一个 option
  - :arrows_counterclockwise: 恢复默认词库:调 jewelry.resetAll()
  - 重置字段:清空 formData

字段行(每个 ComboBox 后跟 3 个 36px 按钮):
  - :heavy_plus_sign: 添加词条:弹 Dialog 输入新值 → jewelry.addItem(category, value)
  - :wastebasket:️ 删除当前词条:删 ComboBox 当前选中项
  - :unlock: / :lock: 字段锁:locked 字段 ComboBox disabled,标签变 textTertiary,:game_die: 跳过

操作行加 :floppy_disk: 下载图片(复用 ImageGenTab 的 SaveFileDialog 逻辑 + imageGen.saveFile)

ComboBox 宽度修复:
  Layout.preferredWidth: 240 + Layout.maximumWidth: 240 + Layout.fillWidth: true
  锁住宽度,避免随机后内容长度(如"小爪层戒臂(如莲花夹层设计)")撑大 RowLayout
  造成视觉跳动。displayText 由 ThemedComboBox.contentItem 的 elide 处理。

视觉验证:QML_DEBUG_TAB=1 进入款式设计 tab,三排顶部按钮 + 8 行字段
(每行 ComboBox + :heavy_plus_sign: + :wastebasket:️ + :unlock:)+ 操作行(生成 + 下载)全就位,UI 无回归。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent eb469aab
1 import QtQuick 1 import QtQuick
2 import QtQuick.Controls.Basic 2 import QtQuick.Controls.Basic
3 import QtQuick.Dialogs
3 import QtQuick.Layouts 4 import QtQuick.Layouts
4 import "components" 5 import "components"
5 import "." as App 6 import "." as App
...@@ -8,7 +9,6 @@ Item { ...@@ -8,7 +9,6 @@ Item {
8 id: tab 9 id: tab
9 10
10 // ===== 状态 ===== 11 // ===== 状态 =====
11 // 8 字段表单值(与 jewelry.categories 顺序一致)
12 property var formData: ({ 12 property var formData: ({
13 "主石形状": "", 13 "主石形状": "",
14 "主石材质": "", 14 "主石材质": "",
...@@ -19,12 +19,16 @@ Item { ...@@ -19,12 +19,16 @@ Item {
19 "辅石镶嵌": "", 19 "辅石镶嵌": "",
20 "特殊元素": "", 20 "特殊元素": "",
21 }) 21 })
22 property var lockedFields: [] // 被 🔓 锁定的字段名(不参与随机)
22 property string assembledPrompt: "" 23 property string assembledPrompt: ""
23 property string currentTaskId: "" 24 property string currentTaskId: ""
24 property string lastResultPath: "" 25 property string lastResultPath: ""
25 property string statusText: "● 就绪" 26 property string statusText: "● 就绪"
26 property color statusColor: App.Theme.success 27 property color statusColor: App.Theme.success
27 28
29 // ➕ 添加词条对话框传参
30 property string addingCategory: ""
31
28 function updateField(category, value) { 32 function updateField(category, value) {
29 var copy = Object.assign({}, tab.formData) 33 var copy = Object.assign({}, tab.formData)
30 copy[category] = value || "" 34 copy[category] = value || ""
...@@ -36,6 +40,43 @@ Item { ...@@ -36,6 +40,43 @@ Item {
36 tab.assembledPrompt = jewelry.previewPrompt(tab.formData) 40 tab.assembledPrompt = jewelry.previewPrompt(tab.formData)
37 } 41 }
38 42
43 function isLocked(category) {
44 return tab.lockedFields.indexOf(category) >= 0
45 }
46
47 function toggleLock(category) {
48 var copy = tab.lockedFields.slice()
49 var i = copy.indexOf(category)
50 if (i >= 0) copy.splice(i, 1)
51 else copy.push(category)
52 tab.lockedFields = copy
53 }
54
55 function randomize() {
56 var copy = Object.assign({}, tab.formData)
57 for (var i = 0; i < jewelry.categories.length; i++) {
58 var cat = jewelry.categories[i]
59 if (tab.isLocked(cat)) continue
60 var opts = jewelry.getOptions(cat)
61 if (opts.length === 0) continue
62 copy[cat] = opts[Math.floor(Math.random() * opts.length)]
63 }
64 tab.formData = copy
65 tab.assembledPrompt = jewelry.previewPrompt(copy)
66 // 同步 ComboBox 显示(model 列表里找到 currentIndex)
67 comboReloadTrigger++
68 }
69
70 function resetFields() {
71 var fresh = {}
72 for (var i = 0; i < jewelry.categories.length; i++) {
73 fresh[jewelry.categories[i]] = ""
74 }
75 tab.formData = fresh
76 tab.reassemble()
77 comboReloadTrigger++
78 }
79
39 function submit() { 80 function submit() {
40 if (tab.currentTaskId !== "") return 81 if (tab.currentTaskId !== "") return
41 if (tab.assembledPrompt.trim().length === 0) { 82 if (tab.assembledPrompt.trim().length === 0) {
...@@ -44,13 +85,8 @@ Item { ...@@ -44,13 +85,8 @@ Item {
44 return 85 return
45 } 86 }
46 try { 87 try {
47 // submitTask 同步返回 task_id,自己记下来给信号过滤用
48 var taskId = imageGen.submitTask( 88 var taskId = imageGen.submitTask(
49 tab.assembledPrompt, 89 tab.assembledPrompt, [], "1:1", "2K", "慢速模式"
50 [], // 款式设计不带参考图
51 "1:1", // 珠宝出图常用比例
52 "2K",
53 "慢速模式" // Pro 模型,质量优先
54 ) 90 )
55 tab.currentTaskId = taskId 91 tab.currentTaskId = taskId
56 tab.statusText = "● 已提交" 92 tab.statusText = "● 已提交"
...@@ -61,19 +97,20 @@ Item { ...@@ -61,19 +97,20 @@ Item {
61 } 97 }
62 } 98 }
63 99
100 // 触发 ComboBox model 刷新(随机 / 重置 / 词库变化时让 currentIndex 重算)
101 property int comboReloadTrigger: 0
102
64 Component.onCompleted: reassemble() 103 Component.onCompleted: reassemble()
65 104
66 Connections { 105 Connections {
67 target: jewelry 106 target: jewelry
68 function onLibraryChanged(category) { 107 function onLibraryChanged(category) {
69 optionsRepeater.reloadAll() 108 tab.comboReloadTrigger++
70 } 109 }
71 } 110 }
72 111
73 // 监听生成信号 — 只对自己提交的 task 做反应(taskId 唯一可区分本 tab vs 图片生成 tab)
74 Connections { 112 Connections {
75 target: imageGen 113 target: imageGen
76
77 function onTaskProgress(taskId, progress, msg) { 114 function onTaskProgress(taskId, progress, msg) {
78 if (taskId !== tab.currentTaskId) return 115 if (taskId !== tab.currentTaskId) return
79 tab.statusText = "● " + (msg || "生成中…") 116 tab.statusText = "● " + (msg || "生成中…")
...@@ -94,6 +131,60 @@ Item { ...@@ -94,6 +131,60 @@ Item {
94 } 131 }
95 } 132 }
96 133
134 // ➕ 添加词条对话框
135 Dialog {
136 id: addItemDialog
137 title: "添加词条 - " + tab.addingCategory
138 anchors.centerIn: parent
139 modal: true
140 width: 400
141 standardButtons: Dialog.Ok | Dialog.Cancel
142
143 ColumnLayout {
144 anchors.fill: parent
145 spacing: App.Theme.space2
146
147 Label {
148 text: "请输入新的「" + tab.addingCategory + "」词条(纯中文):"
149 font.family: App.Theme.fontFamily
150 font.pointSize: App.Theme.fontSm
151 color: App.Theme.textPrimary
152 }
153 ThemedTextField {
154 id: addItemField
155 Layout.fillWidth: true
156 placeholderText: "例如:圆形"
157 }
158 }
159
160 onAccepted: {
161 var v = addItemField.text.trim()
162 if (v) jewelry.addItem(tab.addingCategory, v)
163 addItemField.text = ""
164 }
165 onRejected: addItemField.text = ""
166 }
167
168 // 💾 下载图片
169 FileDialog {
170 id: saveImageDialog
171 title: "保存生成的图片"
172 fileMode: FileDialog.SaveFile
173 nameFilters: ["PNG 图片 (*.png)"]
174 defaultSuffix: "png"
175 onAccepted: {
176 var dest = selectedFile.toString().replace("file:///", "")
177 var ok = imageGen.saveFile(tab.lastResultPath, dest)
178 if (ok) {
179 tab.statusText = "● 已保存到 " + dest
180 tab.statusColor = App.Theme.success
181 } else {
182 tab.statusText = "● 保存失败"
183 tab.statusColor = App.Theme.danger
184 }
185 }
186 }
187
97 RowLayout { 188 RowLayout {
98 anchors.fill: parent 189 anchors.fill: parent
99 anchors.margins: App.Theme.space5 190 anchors.margins: App.Theme.space5
...@@ -101,7 +192,7 @@ Item { ...@@ -101,7 +192,7 @@ Item {
101 192
102 // ===== 左侧:8 字段表单 ===== 193 // ===== 左侧:8 字段表单 =====
103 Card { 194 Card {
104 Layout.preferredWidth: 360 195 Layout.preferredWidth: 420
105 Layout.fillHeight: true 196 Layout.fillHeight: true
106 197
107 ColumnLayout { 198 ColumnLayout {
...@@ -109,9 +200,33 @@ Item { ...@@ -109,9 +200,33 @@ Item {
109 anchors.margins: App.Theme.space4 200 anchors.margins: App.Theme.space4
110 spacing: App.Theme.space3 201 spacing: App.Theme.space3
111 202
112 CaptionLabel { 203 // header
113 text: "款式字段" 204 RowLayout {
114 font.pointSize: App.Theme.fontLg 205 spacing: App.Theme.space3
206 CaptionLabel {
207 text: "款式字段"
208 font.pointSize: App.Theme.fontLg
209 }
210 Item { Layout.fillWidth: true }
211 }
212
213 // 工具按钮行:随机 / 恢复默认词库 / 重置字段
214 RowLayout {
215 Layout.fillWidth: true
216 spacing: App.Theme.space2
217 SecondaryButton {
218 text: "🎲 随机"
219 onClicked: tab.randomize()
220 }
221 SecondaryButton {
222 text: "🔄 恢复默认词库"
223 onClicked: jewelry.resetAll()
224 }
225 SecondaryButton {
226 text: "重置字段"
227 onClicked: tab.resetFields()
228 }
229 Item { Layout.fillWidth: true }
115 } 230 }
116 231
117 ScrollView { 232 ScrollView {
...@@ -124,16 +239,8 @@ Item { ...@@ -124,16 +239,8 @@ Item {
124 spacing: App.Theme.space3 239 spacing: App.Theme.space3
125 240
126 Repeater { 241 Repeater {
127 id: optionsRepeater
128 model: jewelry.categories // 8 个类别名 242 model: jewelry.categories // 8 个类别名
129 243
130 // task #15b 词库变化时重新拉
131 function reloadAll() {
132 // QML 自动重新评估 model:,主动 reset
133 model = []
134 model = jewelry.categories
135 }
136
137 delegate: ColumnLayout { 244 delegate: ColumnLayout {
138 Layout.fillWidth: true 245 Layout.fillWidth: true
139 spacing: 4 246 spacing: 4
...@@ -141,14 +248,68 @@ Item { ...@@ -141,14 +248,68 @@ Item {
141 CaptionLabel { 248 CaptionLabel {
142 text: modelData 249 text: modelData
143 font.pointSize: App.Theme.fontBase 250 font.pointSize: App.Theme.fontBase
251 color: tab.isLocked(modelData)
252 ? App.Theme.textTertiary
253 : App.Theme.textPrimary
144 } 254 }
145 ThemedComboBox { 255 RowLayout {
146 Layout.fillWidth: true 256 Layout.fillWidth: true
147 // 头插一个 "(不选)" 让用户能清空字段 257 spacing: App.Theme.space2
148 model: ["(不选)"].concat(jewelry.getOptions(modelData)) 258
149 onActivated: { 259 ThemedComboBox {
150 var v = currentText === "(不选)" ? "" : currentText 260 id: cb
151 tab.updateField(modelData, v) 261 // 固定宽度:fillWidth + maximumWidth 锁住,避免内容长度变化撑大 RowLayout
262 Layout.fillWidth: true
263 Layout.preferredWidth: 240
264 Layout.maximumWidth: 240
265 // 头插 "(不选)",用 trigger 强制每次随机/重置后刷新
266 property var fullModel: {
267 tab.comboReloadTrigger
268 return ["(不选)"].concat(jewelry.getOptions(modelData))
269 }
270 model: fullModel
271 // 同步 currentIndex 到 formData[modelData]
272 currentIndex: {
273 tab.comboReloadTrigger
274 var v = tab.formData[modelData] || ""
275 if (!v) return 0 // "(不选)"
276 var idx = fullModel.indexOf(v)
277 return idx >= 0 ? idx : 0
278 }
279 enabled: !tab.isLocked(modelData)
280 onActivated: {
281 var v = currentText === "(不选)" ? "" : currentText
282 tab.updateField(modelData, v)
283 }
284 }
285
286 // ➕ 添加词条
287 SecondaryButton {
288 text: "➕"
289 Layout.preferredWidth: 36
290 onClicked: {
291 tab.addingCategory = modelData
292 addItemDialog.open()
293 }
294 }
295 // 🗑️ 删除当前选中词条
296 SecondaryButton {
297 text: "🗑️"
298 Layout.preferredWidth: 36
299 enabled: (tab.formData[modelData] || "").length > 0
300 onClicked: {
301 var v = tab.formData[modelData]
302 if (v) {
303 jewelry.removeItem(modelData, v)
304 tab.updateField(modelData, "")
305 }
306 }
307 }
308 // 🔓 / 🔒 锁定字段(锁定的字段不参与随机)
309 SecondaryButton {
310 text: tab.isLocked(modelData) ? "🔒" : "🔓"
311 Layout.preferredWidth: 36
312 onClicked: tab.toggleLock(modelData)
152 } 313 }
153 } 314 }
154 } 315 }
...@@ -212,26 +373,20 @@ Item { ...@@ -212,26 +373,20 @@ Item {
212 } 373 }
213 } 374 }
214 375
215 // 操作行 376 // 操作行:生成 + 下载 + 状态
216 RowLayout { 377 RowLayout {
217 spacing: App.Theme.space3 378 spacing: App.Theme.space3
218 Layout.fillWidth: true 379 Layout.fillWidth: true
219 380
220 PrimaryButton { 381 PrimaryButton {
221 text: tab.currentTaskId !== "" ? "生成中…" : "生成图片" 382 text: tab.currentTaskId !== "" ? "生成中…" : "🎨 生成图片"
222 enabled: tab.currentTaskId === "" 383 enabled: tab.currentTaskId === ""
223 onClicked: tab.submit() 384 onClicked: tab.submit()
224 } 385 }
225 SecondaryButton { 386 SecondaryButton {
226 text: "重置字段" 387 text: "💾 下载图片"
227 onClicked: { 388 enabled: tab.lastResultPath !== ""
228 var fresh = {} 389 onClicked: saveImageDialog.open()
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 } 390 }
236 Label { 391 Label {
237 text: tab.statusText 392 text: tab.statusText
...@@ -288,7 +443,7 @@ Item { ...@@ -288,7 +443,7 @@ Item {
288 Text { 443 Text {
289 anchors.centerIn: parent 444 anchors.centerIn: parent
290 visible: tab.lastResultPath === "" 445 visible: tab.lastResultPath === ""
291 text: "选择字段后点 \"生成图片\"" 446 text: "点 \"🎲 随机\" 试手气,或选字段后点 \"🎨 生成图片\""
292 color: App.Theme.textTertiary 447 color: App.Theme.textTertiary
293 font.family: App.Theme.fontFamily 448 font.family: App.Theme.fontFamily
294 font.pointSize: App.Theme.fontSm 449 font.pointSize: App.Theme.fontSm
......