mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
chore(landing): update landing
Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
341
apps/landing/DESIGN_SYSTEM.md
Normal file
341
apps/landing/DESIGN_SYSTEM.md
Normal file
@@ -0,0 +1,341 @@
|
||||
# 设计系统使用指南
|
||||
|
||||
## 🎨 颜色系统
|
||||
|
||||
### Pastel Palette Token(自动适配暗色模式)
|
||||
|
||||
```tsx
|
||||
// ✅ 推荐:使用语义化 token
|
||||
bg-background // 主背景
|
||||
bg-background-secondary // 次级背景
|
||||
bg-fill // 填充色
|
||||
bg-fill-secondary // 次级填充
|
||||
bg-material-medium // 材质:中等透明度
|
||||
bg-accent // 强调色
|
||||
|
||||
text-text // 主文本
|
||||
text-text-secondary // 次级文本
|
||||
text-text-tertiary // 三级文本(最淡)
|
||||
|
||||
border-border // 边框色
|
||||
```
|
||||
|
||||
### 自定义 Accent 混合色
|
||||
|
||||
```tsx
|
||||
// 10% ~ 80% 的 accent 混合色
|
||||
bg-accent-10 // 极淡强调色背景
|
||||
bg-accent-20
|
||||
bg-accent-30
|
||||
...
|
||||
bg-accent-80 // 强烈强调色
|
||||
```
|
||||
|
||||
### ❌ 避免硬编码
|
||||
|
||||
```tsx
|
||||
// ❌ 不要这样
|
||||
className="bg-blue-500 text-white shadow-xl"
|
||||
|
||||
// ✅ 应该这样
|
||||
className="bg-accent text-background shadow-medium"
|
||||
```
|
||||
|
||||
## 📐 阴影系统
|
||||
|
||||
从 `lib/design-tokens.ts` 导入统一阴影:
|
||||
|
||||
```tsx
|
||||
import { shadows } from '~/lib/design-tokens'
|
||||
|
||||
// 5 个标准层级
|
||||
shadows.subtle // 轻微阴影 - 用于悬停效果
|
||||
shadows.light // 轻度阴影 - 普通卡片
|
||||
shadows.medium // 中度阴影 - 浮动面板
|
||||
shadows.strong // 强阴影 - 模态框
|
||||
shadows.heavy // 重阴影 - 全屏/Hero
|
||||
```
|
||||
|
||||
### 使用示例
|
||||
|
||||
```tsx
|
||||
// 普通卡片
|
||||
<div className={clsxm('bg-background', shadows.light)}>...</div>
|
||||
|
||||
// 浮动面板
|
||||
<div className={clsxm('bg-background/80 backdrop-blur-xl', shadows.medium)}>
|
||||
...
|
||||
</div>
|
||||
|
||||
// Hero 区块
|
||||
<div className={shadows.heavy}>...</div>
|
||||
```
|
||||
|
||||
## 🔲 圆角系统
|
||||
|
||||
```tsx
|
||||
import { radius } from '~/lib/design-tokens'
|
||||
|
||||
radius.sm // rounded-xl (12px) - 小元素
|
||||
radius.md // rounded-2xl (16px) - 按钮、输入框
|
||||
radius.lg // rounded-3xl (24px) - 卡片
|
||||
radius.xl // rounded-[28px] - 大卡片
|
||||
radius['2xl'] // rounded-[32px] - Section 容器
|
||||
radius['3xl'] // rounded-[40px] - Hero 级别
|
||||
```
|
||||
|
||||
## 💨 模糊度系统
|
||||
|
||||
```tsx
|
||||
import { blur } from '~/lib/design-tokens'
|
||||
|
||||
blur.sm // backdrop-blur-sm (4px)
|
||||
blur.md // backdrop-blur-md (12px)
|
||||
blur.lg // backdrop-blur-lg (16px)
|
||||
blur.xl // backdrop-blur-xl (24px)
|
||||
blur['2xl'] // backdrop-blur-2xl (40px)
|
||||
blur['3xl'] // backdrop-blur-[60px]
|
||||
```
|
||||
|
||||
## 🎴 Glassmorphic 卡片
|
||||
|
||||
### 预设变体
|
||||
|
||||
```tsx
|
||||
import { glassCard } from '~/lib/design-tokens'
|
||||
|
||||
// 4 种预设风格
|
||||
glassCard.default // bg-background/60 + border + backdrop-blur-xl
|
||||
glassCard.elevated // bg-background/80 + backdrop-blur-2xl (更实)
|
||||
glassCard.floating // bg-background/50 + backdrop-blur-[30px] (更透)
|
||||
glassCard.gradient // border-white/15 + backdrop-blur-2xl (配合渐变)
|
||||
```
|
||||
|
||||
### 组合使用
|
||||
|
||||
```tsx
|
||||
import { glassCard, radius, shadows } from '~/lib/design-tokens'
|
||||
|
||||
<div className={clsxm(
|
||||
glassCard.floating,
|
||||
radius.lg,
|
||||
shadows.medium,
|
||||
'p-6'
|
||||
)}>
|
||||
内容
|
||||
</div>
|
||||
```
|
||||
|
||||
## 📏 Typography
|
||||
|
||||
```tsx
|
||||
import { typography } from '~/lib/design-tokens'
|
||||
|
||||
typography.hero // text-4xl sm:text-5xl lg:text-6xl font-semibold
|
||||
typography.h1 // text-3xl lg:text-4xl font-semibold
|
||||
typography.h2 // text-2xl lg:text-3xl font-semibold
|
||||
typography.h3 // text-xl lg:text-2xl font-semibold
|
||||
typography.body // text-base
|
||||
typography.small // text-sm
|
||||
typography.tiny // text-xs
|
||||
typography.label // text-xs tracking-[0.3em] uppercase font-semibold
|
||||
```
|
||||
|
||||
### 使用示例
|
||||
|
||||
```tsx
|
||||
<h1 className={clsxm(typography.hero, 'text-white')}>
|
||||
标题文字
|
||||
</h1>
|
||||
|
||||
<p className={clsxm(typography.label, 'text-text-secondary')}>
|
||||
Section Label
|
||||
</p>
|
||||
```
|
||||
|
||||
## 📦 间距系统
|
||||
|
||||
```tsx
|
||||
import { spacing } from '~/lib/design-tokens'
|
||||
|
||||
spacing.section // space-y-20 - Section 之间
|
||||
spacing.content // space-y-12 - 内容组之间
|
||||
spacing.group // space-y-6 - 组内元素
|
||||
spacing.tight // space-y-3 - 紧密元素
|
||||
```
|
||||
|
||||
## 🎯 图标容器
|
||||
|
||||
```tsx
|
||||
import { iconBox } from '~/lib/design-tokens'
|
||||
|
||||
// 3 种尺寸,自带圆角、背景、居中
|
||||
iconBox.sm // size-8 + rounded-xl
|
||||
iconBox.md // size-10 + rounded-2xl
|
||||
iconBox.lg // size-12 + rounded-2xl
|
||||
|
||||
// 使用示例
|
||||
<span className={iconBox.lg}>
|
||||
<i className="i-lucide-sparkles size-5" />
|
||||
</span>
|
||||
```
|
||||
|
||||
## ⚡ 过渡动画
|
||||
|
||||
```tsx
|
||||
import { transition } from '~/lib/design-tokens'
|
||||
|
||||
transition.fast // duration-200
|
||||
transition.normal // duration-300
|
||||
transition.slow // duration-500
|
||||
|
||||
// 使用示例
|
||||
<button className={clsxm('hover:bg-accent/90', transition.normal)}>
|
||||
按钮
|
||||
</button>
|
||||
```
|
||||
|
||||
## 🎭 Hover 效果
|
||||
|
||||
```tsx
|
||||
import { hover } from '~/lib/design-tokens'
|
||||
|
||||
hover.card // border 和 bg 变化
|
||||
hover.lift // 轻微放大 + 阴影加强
|
||||
hover.glow // 发光效果
|
||||
```
|
||||
|
||||
## 🧩 组件使用模式
|
||||
|
||||
### 标准卡片模式
|
||||
|
||||
```tsx
|
||||
import { Card } from '~/components/landing'
|
||||
import { shadows } from '~/lib/design-tokens'
|
||||
|
||||
<Card variant="floating" size="lg" hoverable>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 内容 */}
|
||||
</div>
|
||||
</Card>
|
||||
```
|
||||
|
||||
### Section 标准布局
|
||||
|
||||
```tsx
|
||||
import { spacing, typography } from '~/lib/design-tokens'
|
||||
|
||||
<section className={spacing.content}>
|
||||
<header className={spacing.tight}>
|
||||
<p className={clsxm(typography.label, 'text-text-secondary')}>
|
||||
Section Label
|
||||
</p>
|
||||
<h2 className={clsxm(typography.h1, 'text-white')}>
|
||||
主标题
|
||||
</h2>
|
||||
<p className="text-base text-text-secondary">
|
||||
描述文字
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* Section 内容 */}
|
||||
</section>
|
||||
```
|
||||
|
||||
## 🌈 渐变组合
|
||||
|
||||
```tsx
|
||||
// Hero 渐变背景
|
||||
<div className="bg-gradient-to-br from-accent/40 via-purple-600/40 to-slate-900/70">
|
||||
...
|
||||
</div>
|
||||
|
||||
// 文字渐变
|
||||
<span className="bg-gradient-to-r from-sky-300 via-accent to-purple-400 bg-clip-text text-transparent">
|
||||
渐变文字
|
||||
</span>
|
||||
```
|
||||
|
||||
## 📋 完整示例
|
||||
|
||||
### 功能卡片
|
||||
|
||||
```tsx
|
||||
import { Card } from '~/components/landing'
|
||||
import { shadows, radius, spacing, typography, iconBox } from '~/lib/design-tokens'
|
||||
import { clsxm } from '~/lib/helper'
|
||||
|
||||
const FeatureCard = () => (
|
||||
<Card variant="floating" size="lg" className={spacing.group}>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={clsxm(iconBox.lg, 'bg-accent/15 text-accent')}>
|
||||
<i className="i-lucide-cpu size-5" />
|
||||
</span>
|
||||
<div>
|
||||
<p className={clsxm(typography.h3, 'text-white')}>
|
||||
性能与体验
|
||||
</p>
|
||||
<p className={clsxm(typography.small, 'text-text-secondary')}>
|
||||
描述文字
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ul className={clsxm(spacing.tight, typography.small, 'text-text-secondary')}>
|
||||
<li className="flex items-start gap-2">
|
||||
<i className="i-lucide-check size-4 text-accent" />
|
||||
<span>功能点 1</span>
|
||||
</li>
|
||||
</ul>
|
||||
</Card>
|
||||
)
|
||||
```
|
||||
|
||||
## 🎨 颜色使用场景指南
|
||||
|
||||
| 场景 | 推荐颜色 | 示例 |
|
||||
|------|----------|------|
|
||||
| 页面背景 | `bg-background` | `<div className="bg-background">` |
|
||||
| 卡片背景 | `bg-background/60` ~ `bg-background/80` | 玻璃态透明度 |
|
||||
| 按钮主色 | `bg-accent text-background` | CTA 按钮 |
|
||||
| 按钮次级 | `bg-fill text-text` | 次要操作 |
|
||||
| 强调文字 | `text-accent` | 重要信息 |
|
||||
| 边框 | `border-border` 或 `border-white/10` | 分隔线 |
|
||||
| 悬浮蒙层 | `bg-background/50 backdrop-blur-xl` | Modal 背景 |
|
||||
|
||||
## 🚫 反模式(避免)
|
||||
|
||||
```tsx
|
||||
// ❌ 硬编码阴影
|
||||
className="shadow-[0_20px_60px_rgba(0,0,0,0.35)]"
|
||||
|
||||
// ✅ 使用 token
|
||||
className={shadows.heavy}
|
||||
|
||||
// ❌ 重复的圆角定义
|
||||
className="rounded-[32px]"
|
||||
className="rounded-[32px]"
|
||||
|
||||
// ✅ 统一使用
|
||||
className={radius['2xl']}
|
||||
|
||||
// ❌ 分散的透明度值
|
||||
className="bg-white/5"
|
||||
className="bg-white/8"
|
||||
className="bg-white/12"
|
||||
|
||||
// ✅ 语义化背景
|
||||
className={glassCard.floating}
|
||||
```
|
||||
|
||||
## 📚 参考资源
|
||||
|
||||
- **Pastel Palette**: 颜色系统基础
|
||||
- **Tailwind CSS v4**: 工具类参考
|
||||
- **Glassmorphic Design**: UI 设计理念
|
||||
- **Motion/Framer Motion**: 动画系统
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2025-11-11
|
||||
**维护者**: Design System Team
|
||||
|
||||
270
apps/landing/QUICK_START.md
Normal file
270
apps/landing/QUICK_START.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# 快速开始:Landing Page 开发指南
|
||||
|
||||
## 🚀 5 分钟上手
|
||||
|
||||
### 1. 添加新 Section
|
||||
|
||||
```tsx
|
||||
// 1️⃣ 创建组件文件:components/landing/MySection.tsx
|
||||
import { spacing, typography } from '~/lib/design-tokens'
|
||||
import { clsxm } from '~/lib/helper'
|
||||
import { Card } from './Card'
|
||||
|
||||
export const MySection = () => (
|
||||
<section className={spacing.content}>
|
||||
<header className={spacing.tight}>
|
||||
<p className={clsxm(typography.label, 'text-text-secondary')}>
|
||||
标签
|
||||
</p>
|
||||
<h2 className={clsxm(typography.h1, 'text-white')}>
|
||||
区块标题
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<Card variant="floating" size="lg">
|
||||
内容区域
|
||||
</Card>
|
||||
</section>
|
||||
)
|
||||
|
||||
// 2️⃣ 在 index.ts 导出
|
||||
export { MySection } from './MySection'
|
||||
|
||||
// 3️⃣ 在 page.tsx 使用
|
||||
import { MySection } from '~/components/landing'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="...">
|
||||
<BackgroundDecor />
|
||||
<div className="...">
|
||||
{/* ...其他 sections */}
|
||||
<MySection />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 创建自定义卡片
|
||||
|
||||
```tsx
|
||||
import { Card } from '~/components/landing'
|
||||
import { iconBox, shadows } from '~/lib/design-tokens'
|
||||
import { clsxm } from '~/lib/helper'
|
||||
|
||||
const ProductCard = ({ icon, title, price }) => (
|
||||
<Card variant="elevated" size="md" hoverable>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={clsxm(iconBox.lg, 'bg-accent/15 text-accent')}>
|
||||
<i className={icon} />
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-white">{title}</h3>
|
||||
<p className="text-sm text-text-secondary">{price}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
```
|
||||
|
||||
### 3. 自定义颜色和阴影
|
||||
|
||||
```tsx
|
||||
import { shadows, radius, blur, glassCard } from '~/lib/design-tokens'
|
||||
|
||||
// 组合使用
|
||||
<div className={clsxm(
|
||||
glassCard.floating, // 玻璃态背景
|
||||
radius.lg, // 圆角
|
||||
shadows.medium, // 阴影
|
||||
'p-6' // 内边距
|
||||
)}>
|
||||
内容
|
||||
</div>
|
||||
|
||||
// 自定义透明度
|
||||
<div className={clsxm(
|
||||
'bg-background/70', // 70% 不透明度
|
||||
blur.xl, // 模糊度
|
||||
'border border-white/10'
|
||||
)}>
|
||||
半透明容器
|
||||
</div>
|
||||
```
|
||||
|
||||
## 🎨 常用模式
|
||||
|
||||
### Pattern 1: 带图标的功能列表
|
||||
|
||||
```tsx
|
||||
const features = [
|
||||
{ icon: 'i-lucide-zap', title: '快速', desc: '毫秒级响应' },
|
||||
{ icon: 'i-lucide-shield', title: '安全', desc: '企业级加密' },
|
||||
]
|
||||
|
||||
<div className="space-y-4">
|
||||
{features.map(item => (
|
||||
<IconCard
|
||||
key={item.title}
|
||||
icon={item.icon}
|
||||
title={item.title}
|
||||
description={item.desc}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Pattern 2: 网格布局的卡片
|
||||
|
||||
```tsx
|
||||
import { FeatureCard } from '~/components/landing'
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{featureGroups.map(group => (
|
||||
<FeatureCard
|
||||
key={group.title}
|
||||
icon={group.icon}
|
||||
title={group.title}
|
||||
description={group.description}
|
||||
bullets={group.bullets}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Pattern 3: Hero 渐变背景
|
||||
|
||||
```tsx
|
||||
<div className={clsxm(
|
||||
'relative overflow-hidden',
|
||||
radius['3xl'],
|
||||
shadows.heavy,
|
||||
'bg-gradient-to-br from-accent/40 via-purple-600/40 to-slate-900/70',
|
||||
'p-10 text-white'
|
||||
)}>
|
||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(255,255,255,0.25),_transparent_55%)] opacity-80" />
|
||||
<div className="relative space-y-6">
|
||||
内容
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Pattern 4: 统计指标条
|
||||
|
||||
```tsx
|
||||
import { MetricCard } from '~/components/landing'
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<MetricCard label="用户" value="10k+" detail="活跃用户" />
|
||||
<MetricCard label="性能" value="99.9%" detail="可用性" />
|
||||
{/* 更多指标 */}
|
||||
</div>
|
||||
```
|
||||
|
||||
## 🎯 设计 Token 速查
|
||||
|
||||
### 快速导入
|
||||
|
||||
```tsx
|
||||
import {
|
||||
shadows, // 阴影
|
||||
radius, // 圆角
|
||||
blur, // 模糊
|
||||
glassCard, // 玻璃态卡片
|
||||
typography, // 文字层级
|
||||
spacing, // 间距
|
||||
iconBox, // 图标容器
|
||||
transition, // 过渡动画
|
||||
} from '~/lib/design-tokens'
|
||||
```
|
||||
|
||||
### 常用组合
|
||||
|
||||
```tsx
|
||||
// 玻璃态浮动卡片
|
||||
className={clsxm(glassCard.floating, radius.lg, shadows.medium)}
|
||||
|
||||
// 标题
|
||||
className={clsxm(typography.h1, 'text-white')}
|
||||
|
||||
// Section 间距
|
||||
className={spacing.content}
|
||||
|
||||
// 图标容器
|
||||
className={clsxm(iconBox.lg, 'bg-accent/15 text-accent')}
|
||||
```
|
||||
|
||||
## 📝 开发检查清单
|
||||
|
||||
在提交代码前,确保:
|
||||
|
||||
- [ ] 使用了设计 token(避免硬编码)
|
||||
- [ ] 单个组件文件 < 500 行
|
||||
- [ ] 导出的组件已添加到 `index.ts`
|
||||
- [ ] 使用了语义化的颜色类(`bg-background` 而非 `bg-white`)
|
||||
- [ ] 阴影使用 `shadows.*` token
|
||||
- [ ] 圆角使用 `radius.*` token
|
||||
- [ ] 运行 `pnpm lint` 无错误
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### Q: Linter 提示 `bg-background` 不是有效的 Tailwind 类?
|
||||
|
||||
**A**: 这是正常的。`bg-background` 等类来自 Pastel Palette,在运行时有效。可以忽略这个警告。
|
||||
|
||||
### Q: 如何修改全局阴影样式?
|
||||
|
||||
**A**: 编辑 `lib/design-tokens.ts` 中的 `shadows` 对象:
|
||||
|
||||
```tsx
|
||||
export const shadows = {
|
||||
medium: 'shadow-[0_8px_32px_rgba(0,0,0,0.12)]', // 修改这里
|
||||
}
|
||||
```
|
||||
|
||||
### Q: 如何添加新的卡片变体?
|
||||
|
||||
**A**: 在 `components/landing/Card.tsx` 中扩展:
|
||||
|
||||
```tsx
|
||||
interface CardProps {
|
||||
variant?: 'default' | 'elevated' | 'floating' | 'gradient' | 'custom'
|
||||
// ^^^^^^ 新增
|
||||
}
|
||||
|
||||
// 添加到 glassCard 定义
|
||||
export const glassCard = {
|
||||
// ...existing
|
||||
custom: 'bg-custom-color border-custom backdrop-blur-lg',
|
||||
}
|
||||
```
|
||||
|
||||
### Q: 如何实现暗色模式?
|
||||
|
||||
**A**: Pastel Palette 已自动支持。在根元素添加 `data-color-mode` 属性:
|
||||
|
||||
```tsx
|
||||
<html data-color-mode="dark">
|
||||
{/* 所有颜色 token 自动切换 */}
|
||||
</html>
|
||||
```
|
||||
|
||||
## 🔗 相关文档
|
||||
|
||||
- [重构详情](./REFACTOR.md) - 查看重构历史和对比
|
||||
- [设计系统](./DESIGN_SYSTEM.md) - 完整的 token 参考
|
||||
- [AGENTS.md](./AGENTS.md) - AI 开发指南
|
||||
- [主项目 README](../../README.md) - 项目整体架构
|
||||
|
||||
## 💡 提示
|
||||
|
||||
1. **优先复用组件**:先查看 `components/landing/` 是否有类似组件
|
||||
2. **保持一致性**:新组件应遵循现有的设计模式
|
||||
3. **命名规范**:组件以 `Section` 或 `Card` 结尾
|
||||
4. **Props 类型**:始终定义 TypeScript 接口
|
||||
|
||||
---
|
||||
|
||||
Happy Coding! 🚀
|
||||
|
||||
199
apps/landing/REFACTOR.md
Normal file
199
apps/landing/REFACTOR.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# Landing Page 重构文档
|
||||
|
||||
## 📋 重构概览
|
||||
|
||||
这次重构优化了 Landing Page 的代码结构、设计系统一致性和可维护性。
|
||||
|
||||
## 🎨 设计系统统一
|
||||
|
||||
### 新增:`lib/design-tokens.ts`
|
||||
|
||||
创建了统一的设计 token 系统,包括:
|
||||
|
||||
- **阴影层级**:`shadows.subtle` → `shadows.heavy`(5 个层级)
|
||||
- **圆角系统**:`radius.sm` → `radius['3xl']`(6 个尺寸)
|
||||
- **模糊度**:`blur.sm` → `blur['3xl']`(6 个级别)
|
||||
- **Glassmorphic 卡片样式**:`glassCard.default/elevated/floating/gradient`
|
||||
- **内阴影**:`innerShadow.subtle/medium/strong`
|
||||
- **过渡动画**:`transition.fast/normal/slow`
|
||||
- **Typography 层级**:`typography.hero/h1/h2/h3/body/small/tiny/label`
|
||||
- **间距系统**:`spacing.section/content/group/tight`
|
||||
- **图标容器**:`iconBox.sm/md/lg`
|
||||
|
||||
### 使用示例
|
||||
|
||||
```tsx
|
||||
import { shadows, radius, blur, glassCard } from '~/lib/design-tokens'
|
||||
|
||||
// 统一的玻璃态卡片
|
||||
<div className={clsxm(glassCard.floating, radius.lg, shadows.medium)}>
|
||||
...
|
||||
</div>
|
||||
```
|
||||
|
||||
## 🧩 组件拆分
|
||||
|
||||
### 原文件结构
|
||||
- `page.tsx`:680 行单文件,包含所有组件和数据
|
||||
|
||||
### 新文件结构
|
||||
|
||||
```
|
||||
components/landing/
|
||||
├── index.ts # 统一导出
|
||||
├── BackgroundDecor.tsx # 背景装饰层
|
||||
├── Card.tsx # 通用卡片组件(4 个变体)
|
||||
├── HeroSection.tsx # Hero 区块
|
||||
├── MetricStrip.tsx # 指标条
|
||||
├── PreviewSection.tsx # 预览区块
|
||||
├── FeatureSection.tsx # 功能区块
|
||||
├── WorkflowSection.tsx # 工作流区块
|
||||
├── TechSection.tsx # 技术栈区块
|
||||
└── CTASection.tsx # CTA 行动召唤区块
|
||||
```
|
||||
|
||||
### 主页面(`page.tsx`)
|
||||
- **重构前**:680 行
|
||||
- **重构后**:29 行(仅组合导入的组件)
|
||||
|
||||
## 🎯 核心改进
|
||||
|
||||
### 1. 颜色系统规范化
|
||||
|
||||
**重构前**:
|
||||
```tsx
|
||||
// ❌ 硬编码颜色和不一致的阴影
|
||||
className="shadow-[0_20px_60px_rgba(0,0,0,0.35)] bg-white/5"
|
||||
className="shadow-[0_25px_80px_rgba(0,0,0,0.35)]"
|
||||
className="shadow-[0_30px_80px_rgba(0,0,0,0.35)]"
|
||||
```
|
||||
|
||||
**重构后**:
|
||||
```tsx
|
||||
// ✅ 使用统一的设计 token
|
||||
className={shadows.heavy}
|
||||
className={shadows.medium}
|
||||
```
|
||||
|
||||
### 2. 阴影层级统一
|
||||
|
||||
所有阴影现在遵循 5 个标准层级:
|
||||
- `subtle`:轻微阴影(卡片悬停)
|
||||
- `light`:轻度阴影(普通卡片)
|
||||
- `medium`:中度阴影(浮动面板)
|
||||
- `strong`:强阴影(模态框)
|
||||
- `heavy`:重阴影(全屏覆盖)
|
||||
|
||||
### 3. 卡片组件抽象
|
||||
|
||||
创建了 4 个通用卡片组件:
|
||||
|
||||
#### `Card`(基础卡片)
|
||||
```tsx
|
||||
<Card variant="floating" size="md" hoverable>
|
||||
自定义内容
|
||||
</Card>
|
||||
```
|
||||
|
||||
#### `IconCard`(带图标的卡片)
|
||||
```tsx
|
||||
<IconCard
|
||||
icon="i-lucide-aperture"
|
||||
title="EXIF HUD"
|
||||
description="完整记录光圈、快门..."
|
||||
meta="f/1.4 · 1/125s"
|
||||
/>
|
||||
```
|
||||
|
||||
#### `FeatureCard`(功能卡片)
|
||||
```tsx
|
||||
<FeatureCard
|
||||
icon="i-lucide-cpu"
|
||||
title="性能与体验"
|
||||
description="WebGL viewer..."
|
||||
bullets={['GPU 管线渲染', '...更多']}
|
||||
/>
|
||||
```
|
||||
|
||||
#### `MetricCard`(指标卡片)
|
||||
```tsx
|
||||
<MetricCard
|
||||
label="WebGL 渲染"
|
||||
value="60fps"
|
||||
detail="平移 · 缩放 · HDR"
|
||||
/>
|
||||
```
|
||||
|
||||
## 📦 组件拆分原则
|
||||
|
||||
遵循项目规则:
|
||||
1. ✅ **每个文件 < 500 行**
|
||||
2. ✅ **避免重复代码**(提取通用 Card 组件)
|
||||
3. ✅ **单一职责**(每个 Section 组件只负责一个区块)
|
||||
4. ✅ **可复用性**(设计 token 可在整个项目中使用)
|
||||
|
||||
## 🔄 迁移指南
|
||||
|
||||
### 如果需要修改样式:
|
||||
|
||||
**重构前**:在每个组件中搜索硬编码的阴影/圆角值
|
||||
|
||||
**重构后**:只需修改 `lib/design-tokens.ts`
|
||||
|
||||
### 如果需要添加新区块:
|
||||
|
||||
```tsx
|
||||
// 1. 创建新组件文件
|
||||
// components/landing/NewSection.tsx
|
||||
import { Card } from './Card'
|
||||
import { shadows, radius } from '~/lib/design-tokens'
|
||||
|
||||
export const NewSection = () => (
|
||||
<section>
|
||||
<Card variant="floating" className={shadows.medium}>
|
||||
...
|
||||
</Card>
|
||||
</section>
|
||||
)
|
||||
|
||||
// 2. 在 index.ts 导出
|
||||
export { NewSection } from './NewSection'
|
||||
|
||||
// 3. 在 page.tsx 引入使用
|
||||
import { NewSection } from '~/components/landing'
|
||||
```
|
||||
|
||||
## 🎨 设计哲学
|
||||
|
||||
### Glassmorphic Depth 原则
|
||||
|
||||
所有组件都遵循「玻璃态深度设计」:
|
||||
1. **分层透明度**:`bg-background/60` → `bg-background/80`
|
||||
2. **模糊背景**:`backdrop-blur-xl` → `backdrop-blur-3xl`
|
||||
3. **精细边框**:`border border-white/10`
|
||||
4. **柔和阴影**:使用 `shadows.*` token
|
||||
5. **内阴影点缀**:`innerShadow.subtle` 营造深度
|
||||
|
||||
## 📊 重构效果
|
||||
|
||||
| 指标 | 重构前 | 重构后 | 改进 |
|
||||
|------|--------|--------|------|
|
||||
| 主文件行数 | 680 行 | 29 行 | ↓ 95.7% |
|
||||
| 组件文件数 | 1 个 | 10 个 | 模块化 |
|
||||
| 重复样式 | ~15 处 | 0 处 | 消除重复 |
|
||||
| 阴影定义 | 12+ 种 | 5 种 | 统一规范 |
|
||||
| 可维护性 | ⭐⭐ | ⭐⭐⭐⭐⭐ | 显著提升 |
|
||||
|
||||
## 🚀 后续优化建议
|
||||
|
||||
1. **响应式优化**:将断点也抽取为 token(`breakpoints.*`)
|
||||
2. **暗色模式**:基于 Pastel Palette 的 `data-color-mode` 扩展
|
||||
3. **动画库**:将 motion 动画参数也抽取为 token
|
||||
4. **性能监控**:添加 React DevTools Profiler 标记
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- 设计系统规范:`apps/landing/AGENTS.md`
|
||||
- Pastel Palette 文档:`@pastel-palette/tailwindcss`
|
||||
- 项目整体架构:根目录 `README.md`
|
||||
|
||||
@@ -11,7 +11,7 @@ const isProd = process.env.NODE_ENV === 'production'
|
||||
let nextConfig: NextConfig = {
|
||||
reactStrictMode: false,
|
||||
productionBrowserSourceMaps: true,
|
||||
output: 'standalone',
|
||||
output: 'export',
|
||||
assetPrefix: isProd ? env.ASSETPREFIX || undefined : undefined,
|
||||
compiler: {
|
||||
// reactRemoveProperties: { properties: ['^data-id$', '^data-(\\w+)-id$'] },
|
||||
|
||||
@@ -55,8 +55,8 @@
|
||||
"@tailwindcss/typography": "0.5.19",
|
||||
"@tanstack/react-query-devtools": "5.90.2",
|
||||
"@types/node": "24.10.0",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"autoprefixer": "10.4.22",
|
||||
"code-inspector-plugin": "1.2.10",
|
||||
"cross-env": "10.1.0",
|
||||
@@ -78,8 +78,6 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
"array-includes": "npm:@nolyfill/array-includes@latest",
|
||||
"array.prototype.findlastindex": "npm:@nolyfill/array.prototype.findlastindex@latest",
|
||||
"array.prototype.flat": "npm:@nolyfill/array.prototype.flat@latest",
|
||||
|
||||
@@ -14,8 +14,6 @@ import { InitInClient } from './InitInClient'
|
||||
|
||||
init()
|
||||
|
||||
export const revalidate = 60
|
||||
|
||||
export function generateViewport(): Viewport {
|
||||
return {
|
||||
themeColor: [
|
||||
|
||||
@@ -1,211 +1,20 @@
|
||||
'use client'
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { m } from 'motion/react'
|
||||
|
||||
import { Button } from '~/components/ui/button/Button'
|
||||
import { clsxm } from '~/lib/helper'
|
||||
|
||||
const heroHighlights = [
|
||||
{ title: 'WebGL Viewer', description: '60fps 缩放 / 漫游 / HDR' },
|
||||
{ title: 'Manifest 驱动', description: 'window.__MANIFEST__ 即时注入' },
|
||||
{ title: '地图探索', description: 'MapLibre GPS / Cluster / Heatmap' },
|
||||
]
|
||||
|
||||
const metrics = [
|
||||
{ label: 'WebGL 渲染', value: '60fps', detail: '平移 · 缩放 · HDR' },
|
||||
{ label: '增量同步', value: 'S3 · GitHub', detail: '多存储后端' },
|
||||
{ label: '照片节点', value: '2k+', detail: 'EXIF · Live Photo · Blurhash' },
|
||||
{ label: '多语言', value: '11', detail: 'i18n · 动态 OG' },
|
||||
]
|
||||
|
||||
const previewHighlights = [
|
||||
{
|
||||
icon: 'i-lucide-aperture',
|
||||
title: 'EXIF HUD',
|
||||
description: '完整记录光圈、快门、ISO、镜头、配方、HDR 信息。',
|
||||
meta: 'f/1.4 · 1/125s · ISO 200',
|
||||
},
|
||||
{
|
||||
icon: 'i-lucide-map',
|
||||
title: '地图探索',
|
||||
description: 'MapLibre 地图、GPS 聚合、热力探索每一次旅程。',
|
||||
meta: '84% 带 GPS · Cluster & Pin',
|
||||
},
|
||||
{
|
||||
icon: 'i-lucide-maximize',
|
||||
title: 'Fullscreen Viewer',
|
||||
description: 'WebGL 全屏查看器支持手势、Live Photo 与分享。',
|
||||
meta: '手势 · 分享 · Live',
|
||||
},
|
||||
]
|
||||
|
||||
const featureGroups = [
|
||||
{
|
||||
icon: 'i-lucide-cpu',
|
||||
title: '性能与体验',
|
||||
description: 'WebGL viewer、Masonry 布局与全屏手势带来原生级互动。',
|
||||
bullets: [
|
||||
'GPU 管线渲染 · Free transform · Tone mapping',
|
||||
'Blurhash 占位、响应式断点与浮动操作面板',
|
||||
'Glassmorphic Depth 系统 + Motion spring 动画',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'i-lucide-database',
|
||||
title: '数据与同步',
|
||||
description: 'builder 多进程处理 + manifest 驱动数据流,自动增量同步。',
|
||||
bullets: [
|
||||
'S3 / GitHub / Eagle / 本地多源存储抽象',
|
||||
'EXIF、Live Photo、Fujifilm Recipe 与缩略图生成',
|
||||
'window.__MANIFEST__ 注入,前端无感刷新',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'i-lucide-plug',
|
||||
title: '接入模式',
|
||||
description: 'SPA、Next.js SSR、be/apps/core 后端三种模式自由切换。',
|
||||
bullets: [
|
||||
'静态/SSR 共享 UI:apps/web + apps/ssr',
|
||||
'be/apps/core 以 Hono + Drizzle 提供实时能力',
|
||||
'OG 渲染 / SEO 元数据 / OpenGraph API',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'i-lucide-globe',
|
||||
title: '全球化 & 分享',
|
||||
description: '多语言、OG、分享嵌入,天然适合出海作品集。',
|
||||
bullets: [
|
||||
'i18next 平台化,11+ 语言文件',
|
||||
'动态 OG 图 + /og/[photoId] API',
|
||||
'分享/嵌入组件、一键复制链接或 iframe',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const workflowSteps = [
|
||||
{
|
||||
icon: 'i-lucide-rocket',
|
||||
title: 'Builder Pipeline',
|
||||
description: 'packages/builder 负责拉取、处理、分析与生成 manifest。',
|
||||
points: [
|
||||
'Storage Sync:S3 / GitHub / Eagle 增量拉取',
|
||||
'Format & Thumbnail:HEIC/TIFF 转码 + Blurhash',
|
||||
'EXIF / GPS / Fujifilm Recipe / HDR 元数据提取',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'i-lucide-plug-zap',
|
||||
title: 'Manifest 注入',
|
||||
description: 'apps/ssr 在 Next.js 层注入 window.__MANIFEST__,开启 SEO/OG。',
|
||||
points: [
|
||||
'index.html 模板内联 manifest',
|
||||
'OG 动态渲染 + Metadata 替换',
|
||||
'MapLibre、PhotoLoader、Viewer 共享数据源',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'i-lucide-monitor-play',
|
||||
title: 'SPA 消费',
|
||||
description: 'apps/web 作为 Vite SPA,自主渲染 Masonry、Viewer、地图与浮动 UI。',
|
||||
points: [
|
||||
'PhotoLoader 单例在客户端初始化',
|
||||
'Jotai + TanStack Query 状态与数据层',
|
||||
'Glassmorphic Depth 组件交付最终体验',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const techStacks = [
|
||||
'React 19',
|
||||
'Next.js 15',
|
||||
'Vite',
|
||||
'Tailwind CSS v4',
|
||||
'Radix UI',
|
||||
'Framer Motion 12',
|
||||
'Jotai',
|
||||
'TanStack Query',
|
||||
'MapLibre',
|
||||
'Sharp',
|
||||
'Drizzle ORM',
|
||||
'Hono',
|
||||
]
|
||||
|
||||
const integrationModes = [
|
||||
{
|
||||
title: 'Standalone SPA',
|
||||
description: '预构建 photos-manifest.json,纯静态部署即可运行。',
|
||||
points: [
|
||||
'Vite + Cloudflare Pages / Vercel / 静态空间',
|
||||
'CI 中运行 builder,增量更新 manifest',
|
||||
'无需服务器即可获得完整体验',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Next.js SSR Host',
|
||||
description: 'apps/ssr 作为 SPA 外壳,负责注入 manifest、OG、SEO。',
|
||||
points: [
|
||||
'route.ts 捕获所有路径,返回注入后的 index.html',
|
||||
'动态 /og/[photoId] 渲染社交卡片',
|
||||
'保留 SPA 的交互,同时具备 SSR 首屏',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Full Backend Flow',
|
||||
description: 'be/apps/core + be/apps/dashboard 驱动实时数据与管理。',
|
||||
points: [
|
||||
'Hono + Drizzle + PostgreSQL 全栈能力',
|
||||
'Manifest 在内存构建后注入页面',
|
||||
'Dashboard 管理、权限与指标监控',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const heroTiles = [
|
||||
{
|
||||
id: 'tile-masonry',
|
||||
title: 'Masonry Flow',
|
||||
subtitle: '自适应列 · Blurhash 过渡',
|
||||
badge: 'GPU',
|
||||
className: 'col-span-2 row-span-2',
|
||||
gradient:
|
||||
'linear-gradient(135deg, rgba(15,76,129,0.65), rgba(0,122,255,0.35))',
|
||||
},
|
||||
{
|
||||
id: 'tile-exif',
|
||||
title: 'EXIF',
|
||||
subtitle: 'Fujifilm Recipe · HDR',
|
||||
badge: 'metadata',
|
||||
className: 'col-span-1 row-span-1',
|
||||
gradient:
|
||||
'linear-gradient(135deg, rgba(47,46,113,0.85), rgba(130,124,252,0.25))',
|
||||
},
|
||||
{
|
||||
id: 'tile-map',
|
||||
title: 'Map Explorer',
|
||||
subtitle: 'GPS Cluster / Heatmap',
|
||||
badge: 'MapLibre',
|
||||
className: 'col-span-1 row-span-2',
|
||||
gradient:
|
||||
'linear-gradient(135deg, rgba(0,150,136,0.8), rgba(0,212,255,0.3))',
|
||||
},
|
||||
{
|
||||
id: 'tile-viewer',
|
||||
title: 'Fullscreen Viewer',
|
||||
subtitle: 'Live Photo / 分享',
|
||||
badge: 'viewer',
|
||||
className: 'col-span-2 row-span-1',
|
||||
gradient:
|
||||
'linear-gradient(135deg, rgba(255,87,34,0.65), rgba(255,193,7,0.25))',
|
||||
},
|
||||
]
|
||||
import { BackgroundDecor } from '~/components/landing/BackgroundDecor'
|
||||
import { CTASection } from '~/components/landing/CTASection'
|
||||
import { FeatureSection } from '~/components/landing/FeatureSection'
|
||||
import { HeroSection } from '~/components/landing/HeroSection'
|
||||
import { MetricStrip } from '~/components/landing/MetricStrip'
|
||||
import { PreviewSection } from '~/components/landing/PreviewSection'
|
||||
import { TechSection } from '~/components/landing/TechSection'
|
||||
import { WorkflowSection } from '~/components/landing/WorkflowSection'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="relative isolate overflow-hidden bg-background pb-32 text-text">
|
||||
<div className="bg-background text-text relative isolate overflow-hidden pb-32">
|
||||
<BackgroundDecor />
|
||||
|
||||
<div className="relative z-10 mx-auto flex w-full max-w-6xl flex-col gap-20 px-4 pb-16 pt-12 sm:px-6 lg:px-0">
|
||||
<div className="relative z-10 mx-auto flex w-full max-w-6xl flex-col gap-20 px-4 pt-12 pb-16 sm:px-6 lg:px-0">
|
||||
<HeroSection />
|
||||
<MetricStrip />
|
||||
<PreviewSection />
|
||||
@@ -217,421 +26,3 @@ export default function Home() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const HeroSection = () => (
|
||||
<section className="relative flex flex-col gap-12 lg:flex-row lg:items-center">
|
||||
<div className="flex flex-1 flex-col gap-8">
|
||||
<span className="inline-flex w-fit items-center gap-2 rounded-full border border-white/10 bg-white/5 px-4 py-1 text-sm text-accent/90 backdrop-blur">
|
||||
<i className="i-lucide-sparkles size-4" aria-hidden />
|
||||
Glassmorphic Depth Design System
|
||||
</span>
|
||||
|
||||
<div className="space-y-5">
|
||||
<h1 className="text-4xl font-semibold leading-tight text-white sm:text-5xl lg:text-6xl">
|
||||
Afilmory
|
||||
<span className="block bg-gradient-to-r from-sky-300 via-accent to-purple-400 bg-clip-text text-transparent">
|
||||
让照片叙事具备科幻感
|
||||
</span>
|
||||
</h1>
|
||||
<p className="max-w-2xl text-base text-text-secondary sm:text-lg">
|
||||
以 WebGL、Motion 与 manifest 驱动的全栈照片站框架。支持增量同步、EXIF
|
||||
深度解析、MapLibre 地理探索,以及 Next.js 提供的 SEO / OG
|
||||
能力,帮你轻松打造沉浸式影像叙事。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button
|
||||
asChild
|
||||
className="min-w-[160px] bg-gradient-to-r from-sky-400 to-accent text-background shadow-lg shadow-accent/30"
|
||||
>
|
||||
<Link href="https://afilmory.innei.in" target="_blank" rel="noreferrer">
|
||||
立即体验
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="secondary">
|
||||
<Link
|
||||
href="https://github.com/Afilmory/photo-gallery-site#-features"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
查看 README
|
||||
</Link>
|
||||
</Button>
|
||||
<Link
|
||||
href="https://github.com/Afilmory/photo-gallery-site"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="group inline-flex items-center gap-1.5 text-sm text-text-secondary transition hover:text-text"
|
||||
>
|
||||
<span>Star on GitHub</span>
|
||||
<i className="i-lucide-arrow-up-right size-4 transition group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<ul className="mt-4 flex flex-wrap gap-6 text-sm text-text-secondary sm:text-base">
|
||||
{heroHighlights.map((item) => (
|
||||
<li key={item.title} className="flex items-center gap-2">
|
||||
<span className="size-2 rounded-full bg-accent/70 shadow-[0_0_12px_rgba(0,122,255,0.8)]" />
|
||||
<div>
|
||||
<p className="font-medium text-text">{item.title}</p>
|
||||
<p>{item.description}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<HeroPreview />
|
||||
</section>
|
||||
)
|
||||
|
||||
const HeroPreview = () => (
|
||||
<div className="relative flex flex-1 justify-center">
|
||||
<div className="relative w-full max-w-xl">
|
||||
<m.div
|
||||
className="grid grid-cols-3 gap-3"
|
||||
initial={{ opacity: 0, y: 32 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: 'easeOut' }}
|
||||
>
|
||||
{heroTiles.map((tile, index) => (
|
||||
<m.div
|
||||
key={tile.id}
|
||||
className={clsxm(
|
||||
'group relative overflow-hidden rounded-[28px] border border-white/15 p-4 text-white shadow-[0_20px_60px_rgba(0,0,0,0.35)] backdrop-blur-2xl',
|
||||
tile.className,
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: tile.gradient,
|
||||
}}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 + index * 0.08, duration: 0.6, ease: 'easeOut' }}
|
||||
>
|
||||
<div className="flex items-center justify-between text-xs uppercase tracking-wide text-white/80">
|
||||
<span>{tile.badge}</span>
|
||||
<i className="i-lucide-chevrons-up size-4 text-white/70" aria-hidden />
|
||||
</div>
|
||||
<p className="mt-6 text-lg font-semibold">{tile.title}</p>
|
||||
<p className="text-sm text-white/80">{tile.subtitle}</p>
|
||||
<div className="pointer-events-none absolute inset-x-4 bottom-4 h-1 rounded-full bg-white/30 opacity-0 blur-lg transition duration-500 group-hover:opacity-100" />
|
||||
</m.div>
|
||||
))}
|
||||
</m.div>
|
||||
|
||||
<m.div
|
||||
aria-hidden
|
||||
className="absolute -right-6 top-4 w-56 rounded-3xl border border-white/10 bg-background/80 p-4 text-sm text-text shadow-2xl backdrop-blur-2xl"
|
||||
initial={{ opacity: 0, y: -20, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{ delay: 0.35, duration: 0.6, ease: 'easeOut' }}
|
||||
>
|
||||
<div className="flex items-center justify-between text-xs font-medium text-text-secondary">
|
||||
window.__MANIFEST__
|
||||
<span className="rounded-full bg-accent/20 px-2 py-0.5 text-[10px] text-accent">
|
||||
hydrated
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 space-y-2 font-mono text-[11px] leading-relaxed text-text-secondary">
|
||||
<p>{'{ data: 2048 photos }'}</p>
|
||||
<p>{'cameras: 12 · lenses: 18'}</p>
|
||||
<p>{'blurhash: true · livePhoto: 86'}</p>
|
||||
</div>
|
||||
</m.div>
|
||||
|
||||
<m.div
|
||||
aria-hidden
|
||||
className="absolute -left-6 bottom-6 w-60 rounded-3xl border border-white/10 bg-gradient-to-br from-emerald-500/20 via-background to-background/80 p-4 text-sm shadow-xl backdrop-blur-2xl"
|
||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{ delay: 0.45, duration: 0.6, ease: 'easeOut' }}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-xs font-semibold text-emerald-200">
|
||||
<i className="i-lucide-map-pin size-4" />
|
||||
Map Explorer
|
||||
</div>
|
||||
<p className="mt-3 text-xs text-white/80">242 geotagged stories online.</p>
|
||||
<div className="mt-4 flex items-center justify-between text-[11px] text-white/70">
|
||||
<span>Cluster · Heatmap</span>
|
||||
<span>↗︎ fully interactive</span>
|
||||
</div>
|
||||
</m.div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const MetricStrip = () => (
|
||||
<section>
|
||||
<m.div
|
||||
className="grid gap-4 rounded-[32px] border border-white/10 bg-white/5 p-6 text-sm text-text-secondary shadow-[0_25px_80px_rgba(0,0,0,0.35)] backdrop-blur-[40px] sm:grid-cols-2 lg:grid-cols-4"
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.4 }}
|
||||
transition={{ duration: 0.7, ease: 'easeOut' }}
|
||||
>
|
||||
{metrics.map((metric) => (
|
||||
<div
|
||||
key={metric.label}
|
||||
className="rounded-2xl border border-white/5 bg-background/60 px-4 py-5 shadow-inner shadow-black/20"
|
||||
>
|
||||
<p className="text-xs uppercase tracking-widest text-text-tertiary">{metric.label}</p>
|
||||
<p className="mt-3 text-2xl font-semibold text-white">{metric.value}</p>
|
||||
<p>{metric.detail}</p>
|
||||
</div>
|
||||
))}
|
||||
</m.div>
|
||||
</section>
|
||||
)
|
||||
|
||||
const PreviewSection = () => (
|
||||
<section className="grid gap-12 lg:grid-cols-[1.1fr_0.9fr] lg:items-center">
|
||||
<div className="space-y-6">
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.3em] text-text-secondary">
|
||||
即刻预览
|
||||
</p>
|
||||
<h2 className="text-3xl font-semibold text-white">沉浸式图库体验的每个细节</h2>
|
||||
<p className="text-base text-text-secondary">
|
||||
Masonry 布局、MapLibre 地图、WebGL Viewer、EXIF HUD
|
||||
等模块相互呼应,通过 motion 的 spring 动画与 glassmorphic 深度层级,构建出一个既具未来感又保持性能稳定的浏览体验。
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{previewHighlights.map((item) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className="group rounded-3xl border border-white/10 bg-background/50 px-5 py-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.08)] backdrop-blur-2xl transition hover:border-accent/40 hover:bg-background/80"
|
||||
>
|
||||
<div className="flex items-center gap-3 text-text">
|
||||
<span className="flex size-10 items-center justify-center rounded-2xl bg-accent/15 text-accent">
|
||||
<i className={clsxm('size-5', item.icon)} aria-hidden />
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<p className="text-lg font-medium">{item.title}</p>
|
||||
<p className="text-sm text-text-secondary">{item.description}</p>
|
||||
</div>
|
||||
<span className="text-xs text-text-tertiary">{item.meta}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-x-8 -top-10 -bottom-10 rounded-[40px] bg-gradient-to-b from-white/10 via-white/5 to-transparent blur-3xl" />
|
||||
<div className="relative rounded-[36px] border border-white/15 bg-white/5 p-6 shadow-[0_25px_80px_rgba(0,0,0,0.45)] backdrop-blur-[35px]">
|
||||
<div className="flex items-center justify-between text-xs text-text-secondary">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="size-2 rounded-full bg-red-400" />
|
||||
<span className="size-2 rounded-full bg-yellow-400" />
|
||||
<span className="size-2 rounded-full bg-emerald-400" />
|
||||
</div>
|
||||
<span>gallery.mxte.cc</span>
|
||||
</div>
|
||||
<div className="mt-6 grid grid-cols-3 gap-3">
|
||||
{Array.from({ length: 9 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="h-24 rounded-2xl bg-gradient-to-br from-white/20 via-white/5 to-white/0 shadow-inner shadow-black/40"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 rounded-2xl border border-white/10 bg-background/70 p-4 text-sm text-text-secondary">
|
||||
<div className="flex items-center justify-between text-text">
|
||||
<span className="font-medium">EXIF · X-T5 · XF16mmF1.4</span>
|
||||
<span className="text-xs text-text-tertiary">Map locked</span>
|
||||
</div>
|
||||
<p className="mt-2 text-text-secondary">
|
||||
GPS 35.6895 / 139.6917 · Fujifilm Classic Chrome · Blurhash ready.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
const FeatureSection = () => (
|
||||
<section className="space-y-6">
|
||||
<header className="space-y-3">
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.3em] text-text-secondary">
|
||||
核心能力
|
||||
</p>
|
||||
<h2 className="text-3xl font-semibold text-white">从性能到叙事的全链路方案</h2>
|
||||
<p className="max-w-3xl text-base text-text-secondary">
|
||||
项目按照 Performance / Data / Integrations / Global Experience
|
||||
四个维度拆分,便于独立扩展与部署。
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{featureGroups.map((group) => (
|
||||
<div
|
||||
key={group.title}
|
||||
className="flex flex-col gap-4 rounded-[32px] border border-white/10 bg-background/50 p-6 shadow-[0_20px_60px_rgba(0,0,0,0.35)] backdrop-blur-[30px]"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex size-12 items-center justify-center rounded-2xl bg-accent/15 text-accent">
|
||||
<i className={clsxm('size-5', group.icon)} aria-hidden />
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-xl font-semibold text-white">{group.title}</p>
|
||||
<p className="text-sm text-text-secondary">{group.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="space-y-2 text-sm text-text-secondary">
|
||||
{group.bullets.map((point) => (
|
||||
<li key={point} className="flex items-start gap-2">
|
||||
<i className="i-lucide-check size-4 text-accent" aria-hidden />
|
||||
<span>{point}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
const WorkflowSection = () => (
|
||||
<section className="space-y-8">
|
||||
<div>
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.3em] text-text-secondary">
|
||||
数据流
|
||||
</p>
|
||||
<h2 className="text-3xl font-semibold text-white">Builder → Manifest → SPA</h2>
|
||||
<p className="text-base text-text-secondary">
|
||||
README 中的流水线拆解为三个关键阶段,帮助你明确扩展点与监控点。
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative rounded-[36px] border border-white/10 bg-white/5 p-8 shadow-[0_30px_80px_rgba(0,0,0,0.35)] backdrop-blur-[45px]">
|
||||
<div className="absolute left-12 top-10 bottom-10 w-px bg-white/10" />
|
||||
<ol className="space-y-10">
|
||||
{workflowSteps.map((step, index) => (
|
||||
<li key={step.title} className="relative pl-16">
|
||||
<div className="absolute left-0 top-1 flex size-12 items-center justify-center rounded-full border border-accent/20 bg-accent/10 text-accent">
|
||||
<i className={clsxm('size-5', step.icon)} aria-hidden />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-semibold text-white">
|
||||
{index + 1}. {step.title}
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-text-secondary">{step.description}</p>
|
||||
<ul className="mt-3 space-y-2 text-sm text-text-secondary">
|
||||
{step.points.map((point) => (
|
||||
<li key={point} className="flex items-start gap-2">
|
||||
<span className="mt-1 size-1.5 rounded-full bg-accent" />
|
||||
<span>{point}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
const TechSection = () => (
|
||||
<section className="space-y-10">
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.3em] text-text-secondary">
|
||||
技术栈 & 接入
|
||||
</p>
|
||||
<h2 className="text-3xl font-semibold text-white">现代化前后端编排</h2>
|
||||
<p className="text-base text-text-secondary">
|
||||
React 19 + Next.js 15 + Vite + motion + Tailwind v4 + Pastel Palette token
|
||||
系统,实现 UI / 动画 / 数据的一致性。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[32px] border border-white/10 bg-background/40 p-6 shadow-[0_25px_70px_rgba(0,0,0,0.4)] backdrop-blur-[30px]">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{techStacks.map((stack, index) => (
|
||||
<m.span
|
||||
key={stack}
|
||||
className="rounded-full border border-white/15 bg-white/5 px-4 py-2 text-sm text-text-secondary"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: index * 0.05, duration: 0.4 }}
|
||||
>
|
||||
{stack}
|
||||
</m.span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid gap-4 lg:grid-cols-3">
|
||||
{integrationModes.map((mode) => (
|
||||
<div
|
||||
key={mode.title}
|
||||
className="rounded-3xl border border-white/10 bg-white/5 p-5 shadow-inner shadow-black/30"
|
||||
>
|
||||
<p className="text-lg font-semibold text-white">{mode.title}</p>
|
||||
<p className="mt-1 text-sm text-text-secondary">{mode.description}</p>
|
||||
<ul className="mt-3 space-y-2 text-sm text-text-secondary">
|
||||
{mode.points.map((point) => (
|
||||
<li key={point} className="flex items-start gap-2">
|
||||
<i className="i-lucide-minus size-4 text-accent" aria-hidden />
|
||||
<span>{point}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
const CTASection = () => (
|
||||
<section>
|
||||
<div className="relative overflow-hidden rounded-[40px] border border-white/10 bg-gradient-to-br from-accent/40 via-purple-600/40 to-slate-900/70 p-10 text-white shadow-[0_35px_80px_rgba(0,0,0,0.5)]">
|
||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(255,255,255,0.25),_transparent_55%)] opacity-80" />
|
||||
<div className="relative space-y-6">
|
||||
<p className="text-sm uppercase tracking-[0.4em] text-white/70">Ready?</p>
|
||||
<h2 className="text-4xl font-semibold leading-tight">
|
||||
构建属于你的 Afilmory,<span className="text-accent">今日即可上线。</span>
|
||||
</h2>
|
||||
<p className="text-lg text-white/80">
|
||||
结合 builder、apps/web、apps/ssr 与 be/apps/core,五分钟完成部署,随时扩展自定义
|
||||
UI、数据源或地图风格。
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<Button
|
||||
asChild
|
||||
className="min-w-[160px] border border-white/30 bg-white/10 text-white hover:bg-white/20"
|
||||
>
|
||||
<Link href="https://github.com/Afilmory/photo-gallery-site" target="_blank" rel="noreferrer">
|
||||
Fork & 部署
|
||||
</Link>
|
||||
</Button>
|
||||
<Link
|
||||
href="https://afilmory.innei.in"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-2 text-sm font-medium text-white/80 hover:text-white"
|
||||
>
|
||||
查看线上示例
|
||||
<i className="i-lucide-arrow-up-right size-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
const BackgroundDecor = () => (
|
||||
<>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'radial-gradient(circle at 20% 20%, rgba(0,122,255,0.35), transparent 45%), radial-gradient(circle at 80% 0%, rgba(156,39,176,0.25), transparent 40%), radial-gradient(circle at 50% 80%, rgba(0,150,136,0.25), transparent 45%)',
|
||||
}}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(120deg,rgba(255,255,255,0.05)_0%,transparent_35%,rgba(255,255,255,0.05)_70%)] opacity-40" />
|
||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle,rgba(255,255,255,0.07)_1px,transparent_1px)] [background-size:120px_120px]" />
|
||||
</>
|
||||
)
|
||||
|
||||
19
apps/landing/src/components/landing/BackgroundDecor.tsx
Normal file
19
apps/landing/src/components/landing/BackgroundDecor.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 背景装饰层
|
||||
* 提供径向渐变、网格和光线效果
|
||||
*/
|
||||
|
||||
export const BackgroundDecor = () => (
|
||||
<>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'radial-gradient(circle at 20% 20%, rgba(0,122,255,0.35), transparent 45%), radial-gradient(circle at 80% 0%, rgba(156,39,176,0.25), transparent 40%), radial-gradient(circle at 50% 80%, rgba(0,150,136,0.25), transparent 45%)',
|
||||
}}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(120deg,rgba(255,255,255,0.05)_0%,transparent_35%,rgba(255,255,255,0.05)_70%)] opacity-40" />
|
||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle,rgba(255,255,255,0.07)_1px,transparent_1px)] [background-size:120px_120px]" />
|
||||
</>
|
||||
)
|
||||
57
apps/landing/src/components/landing/CTASection.tsx
Normal file
57
apps/landing/src/components/landing/CTASection.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
|
||||
import { Button } from '~/components/ui/button/Button'
|
||||
import { radius, shadows } from '~/lib/design-tokens'
|
||||
import { clsxm } from '~/lib/helper'
|
||||
|
||||
export const CTASection = () => (
|
||||
<section>
|
||||
<div
|
||||
className={clsxm(
|
||||
'relative overflow-hidden border border-white/10 bg-gradient-to-br from-accent/40 via-purple-600/40 to-slate-900/70 p-10 text-white',
|
||||
radius['3xl'],
|
||||
shadows.heavy,
|
||||
)}
|
||||
>
|
||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(255,255,255,0.25),_transparent_55%)] opacity-80" />
|
||||
<div className="relative space-y-6">
|
||||
<p className="text-sm tracking-[0.4em] text-white/70 uppercase">
|
||||
Ready?
|
||||
</p>
|
||||
<h2 className="text-4xl leading-tight font-semibold">
|
||||
构建属于你的 Afilmory,
|
||||
<span className="text-accent">今日即可上线。</span>
|
||||
</h2>
|
||||
<p className="text-lg text-white/80">
|
||||
结合 builder、apps/web、apps/ssr 与
|
||||
be/apps/core,五分钟完成部署,随时扩展自定义 UI、数据源或地图风格。
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<Button
|
||||
asChild
|
||||
className="min-w-[160px] border border-white/30 bg-white/10 text-white hover:bg-white/20"
|
||||
>
|
||||
<Link
|
||||
href="https://github.com/Afilmory/photo-gallery-site"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Fork & 部署
|
||||
</Link>
|
||||
</Button>
|
||||
<Link
|
||||
href="https://afilmory.innei.in"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-2 text-sm font-medium text-white/80 hover:text-white"
|
||||
>
|
||||
查看线上示例
|
||||
<i className="i-lucide-arrow-up-right size-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
134
apps/landing/src/components/landing/Card.tsx
Normal file
134
apps/landing/src/components/landing/Card.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* 通用卡片组件 - 基于 Glassmorphic Depth Design System
|
||||
*/
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import { glassCard, radius, shadows } from '~/lib/design-tokens'
|
||||
import { clsxm } from '~/lib/helper'
|
||||
|
||||
interface CardProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
variant?: 'default' | 'elevated' | 'floating' | 'gradient'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
hoverable?: boolean
|
||||
gradient?: string
|
||||
}
|
||||
|
||||
const sizeStyles = {
|
||||
sm: 'p-4',
|
||||
md: 'p-6',
|
||||
lg: 'p-8',
|
||||
}
|
||||
|
||||
export const Card = ({
|
||||
children,
|
||||
className,
|
||||
variant = 'default',
|
||||
size = 'md',
|
||||
hoverable = false,
|
||||
gradient,
|
||||
}: CardProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsxm(
|
||||
glassCard[variant],
|
||||
radius.lg,
|
||||
shadows.medium,
|
||||
sizeStyles[size],
|
||||
hoverable &&
|
||||
'hover:border-accent/30 hover:bg-background/90 transition-all duration-300',
|
||||
className,
|
||||
)}
|
||||
style={gradient ? { backgroundImage: gradient } : undefined}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface IconCardProps {
|
||||
icon: string
|
||||
title: string
|
||||
description?: string
|
||||
meta?: string
|
||||
hoverable?: boolean
|
||||
}
|
||||
|
||||
export const IconCard = ({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
meta,
|
||||
hoverable = true,
|
||||
}: IconCardProps) => {
|
||||
return (
|
||||
<Card variant="default" size="md" hoverable={hoverable} className="group">
|
||||
<div className="text-text flex items-center gap-3">
|
||||
<span className="bg-accent/15 text-accent flex size-10 items-center justify-center rounded-2xl">
|
||||
<i className={clsxm('size-5', icon)} aria-hidden />
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<p className="text-lg font-medium">{title}</p>
|
||||
{description && (
|
||||
<p className="text-text-secondary text-sm">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
{meta && <span className="text-text-tertiary text-xs">{meta}</span>}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
interface FeatureCardProps {
|
||||
icon: string
|
||||
title: string
|
||||
description: string
|
||||
bullets: string[]
|
||||
}
|
||||
|
||||
export const FeatureCard = ({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
bullets,
|
||||
}: FeatureCardProps) => {
|
||||
return (
|
||||
<Card variant="floating" size="lg" className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="bg-accent/15 text-accent flex size-12 items-center justify-center rounded-2xl">
|
||||
<i className={clsxm('size-5', icon)} aria-hidden />
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-xl font-semibold text-white">{title}</p>
|
||||
<p className="text-text-secondary text-sm">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="text-text-secondary space-y-2 text-sm">
|
||||
{bullets.map((point) => (
|
||||
<li key={point} className="flex items-start gap-2">
|
||||
<i className="i-lucide-check text-accent size-4" aria-hidden />
|
||||
<span>{point}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
interface MetricCardProps {
|
||||
label: string
|
||||
value: string
|
||||
detail: string
|
||||
}
|
||||
|
||||
export const MetricCard = ({ label, value, detail }: MetricCardProps) => {
|
||||
return (
|
||||
<div className="rounded-2xl border border-white/10 bg-white/50 px-4 py-5 shadow-inner shadow-white/30">
|
||||
<p className="text-xs tracking-widest text-gray-600 uppercase">{label}</p>
|
||||
<p className="mt-3 text-2xl font-semibold text-gray-900">{value}</p>
|
||||
<p className="text-gray-700">{detail}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
78
apps/landing/src/components/landing/FeatureSection.tsx
Normal file
78
apps/landing/src/components/landing/FeatureSection.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
'use client'
|
||||
|
||||
import { spacing, typography } from '~/lib/design-tokens'
|
||||
import { clsxm } from '~/lib/helper'
|
||||
|
||||
import { FeatureCard } from './Card'
|
||||
|
||||
const featureGroups = [
|
||||
{
|
||||
icon: 'i-lucide-cpu',
|
||||
title: '性能与体验',
|
||||
description: 'WebGL viewer、Masonry 布局与全屏手势带来原生级互动。',
|
||||
bullets: [
|
||||
'GPU 管线渲染 · Free transform · Tone mapping',
|
||||
'Blurhash 占位、响应式断点与浮动操作面板',
|
||||
'Glassmorphic Depth 系统 + Motion spring 动画',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'i-lucide-database',
|
||||
title: '数据与同步',
|
||||
description: 'builder 多进程处理 + manifest 驱动数据流,自动增量同步。',
|
||||
bullets: [
|
||||
'S3 / GitHub / Eagle / 本地多源存储抽象',
|
||||
'EXIF、Live Photo、Fujifilm Recipe 与缩略图生成',
|
||||
'window.__MANIFEST__ 注入,前端无感刷新',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'i-lucide-plug',
|
||||
title: '接入模式',
|
||||
description: 'SPA、Next.js SSR、be/apps/core 后端三种模式自由切换。',
|
||||
bullets: [
|
||||
'静态/SSR 共享 UI:apps/web + apps/ssr',
|
||||
'be/apps/core 以 Hono + Drizzle 提供实时能力',
|
||||
'OG 渲染 / SEO 元数据 / OpenGraph API',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'i-lucide-globe',
|
||||
title: '全球化 & 分享',
|
||||
description: '多语言、OG、分享嵌入,天然适合出海作品集。',
|
||||
bullets: [
|
||||
'i18next 平台化,11+ 语言文件',
|
||||
'动态 OG 图 + /og/[photoId] API',
|
||||
'分享/嵌入组件、一键复制链接或 iframe',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const FeatureSection = () => (
|
||||
<section className={spacing.content}>
|
||||
<header className={spacing.tight}>
|
||||
<p className="text-text-secondary text-sm font-semibold tracking-[0.3em] uppercase">
|
||||
核心能力
|
||||
</p>
|
||||
<h2 className={clsxm(typography.h1, 'text-white')}>
|
||||
从性能到叙事的全链路方案
|
||||
</h2>
|
||||
<p className="text-text-secondary max-w-3xl text-base">
|
||||
项目按照 Performance / Data / Integrations / Global Experience
|
||||
四个维度拆分,便于独立扩展与部署。
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{featureGroups.map((group) => (
|
||||
<FeatureCard
|
||||
key={group.title}
|
||||
icon={group.icon}
|
||||
title={group.title}
|
||||
description={group.description}
|
||||
bullets={group.bullets}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
215
apps/landing/src/components/landing/HeroSection.tsx
Normal file
215
apps/landing/src/components/landing/HeroSection.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
'use client'
|
||||
|
||||
import { m } from 'motion/react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { Button } from '~/components/ui/button/Button'
|
||||
import { blur } from '~/lib/design-tokens'
|
||||
import { clsxm } from '~/lib/helper'
|
||||
|
||||
const heroHighlights = [
|
||||
{ title: 'WebGL Viewer', description: '60fps 缩放 / 漫游 / HDR' },
|
||||
{ title: 'Manifest 驱动', description: 'window.__MANIFEST__ 即时注入' },
|
||||
{ title: '地图探索', description: 'MapLibre GPS / Cluster / Heatmap' },
|
||||
]
|
||||
|
||||
const heroTiles = [
|
||||
{
|
||||
id: 'tile-masonry',
|
||||
title: 'Masonry Flow',
|
||||
subtitle: '自适应列 · Blurhash 过渡',
|
||||
badge: 'GPU',
|
||||
className: 'col-span-2 row-span-2',
|
||||
gradient:
|
||||
'linear-gradient(135deg, rgba(100,160,220,0.35), rgba(140,200,255,0.2))',
|
||||
},
|
||||
{
|
||||
id: 'tile-exif',
|
||||
title: 'EXIF',
|
||||
subtitle: 'Fujifilm Recipe · HDR',
|
||||
badge: 'metadata',
|
||||
className: 'col-span-1 row-span-1',
|
||||
gradient:
|
||||
'linear-gradient(135deg, rgba(160,140,200,0.35), rgba(200,180,255,0.2))',
|
||||
},
|
||||
{
|
||||
id: 'tile-map',
|
||||
title: 'Map Explorer',
|
||||
subtitle: 'GPS Cluster / Heatmap',
|
||||
badge: 'MapLibre',
|
||||
className: 'col-span-1 row-span-2',
|
||||
gradient:
|
||||
'linear-gradient(135deg, rgba(100,200,180,0.35), rgba(140,240,220,0.2))',
|
||||
},
|
||||
{
|
||||
id: 'tile-viewer',
|
||||
title: 'Fullscreen Viewer',
|
||||
subtitle: 'Live Photo / 分享',
|
||||
badge: 'viewer',
|
||||
className: 'col-span-2 row-span-1',
|
||||
gradient:
|
||||
'linear-gradient(135deg, rgba(255,180,120,0.35), rgba(255,210,160,0.2))',
|
||||
},
|
||||
]
|
||||
|
||||
export const HeroSection = () => (
|
||||
<section className="relative flex flex-col gap-12 lg:flex-row lg:items-center">
|
||||
<div className="flex flex-1 flex-col gap-8">
|
||||
<span className="text-accent/90 inline-flex w-fit items-center gap-2 rounded-full border border-white/10 bg-white/5 px-4 py-1 text-sm backdrop-blur">
|
||||
<i className="i-lucide-sparkles size-4" aria-hidden />
|
||||
Glassmorphic Depth Design System
|
||||
</span>
|
||||
|
||||
<div className="space-y-5">
|
||||
<h1 className="text-4xl leading-tight font-semibold text-white sm:text-5xl lg:text-6xl">
|
||||
Afilmory
|
||||
<span className="via-accent block bg-gradient-to-r from-sky-300 to-purple-400 bg-clip-text text-transparent">
|
||||
让照片叙事具备科幻感
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-text-secondary max-w-2xl text-base sm:text-lg">
|
||||
以 WebGL、Motion 与 manifest 驱动的全栈照片站框架。支持增量同步、EXIF
|
||||
深度解析、MapLibre 地理探索,以及 Next.js 提供的 SEO / OG
|
||||
能力,帮你轻松打造沉浸式影像叙事。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button
|
||||
asChild
|
||||
className="to-accent text-background shadow-accent/30 min-w-[160px] bg-gradient-to-r from-sky-400 shadow-lg"
|
||||
>
|
||||
<Link
|
||||
href="https://afilmory.innei.in"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
立即体验
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="secondary">
|
||||
<Link
|
||||
href="https://github.com/Afilmory/photo-gallery-site#-features"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
查看 README
|
||||
</Link>
|
||||
</Button>
|
||||
<Link
|
||||
href="https://github.com/Afilmory/photo-gallery-site"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="group text-text-secondary hover:text-text inline-flex items-center gap-1.5 text-sm transition"
|
||||
>
|
||||
<span>Star on GitHub</span>
|
||||
<i className="i-lucide-arrow-up-right size-4 transition group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<ul className="text-text-secondary mt-4 flex flex-wrap gap-6 text-sm sm:text-base">
|
||||
{heroHighlights.map((item) => (
|
||||
<li key={item.title} className="flex items-center gap-2">
|
||||
<span className="bg-accent/70 size-2 rounded-full shadow-[0_0_12px_rgba(0,122,255,0.8)]" />
|
||||
<div>
|
||||
<p className="text-text font-medium">{item.title}</p>
|
||||
<p>{item.description}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<HeroPreview />
|
||||
</section>
|
||||
)
|
||||
|
||||
const HeroPreview = () => (
|
||||
<div className="relative flex flex-1 justify-center">
|
||||
<div className="relative w-full max-w-xl">
|
||||
<m.div
|
||||
className="grid grid-cols-3 gap-3"
|
||||
initial={{ opacity: 0, y: 32 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: 'easeOut' }}
|
||||
>
|
||||
{heroTiles.map((tile, index) => (
|
||||
<m.div
|
||||
key={tile.id}
|
||||
className={clsxm(
|
||||
'group relative overflow-hidden rounded-[28px] border border-white/20 bg-white/40 p-4 text-gray-800 shadow-[0_8px_32px_rgba(0,0,0,0.06)] backdrop-blur-xl',
|
||||
tile.className,
|
||||
)}
|
||||
style={{ backgroundImage: tile.gradient }}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
delay: 0.2 + index * 0.08,
|
||||
duration: 0.6,
|
||||
ease: 'easeOut',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between text-xs tracking-wide text-gray-600 uppercase">
|
||||
<span>{tile.badge}</span>
|
||||
<i
|
||||
className="i-lucide-chevrons-up size-4 text-gray-500"
|
||||
aria-hidden
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-6 text-lg font-semibold text-gray-900">
|
||||
{tile.title}
|
||||
</p>
|
||||
<p className="text-sm text-gray-700">{tile.subtitle}</p>
|
||||
<div className="pointer-events-none absolute inset-x-4 bottom-4 h-1 rounded-full bg-white/50 opacity-0 blur-lg transition duration-500 group-hover:opacity-100" />
|
||||
</m.div>
|
||||
))}
|
||||
</m.div>
|
||||
|
||||
<m.div
|
||||
aria-hidden
|
||||
className={clsxm(
|
||||
'absolute top-4 -right-6 w-56 rounded-3xl border border-white/20 bg-white/60 p-4 text-sm text-gray-800 shadow-[0_8px_32px_rgba(0,0,0,0.08)]',
|
||||
blur['2xl'],
|
||||
)}
|
||||
initial={{ opacity: 0, y: -20, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{ delay: 0.35, duration: 0.6, ease: 'easeOut' }}
|
||||
>
|
||||
<div className="flex items-center justify-between text-xs font-medium text-gray-600">
|
||||
window.__MANIFEST__
|
||||
<span className="rounded-full bg-cyan-100 px-2 py-0.5 text-[10px] text-cyan-700">
|
||||
hydrated
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 space-y-2 font-mono text-[11px] leading-relaxed text-gray-700">
|
||||
<p>{'{ data: 2048 photos }'}</p>
|
||||
<p>{'cameras: 12 · lenses: 18'}</p>
|
||||
<p>{'blurhash: true · livePhoto: 86'}</p>
|
||||
</div>
|
||||
</m.div>
|
||||
|
||||
<m.div
|
||||
aria-hidden
|
||||
className={clsxm(
|
||||
'absolute bottom-6 -left-6 w-60 rounded-3xl border border-white/20 bg-gradient-to-br from-emerald-100/60 to-white/60 p-4 text-sm shadow-[0_8px_32px_rgba(0,0,0,0.08)]',
|
||||
blur['2xl'],
|
||||
)}
|
||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{ delay: 0.45, duration: 0.6, ease: 'easeOut' }}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-xs font-semibold text-emerald-700">
|
||||
<i className="i-lucide-map-pin size-4" />
|
||||
Map Explorer
|
||||
</div>
|
||||
<p className="mt-3 text-xs text-gray-700">
|
||||
242 geotagged stories online.
|
||||
</p>
|
||||
<div className="mt-4 flex items-center justify-between text-[11px] text-gray-600">
|
||||
<span>Cluster · Heatmap</span>
|
||||
<span>↗︎ fully interactive</span>
|
||||
</div>
|
||||
</m.div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
41
apps/landing/src/components/landing/MetricStrip.tsx
Normal file
41
apps/landing/src/components/landing/MetricStrip.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
'use client'
|
||||
|
||||
import { m } from 'motion/react'
|
||||
|
||||
import { blur, radius, shadows } from '~/lib/design-tokens'
|
||||
import { clsxm } from '~/lib/helper'
|
||||
|
||||
import { MetricCard } from './Card'
|
||||
|
||||
const metrics = [
|
||||
{ label: 'WebGL 渲染', value: '60fps', detail: '平移 · 缩放 · HDR' },
|
||||
{ label: '增量同步', value: 'S3 · GitHub', detail: '多存储后端' },
|
||||
{ label: '照片节点', value: '2k+', detail: 'EXIF · Live Photo · Blurhash' },
|
||||
{ label: '多语言', value: '11', detail: 'i18n · 动态 OG' },
|
||||
]
|
||||
|
||||
export const MetricStrip = () => (
|
||||
<section>
|
||||
<m.div
|
||||
className={clsxm(
|
||||
'grid gap-4 border border-white/15 bg-white/40 p-6 text-sm text-gray-700 sm:grid-cols-2 lg:grid-cols-4',
|
||||
radius['2xl'],
|
||||
blur.xl,
|
||||
shadows.medium,
|
||||
)}
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.4 }}
|
||||
transition={{ duration: 0.7, ease: 'easeOut' }}
|
||||
>
|
||||
{metrics.map((metric) => (
|
||||
<MetricCard
|
||||
key={metric.label}
|
||||
label={metric.label}
|
||||
value={metric.value}
|
||||
detail={metric.detail}
|
||||
/>
|
||||
))}
|
||||
</m.div>
|
||||
</section>
|
||||
)
|
||||
99
apps/landing/src/components/landing/PreviewSection.tsx
Normal file
99
apps/landing/src/components/landing/PreviewSection.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
'use client'
|
||||
|
||||
import { blur, radius, shadows, typography } from '~/lib/design-tokens'
|
||||
import { clsxm } from '~/lib/helper'
|
||||
|
||||
import { IconCard } from './Card'
|
||||
|
||||
const previewHighlights = [
|
||||
{
|
||||
icon: 'i-lucide-aperture',
|
||||
title: 'EXIF HUD',
|
||||
description: '完整记录光圈、快门、ISO、镜头、配方、HDR 信息。',
|
||||
meta: 'f/1.4 · 1/125s · ISO 200',
|
||||
},
|
||||
{
|
||||
icon: 'i-lucide-map',
|
||||
title: '地图探索',
|
||||
description: 'MapLibre 地图、GPS 聚合、热力探索每一次旅程。',
|
||||
meta: '84% 带 GPS · Cluster & Pin',
|
||||
},
|
||||
{
|
||||
icon: 'i-lucide-maximize',
|
||||
title: 'Fullscreen Viewer',
|
||||
description: 'WebGL 全屏查看器支持手势、Live Photo 与分享。',
|
||||
meta: '手势 · 分享 · Live',
|
||||
},
|
||||
]
|
||||
|
||||
export const PreviewSection = () => (
|
||||
<section className="grid gap-12 lg:grid-cols-[1.1fr_0.9fr] lg:items-center">
|
||||
<div className="space-y-6">
|
||||
<p className="text-text-secondary text-sm font-semibold tracking-[0.3em] uppercase">
|
||||
即刻预览
|
||||
</p>
|
||||
<h2 className={clsxm(typography.h1, 'text-white')}>
|
||||
沉浸式图库体验的每个细节
|
||||
</h2>
|
||||
<p className="text-text-secondary text-base">
|
||||
Masonry 布局、MapLibre 地图、WebGL Viewer、EXIF HUD 等模块相互呼应,通过
|
||||
motion 的 spring 动画与 glassmorphic
|
||||
深度层级,构建出一个既具未来感又保持性能稳定的浏览体验。
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{previewHighlights.map((item) => (
|
||||
<IconCard
|
||||
key={item.title}
|
||||
icon={item.icon}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
meta={item.meta}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PreviewMockup />
|
||||
</section>
|
||||
)
|
||||
|
||||
const PreviewMockup = () => (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-x-8 -top-10 -bottom-10 rounded-[40px] bg-gradient-to-b from-white/10 via-white/5 to-transparent blur-3xl" />
|
||||
<div
|
||||
className={clsxm(
|
||||
'relative border border-white/15 bg-white/5 p-6',
|
||||
radius['3xl'],
|
||||
blur['3xl'],
|
||||
shadows.heavy,
|
||||
)}
|
||||
>
|
||||
<div className="text-text-secondary flex items-center justify-between text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="size-2 rounded-full bg-red-400" />
|
||||
<span className="size-2 rounded-full bg-yellow-400" />
|
||||
<span className="size-2 rounded-full bg-emerald-400" />
|
||||
</div>
|
||||
<span>gallery.mxte.cc</span>
|
||||
</div>
|
||||
<div className="mt-6 grid grid-cols-3 gap-3">
|
||||
{Array.from({ length: 9 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="h-24 rounded-2xl bg-gradient-to-br from-white/20 via-white/5 to-white/0 shadow-inner shadow-black/40"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="bg-background/70 text-text-secondary mt-6 rounded-2xl border border-white/10 p-4 text-sm">
|
||||
<div className="text-text flex items-center justify-between">
|
||||
<span className="font-medium">EXIF · X-T5 · XF16mmF1.4</span>
|
||||
<span className="text-text-tertiary text-xs">Map locked</span>
|
||||
</div>
|
||||
<p className="text-text-secondary mt-2">
|
||||
GPS 35.6895 / 139.6917 · Fujifilm Classic Chrome · Blurhash ready.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
115
apps/landing/src/components/landing/TechSection.tsx
Normal file
115
apps/landing/src/components/landing/TechSection.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
'use client'
|
||||
|
||||
import { m } from 'motion/react'
|
||||
|
||||
import { blur, radius, shadows, spacing, typography } from '~/lib/design-tokens'
|
||||
import { clsxm } from '~/lib/helper'
|
||||
|
||||
const techStacks = [
|
||||
'React 19',
|
||||
'Next.js 15',
|
||||
'Vite',
|
||||
'Tailwind CSS v4',
|
||||
'Radix UI',
|
||||
'Framer Motion 12',
|
||||
'Jotai',
|
||||
'TanStack Query',
|
||||
'MapLibre',
|
||||
'Sharp',
|
||||
'Drizzle ORM',
|
||||
'Hono',
|
||||
]
|
||||
|
||||
const integrationModes = [
|
||||
{
|
||||
title: 'Standalone SPA',
|
||||
description: '预构建 photos-manifest.json,纯静态部署即可运行。',
|
||||
points: [
|
||||
'Vite + Cloudflare Pages / Vercel / 静态空间',
|
||||
'CI 中运行 builder,增量更新 manifest',
|
||||
'无需服务器即可获得完整体验',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Next.js SSR Host',
|
||||
description: 'apps/ssr 作为 SPA 外壳,负责注入 manifest、OG、SEO。',
|
||||
points: [
|
||||
'route.ts 捕获所有路径,返回注入后的 index.html',
|
||||
'动态 /og/[photoId] 渲染社交卡片',
|
||||
'保留 SPA 的交互,同时具备 SSR 首屏',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Full Backend Flow',
|
||||
description: 'be/apps/core + be/apps/dashboard 驱动实时数据与管理。',
|
||||
points: [
|
||||
'Hono + Drizzle + PostgreSQL 全栈能力',
|
||||
'Manifest 在内存构建后注入页面',
|
||||
'Dashboard 管理、权限与指标监控',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const TechSection = () => (
|
||||
<section className={spacing.content}>
|
||||
<div className={spacing.tight}>
|
||||
<p className="text-text-secondary text-sm font-semibold tracking-[0.3em] uppercase">
|
||||
技术栈 & 接入
|
||||
</p>
|
||||
<h2 className={clsxm(typography.h1, 'text-white')}>现代化前后端编排</h2>
|
||||
<p className="text-text-secondary text-base">
|
||||
React 19 + Next.js 15 + Vite + motion + Tailwind v4 + Pastel Palette
|
||||
token 系统,实现 UI / 动画 / 数据的一致性。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsxm(
|
||||
'border border-white/10 bg-background/40 p-6',
|
||||
radius['2xl'],
|
||||
blur['3xl'],
|
||||
shadows.heavy,
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{techStacks.map((stack, index) => (
|
||||
<m.span
|
||||
key={stack}
|
||||
className="text-text-secondary rounded-full border border-white/15 bg-white/5 px-4 py-2 text-sm"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: index * 0.05, duration: 0.4 }}
|
||||
>
|
||||
{stack}
|
||||
</m.span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid gap-4 lg:grid-cols-3">
|
||||
{integrationModes.map((mode) => (
|
||||
<div
|
||||
key={mode.title}
|
||||
className="rounded-3xl border border-white/10 bg-white/5 p-5 shadow-inner shadow-black/30"
|
||||
>
|
||||
<p className="text-lg font-semibold text-white">{mode.title}</p>
|
||||
<p className="text-text-secondary mt-1 text-sm">
|
||||
{mode.description}
|
||||
</p>
|
||||
<ul className="text-text-secondary mt-3 space-y-2 text-sm">
|
||||
{mode.points.map((point) => (
|
||||
<li key={point} className="flex items-start gap-2">
|
||||
<i
|
||||
className="i-lucide-minus text-accent size-4"
|
||||
aria-hidden
|
||||
/>
|
||||
<span>{point}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
90
apps/landing/src/components/landing/WorkflowSection.tsx
Normal file
90
apps/landing/src/components/landing/WorkflowSection.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
'use client'
|
||||
|
||||
import { blur, radius, shadows, spacing, typography } from '~/lib/design-tokens'
|
||||
import { clsxm } from '~/lib/helper'
|
||||
|
||||
const workflowSteps = [
|
||||
{
|
||||
icon: 'i-lucide-rocket',
|
||||
title: 'Builder Pipeline',
|
||||
description: 'packages/builder 负责拉取、处理、分析与生成 manifest。',
|
||||
points: [
|
||||
'Storage Sync:S3 / GitHub / Eagle 增量拉取',
|
||||
'Format & Thumbnail:HEIC/TIFF 转码 + Blurhash',
|
||||
'EXIF / GPS / Fujifilm Recipe / HDR 元数据提取',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'i-lucide-plug-zap',
|
||||
title: 'Manifest 注入',
|
||||
description:
|
||||
'apps/ssr 在 Next.js 层注入 window.__MANIFEST__,开启 SEO/OG。',
|
||||
points: [
|
||||
'index.html 模板内联 manifest',
|
||||
'OG 动态渲染 + Metadata 替换',
|
||||
'MapLibre、PhotoLoader、Viewer 共享数据源',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'i-lucide-monitor-play',
|
||||
title: 'SPA 消费',
|
||||
description:
|
||||
'apps/web 作为 Vite SPA,自主渲染 Masonry、Viewer、地图与浮动 UI。',
|
||||
points: [
|
||||
'PhotoLoader 单例在客户端初始化',
|
||||
'Jotai + TanStack Query 状态与数据层',
|
||||
'Glassmorphic Depth 组件交付最终体验',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const WorkflowSection = () => (
|
||||
<section className={spacing.content}>
|
||||
<div>
|
||||
<p className="text-text-secondary text-sm font-semibold tracking-[0.3em] uppercase">
|
||||
数据流
|
||||
</p>
|
||||
<h2 className={clsxm(typography.h1, 'text-white')}>
|
||||
Builder → Manifest → SPA
|
||||
</h2>
|
||||
<p className="text-text-secondary text-base">
|
||||
README 中的流水线拆解为三个关键阶段,帮助你明确扩展点与监控点。
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className={clsxm(
|
||||
'relative border border-white/10 bg-white/5 p-8',
|
||||
radius['3xl'],
|
||||
blur['3xl'],
|
||||
shadows.heavy,
|
||||
)}
|
||||
>
|
||||
<div className="absolute top-10 bottom-10 left-12 w-px bg-white/10" />
|
||||
<ol className="space-y-10">
|
||||
{workflowSteps.map((step, index) => (
|
||||
<li key={step.title} className="relative pl-16">
|
||||
<div className="border-accent/20 bg-accent/10 text-accent absolute top-1 left-0 flex size-12 items-center justify-center rounded-full border">
|
||||
<i className={clsxm('size-5', step.icon)} aria-hidden />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-semibold text-white">
|
||||
{index + 1}. {step.title}
|
||||
</p>
|
||||
<p className="text-text-secondary mt-2 text-sm">
|
||||
{step.description}
|
||||
</p>
|
||||
<ul className="text-text-secondary mt-3 space-y-2 text-sm">
|
||||
{step.points.map((point) => (
|
||||
<li key={point} className="flex items-start gap-2">
|
||||
<span className="bg-accent mt-1 size-1.5 rounded-full" />
|
||||
<span>{point}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
14
apps/landing/src/components/landing/index.ts
Normal file
14
apps/landing/src/components/landing/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Landing Page Components
|
||||
* 统一导出所有 landing 页面组件
|
||||
*/
|
||||
|
||||
export { BackgroundDecor } from './BackgroundDecor'
|
||||
export { Card, FeatureCard, IconCard, MetricCard } from './Card'
|
||||
export { CTASection } from './CTASection'
|
||||
export { FeatureSection } from './FeatureSection'
|
||||
export { HeroSection } from './HeroSection'
|
||||
export { MetricStrip } from './MetricStrip'
|
||||
export { PreviewSection } from './PreviewSection'
|
||||
export { TechSection } from './TechSection'
|
||||
export { WorkflowSection } from './WorkflowSection'
|
||||
124
apps/landing/src/lib/design-tokens.ts
Normal file
124
apps/landing/src/lib/design-tokens.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 设计系统 - 统一的视觉 Token
|
||||
* 基于 Pastel Palette + Glassmorphic Depth Design System
|
||||
*/
|
||||
|
||||
/**
|
||||
* 阴影层级系统
|
||||
* 从 subtle(轻微)到 heavy(强烈)
|
||||
* 针对浅色背景优化,使用更柔和的阴影
|
||||
*/
|
||||
export const shadows = {
|
||||
subtle: 'shadow-[0_2px_8px_rgba(0,0,0,0.02)]',
|
||||
light: 'shadow-[0_4px_16px_rgba(0,0,0,0.04)]',
|
||||
medium: 'shadow-[0_8px_24px_rgba(0,0,0,0.06)]',
|
||||
strong: 'shadow-[0_12px_32px_rgba(0,0,0,0.08)]',
|
||||
heavy: 'shadow-[0_16px_48px_rgba(0,0,0,0.10)]',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 圆角系统
|
||||
*/
|
||||
export const radius = {
|
||||
sm: 'rounded-xl', // 12px
|
||||
md: 'rounded-2xl', // 16px
|
||||
lg: 'rounded-3xl', // 24px
|
||||
xl: 'rounded-[28px]',
|
||||
'2xl': 'rounded-[32px]',
|
||||
'3xl': 'rounded-[40px]',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 模糊度系统
|
||||
*/
|
||||
export const blur = {
|
||||
sm: 'backdrop-blur-sm', // 4px
|
||||
md: 'backdrop-blur-md', // 12px
|
||||
lg: 'backdrop-blur-lg', // 16px
|
||||
xl: 'backdrop-blur-xl', // 24px
|
||||
'2xl': 'backdrop-blur-2xl', // 40px
|
||||
'3xl': 'backdrop-blur-[60px]',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Glassmorphic 卡片基础样式
|
||||
* 统一的玻璃态卡片样式,可通过 variant 调整
|
||||
*/
|
||||
export const glassCard = {
|
||||
// 默认浮动卡片
|
||||
default: 'bg-background/60 border border-border backdrop-blur-xl',
|
||||
// 强调卡片(略微透明)
|
||||
elevated: 'bg-background/80 border border-border backdrop-blur-2xl',
|
||||
// 悬浮面板
|
||||
floating: 'bg-background/50 border border-white/10 backdrop-blur-[30px]',
|
||||
// 带渐变的特殊卡片
|
||||
gradient: 'border border-white/15 backdrop-blur-2xl',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 内阴影样式(用于内陷效果)
|
||||
*/
|
||||
export const innerShadow = {
|
||||
subtle: 'shadow-[inset_0_1px_0_rgba(255,255,255,0.05)]',
|
||||
medium: 'shadow-[inset_0_2px_4px_rgba(0,0,0,0.1)]',
|
||||
strong: 'shadow-[inset_0_2px_8px_rgba(0,0,0,0.2)]',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 发光效果(用于强调元素)
|
||||
*/
|
||||
export const glow = {
|
||||
accent: 'shadow-[0_0_12px_rgba(var(--color-accent-rgb),0.5)]',
|
||||
accentStrong: 'shadow-[0_0_24px_rgba(var(--color-accent-rgb),0.7)]',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 过渡动画
|
||||
*/
|
||||
export const transition = {
|
||||
fast: 'transition-all duration-200 ease-out',
|
||||
normal: 'transition-all duration-300 ease-out',
|
||||
slow: 'transition-all duration-500 ease-out',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Hover 状态样式
|
||||
*/
|
||||
export const hover = {
|
||||
card: 'hover:border-accent/30 hover:bg-background/90 transition-all duration-300',
|
||||
lift: 'hover:scale-[1.02] hover:shadow-strong transition-all duration-300',
|
||||
glow: 'hover:shadow-[0_0_24px_rgba(var(--color-accent-rgb),0.3)] transition-all duration-300',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 间距系统(gap/space)
|
||||
*/
|
||||
export const spacing = {
|
||||
section: 'space-y-20', // section 之间
|
||||
content: 'space-y-12', // 内容组之间
|
||||
group: 'space-y-6', // 组内元素
|
||||
tight: 'space-y-3', // 紧密元素
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Typography 层级
|
||||
*/
|
||||
export const typography = {
|
||||
hero: 'text-4xl sm:text-5xl lg:text-6xl font-semibold leading-tight',
|
||||
h1: 'text-3xl lg:text-4xl font-semibold',
|
||||
h2: 'text-2xl lg:text-3xl font-semibold',
|
||||
h3: 'text-xl lg:text-2xl font-semibold',
|
||||
body: 'text-base',
|
||||
small: 'text-sm',
|
||||
tiny: 'text-xs',
|
||||
label: 'text-xs tracking-[0.3em] uppercase font-semibold',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 图标容器样式
|
||||
*/
|
||||
export const iconBox = {
|
||||
sm: 'flex size-8 items-center justify-center rounded-xl bg-accent/10 text-accent',
|
||||
md: 'flex size-10 items-center justify-center rounded-2xl bg-accent/15 text-accent',
|
||||
lg: 'flex size-12 items-center justify-center rounded-2xl bg-accent/15 text-accent',
|
||||
} as const
|
||||
898
pnpm-lock.yaml
generated
898
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user