Browse Source

feat(全站): 线上地址统一 + 三语补齐 + 死代码清理 + 首页愿景带去重

SEO / 线上地址
- Seo.tsx canonical 由 window.location.origin 固定为 https://www.baskbuddy.com,
  OSS 资源统一走 oss.baskbuddy.com;en 组织名统一为 BaskBuddy (Shanghai) Technology Co., Ltd.
- 新增 public/sitemap.xml(含 xhtml:link 三语 alternates),更新 robots.txt、nginx-spa.conf

i18n 三语
- Privacy.tsx 重写:原 const isZh = locale !== 'en' 使 /hk/privacy 整页渲染简体,
  改为 ZH/HK/EN 三份独立正文,标题与署名按 locale 取值
- Business/NotFound 改用 tr(zh,hk,en) 并移除 isZh;Sustainability/Contact 补 hk 分支
- 8 个 /hk/* 页面的简体字泄漏归零(仅保留语言切换器上的「简」标签)

死代码清理
- global.css 删 997 行 Astro 时代遗留 CSS(余 119 类,0 未引用)
- useClientEffects 移除 .stat-value[data-target] 计数与 .copy-btn 剪贴板两个恒空的观察器
- 删 vercel.json(实际部署走 nginx,零引用)与 src/placeholder.ts

首页
- 新增 CompanyVideo 视频区,以及 MemberProfileModal + i18n/teamDetails 团队详情
- 愿景带去重:删掉与 CompanyVideo 左栏重复的「5大核心赛道 / 8+自研产品」两格,
  只保留「全球 / 长期」;statsIcons 同步缩为两项——它按下标映射,不改则图标整体错位
- 新增 .stat-band--pair:桌面端 2 列、720px 居中,不影响共用该类名的 Investors 四格版;
  英文 Long-term 原在 390px 屏折成两行,按 :lang(en) 单独收一档字号,中文保持原大小

验证:npm run typecheck、npm run build 通过;headless Edge + CDP 于 390/768/1024/1440
四断点逐页核对三语布局,单元格均单行无溢出,Investors 数据带无回归。
master
doublex001 1 month ago
parent
commit
79765c1913
  1. 6
      .gitignore
  2. 16
      index.html
  3. 38
      nginx-spa.conf
  4. 9
      package-lock.json
  5. 3
      package.json
  6. 3
      public/robots.txt
  7. 244
      public/sitemap.xml
  8. 283
      src/components/About.tsx
  9. 23
      src/components/Business.tsx
  10. 113
      src/components/CompanyVideo.tsx
  11. 28
      src/components/Contact.tsx
  12. 1
      src/components/EcosystemMap.tsx
  13. 272
      src/components/Home.tsx
  14. 4
      src/components/Investors.tsx
  15. 71
      src/components/Mascot.tsx
  16. 178
      src/components/MemberProfileModal.tsx
  17. 20
      src/components/News.tsx
  18. 14
      src/components/NotFound.tsx
  19. 225
      src/components/Privacy.tsx
  20. 12
      src/components/Sustainability.tsx
  21. 74
      src/hooks/useClientEffects.ts
  22. 126
      src/i18n/en.ts
  23. 110
      src/i18n/hk.ts
  24. 39
      src/i18n/teamDetails/en.ts
  25. 39
      src/i18n/teamDetails/hk.ts
  26. 27
      src/i18n/teamDetails/index.ts
  27. 47
      src/i18n/teamDetails/zh.ts
  28. 2
      src/i18n/utils.ts
  29. 153
      src/i18n/zh.ts
  30. 17
      src/icons/index.tsx
  31. 2
      src/layout/RootLayout.tsx
  32. 2
      src/pages/AboutPage.tsx
  33. 2
      src/pages/BusinessPage.tsx
  34. 2
      src/pages/ContactPage.tsx
  35. 2
      src/pages/InvestorsPage.tsx
  36. 2
      src/pages/NewsPage.tsx
  37. 2
      src/pages/SustainabilityPage.tsx
  38. 5
      src/placeholder.ts
  39. 40
      src/seo/Seo.tsx
  40. 1192
      src/styles/global.css
  41. 2
      tsconfig.json
  42. 6
      tsconfig.node.json
  43. 13
      vercel.json

6
.gitignore

@ -55,3 +55,9 @@ vite.config.ts.timestamp-*.mjs
# ===== Large source image archives (use individual optimized files instead) ===== # ===== Large source image archives (use individual optimized files instead) =====
*.zip *.zip
public/*.zip public/*.zip
# ===== Local-only media masters (never deployed) =====
# 视频/大图母版留档目录:不提交、也不放进 public/,
# 否则 Vite 会把整个目录拷进 dist/,让每次部署多传上百 MB。
# 线上播放走 OSS 上压好的网页版,见 src/components/CompanyVideo.tsx。
media/

16
index.html

@ -7,27 +7,27 @@
<meta name="referrer" content="strict-origin-when-cross-origin" /> <meta name="referrer" content="strict-origin-when-cross-origin" />
<meta <meta
name="description" name="description"
content="BaskBuddy 巴适伙伴(上海)科技有限公司 — 以温暖科技连接美好生活,四大赛道覆盖内容社交、职场服务、数字支付、人工智能。" content="BaskBuddy 巴适伙伴(上海)科技有限公司 — 以温暖科技连接美好生活,五大赛道覆盖内容社交、职场服务、数字支付、人工智能、生活服务。"
/> />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:site_name" content="BaskBuddy" /> <meta property="og:site_name" content="BaskBuddy" />
<meta property="og:title" content="巴适伙伴 | BaskBuddy" /> <meta property="og:title" content="巴适伙伴 | BaskBuddy" />
<meta <meta
property="og:description" property="og:description"
content="BaskBuddy 巴适伙伴(上海)科技有限公司 — 以温暖科技连接美好生活,四大赛道覆盖内容社交、职场服务、数字支付、人工智能。" content="BaskBuddy 巴适伙伴(上海)科技有限公司 — 以温暖科技连接美好生活,五大赛道覆盖内容社交、职场服务、数字支付、人工智能、生活服务。"
/> />
<meta property="og:url" content="https://baskbuddy.com/" /> <meta property="og:url" content="https://www.baskbuddy.com/" />
<meta property="og:image" content="https://oss.baskbuddy.com/baskbuddy/og-default.png" /> <meta property="og:image" content="https://oss.baskbuddy.com/baskbuddy/hero-bg.jpg" />
<meta property="og:image:alt" content="BaskBuddy 巴适伙伴 — 一生相伴 · 舒适相伴 · 一生信赖" /> <meta property="og:image:alt" content="BaskBuddy 巴适伙伴 — 一生相伴 · 舒适相伴 · 一生信赖" />
<meta property="og:locale" content="zh_CN" /> <meta property="og:locale" content="zh_CN" />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="巴适伙伴 | BaskBuddy" /> <meta name="twitter:title" content="巴适伙伴 | BaskBuddy" />
<meta <meta
name="twitter:description" name="twitter:description"
content="BaskBuddy 巴适伙伴(上海)科技有限公司 — 以温暖科技连接美好生活,四大赛道覆盖内容社交、职场服务、数字支付、人工智能。" content="BaskBuddy 巴适伙伴(上海)科技有限公司 — 以温暖科技连接美好生活,五大赛道覆盖内容社交、职场服务、数字支付、人工智能、生活服务。"
/> />
<meta name="twitter:image" content="https://oss.baskbuddy.com/baskbuddy/og-default.png" /> <meta name="twitter:image" content="https://oss.baskbuddy.com/baskbuddy/hero-bg.jpg" />
<link rel="canonical" href="https://baskbuddy.com/" /> <link rel="canonical" href="https://www.baskbuddy.com/" />
<link rel="icon" type="image/png" href="https://oss.baskbuddy.com/baskbuddy/baskbuddy-logo.png" /> <link rel="icon" type="image/png" href="https://oss.baskbuddy.com/baskbuddy/baskbuddy-logo.png" />
<link rel="apple-touch-icon" href="https://oss.baskbuddy.com/baskbuddy/baskbuddy-logo.png" /> <link rel="apple-touch-icon" href="https://oss.baskbuddy.com/baskbuddy/baskbuddy-logo.png" />
<!-- <!--
@ -37,6 +37,8 @@
故改用 jsDelivr(国内可达)。全站字体已在 src/styles/global.css 的 --font-* 变量统一为思源黑体。 故改用 jsDelivr(国内可达)。全站字体已在 src/styles/global.css 的 --font-* 变量统一为思源黑体。
异步加载(media=print + onload 切回 all)避免阻塞首屏渲染;noscript 内提供同步兜底。 异步加载(media=print + onload 切回 all)避免阻塞首屏渲染;noscript 内提供同步兜底。
--> -->
<!-- 全站图片/视频均托管于 OSS,提前建连可省去首屏素材的 DNS + TLS 往返 -->
<link rel="preconnect" href="https://oss.baskbuddy.com" crossorigin />
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin /> <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link <link
href="https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-sc@5/index.css" href="https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-sc@5/index.css"

38
nginx-spa.conf

@ -1,9 +1,18 @@
# Nginx SPA fallback for baskbuddy.com # Nginx SPA fallback for www.baskbuddy.com
# Drop this snippet into your server {} block and set root to the dist/ folder. # Drop this snippet into your server {} block and set root to the dist/ folder.
# Canonical host is www.baskbuddy.com — it is the only name with a DNS A record,
# and it is what canonical / og:url / hreflang / sitemap all advertise.
# The apex is redirected to www so the two never compete in search results.
server { server {
listen 80; listen 80;
server_name baskbuddy.com www.baskbuddy.com; server_name baskbuddy.com;
return 301 http://www.baskbuddy.com$request_uri;
}
server {
listen 80;
server_name www.baskbuddy.com;
root /var/www/baskbuddy/dist; root /var/www/baskbuddy/dist;
index index.html; index index.html;
@ -11,8 +20,18 @@ server {
gzip on; gzip on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml; gzip_types text/plain text/css application/javascript application/json image/svg+xml;
# static assets (fonts, images, videos, og images, sitemap, robots) # SEO files — must be revalidated often. Keep this block ABOVE the generic
location ~* \.(?:webp|png|jpg|jpeg|mp4|webm|svg|ico|woff2?|css|js|xml|txt)$ { # static-asset block below: nginx picks the first matching regex, and caching
# robots.txt / sitemap.xml as "immutable" for 6 months would hide any update
# from crawlers long after a deploy.
location ~ ^/(robots\.txt|sitemap\.xml)$ {
expires 1h;
access_log off;
add_header Cache-Control "public, max-age=3600";
}
# content-hashed or effectively immutable static assets
location ~* \.(?:webp|png|jpg|jpeg|mp4|webm|svg|ico|woff2?|css|js)$ {
expires 6M; expires 6M;
access_log off; access_log off;
add_header Cache-Control "public, immutable"; add_header Cache-Control "public, immutable";
@ -31,10 +50,17 @@ server {
} }
} }
# HTTPS (recommended) — after obtaining a certificate, proxy/redirect from 443: # HTTPS (recommended) — after obtaining a certificate, redirect 80 -> 443 and
# serve both names from 443, keeping the apex -> www redirect there as well:
# server {
# listen 443 ssl http2;
# server_name baskbuddy.com;
# include /path/to/ssl.conf;
# return 301 https://www.baskbuddy.com$request_uri;
# }
# server { # server {
# listen 443 ssl http2; # listen 443 ssl http2;
# server_name baskbuddy.com www.baskbuddy.com; # server_name www.baskbuddy.com;
# root /var/www/baskbuddy/dist; # root /var/www/baskbuddy/dist;
# include /path/to/ssl.conf; # include /path/to/ssl.conf;
# location / { try_files $uri $uri/ /index.html; } # location / { try_files $uri $uri/ /index.html; }

9
package-lock.json

@ -17,6 +17,7 @@
"tailwind-merge": "^3.0.0" "tailwind-merge": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.13.3",
"@types/react": "^18.3.31", "@types/react": "^18.3.31",
"@types/react-dom": "^18.3.7", "@types/react-dom": "^18.3.7",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
@ -1014,9 +1015,8 @@
"version": "24.13.3", "version": "24.13.3",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-24.13.3.tgz", "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.13.3.tgz",
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.18.0" "undici-types": "~7.18.0"
} }
@ -1840,9 +1840,8 @@
"version": "7.18.2", "version": "7.18.2",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"license": "MIT", "devOptional": true,
"optional": true, "license": "MIT"
"peer": true
}, },
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.2.3", "version": "1.2.3",

3
package.json

@ -6,17 +6,20 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"typecheck": "tsc -b",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.0", "@tailwindcss/vite": "^4.1.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^1.25.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^6.28.0", "react-router-dom": "^6.28.0",
"tailwind-merge": "^3.0.0" "tailwind-merge": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.13.3",
"@types/react": "^18.3.31", "@types/react": "^18.3.31",
"@types/react-dom": "^18.3.7", "@types/react-dom": "^18.3.7",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",

3
public/robots.txt

@ -1,5 +1,4 @@
User-agent: * User-agent: *
Allow: / Allow: /
Disallow: /api/
Sitemap: https://baskbuddy.com/sitemap.xml Sitemap: https://www.baskbuddy.com/sitemap.xml

244
public/sitemap.xml

@ -0,0 +1,244 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://www.baskbuddy.com/</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/"/>
</url>
<url>
<loc>https://www.baskbuddy.com/en</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/"/>
</url>
<url>
<loc>https://www.baskbuddy.com/hk</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/"/>
</url>
<url>
<loc>https://www.baskbuddy.com/about</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/about"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/about"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/about"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/about"/>
</url>
<url>
<loc>https://www.baskbuddy.com/en/about</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/about"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/about"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/about"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/about"/>
</url>
<url>
<loc>https://www.baskbuddy.com/hk/about</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/about"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/about"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/about"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/about"/>
</url>
<url>
<loc>https://www.baskbuddy.com/business</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/business"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/business"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/business"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/business"/>
</url>
<url>
<loc>https://www.baskbuddy.com/en/business</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/business"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/business"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/business"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/business"/>
</url>
<url>
<loc>https://www.baskbuddy.com/hk/business</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/business"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/business"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/business"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/business"/>
</url>
<url>
<loc>https://www.baskbuddy.com/news</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/news"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/news"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/news"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/news"/>
</url>
<url>
<loc>https://www.baskbuddy.com/en/news</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/news"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/news"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/news"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/news"/>
</url>
<url>
<loc>https://www.baskbuddy.com/hk/news</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/news"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/news"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/news"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/news"/>
</url>
<url>
<loc>https://www.baskbuddy.com/sustainability</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/sustainability"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/sustainability"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/sustainability"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/sustainability"/>
</url>
<url>
<loc>https://www.baskbuddy.com/en/sustainability</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/sustainability"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/sustainability"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/sustainability"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/sustainability"/>
</url>
<url>
<loc>https://www.baskbuddy.com/hk/sustainability</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/sustainability"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/sustainability"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/sustainability"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/sustainability"/>
</url>
<url>
<loc>https://www.baskbuddy.com/investors</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/investors"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/investors"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/investors"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/investors"/>
</url>
<url>
<loc>https://www.baskbuddy.com/en/investors</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/investors"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/investors"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/investors"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/investors"/>
</url>
<url>
<loc>https://www.baskbuddy.com/hk/investors</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/investors"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/investors"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/investors"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/investors"/>
</url>
<url>
<loc>https://www.baskbuddy.com/contact</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/contact"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/contact"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/contact"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/contact"/>
</url>
<url>
<loc>https://www.baskbuddy.com/en/contact</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/contact"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/contact"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/contact"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/contact"/>
</url>
<url>
<loc>https://www.baskbuddy.com/hk/contact</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/contact"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/contact"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/contact"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/contact"/>
</url>
<url>
<loc>https://www.baskbuddy.com/privacy</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/privacy"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/privacy"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/privacy"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/privacy"/>
</url>
<url>
<loc>https://www.baskbuddy.com/en/privacy</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/privacy"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/privacy"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/privacy"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/privacy"/>
</url>
<url>
<loc>https://www.baskbuddy.com/hk/privacy</loc>
<lastmod>2026-09-20</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.baskbuddy.com/privacy"/>
<xhtml:link rel="alternate" hreflang="zh-HK" href="https://www.baskbuddy.com/hk/privacy"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://www.baskbuddy.com/en/privacy"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.baskbuddy.com/privacy"/>
</url>
</urlset>

283
src/components/About.tsx

@ -1,13 +1,17 @@
import { useEffect, useRef, useState } from 'react';
import { getLocalizedPath, getTranslation, type Locale } from '../i18n/utils'; import { getLocalizedPath, getTranslation, type Locale } from '../i18n/utils';
import { cssVars } from './css'; import { cssVars } from './css';
import { Icon, icidpIcons, visionMissionIcons, trackIcons } from '../icons'; import { Icon, icidpIcons, visionMissionIcons, trackIcons } from '../icons';
import OrgNetwork from './OrgNetwork'; import OrgNetwork from './OrgNetwork';
import MemberProfileModal, { type DetailState, type MemberProfile } from './MemberProfileModal';
import { loadTeamDetails } from '../i18n/teamDetails';
export default function About({ locale }: { locale: Locale }) { export default function About({ locale }: { locale: Locale }) {
const t = getTranslation(locale); const t = getTranslation(locale);
const a = t.about; const a = t.about;
const icidp = a.icidp; const icidp = a.icidp;
const zh = locale !== 'en'; // 三语辅助:繁体页面必须走 hk 文案,不能并进简体分支(与 Business/Sustainability 同一写法)
const tr = (zh: string, hk: string, en: string) => (locale === 'en' ? en : locale === 'hk' ? hk : zh);
const business = t.business; const business = t.business;
const icidpItems = [ const icidpItems = [
@ -26,9 +30,109 @@ export default function About({ locale }: { locale: Locale }) {
{ key: 'vision', s: a.subnav.vision, href: a.subnav.vision.href }, { key: 'vision', s: a.subnav.vision, href: a.subnav.vision.href },
]; ];
// 管理团队头像:英文取姓名首字母,中文/繁体显示全名 // 管理团队人像位:英文取姓名首字母缩写,中文/繁体取首字(无照片时的品牌渐变占位)
const memberInitial = (name: string) => const memberInitial = (name: string) =>
locale === 'en' ? name.split(' ').filter(Boolean).map((w) => w[0]).join('') : name; locale === 'en' ? name.split(' ').filter(Boolean).map((w) => w[0]).join('') : name[0];
// ===== 管理团队轮播(参照数字 Nexus「06 | Team」) =====
const teamMembers = a.subnav.team.groups.flatMap((g) =>
g.members.map((m) => ({ ...m, group: g.title })),
);
const n = teamMembers.length;
const [teamIdx, setTeamIdx] = useState(0);
const [teamPaused, setTeamPaused] = useState(false);
const current = teamMembers[teamIdx];
// 成员详情弹窗:存「成员对象快照」而不是索引——轮播会自动换人,索引会跟着漂
const [profile, setProfile] = useState<MemberProfile | null>(null);
// 长简介是异步来的(单独 chunk,见 i18n/teamDetails/),头部照常立刻渲染
const [profileDetail, setProfileDetail] = useState<DetailState>({ status: 'loading' });
const profileTriggerRef = useRef<HTMLButtonElement>(null);
// 同一次弹窗对应一个 token:加载期间关掉、或转头点开别人时,迟到的结果直接丢弃
const profileTokenRef = useRef(0);
// 预取失败不必声张:失败不会进缓存,点开时还会再拉一次并走弹窗里的错误文案
const prefetchDetails = () => {
loadTeamDetails(locale).catch(() => {});
};
const openProfile = (m: (typeof teamMembers)[number]) => {
if (!m.detailId) return;
const token = ++profileTokenRef.current;
setProfile({
name: m.name,
role: m.role,
group: m.group,
photo: m.photo,
initials: memberInitial(m.name),
nationality: m.nationality,
detailId: m.detailId,
});
setProfileDetail({ status: 'loading' });
loadTeamDetails(locale)
.then((map) => {
if (profileTokenRef.current !== token) return;
setProfileDetail({ status: 'ready', detail: map[m.detailId!] });
})
.catch(() => {
if (profileTokenRef.current !== token) return;
// 失败不写缓存(见 teamDetails/index.ts),关掉重开会重新拉一次
setProfileDetail({ status: 'error' });
});
};
// 自动轮播:4s 递进;prefers-reduced-motion 用户不自动播;悬停/键盘聚焦时暂停;
// 弹窗打开时也暂停(否则 4s 一到,弹窗里的成员和还焦点的按钮会一起被换掉)
useEffect(() => {
if (teamPaused || profile) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const timer = window.setInterval(() => setTeamIdx((i) => (i + 1) % n), 4000);
return () => window.clearInterval(timer);
}, [teamPaused, profile, n]);
const closeProfile = () => {
profileTokenRef.current++; // 作废在途的简介加载,避免它回来时把状态写回已关闭的弹窗
setProfile(null);
// 焦点还给触发按钮。html{scroll-behavior:smooth} 会把默认的聚焦滚动做成动画,
// 故 preventScroll;按钮若已随路由卸载则跳过。
window.requestAnimationFrame(() => {
const el = profileTriggerRef.current;
if (el?.isConnected) el.focus({ preventScroll: true });
});
};
// 人像卡:有 photo 用照片,否则回落为品牌渐变 + 姓名首字;showInfo 控制底部姓名条(仅当前卡展示)
const renderPortrait = (m: (typeof teamMembers)[number], showInfo = false) => (
<>
{m.photo ? (
<img
src={m.photo}
alt={showInfo ? m.name : ''}
className="absolute inset-0 w-full h-full object-cover"
loading="lazy"
decoding="async"
/>
) : (
<span
aria-hidden="true"
className="absolute inset-0 flex items-center justify-center"
style={{
background:
'linear-gradient(160deg, color-mix(in oklch, var(--brand-accent) 16%, var(--bg-primary)) 0%, color-mix(in oklch, var(--brand-accent) 34%, var(--bg-primary)) 100%)',
}}
>
<span className="text-h1 font-bold font-[family-name:var(--font-display)] text-[var(--brand-accent-deep)]">
{memberInitial(m.name)}
</span>
</span>
)}
{showInfo && (
<span className="absolute inset-x-0 bottom-0 block bg-gradient-to-t from-[color-mix(in_oklch,var(--brand-black)_90%,transparent)] via-[color-mix(in_oklch,var(--brand-black)_45%,transparent)] to-transparent px-4 pb-3 pt-12 text-left">
<span className="block text-body font-semibold text-white leading-tight">{m.name}</span>
<span className="mt-0.5 block text-body-sm text-white/75 leading-snug">{m.role}</span>
</span>
)}
</>
);
// 业务版图:复用 business.tracks(五大赛道),描述取自 home.tracks,顺序一致 // 业务版图:复用 business.tracks(五大赛道),描述取自 home.tracks,顺序一致
const trackKeys = Object.keys(t.home.tracks); // ['social','career','payment','ai','lifeService'] const trackKeys = Object.keys(t.home.tracks); // ['social','career','payment','ai','lifeService']
@ -57,7 +161,7 @@ export default function About({ locale }: { locale: Locale }) {
<div className="text-body font-semibold text-[var(--brand-black)]">{it.s.title}</div> <div className="text-body font-semibold text-[var(--brand-black)]">{it.s.title}</div>
<p className="text-body text-[var(--text-muted)] leading-relaxed flex-1">{it.s.desc}</p> <p className="text-body text-[var(--text-muted)] leading-relaxed flex-1">{it.s.desc}</p>
<span className="inline-flex items-center gap-1 text-body font-semibold text-[var(--brand-accent)]"> <span className="inline-flex items-center gap-1 text-body font-semibold text-[var(--brand-accent)]">
{zh ? '了解更多' : 'Learn more'} {tr('了解更多', '了解更多', 'Learn more')}
<Icon name="arrowRight" className="w-4 h-4 transition-transform duration-200 group-hover:translate-x-1" /> <Icon name="arrowRight" className="w-4 h-4 transition-transform duration-200 group-hover:translate-x-1" />
</span> </span>
</a> </a>
@ -82,10 +186,10 @@ export default function About({ locale }: { locale: Locale }) {
<section id="about-overview" className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-[var(--space-9)] scroll-mt-24"> <section id="about-overview" className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-[var(--space-9)] scroll-mt-24">
<div className="section-head text-center scroll-reveal"> <div className="section-head text-center scroll-reveal">
<span className="section-eyebrow">{a.subnav.company.title}</span> <span className="section-eyebrow">{a.subnav.company.title}</span>
<h2 className="section-title">{zh ? '公司概览' : 'Company Overview'}</h2> <h2 className="section-title">{tr('公司概览', '公司概覽', 'Company Overview')}</h2>
</div> </div>
<p className="mt-3 text-center text-body text-[var(--text-secondary)]"> <p className="mt-3 text-center text-body text-[var(--text-secondary)]">
{zh ? '点击节点,展开了解巴适伙伴' : 'Tap a node to explore BaskBuddy'} {tr('点击节点,展开了解巴适伙伴', '點擊節點,展開了解巴適夥伴', 'Tap a node to explore BaskBuddy')}
</p> </p>
<OrgNetwork data={a.overview.network} locale={locale} /> <OrgNetwork data={a.overview.network} locale={locale} />
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-8"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-8">
@ -102,7 +206,7 @@ export default function About({ locale }: { locale: Locale }) {
<section id="about-business" className="band scroll-mt-24"> <section id="about-business" className="band scroll-mt-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-[var(--space-9)]"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-[var(--space-9)]">
<div className="section-head text-center scroll-reveal"> <div className="section-head text-center scroll-reveal">
<span className="section-eyebrow">{zh ? 'BUSINESS LANDSCAPE' : 'BUSINESS LANDSCAPE'}</span> <span className="section-eyebrow">BUSINESS LANDSCAPE</span>
<h2 className="section-title">{business.title}</h2> <h2 className="section-title">{business.title}</h2>
<p className="section-lead">{business.subtitle}</p> <p className="section-lead">{business.subtitle}</p>
</div> </div>
@ -122,9 +226,15 @@ export default function About({ locale }: { locale: Locale }) {
<h3 className="text-body font-semibold text-[var(--brand-black)]">{track.name}</h3> <h3 className="text-body font-semibold text-[var(--brand-black)]">{track.name}</h3>
<p className="mt-2 text-body leading-relaxed text-[var(--text-secondary)] flex-1">{desc}</p> <p className="mt-2 text-body leading-relaxed text-[var(--text-secondary)] flex-1">{desc}</p>
<div className="mt-4 flex items-center justify-between"> <div className="mt-4 flex items-center justify-between">
<span className="text-body font-mono text-[var(--text-muted)]">{track.products.length} 款产品</span> <span className="text-body font-mono text-[var(--text-muted)]">
{tr(
`${track.products.length} 款产品`,
`${track.products.length} 款產品`,
`${track.products.length} ${track.products.length === 1 ? 'product' : 'products'}`,
)}
</span>
<span className="inline-flex items-center gap-1 text-body font-semibold text-[var(--brand-accent)] group-hover:gap-2 transition-all duration-300"> <span className="inline-flex items-center gap-1 text-body font-semibold text-[var(--brand-accent)] group-hover:gap-2 transition-all duration-300">
{zh ? '查看' : 'View'} {tr('查看', '查看', 'View')}
<Icon name="arrowRight" className="w-3.5 h-3.5" /> <Icon name="arrowRight" className="w-3.5 h-3.5" />
</span> </span>
</div> </div>
@ -139,29 +249,29 @@ export default function About({ locale }: { locale: Locale }) {
<section id="about-vision" className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-[var(--space-9)] scroll-mt-24"> <section id="about-vision" className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-[var(--space-9)] scroll-mt-24">
<div className="section-head text-center scroll-reveal"> <div className="section-head text-center scroll-reveal">
<span className="section-eyebrow">{a.subnav.vision.title}</span> <span className="section-eyebrow">{a.subnav.vision.title}</span>
<h2 className="section-title">{zh ? '愿景及使命' : 'Vision & Mission'}</h2> <h2 className="section-title">{tr('愿景及使命', '願景及使命', 'Vision & Mission')}</h2>
</div> </div>
<div className="grid md:grid-cols-2 gap-4"> <div className="grid md:grid-cols-2 gap-4">
<div className="scroll-reveal rounded-2xl card-surface p-7"> <div className="scroll-reveal rounded-2xl card-surface p-7">
<span className="icon-tile w-10 h-10 mb-3" style={cssVars({ '--track-color': 'var(--brand-accent)' })}><Icon name={visionMissionIcons.vision} className="w-5 h-5" /></span> <span className="icon-tile w-10 h-10 mb-3" style={cssVars({ '--track-color': 'var(--brand-accent)' })}><Icon name={visionMissionIcons.vision} className="w-5 h-5" /></span>
<div className="text-body font-mono tracking-[0.15em] text-[var(--accent)] mb-2">VISION · 愿景</div> <div className="text-body font-mono tracking-[0.15em] text-[var(--accent)] mb-2">{tr('VISION · 愿景', 'VISION · 願景', 'VISION')}</div>
<p className="text-body leading-[1.8] text-[var(--brand-black)] font-medium">{a.visionMission.vision}</p> <p className="text-body leading-[1.8] text-[var(--brand-black)] font-medium">{a.visionMission.vision}</p>
</div> </div>
<div className="scroll-reveal rounded-2xl card-surface p-7"> <div className="scroll-reveal rounded-2xl card-surface p-7">
<span className="icon-tile w-10 h-10 mb-3" style={cssVars({ '--track-color': 'var(--brand-accent)' })}><Icon name={visionMissionIcons.mission} className="w-5 h-5" /></span> <span className="icon-tile w-10 h-10 mb-3" style={cssVars({ '--track-color': 'var(--brand-accent)' })}><Icon name={visionMissionIcons.mission} className="w-5 h-5" /></span>
<div className="text-body font-mono tracking-[0.15em] text-[var(--accent)] mb-2">MISSION · 使命</div> <div className="text-body font-mono tracking-[0.15em] text-[var(--accent)] mb-2">{tr('MISSION · 使命', 'MISSION · 使命', 'MISSION')}</div>
<p className="text-body leading-[1.8] text-[var(--brand-black)] font-medium">{a.visionMission.mission}</p> <p className="text-body leading-[1.8] text-[var(--brand-black)] font-medium">{a.visionMission.mission}</p>
</div> </div>
</div> </div>
<div className="grid sm:grid-cols-2 gap-4 mt-4"> <div className="grid sm:grid-cols-2 gap-4 mt-4">
<div className="scroll-reveal rounded-2xl card-surface p-7"> <div className="scroll-reveal rounded-2xl card-surface p-7">
<span className="icon-tile w-10 h-10 mb-3" style={cssVars({ '--track-color': 'var(--brand-accent)' })}><Icon name={visionMissionIcons.purpose} className="w-5 h-5" /></span> <span className="icon-tile w-10 h-10 mb-3" style={cssVars({ '--track-color': 'var(--brand-accent)' })}><Icon name={visionMissionIcons.purpose} className="w-5 h-5" /></span>
<div className="text-body font-mono tracking-[0.15em] text-[var(--accent)] mb-2">PURPOSE · 初心</div> <div className="text-body font-mono tracking-[0.15em] text-[var(--accent)] mb-2">{tr('PURPOSE · 初心', 'PURPOSE · 初心', 'PURPOSE')}</div>
<p className="text-body leading-[1.8] text-[var(--brand-black)] font-medium">{a.visionMission.purpose}</p> <p className="text-body leading-[1.8] text-[var(--brand-black)] font-medium">{a.visionMission.purpose}</p>
</div> </div>
<div className="scroll-reveal rounded-2xl card-surface p-7"> <div className="scroll-reveal rounded-2xl card-surface p-7">
<span className="icon-tile w-10 h-10 mb-3" style={cssVars({ '--track-color': 'var(--brand-accent)' })}><Icon name={visionMissionIcons.culture} className="w-5 h-5" /></span> <span className="icon-tile w-10 h-10 mb-3" style={cssVars({ '--track-color': 'var(--brand-accent)' })}><Icon name={visionMissionIcons.culture} className="w-5 h-5" /></span>
<div className="text-body font-mono tracking-[0.15em] text-[var(--accent)] mb-2">CULTURE · 文化</div> <div className="text-body font-mono tracking-[0.15em] text-[var(--accent)] mb-2">{tr('CULTURE · 文化', 'CULTURE · 文化', 'CULTURE')}</div>
<p className="text-body leading-[1.8] text-[var(--brand-black)] font-medium">{a.visionMission.culture}</p> <p className="text-body leading-[1.8] text-[var(--brand-black)] font-medium">{a.visionMission.culture}</p>
</div> </div>
</div> </div>
@ -183,7 +293,7 @@ export default function About({ locale }: { locale: Locale }) {
</span> </span>
<h3 className="text-body font-semibold text-[var(--brand-black)]">{item.c.label}</h3> <h3 className="text-body font-semibold text-[var(--brand-black)]">{item.c.label}</h3>
<span className="text-body font-mono mt-1" style={cssVars({ color: 'var(--brand-accent)' })}>{item.c.position}</span> <span className="text-body font-mono mt-1" style={cssVars({ color: 'var(--brand-accent)' })}>{item.c.position}</span>
<span className="mt-3 text-body text-[var(--text-muted)]">{zh ? '点击查看' : 'Tap to flip'}</span> <span className="mt-3 text-body text-[var(--text-muted)]">{tr('点击查看', '點擊查看', 'Tap to flip')}</span>
</div> </div>
<div <div
className="flip-card-back absolute inset-0 rounded-2xl border p-5 flex flex-col justify-center text-center" className="flip-card-back absolute inset-0 rounded-2xl border p-5 flex flex-col justify-center text-center"
@ -208,48 +318,117 @@ export default function About({ locale }: { locale: Locale }) {
</div> </div>
</section> </section>
{/* ===== 管理团队 #about-team ===== */} {/* ===== 管理团队 #about-team
<section id="about-team" className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-[var(--space-9)] scroll-mt-24"> 参照数字 Nexus「06 | Team」:左标题导语 / 中三卡轮播人像 / 右当前成员详情 + 圆点分页。
<div className="section-head text-center scroll-reveal"> OSS 暂无成员照片,先以品牌渐变 + 姓名首字占位,i18n 补 photo 字段即自动换真照片。 */}
<span className="section-eyebrow">{a.subnav.team.title}</span> <section id="about-team" className="band scroll-mt-24">
<h2 className="section-title">{a.subnav.team.title}</h2> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-[var(--space-9)]">
<p className="section-lead">{a.subnav.team.desc}</p> {/* 章节标签(右对齐,写法同首页 INTRODUCTION 标签) */}
</div> <div className="mb-10 flex items-center justify-end gap-3 scroll-reveal">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <span className="w-px h-6 bg-[var(--border-medium)]" aria-hidden="true" />
{a.subnav.team.members.map((m, i) => { <span className="text-body-sm font-mono uppercase tracking-[0.22em] text-[var(--text-muted)]">TEAM</span>
const defaultAvatar = m.gender === 'female' </div>
? 'https://oss.baskbuddy.com/baskbuddy/team-female.jpg'
: m.gender === 'male' <div className="grid items-center gap-10 lg:grid-cols-[minmax(200px,260px)_minmax(0,1fr)_minmax(200px,280px)] lg:gap-8 xl:gap-12">
? 'https://oss.baskbuddy.com/baskbuddy/team-male.jpg' {/* 左:标题 + 导语 */}
: null; <div className="scroll-reveal">
const avatar = m.photo || defaultAvatar; <h2 className="text-h2 font-bold font-[family-name:var(--font-display)] text-[var(--brand-black)] leading-tight pb-4 border-b border-[var(--border-light)]">
return ( {a.subnav.team.title}
<div key={i} className="scroll-reveal rounded-2xl card-surface p-6 flex flex-col items-center text-center"> </h2>
<div <p className="mt-5 text-body leading-[1.9] text-[var(--text-secondary)]">{a.subnav.team.intro}</p>
className="w-20 h-20 rounded-full overflow-hidden mb-4 flex items-center justify-center select-none" </div>
style={cssVars({
background: avatar ? 'var(--brand-accent-soft)' : 'var(--brand-accent-soft)', {/* 中:胶带式滑动轮播 —— 全员连成一条轨道,切换时整体向左平滑推出(margin-left 过渡,
})} 百分比相对视口宽,免 JS 测量居中)。4s 自动轮播,悬停/聚焦暂停;点任意非当前卡切换。
aria-hidden="true" 卡片尺寸 w-64(16rem) + 间距 gap-6(1.5rem),步进 17.5rem。 */}
<div
className="overflow-hidden"
onMouseEnter={() => setTeamPaused(true)}
onMouseLeave={() => setTeamPaused(false)}
onFocus={() => setTeamPaused(true)}
onBlur={() => setTeamPaused(false)}
>
<div
className="flex gap-6 will-change-transform transition-[margin-left] duration-500 ease-out motion-reduce:transition-none"
style={{ marginLeft: `calc(50% - 8rem - ${teamIdx} * 17.5rem)` }}
>
{teamMembers.map((m, i) => (
<button
key={m.name}
type="button"
onClick={() => i !== teamIdx && setTeamIdx(i)}
tabIndex={i === teamIdx ? -1 : 0}
aria-label={m.name}
aria-current={i === teamIdx}
className={`relative w-64 aspect-[3/4] shrink-0 overflow-hidden bg-[var(--bg-subtle)] transition-[transform,opacity,box-shadow] duration-500 ease-out motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand-accent)] ${
i === teamIdx
? 'scale-100 opacity-100 shadow-[var(--shadow-lg)] cursor-default'
: 'scale-[0.9] opacity-70 shadow-none hover:opacity-90'
}`}
>
{renderPortrait(m, i === teamIdx)}
</button>
))}
</div>
</div>
{/* 右:当前成员详情 + 序号/圆点分页 */}
<div className="scroll-reveal" aria-live="polite">
<div className="text-body-sm font-mono uppercase tracking-[0.22em] text-[var(--accent)]">{current.group}</div>
<div className="mt-3 text-h1 font-bold font-[family-name:var(--font-display)] text-[var(--brand-black)] leading-tight">{current.name}</div>
<div className="mt-2 text-body text-[var(--text-secondary)]">{current.role}</div>
{/* 只有带长简介的成员才有详情入口,其余成员版面保持原样 */}
{current.detailId && (
<button
type="button"
ref={profileTriggerRef}
aria-haspopup="dialog"
onClick={() => openProfile(current)}
// 悬停/聚焦就先把这个语种的简介 chunk 拉下来,点开时基本已就绪,
// 占位文案一闪而过(不 preload 的话首屏不会为它多付一分钱)
onMouseEnter={prefetchDetails}
onFocus={prefetchDetails}
className="mt-5 inline-flex items-center gap-2 rounded-full border border-[var(--border-medium)] px-4 py-2 text-body-sm font-semibold text-[var(--brand-black)] transition-colors duration-200 hover:border-[var(--brand-accent)] hover:text-[var(--brand-accent-deep)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand-accent)]"
> >
{avatar ? ( {a.subnav.team.ui.viewProfile}
<img src={avatar} alt="" className="w-full h-full object-cover" loading="lazy" /> <Icon name="arrowRight" className="h-4 w-4" />
) : ( </button>
<span )}
className="font-semibold text-xl leading-none"
style={cssVars({ color: 'var(--brand-accent-deep)' })} <div className="mt-8 flex items-center gap-4">
> <span className="text-h3 font-light font-mono text-[var(--text-muted)]">{String(teamIdx + 1).padStart(2, '0')}</span>
{memberInitial(m.name)} <div className="flex flex-wrap items-center gap-2.5">
</span> {teamMembers.map((m, i) => (
)} <button
key={m.name}
type="button"
onClick={() => setTeamIdx(i)}
aria-label={m.name}
aria-current={i === teamIdx}
className={`h-2.5 w-2.5 rounded-full border transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand-accent)] ${
i === teamIdx
? 'border-[var(--brand-black)] bg-[var(--brand-black)]'
: 'border-[var(--text-muted)] bg-transparent hover:border-[var(--brand-black)]'
}`}
/>
))}
</div> </div>
<div className="text-body font-semibold text-[var(--brand-black)]">{m.name}</div>
<div className="text-body text-[var(--text-muted)] mt-1">{m.role}</div>
</div> </div>
); </div>
})} </div>
</div> </div>
</section> </section>
{/* 成员详情弹窗(原生 <dialog> + showModal,渲染在 top layer) */}
{profile && (
<MemberProfileModal
member={profile}
detail={profileDetail}
labels={a.subnav.team.ui}
onClose={closeProfile}
/>
)}
</> </>
); );
} }

23
src/components/Business.tsx

@ -16,7 +16,6 @@ export default function Business({ locale }: { locale: Locale }) {
const t = getTranslation(locale); const t = getTranslation(locale);
const b = t.business; const b = t.business;
const inv = t.innovation; const inv = t.innovation;
const isZh = locale !== 'en';
const tr = (zh: string, hk: string, en: string) => (locale === 'en' ? en : locale === 'hk' ? hk : zh); const tr = (zh: string, hk: string, en: string) => (locale === 'en' ? en : locale === 'hk' ? hk : zh);
return ( return (
@ -55,11 +54,13 @@ export default function Business({ locale }: { locale: Locale }) {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-[var(--space-8)]"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-[var(--space-8)]">
<div className="text-center scroll-reveal mb-2"> <div className="text-center scroll-reveal mb-2">
<span className="section-eyebrow">ECOSYSTEM</span> <span className="section-eyebrow">ECOSYSTEM</span>
<h2 className="section-title mt-3">{isZh ? '数字生活生态全景图' : 'Digital Life Ecosystem Map'}</h2> <h2 className="section-title mt-3">{tr('数字生活生态全景图', '數字生活生態全景圖', 'Digital Life Ecosystem Map')}</h2>
<p className="section-lead max-w-2xl mx-auto"> <p className="section-lead max-w-2xl mx-auto">
{isZh {tr(
? '同一组产品,两种视角——按赛道看我们造什么,按受众看我们为谁。' '同一组产品,两种视角——按赛道看我们造什么,按受众看我们为谁。',
: 'One product family, two lenses — by track for what we build, by audience for who we serve.'} '同一組產品,兩種視角——按賽道看我們造什麼,按受眾看我們為誰。',
'One product family, two lenses — by track for what we build, by audience for who we serve.',
)}
</p> </p>
</div> </div>
<div className="scroll-reveal"> <div className="scroll-reveal">
@ -77,11 +78,11 @@ export default function Business({ locale }: { locale: Locale }) {
<span className="icon-tile w-12 h-12 shrink-0"> <span className="icon-tile w-12 h-12 shrink-0">
<Icon name={track.icon} className="w-6 h-6" /> <Icon name={track.icon} className="w-6 h-6" />
</span> </span>
<h2 className="text-h1 font-bold font-[var(--font-display)] text-[var(--brand-black)] leading-tight">{track.name}</h2> <h2 className="text-h1 font-bold font-[family-name:var(--font-display)] text-[var(--brand-black)] leading-tight">{track.name}</h2>
</div> </div>
<span className="font-mono text-body tracking-[0.15em] text-[var(--text-muted)]">{String(idx + 1).padStart(2, '0')} / {String(b.tracks.length).padStart(2, '0')}</span> <span className="font-mono text-body tracking-[0.15em] text-[var(--text-muted)]">{String(idx + 1).padStart(2, '0')} / {String(b.tracks.length).padStart(2, '0')}</span>
</div> </div>
<p className="text-body font-medium font-[var(--font-display)] text-[var(--brand-accent)] leading-relaxed mb-8 max-w-3xl"> <p className="text-body font-medium font-[family-name:var(--font-display)] text-[var(--brand-accent)] leading-relaxed mb-8 max-w-3xl">
{trackTaglines[idx][locale]} {trackTaglines[idx][locale]}
</p> </p>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-3.5"> <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-3.5">
@ -185,15 +186,15 @@ export default function Business({ locale }: { locale: Locale }) {
{/* ===== CTA Band ===== */} {/* ===== CTA Band ===== */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-[var(--space-10)]"> <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 text-center"> <div className="cta-band scroll-reveal px-8 py-14 text-center">
<h2 className="relative z-10 text-h1 font-bold font-[var(--font-display)] text-white"> <h2 className="relative z-10 text-h1 font-bold font-[family-name:var(--font-display)] text-white">
{locale === 'en' ? 'Build the digital life ecosystem with BaskBuddy' : '与巴适伙伴,共建数字生活生态'} {tr('与巴适伙伴,共建数字生活生态', '與巴適夥伴,共建數字生活生態', 'Build the digital life ecosystem with BaskBuddy')}
</h2> </h2>
<div className="relative z-10 mt-7 flex flex-col sm:flex-row items-center justify-center gap-3.5"> <div className="relative z-10 mt-7 flex flex-col sm:flex-row items-center justify-center gap-3.5">
<a href={getLocalizedPath('/contact', locale)} className="btn btn-light btn-lg"> <a href={getLocalizedPath('/contact', locale)} className="btn btn-light btn-lg">
{locale === 'en' ? 'Business Cooperation' : '商务合作'} {tr('商务合作', '商務合作', 'Business Cooperation')}
</a> </a>
<a href={getLocalizedPath('/about', locale)} className="btn btn-outline-light btn-lg"> <a href={getLocalizedPath('/about', locale)} className="btn btn-outline-light btn-lg">
{locale === 'en' ? 'About Us' : '了解我们'} {tr('了解我们', '了解我們', 'About Us')}
</a> </a>
</div> </div>
</div> </div>

113
src/components/CompanyVideo.tsx

@ -0,0 +1,113 @@
import { getTranslation, type Locale } from '../i18n/utils';
/* =====================================================================
* 公司宣传视频展示模块(首页第二屏)
*
* 版式:左栏通栏品牌色面板承载文案,右栏为「INTRODUCTION」+ 视频画面。
* 播放策略:直接展示浏览器原生控制条(<video controls>),preload="metadata"。
* ===================================================================== */
/**
* 视频地址(和其他素材一致,走 OSS 直链)。
* OSS 上为压好的网页版(约 16 MB,见 Content-Length: 16030006);未压缩母版
* (约 173 MB)放在仓库根目录 media/ 下仅供本地留档——该目录已被 .gitignore
* 忽略,且不在 public/ 内,因此不会被打进 dist/ 或上传到服务器。
*/
export const COMPANY_VIDEO_SRC = 'https://oss.baskbuddy.com/baskbuddy/introduction.mp4';
/**
* 可选封面图(16:9 观感最佳)。
* 留空则直接展示视频首帧,无需额外准备图片。
*/
export const COMPANY_VIDEO_POSTER = '';
/**
* 左侧面板底色。
* 指定品牌蓝 #3d5afe(与 ICIDP「激情」同色),白字对比度约 5.1:1,稳过 AA。
*/
const PANEL_BG = '#3d5afe';
export default function CompanyVideo({ locale }: { locale: Locale }) {
const v = getTranslation(locale).home.video;
return (
<section className="relative w-full overflow-hidden bg-[var(--bg-primary)] xl:flex xl:min-h-screen xl:items-center">
{/* 桌面端:左侧固定 850px 的品牌色面板,通栏到视口左/上/下缘;
整段 min-h-screen 占满整屏,内容垂直居中。 */}
<div
aria-hidden="true"
className="hidden xl:block absolute inset-y-0 left-0 w-[850px] max-w-[68vw]"
style={{ background: PANEL_BG }}
/>
<div className="relative w-full px-4 sm:px-6 xl:px-0">
<div className="xl:flex xl:items-center">
{/* ===== 左栏:文案(坐落在 850px 蓝色面板内) =====
移动/平板端由文案块自带品牌色底并通栏出血;xl 起交给上面的通栏面板。
版式参照数字 Nexus「01 | Introduction」:大数字 + 单位 + 一段说明的统计块纵列。 */}
<div className="relative xl:w-[850px] xl:shrink-0">
<span
aria-hidden="true"
className="xl:hidden absolute -inset-x-4 sm:-inset-x-6 inset-y-0"
style={{ background: PANEL_BG }}
/>
<div className="relative -mx-4 sm:-mx-6 px-4 sm:px-6 py-14 xl:mx-0 xl:px-0 xl:py-24 xl:pl-20 xl:pr-24">
<h2 className="max-w-[560px] text-h2 xl:text-h1 font-bold font-[family-name:var(--font-display)] leading-[1.15] tracking-tight text-[var(--brand-white)]">
{v.title}
</h2>
<p className="mt-3 max-w-[520px] text-body leading-[1.9] text-[color-mix(in_oklch,var(--brand-white)_80%,transparent)]">
{v.subtitle}
</p>
<div className="mt-10 flex flex-col gap-9 xl:mt-12 xl:gap-10">
{v.intro.map((item) => (
<div key={item.unit} className="scroll-reveal">
<div className="flex flex-wrap items-baseline gap-x-2.5 gap-y-1">
<span className="text-h1 font-bold leading-none font-[family-name:var(--font-display)] tracking-tight text-[var(--brand-white)] whitespace-nowrap">
{item.value}
</span>
<span className="text-h3 font-semibold text-[var(--brand-white)]">
{item.unit}
</span>
</div>
<p className="mt-2.5 max-w-[520px] text-body leading-[1.85] text-[color-mix(in_oklch,var(--brand-white)_78%,transparent)]">
{item.desc}
</p>
</div>
))}
</div>
</div>
</div>
{/* ===== 右栏:章节序号 + 视频画面 =====
视频为直角画框,负外边距左移压在蓝色面板上;宽度随右栏自动拉伸,
在 1920+ 视口约占页面宽度 62%~65%。 */}
<div className="relative mt-12 pb-14 xl:mt-0 xl:min-w-0 xl:flex-1 xl:py-24 xl:pr-8 2xl:pr-12">
<div className="xl:-ml-40">
<div className="mb-6 flex items-center justify-end gap-3">
<span className="text-body-sm font-mono uppercase tracking-[0.22em] text-[var(--text-muted)]">
{v.eyebrow}
</span>
</div>
{/* 16:9 直角画框:黑底兜底,封面未加载/比例不符时也不会露白;
直接展示原生控制条播放 */}
<div className="relative aspect-video w-full overflow-hidden bg-[var(--brand-black)] shadow-[var(--shadow-md)]">
<video
src={COMPANY_VIDEO_SRC}
poster={COMPANY_VIDEO_POSTER || undefined}
className="absolute inset-0 w-full h-full object-cover"
preload="metadata"
playsInline
controls
/>
</div>
</div>
</div>
</div>
</div>
</section>
);
}

28
src/components/Contact.tsx

@ -60,7 +60,7 @@ export default function Contact({ locale }: { locale: Locale }) {
{/* ===== 成长宣言 · 主体卡(完整全文 + 独立标题)===== */} {/* ===== 成长宣言 · 主体卡(完整全文 + 独立标题)===== */}
<section className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-[var(--space-6)]"> <section className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-[var(--space-6)]">
<div className="scroll-reveal mx-auto rounded-2xl card-surface p-7 md:p-10 text-center overflow-hidden" style={cssVars({ boxShadow: 'var(--shadow-lg)' })}> <div className="scroll-reveal mx-auto rounded-2xl card-surface p-7 md:p-10 text-center overflow-hidden" style={cssVars({ boxShadow: 'var(--shadow-lg)' })}>
<h2 className="text-h1 font-[var(--font-display)] font-bold text-[var(--brand-black)] tracking-tight text-center"> <h2 className="text-h1 font-[family-name:var(--font-display)] font-bold text-[var(--brand-black)] tracking-tight text-center">
{j.manifestoTitle} {j.manifestoTitle}
</h2> </h2>
<div className="mt-4 mx-auto w-16 h-[2px] rounded-full" style={{ background: 'linear-gradient(90deg, var(--brand-accent-deep), var(--brand-accent))' }} /> <div className="mt-4 mx-auto w-16 h-[2px] rounded-full" style={{ background: 'linear-gradient(90deg, var(--brand-accent-deep), var(--brand-accent))' }} />
@ -76,7 +76,7 @@ export default function Contact({ locale }: { locale: Locale }) {
<span className="relative z-10 inline-block text-micro font-semibold tracking-[0.15em] uppercase text-[color-mix(in_oklch,var(--brand)_70%,transparent)]" style={{ fontFamily: 'var(--font-body)' }}> <span className="relative z-10 inline-block text-micro font-semibold tracking-[0.15em] uppercase text-[color-mix(in_oklch,var(--brand)_70%,transparent)]" style={{ fontFamily: 'var(--font-body)' }}>
{j.manifestoCallout.eyebrow} {j.manifestoCallout.eyebrow}
</span> </span>
<h2 className="relative z-10 mt-3 text-h1 font-bold font-[var(--font-display)] text-white leading-tight"> <h2 className="relative z-10 mt-3 text-h1 font-bold font-[family-name:var(--font-display)] text-white leading-tight">
{j.manifestoCallout.title} {j.manifestoCallout.title}
</h2> </h2>
<p className="relative z-10 mt-4 text-body md:text-body text-[color-mix(in_oklch,var(--brand-white)_85%,transparent)] max-w-2xl mx-auto leading-relaxed"> <p className="relative z-10 mt-4 text-body md:text-body text-[color-mix(in_oklch,var(--brand-white)_85%,transparent)] max-w-2xl mx-auto leading-relaxed">
@ -202,7 +202,7 @@ export default function Contact({ locale }: { locale: Locale }) {
<div className="icon-tile w-10 h-10" style={cssVars({ '--track-color': 'var(--brand-accent)' })}> <div className="icon-tile w-10 h-10" style={cssVars({ '--track-color': 'var(--brand-accent)' })}>
<Icon name="briefcase" className="w-5 h-5" /> <Icon name="briefcase" className="w-5 h-5" />
</div> </div>
<h2 className="text-h2 font-[var(--font-display)] font-bold tracking-tight text-[var(--brand-black)]">{c.job.title}</h2> <h2 className="text-h2 font-[family-name:var(--font-display)] font-bold tracking-tight text-[var(--brand-black)]">{c.job.title}</h2>
</div> </div>
<p className="text-body leading-[1.7] text-[var(--text-secondary)] mb-3">{c.job.desc}</p> <p className="text-body leading-[1.7] text-[var(--text-secondary)] mb-3">{c.job.desc}</p>
<p className="text-body text-[var(--text-muted)] mb-6">{j.delivery.note}</p> <p className="text-body text-[var(--text-muted)] mb-6">{j.delivery.note}</p>
@ -229,7 +229,7 @@ export default function Contact({ locale }: { locale: Locale }) {
<div className="icon-tile w-10 h-10" style={cssVars({ '--track-color': 'var(--brand-accent)' })}> <div className="icon-tile w-10 h-10" style={cssVars({ '--track-color': 'var(--brand-accent)' })}>
<Icon name="handshake" className="w-5 h-5" /> <Icon name="handshake" className="w-5 h-5" />
</div> </div>
<h2 className="text-h2 font-[var(--font-display)] font-bold tracking-tight text-[var(--brand-black)]">{c.biz.title}</h2> <h2 className="text-h2 font-[family-name:var(--font-display)] font-bold tracking-tight text-[var(--brand-black)]">{c.biz.title}</h2>
</div> </div>
<p className="text-body leading-[1.7] text-[var(--text-secondary)] mb-6">{c.biz.desc}</p> <p className="text-body leading-[1.7] text-[var(--text-secondary)] mb-6">{c.biz.desc}</p>
<div className="mt-auto space-y-4"> <div className="mt-auto space-y-4">
@ -261,7 +261,11 @@ export default function Contact({ locale }: { locale: Locale }) {
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-[color-mix(in_oklch,var(--accent)_32%,transparent)] via-transparent to-transparent" /> <div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-[color-mix(in_oklch,var(--accent)_32%,transparent)] via-transparent to-transparent" />
</div> </div>
<p className="mt-4 text-center text-body text-[var(--text-muted)]"> <p className="mt-4 text-center text-body text-[var(--text-muted)]">
{locale === 'en' ? 'We are in Jing\'an, Shanghai — come say hi' : '我们在上海静安,等你来坐坐'} {locale === 'en'
? 'We are in Jing\'an, Shanghai — come say hi'
: locale === 'hk'
? '我們在上海靜安,等你來坐坐'
: '我们在上海静安,等你来坐坐'}
</p> </p>
</section> </section>
@ -270,11 +274,19 @@ export default function Contact({ locale }: { locale: Locale }) {
<div className="scroll-reveal rounded-2xl card-surface p-6 md:p-8" style={cssVars({ boxShadow: 'var(--shadow-sm)' })}> <div className="scroll-reveal rounded-2xl card-surface p-6 md:p-8" style={cssVars({ boxShadow: 'var(--shadow-sm)' })}>
<div className="flex flex-col sm:flex-row gap-6"> <div className="flex flex-col sm:flex-row gap-6">
<div className="flex-1"> <div className="flex-1">
<h3 className="text-body font-[var(--font-display)] font-bold mb-2 text-[var(--brand-black)]"> <h3 className="text-body font-[family-name:var(--font-display)] font-bold mb-2 text-[var(--brand-black)]">
{locale === 'en' ? 'BaskBuddy (Shanghai) Technology Co., Ltd.' : '巴适伙伴(上海)科技有限公司'} {locale === 'en'
? 'BaskBuddy (Shanghai) Technology Co., Ltd.'
: locale === 'hk'
? '巴適夥伴(上海)科技有限公司'
: '巴适伙伴(上海)科技有限公司'}
</h3> </h3>
<p className="text-body text-[var(--text-muted)] leading-relaxed mb-4"> <p className="text-body text-[var(--text-muted)] leading-relaxed mb-4">
{locale === 'en' ? "Room 410, No.2, Lane 288, Kangning Rd, Jing'an District, Shanghai" : '上海市静安区康宁路288弄2号410室'} {locale === 'en'
? "Room 410, No.2, Lane 288, Kangning Rd, Jing'an District, Shanghai"
: locale === 'hk'
? '上海市靜安區康寧路288弄2號410室'
: '上海市静安区康宁路288弄2号410室'}
</p> </p>
<p className="text-body text-[var(--text-muted)] leading-relaxed"> <p className="text-body text-[var(--text-muted)] leading-relaxed">
{c.privacyNote} {c.privacyNote}

1
src/components/EcosystemMap.tsx

@ -1,7 +1,6 @@
import { useState, type CSSProperties } from 'react'; import { useState, type CSSProperties } from 'react';
import { getTranslation, type Locale } from '../i18n/utils'; import { getTranslation, type Locale } from '../i18n/utils';
import { Icon } from '../icons'; import { Icon } from '../icons';
import { cssVars } from './css';
/* ===================================================================== /* =====================================================================
* 数字生活生态全景图 — 首页 3 受众 × 业务页 5 赛道的「桥」 * 数字生活生态全景图 — 首页 3 受众 × 业务页 5 赛道的「桥」

272
src/components/Home.tsx

@ -1,194 +1,7 @@
import { useEffect, useRef, type CSSProperties } from 'react';
import { getLocalizedPath, getTranslation, type Locale } from '../i18n/utils'; import { getLocalizedPath, getTranslation, type Locale } from '../i18n/utils';
import { PLACEHOLDER_IMG } from '../placeholder';
import { cssVars } from './css'; import { cssVars } from './css';
import { Icon, statsIcons } from '../icons'; import { Icon, statsIcons } from '../icons';
import CompanyVideo from './CompanyVideo';
/**
* 首屏全屏粒子 + 邻近连线 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 }) { export default function Home({ locale }: { locale: Locale }) {
const t = getTranslation(locale); const t = getTranslation(locale);
@ -200,22 +13,6 @@ export default function Home({ locale }: { locale: Locale }) {
const tr = (en: string, hk: string, zh: string) => const tr = (en: string, hk: string, zh: string) =>
locale === 'en' ? en : locale === 'hk' ? hk : zh; 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 ( return (
<> <>
{/* ===== HERO:极简版(用户反馈满屏星星太脏)—— 只保留干净 hero-bg.jpg 原图 + 左下文字阴影 ===== */} {/* ===== HERO:极简版(用户反馈满屏星星太脏)—— 只保留干净 hero-bg.jpg 原图 + 左下文字阴影 ===== */}
@ -242,7 +39,7 @@ export default function Home({ locale }: { locale: Locale }) {
<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="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"> <div className="max-w-3xl scroll-reveal">
<h1 className="text-h1 font-bold leading-[1.1] tracking-tight text-white font-[var(--font-display)]"> <h1 className="text-h1 font-bold leading-[1.1] tracking-tight text-white font-[family-name:var(--font-display)]">
{h.heroTitle} {h.heroTitle}
</h1> </h1>
<p className="mt-5 text-body md:text-body text-white/90 max-w-xl leading-relaxed"> <p className="mt-5 text-body md:text-body text-white/90 max-w-xl leading-relaxed">
@ -259,51 +56,25 @@ export default function Home({ locale }: { locale: Locale }) {
</div> </div>
</section> </section>
{/* ===== 最新资讯(腾讯式三列卡片) ===== */} {/* ===== 公司宣传视频(紧跟首屏,进入即第二屏) ===== */}
<section className="bg-[var(--bg-primary)]"> <CompanyVideo locale={locale} />
<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)]"> 浅灰 band 承接蓝色 INTRODUCTION 与黑底支柱区:蓝 → 浅灰 → 黑,节奏过渡更顺 ===== */}
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 text-center scroll-reveal"> <section className="band">
<p className="text-body md:text-h1 font-bold font-[var(--font-display)] text-[var(--brand-black)] leading-[1.35] tracking-tight"> <div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 pt-[var(--space-10)] pb-[var(--space-9)] text-center scroll-reveal">
<p className="text-body md:text-h1 font-bold font-[family-name:var(--font-display)] text-[var(--brand-black)] leading-[1.35] tracking-tight">
{tr( {tr(
'BaskBuddy improves the quality of life for people around the world through innovative technology, products and services.', 'BaskBuddy improves the quality of life for people around the world through innovative technology, products and services.',
'巴適夥伴透過創新的科技、產品和服務,提升全球各地人們的生活品質。', '巴適夥伴透過創新的科技、產品和服務,提升全球各地人們的生活品質。',
'巴适伙伴通过创新的科技、产品和服务,提升全球各地人们的生活品质。', '巴适伙伴通过创新的科技、产品和服务,提升全球各地人们的生活品质。',
)} )}
</p> </p>
</div> <div className="mt-12 flex flex-col items-center text-[var(--text-muted)]">
<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="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" />
<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" />
<Icon name="arrowDown" className="w-4 h-4 mt-1 animate-bounce" /> </div>
</div> </div>
</section> </section>
@ -336,7 +107,7 @@ export default function Home({ locale }: { locale: Locale }) {
/> />
<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"> <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> <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"> <h2 className="mt-3 text-h1 font-bold font-[family-name:var(--font-display)] text-white leading-tight">
{p.name} {p.name}
</h2> </h2>
<p className="mt-4 text-body md:text-body text-white/80 max-w-xl leading-relaxed">{p.desc}</p> <p className="mt-4 text-body md:text-body text-white/80 max-w-xl leading-relaxed">{p.desc}</p>
@ -349,11 +120,14 @@ export default function Home({ locale }: { locale: Locale }) {
))} ))}
</section> </section>
{/* ===== 愿景带 + 巴适速度 大数字 ===== */} {/* ===== 愿景带 + 愿景性指标 =====
只留「全球 / 长期」两个愿景指标:5 大核心赛道 / 8+ 自研产品 已由上方
CompanyVideo 左栏(home.video.intro)讲述,此处不再重复。
两格用 .stat-band--pair 在桌面端保持 2 列(默认 .stat-band 会排 4 列,空两格)。 */}
<section className="band !bg-[var(--bg-subtle)]"> <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"> <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> <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"> <h2 className="text-h1 md:text-h1 font-bold font-[family-name:var(--font-display)] text-[var(--brand-black)] mt-3">
{tr('Value for Users, Tech for Good', '用戶為本 · 科技向善', '用户为本 · 科技向善')} {tr('Value for Users, Tech for Good', '用戶為本 · 科技向善', '用户为本 · 科技向善')}
</h2> </h2>
<p className="mt-5 text-body text-[var(--text-secondary)] max-w-3xl mx-auto leading-relaxed"> <p className="mt-5 text-body text-[var(--text-secondary)] max-w-3xl mx-auto leading-relaxed">
@ -365,12 +139,12 @@ export default function Home({ locale }: { locale: Locale }) {
</p> </p>
</div> </div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-[var(--space-9)]"> <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"> <div className="stat-band stat-band--pair scroll-reveal mt-6">
{stats.map((sItem, idx) => ( {stats.map((sItem, idx) => (
<div className="stat-cell" key={sItem.label} style={cssVars({ '--track-color': 'var(--accent)' })}> <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)' }} /> <Icon name={statsIcons[idx]} className="w-8 h-8 mx-auto mb-3" style={{ color: 'var(--brand-accent)' }} />
<div className="stat-num"> <div className="stat-num">
<span className="stat-value">{sItem.value}</span> <span>{sItem.value}</span>
{sItem.suffix && <span className="stat-suffix">{sItem.suffix}</span>} {sItem.suffix && <span className="stat-suffix">{sItem.suffix}</span>}
</div> </div>
<div className="stat-label">{sItem.label}</div> <div className="stat-label">{sItem.label}</div>
@ -384,7 +158,7 @@ export default function Home({ locale }: { locale: Locale }) {
{/* ===== CTA Band ===== */} {/* ===== CTA Band ===== */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-[var(--space-10)]"> <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"> <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> <h2 className="relative z-10 text-h1 font-bold font-[family-name: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"> <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( {tr(
'Whether for a job inquiry or business cooperation, we look forward to meeting you with warmth.', 'Whether for a job inquiry or business cooperation, we look forward to meeting you with warmth.',

4
src/components/Investors.tsx

@ -31,7 +31,7 @@ export default function Investors({ locale }: { locale: Locale }) {
<Icon name={statsIcons[idx]} className="w-6 h-6" strokeWidth={2} style={{ color: 'var(--brand-accent)' }} /> <Icon name={statsIcons[idx]} className="w-6 h-6" strokeWidth={2} style={{ color: 'var(--brand-accent)' }} />
</span> </span>
<div className="stat-num"> <div className="stat-num">
<span className="stat-value">{sItem.value}</span> <span>{sItem.value}</span>
{sItem.suffix && <span className="stat-suffix">{sItem.suffix}</span>} {sItem.suffix && <span className="stat-suffix">{sItem.suffix}</span>}
</div> </div>
<div className="stat-label">{sItem.label}</div> <div className="stat-label">{sItem.label}</div>
@ -129,7 +129,7 @@ export default function Investors({ locale }: { locale: Locale }) {
<span className="relative z-10 inline-block text-micro font-semibold tracking-[0.15em] uppercase text-[color-mix(in_oklch,var(--brand)_70%,transparent)]" style={{ fontFamily: 'var(--font-body)' }}> <span className="relative z-10 inline-block text-micro font-semibold tracking-[0.15em] uppercase text-[color-mix(in_oklch,var(--brand)_70%,transparent)]" style={{ fontFamily: 'var(--font-body)' }}>
{ir.contact.eyebrow} {ir.contact.eyebrow}
</span> </span>
<h2 className="relative z-10 mt-3 text-h1 font-bold font-[var(--font-display)] text-white leading-tight">{ir.contact.title}</h2> <h2 className="relative z-10 mt-3 text-h1 font-bold font-[family-name:var(--font-display)] text-white leading-tight">{ir.contact.title}</h2>
<p className="relative z-10 mt-4 text-body text-[color-mix(in_oklch,var(--brand-white)_85%,transparent)] max-w-xl mx-auto leading-relaxed">{ir.contact.desc}</p> <p className="relative z-10 mt-4 text-body text-[color-mix(in_oklch,var(--brand-white)_85%,transparent)] max-w-xl mx-auto leading-relaxed">{ir.contact.desc}</p>
<div className="relative z-10 mt-7 flex flex-col sm:flex-row items-center justify-center gap-3.5"> <div className="relative z-10 mt-7 flex flex-col sm:flex-row items-center justify-center gap-3.5">
<a href={'mailto:' + ir.contact.email} className="btn btn-light btn-lg"> <a href={'mailto:' + ir.contact.email} className="btn btn-light btn-lg">

71
src/components/Mascot.tsx

@ -31,54 +31,54 @@ const MASCOT: Record<string, PageTriggers> = {
], ],
}, },
scroll: [ scroll: [
{ zh: '五大赛道都在这里,想深入了解的话我可以带你跳过去~', hk: '五大赛道都在這裡,想深入了解的話我可以帶你跳過去~', en: 'All five tracks are here — let me jump you straight to whichever you like!' }, { zh: '五大赛道都在这里,想深入了解的话我可以带你跳过去~', hk: '五大賽道都在這裡,想深入了解的話我可以帶你跳過去~', en: 'All five tracks are here — let me jump you straight to whichever you like!' },
{ zh: '慢慢看,不急,好东西值得多停留~', hk: '慢慢看,不急,好东西值得多停留~', en: 'Take your time — good things deserve a second glance.' }, { zh: '慢慢看,不急,好东西值得多停留~', hk: '慢慢看,不急,好東西值得多停留~', en: 'Take your time — good things deserve a second glance.' },
], ],
idle: [ idle: [
{ zh: '在想什么呢?要不要我给你讲讲巴适伙伴?', hk: '在想什麼呢?要不要我给你讲讲巴适伙伴?', en: 'Deep in thought? Want me to walk you through BaskBuddy?' }, { zh: '在想什么呢?要不要我给你讲讲巴适伙伴?', hk: '在想什麼呢?要不要我給你講講巴適夥伴?', en: 'Deep in thought? Want me to walk you through BaskBuddy?' },
{ zh: '别累着自己哦,喝口水歇会儿~', hk: '别累着自己哦,喝口水歇会儿~', en: 'Take a breather — grab a sip of water!' }, { zh: '别累着自己哦,喝口水歇会儿~', hk: '別累著自己哦,喝口水歇會兒~', en: 'Take a breather — grab a sip of water!' },
], ],
}, },
'/about': { '/about': {
enter: { enter: {
mood: 'think', mood: 'think',
msgs: [ msgs: [
{ zh: '想了解我们是怎么来的吗?我慢慢讲给你听~', hk: '想了解我们是怎么来的吗?我慢慢讲给你听~', en: "Curious how we started? Let me tell you the story." }, { zh: '想了解我们是怎么来的吗?我慢慢讲给你听~', hk: '想了解我們是怎麼來的嗎?我慢慢講給你聽~', en: "Curious how we started? Let me tell you the story." },
{ zh: '我们叫「巴适伙伴」,就是想让你的生活更巴适。', hk: '我们叫「巴适伙伴」,就是想让你的生活更巴适。', en: "Our name means 'comfy companion' — we want every day to feel easy." }, { zh: '我们叫「巴适伙伴」,就是想让你的生活更巴适。', hk: '我們叫「巴適夥伴」,就是想讓你的生活更巴適。', en: "Our name means 'comfy companion' — we want every day to feel easy." },
{ zh: '往下翻,看看我们背后的几个巴适小伙伴~', hk: '往下翻,看看我们背后的几个巴适小伙伴~', en: "Scroll down to meet the comfy buddies behind everything." }, { zh: '往下翻,看看我们背后的几个巴适小伙伴~', hk: '往下翻,看看我們背後的幾個巴適小夥伴~', en: "Scroll down to meet the comfy buddies behind everything." },
], ],
}, },
scroll: [ scroll: [
{ zh: 'ICIDP 五个字是我们的灵魂——点每张小卡都可以翻面看哦~', hk: 'ICIDP 五个字是我们的灵魂——点每张小卡都可以翻面看哦~', en: "ICIDP is our soul — tap each little card to flip it!" }, { zh: 'ICIDP 五个字是我们的灵魂——点每张小卡都可以翻面看哦~', hk: 'ICIDP 五個字是我們的靈魂——點每張小卡都可以翻面看哦~', en: "ICIDP is our soul — tap each little card to flip it!" },
], ],
idle: [ idle: [
{ zh: '对哪块感兴趣?点卡片,它们会"翻身"告诉你更多~', hk: '对哪块感兴趣?点卡片,它们会「翻身」告诉你更多~', en: "Curious about any part? The cards will 'flip' and tell you more!" }, { zh: '对哪块感兴趣?点卡片,它们会"翻身"告诉你更多~', hk: '對哪塊感興趣?點卡片,它們會「翻身」告訴你更多~', en: "Curious about any part? The cards will 'flip' and tell you more!" },
], ],
}, },
'/business': { '/business': {
enter: { enter: {
mood: 'cheer', mood: 'cheer',
msgs: [ msgs: [
{ zh: '业务大盘点!先看看我们的数字生活生态全景图~', hk: '业务大盘点!先看看我们的数字生活生态全景图~', en: 'Business time! Check the Digital Life Ecosystem Map first.' }, { zh: '业务大盘点!先看看我们的数字生活生态全景图~', hk: '業務大盤點!先看看我們的數字生活生態全景圖~', en: 'Business time! Check the Digital Life Ecosystem Map first.' },
{ zh: '产品卡可以翻面哦,点一下就能看到完整画像~', hk: '产品卡可以翻面哦,点一下就能看到完整画像~', en: "Product cards can flip — tap one to see the full profile!" }, { zh: '产品卡可以翻面哦,点一下就能看到完整画像~', hk: '產品卡可以翻面哦,點一下就能看到完整畫像~', en: "Product cards can flip — tap one to see the full profile!" },
], ],
}, },
flip: [ flip: [
{ zh: '哇!这张卡翻面了——每个产品都有点意思对吧?', hk: '哇!这张卡翻面了——每个产品都有点意思对吧?', en: "Whoa, flipped! Every product has its own little story, right?" }, { zh: '哇!这张卡翻面了——每个产品都有点意思对吧?', hk: '哇!這張卡翻面了——每個產品都有點意思對吧?', en: "Whoa, flipped! Every product has its own little story, right?" },
{ zh: '我超喜欢这一面的!定位、人群、生态都清清楚楚~', hk: '我超喜欢这一面的!定位、人群、生态都清清楚楚~', en: 'I love this side — role, users, ecosystem, all laid out!' }, { zh: '我超喜欢这一面的!定位、人群、生态都清清楚楚~', hk: '我超喜歡這一面的!定位、人群、生態都清清楚楚~', en: 'I love this side — role, users, ecosystem, all laid out!' },
], ],
eco: [ eco: [
{ zh: '全景图可以两种视角切换——按赛道或按受众,试试另一种!', hk: '全景图可以两种视角切换——按赛道或按受众,试试另一种!', en: 'Two views on the map — by track and by audience. Try switching!' }, { zh: '全景图可以两种视角切换——按赛道或按受众,试试另一种!', hk: '全景圖可以兩種視角切換——按賽道或按受眾,試試另一種!', en: 'Two views on the map — by track and by audience. Try switching!' },
], ],
idle: [ idle: [
{ zh: '五大赛道,选一个你感兴趣的深入看看?', hk: '五大赛道,選一個你感興趣的深入看看?', en: 'Five tracks — pick one that sparks your interest!' }, { zh: '五大赛道,选一个你感兴趣的深入看看?', hk: '五大賽道,選一個你感興趣的深入看看?', en: 'Five tracks — pick one that sparks your interest!' },
], ],
}, },
'/news': { '/news': {
enter: { enter: {
mood: 'think', mood: 'think',
msgs: [ msgs: [
{ zh: '新闻中心还在铺地基,很快就有好消息啦~', hk: '新闻中心还在铺地基,很快就有好消息啦~', en: 'The newsroom is under construction — good news coming soon!' }, { zh: '新闻中心还在铺地基,很快就有好消息啦~', hk: '新聞中心還在鋪地基,很快就有好消息啦~', en: 'The newsroom is under construction — good news coming soon!' },
], ],
}, },
}, },
@ -95,8 +95,8 @@ const MASCOT: Record<string, PageTriggers> = {
enter: { enter: {
mood: 'cheer', mood: 'cheer',
msgs: [ msgs: [
{ zh: '长期主义投资者的小天地!顺便看看我们的业务版图~', hk: '长期主义投资者的小天地!顺便看看我们的业务版图~', en: 'Long-termist\'s corner — while you’re here, check the business map too!' }, { zh: '长期主义投资者的小天地!顺便看看我们的业务版图~', hk: '長期主義投資者的小天地!順便看看我們的業務版圖~', en: 'Long-termist\'s corner — while you’re here, check the business map too!' },
{ zh: '我们不是讲故事,是在造生态——点那个蓝色链接看看~', hk: '我们不是讲故事,是在造生态——点那个蓝色链接看看~', en: 'We’re not just telling a story — we’re building an ecosystem.' }, { zh: '我们不是讲故事,是在造生态——点那个蓝色链接看看~', hk: '我們不是講故事,是在造生態——點那個藍色連結看看~', en: 'We’re not just telling a story — we’re building an ecosystem.' },
], ],
}, },
}, },
@ -104,8 +104,8 @@ const MASCOT: Record<string, PageTriggers> = {
enter: { enter: {
mood: 'wave', mood: 'wave',
msgs: [ msgs: [
{ zh: '想投递简历或谈合作?邮箱在这儿,复制就能发!', hk: '想投递简历或谈合作?邮箱在这儿,复制就能发!', en: 'Apply or pitch? Copy the email and you’re set!' }, { zh: '想投递简历或谈合作?邮箱在这儿,复制就能发!', hk: '想投遞簡歷或談合作?郵箱在這兒,複製就能發!', en: 'Apply or pitch? Copy the email and you’re set!' },
{ zh: '找不到路?别急,我帮你指路~', hk: '找不到路?别急,我帮你指路~', en: "Lost? No worries, let me point you the way." }, { zh: '找不到路?别急,我帮你指路~', hk: '找不到路?別急,我幫你指路~', en: "Lost? No worries, let me point you the way." },
], ],
}, },
}, },
@ -113,7 +113,7 @@ const MASCOT: Record<string, PageTriggers> = {
enter: { enter: {
mood: 'think', mood: 'think',
msgs: [ msgs: [
{ zh: '放心,你的信息我替你守得严严实实。', hk: '放心,你的信息我替你守得严严实实。', en: "Relax — your data is locked up safe with me." }, { zh: '放心,你的信息我替你守得严严实实。', hk: '放心,你的信息我替你守得嚴嚴實實。', en: "Relax — your data is locked up safe with me." },
], ],
}, },
}, },
@ -121,7 +121,7 @@ const MASCOT: Record<string, PageTriggers> = {
enter: { enter: {
mood: 'wave', mood: 'wave',
msgs: [ msgs: [
{ zh: '哎呀,这条小路好像没铺好~我带你回主页?', hk: '哎呀,这条小路好像没铺好~我带你回主页?', en: "Oops, this path isn't built yet — shall I walk you home?" }, { zh: '哎呀,这条小路好像没铺好~我带你回主页?', hk: '哎呀,這條小路好像沒鋪好~我帶你回主頁?', en: "Oops, this path isn't built yet — shall I walk you home?" },
], ],
}, },
}, },
@ -387,17 +387,24 @@ export default function Mascot() {
<rect className="mascot-accent" x="54" y="28" width="12" height="6" rx="3" /> <rect className="mascot-accent" x="54" y="28" width="12" height="6" rx="3" />
{/* 车头灯(暖琥珀黄,像车灯发光) */} {/* 车头灯(暖琥珀黄,像车灯发光) */}
<circle className="mascot-light" cx="26" cy="66" r="3" /> <circle className="mascot-light" cx="26" cy="66" r="3" />
{/* 风挡(车窗,圆角更柔) */} {/* 风挡(车窗,圆角更柔)——浅色玻璃 + 细描边。
<rect x="34" y="45" width="52" height="22" rx="11" fill="#1A1A1A" /> 原为 #1A1A1A 实心黑窗,横在眼睛上会被看成"戴黑眼罩",故改浅玻璃。 */}
{/* 眼白(稍大,更水灵) */} <rect
<circle cx="47" cy="55" r="5" fill="#FFFFFF" /> className="mascot-glass"
<circle cx="73" cy="55" r="5" fill="#FFFFFF" /> x="34"
{/* 眼珠(增大+下偏,看着下方用户,更灵动) */} y="45"
<circle cx="47.5" cy="57" r="3" fill="#1A1A1A" /> width="52"
<circle cx="73.5" cy="57" r="3" fill="#1A1A1A" /> height="22"
rx="11"
stroke="#1A1A1A"
strokeWidth="1.6"
/>
{/* 眼珠(深色,直接落在浅玻璃上;不再需要眼白托底) */}
<circle cx="47.5" cy="56.5" r="4.8" fill="#1A1A1A" />
<circle cx="73.5" cy="56.5" r="4.8" fill="#1A1A1A" />
{/* 高光(左上小白点,让眼睛有神) */} {/* 高光(左上小白点,让眼睛有神) */}
<circle cx="45.7" cy="55.2" r="1.1" fill="#FFFFFF" /> <circle cx="45.8" cy="54.6" r="1.7" fill="#FFFFFF" />
<circle cx="71.7" cy="55.2" r="1.1" fill="#FFFFFF" /> <circle cx="71.8" cy="54.6" r="1.7" fill="#FFFFFF" />
{/* 微笑 */} {/* 微笑 */}
<path d="M42 70 Q60 80 78 70" fill="none" stroke="#1A1A1A" strokeWidth="3" strokeLinecap="round" /> <path d="M42 70 Q60 80 78 70" fill="none" stroke="#1A1A1A" strokeWidth="3" strokeLinecap="round" />
{/* 车轮 */} {/* 车轮 */}

178
src/components/MemberProfileModal.tsx

@ -0,0 +1,178 @@
import { useEffect, useRef } from 'react';
import { Icon } from '@/icons';
import type { MemberDetail, TeamDetailId } from '@/i18n/zh';
/** 弹窗展示的成员快照。刻意做成「对象快照」而不是索引:
轮播的 4s 自动切换会让 teamMembers[teamIdx] 换人,弹窗内容必须钉住点击那一刻的人。
不含长简介——那是异步加载的,走下面的 DetailState。 */
export type MemberProfile = {
name: string;
role: string;
group: string;
photo?: string;
/** 无照片时的占位字(About 的 memberInitial 算好传进来) */
initials: string;
nationality?: string;
detailId: TeamDetailId;
};
/** 长简介的加载状态。弹窗头部(姓名/岗位/国籍)不依赖它,所以点开即有内容,
正文区先占位、加载完再填——长简介在单独的 chunk 里,见 i18n/teamDetails/。 */
export type DetailState =
| { status: 'loading' }
| { status: 'ready'; detail: MemberDetail }
| { status: 'error' };
type Labels = {
close: string;
nationalityLabel: string;
introLabel: string;
strengthsLabel: string;
traitsLabel: string;
loadingLabel: string;
errorLabel: string;
};
/** 成员详情弹窗。
* 用原生 <dialog> + showModal() 而不是 portal + 手写遮罩:
* · top layer 渲染天然逃出 .page-enter / .scroll-reveal 的 transform 包含块
* (否则 fixed 定位会以那个祖先为基准、跟着页面滚);
* · 自带 Esc 关闭、焦点陷阱、背景 inert,省掉 portal / z-index 军备竞赛;
* · 仍是 DOM 子树里的节点,字体与字色自动继承 RootLayout(portal 到 body 会丢)。 */
export default function MemberProfileModal({
member,
detail,
labels,
onClose,
}: {
member: MemberProfile;
detail: DetailState;
labels: Labels;
onClose: () => void;
}) {
const dialogRef = useRef<HTMLDialogElement>(null);
const titleId = 'member-profile-title';
useEffect(() => {
const el = dialogRef.current;
if (!el) return;
// StrictMode 下 effect 会跑两遍,对已 open 的 dialog 再调 showModal() 会抛 InvalidStateError
if (!el.open) el.showModal();
// 焦点落在弹窗容器而非关闭按钮上,屏读先念标题
el.focus();
// 锁背景滚动。body 上已有 overflow-x:hidden 与安全区内边距,所以用 class 而不是内联
// style,避免把 padding 覆盖掉;--sbw 补偿滚动条消失的宽度,防止锁定瞬间横向抖动。
document.body.style.setProperty(
'--sbw',
`${Math.max(0, window.innerWidth - document.documentElement.clientWidth)}px`,
);
document.body.classList.add('is-modal-open');
return () => {
document.body.classList.remove('is-modal-open');
document.body.style.removeProperty('--sbw');
if (el.open) el.close();
};
}, []);
return (
<dialog
ref={dialogRef}
className="member-modal"
tabIndex={-1}
aria-labelledby={titleId}
// Esc:拦下原生关闭,统一由 React 卸载(状态只有一个来源)
onCancel={(e) => {
e.preventDefault();
onClose();
}}
// 点遮罩关闭:::backdrop 不是 DOM 节点,点到它时 target 就是 dialog 本身;
// 弹窗内容由 head + body 两段铺满,所以只有遮罩会命中这个判断。
onMouseDown={(e) => {
if (e.target === dialogRef.current) onClose();
}}
>
<div className="member-modal-head">
<div className="member-modal-portrait">
{member.photo ? (
<img
src={member.photo}
alt=""
className="absolute inset-0 h-full w-full object-cover"
decoding="async"
/>
) : (
<span
aria-hidden="true"
className="absolute inset-0 flex items-center justify-center"
style={{
background:
'linear-gradient(160deg, color-mix(in oklch, var(--brand-accent) 16%, var(--bg-primary)) 0%, color-mix(in oklch, var(--brand-accent) 34%, var(--bg-primary)) 100%)',
}}
>
<span className="text-h3 font-bold font-[family-name:var(--font-display)] text-[var(--brand-accent-deep)]">
{member.initials}
</span>
</span>
)}
</div>
<div className="min-w-0 flex-1">
<div className="text-body-sm font-mono uppercase tracking-[0.22em] text-[var(--text-muted)]">
{member.group}
</div>
<h2
id={titleId}
className="mt-2 text-h2 font-bold font-[family-name:var(--font-display)] leading-tight text-[var(--brand-black)]"
>
{member.name}
</h2>
<div className="mt-1.5 text-body text-[var(--text-secondary)]">{member.role}</div>
{/* 国籍标签:英文国名比中文长得多,nowrap 防止在窄屏被拆成两行。
国籍不随长简介一起异步加载,所以这个标签点开即在。 */}
{member.nationality && (
<span className="mt-3 inline-flex items-center gap-1.5 whitespace-nowrap rounded-full border border-[var(--border-light)] bg-[var(--bg-subtle)] px-3 py-1 text-body-sm text-[var(--text-muted)]">
{labels.nationalityLabel} · {member.nationality}
</span>
)}
</div>
<button
type="button"
onClick={onClose}
aria-label={labels.close}
className="-mt-1 ml-auto flex h-9 w-9 flex-none items-center justify-center rounded-full border border-[var(--border-light)] text-[var(--text-muted)] transition-colors duration-200 hover:border-[var(--brand-black)] hover:text-[var(--brand-black)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand-accent)]"
>
<Icon name="x" className="h-4 w-4" />
</button>
</div>
<div className="member-modal-body">
{detail.status === 'ready' ? (
<>
<section className="member-modal-section">
<h3>{labels.introLabel}</h3>
<p>{detail.detail.intro}</p>
</section>
<section className="member-modal-section">
<h3>{labels.strengthsLabel}</h3>
<p>{detail.detail.strengths}</p>
</section>
<section className="member-modal-section">
<h3>{labels.traitsLabel}</h3>
<p>{detail.detail.traits}</p>
</section>
</>
) : (
// aria-live:加载完成/失败时读屏会念出变化,而不是静默换掉正文。
// min-h 只是给占位一个下限,免得弹窗先矮后高跳一下——不能写进
// .member-modal-body 的 min-height,那条被 flex 收缩用着(见 global.css)。
<p aria-live="polite" className="min-h-[180px] text-body text-[var(--text-muted)]">
{detail.status === 'loading' ? labels.loadingLabel : labels.errorLabel}
</p>
)}
</div>
</dialog>
);
}

20
src/components/News.tsx

@ -1,5 +1,6 @@
import { getLocalizedPath, getTranslation, type Locale } from '../i18n/utils'; import { getLocalizedPath, getTranslation, type Locale } from '../i18n/utils';
import { PLACEHOLDER_IMG } from '../placeholder'; import { cssVars } from './css';
import { Icon } from '../icons';
export default function News({ locale }: { locale: Locale }) { export default function News({ locale }: { locale: Locale }) {
const t = getTranslation(locale); const t = getTranslation(locale);
@ -30,8 +31,21 @@ export default function News({ locale }: { locale: Locale }) {
href={getLocalizedPath('/news', locale)} href={getLocalizedPath('/news', locale)}
className="track-card group overflow-hidden flex flex-col scroll-reveal" 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)]"> {/* 图片位:OSS 无对应素材,改用品牌渐变 + 图标占位(写法同 Contact 的团队图集) */}
<img src={PLACEHOLDER_IMG} alt={item.title} className="w-full h-full object-cover" loading="lazy" decoding="async" /> <div
className="relative aspect-[16/10] overflow-hidden border-b border-[var(--border-light)] flex items-center justify-center"
style={{
background: `linear-gradient(135deg,
color-mix(in oklch, var(--brand-accent) 20%, var(--bg-primary)) 0%,
color-mix(in oklch, var(--track-ai) 10%, var(--bg-primary)) 100%)`,
}}
>
<span
className="icon-tile w-14 h-14 transition-transform duration-700 group-hover:scale-110"
style={cssVars({ background: 'color-mix(in oklch, var(--brand-accent) 16%, transparent)' })}
>
<Icon name="fileText" className="w-7 h-7" style={cssVars({ color: 'var(--brand-accent-deep)' })} />
</span>
<span className="absolute top-3 left-3 text-body px-2 py-1 rounded-md bg-white/80 text-[var(--brand-accent)] font-semibold"> <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} {item.category}
</span> </span>

14
src/components/NotFound.tsx

@ -3,7 +3,7 @@ import { Icon, navIcons } from '../icons';
export default function NotFound({ locale }: { locale: Locale }) { export default function NotFound({ locale }: { locale: Locale }) {
const t = getTranslation(locale); const t = getTranslation(locale);
const isZh = locale !== 'en'; const tr = (zh: string, hk: string, en: string) => (locale === 'en' ? en : locale === 'hk' ? hk : zh);
return ( return (
<div className="gradient-mesh min-h-screen"> <div className="gradient-mesh min-h-screen">
@ -15,14 +15,16 @@ export default function NotFound({ locale }: { locale: Locale }) {
</div> </div>
</div> </div>
<h1 className="scroll-reveal text-h1 font-[var(--font-display)] font-bold mb-4 tracking-tight"> <h1 className="scroll-reveal text-h1 font-[family-name:var(--font-display)] font-bold mb-4 tracking-tight">
<span style={{ color: 'var(--accent)' }}>{isZh ? '走丢了' : 'Lost in Space'}</span> <span style={{ color: 'var(--accent)' }}>{tr('走丢了', '走丟了', 'Lost in Space')}</span>
</h1> </h1>
<p className="scroll-reveal text-[var(--text-muted)] text-body md:text-body font-light leading-relaxed mb-10 max-w-[420px] mx-auto"> <p className="scroll-reveal text-[var(--text-muted)] text-body md:text-body font-light leading-relaxed mb-10 max-w-[420px] mx-auto">
{isZh {tr(
? '抱歉,您访问的页面不存在或已被移动。别担心,让我们带您回到温暖的主路。' '抱歉,您访问的页面不存在或已被移动。别担心,让我们带您回到温暖的主路。',
: 'Sorry, the page you are looking for does not exist or has been moved. Let us take you back home.'} '抱歉,您訪問的頁面不存在或已被移動。別擔心,讓我們帶您回到溫暖的主路。',
'Sorry, the page you are looking for does not exist or has been moved. Let us take you back home.',
)}
</p> </p>
<div className="scroll-reveal flex flex-col sm:flex-row items-center justify-center gap-3"> <div className="scroll-reveal flex flex-col sm:flex-row items-center justify-center gap-3">

225
src/components/Privacy.tsx

@ -1,55 +1,45 @@
import type { Locale } from '../i18n/utils'; import type { Locale } from '../i18n/utils';
export default function Privacy({ locale }: { locale: Locale }) { /* =====================================================================
const isZh = locale !== 'en'; * 隐私政策页(三语)
*
return ( * 正文按语言拆成三份常量再取用,而不是在 JSX 里写 `isZh ? A : B`:
<div className="gradient-mesh min-h-screen"> * 繁体分支必须逐字给出,否则 hk 会落到简体分支上(本项目 hk 的既有约定是
<section className="relative min-h-[32vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8 pt-20 pb-6"> * 「简体正文的繁体字形转换」,用词维持大陆写法:隱私/郵箱/聯繫/視頻,
<div className="max-w-[520px] mx-auto text-center relative z-10"> * 见 src/i18n/hk.ts 的既有译法),不是港式本地化。
<h1 className="scroll-reveal text-h1 font-[var(--font-display)] font-bold mb-3 tracking-tight"> * ===================================================================== */
<span style={{ color: 'var(--accent)' }}>{isZh ? '隐私政策' : 'Privacy Policy'}</span>
</h1> const ZH_BODY = (
<p className="scroll-reveal text-[var(--text-muted)] text-body md:text-body font-light"> <>
{isZh ? '巴适伙伴(上海)科技有限公司 · 最后更新:2026年7月' : 'BaskBuddy (Shanghai) Technology Co., Ltd. · Last updated: July 2026'} <div>
</p> <h2 className="text-body font-bold text-[var(--brand-black)] mb-3">一、信息收集范围</h2>
</div> <p className="whitespace-pre-line">{`我们仅在您主动提供的情况下收集以下个人信息:
</section>
<section className="py-10 md:py-16 px-4 sm:px-6 lg:px-8">
<div className="max-w-[640px] mx-auto relative z-10">
<div className="scroll-reveal rounded-2xl card-surface p-6 md:p-8" style={{ boxShadow: 'var(--shadow-md)' }}>
{isZh ? (
<div className="text-body md:text-body leading-[1.8] text-[var(--text-secondary)] space-y-6">
<div>
<h2 className="text-body font-bold text-[var(--brand-black)] mb-3">一、信息收集范围</h2>
<p className="whitespace-pre-line">{`我们仅在您主动提供的情况下收集以下个人信息:
· 求职咨询:姓名、邮箱、联系电话、意向职位、留言内容 · 求职咨询:姓名、邮箱、联系电话、意向职位、留言内容
· 商务合作:公司名称、联系人、邮箱、合作类型、留言内容 · 商务合作:公司名称、联系人、邮箱、合作类型、留言内容
我们不会通过任何隐蔽手段收集您的个人信息。`}</p> 我们不会通过任何隐蔽手段收集您的个人信息。`}</p>
</div> </div>
<div> <div>
<h2 className="text-body font-bold text-[var(--brand-black)] mb-3">二、信息使用目的</h2> <h2 className="text-body font-bold text-[var(--brand-black)] mb-3">二、信息使用目的</h2>
<p className="whitespace-pre-line">{`收集的信息仅用于以下目的: <p className="whitespace-pre-line">{`收集的信息仅用于以下目的:
· 处理您的求职申请或商务合作咨询 · 处理您的求职申请或商务合作咨询
· 在必要时与您取得联系 · 在必要时与您取得联系
· 改善我们的服务质量 · 改善我们的服务质量
我们不会将您的个人信息用于未经您同意的其他目的。`}</p> 我们不会将您的个人信息用于未经您同意的其他目的。`}</p>
</div> </div>
<div> <div>
<h2 className="text-body font-bold text-[var(--brand-black)] mb-3">三、信息存储与保护</h2> <h2 className="text-body font-bold text-[var(--brand-black)] mb-3">三、信息存储与保护</h2>
<p className="whitespace-pre-line">{`· 您的个人信息存储于中国境内的安全服务器 <p className="whitespace-pre-line">{`· 您的个人信息存储于中国境内的安全服务器
· 我们采用业界标准的安全措施保护您的数据 · 我们采用业界标准的安全措施保护您的数据
· 我们不会将您的个人信息传输至境外 · 我们不会将您的个人信息传输至境外
· 数据保留期限:咨询信息保留不超过2年,期满后自动删除`}</p> · 数据保留期限:咨询信息保留不超过2年,期满后自动删除`}</p>
</div> </div>
<div> <div>
<h2 className="text-body font-bold text-[var(--brand-black)] mb-3">四、您的权利</h2> <h2 className="text-body font-bold text-[var(--brand-black)] mb-3">四、您的权利</h2>
<p className="whitespace-pre-line">{`根据《个人信息保护法》,您享有以下权利: <p className="whitespace-pre-line">{`根据《个人信息保护法》,您享有以下权利:
· 查询权:随时查询我们持有的您的个人信息 · 查询权:随时查询我们持有的您的个人信息
· 更正权:要求更正不准确的信息 · 更正权:要求更正不准确的信息
@ -57,53 +47,111 @@ export default function Privacy({ locale }: { locale: Locale }) {
· 撤回同意权:撤回此前给予的授权同意 · 撤回同意权:撤回此前给予的授权同意
行使以上权利,请发送邮件至:baskbuddy@163.com`}</p> 行使以上权利,请发送邮件至:baskbuddy@163.com`}</p>
</div> </div>
<div> <div>
<h2 className="text-body font-bold text-[var(--brand-black)] mb-3">五、未成年人保护</h2> <h2 className="text-body font-bold text-[var(--brand-black)] mb-3">五、未成年人保护</h2>
<p className="whitespace-pre-line">{`我们高度重视未成年人个人信息的保护。根据《中华人民共和国个人信息保护法》第三十一条,不满十八周岁未成年人的个人信息属于敏感个人信息。我们不会专门以未成年人为服务对象,也不主动收集不满十八周岁未成年人的个人信息。 <p className="whitespace-pre-line">{`我们高度重视未成年人个人信息的保护。根据《中华人民共和国个人信息保护法》第三十一条,不满十八周岁未成年人的个人信息属于敏感个人信息。我们不会专门以未成年人为服务对象,也不主动收集不满十八周岁未成年人的个人信息。
如我们发现在未经其监护人同意的情况下收集了此类信息,将立即删除相关记录,并通知其监护人。`}</p> 如我们发现在未经其监护人同意的情况下收集了此类信息,将立即删除相关记录,并通知其监护人。`}</p>
</div> </div>
<div> <div>
<h2 className="text-body font-bold text-[var(--brand-black)] mb-3">六、联系方式</h2> <h2 className="text-body font-bold text-[var(--brand-black)] mb-3">六、联系方式</h2>
<p className="whitespace-pre-line">{`如有任何隐私相关问题,请联系: <p className="whitespace-pre-line">{`如有任何隐私相关问题,请联系:
巴适伙伴(上海)科技有限公司 巴适伙伴(上海)科技有限公司
邮箱:baskbuddy@163.com 邮箱:baskbuddy@163.com
地址:上海市静安区康宁路288弄2号410室`}</p> 地址:上海市静安区康宁路288弄2号410室`}</p>
</div> </div>
</div> </>
) : ( );
<div className="text-body md:text-body leading-[1.8] text-[var(--text-secondary)] space-y-6">
<div> const HK_BODY = (
<h2 className="text-body font-bold text-[var(--brand-black)] mb-3">1. Information Collection Scope</h2> <>
<p className="whitespace-pre-line">{`We only collect the following personal information when you proactively provide it: <div>
<h2 className="text-body font-bold text-[var(--brand-black)] mb-3">一、信息收集範圍</h2>
<p className="whitespace-pre-line">{`我們僅在您主動提供的情況下收集以下個人信息:
· 求職咨詢:姓名、郵箱、聯繫電話、意向職位、留言內容
· 商務合作:公司名稱、聯繫人、郵箱、合作類型、留言內容
我們不會通過任何隱蔽手段收集您的個人信息。`}</p>
</div>
<div>
<h2 className="text-body font-bold text-[var(--brand-black)] mb-3">二、信息使用目的</h2>
<p className="whitespace-pre-line">{`收集的信息僅用於以下目的:
· 處理您的求職申請或商務合作咨詢
· 在必要時與您取得聯繫
· 改善我們的服務質量
我們不會將您的個人信息用於未經您同意的其他目的。`}</p>
</div>
<div>
<h2 className="text-body font-bold text-[var(--brand-black)] mb-3">三、信息存儲與保護</h2>
<p className="whitespace-pre-line">{`· 您的個人信息存儲於中國境內的安全服務器
· 我們採用業界標準的安全措施保護您的數據
· 我們不會將您的個人信息傳輸至境外
· 數據保留期限:咨詢信息保留不超過2年,期滿後自動刪除`}</p>
</div>
<div>
<h2 className="text-body font-bold text-[var(--brand-black)] mb-3">四、您的權利</h2>
<p className="whitespace-pre-line">{`根據《個人信息保護法》,您享有以下權利:
· 查詢權:隨時查詢我們持有的您的個人信息
· 更正權:要求更正不準確的信息
· 刪除權:要求我們刪除您的個人信息
· 撤回同意權:撤回此前給予的授權同意
行使以上權利,請發送郵件至:baskbuddy@163.com`}</p>
</div>
<div>
<h2 className="text-body font-bold text-[var(--brand-black)] mb-3">五、未成年人保護</h2>
<p className="whitespace-pre-line">{`我們高度重視未成年人個人信息的保護。根據《中華人民共和國個人信息保護法》第三十一條,不滿十八周歲未成年人的個人信息屬於敏感個人信息。我們不會專門以未成年人為服務對象,也不主動收集不滿十八周歲未成年人的個人信息。
如我們發現在未經其監護人同意的情況下收集了此類信息,將立即刪除相關記錄,並通知其監護人。`}</p>
</div>
<div>
<h2 className="text-body font-bold text-[var(--brand-black)] mb-3">六、聯繫方式</h2>
<p className="whitespace-pre-line">{`如有任何隱私相關問題,請聯繫:
巴適夥伴(上海)科技有限公司
郵箱:baskbuddy@163.com
地址:上海市靜安區康寧路288弄2號410室`}</p>
</div>
</>
);
const EN_BODY = (
<>
<div>
<h2 className="text-body font-bold text-[var(--brand-black)] mb-3">1. Information Collection Scope</h2>
<p className="whitespace-pre-line">{`We only collect the following personal information when you proactively provide it:
· Job inquiries: name, email, phone, desired position, message · Job inquiries: name, email, phone, desired position, message
· Business cooperation: company name, contact person, email, cooperation type, message · Business cooperation: company name, contact person, email, cooperation type, message
We do not collect your personal information through any hidden means.`}</p> We do not collect your personal information through any hidden means.`}</p>
</div> </div>
<div> <div>
<h2 className="text-body font-bold text-[var(--brand-black)] mb-3">2. Purpose of Information Use</h2> <h2 className="text-body font-bold text-[var(--brand-black)] mb-3">2. Purpose of Information Use</h2>
<p className="whitespace-pre-line">{`The collected information is used only for the following purposes: <p className="whitespace-pre-line">{`The collected information is used only for the following purposes:
· Processing your job application or business inquiry · Processing your job application or business inquiry
· Contacting you when necessary · Contacting you when necessary
· Improving our service quality · Improving our service quality
We will not use your personal information for purposes beyond your consent.`}</p> We will not use your personal information for purposes beyond your consent.`}</p>
</div> </div>
<div> <div>
<h2 className="text-body font-bold text-[var(--brand-black)] mb-3">3. Information Storage & Protection</h2> <h2 className="text-body font-bold text-[var(--brand-black)] mb-3">3. Information Storage & Protection</h2>
<p className="whitespace-pre-line">{`· Your personal information is stored on secure servers within China <p className="whitespace-pre-line">{`· Your personal information is stored on secure servers within China
· We use industry-standard security measures to protect your data · We use industry-standard security measures to protect your data
· We do not transfer your personal information overseas · We do not transfer your personal information overseas
· Data retention: inquiry information is retained for up to 2 years, then automatically deleted`}</p> · Data retention: inquiry information is retained for up to 2 years, then automatically deleted`}</p>
</div> </div>
<div> <div>
<h2 className="text-body font-bold text-[var(--brand-black)] mb-3">4. Your Rights</h2> <h2 className="text-body font-bold text-[var(--brand-black)] mb-3">4. Your Rights</h2>
<p className="whitespace-pre-line">{`Under the Personal Information Protection Law, you have the following rights: <p className="whitespace-pre-line">{`Under the Personal Information Protection Law, you have the following rights:
· Right to query: request information we hold about you at any time · Right to query: request information we hold about you at any time
· Right to correct: request correction of inaccurate information · Right to correct: request correction of inaccurate information
@ -111,23 +159,52 @@ We will not use your personal information for purposes beyond your consent.`}</p
· Right to withdraw consent: revoke previously granted consent · Right to withdraw consent: revoke previously granted consent
To exercise these rights, please email: baskbuddy@163.com`}</p> To exercise these rights, please email: baskbuddy@163.com`}</p>
</div> </div>
<div> <div>
<h2 className="text-body font-bold text-[var(--brand-black)] mb-3">5. Protection of Minors</h2> <h2 className="text-body font-bold text-[var(--brand-black)] mb-3">5. Protection of Minors</h2>
<p className="whitespace-pre-line">{`We attach great importance to the protection of minors' personal information. Under Article 31 of the Personal Information Protection Law of the People's Republic of China, the personal information of minors under the age of 18 is classified as sensitive personal information. We do not specifically target minors, nor do we actively collect personal information from minors under 18. <p className="whitespace-pre-line">{`We attach great importance to the protection of minors' personal information. Under Article 31 of the Personal Information Protection Law of the People's Republic of China, the personal information of minors under the age of 18 is classified as sensitive personal information. We do not specifically target minors, nor do we actively collect personal information from minors under 18.
If we discover that such information has been collected without the guardian's consent, we will promptly delete the relevant records and notify the guardian.`}</p> If we discover that such information has been collected without the guardian's consent, we will promptly delete the relevant records and notify the guardian.`}</p>
</div> </div>
<div> <div>
<h2 className="text-body font-bold text-[var(--brand-black)] mb-3">6. Contact Information</h2> <h2 className="text-body font-bold text-[var(--brand-black)] mb-3">6. Contact Information</h2>
<p className="whitespace-pre-line">{`For any privacy-related questions, please contact: <p className="whitespace-pre-line">{`For any privacy-related questions, please contact:
BaskBuddy (Shanghai) Technology Co., Ltd. BaskBuddy (Shanghai) Technology Co., Ltd.
Email: baskbuddy@163.com Email: baskbuddy@163.com
Address: Room 410, No.2, Lane 288, Kangning Road, Jing'an District, Shanghai, China`}</p> Address: Room 410, No.2, Lane 288, Kangning Road, Jing'an District, Shanghai, China`}</p>
</div> </div>
</div> </>
)} );
const TITLE: Record<Locale, string> = { zh: '隐私政策', hk: '隱私政策', en: 'Privacy Policy' };
const BYLINE: Record<Locale, string> = {
zh: '巴适伙伴(上海)科技有限公司 · 最后更新:2026年7月',
hk: '巴適夥伴(上海)科技有限公司 · 最後更新:2026年7月',
en: 'BaskBuddy (Shanghai) Technology Co., Ltd. · Last updated: July 2026',
};
export default function Privacy({ locale }: { locale: Locale }) {
return (
<div className="gradient-mesh min-h-screen">
<section className="relative min-h-[32vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8 pt-20 pb-6">
<div className="max-w-[520px] mx-auto text-center relative z-10">
<h1 className="scroll-reveal text-h1 font-[family-name:var(--font-display)] font-bold mb-3 tracking-tight">
<span style={{ color: 'var(--accent)' }}>{TITLE[locale]}</span>
</h1>
<p className="scroll-reveal text-[var(--text-muted)] text-body md:text-body font-light">
{BYLINE[locale]}
</p>
</div>
</section>
<section className="py-10 md:py-16 px-4 sm:px-6 lg:px-8">
<div className="max-w-[640px] mx-auto relative z-10">
<div className="scroll-reveal rounded-2xl card-surface p-6 md:p-8" style={{ boxShadow: 'var(--shadow-md)' }}>
<div className="text-body md:text-body leading-[1.8] text-[var(--text-secondary)] space-y-6">
{locale === 'en' ? EN_BODY : locale === 'hk' ? HK_BODY : ZH_BODY}
</div>
</div> </div>
</div> </div>
</section> </section>

12
src/components/Sustainability.tsx

@ -79,12 +79,12 @@ export default function Sustainability({ locale }: { locale: Locale }) {
</div> </div>
<div className="p-7 flex flex-col flex-1"> <div className="p-7 flex flex-col flex-1">
<div className="flex items-baseline gap-2"> <div className="flex items-baseline gap-2">
<h2 className="text-body font-bold font-[var(--font-display)] text-[var(--brand-black)]">{p.title}</h2> <h2 className="text-body font-bold font-[family-name:var(--font-display)] text-[var(--brand-black)]">{p.title}</h2>
<span className="text-body font-mono text-[var(--text-muted)]">{p.enName}</span> <span className="text-body font-mono text-[var(--text-muted)]">{p.enName}</span>
</div> </div>
<div className="mt-4 flex items-baseline gap-2"> <div className="mt-4 flex items-baseline gap-2">
<span <span
className="text-h1 font-bold font-[var(--font-display)] leading-none" className="text-h1 font-bold font-[family-name:var(--font-display)] leading-none"
style={cssVars({ color: `var(${token})` })} style={cssVars({ color: `var(${token})` })}
> >
{p.metric} {p.metric}
@ -187,8 +187,12 @@ export default function Sustainability({ locale }: { locale: Locale }) {
{/* ===== CTA ===== */} {/* ===== CTA ===== */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-[var(--space-10)]"> <section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-[var(--space-10)]">
<div className="cta-band px-8 py-14 text-center"> <div className="cta-band px-8 py-14 text-center">
<h2 className="relative z-10 text-h1 font-bold font-[var(--font-display)] text-white"> <h2 className="relative z-10 text-h1 font-bold font-[family-name:var(--font-display)] text-white">
{locale === 'en' ? 'Build a sustainable future with BaskBuddy' : '与巴适伙伴,共创可持续未来'} {locale === 'en'
? 'Build a sustainable future with BaskBuddy'
: locale === 'hk'
? '與巴適夥伴,共創可持續未來'
: '与巴适伙伴,共创可持续未来'}
</h2> </h2>
<div className="relative z-10 mt-7"> <div className="relative z-10 mt-7">
<a href={getLocalizedPath('/contact', locale)} className="btn btn-light btn-lg"> <a href={getLocalizedPath('/contact', locale)} className="btn btn-light btn-lg">

74
src/hooks/useClientEffects.ts

@ -7,10 +7,12 @@ import { useEffect } from 'react';
// Behaviors: // Behaviors:
// - .flip-card (About.tsx ICIDP cards): click / keyboard toggle + a11y // - .flip-card (About.tsx ICIDP cards): click / keyboard toggle + a11y
// - .scroll-reveal[-left|-right]: staggered IntersectionObserver reveal // - .scroll-reveal[-left|-right]: staggered IntersectionObserver reveal
// - .stat-value[data-target]: animated count-up //
// - .copy-btn[data-copy]: clipboard copy with label swap // The Astro-era `.stat-value[data-target]` count-up and `.copy-btn[data-copy]`
// The old `#star-field` generation is intentionally dropped — no element // branches were removed: no element ever emits `data-target` / `.copy-btn`
// uses it in the React components (dead code in the Astro layout). // (React renders stat values as static text), so both observers watched an
// always-empty NodeList. Same reasoning as the `#star-field` generator that
// was already dropped — the matching CSS came out of global.css in the same pass.
export function useClientEffects(pathname: string) { export function useClientEffects(pathname: string) {
useEffect(() => { useEffect(() => {
// ----- Flip cards ----- // ----- Flip cards -----
@ -59,73 +61,9 @@ export function useClientEffects(pathname: string) {
.querySelectorAll('.scroll-reveal, .scroll-reveal-left, .scroll-reveal-right') .querySelectorAll('.scroll-reveal, .scroll-reveal-left, .scroll-reveal-right')
.forEach((el) => revealObserver.observe(el)); .forEach((el) => revealObserver.observe(el));
// ----- Animated stat counters -----
const statValues = document.querySelectorAll<HTMLElement>('.stat-value[data-target]');
const statObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const el = entry.target as HTMLElement;
const target = parseFloat(el.getAttribute('data-target') || '0') || 0;
const decimals = parseInt(el.getAttribute('data-decimals') || '0', 10);
const suffix = el.getAttribute('data-suffix') || '';
const prefix = el.getAttribute('data-prefix') || '';
const duration = 1600;
const start = performance.now();
const easeOut = (tt: number) => 1 - Math.pow(1 - tt, 3);
const fmt = (v: number) =>
prefix + (decimals ? v.toFixed(decimals) : Math.round(v).toLocaleString('en-US')) + suffix;
const tick = (now: number) => {
const p = Math.min((now - start) / duration, 1);
el.textContent = fmt(target * easeOut(p));
if (p < 1) requestAnimationFrame(tick);
else el.textContent = fmt(target);
};
requestAnimationFrame(tick);
statObserver.unobserve(el);
});
},
{ threshold: 0.4 },
);
statValues.forEach((el) => statObserver.observe(el));
// ----- Copy-to-clipboard -----
const copyCleanups: Array<() => void> = [];
document.querySelectorAll<HTMLButtonElement>('.copy-btn').forEach((btnEl) => {
const onClick = async () => {
const text = btnEl.getAttribute('data-copy') || '';
const label = btnEl.getAttribute('data-label') || btnEl.textContent || '';
const copied = btnEl.getAttribute('data-copied') || 'Copied';
try {
await navigator.clipboard.writeText(text);
} catch (_e) {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
} catch {
/* ignore */
}
document.body.removeChild(ta);
}
btnEl.textContent = copied;
window.setTimeout(() => {
btnEl.textContent = label;
}, 1500);
};
btnEl.addEventListener('click', onClick);
copyCleanups.push(() => btnEl.removeEventListener('click', onClick));
});
return () => { return () => {
revealObserver.disconnect(); revealObserver.disconnect();
statObserver.disconnect();
flipCleanups.forEach((fn) => fn()); flipCleanups.forEach((fn) => fn());
copyCleanups.forEach((fn) => fn());
}; };
}, [pathname]); }, [pathname]);
} }

126
src/i18n/en.ts

@ -1,6 +1,8 @@
import type { TeamGroup } from './zh';
export const en = { export const en = {
brand: 'BaskBuddy', brand: 'BaskBuddy',
brandSub: 'Bashi Partners', brandSub: 'BaskBuddy (Shanghai)',
nav: { nav: {
home: 'Home', home: 'Home',
about: 'About Us', about: 'About Us',
@ -16,6 +18,28 @@ export const en = {
sloganIntro: 'Digital Life Service Provider · Content Social, Career Services, Digital Payment, AI, Life Services', sloganIntro: 'Digital Life Service Provider · Content Social, Career Services, Digital Payment, AI, Life Services',
heroTitle: 'Opening the Digital Future through Intelligent Innovation', heroTitle: 'Opening the Digital Future through Intelligent Innovation',
heroCta: 'Learn More', heroCta: 'Learn More',
video: {
eyebrow: 'INTRODUCTION',
title: 'Inside BaskBuddy',
subtitle: 'A short film on how we connect beautiful life with warm technology.',
intro: [
{
value: '5',
unit: 'Core Tracks',
desc: 'As the pillars of our digital-life ecosystem, we dive deep into content & social, career services, digital payment, AI and life services — answering real user needs through a multi-brand matrix.',
},
{
value: '8',
unit: 'In-house Products',
desc: 'As a product network connecting users and life — from youth social and career growth to secure payment and wish fulfillment — covering every digital-life scenario, keeping warmth within reach.',
},
{
value: '2026',
unit: 'Year of Launch',
desc: 'A new starting point for our promise of "lifelong companionship, lifelong trust" — connecting everyone with warm technology toward a globally leading digital-life ecosystem.',
},
],
},
trackOverview: 'Five Core Tracks', trackOverview: 'Five Core Tracks',
enterLab: 'Explore Innovation Lab', enterLab: 'Explore Innovation Lab',
enterBusiness: 'View Business Map', enterBusiness: 'View Business Map',
@ -32,8 +56,8 @@ export const en = {
title: 'BaskBuddy Velocity', title: 'BaskBuddy Velocity',
subtitle: 'The solid foundation connecting beautiful life with warm tech', subtitle: 'The solid foundation connecting beautiful life with warm tech',
items: [ items: [
{ value: '5', suffix: '', label: 'Core Tracks', sub: 'Social · Career · Payment · AI · Life Services' }, // Vision-only metrics. The 5 core tracks / 8+ self-built products figures are
{ value: '8', suffix: '+', label: 'Self-built Products', sub: 'Covering the full digital life scene' }, // already told by CompanyVideo's left column (home.video.intro) — not repeated here.
{ value: 'Global', suffix: '', label: 'Service Vision', sub: 'Deliver quality products to users worldwide' }, { value: 'Global', suffix: '', label: 'Service Vision', sub: 'Deliver quality products to users worldwide' },
{ value: 'Long-term', suffix: '', label: 'Value Principle', sub: 'Win-win with users, partners, society' }, { value: 'Long-term', suffix: '', label: 'Value Principle', sub: 'Win-win with users, partners, society' },
], ],
@ -41,7 +65,7 @@ export const en = {
about: { about: {
eyebrow: 'ABOUT BASKBUDDY', eyebrow: 'ABOUT BASKBUDDY',
title: 'Inside BaskBuddy', title: 'Inside BaskBuddy',
text: 'Bashi Partners (Shanghai) Technology Co., Ltd. is a high-tech enterprise focused on the digital life services field. Through a multi-brand, multi-vertical-track product layout covering Content & Social, Career Services, Digital Payment, Artificial Intelligence and Life Services, we connect everyone’s beautiful life with warm technology.', text: 'BaskBuddy (Shanghai) Technology Co., Ltd. is a high-tech enterprise focused on the digital life services field. Through a multi-brand, multi-vertical-track product layout covering Content & Social, Career Services, Digital Payment, Artificial Intelligence and Life Services, we connect everyone’s beautiful life with warm technology.',
cta: 'Learn More', cta: 'Learn More',
}, },
pillars: { pillars: {
@ -61,7 +85,7 @@ export const en = {
cta: 'View Business Map', cta: 'View Business Map',
}, },
footer: { footer: {
copyright: `© ${new Date().getFullYear()} Bashi Partners (Shanghai) Technology Co., Ltd.`, copyright: `© ${new Date().getFullYear()} BaskBuddy (Shanghai) Technology Co., Ltd.`,
icidp: 'ICIDP: Integrity · Courage · Innovation · Devotion · Passion', icidp: 'ICIDP: Integrity · Courage · Innovation · Devotion · Passion',
}, },
}, },
@ -92,7 +116,7 @@ export const en = {
about: { about: {
title: 'About Us', title: 'About Us',
subtitle: 'BaskBuddy — Together for Life', subtitle: 'BaskBuddy — Together for Life',
intro: `Bashi Partners (Shanghai) Technology Co., Ltd. is a high-tech enterprise focused on the digital life services field. Through a multi-brand, multi-vertical-track product layout that delivers quality products and services to diverse user groups, we operate multiple independent subsidiaries specializing in Content & Social (Youthome, CrazyCool, MeLove), Career Services (mydreamjob, Peaksight), Digital Payment (Trustdeal), Artificial Intelligence (AbuAI), and Life Services (HearWish). We always put user needs first, prioritize quality and technological innovation, create growth space for employees, create a better life for users, and create long-term value for society.`, intro: `BaskBuddy (Shanghai) Technology Co., Ltd. is a high-tech enterprise focused on the digital life services field. Through a multi-brand, multi-vertical-track product layout that delivers quality products and services to diverse user groups, we operate multiple independent subsidiaries specializing in Content & Social (Youthome, CrazyCool, MeLove), Career Services (mydreamjob, Peaksight), Digital Payment (Trustdeal), Artificial Intelligence (AbuAI), and Life Services (HearWish). We always put user needs first, prioritize quality and technological innovation, create growth space for employees, create a better life for users, and create long-term value for society.`,
visionMission: { visionMission: {
vision: 'Build a world-leading digital life service ecosystem where users of different needs receive quality-adapted product experiences', vision: 'Build a world-leading digital life service ecosystem where users of different needs receive quality-adapted product experiences',
mission: 'Connect people and services through innovative technology, leveraging diverse vertical products to create convenient, efficient, and valuable beautiful lives for different user groups', mission: 'Connect people and services through innovative technology, leveraging diverse vertical products to create convenient, efficient, and valuable beautiful lives for different user groups',
@ -102,7 +126,7 @@ export const en = {
icidp: { icidp: {
title: 'ICIDP Core Values', title: 'ICIDP Core Values',
integrity: { label: 'Integrity', position: 'Foundation', dialect: 'Being reliable is the real deal', layers: ['To users: trustworthy product commitments and service quality', 'To partners: transparent win-win cooperation and mutual growth', 'To self: consistency between words and actions'] }, integrity: { label: 'Integrity', position: 'Foundation', dialect: 'Being reliable is the real deal', layers: ['To users: trustworthy product commitments and service quality', 'To partners: transparent win-win cooperation and mutual growth', 'To self: consistency between words and actions'] },
courage: { label: 'Courage', position: 'Drive', dialect: 'Bold thinking, bold action — that\'s Bashi', layers: ['Brave enough to break technical and cognitive boundaries', 'Brave enough to challenge industry conventions', 'Brave enough to admit mistakes and correct course quickly'] }, courage: { label: 'Courage', position: 'Drive', dialect: 'Bold thinking, bold action — that\'s the BaskBuddy way', layers: ['Brave enough to break technical and cognitive boundaries', 'Brave enough to challenge industry conventions', 'Brave enough to admit mistakes and correct course quickly'] },
innovation: { label: 'Innovation', position: 'Soul', dialect: 'Being different makes it interesting', layers: ['Tech innovation: beyond code, transforming mindsets', 'Product innovation: defining value from user pain points', 'Org innovation: every member becomes an innovation source'] }, innovation: { label: 'Innovation', position: 'Soul', dialect: 'Being different makes it interesting', layers: ['Tech innovation: beyond code, transforming mindsets', 'Product innovation: defining value from user pain points', 'Org innovation: every member becomes an innovation source'] },
devotion: { label: 'Devotion', position: 'Warmth', dialect: 'Generosity shows true vision', layers: ['Infusing love into every product detail', 'Investing time into every team collaboration', 'Returning value through every social action'] }, devotion: { label: 'Devotion', position: 'Warmth', dialect: 'Generosity shows true vision', layers: ['Infusing love into every product detail', 'Investing time into every team collaboration', 'Returning value through every social action'] },
passion: { label: 'Passion', position: 'Flame', dialect: 'Passion outlasts the longest days', layers: ['For career: waking up with light in your eyes still', 'For team: everyone is a spark that can ignite others', 'For world: still loving life beyond work hours'] }, passion: { label: 'Passion', position: 'Flame', dialect: 'Passion outlasts the longest days', layers: ['For career: waking up with light in your eyes still', 'For team: everyone is a spark that can ignite others', 'For world: still loving life beyond work hours'] },
@ -113,15 +137,74 @@ export const en = {
team: { team: {
title: 'Management Team', title: 'Management Team',
desc: 'The core partners driving us forward', desc: 'The core partners driving us forward',
intro: 'We are a specialised, tightly coordinated team led by our Legal Representative, covering full-stack, front-end, back-end and Android development and finance — supporting product R&D and business operations every day.',
icon: 'users', icon: 'users',
href: '#about-team', href: '#about-team',
members: [ // UI copy for the profile dialog. The long member bios are NOT here — they
{ name: 'Zhang Shun', role: 'Software Engineer', gender: 'male' }, // live in src/i18n/teamDetails/ so they load as a separate lazy chunk.
{ name: 'Zhang Buyun', role: 'Software Engineer', gender: 'female' }, ui: {
{ name: 'Chen Xueni', role: 'Software Engineer', gender: 'female' }, viewProfile: 'View profile',
{ name: 'Chen Manli', role: 'Finance Specialist', gender: 'female' }, close: 'Close',
{ name: 'Yang Xingxing', role: 'Software Engineer', gender: 'female' }, nationalityLabel: 'Nationality',
], introLabel: 'Role',
strengthsLabel: 'Technical strengths',
traitsLabel: 'Personal qualities',
loadingLabel: 'Loading profile…',
errorLabel: 'This profile could not be loaded — close and reopen to try again.',
},
groups: [
{
key: 'management',
title: 'Management',
members: [
{ name: 'Chen Pengtao', role: 'Legal Representative', photo: 'https://oss.baskbuddy.com/baskbuddy/team-male.jpg' },
],
},
{
key: 'engineering',
title: 'Engineering',
members: [
{ name: 'Wu Qiming', role: 'Android Engineer', photo: 'https://oss.baskbuddy.com/baskbuddy/team-male.jpg' },
{
name: 'Yang Xingxing',
role: 'Full-Stack Engineer',
photo: 'https://oss.baskbuddy.com/baskbuddy/team-female.jpg',
nationality: 'China',
detailId: 'yang-xingxing',
},
{
name: 'Zhang Shun',
role: 'Backend Engineer & Product Manager',
photo: 'https://oss.baskbuddy.com/baskbuddy/team-male.jpg',
nationality: 'China',
detailId: 'zhang-shun',
},
{
name: 'Chen Jie',
role: 'Front-End Engineer',
photo: 'https://oss.baskbuddy.com/baskbuddy/team-female.jpg',
nationality: 'China',
detailId: 'chen-jie',
},
{
name: 'Zhang Buyun',
role: 'Front-End Engineer & UI Designer',
photo: 'https://oss.baskbuddy.com/baskbuddy/team-female.jpg',
nationality: 'China',
detailId: 'zhang-buyun',
},
{ name: 'Liu Xiaoqiang', role: 'Backend Engineer', photo: 'https://oss.baskbuddy.com/baskbuddy/team-male.jpg' },
{ name: 'Li Shibo', role: 'Backend Engineer', photo: 'https://oss.baskbuddy.com/baskbuddy/team-male.jpg' },
],
},
{
key: 'finance',
title: 'Finance',
members: [
{ name: 'Zhang Mengxiang', role: 'Finance Specialist', photo: 'https://oss.baskbuddy.com/baskbuddy/team-female.jpg' },
],
},
] satisfies TeamGroup[],
}, },
vision: { title: 'Vision & Mission', desc: 'Where we are going and why we started', icon: 'telescope', href: '#about-vision' }, vision: { title: 'Vision & Mission', desc: 'Where we are going and why we started', icon: 'telescope', href: '#about-vision' },
}, },
@ -134,7 +217,10 @@ export const en = {
{ value: '5', label: 'Core tracks' }, { value: '5', label: 'Core tracks' },
{ value: '8', label: 'Products' }, { value: '8', label: 'Products' },
{ value: 'Global', label: 'Ecosystem' }, { value: 'Global', label: 'Ecosystem' },
{ value: '2026', label: 'Founded' }, // 与 zh 的「起航之年」/ home.video.intro 的 'Year of Launch' 对齐。
// 不要写 'Founded':投资人路线图把 2024—2025 记为「搭骨架」阶段,
// 说 2026 才成立与自家路线图自相矛盾。
{ value: '2026', label: 'Year of Launch' },
], ],
network: { network: {
center: { center: {
@ -145,7 +231,7 @@ export const en = {
nodes: [ nodes: [
{ id: 'position', title: 'Positioning', desc: 'BaskBuddy (Shanghai) Technology Co., Ltd. is a high-tech enterprise focused on digital lifestyle services, delivering quality products and services to diverse user groups through a multi-brand, multi-vertical product portfolio.' }, { id: 'position', title: 'Positioning', desc: 'BaskBuddy (Shanghai) Technology Co., Ltd. is a high-tech enterprise focused on digital lifestyle services, delivering quality products and services to diverse user groups through a multi-brand, multi-vertical product portfolio.' },
{ id: 'tracks', title: 'Five Core Tracks', desc: 'Built on five pillars — Content & Social, Career Services, Digital Payment, AI, and Life Services — with 8 self-developed products covering every scenario of users digital life.', link: '/business' }, { id: 'tracks', title: 'Five Core Tracks', desc: 'Built on five pillars — Content & Social, Career Services, Digital Payment, AI, and Life Services — with 8 self-developed products covering every scenario of users digital life.', link: '/business' },
{ id: 'team', title: 'Management', desc: 'A core team of five partners — Zhang Shun, Zhang Buyun, Chen Xueni, Chen Manli, and Yang Xingxing — focused on software R&D and product innovation.', link: '#about-team' }, { id: 'team', title: 'Management', desc: 'A core team of nine covering engineering and finance — focused on product R&D and business operations.', link: '#about-team' },
{ id: 'vision', title: 'Vision & Mission', desc: 'Vision: build the world-leading digital lifestyle service ecosystem. Mission: connect people and services through innovative technology, creating convenient, efficient, and valuable lives for diverse users.' }, { id: 'vision', title: 'Vision & Mission', desc: 'Vision: build the world-leading digital lifestyle service ecosystem. Mission: connect people and services through innovative technology, creating convenient, efficient, and valuable lives for diverse users.' },
{ id: 'data', title: 'Key Facts', desc: '5 core tracks · 8 products · global ecosystem · founded in 2026.' }, { id: 'data', title: 'Key Facts', desc: '5 core tracks · 8 products · global ecosystem · founded in 2026.' },
{ id: 'life', title: 'Connecting Life', desc: 'Treating every ordinary day with care — technology fades into the background, warmth remains.' }, { id: 'life', title: 'Connecting Life', desc: 'Treating every ordinary day with care — technology fades into the background, warmth remains.' },
@ -237,9 +323,9 @@ Let's go — let's journey toward a beautiful future together with BaskBuddy!`,
title: 'A group of young people, making BaskBuddy real', title: 'A group of young people, making BaskBuddy real',
subtitle: 'Brainstorms, pair programming, celebrating every small win — striving here has warmth.', subtitle: 'Brainstorms, pair programming, celebrating every small win — striving here has warmth.',
items: [ items: [
{ img: 'https://oss.baskbuddy.com/baskbuddy/team-brainstorm.jpg', caption: 'Ideas colliding at the whiteboard' }, { caption: 'Ideas colliding at the whiteboard' },
{ img: 'https://oss.baskbuddy.com/baskbuddy/team-pairing.jpg', caption: 'Pair programming — ideas into code' }, { caption: 'Pair programming — ideas into code' },
{ img: 'https://oss.baskbuddy.com/baskbuddy/team-celebrate.jpg', caption: 'Every small win deserves a toast' }, { caption: 'Every small win deserves a toast' },
], ],
}, },
growthIncentive: { growthIncentive: {
@ -404,7 +490,7 @@ Let's go — let's journey toward a beautiful future together with BaskBuddy!`,
legal: { legal: {
privacy: 'Privacy Policy', privacy: 'Privacy Policy',
}, },
copyright: `© ${new Date().getFullYear()} Bashi Partners (Shanghai) Technology Co., Ltd.`, copyright: `© ${new Date().getFullYear()} BaskBuddy (Shanghai) Technology Co., Ltd.`,
icidpFull: 'Integrity · Courage · Innovation · Devotion · Passion', icidpFull: 'Integrity · Courage · Innovation · Devotion · Passion',
}, },
}; };

110
src/i18n/hk.ts

@ -1,3 +1,5 @@
import type { TeamGroup } from './zh';
export const hk = { export const hk = {
brand: 'BaskBuddy', brand: 'BaskBuddy',
brandSub: '巴適夥伴', brandSub: '巴適夥伴',
@ -16,6 +18,28 @@ export const hk = {
sloganIntro: '覆蓋內容社交 · 職場服務 · 數字支付 · AI · 生活服務的數字生活服務商', sloganIntro: '覆蓋內容社交 · 職場服務 · 數字支付 · AI · 生活服務的數字生活服務商',
heroTitle: '以智能創新開創數字未來', heroTitle: '以智能創新開創數字未來',
heroCta: '了解我們', heroCta: '了解我們',
video: {
eyebrow: 'INTRODUCTION',
title: '走進巴適夥伴',
subtitle: '用一段影像,看見我們如何以溫暖科技連接美好生活。',
intro: [
{
value: '5',
unit: '大核心賽道',
desc: '作為數字生活服務生態的支柱佈局,深耕內容社交、職場服務、數字支付、人工智能與生活服務,以多品牌矩陣回應不同用戶群體的真實需求。',
},
{
value: '8',
unit: '自研產品',
desc: '作為連接用戶與生活的產品網絡,從青年社交、求職升學,到安全支付與心願達成,覆蓋數字生活全場景,讓溫暖觸手可及。',
},
{
value: '2026',
unit: '起航之年',
desc: '作為「一生相伴·一生信賴」承諾的新起點,以溫暖科技連接每一個人,穩步邁向全球領先的數字生活服務生態。',
},
],
},
trackOverview: '五大核心賽道', trackOverview: '五大核心賽道',
enterLab: '探索創新實驗室', enterLab: '探索創新實驗室',
enterBusiness: '查看業務版圖', enterBusiness: '查看業務版圖',
@ -32,8 +56,8 @@ export const hk = {
title: '巴適速度', title: '巴適速度',
subtitle: '以溫暖科技,連接美好生活的堅實底座', subtitle: '以溫暖科技,連接美好生活的堅實底座',
items: [ items: [
{ value: '5', suffix: '大', label: '核心賽道', sub: '內容社交 · 職場服務 · 數字支付 · AI · 生活服務' }, // 只保留願景性指標。5 大核心賽道 / 8+ 自研產品已由上方 CompanyVideo 左欄
{ value: '8', suffix: '+', label: '自研產品', sub: '覆蓋數字生活全場景' }, // (home.video.intro)負責講,這裡不再重複第二遍。
{ value: '全球', suffix: '', label: '服務願景', sub: '讓優質產品觸達全球用戶' }, { value: '全球', suffix: '', label: '服務願景', sub: '讓優質產品觸達全球用戶' },
{ value: '長期', suffix: '', label: '價值主義', sub: '與用戶、夥伴、社會共贏' }, { value: '長期', suffix: '', label: '價值主義', sub: '與用戶、夥伴、社會共贏' },
], ],
@ -113,15 +137,73 @@ export const hk = {
team: { team: {
title: '管理團隊', title: '管理團隊',
desc: '推動公司前行的核心夥伴', desc: '推動公司前行的核心夥伴',
intro: '我們是一支專業分工、協同高效的團隊,由法人代表領銜,成員覆蓋全棧、前端、後端、安卓開發及財務等方向,持續為產品研發與業務運營提供支持。',
icon: 'users', icon: 'users',
href: '#about-team', href: '#about-team',
members: [ // 詳情彈窗的 UI 文案(成員長簡介在 src/i18n/teamDetails/,按需載入)
{ name: '張順', role: '軟件開發工程師', gender: 'male' }, ui: {
{ name: '張卜允', role: '軟件開發工程師', gender: 'female' }, viewProfile: '查看詳情',
{ name: '陳雪妮', role: '軟件開發工程師', gender: 'female' }, close: '關閉',
{ name: '陳曼麗', role: '財務專員', gender: 'female' }, nationalityLabel: '國籍',
{ name: '楊幸幸', role: '軟件開發工程師', gender: 'female' }, introLabel: '崗位介紹',
], strengthsLabel: '技術優勢',
traitsLabel: '個人特點',
loadingLabel: '正在載入簡介…',
errorLabel: '簡介暫時載入失敗,關閉後重開可再試一次。',
},
groups: [
{
key: 'management',
title: '管理層',
members: [
{ name: '陳蓬濤', role: '法人代表', photo: 'https://oss.baskbuddy.com/baskbuddy/team-male.jpg' },
],
},
{
key: 'engineering',
title: '技術研發',
members: [
{ name: '吳啟明', role: '安卓開發工程師', photo: 'https://oss.baskbuddy.com/baskbuddy/team-male.jpg' },
{
name: '楊幸幸',
role: '全棧開發工程師',
photo: 'https://oss.baskbuddy.com/baskbuddy/team-female.jpg',
nationality: '中國',
detailId: 'yang-xingxing',
},
{
name: '張順',
role: '後端開發工程師兼產品經理',
photo: 'https://oss.baskbuddy.com/baskbuddy/team-male.jpg',
nationality: '中國',
detailId: 'zhang-shun',
},
{
name: '陳潔',
role: '前端開發工程師',
photo: 'https://oss.baskbuddy.com/baskbuddy/team-female.jpg',
nationality: '中國',
detailId: 'chen-jie',
},
{
name: '張卜允',
role: '前端開發工程師兼 UI 設計師',
photo: 'https://oss.baskbuddy.com/baskbuddy/team-female.jpg',
nationality: '中國',
detailId: 'zhang-buyun',
},
{ name: '劉瀟強', role: '後端開發工程師', photo: 'https://oss.baskbuddy.com/baskbuddy/team-male.jpg' },
{ name: '李世博', role: '後端開發工程師', photo: 'https://oss.baskbuddy.com/baskbuddy/team-male.jpg' },
],
},
{
key: 'finance',
title: '財務',
members: [
{ name: '張夢想', role: '財務專員', photo: 'https://oss.baskbuddy.com/baskbuddy/team-female.jpg' },
],
},
] satisfies TeamGroup[],
}, },
vision: { title: '願景及使命', desc: '我們要去的地方,與為何出發', icon: 'telescope', href: '#about-vision' }, vision: { title: '願景及使命', desc: '我們要去的地方,與為何出發', icon: 'telescope', href: '#about-vision' },
}, },
@ -145,7 +227,7 @@ export const hk = {
nodes: [ nodes: [
{ id: 'position', title: '公司定位', desc: '巴適夥伴(上海)科技有限公司是一家聚焦數字生活服務領域的高新技術企業,依託多品牌、多垂直賽道的產品佈局,面向不同用戶群體提供優質產品與服務。' }, { id: 'position', title: '公司定位', desc: '巴適夥伴(上海)科技有限公司是一家聚焦數字生活服務領域的高新技術企業,依託多品牌、多垂直賽道的產品佈局,面向不同用戶群體提供優質產品與服務。' },
{ id: 'tracks', title: '五大核心賽道', desc: '以內容社交、職場服務、數字支付、人工智能、生活服務五大核心賽道為支柱,孵化 8 自研產品,覆蓋用戶數字生活全場景。', link: '/business' }, { id: 'tracks', title: '五大核心賽道', desc: '以內容社交、職場服務、數字支付、人工智能、生活服務五大核心賽道為支柱,孵化 8 自研產品,覆蓋用戶數字生活全場景。', link: '/business' },
{ id: 'team', title: '管理團隊', desc: '由張順、張卜允、陳雪妮、陳曼麗、楊幸幸五位夥伴組成的核心團隊,專注軟件研發與產品創新。', link: '#about-team' }, { id: 'team', title: '管理團隊', desc: '由 9 位夥伴組成的核心團隊,覆蓋技術研發與財務,專注產品研發與業務運營。', link: '#about-team' },
{ id: 'vision', title: '願景使命', desc: '願景:打造全球領先的數字生活服務生態。使命:以創新技術連接人與服務,為不同用戶群體打造便捷高效、富有價值的美好生活。' }, { id: 'vision', title: '願景使命', desc: '願景:打造全球領先的數字生活服務生態。使命:以創新技術連接人與服務,為不同用戶群體打造便捷高效、富有價值的美好生活。' },
{ id: 'data', title: '關鍵數據', desc: '5 大核心賽道 · 8 自研產品 · 全球生態佈局 · 2026 啟航之年。' }, { id: 'data', title: '關鍵數據', desc: '5 大核心賽道 · 8 自研產品 · 全球生態佈局 · 2026 啟航之年。' },
{ id: 'life', title: '連接用戶與生活', desc: '讓每一個平凡的日常,都被用心對待——技術終將隱於無形,溫暖始終相伴。' }, { id: 'life', title: '連接用戶與生活', desc: '讓每一個平凡的日常,都被用心對待——技術終將隱於無形,溫暖始終相伴。' },
@ -237,9 +319,9 @@ export const hk = {
title: '一群年輕人,正把巴適做成現實', title: '一群年輕人,正把巴適做成現實',
subtitle: '腦力激盪、結對編程、慶祝每一次小小的勝利——奮鬥在這裡是有溫度的。', subtitle: '腦力激盪、結對編程、慶祝每一次小小的勝利——奮鬥在這裡是有溫度的。',
items: [ items: [
{ img: 'https://oss.baskbuddy.com/baskbuddy/team-brainstorm.jpg', caption: '白板前的靈感碰撞' }, { caption: '白板前的靈感碰撞' },
{ img: 'https://oss.baskbuddy.com/baskbuddy/team-pairing.jpg', caption: '結對編程,把想法落到程式碼' }, { caption: '結對編程,把想法落到程式碼' },
{ img: 'https://oss.baskbuddy.com/baskbuddy/team-celebrate.jpg', caption: '每一個小勝利,都值得舉杯' }, { caption: '每一個小勝利,都值得舉杯' },
], ],
}, },
growthIncentive: { growthIncentive: {
@ -404,7 +486,7 @@ export const hk = {
legal: { legal: {
privacy: '隱私政策', privacy: '隱私政策',
}, },
copyright: `© ${new Date().getFullYear()} 巴適夥伴(上海)科技有限公司`, copyright: `© ${new Date().getFullYear()} 巴適夥伴(上海)科技有限公司 BaskBuddy (Shanghai) Technology Co., Ltd.`,
icidpFull: 'Integrity · Courage · Innovation · Devotion · Passion', icidpFull: 'Integrity · Courage · Innovation · Devotion · Passion',
}, },
}; };

39
src/i18n/teamDetails/en.ts

@ -0,0 +1,39 @@
import type { MemberDetail, TeamDetailId } from '../zh';
/** Team member profiles (English). See ./zh.ts for why these live outside en.ts. */
export const enTeamDetails = {
'yang-xingxing': {
intro:
'Yang Xingxing is a full-stack engineer and the front-end lead for our core job-and-recruitment intelligence product. They own front-end and back-end architecture, core system development, API governance and feature iteration, and take part in requirement reviews and technical decision-making. Day to day they build with Vibe Coding, and have shipped RAG retrieval-augmented systems and Agent modules that bring large-model capability into our business systems.',
strengths:
'Fluent across mainstream front-end frameworks such as React and Vue, with Node.js, Java and Python on the back end, and solid grounding in microservice architecture, high-concurrency optimisation and cloud-native deployment. Able to build RAG knowledge bases, vector search and document-processing pipelines, orchestrate Agent tasks, and carry a project independently from requirement analysis and system design through to delivery — using AI tooling where it genuinely speeds development up.',
traits:
'Always tracking frontier work on Agents and RAG, and putting it into production. Rigorous in code, with a focus on stability, maintainability and extensibility. Communicates well across teams, reads business requirements accurately, and delivers solutions that are both technically sound and commercially useful.',
},
'zhang-shun': {
intro:
'Zhang Shun is a back-end engineer and product manager, and leads our core job-and-recruitment intelligence product. Their work covers back-end system design, feature development, database design, API development, performance tuning and day-to-day system maintenance.',
strengths:
'Experienced with Java, Spring Boot, MySQL and Redis, with a strong programming foundation and solid system-design ability. They can independently handle requirement analysis, API design, feature development and troubleshooting, with a firm focus on security, stability and code quality, backed by deep experience in data processing, service architecture and business logic.',
traits:
'Careful and rigorous, with a strong sense of responsibility, good logical thinking and clear communication. They hold to coding standards, data security and risk control, are skilled at tracking down complex technical problems, and work closely with every team to land projects on stable, dependable technical foundations.',
},
'chen-jie': {
intro:
'Chen Jie is a front-end engineer and the front-end lead for our core content-and-social intelligence product. They are responsible for building and maintaining project pages — layout, interactions, API integration and multi-device adaptation.',
strengths:
'Familiar with the front-end development workflow and equipped with a solid technical foundation, they deliver page development, performance optimisation and troubleshooting efficiently, and have a deep understanding of mobile adaptation and user experience.',
traits:
'Meticulous and responsible, with close attention to coding standards and detail, plus patience and good communication skills. They work steadily alongside product, design, back-end and QA teams to make sure projects ship on schedule and to a high standard.',
},
'zhang-buyun': {
intro:
'Zhang Buyun is a front-end engineer and UI designer, and leads our core content-and-social intelligence product. They build and maintain our H5, WeChat Mini Program and PC pages, integrate APIs with back-end engineers, implement interactions and motion, and keep improving front-end performance and cross-platform compatibility — alongside requirement reviews, technical design and production troubleshooting.',
strengths:
'Comfortable with the mainstream front-end stack (Vue, React, Taro and more), with a solid grounding in HTML, CSS and JavaScript. They turn UI designs into faithful code, have hands-on experience with front-end engineering, componentisation and performance work, and can take a page from first build all the way through to API integration.',
traits:
'Clear-headed and detail-oriented, with high standards for code quality and user experience. They communicate well and work as part of a team, turning business requirements into technical implementation quickly. Steady and quick to learn, they work patiently through problems and coordinate closely with product, design and back-end colleagues.',
},
} satisfies Record<TeamDetailId, MemberDetail>;
export default enTeamDetails;

39
src/i18n/teamDetails/hk.ts

@ -0,0 +1,39 @@
import type { MemberDetail, TeamDetailId } from '../zh';
/** 團隊成員長簡介(繁體)。拆分理由見 ./zh.ts。 */
export const hkTeamDetails = {
'yang-xingxing': {
intro:
'楊幸幸,全棧開發工程師,目前擔任公司核心業務——求職招聘智能服務產品的前端負責人,主要負責前後端架構設計、核心業務系統研發、接口治理與產品功能迭代,參與需求評審與技術方案決策;日常以 Vibe Coding 方式編碼開發,同時基於業務場景落地 RAG 檢索增強系統與 Agent 智能體模組,推動大模型能力與業務系統整合。',
strengths:
'精通 React/Vue 等主流前端框架與 Node.js/Java/Python 後端技術,熟悉微服務架構、高併發場景優化與雲原生部署;具備 RAG 知識庫建構、向量檢索、文件處理及 Agent 任務編排能力,擁有從需求分析、系統設計到業務開發與 AI 應用落地的全鏈路獨立開發能力,能合理運用 AI 開發工具提升編碼效率。',
traits:
'持續關注 Agent、RAG 等前沿技術並落地業務,編碼嚴謹,重視系統穩定性、可維護性與可擴展性;擅長跨團隊溝通協作,能準確理解業務需求,輸出兼具技術合理性與業務價值的解決方案,善用 AI 工具賦能研發,為團隊產品迭代與技術建設提供支撐。',
},
'zhang-shun': {
intro:
'張順,現任公司後端開發工程師兼產品經理,目前擔任公司核心業務——求職招聘智能服務產品的負責人,主要負責後端系統設計、業務功能開發、數據庫設計、接口開發、效能優化以及系統日常維護工作。',
strengths:
'熟悉 Java、Spring Boot、MySQL、Redis 等後端技術,具備紮實的編程基礎與良好的系統設計能力,能獨立完成業務需求分析、接口設計、功能開發與問題排查,重視系統安全性、穩定性與程式碼品質,在數據處理、服務架構及業務邏輯實現上經驗豐富。',
traits:
'做事認真嚴謹,責任心強,具備良好的邏輯思維、問題分析與溝通協作能力。工作中注重程式碼規範、數據安全與風險控制,善於定位並解決複雜技術問題,能積極配合各部門推進項目落地,以穩定可靠的技術方案為公司業務發展提供後端支持。',
},
'chen-jie': {
intro:
'陳潔,現任前端開發工程師,目前擔任公司核心業務——內容社交智能服務產品的前端負責人,主要負責公司項目頁面的開發與維護,包括頁面搭建、互動實現、接口聯調以及多機型適配等工作。',
strengths:
'熟悉前端開發流程,具備紮實的前端技術基礎,能高效完成頁面開發、效能優化和問題排查,對移動端適配與用戶體驗有很深的體會和了解。',
traits:
'做事認真細緻,責任心強,注重程式碼規範與細節打磨,具備良好的耐心與溝通能力。工作中踏實穩重,善於配合產品、設計、後端及測試團隊完成協作,以專業嚴謹的工作態度保障項目按期高品質上線。',
},
'zhang-buyun': {
intro:
'張卜允,現任前端開發工程師兼 UI 設計師,目前擔任公司核心業務——內容社交智能服務產品的負責人,主要負責公司 H5、微信小程序及 PC 端頁面的開發與維護,與後端工程師對接接口聯調,實現頁面互動與動態效果,持續優化前端效能與多端相容性,並參與需求評審、技術方案設計及線上問題排查等工作。',
strengths:
'熟悉主流前端技術棧(Vue/React/Taro 等),具備紮實的 HTML、CSS、JavaScript 基礎,能高效還原 UI 設計稿,對前端工程化、組件化開發及效能優化有一定實踐經驗,能獨立完成從頁面搭建到接口對接的完整開發流程。',
traits:
'邏輯清晰,注重細節,對程式碼規範與用戶體驗有較高要求;具備良好的溝通能力與團隊協作意識,能快速理解業務需求並轉化為技術實現。工作中踏實穩重,學習能力強,面對問題耐心排查,善於配合產品、設計及後端同事推進項目,以嚴謹負責的技術態度為公司產品研發與業務落地提供可靠保障。',
},
} satisfies Record<TeamDetailId, MemberDetail>;
export default hkTeamDetails;

27
src/i18n/teamDetails/index.ts

@ -0,0 +1,27 @@
import type { MemberDetail, TeamDetailId } from '../zh';
import type { Locale } from '../utils';
export type TeamDetailMap = Record<TeamDetailId, MemberDetail>;
/** 每个语种一个 chunk。Vite 看到的是静态字面量的 import(),能静态分析出这三个分包,
* 所以它们不会被打进主包——这正是把长简介从 i18n/*.ts 里搬出来的目的。 */
const loaders: Record<Locale, () => Promise<{ default: TeamDetailMap }>> = {
zh: () => import('./zh'),
hk: () => import('./hk'),
en: () => import('./en'),
};
/** 缓存的是 Promise 而不是结果:弹窗开着时若因故重入,也不会并发打两次请求。 */
const cache = new Map<Locale, Promise<TeamDetailMap>>();
/** 按需拉取某个语种的成员长简介。同一语种只发一次请求。 */
export function loadTeamDetails(locale: Locale): Promise<TeamDetailMap> {
let pending = cache.get(locale);
if (!pending) {
pending = loaders[locale]().then((m) => m.default);
// 失败时把 promise 从缓存里摘掉,否则后续每次重试都只会拿到同一个 rejected promise
pending.catch(() => cache.delete(locale));
cache.set(locale, pending);
}
return pending;
}

47
src/i18n/teamDetails/zh.ts

@ -0,0 +1,47 @@
import type { MemberDetail, TeamDetailId } from '../zh';
/** 团队成员长简介(简体)。
*
* 刻意从 zh.ts 里拆出来单独放,原因只有一个:这三段正文每人 300–450 字,而
* 9 位成员里只有 4 位有——它却跟着 locale 一起被打进主包,导致首页、业务、
* 新闻这些根本用不到的页面每次首屏都在下载它。挪到这里之后,由 About 在用户
* 真要开弹窗时才 `import()`,见 ./index.ts。
*
* 用 Record<TeamDetailId, ...> 收口:少写一位成员、或写了 id 联合类型里没有的
* 键,都会在 tsc 阶段报错。三个语种各一份,缺一即挂。 */
export const zhTeamDetails = {
'yang-xingxing': {
intro:
'杨幸幸,全栈开发工程师,目前担任公司核心业务——求职招聘智能服务产品的前端负责人,主要负责前后端架构设计、核心业务系统研发、接口治理与产品功能迭代,参与需求评审与技术方案决策;日常以 Vibe Coding 方式编码开发,同时基于业务场景落地 RAG 检索增强系统与 Agent 智能体模块,推动大模型能力与业务系统集成。',
strengths:
'精通 React/Vue 等主流前端框架与 Node.js/Java/Python 后端技术,熟悉微服务架构、高并发场景优化与云原生部署;具备 RAG 知识库构建、向量检索、文档处理及 Agent 任务编排能力,拥有从需求分析、系统设计到业务开发与 AI 应用落地的全链路独立开发能力,能够合理运用 AI 开发工具提升编码效率。',
traits:
'持续关注 Agent、RAG 等前沿技术并落地业务,编码严谨,重视系统稳定性、可维护性与可扩展性;擅长跨团队沟通协作,能够准确理解业务需求,输出兼具技术合理性与业务价值的解决方案,善用 AI 工具赋能研发,为团队产品迭代与技术建设提供支撑。',
},
'zhang-shun': {
intro:
'张顺,现任公司后端开发工程师兼产品经理,目前担任公司核心业务——求职招聘智能服务产品的负责人,主要负责后端系统设计、业务功能开发、数据库设计、接口开发、性能优化以及系统日常维护工作。',
strengths:
'熟悉 Java、Spring Boot、MySQL、Redis 等后端技术,具备扎实的编程基础和良好的系统设计能力,能够独立完成业务需求分析、接口设计、功能开发与问题排查,重视系统安全性、稳定性和代码质量,在数据处理、服务架构及业务逻辑实现上经验丰富。',
traits:
'做事认真严谨,责任心强,具备良好的逻辑思维、问题分析与沟通协作能力。工作中注重代码规范、数据安全和风险控制,善于定位并解决复杂技术问题,能够积极配合各部门推进项目落地,以稳定可靠的技术方案为公司业务发展提供后端支持。',
},
'chen-jie': {
intro:
'陈洁,现任前端开发工程师,目前担任公司核心业务——内容社交智能服务产品的前端负责人,主要负责公司项目页面的开发与维护,包括页面搭建、交互实现、接口联调以及多机型适配等工作。',
strengths:
'熟悉前端开发流程,具备扎实的前端技术基础,能够高效完成页面开发、性能优化和问题排查,对移动端适配与用户体验有很深的体会和了解。',
traits:
'做事认真细致,责任心强,注重代码规范与细节打磨,具备良好的耐心与沟通能力。工作中踏实稳重,善于配合产品、设计、后端及测试团队完成协作,以专业严谨的工作态度保障项目按期高质量上线。',
},
'zhang-buyun': {
intro:
'张卜允,现任前端开发工程师兼 UI 设计师,目前担任公司核心业务——内容社交智能服务产品的负责人,主要负责公司 H5、微信小程序及 PC 端页面的开发与维护,与后端工程师对接接口联调,实现页面交互与动态效果,持续优化前端性能与多端兼容性,并参与需求评审、技术方案设计及线上问题排查等工作。',
strengths:
'熟悉主流前端技术栈(Vue/React/Taro 等),具备扎实的 HTML、CSS、JavaScript 基础,能够高效还原 UI 设计稿,对前端工程化、组件化开发及性能优化有一定实践经验,能够独立完成从页面搭建到接口对接的完整开发流程。',
traits:
'逻辑清晰,注重细节,对代码规范和用户体验有较高要求;具备良好的沟通能力与团队协作意识,能够快速理解业务需求并转化为技术实现。工作中踏实稳重,学习能力强,面对问题耐心排查,善于配合产品、设计及后端同事推进项目,以严谨负责的技术态度为公司产品研发与业务落地提供可靠保障。',
},
} satisfies Record<TeamDetailId, MemberDetail>;
export default zhTeamDetails;

2
src/i18n/utils.ts

@ -30,5 +30,3 @@ export function getLocalizedPath(path: string, locale: Locale): string {
} }
return path; return path;
} }
// 赛道配图已统一使用 src/placeholder.ts 的 PLACEHOLDER_IMG,不再维护按赛道映射的静态图。

153
src/i18n/zh.ts

@ -1,3 +1,45 @@
// 简体中文是 `Record<Locale, typeof zh>` 里的基准形状,en/hk 都按它做结构校验,
// 所以下面这些「当前为空 / 字段可选」的集合必须在这里显式标注类型——
// 否则空数组会被推断成 never[],消费方(Home / News / About)一读字段就报 TS2339。
export type NewsItem = {
title: string;
category: string;
date: string;
};
/** 成员长简介(弹窗正文)。三段标题「岗位介绍 / 技术优势 / 个人特点」是 UI 文案,
放在 about.subnav.team.ui 里,不随成员重复。
正文本身不存在这儿——见 teamDetails/,那里按需加载。 */
export type MemberDetail = {
intro: string;
strengths: string;
traits: string;
};
/** 有长简介的成员 id。正文在 teamDetails/<locale>.ts,用它做键;
这个联合类型是「三语必须齐全」的编译期闸门(见 teamDetails/zh.ts 的 satisfies)。 */
export type TeamDetailId = 'yang-xingxing' | 'zhang-shun' | 'chen-jie' | 'zhang-buyun';
export type TeamMember = {
name: string;
role: string;
/** 头像图 URL;留空则回落为姓名首字占位圆,见 About.tsx 的 memberInitial() */
photo?: string;
/** 国籍。刻意与长简介分开存:弹窗头部的国籍标签要立刻渲染,
不能等 teamDetails 那个 chunk 加载完再冒出来。 */
nationality?: string;
/** 有值即表示这位成员有长简介、出现「查看详情」入口(团队 9 人中目前 4 位有);
正文在同 id 的 teamDetails/<locale>.ts 里,点开时按需加载。 */
detailId?: TeamDetailId;
};
/** 管理团队按职能分组展示(技术研发 / 设计 / 财务) */
export type TeamGroup = {
key: string;
title: string;
members: TeamMember[];
};
export const zh = { export const zh = {
brand: 'BaskBuddy', brand: 'BaskBuddy',
brandSub: '巴适伙伴', brandSub: '巴适伙伴',
@ -16,6 +58,28 @@ export const zh = {
sloganIntro: '覆盖内容社交 · 职场服务 · 数字支付 · AI · 生活服务的数字生活服务商', sloganIntro: '覆盖内容社交 · 职场服务 · 数字支付 · AI · 生活服务的数字生活服务商',
heroTitle: '以智能创新开创数字未来', heroTitle: '以智能创新开创数字未来',
heroCta: '了解我们', heroCta: '了解我们',
video: {
eyebrow: 'INTRODUCTION',
title: '走进巴适伙伴',
subtitle: '用一段影像,看见我们如何以温暖科技连接美好生活。',
intro: [
{
value: '5',
unit: '大核心赛道',
desc: '作为数字生活服务生态的支柱布局,深耕内容社交、职场服务、数字支付、人工智能与生活服务,以多品牌矩阵回应不同用户群体的真实需求。',
},
{
value: '8',
unit: '自研产品',
desc: '作为连接用户与生活的产品网络,从青年社交、求职升学,到安全支付与心愿达成,覆盖数字生活全场景,让温暖触手可及。',
},
{
value: '2026',
unit: '起航之年',
desc: '作为"一生相伴·一生信赖"承诺的新起点,以温暖科技连接每一个人,稳步迈向全球领先的数字生活服务生态。',
},
],
},
trackOverview: '五大核心赛道', trackOverview: '五大核心赛道',
enterLab: '探索创新实验室', enterLab: '探索创新实验室',
enterBusiness: '查看业务版图', enterBusiness: '查看业务版图',
@ -32,8 +96,8 @@ export const zh = {
title: '巴适速度', title: '巴适速度',
subtitle: '以温暖科技,连接美好生活的坚实底座', subtitle: '以温暖科技,连接美好生活的坚实底座',
items: [ items: [
{ value: '5', suffix: '大', label: '核心赛道', sub: '内容社交 · 职场服务 · 数字支付 · AI · 生活服务' }, // 只保留愿景性指标。5 大核心赛道 / 8+ 自研产品已由上方 CompanyVideo 左栏
{ value: '8', suffix: '+', label: '自研产品', sub: '覆盖数字生活全场景' }, // (home.video.intro)负责讲,这里不再重复第二遍。
{ value: '全球', suffix: '', label: '服务愿景', sub: '让优质产品触达全球用户' }, { value: '全球', suffix: '', label: '服务愿景', sub: '让优质产品触达全球用户' },
{ value: '长期', suffix: '', label: '价值主义', sub: '与用户、伙伴、社会共赢' }, { value: '长期', suffix: '', label: '价值主义', sub: '与用户、伙伴、社会共赢' },
], ],
@ -113,15 +177,74 @@ export const zh = {
team: { team: {
title: '管理团队', title: '管理团队',
desc: '推动公司前行的核心伙伴', desc: '推动公司前行的核心伙伴',
// 区块简介:用于 #about-team 的 section-lead(desc 太短,撑不起区块导语)
intro: '我们是一支专业分工、协同高效的团队,由法人代表领衔,成员覆盖全栈、前端、后端、安卓开发及财务等方向,持续为产品研发与业务运营提供支持。',
icon: 'users', icon: 'users',
href: '#about-team', href: '#about-team',
members: [ // 详情弹窗的 UI 文案(成员长简介在 src/i18n/teamDetails/,按需加载)
{ name: '张顺', role: '软件开发工程师', gender: 'male' }, ui: {
{ name: '张卜允', role: '软件开发工程师', gender: 'female' }, viewProfile: '查看详情',
{ name: '陈雪妮', role: '软件开发工程师', gender: 'female' }, close: '关闭',
{ name: '陈曼丽', role: '财务专员', gender: 'female' }, nationalityLabel: '国籍',
{ name: '杨幸幸', role: '软件开发工程师', gender: 'female' }, introLabel: '岗位介绍',
], strengthsLabel: '技术优势',
traitsLabel: '个人特点',
loadingLabel: '正在加载简介…',
errorLabel: '简介暂时加载失败,关闭后重开可再试一次。',
},
groups: [
{
key: 'management',
title: '管理层',
members: [
{ name: '陈蓬涛', role: '法人代表', photo: 'https://oss.baskbuddy.com/baskbuddy/team-male.jpg' },
],
},
{
key: 'engineering',
title: '技术研发',
members: [
{ name: '吴启明', role: '安卓开发工程师', photo: 'https://oss.baskbuddy.com/baskbuddy/team-male.jpg' },
{
name: '杨幸幸',
role: '全栈开发工程师',
photo: 'https://oss.baskbuddy.com/baskbuddy/team-female.jpg',
nationality: '中国',
detailId: 'yang-xingxing',
},
{
name: '张顺',
role: '后端开发工程师兼产品经理',
photo: 'https://oss.baskbuddy.com/baskbuddy/team-male.jpg',
nationality: '中国',
detailId: 'zhang-shun',
},
{
name: '陈洁',
role: '前端开发工程师',
photo: 'https://oss.baskbuddy.com/baskbuddy/team-female.jpg',
nationality: '中国',
detailId: 'chen-jie',
},
{
name: '张卜允',
role: '前端开发工程师兼UI设计师',
photo: 'https://oss.baskbuddy.com/baskbuddy/team-female.jpg',
nationality: '中国',
detailId: 'zhang-buyun',
},
{ name: '刘潇强', role: '后端开发工程师', photo: 'https://oss.baskbuddy.com/baskbuddy/team-male.jpg' },
{ name: '李世博', role: '后端开发工程师', photo: 'https://oss.baskbuddy.com/baskbuddy/team-male.jpg' },
],
},
{
key: 'finance',
title: '财务',
members: [
{ name: '张梦想', role: '财务专员', photo: 'https://oss.baskbuddy.com/baskbuddy/team-female.jpg' },
],
},
] satisfies TeamGroup[],
}, },
vision: { title: '愿景及使命', desc: '我们要去的地方,与为何出发', icon: 'telescope', href: '#about-vision' }, vision: { title: '愿景及使命', desc: '我们要去的地方,与为何出发', icon: 'telescope', href: '#about-vision' },
}, },
@ -145,7 +268,7 @@ export const zh = {
nodes: [ nodes: [
{ id: 'position', title: '公司定位', desc: '巴适伙伴(上海)科技有限公司是一家聚焦数字生活服务领域的高科技企业,依托多品牌、多垂直赛道的产品布局,面向不同用户群体提供优质产品与服务。' }, { id: 'position', title: '公司定位', desc: '巴适伙伴(上海)科技有限公司是一家聚焦数字生活服务领域的高科技企业,依托多品牌、多垂直赛道的产品布局,面向不同用户群体提供优质产品与服务。' },
{ id: 'tracks', title: '五大核心赛道', desc: '以内容社交、职场服务、数字支付、人工智能、生活服务五大核心赛道为支柱,孵化 8 自研产品,覆盖用户数字生活全场景。', link: '/business' }, { id: 'tracks', title: '五大核心赛道', desc: '以内容社交、职场服务、数字支付、人工智能、生活服务五大核心赛道为支柱,孵化 8 自研产品,覆盖用户数字生活全场景。', link: '/business' },
{ id: 'team', title: '管理团队', desc: '由张顺、张卜允、陈雪妮、陈曼丽、杨幸幸五位伙伴组成的核心团队,专注软件研发与产品创新。', link: '#about-team' }, { id: 'team', title: '管理团队', desc: '由 9 位伙伴组成的核心团队,覆盖技术研发与财务,专注产品研发与业务运营。', link: '#about-team' },
{ id: 'vision', title: '愿景使命', desc: '愿景:打造全球领先的数字生活服务生态。使命:以创新技术连接人与服务,为不同用户群体打造便捷高效、富有价值的美好生活。' }, { id: 'vision', title: '愿景使命', desc: '愿景:打造全球领先的数字生活服务生态。使命:以创新技术连接人与服务,为不同用户群体打造便捷高效、富有价值的美好生活。' },
{ id: 'data', title: '关键数据', desc: '5 大核心赛道 · 8 自研产品 · 全球生态布局 · 2026 起航之年。' }, { id: 'data', title: '关键数据', desc: '5 大核心赛道 · 8 自研产品 · 全球生态布局 · 2026 起航之年。' },
{ id: 'life', title: '连接用户与生活', desc: '让每一个平凡的日常,都被用心对待——技术终将隐于无形,温暖始终相伴。' }, { id: 'life', title: '连接用户与生活', desc: '让每一个平凡的日常,都被用心对待——技术终将隐于无形,温暖始终相伴。' },
@ -237,9 +360,9 @@ export const zh = {
title: '一群年轻人,正把巴适做成现实', title: '一群年轻人,正把巴适做成现实',
subtitle: '头脑风暴、结对编程、庆祝每一次小小的胜利——奋斗在这里是有温度的。', subtitle: '头脑风暴、结对编程、庆祝每一次小小的胜利——奋斗在这里是有温度的。',
items: [ items: [
{ img: 'https://oss.baskbuddy.com/baskbuddy/team-brainstorm.jpg', caption: '白板前的灵感碰撞' }, { caption: '白板前的灵感碰撞' },
{ img: 'https://oss.baskbuddy.com/baskbuddy/team-pairing.jpg', caption: '结对编程,把想法落到代码' }, { caption: '结对编程,把想法落到代码' },
{ img: 'https://oss.baskbuddy.com/baskbuddy/team-celebrate.jpg', caption: '每一个小胜利,都值得举杯' }, { caption: '每一个小胜利,都值得举杯' },
], ],
}, },
growthIncentive: { growthIncentive: {
@ -342,7 +465,7 @@ export const zh = {
subtitle: '每一步,都值得被记录', subtitle: '每一步,都值得被记录',
intro: '这里将汇集巴适伙伴的公司动态、品牌观点与行业洞察。', intro: '这里将汇集巴适伙伴的公司动态、品牌观点与行业洞察。',
empty: '暂无新闻动态,敬请期待。', empty: '暂无新闻动态,敬请期待。',
items: [], items: [] as NewsItem[],
}, },
investors: { investors: {
title: '投资者关系', title: '投资者关系',
@ -404,7 +527,7 @@ export const zh = {
legal: { legal: {
privacy: '隐私政策', privacy: '隐私政策',
}, },
copyright: `© ${new Date().getFullYear()} 巴适伙伴(上海)科技有限公司`, copyright: `© ${new Date().getFullYear()} 巴适伙伴(上海)科技有限公司 BaskBuddy (Shanghai) Technology Co., Ltd.`,
icidpFull: 'Integrity · Courage · Innovation · Devotion · Passion', icidpFull: 'Integrity · Courage · Innovation · Devotion · Passion',
}, },
}; };

17
src/icons/index.tsx

@ -57,7 +57,9 @@ import {
Leaf, Leaf,
FileText, FileText,
Copy, Copy,
Play,
Check, Check,
X,
} from 'lucide-react'; } from 'lucide-react';
export const iconMap = { export const iconMap = {
@ -114,7 +116,9 @@ export const iconMap = {
leaf: Leaf, leaf: Leaf,
fileText: FileText, fileText: FileText,
copy: Copy, copy: Copy,
play: Play,
check: Check, check: Check,
x: X,
} as const; } as const;
export type IconName = keyof typeof iconMap; export type IconName = keyof typeof iconMap;
@ -192,12 +196,7 @@ export const longTermIncentiveIcons: IconName[] = ['pieChart', 'sprout', 'handsh
export const benefitIcons: IconName[] = ['clock', 'home', 'heartPulse', 'users', 'bookOpen']; export const benefitIcons: IconName[] = ['clock', 'home', 'heartPulse', 'users', 'bookOpen'];
export const statsIcons: IconName[] = ['layoutGrid', 'package', 'telescope', 'globe']; // 首页「愿景带」两格(按 home.stats.items 顺序:服务愿景 / 价值主义)。
// 原为四格 ['layoutGrid','package','telescope','globe'] 对应 5大/8+/全球/长期;
// Home「巴适速度」四宫格:每格承一种赛道色,强化全站色彩体系 // 5大 与 8+ 已删(与 CompanyVideo 左栏重复),故此数组同步缩为两项,否则图标整体错位。
export const statsColors: string[] = [ export const statsIcons: IconName[] = ['telescope', 'globe'];
'var(--track-social)',
'var(--track-career)',
'var(--track-payment)',
'var(--track-ai)',
];

2
src/layout/RootLayout.tsx

@ -16,7 +16,7 @@ export default function RootLayout() {
useClientEffects(location.pathname); useClientEffects(location.pathname);
return ( return (
<div className="min-h-screen font-[var(--font-body)] antialiased bg-[var(--bg-primary)] text-[var(--text-primary)]"> <div className="min-h-screen font-[family-name:var(--font-body)] antialiased bg-[var(--bg-primary)] text-[var(--text-primary)]">
<SiteShell locale={locale}> <SiteShell locale={locale}>
<Suspense fallback={<div className="page-loading" aria-live="polite" />}> <Suspense fallback={<div className="page-loading" aria-live="polite" />}>
<div key={location.pathname} className="page-enter"> <div key={location.pathname} className="page-enter">

2
src/pages/AboutPage.tsx

@ -7,7 +7,7 @@ export default function AboutPage() {
const t = getTranslation(locale); const t = getTranslation(locale);
return ( return (
<> <>
<Seo title={t.nav.about} description={t.about.intro} locale={locale} ogImageOverride="https://oss.baskbuddy.com/baskbuddy/og-about.png" /> <Seo title={t.nav.about} description={t.about.intro} locale={locale} />
<About locale={locale} /> <About locale={locale} />
</> </>
); );

2
src/pages/BusinessPage.tsx

@ -7,7 +7,7 @@ export default function BusinessPage() {
const t = getTranslation(locale); const t = getTranslation(locale);
return ( return (
<> <>
<Seo title={t.nav.business} description={t.business.subtitle} locale={locale} ogImageOverride="https://oss.baskbuddy.com/baskbuddy/og-business.png" /> <Seo title={t.nav.business} description={t.business.subtitle} locale={locale} />
<Business locale={locale} /> <Business locale={locale} />
</> </>
); );

2
src/pages/ContactPage.tsx

@ -7,7 +7,7 @@ export default function ContactPage() {
const t = getTranslation(locale); const t = getTranslation(locale);
return ( return (
<> <>
<Seo title={t.contact.title} description={t.contact.subtitle} locale={locale} ogImageOverride="https://oss.baskbuddy.com/baskbuddy/og-contact.png" /> <Seo title={t.contact.title} description={t.contact.subtitle} locale={locale} />
<Contact locale={locale} /> <Contact locale={locale} />
</> </>
); );

2
src/pages/InvestorsPage.tsx

@ -7,7 +7,7 @@ export default function InvestorsPage() {
const t = getTranslation(locale); const t = getTranslation(locale);
return ( return (
<> <>
<Seo title={t.nav.investors} description={t.investors.intro} locale={locale} ogImageOverride="https://oss.baskbuddy.com/baskbuddy/og-default.png" /> <Seo title={t.nav.investors} description={t.investors.intro} locale={locale} />
<Investors locale={locale} /> <Investors locale={locale} />
</> </>
); );

2
src/pages/NewsPage.tsx

@ -7,7 +7,7 @@ export default function NewsPage() {
const t = getTranslation(locale); const t = getTranslation(locale);
return ( return (
<> <>
<Seo title={t.nav.news} description={t.news.intro} locale={locale} ogImageOverride="https://oss.baskbuddy.com/baskbuddy/og-default.png" /> <Seo title={t.nav.news} description={t.news.intro} locale={locale} />
<News locale={locale} /> <News locale={locale} />
</> </>
); );

2
src/pages/SustainabilityPage.tsx

@ -7,7 +7,7 @@ export default function SustainabilityPage() {
const t = getTranslation(locale); const t = getTranslation(locale);
return ( return (
<> <>
<Seo title={t.nav.sustainability} description={t.sustainability.intro} locale={locale} ogImageOverride="https://oss.baskbuddy.com/baskbuddy/og-default.png" /> <Seo title={t.nav.sustainability} description={t.sustainability.intro} locale={locale} />
<Sustainability locale={locale} /> <Sustainability locale={locale} />
</> </>
); );

5
src/placeholder.ts

@ -1,5 +0,0 @@
// 统一占位图:全站内容图(办公/团队/业务/新闻/横幅等)共用此图。
// 图片统一托管于 OSS:https://oss.baskbuddy.com/baskbuddy/
// 若要替换占位图,把对应文件上传到 OSS 后改本常量一处即可全局生效。
export const PLACEHOLDER_IMG =
'https://oss.baskbuddy.com/baskbuddy/placeholder.jpg';

40
src/seo/Seo.tsx

@ -6,23 +6,32 @@ interface SeoProps {
description?: string; description?: string;
keywords?: string; keywords?: string;
locale: Locale; locale: Locale;
/** Per-page og:image override (default: OSS og-default.png) */
ogImageOverride?: string;
} }
const DEFAULT_DESC_ZH = const DEFAULT_DESC_ZH =
'BaskBuddy 巴适伙伴(上海)科技有限公司 — 以温暖科技连接美好生活,五大赛道覆盖内容社交、职场服务、数字支付、人工智能、生活服务'; 'BaskBuddy 巴适伙伴(上海)科技有限公司 — 以温暖科技连接美好生活,五大赛道覆盖内容社交、职场服务、数字支付、人工智能、生活服务';
const DEFAULT_DESC_EN = const DEFAULT_DESC_EN =
'BaskBuddy (Bashi Partners Shanghai) — Connecting beautiful life with warm technology. Five tracks: Content & Social, Career Services, Digital Payment, Artificial Intelligence, Life Services'; 'BaskBuddy (Shanghai) Technology Co., Ltd. — Connecting beautiful life with warm technology. Five tracks: Content & Social, Career Services, Digital Payment, Artificial Intelligence, Life Services';
const DEFAULT_DESC_HK = const DEFAULT_DESC_HK =
'BaskBuddy 巴適夥伴(上海)科技有限公司 — 以溫暖科技連接美好生活,五大賽道覆蓋內容社交、職場服務、數字支付、人工智能、生活服務'; 'BaskBuddy 巴適夥伴(上海)科技有限公司 — 以溫暖科技連接美好生活,五大賽道覆蓋內容社交、職場服務、數字支付、人工智能、生活服務';
const DEFAULT_KW_ZH = const DEFAULT_KW_ZH =
'巴适伙伴,BaskBuddy,数字生活服务,内容社交,职场服务,数字支付,人工智能,ICIDP,企业官网,上海科技公司'; '巴适伙伴,BaskBuddy,数字生活服务,内容社交,职场服务,数字支付,人工智能,ICIDP,企业官网,上海科技公司';
const DEFAULT_KW_EN = const DEFAULT_KW_EN =
'BaskBuddy,Bashi Partners,Digital Life Services,Content Social,Career Services,Digital Payment,Artificial Intelligence,ICIDP,Shanghai Tech Company'; 'BaskBuddy,BaskBuddy Shanghai,Digital Life Services,Content Social,Career Services,Digital Payment,Artificial Intelligence,ICIDP,Shanghai Tech Company';
const DEFAULT_KW_HK = const DEFAULT_KW_HK =
'巴適夥伴,BaskBuddy,數字生活服務,內容社交,職場服務,數字支付,人工智能,ICIDP,企業官網,上海科技公司'; '巴適夥伴,BaskBuddy,數字生活服務,內容社交,職場服務,數字支付,人工智能,ICIDP,企業官網,上海科技公司';
// ===== Canonical origin =====
// 线上只有 www.baskbuddy.com 有 DNS 解析(裸域 baskbuddy.com 在 Google / AliDNS /
// Cloudflare 上均无 A 记录),故 canonical / og:url / hreflang 一律锚定 www。
// 这里刻意不用 window.location.origin:否则从裸域、IP 或测试域名访问时会把非规范
// 地址写进 canonical,反而误导搜索引擎。
const SITE_ORIGIN = 'https://www.baskbuddy.com';
// 全站图片/视频托管于 OSS(国内直连快),OG 分享图同源。
const OSS_BASE = 'https://oss.baskbuddy.com/baskbuddy';
const OG_IMAGE = `${OSS_BASE}/hero-bg.jpg`;
function setMeta(uniqueAttr: string, attrs: Record<string, string>, content: string) { function setMeta(uniqueAttr: string, attrs: Record<string, string>, content: string) {
const selector = `meta[${uniqueAttr}]`; const selector = `meta[${uniqueAttr}]`;
let el = document.head.querySelector(selector) as HTMLMetaElement | null; let el = document.head.querySelector(selector) as HTMLMetaElement | null;
@ -47,16 +56,16 @@ function setLink(uniqueAttr: string, attrs: Record<string, string>, href: string
// Recreates the per-page <head> metadata that BaseLayout.astro used to emit. // Recreates the per-page <head> metadata that BaseLayout.astro used to emit.
// Runs on every route change so the document title / meta stay in sync in the SPA. // Runs on every route change so the document title / meta stay in sync in the SPA.
export default function Seo({ title, description, keywords, locale, ogImageOverride }: SeoProps) { export default function Seo({ title, description, keywords, locale }: SeoProps) {
useEffect(() => { useEffect(() => {
const t = getTranslation(locale); const t = getTranslation(locale);
const brandName = t.brand || 'BaskBuddy'; const brandName = t.brand || 'BaskBuddy';
const finalDescription = description ?? (locale === 'en' ? DEFAULT_DESC_EN : locale === 'hk' ? DEFAULT_DESC_HK : DEFAULT_DESC_ZH); const finalDescription = description ?? (locale === 'en' ? DEFAULT_DESC_EN : locale === 'hk' ? DEFAULT_DESC_HK : DEFAULT_DESC_ZH);
const finalKeywords = keywords ?? (locale === 'en' ? DEFAULT_KW_EN : locale === 'hk' ? DEFAULT_KW_HK : DEFAULT_KW_ZH); const finalKeywords = keywords ?? (locale === 'en' ? DEFAULT_KW_EN : locale === 'hk' ? DEFAULT_KW_HK : DEFAULT_KW_ZH);
const orgName = const orgName =
locale === 'en' ? 'Bashi Partners (Shanghai) Technology Co., Ltd.' : locale === 'hk' ? '巴適夥伴(上海)科技有限公司' : '巴适伙伴(上海)科技有限公司'; locale === 'en' ? 'BaskBuddy (Shanghai) Technology Co., Ltd.' : locale === 'hk' ? '巴適夥伴(上海)科技有限公司' : '巴适伙伴(上海)科技有限公司';
const origin = window.location.origin; const origin = SITE_ORIGIN;
const pathname = window.location.pathname; const pathname = window.location.pathname;
const canonicalUrl = origin + pathname; const canonicalUrl = origin + pathname;
const segs = pathname.split('/'); const segs = pathname.split('/');
@ -66,9 +75,7 @@ export default function Seo({ title, description, keywords, locale, ogImageOverr
const enAlternate = origin + (base === '/' ? '/en' : `/en${base}`); const enAlternate = origin + (base === '/' ? '/en' : `/en${base}`);
const hkAlternate = origin + (base === '/' ? '/hk' : `/hk${base}`); const hkAlternate = origin + (base === '/' ? '/hk' : `/hk${base}`);
const logoPath = 'https://oss.baskbuddy.com/baskbuddy/baskbuddy-logo.png'; const logoPath = `${OSS_BASE}/baskbuddy-logo.png`;
const ogImagePath = ogImageOverride || 'https://oss.baskbuddy.com/baskbuddy/og-default.png';
const ogImage = ogImagePath.startsWith('http') ? ogImagePath : origin + ogImagePath;
// Document level // Document level
document.documentElement.lang = locale === 'en' ? 'en' : locale === 'hk' ? 'zh-HK' : 'zh-CN'; document.documentElement.lang = locale === 'en' ? 'en' : locale === 'hk' ? 'zh-HK' : 'zh-CN';
@ -91,13 +98,9 @@ export default function Seo({ title, description, keywords, locale, ogImageOverr
setMeta('property="og:title"', { property: 'og:title' }, `${title} | ${brandName}`); setMeta('property="og:title"', { property: 'og:title' }, `${title} | ${brandName}`);
setMeta('property="og:description"', { property: 'og:description' }, finalDescription); setMeta('property="og:description"', { property: 'og:description' }, finalDescription);
setMeta('property="og:url"', { property: 'og:url' }, canonicalUrl); setMeta('property="og:url"', { property: 'og:url' }, canonicalUrl);
setMeta('property="og:image"', { property: 'og:image' }, ogImage);
setMeta(
'property="og:image:alt"',
{ property: 'og:image:alt' },
`${brandName} — ${locale === 'en' ? 'Together for Life' : locale === 'hk' ? '巴適夥伴,一生相伴' : '巴适伙伴,一生相伴'}`,
);
const ogLocale = locale === 'en' ? 'en_US' : locale === 'hk' ? 'zh_HK' : 'zh_CN'; const ogLocale = locale === 'en' ? 'en_US' : locale === 'hk' ? 'zh_HK' : 'zh_CN';
setMeta('property="og:image"', { property: 'og:image' }, OG_IMAGE);
setMeta('property="og:image:alt"', { property: 'og:image:alt' }, `${brandName} ${t.brandSub || '巴适伙伴'}`);
setMeta('property="og:locale"', { property: 'og:locale' }, ogLocale); setMeta('property="og:locale"', { property: 'og:locale' }, ogLocale);
setMeta( setMeta(
'property="og:locale:alt"', 'property="og:locale:alt"',
@ -108,11 +111,10 @@ export default function Seo({ title, description, keywords, locale, ogImageOverr
// Twitter Card // Twitter Card
setMeta('name="twitter:card"', { name: 'twitter:card' }, 'summary_large_image'); setMeta('name="twitter:card"', { name: 'twitter:card' }, 'summary_large_image');
setMeta('name="twitter:image"', { name: 'twitter:image' }, OG_IMAGE);
setMeta('name="twitter:site"', { name: 'twitter:site' }, '@baskbuddy'); setMeta('name="twitter:site"', { name: 'twitter:site' }, '@baskbuddy');
setMeta('name="twitter:title"', { name: 'twitter:title' }, `${title} | ${brandName}`); setMeta('name="twitter:title"', { name: 'twitter:title' }, `${title} | ${brandName}`);
setMeta('name="twitter:description"', { name: 'twitter:description' }, finalDescription); setMeta('name="twitter:description"', { name: 'twitter:description' }, finalDescription);
setMeta('name="twitter:image"', { name: 'twitter:image' }, ogImage);
setMeta('name="twitter:image:alt"', { name: 'twitter:image:alt' }, `${brandName} Logo`);
// Organization JSON-LD // Organization JSON-LD
const jsonLdOrg = { const jsonLdOrg = {
@ -140,7 +142,7 @@ export default function Seo({ title, description, keywords, locale, ogImageOverr
document.head.appendChild(script); document.head.appendChild(script);
} }
script.textContent = JSON.stringify(jsonLdOrg); script.textContent = JSON.stringify(jsonLdOrg);
}, [title, description, keywords, locale, ogImageOverride]); }, [title, description, keywords, locale]);
return null; return null;
} }

1192
src/styles/global.css

File diff suppressed because it is too large

2
tsconfig.json

@ -10,6 +10,8 @@
"isolatedModules": true, "isolatedModules": true,
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
// 增量编译缓存也放进 node_modules,别在项目根留下 tsconfig.tsbuildinfo
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "react", "jsxImportSource": "react",
"strict": true, "strict": true,

6
tsconfig.node.json

@ -6,6 +6,12 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": true, "strict": true,
// 本工程只做类型检查,不产出可运行 JS。
// 注意:被 tsconfig.json 引用的 composite 工程不允许设 "noEmit"(TS6310),
// 所以退而求其次 —— 只 emit 声明文件,并把产物关进 node_modules/.tmp,
// 避免 `tsc -b` 把 vite.config.js / vite.config.d.ts 吐在项目根目录。
"emitDeclarationOnly": true,
"outDir": "./node_modules/.tmp/node-types",
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo" "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
}, },
"include": ["vite.config.ts"] "include": ["vite.config.ts"]

13
vercel.json

@ -1,13 +0,0 @@
{
"rewrites": [
{ "source": "/robots.txt", "destination": "/robots.txt" },
{ "source": "/sitemap.xml", "destination": "/sitemap.xml" },
{ "source": "/og-default.png", "destination": "/og-default.png" },
{ "source": "/og-about.png", "destination": "/og-about.png" },
{ "source": "/og-business.png", "destination": "/og-business.png" },
{ "source": "/og-innovation.png", "destination": "/og-innovation.png" },
{ "source": "/og-join.png", "destination": "/og-join.png" },
{ "source": "/og-contact.png", "destination": "/og-contact.png" },
{ "source": "/(.*)", "destination": "/index.html" }
]
}
Loading…
Cancel
Save