Files

466 lines
18 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useCallback, Suspense, lazy } from 'react';
import { Cursor, Loading } from '../src';
import '../src/styles/index.less';
import '@fontsource/nunito/latin-500.css';
import '@fontsource/nunito/latin-700.css';
import '@fontsource/nunito/latin-900.css';
import '@fontsource/noto-sans-sc/latin-400.css';
import '@fontsource/noto-sans-sc/latin-500.css';
import '@fontsource/noto-sans-sc/latin-700.css';
import '@fontsource/noto-sans-sc/chinese-simplified-400.css';
import '@fontsource/noto-sans-sc/chinese-simplified-500.css';
import '@fontsource/noto-sans-sc/chinese-simplified-700.css';
import '@fontsource/zen-maru-gothic/latin-500.css';
import '@fontsource/zen-maru-gothic/latin-700.css';
import '@fontsource/zen-maru-gothic/latin-900.css';
import '@fontsource/zen-maru-gothic/japanese-500.css';
import '@fontsource/zen-maru-gothic/japanese-700.css';
import '@fontsource/zen-maru-gothic/japanese-900.css';
import HomePage from './HomePage';
import { PAGE_INFO } from './pageInfo';
import { useIsMobile } from './tools';
// Lazy-load ComponentPage so homepage does not pull in every demo on initial load
const ComponentPage = lazy(() => import('./ComponentPage'));
// ============================================
// Simple hash router
// ============================================
const useHash = () => {
const [hash, setHash] = useState(
() => window.location.hash.slice(1) || '/'
);
useEffect(() => {
const onHashChange = () =>
setHash(window.location.hash.slice(1) || '/');
window.addEventListener('hashchange', onHashChange);
return () => window.removeEventListener('hashchange', onHashChange);
}, []);
const navigate = useCallback((path: string) => {
window.location.hash = path;
}, []);
return { hash, navigate };
};
interface MenuItemChild {
key: string;
label: string;
}
interface MenuItem {
key: string;
label: string;
children?: MenuItemChild[];
}
// ============================================
// Menu config
// ============================================
const MENU_ITEMS: MenuItem[] = [
{
key: 'cat-basic',
label: '── 基础组件 ──',
children: [
{ key: 'button', label: 'Button 按钮' },
{ key: 'input', label: 'Input 输入框' },
{ key: 'switch', label: 'Switch 开关' },
{ key: 'card', label: 'Card 卡片' },
{ key: 'collapse', label: 'Collapse 折叠面板' },
{ key: 'cursor', label: 'Cursor 光标' },
{ key: 'modal', label: 'Modal 弹窗' },
{ key: 'typewriter', label: 'Typewriter 打字机' },
{ key: 'divider-comp', label: 'Divider 分割线' },
{ key: 'icon', label: 'Icon 图标' },
{ key: 'select', label: 'Select 选择器' },
{ key: 'checkbox', label: 'Checkbox 多选框' },
{ key: 'tabs', label: 'Tabs 标签页' },
{ key: 'footer', label: 'Footer 页脚' },
{ key: 'codeblock', label: 'CodeBlock 代码高亮' },
{ key: 'loading', label: 'Loading 加载' },
{ key: 'table', label: 'Table 表格' },
],
},
{
key: 'cat-complex',
label: '── 复杂组件 ──',
children: [
{ key: 'time', label: 'Time 时间' },
{ key: 'phone', label: 'Phone 手机' },
],
},
];
// ============================================
// Shared styles
// ============================================
const S = {
layout: {
display: 'flex',
height: '100dvh',
overflow: 'hidden',
fontFamily: "Nunito, 'Noto Sans SC', 'Zen Maru Gothic', -apple-system, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif",
background: `url(${new URL('./img/content_bg_pc.jpg', import.meta.url).href}) center / auto repeat`,
} as React.CSSProperties,
sidebar: {
width: 220,
minWidth: 220,
background: `url(${new URL('./img/menu_bg.svg', import.meta.url).href}) center/cover no-repeat`,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
} as React.CSSProperties,
homeBg: {
background: `url(${new URL('./img/home_bg.webp', import.meta.url).href}) 0 0 / auto repeat, #7DC395`,
animation: 'bgScroll 80s linear infinite',
} as React.CSSProperties,
sidebarHeader: {
padding: '20px 16px 12px',
borderBottom: '1px solid #e8e2d6',
fontWeight: 700,
fontSize: 15,
color: '#725d42',
letterSpacing: -0.3,
display: 'flex',
alignItems: 'center',
} as React.CSSProperties,
menuList: {
flex: 1,
overflow: 'auto',
padding: '8px 0',
} as React.CSSProperties,
menuItem: (active: boolean) =>
({
display: 'flex',
alignItems: 'center',
margin: '1px 5px',
height: 40,
padding: '0 16px',
fontFamily: "Nunito, 'Noto Sans SC', 'Zen Maru Gothic', -apple-system, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif",
fontStyle: 'normal',
fontWeight: 600,
fontSize: 14,
paddingLeft: 26,
color: active ? '#fff' : '#8a7b66',
background: active ? '#B7C6E5' : 'transparent',
borderRadius: 12,
borderRight: 'none',
transition: 'all 0.15s',
}) as React.CSSProperties,
main: {
flex: 1,
overflow: 'auto',
padding: '32px 40px',
} as React.CSSProperties,
};
// ============================================
// Sidebar content (shared between desktop & mobile drawer)
// ============================================
const SidebarContent: React.FC<{
activeKey: string;
onNavigate: (path: string) => void;
}> = ({ activeKey, onNavigate }) => (
<>
<div style={S.sidebarHeader} onClick={() => onNavigate('/')}>
<img
src={new URL('./img/nook-phone/nook1.svg', import.meta.url).href}
style={{ width: 24, height: 24, marginRight: 8 }}
alt="nook"
/>
Animal
</div>
<nav style={S.menuList}>
{MENU_ITEMS.map((item) => {
if (item.children) {
return (
<div key={item.key}>
<div
style={{
padding: '12px 16px 4px',
fontSize: 11,
color: '#a0936e',
fontWeight: 600,
letterSpacing: 0.5,
}}
>
{item.label}
</div>
{item.children.map((child) => (
<div
key={child.key}
style={S.menuItem(activeKey === child.key)}
onClick={() => onNavigate(`/${child.key}`)}
onMouseEnter={(e) => {
if (activeKey !== child.key)
e.currentTarget.style.background = '#d6dff0';
}}
onMouseLeave={(e) => {
if (activeKey !== child.key)
e.currentTarget.style.background = 'transparent';
}}
>
<span
style={{
color: activeKey === child.key ? '#fff' : '#8a7b66',
}}
>
{child.label}
</span>
</div>
))}
</div>
);
}
return (
<div
key={item.key}
style={S.menuItem(activeKey === item.key)}
onClick={() => onNavigate(`/${item.key}`)}
onMouseEnter={(e) => {
if (activeKey !== item.key)
e.currentTarget.style.background = '#d6dff0';
}}
onMouseLeave={(e) => {
if (activeKey !== item.key)
e.currentTarget.style.background = 'transparent';
}}
>
<span style={{ color: activeKey === item.key ? '#fff' : '#8a7b66' }}>
{item.label}
</span>
</div>
);
})}
</nav>
</>
);
// ============================================
// App
// ============================================
const App: React.FC = () => {
const { hash, navigate } = useHash();
const isMobile = useIsMobile();
const [drawerOpen, setDrawerOpen] = useState(false);
const [loadingActive, setLoadingActive] = useState(false);
const [loadingMounted, setLoadingMounted] = useState(false);
const mainRef = React.useRef<HTMLElement>(null);
const activeKey =
hash.startsWith('/') && hash.length > 1 ? hash.slice(1) : 'home';
const isHomePage = activeKey === 'home';
// Close drawer when switching to desktop
useEffect(() => {
if (!isMobile) setDrawerOpen(false);
}, [isMobile]);
// Close drawer when route changes + scroll main to top
useEffect(() => {
setDrawerOpen(false);
mainRef.current?.scrollTo({ top: 0 });
}, [activeKey]);
const handleNavigate = useCallback(
(path: string) => {
navigate(path);
setDrawerOpen(false);
},
[navigate]
);
// 首页跳转到组件页时显示 2s Loading 覆盖层
const handleHomeNavigate = useCallback(
(path: string) => {
setLoadingMounted(true);
setLoadingActive(true);
navigate(path);
// 2s 后开始关闭,再多留 1.5s 给关闭扩散动画后卸载
window.setTimeout(() => setLoadingActive(false), 2000);
window.setTimeout(() => setLoadingMounted(false), 3500);
},
[navigate]
);
return (
<Cursor>
<style>{`
@keyframes bgScroll {
0% { background-position: 100% 0%; }
100% { background-position: 0% 100%; }
}
`}</style>
{isHomePage ? (
/* Home page — full screen, no sidebar */
<div
style={{
...S.layout,
...S.homeBg,
justifyContent: 'center',
}}
>
<HomePage onNavigate={handleHomeNavigate} />
</div>
) : (
/* Component page — with sidebar */
<div style={S.layout}>
{/* Desktop sidebar */}
{!isMobile && (
<aside style={S.sidebar}>
<SidebarContent
activeKey={activeKey}
onNavigate={handleNavigate}
/>
</aside>
)}
{/* Mobile top bar */}
{isMobile && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
height: 52,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 12px',
background: 'rgba(255,252,244,0.92)',
backdropFilter: 'blur(8px)',
borderBottom: '1px solid #e8e2d6',
zIndex: 50,
fontFamily: S.layout.fontFamily,
}}
>
<button
onClick={() => navigate('/')}
style={{
background: 'none',
border: 'none',
fontSize: 20,
color: '#725d42',
padding: '4px 8px',
borderRadius: 8,
lineHeight: 1,
}}
>
</button>
<span
style={{
fontWeight: 700,
fontSize: 15,
color: '#725d42',
}}
>
{PAGE_INFO[activeKey]?.title ?? '组件文档'}
</span>
<button
onClick={() => setDrawerOpen(true)}
style={{
background: 'none',
border: 'none',
fontSize: 20,
color: '#725d42',
padding: '4px 8px',
borderRadius: 8,
lineHeight: 1,
}}
>
</button>
</div>
)}
{/* Mobile drawer overlay */}
{isMobile && drawerOpen && (
<>
<div
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0,0,0,0.35)',
zIndex: 98,
}}
onClick={() => setDrawerOpen(false)}
/>
<aside
style={{
...S.sidebar,
position: 'fixed',
left: 0,
top: 0,
bottom: 0,
width: 240,
zIndex: 99,
boxShadow: '4px 0 24px rgba(0,0,0,0.15)',
}}
>
<SidebarContent
activeKey={activeKey}
onNavigate={handleNavigate}
/>
</aside>
</>
)}
<main
ref={mainRef}
style={{
...S.main,
position: 'relative',
zIndex: 1,
padding: isMobile ? '16px' : '32px 40px',
paddingTop: isMobile ? 68 : 32,
}}
>
<Suspense fallback={null}>
<ComponentPage activeKey={activeKey} />
</Suspense>
</main>
{!isMobile && (
<img
src={
new URL('./img/guide-bg-line.webp', import.meta.url).href
}
alt=""
loading="lazy"
decoding="async"
style={{
position: 'fixed',
left: 220,
right: 0,
bottom: 0,
width: 'calc(100% - 220px)',
pointerEvents: 'none',
zIndex: 0,
}}
/>
)}
</div>
)}
{/* 首页跳转组件页的过场 Loading,全屏覆盖 */}
{loadingMounted && (
<div
style={{
position: 'fixed',
inset: 0,
zIndex: 9999,
pointerEvents: loadingActive ? 'auto' : 'none',
}}
>
<Loading active={loadingActive} />
</div>
)}
</Cursor>
);
};
export default App;