定价页重构:SiliconFlow风格卡片布局 + 炫酷动画效果
- 移除侧边栏,改为全宽布局,顶部Hero区域(渐变标题+搜索框+热门标签) - 新增横向筛选pill按钮(模型类型/供应商/服务商) - 卡片网格3列响应式布局,悬停发光+上浮+微缩放效果 - 新增浮动粒子背景、卡片入场逐个动画、搜索框光晕脉冲 - 新增流光扫过效果、价格数字脉冲发光 - 新增DeepSeek-V4/MiniMax模型默认倍率 - 更新setup.ps1支持DeepSeek+Minimax双供应商配置
This commit is contained in:
parent
8df9865ff5
commit
5a68d88fa9
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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)" }
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
.sf-model-card {
|
||||
height: auto;
|
||||
min-height: 180px;
|
||||
}
|
||||
.models-filter-item.active {
|
||||
background: var(--primary-light, #eff6ff);
|
||||
color: var(--primary-color, #3b82f6);
|
||||
font-weight: 500;
|
||||
.sf-particles {
|
||||
display: none;
|
||||
}
|
||||
.models-filter-item .count {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted, #9ca3af);
|
||||
}
|
||||
|
||||
/* ==================== semi-ui 缁勪欢鑷畾涔夋牱寮?==================== */
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue