This commit is contained in:
2025-11-19 21:31:06 +08:00
committed by LeonMMcoset
parent 29d249eb15
commit 68f76cf2bd
84 changed files with 1104 additions and 229 deletions

View File

@@ -14,6 +14,13 @@ import CssBaseline from "@mui/material/CssBaseline";
import { styled } from "@mui/material/styles";
import ListSubheader from "@mui/material/ListSubheader";
import useScrollTrigger from "@mui/material/useScrollTrigger";
import Drawer from "@mui/material/Drawer";
import IconButton from "@mui/material/IconButton";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemText from "@mui/material/ListItemText";
import Divider from "@mui/material/Divider";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
function colorLog(message: string, color: 'reset' | 'red' | 'green' | 'yellow' | 'blue') {
const colors = {
@@ -56,6 +63,24 @@ interface ClientLayoutProps {
const ClientLayout: React.FC<ClientLayoutProps> = ({ children }) => {
// 将hooks移到组件内部
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [mobileDrawerOpen, setMobileDrawerOpen] = React.useState(false);
const [isMobile, setIsMobile] = React.useState(false);
// 监听窗口大小变化,判断是否为移动设备
React.useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 768); // 768px为移动端断点
};
// 初始化时判断
handleResize();
// 添加事件监听器
window.addEventListener('resize', handleResize);
// 清理函数
return () => window.removeEventListener('resize', handleResize);
}, []);
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
@@ -64,6 +89,112 @@ const ClientLayout: React.FC<ClientLayoutProps> = ({ children }) => {
const handleClose = () => {
setAnchorEl(null);
};
// 定义顶栏导航项的类型
interface NavItem {
id: string;
label: string;
href: string;
type: 'link' | 'menu';
children?: NavItem[];
}
// 定义导航项目菜单的类型
interface ProjectItem {
id: string;
label: string;
href: string;
category: string;
}
// 动态定义顶部导航链接
const navItems: NavItem[] = [
{
id: 'home',
label: 'LeonCloud',
href: '/',
type: 'link'
},
{
id: 'project',
label: '项目',
href: '',
type: 'menu'
},
{
id: 'support',
label: '技术支持',
href: '/support',
type: 'link'
},
{
id: 'joinus',
label: '加入我们',
href: '/joinus',
type: 'link'
}
];
// 动态定义项目列表
const projectItems: ProjectItem[] = [
{
id: 'leonpan',
label: 'LeonPan',
href: '/project/leonpan',
category: 'Web'
},
{
id: 'leonapp',
label: 'LeonAPP',
href: '/project/leonapp',
category: 'Web'
},
{
id: 'leonbasic',
label: 'LeonBasic',
href: '/project/leonbasic',
category: '其它'
}
];
// 按分类组织项目
const projectsByCategory = projectItems.reduce((acc, item) => {
if (!acc[item.category]) {
acc[item.category] = [];
}
acc[item.category].push(item);
return acc;
}, {} as Record<string, ProjectItem[]>);
// 侧边栏开关控制
const toggleDrawer = (open: boolean) => () => {
setMobileDrawerOpen(open);
};
// 侧边栏项目点击处理
const handleDrawerItemClick = (href: string) => {
window.location.href = href;
setMobileDrawerOpen(false);
};
// 分类展开状态管理 - 现在在projectsByCategory定义之后初始化
const [expandedCategories, setExpandedCategories] = React.useState<Record<string, boolean>>({
// 直接初始化所有已知分类为展开状态
'Web': true,
'其它': true
});
// 项目部分的展开状态管理
const [projectsExpanded, setProjectsExpanded] = React.useState(false);
// 切换分类展开/收缩状态
const toggleCategory = (category: string) => {
setExpandedCategories(prev => ({
...prev,
[category]: !prev[category]
}));
};
interface Props {
/**
* Injected by the documentation to work in an iframe.
@@ -97,85 +228,246 @@ const ClientLayout: React.FC<ClientLayoutProps> = ({ children }) => {
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static">
<Toolbar>
{/* 左侧区域Logo和项目按钮 */}
<Box sx={{ display: "flex", alignItems: "center" }}>
{/* 响应式顶栏 - 移动端显示汉堡菜单,桌面端显示完整导航 */}
<Box sx={{ display: "flex", alignItems: "center", flexGrow: 1 }}>
{/* 移动端:汉堡菜单按钮 */}
{isMobile && (
<IconButton
color="inherit"
aria-label="打开菜单"
edge="start"
onClick={toggleDrawer(true)}
sx={{ mr: 2 }}
>
<Box sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
width: 24,
height: 18
}}>
<Box sx={{ height: 2, bgcolor: 'white', width: '100%' }} />
<Box sx={{ height: 2, bgcolor: 'white', width: '100%' }} />
<Box sx={{ height: 2, bgcolor: 'white', width: '100%' }} />
</Box>
</IconButton>
)}
{/* 首页链接(在所有设备上都显示) */}
<Typography
variant="h6"
variant={isMobile ? "h6" : "h6"}
component="div"
sx={{ mr: 2, cursor: 'pointer' }}
onClick={() => (window.location.href = "/")}
sx={{
cursor: 'pointer',
'&:hover': {
opacity: 0.8
}
}}
onClick={() => (window.location.href = '/')}
>
LeonCloud
</Typography>
<Button
id="menu-appbar"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleMenu}
color="inherit"
>
<Typography variant="body1" component="span">
</Typography>
</Button>
{/* 桌面端:完整导航链接 */}
{!isMobile && (
<Box sx={{ display: "flex", alignItems: "center", ml: 2 }}>
{navItems.filter(item => item.id !== 'home').map((item) => {
if (item.type === 'link') {
return (
<Typography
key={item.id}
variant="body1"
component="div"
sx={{
mr: 2,
cursor: 'pointer',
'&:hover': {
opacity: 0.8
}
}}
onClick={() => (window.location.href = item.href)}
>
{item.label}
</Typography>
);
} else if (item.type === 'menu') {
return (
<Button
key={item.id}
id="menu-appbar"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleMenu}
color="inherit"
>
<Typography variant="body1" component="span">
{item.label}
</Typography>
</Button>
);
}
return null;
})}
</Box>
)}
</Box>
{/* 右侧区域留空,让左侧内容靠左 */}
{/* <Box sx={{ flexGrow: 1 }} /> */}
<Menu
id="menu-appbar"
anchorEl={anchorEl}
// anchorOrigin={{
// vertical: 'top',
// horizontal: 'left',
// }}
keepMounted
// transformOrigin={{
// vertical: 'top',
// horizontal: 'left',
// }}
open={Boolean(anchorEl)}
onClose={handleClose}
>
<StyledListHeader>Web</StyledListHeader>
<MenuItem
onClick={() => (window.location.href = "/project/leonpan")}
{/* 桌面端:项目下拉菜单 */}
{!isMobile && (
<Menu
id="menu-appbar"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}
>
LeonPan
</MenuItem>
<MenuItem
onClick={() => (window.location.href = "/project/leonapp")}
>
LeonAPP
</MenuItem>
{/* <MenuItem onClick={handleClose}>My account</MenuItem> */}
<StyledListHeader></StyledListHeader>
<MenuItem
onClick={() => (window.location.href = "/project/leonbasic")}
>
LeonBasic
</MenuItem>
</Menu>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography
variant="body1"
component="span"
sx={{ mr: 2, cursor: 'pointer' }}
onClick={() => (window.location.href = "/support")}
>
</Typography>
<Button
id="menu-appbar"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleMenu}
color="inherit"
></Button></Box><Box sx={{ flexGrow: 1 }} />
</Toolbar>
</AppBar>
{Object.entries(projectsByCategory).map(([category, items]) => (
<React.Fragment key={category}>
<StyledListHeader>{category}</StyledListHeader>
{items.map((project) => (
<MenuItem
key={project.id}
onClick={() => {
window.location.href = project.href;
handleClose();
}}
>
{project.label}
</MenuItem>
))}
</React.Fragment>
))}
</Menu>
)}
</Toolbar>
</AppBar>
</Box>
{/* 移动端侧边栏 */}
<Drawer
anchor="left"
open={mobileDrawerOpen}
onClose={toggleDrawer(false)}
>
<Box sx={{ width: 250 }} role="presentation">
{/* 侧边栏头部 */}
<Box sx={{
bgcolor: 'primary.main',
color: 'white',
p: 2,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<Typography variant="h6"></Typography>
<IconButton
edge="end"
color="inherit"
onClick={toggleDrawer(false)}
aria-label="关闭菜单"
>
<ChevronRightIcon />
</IconButton>
</Box>
<Divider />
{/* 侧边栏导航链接 */}
<List>
{navItems.filter(item => item.id !== 'home').map((item) => {
if (item.type === 'link') {
return (
<ListItem
component="a"
href={item.href}
key={item.id}
onClick={(e) => {
e.preventDefault();
handleDrawerItemClick(item.href);
}}
>
<ListItemText primary={item.label} />
</ListItem>
);
} else if (item.type === 'menu') {
return (
<React.Fragment key={item.id}>
<ListItem
component="div"
onClick={() => setProjectsExpanded(prev => !prev)}
sx={{
pl: 1,
cursor: 'pointer',
'&:hover': { backgroundColor: 'rgba(0,0,0,0.04)' },
display: 'flex',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.02)',
fontWeight: 'bold'
}}
>
<ChevronRightIcon
fontSize="small"
sx={{
mr: 1,
transform: projectsExpanded ? 'rotate(90deg)' : 'none',
transition: 'transform 0.2s ease-in-out'
}}
/>
<ListItemText
primary={item.label}
/>
</ListItem>
{projectsExpanded && Object.entries(projectsByCategory).map(([category, projects]) => (
<React.Fragment key={category}>
<ListItem
component="div"
onClick={() => toggleCategory(category)}
sx={{
pl: 2,
cursor: 'pointer',
'&:hover': { backgroundColor: 'rgba(0,0,0,0.04)' },
display: 'flex',
alignItems: 'center'
}}
>
<ChevronRightIcon
fontSize="small"
sx={{
mr: 1,
transform: expandedCategories[category] ? 'rotate(90deg)' : 'none',
transition: 'transform 0.2s ease-in-out'
}}
/>
<ListItemText
primary={category}
primaryTypographyProps={{ fontSize: '0.7rem' }}
/>
</ListItem>
{expandedCategories[category] && projects.map((project) => (
<ListItem
component="a"
href={project.href}
key={project.id}
onClick={(e) => {
e.preventDefault();
handleDrawerItemClick(project.href);
}}
sx={{ pl: 6 }}
>
<ListItemText primary={project.label} />
</ListItem>
))}
</React.Fragment>
))}
</React.Fragment>
);
}
return null;
})}
</List>
</Box>
</Drawer>
{children}
{/* 页脚区域 */}

412
app/joinus/page.tsx Normal file
View File

@@ -0,0 +1,412 @@
"use client";
import React from "react";
import Container from "@mui/material/Container";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import CardHeader from "@mui/material/CardHeader";
import Button from "@mui/material/Button";
import MailIcon from "@mui/icons-material/Mail";
import GroupIcon from "@mui/icons-material/Group";
import CodeIcon from "@mui/icons-material/Code";
import MessageIcon from "@mui/icons-material/Chat";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import Timeline from "@mui/lab/Timeline";
import TimelineItem from "@mui/lab/TimelineItem";
import TimelineSeparator from "@mui/lab/TimelineSeparator";
import TimelineConnector from "@mui/lab/TimelineConnector";
import TimelineContent from "@mui/lab/TimelineContent";
import TimelineDot from "@mui/lab/TimelineDot";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTheme } from "@mui/material/styles";
import ClientLayout from "../components/ClientLayout";
const Joinus: React.FC = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const isMedium = useMediaQuery(theme.breakpoints.down('md'));
const handleEmailClick = () => {
window.location.href = 'mailto:leonmmcoset@outlook.com';
};
// 要求列表数据
const requirements = [
{ text: "能会至少一种编程语言的开发人员", icon: <CodeIcon /> },
{ text: "有良好的沟通能力和团队合作精神", icon: <MessageIcon /> },
{ text: "无违法前科", icon: <CheckCircleIcon /> },
{ text: "拥有微信或QQ账号", icon: <GroupIcon /> }
];
// 加入步骤数据
const steps = [
{
title: "发送申请",
description: "通过我们的邮箱发送你的申请意愿",
icon: <MailIcon />
},
{
title: "等待回复",
description: "我们会在三天内回复你的申请",
icon: <MessageIcon />
},
{
title: "加入群聊",
description: "获得微信/QQ内部群聊的邀请",
icon: <GroupIcon />
},
{
title: "讨论技术",
description: "在群聊中讨论你的技术方向和项目",
icon: <CodeIcon />
}
];
// 动画延迟函数
const getDelay = (index: number) => {
return `${index * 0.2}s`;
};
return (
<Container maxWidth="lg" sx={{ py: { xs: 8, md: 12 } }}>
{/* 页面标题 */}
<Box
sx={{
textAlign: 'center',
mb: { xs: 8, md: 12 },
animation: 'fadeInUp 0.8s ease-out',
'@keyframes fadeInUp': {
'from': {
opacity: 0,
transform: 'translateY(30px)'
},
'to': {
opacity: 1,
transform: 'translateY(0)'
}
}
}}
>
<Typography
variant={isMobile ? "h3" : "h2"}
component="h1"
gutterBottom
fontWeight="bold"
sx={{ color: 'primary.main' }}
>
</Typography>
<Typography
variant="body1"
sx={{
maxWidth: 700,
mx: 'auto',
fontSize: { xs: '1rem', md: '1.125rem' },
lineHeight: 1.6
}}
>
</Typography>
</Box>
{/* 要求部分 */}
<Box
sx={{
mb: { xs: 10, md: 16 },
animation: 'fadeInUp 0.8s ease-out 0.2s both',
}}
>
<Typography
variant="h4"
component="h2"
gutterBottom
sx={{
textAlign: 'center',
mb: { xs: 6, md: 8 },
position: 'relative',
'&::after': {
content: '""',
position: 'absolute',
bottom: -16,
left: '50%',
transform: 'translateX(-50%)',
width: 50,
height: 3,
backgroundColor: 'primary.main'
}
}}
>
</Typography>
<Box
sx={{
display: 'grid',
gridTemplateColumns: {
xs: '1fr',
sm: '1fr 1fr',
md: '1fr 1fr 1fr 1fr'
},
gap: { xs: 4, md: 6 }
}}
>
{requirements.map((requirement, index) => (
<Card
key={index}
sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-8px)',
boxShadow: '0 12px 20px rgba(0,0,0,0.1)',
},
animation: 'fadeInUp 0.6s ease-out both',
animationDelay: getDelay(index)
}}
elevation={2}
>
<CardHeader
avatar={
<Box sx={{
bgcolor: 'primary.light',
width: 50,
height: 50,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '50%',
color: 'primary.dark'
}}>
{requirement.icon}
</Box>
}
title={
<Typography
variant="h6"
component="div"
sx={{ fontWeight: 600 }}
>
{requirement.text}
</Typography>
}
/>
</Card>
))}
</Box>
<Box
sx={{
mt: 8,
textAlign: 'center',
animation: 'fadeInUp 0.8s ease-out 0.8s both',
}}
>
<Typography
variant="h6"
component="p"
sx={{
fontWeight: 600,
color: 'white',
backgroundColor: 'primary.light',
py: 2,
px: 4,
display: 'inline-block',
borderRadius: 2
}}
>
</Typography>
</Box>
</Box>
{/* 加入步骤 - 使用Timeline组件 */}
<Box
sx={{
mb: { xs: 10, md: 16 },
animation: 'fadeInUp 0.8s ease-out 0.4s both',
}}
>
<Typography
variant="h4"
component="h2"
gutterBottom
sx={{
textAlign: 'center',
mb: { xs: 8, md: 10 },
position: 'relative',
'&::after': {
content: '""',
position: 'absolute',
bottom: -16,
left: '50%',
transform: 'translateX(-50%)',
width: 50,
height: 3,
backgroundColor: 'primary.main'
}
}}
>
</Typography>
<Box sx={{ px: { xs: 1, md: 4 } }}>
<Timeline position={isMobile ? "alternate" : "left"}>
{steps.map((step, index) => (
<TimelineItem key={index} sx={{ animation: 'fadeInUp 0.6s ease-out both', animationDelay: getDelay(index) }}>
<TimelineSeparator>
<TimelineDot
sx={{
bgcolor: 'primary.main',
color: 'white',
width: isMobile ? 48 : 56,
height: isMobile ? 48 : 56,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'& .MuiSvgIcon-root': {
fontSize: isMobile ? 24 : 28
}
}}
>
{step.icon}
</TimelineDot>
{index < steps.length - 1 && <TimelineConnector sx={{ bgcolor: 'primary.light' }} />}
</TimelineSeparator>
<TimelineContent>
<Card
sx={{
maxWidth: isMobile ? '100%' : 600,
ml: isMobile ? 0 : 2,
transition: 'all 0.3s ease',
'&:hover': {
boxShadow: '0 8px 16px rgba(0,0,0,0.1)'
}
}}
elevation={3}
>
<CardContent sx={{ p: { xs: 3, md: 4 } }}>
<Typography
variant="h5"
component="h3"
gutterBottom
fontWeight="bold"
>
{index + 1}{step.title}
</Typography>
<Typography
variant="body1"
sx={{
fontSize: { xs: '1rem', md: '1.0625rem' },
lineHeight: 1.7
}}
>
{step.description}
</Typography>
{index === 0 && (
<Button
variant="contained"
color="primary"
sx={{ mt: 3 }}
startIcon={<MailIcon />}
onClick={handleEmailClick}
>
</Button>
)}
</CardContent>
</Card>
</TimelineContent>
</TimelineItem>
))}
</Timeline>
</Box>
</Box>
{/* CTA 区域 */}
<Box
sx={{
textAlign: 'center',
py: { xs: 8, md: 12 },
bgcolor: 'primary.light',
borderRadius: 4,
position: 'relative',
overflow: 'hidden',
animation: 'fadeInUp 0.8s ease-out 0.6s both',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
bgcolor: 'primary.light',
opacity: 0.2,
zIndex: 0
}
}}
>
<Box sx={{ position: 'relative', zIndex: 1 }}>
<Typography
variant="h4"
component="h2"
gutterBottom
fontWeight="bold"
sx={{ color: 'white' }}
>
</Typography>
<Typography
variant="body1"
sx={{
maxWidth: 600,
mx: 'auto',
mb: 4,
fontSize: { xs: '1rem', md: '1.125rem' },
color: 'white'
}}
>
</Typography>
<Button
variant="contained"
color="primary"
size={isMobile ? "large" : "medium"}
sx={{
px: { xs: 4, md: 5 },
py: { xs: 1.5, md: 1 },
fontWeight: 600,
'&:hover': {
transform: 'translateY(-2px)'
}
}}
startIcon={<MailIcon />}
onClick={handleEmailClick}
>
</Button>
</Box>
</Box>
{/* 动画样式 */}
<style jsx global>{`
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`}</style>
</Container>
);
};
export default Joinus;