11
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

This commit is contained in:
francis-fh
2026-06-04 16:25:38 +08:00
parent 3815dd63da
commit 03f26885c5
32 changed files with 2642 additions and 3062 deletions

View File

@@ -3,39 +3,39 @@ import {
Input,
Select,
Button,
Typography,
Tag,
Avatar,
Space,
message,
Dropdown,
MenuProps,
Badge
} from 'antd';
import {
SearchOutlined,
EnvironmentOutlined,
UserOutlined,
FileTextOutlined,
HomeOutlined,
LogoutOutlined,
BellOutlined
BellOutlined,
ReadOutlined,
LoginOutlined,
} from '@ant-design/icons';
import { history, useLocation, useModel } from '@umijs/max';
import {
ensureJobPortalLogin,
isJobPortalLoggedIn,
PORTAL_LOGIN_URL,
} from '@/utils/jobPortalAuth';
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 portalLogo from '@/assets/logo.png';
import './index.less';
const { Text } = Typography;
const { Option } = Select;
/** 人社门户首页(顶部 Logo 点击跳转) */
const PORTAL_HOME_URL = 'http://218.31.252.15:9081/hrss-web-vue/home';
interface JobPortalHeaderProps {
showSearch?: boolean; // 是否显示搜索区域
showHotJobs?: boolean; // 是否显示热门职位
@@ -77,34 +77,42 @@ const JobPortalHeader: React.FC<JobPortalHeaderProps> = ({
};
const userName = getUserName();
const loggedIn = isJobPortalLoggedIn();
// 判断激活导航(简化为 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');
const isPolicy = location.pathname.startsWith('/job-portal/policy');
// 获取未读消息数量
// 获取未读消息数量(仅登录用户)
useEffect(() => {
if (!loggedIn) {
setUnreadCount(0);
return;
}
const fetchUnreadCount = async () => {
if (!isJobPortalLoggedIn()) {
setUnreadCount(0);
return;
}
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);
@@ -112,13 +120,35 @@ const JobPortalHeader: React.FC<JobPortalHeaderProps> = ({
clearInterval(interval);
window.removeEventListener('messageCountUpdated', handleMessageCountUpdate);
};
}, []);
}, [loggedIn]);
// 背景图片配置
const backgroundImages = [topBg1, topBg2, topBg3, topBg4];
// 热门职位
const hotJobs = ['Java', '产品经理', '前端开发工程师', '测试工程师', '运维工程师', '数据分析师', '平面设计'];
// 热门职位(兜底)
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,
}));
};
// 背景图轮播效果
useEffect(() => {
@@ -181,47 +211,18 @@ const JobPortalHeader: React.FC<JobPortalHeaderProps> = ({
history.push(path);
};
// 退出登录
const handleLogout = async () => {
try {
await logout();
clearSessionToken();
setRemoteMenu(null);
message.success('退出登录成功');
// history.push('/user/login');
window.location.href = 'http://218.31.252.15:9081/hrss-web-vue/home';
} catch (error) {
message.error('退出登录失败');
}
const handleGoToLogin = () => {
window.location.href = PORTAL_LOGIN_URL;
};
// 下拉菜单点击处理
const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
if (key === 'logout') {
handleLogout();
} else if (key === 'personal-center') {
handleNavClick('/job-portal/personal-center');
/** 需登录的导航:未登录弹窗提示 */
const handleAuthNavClick = (path: string, actionHint: string) => {
if (!ensureJobPortalLogin(actionHint)) {
return;
}
handleNavClick(path);
};
// 下拉菜单项
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">
{/* 背景图片轮播 */}
@@ -237,108 +238,127 @@ const JobPortalHeader: React.FC<JobPortalHeaderProps> = ({
))}
</div>
{/* 顶部导航栏 */}
<div className="header-nav">
<div className="nav-container">
<div className="nav-left">
{/* <div className="logo" onClick={() => handleNavClick('/job-portal')}> */}
<div className="logo" onClick={() => handleNavClick('http://218.31.252.15:9081/hrss-web-vue/home', true)}>
<span className="logo-text"></span>
</div>
{/* 顶部栏:问候 + 导航 + 快捷操作 */}
<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>
</div>
<div className="nav-right">
<Space size="large">
<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>
<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">
<Button
type="text"
icon={<HomeOutlined />}
onClick={() => handleNavClick('/job-portal')}
className={`nav-btn${isHome?' active':''}`}
icon={<BellOutlined />}
onClick={() => handleAuthNavClick('/job-portal/message', '查看消息')}
className={`nav-btn${isMessage ? ' active' : ''}`}
>
</Button>
</Badge>
</nav>
<div className="utility-actions">
{loggedIn ? (
<Button
type="text"
icon={<FileTextOutlined />}
onClick={() => handleNavClick('/job-portal/resume')}
className={`nav-btn${isResume?' active':''}`}
icon={<UserOutlined />}
onClick={() => handleNavClick('/job-portal/personal-center')}
className={`nav-btn user-btn${isMine ? ' active' : ''}`}
>
{userName}
</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={<LoginOutlined />}
onClick={handleGoToLogin}
className="nav-btn login-btn"
>
<Button
type="text"
icon={<UserOutlined />}
className={`nav-btn user-btn${isMine?' active':''}`}
>
{userName}
</Button>
</Dropdown>
</Space>
</Button>
)}
</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 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>
</div>
{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>
)}
</div>
)}
</div>
</div>
</div>
</div>
)}
</div>
);