这是一个巴适伙伴企业官网项目
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

254 lines
12 KiB

import { useState, type CSSProperties } from 'react';
import { getTranslation, type Locale } from '../i18n/utils';
import { Icon } from '../icons';
import { cssVars } from './css';
/* =====================================================================
* 数字生活生态全景图 — 首页 3 受众 × 业务页 5 赛道的「桥」
* 依据:business-architecture.md §4 产品×受众矩阵(2026-08 已拍板)
* 同一组 8 个产品,两种视角:按赛道(连接/成长/交易/理解/成全)/ 按受众(消费者/企业/未来)
* ===================================================================== */
// 已确认的产品×受众矩阵。顺序与 i18n business.tracks[].products[] 严格一致。
// C=消费者 B=企业 F=未来(平台层)
type Aud = { c: boolean; b: boolean; f: boolean };
const MATRIX: Aud[][] = [
// 内容社交:青核 / 音酷 / 蜜恋
[{ c: true, b: false, f: false }, { c: true, b: false, f: false }, { c: true, b: false, f: false }],
// 职场服务:职来 / 看见高山
[{ c: true, b: true, f: false }, { c: true, b: true, f: false }],
// 数字支付:妥哒
[{ c: true, b: true, f: false }],
// 人工智能:阿布AI
[{ c: true, b: false, f: true }],
// 生活服务:听愿
[{ c: true, b: false, f: true }],
];
type Lens = 'track' | 'audience';
type Sel =
| { kind: 'center' }
| { kind: 'track'; i: number }
| { kind: 'aud'; key: 'c' | 'b' | 'f' };
// 5 赛道节点角度(五边形:从顶点顺时针每 72°)
const TRACK_ANGLES = [-90, -18, 54, 126, 198];
// 3 受众节点角度(顶 / 左下 / 右下)
const AUD_ANGLES = [-90, 150, 30];
function polar(angleDeg: number, r: number) {
const a = (angleDeg * Math.PI) / 180;
return { x: 50 + r * Math.cos(a), y: 50 + r * Math.sin(a) };
}
export default function EcosystemMap({ locale }: { locale: Locale }) {
const t = getTranslation(locale);
const tracks = t.business.tracks;
const isEn = locale === 'en';
const tr = (zh: string, hk: string, en: string) => (isEn ? en : locale === 'hk' ? hk : zh);
const [lens, setLens] = useState<Lens>('track');
const [sel, setSel] = useState<Sel>({ kind: 'track', i: 0 });
const audienceMeta: { key: 'c' | 'b' | 'f'; name: string; icon: string; def: string; angle: number }[] = [
{ key: 'c', name: tr('消费者', '消費者', 'Consumers'), icon: 'users', def: tr('个体的数字生活', '個體的數位生活', "Individuals' digital life"), angle: AUD_ANGLES[0] },
{ key: 'b', name: tr('企业', '企業', 'Enterprises'), icon: 'briefcase', def: tr('组织的效率与增长', '組織的效率與增長', 'Organizational efficiency & growth'), angle: AUD_ANGLES[1] },
{ key: 'f', name: tr('未来', '未來', 'Future'), icon: 'rocket', def: tr('前沿与平台层', '前沿與平台層', 'Frontier & platform layer'), angle: AUD_ANGLES[2] },
];
const actionLabel = (i: number) =>
tr(['连接', '成长', '交易', '理解', '成全'][i], ['連接', '成長', '交易', '理解', '成全'][i], ['Connect', 'Grow', 'Transact', 'Understand', 'Fulfill'][i]);
const totalProducts = tracks.reduce((n, tk) => n + tk.products.length, 0);
// 切换视角时回到该视角的默认选中
const switchLens = (next: Lens) => {
setLens(next);
setSel(next === 'track' ? { kind: 'track', i: 0 } : { kind: 'aud', key: 'c' });
};
// —— 右侧详情面板的产品列表 ——
type PItem = { name: string; desc: string; icon?: string; aud: Aud };
let panelTitle: string;
let panelEyebrow: string;
let panelNote: string;
let products: PItem[];
if (sel.kind === 'center') {
panelEyebrow = tr('数字生活生态', '數位生活生態', 'Digital Life Ecosystem');
panelTitle = tr('巴适伙伴', '巴適夥伴', 'BaskBuddy');
panelNote = tr(
`以"温暖科技"连接人、机会、价值与智能——${tracks.length} 大赛道 · ${totalProducts} 个自研产品,共建数字生活生态。`,
`以「溫暖科技」連接人、機會、價值與智能——${tracks.length} 大賽道 · ${totalProducts} 個自研產品,共建數位生活生態。`,
`Connecting people, opportunities, value & intelligence with "warm tech" — ${tracks.length} tracks · ${totalProducts} self-built products.`,
);
products = tracks.flatMap((tk, ti) =>
tk.products.map((p, pi) => ({ name: p.name, desc: p.desc, icon: p.icon, aud: MATRIX[ti][pi] })),
);
} else if (sel.kind === 'track') {
const ti = sel.i;
const tk = tracks[ti];
panelEyebrow = tr('赛道', '賽道', 'Track');
panelTitle = `${tk.name} · ${actionLabel(ti)}`;
panelNote = tr(
['人与人的连接', '人与机会的连接', '人与价值的连接', '人与智能的连接', '人与心愿的连接'][ti],
['人與人的連接', '人與機會的連接', '人與價值的連接', '人與智能的連接', '人與心願的連接'][ti],
['People ↔ people', 'People ↔ opportunities', 'People ↔ value', 'People ↔ intelligence', 'People ↔ wishes'][ti],
);
products = tk.products.map((p, pi) => ({ name: p.name, desc: p.desc, icon: p.icon, aud: MATRIX[ti][pi] }));
} else {
const key = sel.key;
const am = audienceMeta.find((a) => a.key === key)!;
panelEyebrow = tr('受众', '受眾', 'Audience');
panelTitle = am.name;
panelNote = am.def;
products = tracks.flatMap((tk, ti) =>
tk.products.flatMap((p, pi) =>
MATRIX[ti][pi][key]
? [{ name: p.name, desc: p.desc, icon: p.icon, aud: MATRIX[ti][pi] }]
: [],
),
);
}
const audDot = (a: Aud) => (
<span className="eco-aud-dots" aria-hidden="true">
{a.c && <i className="eco-dot eco-dot-c" title={tr('消费者', '消費者', 'Consumers')}>C</i>}
{a.b && <i className="eco-dot eco-dot-b" title={tr('企业', '企業', 'Enterprises')}>B</i>}
{a.f && <i className="eco-dot eco-dot-f" title={tr('未来', '未來', 'Future')}>F</i>}
</span>
);
return (
<div className="eco-shell">
{/* —— 视角切换 —— */}
<div className="eco-lens" role="tablist" aria-label={tr('全景图视角', '全景圖視角', 'Map view')}>
{(['track', 'audience'] as Lens[]).map((l) => (
<button
key={l}
type="button"
role="tab"
aria-selected={lens === l}
className={`eco-lens-btn${lens === l ? ' is-active' : ''}`}
onClick={() => switchLens(l)}
>
{l === 'track' ? tr('按赛道看', '按賽道看', 'By track') : tr('按受众看', '按受眾看', 'By audience')}
</button>
))}
</div>
<div className="eco-body">
{/* —— 全景图舞台 —— */}
<div className="eco-stage" role="group" aria-label={tr('生态全景图', '生態全景圖', 'Ecosystem map')}>
{/* 连接线 */}
<svg className="eco-lines" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
{(lens === 'track' ? TRACK_ANGLES : AUD_ANGLES).map((ang, i) => {
const p = polar(ang, 36);
const on =
lens === 'track'
? sel.kind === 'track' && sel.i === i
: sel.kind === 'aud' && sel.key === audienceMeta[i].key;
return <line key={i} x1={50} y1={50} x2={p.x} y2={p.y} className={`eco-line${on ? ' is-active' : ''}`} />;
})}
</svg>
{/* 中心枢纽 */}
<button
type="button"
className={`eco-hub${sel.kind === 'center' ? ' is-active' : ''}`}
style={{ left: '50%', top: '50%' } as CSSProperties}
onClick={() => setSel({ kind: 'center' })}
aria-pressed={sel.kind === 'center'}
>
<span className="eco-hub-glow" aria-hidden="true" />
<span className="eco-hub-title">{tr('巴适伙伴', '巴適夥伴', 'BaskBuddy')}</span>
<span className="eco-hub-sub">{tr('数字生活生态', '數位生活生態', 'Digital Life')}</span>
</button>
{/* 赛道节点 */}
{lens === 'track' &&
tracks.map((tk, i) => {
const p = polar(TRACK_ANGLES[i], 36);
const on = sel.kind === 'track' && sel.i === i;
return (
<button
key={tk.name}
type="button"
className={`eco-node eco-float${on ? ' is-active' : ''}`}
style={{ left: `${p.x}%`, top: `${p.y}%`, '--dly': `${i * 0.6}s` } as CSSProperties}
onClick={() => setSel({ kind: 'track', i })}
aria-pressed={on}
>
<span className="eco-node-icon">
<Icon name={tk.icon} className="w-5 h-5" />
</span>
<span className="eco-node-name">{tk.name}</span>
<span className="eco-node-action">{actionLabel(i)}</span>
<span className="eco-node-count">{tk.products.length}</span>
</button>
);
})}
{/* 受众节点 */}
{lens === 'audience' &&
audienceMeta.map((am) => {
const p = polar(am.angle, 36);
const on = sel.kind === 'aud' && sel.key === am.key;
const count = tracks.reduce(
(n, tk, ti) => n + tk.products.filter((_, pi) => MATRIX[ti][pi][am.key]).length,
0,
);
return (
<button
key={am.key}
type="button"
className={`eco-node eco-float${on ? ' is-active' : ''}`}
style={{ left: `${p.x}%`, top: `${p.y}%`, '--dly': `${am.key === 'c' ? 0 : am.key === 'b' ? 0.6 : 1.2}s` } as CSSProperties}
onClick={() => setSel({ kind: 'aud', key: am.key })}
aria-pressed={on}
>
<span className="eco-node-icon">
<Icon name={am.icon} className="w-5 h-5" />
</span>
<span className="eco-node-name">{am.name}</span>
<span className="eco-node-count">{count}</span>
</button>
);
})}
</div>
{/* —— 详情面板 —— */}
<aside className="eco-panel" aria-live="polite">
<span className="section-eyebrow">{panelEyebrow}</span>
<h3 className="eco-panel-title">{panelTitle}</h3>
<p className="eco-panel-note">{panelNote}</p>
<ul className="eco-products">
{products.map((p) => (
<li key={p.name} className="eco-product">
<span className="eco-product-icon">
{p.icon && <Icon name={p.icon} className="w-[18px] h-[18px]" />}
</span>
<div className="eco-product-main">
<div className="eco-product-head">
<span className="eco-product-name">{p.name}</span>
{audDot(p.aud)}
</div>
<p className="eco-product-desc">{p.desc}</p>
</div>
</li>
))}
</ul>
</aside>
</div>
{/* 受众图例(桥接说明) */}
<p className="eco-legend">
{tr(
'同一组产品,两种视角:按"赛道"看我们造什么,按"受众"看我们为谁。',
'同一組產品,兩種視角:按「賽道」看我們造什麼,按「受眾」看我們為誰。',
'Same products, two lenses: "By track" shows what we build; "By audience" shows who we serve.',
)}
</p>
</div>
);
}