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.
107 lines
3.2 KiB
107 lines
3.2 KiB
import { useState, type CSSProperties } from 'react';
|
|
import { getLocalizedPath, type Locale } from '../i18n/utils';
|
|
|
|
type NetNode = {
|
|
id: string;
|
|
title: string;
|
|
desc: string;
|
|
link?: string;
|
|
};
|
|
|
|
export type OrgNetworkData = {
|
|
center: { title: string; subtitle: string; desc: string };
|
|
nodes: NetNode[];
|
|
};
|
|
|
|
// 6 个子节点均匀分布在圆周上(从正上方开始,顺时针 60° 间隔)
|
|
const ANGLES = [-90, -30, 30, 90, 150, 210];
|
|
const R = 38; // 半径(占容器百分比)
|
|
|
|
function nodePos(i: number) {
|
|
const a = (ANGLES[i] * Math.PI) / 180;
|
|
return {
|
|
x: 50 + R * Math.cos(a),
|
|
y: 50 + R * Math.sin(a),
|
|
};
|
|
}
|
|
|
|
export default function OrgNetwork({ data, locale }: { data: OrgNetworkData; locale: Locale }) {
|
|
const [active, setActive] = useState<string>('center');
|
|
|
|
const current =
|
|
active === 'center'
|
|
? { title: data.center.title, desc: data.center.desc }
|
|
: data.nodes.find((n) => n.id === active) ?? { title: data.center.title, desc: data.center.desc };
|
|
const link =
|
|
active === 'center' ? undefined : data.nodes.find((n) => n.id === active)?.link;
|
|
|
|
const learnMore = locale === 'en' ? 'Learn more' : '了解更多';
|
|
|
|
return (
|
|
<div className="org-shell">
|
|
{/* 辐射网络图 */}
|
|
<div className="org-net">
|
|
<svg className="org-lines" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
|
|
{data.nodes.map((n, i) => {
|
|
const p = nodePos(i);
|
|
const on = active === n.id;
|
|
return (
|
|
<line
|
|
key={n.id}
|
|
x1={50}
|
|
y1={50}
|
|
x2={p.x}
|
|
y2={p.y}
|
|
className={`org-line${on ? ' is-active' : ''}`}
|
|
/>
|
|
);
|
|
})}
|
|
</svg>
|
|
|
|
{/* 中心节点 */}
|
|
<button
|
|
type="button"
|
|
className={`org-node org-node-center${active === 'center' ? ' is-active' : ''}`}
|
|
style={{ left: '50%', top: '50%' } as CSSProperties}
|
|
onClick={() => setActive('center')}
|
|
aria-pressed={active === 'center'}
|
|
>
|
|
<span className="org-node-title">{data.center.title}</span>
|
|
<span className="org-node-sub">{data.center.subtitle}</span>
|
|
</button>
|
|
|
|
{/* 子节点 */}
|
|
{data.nodes.map((n, i) => {
|
|
const p = nodePos(i);
|
|
return (
|
|
<button
|
|
key={n.id}
|
|
type="button"
|
|
className={`org-node${active === n.id ? ' is-active' : ''}`}
|
|
style={{ left: `${p.x}%`, top: `${p.y}%` } as CSSProperties}
|
|
onClick={() => setActive(n.id)}
|
|
aria-pressed={active === n.id}
|
|
>
|
|
<span className="org-node-title">{n.title}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* 详情面板 */}
|
|
<div className="org-panel">
|
|
<h3 className="org-panel-title">{current.title}</h3>
|
|
<p className="org-panel-desc">{current.desc}</p>
|
|
{link && (
|
|
<a
|
|
className="org-panel-link"
|
|
href={link.startsWith('#') ? link : getLocalizedPath(link, locale)}
|
|
>
|
|
{learnMore}
|
|
<span aria-hidden="true">→</span>
|
|
</a>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|