定价页重构:SiliconFlow风格卡片布局 + 炫酷动画效果

- 移除侧边栏,改为全宽布局,顶部Hero区域(渐变标题+搜索框+热门标签)
- 新增横向筛选pill按钮(模型类型/供应商/服务商)
- 卡片网格3列响应式布局,悬停发光+上浮+微缩放效果
- 新增浮动粒子背景、卡片入场逐个动画、搜索框光晕脉冲
- 新增流光扫过效果、价格数字脉冲发光
- 新增DeepSeek-V4/MiniMax模型默认倍率
- 更新setup.ps1支持DeepSeek+Minimax双供应商配置
This commit is contained in:
wangxiaoji 2026-07-28 09:29:57 +08:00
parent 8df9865ff5
commit 5a68d88fa9
9 changed files with 1532 additions and 1105 deletions

View File

@ -274,6 +274,14 @@ var defaultModelRatio = map[string]float64{
"deepseek-ai/DeepSeek-V3-0324": 0.8,
"deepseek-ai/DeepSeek-V3.1": 0.8,
"DeepSeek-V3.2": 0.8,
"deepseek-V4": 1.0,
"deepseek-v4-Flash": 0.2,
// MiniMax
"MiniMax-M2.1": 1.0,
"MiniMax-M2.1-highspeed": 0.5,
"MiniMax-M2": 0.8,
"MiniMax-M2.5": 1.2,
"MiniMax-M2.5-highspeed": 0.6,
}
var defaultModelPrice = map[string]float64{

56
setup.ps1 Normal file
View File

@ -0,0 +1,56 @@
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# Login
$loginResp = Invoke-WebRequest -Uri "http://localhost:3000/api/user/login" -Method POST -Body '{"username":"admin","password":"admin12345"}' -ContentType "application/json; charset=utf-8" -UseBasicParsing -SessionVariable ws
Write-Host "Login OK"
$hdr = @{"New-Api-User"="1"}
# Create channels
$ch1 = '{"mode":"single","channel":{"name":"DeepSeek","type":43,"base_url":"https://api.deepseek.com","key":"sk-ds-test","models":"deepseek-V4,deepseek-v4-Flash","status":1,"supplier_type":"\u516c\u6709\u4e91","other":"{}","setting":"{}"}}'
$ch2 = '{"mode":"single","channel":{"name":"Minimax","type":35,"base_url":"https://api.minimax.chat","key":"sk-mm-test","models":"MiniMax-M2.1,MiniMax-M2.1-highspeed,MiniMax-M2,MiniMax-M2.5,MiniMax-M2.5-highspeed","status":1,"supplier_type":"\u516c\u6709\u4e91","other":"{}","setting":"{}"}}'
foreach ($ch in @($ch1,$ch2)) {
$bytes = [System.Text.Encoding]::UTF8.GetBytes($ch)
$r = Invoke-WebRequest -Uri "http://localhost:3000/api/channel/" -Method POST -Body $bytes -ContentType "application/json; charset=utf-8" -UseBasicParsing -WebSession $ws -Headers $hdr
Write-Host "Channel created"
}
# Sync abilities
$m1 = [System.Text.Encoding]::UTF8.GetBytes('{"models":["deepseek-V4","deepseek-v4-Flash"]}')
$m2 = [System.Text.Encoding]::UTF8.GetBytes('{"models":["MiniMax-M2.1","MiniMax-M2.1-highspeed","MiniMax-M2","MiniMax-M2.5","MiniMax-M2.5-highspeed"]}')
Invoke-WebRequest -Uri "http://localhost:3000/api/channel/1/models" -Method PATCH -Body $m1 -ContentType "application/json; charset=utf-8" -UseBasicParsing -WebSession $ws -Headers $hdr | Out-Null
Invoke-WebRequest -Uri "http://localhost:3000/api/channel/2/models" -Method PATCH -Body $m2 -ContentType "application/json; charset=utf-8" -UseBasicParsing -WebSession $ws -Headers $hdr | Out-Null
Write-Host "Abilities synced"
# Create model metas
$names = @("deepseek-V4","deepseek-v4-Flash","MiniMax-M2.1","MiniMax-M2.1-highspeed","MiniMax-M2","MiniMax-M2.5","MiniMax-M2.5-highspeed")
foreach ($n in $names) {
$body = [System.Text.Encoding]::UTF8.GetBytes("{`"model_name`":`"$n`",`"status`":1}")
Invoke-WebRequest -Uri "http://localhost:3000/api/models/" -Method POST -Body $body -ContentType "application/json; charset=utf-8" -UseBasicParsing -WebSession $ws -Headers $hdr | Out-Null
Write-Host "Meta: $n"
}
# Seed test results (required for pricing page to display data)
$testModels = @(
@{ch=1; ms=@("deepseek-V4","deepseek-v4-Flash"); msrt=200},
@{ch=2; ms=@("MiniMax-M2.1","MiniMax-M2.1-highspeed","MiniMax-M2","MiniMax-M2.5","MiniMax-M2.5-highspeed"); msrt=300}
)
foreach ($g in $testModels) {
foreach ($m in $g.ms) {
$body = [System.Text.Encoding]::UTF8.GetBytes("{`"channel_id`":$($g.ch),`"model_name`":`"$m`",`"manual_display_response_time`":$($g.msrt),`"manual_stability_grade`":5}")
Invoke-WebRequest -Uri "http://localhost:3000/api/channel/model-test-result-display" -Method PUT -Body $body -ContentType "application/json; charset=utf-8" -UseBasicParsing -WebSession $ws -Headers $hdr | Out-Null
Write-Host "Test result: ch=$($g.ch) $m"
}
}
# Reset ratios
$rr = Invoke-WebRequest -Uri "http://localhost:3000/api/option/rest_model_ratio" -Method POST -UseBasicParsing -WebSession $ws -Headers $hdr
Write-Host "Ratio reset: $($rr.Content)"
# Check
$resp = Invoke-WebRequest -Uri "http://localhost:3000/api/pricing" -UseBasicParsing -WebSession $ws -Headers $hdr
$data = $resp.Content | ConvertFrom-Json
Write-Host "`nPricing data count: $($data.data.Count)"
$data.data | ForEach-Object { Write-Host " $($_.model_name)" }

View File

@ -6,6 +6,14 @@ it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
@ -13,15 +21,6 @@ import React from 'react';
import { Search } from 'lucide-react';
import PricingCardView from '../view/card/PricingCardView';
// Tab tag
const TAB_TO_TAG = {
all: 'all',
text: '文本',
image: '图片',
audio: '音频',
video: '视频',
};
const TYPE_TABS = [
{ key: 'all', labelKey: '全部' },
{ key: 'text', labelKey: '文本' },
@ -30,9 +29,20 @@ const TYPE_TABS = [
{ key: 'video', labelKey: '视频' },
];
const TAB_TO_TAG = {
all: 'all',
text: '文本',
image: '图片',
audio: '音频',
video: '视频',
};
const ModelsContent = ({
filteredModels,
models,
filterTag, setFilterTag,
filterVendor, setFilterVendor,
filterSupplierType, setFilterSupplierType,
searchValue, setSearchValue,
loading,
isMobile,
@ -40,48 +50,161 @@ const ModelsContent = ({
t,
...cardProps
}) => {
// filterTag tab key
const activeTabKey = filterTag === 'all' ? 'all'
: (Object.entries(TAB_TO_TAG).find(([, v]) => v === filterTag)?.[0] || 'all');
// Get unique vendors for filter pills
const vendors = React.useMemo(() => {
if (!models) return [];
const names = [...new Set(models.map((m) => m.vendor_name).filter(Boolean))].sort();
return names;
}, [models]);
// Get unique supplier types
const supplierTypes = React.useMemo(() => {
if (!models) return [];
const types = new Set();
models.forEach((m) => {
(m.channel_list || []).forEach((ch) => {
if (ch.supplier_type) types.add(ch.supplier_type);
});
});
return [...types].sort();
}, [models]);
return (
<>
{/* Type Tabs */}
<div className='models-tabs-bar'>
<div className='sf-pricing-inner'>
{/* Floating Particles */}
<div className='sf-particles'>
{Array.from({ length: 8 }, (_, i) => (
<div key={i} className='sf-particle' />
))}
</div>
{/* Hero Section */}
<div className='sf-hero'>
<h1 className='sf-hero-title'>
{t('探索最适合的 AI 模型')}
</h1>
<p className='sf-hero-subtitle'>
{t('为您的应用找到最优质的模型与服务')}
</p>
{/* Search Bar */}
<div className='sf-search-bar'>
<Search size={16} className='sf-search-icon' />
<input
type='text'
placeholder={t('搜索模型名称、供应商、应用场景...')}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
className='sf-search-input'
/>
</div>
{/* Hot Models Quick Tags */}
{models && models.length > 0 && (
<div className='sf-hot-tags'>
<span className='sf-hot-label'>{t('热门模型')}</span>
{models.slice(0, isMobile ? 4 : 7).map((m) => (
<button
key={m.model_name}
className='sf-hot-tag'
onClick={() => setSearchValue(m.model_name)}
>
{m.model_name}
</button>
))}
</div>
)}
</div>
{/* Filter Section */}
<div className='sf-filter-section'>
{/* Type Filters */}
<div className='sf-filter-row'>
<span className='sf-filter-label'>{t('模型类型')}</span>
<div className='sf-filter-pills'>
{TYPE_TABS.map((tab) => (
<button
key={tab.key}
className={`models-tab-btn${activeTabKey === tab.key ? ' active' : ''}`}
className={`sf-filter-pill${activeTabKey === tab.key ? ' active' : ''}`}
onClick={() => setFilterTag(TAB_TO_TAG[tab.key])}
>
{t(tab.labelKey)}
</button>
))}
<div className='flex-1' />
<div className='relative'>
<Search size={15} className='absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground/60' />
<input
type='text'
placeholder={t('搜索模型名称...')}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
className='w-48 bg-muted/30 border border-input rounded-lg pl-9 pr-3 py-1.5 text-sm text-foreground placeholder-muted-foreground/50 focus:outline-none focus:border-ring focus:bg-muted/50 transition-all'
/>
</div>
</div>
{/* Vendor Filters */}
{vendors.length > 0 && (
<div className='sf-filter-row'>
<span className='sf-filter-label'>{t('供应商')}</span>
<div className='sf-filter-pills'>
<button
className={`sf-filter-pill${filterVendor === 'all' ? ' active' : ''}`}
onClick={() => setFilterVendor('all')}
>
{t('全部')}
</button>
{vendors.map((v) => (
<button
key={v}
className={`sf-filter-pill${filterVendor === v ? ' active' : ''}`}
onClick={() => setFilterVendor(filterVendor === v ? 'all' : v)}
>
{v}
</button>
))}
</div>
</div>
)}
{/* Supplier Type Filters */}
{supplierTypes.length > 0 && (
<div className='sf-filter-row'>
<span className='sf-filter-label'>{t('服务商')}</span>
<div className='sf-filter-pills'>
<button
className={`sf-filter-pill${filterSupplierType === 'all' ? ' active' : ''}`}
onClick={() => setFilterSupplierType('all')}
>
{t('全部')}
</button>
{supplierTypes.map((type) => (
<button
key={type}
className={`sf-filter-pill${filterSupplierType === type ? ' active' : ''}`}
onClick={() => setFilterSupplierType(filterSupplierType === type ? 'all' : type)}
>
{type}
</button>
))}
</div>
</div>
)}
</div>
{/* Results Header */}
<div className='sf-results-header'>
<h2 className='sf-results-title'>
{t('为您找到')} <span className='sf-results-count'>{filteredModels?.length || 0}</span> {t('个模型')}
</h2>
</div>
{/* Model Cards Grid */}
<div className='flex-1 overflow-y-auto'>
<div className='sf-cards-container'>
<PricingCardView
filteredModels={filteredModels}
loading={loading}
blurPricing={blurPricing}
t={t}
gridCols={2}
gridCols={isMobile ? 1 : 3}
{...cardProps}
/>
</div>
</>
</div>
);
};

View File

@ -19,7 +19,6 @@ For commercial licensing, please contact support@quantumnous.com
import React, { useContext, useMemo } from 'react';
import { ImagePreview } from '@douyinfe/semi-ui';
import ModelsSidebar from './ModelsSidebar';
import ModelsContent from './ModelsContent';
import ModelDetailSideSheet from '../modal/ModelDetailSideSheet';
import { useModelPricingData } from '../../../../hooks/model-pricing/useModelPricingData';
@ -59,22 +58,12 @@ const PricingPage = () => {
};
return (
<div className='bg-background'>
<div className='models-page-layout'>
{!isMobile && (
<aside className='models-sidebar'>
<ModelsSidebar {...allProps} />
</aside>
)}
<main className='models-content'>
<div className='sf-pricing-page'>
<ModelsContent
{...allProps}
isMobile={isMobile}
sidebarProps={allProps}
/>
</main>
</div>
<ImagePreview
src={pricingData.modalImageUrl}

View File

@ -19,7 +19,6 @@ For commercial licensing, please contact support@quantumnous.com
import React from 'react';
import {
Card,
Empty,
Pagination,
Avatar,
@ -39,13 +38,6 @@ import {
import PricingCardSkeleton from './PricingCardSkeleton';
import { useMinimumLoadingTime } from '../../../../../hooks/common/useMinimumLoadingTime';
import { useIsMobile } from '../../../../../hooks/common/useIsMobile';
const CARD_STYLES = {
container:
'w-12 h-12 rounded-2xl flex items-center justify-center relative shadow-md',
icon: 'w-8 h-8 flex items-center justify-center',
selected: 'border-blue-500 bg-blue-50',
default: 'border-gray-200 hover:border-gray-300',
};
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@ -72,7 +64,7 @@ const PricingCardView = ({
channelVideoRatio = {},
channelVideoCompletionRatio = {},
channelVideoPrice = {},
gridCols = 0, // > 0 使 grid gridCols=2 2
gridCols = 3,
}) => {
const showSkeleton = useMinimumLoadingTime(loading);
const startIndex = (currentPage - 1) * pageSize;
@ -93,9 +85,9 @@ const PricingCardView = ({
<span
key={idx}
style={{
color: '#ef4444',
color: '#1976d2',
fontWeight: 700,
backgroundColor: 'rgba(239, 68, 68, 0.12)',
backgroundColor: 'rgba(25, 118, 210, 0.12)',
borderRadius: 4,
}}
>
@ -119,7 +111,6 @@ const PricingCardView = ({
groupRatio,
);
//
const formatPrice = (priceUSD) => {
const rawDisplayPrice = displayPrice(priceUSD);
const unitDivisor = tokenUnit === 'K' ? 1000 : 1;
@ -141,7 +132,7 @@ const PricingCardView = ({
}
}
return { value: parseFloat(numericPrice.toFixed(2)), symbol };
return { value: parseFloat(numericPrice.toFixed(4)), symbol };
};
const modelHasVideoRatio =
@ -157,7 +148,6 @@ const PricingCardView = ({
model.video_price !== undefined &&
Number.isFinite(Number(model.video_price));
// relay ch.model_ratio
const prices = {
input: [],
output: [],
@ -181,18 +171,6 @@ const PricingCardView = ({
const cid = ch.channel_id;
const mname = model.model_name;
// ============================================================
//
// ch.model_ratio / ch.model_price
// = price_discount_percent / 100
// = markup_discount_rate / 100
//
// = (ch.model_ratio × costDisc + globalMr × markupRate) × 2 × groupRatio
// = (ch.model_ratio × cr × costDisc + globalMr × globalCR × markupRate) × 2 × groupRatio
// = (ch.model_ratio × cacheRatio × costDisc + globalMr × globalCacheR × markupRate) × 2 × groupRatio
// = (ch.model_ratio × createCacheRatio × costDisc + globalMr × globalCreateCacheR × markupRate) × 2 × groupRatio
// = (ch.model_price × costDisc + globalMp × markupRate) × groupRatio
// ============================================================
const priceDiscountPercent =
ch.price_discount_percent != null ? ch.price_discount_percent : 100;
const markupDiscountPercent = ch.markup_discount_rate || 0;
@ -212,7 +190,6 @@ const PricingCardView = ({
originalPrices.videoFlat.push(formatPrice(flatUsd));
}
//
const globalCR = model.completion_ratio || 0;
const globalCacheR =
model.cache_ratio != null ? Number(model.cache_ratio) : 0;
@ -236,7 +213,6 @@ const PricingCardView = ({
globalCreateCacheRatio: globalCreateCacheR,
});
//
if (model.quota_type === 0) {
if (ch.model_ratio !== undefined && ch.model_ratio !== null) {
prices.input.push(
@ -244,7 +220,6 @@ const PricingCardView = ({
);
originalPrices.input.push(formatPrice(billingRates.inputRatioPrice));
// completion_ratio
if (
ch.completion_ratio !== undefined &&
ch.completion_ratio !== null
@ -257,7 +232,6 @@ const PricingCardView = ({
);
}
// cache_ratio
if (
ch.cache_ratio !== undefined &&
ch.cache_ratio !== null
@ -268,7 +242,6 @@ const PricingCardView = ({
originalPrices.cache.push(formatPrice(billingRates.cacheRatioPrice));
}
// create_cache_ratio
if (
ch.create_cache_ratio !== undefined &&
ch.create_cache_ratio !== null
@ -321,9 +294,7 @@ const PricingCardView = ({
);
}
}
}
//
else if (model.quota_type === 1 || ch.quota_type === 1) {
} else if (model.quota_type === 1 || ch.quota_type === 1) {
if (!skipSimpleFixed && ch.model_price !== undefined && ch.model_price !== null) {
prices.fixed.push(
formatPrice(billingRates.effModelPrice * usedGroupRatio),
@ -333,7 +304,6 @@ const PricingCardView = ({
}
});
// channel
const rootPrices = {};
if (model.quota_type === 0) {
if (model.model_ratio !== undefined && model.model_ratio !== null) {
@ -386,7 +356,6 @@ const PricingCardView = ({
rootPrices.videoFlat = formatPrice(Number(model.video_price));
}
// channel 线
const getOriginal = (rootPrice, channelPriceArray) => {
if (!rootPrice || !channelPriceArray || channelPriceArray.length === 0)
return null;
@ -401,7 +370,6 @@ const PricingCardView = ({
return null;
};
//
const calculateRange = (priceArray) => {
if (priceArray.length === 0) return null;
if (priceArray.length === 1) {
@ -467,7 +435,6 @@ const PricingCardView = ({
};
};
// 使 channel
const getModelPriceItemsForCard = (model, priceData) => {
const hint = model.video_flat_clip_hint;
const useTieredVideoFlat =
@ -485,12 +452,10 @@ const PricingCardView = ({
skipSimpleFixed: useTieredImagePerImage,
});
// channel 使
if (!channelPrices) {
return getModelPriceItems(priceData, t, siteDisplayType);
}
// 使 channel
const items = [];
const {
input,
@ -507,7 +472,6 @@ const PricingCardView = ({
quotaType,
} = channelPrices;
//
if (quotaType === 1 && fixed) {
items.push({
key: 'fixed',
@ -518,13 +482,11 @@ const PricingCardView = ({
suffix: fixedSuffix,
original: original?.fixed,
});
}
//
else {
} else {
if (input) {
items.push({
key: 'input',
label: t('输入价格'),
label: t('输入'),
value:
input.single ||
`${input.symbol}${input.min} ~ ${input.symbol}${input.max}`,
@ -536,7 +498,7 @@ const PricingCardView = ({
if (output) {
items.push({
key: 'output',
label: t('输出价格'),
label: t('输出'),
value:
output.single ||
`${output.symbol}${output.min} ~ ${output.symbol}${output.max}`,
@ -548,7 +510,7 @@ const PricingCardView = ({
if (videoToken) {
items.push({
key: 'video-token',
label: t('视频(倍率计价)'),
label: t('视频'),
value:
videoToken.single ||
`${videoToken.symbol}${videoToken.min} ~ ${videoToken.symbol}${videoToken.max}`,
@ -556,33 +518,12 @@ const PricingCardView = ({
original: original?.videoToken,
});
}
// /
// if (cache) {
// items.push({
// key: 'cache',
// label: t(''),
// value: cache.single || `${cache.symbol}${cache.min} ~ ${cache.symbol}${cache.max}`,
// suffix: unitSuffix,
// original: original?.cache,
// });
// }
//
// if (createCache) {
// items.push({
// key: 'create-cache',
// label: t(''),
// value: createCache.single || `${createCache.symbol}${createCache.min} ~ ${createCache.symbol}${createCache.max}`,
// suffix: unitSuffix,
// original: original?.createCache,
// });
// }
}
if (videoFlat) {
items.push({
key: 'video-flat',
label: t('视频按条(固定价)'),
label: t('视频按条'),
value:
videoFlat.single ||
`${videoFlat.symbol}${videoFlat.min} ~ ${videoFlat.symbol}${videoFlat.max}`,
@ -603,7 +544,7 @@ const PricingCardView = ({
key: 'video-flat-tiered',
label: perSecond ? t('按秒') : t('按条'),
valueNode: (
<span className='font-bold text-black'>
<span className='font-bold text-[#1976d2]'>
{t('最低价')}
{displayPrice(usd)}
{perSecond ? t('/秒起') : t('/条起')}
@ -624,7 +565,7 @@ const PricingCardView = ({
key: 'image-per-image-tiered',
label: t('按张'),
valueNode: (
<span className='font-bold text-black'>
<span className='font-bold text-[#1976d2]'>
{t('最低价')}
{displayPrice(usd)}
{t('/张起')}
@ -636,50 +577,47 @@ const PricingCardView = ({
return items;
};
//
const getModelIcon = (model) => {
if (!model || !model.model_name) {
return (
<div className={CARD_STYLES.container}>
<Avatar size='large'>?</Avatar>
<div className='sf-card-icon-wrap'>
<Avatar size='small'>?</Avatar>
</div>
);
}
// 1) 使
if (model.icon) {
return (
<div className={CARD_STYLES.container}>
<div className={CARD_STYLES.icon}>
{getLobeHubIcon(model.icon, 32)}
<div className='sf-card-icon-wrap'>
<div className='sf-card-icon'>
{getLobeHubIcon(model.icon, 28)}
</div>
</div>
);
}
// 2) 退
if (model.vendor_icon) {
return (
<div className={CARD_STYLES.container}>
<div className={CARD_STYLES.icon}>
{getLobeHubIcon(model.vendor_icon, 32)}
<div className='sf-card-icon-wrap'>
<div className='sf-card-icon'>
{getLobeHubIcon(model.vendor_icon, 28)}
</div>
</div>
);
}
// 使
const avatarText =
(model.model_name || '').slice(0, 2).toUpperCase() || 'AI';
return (
<div className={CARD_STYLES.container}>
<div className='sf-card-icon-wrap'>
<Avatar
size='large'
size='small'
style={{
width: 48,
height: 48,
borderRadius: 16,
fontSize: 16,
width: 36,
height: 36,
borderRadius: 10,
fontSize: 13,
fontWeight: 'bold',
backgroundColor: '#e3f2fd',
color: '#1976d2',
}}
>
{avatarText}
@ -688,40 +626,50 @@ const PricingCardView = ({
);
};
//
const getModelDescription = (record) => {
return record.description || '';
};
//
// Get supplier type tag color
const getSupplierTypeTag = (model) => {
const types = new Set();
(model.channel_list || []).forEach((ch) => {
if (ch.supplier_type) types.add(ch.supplier_type);
});
const firstType = [...types][0];
if (!firstType) return null;
return firstType;
};
// Get model tags
const getModelTags = (model) => {
if (!model.tags) return [];
return String(model.tags).split(/[,;|]/).map(s => s.trim()).filter(Boolean);
};
if (showSkeleton) {
return (
<>
<PricingCardSkeleton />
</>
);
return <PricingCardSkeleton />;
}
if (!filteredModels || filteredModels.length === 0) {
return (
<>
<div className='flex justify-center items-center py-20'>
<Empty
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
darkModeImage={
<IllustrationNoResultDark style={{ width: 150, height: 150 }} />
}
description={t('搜索无结果')}
description={
<span style={{ color: '#90a4ae' }}>{t('搜索无结果')}</span>
}
/>
</div>
</>
);
}
return (
<>
<div className={gridCols > 0 ? '' : 'px-2 pt-2'}>
<div className={gridCols > 0 ? 'grid gap-4' : 'flex flex-wrap gap-4'} style={gridCols > 0 ? { gridTemplateColumns: `repeat(${gridCols}, 1fr)` } : undefined}>
<div className='sf-card-grid' style={{ gridTemplateColumns: `repeat(${gridCols}, 1fr)` }}>
{paginatedModels.map((model, index) => {
const modelKey = getModelKey(model);
@ -737,88 +685,80 @@ const PricingCardView = ({
quotaDisplayType: siteDisplayType,
});
const supplierType = getSupplierTypeTag(model);
const tags = getModelTags(model);
return (
<Card
<div
key={modelKey || index}
className={`${gridCols > 0 ? '!w-full' : 'flex-1 min-w-[350px] max-w-[600px]'} !rounded-2xl transition-all duration-200 hover:shadow-lg border ${blurPricing ? '' : 'cursor-pointer'} ${CARD_STYLES.default}`}
bodyStyle={{ height: '100%' }}
className={`sf-model-card group ${blurPricing ? '' : 'cursor-pointer'}`}
onClick={() =>
!blurPricing && openModelDetail && openModelDetail(model)
}
>
<div className='flex flex-col h-full'>
{/* 头部:图标 + 模型名称 */}
<div className='flex items-start mb-3'>
<div className='flex items-start space-x-3 flex-1 min-w-0'>
<div className='sf-shimmer-overlay' />
{/* Card Header: Provider + Tag */}
<div className='sf-card-header'>
<div className='sf-card-provider'>
{getModelIcon(model)}
<div className='flex-1 min-w-0'>
<h3 className='text-lg font-bold text-gray-900 truncate'>
<span className='sf-card-provider-name'>
{model.vendor_name || model.model_name?.split('/')[0] || 'AI'}
</span>
</div>
{supplierType && (
<span className='sf-card-type-tag'>{supplierType}</span>
)}
</div>
{/* Default View: Model Name + Tags + Pricing */}
<div className='sf-card-default-view'>
<h3 className='sf-card-model-name'>
{renderHighlightedText(model.model_name)}
</h3>
</div>
</div>
</div>
{/* 模型描述 */}
<div
className='mb-4'
style={
blurPricing
? {
filter: 'blur(6px)',
userSelect: 'none',
pointerEvents: 'none',
}
: undefined
}
>
<p
className='text-xs line-clamp-2 leading-relaxed'
style={{ color: 'var(--semi-color-text-2)' }}
>
{renderHighlightedText(getModelDescription(model))}
</p>
{/* Tags */}
{tags.length > 0 && (
<div className='sf-card-tags'>
{tags.slice(0, 3).map((tag) => (
<span key={tag} className='sf-card-tag'>{tag}</span>
))}
</div>
)}
{/* 价格:输入 + 输出并排 */}
{/* Pricing */}
<div
className='mt-auto'
style={
blurPricing
? {
filter: 'blur(6px)',
userSelect: 'none',
pointerEvents: 'none',
}
: undefined
}
className='sf-card-pricing'
style={blurPricing ? { filter: 'blur(6px)', userSelect: 'none', pointerEvents: 'none' } : undefined}
>
{(() => {
const items = getModelPriceItemsForCard(model, priceData);
if (items.length === 0) return null;
return (
<div className='flex flex-wrap gap-3'>
{items.map(item => (
<div key={item.key} className='flex-1 rounded-xl px-3 py-2.5' style={{backgroundColor: 'var(--semi-color-fill-0)', minWidth: '120px'}}>
<div className='text-[10px] mb-0.5' style={{color: 'var(--semi-color-text-2)'}}>{item.label}</div>
<div className='text-sm font-bold text-gray-900'>
return items.slice(0, 2).map(item => (
<div key={item.key} className='sf-price-item'>
<span className='sf-price-label'>{item.label}</span>
<span className='sf-price-value'>
{item.valueNode || (item.value + (item.suffix || ''))}
</span>
</div>
</div>
))}
</div>
);
));
})()}
</div>
</div>
</Card>
{/* Hover View: Description */}
<div className='sf-card-hover-view'>
<p className='sf-card-description'>
{getModelDescription(model) || t('暂无描述')}
</p>
</div>
</div>
);
})}
</div>
{/* 分页 */}
{/* Pagination */}
{filteredModels.length > 0 && (
<div className='flex justify-center mt-6 py-4 border-t pricing-pagination-divider'>
<div className='sf-pagination'>
<Pagination
currentPage={currentPage}
pageSize={pageSize}
@ -835,7 +775,6 @@ const PricingCardView = ({
/>
</div>
)}
</div>
</>
);
};

View File

@ -98,7 +98,7 @@ const PricingTable = ({
const ModelTable = useMemo(
() => (
<Card className='!rounded-xl overflow-hidden' bordered={false}>
<Card className='!rounded-xl overflow-hidden !border-[#e3f2fd]' bordered>
<Table
columns={processedColumns}
dataSource={filteredModels}
@ -112,13 +112,13 @@ const PricingTable = ({
})}
empty={
<div className='flex flex-col items-center justify-center px-6 py-16 text-center'>
<div className='mb-3 flex h-12 w-12 items-center justify-center rounded-2xl bg-slate-100 text-slate-400 dark:bg-white/5 dark:text-white/40'>
<div className='mb-3 flex h-12 w-12 items-center justify-center rounded-2xl bg-[#e3f2fd] text-[#90a4ae]'>
<SearchX size={22} strokeWidth={1.5} />
</div>
<div className='text-sm font-medium text-slate-700 dark:text-white/80'>
<div className='text-sm font-medium text-[#374151]'>
{t('搜索无结果')}
</div>
<div className='mt-1 text-xs text-slate-400 dark:text-white/40'>
<div className='mt-1 text-xs text-[#90a4ae]'>
{t('试试调整筛选条件或换个关键词')}
</div>
</div>

View File

@ -37,13 +37,13 @@ function renderQuotaType(type, t) {
switch (type) {
case 1:
return (
<Tag color='teal' shape='circle'>
<Tag color='blue' shape='circle'>
{t('按次计费')}
</Tag>
);
case 0:
return (
<Tag color='violet' shape='circle'>
<Tag color='cyan' shape='circle'>
{t('按量计费')}
</Tag>
);
@ -201,7 +201,7 @@ export const getPricingTableColumns = ({
<span>{t('倍率')}</span>
<Tooltip content={t('倍率是为了方便换算不同价格的模型')}>
<IconHelpCircle
className='text-blue-500 cursor-pointer'
className='text-[#1976d2] cursor-pointer'
onClick={() => {
setModalImageUrl('/ratio.png');
setIsModalOpenurl(true);
@ -217,17 +217,17 @@ export const getPricingTableColumns = ({
return (
<div className='space-y-1'>
<div className='text-gray-700'>
<div className='text-[#374151]'>
{t('模型倍率')}
{record.quota_type === 0
? (priceData?.inputRatio ?? text)
: t('无')}
</div>
<div className='text-gray-700'>
<div className='text-[#374151]'>
{t('输出倍率')}
{record.quota_type === 0 ? completionRatio : t('无')}
</div>
<div className='text-gray-700'>
<div className='text-[#374151]'>
{t('分组倍率')}{priceData?.usedGroupRatio ?? '-'}
</div>
</div>
@ -246,7 +246,7 @@ export const getPricingTableColumns = ({
return (
<div className='space-y-1'>
{priceItems.map((item) => (
<div key={item.key} className='text-gray-700'>
<div key={item.key} className='text-[#374151]'>
{item.label} {item.value}
{item.suffix}
</div>

865
web/src/index.css vendored
View File

@ -2,100 +2,104 @@
/* 这些变量叠加在 Semi Design 之上,供新的 shadcn 风格组件使用 */
:root {
/* shadcn/ui 基础色板 — Slate 中性色调 (220° hue) */
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--foreground: 222 47% 11%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--card-foreground: 222 47% 11%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 220 5% 96%;
--secondary-foreground: 220 6% 10%;
--muted: 220 5% 96%;
--muted-foreground: 220 4% 56%;
--accent: 220 5% 96%;
--accent-foreground: 220 6% 10%;
--popover-foreground: 222 47% 11%;
--primary: 221 83% 53%;
--primary-foreground: 0 0% 100%;
--secondary: 210 40% 96%;
--secondary-foreground: 222 47% 11%;
--muted: 210 40% 96%;
--muted-foreground: 215 16% 47%;
--accent: 221 83% 53%;
--accent-foreground: 0 0% 100%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 220 13% 91%;
--input: 220 13% 91%;
--ring: 0 0% 9%;
--border: 214 32% 91%;
--input: 214 32% 91%;
--ring: 221 83% 53%;
--radius: 0.5rem;
--link: 0 0% 9%;
--link-hover: 0 0% 20%;
--chart-1: 160 60% 45%;
--chart-2: 217 91% 60%;
--link: 221 83% 53%;
--link-hover: 221 83% 40%;
--chart-1: 221 83% 53%;
--chart-2: 160 60% 45%;
--chart-3: 43 96% 56%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
.dark {
--background: 0 0% 5.5%;
--foreground: 0 0% 90%;
--card: 0 0% 10%;
--card-foreground: 0 0% 90%;
--popover: 0 0% 8%;
--popover-foreground: 0 0% 90%;
--primary: 236 52% 62%;
--background: 210 60% 97%;
--foreground: 222 47% 11%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 217 91% 60%;
--primary-foreground: 0 0% 100%;
--secondary: 0 0% 13%;
--secondary-foreground: 0 0% 90%;
--muted: 0 0% 9%;
--muted-foreground: 0 0% 56%;
--accent: 236 52% 62%;
--secondary: 210 40% 93%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 93%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 217 91% 60%;
--accent-foreground: 0 0% 100%;
--destructive: 0 62% 45%;
--destructive-foreground: 0 0% 100%;
--border: 0 0% 100% / 0.08;
--input: 0 0% 100% / 0.06;
--ring: 236 52% 62%;
--link: 236 52% 72%;
--link-hover: 236 52% 62%;
--border: 214.3 31.8% 85%;
--input: 214.3 31.8% 85%;
--ring: 217 91% 60%;
--link: 217 91% 60%;
--link-hover: 217 91% 50%;
--radius: 0.375rem;
--chart-1: 236 52% 62%;
--chart-1: 217 91% 60%;
--chart-2: 160 60% 50%;
--chart-3: 43 96% 56%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
/* === Linear Dark: Semi Design 暗色覆盖 === */
.dark {
--semi-color-primary: rgba(94, 106, 210, 1);
--semi-color-primary-hover: rgba(114, 126, 230, 1);
--semi-color-primary-active: rgba(78, 90, 190, 1);
--semi-color-primary-disabled: rgba(94, 106, 210, 0.35);
--semi-color-primary-light-default: rgba(94, 106, 210, 0.12);
--semi-color-primary-light-hover: rgba(94, 106, 210, 0.18);
--semi-color-primary-light-active: rgba(94, 106, 210, 0.24);
--semi-color-bg-0: rgba(14, 14, 14, 1);
--semi-color-bg-1: rgba(19, 19, 19, 1);
--semi-color-bg-2: rgba(25, 25, 25, 1);
--semi-color-bg-3: rgba(32, 32, 32, 1);
--semi-color-bg-4: rgba(40, 40, 40, 1);
--semi-color-text-0: rgba(230, 230, 230, 1);
--semi-color-text-1: rgba(190, 190, 190, 1);
--semi-color-text-2: rgba(140, 140, 140, 1);
--semi-color-text-3: rgba(100, 100, 100, 1);
--semi-color-border: rgba(255, 255, 255, 0.08);
--semi-color-fill-0: rgba(255, 255, 255, 0.04);
--semi-color-fill-1: rgba(255, 255, 255, 0.06);
--semi-color-fill-2: rgba(255, 255, 255, 0.08);
--semi-color-link: rgba(130, 143, 255, 1);
--semi-color-link-hover: rgba(158, 168, 255, 1);
--semi-color-link-active: rgba(110, 122, 235, 1);
--semi-color-link-visited: rgba(130, 143, 255, 0.8);
--semi-color-tertiary: rgba(255, 255, 255, 0.06);
--semi-color-tertiary-hover: rgba(255, 255, 255, 0.10);
--semi-color-tertiary-active: rgba(255, 255, 255, 0.14);
--semi-shadow-elevated: 0 0 0 1px rgba(255,255,255,0.06), 0 2px 8px rgba(0,0,0,0.3);
/* === Linear Dark: Semi Design 暗色覆盖 — 蓝白渐变主题 === */
body[theme-mode='dark'] {
--semi-color-primary: rgba(59, 130, 246, 1);
--semi-color-primary-hover: rgba(79, 150, 255, 1);
--semi-color-primary-active: rgba(37, 99, 235, 1);
--semi-color-primary-disabled: rgba(59, 130, 246, 0.35);
--semi-color-primary-light-default: rgba(59, 130, 246, 0.12);
--semi-color-primary-light-hover: rgba(59, 130, 246, 0.18);
--semi-color-primary-light-active: rgba(59, 130, 246, 0.24);
--semi-color-bg-0: rgba(240, 247, 255, 1);
--semi-color-bg-1: rgba(235, 244, 255, 1);
--semi-color-bg-2: rgba(224, 240, 253, 1);
--semi-color-bg-3: rgba(214, 232, 248, 1);
--semi-color-bg-4: rgba(204, 224, 244, 1);
--semi-color-nav-bg: rgba(240, 247, 255, 1);
--semi-color-text-0: rgba(15, 23, 42, 1);
--semi-color-text-1: rgba(51, 65, 85, 1);
--semi-color-text-2: rgba(100, 116, 139, 1);
--semi-color-text-3: rgba(148, 163, 184, 1);
--semi-color-border: rgba(191, 219, 254, 1);
--semi-color-fill-0: rgba(59, 130, 246, 0.04);
--semi-color-fill-1: rgba(59, 130, 246, 0.06);
--semi-color-fill-2: rgba(59, 130, 246, 0.08);
--semi-color-link: rgba(59, 130, 246, 1);
--semi-color-link-hover: rgba(37, 99, 235, 1);
--semi-color-link-active: rgba(29, 78, 216, 1);
--semi-color-link-visited: rgba(59, 130, 246, 0.8);
--semi-color-tertiary: rgba(59, 130, 246, 0.06);
--semi-color-tertiary-hover: rgba(59, 130, 246, 0.10);
--semi-color-tertiary-active: rgba(59, 130, 246, 0.14);
--semi-color-overlay-bg: rgba(22, 22, 26, .4);
--semi-shadow-elevated: 0 0 0 1px rgba(0,0,0,0.06), 0 2px 8px rgba(0,0,0,0.1);
--semi-color-success: rgba(52, 211, 153, 1);
--semi-color-danger: rgba(248, 113, 113, 1);
--semi-color-warning: rgba(251, 191, 36, 1);
--semi-color-info: rgba(94, 106, 210, 1);
--semi-color-info: rgba(59, 130, 246, 1);
--semi-color-default: rgba(59, 130, 246, 0.04);
--semi-color-default-hover: rgba(59, 130, 246, 0.06);
--semi-color-default-active: rgba(59, 130, 246, 0.08);
}
/* 确保按钮圆角在暗色模式下生效(覆盖 Semi 库默认值) */
@ -108,17 +112,25 @@
border-radius: 8px !important;
}
/* 全局基础样式 — 使用新字体族 */
/* 全局基础样式 — 蓝白渐变主题 */
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
color: hsl(var(--foreground));
background-color: hsl(var(--background));
background: linear-gradient(135deg, #e0f0ff 0%, #edf5ff 20%, #f5faff 40%, #ffffff 60%, #f0f7ff 80%, #e8f4fd 100%);
background-attachment: fixed;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ==================== Tailwind CSS 配置 ==================== */
/* 暗色模式 body 背景也使用蓝白渐变 */
html.dark body {
background: linear-gradient(135deg, #e0f0ff 0%, #edf5ff 20%, #f5faff 40%, #ffffff 60%, #f0f7ff 80%, #e8f4fd 100%);
background-attachment: fixed;
}
/* ==================== Tailwind CSS 閰嶇疆 ==================== */
@layer tailwind-base, semi, tailwind-components, tailwind-utils;
@ -1610,183 +1622,598 @@ html.dark .with-pastel-balls::before {
padding-top: 0.75rem !important;
}
/* ==================== 妯″瀷瀹氫环椤甸潰甯冨眬 ==================== */
.pricing-layout {
height: calc(100vh - 60px);
overflow: hidden;
margin-top: 60px;
/* ==================== SiliconFlow-style Pricing Page (Cool Effects) ==================== */
/* ─── Keyframe Animations ─── */
@keyframes sf-gradient-shift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.pricing-sidebar {
width: clamp(280px, 24vw, 520px) !important;
min-width: clamp(280px, 24vw, 520px) !important;
max-width: clamp(280px, 24vw, 520px) !important;
height: calc(100vh - 60px);
background-color: var(--semi-color-bg-0);
overflow: auto;
@keyframes sf-float {
0%, 100% { transform: translateY(0) translateX(0); }
25% { transform: translateY(-20px) translateX(10px); }
50% { transform: translateY(-10px) translateX(-5px); }
75% { transform: translateY(-25px) translateX(8px); }
}
.pricing-content {
height: calc(100vh - 60px);
background-color: var(--semi-color-bg-0);
display: flex;
flex-direction: column;
@keyframes sf-pulse-glow {
0%, 100% { box-shadow: 0 0 20px rgba(25, 118, 210, 0.1); }
50% { box-shadow: 0 0 40px rgba(25, 118, 210, 0.25); }
}
.pricing-pagination-divider {
border-color: var(--semi-color-border);
@keyframes sf-card-in {
from { opacity: 0; transform: translateY(30px) scale(0.96); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.pricing-content-mobile {
height: 100%;
display: flex;
flex-direction: column;
overflow: auto;
@keyframes sf-shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.pricing-search-header {
padding: 0.5rem;
background-color: var(--semi-color-bg-0);
flex-shrink: 0;
position: sticky;
@keyframes sf-glow-ring {
0%, 100% { opacity: 0.3; transform: scale(1); }
50% { opacity: 0.8; transform: scale(1.02); }
}
@keyframes sf-fade-up {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes sf-search-glow {
0%, 100% { box-shadow: 0 0 0 0 rgba(25, 118, 210, 0); }
50% { box-shadow: 0 0 0 8px rgba(25, 118, 210, 0.08); }
}
@keyframes sf-dot-float {
0% { transform: translateY(100vh) scale(0); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translateY(-10vh) scale(1); opacity: 0; }
}
@keyframes sf-title-reveal {
from { opacity: 0; transform: translateY(30px); filter: blur(8px); }
to { opacity: 1; transform: translateY(0); filter: blur(0); }
}
@keyframes sf-price-glow {
0%, 100% { text-shadow: none; }
50% { text-shadow: 0 0 8px rgba(25, 118, 210, 0.3); }
}
/* Page Container */
.sf-pricing-page {
min-height: 100vh;
background: #f7f9fc;
position: relative;
overflow-x: hidden;
}
/* ─── Floating Particles Background ─── */
.sf-particles {
position: fixed;
top: 0;
z-index: 5;
}
.pricing-view-container {
flex: 1;
overflow: auto;
}
.pricing-view-container-mobile {
flex: 1;
overflow: auto;
min-height: 0;
}
/* ─── Models 新布局 ─── */
.models-page-layout {
display: flex;
max-width: 1440px;
margin: 0 auto;
padding: 56px 24px 0;
gap: 32px;
height: 100vh;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 0;
overflow: hidden;
}
.sf-particle {
position: absolute;
width: 4px;
height: 4px;
border-radius: 50%;
background: rgba(25, 118, 210, 0.15);
animation: sf-dot-float linear infinite;
}
.sf-particle:nth-child(1) { left: 10%; animation-duration: 12s; animation-delay: 0s; width: 6px; height: 6px; background: rgba(25, 118, 210, 0.1); }
.sf-particle:nth-child(2) { left: 25%; animation-duration: 15s; animation-delay: 2s; width: 4px; height: 4px; }
.sf-particle:nth-child(3) { left: 40%; animation-duration: 10s; animation-delay: 4s; width: 8px; height: 8px; background: rgba(66, 165, 245, 0.12); }
.sf-particle:nth-child(4) { left: 55%; animation-duration: 18s; animation-delay: 1s; width: 3px; height: 3px; }
.sf-particle:nth-child(5) { left: 70%; animation-duration: 13s; animation-delay: 3s; width: 5px; height: 5px; background: rgba(21, 101, 192, 0.1); }
.sf-particle:nth-child(6) { left: 85%; animation-duration: 16s; animation-delay: 5s; width: 4px; height: 4px; }
.sf-particle:nth-child(7) { left: 15%; animation-duration: 14s; animation-delay: 6s; width: 7px; height: 7px; background: rgba(25, 118, 210, 0.08); }
.sf-particle:nth-child(8) { left: 60%; animation-duration: 11s; animation-delay: 7s; width: 3px; height: 3px; }
.sf-pricing-inner {
max-width: 1400px;
margin: 0 auto;
padding: 0 32px 60px;
position: relative;
z-index: 1;
}
/* ─── Hero Section ─── */
.sf-hero {
text-align: center;
padding: 70px 0 50px;
position: relative;
}
.sf-hero::before {
content: '';
position: absolute;
top: -100px;
left: 50%;
transform: translateX(-50%);
width: 800px;
height: 500px;
background: radial-gradient(ellipse, rgba(25, 118, 210, 0.06) 0%, transparent 70%);
pointer-events: none;
animation: sf-pulse-glow 6s ease-in-out infinite;
}
.sf-hero-title {
font-size: 48px;
font-weight: 800;
margin: 0 0 16px;
line-height: 1.2;
background: linear-gradient(135deg, #0d47a1 0%, #1976d2 25%, #42a5f5 50%, #1976d2 75%, #0d47a1 100%);
background-size: 200% 200%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: sf-gradient-shift 4s ease infinite, sf-title-reveal 0.8s ease-out;
text-shadow: none;
}
.sf-hero-subtitle {
font-size: 17px;
color: #546e7a;
margin: 0 0 36px;
animation: sf-fade-up 0.8s ease-out 0.2s both;
}
/* ─── Search Bar ─── */
.sf-search-bar {
position: relative;
max-width: 620px;
margin: 0 auto 28px;
animation: sf-fade-up 0.8s ease-out 0.4s both;
}
.sf-search-icon {
position: absolute;
left: 18px;
top: 50%;
transform: translateY(-50%);
color: #90a4ae;
transition: color 0.3s;
}
.sf-search-input {
width: 100%;
height: 52px;
padding: 0 18px 0 48px;
border: 2px solid #e3f2fd;
border-radius: 14px;
background: #fff;
font-size: 15px;
color: #0a1929;
outline: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 16px rgba(25, 118, 210, 0.06);
box-sizing: border-box;
}
.models-sidebar {
width: 280px;
flex-shrink: 0;
overflow-y: auto;
padding: 0 16px 24px 0;
border-right: 1px solid var(--border-color, #e5e7eb);
.sf-search-input::placeholder {
color: #b0bec5;
transition: color 0.3s;
}
.sf-search-input:focus {
border-color: #1976d2;
box-shadow: 0 4px 24px rgba(25, 118, 210, 0.18), 0 0 0 4px rgba(25, 118, 210, 0.06);
animation: sf-search-glow 2s ease-in-out infinite;
}
.sf-search-input:focus ~ .sf-search-icon {
color: #1976d2;
}
.models-sidebar::-webkit-scrollbar {
width: 4px;
}
.models-sidebar::-webkit-scrollbar-thumb {
background: transparent;
}
.models-sidebar:hover::-webkit-scrollbar-thumb {
background: rgba(0,0,0,0.15);
border-radius: 2px;
/* ─── Hot Tags ─── */
.sf-hot-tags {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
animation: sf-fade-up 0.8s ease-out 0.6s both;
}
.models-content {
flex: 1;
min-width: 0;
.sf-hot-label {
font-size: 13px;
font-weight: 600;
color: #546e7a;
}
.sf-hot-tag {
padding: 5px 14px;
border: 1px solid #e3f2fd;
border-radius: 20px;
background: #fff;
font-size: 12px;
color: #374151;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.sf-hot-tag::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(25, 118, 210, 0.08), rgba(66, 165, 245, 0.08));
opacity: 0;
transition: opacity 0.25s;
}
.sf-hot-tag:hover {
border-color: #1976d2;
color: #1976d2;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.15);
}
.sf-hot-tag:hover::before {
opacity: 1;
}
/* ─── Filter Section ─── */
.sf-filter-section {
padding: 24px 0;
display: flex;
flex-direction: column;
gap: 16px;
animation: sf-fade-up 0.8s ease-out 0.7s both;
}
.sf-filter-row {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.sf-filter-label {
font-size: 14px;
font-weight: 600;
color: #374151;
min-width: 70px;
flex-shrink: 0;
}
.sf-filter-pills {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.sf-filter-pill {
padding: 7px 18px;
border: 1.5px solid #e3f2fd;
border-radius: 10px;
background: #fff;
font-size: 13px;
color: #546e7a;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.sf-filter-pill:hover {
border-color: #1976d2;
color: #1976d2;
transform: translateY(-1px);
box-shadow: 0 3px 10px rgba(25, 118, 210, 0.1);
}
.sf-filter-pill.active {
border-color: #1976d2;
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
color: #1976d2;
font-weight: 600;
box-shadow: 0 2px 12px rgba(25, 118, 210, 0.2);
}
/* ─── Results Header ─── */
.sf-results-header {
padding: 16px 0 8px;
animation: sf-fade-up 0.8s ease-out 0.8s both;
}
.sf-results-title {
font-size: 22px;
font-weight: 700;
color: #0a1929;
margin: 0;
}
.sf-results-count {
color: #1976d2;
font-weight: 800;
animation: sf-price-glow 3s ease-in-out infinite;
}
/* ─── Card Grid ─── */
.sf-card-grid {
display: grid;
gap: 22px;
padding: 20px 0;
}
/* ─── Model Card ─── */
.sf-model-card {
position: relative;
background: #fff;
border: 1.5px solid #e3f2fd;
border-radius: 16px;
padding: 24px;
height: 230px;
display: flex;
flex-direction: column;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
cursor: pointer;
animation: sf-card-in 0.6s ease-out both;
/* Staggered entrance */
}
.sf-model-card:nth-child(1) { animation-delay: 0.1s; }
.sf-model-card:nth-child(2) { animation-delay: 0.15s; }
.sf-model-card:nth-child(3) { animation-delay: 0.2s; }
.sf-model-card:nth-child(4) { animation-delay: 0.25s; }
.sf-model-card:nth-child(5) { animation-delay: 0.3s; }
.sf-model-card:nth-child(6) { animation-delay: 0.35s; }
.sf-model-card:nth-child(7) { animation-delay: 0.4s; }
.sf-model-card:nth-child(8) { animation-delay: 0.45s; }
.sf-model-card:nth-child(9) { animation-delay: 0.5s; }
/* Glow border on hover */
.sf-model-card::before {
content: '';
position: absolute;
inset: -2px;
border-radius: 18px;
background: linear-gradient(135deg, #1976d2, #42a5f5, #1976d2);
opacity: 0;
z-index: -1;
transition: opacity 0.4s ease;
}
.sf-model-card::after {
content: '';
position: absolute;
inset: 0;
border-radius: 16px;
background: #fff;
z-index: -1;
}
.sf-model-card:hover {
transform: translateY(-6px) scale(1.01);
border-color: transparent;
box-shadow: 0 12px 40px rgba(25, 118, 210, 0.2), 0 0 60px rgba(25, 118, 210, 0.06);
}
.sf-model-card:hover::before {
opacity: 1;
animation: sf-glow-ring 2s ease-in-out infinite;
}
/* Shimmer effect on hover */
.sf-model-card .sf-shimmer-overlay {
position: absolute;
inset: 0;
border-radius: 16px;
background: linear-gradient(90deg, transparent 0%, rgba(25, 118, 210, 0.04) 50%, transparent 100%);
background-size: 200% 100%;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.sf-model-card:hover .sf-shimmer-overlay {
opacity: 1;
animation: sf-shimmer 2s linear infinite;
}
/* Card Header */
.sf-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.sf-card-provider {
display: flex;
align-items: center;
gap: 10px;
}
.sf-card-icon-wrap {
width: 40px;
height: 40px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
flex-shrink: 0;
transition: all 0.3s;
}
.sf-model-card:hover .sf-card-icon-wrap {
transform: scale(1.08);
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.2);
}
.sf-card-icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
}
.sf-card-provider-name {
font-size: 13px;
color: #546e7a;
font-weight: 500;
}
.sf-card-type-tag {
font-size: 11px;
padding: 3px 10px;
border-radius: 6px;
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
color: #1976d2;
font-weight: 600;
letter-spacing: 0.02em;
}
/* Default View */
.sf-card-default-view {
flex: 1;
display: flex;
flex-direction: column;
transition: opacity 0.4s ease;
}
.sf-card-model-name {
font-size: 17px;
font-weight: 700;
color: #0a1929;
margin: 0 0 10px;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.3s;
}
.sf-model-card:hover .sf-card-model-name {
color: #1976d2;
}
.sf-card-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.sf-card-tag {
font-size: 11px;
padding: 3px 10px;
border-radius: 6px;
background: #f0f7ff;
color: #1976d2;
font-weight: 500;
transition: all 0.25s;
}
.sf-model-card:hover .sf-card-tag {
background: #e3f2fd;
}
/* Pricing */
.sf-card-pricing {
margin-top: auto;
display: flex;
gap: 16px;
}
.sf-price-item {
display: flex;
flex-direction: column;
gap: 3px;
}
.sf-price-label {
font-size: 11px;
color: #90a4ae;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.sf-price-value {
font-size: 15px;
font-weight: 700;
color: #1976d2;
transition: all 0.3s;
}
.sf-model-card:hover .sf-price-value {
animation: sf-price-glow 2s ease-in-out infinite;
}
/* Hover View */
.sf-card-hover-view {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 24px;
background: linear-gradient(transparent, rgba(255,255,255,0.97) 15%);
opacity: 0;
transform: translateY(12px);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
}
.sf-model-card:hover .sf-card-hover-view {
opacity: 1;
transform: translateY(0);
}
.sf-model-card:hover .sf-card-default-view {
opacity: 0.2;
}
.sf-card-description {
font-size: 13px;
color: #546e7a;
line-height: 1.6;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.models-tabs-bar {
/* ─── Pagination ─── */
.sf-pagination {
display: flex;
gap: 8px;
padding: 16px 0;
flex-shrink: 0;
border-bottom: 1px solid var(--border-color, #e5e7eb);
justify-content: center;
padding: 36px 0 16px;
}
.models-tab-btn {
padding: 6px 20px;
border-radius: 20px;
border: 1px solid var(--border-color, #d1d5db);
background: transparent;
cursor: pointer;
/* ─── Responsive ─── */
@media (max-width: 1024px) {
.sf-pricing-inner {
padding: 0 20px 40px;
}
.sf-hero-title {
font-size: 36px;
}
}
@media (max-width: 768px) {
.sf-pricing-inner {
padding: 0 16px 32px;
}
.sf-hero {
padding: 40px 0 24px;
}
.sf-hero-title {
font-size: 28px;
}
.sf-hero-subtitle {
font-size: 14px;
color: var(--text-muted, #6b7280);
transition: all 0.2s;
white-space: nowrap;
}
.models-tab-btn:hover {
border-color: var(--primary-color, #3b82f6);
color: var(--primary-color, #3b82f6);
}
.models-tab-btn.active {
background: var(--primary-color, #3b82f6);
color: white;
border-color: var(--primary-color, #3b82f6);
}
.models-card-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
padding: 16px 0;
overflow-y: auto;
flex: 1;
}
.models-card-grid::-webkit-scrollbar {
width: 6px;
}
.models-card-grid::-webkit-scrollbar-thumb {
background: rgba(0,0,0,0.12);
border-radius: 3px;
}
/* 筛选面板标题 */
.models-filter-section {
margin-bottom: 24px;
}
.models-filter-title {
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted, #6b7280);
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-color, #e5e7eb);
}
.models-filter-item {
display: flex;
align-items: center;
padding: 6px 10px;
border-radius: 8px;
cursor: pointer;
transition: all 0.15s;
font-size: 14px;
color: var(--text-color, #374151);
}
.sf-filter-row {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.models-filter-item:hover {
background: var(--hover-bg, #f3f4f6);
}
.models-filter-item.active {
background: var(--primary-light, #eff6ff);
color: var(--primary-color, #3b82f6);
font-weight: 500;
}
.models-filter-item .count {
margin-left: auto;
font-size: 12px;
color: var(--text-muted, #9ca3af);
}
.sf-model-card {
height: auto;
min-height: 180px;
}
.sf-particles {
display: none;
}
}
/* ==================== semi-ui 缁勪欢鑷畾涔夋牱寮?==================== */

File diff suppressed because it is too large Load Diff