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": {
"allow": [
"Bash(npx vue-tsc --noEmit)"
"Bash(npx vue-tsc --noEmit)",
"Bash(timeout 60 npx vue-tsc:*)"
],
"deny": [],
"ask": []

View File

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

View File

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

View File

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