Files
shz-admin/src/components/JobPortalHeader/index.tsx

393 lines
12 KiB
TypeScript
Raw Normal View History

2025-11-10 16:28:01 +08:00
import React, { useState, useEffect } from 'react';
import {
Input,
Select,
Button,
Tag,
message,
Badge
} from 'antd';
import {
SearchOutlined,
UserOutlined,
FileTextOutlined,
HomeOutlined,
2026-06-04 16:25:38 +08:00
BellOutlined,
ReadOutlined,
LoginOutlined,
2026-06-05 16:27:19 +08:00
FundProjectionScreenOutlined,
2025-11-10 16:28:01 +08:00
} from '@ant-design/icons';
import { history, useLocation, useModel } from '@umijs/max';
2026-06-04 16:25:38 +08:00
import {
ensureJobPortalLogin,
2026-06-04 18:47:59 +08:00
getGetInfoCache,
2026-06-04 16:25:38 +08:00
isJobPortalLoggedIn,
PORTAL_LOGIN_URL,
2026-06-04 18:47:59 +08:00
prefetchJobPortalUserInfo,
2026-06-04 16:25:38 +08:00
} from '@/utils/jobPortalAuth';
2025-11-10 16:28:01 +08:00
import { getJobRecommend } from '@/services/common/jobTitle';
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';
2026-06-04 16:25:38 +08:00
import portalLogo from '@/assets/logo.png';
2025-11-10 16:28:01 +08:00
import './index.less';
const { Option } = Select;
2026-06-04 16:25:38 +08:00
/** 人社门户首页(顶部 Logo 点击跳转) */
const PORTAL_HOME_URL = 'http://218.31.252.15:9081/hrss-web-vue/home';
2025-11-10 16:28:01 +08:00
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;
2026-06-04 18:47:59 +08:00
// 获取用户名,优先 initialState其次 getInfo 缓存,最后 appUser 缓存
2025-11-10 16:28:01 +08:00
const getUserName = (): string => {
if (currentUser?.nickName) {
return currentUser.nickName;
}
2026-06-04 18:47:59 +08:00
const getInfoUser = getGetInfoCache()?.user;
if (getInfoUser?.nickName) {
return String(getInfoUser.nickName);
}
if (getInfoUser?.userName) {
return String(getInfoUser.userName);
}
2025-11-10 16:28:01 +08:00
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();
2026-06-04 16:25:38 +08:00
const loggedIn = isJobPortalLoggedIn();
2025-11-10 16:28:01 +08:00
2026-06-04 18:47:59 +08:00
// SSO 仅有 token、本地尚无 userInfo 时预拉求职者资料
useEffect(() => {
if (loggedIn) {
prefetchJobPortalUserInfo();
}
}, [loggedIn]);
2025-11-10 16:28:01 +08:00
// 判断激活导航(简化为 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');
2026-06-04 16:25:38 +08:00
const isPolicy = location.pathname.startsWith('/job-portal/policy');
2026-06-05 16:27:19 +08:00
const isCareerRecommend = location.pathname.startsWith('/job-portal/career-recommendation');
2025-11-10 16:28:01 +08:00
2026-06-04 16:25:38 +08:00
// 获取未读消息数量(仅登录用户)
2025-11-10 16:28:01 +08:00
useEffect(() => {
2026-06-04 16:25:38 +08:00
if (!loggedIn) {
setUnreadCount(0);
return;
}
2025-11-10 16:28:01 +08:00
const fetchUnreadCount = async () => {
2026-06-04 16:25:38 +08:00
if (!isJobPortalLoggedIn()) {
setUnreadCount(0);
return;
}
2025-11-10 16:28:01 +08:00
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);
fetchUnreadCount();
const interval = setInterval(fetchUnreadCount, 30000);
return () => {
clearInterval(interval);
window.removeEventListener('messageCountUpdated', handleMessageCountUpdate);
};
2026-06-04 16:25:38 +08:00
}, [loggedIn]);
2025-11-10 16:28:01 +08:00
// 背景图片配置
const backgroundImages = [topBg1, topBg2, topBg3, topBg4];
2026-06-04 16:25:38 +08:00
// 热门职位(兜底)
const hotJobs = ['Java', '产品经理', '前端开发', '测试工程师', '运维工程师', '数据分析'];
const HOT_JOB_DISPLAY_MAX = 12;
const HOT_JOB_SHOW_COUNT = 6;
const formatHotJobLabel = (title: string) => {
const text = (title || '').trim();
if (text.length <= HOT_JOB_DISPLAY_MAX) return text;
return `${text.slice(0, HOT_JOB_DISPLAY_MAX)}`;
};
const getHotJobItems = (): { key: string | number; title: string }[] => {
if (jobRecommendData?.data?.length) {
return jobRecommendData.data.slice(0, HOT_JOB_SHOW_COUNT).map((job: any) => ({
key: job.jobId ?? job.jobTitle,
title: job.jobTitle || '',
}));
}
return hotJobs.slice(0, HOT_JOB_SHOW_COUNT).map((title) => ({
key: title,
title,
}));
};
2025-11-10 16:28:01 +08:00
// 背景图轮播效果
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 } });
};
// 处理导航点击
2026-05-29 15:23:27 +08:00
const handleNavClick = (path: string, wl: boolean = false) => {
if (wl) {
window.open(path, '_blank');
return;
}
2025-11-10 16:28:01 +08:00
history.push(path);
};
2026-06-04 16:25:38 +08:00
const handleGoToLogin = () => {
window.location.href = PORTAL_LOGIN_URL;
2025-11-10 16:28:01 +08:00
};
2026-06-04 16:25:38 +08:00
/** 需登录的导航:未登录弹窗提示 */
const handleAuthNavClick = (path: string, actionHint: string) => {
if (!ensureJobPortalLogin(actionHint)) {
return;
2025-11-10 16:28:01 +08:00
}
2026-06-04 16:25:38 +08:00
handleNavClick(path);
2025-11-10 16:28:01 +08:00
};
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>
2026-06-04 16:25:38 +08:00
{/* 顶部栏:问候 + 导航 + 快捷操作 */}
<div className="top-utility-bar">
<div className="utility-container">
<div className="header-brand" onClick={() => { window.location.href = PORTAL_HOME_URL; }}>
<img className="header-logo" src={portalLogo} alt="石城智慧就业" />
<span className="header-title"></span>
2025-11-10 16:28:01 +08:00
</div>
2026-06-04 16:25:38 +08:00
<nav className="top-nav">
<Button
type="text"
icon={<HomeOutlined />}
onClick={() => handleNavClick('/job-portal')}
className={`nav-btn${isHome ? ' active' : ''}`}
>
</Button>
<Button
type="text"
icon={<FileTextOutlined />}
onClick={() => handleAuthNavClick('/job-portal/resume', '查看简历')}
className={`nav-btn${isResume ? ' active' : ''}`}
>
</Button>
2026-06-09 16:08:39 +08:00
{/* <Button
2026-06-05 16:27:19 +08:00
type="text"
icon={<FundProjectionScreenOutlined />}
onClick={() => handleNavClick('/job-portal/career-recommendation')}
className={`nav-btn${isCareerRecommend ? ' active' : ''}`}
>
2026-06-09 16:08:39 +08:00
</Button> */}
2026-06-04 16:25:38 +08:00
<Button
type="text"
icon={<ReadOutlined />}
onClick={() => handleNavClick('/job-portal/policy')}
className={`nav-btn${isPolicy ? ' active' : ''}`}
>
</Button>
<Badge count={unreadCount > 0 ? unreadCount : 0} offset={[8, 0]} size="small">
2025-11-10 16:28:01 +08:00
<Button
type="text"
2026-06-04 16:25:38 +08:00
icon={<BellOutlined />}
onClick={() => handleAuthNavClick('/job-portal/message', '查看消息')}
className={`nav-btn${isMessage ? ' active' : ''}`}
2025-11-10 16:28:01 +08:00
>
2026-06-04 16:25:38 +08:00
2025-11-10 16:28:01 +08:00
</Button>
2026-06-04 16:25:38 +08:00
</Badge>
</nav>
<div className="utility-actions">
{loggedIn ? (
2025-11-10 16:28:01 +08:00
<Button
type="text"
2026-06-04 16:25:38 +08:00
icon={<UserOutlined />}
onClick={() => handleNavClick('/job-portal/personal-center')}
className={`nav-btn user-btn${isMine ? ' active' : ''}`}
2025-11-10 16:28:01 +08:00
>
2026-06-04 16:25:38 +08:00
{userName}
2025-11-10 16:28:01 +08:00
</Button>
2026-06-04 16:25:38 +08:00
) : (
<Button
type="text"
icon={<LoginOutlined />}
onClick={handleGoToLogin}
className="nav-btn login-btn"
2025-11-10 16:28:01 +08:00
>
2026-06-04 16:25:38 +08:00
</Button>
)}
2025-11-10 16:28:01 +08:00
</div>
</div>
</div>
{showSearch && (
2026-06-04 16:25:38 +08:00
<div className="banner-section">
<div className="banner-inner">
<div className="search-section">
<div className="search-panel">
<div className="search-bar">
<Select
defaultValue="job-type"
className="search-type-select"
popupMatchSelectWidth={false}
bordered={false}
>
<Option value="job-type"></Option>
</Select>
<span className="search-divider" />
<Input
className="search-input"
placeholder="搜索职位、公司"
bordered={false}
value={searchValue}
onChange={handleSearchInputChange}
onKeyPress={handleSearchKeyPress}
/>
<Button
type="primary"
className="search-submit"
icon={<SearchOutlined />}
onClick={handleSearch}
>
</Button>
2025-11-10 16:28:01 +08:00
</div>
2026-06-04 16:25:38 +08:00
{showHotJobs && (
<div className="hot-jobs">
<span className="hot-jobs-label"></span>
<div className="hot-jobs-container">
{getHotJobItems().map((item) => (
<Tag
key={item.key}
className="hot-job-tag"
title={item.title}
onClick={() => handleHotJobClick(item.title)}
>
{formatHotJobLabel(item.title)}
</Tag>
))}
</div>
</div>
)}
2025-11-10 16:28:01 +08:00
</div>
2026-06-04 16:25:38 +08:00
</div>
2025-11-10 16:28:01 +08:00
</div>
2026-06-04 16:25:38 +08:00
</div>
2025-11-10 16:28:01 +08:00
)}
</div>
);
};
export default JobPortalHeader;