fix: 模型选择优化
This commit is contained in:
@@ -1,28 +1,40 @@
|
||||
<!-- 切换模型 -->
|
||||
<script setup lang="ts">
|
||||
import type { GetSessionListVO } from '@/api/model/types';
|
||||
import { Lock } from '@element-plus/icons-vue';
|
||||
import Popover from '@/components/Popover/index.vue';
|
||||
|
||||
import { Check, Lock, Right } from '@element-plus/icons-vue';
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||
import { useResponsive } from '@/hooks/useResponsive';
|
||||
import { useModelStore } from '@/stores/modules/model';
|
||||
import { showProductPackage } from '@/utils/product-package.ts';
|
||||
import { isUserVip } from '@/utils/user';
|
||||
import { modelList as localModelList } from './modelData';
|
||||
|
||||
const modelStore = useModelStore();
|
||||
const { isMobile } = useResponsive();
|
||||
const dialogVisible = ref(false);
|
||||
const activeTab = ref('provider'); // 'provider' | 'api'
|
||||
const scrollbarRef = ref();
|
||||
|
||||
// 检查模型是否可用
|
||||
function isModelAvailable(item: GetSessionListVO) {
|
||||
return isUserVip() || item.isFree;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await modelStore.requestModelList();
|
||||
// 虽然使用了本地数据用于展示,但可能仍需请求后端以保持某些状态同步,或者直接使用本地数据初始化store
|
||||
// 这里我们优先使用本地数据来填充store,或者仅在UI上使用本地数据
|
||||
// 为了兼容现有逻辑,我们尽量保持 modelStore 的使用,但列表展示主要依赖 localModelList
|
||||
// 如果后端返回列表为空,可以用本地列表兜底
|
||||
if (modelStore.modelList.length === 0) {
|
||||
modelStore.modelList = localModelList;
|
||||
}
|
||||
|
||||
// 设置默认模型
|
||||
if (
|
||||
modelStore.modelList.length > 0
|
||||
&& (!modelStore.currentModelInfo || !modelStore.currentModelInfo.modelId)
|
||||
(!modelStore.currentModelInfo || !modelStore.currentModelInfo.modelId)
|
||||
&& localModelList.length > 0
|
||||
) {
|
||||
modelStore.setCurrentModelInfo(modelStore.modelList[0]);
|
||||
modelStore.setCurrentModelInfo(localModelList[0]);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -38,42 +50,65 @@ const apiTypeNameMap: Record<string, string> = {
|
||||
GenerateContent: 'Google Gemini API',
|
||||
};
|
||||
|
||||
// 按 API 类型分组的模型列表
|
||||
const groupedModelList = computed(() => {
|
||||
// 按 API 类型分组
|
||||
const groupedByApiType = computed(() => {
|
||||
const groups: Record<string, GetSessionListVO[]> = {};
|
||||
|
||||
modelStore.modelList.forEach((model) => {
|
||||
localModelList.forEach((model) => {
|
||||
const apiType = model.modelApiType || 'Completions';
|
||||
if (!groups[apiType]) {
|
||||
groups[apiType] = [];
|
||||
}
|
||||
groups[apiType].push(model);
|
||||
});
|
||||
|
||||
return groups;
|
||||
});
|
||||
|
||||
console.log('groupedModelList---', groupedModelList.value);
|
||||
/* 弹出面板 开始 */
|
||||
const popoverStyle = ref({
|
||||
width: '200px',
|
||||
padding: '4px',
|
||||
height: 'fit-content',
|
||||
background: 'var(--el-bg-color, #fff)',
|
||||
border: '1px solid var(--el-border-color-light)',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 12px 0 rgba(0, 0, 0, 0.1)',
|
||||
// 按 厂商 (Provider) 分组
|
||||
const groupedByProvider = computed(() => {
|
||||
const groups: Record<string, GetSessionListVO[]> = {};
|
||||
localModelList.forEach((model) => {
|
||||
const provider = model.providerName || 'Other';
|
||||
if (!groups[provider]) {
|
||||
groups[provider] = [];
|
||||
}
|
||||
groups[provider].push(model);
|
||||
});
|
||||
return groups;
|
||||
});
|
||||
const popoverRef = ref();
|
||||
|
||||
// 显示
|
||||
async function showPopover() {
|
||||
// 获取最新的模型列表
|
||||
|
||||
return await modelStore.requestModelList();
|
||||
// 打开弹窗
|
||||
function openDialog() {
|
||||
dialogVisible.value = true;
|
||||
// 打开时定位到当前模型
|
||||
nextTick(() => {
|
||||
scrollToCurrentModel();
|
||||
});
|
||||
}
|
||||
|
||||
// 监听 tab 切换,自动定位
|
||||
watch(activeTab, () => {
|
||||
nextTick(() => {
|
||||
scrollToCurrentModel();
|
||||
});
|
||||
});
|
||||
|
||||
// 定位到当前模型
|
||||
function scrollToCurrentModel() {
|
||||
const currentId = modelStore.currentModelInfo?.modelId;
|
||||
if (!currentId)
|
||||
return;
|
||||
|
||||
// 根据当前 tab 构建对应的 ID
|
||||
const elementId = activeTab.value === 'provider'
|
||||
? `provider-model-${currentId}`
|
||||
: `api-model-${currentId}`;
|
||||
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
// 点击
|
||||
// 处理模型点击
|
||||
function handleModelClick(item: GetSessionListVO) {
|
||||
if (!isModelAvailable(item)) {
|
||||
@@ -111,24 +146,24 @@ function handleModelClick(item: GetSessionListVO) {
|
||||
.catch(() => {
|
||||
// 点击右上角关闭或“关闭”按钮,不执行任何操作
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
modelStore.setCurrentModelInfo(item);
|
||||
popoverRef.value?.hide?.();
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
|
||||
function goToModelLibrary() {
|
||||
window.location.href = '/model-library';
|
||||
}
|
||||
|
||||
/* -------------------------------
|
||||
模型样式规则
|
||||
规则1:普通灰色(免费模型)
|
||||
规则2:金色光泽(VIP/付费)
|
||||
规则3:彩色流光(尊享/高级)
|
||||
-------------------------------- */
|
||||
function getModelStyleClass(mode: any) {
|
||||
if (!mode) {
|
||||
if (!mode)
|
||||
return;
|
||||
}
|
||||
// isPremiumPackage
|
||||
const name = mode.modelName.toLowerCase();
|
||||
|
||||
const isPremiumPackage = mode.isPremiumPackage;
|
||||
|
||||
// 规则3:彩色流光
|
||||
@@ -151,99 +186,166 @@ function getModelStyleClass(mode: any) {
|
||||
before:content-[''] before:absolute before:-inset-2 before:-z-10
|
||||
before:animate-goldShine
|
||||
`;
|
||||
// 金色背景
|
||||
// before:bg-[linear-gradient(135deg,#BF953F,#FCF6BA,#B38728,#FBF5B7,#AA771C)]
|
||||
}
|
||||
|
||||
/* -------------------------------
|
||||
外层卡片样式(选中态 + hover 动效)
|
||||
-------------------------------- */
|
||||
function getWrapperClass(item: GetSessionListVO) {
|
||||
const isSelected = item.modelName === currentModelName.value;
|
||||
const isSelected = item.modelId === modelStore.currentModelInfo?.modelId;
|
||||
const available = isModelAvailable(item);
|
||||
|
||||
return [
|
||||
'p-2 rounded-md text-sm transition-all duration-300 relative select-none flex items-center justify-between',
|
||||
'p-3 rounded-lg text-sm transition-all duration-300 relative select-none flex items-center justify-between cursor-pointer mb-2',
|
||||
available
|
||||
? 'hover:scale-[1.03] hover:shadow-[0_0_8px_rgba(0,0,0,0.1)] hover:border-gray-300'
|
||||
: 'opacity-60 cursor-not-allowed',
|
||||
? 'hover:bg-gray-50 hover:shadow-sm'
|
||||
: 'opacity-60 cursor-not-allowed bg-gray-50',
|
||||
isSelected
|
||||
? 'border-2 border-blue-700 shadow-[0_0_10px_rgba(29,78,216,1)]'
|
||||
: 'border border-transparent cursor-pointer',
|
||||
? 'border-2 border-primary bg-primary-light-9 shadow-md'
|
||||
: 'border border-gray-200',
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="model-select" data-tour="model-select">
|
||||
<Popover
|
||||
ref="popoverRef"
|
||||
placement="top-start"
|
||||
:offset="[4, 0]"
|
||||
popover-class="popover-content"
|
||||
:popover-style="popoverStyle"
|
||||
trigger="clickTarget"
|
||||
@show="showPopover"
|
||||
<!-- 触发按钮 -->
|
||||
<div
|
||||
class="model-select-box select-none flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-12px border-[rgba()] leading-snug"
|
||||
@click="openDialog"
|
||||
>
|
||||
<!-- 触发元素插槽 -->
|
||||
<template #trigger>
|
||||
<div
|
||||
class="model-select-box select-none flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-12px border-[rgba()] leading-snug"
|
||||
>
|
||||
<div class="model-select-box-icon">
|
||||
<SvgIcon name="models" size="12" />
|
||||
</div>
|
||||
<div :class="getModelStyleClass(modelStore.currentModelInfo)" class="model-select-box-text font-size-12px">
|
||||
{{ currentModelName }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="popover-content-box">
|
||||
<!-- 按 API 类型分组展示 -->
|
||||
<template v-for="(models, apiType) in groupedModelList" :key="apiType">
|
||||
<!-- 分组标题 -->
|
||||
<div class="group-title">
|
||||
{{ apiTypeNameMap[apiType] || apiType }}
|
||||
</div>
|
||||
|
||||
<!-- 该分组下的模型列表 -->
|
||||
<div
|
||||
v-for="item in models"
|
||||
:key="item.id"
|
||||
:class="getWrapperClass(item)"
|
||||
@click="handleModelClick(item)"
|
||||
>
|
||||
<Popover
|
||||
trigger-class="popover-trigger-item-text"
|
||||
popover-class="rounded-tooltip"
|
||||
placement="right"
|
||||
trigger="hover"
|
||||
:offset="[12, 0]"
|
||||
>
|
||||
<template #trigger>
|
||||
<span :class="getModelStyleClass(item)">
|
||||
{{ item.modelName }}
|
||||
</span>
|
||||
</template>
|
||||
<div
|
||||
class="popover-content-box-item-text text-wrap max-w-200px rounded-lg p-8px font-size-12px line-height-tight"
|
||||
>
|
||||
{{ item.remark }}
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<!-- VIP锁定图标 -->
|
||||
<el-icon
|
||||
v-if="!isModelAvailable(item)"
|
||||
class="absolute right-1 top-1/2 transform -translate-y-1/2"
|
||||
>
|
||||
<Lock />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<div class="model-select-box-icon">
|
||||
<SvgIcon name="models" size="12" />
|
||||
</div>
|
||||
</Popover>
|
||||
<div :class="getModelStyleClass(modelStore.currentModelInfo)" class="model-select-box-text font-size-12px">
|
||||
{{ currentModelName || '选择模型' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型选择弹窗 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="切换模型"
|
||||
:width="isMobile ? '95%' : '600px'"
|
||||
class="model-select-dialog"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
align-center
|
||||
>
|
||||
<div class="model-list-container relative">
|
||||
<!-- 右上角前往模型库按钮 -->
|
||||
<div class="absolute right-0 top-0 z-10">
|
||||
<el-button type="primary" link size="small" @click="goToModelLibrary">
|
||||
前往模型库
|
||||
<el-icon class="ml-1">
|
||||
<Right />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab" class="model-tabs">
|
||||
<!-- 厂商分类 Tab -->
|
||||
<el-tab-pane label="厂商类型" name="provider">
|
||||
<el-scrollbar ref="scrollbarRef" height="400px">
|
||||
<div class="px-2">
|
||||
<template v-for="(models, provider) in groupedByProvider" :key="provider">
|
||||
<div class="group-title text-gray-500 text-xs font-bold mb-2 mt-4 px-1">
|
||||
{{ provider }}
|
||||
</div>
|
||||
<div
|
||||
v-for="item in models"
|
||||
:id="`provider-model-${item.modelId}`"
|
||||
:key="item.id"
|
||||
:class="getWrapperClass(item)"
|
||||
@click="handleModelClick(item)"
|
||||
>
|
||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||
<!-- 模型 Logo -->
|
||||
<div class="w-8 h-8 flex-shrink-0 rounded-full bg-white border border-gray-100 flex items-center justify-center overflow-hidden p-1">
|
||||
<img v-if="item.iconUrl" :src="item.iconUrl" class="w-full h-full object-contain" alt="icon">
|
||||
<SvgIcon v-else name="models" size="16" />
|
||||
</div>
|
||||
|
||||
<!-- 模型信息 -->
|
||||
<div class="flex flex-col gap-0.5 flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span :class="getModelStyleClass(item)" class="font-medium truncate">
|
||||
{{ item.modelName }}
|
||||
</span>
|
||||
<span v-if="item.isFree" class="text-[10px] px-1.5 py-0.5 bg-green-100 text-green-600 rounded-full">免费</span>
|
||||
<span v-if="item.isPremiumPackage" class="text-[10px] px-1.5 py-0.5 bg-orange-100 text-orange-600 rounded-full">VIP</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 truncate" :title="item.modelDescribe">
|
||||
{{ item.modelDescribe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选中/锁定图标 -->
|
||||
<div class="flex items-center">
|
||||
<el-icon v-if="item.modelId === modelStore.currentModelInfo?.modelId" class="text-primary mr-2" :size="18">
|
||||
<Check />
|
||||
</el-icon>
|
||||
<el-icon v-if="!isModelAvailable(item)" class="text-gray-400">
|
||||
<Lock />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- API类型分类 Tab -->
|
||||
<el-tab-pane label="API类型" name="api">
|
||||
<el-scrollbar height="400px">
|
||||
<div class="px-2">
|
||||
<template v-for="(models, apiType) in groupedByApiType" :key="apiType">
|
||||
<div class="group-title text-gray-500 text-xs font-bold mb-2 mt-4 px-1">
|
||||
{{ apiTypeNameMap[apiType] || apiType }}
|
||||
</div>
|
||||
<div
|
||||
v-for="item in models"
|
||||
:id="`api-model-${item.modelId}`"
|
||||
:key="item.id"
|
||||
:class="getWrapperClass(item)"
|
||||
@click="handleModelClick(item)"
|
||||
>
|
||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div class="w-8 h-8 flex-shrink-0 rounded-full bg-white border border-gray-100 flex items-center justify-center overflow-hidden p-1">
|
||||
<img v-if="item.iconUrl" :src="item.iconUrl" class="w-full h-full object-contain" alt="icon">
|
||||
<SvgIcon v-else name="models" size="16" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span :class="getModelStyleClass(item)" class="font-medium truncate">
|
||||
{{ item.modelName }}
|
||||
</span>
|
||||
<span v-if="item.isFree" class="text-[10px] px-1.5 py-0.5 bg-green-100 text-green-600 rounded-full">免费</span>
|
||||
<span v-if="item.isPremiumPackage" class="text-[10px] px-1.5 py-0.5 bg-orange-100 text-orange-600 rounded-full">VIP</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 truncate" :title="item.modelDescribe">
|
||||
{{ item.modelDescribe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 选中/锁定图标 -->
|
||||
<div class="flex items-center">
|
||||
<el-icon v-if="item.modelId === modelStore.currentModelInfo?.modelId" class="text-primary mr-2" :size="18">
|
||||
<Check />
|
||||
</el-icon>
|
||||
<el-icon v-if="!isModelAvailable(item)" class="text-gray-400">
|
||||
<Lock />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -255,46 +357,15 @@ function getWrapperClass(item: GetSessionListVO) {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.popover-content-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
height: 300px;
|
||||
overflow: hidden auto;
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
:deep(.model-select-dialog) {
|
||||
max-width: 100% !important;
|
||||
margin-top: 10vh !important;
|
||||
|
||||
// 分组标题样式
|
||||
.group-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #409eff;
|
||||
padding: 8px 8px 4px 8px;
|
||||
margin-top: 4px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--el-bg-color, #fff);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
:deep(.popover-trigger-item-text) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.popover-content-box-item-text {
|
||||
color: white;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
// 滚动条样式
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #cccccc;
|
||||
border-radius: 4px;
|
||||
.el-dialog__body {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,19 +381,21 @@ function getWrapperClass(item: GetSessionListVO) {
|
||||
100% { transform: translateX(100%) translateY(100%); }
|
||||
}
|
||||
|
||||
/* 柔光 hover 动效 */
|
||||
@keyframes glowPulse {
|
||||
0%, 100% { box-shadow: 0 0 6px rgba(37,99,235,0.2); }
|
||||
50% { box-shadow: 0 0 10px rgba(37,99,235,0.5); }
|
||||
}
|
||||
|
||||
.animate-gradientFlow {
|
||||
animation: gradientFlow 3s ease infinite;
|
||||
}
|
||||
.animate-goldShine {
|
||||
animation: goldShine 4s linear infinite;
|
||||
}
|
||||
.animate-glowPulse {
|
||||
animation: glowPulse 2s ease-in-out infinite;
|
||||
|
||||
/* 定义一些颜色变量辅助类,如果项目没有定义的话 */
|
||||
.text-primary {
|
||||
color: var(--el-color-primary, #409eff);
|
||||
}
|
||||
.bg-primary-light-9 {
|
||||
background-color: var(--el-color-primary-light-9, #ecf5ff);
|
||||
}
|
||||
.border-primary {
|
||||
border-color: var(--el-color-primary, #409eff);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user