147 lines
5.2 KiB
Vue
147 lines
5.2 KiB
Vue
<template>
|
|
<div class="model-select">
|
|
<span class="model-select-label">{{ label }}</span>
|
|
<AppMenu v-model:open="isOpen" :min-width="280">
|
|
<template #trigger>
|
|
<button type="button" class="model-select-trigger" :class="{ open: isOpen }">
|
|
<img v-if="triggerIcon" :src="triggerIcon" class="model-select-icon" alt="" />
|
|
<span v-else-if="currentOption?.provider" class="model-select-provider">{{ currentOption.provider }}</span>
|
|
<span class="model-select-value">{{ currentLabel }}</span>
|
|
<ChevronDown :size="11" class="model-select-arrow" :class="{ open: isOpen }" />
|
|
</button>
|
|
</template>
|
|
|
|
<AppMenuItem v-if="!hideDefault" :selected="modelValue === ''" @click="pick('')">
|
|
<img v-if="defaultIcon" :src="defaultIcon" class="model-select-icon sm" alt="" />
|
|
<span class="model-select-dim">{{ effectiveDefaultLabel }}</span>
|
|
</AppMenuItem>
|
|
<AppMenuItem
|
|
v-for="o in options"
|
|
:key="o.key || o.model"
|
|
:selected="modelValue === (o.key || o.model)"
|
|
:title="`${o.model}${o.configName ? ` · ${o.configName}` : ''}`"
|
|
@click="pick(o.key || o.model)"
|
|
>
|
|
<span class="model-select-option-name">{{ optionLabel(o) }}</span>
|
|
<template v-if="modelIconUrl(o.provider, o.model)" #trailing>
|
|
<img :src="modelIconUrl(o.provider, o.model)" class="model-select-icon sm" alt="" />
|
|
</template>
|
|
</AppMenuItem>
|
|
</AppMenu>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { ChevronDown } from 'lucide-vue-next'
|
|
import { modelIconUrl } from '~/composables/useProviderIcon'
|
|
|
|
const props = defineProps({
|
|
label: { type: String, required: true }, // 改写 / 图片 / 视频
|
|
modelValue: { type: String, default: '' }, // '' = 默认(配置首个模型);选中值为 'provider/model' 复合键
|
|
options: { type: Array, default: () => [] }, // [{ key, model, provider, configId, configName }]
|
|
defaultLabel: { type: String, default: '' }, // 缺省回退 t('common.default')
|
|
showConfig: { type: Boolean, default: false }, // 多配置时显示来源配置名
|
|
hideDefault: { type: Boolean, default: false }, // 无「默认」语义的选择器(如分辨率)隐藏默认项
|
|
})
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
const { t } = useI18n()
|
|
|
|
const isOpen = ref(false)
|
|
|
|
const effectiveDefaultLabel = computed(() => props.defaultLabel || t('common.default'))
|
|
const currentOption = computed(() => props.options.find(o => (o.key || o.model) === props.modelValue) || null)
|
|
const optionLabel = o => o.gpu && o.gpu !== 'auto'
|
|
? `${o.gpu.toUpperCase()}${o.comfyLabel ? ` · ${o.comfyLabel}` : ''} · ${o.model}`
|
|
: props.showConfig && o.configName ? `${o.model} · ${o.configName}` : o.model
|
|
const currentLabel = computed(() => currentOption.value ? optionLabel(currentOption.value) : effectiveDefaultLabel.value)
|
|
// 默认 = 首个选项;未显式选模型时也展示默认模型的厂商图标(与 defaultLabel 取 options[0] 同口径)
|
|
// 图标按模型名推断(网关 provider=openai 但模型实为 deepseek 等场景),回退 provider
|
|
const defaultIcon = computed(() => modelIconUrl(props.options[0]?.provider, props.options[0]?.model))
|
|
const triggerIcon = computed(() =>
|
|
currentOption.value
|
|
? modelIconUrl(currentOption.value.provider, currentOption.value.model)
|
|
: defaultIcon.value)
|
|
|
|
function pick(model) {
|
|
emit('update:modelValue', model)
|
|
isOpen.value = false
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.model-select {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.model-select-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: var(--text-3);
|
|
}
|
|
.model-select-trigger {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
height: 26px;
|
|
max-width: 210px;
|
|
padding: 0 8px 0 10px;
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: 999px;
|
|
background: var(--bg-input);
|
|
color: var(--text-1);
|
|
font: 11px var(--font-body);
|
|
cursor: pointer;
|
|
transition: border-color 0.16s, box-shadow 0.16s, background 0.16s;
|
|
}
|
|
.model-select-trigger:hover { border-color: var(--border-hover); }
|
|
.model-select-trigger.open,
|
|
.model-select-trigger:focus-visible {
|
|
border-color: var(--border-focus);
|
|
box-shadow: 0 0 0 3px var(--button-focus);
|
|
outline: none;
|
|
}
|
|
.model-select-value {
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.model-select-icon {
|
|
flex-shrink: 0;
|
|
width: 14px;
|
|
height: 14px;
|
|
object-fit: contain;
|
|
border-radius: 2.5px;
|
|
}
|
|
.model-select-icon.sm { width: 12px; height: 12px; }
|
|
.model-select-provider {
|
|
flex-shrink: 0;
|
|
padding: 1px 5px;
|
|
border-radius: 4px;
|
|
background: var(--accent-bg);
|
|
color: var(--accent);
|
|
font-size: 9px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
.model-select-arrow {
|
|
flex-shrink: 0;
|
|
color: var(--text-3);
|
|
transition: transform 0.18s var(--ease-out);
|
|
}
|
|
.model-select-arrow.open { transform: rotate(180deg); }
|
|
|
|
/* 菜单项:模型名完整优先,超长才省略(tooltip 有全名) */
|
|
.model-select-option-name {
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
max-width: 320px;
|
|
}
|
|
.model-select-dim { color: var(--text-3); }
|
|
</style>
|