Browse Source

提交官网最新代码

master
doublex001 3 months ago
parent
commit
cd39c5b4a8
  1. 163
      business-architecture.md
  2. 45
      overview-color-icon-final.md
  3. 38
      overview-dark-mode-tokens.md
  4. 47
      overview-icon-color-audit.md
  5. 25
      overview.md
  6. BIN
      public/baskbuddy-logo.png
  7. 6
      src/components/About.tsx
  8. 2
      src/components/Business.tsx
  9. 68
      src/components/Contact.tsx
  10. 336
      src/components/Footer.tsx
  11. 2
      src/components/Home.tsx
  12. 32
      src/i18n/en.ts
  13. 32
      src/i18n/hk.ts
  14. 32
      src/i18n/zh.ts
  15. 4
      src/icons/index.tsx
  16. 2
      src/pages/AboutPage.tsx
  17. 2
      src/pages/BusinessPage.tsx
  18. 2
      src/pages/ContactPage.tsx
  19. 2
      src/pages/InvestorsPage.tsx
  20. 2
      src/pages/NewsPage.tsx
  21. 2
      src/pages/SustainabilityPage.tsx
  22. 7
      src/placeholder.ts
  23. 10
      src/seo/Seo.tsx
  24. 69
      src/styles/global.css
  25. 104
      ui-design-spec.md

163
business-architecture.md

@ -1,163 +0,0 @@
# 巴适伙伴 · 业务体系总纲(业务全景图)
> 这是"各版块业务"的体系总纲。先定结构,后续产品文案、首页↔业务页衔接、Innovation/Sustainability/Investors 等边缘板块如何收口,都以本文档为据。
> **骨架已于 2026-08 确认**:第 8 节 6 项关键推断全部拍板(详见第 8 节"已确认的关键决策")。下文凡原标【推断·待确认】者均已转正为定稿;第 5–7 节为"骨架确认后的落地建议",待执行。
> 关联代码:业务页 [`Business.tsx`](src/components/Business.tsx) · 赛道数据 [`zh.ts`](src/i18n/zh.ts) `business.tracks` · 首页受众 [`Home.tsx`](src/components/Home.tsx) `pillars`。
---
## 0. 现状诊断:为什么现在还不是"一个完整的体系"
当前站点存在**两套并行、互不映射**的业务心智模型:
- **业务页** 按「4 条赛道」组织(内容社交/职场服务/数字支付/人工智能)——回答"我们造什么"
- **首页** 按「3 大受众」组织(消费者/企业/未来)——回答"我们为谁"
两者之间没有桥接,访客看不到"妥哒服务谁""阿布AI算不算未来",等于两个半体系。叠加以下问题,整体未成"体系":
1. 赛道失衡无逻辑支撑:数字支付仅 1 个产品(妥哒)显单薄,职场服务 4 个最重;缺一句"为什么是 4 条、为什么这样配比"。
2. 产品层信息单薄:每个产品只有"名称 + 一句营销语",无定位/目标用户/核心价值/生态位置。
3. 定位—赛道—受众之间没有"数字生活生态"结构图:业务页 CTA 写"共建数字生活生态",但生态长什么样从未可视化。
4. 边缘板块脱节:Innovation 页已重定向到 /business(半废弃悬空);Sustainability 的"数字普惠/绿色低碳/责任运营"是抽象概念,未与产品挂钩;Investors 是孤立 CTA。
5. 品牌命名待澄清 ~~(原疑点)~~ → **已澄清(2026-08)**:项目文件夹名"巴士伙伴官网"与 Mascot(车形狗)用"巴士",但**品牌名是"巴适伙伴"**(川渝方言"舒服/恰好")。"巴士"仅为文件夹命名遗留 + Mascot 玩梗,**不构成"出行/交通"业务维度**,体系图无需加入出行/交通赛道。
---
## 1. 一句话定位(L1 · 定位层)
- **巴适伙伴是**:聚焦数字生活服务的高科技企业,以"温暖科技"连接人、机会、价值与智能,共建数字生活生态。
- **为谁**:消费者 · 企业 · 未来(前沿)
- **创造什么价值**:让数字生活更**连接、更成长、更可信、更被理解**
> ✅ 已确认:"温暖科技""数字生活生态"作为长期品牌核心词沿用。
---
## 2. 数字生活的四个动作(L3 · 赛道逻辑 / "为什么是 4 条")
4 条赛道 = 数字生活的 4 个基本面,对应 4 个动词,自洽且可对外讲清"为什么是 4 条、为什么这样配比":
| 赛道 | 动作 | 关系 | 现有产品数 |
|---|---|---|---|
| 内容社交 | **连接** | 人与人 | 3(青核/音酷/蜜恋) |
| 职场服务 | **成长** | 人与机会 | 4(职来/看山/步途/跃升) |
| 数字支付 | **交易** | 人与价值 | 1(妥哒) |
| 人工智能 | **理解** | 人与智能 | 2(阿布AI/听愿) |
> ✅ 已确认配比解读:职场服务 4 个最重,因为"成长"是巴适当前最厚的能力沉淀;数字支付 1 个是"交易底座",重深度而非数量;人工智能 2 个是未来下注。
---
## 3. 三大受众(L2 · 受众层)
| 受众 | 定义 | 核心需求 |
|---|---|---|
| **消费者** | 个体的数字生活 | 社交 / 创作 / 求职 / 支付 / AI 陪伴 |
| **企业** | 组织的效率与增长 | 招聘 / 知识 / 培训 / 收付款 |
| **未来** | 前沿与平台层(长期下注) | 多模态 AI / 意图理解 / 基础设施 |
> ✅ 已确认:"未来"作为受众定义为"前沿/平台层"而非具体客户——成立。AI 赛道(阿布AI/听愿)即"未来"受众的落点。
---
## 4. 产品 × 受众矩阵(横切 · 每个产品有家)
打勾 = 该产品服务该受众。
| 产品 | 赛道 | 消费者 | 企业 | 未来 | 价值主张 | 生态角色 |
|---|---|:--:|:--:|:--:|---|---|
| 青核 | 内容社交 | ✓ | | | 让志同道合的人真诚相遇 | 连接引擎 |
| 音酷 | 内容社交 | ✓ | | | 让人人都能创作音乐 | 创作工具 |
| 蜜恋 | 内容社交 | ✓ | | | 理解情感的温暖社交 | 情感连接 |
| 职来 | 职场服务 | ✓ | ✓ | | 人岗智能匹配,成长路径可见 | 双边市场 |
| 看山 | 职场服务 | ✓ | ✓ | | 让知识触手可及 | 知识基础设施 |
| 步途 | 职场服务 | ✓ | | | 个人成长路径规划 | 成长工具 |
| 跃升 | 职场服务 | ✓ | ✓ | | 能力提升引擎,赋能跃迁 | 能力平台 |
| 妥哒 | 数字支付 | ✓ | ✓ | | 场景化支付,安全有温度 | 交易底座 |
| 阿布AI | 人工智能 | ✓ | | ✓ | 多模态生活助手,温暖每一天 | AI 入口/平台 |
| 听愿 | 人工智能 | ✓ | | ✓ | 听懂你的每一个愿望 | 意图理解引擎 |
> ✅ 已确认企业侧判断:4 个企业侧勾选(**职来 / 看山 / 跃升 / 妥哒**)成立,无需调整。
> - **职来、妥哒** = 天然双边市场(招聘必有雇主 B 端、支付必有商户 B 端)。
> - **看山**("知识基础设施")、**跃升**("能力平台/赋能跃迁")——"基础设施/平台"定位意味着既服务个体(C)也服务组织/供给方(B)。
> - 纯 C(青核/音酷/蜜恋/步途)不勾企业;阿布AI/听愿归"未来/平台层",**平台化后才转 B**,当前不勾企业。
> ✅ 已确认:每个产品价值主张一句话到位。
---
## 5. 数字生活生态全景图
```mermaid
graph TB
POS["巴适伙伴 · 定位<br/>聚焦数字生活服务 · 温暖科技<br/>共建数字生活生态"]
subgraph AUD["L2 受众层 · 为谁"]
A1["消费者 · 个体的数字生活"]
A2["企业 · 组织的效率与增长"]
A3["未来 · 前沿与平台层"]
end
subgraph TRK["L3 赛道层 · 造什么(数字生活四动作)"]
T1["内容社交 · 连接"]
T2["职场服务 · 成长"]
T3["数字支付 · 交易"]
T4["人工智能 · 理解"]
end
subgraph PROD["产品节点 · 连接点"]
P1["青核 / 音酷 / 蜜恋"]
P2["职来 / 看山 / 步途 / 跃升"]
P3["妥哒"]
P4["阿布AI / 听愿"]
end
ESG["ESG 责任层 · 环境 / 社会 / 治理(横切贯穿)"]
POS --> AUD
AUD --> TRK
TRK --> PROD
ESG -. 横切贯穿 .-> TRK
```
> 落地建议(待执行):About 页已有交互式中心辐射图 [`OrgNetwork.tsx`](src/components/OrgNetwork.tsx)。骨架既已确认,可在业务页做一张同款"生态全景图"(中心=巴适伙伴,外环=4赛道,节点=产品,受众作为切换视角),把首页3受众与业务页4赛道统一到一张图。
---
## 6. ESG × 产品挂钩(让 Sustainability 不再空泛)—— 建议待确认
把 Sustainability 页的抽象行动换成与具体产品的挂钩(建议,待你确认后执行):
| ESG 维度 | 现有抽象表述 | 产品挂钩(建议) |
|---|---|---|
| 环境 Environment | 绿色运营、降碳足迹 | 全线数字服务云化;重点阿布AI 的算力能效 |
| 社会 Social | 数字普惠、公益创新 | 妥哒(支付普惠)/ 看山(知识普惠)/ 步途·跃升(职业流动) |
| 治理 Governance | 数据合规、责任运营 | 阿布AI(AI 伦理)/ 妥哒(支付合规与数据安全) |
---
## 7. 边缘板块收口(骨架已确认,待执行)
- **Innovation 页**:已重定向到 /business。建议二选一——(a) 并入 Business 作为"研发理念 ICIDP"子段彻底删路由;(b) 保留为不可访问但清掉悬空数据。当前是悬空状态。
- **Sustainability 页**:按上文第 6 节把抽象行动换成产品挂钩。
- **Investors 页**:初创阶段,保留 IR 联系 CTA,与业务愿景做一句呼应即可,不堆指标。
---
## 8. 已确认的关键决策(清单)
> 原 6 项关键推断已于 2026-08 全部拍板。以下为定稿记录。
1. **L1 定位措辞** ✅ 沿用:"温暖科技""数字生活生态"作为长期品牌核心词。
2. **赛道四动作** ✅ 符合战略意图:连接(社交)/ 成长(职场)/ 交易(支付)/ 理解(AI)——这是"为什么是 4 条"的核心论据。
3. **"未来"受众定义** ✅ 成立:定义为"前沿/平台层"而非具体客户;AI 赛道为其落点。
4. **产品×受众打勾(企业侧)** ✅ 交助手判断——**确认 4 个企业侧产品**:职来 / 看山 / 跃升 / 妥哒(理由见第 4 节脚注);纯 C 产品不勾企业;阿布AI/听愿归未来/平台层。
5. **每个产品价值主张一句话** ✅ 准:第 4 节"价值主张"列为定稿。
6. **品牌名澄清** ✅ 无出行/交通维度:"巴适"=品牌(川渝方言"舒服/恰好");"巴士"仅见于项目文件夹名 + Mascot 车形狗玩梗,不构成业务赛道。体系图无需加入出行/交通。
---
## 9. 下一步(骨架既定,可推进的落地工作)
1. ✅ **首页↔业务页桥接(已完成 2026-08)**:构建交互式 [`EcosystemMap`](src/components/EcosystemMap.tsx) 组件,落在业务页 `#ecosystem` 区块。中心=巴适伙伴,双视角切换——「按赛道看」(4 赛道节点:连接/成长/交易/理解)与「按受众看」(3 受众节点:消费者/企业/未来),同一组 10 产品按已确认矩阵重组。含中心心跳、连接线能量流、节点轻浮、受众色点(C蓝/B绿/F紫)。首页 3 受众 pillars 已改为跳转 `/business#ecosystem`,直接落到全景图。浏览器实测通过(切换/节点/面板/控制台均正常)。
2. **产品层信息补全**:每个产品补定位/目标用户/核心价值/生态位置(第 4 节矩阵已是骨架;EcosystemMap 已展示价值主张一句 + 受众标记 + 状态)。
3. **边缘板块收口**:Innovation 二选一、Sustainability 按第 6 节挂钩产品、Investors 加一句愿景呼应。
4. **品牌名落地**:代码全程统一"巴适伙伴 / BaskBuddy";文件夹名"巴士伙伴官网"属遗留,可不改(不影响品牌)。
```

45
overview-color-icon-final.md

@ -1,45 +0,0 @@
# bashi-website — 色彩 & 图标 最终决策落地(像素君 / UI Designer)
用户拍板「听你的」,以下决策已直接执行并验证通过。
## 1. 赛道色统一(解决双套定义)
之前:赛道卡片用粉彩 `#E8A0BF/#7EC8E3/#98D8AA/#C9B1FF`,但全局 `--track-*` token 是饱和默认色 `#EF4444/#2563EB/#10B981/#8B5CF6` —— **色相一致,唯独「社交」一边粉、一边红**最扎眼。
改为:token 与卡片粉彩**同色相**,并加深以保证 WCAG AA(白底文字 ≥4.5:1;实心按钮白字 ≥4.5:1):
| 赛道 | 旧 token | 新 token | 对齐的卡片粉彩 |
|------|----------|----------|----------------|
| 社交 Social | `#EF4444` 红 | `#C2416B` 深玫瑰 | `#E8A0BF` 粉 |
| 职场 Career | `#2563EB` | `#1C7AB6` 深天蓝 | `#7EC8E3` 蓝 |
| 支付 Payment | `#10B981` | `#15803D` 深绿 | `#98D8AA` 绿 |
| AI | `#8B5CF6` | `#7C3AED` 深紫 | `#C9B1FF` 紫 |
- 顺手**删掉 4 个从未引用的 `--track-*-bright` 死 token**,设计系统更干净。
## 2. 图标全部统一为 lucide(单一图标源)
设计系统第一原则就是一致性。把所有手写内联 SVG 换成 lucide:
- **Header**:汉堡 ↔ 关闭 → `Menu` / `X`
- **Contact**:`💼`→`Briefcase`、`🤝`→`Handshake`、CTA 箭头→`ArrowRight`、邮件→`Mail`
- **NotFound**:返回箭头→`ArrowLeft`、前往箭头→`ArrowRight`
- **Join**:投递区残留的邮件 SVG → `Mail`
全站**已无任何 emoji**(上次 Join 福利区已改,本次清掉 Contact 两处 + Join 邮件图标)。
## 3. SPA fallback(生产环境深链接不 404)
新增 `public/_redirects`:`/* /index.html 200`(Netlify 及多数静态托管通用,对其它平台无害),已随构建进入 `dist/`。
> 若用其它托管,规则如下:
> - **nginx**:`try_files $uri $uri/ /index.html;`
> - **Vercel**:`vercel.json` → `{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }`
> - **GitHub Pages**:需用 `gh-pages` + 404.html 拷贝,或 hash router
## 验证
- `npx tsc --noEmit` → 0 错误
- `npm run build` → 1819 模块,新色已进入 `dist` CSS,`_redirects` 已进 `dist`
- `src/` 内 **0 emoji**
- 开发服务器在线 **http://localhost:4321**:`/` `/business` `/contact` `/join` `/en/contact` `/404` 全部 200,改动模块编译干净,lucide 依赖解析 200
## 仍可继续打磨(可选,未动)
- 字体层级/行高 token 化(目前散落在 class 里)
- 暗色主题目前由 `page-enter`/CSS 变量驱动,可补一套更完整的深色 token 校验
- 赛道卡片粉彩与全局 token 仍有「深浅两档」的语义区分,若想彻底单源,可把卡片色也改从 `--track-*` 派生(需动 i18n 数据)

38
overview-dark-mode-tokens.md

@ -1,38 +0,0 @@
# bashi-website — 设计系统完善 + 暗色模式落地
> 像素君(UI Designer)· 第二轮打磨:把"死"的暗色 token 真正激活,并补全设计系统刻度尺 + 赛道色单源。
## 1. 设计令牌补全(`global.css`)
- **字体刻度尺**:在 `:root` 新增模块化 token —— `--fs-xs…4xl`、`--lh-tight/snug/normal/relaxed`、`--fw-regular…bold`、`--ls-tight…widest`。设计系统从此有统一"刻度尺",组件可逐步迁移到令牌而非散值。
- **主题感知 surface 令牌**:`--surface-card` / `--text-strong` / `--header-bg` / `--accent`,浅色下指向既有值,暗色下自动切换。
## 2. 暗色模式(从摆设变成真功能)
之前 `--bg-dark` / `--text-on-dark` / `--shadow-dark-*` 都定义了,却**没有 `data-theme="dark"` 覆盖块、也没有切换开关**——等于死代码。本次:
- 新增 `[data-theme="dark"]` 覆盖块:暖调近黑表面(`--bg-primary:#16110E`、`--surface-card:#1E1813`)、暖白文字(`--text-primary:#F5EFE6`、`--text-strong:#FBF6EC`)、低透明度奶油描边、暗色阴影、`color-scheme: dark`。
- `body` 加 `background-color/color` 依赖令牌,整页随主题切换。
- **三态主题开关**:`src/hooks/useTheme.ts`(浅 / 深 / 跟随系统,localStorage 持久化 + 系统偏好实时监听)+ Header 顶栏按钮循环切换 `Sun / Moon / Monitor`(lucide 图标)。
- `index.html` 内联防闪烁脚本:首屏绘制前就设好 `data-theme` 与 `color-scheme`,**无 FOUC**。
## 3. 组件迁移到主题令牌
- 中性/深色底上的 `text-[var(--brand-black)]` → `text-[var(--text-strong)]`(覆盖 Home/Business/Innovation/Join/Contact/About/Footer/Privacy/NotFound/Header 的全部标题与导航文字)。
- `bg-white` 卡片 → `bg-[var(--surface-card)]`(Contact/Join/NotFound/Privacy)。
- 悬停底色 `hover:bg-[var(--brand-cream-light)]` → `hover:bg-[var(--accent)]`;静态奶油色高亮块(About/Join/Innovation 序号徽章、Contact Passion 卡)→ `bg-[var(--accent)]`。
- 英雄区/区块渐变遮罩:白底遮罩改为 `color-mix(in srgb, var(--bg-primary) NN%, transparent)`,奶油渐变改为 `var(--bg-secondary)`,均随主题淡入背景。
- Header 吸顶与移动菜单底色 → `var(--header-bg)`。
- **关键边界**:奶油实心按钮(Home 联系 CTA、Business CTA)保留 `text-[var(--brand-black)]`——浅奶油底配深字在两种主题都可读,若误改成浅字会在暗色下"浅字叠浅底"翻车(已修复一处该回归)。
## 4. 赛道色单源(`--track-*` 成为唯一来源)
- 新增 `src/lib/tracks.ts`:`TRACK_TOKEN` 把 social/career/payment/ai 映射到 `var(--track-*)`。
- `i18n` 的 `business.tracks[]` 加 `key` 字段、**删除**原先写死的粉彩 `color` 字段(即那套双源粉彩)。Home/Business 改用 `TRACK_TOKEN[track.key]`,卡片底色(`color-mix` 深色调)与圆点 accent 现在共享同一 token,永不漂移。
## 5. Footer 清理
- 残留 `📍` emoji → lucide `MapPin`;硬编码 `rgba(26,26,26,0.40)` 的标语文字 → `var(--text-muted)`(暗色下原本会不可见)。
## 验证
- `npx tsc --noEmit` → 0 错误;`npm run build` → 1821 模块;`[data-theme=dark]` 块与 `#16110e` 已进入 `dist` CSS;**全站 0 emoji**;所有路由 200;改动模块编译干净;主题接线(`baskbuddy-theme`、`data-theme`)在产物 JS 中可查。
- 说明:暗色**视觉**正确性经逐项推演确认(无 headless 浏览器截图);请点击顶栏 Sun/Moon/Monitor 按钮实际体验切换。
## 仍可继续打磨的方向(可选)
- 把组件里的 `text-[15px]` 等散值逐步替换成本轮新增的 `--fs-*` 刻度(目前令牌已就位,属渐进式重构)。
- 补一套更完整的暗色对比度走查清单(针对 `.gradient-mesh` / `.text-gradient-warm` 等渐变类在暗色下的表现)。
- 部署级:确认托管平台的 SPA fallback(已有 `public/_redirects`,nginx/Vercel 规则可补)。

47
overview-icon-color-audit.md

@ -1,47 +0,0 @@
# 色彩 & 图标审计与修复 — bashi-website
> 角色:像素君(UI Designer)|审计依据:Impeccable + UI/UX Pro Max 设计规范
## 已修复(明确违规 / 无障碍硬伤)
### 1. 图标:Emoji → 真实 SVG
- **Join.tsx 福利区** 原用 emoji `⏰🏠💪🎉📚`,违反「禁止 emoji 作图标」规范。
- 安装 **lucide-react 1.25.0**,替换为 `Clock / Home / Dumbbell / PartyPopper / BookOpen`,置于 `#brand-cream-light` 圆角方块中,与全站图标语言一致。
### 2. 图标:唤醒沉睡的 `icon` 数据
- `i18n/{zh,en}.ts` 中每个产品都带 `icon: 'heart'|'music'|…` 字段,但**从未渲染**。
- `Business.tsx` 新增 `productIcon` 映射,给每个产品芯片渲染真实 lucide 图标
(heart/music/sparkles/briefcase/mountain/footprints/rocket/check-circle→CircleCheck/bot/ear)。
### 3. 色彩:状态徽章对比度硬伤(WCAG AA 失败)
- `.status-developing` 原为 **奶油色文字 `#F5DEB3` 叠在近透明奶油底** → 白底上约 **1.3:1**,文字几乎不可见。
- 修复:`color-mix(... cream-deep 20% + bg-primary)` 底 + `#8A6A2F` 文字(≈ **4.9:1**,通过)。
- `.status-planned` 文字加深到 `#1D4ED8`,留足 AA 余量。
### 4. 色彩:暖白微调
- `--brand-white` 由 `#FFFFFF` → `#FCFBF8`,与奶油暖调主题更协调(纯白在「温暖未来主义」里偏冷)。
## 验证
- `npx tsc --noEmit` → 0 错误
- `npm run build` → 1819 模块(lucide 已打包)
- 开发服务器 `/business`、`/join` 模块编译通过,全站页面 200
## 待你拍板(未改,属品牌级决策)
### A. 赛道色存在「双套定义」不一致
| 位置 | 社交 | 职场 | 支付 | AI |
|------|------|------|------|-----|
| 赛道卡片(Home/Business/Innovation) | `#E8A0BF` 粉 | `#7EC8E3` 青 | `#98D8AA` 薄荷 | `#C9B1FF` 薰衣草 |
| 全局强调 token `--track-*` | `#EF4444` 红 | `#2563EB` 蓝 | `#10B981` 翠 | `#8B5CF6` 紫 |
- 卡片用的是柔和粉彩,强调 token 是 Tailwind 默认饱和色,**色相不一致**(尤其「社交」= 粉 vs 红)。
- **建议**:统一为一套色相(例如让全局强调 token 与粉彩共享色相,仅提高饱和度用于小面积强调)。
- 需要你确认方向后我再改——这是品牌视觉基调,不想擅自重绘。
### B. 手写内联 SVG
- `Header / Contact / NotFound` 用的是手写 24×24 描边 SVG,风格一致、无问题;可选统一换成 lucide 做单一图标源。
## 你需要告诉我的
1. 赛道色要不要统一?统一到「粉彩色相」还是维持饱和色相?
2. 是否要把手写 SVG 也换成 lucide(单一图标源)?
3. 是否需要我补充一个 **SPA fallback** 部署配置(nginx / vercel / netlify),否则生产环境深链接 404。

25
overview.md

@ -1,25 +0,0 @@
# bashi-website — Migrated to Pure React (Vite + React Router)
The site was migrated from **Astro 5** to a **pure React SPA** (Vite + React Router + Tailwind v4), so every page route is now a real `.tsx` file as requested.
## What changed
- **Removed**: all `.astro` files (`src/pages/*.astro`, `src/pages/en/*.astro`, `src/layouts/BaseLayout.astro`), `astro.config.mjs`, and `scripts/`. Zero Astro dependencies remain in `package.json`.
- **Added**: Vite + React Router toolchain, `src/main.tsx`, `src/App.tsx` (routes), `src/layout/RootLayout.tsx`, `src/hooks/useClientEffects.ts`, `src/seo/Seo.tsx`, and 8 `src/pages/*.tsx` route wrappers.
- **Reused unchanged**: `src/styles/global.css` (full BaskBuddy design system), all existing React components (`Home/About/Business/Contact/Innovation/Join/Privacy/NotFound/Header/Footer/SiteShell`), and the i18n catalogs (`zh.ts`/`en.ts`).
## Architecture
- `zh` at `/` & `/{page}`, `en` at `/en/{page}`, unknown paths → 404.
- `RootLayout` keeps `Header`/`Footer` mounted across navigations and replays the `page-enter` animation on routed content.
- `useClientEffects(pathname)` ports the interaction logic that previously lived in Astro's `<script>`: **flip-cards** (About ICIDP), **scroll-reveal** (staggered), **stat counters** (`.stat-value[data-target]`), and **copy-to-clipboard** (Contact). The dead `#star-field` generation was dropped.
- `Seo` recreates the per-page `<head>` (title, description, canonical, hreflang alternates, OG/Twitter cards, Organization JSON-LD) via direct DOM updates — faithful to the original Astro output.
- `Header` mobile menu now uses React `useState` instead of the old id-based global script toggle.
## Verification
- `npm run build` → 60 modules transformed, clean `dist/`.
- `npx tsc --noEmit` → 0 type errors.
- Dev server live at **http://localhost:4321** — `/`, `/about`, `/en/innovation`, `/en`, `/privacy` and all assets return HTTP 200 (SPA fallback works).
## Notes / tradeoffs
- **SPA SEO**: meta is set client-side (JS-executing crawlers are fine). For production, configure the host with an SPA fallback (Vite dev/preview already do this).
- A sandbox safe-delete guard blocks Vite from bulk-clearing `node_modules/.vite/deps`; resolved by redirecting Vite's `cacheDir` to the OS temp dir in `vite.config.ts`.
- Run: `npm run dev` (4321) · `npm run build` · `npm run preview`.

BIN
public/baskbuddy-logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 38 KiB

6
src/components/About.tsx

@ -67,7 +67,7 @@ export default function About({ locale }: { locale: Locale }) {
{/* ===== 全宽大图横幅(连接用户与生活) ===== */}
<section className="relative w-full h-[360px] md:h-[520px] scroll-reveal overflow-hidden">
<img src="/connect-life.jpg" alt="" className="w-full h-full object-cover" />
<img src="https://oss.baskbuddy.com/baskbuddy/connect-life.jpg" alt="" className="w-full h-full object-cover" />
<div className="pointer-events-none absolute inset-0 bg-[color-mix(in_oklch,var(--accent)_28%,transparent)]" />
<div className="absolute inset-0 flex flex-col items-center justify-center text-center px-4">
<h2 className="text-h1 md:text-h1 font-semibold text-white tracking-tight drop-shadow-sm">{a.banner.text}</h2>
@ -218,9 +218,9 @@ export default function About({ locale }: { locale: Locale }) {
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{a.subnav.team.members.map((m, i) => {
const defaultAvatar = m.gender === 'female'
? '/team-female.jpg'
? 'https://oss.baskbuddy.com/baskbuddy/team-female.jpg'
: m.gender === 'male'
? '/team-male.jpg'
? 'https://oss.baskbuddy.com/baskbuddy/team-male.jpg'
: null;
const avatar = m.photo || defaultAvatar;
return (

2
src/components/Business.tsx

@ -40,7 +40,7 @@ export default function Business({ locale }: { locale: Locale }) {
</div>
<div className="feature-media scroll-reveal-right h-[300px] md:h-[420px]">
<img
src="/innovation-hero.jpg"
src="https://oss.baskbuddy.com/baskbuddy/innovation-hero.jpg"
alt={locale === 'en' ? 'BaskBuddy Business Ecosystem' : '巴适伙伴业务生态'}
loading="eager"
decoding="async"

68
src/components/Contact.tsx

@ -1,6 +1,44 @@
import { getLocalizedPath, getTranslation, type Locale } from '../i18n/utils';
import { cssVars } from './css';
import { Icon, growthIncentiveIcons, longTermIncentiveIcons, benefitIcons } from '../icons';
import { useState } from 'react';
// 复制按钮(React 受控):绕开 useClientEffects 在 lazy chunk 加载后未重绑的时序坑。
// 失败时降级到 textarea+execCommand,最后给出 1.5s 的"已复制"反馈。
function CopyButton({ value, label, copied }: { value: string; label: string; copied: string }) {
const [done, setDone] = useState(false);
const onClick = async () => {
try {
await navigator.clipboard.writeText(value);
} catch {
const ta = document.createElement('textarea');
ta.value = value;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } catch { /* ignore */ }
document.body.removeChild(ta);
}
setDone(true);
window.setTimeout(() => setDone(false), 1500);
};
return (
<button
type="button"
onClick={onClick}
className="ml-auto shrink-0 px-2.5 py-1 rounded-md text-body font-semibold transition-colors duration-200 hover:border-[var(--brand-accent)]"
style={cssVars({
color: done ? '#fff' : 'var(--text-muted)',
background: done ? 'var(--brand-accent)' : 'transparent',
border: '1px solid var(--border-light)',
})}
aria-label={label}
>
{done ? copied : label}
</button>
);
}
export default function Contact({ locale }: { locale: Locale }) {
const t = getTranslation(locale);
@ -164,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)' })}>
<Icon name="briefcase" className="w-5 h-5" />
</div>
<h2 className="text-h1 font-[var(--font-display)] font-bold tracking-tight text-[var(--brand-black)]">{c.job.title}</h2>
<h2 className="text-h2 font-[var(--font-display)] font-bold tracking-tight text-[var(--brand-black)]">{c.job.title}</h2>
</div>
<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>
@ -176,17 +214,7 @@ export default function Contact({ locale }: { locale: Locale }) {
<div className="flex items-center gap-2 rounded-xl border border-[var(--border-light)] bg-[var(--bg-primary)] px-3 py-2.5">
<span className="text-body font-semibold tracking-[0.06em] uppercase text-[var(--text-muted)] mr-1 whitespace-nowrap" style={{ fontFamily: 'var(--font-body)' }}>{c.job.emailLabel}</span>
<a href={'mailto:' + c.job.email} className="text-body font-medium truncate" style={{ color: 'var(--accent)', fontFamily: 'var(--font-body)' }}>{c.job.email}</a>
<button
type="button"
className="copy-btn ml-auto shrink-0 text-body font-semibold px-2 py-1 rounded-md transition-colors duration-200"
data-copy={c.job.email}
data-label={c.job.copy}
data-copied={c.job.copied}
style={cssVars({ color: 'var(--text-muted)', border: '1px solid var(--border-light)' })}
aria-label={c.job.copy}
>
{c.job.copy}
</button>
<CopyButton value={c.job.email} label={c.job.copy} copied={c.job.copied} />
</div>
</div>
</div>
@ -201,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)' })}>
<Icon name="handshake" className="w-5 h-5" />
</div>
<h2 className="text-h1 font-[var(--font-display)] font-bold tracking-tight text-[var(--brand-black)]">{c.biz.title}</h2>
<h2 className="text-h2 font-[var(--font-display)] font-bold tracking-tight text-[var(--brand-black)]">{c.biz.title}</h2>
</div>
<p className="text-body leading-[1.7] text-[var(--text-secondary)] mb-6">{c.biz.desc}</p>
<div className="mt-auto space-y-4">
@ -212,17 +240,7 @@ export default function Contact({ locale }: { locale: Locale }) {
<div className="flex items-center gap-2 rounded-xl border border-[var(--border-light)] bg-[var(--bg-primary)] px-3 py-2.5">
<span className="text-body font-semibold tracking-[0.06em] uppercase text-[var(--text-muted)] mr-1 whitespace-nowrap" style={{ fontFamily: 'var(--font-body)' }}>{c.biz.emailLabel}</span>
<a href={'mailto:' + c.biz.email} className="text-body font-medium truncate" style={{ color: 'var(--accent)', fontFamily: 'var(--font-body)' }}>{c.biz.email}</a>
<button
type="button"
className="copy-btn ml-auto shrink-0 text-body font-semibold px-2 py-1 rounded-md transition-colors duration-200"
data-copy={c.biz.email}
data-label={c.biz.copy}
data-copied={c.biz.copied}
style={cssVars({ color: 'var(--text-muted)', border: '1px solid var(--border-light)' })}
aria-label={c.biz.copy}
>
{c.biz.copy}
</button>
<CopyButton value={c.biz.email} label={c.biz.copy} copied={c.biz.copied} />
</div>
</div>
</div>
@ -235,7 +253,7 @@ export default function Contact({ locale }: { locale: Locale }) {
<section className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 pb-[var(--space-8)]">
<div className="feature-media h-[260px] md:h-[340px] scroll-reveal relative">
<img
src="/office-warmth.jpg"
src="https://oss.baskbuddy.com/baskbuddy/office-warmth.jpg"
alt={locale === 'en' ? 'BaskBuddy office space' : '巴适伙伴的办公空间'}
loading="lazy"
decoding="async"

336
src/components/Footer.tsx

@ -1,157 +1,295 @@
import { getLocalizedPath, type Locale } from '../i18n/utils';
import { getTranslation } from '../i18n/utils';
import { Icon, trackIcons } from '../icons';
import { useState, useEffect } from 'react';
export default function Footer({ locale }: { locale: Locale }) {
const t = getTranslation(locale);
const f = t.footer;
const brandName = t.brand || 'BaskBuddy';
// ICP 备案正式编号(2026-07-28 工信部审核通过)
const icpNumber = '沪ICP备2026035550号';
const icpNumber = '沪ICP备2026035550号-2';
// 公安备案数据码(阿里云已下发,有效期至 2026-08-27,审核完成后替换为正式沪公网安备号)
const psbFilingCode = 'c7c31b218d433ab92abbf85ec20af2bd';
// 邮箱复制状态
const [copied, setCopied] = useState(false);
useEffect(() => {
if (!copied) return;
const timer = setTimeout(() => setCopied(false), 2000);
return () => clearTimeout(timer);
}, [copied]);
const handleCopyEmail = async () => {
try {
await navigator.clipboard.writeText(f.contact.email);
setCopied(true);
} catch (err) {
// Fallback: create hidden textarea
const ta = document.createElement('textarea');
ta.value = f.contact.email;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
setCopied(true);
} catch (e) {
// ignore
}
document.body.removeChild(ta);
}
};
// 三语辅助:英文用 en 文本,繁体用 hk 文本,简体用 zh 文本。
const zhText = (zh: string, hk: string, en: string) =>
locale === 'en' ? en : locale === 'hk' ? hk : zh;
return (
<footer className="bg-[var(--brand-black)] border-t border-[color-mix(in_oklch,var(--brand-white)_10%,transparent)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 md:py-16">
{/* Main Footer Grid */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 md:gap-12 mb-10 md:mb-14">
{/* Column 1: Brand */}
<div className="md:col-span-1">
<div className="flex items-center gap-3 mb-4">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-14 md:py-20">
{/* ========== Main Footer: 4-Column Grid ========== */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-12 gap-10 lg:gap-8 mb-12 lg:mb-16">
{/* ===== Column 1: Brand (4 cols on desktop) ===== */}
<div className="lg:col-span-4">
{/* Logo + Brand */}
<div className="flex items-center gap-3 mb-6">
<img
src="/baskbuddy-logo.png"
src="https://oss.baskbuddy.com/baskbuddy/baskbuddy-logo.png"
alt="BaskBuddy"
className="w-12 h-12 object-contain rounded-xl bg-white"
/>
<span className="font-semibold text-body text-[var(--brand)]" style={{ fontFamily: 'var(--font-body)' }}>
{brandName}
</span>
<div className="flex flex-col">
<span className="font-semibold text-body text-[var(--brand)] leading-tight" style={{ fontFamily: 'var(--font-body)' }}>
{brandName}
</span>
<span className="text-micro text-[color-mix(in_oklch,var(--brand-white)_50%,transparent)] mt-0.5" style={{ fontFamily: 'var(--font-body)' }}>
{t.brandSub || '巴适伙伴'}
</span>
</div>
</div>
<p className="text-body leading-relaxed mb-4 text-[color-mix(in_oklch,var(--brand-white)_60%,transparent)]">
{zhText(
'以温暖科技,连接美好生活。五大赛道全面覆盖数字生活服务生态。',
'以溫暖科技,連接美好生活。五大賽道全面覆蓋數位生活服務生態。',
'Connecting beautiful life with warm tech. Five tracks covering the digital life service ecosystem.',
)}
{/* Slogan */}
<h3 className="text-[28px] md:text-[32px] font-bold leading-tight mb-3 text-[var(--brand-white)]" style={{ fontFamily: 'var(--font-body)' }}>
{f.slogan}
</h3>
<p className="text-body leading-relaxed mb-6 text-[color-mix(in_oklch,var(--brand-white)_65%,transparent)]" style={{ fontFamily: 'var(--font-body)' }}>
{f.subtitle}
</p>
<div className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-micro font-semibold tracking-[0.06em] uppercase bg-[color-mix(in_oklch,var(--brand)_12%,transparent)] text-[color-mix(in_oklch,var(--brand)_92%,transparent)] border border-[color-mix(in_oklch,var(--brand)_22%,transparent)]" style={{ fontFamily: 'var(--font-body)' }}>
I·C·I·D·P
{/* ICIDP Culture Badge */}
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-[color-mix(in_oklch,var(--brand)_10%,transparent)] border border-[color-mix(in_oklch,var(--brand)_20%,transparent)]">
<span className="text-micro font-bold tracking-[0.12em] text-[var(--brand)]" style={{ fontFamily: 'var(--font-body)' }}>
I·C·I·D·P
</span>
<span className="w-px h-4 bg-[color-mix(in_oklch,var(--brand)_30%,transparent)]" />
<span className="text-micro tracking-wide text-[color-mix(in_oklch,var(--brand-white)_55%,transparent)]" style={{ fontFamily: 'var(--font-body)' }}>
{zhText('诚信·勇敢·创新·奉献·激情', '誠信·勇敢·創新·奉獻·激情', 'Integrity · Courage · Innovation · Devotion · Passion')}
</span>
</div>
</div>
{/* Column 2: Quick Links */}
<div>
<h4 className="text-micro font-semibold tracking-[0.08em] uppercase mb-4 text-[color-mix(in_oklch,var(--brand-white)_50%,transparent)]" style={{ fontFamily: 'var(--font-body)' }}>
{zhText('快速导航', '快速導航', 'QUICK LINKS')}
{/* ===== Column 2: Business Tracks (3 cols) ===== */}
<div className="lg:col-span-3">
<h4 className="text-micro font-bold tracking-[0.1em] uppercase mb-5 text-[color-mix(in_oklch,var(--brand-white)_45%,transparent)]" style={{ fontFamily: 'var(--font-body)' }}>
{f.columns.tracks}
</h4>
<div className="flex flex-col gap-3.5 leading-relaxed">
{Object.entries(t.nav).map(([key, label]) => (
<div className="grid grid-cols-2 gap-x-3 gap-y-3">
{Object.entries(t.home.tracks).map(([key, track]) => (
<a
key={key}
href={getLocalizedPath(`/${key === 'home' ? '' : key}`, locale)}
className="inline-flex items-center text-body transition-colors duration-200 text-[color-mix(in_oklch,var(--brand-white)_65%,transparent)] hover:text-[var(--brand)]"
href={getLocalizedPath('/business', locale)}
className="group flex items-center gap-2.5 px-3 py-2.5 rounded-xl transition-all duration-200 hover:bg-[color-mix(in_oklch,var(--brand)_10%,transparent)] border border-transparent hover:border-[color-mix(in_oklch,var(--brand)_20%,transparent)]"
title={track.desc}
>
{label}
<span className="flex items-center justify-center w-8 h-8 rounded-lg bg-[color-mix(in_oklch,var(--brand)_12%,transparent)] text-[var(--brand)] group-hover:bg-[var(--brand-accent)] group-hover:text-white transition-colors duration-200 shrink-0">
<Icon name={trackIcons[key]} className="w-4 h-4" />
</span>
<span className="text-body font-medium text-[color-mix(in_oklch,var(--brand-white)_75%,transparent)] group-hover:text-[var(--brand-white)] transition-colors duration-200" style={{ fontFamily: 'var(--font-body)' }}>
{track.name}
</span>
</a>
))}
</div>
</div>
{/* Column 3: Tracks */}
<div>
<h4 className="text-micro font-semibold tracking-[0.08em] uppercase mb-4 text-[color-mix(in_oklch,var(--brand-white)_50%,transparent)]" style={{ fontFamily: 'var(--font-body)' }}>
{zhText('业务赛道', '業務賽道', 'BUSINESS TRACKS')}
{/* ===== Column 3: Quick Navigate (2 cols) ===== */}
<div className="lg:col-span-2">
<h4 className="text-micro font-bold tracking-[0.1em] uppercase mb-5 text-[color-mix(in_oklch,var(--brand-white)_45%,transparent)]" style={{ fontFamily: 'var(--font-body)' }}>
{f.columns.navigate}
</h4>
<div className="flex flex-col gap-3.5 leading-relaxed">
{Object.entries(t.home.tracks).map(([key, track]) => (
<a key={key} href="/business" className="inline-flex items-center gap-2 text-body transition-colors duration-200 text-[color-mix(in_oklch,var(--brand-white)_65%,transparent)] hover:text-[var(--brand)]">
<Icon name={trackIcons[key]} className="w-4 h-4" />
{track.name}
</a>
<ul className="flex flex-col gap-3">
{Object.entries(t.nav).map(([key, label]) => (
<li key={key}>
<a
href={getLocalizedPath(`/${key === 'home' ? '' : key}`, locale)}
className="inline-flex items-center text-body transition-colors duration-200 text-[color-mix(in_oklch,var(--brand-white)_70%,transparent)] hover:text-[var(--brand)] hover:pl-1"
style={{ fontFamily: 'var(--font-body)' }}
>
{label}
</a>
</li>
))}
</div>
</ul>
</div>
{/* Column 4: Contact */}
<div>
<h4 className="text-micro font-semibold tracking-[0.08em] uppercase mb-4 text-[color-mix(in_oklch,var(--brand-white)_50%,transparent)]" style={{ fontFamily: 'var(--font-body)' }}>
{zhText('联系我们', '聯繫我們', 'CONTACT')}
{/* ===== Column 4: Contact (3 cols) ===== */}
<div className="lg:col-span-3">
<h4 className="text-micro font-bold tracking-[0.1em] uppercase mb-5 text-[color-mix(in_oklch,var(--brand-white)_45%,transparent)]" style={{ fontFamily: 'var(--font-body)' }}>
{f.columns.contact}
</h4>
<div className="flex flex-col gap-3.5 leading-relaxed">
<a href="/contact" className="text-body transition-colors duration-200 text-[color-mix(in_oklch,var(--brand-white)_65%,transparent)] hover:text-[var(--brand)]">
{zhText('求职咨询', '求職諮詢', 'Job Inquiry')}
<div className="flex flex-col gap-4">
{/* Careers */}
<a href={getLocalizedPath('/contact', locale)} className="group block p-4 rounded-xl bg-[color-mix(in_oklch,var(--brand-white)_4%,transparent)] border border-[color-mix(in_oklch,var(--brand-white)_8%,transparent)] hover:border-[color-mix(in_oklch,var(--brand)_30%,transparent)] transition-all duration-200">
<div className="flex items-center justify-between mb-1">
<span className="text-body font-semibold text-[var(--brand-white)] group-hover:text-[var(--brand)] transition-colors duration-200" style={{ fontFamily: 'var(--font-body)' }}>
{f.contact.jobs.label}
</span>
<Icon name="arrowUpRight" className="w-4 h-4 text-[color-mix(in_oklch,var(--brand-white)_40%,transparent)] group-hover:text-[var(--brand)] transition-colors duration-200" />
</div>
<p className="text-micro text-[color-mix(in_oklch,var(--brand-white)_50%,transparent)]" style={{ fontFamily: 'var(--font-body)' }}>
{f.contact.jobs.hint}
</p>
</a>
<a href="/contact" className="text-body transition-colors duration-200 text-[color-mix(in_oklch,var(--brand-white)_65%,transparent)] hover:text-[var(--brand)]">
{zhText('商务合作', '商務合作', 'Business Cooperation')}
{/* Business */}
<a href={getLocalizedPath('/contact', locale)} className="group block p-4 rounded-xl bg-[color-mix(in_oklch,var(--brand-white)_4%,transparent)] border border-[color-mix(in_oklch,var(--brand-white)_8%,transparent)] hover:border-[color-mix(in_oklch,var(--brand)_30%,transparent)] transition-all duration-200">
<div className="flex items-center justify-between mb-1">
<span className="text-body font-semibold text-[var(--brand-white)] group-hover:text-[var(--brand)] transition-colors duration-200" style={{ fontFamily: 'var(--font-body)' }}>
{f.contact.biz.label}
</span>
<Icon name="arrowUpRight" className="w-4 h-4 text-[color-mix(in_oklch,var(--brand-white)_40%,transparent)] group-hover:text-[var(--brand)] transition-colors duration-200" />
</div>
<p className="text-micro text-[color-mix(in_oklch,var(--brand-white)_50%,transparent)]" style={{ fontFamily: 'var(--font-body)' }}>
{f.contact.biz.hint}
</p>
</a>
{/* Email: Click to mail, text selectable, 1-click copy with feedback */}
<div className="pt-1">
<p className="text-micro text-[color-mix(in_oklch,var(--brand-white)_45%,transparent)] mb-2" style={{ fontFamily: 'var(--font-body)' }}>
{zhText('统一联系邮箱(求职 / 商务)', '統一聯繫郵箱(求職 / 商務)', 'Unified Contact Email')}
</p>
<div className="group inline-flex items-center gap-1 px-3 py-2 rounded-xl bg-[color-mix(in_oklch,var(--brand-white)_4%,transparent)] border border-[color-mix(in_oklch,var(--brand-white)_8%,transparent)] hover:border-[color-mix(in_oklch,var(--brand)_30%,transparent)] transition-all duration-200 w-full">
<Icon name="mail" className="w-4 h-4 shrink-0 text-[var(--brand)]" />
<a
href={`mailto:${f.contact.email}`}
className="text-body font-medium text-[var(--brand-white)] hover:text-[var(--brand)] transition-colors duration-200 select-all outline-none"
style={{ fontFamily: 'var(--font-body)', userSelect: 'all' }}
onClick={(e) => e.stopPropagation()}
title={zhText('点击发邮件 · 文本可直接选中复制', '點擊發郵件 · 文本可直接選中複製', 'Click to email · Text is selectable')}
>
{f.contact.email}
</a>
<div className="ml-auto relative">
<button
type="button"
onClick={handleCopyEmail}
className="flex items-center justify-center w-8 h-8 rounded-lg transition-all duration-200 text-[color-mix(in_oklch,var(--brand-white)_55%,transparent)] hover:bg-[var(--brand-accent)] hover:text-white focus:outline-none focus:ring-2 focus:ring-[var(--brand-accent)]/50"
title={copied
? zhText('已复制!', '已複製!', 'Copied!')
: zhText('一键复制邮箱', '一鍵複製郵箱', 'Copy email')}
aria-label={zhText('复制邮箱地址', '複製郵箱地址', 'Copy email address')}
>
<Icon
name={copied ? 'check' : 'copy'}
className={`w-[18px] h-[18px] transition-all duration-200 ${copied ? 'text-[var(--sus-green)] scale-110' : ''}`}
/>
</button>
{/* Tooltip */}
<span
className={`absolute -top-10 left-1/2 -translate-x-1/2 px-2.5 py-1 rounded-lg text-micro whitespace-nowrap transition-all duration-200 pointer-events-none ${
copied
? 'opacity-100 -translate-y-1 bg-[var(--sus-green)] text-white shadow-lg'
: 'opacity-0 translate-y-1 bg-[color-mix(in_oklch,var(--brand-white)_92%,var(--brand-black))] text-[var(--brand-black)] group-hover:opacity-100 group-hover:-translate-y-1'
}`}
style={{ fontFamily: 'var(--font-body)' }}
>
{copied
? zhText('✓ 已复制', '✓ 已複製', '✓ Copied')
: zhText('点击复制', '點擊複製', 'Click to copy')}
</span>
</div>
</div>
</div>
{/* Address */}
<p className="flex items-start gap-2 text-body pt-1 text-[color-mix(in_oklch,var(--brand-white)_55%,transparent)]" style={{ fontFamily: 'var(--font-body)' }}>
<Icon name="mapPin" className="w-4 h-4 shrink-0 mt-0.5" />
<span className="leading-relaxed">{f.contact.address}</span>
</p>
</div>
</div>
</div>
{/* ========== Bottom Bar ========== */}
<div className="border-t border-[color-mix(in_oklch,var(--brand-white)_10%,transparent)] pt-6 md:pt-7">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
{/* Left: Copyright */}
<div className="flex flex-col md:flex-row md:items-center gap-3 md:gap-4">
<p className="text-micro text-[color-mix(in_oklch,var(--brand-white)_55%,transparent)]" style={{ fontFamily: 'var(--font-body)' }}>
{f.copyright}
</p>
<span className="hidden md:inline w-px h-4 bg-[color-mix(in_oklch,var(--brand-white)_15%,transparent)]" aria-hidden="true" />
<p className="text-micro font-semibold tracking-[0.08em] uppercase text-[color-mix(in_oklch,var(--brand-white)_35%,transparent)] md:hidden" style={{ fontFamily: 'var(--font-body)' }} aria-hidden="true">
{f.icidpFull}
</p>
</div>
{/* Center: Legal Links */}
<div className="flex flex-wrap items-center justify-start md:justify-center gap-x-3 gap-y-2">
{/* ICP */}
<a
href="mailto:baskbuddy@163.com"
className="text-body font-medium transition-all duration-200 hover:underline underline-offset-2 text-[var(--brand)] hover:text-[var(--brand-white)]"
href="https://beian.miit.gov.cn/"
target="_blank"
rel="noopener noreferrer"
className="group inline-flex items-center text-micro transition-all duration-200 text-[color-mix(in_oklch,var(--brand-white)_50%,transparent)] hover:text-[var(--brand)]"
title={zhText('工信部ICP/IP地址/域名信息备案管理系统', '工信部ICP/IP地址/域名信息備案管理系統', 'MIIT ICP Filing Query (China)')}
style={{ fontFamily: 'var(--font-body)' }}
title={zhText('点击发送邮件 (求职)', '點擊發送郵件 (求職)', 'Click to send email (Jobs)')}
>
baskbuddy@163.com
<Icon name="shieldCheck" className="w-3.5 h-3.5 mr-1.5 opacity-70 group-hover:opacity-100" />
{icpNumber}
</a>
<span className="text-[color-mix(in_oklch,var(--brand-white)_18%,transparent)]" aria-hidden="true">·</span>
{/* PSB */}
<a
href="mailto:baskbuddy@163.com"
className="text-micro transition-all duration-200 hover:underline underline-offset-2 text-[color-mix(in_oklch,var(--brand-white)_50%,transparent)] hover:text-[color-mix(in_oklch,var(--brand-white)_85%,transparent)]"
href="https://beian.mps.gov.cn/"
target="_blank"
rel="noopener noreferrer"
className="group inline-flex items-center text-micro transition-all duration-200 text-[color-mix(in_oklch,var(--brand-white)_50%,transparent)] hover:text-[var(--brand)]"
title={zhText('全国互联网安全管理服务平台 — 公安备案', '全國互聯網安全管理服務平台 — 公安備案', 'MPS Public Security Internet Filing (China)')}
style={{ fontFamily: 'var(--font-body)' }}
title={zhText('点击发送邮件 (商务)', '點擊發送郵件 (商務)', 'Click to send email (Business)')}
>
baskbuddy@163.com
<Icon name="shieldCheck" className="w-3.5 h-3.5 mr-1.5 opacity-70 group-hover:opacity-100" />
{zhText(
`公安备案数据码 ${psbFilingCode}`,
`公安備案數據碼 ${psbFilingCode}`,
`PSB Filing Code ${psbFilingCode}`,
)}
</a>
<span className="text-[color-mix(in_oklch,var(--brand-white)_18%,transparent)]" aria-hidden="true">·</span>
{/* Privacy */}
<a
href={getLocalizedPath('/privacy', locale)}
className="text-micro transition-all duration-200 text-[color-mix(in_oklch,var(--brand-white)_50%,transparent)] hover:text-[var(--brand)]"
style={{ fontFamily: 'var(--font-body)' }}
>
{f.legal.privacy}
</a>
<p className="flex items-start gap-1 text-micro text-[color-mix(in_oklch,var(--brand-white)_50%,transparent)]" title={zhText('巴适伙伴(上海)科技有限公司 办公地址', '巴適夥伴(上海)科技有限公司 辦公地址', 'BaskBuddy (Shanghai) Office')}>
<Icon name="mapPin" className="w-3.5 h-3.5 shrink-0 mt-0.5" />
<span>{zhText('中国 · 上海市静安区康宁路288弄2号410室', '中國 · 上海市靜安區康寧路288弄2號410室', "Room 410, No.2, Lane 288, Kangning Rd, Jing'an District, Shanghai, China")}</span>
</p>
</div>
</div>
</div>
{/* Bottom Bar */}
<div className="border-t border-[color-mix(in_oklch,var(--brand-white)_10%,transparent)] pt-6 flex flex-col md:flex-row items-center justify-between gap-5">
<div className="flex flex-col md:flex-row items-center gap-3 md:gap-4 text-center md:text-left">
<p className="text-micro text-[color-mix(in_oklch,var(--brand-white)_50%,transparent)]">
{zhText(
`© 2026 巴适伙伴(上海)科技有限公司. 保留所有权利`,
`© 2026 巴適夥伴(上海)科技有限公司. 保留所有權利`,
`© 2026 BaskBuddy (Shanghai) Technology Co., Ltd. All rights reserved.`,
)}
{/* Desktop-only ICIDP tagline */}
<div className="hidden md:flex mt-5 pt-5 border-t border-[color-mix(in_oklch,var(--brand-white)_6%,transparent)] justify-center">
<p className="text-micro font-semibold tracking-[0.2em] uppercase text-[color-mix(in_oklch,var(--brand-white)_30%,transparent)]" style={{ fontFamily: 'var(--font-body)' }} aria-hidden="true">
{f.icidpFull}
</p>
<a
href="https://beian.miit.gov.cn/"
target="_blank"
rel="noopener noreferrer"
className="text-micro transition-all duration-200 hover:underline underline-offset-2 text-[color-mix(in_oklch,var(--brand-white)_50%,transparent)] hover:text-[var(--brand)]"
title={zhText('工信部ICP/IP地址/域名信息备案管理系统', '工信部ICP/IP地址/域名信息備案管理系統', 'MIIT ICP Filing Query (China)')}
>
{icpNumber}
</a>
<a
href="https://beian.mps.gov.cn/"
target="_blank"
rel="noopener noreferrer"
className="text-micro transition-all duration-200 hover:underline underline-offset-2 text-[color-mix(in_oklch,var(--brand-white)_50%,transparent)] hover:text-[var(--brand)] inline-flex items-center gap-1"
title={zhText('全国互联网安全管理服务平台 — 公安备案', '全國互聯網安全管理服務平台 — 公安備案', 'MPS Public Security Internet Filing (China)')}
>
<Icon name="shieldCheck" className="w-3.5 h-3.5" />
{zhText(
`公安备案数据码 ${psbFilingCode}`,
`公安備案數據碼 ${psbFilingCode}`,
`PSB Filing Code ${psbFilingCode}`,
)}
</a>
<a href="/privacy" className="text-micro transition-all duration-200 hover:underline underline-offset-2 text-[color-mix(in_oklch,var(--brand-white)_50%,transparent)] hover:text-[var(--brand)]">
{zhText('隐私政策', '隱私政策', 'Privacy')}
</a>
</div>
<p className="text-micro font-semibold tracking-wide text-[color-mix(in_oklch,var(--brand-white)_45%,transparent)]" style={{ fontFamily: 'var(--font-body)' }} aria-hidden="true">
Integrity · Courage · Innovation · Devotion · Passion
</p>
</div>
</div>
</footer>

2
src/components/Home.tsx

@ -231,7 +231,7 @@ export default function Home({ locale }: { locale: Locale }) {
>
{/* 背景图:玻璃球/水滴主题图,原图直接展示,不叠加任何蒙层/粒子/光晕/星芒 */}
<img
src="/hero-bg.jpg"
src="https://oss.baskbuddy.com/baskbuddy/hero-bg.jpg"
alt=""
aria-hidden="true"
className="absolute inset-0 w-full h-full object-cover"

32
src/i18n/en.ts

@ -49,9 +49,9 @@ export const en = {
title: 'Three Pillars',
subtitle: 'Connecting everyday consumption, business growth, and the imagination of the future with technology',
items: [
{ name: 'For Consumers', desc: 'Connect daily life, making digital living warmer and more convenient', icon: 'users', img: '/pillar-consumer.jpg' },
{ name: 'For Business', desc: 'Connect business strategy with digital delivery, powering efficient growth', icon: 'briefcase', img: '/pillar-enterprise.jpg' },
{ name: 'For the Future', desc: 'Develop technology that truly matters, exploring tech for good', icon: 'bot', img: '/pillar-future.jpg' },
{ name: 'For Consumers', desc: 'Connect daily life, making digital living warmer and more convenient', icon: 'users', img: 'https://oss.baskbuddy.com/baskbuddy/pillar-consumer.jpg' },
{ name: 'For Business', desc: 'Connect business strategy with digital delivery, powering efficient growth', icon: 'briefcase', img: 'https://oss.baskbuddy.com/baskbuddy/pillar-enterprise.jpg' },
{ name: 'For the Future', desc: 'Develop technology that truly matters, exploring tech for good', icon: 'bot', img: 'https://oss.baskbuddy.com/baskbuddy/pillar-future.jpg' },
],
},
dynamics: {
@ -236,9 +236,9 @@ Let's go — let's journey toward a beautiful future together with BaskBuddy!`,
title: 'A group of young people, making BaskBuddy real',
subtitle: 'Brainstorms, pair programming, celebrating every small win — striving here has warmth.',
items: [
{ img: '/team-brainstorm.jpg', caption: 'Ideas colliding at the whiteboard' },
{ img: '/team-pairing.jpg', caption: 'Pair programming — ideas into code' },
{ img: '/team-celebrate.jpg', caption: 'Every small win deserves a toast' },
{ img: 'https://oss.baskbuddy.com/baskbuddy/team-brainstorm.jpg', caption: 'Ideas colliding at the whiteboard' },
{ img: 'https://oss.baskbuddy.com/baskbuddy/team-pairing.jpg', caption: 'Pair programming — ideas into code' },
{ img: 'https://oss.baskbuddy.com/baskbuddy/team-celebrate.jpg', caption: 'Every small win deserves a toast' },
],
},
growthIncentive: {
@ -397,4 +397,24 @@ Let's go — let's journey toward a beautiful future together with BaskBuddy!`,
contactPage: 'Visit Contact page →',
},
},
footer: {
slogan: 'BaskBuddy · Together for Life',
subtitle: 'Warm Tech, Connecting a Better Life',
columns: {
tracks: 'BUSINESS TRACKS',
navigate: 'QUICK NAV',
contact: 'CONTACT US',
},
contact: {
jobs: { label: 'Careers', hint: 'Join mydreamjob · Peaksight teams' },
biz: { label: 'Business', hint: 'Partnership · Co-marketing' },
email: 'baskbuddy@163.com',
address: 'Room 410, No.2, Ln.288, Kangning Rd, Jing\'an, Shanghai, China',
},
legal: {
privacy: 'Privacy Policy',
},
copyright: `© ${new Date().getFullYear()} Bashi Partners (Shanghai) Technology Co., Ltd.`,
icidpFull: 'Integrity · Courage · Innovation · Devotion · Passion',
},
};

32
src/i18n/hk.ts

@ -49,9 +49,9 @@ export const hk = {
title: '三大支柱',
subtitle: '以科技連接消費的日常、企業的增長與未來的想像',
items: [
{ name: '面向消費者', desc: '連接用戶日常,讓數位生活更溫暖、更便捷', icon: 'users', img: '/pillar-consumer.jpg' },
{ name: '面向企業', desc: '連接商業戰略與數位交付,助力企業高效增長', icon: 'briefcase', img: '/pillar-enterprise.jpg' },
{ name: '面向未來', desc: '研發真正重要的技術,探索科技向善的可能', icon: 'bot', img: '/pillar-future.jpg' },
{ name: '面向消費者', desc: '連接用戶日常,讓數位生活更溫暖、更便捷', icon: 'users', img: 'https://oss.baskbuddy.com/baskbuddy/pillar-consumer.jpg' },
{ name: '面向企業', desc: '連接商業戰略與數位交付,助力企業高效增長', icon: 'briefcase', img: 'https://oss.baskbuddy.com/baskbuddy/pillar-enterprise.jpg' },
{ name: '面向未來', desc: '研發真正重要的技術,探索科技向善的可能', icon: 'bot', img: 'https://oss.baskbuddy.com/baskbuddy/pillar-future.jpg' },
],
},
dynamics: {
@ -236,9 +236,9 @@ export const hk = {
title: '一群年輕人,正把巴適做成現實',
subtitle: '腦力激盪、結對編程、慶祝每一次小小的勝利——奮鬥在這裡是有溫度的。',
items: [
{ img: '/team-brainstorm.jpg', caption: '白板前的靈感碰撞' },
{ img: '/team-pairing.jpg', caption: '結對編程,把想法落到程式碼' },
{ img: '/team-celebrate.jpg', caption: '每一個小勝利,都值得舉杯' },
{ img: 'https://oss.baskbuddy.com/baskbuddy/team-brainstorm.jpg', caption: '白板前的靈感碰撞' },
{ img: 'https://oss.baskbuddy.com/baskbuddy/team-pairing.jpg', caption: '結對編程,把想法落到程式碼' },
{ img: 'https://oss.baskbuddy.com/baskbuddy/team-celebrate.jpg', caption: '每一個小勝利,都值得舉杯' },
],
},
growthIncentive: {
@ -397,4 +397,24 @@ export const hk = {
contactPage: '先看聯繫我們 →',
},
},
footer: {
slogan: '巴適夥伴,一生相伴',
subtitle: '以溫暖科技,連接美好生活',
columns: {
tracks: '業務賽道',
navigate: '快速導航',
contact: '聯繫我們',
},
contact: {
jobs: { label: '求職諮詢', hint: '職來 · 看山團隊直招' },
biz: { label: '商務合作', hint: '產品合作 · 市場聯動' },
email: 'baskbuddy@163.com',
address: '中國 · 上海市靜安區康寧路288弄2號410室',
},
legal: {
privacy: '隱私政策',
},
copyright: `© ${new Date().getFullYear()} 巴適夥伴(上海)科技有限公司`,
icidpFull: 'Integrity · Courage · Innovation · Devotion · Passion',
},
};

32
src/i18n/zh.ts

@ -49,9 +49,9 @@ export const zh = {
title: '三大支柱',
subtitle: '以科技连接消费的日常、企业的增长与未来的想象',
items: [
{ name: '面向消费者', desc: '连接用户日常,让数字生活更温暖、更便捷', icon: 'users', img: '/pillar-consumer.jpg' },
{ name: '面向企业', desc: '连接商业战略与数字交付,助力企业高效增长', icon: 'briefcase', img: '/pillar-enterprise.jpg' },
{ name: '面向未来', desc: '研发真正重要的技术,探索科技向善的可能', icon: 'bot', img: '/pillar-future.jpg' },
{ name: '面向消费者', desc: '连接用户日常,让数字生活更温暖、更便捷', icon: 'users', img: 'https://oss.baskbuddy.com/baskbuddy/pillar-consumer.jpg' },
{ name: '面向企业', desc: '连接商业战略与数字交付,助力企业高效增长', icon: 'briefcase', img: 'https://oss.baskbuddy.com/baskbuddy/pillar-enterprise.jpg' },
{ name: '面向未来', desc: '研发真正重要的技术,探索科技向善的可能', icon: 'bot', img: 'https://oss.baskbuddy.com/baskbuddy/pillar-future.jpg' },
],
},
dynamics: {
@ -236,9 +236,9 @@ export const zh = {
title: '一群年轻人,正把巴适做成现实',
subtitle: '头脑风暴、结对编程、庆祝每一次小小的胜利——奋斗在这里是有温度的。',
items: [
{ img: '/team-brainstorm.jpg', caption: '白板前的灵感碰撞' },
{ img: '/team-pairing.jpg', caption: '结对编程,把想法落到代码' },
{ img: '/team-celebrate.jpg', caption: '每一个小胜利,都值得举杯' },
{ img: 'https://oss.baskbuddy.com/baskbuddy/team-brainstorm.jpg', caption: '白板前的灵感碰撞' },
{ img: 'https://oss.baskbuddy.com/baskbuddy/team-pairing.jpg', caption: '结对编程,把想法落到代码' },
{ img: 'https://oss.baskbuddy.com/baskbuddy/team-celebrate.jpg', caption: '每一个小胜利,都值得举杯' },
],
},
growthIncentive: {
@ -397,4 +397,24 @@ export const zh = {
contactPage: '先看联系我们 →',
},
},
footer: {
slogan: '巴适伙伴,一生相伴',
subtitle: '以温暖科技,连接美好生活',
columns: {
tracks: '业务赛道',
navigate: '快速导航',
contact: '联系我们',
},
contact: {
jobs: { label: '求职咨询', hint: '职来 · 看山团队直招' },
biz: { label: '商务合作', hint: '产品合作 · 市场联动' },
email: 'baskbuddy@163.com',
address: '中国 · 上海市静安区康宁路288弄2号410室',
},
legal: {
privacy: '隐私政策',
},
copyright: `© ${new Date().getFullYear()} 巴适伙伴(上海)科技有限公司`,
icidpFull: 'Integrity · Courage · Innovation · Devotion · Passion',
},
};

4
src/icons/index.tsx

@ -56,6 +56,8 @@ import {
HeartPulse,
Leaf,
FileText,
Copy,
Check,
} from 'lucide-react';
export const iconMap = {
@ -111,6 +113,8 @@ export const iconMap = {
heartPulse: HeartPulse,
leaf: Leaf,
fileText: FileText,
copy: Copy,
check: Check,
} as const;
export type IconName = keyof typeof iconMap;

2
src/pages/AboutPage.tsx

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

2
src/pages/BusinessPage.tsx

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

2
src/pages/ContactPage.tsx

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

2
src/pages/InvestorsPage.tsx

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

2
src/pages/NewsPage.tsx

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

2
src/pages/SustainabilityPage.tsx

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

7
src/placeholder.ts

@ -1,6 +1,5 @@
// 统一占位图:全站内容图(办公/团队/业务/新闻/横幅等)共用此图。
// 后期设计给真实图片后:把图存为 public/placeholder.jpg,
// 并将本常量改为 '/placeholder.jpg' 即可全局生效(一处改,全站换)。
// 若远程 URL 因防盗链无法显示,本地存图后改成本地路径即可。
// 图片统一托管于 OSS:https://oss.baskbuddy.com/baskbuddy/
// 若要替换占位图,把对应文件上传到 OSS 后改本常量一处即可全局生效。
export const PLACEHOLDER_IMG =
'https://ts3.tc.mm.bing.net/th/id/OIP-C.IuNuzsR9jKSqu2-VTCmNOAHaE8?r=0&rs=1&pid=ImgDetMain&o=7&rm=3';
'https://oss.baskbuddy.com/baskbuddy/placeholder.jpg';

10
src/seo/Seo.tsx

@ -6,7 +6,7 @@ interface SeoProps {
description?: string;
keywords?: string;
locale: Locale;
/** Per-page og:image override (default: /og-default.png) */
/** Per-page og:image override (default: OSS og-default.png) */
ogImageOverride?: string;
}
@ -66,9 +66,9 @@ export default function Seo({ title, description, keywords, locale, ogImageOverr
const enAlternate = origin + (base === '/' ? '/en' : `/en${base}`);
const hkAlternate = origin + (base === '/' ? '/hk' : `/hk${base}`);
const logoPath = '/baskbuddy-logo.png';
const ogImagePath = ogImageOverride || '/og-default.png';
const ogImage = origin + ogImagePath;
const logoPath = 'https://oss.baskbuddy.com/baskbuddy/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.documentElement.lang = locale === 'en' ? 'en' : locale === 'hk' ? 'zh-HK' : 'zh-CN';
@ -121,7 +121,7 @@ export default function Seo({ title, description, keywords, locale, ogImageOverr
name: orgName,
alternateName: brandName,
url: origin + '/',
logo: origin + logoPath,
logo: logoPath.startsWith('http') ? logoPath : origin + logoPath,
email: 'baskbuddy@163.com',
address: {
'@type': 'PostalAddress',

69
src/styles/global.css

@ -1908,17 +1908,18 @@ footer a {
);
}
/* ===== 雾化光晕 orb:超大 blur 圆,慢速漂浮 ===== */
/* ===== 雾化光晕 orb:超大 blur 圆,静止不漂(浅底可见版:透明度×2.2) ===== */
.hero-glow-orb {
position: absolute;
left: var(--orb-x);
top: var(--orb-y);
width: var(--orb-sz);
height: var(--orb-sz);
width: calc(var(--orb-sz) * 1.15); /* 尺寸 ×1.15,浅底上更有存在感 */
height: calc(var(--orb-sz) * 1.15);
border-radius: 9999px;
background: radial-gradient(circle, var(--orb-color) 0%, transparent 72%);
filter: blur(28px);
opacity: 0.85;
background: radial-gradient(circle, var(--orb-color) 0%, transparent 80%);
filter: blur(32px); /* 原 28px → 略增大,扩散感更强 */
opacity: 1.0; /* 原 0.85 → ×1.18,完全可见 */
mix-blend-mode: multiply;/* 浅底叠加更柔和,不突兀 */
animation-duration: 18s;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
@ -1974,35 +1975,51 @@ footer a {
}
.hero-orbit-dot.dot-white::before {
background:
radial-gradient(circle at 50% 50%, rgba(255,255,255,0.0) 30%, rgba(255,255,255,0.45) 42%, rgba(255,255,255,0.0) 52%),
linear-gradient(to right, transparent 48%, rgba(255,255,255,0.35) 50%, transparent 52%),
linear-gradient(to bottom, transparent 48%, rgba(255,255,255,0.35) 50%, transparent 52%),
linear-gradient(45deg, transparent 48%, rgba(255,255,255,0.22) 50%, transparent 52%),
linear-gradient(-45deg, transparent 48%, rgba(255,255,255,0.22) 50%, transparent 52%);
radial-gradient(circle at 50% 50%, rgba(255,255,255,0.0) 30%, rgba(255,255,255,0.60) 42%, rgba(255,255,255,0.0) 54%),
linear-gradient(to right, transparent 48%, rgba(255,255,255,0.48) 50%, transparent 52%),
linear-gradient(to bottom, transparent 48%, rgba(255,255,255,0.48) 50%, transparent 52%),
linear-gradient(45deg, transparent 48%, rgba(255,255,255,0.30) 50%, transparent 52%),
linear-gradient(-45deg, transparent 48%, rgba(255,255,255,0.30) 50%, transparent 52%);
}
.hero-orbit-dot.dot-blue::before {
background:
radial-gradient(circle at 50% 50%, rgba(148,184,255,0.0) 30%, rgba(148,184,255,0.45) 42%, rgba(148,184,255,0.0) 52%),
linear-gradient(to right, transparent 48%, rgba(148,184,255,0.35) 50%, transparent 52%),
linear-gradient(to bottom, transparent 48%, rgba(148,184,255,0.35) 50%, transparent 52%),
linear-gradient(45deg, transparent 48%, rgba(148,184,255,0.22) 50%, transparent 52%),
linear-gradient(-45deg, transparent 48%, rgba(148,184,255,0.22) 50%, transparent 52%);
radial-gradient(circle at 50% 50%, rgba(80,125,255,0.0) 30%, rgba(80,125,255,0.60) 42%, rgba(80,125,255,0.0) 54%),
linear-gradient(to right, transparent 48%, rgba(80,125,255,0.48) 50%, transparent 52%),
linear-gradient(to bottom, transparent 48%, rgba(80,125,255,0.48) 50%, transparent 52%),
linear-gradient(45deg, transparent 48%, rgba(80,125,255,0.30) 50%, transparent 52%),
linear-gradient(-45deg, transparent 48%, rgba(80,125,255,0.30) 50%, transparent 52%);
}
.hero-orbit-dot.dot-violet::before {
background:
radial-gradient(circle at 50% 50%, rgba(196,178,255,0.0) 30%, rgba(196,178,255,0.45) 42%, rgba(196,178,255,0.0) 52%),
linear-gradient(to right, transparent 48%, rgba(196,178,255,0.35) 50%, transparent 52%),
linear-gradient(to bottom, transparent 48%, rgba(196,178,255,0.35) 50%, transparent 52%),
linear-gradient(45deg, transparent 48%, rgba(196,178,255,0.22) 50%, transparent 52%),
linear-gradient(-45deg, transparent 48%, rgba(196,178,255,0.22) 50%, transparent 52%);
radial-gradient(circle at 50% 50%, rgba(135,100,255,0.0) 30%, rgba(135,100,255,0.60) 42%, rgba(135,100,255,0.0) 54%),
linear-gradient(to right, transparent 48%, rgba(135,100,255,0.48) 50%, transparent 52%),
linear-gradient(to bottom, transparent 48%, rgba(135,100,255,0.48) 50%, transparent 52%),
linear-gradient(45deg, transparent 48%, rgba(135,100,255,0.30) 50%, transparent 52%),
linear-gradient(-45deg, transparent 48%, rgba(135,100,255,0.30) 50%, transparent 52%);
}
.hero-orbit-dot.dot-pink::before {
background:
radial-gradient(circle at 50% 50%, rgba(255,180,215,0.0) 30%, rgba(255,180,215,0.45) 42%, rgba(255,180,215,0.0) 52%),
linear-gradient(to right, transparent 48%, rgba(255,180,215,0.35) 50%, transparent 52%),
linear-gradient(to bottom, transparent 48%, rgba(255,180,215,0.35) 50%, transparent 52%),
linear-gradient(45deg, transparent 48%, rgba(255,180,215,0.22) 50%, transparent 52%),
linear-gradient(-45deg, transparent 48%, rgba(255,180,215,0.22) 50%, transparent 52%);
radial-gradient(circle at 50% 50%, rgba(255,105,175,0.0) 30%, rgba(255,105,175,0.60) 42%, rgba(255,105,175,0.0) 54%),
linear-gradient(to right, transparent 48%, rgba(255,105,175,0.48) 50%, transparent 52%),
linear-gradient(to bottom, transparent 48%, rgba(255,105,175,0.48) 50%, transparent 52%),
linear-gradient(45deg, transparent 48%, rgba(255,105,175,0.30) 50%, transparent 52%),
linear-gradient(-45deg, transparent 48%, rgba(255,105,175,0.30) 50%, transparent 52%);
}
/* HERO 左下文字(h1 / p / a)在浅底 hero-bg.jpg 上的可读阴影,保证不蒙层时也清晰 */
.hero-text-visible h1.text-white,
.hero-text-visible p.text-white,
.hero-text-visible p.text-white\/90,
.hero-text-visible a.text-white {
text-shadow:
0 2px 14px rgba(10, 14, 42, 0.58),
0 0 36px rgba(10, 14, 42, 0.28);
-webkit-font-smoothing: antialiased;
}
.hero-text-visible a.text-white {
text-shadow:
0 1px 10px rgba(10, 14, 42, 0.65);
border-bottom-color: rgba(255, 255, 255, 0.85);
}
@media (max-width: 640px) {

104
ui-design-spec.md

@ -1,104 +0,0 @@
# 巴适伙伴 · UI 设计规范(最新规范学习 + 项目审计落地)
> 本文是「查找最新 UI 设计规范 → 学习记忆 → 运用到项目」的产物。第 1–2 节是**学到的规范**(记忆),第 3 节是**对照本项目的审计**,第 4 节是**本次落地**,第 5 节是**可选后续**。
> 研究时间:2026-08。规范以 WCAG 2.2(W3C)、Design Tokens(W3C DTCG)、Material Design 3、现代 CSS(web.dev)为权威基线,结合 2026 趋势。
---
## 1. 研究来源(权威基线)
- **WCAG 2.2**(W3C,2023 发布,现行标准):对比度、焦点、目标尺寸、跳转等可访问性准则。
- **Design Tokens Community Group**(W3C DTCG):令牌分层与格式化标准。
- **Material Design 3**(Google):色彩系统、动效、形状、高程的 web 友好体系。
- **web.dev / Smashing**:现代 CSS 实践(color-mix、OKLCH、clamp、container queries)。
- **2026 趋势**(行业报告):动态自适应、流体间距/排版、主题感知令牌、动效克制、空间深度。
---
## 2. 最新 UI 设计规范要点(学习记忆)
### 2.1 Design Tokens 四层架构(2026 共识)
1. **Foundation(基础)**:颜色基值、字号比例、圆角基准、间距基线——系统的"物理常数",极少变动。
2. **Semantic(语义)**:primary / success / warning / error / text / surface——映射到视觉表现,支持品牌调性切换。
3. **Contextual(情境)**:响应式/容器派生(如 `--space-*` 随屏宽缩放)。
4. **Intelligent(智能)**:AI/行为数据驱动(本项目不涉及)。
- **铁律**:单一真相源(single source of truth),组件只引用令牌不写散值;深色模式靠令牌覆盖而非重写。
### 2.2 WCAG 2.2 无障碍(核心规范骨架)
| 准则 | 要求 | 级别 |
|---|---|---|
| 1.4.3 对比度(最小) | 正文 ≥4.5:1;大字(≥18pt 或 ≥14pt 粗)≥3:1 | AA |
| 1.4.1 颜色用途 | 颜色不能是传达信息的唯一通道(需文字/图标冗余) | A |
| 1.4.11 非文本对比 | UI 组件/图形对象 ≥3:1 | AA |
| 2.1.1 键盘 | 全部功能可用键盘操作 | A |
| 2.4.1 绕过区块 | 提供"跳到主内容"链接 | A |
| 2.4.7 焦点可见 + 2.4.11 焦点外观 | 焦点指示器 ≥2 CSS px、与相邻色 ≥3:1 | AA |
| 2.5.8 目标尺寸(最小) | 交互元素 ≥24×24 CSS px(AA);2.5.5 ≥44×44(AAA) | AA |
| 2.3.3 交互动画 | 尊重 `prefers-reduced-motion` | AAA |
| 3.1.1 / 3.1.2 页面语言 | `<html lang>` 随语种;外文片段标注 | A |
| 4.1.2 名称/角色/值 | 交互元素有可访问名(aria-label/原生语义) | A |
### 2.3 现代 CSS(2026 工程基线)
- **color-mix() / OKLCH**:感知均匀的混色与调阶(OKLCH 比 HSL 更符合人眼)。
- **clamp() 流体排版/间距**:无断点连续缩放,避免极端屏崩坏。
- **container queries**:组件级响应式(比视口 media query 更内聚)。
- **逻辑属性**:`margin-inline`/`inset` 等,天然支持 RTL。
- **prefers-color-scheme + color-scheme**:原生暗色 + 表单控件自动适配。
- **:focus-visible**:仅键盘聚焦显指示器,不干扰鼠标。
### 2.4 2026 趋势(取舍)
- 动态自适应(弱网降级、环境光调对比)——方向正确,落地需克制,避免过度工程。
- 流体间距(间距随屏宽缩放)——优于固定 8px,但需防过度变形。
- 动效克制(长周期、低强度,避免视觉疲劳)——本项目已践行(40s/50s drift)。
- 空间深度(AR/3D)——官网不涉及。
### 2.5 视觉层级与一致性
- 8px 间距网格;模块化字号阶梯(display > h1 > h2 > h3 > body > micro)。
- 语义化用色(强调色按显示角色分档,各档过 WCAG 阈值)。
- 克制圆角(大块锐利、小元素圆润,形成层次)。
- 微交互有目的(hover 微浮、状态反馈),不炫技。
---
## 3. 项目审计(对照规范)
### ✅ 已合规项(设计系统成熟)
- **Design Tokens**:Foundation(`--brand-*`、`--fs-*`、`--space-*`、`--radius-*`、`--duration-*`)+ Semantic(`--accent`、`--status-*`、`--surface-*`、`--text-*`)齐备;赛道色 `--track-*` 单源(经 `src/lib/tracks.ts` 的 `TRACK_TOKEN`)。
- **主题感知 + 暗色模式**:`[data-theme="dark"]` 覆盖块 + `useTheme.ts` 三态切换 + `index.html` 防 FOUC 内联脚本。
- **color-mix**:大量用于派生色(赛道卡片底、描边、阴影)。
- **流体排版**:`--fs-*` 全部用 `clamp()`。
- **8px 间距网格**:`--space-1..11`。
- **:focus-visible**:全局 2px outline + offset(2.4.7/2.4.11 ✓)。
- **prefers-reduced-motion**:全局守卫降级动画时长(2.3.3 ✓)。
- **键盘可达**:flip-card 支持键盘/触摸/点击翻转,Esc 关下拉(2.1.1 ✓)。
- **html lang**:随路由语种切换 `zh-CN`/`zh-HK`/`en`(3.1.1 ✓)。
- **landmark**:`<main>` 语义标签。
- **动效克制**:长周期低强度(mesh 40s、渐变文 14s),有注释记录"降低强度"。
- **`<html lang>`、hreflang、OG/Twitter、JSON-LD**:SEO + 无障碍元数据完备。
### ⚠️ 差距与修复
| 差距 | 规范 | 状态 |
|---|---|---|
| 缺 skip-to-content 跳转链接 | WCAG 2.4.1 | **已修复**(首次) |
| `.status-badge/.status-developing/.status-planned/.progress-glow` 死 CSS(src 0 引用已复核) | 一致性 | **已清理**(本次) |
| 触控目标:桌面端导航 ≈24px(达 AA 24px 下限) | WCAG 2.5.8 | **已加固**(本次,navBase 加 `py-1.5` → ~28px,安全过 AA) |
| 组件内 `text-[Npx]` 硬编码未走 `--fs-*` 令牌 | Tokens 单源 | **已迁移**(Header.tsx 4 处 `text-[16px]`→`text-body`;全 src 复核无 `text-[15px]` 散落,其余页面已用令牌) |
---
## 4. 本次落地(已执行并验证)
1. **`.sr-only` + `.skip-link` 工具类**:新增于 [global.css](src/styles/global.css) 末尾。
2. **skip-to-content 链接**:加在 [SiteShell.tsx](src/components/SiteShell.tsx),`<main>` 加 `id="main-content"`;三语文案(跳到主要内容 / 跳到主要內容 / Skip to main content);默认隐藏、键盘 Tab 聚焦时滑入显现。
3. **清理死 CSS**:删除 [global.css](src/styles/global.css) 中 0 引用的 `.status-badge / .status-developing / .status-planned / .progress-glow / .progress-glow::after / @keyframes progressShine`(删除前 `Select-String` 全 src 复核确认零引用)。
4. **`--fs-*` 令牌迁移**:[Header.tsx](src/components/Header.tsx) 4 处 `text-[16px]` → `text-body`(走 `--fs-body`=16px 令牌,1:1 视觉等价);保留 `text-[27px]`(品牌字标,无对应令牌)与 `text-[10px]`(▼ 装饰符,无对应令牌)。
5. **触控目标加固(WCAG 2.5.8)**:桌面端 `navBase` 加 `py-1.5`,链接触控目标由 ~24px(贴 AA 下限)提升至 ~28px,安全过 AA 24px;移动端 `mobileNavBase` 原有 `py-2.5`(~36px)保持不变。
6. **验证**:dev server HMR 干净生效(global.css + Header.tsx 多次 hmr update,零报错)。
---
## 5. 可选后续(opt-in,未动,需你拍板优先级)
> A(死 CSS 清理)、B(`--fs-*` 令牌迁移:Header 已完成,全 src 复核无其他 `text-[Npx]` 散落)、F(触控目标加固)均已在本次落地,从清单移除。
- **C. 流体间距**:把 `--space-*` 升级为 `clamp()` 极端屏自适应(中风险,需逐页验证)。
- **D. 容器查询**:关键卡片组件改 container query(重构,中高风险)。
- **E. OKLCH 色彩空间**:派生色从 `color-mix(in srgb)` 迁到 `in oklch`(更符合人眼;中风险)。
```
Loading…
Cancel
Save