507 lines
11 KiB
Vue
507 lines
11 KiB
Vue
|
|
<script setup lang="ts">
|
|||
|
|
import { ref, computed, watch } from 'vue'
|
|||
|
|
import type { ModelItem } from '@/types'
|
|||
|
|
|
|||
|
|
const props = defineProps<{
|
|||
|
|
modelValue: boolean
|
|||
|
|
models: ModelItem[]
|
|||
|
|
/** 当前已选模型 id,打开弹窗时用于初始化草稿与 Tab */
|
|||
|
|
currentModelId: string | number
|
|||
|
|
}>()
|
|||
|
|
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
(e: 'update:modelValue', value: boolean): void
|
|||
|
|
(e: 'confirm', modelId: string | number): void
|
|||
|
|
}>()
|
|||
|
|
|
|||
|
|
const activeModelTab = ref<'official' | 'mine'>('official')
|
|||
|
|
const activeModelSeries = ref('')
|
|||
|
|
const draftModelId = ref<string | number>('')
|
|||
|
|
const modelSearchKeyword = ref('')
|
|||
|
|
|
|||
|
|
/** 根据模型名提取系列名(去掉参数量与日期后缀) */
|
|||
|
|
function getModelSeries(name: string): string {
|
|||
|
|
return name
|
|||
|
|
.replace(/-\d+(?:\.\d+)?B.*$/i, '')
|
|||
|
|
.replace(/-\d+(?:\.\d+)?b.*$/i, '')
|
|||
|
|
.replace(/-\d{4}.*$/, '')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const dialogModels = computed(() => {
|
|||
|
|
const keyword = modelSearchKeyword.value.trim().toLowerCase()
|
|||
|
|
const tabModels = props.models.filter((model) => {
|
|||
|
|
if (activeModelTab.value === 'official') return model.model_source !== 'api'
|
|||
|
|
return model.model_source === 'api'
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
if (!keyword) return tabModels
|
|||
|
|
return tabModels.filter((model) => {
|
|||
|
|
return [model.name, model.description, model.path, model.online_model_name]
|
|||
|
|
.filter(Boolean)
|
|||
|
|
.some((text) => String(text).toLowerCase().includes(keyword))
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const modelSeriesGroups = computed(() => {
|
|||
|
|
const groupMap = new Map<string, ModelItem[]>()
|
|||
|
|
dialogModels.value.forEach((model) => {
|
|||
|
|
const series = getModelSeries(model.name)
|
|||
|
|
const list = groupMap.get(series) || []
|
|||
|
|
list.push(model)
|
|||
|
|
groupMap.set(series, list)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
return Array.from(groupMap.entries()).map(([name, list]) => ({
|
|||
|
|
name,
|
|||
|
|
count: list.length,
|
|||
|
|
models: list,
|
|||
|
|
}))
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const currentSeriesModels = computed(() => {
|
|||
|
|
return modelSeriesGroups.value.find((group) => group.name === activeModelSeries.value)?.models || []
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const draftModel = computed(() => props.models.find((model) => model.id === draftModelId.value))
|
|||
|
|
|
|||
|
|
function syncActiveModelSeries() {
|
|||
|
|
const firstSeries = modelSeriesGroups.value[0]?.name || ''
|
|||
|
|
if (!activeModelSeries.value || !modelSeriesGroups.value.some((group) => group.name === activeModelSeries.value)) {
|
|||
|
|
activeModelSeries.value = firstSeries
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 弹窗打开时初始化:草稿沿用当前选中、Tab 按来源切换、系列定位到草稿所在组
|
|||
|
|
watch(
|
|||
|
|
() => props.modelValue,
|
|||
|
|
(visible) => {
|
|||
|
|
if (visible) {
|
|||
|
|
const current = props.models.find((model) => model.id === props.currentModelId)
|
|||
|
|
activeModelTab.value = current?.model_source === 'api' ? 'mine' : 'official'
|
|||
|
|
draftModelId.value = props.currentModelId
|
|||
|
|
syncActiveModelSeries()
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
watch([activeModelTab, modelSearchKeyword], () => {
|
|||
|
|
syncActiveModelSeries()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
function chooseModelSeries(series: string) {
|
|||
|
|
activeModelSeries.value = series
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function chooseDraftModel(model: ModelItem) {
|
|||
|
|
draftModelId.value = model.id
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleCancel() {
|
|||
|
|
emit('update:modelValue', false)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleConfirm() {
|
|||
|
|
if (draftModelId.value !== '' && draftModelId.value !== undefined && draftModelId.value !== null) {
|
|||
|
|
emit('confirm', draftModelId.value)
|
|||
|
|
}
|
|||
|
|
emit('update:modelValue', false)
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<el-dialog
|
|||
|
|
:model-value="modelValue"
|
|||
|
|
class="model-select-dialog"
|
|||
|
|
width="860px"
|
|||
|
|
top="10vh"
|
|||
|
|
:show-close="false"
|
|||
|
|
append-to-body
|
|||
|
|
@update:model-value="emit('update:modelValue', $event)"
|
|||
|
|
>
|
|||
|
|
<template #header>
|
|||
|
|
<div class="model-dialog-header">
|
|||
|
|
<div class="model-dialog-title">
|
|||
|
|
<span>选择模型</span>
|
|||
|
|
<em>仅可选 1 个模型</em>
|
|||
|
|
</div>
|
|||
|
|
<button class="model-dialog-close" type="button" @click="handleCancel">
|
|||
|
|
<i class="fa fa-close" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<div class="model-dialog-body">
|
|||
|
|
<div class="model-dialog-toolbar">
|
|||
|
|
<el-tabs v-model="activeModelTab" class="model-tabs">
|
|||
|
|
<el-tab-pane label="官方模型" name="official" />
|
|||
|
|
<el-tab-pane label="我的模型" name="mine" />
|
|||
|
|
</el-tabs>
|
|||
|
|
<el-input
|
|||
|
|
v-model="modelSearchKeyword"
|
|||
|
|
class="model-search"
|
|||
|
|
placeholder="从全部模型中搜索"
|
|||
|
|
clearable
|
|||
|
|
>
|
|||
|
|
<template #suffix>
|
|||
|
|
<i class="fa fa-search" />
|
|||
|
|
</template>
|
|||
|
|
</el-input>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="model-dialog-content">
|
|||
|
|
<aside class="model-series-list">
|
|||
|
|
<div class="column-title">模型系列</div>
|
|||
|
|
<button
|
|||
|
|
v-for="series in modelSeriesGroups"
|
|||
|
|
:key="series.name"
|
|||
|
|
class="model-series-item"
|
|||
|
|
:class="{ active: activeModelSeries === series.name }"
|
|||
|
|
type="button"
|
|||
|
|
@click="chooseModelSeries(series.name)"
|
|||
|
|
>
|
|||
|
|
<i class="fa fa-star-o" />
|
|||
|
|
<span>{{ series.name }}</span>
|
|||
|
|
<em>{{ series.count }}</em>
|
|||
|
|
</button>
|
|||
|
|
<el-empty v-if="!modelSeriesGroups.length" description="暂无模型" :image-size="80" />
|
|||
|
|
</aside>
|
|||
|
|
|
|||
|
|
<section class="model-version-list">
|
|||
|
|
<div class="column-title">快照版本</div>
|
|||
|
|
<button
|
|||
|
|
v-for="model in currentSeriesModels"
|
|||
|
|
:key="model.id"
|
|||
|
|
class="model-version-item"
|
|||
|
|
:class="{ active: draftModelId === model.id }"
|
|||
|
|
type="button"
|
|||
|
|
@click="chooseDraftModel(model)"
|
|||
|
|
>
|
|||
|
|
<div class="model-version-main">
|
|||
|
|
<strong>{{ model.name }}</strong>
|
|||
|
|
<span>{{ model.description || model.path || model.online_model_name || '暂无模型描述' }}</span>
|
|||
|
|
</div>
|
|||
|
|
<span class="model-radio-dot" />
|
|||
|
|
</button>
|
|||
|
|
<el-empty v-if="!currentSeriesModels.length" description="暂无快照版本" :image-size="80" />
|
|||
|
|
</section>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<template #footer>
|
|||
|
|
<div class="model-dialog-footer">
|
|||
|
|
<div class="selected-model-summary">
|
|||
|
|
<span>已选 {{ draftModel ? 1 : 0 }}/1</span>
|
|||
|
|
<div v-if="draftModel" class="selected-model-pill">
|
|||
|
|
<i class="fa fa-star-o" />
|
|||
|
|
{{ draftModel.name }}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="model-dialog-actions">
|
|||
|
|
<el-button @click="handleCancel">取消</el-button>
|
|||
|
|
<el-button type="primary" @click="handleConfirm">确定</el-button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
</el-dialog>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped lang="scss">
|
|||
|
|
:deep(.model-select-dialog) {
|
|||
|
|
max-width: calc(100vw - 48px);
|
|||
|
|
border-radius: 6px;
|
|||
|
|
box-shadow: 0 18px 46px rgba(15, 23, 42, 0.18);
|
|||
|
|
|
|||
|
|
.el-dialog__header {
|
|||
|
|
padding: 0;
|
|||
|
|
margin: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.el-dialog__body {
|
|||
|
|
padding: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.el-dialog__footer {
|
|||
|
|
padding: 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-dialog-header {
|
|||
|
|
height: 56px;
|
|||
|
|
padding: 0 20px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
border-bottom: 1px solid #eef0f5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-dialog-title {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: baseline;
|
|||
|
|
gap: 12px;
|
|||
|
|
|
|||
|
|
span {
|
|||
|
|
font-size: 16px;
|
|||
|
|
font-weight: 700;
|
|||
|
|
color: #1f2937;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
em {
|
|||
|
|
font-style: normal;
|
|||
|
|
font-size: 13px;
|
|||
|
|
color: #94a3b8;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-dialog-close {
|
|||
|
|
width: 30px;
|
|||
|
|
height: 30px;
|
|||
|
|
border: 0;
|
|||
|
|
background: transparent;
|
|||
|
|
color: #64748b;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 16px;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
|
|||
|
|
&:hover {
|
|||
|
|
background: #f5f6fb;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-dialog-body {
|
|||
|
|
height: 452px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-dialog-toolbar {
|
|||
|
|
height: 76px;
|
|||
|
|
padding: 14px 20px 0;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: 18px;
|
|||
|
|
border-bottom: 1px solid #eef0f5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-tabs {
|
|||
|
|
min-width: 210px;
|
|||
|
|
|
|||
|
|
:deep(.el-tabs__item) {
|
|||
|
|
height: 42px;
|
|||
|
|
padding: 0 18px 0 0;
|
|||
|
|
font-size: 14px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #475569;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(.el-tabs__active-bar) {
|
|||
|
|
height: 2px;
|
|||
|
|
background: #2563eb;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(.el-tabs__nav-wrap::after) {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-search {
|
|||
|
|
width: 280px;
|
|||
|
|
|
|||
|
|
:deep(.el-input__wrapper) {
|
|||
|
|
border-radius: 6px;
|
|||
|
|
min-height: 36px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-dialog-content {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 280px 1fr;
|
|||
|
|
height: 376px;
|
|||
|
|
min-height: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-series-list,
|
|||
|
|
.model-version-list {
|
|||
|
|
padding: 16px 20px;
|
|||
|
|
overflow: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-series-list {
|
|||
|
|
border-right: 1px solid #eef0f5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.column-title {
|
|||
|
|
font-size: 13px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #64748b;
|
|||
|
|
margin-bottom: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-series-item,
|
|||
|
|
.model-version-item {
|
|||
|
|
width: 100%;
|
|||
|
|
border: 1px solid #e5e7eb;
|
|||
|
|
background: #fff;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.2s;
|
|||
|
|
color: #1f2937;
|
|||
|
|
|
|||
|
|
&:hover {
|
|||
|
|
border-color: #94a3b8;
|
|||
|
|
background: #f8fafc;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&.active {
|
|||
|
|
border-color: #2563eb;
|
|||
|
|
background: #eff6ff;
|
|||
|
|
box-shadow: inset 0 0 0 1px #2563eb;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-series-item {
|
|||
|
|
height: 44px;
|
|||
|
|
padding: 0 12px;
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: auto 1fr auto;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 10px;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
text-align: left;
|
|||
|
|
|
|||
|
|
i {
|
|||
|
|
color: #2563eb;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
span {
|
|||
|
|
font-size: 14px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
em {
|
|||
|
|
font-style: normal;
|
|||
|
|
color: #777b92;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-version-item {
|
|||
|
|
min-height: 64px;
|
|||
|
|
padding: 12px 14px;
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 1fr auto;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 14px;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
text-align: left;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-version-main {
|
|||
|
|
display: grid;
|
|||
|
|
gap: 6px;
|
|||
|
|
min-width: 0;
|
|||
|
|
|
|||
|
|
strong {
|
|||
|
|
color: #1f2937;
|
|||
|
|
font-size: 14px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
span {
|
|||
|
|
color: #64748b;
|
|||
|
|
font-size: 13px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-radio-dot {
|
|||
|
|
width: 16px;
|
|||
|
|
height: 16px;
|
|||
|
|
border: 1px solid #cbd5e1;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-version-item.active .model-radio-dot {
|
|||
|
|
border-color: #2563eb;
|
|||
|
|
|
|||
|
|
&::after {
|
|||
|
|
content: '';
|
|||
|
|
position: absolute;
|
|||
|
|
inset: 3px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: #2563eb;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-dialog-footer {
|
|||
|
|
height: 60px;
|
|||
|
|
padding: 0 20px;
|
|||
|
|
border-top: 1px solid #eef0f5;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.selected-model-summary {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 10px;
|
|||
|
|
color: #475569;
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.selected-model-pill {
|
|||
|
|
max-width: 300px;
|
|||
|
|
height: 28px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
padding: 0 10px;
|
|||
|
|
border: 1px solid #dbeafe;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
background: #eff6ff;
|
|||
|
|
color: #1f2937;
|
|||
|
|
font-weight: 600;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
|
|||
|
|
i {
|
|||
|
|
color: #2563eb;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-dialog-actions {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 900px) {
|
|||
|
|
.model-dialog-toolbar,
|
|||
|
|
.model-dialog-footer {
|
|||
|
|
height: auto;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
flex-direction: column;
|
|||
|
|
padding: 18px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-search {
|
|||
|
|
width: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-dialog-content {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-series-list {
|
|||
|
|
border-right: 0;
|
|||
|
|
border-bottom: 1px solid #eef0f5;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|