后端: - Distributor 模型新增 CurrencySymbol 字段 (default: ¥) - /api/dist/site/info 响应中返回 currency_symbol - PUT /api/distributor/self 支持更新 currency_symbol 分站管理后台 (web): - DistributorSettings 新增货币符号切换 (¥ CNY / $ USD) - 添加 i18n 翻译 (en/zh/ja/fr/ru/vi) 分站前端 (dist-site): - Dashboard: 余额、已用额度使用动态符号 - Packages: 套餐价格、原价、订阅余额使用动态符号 - Pricing: 模型输入/输出价格使用动态符号 - Topup: 预设金额、支付金额、历史记录使用动态符号 - 7 个主题 Home 页: 套餐价格展示使用动态符号 - 所有页面通过 useSite().currency_symbol 读取,统一 fallback ¥ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
244 lines
11 KiB
React
244 lines
11 KiB
React
import React, { useState, useEffect } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useAuth } from '../../context/AuthContext';
|
|
import { useSite } from '../../context/SiteContext';
|
|
import { getSiteModels, getSitePackages } from '../../api';
|
|
import GradientText from '../../components/bits/GradientText';
|
|
import BlurText from '../../components/bits/BlurText';
|
|
import SpotlightCard from '../../components/bits/SpotlightCard';
|
|
import CountUp from '../../components/bits/CountUp';
|
|
import StarBorder from '../../components/bits/StarBorder';
|
|
|
|
export default function StarterHome() {
|
|
const { t } = useTranslation();
|
|
const { user } = useAuth();
|
|
const { site } = useSite();
|
|
const cs = site?.currency_symbol || '¥';
|
|
const [models, setModels] = useState([]);
|
|
const [packages, setPackages] = useState([]);
|
|
|
|
useEffect(() => {
|
|
getSiteModels().then(r => { if (r.data.success) setModels(r.data.data || []); }).catch(() => {});
|
|
getSitePackages().then(r => { if (r.data.success) setPackages(r.data.data || []); }).catch(() => {});
|
|
}, []);
|
|
|
|
const enabledModels = models.filter(m => m.enabled !== false);
|
|
|
|
return (
|
|
<div className="relative">
|
|
{/* Gradient Background */}
|
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
|
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[600px] bg-gradient-radial from-brand-600/20 via-purple-600/5 to-transparent blur-3xl" />
|
|
<div className="absolute top-[400px] right-0 w-[500px] h-[500px] bg-gradient-radial from-pink-600/10 to-transparent blur-3xl" />
|
|
</div>
|
|
|
|
{/* Hero Section */}
|
|
<section className="relative max-w-7xl mx-auto px-6 pt-24 pb-20">
|
|
<div className="text-center max-w-4xl mx-auto">
|
|
<div className="mb-6">
|
|
<GradientText
|
|
colors={['#818cf8', '#c084fc', '#f472b6', '#818cf8']}
|
|
animationSpeed={6}
|
|
className="text-5xl md:text-7xl font-extrabold leading-tight tracking-tight"
|
|
>
|
|
{site?.name || t('home.defaultHeroTitle')}
|
|
</GradientText>
|
|
</div>
|
|
|
|
<BlurText
|
|
text={t('home.heroSubtitle')}
|
|
className="text-lg md:text-xl text-neutral-400 max-w-2xl mx-auto mb-10 leading-relaxed"
|
|
delay={50}
|
|
animateBy="words"
|
|
/>
|
|
|
|
<div className="flex items-center justify-center gap-4">
|
|
{user ? (
|
|
<Link to="/dashboard">
|
|
<StarBorder as="div" color="#818cf8" speed="5s">
|
|
{t('home.goToDashboard')}
|
|
</StarBorder>
|
|
</Link>
|
|
) : (
|
|
<>
|
|
<Link to="/register" className="btn-primary text-base !px-8 !py-3">
|
|
{t('home.getStarted')}
|
|
</Link>
|
|
<Link to="/pricing" className="btn-secondary text-base !px-8 !py-3">
|
|
{t('home.viewPricing')}
|
|
</Link>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-3 gap-6 max-w-xl mx-auto mt-20">
|
|
<div className="text-center">
|
|
<div className="text-3xl font-bold text-white">
|
|
<CountUp from={0} to={enabledModels.length || 50} duration={2} separator="" />+
|
|
</div>
|
|
<p className="text-sm text-neutral-500 mt-1">{t('home.aiModels')}</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-3xl font-bold text-white">
|
|
<CountUp from={0} to={99.9} duration={2.5} />%
|
|
</div>
|
|
<p className="text-sm text-neutral-500 mt-1">{t('home.uptime')}</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-3xl font-bold text-white">
|
|
<<CountUp from={200} to={50} duration={2} direction="down" />ms
|
|
</div>
|
|
<p className="text-sm text-neutral-500 mt-1">{t('home.latency')}</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Features Section */}
|
|
<section className="relative max-w-7xl mx-auto px-6 py-20">
|
|
<div className="text-center mb-12">
|
|
<h2 className="text-3xl font-bold text-white mb-3">{t('home.whyChooseUs')}</h2>
|
|
<p className="text-neutral-400">{t('home.whyChooseUsDesc')}</p>
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-3 gap-6">
|
|
<SpotlightCard className="!bg-neutral-900/60 !border-neutral-800/60" spotlightColor="rgba(129,140,248,0.15)">
|
|
<div className="w-12 h-12 rounded-xl bg-brand-500/10 flex items-center justify-center mb-4">
|
|
<svg className="w-6 h-6 text-brand-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-lg font-semibold text-white mb-2">{t('home.lightningFast')}</h3>
|
|
<p className="text-sm text-neutral-400 leading-relaxed">{t('home.lightningFastDesc')}</p>
|
|
</SpotlightCard>
|
|
|
|
<SpotlightCard className="!bg-neutral-900/60 !border-neutral-800/60" spotlightColor="rgba(192,132,252,0.15)">
|
|
<div className="w-12 h-12 rounded-xl bg-purple-500/10 flex items-center justify-center mb-4">
|
|
<svg className="w-6 h-6 text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-lg font-semibold text-white mb-2">{t('home.securePrivate')}</h3>
|
|
<p className="text-sm text-neutral-400 leading-relaxed">{t('home.securePrivateDesc')}</p>
|
|
</SpotlightCard>
|
|
|
|
<SpotlightCard className="!bg-neutral-900/60 !border-neutral-800/60" spotlightColor="rgba(244,114,182,0.15)">
|
|
<div className="w-12 h-12 rounded-xl bg-pink-500/10 flex items-center justify-center mb-4">
|
|
<svg className="w-6 h-6 text-pink-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-lg font-semibold text-white mb-2">{t('home.payAsYouGo')}</h3>
|
|
<p className="text-sm text-neutral-400 leading-relaxed">{t('home.payAsYouGoDesc')}</p>
|
|
</SpotlightCard>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Models Preview */}
|
|
{enabledModels.length > 0 && (
|
|
<section className="relative max-w-7xl mx-auto px-6 py-20">
|
|
<div className="text-center mb-12">
|
|
<h2 className="text-3xl font-bold text-white mb-3">{t('home.availableModels')}</h2>
|
|
<p className="text-neutral-400">
|
|
{t('home.availableModelsDesc', { count: enabledModels.length })}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
|
{enabledModels.slice(0, 12).map((m, i) => (
|
|
<div
|
|
key={m.id || i}
|
|
className="glass-sm px-4 py-3 flex items-center gap-3 hover:bg-white/[0.06] transition-colors group"
|
|
>
|
|
<div className="w-2 h-2 rounded-full bg-green-500 flex-shrink-0" />
|
|
<span className="text-sm font-mono text-neutral-300 group-hover:text-white transition-colors truncate">
|
|
{m.display_name || m.model_name}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{enabledModels.length > 12 && (
|
|
<div className="text-center mt-6">
|
|
<Link to="/pricing" className="text-sm text-brand-400 hover:text-brand-300 transition-colors">
|
|
{t('home.viewAllModels', { count: enabledModels.length })} →
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</section>
|
|
)}
|
|
|
|
{/* Packages Preview */}
|
|
{packages.length > 0 && (
|
|
<section className="relative max-w-7xl mx-auto px-6 py-20">
|
|
<div className="text-center mb-12">
|
|
<h2 className="text-3xl font-bold text-white mb-3">{t('home.plansPackages')}</h2>
|
|
<p className="text-neutral-400">{t('home.choosePlan')}</p>
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-3 gap-6 max-w-4xl mx-auto">
|
|
{packages.filter(p => p.enabled).slice(0, 3).map((pkg) => (
|
|
<div key={pkg.id} className="glass rounded-2xl p-6 flex flex-col hover:border-brand-500/30 transition-colors">
|
|
<h3 className="text-lg font-semibold text-white mb-1">{pkg.name}</h3>
|
|
{pkg.description && (
|
|
<p className="text-sm text-neutral-400 mb-4">{pkg.description}</p>
|
|
)}
|
|
<div className="mt-auto pt-4">
|
|
<div className="flex items-baseline gap-1">
|
|
<span className="text-3xl font-bold text-white">{cs}{pkg.price}</span>
|
|
{pkg.original_price > pkg.price && (
|
|
<span className="text-sm text-neutral-500 line-through">{cs}{pkg.original_price}</span>
|
|
)}
|
|
</div>
|
|
{pkg.duration > 0 && (
|
|
<p className="text-xs text-neutral-500 mt-1">{t('home.days', { count: pkg.duration })}</p>
|
|
)}
|
|
</div>
|
|
<Link
|
|
to={user ? '/packages' : '/register'}
|
|
className="btn-primary text-center mt-4 !py-2"
|
|
>
|
|
{user ? t('home.subscribe') : t('home.getStarted')}
|
|
</Link>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* CTA Section */}
|
|
<section className="relative max-w-7xl mx-auto px-6 py-24">
|
|
<div className="glass rounded-3xl p-12 text-center relative overflow-hidden">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-brand-600/10 via-transparent to-purple-600/10" />
|
|
<div className="relative z-10">
|
|
<h2 className="text-3xl font-bold text-white mb-4">
|
|
{t('home.readyToStart')}
|
|
</h2>
|
|
<p className="text-neutral-400 mb-8 max-w-md mx-auto">
|
|
{t('home.readyToStartDesc')}
|
|
</p>
|
|
<div className="flex items-center justify-center gap-4">
|
|
{user ? (
|
|
<Link to="/dashboard" className="btn-primary text-base !px-8 !py-3">
|
|
{t('home.goToDashboard')}
|
|
</Link>
|
|
) : (
|
|
<>
|
|
<Link to="/register" className="btn-primary text-base !px-8 !py-3">
|
|
{t('home.createFreeAccount')}
|
|
</Link>
|
|
<Link to="/login" className="btn-secondary text-base !px-8 !py-3">
|
|
{t('home.signIn')}
|
|
</Link>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|