You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
407 lines
19 KiB
407 lines
19 KiB
import { useEffect, useRef, type CSSProperties } from 'react';
|
|
import { getLocalizedPath, getTranslation, type Locale } from '../i18n/utils';
|
|
import { PLACEHOLDER_IMG } from '../placeholder';
|
|
import { cssVars } from './css';
|
|
import { Icon, statsIcons } from '../icons';
|
|
|
|
/**
|
|
* 首屏全屏粒子 + 邻近连线 Canvas 背景:
|
|
* - 4 色粒子(白/蓝/紫/极光淡粉)约 70~95 颗(按屏幕面积自适应)
|
|
* - 邻近粒子间画淡蓝连线(dist < 120)形成柔和"网"
|
|
* - 鼠标靠近时粒子轻微吸附,连线高亮
|
|
* - prefers-reduced-motion 下固定为静态散点,不执行动画
|
|
*/
|
|
function HeroParticleCanvas() {
|
|
const ref = useRef<HTMLCanvasElement>(null);
|
|
|
|
useEffect(() => {
|
|
const canvas = ref.current;
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
let raf = 0;
|
|
let dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
|
|
type P = {
|
|
x: number; y: number;
|
|
vx: number; vy: number;
|
|
r: number;
|
|
color: string;
|
|
alpha: number;
|
|
alphaSpeed: number;
|
|
};
|
|
|
|
const PALETTE = [
|
|
'255, 255, 255', // 白星
|
|
'148, 184, 255', // 品牌淡蓝
|
|
'196, 178, 255', // 薰衣草紫
|
|
'255, 180, 215', // 极光淡粉
|
|
];
|
|
|
|
let W = 0, H = 0;
|
|
let particles: P[] = [];
|
|
const mouse = { x: -9999, y: -9999, active: false };
|
|
|
|
const resize = () => {
|
|
const { clientWidth, clientHeight } = canvas.parentElement || canvas;
|
|
W = clientWidth;
|
|
H = clientHeight;
|
|
dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
canvas.width = Math.floor(W * dpr);
|
|
canvas.height = Math.floor(H * dpr);
|
|
canvas.style.width = W + 'px';
|
|
canvas.style.height = H + 'px';
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
|
|
const area = W * H;
|
|
const count = Math.max(55, Math.min(100, Math.floor(area / 14000)));
|
|
particles = Array.from({ length: count }, () => ({
|
|
x: Math.random() * W,
|
|
y: Math.random() * H,
|
|
vx: (Math.random() - 0.5) * 0.10, // 速度砍 71%,停止"游来游去"的感觉
|
|
vy: (Math.random() - 0.5) * 0.10,
|
|
// 浅底可见版:粒子尺寸 +33%(原 0.6~2.0 → 现 0.8~2.6)
|
|
r: Math.random() * 1.8 + 0.8,
|
|
color: PALETTE[Math.floor(Math.random() * PALETTE.length)],
|
|
// 浅底可见版:alpha 基准 +30%(原 0.4~0.9 → 现 0.65~0.95)
|
|
alpha: Math.random() * 0.30 + 0.65,
|
|
alphaSpeed: (Math.random() - 0.5) * 0.009,
|
|
}));
|
|
};
|
|
|
|
const step = () => {
|
|
ctx.clearRect(0, 0, W, H);
|
|
|
|
// 1) 粒子
|
|
for (let i = 0; i < particles.length; i++) {
|
|
const p = particles[i];
|
|
if (!reduced) {
|
|
// 鼠标吸附(力度很小,避免头晕)
|
|
if (mouse.active) {
|
|
const dx = mouse.x - p.x, dy = mouse.y - p.y;
|
|
const d2 = dx * dx + dy * dy;
|
|
if (d2 < 140 * 140) {
|
|
const f = (1 - Math.sqrt(d2) / 140) * 0.04;
|
|
p.vx += dx * f * 0.02;
|
|
p.vy += dy * f * 0.02;
|
|
}
|
|
}
|
|
p.x += p.vx;
|
|
p.y += p.vy;
|
|
// 速度阻尼
|
|
p.vx *= 0.992; p.vy *= 0.992;
|
|
// 边界反弹
|
|
if (p.x < p.r) { p.x = p.r; p.vx = Math.abs(p.vx); }
|
|
if (p.x > W - p.r) { p.x = W - p.r; p.vx = -Math.abs(p.vx); }
|
|
if (p.y < p.r) { p.y = p.r; p.vy = Math.abs(p.vy); }
|
|
if (p.y > H - p.r) { p.y = H - p.r; p.vy = -Math.abs(p.vy); }
|
|
|
|
// 闪烁 alpha(浅底版:最低不低于 0.55 保证可见)
|
|
p.alpha += p.alphaSpeed;
|
|
if (p.alpha > 0.98 || p.alpha < 0.55) p.alphaSpeed = -p.alphaSpeed;
|
|
}
|
|
|
|
ctx.beginPath();
|
|
// 浅底可见版:发光半径 r×3.5(原 r×3),中间色 alpha*0.45(原 *0.35)
|
|
const g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r * 3.5);
|
|
g.addColorStop(0, `rgba(${p.color}, ${p.alpha})`);
|
|
g.addColorStop(0.5, `rgba(${p.color}, ${p.alpha * 0.45})`);
|
|
g.addColorStop(1, `rgba(${p.color}, 0)`);
|
|
ctx.fillStyle = g;
|
|
ctx.arc(p.x, p.y, p.r * 3.5, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
// 2) 邻近连线(浅底可见版:MAX+25%,基础 alpha+50%,线宽 x1.43)
|
|
if (!reduced) {
|
|
const MAX = 150; // 原 120 → +25%
|
|
for (let i = 0; i < particles.length; i++) {
|
|
const a = particles[i];
|
|
for (let j = i + 1; j < particles.length; j++) {
|
|
const b = particles[j];
|
|
const dx = a.x - b.x, dy = a.y - b.y;
|
|
const d2 = dx * dx + dy * dy;
|
|
if (d2 < MAX * MAX) {
|
|
const d = Math.sqrt(d2);
|
|
const t = 1 - d / MAX;
|
|
// 鼠标增强:若 a 或 b 靠近鼠标,连线加亮
|
|
let boost = 0;
|
|
if (mouse.active) {
|
|
const adx = a.x - mouse.x, ady = a.y - mouse.y;
|
|
const bdx = b.x - mouse.x, bdy = b.y - mouse.y;
|
|
const near = Math.min(adx * adx + ady * ady, bdx * bdx + bdy * bdy);
|
|
if (near < 140 * 140) boost = (1 - Math.sqrt(near) / 140) * 0.65; // 原 0.45 → +44%
|
|
}
|
|
const alpha = Math.min(0.72, t * 0.42 + boost); // 原 0.55 / t*0.28 → 大幅增强
|
|
ctx.strokeStyle = `rgba(100, 140, 230, ${alpha})`; // 颜色略深,浅底上对比更强
|
|
ctx.lineWidth = 1.0; // 原 0.7 → x1.43
|
|
ctx.beginPath();
|
|
ctx.moveTo(a.x, a.y);
|
|
ctx.lineTo(b.x, b.y);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!reduced) raf = requestAnimationFrame(step);
|
|
};
|
|
|
|
const onMove = (e: PointerEvent) => {
|
|
const rect = canvas.getBoundingClientRect();
|
|
mouse.x = e.clientX - rect.left;
|
|
mouse.y = e.clientY - rect.top;
|
|
mouse.active = true;
|
|
};
|
|
const onLeave = () => { mouse.active = false; mouse.x = -9999; mouse.y = -9999; };
|
|
const onResize = () => { resize(); };
|
|
|
|
resize();
|
|
raf = requestAnimationFrame(step);
|
|
window.addEventListener('resize', onResize);
|
|
window.addEventListener('pointermove', onMove, { passive: true });
|
|
window.addEventListener('pointerleave', onLeave);
|
|
|
|
return () => {
|
|
cancelAnimationFrame(raf);
|
|
window.removeEventListener('resize', onResize);
|
|
window.removeEventListener('pointermove', onMove);
|
|
window.removeEventListener('pointerleave', onLeave);
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<canvas
|
|
ref={ref}
|
|
aria-hidden="true"
|
|
className="absolute inset-0 w-full h-full pointer-events-none"
|
|
style={{ zIndex: 3 }}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/** 4 颗雾化光晕 orb,慢速漂浮在背景(品牌蓝 / 薰衣草紫 / 极光粉 / 淡绿) */
|
|
const HERO_GLOW_ORBS = [
|
|
{ c: 'rgba(61, 90, 254, 0.20)', x: '8%', y: '15%', sz: 380, delay: '0s', anim: 'hero-orb-float-1' },
|
|
{ c: 'rgba(166, 140, 255, 0.18)', x: '78%', y: '72%', sz: 460, delay: '-6s', anim: 'hero-orb-float-2' },
|
|
{ c: 'rgba(255, 145, 193, 0.14)', x: '35%', y: '82%', sz: 340, delay: '-11s', anim: 'hero-orb-float-1' },
|
|
{ c: 'rgba(120, 220, 190, 0.12)', x: '60%', y: '8%', sz: 300, delay: '-3s', anim: 'hero-orb-float-2' },
|
|
] as const;
|
|
|
|
export default function Home({ locale }: { locale: Locale }) {
|
|
const t = getTranslation(locale);
|
|
const h = t.home;
|
|
const stats = h.stats.items;
|
|
const pillars = t.home.pillars.items;
|
|
|
|
// 三语辅助:英文 / 繁体 / 简体
|
|
const tr = (en: string, hk: string, zh: string) =>
|
|
locale === 'en' ? en : locale === 'hk' ? hk : zh;
|
|
|
|
const news = t.news.items.slice(0, 3);
|
|
|
|
// 环绕中央玻璃球的轨道粒子:4 圈 · 12 颗发光星芒(颜色 4 色循环)
|
|
// (方案B:用户不喜欢游来游去 → 已在 global.css 停掉公转和光晕漂浮,只保留呼吸闪烁)
|
|
// 半径 65px 等差,角度使用非机械等分(非 0/120/240),让分布更自然
|
|
const DOT_PALETTE = ['white', 'blue', 'violet', 'pink'] as const;
|
|
const orbits = [
|
|
{ r: 145, d: '18s', rev: false, dots: [{ sz: 5, a: '8deg' }, { sz: 4, a: '148deg' }, { sz: 5, a: '268deg' }] },
|
|
{ r: 210, d: '24s', rev: true, dots: [{ sz: 5, a: '62deg' }, { sz: 4, a: '188deg' }, { sz: 5, a: '308deg' }] },
|
|
{ r: 275, d: '30s', rev: false, dots: [{ sz: 4, a: '22deg' }, { sz: 4, a: '142deg' }, { sz: 4, a: '270deg' }] },
|
|
{ r: 340, d: '36s', rev: true, dots: [{ sz: 4, a: '78deg' }, { sz: 3, a: '200deg' }, { sz: 4, a: '322deg' }] },
|
|
] as const;
|
|
// 展开总长度,用于 stagger 相位
|
|
const totalOrbitDots = orbits.reduce((n, o) => n + o.dots.length, 0);
|
|
void totalOrbitDots;
|
|
|
|
return (
|
|
<>
|
|
{/* ===== HERO:极简版(用户反馈满屏星星太脏)—— 只保留干净 hero-bg.jpg 原图 + 左下文字阴影 ===== */}
|
|
<section
|
|
className="hero-text-visible relative w-full h-[calc(100vh-60px)] min-h-[520px] max-h-[900px] overflow-hidden"
|
|
style={{
|
|
// 由主题变量派生的淡蓝紫渐变:色相严格锚定 --brand-accent 品牌蓝,不再硬编码随机淡蓝紫
|
|
background: `linear-gradient(135deg,
|
|
color-mix(in oklch, var(--brand-accent) 8%, var(--bg-primary)) 0%,
|
|
color-mix(in oklch, var(--brand-accent) 5%, var(--brand)) 50%,
|
|
color-mix(in oklch, var(--track-ai) 7%, var(--bg-primary)) 100%)`,
|
|
}}
|
|
>
|
|
{/* 背景图:玻璃球/水滴主题图,原图直接展示,不叠加任何蒙层/粒子/光晕/星芒 */}
|
|
<img
|
|
src="/hero-bg.jpg"
|
|
alt=""
|
|
aria-hidden="true"
|
|
className="absolute inset-0 w-full h-full object-cover"
|
|
style={{ zIndex: 0 }}
|
|
loading="eager"
|
|
decoding="async"
|
|
/>
|
|
|
|
<div className="relative z-10 h-full w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col justify-end pb-16 md:pb-24">
|
|
<div className="max-w-3xl scroll-reveal">
|
|
<h1 className="text-h1 font-bold leading-[1.1] tracking-tight text-white font-[var(--font-display)]">
|
|
{h.heroTitle}
|
|
</h1>
|
|
<p className="mt-5 text-body md:text-body text-white/90 max-w-xl leading-relaxed">
|
|
{h.sloganIntro}
|
|
</p>
|
|
<a
|
|
href={getLocalizedPath('/about', locale)}
|
|
className="mt-8 inline-flex items-center gap-2 text-body font-semibold text-white border-b border-white/60 pb-1 hover:border-white hover:gap-3 transition-all duration-300 group"
|
|
>
|
|
{h.heroCta}
|
|
<Icon name="arrowRight" className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* ===== 最新资讯(腾讯式三列卡片) ===== */}
|
|
<section className="bg-[var(--bg-primary)]">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-[var(--space-10)]">
|
|
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4 mb-8">
|
|
<div className="text-left">
|
|
<span className="section-eyebrow">NEWSROOM</span>
|
|
<h2 className="section-title mt-3">{t.news.title}</h2>
|
|
</div>
|
|
<a href={getLocalizedPath('/news', locale)} className="inline-flex items-center gap-1.5 text-body font-semibold text-[var(--brand-accent)] hover:gap-2.5 transition-all duration-300">
|
|
{tr('View all news', '查看全部新聞', '查看全部新闻')}
|
|
<Icon name="arrowRight" className="w-4 h-4" />
|
|
</a>
|
|
</div>
|
|
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{news.map((item, idx) => (
|
|
<a key={idx} href={getLocalizedPath('/news', locale)} className="track-card group overflow-hidden flex flex-col scroll-reveal">
|
|
<div className="relative aspect-[16/10] overflow-hidden border-b border-[var(--border-light)]">
|
|
<img src={PLACEHOLDER_IMG} alt={item.title} className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105" loading="lazy" decoding="async" />
|
|
<span className="absolute top-3 left-3 text-body px-2 py-1 rounded-md bg-white/80 text-[var(--brand-accent)] font-semibold">{item.category}</span>
|
|
</div>
|
|
<div className="p-5">
|
|
<span className="text-body text-[var(--text-muted)] font-mono">{item.date}</span>
|
|
<h3 className="mt-2 text-body font-semibold text-[var(--brand-black)] leading-snug group-hover:text-[var(--brand-accent)] transition-colors duration-200">{item.title}</h3>
|
|
</div>
|
|
</a>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* ===== 大文案 + 向下滚动指示(腾讯式) ===== */}
|
|
<section className="bg-[var(--bg-primary)] pb-[var(--space-10)]">
|
|
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 text-center scroll-reveal">
|
|
<p className="text-body md:text-h1 font-bold font-[var(--font-display)] text-[var(--brand-black)] leading-[1.35] tracking-tight">
|
|
{tr(
|
|
'BaskBuddy improves the quality of life for people around the world through innovative technology, products and services.',
|
|
'巴適夥伴透過創新的科技、產品和服務,提升全球各地人們的生活品質。',
|
|
'巴适伙伴通过创新的科技、产品和服务,提升全球各地人们的生活品质。',
|
|
)}
|
|
</p>
|
|
</div>
|
|
<div className="mt-16 flex flex-col items-center text-[var(--text-muted)]">
|
|
<span className="text-body font-mono tracking-[0.3em]">SCROLL</span>
|
|
<span className="mt-3 w-px h-14 bg-gradient-to-b from-[var(--text-muted)] to-transparent animate-pulse" />
|
|
<Icon name="arrowDown" className="w-4 h-4 mt-1 animate-bounce" />
|
|
</div>
|
|
</section>
|
|
|
|
{/* ===== 业务大图入口(黑底 · 文字左侧,对应腾讯 "面向消费者 / 企业 / 未来") ===== */}
|
|
<section className="bg-[var(--brand-black)]">
|
|
{pillars.map((p, i) => (
|
|
<a
|
|
key={p.name}
|
|
href={getLocalizedPath('/business', locale) + '#ecosystem'}
|
|
className="group relative block h-[68vh] min-h-[420px] overflow-hidden"
|
|
>
|
|
<img
|
|
src={p.img}
|
|
alt={p.name}
|
|
className="absolute inset-0 w-full h-full object-cover transition-transform duration-[900ms] group-hover:scale-105"
|
|
loading="lazy"
|
|
decoding="async"
|
|
/>
|
|
{/* 左深右透蒙层:保证左侧白色标题在浅色图上仍清晰(色相锚定 accent+品牌蓝,不硬编码深蓝 #0a0e2a) */}
|
|
<div
|
|
className="absolute inset-0 pointer-events-none"
|
|
aria-hidden="true"
|
|
style={{
|
|
background: `linear-gradient(90deg,
|
|
color-mix(in oklch, color-mix(in oklch, var(--accent) 75%, var(--brand-accent-darker) 25%) 78%, transparent) 0%,
|
|
color-mix(in oklch, color-mix(in oklch, var(--accent) 75%, var(--brand-accent-darker) 25%) 55%, transparent) 35%,
|
|
color-mix(in oklch, color-mix(in oklch, var(--accent) 75%, var(--brand-accent-darker) 25%) 15%, transparent) 70%,
|
|
transparent 100%)`,
|
|
}}
|
|
/>
|
|
<div className="relative z-10 h-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col justify-center">
|
|
<span className="text-body font-mono tracking-[0.3em] text-[var(--brand-accent)]">0{i + 1}</span>
|
|
<h2 className="mt-3 text-h1 font-bold font-[var(--font-display)] text-white leading-tight">
|
|
{p.name}
|
|
</h2>
|
|
<p className="mt-4 text-body md:text-body text-white/80 max-w-xl leading-relaxed">{p.desc}</p>
|
|
<span className="mt-7 inline-flex items-center gap-2 text-body font-semibold text-white border-b border-white/50 pb-1 w-fit transition-all duration-300 group-hover:gap-3 group-hover:border-white">
|
|
{tr('Learn more', '了解更多', '了解更多')}
|
|
<Icon name="arrowRight" className="w-4 h-4" />
|
|
</span>
|
|
</div>
|
|
</a>
|
|
))}
|
|
</section>
|
|
|
|
{/* ===== 愿景带 + 巴适速度 大数字 ===== */}
|
|
<section className="band !bg-[var(--bg-subtle)]">
|
|
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 pt-[var(--space-10)] text-center">
|
|
<span className="section-eyebrow">OUR VISION</span>
|
|
<h2 className="text-h1 md:text-h1 font-bold font-[var(--font-display)] text-[var(--brand-black)] mt-3">
|
|
{tr('Value for Users, Tech for Good', '用戶為本 · 科技向善', '用户为本 · 科技向善')}
|
|
</h2>
|
|
<p className="mt-5 text-body text-[var(--text-secondary)] max-w-3xl mx-auto leading-relaxed">
|
|
{tr(
|
|
'Connect people and services through innovative technology — creating a better life for users and long-term value for society.',
|
|
'以創新技術連接人與服務,為用戶創造美好生活,為社會創造長期價值。',
|
|
'以创新技术连接人与服务,为用户创造美好生活,为社会创造长期价值。',
|
|
)}
|
|
</p>
|
|
</div>
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-[var(--space-9)]">
|
|
<div className="stat-band scroll-reveal mt-6">
|
|
{stats.map((sItem, idx) => (
|
|
<div className="stat-cell" key={sItem.label} style={cssVars({ '--track-color': 'var(--accent)' })}>
|
|
<Icon name={statsIcons[idx]} className="w-8 h-8 mx-auto mb-3" style={{ color: 'var(--brand-accent)' }} />
|
|
<div className="stat-num">
|
|
<span className="stat-value">{sItem.value}</span>
|
|
{sItem.suffix && <span className="stat-suffix">{sItem.suffix}</span>}
|
|
</div>
|
|
<div className="stat-label">{sItem.label}</div>
|
|
<div className="stat-sub">{sItem.sub}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* ===== CTA Band ===== */}
|
|
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-[var(--space-10)]">
|
|
<div className="cta-band scroll-reveal px-8 py-14 md:py-16 text-center">
|
|
<h2 className="relative z-10 text-h1 font-bold font-[var(--font-display)] text-white">{t.contact.subtitle}</h2>
|
|
<p className="relative z-10 mt-3 text-body text-[color-mix(in_oklch,var(--brand-white)_70%,transparent)] max-w-xl mx-auto">
|
|
{tr(
|
|
'Whether for a job inquiry or business cooperation, we look forward to meeting you with warmth.',
|
|
'無論是求職諮詢還是商務合作,我們都期待與你溫暖相遇。',
|
|
'无论是求职咨询还是商务合作,我们都期待与你温暖相遇。',
|
|
)}
|
|
</p>
|
|
<div className="relative z-10 mt-8 flex flex-col sm:flex-row items-center justify-center gap-3.5">
|
|
<a href={getLocalizedPath('/contact', locale)} className="btn btn-light btn-lg">
|
|
{t.contact.title}
|
|
</a>
|
|
<a href={getLocalizedPath('/about', locale)} className="btn btn-outline-light btn-lg">
|
|
{tr('About Us', '了解我們', '了解我们')}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</>
|
|
);
|
|
}
|
|
|