feat: 优化生成配置布局与选项控件
This commit is contained in:
@@ -167,7 +167,7 @@ onScopeDispose(() => controller?.abort())
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NCollapse class="production-tool-section" :default-expanded-names="['creative-profile']">
|
<NCollapse class="production-tool-section creative-profile-settings" :default-expanded-names="['creative-profile']">
|
||||||
<NCollapseItem name="creative-profile" title="项目画布与生成模型">
|
<NCollapseItem name="creative-profile" title="项目画布与生成模型">
|
||||||
<NSpin :show="loading">
|
<NSpin :show="loading">
|
||||||
<NAlert v-if="error" type="error" :show-icon="false" class="mb-3">{{ error }}</NAlert>
|
<NAlert v-if="error" type="error" :show-icon="false" class="mb-3">{{ error }}</NAlert>
|
||||||
@@ -220,7 +220,7 @@ onScopeDispose(() => controller?.abort())
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div class="page-form-actions mt-4">
|
<div class="page-form-actions creative-profile-actions">
|
||||||
<NButton
|
<NButton
|
||||||
type="primary"
|
type="primary"
|
||||||
:loading="saving"
|
:loading="saving"
|
||||||
@@ -238,11 +238,17 @@ onScopeDispose(() => controller?.abort())
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
@reference "../../../styles/styles.css";
|
@reference "../../../styles/styles.css";
|
||||||
.creative-profile-grid {
|
.creative-profile-grid {
|
||||||
@apply grid grid-cols-[repeat(2,_minmax(0,_1fr))] gap-x-5;
|
@apply grid grid-cols-[repeat(2,_minmax(0,_1fr))] gap-x-5 gap-y-5;
|
||||||
}
|
}
|
||||||
.model-section {
|
.model-section {
|
||||||
@apply min-w-0 p-4 bg-(--app-subtle);
|
@apply min-w-0 p-4 bg-(--app-subtle);
|
||||||
}
|
}
|
||||||
|
.creative-profile-settings {
|
||||||
|
@apply mb-7;
|
||||||
|
}
|
||||||
|
.creative-profile-actions {
|
||||||
|
@apply mt-5 pt-1;
|
||||||
|
}
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.creative-profile-grid {
|
.creative-profile-grid {
|
||||||
@apply grid-cols-[minmax(0,_1fr)];
|
@apply grid-cols-[minmax(0,_1fr)];
|
||||||
|
|||||||
@@ -23,6 +23,45 @@ function selectOptions(schema: GenerationOptionSchema) {
|
|||||||
: []
|
: []
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDuration(schema: GenerationOptionSchema) {
|
||||||
|
return /duration|时长/i.test(`${schema.key} ${schema.label}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionKey(value: string | number) {
|
||||||
|
return `${typeof value}:${value}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 尺寸和时长使用有限选项,避免用户输入 Provider 不支持的自由值。 */
|
||||||
|
function constrainedOptions(schema: GenerationOptionSchema, current: string | number | boolean | undefined) {
|
||||||
|
const options = selectOptions(schema)
|
||||||
|
if (schema.type === 'size') {
|
||||||
|
for (const value of ['1K', '1.5K', '2K']) options.push({ label: value, value })
|
||||||
|
} else if (
|
||||||
|
isDuration(schema) &&
|
||||||
|
Number.isInteger(schema.min) &&
|
||||||
|
Number.isInteger(schema.max) &&
|
||||||
|
schema.max! >= schema.min! &&
|
||||||
|
schema.max! - schema.min! <= 60
|
||||||
|
) {
|
||||||
|
for (let value = schema.min!; value <= schema.max!; value++) {
|
||||||
|
options.push({ label: `${value} 秒`, value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof schema.providerDefault === 'string' || typeof schema.providerDefault === 'number') {
|
||||||
|
options.push({ label: String(schema.providerDefault), value: schema.providerDefault })
|
||||||
|
}
|
||||||
|
if (typeof current === 'string' || typeof current === 'number') {
|
||||||
|
options.push({ label: String(current), value: current })
|
||||||
|
}
|
||||||
|
const seen = new Set<string>()
|
||||||
|
return options.filter(option => {
|
||||||
|
const key = optionKey(option.value)
|
||||||
|
if (seen.has(key)) return false
|
||||||
|
seen.add(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -42,11 +81,11 @@ function selectOptions(schema: GenerationOptionSchema) {
|
|||||||
>启用</NCheckbox
|
>启用</NCheckbox
|
||||||
>
|
>
|
||||||
<NSelect
|
<NSelect
|
||||||
v-else-if="schema.type === 'select'"
|
v-else-if="schema.type === 'select' || schema.type === 'size' || isDuration(schema)"
|
||||||
:value="selectValue(modelValue[schema.key])"
|
:value="selectValue(modelValue[schema.key])"
|
||||||
:options="selectOptions(schema)"
|
:options="constrainedOptions(schema, modelValue[schema.key])"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
clearable
|
:clearable="!schema.required"
|
||||||
@update:value="update(modelValue, schema.key, $event)"
|
@update:value="update(modelValue, schema.key, $event)"
|
||||||
/>
|
/>
|
||||||
<NInputNumber
|
<NInputNumber
|
||||||
@@ -75,8 +114,10 @@ function selectOptions(schema: GenerationOptionSchema) {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
@reference "../../../styles/styles.css";
|
@reference "../../../styles/styles.css";
|
||||||
.generation-option-grid {
|
.generation-option-grid {
|
||||||
grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
|
@apply grid grid-cols-[repeat(2,_minmax(0,_1fr))] gap-x-4 gap-y-5;
|
||||||
@apply grid gap-x-5 gap-y-1;
|
}
|
||||||
|
.generation-option-grid :deep(.n-form-item) {
|
||||||
|
@apply min-w-0 mb-0;
|
||||||
}
|
}
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.generation-option-grid {
|
.generation-option-grid {
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { NInputNumber, NSelect } from 'naive-ui'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import GenerationOptionFields from '../../../src/features/generation-config/components/GenerationOptionFields.vue'
|
||||||
|
import type { GenerationOptionSchema } from '../../../src/features/generation-config/types'
|
||||||
|
|
||||||
|
function mountFields(schemas: GenerationOptionSchema[]) {
|
||||||
|
return mount(GenerationOptionFields, {
|
||||||
|
props: { schemas, modelValue: { size: '2K', durationSeconds: 4, seed: 1 } }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('生成模型参数字段', () => {
|
||||||
|
it('图片尺寸使用下拉框并保留标准尺寸与当前值', () => {
|
||||||
|
const wrapper = mountFields([
|
||||||
|
{ key: 'size', label: '图片尺寸', type: 'size', required: true, providerDefault: 'auto' }
|
||||||
|
])
|
||||||
|
const select = wrapper.getComponent(NSelect)
|
||||||
|
expect(select.props('clearable')).toBe(false)
|
||||||
|
expect(select.props('options')).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ value: '1K' }),
|
||||||
|
expect.objectContaining({ value: '1.5K' }),
|
||||||
|
expect.objectContaining({ value: '2K' }),
|
||||||
|
expect.objectContaining({ value: 'auto' })
|
||||||
|
])
|
||||||
|
)
|
||||||
|
expect(wrapper.findComponent(NInputNumber).exists()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('视频时长按后端范围生成秒数选项,其他数值仍可输入', () => {
|
||||||
|
const wrapper = mountFields([
|
||||||
|
{
|
||||||
|
key: 'durationSeconds',
|
||||||
|
label: '生成时长',
|
||||||
|
type: 'integer',
|
||||||
|
required: true,
|
||||||
|
min: 4,
|
||||||
|
max: 6,
|
||||||
|
specialValues: [{ label: '自动', value: -1 }]
|
||||||
|
},
|
||||||
|
{ key: 'seed', label: '随机种子', type: 'integer', required: false }
|
||||||
|
])
|
||||||
|
const select = wrapper.getComponent(NSelect)
|
||||||
|
expect(select.props('options')).toEqual([
|
||||||
|
{ label: '自动', value: -1 },
|
||||||
|
{ label: '4 秒', value: 4 },
|
||||||
|
{ label: '5 秒', value: 5 },
|
||||||
|
{ label: '6 秒', value: 6 }
|
||||||
|
])
|
||||||
|
expect(wrapper.findAllComponents(NInputNumber)).toHaveLength(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user