Skip to content

Commit d727eaf

Browse files
committed
feat: 添加关于页面
1 parent 826db06 commit d727eaf

File tree

10 files changed

+313
-2
lines changed

10 files changed

+313
-2
lines changed

src/components/TypingAnimation.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import clsx from 'clsx';
2+
import type { MotionProps } from 'motion/react';
3+
import { motion } from 'motion/react';
4+
import { useEffect, useRef, useState } from 'react';
5+
6+
interface TypingAnimationProps extends MotionProps {
7+
as?: React.ElementType;
8+
children: string;
9+
className?: string;
10+
delay?: number;
11+
duration?: number;
12+
startOnView?: boolean;
13+
}
14+
15+
export function TypingAnimation({
16+
as: Component = 'div',
17+
children,
18+
className,
19+
delay = 0,
20+
duration = 50,
21+
startOnView = false,
22+
...props
23+
}: TypingAnimationProps) {
24+
const MotionComponent = motion.create(Component, {
25+
forwardMotionProps: true
26+
});
27+
28+
const [displayedText, setDisplayedText] = useState<string>('');
29+
const [started, setStarted] = useState(false);
30+
const elementRef = useRef<HTMLElement | null>(null);
31+
32+
useEffect(() => {
33+
if (!startOnView) {
34+
const startTimeout = setTimeout(() => {
35+
setStarted(true);
36+
}, delay);
37+
return () => clearTimeout(startTimeout);
38+
}
39+
40+
const observer = new IntersectionObserver(
41+
([entry]) => {
42+
if (entry.isIntersecting) {
43+
setTimeout(() => {
44+
setStarted(true);
45+
}, delay);
46+
observer.disconnect();
47+
}
48+
},
49+
{ threshold: 0.1 }
50+
);
51+
52+
if (elementRef.current) {
53+
observer.observe(elementRef.current);
54+
}
55+
56+
return () => observer.disconnect();
57+
}, [delay, startOnView]);
58+
59+
useEffect(() => {
60+
if (!started) return;
61+
62+
let i = 0;
63+
const typingEffect = setInterval(() => {
64+
if (i < children.length) {
65+
setDisplayedText(children.substring(0, i + 1));
66+
i += 1;
67+
} else {
68+
clearInterval(typingEffect);
69+
}
70+
}, duration);
71+
72+
// eslint-disable-next-line consistent-return
73+
return () => {
74+
clearInterval(typingEffect);
75+
};
76+
}, [children, duration, started]);
77+
78+
return (
79+
<MotionComponent
80+
className={clsx('tracking-[-0.02em]', className)}
81+
ref={elementRef}
82+
{...props}
83+
>
84+
{displayedText}
85+
</MotionComponent>
86+
);
87+
}

src/pages/(base)/about/index.tsx

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { TypingAnimation } from '@/components/TypingAnimation';
2+
import pkg from '~/package.json';
3+
4+
import HeaderDescription from './modules/header-description';
5+
import type { CardInfo, PkgJson, PkgVersionInfo } from './modules/shared';
6+
import { transformVersionData } from './modules/shared';
7+
8+
const latestBuildTime = BUILD_TIME;
9+
10+
// 解构 package.json 数据
11+
const { dependencies, devDependencies, name, version } = pkg;
12+
13+
// 格式化 package.json 数据
14+
const pkgJson: PkgJson = {
15+
dependencies: Object.entries(dependencies).map(transformVersionData),
16+
devDependencies: Object.entries(devDependencies).map(transformVersionData),
17+
name,
18+
version
19+
};
20+
21+
// 抽离渲染组件
22+
const TagItem = ({ nameOrHref }: PkgVersionInfo) => <ATag color="blue">{nameOrHref}</ATag>;
23+
24+
const Link = ({ label, nameOrHref }: PkgVersionInfo) => (
25+
<a
26+
className="text-primary"
27+
href={nameOrHref}
28+
rel="noopener noreferrer"
29+
target="_blank"
30+
>
31+
{label}
32+
</a>
33+
);
34+
35+
// 获取卡片信息的自定义 Hook
36+
function useGetCardInfo(): CardInfo[] {
37+
const { t } = useTranslation();
38+
39+
// 项目基本信息
40+
const packageInfo: PkgVersionInfo[] = [
41+
{
42+
label: t('page.about.projectInfo.version'),
43+
nameOrHref: pkgJson.version,
44+
render: TagItem
45+
},
46+
{
47+
label: t('page.about.projectInfo.latestBuildTime'),
48+
nameOrHref: latestBuildTime,
49+
render: TagItem
50+
},
51+
{
52+
label: t('page.about.projectInfo.githubLink'),
53+
nameOrHref: pkg.homepage,
54+
render: Link
55+
},
56+
{
57+
label: t('page.about.projectInfo.previewLink'),
58+
nameOrHref: pkg.website,
59+
render: Link
60+
}
61+
];
62+
63+
// 卡片信息配置
64+
return [
65+
{
66+
content: packageInfo,
67+
title: t('page.about.projectInfo.title')
68+
},
69+
{
70+
content: pkgJson.dependencies,
71+
title: t('page.about.prdDep')
72+
},
73+
{
74+
content: pkgJson.devDependencies, // 修复之前使用错误的 dependencies
75+
title: t('page.about.devDep')
76+
}
77+
];
78+
}
79+
80+
// 主组件
81+
const About = () => {
82+
const { t } = useTranslation();
83+
84+
const cardInfo = useGetCardInfo();
85+
86+
return (
87+
<ASpace
88+
className="w-full"
89+
direction="vertical"
90+
size={16}
91+
>
92+
<ACard
93+
className="card-wrapper"
94+
size="small"
95+
title={t('page.about.title')}
96+
variant="borderless"
97+
>
98+
<TypingAnimation className="h-54px text-12px">{t('page.about.introduction')}</TypingAnimation>
99+
</ACard>
100+
101+
{cardInfo.map(HeaderDescription)}
102+
</ASpace>
103+
);
104+
};
105+
106+
export const handle = {
107+
i18nKey: 'route.about',
108+
icon: 'fluent:book-information-24-regular',
109+
order: 9,
110+
title: 'about'
111+
};
112+
113+
export default About;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { CardInfo } from './shared';
2+
3+
const HeaderDescription = (item: CardInfo) => {
4+
return (
5+
<ACard
6+
bordered={false}
7+
className="card-wrapper"
8+
key={item.title}
9+
size="small"
10+
title={item.title}
11+
>
12+
<ADescriptions
13+
bordered
14+
column={{ lg: 2, md: 2, sm: 2, xl: 2, xs: 1, xxl: 2 }}
15+
size="small"
16+
>
17+
{item.content.map(pkgInfo => (
18+
<ADescriptions.Item
19+
key={pkgInfo.label}
20+
label={pkgInfo.label}
21+
>
22+
{pkgInfo.render ? pkgInfo.render(pkgInfo) : pkgInfo.nameOrHref}
23+
</ADescriptions.Item>
24+
))}
25+
</ADescriptions>
26+
</ACard>
27+
);
28+
};
29+
30+
export default HeaderDescription;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export interface PkgVersionInfo {
2+
label: string;
3+
nameOrHref: string;
4+
render?: (record: PkgVersionInfo) => JSX.Element;
5+
}
6+
7+
export interface CardInfo {
8+
content: PkgVersionInfo[];
9+
title: string;
10+
}
11+
12+
export interface PkgJson {
13+
dependencies: PkgVersionInfo[];
14+
devDependencies: PkgVersionInfo[];
15+
name: string;
16+
version: string;
17+
}
18+
19+
export interface PackageInfo {
20+
isLink: boolean;
21+
label: string;
22+
titleOrHref: string;
23+
}
24+
25+
export function transformVersionData(tuple: [string, string]): PkgVersionInfo {
26+
const [$name, $version] = tuple;
27+
return {
28+
label: $name,
29+
nameOrHref: $version
30+
};
31+
}

src/pages/(base)/function/index.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const Function = () => {
2+
return null;
3+
};
4+
5+
export const handle = {
6+
i18nKey: 'route.function',
7+
icon: 'icon-park-outline:all-application',
8+
order: 6,
9+
title: 'function'
10+
};
11+
12+
export default Function;

src/router/elegant/imports.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export const layouts: Record<string, () => Promise<any>> = {
1717
};
1818

1919
export const pages: Record<string, () => Promise<any>> = {
20+
"(base)_about": () => import("@/pages/(base)/about/index.tsx"),
21+
"(base)_function_event-bus": () => import("@/pages/(base)/function/event-bus/index.tsx"),
22+
"(base)_function": () => import("@/pages/(base)/function/index.tsx"),
2023
"(base)_home": () => import("@/pages/(base)/home/index.tsx"),
2124
"(base)_manage": () => import("@/pages/(base)/manage/index.tsx"),
2225
"(base)_manage_menu": () => import("@/pages/(base)/manage/menu/index.tsx"),
@@ -25,6 +28,8 @@ export const pages: Record<string, () => Promise<any>> = {
2528
"(base)_multi-menu_first": () => import("@/pages/(base)/multi-menu/first/index.tsx"),
2629
"(base)_multi-menu": () => import("@/pages/(base)/multi-menu/index.tsx"),
2730
"(base)_multi-menu_second_child_home": () => import("@/pages/(base)/multi-menu/second/child/home/index.tsx"),
31+
"(base)_multi-menu_second_child": () => import("@/pages/(base)/multi-menu/second/child/index.tsx"),
32+
"(base)_multi-menu_second": () => import("@/pages/(base)/multi-menu/second/index.tsx"),
2833
"(base)_projects_[pid]_edit_[id]": () => import("@/pages/(base)/projects/[pid]/edit/[id].tsx"),
2934
"(base)_projects_[pid]_edit": () => import("@/pages/(base)/projects/[pid]/edit/index.tsx"),
3035
"(base)_projects_[pid]": () => import("@/pages/(base)/projects/[pid]/index.tsx"),

src/router/elegant/routeMap.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ export const routeMap: RouteMap = {
2828
"document_procomponents": "/document/procomponents",
2929
"document_antd": "/document/antd",
3030
"logout": "/logout",
31+
"(base)_about": "/about",
32+
"(base)_function": "/function",
33+
"(base)_function_event-bus": "/function/event-bus",
3134
"(base)_home": "/home",
3235
"(base)_manage": "/manage",
3336
"(base)_manage_menu": "/manage/menu",

src/router/elegant/routes.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,23 @@ export const generatedRoutes = [
1717
name: '(base)',
1818
path: null,
1919
children: [
20+
{
21+
matchedFiles: [null, '/src/pages/(base)/about/index.tsx', null, null],
22+
name: '(base)_about',
23+
path: '/about'
24+
},
25+
{
26+
matchedFiles: [null, '/src/pages/(base)/function/index.tsx', null, null],
27+
name: '(base)_function',
28+
path: '/function',
29+
children: [
30+
{
31+
matchedFiles: [null, '/src/pages/(base)/function/event-bus/index.tsx', null, null],
32+
name: '(base)_function_event-bus',
33+
path: '/function/event-bus'
34+
}
35+
]
36+
},
2037
{ matchedFiles: [null, '/src/pages/(base)/home/index.tsx', null, null], name: '(base)_home', path: '/home' },
2138
{
2239
matchedFiles: [null, '/src/pages/(base)/manage/index.tsx', null, null],
@@ -53,12 +70,12 @@ export const generatedRoutes = [
5370
]
5471
},
5572
{
56-
matchedFiles: [null, null, null, null],
73+
matchedFiles: [null, '/src/pages/(base)/multi-menu/second/index.tsx', null, null],
5774
name: '(base)_multi-menu_second',
5875
path: '/multi-menu/second',
5976
children: [
6077
{
61-
matchedFiles: [null, null, null, null],
78+
matchedFiles: [null, '/src/pages/(base)/multi-menu/second/child/index.tsx', null, null],
6279
name: '(base)_multi-menu_second_child',
6380
path: '/multi-menu/second/child',
6481
children: [

src/types/auto-imports.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ declare global {
1212
const ACol: typeof import('antd')['Col']
1313
const AColorPicker: typeof import('antd')['ColorPicker']
1414
const AConfigProvider: typeof import('antd')['ConfigProvider']
15+
const ADescriptions: typeof import('antd')['Descriptions']
1516
const ADivider: typeof import('antd')['Divider']
1617
const ADrawer: typeof import('antd')['Drawer']
1718
const ADropdown: typeof import('antd')['Dropdown']
@@ -27,7 +28,9 @@ declare global {
2728
const ASpace: typeof import('antd')['Space']
2829
const AStatistic: typeof import('antd')['Statistic']
2930
const ASwitch: typeof import('antd')['Switch']
31+
const ATag: typeof import('antd')['Tag']
3032
const ATooltip: typeof import('antd')['Tooltip']
33+
const ATypography: typeof import('antd')['Typography']
3134
const AWatermark: typeof import('antd')['Watermark']
3235
const BetterScroll: typeof import('../components/BetterScroll')['default']
3336
const BeyondHiding: typeof import('../components/BeyondHiding')['default']
@@ -37,7 +40,9 @@ declare global {
3740
const FilpText: typeof import('../components/FilpText')['default']
3841
const FullScreen: typeof import('../components/FullScreen')['default']
3942
const IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined.tsx')['default']
43+
const IconAntDesignInboxOutlined: typeof import('~icons/ant-design/inbox-outlined.tsx')['default']
4044
const IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined.tsx')['default']
45+
const IconAntDesignSendOutlined: typeof import('~icons/ant-design/send-outlined.tsx')['default']
4146
const IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen.tsx')['default']
4247
const IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit.tsx')['default']
4348
const IconLocalBanner: typeof import('~icons/local/banner.tsx')['default']

0 commit comments

Comments
 (0)