style: 修改模型选择列表
This commit is contained in:
@@ -113,7 +113,7 @@ namespace Yi.Abp.Web
|
||||
//本地开发环境,可以禁用作业执行
|
||||
if (host.IsDevelopment())
|
||||
{
|
||||
//Configure<AbpBackgroundWorkerOptions>(options => { options.IsEnabled = false; });
|
||||
Configure<AbpBackgroundWorkerOptions>(options => { options.IsEnabled = false; });
|
||||
}
|
||||
|
||||
//请求日志
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
<body>
|
||||
<!-- 加载动画容器 -->
|
||||
<div id="yixinai-loader" class="loader-container">
|
||||
<div class="loader-title">意心Ai 3.1</div>
|
||||
<div class="loader-title">意心Ai 3.2</div>
|
||||
<div class="loader-subtitle">海外地址,仅首次访问预计加载约10秒,无需梯子</div>
|
||||
<div class="loader-logo">
|
||||
<div class="pulse-box"></div>
|
||||
|
||||
@@ -14,12 +14,65 @@ const { isMobile } = useResponsive();
|
||||
const dialogVisible = ref(false);
|
||||
const activeTab = ref('provider'); // 'provider' | 'api'
|
||||
const scrollbarRef = ref();
|
||||
const activeProviderGroup = ref('');
|
||||
const activeApiGroup = ref('');
|
||||
const isScrolling = ref(false);
|
||||
|
||||
// 检查模型是否可用
|
||||
function isModelAvailable(item: GetSessionListVO) {
|
||||
return isUserVip() || item.isFree;
|
||||
}
|
||||
|
||||
// 滚动到指定分组
|
||||
function scrollToGroup(type: 'provider' | 'api', key: string) {
|
||||
isScrolling.value = true;
|
||||
const id = type === 'provider' ? `group-provider-${key}` : `group-api-${key}`;
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
if (type === 'provider') {
|
||||
activeProviderGroup.value = key;
|
||||
}
|
||||
else {
|
||||
activeApiGroup.value = key;
|
||||
}
|
||||
// 延迟重置滚动状态,防止触发 scroll 事件
|
||||
setTimeout(() => {
|
||||
isScrolling.value = false;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// 监听滚动事件,更新侧边栏选中状态
|
||||
function handleScroll({ scrollTop }: { scrollTop: number }) {
|
||||
if (isScrolling.value)
|
||||
return;
|
||||
|
||||
const type = activeTab.value;
|
||||
const groups = type === 'provider' ? groupedByProvider.value : groupedByApiType.value;
|
||||
const keys = Object.keys(groups);
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const id = type === 'provider' ? `group-provider-${key}` : `group-api-${key}`;
|
||||
const element = document.getElementById(id);
|
||||
|
||||
if (element) {
|
||||
const { offsetTop, offsetHeight } = element;
|
||||
// 这里的 50 是一个偏移量,可以根据实际情况调整
|
||||
if (scrollTop >= offsetTop - 50 && scrollTop < offsetTop + offsetHeight) {
|
||||
if (type === 'provider') {
|
||||
activeProviderGroup.value = key;
|
||||
}
|
||||
else {
|
||||
activeApiGroup.value = key;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 虽然使用了本地数据用于展示,但可能仍需请求后端以保持某些状态同步,或者直接使用本地数据初始化store
|
||||
// 这里我们优先使用本地数据来填充store,或者仅在UI上使用本地数据
|
||||
@@ -53,7 +106,7 @@ const apiTypeNameMap: Record<string, string> = {
|
||||
// 按 API 类型分组
|
||||
const groupedByApiType = computed(() => {
|
||||
const groups: Record<string, GetSessionListVO[]> = {};
|
||||
localModelList.forEach((model) => {
|
||||
modelStore.modelList.forEach((model) => {
|
||||
const apiType = model.modelApiType || 'Completions';
|
||||
if (!groups[apiType]) {
|
||||
groups[apiType] = [];
|
||||
@@ -66,7 +119,7 @@ const groupedByApiType = computed(() => {
|
||||
// 按 厂商 (Provider) 分组
|
||||
const groupedByProvider = computed(() => {
|
||||
const groups: Record<string, GetSessionListVO[]> = {};
|
||||
localModelList.forEach((model) => {
|
||||
modelStore.modelList.forEach((model) => {
|
||||
const provider = model.providerName || 'Other';
|
||||
if (!groups[provider]) {
|
||||
groups[provider] = [];
|
||||
@@ -83,6 +136,8 @@ function openDialog() {
|
||||
nextTick(() => {
|
||||
scrollToCurrentModel();
|
||||
});
|
||||
// 每次打开弹窗都重新请求模型列表
|
||||
modelStore.requestModelList();
|
||||
}
|
||||
|
||||
// 监听 tab 切换,自动定位
|
||||
@@ -249,7 +304,7 @@ function getWrapperClass(item: GetSessionListVO) {
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="切换模型"
|
||||
:width="isMobile ? '95%' : '600px'"
|
||||
:width="isMobile ? '95%' : '900px'"
|
||||
class="model-select-dialog"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@@ -269,108 +324,142 @@ function getWrapperClass(item: GetSessionListVO) {
|
||||
<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.id}`"
|
||||
: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 flex-wrap">
|
||||
<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">尊享</span>
|
||||
<span v-else-if="!item.isFree" class="text-[10px] px-1.5 py-0.5 bg-yellow-100 text-yellow-600 rounded-full">VIP</span>
|
||||
<!-- 显示 API 类型 -->
|
||||
<span class="text-[10px] px-1.5 py-0.5 bg-gray-100 text-gray-600 rounded-full">{{ item.modelApiType }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 break-words whitespace-normal line-clamp-2" :title="item.modelDescribe">
|
||||
{{ item.modelDescribe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选中/锁定图标 -->
|
||||
<div class="flex items-center">
|
||||
<el-icon v-if="isCurrentModel(item)" 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 class="flex h-[600px]">
|
||||
<!-- 侧边导航 -->
|
||||
<div class="w-28 flex-shrink-0 border-r border-gray-100 overflow-y-auto mr-2">
|
||||
<div
|
||||
v-for="(_, provider) in groupedByProvider"
|
||||
:key="provider"
|
||||
class="cursor-pointer px-3 py-2.5 text-xs hover:bg-gray-50 truncate transition-colors duration-200 border-l-2 border-transparent"
|
||||
:class="{ 'text-primary font-bold bg-blue-50 border-primary': activeProviderGroup === provider, 'text-gray-600': activeProviderGroup !== provider }"
|
||||
@click="scrollToGroup('provider', provider as string)"
|
||||
>
|
||||
{{ provider }}
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<!-- 内容列表 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<el-scrollbar ref="scrollbarRef" height="100%" @scroll="handleScroll">
|
||||
<div class="px-2 pb-4">
|
||||
<template v-for="(models, provider) in groupedByProvider" :key="provider">
|
||||
<div :id="`group-provider-${provider}`" class="group-title text-gray-500 text-xs font-bold mb-2 mt-4 px-1 pt-2">
|
||||
{{ provider }}
|
||||
</div>
|
||||
<div
|
||||
v-for="item in models"
|
||||
:id="`provider-model-${item.id}`"
|
||||
: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 flex-wrap">
|
||||
<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">尊享</span>
|
||||
<span v-else-if="!item.isFree" class="text-[10px] px-1.5 py-0.5 bg-yellow-100 text-yellow-600 rounded-full">VIP</span>
|
||||
<!-- 显示 API 类型 -->
|
||||
<span class="text-[10px] px-1.5 py-0.5 bg-gray-100 text-gray-600 rounded-full">{{ item.modelApiType }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 break-words whitespace-normal line-clamp-2" :title="item.modelDescribe">
|
||||
{{ item.modelDescribe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选中/锁定图标 -->
|
||||
<div class="flex items-center">
|
||||
<el-icon v-if="isCurrentModel(item)" 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>
|
||||
</div>
|
||||
</div>
|
||||
</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.id}`"
|
||||
: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 flex-wrap">
|
||||
<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">尊享</span>
|
||||
<span v-else-if="!item.isFree" class="text-[10px] px-1.5 py-0.5 bg-yellow-100 text-yellow-600 rounded-full">VIP</span>
|
||||
<!-- 显示 厂商名称 -->
|
||||
<span class="text-[10px] px-1.5 py-0.5 bg-blue-50 text-blue-600 rounded-full">{{ item.providerName }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 break-words whitespace-normal line-clamp-2" :title="item.modelDescribe">
|
||||
{{ item.modelDescribe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 选中/锁定图标 -->
|
||||
<div class="flex items-center">
|
||||
<el-icon v-if="isCurrentModel(item)" 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 class="flex h-[600px]">
|
||||
<!-- 侧边导航 -->
|
||||
<div class="w-28 flex-shrink-0 border-r border-gray-100 overflow-y-auto mr-2">
|
||||
<div
|
||||
v-for="(_, apiType) in groupedByApiType"
|
||||
:key="apiType"
|
||||
class="cursor-pointer px-3 py-2.5 text-xs hover:bg-gray-50 transition-colors duration-200 border-l-2 border-transparent leading-tight"
|
||||
:class="{ 'text-primary font-bold bg-blue-50 border-primary': activeApiGroup === apiType, 'text-gray-600': activeApiGroup !== apiType }"
|
||||
@click="scrollToGroup('api', apiType as string)"
|
||||
>
|
||||
<div v-html="(apiTypeNameMap[apiType] || apiType).replace(/ /g, '<br/>')" />
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<!-- 内容列表 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<el-scrollbar height="100%" @scroll="handleScroll">
|
||||
<div class="px-2 pb-4">
|
||||
<template v-for="(models, apiType) in groupedByApiType" :key="apiType">
|
||||
<div :id="`group-api-${apiType}`" class="group-title text-gray-500 text-xs font-bold mb-2 mt-4 px-1 pt-2">
|
||||
{{ apiTypeNameMap[apiType] || apiType }}
|
||||
</div>
|
||||
<div
|
||||
v-for="item in models"
|
||||
:id="`api-model-${item.id}`"
|
||||
: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 flex-wrap">
|
||||
<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">尊享</span>
|
||||
<span v-else-if="!item.isFree" class="text-[10px] px-1.5 py-0.5 bg-yellow-100 text-yellow-600 rounded-full">VIP</span>
|
||||
<!-- 显示 厂商名称 -->
|
||||
<span class="text-[10px] px-1.5 py-0.5 bg-blue-50 text-blue-600 rounded-full">{{ item.providerName }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 break-words whitespace-normal line-clamp-2" :title="item.modelDescribe">
|
||||
{{ item.modelDescribe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 选中/锁定图标 -->
|
||||
<div class="flex items-center">
|
||||
<el-icon v-if="isCurrentModel(item)" 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>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
@@ -427,4 +516,4 @@ function getWrapperClass(item: GetSessionListVO) {
|
||||
.border-primary {
|
||||
border-color: var(--el-color-primary, #409eff);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
Reference in New Issue
Block a user