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
import QtQuick
import QtQuick.Controls.Basic
import QtQuick.Dialogs
import QtQuick.Layouts
import "components"
import "." as App
......@@ -8,7 +9,6 @@ Item {
id: tab
// ===== 状态 =====
// 8 字段表单值(与 jewelry.categories 顺序一致)
property var formData: ({
"主石形状": "",
"主石材质": "",
......@@ -19,12 +19,16 @@ Item {
"辅石镶嵌": "",
"特殊元素": "",
})
property var lockedFields: [] // 被 🔓 锁定的字段名(不参与随机)
property string assembledPrompt: ""
property string currentTaskId: ""
property string lastResultPath: ""
property string statusText: "● 就绪"
property color statusColor: App.Theme.success
// ➕ 添加词条对话框传参
property string addingCategory: ""
function updateField(category, value) {
var copy = Object.assign({}, tab.formData)
copy[category] = value || ""
......@@ -36,6 +40,43 @@ Item {
tab.assembledPrompt = jewelry.previewPrompt(tab.formData)
}
function isLocked(category) {
return tab.lockedFields.indexOf(category) >= 0
}
function toggleLock(category) {
var copy = tab.lockedFields.slice()
var i = copy.indexOf(category)
if (i >= 0) copy.splice(i, 1)
else copy.push(category)
tab.lockedFields = copy
}
function randomize() {
var copy = Object.assign({}, tab.formData)
for (var i = 0; i < jewelry.categories.length; i++) {
var cat = jewelry.categories[i]
if (tab.isLocked(cat)) continue
var opts = jewelry.getOptions(cat)
if (opts.length === 0) continue
copy[cat] = opts[Math.floor(Math.random() * opts.length)]
}
tab.formData = copy
tab.assembledPrompt = jewelry.previewPrompt(copy)
// 同步 ComboBox 显示(model 列表里找到 currentIndex)
comboReloadTrigger++
}
function resetFields() {
var fresh = {}
for (var i = 0; i < jewelry.categories.length; i++) {
fresh[jewelry.categories[i]] = ""
}
tab.formData = fresh
tab.reassemble()
comboReloadTrigger++
}
function submit() {
if (tab.currentTaskId !== "") return
if (tab.assembledPrompt.trim().length === 0) {
......@@ -44,13 +85,8 @@ Item {
return
}
try {
// submitTask 同步返回 task_id,自己记下来给信号过滤用
var taskId = imageGen.submitTask(
tab.assembledPrompt,
[], // 款式设计不带参考图
"1:1", // 珠宝出图常用比例
"2K",
"慢速模式" // Pro 模型,质量优先
tab.assembledPrompt, [], "1:1", "2K", "慢速模式"
)
tab.currentTaskId = taskId
tab.statusText = "● 已提交"
......@@ -61,19 +97,20 @@ Item {
}
}
// 触发 ComboBox model 刷新(随机 / 重置 / 词库变化时让 currentIndex 重算)
property int comboReloadTrigger: 0
Component.onCompleted: reassemble()
Connections {
target: jewelry
function onLibraryChanged(category) {
optionsRepeater.reloadAll()
tab.comboReloadTrigger++
}
}
// 监听生成信号 — 只对自己提交的 task 做反应(taskId 唯一可区分本 tab vs 图片生成 tab)
Connections {
target: imageGen
function onTaskProgress(taskId, progress, msg) {
if (taskId !== tab.currentTaskId) return
tab.statusText = "● " + (msg || "生成中…")
......@@ -94,6 +131,60 @@ Item {
}
}
// ➕ 添加词条对话框
Dialog {
id: addItemDialog
title: "添加词条 - " + tab.addingCategory
anchors.centerIn: parent
modal: true
width: 400
standardButtons: Dialog.Ok | Dialog.Cancel
ColumnLayout {
anchors.fill: parent
spacing: App.Theme.space2
Label {
text: "请输入新的「" + tab.addingCategory + "」词条(纯中文):"
font.family: App.Theme.fontFamily
font.pointSize: App.Theme.fontSm
color: App.Theme.textPrimary
}
ThemedTextField {
id: addItemField
Layout.fillWidth: true
placeholderText: "例如:圆形"
}
}
onAccepted: {
var v = addItemField.text.trim()
if (v) jewelry.addItem(tab.addingCategory, v)
addItemField.text = ""
}
onRejected: addItemField.text = ""
}
// 💾 下载图片
FileDialog {
id: saveImageDialog
title: "保存生成的图片"
fileMode: FileDialog.SaveFile
nameFilters: ["PNG 图片 (*.png)"]
defaultSuffix: "png"
onAccepted: {
var dest = selectedFile.toString().replace("file:///", "")
var ok = imageGen.saveFile(tab.lastResultPath, dest)
if (ok) {
tab.statusText = "● 已保存到 " + dest
tab.statusColor = App.Theme.success
} else {
tab.statusText = "● 保存失败"
tab.statusColor = App.Theme.danger
}
}
}
RowLayout {
anchors.fill: parent
anchors.margins: App.Theme.space5
......@@ -101,7 +192,7 @@ Item {
// ===== 左侧:8 字段表单 =====
Card {
Layout.preferredWidth: 360
Layout.preferredWidth: 420
Layout.fillHeight: true
ColumnLayout {
......@@ -109,9 +200,33 @@ Item {
anchors.margins: App.Theme.space4
spacing: App.Theme.space3
CaptionLabel {
text: "款式字段"
font.pointSize: App.Theme.fontLg
// header
RowLayout {
spacing: App.Theme.space3
CaptionLabel {
text: "款式字段"
font.pointSize: App.Theme.fontLg
}
Item { Layout.fillWidth: true }
}
// 工具按钮行:随机 / 恢复默认词库 / 重置字段
RowLayout {
Layout.fillWidth: true
spacing: App.Theme.space2
SecondaryButton {
text: "🎲 随机"
onClicked: tab.randomize()
}
SecondaryButton {
text: "🔄 恢复默认词库"
onClicked: jewelry.resetAll()
}
SecondaryButton {
text: "重置字段"
onClicked: tab.resetFields()
}
Item { Layout.fillWidth: true }
}
ScrollView {
......@@ -124,16 +239,8 @@ Item {
spacing: App.Theme.space3
Repeater {
id: optionsRepeater
model: jewelry.categories // 8 个类别名
// task #15b 词库变化时重新拉
function reloadAll() {
// QML 自动重新评估 model:,主动 reset
model = []
model = jewelry.categories
}
delegate: ColumnLayout {
Layout.fillWidth: true
spacing: 4
......@@ -141,14 +248,68 @@ Item {
CaptionLabel {
text: modelData
font.pointSize: App.Theme.fontBase
color: tab.isLocked(modelData)
? App.Theme.textTertiary
: App.Theme.textPrimary
}
ThemedComboBox {
RowLayout {
Layout.fillWidth: true
// 头插一个 "(不选)" 让用户能清空字段
model: ["(不选)"].concat(jewelry.getOptions(modelData))
onActivated: {
var v = currentText === "(不选)" ? "" : currentText
tab.updateField(modelData, v)
spacing: App.Theme.space2
ThemedComboBox {
id: cb
// 固定宽度:fillWidth + maximumWidth 锁住,避免内容长度变化撑大 RowLayout
Layout.fillWidth: true
Layout.preferredWidth: 240
Layout.maximumWidth: 240
// 头插 "(不选)",用 trigger 强制每次随机/重置后刷新
property var fullModel: {
tab.comboReloadTrigger
return ["(不选)"].concat(jewelry.getOptions(modelData))
}
model: fullModel
// 同步 currentIndex 到 formData[modelData]
currentIndex: {
tab.comboReloadTrigger
var v = tab.formData[modelData] || ""
if (!v) return 0 // "(不选)"
var idx = fullModel.indexOf(v)
return idx >= 0 ? idx : 0
}
enabled: !tab.isLocked(modelData)
onActivated: {
var v = currentText === "(不选)" ? "" : currentText
tab.updateField(modelData, v)
}
}
// ➕ 添加词条
SecondaryButton {
text: "➕"
Layout.preferredWidth: 36
onClicked: {
tab.addingCategory = modelData
addItemDialog.open()
}
}
// 🗑️ 删除当前选中词条
SecondaryButton {
text: "🗑️"
Layout.preferredWidth: 36
enabled: (tab.formData[modelData] || "").length > 0
onClicked: {
var v = tab.formData[modelData]
if (v) {
jewelry.removeItem(modelData, v)
tab.updateField(modelData, "")
}
}
}
// 🔓 / 🔒 锁定字段(锁定的字段不参与随机)
SecondaryButton {
text: tab.isLocked(modelData) ? "🔒" : "🔓"
Layout.preferredWidth: 36
onClicked: tab.toggleLock(modelData)
}
}
}
......@@ -212,26 +373,20 @@ Item {
}
}
// 操作行
// 操作行:生成 + 下载 + 状态
RowLayout {
spacing: App.Theme.space3
Layout.fillWidth: true
PrimaryButton {
text: tab.currentTaskId !== "" ? "生成中…" : "生成图片"
text: tab.currentTaskId !== "" ? "生成中…" : "🎨 生成图片"
enabled: tab.currentTaskId === ""
onClicked: tab.submit()
}
SecondaryButton {
text: "重置字段"
onClicked: {
var fresh = {}
for (var i = 0; i < jewelry.categories.length; i++) {
fresh[jewelry.categories[i]] = ""
}
tab.formData = fresh
tab.reassemble()
}
text: "💾 下载图片"
enabled: tab.lastResultPath !== ""
onClicked: saveImageDialog.open()
}
Label {
text: tab.statusText
......@@ -288,7 +443,7 @@ Item {
Text {
anchors.centerIn: parent
visible: tab.lastResultPath === ""
text: "选择字段后点 \"生成图片\""
text: "点 \"🎲 随机\" 试手气,或选字段后点 \"🎨 生成图片\""
color: App.Theme.textTertiary
font.family: App.Theme.fontFamily
font.pointSize: App.Theme.fontSm
......