style: 修改模型选择列表

This commit is contained in:
ccnetcore
2026-01-11 20:39:53 +08:00
parent a1ddd1c3e2
commit 7ed7201d10
3 changed files with 191 additions and 102 deletions

View File

@@ -113,7 +113,7 @@ namespace Yi.Abp.Web
//本地开发环境,可以禁用作业执行
if (host.IsDevelopment())
{
//Configure<AbpBackgroundWorkerOptions>(options => { options.IsEnabled = false; });
Configure<AbpBackgroundWorkerOptions>(options => { options.IsEnabled = false; });
}
//请求日志

View File

@@ -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>

View File

@@ -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>