Files
Yi.Admin/Yi.Ai.Vue3/src/pages/modelLibrary/index.vue

2100 lines
58 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import type { ModelApiTypeOption, ModelLibraryDto, ModelTypeOption } from '@/api/model/types';
import { Box, Close, CopyDocument, OfficeBuilding, Search } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { getApiTypeOptions, getModelLibraryList, getModelTypeOptions, getProviderList } from '@/api/model';
import { useScreenStore } from '@/hooks/useScreen';
const router = useRouter();
const { isMobile } = useScreenStore();
const showMobileFilter = ref(false);
const loading = ref(false);
const modelList = ref<ModelLibraryDto[]>([]);
const totalCount = ref(0);
const currentPage = ref(1);
const pageSize = ref(12);
// 筛选条件
const searchKey = ref('');
const selectedProviders = ref<string[]>([]);
const selectedModelTypes = ref<number[]>([]);
const selectedApiTypes = ref<number[]>([]);
const isPremiumOnly = ref(false);
// 供应商列表
const providerList = ref<string[]>([]);
// 模型类型选项
const modelTypeOptions = ref<ModelTypeOption[]>([]);
// API类型选项
const apiTypeOptions = ref<ModelApiTypeOption[]>([]);
function goToMonitor() {
window.open('http://data.ccnetcore.com:91/?period=24h', '_blank');
}
// 格式化倍率显示
function formatMultiplier(multiplier: number): string {
if (multiplier >= 10000) {
// 倍率大于等于10000显示为次数
const tokenCount = multiplier / 10000;
return `${tokenCount}w/次`;
}
// 倍率小于10000显示为倍率
return `${multiplier}×`;
}
async function fetchModelList() {
loading.value = true;
try {
const params = {
searchKey: searchKey.value || undefined,
providerNames: selectedProviders.value.length > 0 ? selectedProviders.value : undefined,
modelTypes: selectedModelTypes.value.length > 0 ? selectedModelTypes.value : undefined,
modelApiTypes: selectedApiTypes.value.length > 0 ? selectedApiTypes.value : undefined,
isPremiumOnly: isPremiumOnly.value || undefined,
skipCount: currentPage.value,
maxResultCount: pageSize.value,
};
const response = await getModelLibraryList(params);
const data = response.data;
modelList.value = data.items;
totalCount.value = data.totalCount;
}
catch (error) {
console.error('获取模型列表失败:', error);
ElMessage.error('获取模型列表失败');
}
finally {
loading.value = false;
}
}
// 获取所有供应商列表(用于筛选栏)
async function fetchProviderList() {
try {
const response = await getProviderList();
providerList.value = response.data.sort();
}
catch (error) {
console.error('获取供应商列表失败:', error);
}
}
// 获取模型类型选项
async function fetchModelTypeOptions() {
try {
const response = await getModelTypeOptions();
modelTypeOptions.value = response.data;
}
catch (error) {
console.error('获取模型类型选项失败:', error);
}
}
// 获取API类型选项
async function fetchApiTypeOptions() {
try {
const response = await getApiTypeOptions();
apiTypeOptions.value = response.data;
}
catch (error) {
console.error('获取API类型选项失败:', error);
}
}
function copyModelId(modelId: string) {
navigator.clipboard.writeText(modelId).then(() => {
ElMessage.success('模型ID已复制');
});
}
function resetFilters() {
searchKey.value = '';
selectedProviders.value = [];
selectedModelTypes.value = [];
selectedApiTypes.value = [];
isPremiumOnly.value = false;
currentPage.value = 1;
}
function toggleProvider(provider: string | null) {
if (provider === null) {
// 点击"全部",清空所有选择
selectedProviders.value = [];
}
else {
const index = selectedProviders.value.indexOf(provider);
if (index > -1) {
selectedProviders.value.splice(index, 1);
}
else {
selectedProviders.value.push(provider);
}
}
currentPage.value = 1;
}
function toggleModelType(type: number | null) {
if (type === null) {
// 点击"全部",清空所有选择
selectedModelTypes.value = [];
}
else {
const index = selectedModelTypes.value.indexOf(type);
if (index > -1) {
selectedModelTypes.value.splice(index, 1);
}
else {
selectedModelTypes.value.push(type);
}
}
currentPage.value = 1;
}
function toggleApiType(type: number | null) {
if (type === null) {
// 点击"全部",清空所有选择
selectedApiTypes.value = [];
}
else {
const index = selectedApiTypes.value.indexOf(type);
if (index > -1) {
selectedApiTypes.value.splice(index, 1);
}
else {
selectedApiTypes.value.push(type);
}
}
currentPage.value = 1;
}
function togglePremiumOnly(isPremium: boolean | null) {
if (isPremium === null) {
// 点击"全部"设置为false
isPremiumOnly.value = false;
}
else {
isPremiumOnly.value = isPremium;
}
currentPage.value = 1;
}
function handlePageChange(page: number) {
currentPage.value = page;
fetchModelList();
}
function handleSizeChange(size: number) {
pageSize.value = size;
currentPage.value = 1;
fetchModelList();
}
function goToHome() {
router.push('/');
}
// 监听筛选条件变化,自动刷新数据
// 搜索关键词使用防抖,其他条件立即触发
let searchTimer: ReturnType<typeof setTimeout> | null = null;
watch(searchKey, () => {
if (searchTimer) {
clearTimeout(searchTimer);
}
searchTimer = setTimeout(() => {
currentPage.value = 1;
fetchModelList();
}, 500);
});
watch([selectedProviders, selectedModelTypes, selectedApiTypes, isPremiumOnly], () => {
currentPage.value = 1;
fetchModelList();
}, { deep: true });
function openMobileFilter() {
showMobileFilter.value = true;
}
function closeMobileFilter() {
showMobileFilter.value = false;
}
onMounted(() => {
fetchProviderList();
fetchModelTypeOptions();
fetchApiTypeOptions();
fetchModelList();
});
</script>
<template>
<div class="model-library-container">
<!-- 顶部横幅 -->
<div class="banner-section">
<div class="banner-content">
<div class="banner-header">
<div class="banner-left">
<div class="banner-text-section">
<h1 class="banner-title">
意心AI模型库
</h1>
<p class="banner-subtitle">
探索并接入全球顶尖AI模型覆盖文本图像嵌入等多个领域
</p>
<p class="banner-subtitle">
尊享Token = 实际消耗Token * 当前模型倍率
</p>
</div>
<!-- 统计信息卡片 -->
<div class="stats-cards">
<div class="stat-card">
<div class="stat-icon">
<el-icon><Box /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">
{{ totalCount }}
</div>
<div class="stat-label">
可用模型
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<el-icon><OfficeBuilding /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">
{{ providerList.length > 1 ? providerList.length : 1 - 1 }}
</div>
<div class="stat-label">
支持供应商
</div>
</div>
</div>
</div>
</div>
<div class="monitor-card" @click="goToMonitor">
<!-- 动态网格背景 -->
<div class="grid-background">
<div v-for="i in 6" :key="`line-${i}`" class="grid-line" />
</div>
<!-- 扫描线效果 -->
<div class="scan-line" />
<!-- 粒子效果 -->
<div class="particles">
<span v-for="i in 8" :key="`particle-${i}`" class="particle" />
</div>
<!-- 点击引导手势 -->
<div class="click-hint">
<svg class="hand-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 11V6C9 5.44772 9.44772 5 10 5C10.5523 5 11 5.44772 11 6V11M9 11V16.5C9 17.8807 10.1193 19 11.5 19H12.5C13.8807 19 15 17.8807 15 16.5V11M9 11H7.5C6.67157 11 6 11.6716 6 12.5C6 13.3284 6.67157 14 7.5 14H9M15 11V8C15 7.44772 15.4477 7 16 7C16.5523 7 17 7.44772 17 8V11M15 11H17.5C18.3284 11 19 11.6716 19 12.5C19 13.3284 18.3284 14 17.5 14H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<span class="hint-text">点击查看</span>
</div>
<div class="monitor-content">
<!-- 左侧AI神经网络节点 + 脉冲圆环 -->
<div class="monitor-visual">
<div class="pulse-rings">
<div class="pulse-ring" />
<div class="pulse-ring" />
<div class="pulse-ring" />
</div>
<!-- AI神经网络节点 -->
<div class="ai-network">
<!-- 中心节点 -->
<div class="center-node">
<div class="node-core" />
</div>
<!-- 外围节点 -->
<div class="outer-nodes">
<div v-for="i in 6" :key="`node-${i}`" class="outer-node" :style="{ transform: `rotate(${(i - 1) * 60}deg) translateY(-25px)` }">
<div class="node-dot" />
<div class="connection-line" />
</div>
</div>
<!-- 数据流动粒子 -->
<div class="data-particles">
<span v-for="i in 6" :key="`data-${i}`" class="data-particle" :style="{ animationDelay: `${(i - 1) * 0.3}s` }" />
</div>
</div>
<div class="status-dot">
<span class="dot-core" />
</div>
</div>
<!-- 右侧信息 + 实时数据流 -->
<div class="monitor-info">
<div class="monitor-header">
<span class="status-badge">
<span class="badge-dot" />
LIVE
</span>
<div class="monitor-title">
实时监控
</div>
</div>
<div class="monitor-subtitle">
服务可用性矩阵
</div>
<!-- 实时数据波形 -->
<div class="waveform">
<span v-for="i in 12" :key="`bar-${i}`" class="wave-bar" />
</div>
</div>
</div>
<!-- 边框光效 -->
<div class="border-glow" />
</div>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="main-content">
<div class="content-wrapper">
<!-- 左侧筛选栏 -->
<aside class="filter-sidebar" :class="{ 'mobile-active': showMobileFilter }">
<!-- 移动端遮罩 -->
<div v-if="isMobile && showMobileFilter" class="mobile-overlay" @click="closeMobileFilter" />
<div class="filter-section">
<div class="filter-header">
<h3 class="filter-title">
<el-icon><i-ep-filter /></el-icon>
筛选条件
</h3>
<div class="filter-header-actions">
<el-button link type="primary" size="small" @click="resetFilters">
重置
</el-button>
<el-icon v-if="isMobile" class="close-btn" @click="closeMobileFilter">
<Close />
</el-icon>
</div>
</div>
<!-- 搜索框 -->
<div class="filter-group">
<el-input
v-model="searchKey"
placeholder="搜索模型..."
clearable
size="default"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<!-- 供应商 -->
<div class="filter-group">
<h4 class="filter-group-title">
供应商
</h4>
<div class="filter-tags">
<el-check-tag
:checked="selectedProviders.length === 0"
class="filter-tag"
@change="toggleProvider(null)"
>
全部供应商
</el-check-tag>
<el-check-tag
v-for="provider in providerList"
:key="provider"
:checked="selectedProviders.includes(provider)"
class="filter-tag"
@change="toggleProvider(provider)"
>
{{ provider }}
</el-check-tag>
</div>
</div>
<!-- 模型类型 -->
<div class="filter-group">
<h4 class="filter-group-title">
模型类型
</h4>
<div class="filter-tags">
<el-check-tag
:checked="selectedModelTypes.length === 0"
class="filter-tag"
@change="toggleModelType(null)"
>
全部类型
</el-check-tag>
<el-check-tag
v-for="option in modelTypeOptions"
:key="option.value"
:checked="selectedModelTypes.includes(option.value)"
class="filter-tag"
@change="toggleModelType(option.value)"
>
{{ option.label }}
</el-check-tag>
</div>
</div>
<!-- API类型 -->
<div class="filter-group">
<h4 class="filter-group-title">
API类型
</h4>
<div class="filter-tags">
<el-check-tag
:checked="selectedApiTypes.length === 0"
class="filter-tag"
@change="toggleApiType(null)"
>
全部API类型
</el-check-tag>
<el-check-tag
v-for="option in apiTypeOptions"
:key="option.value"
:checked="selectedApiTypes.includes(option.value)"
class="filter-tag"
@change="toggleApiType(option.value)"
>
{{ option.label }}
</el-check-tag>
</div>
</div>
<!-- 计费类型 -->
<div class="filter-group">
<h4 class="filter-group-title">
计费类型
</h4>
<div class="filter-tags">
<el-check-tag
:checked="!isPremiumOnly"
class="filter-tag"
@change="togglePremiumOnly(null)"
>
全部模型
</el-check-tag>
<el-check-tag
:checked="isPremiumOnly"
class="filter-tag"
@change="togglePremiumOnly(true)"
>
仅尊享模型
</el-check-tag>
</div>
</div>
</div>
</aside>
<!-- 右侧模型列表 -->
<main class="model-list-section">
<!-- 移动端筛选按钮 -->
<div v-if="isMobile" class="mobile-filter-bar">
<el-button type="primary" :icon="Search" plain class="w-full" @click="openMobileFilter">
筛选与搜索
</el-button>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-wrapper">
<el-skeleton :rows="8" animated />
</div>
<!-- 空状态 -->
<div v-else-if="modelList.length === 0" class="empty-wrapper">
<el-empty description="未找到符合条件的模型">
<el-button type="primary" @click="resetFilters">
清除筛选条件
</el-button>
</el-empty>
</div>
<!-- 模型网格 -->
<el-row v-else :gutter="20" class="model-grid">
<el-col
v-for="model in modelList"
:key="model.modelId"
:xs="24"
:sm="8"
:lg="6"
>
<div
class="model-card"
:class="{ 'premium-card': model.isPremium }"
>
<el-button
circle
size="small"
:icon="CopyDocument"
class="copy-btn-corner"
title="复制模型ID"
@click="copyModelId(model.modelId)"
/>
<div class="model-card-header">
<div class="model-icon">
<img
v-if="model.iconUrl"
:src="model.iconUrl"
:alt="model.name"
class="icon-img"
>
<div v-else class="icon-placeholder">
{{ model.name.charAt(0).toUpperCase() }}
</div>
</div>
<div class="model-info">
<h3 class="model-name">
{{ model.name }}
</h3>
<div class="model-provider">
{{ model.providerName }}
</div>
</div>
</div>
<div class="model-id">
<span class="model-id-label">ModelId:</span>
<span class="model-id-value">{{ model.modelId }}</span>
</div>
<p v-if="model.description" class="model-description">
{{ model.description }}
</p>
<p v-else class="model-description placeholder">
{{ model.modelId }}
</p>
<div class="model-footer">
<div class="model-tags">
<el-tag size="small">
{{ model.modelTypeName }}
</el-tag>
<el-tag v-for="item in model.modelApiTypes" :key="item" size="small">
{{ item.modelApiTypeName }}
</el-tag>
</div>
<div class="model-pricing">
<span class="pricing-value">{{ formatMultiplier(model.multiplierShow) }}</span>
</div>
</div>
</div>
</el-col>
</el-row>
<!-- 分页 -->
<div v-if="totalCount > 0 && !loading" class="pagination-wrapper">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[12, 24, 48, 96]"
:total="totalCount"
:layout="isMobile ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper'"
prev-text="上一页"
next-text="下一页"
background
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</main>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.model-library-container {
width: 100%;
height: 100%;
background: linear-gradient(180deg, #f5f7fa 0%, #ffffff 100%);
overflow: auto;
// 顶部横幅
.banner-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40px 24px;
position: relative;
overflow: hidden;
// 装饰性背景元素
&::before,
&::after {
content: '';
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
}
&::before {
width: 400px;
height: 400px;
top: -200px;
right: -100px;
}
&::after {
width: 300px;
height: 300px;
bottom: -150px;
left: -100px;
}
.banner-content {
//max-width: 1400px;
margin: 0 auto;
position: relative;
z-index: 1;
.banner-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 32px;
.banner-left {
flex: 1;
display: flex;
align-items: center;
gap: 32px;
.banner-text-section {
flex-shrink: 0;
.banner-title {
font-size: 36px;
font-weight: 800;
color: white;
margin: 0 0 12px 0;
letter-spacing: -0.5px;
}
.banner-subtitle {
font-size: 15px;
color: rgba(255, 255, 255, 0.95);
margin: 0;
line-height: 1.6;
max-width: 500px;
}
}
// 统计卡片
.stats-cards {
display: flex;
gap: 16px;
flex-shrink: 0;
.stat-card {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 12px;
padding: 16px 20px;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.3s;
min-width: 160px;
&:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
}
.stat-icon {
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.25);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: white;
}
.stat-info {
.stat-value {
font-size: 28px;
font-weight: 700;
color: white;
line-height: 1;
margin-bottom: 6px;
}
.stat-label {
font-size: 13px;
color: rgba(255, 255, 255, 0.9);
white-space: nowrap;
}
}
}
}
}
.monitor-card {
background: rgba(10, 15, 30, 0.6);
backdrop-filter: blur(20px);
border: 1px solid rgba(103, 194, 58, 0.3);
border-radius: 20px;
padding: 20px 28px;
cursor: pointer;
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
flex-shrink: 0;
position: relative;
overflow: hidden;
min-width: 280px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg,
rgba(103, 194, 58, 0.1) 0%,
rgba(64, 158, 255, 0.1) 100%);
opacity: 0;
transition: opacity 0.5s;
}
&:hover {
transform: translateY(-6px) scale(1.03);
border-color: rgba(103, 194, 58, 0.6);
box-shadow:
0 16px 48px rgba(103, 194, 58, 0.3),
0 0 80px rgba(103, 194, 58, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
&::before {
opacity: 1;
}
.click-hint {
opacity: 0;
transform: translate(-50%, -50%) scale(0.8);
}
.scan-line {
animation: scanMove 2s linear infinite;
}
.wave-bar {
animation: waveformPulse 0.8s ease-in-out infinite;
}
.border-glow {
opacity: 1;
}
}
// 点击引导
.click-hint {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
z-index: 10;
pointer-events: none;
opacity: 1;
transition: all 0.3s;
animation: clickHintFloat 2s ease-in-out infinite;
.hand-icon {
width: 32px;
height: 32px;
color: rgba(255, 255, 255, 0.9);
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
animation: handClick 1.5s ease-in-out infinite;
}
.hint-text {
font-size: 12px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
background: rgba(103, 194, 58, 0.3);
padding: 4px 12px;
border-radius: 12px;
border: 1px solid rgba(103, 194, 58, 0.5);
white-space: nowrap;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
}
// 动态网格背景
.grid-background {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0.15;
pointer-events: none;
.grid-line {
position: absolute;
background: linear-gradient(90deg, transparent, rgba(103, 194, 58, 0.3), transparent);
height: 1px;
width: 100%;
animation: gridFlow 3s ease-in-out infinite;
&:nth-child(1) { top: 16.66%; animation-delay: 0.2s; }
&:nth-child(2) { top: 33.32%; animation-delay: 0.4s; }
&:nth-child(3) { top: 49.98%; animation-delay: 0.6s; }
&:nth-child(4) { top: 66.64%; animation-delay: 0.8s; }
&:nth-child(5) { top: 83.3%; animation-delay: 1s; }
&:nth-child(6) { top: 99.96%; animation-delay: 1.2s; }
}
}
// 扫描线
.scan-line {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(103, 194, 58, 0.8), transparent);
box-shadow: 0 0 10px rgba(103, 194, 58, 0.8);
pointer-events: none;
}
// 粒子效果
.particles {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
.particle {
position: absolute;
width: 2px;
height: 2px;
background: rgba(103, 194, 58, 0.6);
border-radius: 50%;
animation: particleFloat 4s ease-in-out infinite;
&:nth-child(1) { left: 12%; animation-delay: 0.3s; animation-duration: 3.5s; }
&:nth-child(2) { left: 24%; animation-delay: 0.6s; animation-duration: 4s; }
&:nth-child(3) { left: 36%; animation-delay: 0.9s; animation-duration: 4.5s; }
&:nth-child(4) { left: 48%; animation-delay: 1.2s; animation-duration: 5s; }
&:nth-child(5) { left: 60%; animation-delay: 1.5s; animation-duration: 5.5s; }
&:nth-child(6) { left: 72%; animation-delay: 1.8s; animation-duration: 6s; }
&:nth-child(7) { left: 84%; animation-delay: 2.1s; animation-duration: 6.5s; }
&:nth-child(8) { left: 96%; animation-delay: 2.4s; animation-duration: 7s; }
}
}
// 边框光效
.border-glow {
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: linear-gradient(45deg,
rgba(103, 194, 58, 0.5),
rgba(64, 158, 255, 0.5),
rgba(103, 194, 58, 0.5));
background-size: 200% 200%;
border-radius: 20px;
opacity: 0;
filter: blur(8px);
z-index: -1;
animation: borderGlowMove 3s ease infinite;
transition: opacity 0.5s;
}
// 内容区域
.monitor-content {
display: flex;
align-items: center;
gap: 20px;
position: relative;
z-index: 1;
// 左侧视觉效果
.monitor-visual {
position: relative;
width: 70px;
height: 70px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
// 脉冲圆环
.pulse-rings {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.pulse-ring {
position: absolute;
top: 50%;
left: 50%;
width: 70px;
height: 70px;
border: 2px solid rgba(103, 194, 58, 0.4);
border-radius: 50%;
transform: translate(-50%, -50%);
animation: pulseRing 2s ease-out infinite;
&:nth-child(2) {
animation-delay: 0.6s;
}
&:nth-child(3) {
animation-delay: 1.2s;
}
}
}
// AI神经网络
.ai-network {
position: relative;
width: 60px;
height: 60px;
z-index: 2;
animation: networkRotate 8s linear infinite;
// 中心节点
.center-node {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 16px;
height: 16px;
.node-core {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #67c23a, #409eff);
border-radius: 50%;
box-shadow:
0 0 15px rgba(103, 194, 58, 0.8),
0 0 30px rgba(64, 158, 255, 0.4),
inset 0 0 8px rgba(255, 255, 255, 0.3);
animation: centerNodePulse 2s ease-in-out infinite;
}
}
// 外围节点
.outer-nodes {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
transform: translate(-50%, -50%);
.outer-node {
position: absolute;
top: 50%;
left: 50%;
transform-origin: center;
.node-dot {
width: 8px;
height: 8px;
background: rgba(103, 194, 58, 0.8);
border: 1px solid rgba(103, 194, 58, 1);
border-radius: 50%;
box-shadow: 0 0 8px rgba(103, 194, 58, 0.6);
animation: outerNodeBlink 2s ease-in-out infinite;
animation-delay: inherit;
}
.connection-line {
position: absolute;
bottom: 0;
left: 50%;
width: 1px;
height: 25px;
background: linear-gradient(to top,
rgba(103, 194, 58, 0.6),
transparent);
transform: translateX(-50%);
animation: connectionPulse 2s ease-in-out infinite;
animation-delay: inherit;
}
}
}
// 数据流动粒子
.data-particles {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
transform: translate(-50%, -50%);
.data-particle {
position: absolute;
top: 50%;
left: 50%;
width: 3px;
height: 3px;
background: #409eff;
border-radius: 50%;
box-shadow: 0 0 6px rgba(64, 158, 255, 0.8);
animation: dataParticleMove 2s ease-in-out infinite;
}
}
}
// 状态点
.status-dot {
position: absolute;
top: 8px;
right: 8px;
z-index: 3;
.dot-core {
display: block;
width: 10px;
height: 10px;
background: #67c23a;
border-radius: 50%;
box-shadow:
0 0 0 0 rgba(103, 194, 58, 0.7),
0 0 15px rgba(103, 194, 58, 0.8);
animation: statusDotPulse 2s ease-in-out infinite;
}
}
}
// 右侧信息区域
.monitor-info {
flex: 1;
min-width: 0;
.monitor-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 6px;
.status-badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
background: rgba(103, 194, 58, 0.2);
border: 1px solid rgba(103, 194, 58, 0.5);
border-radius: 12px;
font-size: 11px;
font-weight: 700;
color: #67c23a;
letter-spacing: 1px;
text-transform: uppercase;
.badge-dot {
width: 6px;
height: 6px;
background: #67c23a;
border-radius: 50%;
animation: badgeDotBlink 1.5s ease-in-out infinite;
}
}
.monitor-title {
font-size: 18px;
font-weight: 700;
color: white;
letter-spacing: 0.5px;
}
}
.monitor-subtitle {
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 10px;
letter-spacing: 0.3px;
}
// 波形图
.waveform {
display: flex;
align-items: flex-end;
gap: 3px;
height: 24px;
.wave-bar {
flex: 1;
background: rgba(103, 194, 58, 0.4);
border-radius: 2px 2px 0 0;
min-width: 2px;
transition: all 0.3s;
&:nth-child(1) { height: 40%; }
&:nth-child(2) { height: 70%; }
&:nth-child(3) { height: 50%; }
&:nth-child(4) { height: 85%; }
&:nth-child(5) { height: 60%; }
&:nth-child(6) { height: 90%; }
&:nth-child(7) { height: 75%; }
&:nth-child(8) { height: 55%; }
&:nth-child(9) { height: 80%; }
&:nth-child(10) { height: 65%; }
&:nth-child(11) { height: 45%; }
&:nth-child(12) { height: 70%; }
}
}
}
}
}
}
}
}
// 主内容区
.main-content {
padding: 32px 16px;
.content-wrapper {
max-width: 100%;
width: 100%;
margin: 0 auto;
padding: 0 8px;
display: flex;
gap: 24px;
// 左侧筛选栏
.filter-sidebar {
width: 260px;
flex-shrink: 0;
.filter-section {
background: white;
border-radius: 16px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
position: sticky;
top: 24px;
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 2px solid #f0f0f0;
.filter-title {
font-size: 16px;
font-weight: 700;
color: #303133;
margin: 0;
display: flex;
align-items: center;
gap: 8px;
}
}
.filter-group {
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
.filter-group-title {
font-size: 13px;
font-weight: 600;
color: #606266;
margin: 0 0 12px 0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.filter-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
.filter-tag {
cursor: pointer;
transition: all 0.3s;
user-select: none;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
}
}
.filter-options {
display: flex;
flex-direction: column;
gap: 4px;
.filter-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
user-select: none;
.option-label {
font-size: 14px;
color: #606266;
flex: 1;
}
.option-check {
color: #667eea;
font-size: 16px;
display: flex;
align-items: center;
}
&:hover {
background: #f5f7fa;
}
&.active {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
border: 1px solid rgba(102, 126, 234, 0.3);
.option-label {
color: #667eea;
font-weight: 600;
}
}
}
}
}
}
}
// 右侧模型列表
.model-list-section {
flex: 1;
min-width: 0;
width: 100%;
.loading-wrapper,
.empty-wrapper {
background: white;
border-radius: 16px;
padding: 80px 40px;
text-align: center;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
// 模型网格
.model-grid {
margin-bottom: 32px;
.model-card {
margin-bottom: 20px;
background: white;
border-radius: 16px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
border: 1px solid #f0f0f0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
opacity: 0;
transition: opacity 0.3s;
}
// 尊享模型流光溢彩效果
&.premium-card {
border: 2px solid transparent;
background-image:
linear-gradient(white, white),
linear-gradient(45deg, #ff0000, #ff8000, #ffff00, #00ff00, #00ffff, #0000ff, #8000ff, #ff0080);
background-origin: border-box;
background-clip: padding-box, border-box;
animation: gradientFlow 3s ease infinite;
background-size: 400% 400%;
&::before {
background: linear-gradient(90deg, #ff0000, #ff8000, #ffff00, #00ff00, #00ffff, #0000ff, #8000ff, #ff0080);
background-size: 400% 400%;
animation: gradientFlow 3s ease infinite;
opacity: 1;
height: 3px;
}
&:hover {
box-shadow: 0 12px 32px rgba(255, 0, 128, 0.25);
}
}
&:hover {
transform: translateY(-4px);
box-shadow: 0 12px 32px rgba(102, 126, 234, 0.15);
border-color: #667eea;
&::before {
opacity: 1;
}
.copy-btn-corner {
opacity: 1;
}
}
// 右上角复制按钮
.copy-btn-corner {
position: absolute;
top: 16px;
right: 16px;
z-index: 10;
opacity: 1;
transition: all 0.3s;
background: rgba(255, 255, 255, 0.9);
border: 1px solid #e0e0e0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
&:hover {
background: white;
border-color: #667eea;
color: #667eea;
transform: scale(1.1);
}
}
.model-card-header {
display: flex;
gap: 16px;
margin-bottom: 16px;
align-items: flex-start;
.model-icon {
width: 56px;
height: 56px;
flex-shrink: 0;
.icon-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 12px;
border: 2px solid #f0f0f0;
}
.icon-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
font-weight: 700;
}
}
.model-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
.model-name {
font-size: 18px;
font-weight: 600;
color: #303133;
margin: 0 0 4px 0;
line-height: 1.4;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
padding-right: 24px; // 防止文字遮挡复制按钮
}
.model-provider {
font-size: 13px;
color: #909399;
font-weight: 500;
}
}
}
// ModelId 显示区域
.model-id {
margin-bottom: 12px;
padding: 8px 12px;
background: #f8f9fa;
border-radius: 8px;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
display: flex;
align-items: center;
gap: 8px;
.model-id-label {
font-size: 11px;
color: #909399;
font-weight: 600;
letter-spacing: 0.5px;
}
.model-id-value {
font-size: 12px;
color: #606266;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.model-description {
font-size: 14px;
color: #606266;
line-height: 1.7;
margin: 0 0 20px 0;
-webkit-box-orient: vertical;
min-height: 48px; /* 保持2行的高度 */
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
/* 添加过渡效果 */
transition: all 0.3s ease;
max-height: 3.4em; /* 2行高度 (1.7 * 2 = 3.4em) */
&.placeholder {
color: #c0c4cc;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 12px;
}
/* 悬停时展开 */
&:hover {
-webkit-line-clamp: unset; /* 取消行数限制 */
line-clamp: unset;
max-height: none; /* 取消最大高度限制 */
overflow: visible; /* 显示全部内容 */
/* 可选:添加背景或边框突出显示 */
background-color: #f9f9f9;
//padding: 8px 12px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
//margin-bottom: 20px; /* 保持原有间距 */
/* 如果是绝对定位的父容器可以增加z-index */
z-index: 10;
position: relative;
}
}
.model-footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 6px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
.model-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
flex: 1;
}
.model-pricing {
display: flex;
align-items: center;
justify-content: center;
padding: 6px 10px;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.08) 100%);
border-radius: 8px;
white-space: nowrap;
flex-shrink: 0;
.pricing-value {
font-size: 14px;
font-weight: 700;
color: #667eea;
}
}
}
}
}
// 分页
.pagination-wrapper {
display: flex;
justify-content: center;
padding: 0px 0;
:deep(.el-pagination) {
gap: 8px;
.btn-prev,
.btn-next {
border-radius: 10px;
padding: 0 16px;
font-weight: 500;
transition: all 0.3s;
&:hover:not(:disabled) {
color: #667eea;
transform: translateY(-2px);
}
}
.el-pager li {
border-radius: 10px;
min-width: 40px;
height: 40px;
line-height: 40px;
transition: all 0.3s;
&:hover {
color: #667eea;
transform: translateY(-2px);
}
&.is-active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
}
}
}
}
}
}
}
// 流光溢彩动画
@keyframes gradientFlow {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
@media screen and (max-width: 768px) {
.model-library-container {
.banner-section {
padding: 24px 16px;
.banner-content {
.banner-header {
flex-direction: column;
align-items: stretch;
gap: 24px;
.banner-left {
flex-direction: column;
align-items: flex-start;
gap: 24px;
.stats-cards {
width: 100%;
flex-wrap: wrap;
.stat-card {
flex: 1;
min-width: 140px;
}
}
}
.monitor-card {
width: 100%;
}
}
}
}
.main-content {
padding: 16px 12px;
.content-wrapper {
flex-direction: column;
padding: 0;
.filter-sidebar {
width: 0;
height: 0;
overflow: hidden;
&.mobile-active {
width: auto;
height: auto;
overflow: visible;
.mobile-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1999;
backdrop-filter: blur(4px);
}
.filter-section {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 80%;
max-width: 320px;
z-index: 2000;
border-radius: 0 16px 16px 0;
overflow-y: auto;
animation: slideIn 0.3s ease;
}
}
.filter-section {
margin: 0;
.filter-header {
.filter-header-actions {
display: flex;
align-items: center;
gap: 16px;
.close-btn {
font-size: 20px;
cursor: pointer;
color: #909399;
&:hover {
color: #606266;
}
}
}
}
}
}
.model-list-section {
.mobile-filter-bar {
margin-bottom: 16px;
position: sticky;
top: 0;
z-index: 100;
background: rgba(245, 247, 250, 0.95);
backdrop-filter: blur(10px);
padding: 12px 4px;
margin: -16px -4px 16px -4px;
.w-full {
width: 100%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
}
.pagination-wrapper {
:deep(.el-pagination) {
.el-pagination__jump {
display: none !important;
}
}
}
.model-grid {
margin-bottom: 24px;
.model-card {
padding: 20px; // 增加内边距
margin-bottom: 16px; // 增加卡片间距
border-radius: 16px;
.copy-btn-corner {
top: 16px;
right: 16px;
width: 32px;
height: 32px;
font-size: 14px;
}
.model-card-header {
margin-bottom: 16px;
gap: 16px;
.model-icon {
width: 48px;
height: 48px;
.icon-placeholder {
font-size: 20px;
border-radius: 10px;
}
.icon-img {
border-radius: 10px;
}
}
.model-info {
.model-name {
font-size: 17px;
margin-bottom: 4px;
padding-right: 36px; // 移动端预留更多空间给复制按钮
}
.model-provider {
font-size: 13px;
}
}
}
.model-id {
padding: 6px 10px;
margin-bottom: 12px;
.model-id-label {
font-size: 11px;
}
.model-id-value {
font-size: 11px;
}
}
.model-description {
font-size: 13px;
line-height: 1.5;
margin-bottom: 12px;
min-height: auto;
-webkit-line-clamp: 2;
max-height: 3em;
&:hover {
/* 移动端取消悬停展开,或者改为点击展开(这里简单处理为取消悬停效果以免遮挡) */
-webkit-line-clamp: 2;
max-height: 3em;
overflow: hidden;
position: static;
box-shadow: none;
background: transparent;
padding: 0;
}
}
.model-footer {
padding-top: 12px;
gap: 8px;
.model-tags {
gap: 4px;
:deep(.el-tag) {
height: 20px;
padding: 0 6px;
font-size: 10px;
}
}
.model-pricing {
padding: 4px 8px;
.pricing-value {
font-size: 12px;
}
}
}
}
}
}
}
}
}
}
@keyframes slideIn {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
@keyframes heartbeat {
0% { transform: scale(1); }
14% { transform: scale(1.3); }
28% { transform: scale(1); }
42% { transform: scale(1.3); }
70% { transform: scale(1); }
}
@keyframes pulse {
0% { opacity: 1; box-shadow: 0 0 0 0 rgba(103, 194, 58, 0.7); }
70% { opacity: 0.7; box-shadow: 0 0 0 6px rgba(103, 194, 58, 0); }
100% { opacity: 1; box-shadow: 0 0 0 0 rgba(103, 194, 58, 0); }
}
// 网格流动
@keyframes gridFlow {
0%, 100% {
opacity: 0.15;
transform: translateX(0);
}
50% {
opacity: 0.3;
transform: translateX(10px);
}
}
// 扫描线移动
@keyframes scanMove {
0% {
top: 0;
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
top: 100%;
opacity: 0;
}
}
// 粒子漂浮
@keyframes particleFloat {
0% {
transform: translateY(100%);
opacity: 0;
}
10% {
opacity: 0.6;
}
90% {
opacity: 0.6;
}
100% {
transform: translateY(-100%);
opacity: 0;
}
}
// 边框光效移动
@keyframes borderGlowMove {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
// 点击引导浮动
@keyframes clickHintFloat {
0%, 100% {
transform: translate(-50%, -50%);
}
50% {
transform: translate(-50%, -55%);
}
}
// 手势点击动画
@keyframes handClick {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(0.9);
}
}
// 脉冲圆环
@keyframes pulseRing {
0% {
transform: translate(-50%, -50%) scale(0.8);
opacity: 0.6;
}
100% {
transform: translate(-50%, -50%) scale(1.4);
opacity: 0;
}
}
// 3D立方体旋转
@keyframes cubeRotate {
0% {
transform: rotateX(-20deg) rotateY(-20deg);
}
100% {
transform: rotateX(-20deg) rotateY(340deg);
}
}
// AI神经网络旋转
@keyframes networkRotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
// 中心节点脉冲
@keyframes centerNodePulse {
0%, 100% {
transform: scale(1);
box-shadow:
0 0 15px rgba(103, 194, 58, 0.8),
0 0 30px rgba(64, 158, 255, 0.4),
inset 0 0 8px rgba(255, 255, 255, 0.3);
}
50% {
transform: scale(1.2);
box-shadow:
0 0 25px rgba(103, 194, 58, 1),
0 0 50px rgba(64, 158, 255, 0.6),
inset 0 0 12px rgba(255, 255, 255, 0.5);
}
}
// 状态点脉冲
@keyframes statusDotPulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(103, 194, 58, 0.7), 0 0 15px rgba(103, 194, 58, 0.8);
}
50% {
box-shadow: 0 0 0 8px rgba(103, 194, 58, 0), 0 0 25px rgba(103, 194, 58, 1);
}
}
// 徽章点闪烁
@keyframes badgeDotBlink {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(0.8);
}
}
// 波形脉冲
@keyframes waveformPulse {
0%, 100% {
background: rgba(103, 194, 58, 0.4);
transform: scaleY(1);
}
50% {
background: rgba(103, 194, 58, 0.8);
transform: scaleY(1.3);
}
}
// 外围节点闪烁
@keyframes outerNodeBlink {
0%, 100% {
opacity: 0.6;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.3);
}
}
// 连接线脉冲
@keyframes connectionPulse {
0%, 100% {
opacity: 0.4;
}
50% {
opacity: 1;
}
}
// 数据粒子移动
@keyframes dataParticleMove {
0% {
transform: translate(-50%, -50%) translateY(0) scale(0);
opacity: 0;
}
20% {
opacity: 1;
transform: translate(-50%, -50%) translateY(-5px) scale(1);
}
80% {
opacity: 1;
transform: translate(-50%, -50%) translateY(-20px) scale(1);
}
100% {
transform: translate(-50%, -50%) translateY(-25px) scale(0);
opacity: 0;
}
}
</style>