fix: 模型库优化

This commit is contained in:
Gsh
2025-12-10 01:34:40 +08:00
parent 1a32fa9e20
commit c319b0b4e4
6 changed files with 444 additions and 220 deletions

View File

@@ -1,7 +1,8 @@
{ {
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(npx vue-tsc --noEmit)" "Bash(npx vue-tsc --noEmit)",
"Bash(timeout 60 npx vue-tsc:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@@ -12,14 +12,14 @@ export function getModelLibraryList(params?: ModelLibraryGetListInput) {
if (params?.searchKey) { if (params?.searchKey) {
queryParams.append('SearchKey', params.searchKey); queryParams.append('SearchKey', params.searchKey);
} }
if (params?.providerName) { if (params?.providerNames && params.providerNames.length > 0) {
queryParams.append('ProviderName', params.providerName); params.providerNames.forEach(name => queryParams.append('ProviderNames', name));
} }
if (params?.modelType !== undefined) { if (params?.modelTypes && params.modelTypes.length > 0) {
queryParams.append('ModelType', params.modelType.toString()); params.modelTypes.forEach(type => queryParams.append('ModelTypes', type.toString()));
} }
if (params?.modelApiType !== undefined) { if (params?.modelApiTypes && params.modelApiTypes.length > 0) {
queryParams.append('ModelApiType', params.modelApiType.toString()); params.modelApiTypes.forEach(type => queryParams.append('ModelApiTypes', type.toString()));
} }
if (params?.isPremiumOnly !== undefined) { if (params?.isPremiumOnly !== undefined) {
queryParams.append('IsPremiumOnly', params.isPremiumOnly.toString()); queryParams.append('IsPremiumOnly', params.isPremiumOnly.toString());

View File

@@ -46,9 +46,9 @@ export interface ModelLibraryDto {
// 获取模型库列表查询参数 // 获取模型库列表查询参数
export interface ModelLibraryGetListInput { export interface ModelLibraryGetListInput {
searchKey?: string; searchKey?: string;
providerName?: string; providerNames?: string[];
modelType?: ModelTypeEnum; modelTypes?: ModelTypeEnum[];
modelApiType?: ModelApiTypeEnum; modelApiTypes?: ModelApiTypeEnum[];
isPremiumOnly?: boolean; isPremiumOnly?: boolean;
skipCount?: number; skipCount?: number;
maxResultCount?: number; maxResultCount?: number;

View File

@@ -0,0 +1,98 @@
<template>
<div class="flex gap-2">
<el-tag v-for="tag in tags" :key="tag.name" closable :type="tag.type">
{{ tag.name }}
</el-tag>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import type { TagProps } from 'element-plus'
interface TagsItem {
name: string
type: TagProps['type']
}
const tags = ref<TagsItem[]>([
{ name: 'Tag 1', type: 'primary' },
{ name: 'Tag 2', type: 'success' },
{ name: 'Tag 3', type: 'info' },
{ name: 'Tag 4', type: 'warning' },
{ name: 'Tag 5', type: 'danger' },
])
</script>
<template>
<div class="flex gap-2">
<el-check-tag checked>Checked</el-check-tag>
<el-check-tag :checked="checked" @change="onChange">Toggle me</el-check-tag>
<el-check-tag disabled>Disabled</el-check-tag>
</div>
<div class="flex gap-2 mt-4">
<el-check-tag :checked="checked1" type="primary" @change="onChange1">
Tag 1
</el-check-tag>
<el-check-tag :checked="checked2" type="success" @change="onChange2">
Tag 2
</el-check-tag>
<el-check-tag :checked="checked3" type="info" @change="onChange3">
Tag 3
</el-check-tag>
<el-check-tag :checked="checked4" type="warning" @change="onChange4">
Tag 4
</el-check-tag>
<el-check-tag :checked="checked5" type="danger" @change="onChange5">
Tag 5
</el-check-tag>
<el-check-tag
:checked="checked6"
disabled
type="success"
@change="onChange6"
>
Tag 6
</el-check-tag>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const checked = ref(false)
const checked1 = ref(true)
const checked2 = ref(true)
const checked3 = ref(true)
const checked4 = ref(true)
const checked5 = ref(true)
const checked6 = ref(true)
const onChange = (status: boolean) => {
checked.value = status
}
const onChange1 = (status: boolean) => {
checked1.value = status
}
const onChange2 = (status: boolean) => {
checked2.value = status
}
const onChange3 = (status: boolean) => {
checked3.value = status
}
const onChange4 = (status: boolean) => {
checked4.value = status
}
const onChange5 = (status: boolean) => {
checked5.value = status
}
const onChange6 = (status: boolean) => {
checked6.value = status
}
</script>

View File

@@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ModelApiTypeOption, ModelLibraryDto, ModelTypeOption } from '@/api/model/types';
import { Box, CopyDocument, HomeFilled, OfficeBuilding, Search } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { onMounted, ref, watch } from 'vue'; import { onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { getApiTypeOptions, getModelLibraryList, getModelTypeOptions, getProviderList } from '@/api/model'; import { getApiTypeOptions, getModelLibraryList, getModelTypeOptions, getProviderList } from '@/api/model';
import type { ModelApiTypeOption, ModelLibraryDto, ModelTypeOption } from '@/api/model/types';
import { Search, CopyDocument, HomeFilled } from '@element-plus/icons-vue';
const router = useRouter(); const router = useRouter();
@@ -16,32 +16,28 @@ const pageSize = ref(10);
// 筛选条件 // 筛选条件
const searchKey = ref(''); const searchKey = ref('');
const selectedProvider = ref<string>(''); const selectedProviders = ref<string[]>([]);
const selectedModelType = ref<number | undefined>(undefined); const selectedModelTypes = ref<number[]>([]);
const selectedApiType = ref<number | undefined>(undefined); const selectedApiTypes = ref<number[]>([]);
const isPremiumOnly = ref(false); const isPremiumOnly = ref(false);
// 供应商列表 // 供应商列表
const providerList = ref<string[]>(['全部供应商']); const providerList = ref<string[]>([]);
// 模型类型选项 // 模型类型选项
const modelTypeOptions = ref<ModelTypeOption[]>([ const modelTypeOptions = ref<ModelTypeOption[]>([]);
{ label: '全部类型', value: undefined as any },
]);
// API类型选项 // API类型选项
const apiTypeOptions = ref<ModelApiTypeOption[]>([ const apiTypeOptions = ref<ModelApiTypeOption[]>([]);
{ label: '全部API类型', value: undefined as any },
]);
async function fetchModelList() { async function fetchModelList() {
loading.value = true; loading.value = true;
try { try {
const params = { const params = {
searchKey: searchKey.value || undefined, searchKey: searchKey.value || undefined,
providerName: selectedProvider.value && selectedProvider.value !== '全部供应商' ? selectedProvider.value : undefined, providerNames: selectedProviders.value.length > 0 ? selectedProviders.value : undefined,
modelType: selectedModelType.value, modelTypes: selectedModelTypes.value.length > 0 ? selectedModelTypes.value : undefined,
modelApiType: selectedApiType.value, modelApiTypes: selectedApiTypes.value.length > 0 ? selectedApiTypes.value : undefined,
isPremiumOnly: isPremiumOnly.value || undefined, isPremiumOnly: isPremiumOnly.value || undefined,
skipCount: currentPage.value, skipCount: currentPage.value,
maxResultCount: pageSize.value, maxResultCount: pageSize.value,
@@ -66,7 +62,7 @@ async function fetchModelList() {
async function fetchProviderList() { async function fetchProviderList() {
try { try {
const response = await getProviderList(); const response = await getProviderList();
providerList.value = ['全部供应商', ...response.data.sort()]; providerList.value = response.data.sort();
} }
catch (error) { catch (error) {
console.error('获取供应商列表失败:', error); console.error('获取供应商列表失败:', error);
@@ -77,10 +73,7 @@ async function fetchProviderList() {
async function fetchModelTypeOptions() { async function fetchModelTypeOptions() {
try { try {
const response = await getModelTypeOptions(); const response = await getModelTypeOptions();
modelTypeOptions.value = [ modelTypeOptions.value = response.data;
{ label: '全部类型', value: undefined as any },
...response.data,
];
} }
catch (error) { catch (error) {
console.error('获取模型类型选项失败:', error); console.error('获取模型类型选项失败:', error);
@@ -91,10 +84,7 @@ async function fetchModelTypeOptions() {
async function fetchApiTypeOptions() { async function fetchApiTypeOptions() {
try { try {
const response = await getApiTypeOptions(); const response = await getApiTypeOptions();
apiTypeOptions.value = [ apiTypeOptions.value = response.data;
{ label: '全部API类型', value: undefined as any },
...response.data,
];
} }
catch (error) { catch (error) {
console.error('获取API类型选项失败:', error); console.error('获取API类型选项失败:', error);
@@ -109,15 +99,72 @@ function copyModelId(modelId: string) {
function resetFilters() { function resetFilters() {
searchKey.value = ''; searchKey.value = '';
selectedProvider.value = ''; selectedProviders.value = [];
selectedModelType.value = undefined; selectedModelTypes.value = [];
selectedApiType.value = undefined; selectedApiTypes.value = [];
isPremiumOnly.value = false; isPremiumOnly.value = false;
currentPage.value = 1; currentPage.value = 1;
} }
function selectProvider(provider: string) { function toggleProvider(provider: string | null) {
selectedProvider.value = provider === selectedProvider.value ? '' : provider; 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; currentPage.value = 1;
} }
@@ -149,7 +196,7 @@ watch(searchKey, () => {
}, 500); }, 500);
}); });
watch([selectedProvider, selectedModelType, selectedApiType, isPremiumOnly], () => { watch([selectedProviders, selectedModelTypes, selectedApiTypes, isPremiumOnly], () => {
currentPage.value = 1; currentPage.value = 1;
fetchModelList(); fetchModelList();
}, { deep: true }); }, { deep: true });
@@ -169,42 +216,42 @@ onMounted(() => {
<div class="banner-content"> <div class="banner-content">
<div class="banner-header"> <div class="banner-header">
<div class="banner-left"> <div class="banner-left">
<h1 class="banner-title">AI模型广场</h1> <div class="banner-text-section">
<p class="banner-subtitle"> <h1 class="banner-title">AI模型广场</h1>
探索并接入全球顶尖AI模型覆盖文本生成图像理解代码辅助等多个领域 <p class="banner-subtitle">
</p> 探索并接入全球顶尖AI模型覆盖文本生成图像理解代码辅助等多个领域
</div> </p>
<div class="banner-right"> </div>
<!-- 统计信息卡片 --> <!-- 统计信息卡片 -->
<div class="stats-cards"> <div class="stats-cards">
<div class="stat-card"> <div class="stat-card">
<div class="stat-icon"> <div class="stat-icon">
<el-icon><i-ep-box /></el-icon> <el-icon><Box /></el-icon>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<div class="stat-value">{{ totalCount }}</div> <div class="stat-value">{{ totalCount }}</div>
<div class="stat-label">模型</div> <div class="stat-label">可用模型</div>
</div> </div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="stat-icon"> <div class="stat-icon">
<el-icon><i-ep-office-building /></el-icon> <el-icon><OfficeBuilding /></el-icon>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<div class="stat-value">{{ providerList.length - 1 }}</div> <div class="stat-value">{{ providerList.length - 1 }}</div>
<div class="stat-label">供应商</div> <div class="stat-label">支持供应商</div>
</div> </div>
</div> </div>
</div> </div>
<el-button
:icon="HomeFilled"
class="home-btn"
round
@click="goToHome"
>
返回首页
</el-button>
</div> </div>
<el-button
:icon="HomeFilled"
class="home-btn"
round
@click="goToHome"
>
返回首页
</el-button>
</div> </div>
</div> </div>
</div> </div>
@@ -241,85 +288,99 @@ onMounted(() => {
<!-- 供应商 --> <!-- 供应商 -->
<div class="filter-group"> <div class="filter-group">
<h4 class="filter-group-title">供应商</h4> <h4 class="filter-group-title">
<div class="filter-options"> 供应商
<div </h4>
v-for="provider in providerList.filter(p => p !== '全部供应商')" <div class="filter-tags">
:key="provider" <el-check-tag
class="filter-option" :checked="selectedProviders.length === 0"
:class="{ active: selectedProvider === provider }" class="filter-tag"
@click="selectedProvider = selectedProvider === provider ? '' : provider; currentPage = 1" @change="toggleProvider(null)"
> >
<span class="option-label">{{ provider }}</span> 全部供应商
<span v-if="selectedProvider === provider" class="option-check"> </el-check-tag>
<el-icon><i-ep-check /></el-icon> <el-check-tag
</span> v-for="provider in providerList"
</div> :key="provider"
:checked="selectedProviders.includes(provider)"
class="filter-tag"
@change="toggleProvider(provider)"
>
{{ provider }}
</el-check-tag>
</div> </div>
</div> </div>
<!-- 模型类型 --> <!-- 模型类型 -->
<div class="filter-group"> <div class="filter-group">
<h4 class="filter-group-title">模型类型</h4> <h4 class="filter-group-title">
<div class="filter-options"> 模型类型
<div </h4>
v-for="option in modelTypeOptions.filter(o => o.value !== undefined)" <div class="filter-tags">
:key="option.value" <el-check-tag
class="filter-option" :checked="selectedModelTypes.length === 0"
:class="{ active: selectedModelType === option.value }" class="filter-tag"
@click="selectedModelType = option.value" @change="toggleModelType(null)"
> >
<span class="option-label">{{ option.label }}</span> 全部类型
<span v-if="selectedModelType === option.value" class="option-check"> </el-check-tag>
<el-icon><i-ep-check /></el-icon> <el-check-tag
</span> v-for="option in modelTypeOptions"
</div> :key="option.value"
:checked="selectedModelTypes.includes(option.value)"
class="filter-tag"
@change="toggleModelType(option.value)"
>
{{ option.label }}
</el-check-tag>
</div> </div>
</div> </div>
<!-- API类型 --> <!-- API类型 -->
<div class="filter-group"> <div class="filter-group">
<h4 class="filter-group-title">API类型</h4> <h4 class="filter-group-title">
<div class="filter-options"> API类型
<div </h4>
v-for="option in apiTypeOptions.filter(o => o.value !== undefined)" <div class="filter-tags">
:key="option.value" <el-check-tag
class="filter-option" :checked="selectedApiTypes.length === 0"
:class="{ active: selectedApiType === option.value }" class="filter-tag"
@click="selectedApiType = option.value" @change="toggleApiType(null)"
> >
<span class="option-label">{{ option.label }}</span> 全部API类型
<span v-if="selectedApiType === option.value" class="option-check"> </el-check-tag>
<el-icon><i-ep-check /></el-icon> <el-check-tag
</span> v-for="option in apiTypeOptions"
</div> :key="option.value"
:checked="selectedApiTypes.includes(option.value)"
class="filter-tag"
@change="toggleApiType(option.value)"
>
{{ option.label }}
</el-check-tag>
</div> </div>
</div> </div>
<!-- 计费类型 --> <!-- 计费类型 -->
<div class="filter-group"> <div class="filter-group">
<h4 class="filter-group-title">计费类型</h4> <h4 class="filter-group-title">
<div class="filter-options"> 计费类型
<div </h4>
class="filter-option" <div class="filter-tags">
:class="{ active: !isPremiumOnly }" <el-check-tag
@click="isPremiumOnly = false" :checked="!isPremiumOnly"
class="filter-tag"
@change="togglePremiumOnly(null)"
> >
<span class="option-label">全部模型</span> 全部模型
<span v-if="!isPremiumOnly" class="option-check"> </el-check-tag>
<el-icon><i-ep-check /></el-icon> <el-check-tag
</span> :checked="isPremiumOnly"
</div> class="filter-tag"
<div @change="togglePremiumOnly(true)"
class="filter-option"
:class="{ active: isPremiumOnly }"
@click="isPremiumOnly = true"
> >
<span class="option-label">仅尊享模型</span> 仅尊享模型
<span v-if="isPremiumOnly" class="option-check"> </el-check-tag>
<el-icon><i-ep-check /></el-icon>
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -347,6 +408,7 @@ onMounted(() => {
v-for="model in modelList" v-for="model in modelList"
:key="model.modelId" :key="model.modelId"
class="model-card" class="model-card"
:class="{ 'premium-card': model.isPremium }"
> >
<div class="model-card-header"> <div class="model-card-header">
<div class="model-icon"> <div class="model-icon">
@@ -386,26 +448,16 @@ onMounted(() => {
<div class="model-footer"> <div class="model-footer">
<div class="model-tags"> <div class="model-tags">
<el-tag v-if="model.isPremium" size="small" type="warning" effect="dark"> <el-tag size="small">
尊享
</el-tag>
<el-tag size="small" effect="plain">
{{ model.modelTypeName }} {{ model.modelTypeName }}
</el-tag> </el-tag>
<el-tag size="small" effect="plain"> <el-tag size="small">
{{ model.modelApiTypeName }} {{ model.modelApiTypeName }}
</el-tag> </el-tag>
</div> </div>
<div class="model-pricing"> <div class="model-pricing">
<div class="price-item"> <span class="pricing-label">计费倍率</span>
<span class="price-label">输入</span> <span class="pricing-value">{{ model.multiplierShow }}×</span>
<span class="price-value">¥{{ model.multiplierShow }}</span>
</div>
<div class="price-divider">|</div>
<div class="price-item">
<span class="price-label">输出</span>
<span class="price-value">¥{{ (model.multiplierShow * 2).toFixed(4) }}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -476,49 +528,52 @@ onMounted(() => {
.banner-header { .banner-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: flex-start;
gap: 32px; gap: 32px;
.banner-left { .banner-left {
flex: 1; flex: 1;
.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: 600px;
}
}
.banner-right {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; 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 { .stats-cards {
display: flex; display: flex;
gap: 12px; gap: 16px;
flex-shrink: 0;
.stat-card { .stat-card {
background: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.25); border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 12px; border-radius: 12px;
padding: 12px 16px; padding: 16px 20px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
transition: all 0.3s; transition: all 0.3s;
min-width: 160px;
&:hover { &:hover {
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
@@ -526,48 +581,50 @@ onMounted(() => {
} }
.stat-icon { .stat-icon {
width: 40px; width: 48px;
height: 40px; height: 48px;
background: rgba(255, 255, 255, 0.25); background: rgba(255, 255, 255, 0.25);
border-radius: 8px; border-radius: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 20px; font-size: 24px;
color: white; color: white;
} }
.stat-info { .stat-info {
.stat-value { .stat-value {
font-size: 24px; font-size: 28px;
font-weight: 700; font-weight: 700;
color: white; color: white;
line-height: 1; line-height: 1;
margin-bottom: 4px; margin-bottom: 6px;
} }
.stat-label { .stat-label {
font-size: 12px; font-size: 13px;
color: rgba(255, 255, 255, 0.85); color: rgba(255, 255, 255, 0.9);
white-space: nowrap;
} }
} }
} }
} }
}
.home-btn { .home-btn {
background: rgba(255, 255, 255, 0.25); background: rgba(255, 255, 255, 0.25);
border: 1px solid rgba(255, 255, 255, 0.4); border: 1px solid rgba(255, 255, 255, 0.4);
color: white; color: white;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
padding: 10px 24px; padding: 10px 24px;
font-weight: 500; font-weight: 500;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
flex-shrink: 0;
&:hover { &:hover {
background: rgba(255, 255, 255, 0.35); background: rgba(255, 255, 255, 0.35);
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
}
} }
} }
} }
@@ -576,11 +633,12 @@ onMounted(() => {
// 主内容区 // 主内容区
.main-content { .main-content {
padding: 32px 24px; padding: 32px 16px;
.content-wrapper { .content-wrapper {
max-width: 1400px; max-width: 100%;
margin: 0 auto; margin: 0 auto;
padding: 0 8px;
display: flex; display: flex;
gap: 24px; gap: 24px;
@@ -632,6 +690,23 @@ onMounted(() => {
letter-spacing: 0.5px; 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 { .filter-options {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -724,6 +799,30 @@ onMounted(() => {
transition: opacity 0.3s; 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 { &:hover {
transform: translateY(-4px); transform: translateY(-4px);
box-shadow: 0 12px 32px rgba(102, 126, 234, 0.15); box-shadow: 0 12px 32px rgba(102, 126, 234, 0.15);
@@ -779,7 +878,7 @@ onMounted(() => {
font-weight: 600; font-weight: 600;
color: #303133; color: #303133;
margin: 0 0 6px 0; margin: 0 0 6px 0;
overflow: hidden; //overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
@@ -806,7 +905,7 @@ onMounted(() => {
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; //overflow: hidden;
min-height: 48px; min-height: 48px;
&.placeholder { &.placeholder {
@@ -828,35 +927,27 @@ onMounted(() => {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 6px; gap: 6px;
flex: 1;
} }
.model-pricing { .model-pricing {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
font-size: 13px; padding: 6px 12px;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.08) 100%);
border-radius: 8px;
white-space: nowrap; white-space: nowrap;
.price-item { .pricing-label {
display: flex; font-size: 12px;
flex-direction: column; color: #909399;
align-items: center;
gap: 2px;
.price-label {
font-size: 11px;
color: #909399;
}
.price-value {
font-size: 14px;
font-weight: 600;
color: #303133;
}
} }
.price-divider { .pricing-value {
color: #dcdfe6; font-size: 16px;
font-weight: 700;
color: #667eea;
} }
} }
} }
@@ -925,33 +1016,56 @@ onMounted(() => {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
.banner-left .banner-title { .banner-left {
font-size: 28px;
}
.banner-left .banner-subtitle {
font-size: 14px;
}
.banner-right {
width: 100%; width: 100%;
flex-direction: column-reverse; flex-direction: column;
align-items: flex-start;
gap: 20px;
.stats-cards { .banner-text-section {
width: 100%; width: 100%;
flex-direction: row;
.stat-card { .banner-title {
flex: 1; font-size: 28px;
justify-content: center; }
.banner-subtitle {
font-size: 14px;
} }
} }
.home-btn { .stats-cards {
width: 100%; width: 100%;
justify-content: center; gap: 12px;
.stat-card {
flex: 1;
min-width: 0;
padding: 12px 16px;
.stat-icon {
width: 40px;
height: 40px;
font-size: 20px;
}
.stat-info {
.stat-value {
font-size: 24px;
}
.stat-label {
font-size: 12px;
}
}
}
} }
} }
.home-btn {
width: 100%;
justify-content: center;
}
} }
} }
@@ -983,10 +1097,7 @@ onMounted(() => {
.model-pricing { .model-pricing {
width: 100%; width: 100%;
justify-content: space-around; justify-content: center;
padding: 12px;
background: #f5f7fa;
border-radius: 8px;
} }
} }
} }
@@ -1022,4 +1133,17 @@ onMounted(() => {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
} }
} }
// 流光溢彩动画
@keyframes gradientFlow {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
</style> </style>

View File

@@ -19,6 +19,7 @@ declare module 'vue' {
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckTag: typeof import('element-plus/es')['ElCheckTag']
ElCollapse: typeof import('element-plus/es')['ElCollapse'] ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElContainer: typeof import('element-plus/es')['ElContainer'] ElContainer: typeof import('element-plus/es')['ElContainer']