定价页重构: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-0324": 0.8,
"deepseek-ai/DeepSeek-V3.1": 0.8, "deepseek-ai/DeepSeek-V3.1": 0.8,
"DeepSeek-V3.2": 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{ 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 published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version. 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 For commercial licensing, please contact support@quantumnous.com
*/ */
@ -13,15 +21,6 @@ import React from 'react';
import { Search } from 'lucide-react'; import { Search } from 'lucide-react';
import PricingCardView from '../view/card/PricingCardView'; import PricingCardView from '../view/card/PricingCardView';
// Tab tag
const TAB_TO_TAG = {
all: 'all',
text: '文本',
image: '图片',
audio: '音频',
video: '视频',
};
const TYPE_TABS = [ const TYPE_TABS = [
{ key: 'all', labelKey: '全部' }, { key: 'all', labelKey: '全部' },
{ key: 'text', labelKey: '文本' }, { key: 'text', labelKey: '文本' },
@ -30,9 +29,20 @@ const TYPE_TABS = [
{ key: 'video', labelKey: '视频' }, { key: 'video', labelKey: '视频' },
]; ];
const TAB_TO_TAG = {
all: 'all',
text: '文本',
image: '图片',
audio: '音频',
video: '视频',
};
const ModelsContent = ({ const ModelsContent = ({
filteredModels, filteredModels,
models,
filterTag, setFilterTag, filterTag, setFilterTag,
filterVendor, setFilterVendor,
filterSupplierType, setFilterSupplierType,
searchValue, setSearchValue, searchValue, setSearchValue,
loading, loading,
isMobile, isMobile,
@ -40,48 +50,161 @@ const ModelsContent = ({
t, t,
...cardProps ...cardProps
}) => { }) => {
// filterTag tab key
const activeTabKey = filterTag === 'all' ? 'all' const activeTabKey = filterTag === 'all' ? 'all'
: (Object.entries(TAB_TO_TAG).find(([, v]) => v === filterTag)?.[0] || '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 ( return (
<> <div className='sf-pricing-inner'>
{/* Type Tabs */} {/* Floating Particles */}
<div className='models-tabs-bar'> <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) => ( {TYPE_TABS.map((tab) => (
<button <button
key={tab.key} 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])} onClick={() => setFilterTag(TAB_TO_TAG[tab.key])}
> >
{t(tab.labelKey)} {t(tab.labelKey)}
</button> </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>
</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 */} {/* Model Cards Grid */}
<div className='flex-1 overflow-y-auto'> <div className='sf-cards-container'>
<PricingCardView <PricingCardView
filteredModels={filteredModels} filteredModels={filteredModels}
loading={loading} loading={loading}
blurPricing={blurPricing} blurPricing={blurPricing}
t={t} t={t}
gridCols={2} gridCols={isMobile ? 1 : 3}
{...cardProps} {...cardProps}
/> />
</div> </div>
</> </div>
); );
}; };

View File

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

View File

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

View File

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

View File

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

865
web/src/index.css vendored
View File

@ -2,100 +2,104 @@
/* 这些变量叠加在 Semi Design 之上,供新的 shadcn 风格组件使用 */ /* 这些变量叠加在 Semi Design 之上,供新的 shadcn 风格组件使用 */
:root { :root {
/* shadcn/ui 基础色板 — Slate 中性色调 (220° hue) */
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 240 10% 3.9%; --foreground: 222 47% 11%;
--card: 0 0% 100%; --card: 0 0% 100%;
--card-foreground: 240 10% 3.9%; --card-foreground: 222 47% 11%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%; --popover-foreground: 222 47% 11%;
--primary: 0 0% 9%; --primary: 221 83% 53%;
--primary-foreground: 0 0% 98%; --primary-foreground: 0 0% 100%;
--secondary: 220 5% 96%; --secondary: 210 40% 96%;
--secondary-foreground: 220 6% 10%; --secondary-foreground: 222 47% 11%;
--muted: 220 5% 96%; --muted: 210 40% 96%;
--muted-foreground: 220 4% 56%; --muted-foreground: 215 16% 47%;
--accent: 220 5% 96%; --accent: 221 83% 53%;
--accent-foreground: 220 6% 10%; --accent-foreground: 0 0% 100%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 220 13% 91%; --border: 214 32% 91%;
--input: 220 13% 91%; --input: 214 32% 91%;
--ring: 0 0% 9%; --ring: 221 83% 53%;
--radius: 0.5rem; --radius: 0.5rem;
--link: 0 0% 9%; --link: 221 83% 53%;
--link-hover: 0 0% 20%; --link-hover: 221 83% 40%;
--chart-1: 160 60% 45%; --chart-1: 221 83% 53%;
--chart-2: 217 91% 60%; --chart-2: 160 60% 45%;
--chart-3: 43 96% 56%; --chart-3: 43 96% 56%;
--chart-4: 280 65% 60%; --chart-4: 280 65% 60%;
--chart-5: 340 75% 55%; --chart-5: 340 75% 55%;
} }
.dark { .dark {
--background: 0 0% 5.5%; --background: 210 60% 97%;
--foreground: 0 0% 90%; --foreground: 222 47% 11%;
--card: 0 0% 10%; --card: 0 0% 100%;
--card-foreground: 0 0% 90%; --card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 8%; --popover: 0 0% 100%;
--popover-foreground: 0 0% 90%; --popover-foreground: 222.2 84% 4.9%;
--primary: 236 52% 62%; --primary: 217 91% 60%;
--primary-foreground: 0 0% 100%; --primary-foreground: 0 0% 100%;
--secondary: 0 0% 13%; --secondary: 210 40% 93%;
--secondary-foreground: 0 0% 90%; --secondary-foreground: 222.2 47.4% 11.2%;
--muted: 0 0% 9%; --muted: 210 40% 93%;
--muted-foreground: 0 0% 56%; --muted-foreground: 215.4 16.3% 46.9%;
--accent: 236 52% 62%; --accent: 217 91% 60%;
--accent-foreground: 0 0% 100%; --accent-foreground: 0 0% 100%;
--destructive: 0 62% 45%; --destructive: 0 62% 45%;
--destructive-foreground: 0 0% 100%; --destructive-foreground: 0 0% 100%;
--border: 0 0% 100% / 0.08; --border: 214.3 31.8% 85%;
--input: 0 0% 100% / 0.06; --input: 214.3 31.8% 85%;
--ring: 236 52% 62%; --ring: 217 91% 60%;
--link: 236 52% 72%; --link: 217 91% 60%;
--link-hover: 236 52% 62%; --link-hover: 217 91% 50%;
--radius: 0.375rem; --radius: 0.375rem;
--chart-1: 236 52% 62%; --chart-1: 217 91% 60%;
--chart-2: 160 60% 50%; --chart-2: 160 60% 50%;
--chart-3: 43 96% 56%; --chart-3: 43 96% 56%;
--chart-4: 280 65% 60%; --chart-4: 280 65% 60%;
--chart-5: 340 75% 55%; --chart-5: 340 75% 55%;
} }
/* === Linear Dark: Semi Design 暗色覆盖 === */ /* === Linear Dark: Semi Design 暗色覆盖 — 蓝白渐变主题 === */
.dark { body[theme-mode='dark'] {
--semi-color-primary: rgba(94, 106, 210, 1); --semi-color-primary: rgba(59, 130, 246, 1);
--semi-color-primary-hover: rgba(114, 126, 230, 1); --semi-color-primary-hover: rgba(79, 150, 255, 1);
--semi-color-primary-active: rgba(78, 90, 190, 1); --semi-color-primary-active: rgba(37, 99, 235, 1);
--semi-color-primary-disabled: rgba(94, 106, 210, 0.35); --semi-color-primary-disabled: rgba(59, 130, 246, 0.35);
--semi-color-primary-light-default: rgba(94, 106, 210, 0.12); --semi-color-primary-light-default: rgba(59, 130, 246, 0.12);
--semi-color-primary-light-hover: rgba(94, 106, 210, 0.18); --semi-color-primary-light-hover: rgba(59, 130, 246, 0.18);
--semi-color-primary-light-active: rgba(94, 106, 210, 0.24); --semi-color-primary-light-active: rgba(59, 130, 246, 0.24);
--semi-color-bg-0: rgba(14, 14, 14, 1); --semi-color-bg-0: rgba(240, 247, 255, 1);
--semi-color-bg-1: rgba(19, 19, 19, 1); --semi-color-bg-1: rgba(235, 244, 255, 1);
--semi-color-bg-2: rgba(25, 25, 25, 1); --semi-color-bg-2: rgba(224, 240, 253, 1);
--semi-color-bg-3: rgba(32, 32, 32, 1); --semi-color-bg-3: rgba(214, 232, 248, 1);
--semi-color-bg-4: rgba(40, 40, 40, 1); --semi-color-bg-4: rgba(204, 224, 244, 1);
--semi-color-text-0: rgba(230, 230, 230, 1); --semi-color-nav-bg: rgba(240, 247, 255, 1);
--semi-color-text-1: rgba(190, 190, 190, 1); --semi-color-text-0: rgba(15, 23, 42, 1);
--semi-color-text-2: rgba(140, 140, 140, 1); --semi-color-text-1: rgba(51, 65, 85, 1);
--semi-color-text-3: rgba(100, 100, 100, 1); --semi-color-text-2: rgba(100, 116, 139, 1);
--semi-color-border: rgba(255, 255, 255, 0.08); --semi-color-text-3: rgba(148, 163, 184, 1);
--semi-color-fill-0: rgba(255, 255, 255, 0.04); --semi-color-border: rgba(191, 219, 254, 1);
--semi-color-fill-1: rgba(255, 255, 255, 0.06); --semi-color-fill-0: rgba(59, 130, 246, 0.04);
--semi-color-fill-2: rgba(255, 255, 255, 0.08); --semi-color-fill-1: rgba(59, 130, 246, 0.06);
--semi-color-link: rgba(130, 143, 255, 1); --semi-color-fill-2: rgba(59, 130, 246, 0.08);
--semi-color-link-hover: rgba(158, 168, 255, 1); --semi-color-link: rgba(59, 130, 246, 1);
--semi-color-link-active: rgba(110, 122, 235, 1); --semi-color-link-hover: rgba(37, 99, 235, 1);
--semi-color-link-visited: rgba(130, 143, 255, 0.8); --semi-color-link-active: rgba(29, 78, 216, 1);
--semi-color-tertiary: rgba(255, 255, 255, 0.06); --semi-color-link-visited: rgba(59, 130, 246, 0.8);
--semi-color-tertiary-hover: rgba(255, 255, 255, 0.10); --semi-color-tertiary: rgba(59, 130, 246, 0.06);
--semi-color-tertiary-active: rgba(255, 255, 255, 0.14); --semi-color-tertiary-hover: rgba(59, 130, 246, 0.10);
--semi-shadow-elevated: 0 0 0 1px rgba(255,255,255,0.06), 0 2px 8px rgba(0,0,0,0.3); --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-success: rgba(52, 211, 153, 1);
--semi-color-danger: rgba(248, 113, 113, 1); --semi-color-danger: rgba(248, 113, 113, 1);
--semi-color-warning: rgba(251, 191, 36, 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 库默认值) */ /* 确保按钮圆角在暗色模式下生效(覆盖 Semi 库默认值) */
@ -108,17 +112,25 @@
border-radius: 8px !important; border-radius: 8px !important;
} }
/* 全局基础样式 — 使用新字体族 */ /* 全局基础样式 — 蓝白渐变主题 */
body { body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans SC', 'Microsoft YaHei', sans-serif; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
color: hsl(var(--foreground)); 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; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
/* ==================== Tailwind CSS 配置 ==================== */ /* ==================== 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 閰嶇疆 ==================== */ /* ==================== Tailwind CSS 閰嶇疆 ==================== */
@layer tailwind-base, semi, tailwind-components, tailwind-utils; @layer tailwind-base, semi, tailwind-components, tailwind-utils;
@ -1610,183 +1622,598 @@ html.dark .with-pastel-balls::before {
padding-top: 0.75rem !important; padding-top: 0.75rem !important;
} }
/* ==================== 妯″瀷瀹氫环椤甸潰甯冨眬 ==================== */ /* ==================== SiliconFlow-style Pricing Page (Cool Effects) ==================== */
.pricing-layout {
height: calc(100vh - 60px); /* ─── Keyframe Animations ─── */
overflow: hidden; @keyframes sf-gradient-shift {
margin-top: 60px; 0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
} }
.pricing-sidebar { @keyframes sf-float {
width: clamp(280px, 24vw, 520px) !important; 0%, 100% { transform: translateY(0) translateX(0); }
min-width: clamp(280px, 24vw, 520px) !important; 25% { transform: translateY(-20px) translateX(10px); }
max-width: clamp(280px, 24vw, 520px) !important; 50% { transform: translateY(-10px) translateX(-5px); }
height: calc(100vh - 60px); 75% { transform: translateY(-25px) translateX(8px); }
background-color: var(--semi-color-bg-0);
overflow: auto;
} }
.pricing-content { @keyframes sf-pulse-glow {
height: calc(100vh - 60px); 0%, 100% { box-shadow: 0 0 20px rgba(25, 118, 210, 0.1); }
background-color: var(--semi-color-bg-0); 50% { box-shadow: 0 0 40px rgba(25, 118, 210, 0.25); }
display: flex;
flex-direction: column;
} }
.pricing-pagination-divider { @keyframes sf-card-in {
border-color: var(--semi-color-border); from { opacity: 0; transform: translateY(30px) scale(0.96); }
to { opacity: 1; transform: translateY(0) scale(1); }
} }
.pricing-content-mobile { @keyframes sf-shimmer {
height: 100%; 0% { background-position: -200% 0; }
display: flex; 100% { background-position: 200% 0; }
flex-direction: column;
overflow: auto;
} }
.pricing-search-header { @keyframes sf-glow-ring {
padding: 0.5rem; 0%, 100% { opacity: 0.3; transform: scale(1); }
background-color: var(--semi-color-bg-0); 50% { opacity: 0.8; transform: scale(1.02); }
flex-shrink: 0; }
position: sticky;
@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; top: 0;
z-index: 5; left: 0;
} width: 100%;
height: 100%;
.pricing-view-container { pointer-events: none;
flex: 1; z-index: 0;
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;
overflow: hidden; 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; box-sizing: border-box;
} }
.sf-search-input::placeholder {
.models-sidebar { color: #b0bec5;
width: 280px; transition: color 0.3s;
flex-shrink: 0; }
overflow-y: auto; .sf-search-input:focus {
padding: 0 16px 24px 0; border-color: #1976d2;
border-right: 1px solid var(--border-color, #e5e7eb); 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 { /* ─── Hot Tags ─── */
width: 4px; .sf-hot-tags {
} display: flex;
.models-sidebar::-webkit-scrollbar-thumb { align-items: center;
background: transparent; justify-content: center;
} gap: 10px;
.models-sidebar:hover::-webkit-scrollbar-thumb { flex-wrap: wrap;
background: rgba(0,0,0,0.15); animation: sf-fade-up 0.8s ease-out 0.6s both;
border-radius: 2px;
} }
.models-content { .sf-hot-label {
flex: 1; font-size: 13px;
min-width: 0; 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; display: flex;
flex-direction: column; 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; overflow: hidden;
} }
.models-tabs-bar { /* ─── Pagination ─── */
.sf-pagination {
display: flex; display: flex;
gap: 8px; justify-content: center;
padding: 16px 0; padding: 36px 0 16px;
flex-shrink: 0;
border-bottom: 1px solid var(--border-color, #e5e7eb);
} }
.models-tab-btn { /* ─── Responsive ─── */
padding: 6px 20px; @media (max-width: 1024px) {
border-radius: 20px; .sf-pricing-inner {
border: 1px solid var(--border-color, #d1d5db); padding: 0 20px 40px;
background: transparent; }
cursor: pointer; .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; font-size: 14px;
color: var(--text-muted, #6b7280); }
transition: all 0.2s; .sf-filter-row {
white-space: nowrap; flex-direction: column;
} align-items: flex-start;
.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);
gap: 8px; gap: 8px;
} }
.models-filter-item:hover { .sf-model-card {
background: var(--hover-bg, #f3f4f6); height: auto;
} min-height: 180px;
.models-filter-item.active { }
background: var(--primary-light, #eff6ff); .sf-particles {
color: var(--primary-color, #3b82f6); display: none;
font-weight: 500; }
}
.models-filter-item .count {
margin-left: auto;
font-size: 12px;
color: var(--text-muted, #9ca3af);
} }
/* ==================== semi-ui 缁勪欢鑷畾涔夋牱寮?==================== */ /* ==================== semi-ui 缁勪欢鑷畾涔夋牱寮?==================== */

File diff suppressed because it is too large Load Diff