- 输入框从75%缩至62% - 发送按钮去掉文字改为44px正圆形,无提示词时浅灰底+深色箭头,有提示词时稍深灰底+白色箭头 - 绘画平台 getDefaultModel() 返回空字符串,modelSelector 加载后优先选 text tag 模型 - CLAUDE.md 新增 Display Store/Canvas/视图层架构章节,移除已删除的 model-configs 目录,修正环境变量表
169 lines
4.8 KiB
Vue
169 lines
4.8 KiB
Vue
<template>
|
||
<Select
|
||
v-model="selectValue"
|
||
:grouped-options="modelGroups"
|
||
class="model-select"
|
||
position="top"
|
||
>
|
||
<template #prefix>
|
||
<img src="@/assets/dialog/model.svg" alt="" style="width: 16px;">
|
||
</template>
|
||
</Select>
|
||
</template>
|
||
|
||
<script setup>
|
||
import Select from '@/components/Select/index.vue'
|
||
import { fetchPlatformModels, getPlatformCode } from '@/utils/modelApi'
|
||
|
||
const props = defineProps({
|
||
modelValue: { type: String, default: 'Flux 2' },
|
||
typeValue: { type: String, default: 'text' }
|
||
})
|
||
|
||
const emit = defineEmits(['update:modelValue', 'update:typeValue'])
|
||
|
||
const platformModels = ref([])
|
||
|
||
const categoryMap = [
|
||
{ tag: 'text', label: '生成模型', inputType: 'text' },
|
||
{ tag: 'edit', label: '编辑模型', inputType: 'image' },
|
||
{ tag: 'vision', label: '视觉理解模型', inputType: 'vision' }
|
||
]
|
||
|
||
function parseValue(encoded) {
|
||
if (!encoded) return null
|
||
const idx = encoded.indexOf('::')
|
||
if (idx === -1) return null
|
||
return { tag: encoded.substring(0, idx), modelName: encoded.substring(idx + 2) }
|
||
}
|
||
|
||
function encodeValue(tag, modelName) {
|
||
return `${tag}::${modelName}`
|
||
}
|
||
|
||
function findTagForModel(modelName) {
|
||
for (const cat of categoryMap) {
|
||
const model = platformModels.value.find((m) => (m.display_name || m.name) === modelName && m.tags?.includes(cat.tag))
|
||
if (model) return cat.tag
|
||
}
|
||
return 'text'
|
||
}
|
||
|
||
function tagToInputType(tag) {
|
||
const cat = categoryMap.find((c) => c.tag === tag)
|
||
return cat?.inputType || 'text'
|
||
}
|
||
|
||
// Select 双向绑定值(内部编码)
|
||
const selectValue = computed({
|
||
get: () => {
|
||
if (!props.modelValue) return ''
|
||
const tag = findTagForModel(props.modelValue)
|
||
return encodeValue(tag, props.modelValue)
|
||
},
|
||
set: (encoded) => {
|
||
const parsed = parseValue(encoded)
|
||
if (!parsed) return
|
||
emit('update:modelValue', parsed.modelName)
|
||
emit('update:typeValue', tagToInputType(parsed.tag))
|
||
}
|
||
})
|
||
|
||
// 从 API 加载模型列表
|
||
const loadModels = async () => {
|
||
try {
|
||
const code = getPlatformCode('Painting')
|
||
const models = await fetchPlatformModels(code)
|
||
platformModels.value = models || []
|
||
} catch (error) {
|
||
console.error('加载平台模型列表失败:', error)
|
||
}
|
||
}
|
||
|
||
loadModels()
|
||
|
||
// 按固定分类分组,value 编码为 tag::displayName
|
||
const modelGroups = computed(() => {
|
||
const models = platformModels.value
|
||
if (models.length === 0) return []
|
||
|
||
return categoryMap
|
||
.filter((cat) => models.some((m) => m.tags?.includes(cat.tag)))
|
||
.map((cat) => ({
|
||
label: cat.label,
|
||
options: models
|
||
.filter((m) => m.tags?.includes(cat.tag))
|
||
.map((m) => ({
|
||
value: `${cat.tag}::${m.display_name || m.name}`,
|
||
label: m.display_name || m.name,
|
||
disabled: m.disabled || false
|
||
}))
|
||
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }))
|
||
}))
|
||
})
|
||
|
||
// 模型列表加载后自动纠正不可用模型,优先选文生图(text tag)第一个
|
||
watch(platformModels, (models) => {
|
||
if (models.length === 0) return
|
||
const currentModel = models.find((m) => (m.display_name || m.name) === props.modelValue || m.id === props.modelValue)
|
||
if (!currentModel || currentModel.disabled) {
|
||
const firstText = models.find((m) => !m.disabled && m.tags?.includes('text'))
|
||
const fallback = firstText || models.find((m) => !m.disabled)
|
||
if (fallback) {
|
||
emit('update:modelValue', fallback.display_name || fallback.name)
|
||
emit('update:typeValue', tagToInputType(findTagForModel(fallback.display_name || fallback.name)))
|
||
}
|
||
}
|
||
}, { immediate: true })
|
||
|
||
// 外部改变 modelValue 时校验是否可用
|
||
watch(() => props.modelValue, (newValue) => {
|
||
if (!newValue) return
|
||
const models = platformModels.value
|
||
if (models.length === 0) return
|
||
const currentModel = models.find((m) => (m.display_name || m.name) === newValue)
|
||
if (currentModel && currentModel.disabled) {
|
||
const firstEnabled = models.find((m) => !m.disabled)
|
||
if (firstEnabled) {
|
||
emit('update:modelValue', firstEnabled.display_name || firstEnabled.name)
|
||
emit('update:typeValue', tagToInputType(findTagForModel(firstEnabled.display_name || firstEnabled.name)))
|
||
}
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.model-select {
|
||
:deep(.select-header) {
|
||
height: 40px;
|
||
padding: 0 15px;
|
||
border-radius: 10px;
|
||
border: 1px solid #E8E9EB;
|
||
background: #f5f6f7;
|
||
|
||
&:hover {
|
||
background: #e9eaeb;
|
||
}
|
||
}
|
||
|
||
:deep(.select-text) {
|
||
font-size: 14px;
|
||
}
|
||
|
||
:deep(.dropdown-menu) {
|
||
max-height: 510px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
:deep(.dropdown-item) {
|
||
min-width: 120px;
|
||
|
||
&.active {
|
||
background: rgba(0, 15, 51, 0.10);
|
||
color: #000F33;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
</style>
|