2025-10-16 01:20:11 +08:00
|
|
|
|
<!-- 切换模型 -->
|
2025-06-17 22:37:37 +08:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import type { GetSessionListVO } from '@/api/model/types';
|
2026-01-11 19:07:47 +08:00
|
|
|
|
import { Check, Lock, Right } from '@element-plus/icons-vue';
|
2025-06-17 22:37:37 +08:00
|
|
|
|
import SvgIcon from '@/components/SvgIcon/index.vue';
|
2026-01-11 19:07:47 +08:00
|
|
|
|
import { useResponsive } from '@/hooks/useResponsive';
|
2025-06-17 22:37:37 +08:00
|
|
|
|
import { useModelStore } from '@/stores/modules/model';
|
2025-09-03 10:56:44 +08:00
|
|
|
|
import { showProductPackage } from '@/utils/product-package.ts';
|
2025-08-04 23:11:42 +08:00
|
|
|
|
import { isUserVip } from '@/utils/user';
|
2026-01-11 19:07:47 +08:00
|
|
|
|
import { modelList as localModelList } from './modelData';
|
2026-02-01 00:30:44 +08:00
|
|
|
|
import { useRouter } from 'vue-router';
|
2025-06-17 22:37:37 +08:00
|
|
|
|
|
|
|
|
|
|
const modelStore = useModelStore();
|
2026-02-01 00:30:44 +08:00
|
|
|
|
const router = useRouter();
|
2026-01-11 19:07:47 +08:00
|
|
|
|
const { isMobile } = useResponsive();
|
|
|
|
|
|
const dialogVisible = ref(false);
|
|
|
|
|
|
const activeTab = ref('provider'); // 'provider' | 'api'
|
|
|
|
|
|
const scrollbarRef = ref();
|
2026-01-11 20:39:53 +08:00
|
|
|
|
const activeProviderGroup = ref('');
|
|
|
|
|
|
const activeApiGroup = ref('');
|
|
|
|
|
|
const isScrolling = ref(false);
|
2026-01-11 21:00:02 +08:00
|
|
|
|
const filterPremiumOnly = ref(false); // 是否只显示尊享模型
|
2026-01-11 19:07:47 +08:00
|
|
|
|
|
2025-06-30 17:53:59 +08:00
|
|
|
|
// 检查模型是否可用
|
|
|
|
|
|
function isModelAvailable(item: GetSessionListVO) {
|
2026-01-08 23:55:39 +08:00
|
|
|
|
return isUserVip() || item.isFree;
|
2025-06-30 17:53:59 +08:00
|
|
|
|
}
|
2025-06-17 22:37:37 +08:00
|
|
|
|
|
2026-01-11 20:39:53 +08:00
|
|
|
|
// 滚动到指定分组
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-17 22:37:37 +08:00
|
|
|
|
onMounted(async () => {
|
2026-01-11 19:07:47 +08:00
|
|
|
|
// 虽然使用了本地数据用于展示,但可能仍需请求后端以保持某些状态同步,或者直接使用本地数据初始化store
|
|
|
|
|
|
// 这里我们优先使用本地数据来填充store,或者仅在UI上使用本地数据
|
|
|
|
|
|
// 为了兼容现有逻辑,我们尽量保持 modelStore 的使用,但列表展示主要依赖 localModelList
|
|
|
|
|
|
// 如果后端返回列表为空,可以用本地列表兜底
|
|
|
|
|
|
if (modelStore.modelList.length === 0) {
|
|
|
|
|
|
modelStore.modelList = localModelList;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-16 01:20:11 +08:00
|
|
|
|
// 设置默认模型
|
|
|
|
|
|
if (
|
2026-01-11 19:07:47 +08:00
|
|
|
|
(!modelStore.currentModelInfo || !modelStore.currentModelInfo.modelId)
|
|
|
|
|
|
&& localModelList.length > 0
|
2025-10-16 01:20:11 +08:00
|
|
|
|
) {
|
2026-01-11 19:07:47 +08:00
|
|
|
|
modelStore.setCurrentModelInfo(localModelList[0]);
|
2025-06-17 22:37:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-16 01:20:11 +08:00
|
|
|
|
const currentModelName = computed(
|
|
|
|
|
|
() => modelStore.currentModelInfo && modelStore.currentModelInfo.modelName,
|
|
|
|
|
|
);
|
2025-06-17 22:37:37 +08:00
|
|
|
|
|
2026-01-11 21:00:02 +08:00
|
|
|
|
// 过滤后的模型列表
|
|
|
|
|
|
const filteredModelList = computed(() => {
|
|
|
|
|
|
if (filterPremiumOnly.value) {
|
|
|
|
|
|
return modelStore.modelList.filter(model => model.isPremiumPackage);
|
|
|
|
|
|
}
|
|
|
|
|
|
return modelStore.modelList;
|
|
|
|
|
|
});
|
2026-01-11 00:15:31 +08:00
|
|
|
|
|
2026-01-11 19:07:47 +08:00
|
|
|
|
// 按 API 类型分组
|
|
|
|
|
|
const groupedByApiType = computed(() => {
|
2026-01-11 00:15:31 +08:00
|
|
|
|
const groups: Record<string, GetSessionListVO[]> = {};
|
2026-01-11 21:00:02 +08:00
|
|
|
|
filteredModelList.value.forEach((model) => {
|
2026-01-11 00:15:31 +08:00
|
|
|
|
const apiType = model.modelApiType || 'Completions';
|
|
|
|
|
|
if (!groups[apiType]) {
|
|
|
|
|
|
groups[apiType] = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
groups[apiType].push(model);
|
|
|
|
|
|
});
|
2026-01-11 19:07:47 +08:00
|
|
|
|
return groups;
|
|
|
|
|
|
});
|
2026-01-11 00:15:31 +08:00
|
|
|
|
|
2026-01-11 19:07:47 +08:00
|
|
|
|
// 按 厂商 (Provider) 分组
|
|
|
|
|
|
const groupedByProvider = computed(() => {
|
|
|
|
|
|
const groups: Record<string, GetSessionListVO[]> = {};
|
2026-01-11 21:00:02 +08:00
|
|
|
|
filteredModelList.value.forEach((model) => {
|
2026-01-11 19:07:47 +08:00
|
|
|
|
const provider = model.providerName || 'Other';
|
|
|
|
|
|
if (!groups[provider]) {
|
|
|
|
|
|
groups[provider] = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
groups[provider].push(model);
|
|
|
|
|
|
});
|
2026-01-11 00:15:31 +08:00
|
|
|
|
return groups;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-11 19:07:47 +08:00
|
|
|
|
// 打开弹窗
|
|
|
|
|
|
function openDialog() {
|
|
|
|
|
|
dialogVisible.value = true;
|
|
|
|
|
|
// 打开时定位到当前模型
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
scrollToCurrentModel();
|
|
|
|
|
|
});
|
2026-01-11 20:39:53 +08:00
|
|
|
|
// 每次打开弹窗都重新请求模型列表
|
|
|
|
|
|
modelStore.requestModelList();
|
2026-01-11 19:07:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 监听 tab 切换,自动定位
|
|
|
|
|
|
watch(activeTab, () => {
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
scrollToCurrentModel();
|
|
|
|
|
|
});
|
2025-06-17 22:37:37 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-11 19:42:33 +08:00
|
|
|
|
// 检查是否为当前选中的模型
|
|
|
|
|
|
function isCurrentModel(item: GetSessionListVO) {
|
|
|
|
|
|
const current = modelStore.currentModelInfo;
|
|
|
|
|
|
if (!current)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
// 优先使用唯一 ID 匹配
|
|
|
|
|
|
if (item.id && current.id) {
|
|
|
|
|
|
return item.id === current.id;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 降级使用组合键匹配
|
|
|
|
|
|
return item.modelId === current.modelId
|
|
|
|
|
|
&& item.modelApiType === current.modelApiType
|
|
|
|
|
|
&& item.providerName === current.providerName;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-11 19:07:47 +08:00
|
|
|
|
// 定位到当前模型
|
|
|
|
|
|
function scrollToCurrentModel() {
|
2026-01-11 19:42:33 +08:00
|
|
|
|
const current = modelStore.currentModelInfo;
|
|
|
|
|
|
if (!current)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
// 使用 item.id 构建 ID (假设 modelData.ts 中的 id 是唯一的)
|
|
|
|
|
|
// 如果没有 id,则无法精确定位,这里假设都有 id
|
|
|
|
|
|
const currentId = current.id;
|
2026-01-11 19:07:47 +08:00
|
|
|
|
if (!currentId)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
// 根据当前 tab 构建对应的 ID
|
|
|
|
|
|
const elementId = activeTab.value === 'provider'
|
|
|
|
|
|
? `provider-model-${currentId}`
|
|
|
|
|
|
: `api-model-${currentId}`;
|
2026-01-11 00:15:31 +08:00
|
|
|
|
|
2026-01-11 19:07:47 +08:00
|
|
|
|
const element = document.getElementById(elementId);
|
|
|
|
|
|
if (element) {
|
|
|
|
|
|
element.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
|
|
|
|
}
|
2025-06-17 22:37:37 +08:00
|
|
|
|
}
|
2025-06-30 17:53:59 +08:00
|
|
|
|
|
2025-10-16 01:20:11 +08:00
|
|
|
|
// 处理模型点击
|
2025-06-30 17:53:59 +08:00
|
|
|
|
function handleModelClick(item: GetSessionListVO) {
|
|
|
|
|
|
if (!isModelAvailable(item)) {
|
|
|
|
|
|
ElMessageBox.confirm(
|
|
|
|
|
|
`
|
2025-10-16 01:20:11 +08:00
|
|
|
|
<div class="text-center leading-relaxed">
|
|
|
|
|
|
<h3 class="text-lg font-bold mb-3">${isUserVip() ? 'YiXinAI-VIP 会员' : '成为 YiXinAI-VIP'}</h3>
|
|
|
|
|
|
<p class="mb-2">
|
|
|
|
|
|
${
|
|
|
|
|
|
isUserVip()
|
2025-06-30 21:08:32 +08:00
|
|
|
|
? '您已是尊贵会员,享受全部 AI 模型与专属服务。感谢支持!'
|
2025-10-16 01:20:11 +08:00
|
|
|
|
: '解锁所有 AI 模型,无限加速,专属客服,尽享尊贵体验。'
|
|
|
|
|
|
}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
${
|
|
|
|
|
|
isUserVip()
|
|
|
|
|
|
? '<p class="text-sm text-gray-500">您可随时访问产品页面查看更多特权内容。</p>'
|
|
|
|
|
|
: '<p class="text-sm text-gray-500">请点击右上角登录按钮,登录后进行购买!</p>'
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`,
|
2025-08-04 23:11:42 +08:00
|
|
|
|
isUserVip() ? '会员状态' : '会员尊享',
|
2025-06-30 17:53:59 +08:00
|
|
|
|
{
|
2025-09-03 10:56:44 +08:00
|
|
|
|
confirmButtonText: '产品查看',
|
2025-06-30 17:53:59 +08:00
|
|
|
|
cancelButtonText: '关闭',
|
|
|
|
|
|
dangerouslyUseHTMLString: true,
|
|
|
|
|
|
type: 'info',
|
|
|
|
|
|
center: true,
|
|
|
|
|
|
roundButton: true,
|
|
|
|
|
|
},
|
2025-10-16 01:20:11 +08:00
|
|
|
|
)
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
showProductPackage();
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {
|
|
|
|
|
|
// 点击右上角关闭或“关闭”按钮,不执行任何操作
|
|
|
|
|
|
});
|
2026-01-11 19:07:47 +08:00
|
|
|
|
return;
|
2025-06-30 17:53:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-17 22:37:37 +08:00
|
|
|
|
modelStore.setCurrentModelInfo(item);
|
2026-01-11 19:07:47 +08:00
|
|
|
|
dialogVisible.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function goToModelLibrary() {
|
2026-02-01 00:30:44 +08:00
|
|
|
|
router.push('/model-library');
|
2025-06-17 22:37:37 +08:00
|
|
|
|
}
|
2025-10-12 18:30:34 +08:00
|
|
|
|
|
|
|
|
|
|
/* -------------------------------
|
|
|
|
|
|
模型样式规则
|
|
|
|
|
|
-------------------------------- */
|
2025-11-25 22:14:48 +08:00
|
|
|
|
function getModelStyleClass(mode: any) {
|
2026-01-11 19:07:47 +08:00
|
|
|
|
if (!mode)
|
2025-11-12 23:08:52 +08:00
|
|
|
|
return;
|
2026-01-11 19:07:47 +08:00
|
|
|
|
|
2025-11-25 22:14:48 +08:00
|
|
|
|
const isPremiumPackage = mode.isPremiumPackage;
|
2025-10-12 18:30:34 +08:00
|
|
|
|
|
2026-01-11 19:42:33 +08:00
|
|
|
|
// 规则3:彩色流光 (尊享)
|
2025-11-25 22:14:48 +08:00
|
|
|
|
if (isPremiumPackage) {
|
2025-10-14 21:29:20 +08:00
|
|
|
|
return `
|
|
|
|
|
|
text-transparent bg-clip-text
|
|
|
|
|
|
bg-[linear-gradient(45deg,#ff0000,#ff8000,#ffff00,#00ff00,#00ffff,#0000ff,#8000ff,#ff0080)]
|
|
|
|
|
|
bg-[length:400%_400%] animate-gradientFlow
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
2025-10-12 18:30:34 +08:00
|
|
|
|
|
2026-01-08 23:55:39 +08:00
|
|
|
|
// 规则2:普通灰(免费模型)
|
|
|
|
|
|
if (mode.isFree) {
|
2025-10-12 18:30:34 +08:00
|
|
|
|
return 'text-gray-700';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-11 19:42:33 +08:00
|
|
|
|
// 规则1:金色光泽 (VIP)
|
2025-10-12 18:30:34 +08:00
|
|
|
|
return `
|
|
|
|
|
|
text-[#B38728] font-semibold relative overflow-hidden
|
|
|
|
|
|
before:content-[''] before:absolute before:-inset-2 before:-z-10
|
|
|
|
|
|
before:animate-goldShine
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* -------------------------------
|
|
|
|
|
|
外层卡片样式(选中态 + hover 动效)
|
|
|
|
|
|
-------------------------------- */
|
|
|
|
|
|
function getWrapperClass(item: GetSessionListVO) {
|
2026-01-11 19:42:33 +08:00
|
|
|
|
const isSelected = isCurrentModel(item);
|
2025-10-12 18:30:34 +08:00
|
|
|
|
const available = isModelAvailable(item);
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
2026-01-11 19:07:47 +08:00
|
|
|
|
'p-3 rounded-lg text-sm transition-all duration-300 relative select-none flex items-center justify-between cursor-pointer mb-2',
|
2025-10-12 18:30:34 +08:00
|
|
|
|
available
|
2026-01-11 19:07:47 +08:00
|
|
|
|
? 'hover:bg-gray-50 hover:shadow-sm'
|
|
|
|
|
|
: 'opacity-60 cursor-not-allowed bg-gray-50',
|
2025-10-12 18:30:34 +08:00
|
|
|
|
isSelected
|
2026-01-11 19:07:47 +08:00
|
|
|
|
? 'border-2 border-primary bg-primary-light-9 shadow-md'
|
|
|
|
|
|
: 'border border-gray-200',
|
2025-10-12 18:30:34 +08:00
|
|
|
|
];
|
|
|
|
|
|
}
|
2025-06-17 22:37:37 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2025-11-17 01:05:57 +08:00
|
|
|
|
<div class="model-select" data-tour="model-select">
|
2026-01-11 19:07:47 +08:00
|
|
|
|
<!-- 触发按钮 -->
|
|
|
|
|
|
<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"
|
2025-06-17 22:37:37 +08:00
|
|
|
|
>
|
2026-01-11 19:07:47 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 模型选择弹窗 -->
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
|
v-model="dialogVisible"
|
|
|
|
|
|
title="切换模型"
|
2026-01-11 20:39:53 +08:00
|
|
|
|
:width="isMobile ? '95%' : '900px'"
|
2026-01-11 19:07:47 +08:00
|
|
|
|
class="model-select-dialog"
|
|
|
|
|
|
append-to-body
|
|
|
|
|
|
destroy-on-close
|
|
|
|
|
|
align-center
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="model-list-container relative">
|
2026-01-11 21:00:02 +08:00
|
|
|
|
<!-- 右上角操作区域 -->
|
|
|
|
|
|
<div class="absolute right-0 top-1 z-10 flex items-center gap-3">
|
|
|
|
|
|
<!-- 尊享过滤标签 -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="premium-filter-tag"
|
|
|
|
|
|
:class="{ 'premium-filter-tag--active': filterPremiumOnly }"
|
|
|
|
|
|
@click="filterPremiumOnly = !filterPremiumOnly"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="premium-filter-tag__text">仅看尊享模型</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-11 19:07:47 +08:00
|
|
|
|
<el-button type="primary" link size="small" @click="goToModelLibrary">
|
|
|
|
|
|
前往模型库
|
|
|
|
|
|
<el-icon class="ml-1">
|
|
|
|
|
|
<Right />
|
|
|
|
|
|
</el-icon>
|
|
|
|
|
|
</el-button>
|
2025-06-17 22:37:37 +08:00
|
|
|
|
</div>
|
2026-01-11 19:07:47 +08:00
|
|
|
|
|
|
|
|
|
|
<el-tabs v-model="activeTab" class="model-tabs">
|
|
|
|
|
|
<!-- 厂商分类 Tab -->
|
|
|
|
|
|
<el-tab-pane label="厂商类型" name="provider">
|
2026-01-11 20:39:53 +08:00
|
|
|
|
<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"
|
2026-01-11 21:00:02 +08:00
|
|
|
|
class="cursor-pointer px-2 py-2.5 text-xs hover:bg-gray-50 truncate transition-colors duration-200 border-l-2 border-transparent"
|
2026-01-11 20:39:53 +08:00
|
|
|
|
: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>
|
|
|
|
|
|
<!-- 内容列表 -->
|
|
|
|
|
|
<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 }}
|
2026-01-11 19:07:47 +08:00
|
|
|
|
</div>
|
2026-01-11 20:39:53 +08:00
|
|
|
|
<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>
|
2026-01-11 19:07:47 +08:00
|
|
|
|
</div>
|
2026-01-11 20:39:53 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 选中/锁定图标 -->
|
|
|
|
|
|
<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>
|
2026-01-11 19:07:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-11 20:39:53 +08:00
|
|
|
|
</template>
|
2026-01-11 19:07:47 +08:00
|
|
|
|
</div>
|
2026-01-11 20:39:53 +08:00
|
|
|
|
</el-scrollbar>
|
2026-01-11 00:15:31 +08:00
|
|
|
|
</div>
|
2026-01-11 20:39:53 +08:00
|
|
|
|
</div>
|
2026-01-11 19:07:47 +08:00
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- API类型分类 Tab -->
|
|
|
|
|
|
<el-tab-pane label="API类型" name="api">
|
2026-01-11 20:39:53 +08:00
|
|
|
|
<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"
|
2026-01-11 21:00:02 +08:00
|
|
|
|
class="cursor-pointer px-1 py-2.5 text-xs hover:bg-gray-50 transition-colors duration-200 border-l-2 border-transparent leading-tight"
|
2026-01-11 20:39:53 +08:00
|
|
|
|
:class="{ 'text-primary font-bold bg-blue-50 border-primary': activeApiGroup === apiType, 'text-gray-600': activeApiGroup !== apiType }"
|
|
|
|
|
|
@click="scrollToGroup('api', apiType as string)"
|
|
|
|
|
|
>
|
2026-01-11 21:00:02 +08:00
|
|
|
|
<div v-html="(apiType).replace(/ /g, '<br/>')" />
|
2026-01-11 20:39:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 内容列表 -->
|
|
|
|
|
|
<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">
|
2026-01-11 21:00:02 +08:00
|
|
|
|
{{ apiType }}
|
2026-01-11 19:07:47 +08:00
|
|
|
|
</div>
|
2026-01-11 20:39:53 +08:00
|
|
|
|
<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>
|
2026-01-11 19:07:47 +08:00
|
|
|
|
</div>
|
2026-01-11 20:39:53 +08:00
|
|
|
|
<!-- 选中/锁定图标 -->
|
|
|
|
|
|
<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>
|
2026-01-11 19:07:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-11 20:39:53 +08:00
|
|
|
|
</template>
|
2026-01-11 19:07:47 +08:00
|
|
|
|
</div>
|
2026-01-11 20:39:53 +08:00
|
|
|
|
</el-scrollbar>
|
2026-01-11 19:07:47 +08:00
|
|
|
|
</div>
|
2026-01-11 20:39:53 +08:00
|
|
|
|
</div>
|
2026-01-11 19:07:47 +08:00
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
</el-tabs>
|
2025-06-17 22:37:37 +08:00
|
|
|
|
</div>
|
2026-01-11 19:07:47 +08:00
|
|
|
|
</el-dialog>
|
2025-06-17 22:37:37 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2025-10-16 01:20:11 +08:00
|
|
|
|
<style scoped lang="scss">
|
2025-10-15 00:04:17 +08:00
|
|
|
|
.model-select-box {
|
|
|
|
|
|
color: var(--el-color-primary, #409eff);
|
|
|
|
|
|
background: var(--el-color-primary-light-9, rgb(235.9 245.3 255));
|
|
|
|
|
|
border: 1px solid var(--el-color-primary, #409eff);
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-11 19:07:47 +08:00
|
|
|
|
/* 移动端适配 */
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
:deep(.model-select-dialog) {
|
|
|
|
|
|
max-width: 100% !important;
|
|
|
|
|
|
margin-top: 10vh !important;
|
2025-10-16 01:20:11 +08:00
|
|
|
|
|
2026-01-11 19:07:47 +08:00
|
|
|
|
.el-dialog__body {
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
}
|
2025-10-16 01:20:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-12 18:30:34 +08:00
|
|
|
|
/* 彩色流光动画 */
|
|
|
|
|
|
@keyframes gradientFlow {
|
|
|
|
|
|
0%, 100% { background-position: 0 50%; }
|
|
|
|
|
|
50% { background-position: 100% 50%; }
|
2025-06-17 22:37:37 +08:00
|
|
|
|
}
|
2025-10-12 18:30:34 +08:00
|
|
|
|
|
|
|
|
|
|
/* 金色光泽动画 */
|
|
|
|
|
|
@keyframes goldShine {
|
|
|
|
|
|
0% { transform: translateX(-100%) translateY(-100%); }
|
|
|
|
|
|
100% { transform: translateX(100%) translateY(100%); }
|
2025-06-17 22:37:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-12 18:30:34 +08:00
|
|
|
|
.animate-gradientFlow {
|
|
|
|
|
|
animation: gradientFlow 3s ease infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
.animate-goldShine {
|
|
|
|
|
|
animation: goldShine 4s linear infinite;
|
|
|
|
|
|
}
|
2026-01-11 19:07:47 +08:00
|
|
|
|
|
|
|
|
|
|
/* 定义一些颜色变量辅助类,如果项目没有定义的话 */
|
|
|
|
|
|
.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);
|
2025-06-17 22:37:37 +08:00
|
|
|
|
}
|
2026-01-11 21:00:02 +08:00
|
|
|
|
|
|
|
|
|
|
/* 尊享过滤标签 */
|
|
|
|
|
|
.premium-filter-tag {
|
|
|
|
|
|
padding: 4px 12px;
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
border: 1.5px solid transparent;
|
|
|
|
|
|
background: linear-gradient(white, white) padding-box,
|
|
|
|
|
|
linear-gradient(90deg, #ff6b6b, #feca57, #48dbfb, #ff9ff3, #54a0ff, #5f27cd) border-box;
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
|
|
|
|
|
|
&__text {
|
|
|
|
|
|
background: linear-gradient(90deg, #ff6b6b, #feca57, #48dbfb, #ff9ff3, #54a0ff, #5f27cd);
|
|
|
|
|
|
background-size: 200% 200%;
|
|
|
|
|
|
-webkit-background-clip: text;
|
|
|
|
|
|
background-clip: text;
|
|
|
|
|
|
color: transparent;
|
|
|
|
|
|
animation: gradientFlow 3s ease infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
transform: scale(1.05);
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(255, 107, 107, 0.3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&--active {
|
|
|
|
|
|
background: linear-gradient(90deg, #ff6b6b, #feca57, #48dbfb, #ff9ff3, #54a0ff, #5f27cd);
|
|
|
|
|
|
background-size: 200% 200%;
|
|
|
|
|
|
animation: gradientFlow 3s ease infinite;
|
|
|
|
|
|
|
|
|
|
|
|
.premium-filter-tag__text {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
-webkit-background-clip: text;
|
|
|
|
|
|
background-clip: text;
|
|
|
|
|
|
color: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|