Files
shz-admin/src/components/JobPortalHeader/index.tsx
chenshaohua 30c8a7e8cf
Some checks failed
Node CI / build (14.x, macOS-latest) (push) Has been cancelled
Node CI / build (14.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (14.x, windows-latest) (push) Has been cancelled
Node CI / build (16.x, macOS-latest) (push) Has been cancelled
Node CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (16.x, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
coverage CI / build (push) Has been cancelled
Node pnpm CI / build (16.x, macOS-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, windows-latest) (push) Has been cancelled
first commit
2025-11-10 16:28:01 +08:00

342 lines
10 KiB
TypeScript
Raw 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 } from 'react';
import {
Input,
Select,
Button,
Typography,
Tag,
Avatar,
Space,
message,
Dropdown,
MenuProps,
Badge
} from 'antd';
import {
SearchOutlined,
EnvironmentOutlined,
UserOutlined,
FileTextOutlined,
HomeOutlined,
LogoutOutlined,
BellOutlined
} from '@ant-design/icons';
import { history, useLocation, useModel } from '@umijs/max';
import { getJobRecommend } from '@/services/common/jobTitle';
import { logout } from '@/services/system/auth';
import { clearSessionToken } from '@/access';
import { setRemoteMenu } from '@/services/session';
import { getMessageTotal } from '@/services/jobportal/user';
import topBg1 from '@/assets/images/top-bg1.png';
import topBg2 from '@/assets/images/top-bg2.png';
import topBg3 from '@/assets/images/top-bg3.png';
import topBg4 from '@/assets/images/top-bg4.png';
import './index.less';
const { Text } = Typography;
const { Option } = Select;
interface JobPortalHeaderProps {
showSearch?: boolean; // 是否显示搜索区域
showHotJobs?: boolean; // 是否显示热门职位
}
const JobPortalHeader: React.FC<JobPortalHeaderProps> = ({
showSearch = true,
showHotJobs = true
}) => {
const [searchValue, setSearchValue] = useState<string>('');
const [jobRecommendData, setJobRecommendData] = useState<any>(null);
const [currentBgIndex, setCurrentBgIndex] = useState<number>(0);
const [unreadCount, setUnreadCount] = useState<number>(0);
const location = useLocation();
// 获取当前登录用户信息
const { initialState } = useModel('@@initialState');
const currentUser = initialState?.currentUser;
// 获取用户名,优先使用 initialState其次使用 localStorage 中的 userInfo
const getUserName = (): string => {
if (currentUser?.nickName) {
return currentUser.nickName;
}
// 尝试从 localStorage 获取用户信息
try {
const cachedUserInfo = localStorage.getItem('userInfo');
if (cachedUserInfo) {
const userInfo = JSON.parse(cachedUserInfo);
if (userInfo?.name) {
return userInfo.name;
}
}
} catch (error) {
console.error('读取用户信息失败:', error);
}
return '我的';
};
const userName = getUserName();
// 判断激活导航(简化为 startsWith 匹配)
const isHome = /^\/job-portal(\/(list|detail))?$/.test(location.pathname);
const isResume = location.pathname.startsWith('/job-portal/resume');
const isMine = location.pathname.startsWith('/job-portal/personal-center') || location.pathname.startsWith('/job-portal/profile');
const isMessage = location.pathname.startsWith('/job-portal/message');
// 获取未读消息数量
useEffect(() => {
const fetchUnreadCount = async () => {
try {
const response = await getMessageTotal();
if (response?.code === 200 && response?.data) {
setUnreadCount(response.data.wdxx || 0);
}
} catch (error) {
// 静默处理错误,不影响用户体验
console.error('获取未读消息数量失败:', error);
}
};
// 监听消息数量更新事件
const handleMessageCountUpdate = () => {
fetchUnreadCount();
};
window.addEventListener('messageCountUpdated', handleMessageCountUpdate);
// 每30秒刷新一次未读消息数量
fetchUnreadCount();
const interval = setInterval(fetchUnreadCount, 30000);
return () => {
clearInterval(interval);
window.removeEventListener('messageCountUpdated', handleMessageCountUpdate);
};
}, []);
// 背景图片配置
const backgroundImages = [topBg1, topBg2, topBg3, topBg4];
// 热门职位
const hotJobs = ['Java', '产品经理', '前端开发工程师', '测试工程师', '运维工程师', '数据分析师', '平面设计'];
// 背景图轮播效果
useEffect(() => {
const interval = setInterval(() => {
setCurrentBgIndex((prevIndex) => (prevIndex + 1) % backgroundImages.length);
}, 2000); // 每2秒切换一次
return () => clearInterval(interval);
}, [backgroundImages.length]);
// 获取推荐职位数据
React.useEffect(() => {
const fetchJobRecommendData = async () => {
try {
const response = await getJobRecommend();
setJobRecommendData(response);
} catch (error) {
// 静默处理错误,不影响用户体验
}
};
if (showHotJobs) {
fetchJobRecommendData();
}
}, [showHotJobs]);
// 处理搜索功能
const handleSearch = () => {
if (searchValue.trim()) {
// 跳转到职位列表页面,传递搜索参数
history.push(`/job-portal/list`, { queryParams: { name: searchValue.trim() } });
} else {
message.warning('请输入搜索关键词');
}
};
// 处理搜索框输入
const handleSearchInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchValue(e.target.value);
};
// 处理回车搜索
const handleSearchKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleSearch();
}
};
// 处理热门职位点击
const handleHotJobClick = (job: string) => {
history.push(`/job-portal/list`, { queryParams: { name: job } });
};
// 处理导航点击
const handleNavClick = (path: string) => {
history.push(path);
};
// 退出登录
const handleLogout = async () => {
try {
await logout();
clearSessionToken();
setRemoteMenu(null);
message.success('退出登录成功');
history.push('/user/login');
} catch (error) {
message.error('退出登录失败');
}
};
// 下拉菜单点击处理
const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
if (key === 'logout') {
handleLogout();
} else if (key === 'personal-center') {
handleNavClick('/job-portal/personal-center');
}
};
// 下拉菜单项
const menuItems: MenuProps['items'] = [
{
key: 'personal-center',
icon: <UserOutlined />,
label: '个人中心',
},
{
type: 'divider',
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录',
danger: true,
},
];
return (
<div className="job-portal-header">
{/* 背景图片轮播 */}
<div className="background-carousel">
{backgroundImages.map((image, index) => (
<div
key={index}
className={`background-slide ${index === currentBgIndex ? 'active' : ''}`}
style={{
backgroundImage: `url(${image})`
}}
/>
))}
</div>
{/* 顶部导航栏 */}
<div className="header-nav">
<div className="nav-container">
<div className="nav-left">
<div className="logo" onClick={() => handleNavClick('/job-portal')}>
<span className="logo-text"></span>
</div>
</div>
<div className="nav-right">
<Space size="large">
<Button
type="text"
icon={<HomeOutlined />}
onClick={() => handleNavClick('/job-portal')}
className={`nav-btn${isHome?' active':''}`}
>
</Button>
<Button
type="text"
icon={<FileTextOutlined />}
onClick={() => handleNavClick('/job-portal/resume')}
className={`nav-btn${isResume?' active':''}`}
>
</Button>
<Badge count={unreadCount > 0 ? unreadCount : 0} offset={[10, 0]} size="small">
<Button
type="text"
icon={<BellOutlined />}
onClick={() => handleNavClick('/job-portal/message')}
className={`nav-btn${isMessage?' active':''}`}
>
</Button>
</Badge>
<Dropdown
menu={{
items: menuItems,
onClick: handleMenuClick,
}}
placement="bottomRight"
>
<Button
type="text"
icon={<UserOutlined />}
className={`nav-btn user-btn${isMine?' active':''}`}
>
{userName}
</Button>
</Dropdown>
</Space>
</div>
</div>
</div>
{/* 搜索区域 */}
{showSearch && (
<div className="search-section">
<div className="search-container">
<div className="search-bar">
<Select
defaultValue="职位类型"
style={{ width: 120 }}
bordered={false}
>
<Option value="job-type"></Option>
</Select>
<Input
placeholder="搜索职位、公司"
style={{ flex: 1, border: 'none' }}
bordered={false}
value={searchValue}
onChange={handleSearchInputChange}
onKeyPress={handleSearchKeyPress}
/>
<Button type="text" icon={<EnvironmentOutlined />} />
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>
</Button>
</div>
{/* 热门职位 */}
{showHotJobs && (
<div className="hot-jobs">
<Text strong>:</Text>
<div className="hot-jobs-container">
{(jobRecommendData?.data?.slice(0, 8) || hotJobs.slice(0, 7)).map((job: any, index: number) => (
<Tag
key={job.jobId || job}
className="hot-job-tag"
onClick={() => handleHotJobClick(job.jobTitle || job)}
>
{job.jobTitle || job}
</Tag>
))}
</div>
</div>
)}
</div>
</div>
)}
</div>
);
};
export default JobPortalHeader;