feat: add new file
This commit is contained in:
33
frontend/src/App.jsx
Normal file
33
frontend/src/App.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useState } from 'react'
|
||||
import Header from './components/Header'
|
||||
import Hero from './components/Hero'
|
||||
import ClawLoop from './components/ClawLoop'
|
||||
import CoreFeatures from './components/CoreFeatures'
|
||||
import Pricing from './components/Pricing'
|
||||
import NewsDocs from './components/NewsDocs'
|
||||
import StatsFooter from './components/StatsFooter'
|
||||
import ActivateModal from './components/ActivateModal'
|
||||
import QrModal from './components/QrModal'
|
||||
|
||||
function App() {
|
||||
const [activateOpen, setActivateOpen] = useState(false)
|
||||
const [qrOpen, setQrOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-space-black">
|
||||
<Header onActivate={() => setActivateOpen(true)} />
|
||||
<main>
|
||||
<Hero onLead={() => setQrOpen(true)} />
|
||||
<ClawLoop />
|
||||
<CoreFeatures />
|
||||
<Pricing />
|
||||
<NewsDocs />
|
||||
</main>
|
||||
<StatsFooter />
|
||||
<ActivateModal open={activateOpen} onClose={() => setActivateOpen(false)} />
|
||||
<QrModal open={qrOpen} onClose={() => setQrOpen(false)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
18
frontend/src/api.js
Normal file
18
frontend/src/api.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const API_BASE = import.meta.env.VITE_API_URL || '/api'
|
||||
|
||||
export async function getStats() {
|
||||
const res = await fetch(`${API_BASE}/stats`)
|
||||
if (!res.ok) throw new Error('Failed to fetch stats')
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function validateActivationCode(code) {
|
||||
const res = await fetch(`${API_BASE}/activate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code: code.trim() }),
|
||||
})
|
||||
const data = await res.json().catch(() => ({}))
|
||||
if (!res.ok) throw new Error(data.message || '激活失败')
|
||||
return data
|
||||
}
|
||||
89
frontend/src/components/ActivateModal.jsx
Normal file
89
frontend/src/components/ActivateModal.jsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { useState } from 'react'
|
||||
import { validateActivationCode } from '../api'
|
||||
|
||||
export default function ActivateModal({ open, onClose }) {
|
||||
const [code, setCode] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState(false)
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
if (!code.trim()) return
|
||||
setError('')
|
||||
setLoading(true)
|
||||
try {
|
||||
await validateActivationCode(code)
|
||||
setSuccess(true)
|
||||
setCode('')
|
||||
} catch (err) {
|
||||
setError(err.message || '激活失败,请检查激活码')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm" onClick={onClose}>
|
||||
<div
|
||||
className="w-full max-w-md rounded-2xl border border-white/10 bg-space-black-light p-6 shadow-xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-xl font-semibold text-white">线下激活</h3>
|
||||
<button className="p-2 text-gray-400 hover:text-white" onClick={onClose} aria-label="关闭">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{success ? (
|
||||
<div className="text-center py-4">
|
||||
<p className="text-brand-orange font-medium mb-2">激活成功</p>
|
||||
<p className="text-gray-400 text-sm">请在小程序中绑定此设备继续使用。</p>
|
||||
<button
|
||||
className="mt-4 px-4 py-2 rounded-lg bg-brand-orange text-white"
|
||||
onClick={() => { setSuccess(false); onClose(); }}
|
||||
>
|
||||
完成
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<p className="text-gray-400 text-sm mb-4">
|
||||
请输入现场获得的激活码,关联小程序后即可使用阿虾。
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
placeholder="请输入激活码"
|
||||
className="w-full px-4 py-3 rounded-xl border border-white/20 bg-space-black text-white placeholder-gray-500 focus:border-brand-orange focus:outline-none"
|
||||
autoFocus
|
||||
/>
|
||||
{error && <p className="mt-2 text-sm text-red-400">{error}</p>}
|
||||
<div className="mt-6 flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="flex-1 py-3 rounded-xl border border-white/20 text-white hover:bg-white/5"
|
||||
onClick={onClose}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="flex-1 py-3 rounded-xl bg-brand-orange text-white font-medium hover:bg-brand-orange-dark disabled:opacity-50"
|
||||
>
|
||||
{loading ? '验证中…' : '激活'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
51
frontend/src/components/ClawLoop.jsx
Normal file
51
frontend/src/components/ClawLoop.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
const steps = [
|
||||
{
|
||||
title: '极速装机',
|
||||
desc: '一行命令,Docker 镜像秒级部署。',
|
||||
icon: '⚡',
|
||||
},
|
||||
{
|
||||
title: '多端唤醒',
|
||||
desc: '在熟悉的 IM 软件里指挥 AI 干活。',
|
||||
icon: '💬',
|
||||
},
|
||||
{
|
||||
title: '算力自由',
|
||||
desc: '全网最低价 Token 供应,多模型一键切换。',
|
||||
icon: '🔌',
|
||||
},
|
||||
]
|
||||
|
||||
export default function ClawLoop() {
|
||||
return (
|
||||
<section className="py-16 sm:py-24 px-4 sm:px-6 bg-space-black-light/50">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-white text-center mb-4">
|
||||
The A-Claw 闭环
|
||||
</h2>
|
||||
<p className="text-gray-400 text-center mb-12">
|
||||
从部署到使用,一条龙搞定
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-8">
|
||||
{steps.map((step, i) => (
|
||||
<div key={i} className="relative">
|
||||
<div className="card-hover rounded-2xl border border-white/10 bg-space-black-lighter/80 p-6 sm:p-8 h-full">
|
||||
<div className="text-3xl mb-4">{step.icon}</div>
|
||||
<h3 className="text-xl font-semibold text-white mb-2">{step.title}</h3>
|
||||
<p className="text-gray-400">{step.desc}</p>
|
||||
</div>
|
||||
{i < steps.length - 1 && (
|
||||
<div className="hidden md:block absolute top-1/2 -right-4 w-8 text-brand-orange/50" style={{ transform: 'translateY(-50%)' }}>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" className="w-6 h-6">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
42
frontend/src/components/ClawSvg.jsx
Normal file
42
frontend/src/components/ClawSvg.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
/** 3D 机械螯爪(A-Claw)线条 - 首屏背景 */
|
||||
export default function ClawSvg() {
|
||||
return (
|
||||
<svg
|
||||
className="absolute inset-0 w-full h-full pointer-events-none animate-claw"
|
||||
viewBox="0 0 800 600"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="clawGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor="#FF6B35" stopOpacity="0.15" />
|
||||
<stop offset="100%" stopColor="#FF6B35" stopOpacity="0.05" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{/* Simplified mechanical claw outline - left pincer */}
|
||||
<path
|
||||
d="M 200 280 Q 280 240 360 260 L 400 300 L 380 340 Q 300 360 220 320 Z"
|
||||
stroke="url(#clawGrad)"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
opacity="0.6"
|
||||
/>
|
||||
{/* Right pincer */}
|
||||
<path
|
||||
d="M 600 280 Q 520 240 440 260 L 400 300 L 420 340 Q 500 360 580 320 Z"
|
||||
stroke="url(#clawGrad)"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
opacity="0.6"
|
||||
/>
|
||||
{/* Center joint */}
|
||||
<circle cx="400" cy="300" r="24" stroke="#FF6B35" strokeWidth="1" fill="none" opacity="0.25" />
|
||||
<circle cx="400" cy="300" r="8" stroke="#FF6B35" strokeWidth="1" fill="none" opacity="0.4" />
|
||||
{/* Arm lines */}
|
||||
<path d="M 400 300 L 400 180" stroke="#FF6B35" strokeWidth="1" opacity="0.2" />
|
||||
<path d="M 400 300 L 320 220" stroke="#FF6B35" strokeWidth="0.8" opacity="0.15" />
|
||||
<path d="M 400 300 L 480 220" stroke="#FF6B35" strokeWidth="0.8" opacity="0.15" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
34
frontend/src/components/CoreFeatures.jsx
Normal file
34
frontend/src/components/CoreFeatures.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
const features = [
|
||||
{ title: 'IM 全适配', desc: '原生支持钉钉、企微、飞书、QQ。', emoji: '📱' },
|
||||
{ title: '模型聚合', desc: '内置 OpenAI, Claude, DeepSeek 等主流模型。', emoji: '🧠' },
|
||||
{ title: '云端常驻', desc: '7x24 小时在线,无需本地挂机。', emoji: '☁️' },
|
||||
{ title: '隐私安全', desc: '容器化隔离,密钥存储在用户自己的环境中。', emoji: '🔒' },
|
||||
]
|
||||
|
||||
export default function CoreFeatures() {
|
||||
return (
|
||||
<section id="features" className="py-16 sm:py-24 px-4 sm:px-6">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-white text-center mb-4">
|
||||
核心功能
|
||||
</h2>
|
||||
<p className="text-gray-400 text-center mb-12">
|
||||
技术实力,一网打尽
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6">
|
||||
{features.map((f, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="card-hover rounded-xl border border-white/10 bg-space-black-lighter/60 p-5 sm:p-6"
|
||||
>
|
||||
<div className="text-2xl mb-3">{f.emoji}</div>
|
||||
<h3 className="font-semibold text-white mb-2">{f.title}</h3>
|
||||
<p className="text-gray-400 text-sm">{f.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
58
frontend/src/components/Header.jsx
Normal file
58
frontend/src/components/Header.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function Header({ onActivate }) {
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<header className="fixed top-0 left-0 right-0 z-50 bg-space-black/90 backdrop-blur-md border-b border-white/5">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 flex items-center justify-between h-14 sm:h-16">
|
||||
<a href="/" className="flex items-center gap-2">
|
||||
<img src="/image/logo.png" alt="Aiclw" className="h-8 w-8 sm:h-9 sm:w-9 rounded-lg" />
|
||||
<span className="font-bold text-lg text-white">Aiclw</span>
|
||||
</a>
|
||||
|
||||
<nav className="hidden md:flex items-center gap-6">
|
||||
<a href="#features" className="text-gray-400 hover:text-white transition">功能</a>
|
||||
<a href="#pricing" className="text-gray-400 hover:text-white transition">价格</a>
|
||||
<a href="#docs" className="text-gray-400 hover:text-white transition">文档</a>
|
||||
<button
|
||||
onClick={onActivate}
|
||||
className="px-4 py-2 rounded-lg bg-brand-orange text-white font-medium hover:bg-brand-orange-dark transition"
|
||||
>
|
||||
线下激活
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<button
|
||||
className="md:hidden p-2 text-gray-400 hover:text-white"
|
||||
onClick={() => setMenuOpen(!menuOpen)}
|
||||
aria-label="菜单"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{menuOpen ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{menuOpen && (
|
||||
<div className="md:hidden border-t border-white/5 bg-space-black">
|
||||
<div className="px-4 py-3 flex flex-col gap-2">
|
||||
<a href="#features" className="py-2 text-gray-400 hover:text-white" onClick={() => setMenuOpen(false)}>功能</a>
|
||||
<a href="#pricing" className="py-2 text-gray-400 hover:text-white" onClick={() => setMenuOpen(false)}>价格</a>
|
||||
<a href="#docs" className="py-2 text-gray-400 hover:text-white" onClick={() => setMenuOpen(false)}>文档</a>
|
||||
<button
|
||||
onClick={() => { onActivate(); setMenuOpen(false); }}
|
||||
className="mt-2 py-3 rounded-lg bg-brand-orange text-white font-medium"
|
||||
>
|
||||
线下激活
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
48
frontend/src/components/Hero.jsx
Normal file
48
frontend/src/components/Hero.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import ClawSvg from './ClawSvg'
|
||||
|
||||
export default function Hero({ onLead }) {
|
||||
return (
|
||||
<section className="relative min-h-screen flex items-center justify-center overflow-hidden pt-14">
|
||||
{/* Background: subtle grid + claw lines */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-space-black via-space-black to-space-black-light" />
|
||||
<div className="absolute inset-0 opacity-20" style={{ backgroundImage: 'radial-gradient(circle at 50% 50%, #FF6B35 0%, transparent 50%)' }} />
|
||||
<div className="absolute inset-0">
|
||||
<ClawSvg />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 max-w-4xl mx-auto px-4 sm:px-6 text-center">
|
||||
<h1 className="text-4xl sm:text-5xl md:text-6xl font-bold text-white leading-tight mb-4 sm:mb-6">
|
||||
让 AI 长出手,<br className="sm:hidden" />在云端走。
|
||||
</h1>
|
||||
<p className="text-gray-400 text-lg sm:text-xl max-w-2xl mx-auto mb-8 sm:mb-10">
|
||||
一键部署你的 OpenClaw 数字员工,打通钉钉、企微、飞书,让自动化触手可及。
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<button
|
||||
onClick={onLead}
|
||||
className="px-6 py-3.5 rounded-xl bg-brand-orange text-white font-semibold hover:bg-brand-orange-dark transition shadow-lg shadow-brand-orange/25"
|
||||
>
|
||||
立即领养阿虾
|
||||
</button>
|
||||
<a
|
||||
href="https://github.com" target="_blank" rel="noopener noreferrer"
|
||||
className="px-6 py-3.5 rounded-xl border border-white/20 text-white font-medium hover:border-brand-orange hover:bg-white/5 transition"
|
||||
>
|
||||
查看部署文档
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative code flow */}
|
||||
<div className="absolute bottom-8 left-0 right-0 overflow-hidden opacity-40">
|
||||
<div className="flex animate-flow whitespace-nowrap font-mono text-sm text-brand-orange/80">
|
||||
<span className="mr-8">$ docker pull aiclw/openclaw</span>
|
||||
<span className="mr-8">→ 钉钉 / 企微 / 飞书 已就绪</span>
|
||||
<span className="mr-8">token: ********</span>
|
||||
<span className="mr-8">$ docker pull aiclw/openclaw</span>
|
||||
<span className="mr-8">→ 钉钉 / 企微 / 飞书 已就绪</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
48
frontend/src/components/NewsDocs.jsx
Normal file
48
frontend/src/components/NewsDocs.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
const items = [
|
||||
{
|
||||
title: '阿虾周报',
|
||||
desc: '每周更新 AI Agent 的前沿玩法,为小程序导流。',
|
||||
link: '#',
|
||||
label: '阅读周报',
|
||||
},
|
||||
{
|
||||
title: '新手村',
|
||||
desc: '从 Docker 安装到第一个 IM 机器人上线的保姆级教程。',
|
||||
link: '#',
|
||||
label: '进入新手村',
|
||||
},
|
||||
]
|
||||
|
||||
export default function NewsDocs() {
|
||||
return (
|
||||
<section id="docs" className="py-16 sm:py-24 px-4 sm:px-6">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-white text-center mb-4">
|
||||
内容与文档
|
||||
</h2>
|
||||
<p className="text-gray-400 text-center mb-12">
|
||||
阿虾周报 · 新手村
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
{items.map((item, i) => (
|
||||
<a
|
||||
key={i}
|
||||
href={item.link}
|
||||
className="card-hover block rounded-2xl border border-white/10 bg-space-black-lighter/60 p-6 sm:p-8 hover:border-brand-orange/50"
|
||||
>
|
||||
<h3 className="text-xl font-semibold text-white mb-2">{item.title}</h3>
|
||||
<p className="text-gray-400 text-sm mb-4">{item.desc}</p>
|
||||
<span className="text-brand-orange font-medium text-sm inline-flex items-center gap-1">
|
||||
{item.label}
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
76
frontend/src/components/Pricing.jsx
Normal file
76
frontend/src/components/Pricing.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
const plans = [
|
||||
{
|
||||
name: '免费版',
|
||||
price: '0',
|
||||
unit: '元部署',
|
||||
desc: '使用自带 Key,提供基础社区支持。',
|
||||
cta: '免费开始',
|
||||
highlight: false,
|
||||
},
|
||||
{
|
||||
name: '阿虾订阅版',
|
||||
badge: '推荐',
|
||||
price: '19.9',
|
||||
unit: '元/月',
|
||||
desc: '低至 ¥19.9/月,赠送百万级「虾油」Token,支持专属模型通道。',
|
||||
cta: '立即订阅',
|
||||
highlight: true,
|
||||
},
|
||||
{
|
||||
name: '企业定制',
|
||||
price: '定制',
|
||||
unit: '',
|
||||
desc: '针对线下场景的批量部署与私有化协议。',
|
||||
cta: '联系商务',
|
||||
highlight: false,
|
||||
},
|
||||
]
|
||||
|
||||
export default function Pricing() {
|
||||
return (
|
||||
<section id="pricing" className="py-16 sm:py-24 px-4 sm:px-6 bg-space-black-light/50">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-white text-center mb-4">
|
||||
价格与算力
|
||||
</h2>
|
||||
<p className="text-gray-400 text-center mb-12">
|
||||
按需选择,透明定价
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{plans.map((plan, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`rounded-2xl border p-6 sm:p-8 flex flex-col ${
|
||||
plan.highlight
|
||||
? 'border-brand-orange bg-brand-orange/5 shadow-lg shadow-brand-orange/10'
|
||||
: 'border-white/10 bg-space-black-lighter/60 card-hover'
|
||||
}`}
|
||||
>
|
||||
{plan.badge && (
|
||||
<span className="inline-block px-3 py-1 rounded-full bg-brand-orange text-white text-sm font-medium mb-4 w-fit">
|
||||
{plan.badge}
|
||||
</span>
|
||||
)}
|
||||
<h3 className="text-xl font-semibold text-white mb-2">{plan.name}</h3>
|
||||
<div className="mb-4">
|
||||
<span className="text-3xl font-bold text-white">{plan.price}</span>
|
||||
{plan.unit && <span className="text-gray-400 ml-1">{plan.unit}</span>}
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm flex-1 mb-6">{plan.desc}</p>
|
||||
<button
|
||||
className={`w-full py-3 rounded-xl font-medium transition ${
|
||||
plan.highlight
|
||||
? 'bg-brand-orange text-white hover:bg-brand-orange-dark'
|
||||
: 'border border-white/20 text-white hover:border-brand-orange hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
{plan.cta}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
29
frontend/src/components/QrModal.jsx
Normal file
29
frontend/src/components/QrModal.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
export default function QrModal({ open, onClose }) {
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm" onClick={onClose}>
|
||||
<div
|
||||
className="w-full max-w-sm rounded-2xl border border-white/10 bg-space-black-light p-6 shadow-xl text-center"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex justify-end -mt-2 -mr-2 mb-2">
|
||||
<button className="p-2 text-gray-400 hover:text-white" onClick={onClose} aria-label="关闭">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">立即领养阿虾</h3>
|
||||
<p className="text-gray-400 text-sm mb-6">扫码打开小程序,领养你的数字员工</p>
|
||||
<div className="inline-flex items-center justify-center w-48 h-48 rounded-xl bg-white p-2">
|
||||
{/* 占位:实际替换为小程序二维码图片 */}
|
||||
<div className="w-full h-full rounded-lg bg-gray-200 flex items-center justify-center text-gray-500 text-sm">
|
||||
小程序二维码
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-4 text-gray-500 text-xs">请使用微信扫码</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
51
frontend/src/components/StatsFooter.jsx
Normal file
51
frontend/src/components/StatsFooter.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { getStats } from '../api'
|
||||
|
||||
const fallback = {
|
||||
nodes: 1280,
|
||||
tasksToday: 45000,
|
||||
hoursSaved: 8600,
|
||||
}
|
||||
|
||||
function formatNum(n) {
|
||||
if (n >= 10000) return (n / 10000).toFixed(1) + '万+'
|
||||
return n.toLocaleString() + '+'
|
||||
}
|
||||
|
||||
export default function StatsFooter() {
|
||||
const [stats, setStats] = useState(fallback)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
getStats()
|
||||
.then(setStats)
|
||||
.catch(() => setStats(fallback))
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
const items = [
|
||||
{ label: '已激活「阿虾」节点', value: formatNum(stats.nodes) },
|
||||
{ label: '今日处理自动化任务', value: formatNum(stats.tasksToday) },
|
||||
{ label: '已节省人力时长', value: formatNum(stats.hoursSaved) + ' 小时' },
|
||||
]
|
||||
|
||||
return (
|
||||
<footer className="border-t border-white/10 bg-space-black">
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6 py-12">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-8 text-center">
|
||||
{items.map((item, i) => (
|
||||
<div key={i}>
|
||||
<div className="text-2xl sm:text-3xl font-bold text-brand-orange mb-1">
|
||||
{loading ? '—' : item.value}
|
||||
</div>
|
||||
<div className="text-gray-400 text-sm">{item.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-10 pt-8 border-t border-white/5 text-center text-gray-500 text-sm">
|
||||
Aiclw · 连接算力,执行万物
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
40
frontend/src/index.css
Normal file
40
frontend/src/index.css
Normal file
@@ -0,0 +1,40 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--brand-orange: #FF6B35;
|
||||
--space-black: #121212;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-space-black text-gray-100 antialiased;
|
||||
font-family: 'DM Sans', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* Terminal / code flow styling */
|
||||
.terminal-line {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
@apply text-brand-orange/90 text-sm;
|
||||
}
|
||||
|
||||
/* Card hover - Agentic UI */
|
||||
.card-hover {
|
||||
@apply transition-all duration-300;
|
||||
}
|
||||
.card-hover:hover {
|
||||
@apply border-brand-orange/50 shadow-lg shadow-brand-orange/10 -translate-y-0.5;
|
||||
}
|
||||
|
||||
/* Claw SVG animation */
|
||||
@keyframes claw-float {
|
||||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
||||
50% { transform: translateY(-6px) rotate(1deg); }
|
||||
}
|
||||
.animate-claw {
|
||||
animation: claw-float 4s ease-in-out infinite;
|
||||
}
|
||||
10
frontend/src/main.jsx
Normal file
10
frontend/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
Reference in New Issue
Block a user