Merge branch 'feature/homepage-redesign' into feature/project-init

This commit is contained in:
vsxd 2026-03-13 10:05:39 +08:00
commit 1a5bc12fb7
8 changed files with 6476 additions and 3 deletions

6
package-lock.json generated Normal file
View file

@ -0,0 +1,6 @@
{
"name": "homepage-redesign",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View file

@ -0,0 +1,75 @@
# SkillHub 落地页重新设计
## 设计理念
采用"技术编织"(Tech Weave)美学风格,通过以下元素传达 SkillHub 作为企业级技能注册中心的专业性和创新性:
### 视觉特点
1. **动态粒子系统**
- Canvas 实现的粒子连接动画
- 象征技能之间的连接和协作
- 80个粒子节点,动态连线效果
2. **配色方案**
- 深蓝紫色基调(slate-950, indigo-950)
- 霓虹青色点缀(cyan-500)
- 紫罗兰色辅助(violet-500)
- 营造科技感和未来感
3. **字体选择**
- 标题:Syne(几何感强,现代)
- 正文:IBM Plex Sans(技术感,易读)
- 代码:JetBrains Mono
4. **动效设计**
- 渐入动画(fade-up, fade-in)
- 悬浮卡片效果(hover:scale-105)
- 渐变光晕(gradient glow)
- 脉冲动画(pulse)
### 内容结构
1. **Hero 区域**
- 大标题 + 渐变文字效果
- 搜索栏(带光晕效果)
- CTA 按钮(探索技能 / 发布技能)
- 统计数据展示(技能包、下载量、团队)
2. **特性展示**
- 6个核心特性卡片
- 图标 + 标题 + 描述
- 悬浮交互效果
3. **CTA 区域**
- 快速开始指引
- 命令行示例
- 行动按钮
4. **页脚**
- 版权信息
- 导航链接
## 技术实现
- React + TypeScript
- TailwindCSS
- Canvas API(粒子动画)
- TanStack Router
## 文件变更
- 新增:`web/src/pages/landing.tsx` - 新的落地页组件
- 修改:`web/src/app/router.tsx` - 路由配置,将 landing 设为首页
- 修改:`web/index.html` - 更新字体引用
- 修改:`web/src/index.css` - 更新字体配置
## 本地预览
```bash
cd web
npm install --legacy-peer-deps
npm run dev
```
访问 `http://localhost:5173` 查看新的落地页。

55
web/PREVIEW.md Normal file
View file

@ -0,0 +1,55 @@
# 落地页预览说明
## 如何查看
1. 启动开发服务器:
```bash
cd web
npm run dev
```
2. 访问 `http://localhost:5173`
## 页面特性
### 动态效果
- 背景粒子动画会自动运行
- 鼠标悬停在特性卡片上会有缩放和颜色变化
- 按钮有渐变和阴影效果
- 搜索栏有光晕效果
### 响应式设计
- 移动端:单列布局,较小字体
- 平板:2列特性卡片
- 桌面:3列特性卡片,大标题
### 交互元素
- "探索技能" 按钮 → 跳转到搜索页面
- "发布技能" 按钮 → 跳转到发布页面
- 搜索栏 → 输入后跳转到搜索结果
- 统计数字 → 悬停有渐变效果
## 与原首页的区别
| 特性 | 原首页 | 新落地页 |
|------|--------|----------|
| 主要内容 | 技能列表(热门/最新) | 产品介绍和特性展示 |
| 视觉风格 | 简洁卡片式 | 科技感渐变 + 粒子动画 |
| 配色 | 浅色为主 | 深色主题 + 霓虹色 |
| 目标 | 快速浏览技能 | 吸引新用户了解产品 |
| 路由 | `/` | `/`(新),原首页移至 `/skills` |
## 设计亮点
1. **粒子连接动画**:80个粒子节点,距离小于120px时自动连线,透明度随距离变化
2. **渐变文字**:标题使用 cyan → blue → violet 三色渐变
3. **光晕效果**:搜索栏和背景有模糊的光晕装饰
4. **分层动画**:不同元素有不同的延迟时间,形成层次感
5. **悬浮交互**:卡片悬停时会上浮并显示边框高亮
## 技术细节
- 粒子系统使用 Canvas 2D API
- 动画使用 CSS animations 和 transitions
- 响应式使用 Tailwind 的 breakpoint 系统
- 字体通过 Google Fonts CDN 加载

View file

@ -6,7 +6,7 @@
<title>SkillHub</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300..700;1,9..40,300..700&family=Outfit:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=IBM+Plex+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>

6035
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
import { createRouter, createRoute, createRootRoute, redirect } from '@tanstack/react-router'
import { Layout } from './layout'
import { LandingPage } from '@/pages/landing'
import { HomePage } from '@/pages/home'
import { LoginPage } from '@/pages/login'
import { DashboardPage } from '@/pages/dashboard'
@ -24,6 +25,12 @@ const rootRoute = createRootRoute({
const homeRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: LandingPage,
})
const skillsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/skills',
component: HomePage,
})
@ -189,6 +196,7 @@ const adminAuditLogRoute = createRoute({
const routeTree = rootRoute.addChildren([
homeRoute,
skillsRoute,
loginRoute,
searchRoute,
namespaceRoute,

View file

@ -75,11 +75,11 @@
body {
@apply bg-background text-foreground antialiased;
font-family: 'DM Sans', system-ui, sans-serif;
font-family: 'IBM Plex Sans', system-ui, sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Outfit', 'DM Sans', system-ui, sans-serif;
font-family: 'Syne', 'IBM Plex Sans', system-ui, sans-serif;
}
code, pre, kbd {

294
web/src/pages/landing.tsx Normal file
View file

@ -0,0 +1,294 @@
import { useNavigate } from '@tanstack/react-router'
import { SearchBar } from '@/features/search/search-bar'
import { Button } from '@/shared/ui/button'
import { useEffect, useRef, useState } from 'react'
export function LandingPage() {
const navigate = useNavigate()
const canvasRef = useRef<HTMLCanvasElement>(null)
const [stats] = useState({
skills: '1000+',
downloads: '50K+',
teams: '200+',
})
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
const resize = () => {
if (canvas) {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
}
}
resize()
window.addEventListener('resize', resize)
class Particle {
x: number
y: number
vx: number
vy: number
radius: number
constructor(canvasWidth: number, canvasHeight: number) {
this.x = Math.random() * canvasWidth
this.y = Math.random() * canvasHeight
this.vx = (Math.random() - 0.5) * 0.3
this.vy = (Math.random() - 0.5) * 0.3
this.radius = Math.random() * 1.5 + 0.5
}
update(canvasWidth: number, canvasHeight: number) {
this.x += this.vx
this.y += this.vy
if (this.x < 0 || this.x > canvasWidth) this.vx *= -1
if (this.y < 0 || this.y > canvasHeight) this.vy *= -1
}
draw(context: CanvasRenderingContext2D) {
context.beginPath()
context.arc(this.x, this.y, this.radius, 0, Math.PI * 2)
context.fillStyle = 'rgba(56, 189, 248, 0.6)'
context.fill()
}
}
const particles: Particle[] = []
const particleCount = 80
for (let i = 0; i < particleCount; i++) {
particles.push(new Particle(canvas.width, canvas.height))
}
const connectParticles = () => {
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x
const dy = particles[i].y - particles[j].y
const distance = Math.sqrt(dx * dx + dy * dy)
if (distance < 120) {
ctx.beginPath()
const opacity = 0.15 * (1 - distance / 120)
ctx.strokeStyle = 'rgba(56, 189, 248, ' + opacity + ')'
ctx.lineWidth = 0.5
ctx.moveTo(particles[i].x, particles[i].y)
ctx.lineTo(particles[j].x, particles[j].y)
ctx.stroke()
}
}
}
}
const animate = () => {
if (!canvas) return
ctx.clearRect(0, 0, canvas.width, canvas.height)
particles.forEach(particle => {
particle.update(canvas.width, canvas.height)
particle.draw(ctx)
})
connectParticles()
requestAnimationFrame(animate)
}
animate()
return () => {
window.removeEventListener('resize', resize)
}
}, [])
const handleSearch = (query: string) => {
navigate({ to: '/search', search: { q: query, sort: 'relevance', page: 1 } })
}
const features = [
{
icon: '🔒',
title: '私有部署',
description: '完全自托管,数据主权在您手中。一键部署到您的基础设施,防火墙后安全运行。',
},
{
icon: '📦',
title: '版本管理',
description: '语义化版本控制,自定义标签(beta、stable),自动追踪 latest 版本。',
},
{
icon: '🔍',
title: '智能搜索',
description: '全文搜索,支持命名空间、下载量、评分、时间等多维度筛选。',
},
{
icon: '👥',
title: '团队协作',
description: '命名空间管理,角色权限控制(Owner/Admin/Member),发布策略配置。',
},
{
icon: '✅',
title: '审核治理',
description: '团队内审核,平台级审批,全流程审计日志,满足合规要求。',
},
{
icon: '⚡',
title: 'CLI 优先',
description: '原生 REST API,兼容现有 ClawHub CLI 工具,无需客户端改动。',
},
]
return (
<div className="relative min-h-screen overflow-hidden bg-gradient-to-br from-slate-950 via-indigo-950 to-slate-900">
<canvas
ref={canvasRef}
className="absolute inset-0 pointer-events-none"
style={{ opacity: 0.4 }}
/>
<div className="absolute top-0 left-1/4 w-96 h-96 bg-cyan-500/20 rounded-full blur-3xl" style={{ animation: 'pulse 4s ease-in-out infinite' }} />
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-violet-500/20 rounded-full blur-3xl" style={{ animation: 'pulse 4s ease-in-out infinite 2s' }} />
<div className="relative z-10 container mx-auto px-6 py-20">
<div className="flex flex-col items-center text-center space-y-12 pt-20 pb-32">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-cyan-500/10 border border-cyan-500/30 backdrop-blur-sm animate-fade-in">
<div className="w-2 h-2 rounded-full bg-cyan-400 animate-pulse" />
<span className="text-sm text-cyan-300 font-medium">企业级技能注册中心</span>
</div>
<div className="space-y-6 max-w-5xl">
<h1 className="text-7xl md:text-8xl lg:text-9xl font-black tracking-tight animate-fade-up">
<span className="block text-transparent bg-clip-text bg-gradient-to-r from-cyan-300 via-blue-400 to-violet-400">
SkillHub
</span>
</h1>
<p className="text-2xl md:text-3xl text-slate-300 font-light leading-relaxed animate-fade-up delay-1">
发布、发现、管理
<span className="text-cyan-400 font-medium"> Agent 技能包</span>
</p>
<p className="text-lg text-slate-400 max-w-2xl mx-auto animate-fade-up delay-2">
自托管的私有技能注册平台,为团队提供安全、高效的技能共享与协作空间
</p>
</div>
<div className="w-full max-w-3xl animate-fade-up delay-3">
<div className="relative group">
<div className="absolute -inset-1 bg-gradient-to-r from-cyan-500 to-violet-500 rounded-2xl blur opacity-25 group-hover:opacity-40 transition duration-500" />
<div className="relative bg-slate-900/80 backdrop-blur-xl rounded-2xl p-2 border border-slate-700/50">
<SearchBar onSearch={handleSearch} />
</div>
</div>
</div>
<div className="flex flex-wrap items-center justify-center gap-4 animate-fade-up delay-4">
<Button
size="lg"
className="bg-gradient-to-r from-cyan-500 to-blue-500 hover:from-cyan-400 hover:to-blue-400 text-white font-semibold px-8 py-6 text-lg rounded-xl shadow-lg shadow-cyan-500/25 hover:shadow-cyan-500/40 transition-all duration-300 hover:scale-105"
onClick={() => navigate({ to: '/search', search: { q: '', sort: 'relevance', page: 1 } })}
>
探索技能
</Button>
<Button
size="lg"
variant="outline"
className="border-2 border-slate-600 hover:border-cyan-500 text-slate-200 hover:text-cyan-300 font-semibold px-8 py-6 text-lg rounded-xl backdrop-blur-sm bg-slate-800/30 hover:bg-slate-800/50 transition-all duration-300"
onClick={() => navigate({ to: '/dashboard/publish' })}
>
发布技能
</Button>
</div>
<div className="grid grid-cols-3 gap-8 md:gap-16 pt-12 animate-fade-up delay-5">
{Object.entries(stats).map(([key, value]) => (
<div key={key} className="text-center group cursor-default">
<div className="text-4xl md:text-5xl font-bold text-transparent bg-clip-text bg-gradient-to-br from-cyan-300 to-blue-400 group-hover:from-cyan-200 group-hover:to-blue-300 transition-all duration-300">
{value}
</div>
<div className="text-sm md:text-base text-slate-400 mt-2 capitalize">
{key === 'skills' && '技能包'}
{key === 'downloads' && '下载量'}
{key === 'teams' && '团队'}
</div>
</div>
))}
</div>
</div>
<div className="py-20 space-y-16">
<div className="text-center space-y-4 animate-fade-up">
<h2 className="text-4xl md:text-5xl font-bold text-slate-100">
为什么选择 <span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-violet-400">SkillHub</span>
</h2>
<p className="text-lg text-slate-400 max-w-2xl mx-auto">
专为企业打造的技能管理平台,提供完整的发布、发现、治理能力
</p>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{features.map((feature, idx) => (
<div
key={idx}
className="group relative p-6 rounded-2xl bg-slate-800/40 backdrop-blur-sm border border-slate-700/50 hover:border-cyan-500/50 transition-all duration-300 hover:scale-105 animate-fade-up"
style={{ animationDelay: (idx * 0.1) + 's' }}
>
<div className="absolute inset-0 bg-gradient-to-br from-cyan-500/5 to-violet-500/5 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative space-y-3">
<div className="text-4xl">{feature.icon}</div>
<h3 className="text-xl font-bold text-slate-100 group-hover:text-cyan-300 transition-colors">
{feature.title}
</h3>
<p className="text-slate-400 leading-relaxed">
{feature.description}
</p>
</div>
</div>
))}
</div>
</div>
<div className="py-20">
<div className="relative max-w-4xl mx-auto p-12 rounded-3xl bg-gradient-to-br from-cyan-500/10 to-violet-500/10 backdrop-blur-sm border border-cyan-500/30 text-center space-y-8 animate-fade-up">
<div className="absolute inset-0 bg-gradient-to-br from-cyan-500/5 to-violet-500/5 rounded-3xl blur-xl" />
<div className="relative space-y-6">
<h2 className="text-4xl md:text-5xl font-bold text-slate-100">
准备好开始了吗?
</h2>
<p className="text-lg text-slate-300 max-w-2xl mx-auto">
一条命令即可启动本地开发环境,体验完整的技能管理流程
</p>
<div className="inline-block p-4 rounded-xl bg-slate-900/80 backdrop-blur-sm border border-slate-700/50">
<code className="text-cyan-400 text-lg font-mono">make dev-all</code>
</div>
<div className="flex flex-wrap items-center justify-center gap-4 pt-4">
<Button
size="lg"
className="bg-gradient-to-r from-cyan-500 to-blue-500 hover:from-cyan-400 hover:to-blue-400 text-white font-semibold px-8 py-6 text-lg rounded-xl shadow-lg shadow-cyan-500/25 hover:shadow-cyan-500/40 transition-all duration-300 hover:scale-105"
onClick={() => navigate({ to: '/search', search: { q: '', sort: 'relevance', page: 1 } })}
>
立即体验
</Button>
</div>
</div>
</div>
</div>
</div>
<div className="relative z-10 border-t border-slate-800/50 backdrop-blur-sm">
<div className="container mx-auto px-6 py-8">
<div className="flex flex-col md:flex-row items-center justify-between gap-4 text-slate-400 text-sm">
<div>© 2026 SkillHub. MIT License.</div>
<div className="flex items-center gap-6">
<a href="#" className="hover:text-cyan-400 transition-colors">文档</a>
<a href="#" className="hover:text-cyan-400 transition-colors">GitHub</a>
<a href="#" className="hover:text-cyan-400 transition-colors">社区</a>
</div>
</div>
</div>
</div>
</div>
)
}